Skip to content

feat(docs): [experimental] LLM documentation surface — API + stories feeds shipped with the Storybook - #5224

Draft
F2X wants to merge 21 commits into
masterfrom
feat/llms-doc-surface
Draft

F2X wants to merge 21 commits into
masterfrom
feat/llms-doc-surface

Conversation

@F2X

@F2X F2X commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Experimental — under test. The feeds and their format may change or be withdrawn without a deprecation cycle.

Description

Ships an LLM-consumable documentation surface with every Storybook deploy.

Coding agents (Copilot, Claude, CI bots) can read the whole public API of @lucca-front/ng and @lucca/prisme, plus every documented story, from https://lucca-front.lucca.io/<ref>/storybook/llms.txt — unauthenticated, versioned per ref, zero new infrastructure.

  • Storybook channel: llms.txt index, llms-full.txt (API + stories), deprecations.json, plus 136 windowed per-entry-point and per-category feeds
  • npm channel: llms.txt proxies llms-api.txt beside it, plus llms-stories.txt on ng — read from node_modules at the installed version
  • ts-morph extraction: 1083 declarations from 1066 exports, 0 unresolved; 637 story files in 16 categories (28 component-rendered)
  • CI gates: coverage ratchet, smoke (API probes, 18 named anchors, feed counts, no internal host), pack verification both ways, story floor + 4 story anchors

Impact

Evidence — gates output and shipped files
$ npm run docs:api-smoke
[llms-smoke] ok: header
[llms-smoke] ok: component selector
[llms-smoke] ok: component inputs (generated)
[llms-smoke] ok: component outputs (generated)
[llms-smoke] ok: deprecation callout
[llms-smoke] ok: stories section
[llms-smoke] ok: 1083 public API entries (floor 500)
[llms-smoke] ok: 18/18 named feed anchors present
[llms-smoke] ok: 120 entry-point feeds (floor 80)
[llms-smoke] ok: 16 story-category feeds (floor 10)
[llms-smoke] ok: llms.txt links every generated feed
[llms-smoke] ok: no internal host in any of 139 generated files
[llms-smoke] OK: llms-full.txt is 1226 KB, 136 windowed feeds all indexed.

$ npm run docs:api-coverage
[PASS] @lucca-front/ng + @lucca/prisme: 105/1083 public exports documented = 10% (baseline 105 documented)

$ npm run build   # → dist/, publish.yml ships the whole folder
[llms-pack] @lucca-front/ng: 1070 API entries (492 KB) + 147 documented components from 637 story files (745 KB) → dist/ng/{llms.txt,llms-api.txt,llms-stories.txt} (in tarball, 477 files)
[llms-pack] @lucca/prisme: 57 API entries (28 KB) + no stories corpus (opted out) → dist/prisme/{llms.txt,llms-api.txt} (in tarball, 28 files)

$ npm run build-storybook   # → storybook-static/
llms.txt  llms-full.txt (1226 KB)  deprecations.json (36 KB)  llms/ (136 files)

$ npx vitest run --project api-docs
Test Files  3 passed (3)   Tests  142 passed (142)
Evidence — every gate verified by breaking it
# a single lost feed family, with every numeric floor still green
$ rm .storybook/public/llms/ng-icon.md && npm run docs:api-smoke
[llms-smoke] ok: 1083 public API entries (floor 500)
[llms-smoke] MISSING: 17/18 named feed anchors present
[llms-smoke] ok: 119 entry-point feeds (floor 80)
[llms-smoke] FAIL: named feed anchor(s) gone: ng-icon.md.

# a lost story family, same shape one channel down
$ mv stories/documentation/actions/button /tmp && npm run docs:llms-pack
[llms-pack] FAIL: @lucca-front/ng: story anchor(s) gone from the packaged corpus: ## Actions / Button.

