Skip to content

chore(docs): flatten MDX tab JSX out of the generated LLM artifacts - #408

Merged
allxsmith merged 2 commits into
mainfrom
docs/flatten-llms-tabs
Jul 29, 2026
Merged

allxsmith merged 2 commits into
mainfrom
docs/flatten-llms-tabs

Conversation

@allxsmith

@allxsmith allxsmith commented Jul 29, 2026 •

Copy link
Copy Markdown
Owner

Unblocks #402. This is the LLM-pipeline half only — no docs pages change yet, and PackageManagerTabs doesn't exist yet. Landing the flattener first means the tabs migration can go page-by-page without regressing the machine-readable docs on the way.

Why a build step and not a plugin

docusaurus-plugin-llms reads the source markdown, not rendered HTML. Its only cleanup (cleanMarkdownContent, utils.ts:587) strips a fixed allowlist of plain HTML tags that doesn't include Tabs/TabItem, and PluginOptions (types.ts:69) exposes no transform hook. So tab JSX lands verbatim in llms.txt, llms-full.txt and every per-page .md twin — 112 tag occurrences today, from the skills pages alone.

It can't be a sibling Docusaurus plugin either: core runs every plugin's postBuild under Promise.all (@docusaurus/core/lib/commands/build/buildLocale.js:106), so it would race the llms plugin nondeterministically. Hence docusaurus build && node scripts/flatten-llms-tabs.mjs, which is also the path deploy.yml / test-deploy.yml already take via turbo run build --filter=@allxsmith/bestax-docs.

Two transforms, because there are two kinds of tabs

kind example rule
equivalent alternatives <PackageManagerTabs command="add …" /> collapse to one fenced pnpm block
complementary content skills pages' ProfileCard.tsx / _profilecard.scss / Usage linearize to #### headings, keep every body

pnpm for the first because it's the default tab — what an agent copies out of llms.txt has to match what a reader sees. command is authored in pnpm's verb vocabulary so this stays a passthrough; npm/yarn/bun translation will live in the React component rather than being duplicated here.

The second rule matters: my first draft of this kept only the first TabItem, which would have silently deleted the skills pages' other two tabs.

The trap worth reviewing

bestax-bulma has its own Tabs component. <Tabs\b matches <Tabs.List — \b sits between the s and the .. My first pass left 94 stragglers and would have corrupted docs/api/components/tabs.md.

Two mitigations, both required and both tested:

  • match <Tabs(?=[\s>]), never <Tabs\b
  • mask code before transforming — fenced blocks and inline spans, since the migration guides write `<Tabs>` in prose. Mask rather than split, because a <Tabs> block wraps fences and the document has to stay contiguous for <Tabs>…</Tabs> to match across it.

Verification

node --test — 13 tests, including the <Tabs.List>-in-fence regression, inline-span survival, idempotency, and placeholder-collision safety. Wired as the docs package's test script, so pnpm test / pnpm all pick it up through turbo with no CI changes.

End-to-end against the current build/:

theme Tabs/TabItem left outside code: 0     (was 8 before inline-span masking)
library <Tabs.List> preserved       : 19
docs/api/components/tabs.md         : byte-identical
second run                          : 0 files rewritten (idempotent)

Known limitation

A braced-template prop — command={`add foo`} — is deliberately unsupported: its backticks are indistinguishable from an inline code span, and masking has to run first. It's left untouched in the artifact rather than mangled, and there's a test pinning that behavior. Authors use a plain string.

Not in scope

Converting the ~18 docs pages, building PackageManagerTabs, and the homepage hero links — all still #402.


Unrelated, noticed while running the gate: pnpm format:check fails locally on 133 files under .claude/worktrees/ (the skill-loop eval worktree). That path is git-ignored so CI is unaffected, but Prettier only honors .prettierignore, so local pnpm all is noisy. Happy to add a .prettierignore entry in a separate PR if you want it.

Summary by CodeRabbit

  • New Features

    • Documentation builds now produce cleaner LLM-friendly output by converting package manager and content tabs into readable commands and headings.
    • Generated documentation preserves tab content while removing rendering-specific markup.
  • Documentation

    • Added guidance describing the updated documentation pipeline and tab rendering behavior.
  • Tests

    • Added coverage for tab conversion, code protection, import cleanup, supported commands, and repeated processing.

docusaurus-plugin-llms reads source markdown and exposes no transform
hook, so <Tabs>/<TabItem> JSX lands verbatim in llms.txt, llms-full.txt
and every per-page .md twin. Today that already leaks 112 tab tags from
the skills pages; #402 would spread it across ~18 more.