# an .npmignore line added upstream: corpus written, not published (28 files -> 27)
$ echo llms-api.txt >> dist/prisme/.npmignore && npm run docs:llms-pack
[llms-pack] FAIL: @lucca/prisme: llms-api.txt written to dist/prisme but ABSENT from
the published tarball (27 files) — check `files`/.npmignore in the dist manifest.

# and the other way: a stale corpus reaching a tarball that opted out
$ cp dist/ng/llms-stories.txt dist/prisme/ && npm run docs:llms-pack   # cleanup neutralised
[llms-pack] FAIL: @lucca/prisme: opted out of the stories corpus, yet llms-stories.txt
reaches the tarball — a stale dist file is being published.

Post-deploy proof: curl -I https://lucca-front.lucca.io/master/storybook/llms-full.txt returns 200 once the next master deploy runs (it 404s today).


Technical notes for reviewers
  • ts-morph, not Compodoc: the pinned Compodoc 1.1.16 under-resolves signal input()/output() (feat(docs): single LLM documentation surface via Compodoc #5167 was closed for that); ts-morph reads them straight from source. Output is deterministic — identical source yields byte-identical files.
  • @lucca/prisme shares the ng topology in-repo (secondary entry points with public-api.ts barrels); documenting it also resolves the @lucca-front/ng button/icon re-exports cleanly.
  • Stories extraction is purely syntactic (no Storybook runtime, no browser): Meta title, template: literals (following one level of in-file indirection — the Template/getTemplate and cleanupTemplate(...) idioms), story export names, argTypes.*.description; createTestStory wrappers skipped. 28/636 files carry no static template (component-rendered) — they still contribute titles and prop descriptions.
  • deprecations.json resolves by declaration, not by name. 3 of its 126 entries reach several entry points (both tooltip barrels, both button barrels); where two classes share a name, only the barrel declaring the deprecated one is listed, so a lint rule keyed on the import path flags the right barrel.
  • The feed anchors are named, never derived. The index and the llms/ folder are produced by the same extraction, so a lost entry point vanishes from both and a count floor of 80 against ~120 feeds lets it through. The 18 anchors cover one feed per package, per nesting depth and per story-category arm. The packaged stories corpus carries the same discipline: 4 named component anchors, because a floor of 400 against 636 story files also lets a whole family through. Add an anchor when a family becomes load-bearing; never trim the list to make the gate pass.
  • The npm channel packs rather than trusting the write, and asserts file by file. A files allowlist or .npmignore introduced upstream drops a feed at publish time with the build still green — only npm pack sees the published file list (~0.9 s per package). An allowlist naming llms-api.txt alone would drop the proxy and the stories, so all three names are checked.
  • llms.txt proxies, it never copies. In the tarball it lists the entry points and relatively links ./llms-api.txt and ./llms-stories.txt; the 136 split feeds stay on the deploy, since shipping them would duplicate ~300 KB of the same bytes. The split is what makes the corpora usable: an agent after a signature reads 288 KB (23 KB on prisme), never the 722 KB of templates. And the link must stay relative — an absolute docs URL answers for master while the package is pinned, the version leak this channel exists to close.
  • @lucca-front/ng carries the stories; @lucca/prisme opts out. 388 of the 636 story files import neither package (they document the CSS design system), so splitting the corpus per package by import would drop 61% of the examples from both, so ng carries it whole (+134 KB gz, +11%). That corpus meant +94% on a 90 KB package, so prisme ships its API alone (+7 KB gz) and its proxy links the stories on the deploy — which tracks master while the package is pinned, so the version guarantee holds for prisme's API, not its examples. Flip stories in PACK_TARGETS to undo.
  • No dependency on @lucca/docs-kit. The kit's bundler covers the tag-sourced Starlight pipeline (Pagga.Front, lu-swag): its files-allowlist guard is a no-op here and its archived-snapshot guard has no subject (the Storybook is versioned per deployment, not by tag). lucca-front carries no external @lucca/* dependency — both of its own are in-repo.
  • lucca docs is a parallel channel, not a substitute. It serves authenticated devs fleet-wide from an Astro site published by a build.cake PublishAstroDocs task; lucca-front has none, is absent from its registry, and per LuccaSA/Guildes#75 the DD arm is 401-gated.
  • The deployed index links relatively, not to master, and lists the zeroheight Prisme reference as prose documentation; machine extraction of zeroheight is a separate decision (feat(docs): aggregate Prisme (zeroheight) guidance into the llms*.txt feeds #5222).
  • Drive-by, separate commit: removed the orphan build-compodoc npm script and its tsconfig.compodoc-html.json (referenced by no workflow or script; the Storybook builder's own compodoc integration in angular.json is untouched).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TN1HaKioHi3vwVHDyeUafV

F2X and others added 3 commits August 5, 2026 13:57
- Resolve the full public surface from the TypeScript compiler API (ts-morph)
  across all secondary entry-point barrels, emitting a deterministic
  .storybook/public/llms-full.txt + deprecations.json — 1002 entries / 711 inputs,
  vs 214 inputs under the closed Compodoc-1.1.16 attempt (#5167), 0 unmatched.
- Wire docs:llms into build-storybook, add generate+smoke+coverage gates to
  build.yml, and a node-env `api-docs` vitest project (33 tests, byte-deterministic).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s and index

- extract-stories.mjs: static ts-morph extraction of stories/documentation
  (620 stories, 1159 argTypes descriptions, both Angular and HTML&CSS arms)
- generate-llms.mjs: documents @lucca/prisme alongside @lucca-front/ng,
  emits llms.txt index + one llms/<slug>.md per entry point and per story
  category, appends the stories corpus to llms-full.txt
- check-llms-surface.mjs: feed-count consistency gate (every generated feed
  linked from llms.txt), floors per feed kind, internal-host guard
Referenced by no workflow or script; the Storybook builder's own compodoc
integration (angular.json) is untouched.
@F2X F2X added the 🔨 Technical Doesn't affect the output (refactor, dependencies update, cleaning, etc.) label Aug 6, 2026
@F2X
F2X requested a balanced review from Copilot August 6, 2026 11:11
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

🚀 Storybook preview deployed: https://pub-dc6d99acd6874e2aaff6219dd8a13ae2.r2.dev/PR-5224/index.html

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a generated, Storybook-hosted LLM documentation surface for public APIs and stories.

Changes:

  • Extracts API and Storybook metadata with ts-morph.
  • Generates full, indexed, windowed, and deprecation feeds.
  • Adds Vitest coverage and CI validation gates.

Reviewed changes

Copilot reviewed 12 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
.github/workflows/build.yml Runs documentation validation in CI.
.gitignore Ignores generated feeds.
package.json Adds scripts and ts-morph.
package-lock.json Locks the new dependency.
vitest.config.ts Adds the API-docs test project.
tsconfig.compodoc-html.json Removes obsolete Compodoc configuration.
scripts/api-docs/extract-api.mjs Extracts public TypeScript APIs.
scripts/api-docs/extract-api.spec.mjs Tests API extraction.
scripts/api-docs/extract-stories.mjs Extracts documented stories.
scripts/api-docs/extract-stories.spec.mjs Tests story extraction.
scripts/api-docs/generate-llms.mjs Generates documentation feeds.
scripts/api-docs/generate-llms.spec.mjs Tests feed rendering.
scripts/api-docs/check-llms-surface.mjs Smoke-tests generated artifacts.
scripts/api-docs/check-api-coverage.mjs Enforces JSDoc coverage.
Suppressed comments (3)

scripts/api-docs/extract-api.mjs:179

  • The generated input name must use Angular's public alias, not the backing property name. The current output documents luTooltipInput although consumers bind [luTooltip] (packages/ng/tooltip/trigger/tooltip-trigger.directive.ts:68), and the repository contains several other aliased signal inputs. Read the alias option for input, input.required, and model.
				name: prop.getName(),

scripts/api-docs/extract-api.mjs:175

  • Angular model() declares both an input and an implicit <name>Change output, but this branch records only the input. Consequently APIs such as IndexTableRowComponent.selectedChange (packages/ng/index-table/index-table-row/index-table-row.component.ts:52) disappear from the promised output surface. Add the corresponding output, including alias handling, whenever the callee is model or model.required.
		if (INPUT_CALLEES.has(callee)) {

scripts/api-docs/extract-api.mjs:169

  • getProperties() only yields declarations on this class, so inherited Angular inputs/outputs are omitted. For example, DurationPickerComponent extends BasePickerComponent, whose public step, size, and disabled model (packages/ng/time/core/base-picker.component.ts:15-19) are part of the derived component API but will not be rendered. Traverse base classes and merge inherited members while allowing derived overrides.
	for (const prop of classNode.getProperties()) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/api-docs/extract-api.mjs
Comment thread scripts/api-docs/extract-api.mjs Outdated
Comment thread scripts/api-docs/extract-stories.mjs Outdated
Comment thread scripts/api-docs/check-api-coverage.mjs Outdated
Comment thread scripts/api-docs/generate-llms.mjs Outdated
…y stories, exact gates

- extract decorator-based @Input/@output (properties and setters, aliases),
  EventEmitter payloads and outputFromObservable()
- publish overload signatures instead of the implementation signature
- keep stories built by non-test factory calls (generateStory) and resolve
  shorthand template properties
- coverage gate compares the exact ratio, not the rounded display value
- deprecations.json declares both documented packages
- generate-package-llms.mjs writes dist/ng and dist/prisme llms-full.txt
  after the ng-packagr build (publish.yml ships the whole dist folder)
- committed guardrail: the build fails when a package's extraction
  collapses below its floor or its dist folder is missing
- agents read the doc from node_modules at the exact installed version
  (npm channel of the doc-for-LLM epic, same shape as lu-swag#260)
@F2X
F2X requested review from shajz and a balanced review from Copilot August 6, 2026 12:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 15 changed files in this pull request and generated no new comments.

Suppressed comments (7)

scripts/api-docs/extract-api.mjs:241

  • Signal input aliases are not applied here, so the generated binding names are wrong. For example, LinkComponent.luHref = input('', { alias: 'href' }) is emitted as luHref, and the same happens for luLink, luTooltip, and other aliased signals. Parse the signal options for input, input.required, and model and emit the configured alias as the public name.
				name: prop.getName(),

scripts/api-docs/extract-stories.mjs:237

  • The renderer never emits story.name, and stories without described arg types are skipped entirely by this loop. Files such as simple-select.stories.ts contain many distinct variants (Basic, Minimal, WithDisplayer, etc.); their templates are currently dumped together under one file heading with no indication of which story they belong to. Preserve template/description data per story and render a heading for every extracted story so the promised story corpus remains identifiable.
			for (const story of file.stories) {

scripts/api-docs/extract-api.mjs:237

  • model() and model.required() enter this input-only branch, but an Angular model also exposes an output named <modelName>Change. Consequently components such as Calendar2Component lose dateChange, modeChange, and the other model outputs from the advertised full API. Add the corresponding output entry (using the model alias when present).

This issue also appears on line 241 of the same file.

		if (INPUT_CALLEES.has(callee)) {

scripts/api-docs/extract-api.mjs:373

  • The entity is built only from members declared directly on the concrete class. Angular components inherit inputs and outputs: for example, LuSimpleSelectInputComponent extends ALuSelectInputComponent, whose placeholder, clearable, panelClosed, and many other bindings are part of the usable component API, but this feed emits only the subclass's three own inputs. Traverse base classes and merge inherited Angular members and public methods, with derived declarations taking precedence.
			return { ...base, selector: selectorOf(node), ...membersOf(node), methodsClass: methodsOf(node) };

scripts/api-docs/generate-llms.mjs:329

  • The method signature is placed in a Markdown table without escaping pipes in argument types. Public methods such as callCanDeactivateFn(canDeactivateFn: CanDeactivateFn<C> | DeprecatedGuard) therefore add an unintended table column and corrupt the generated feed. Escape | in the complete signature before writing the cell.
			const args = (m.args || []).map((a) => `${a.name}: ${a.type || 'unknown'}`).join(', ');
			lines.push(`| \`${m.name}(${args})\` | ${typeCell(m.returnType)} | ${cleanCell(m.rawdescription || m.description)} |`);

scripts/api-docs/extract-stories.mjs:183

  • For helper-built stories, storyObject is the generateStory({...}) configuration, while the actual Storybook argTypes are under storyPartial.argTypes. Calling argTypesOf only at the top level drops those descriptions; for example, simple-select.stories.ts documents AddOption's addOptionLabel and addOptionStrategy there, but neither reaches the feed. Resolve the helper's storyPartial object and merge its arg types with the Meta defaults.

This issue also appears on line 237 of the same file.

		const own = argTypesOf(storyObject);
		stories.push({ name: varDecl.getName(), argTypes: own.length ? own : metaArgTypes });

scripts/api-docs/generate-llms.mjs:52

  • Hard-coding master means every versioned deployment's index crosses versions: following a link from v21.3/storybook/llms.txt loads the master API/story feed rather than v21.3. This contradicts the PR's per-ref documentation contract and can give agents APIs unavailable in the selected release. Use URLs relative to the current Storybook directory, or inject the deployed ref when generating the index.
export const CANONICAL_BASE_URL = 'https://lucca-front.lucca.io/master/storybook';

@F2X
F2X marked this pull request as ready for review August 31, 2026 08:42
@F2X
F2X requested a review from a team as a code owner August 31, 2026 08:42
@F2X
F2X marked this pull request as draft August 31, 2026 13:31
@F2X F2X added this to the 22.1 milestone Sep 9, 2026
Resolve the package.json/package-lock.json conflict that made Jenkins
pr-merge unbuildable: keep master's storybook ~10.5.3 and stylelint
^17.14.1 bumps, keep this branch's ts-morph ~28.0.0, and regenerate the
lockfile from master's baseline.

Verified after the merge: npm run docs:api renders 1066 public exports
and passes all 11 llms-surface smoke gates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dbd3kZcm4ms8MAAVH97BQ2
- deprecations.json carries the import paths of each deprecated symbol: the
  path, not the bare name, is the discriminant — 3 of 103 entries surface
  through two entry points, so a consumer keyed on the name alone cannot say
  which import to flag
- the llms smoke gate pins 18 feed anchors by name: index and feeds come out of
  the same extraction, so a single lost family stays above every numeric floor
  (measured: dropping ng-icon keeps 119/80 feeds and the link check green)
- the npm channel step packs instead of trusting the write: a `files` allowlist
  or .npmignore added upstream drops the feed at publish time with the build
  still green (measured on dist/ng, 230 files, feed absent)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN1HaKioHi3vwVHDyeUafV
- Add `renderPackageIndex()`: a per-package llmstxt.org index written next to the
  corpus in `dist/{ng,prisme}`, so an agent that finds `node_modules/@lucca-front/ng/
  llms.txt` gets the entry-point list and a relative `./llms-full.txt` pointer at the
  exact installed version. The split per-entry-point feeds stay out of the tarball —
  they would duplicate ~300 KB of the same bytes for no new information.
- Assert the tarball per file instead of on `llms-full.txt` alone: a `files` allowlist
  naming only the corpus dropped the index with the build still green (proof: exit 1
  once asserted, exit 0 restored).
- Cost measured: +11 KB on @lucca-front/ng (9.9 MB), +1.5 KB on @lucca/prisme.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TN1HaKioHi3vwVHDyeUafV
@F2X
F2X marked this pull request as ready for review September 9, 2026 10:12
@F2X
F2X requested a review from jeremie-lucca September 9, 2026 10:12
The alias was resolved for the decorator forms only. `input()`, `model()`,
`output()` and `outputFromObservable()` published `prop.getName()`, so the
corpus named members that do not exist in the template API — an agent reading
the feed wrote `(closeOutput)` where the component accepts `(close)`.

- resolve `{ alias }` for every signal factory, per-form options position
- reuse the same reader in `decoratorConfig`

Measured on the library: 66 declarations carry an alias on a signal factory
(53 inputs, 13 `outputFromObservable`); before this, 61 leaked their property
name into `llms-full.txt`. `LuDatePickerComponent` now publishes `close`,
`open`, `hovered`, `onSelectValue` instead of the `*Output` property names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dbd3kZcm4ms8MAAVH97BQ2

This comment was marked as outdated.

F2X and others added 4 commits September 10, 2026 09:29
…ping

The extractor read each class in isolation and only its own declared members,
so four public forms never reached the generated feeds:

- inherited bindings — `DateInputComponent extends AbstractDateComponent` lost
  `format`, `ranges`, `hideToday`, `calendarMode`; the base chain is now walked,
  most-derived first, and a derived declaration overrides the inherited one;
- `model()` published its input but not the implicit `<publicName>Change` output,
  so `Calendar2Component.dateChange` existed nowhere. Built on the alias, not the
  property name, and typed from the model's value type;
- `hostDirectives` forwarded bindings — `LuDropdownTriggerDirective` publishes
  `luDropdownPosition` and friends and none could reach the arrays. Only the
  listed bindings are forwarded, and `'name: alias'` publishes the alias;
- interface method signatures were discarded by `propertiesOf` alone, so
  `ILuPopupRef` documented its observables and not `open`/`close`/`dismiss`.

Measured on the generated corpus: `DateInputComponent` goes from its own inputs
to 47, 64 `<name>Change` outputs appear, and `ILuPopupRef` gains its three methods.

Addresses PR review comments on #5224 (threads on extract-api.mjs:220/225/264/354
and two suppressed findings on the same class of defect).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHWgqrmwb1XiAPqZxY2sHy
Four defects in the generated output, none of them visible from the extraction:

- a method argument was written into a Markdown table without escaping `|`, so
  `callCanDeactivateFn(canDeactivateFn: CanDeactivateFn<C> | DeprecatedGuard)`
  opened an extra column and corrupted the row. `typeCell` escaped the return
  type but nothing escaped the arguments; the whole class is swept — the enum
  value cell went through `defaultCell` too;
- the interface renderer had no Methods table, so the signatures the extractor
  now produces had nowhere to land. The class and interface renderers share one
  `methodsTable`, and `### Properties` only appears when a Methods table follows it;
- the deployed `llms.txt` hard-wired `https://lucca-front.lucca.io/master/storybook`,
  so opening `v21.3/storybook/llms.txt` or a PR preview sent every feed request to
  master — an agent could be handed APIs absent from the release it selected. The
  index links relatively; all four artifacts sit in `.storybook/public/`, so `.`
  resolves. `CANONICAL_BASE_URL` stays for the npm packages' fallbacks, which are
  explicitly labelled as tracking master;
- the feed directory was recreated but never cleared, so a removed or renamed
  entry point left its `.md` behind and `build-storybook` — which runs the
  generator without the smoke check — copied it into the deployment. Purged first.

Addresses PR review comments on #5224 (threads on generate-llms.mjs:819/839, the
interface half of the extract-api:354 thread, and two suppressed findings).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHWgqrmwb1XiAPqZxY2sHy
…as HTML

Three defects in the stories feed:

- a `TemplateExpression` was returned as raw source and then fenced as `html`,
  so `button-basic.stories.ts` published `${luButton !== ...}` and
  `${generateInputs(...)}` as if a consumer could paste it. Statically resolving
  those interpolations would mean running the helpers, so the honest fix is to
  label the block for what it is: an interpolated template fences as `ts`.
  Measured on the regenerated corpus: 0 occurrences of `${` remain inside an
  `html` fence;
- stories were not named in the output — a file's templates were dumped together
  under one heading, and a story with no described argTypes was skipped entirely,
  so `simple-select.stories.ts`'s `Basic`/`Minimal`/`WithDisplayer` were
  indistinguishable. A `**Stories:**` line lists them all, and each argTypes table
  is headed by the stories that share it (the dedup is kept, it is now attributed);
- for helper-built stories the real Storybook config sits under `storyPartial`,
  so `argTypesOf` at the top level dropped `AddOption`'s `addOptionLabel` and
  `addOptionStrategy`. Meta, own and `storyPartial` argTypes now merge by prop
  name, later winning — which is how Storybook resolves them.

Addresses PR review comments on #5224 (thread on extract-stories.mjs:47 and two
suppressed findings on :183 and :237).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHWgqrmwb1XiAPqZxY2sHy
The gate was described as a ratchet but measured coverage at 102/1066 (9.6%)
against a 5% floor: half the documented exports could be deleted and CI stayed
green. That is the regression the gate exists to catch.

Pinning the floor at the current ratio instead would fail any PR that adds
undocumented exports — not a regression in what is documented, and exactly the
"blocking unrelated PRs" the original note was avoiding. So the gate ratchets the
documented COUNT: it fails when a public export loses its JSDoc description, and
is indifferent to the surface growing. The percentage is still reported, and a run
above the baseline prints the value to raise MIN_DOCUMENTED to.

Verified both paths: 102 passes, and a baseline of 103 fails with
"1 public export(s) lost their JSDoc description since the baseline".

Addresses the PR review comment on #5224 (check-api-coverage.mjs:19).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHWgqrmwb1XiAPqZxY2sHy

This comment was marked as outdated.

…actor sees

- split a story title at its `Angular`/`HTML&CSS` arm instead of a fixed
  position: 22 nested components (`Forms/Fields/CheckboxField`) stopped
  collapsing onto their parent, 129 → 147 component groups over 636 files
- a transformed input documents its write type (`ButtonComponent.block` is
  a boolean-attribute union, not `boolean`), and an interface publishes the
  members it inherits, the nearest declaration winning
- a generic class declares its type parameters, and a template-literal type
  gets a doubled fence so its backticks no longer break the table cell

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNHjonxsYvpdAXQfXbydYJ
Four findings Copilot folded into its suppressed block, none of which carried a
thread — the same defect family as the visible ones: the feed published what the
extractor happened to walk, not what a consumer may call.

- a public static method is as callable as an instance one:
  `PhoneNumberFormatter.format` and `PhoneNumberValidators.validPhoneNumber`
  rendered as empty sections, they now read as `static format(…)`
- a class publishes its public instance properties and getters, minus the ones
  already listed as inputs or outputs (`LuTitleStrategy.title$`,
  `ALuPopupRef.onOpen`)
- a method declares its own type parameters (`LuPopup.open<T, D, R>`), on classes
  and interfaces alike — the signature named `D` and `R` without defining them
- an `outputFromObservable` with an inferred payload falls back to the resolved
  `OutputEmitterRef<T>`: `LuDropdownItemDirective.onSelect` is `boolean`, not
  `unknown`

llms-full.txt grows 1094 → 1223 KB; coverage, pack and smoke gates stay green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNHjonxsYvpdAXQfXbydYJ
Self-review of the previous commit, on the regenerated feed: opening the class
surface let two kinds of non-API in.

- an aliased signal input was published twice — once under its alias, once as the
  raw declaration (`addOptionLabelInput: InputSignal<…>`). The alias means the
  declaration name never matches the published one, so the factory call is what
  identifies a binding, not its name
- `ngTemplateContextGuard` read as a callable static on 16 directives; it is a
  template type-check contract nobody calls, filtered next to the lifecycle hooks
- an `_`-prefixed property is internal by convention here, and stays out

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNHjonxsYvpdAXQfXbydYJ

This comment was marked as outdated.

Copilot's third round, plus one finding of my own in the same family.

- two distinct exports sharing a name kept only the first: `LinkComponent` and
  `ALuSelectInputComponent` each lost one of their two APIs. Merge and selection
  are keyed on the declaration now, and a collision states its import path under
  the heading — 1083 declarations from 1066 exported names, 17 recovered
- an unresolved public export vanished from every corpus while the entry floors
  stayed green; generation fails on it instead
- the internal-host gate scanned llms.txt alone. It now scans all 139 generated
  files: proven by planting `dd.lucca.tech` in a windowed feed — exit 1, exit 0
  once regenerated
- a pipe publishes the name a template pipes through (`value | luDate`), and a
  class or a service its public constructor (`new LuStringDateAdapter(_locale)`)
- mine, same family as the pipe name: `exportAs` was dropped on 24 directives, so
  `#ref="luDropdownTrigger"` could not be derived from the feed

The ratchet locks the gain in: MIN_DOCUMENTED 102 → 105, as the gate itself asks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNHjonxsYvpdAXQfXbydYJ
…enerates it

Twelve findings from an xhigh review of the whole surface. Two were live in the
published artifacts, two crashed on demand, and the rest were guards the code was
missing on paths it already walks elsewhere.

Live:
- a deprecation's import paths resolved by bare name, so `ALuSelectInputComponent
  .grouping` — deprecated only on the core-select class — was published as
  reachable from `@lucca-front/ng/select` too, whose class has no such member.
  Resolution goes through the declaration now; the three genuine re-exports keep
  both of their paths
- the constructor fence reused the table's `|` escaping, so `new LuDialogRef(…
  LuDialogResult<C> \| typeof DISMISSED_VALUE …)` did not compile for anyone who
  copied it. 0 escaped pipes left in a ```ts fence

Crashes, both reproduced by a test before the fix:
- a hostDirectives cycle recursed until the stack gave out — the guard classChain
  and interfaceChain already carry
- an exported name merged from an interface and a function called `getParameters`
  on the interface: `TypeError`, killing `npm run build`

Gates and blast radius:
- `assertFullyResolved` threw inside `generateAll`, and `build-storybook` runs it —
  an `export * as ns` would have taken the Storybook deploy down for a docs gap.
  It is strict in `docs:api` (CI, every push) and a warning elsewhere: measured
  with a real namespace export, exit 1 strict, exit 0 with a warning otherwise
- two entry points flattening to one slug would have overwritten a feed with every
  floor still green — refused now
- the smoke gate read `deprecations.json` outside its own try/catch, turning a
  missing artifact into a raw ENOENT stack
- an argTypes table keyed on prop names alone would attribute one story's prose to
  another; keyed on the whole descriptor now (0 of 637 files hit it today)
- `docs:llms-pack` paid the 11 s extraction before the dist check that is the
  likely failure

Lint, and the reason none of this was caught:
- `lint:es` covered `packages/` only, and every eslint block targets `**/*.ts`, so
  the ~2400 lines under `scripts/api-docs/` shipped unlinted — an unused const there
  reports 0 problems, the same line under `packages/` reports 1. Both scopes are
  linted now, and the tree is clean under the base JS rules
- the compodoc-shaped fallbacks the extractor never fills are gone: 12 shims
  removed, `llms-full.txt` byte-identical before and after (05e0e370a1e575b2)

Not taken, with the measurement: the double extraction is real but costs 11 s per
run, not minutes; and `cell`/`cleanCell` plus the two directory walkers diverge in
no file of the current corpus, while sharing them needs a new module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WNHjonxsYvpdAXQfXbydYJ
@F2X
F2X marked this pull request as draft September 10, 2026 13:21
@F2X F2X changed the title feat(docs): LLM documentation surface — API + stories feeds shipped with the Storybook feat(docs): [experimental] LLM documentation surface — API + stories feeds shipped with the Storybook Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 Technical Doesn't affect the output (refactor, dependencies update, cleaning, etc.)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: ship the llms*.txt surface with the public Storybook R2 deploy

3 participants