Add scripts/flatten-llms-tabs.mjs, chained onto the docs build. It can't
be a Docusaurus plugin: core runs every plugin's postBuild under
Promise.all, so a sibling would race the llms plugin.

Two transforms, because there are two kinds of tabs:

- <PackageManagerTabs command="..." /> collapses to a single fenced pnpm
  block, matching the default tab, so what an agent copies out of
  llms.txt is what a reader sees.
- Generic <Tabs> linearizes to #### headings, keeping every TabItem
  body. Keeping only the first would have deleted the skills pages'
  complementary ProfileCard.tsx / _profilecard.scss / Usage tabs.

Both run only outside code, fenced and inline. That is load-bearing, not
defensive: bestax-bulma has its own Tabs component, and `<Tabs\b` matches
`<Tabs.List` because \b sits between the s and the dot. Without masking,
the flattener corrupts docs/api/components/tabs.md.

Verified against the current build/: 0 theme tabs left outside code,
all 19 <Tabs.List> usages preserved, api/components/tabs.md byte
identical, and idempotent on a second run.
@coderabbitai

coderabbitai Bot commented Jul 29, 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: 4d529f90-5801-49b7-a953-54cde1bfde9d

📥 Commits

Reviewing files that changed from the base of the PR and between 9616e52 and 867b0e2.

📒 Files selected for processing (2)
  • docs/scripts/flatten-llms-tabs.mjs
  • docs/scripts/flatten-llms-tabs.test.mjs

Walkthrough

Adds a post-build Node script that flattens tab components in generated LLM documentation artifacts, preserves code examples, removes related imports, and includes tests, build wiring, and pipeline documentation.

Changes

LLM tab flattening

Layer / File(s) Summary
Tab transformation and validation
docs/scripts/flatten-llms-tabs.mjs, docs/scripts/flatten-llms-tabs.test.mjs
Package-manager tabs become pnpm blocks, generic tabs become headings with preserved bodies, code content remains unchanged, imports are cleaned up, and transformations are idempotent.
Artifact traversal and build integration
docs/scripts/flatten-llms-tabs.mjs, docs/package.json, docs/CLAUDE.md
Matching Markdown and llms*.txt files are rewritten after docusaurus build; build/test commands and pipeline behavior are documented.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BuildScript
  participant Docusaurus
  participant FlattenScript
  participant LLMArtifacts
  BuildScript->>Docusaurus: run docusaurus build
  Docusaurus-->>LLMArtifacts: generate documentation artifacts
  BuildScript->>FlattenScript: run post-build flattening
  FlattenScript->>LLMArtifacts: scan and rewrite matching files
  FlattenScript-->>BuildScript: report rewritten file count
Loading

Suggested labels: deep-review

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the main change, issue, type, tests, and context, but it doesn't follow the repo's template structure or package checklist format. Reformat the PR description to match the template headings, explicitly mark the affected package(s), and fill the checklist items in checkbox form.
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: flattening MDX tab JSX in generated LLM artifacts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/flatten-llms-tabs

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://d24a409c.bestax.pages.dev

@allxsmith

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/scripts/flatten-llms-tabs.mjs`:
- Around line 106-107: Update the masking logic around the fenced-block and
inline-span replacements to use a CommonMark-compatible scanner that protects
all valid Markdown code delimiters, including tilde fences, longer backtick
fences, and multiline inline code, before flattening <Tabs> markup. Add
regression coverage for each affected form while preserving existing masking
behavior.
- Line 142: Update the prefilter in transform() so artifacts containing
tab-related imports are processed even when before lacks literal tab JSX;
include the relevant import patterns in the predicate or remove the guard and
rely on the existing after === before check, while preserving current skipping
behavior for unrelated artifacts.
- Around line 115-120: Move the import-removal and repeated-newline cleanup
replacements into the callback passed to outsideCode, applying them to each
chunk after flattenGenericTabs(flattenPackageManagerTabs(chunk)) and before
outsideCode restores held code. Keep outsideCode as the outer wrapper so fenced
examples retain their imports and intentional spacing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 17348f9b-c640-4dbd-bdb7-83aa8762cc30

📥 Commits

Reviewing files that changed from the base of the PR and between 5d114e1 and 9616e52.

📒 Files selected for processing (4)
  • docs/CLAUDE.md
  • docs/package.json
  • docs/scripts/flatten-llms-tabs.mjs
  • docs/scripts/flatten-llms-tabs.test.mjs

Comment thread docs/scripts/flatten-llms-tabs.mjs Outdated
Comment thread docs/scripts/flatten-llms-tabs.mjs
Comment thread docs/scripts/flatten-llms-tabs.mjs Outdated
…e mask

Three findings from the CodeRabbit review of #408:

1. Masking only recognised exact triple-backtick fences. Tilde fences and
   longer fences went unprotected. Replaced the regex with a line-based
   CommonMark scanner: either fence character, length >= 3, closing fence
   the same character and at least as long, up to 3 leading spaces, and a
   backtick info string may not contain backticks. An unterminated fence
   now runs to end of document and is masked rather than rewritten.

   Reproduced for tilde fences, which the old code mutated. The longer
   fence case in docs/api/helpers/theme.md and tutorial-basics did not
   reproduce as an observable defect, but the scanner removes the class.

   Multiline inline spans are still matched single-line on purpose: an
   unpaired backtick in prose would otherwise swallow the document.

2. Import removal and blank-line collapsing ran after held code was
   restored, so they reached into fenced examples — stripping
   `import Tabs …` demo lines and collapsing deliberate blank lines in
   code blocks. Both reproduced; both now run inside the mask.

3. The main() prefilter skipped artifacts carrying only tab imports,
   which transform() would still have cleaned. Shared as NEEDS_FLATTENING
   so the predicate and the transform agree.

21 tests, up from 13. Output over the current build/ is byte-identical to
the pre-fix run — 0 theme tabs outside code, 19 <Tabs.List> preserved,
api/components/tabs.md unchanged, idempotent — so this closes latent edge
cases without moving real-world output.
@allxsmith

Copy link
Copy Markdown
Owner Author

All three addressed in 867b0e2. Two reproduced as real bugs, one didn't — details below, since the distinction matters for what changed.

1. Mask all valid Markdown code delimiters — fixed

Replaced the fence regex with a line-based CommonMark scanner: either fence character, length ≥ 3, closing fence the same character and at least as long, up to 3 leading spaces of indentation, and a backtick fence's info string may not contain backticks. An unterminated fence now runs to end of document and is masked rather than rewritten — when we can't confidently parse, we don't mutate.

Tilde fences reproduced. Against the pre-fix code:

UNCHANGED (ok)   long fence wrapping short
MUTATED  (bug)   tilde fence
MUTATED  (bug)   import inside fence
MUTATED  (bug)   blank lines in fence

Longer fences did not reproduce. docs/api/helpers/theme.md and docs/tutorial-basics/markdown-features.mdx do wrap ``` examples in ```` fences, so I built the test from that real shape — but old and new agree on it exactly (fenced example intact, real tab flattened, no #### Nope leak). Fixed anyway, since the scanner removes the class rather than the instance, but I'd rather be straight that I couldn't produce a failing case for that half.

Multiline inline spans are still single-line on purpose. CommonMark permits them, but matching across newlines means one unpaired backtick in prose swallows the rest of the document. The failure mode of guessing wrong is far worse than the miss, so this is documented in the code rather than implemented.

2. Keep cleanup inside outsideCode — fixed, as suggested

Confirmed both halves against the pre-fix code: a fenced example demonstrating import Tabs from '@theme/Tabs'; lost that line, and \n{3,} collapsed deliberate blank lines inside code blocks. Took the suggested diff. Regression tests for each.

3. Don't skip import-only artifacts — fixed

Extracted the predicate as an exported NEEDS_FLATTENING used by both main() and the tests, so the prefilter and transform can't drift apart again.

Worth noting for context: docusaurus-plugin-llms runs with excludeImports: true, and the deployed llms-full.txt contains 0 import lines — the plugin strips them upstream before this script ever runs. So the import handling here is defence in depth rather than load-bearing today. Still worth the consistency.

Verification

21 tests, up from 13. Output over the current build/ is byte-identical to the pre-fix run:

theme Tabs/TabItem outside code: 0
library <Tabs.List> preserved  : 19
flattened #### headings        : 165
docs/api/components/tabs.md    : byte-identical
second run                     : 0 files rewritten

So this closes latent edge cases without moving real-world output — which is the outcome I wanted from a review fix.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://4474abae.bestax.pages.dev

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep review — 0 blocking · 2 advisory

# Severity Area Finding Location
1 🔵 Advisory Robustness Post-mask cleanup (import strip + \n{3,} collapse) runs on restored text including code fences, breaking the "never touch code" invariant the masking establishes. Harmless today, latent for future pages. docs/scripts/flatten-llms-tabs.mjs:118
2 🔵 Advisory Robustness Code masking only recognizes 3-backtick fences (^```…^```$); ~~~ tilde fences and ≥4-backtick fences are not masked, so a <Tabs.List>/theme-tab lookalike inside one would be rewritten. None exist today (Prettier normalizes to 3-backtick). docs/scripts/flatten-llms-tabs.mjs:106

Overall: The change is sound and unusually well-guarded. The load-bearing decisions — <Tabs(?=[\s>]) instead of <Tabs\b, masking code (fenced and inline) before transforming, and keeping every TabItem body — are correct and each has a matching regression test (13/13 pass; pnpm test is wired via the new docs test task, which turbo run test picks up). I verified against the real corpus: every library <Tabs …> usage in tabs.md, hero.md, components.md, and the migration guides lives inside a ```tsx live fence (so it's masked), the skills pages' <Tabs> blocks (theming.mdx, custom-component.mdx) linearize to #### headings keeping all bodies, and their in-fence imports are import React, {…} / named imports that the strip regex cannot match. The riskiest part is the two post-mask passes in transform() — see below — but they have no effect on current content. The human should decide whether to move those two passes inside outsideCode before PackageManagerTabs and the #402 page migration expand the corpus.

Residual risk — ways tab JSX could still leak into (or corrupt) the artifacts:

  • Theme tabs surviving un-flattened: refuted — the file gate /<Tabs|<TabItem|<PackageManagerTabs/ plus <Tabs(?=[\s>])…</Tabs> cover both current skills blocks; the PR's e2e check reports 0 theme tabs left outside code and I confirmed the sources.
  • Library <Tabs.List> corrupted: refuted for current content — all 19 usages sit inside 3-backtick fences and are masked; tabs.md is byte-identical. Only breaks under advisory #2's exotic fence syntax, which Prettier prevents.
  • Content silently deleted: refuted — flattenGenericTabs maps every TabItem (not just the first) and items.length===0 falls back to inner.trim(); the placeholder-collision test pins that digit runs in prose can't be mistaken for a masked-code sentinel.
  • Documented code mutated: the one real (but currently inert) gap — advisory #1. Requires a future fenced example containing a default import Tabs/TabItem/PackageManagerTabs from … line or ≥3 consecutive blank lines; none exist today (Prettier caps blank runs at 1; the skills fences use named / React imports).

🏄 Clean set, dude — this flattener reads the break perfectly: masks the code, keeps every tab body, and dodges the <Tabs.List shorebreak that would've wiped out the library docs. The only ripples are two cleanup passes surfing just outside the mask, and they're glassy today. Paddle it out. 🌊

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a post-build “LLM artifact” cleanup step to the docs pipeline so that MDX tab JSX (<Tabs>, <TabItem>, and the upcoming <PackageManagerTabs />) doesn’t leak into llms.txt, llms-full.txt, or the per-page .md twins produced by docusaurus-plugin-llms. This supports the planned page-by-page migration in #402 without degrading the machine-readable docs surface.

Changes:

  • Introduces docs/scripts/flatten-llms-tabs.mjs to rewrite only the generated LLM artifacts after docusaurus build, flattening tab JSX outside code (fenced + inline).
  • Adds node --test coverage for the transforms and artifact selection logic.
  • Wires the flattener into the docs build and documents the pipeline behavior in docs/CLAUDE.md.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
docs/scripts/flatten-llms-tabs.mjs Implements post-build transforms to flatten tab JSX in LLM artifacts while masking fenced and inline code.
docs/scripts/flatten-llms-tabs.test.mjs Adds node:test coverage for the transforms (tabs flattening, code masking, idempotency, artifact detection).
docs/package.json Chains the flattener after docusaurus build and adds a docs-package test script to run the new tests.
docs/CLAUDE.md Documents why/when the flattener runs and what it guarantees about code-block safety and output shape.

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

@allxsmith
allxsmith merged commit 69f9fe7 into main Jul 29, 2026
20 checks passed
@allxsmith
allxsmith deleted the docs/flatten-llms-tabs branch July 29, 2026 11:46
allxsmith added a commit that referenced this pull request Jul 31, 2026
…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
allxsmith added a commit that referenced this pull request Jul 31, 2026
…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
allxsmith added a commit that referenced this pull request Jul 31, 2026
…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
allxsmith added a commit that referenced this pull request Jul 31, 2026
…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
allxsmith added a commit that referenced this pull request Jul 31, 2026
…420)

* 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

* 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

* 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): 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): 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): 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): 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
…#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
* 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