Uh oh!
There was an error while loading. Please reload this page.
fix(spec-docs): a module header's headings start at the page's section level - #12551
Conversation
…n level The 38 generated pages under `content/docs/references/**` rendered two `<h1>`. The second one was never in the MDX: it came from a JSDoc file header in `packages/spec/src/**`, which `build-docs.ts` copied verbatim into the page. A `.zod.ts` header is written as if it were a standalone document, so its author opens a section with `# ` — embedded under a page whose `<h1>` is already its frontmatter `title`, that compiles to a second one. 43 headings across the 38 pages. `renderFileDescription` now renumbers the fragment so its SHALLOWEST heading sits at `ctx.sectionLevel`, which `build-docs.ts` supplies as the level its own `## TypeScript Usage` and `## <SchemaName>` sections use. Renumbering rather than rewriting every `# ` to `## `, for three reasons the pins carry: 26 described modules already start at level 2 and a blanket shift would regenerate 26 pages that were never wrong; a block mixing `#` and `##` would collide two source levels into one; and — the load-bearing one — the shift is scoped to `prose` lines, so a `# Install pnpm globally` inside a fence is left alone. That last is why this lives beside `classifyLines` instead of running as a regex over the emitted string: a fence-blind pass corrupts working snippets to satisfy a rule about HTML those lines never produce. With the tree clean, `scripts/check-docs-single-h1.mjs` goes `DEAD-EXCLUSION` on its own carve-out, so that entry is deleted here. The gate's judged population grows from 356 to 394 pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
huangyiirene
commented
Aug 26, 2026
PM review — ACCEPT ( Contract verdict: Clause-② re-checked against the real diff — no on both limbs. No On the implementation deviating from the card's suggested blanket Points that carried the review:
Instrument disclosures (5 — including the discarded PREREQUISITE-NOT-MET readings rebuilt and re-run, and the TS2345s caught by typecheck after a green-but-type-erased vitest) are the discipline we keep — noted. Landing: waiting for every check green on Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12249
The 38 generated pages under
content/docs/references/**rendered two<h1>. The second one was never in the MDX — it came from a JSDoc file header inpackages/spec/src/**thatpackages/spec/scripts/build-docs.tscopied verbatim into the page. A.zod.tsheader is written as if it were a standalone document, so its author opens a section with#; embedded under a page whose<h1>is already its frontmattertitle(DocsTitle), that compiles to a second one.Option A per triage (comment 5413091398), not option B: the generator owns the page's heading levels, so the level rule belongs where the page is emitted, not in 41 comments that feed it. No
packages/spec/src/**JSDoc header is touched.What changed
packages/spec/scripts/lib/file-description.ts—renderFileDescriptionrenumbers the fragment so its shallowest heading sits atctx.sectionLevel, moving every other heading with it.packages/spec/scripts/build-docs.ts— declaresPAGE_SECTION_LEVEL = 2, uses it for its own## TypeScript Usageemission, and hands it torenderFileDescriptionas context. The level is injected rather than hard-coded in the renderer for the reasonfromCategoryis (#6484): only the page knows it. A.zod.tsheader cannot know what page it will be embedded in.scripts/check-docs-single-h1.mjs— thecontent/docs/references/**carve-out is deleted, per the issue's acceptance. The gate's judged population grows 356 → 394 pages. Its self-test was reworked in the same edit: the dead-exclusion limb and the "violations are still counted" control now run against the remainingreleases/**entry, the references fixture is kept as a positive control that the tree is back in scope, andscannedmoves 9 → 10.Why a renumbering and not
#→##Three properties the blanket demotion does not have, each pinned:
+1would have moved 105 correct headings a level deeper and regenerated 26 pages that were never wrong.automation/control-flowwrites##with###under it. Rewriting only#lines collides two source levels into one wherever a block mixes them.proselines, so a# Install pnpm globallyinside a fenced or indented block is untouched. This is why it lives besideclassifyLinesrather than as a regex over the emitted string inbuild-docs.ts—classifyLinesis already the one model in this pipeline that knows which lines are code, and a fence-blind pass rewrites working snippets to satisfy a rule about HTML those lines never produce (the same false positivecheck-docs-single-h1.mjs's own header measures at ten pages and calls more expensive than the defect).A shift that would push a heading past level 6 throws rather than emitting a seven-hash line. Unreachable on the current corpus — measured: no level-1-bearing description goes deeper than level 2 — and a loud failure is the right answer if a source ever gets there.
Regeneration
pnpm --filter @objectstack/spec gen:schema && gen:docs(never hand-edited). Exactly 38 pages changed, as the card predicted. Every one of the 89 changed lines is an ATX heading and nothing else — 43 level-1 headings became level 2, and the 46 level-2 headings inside those same 38 blocks moved to level 3 with them:Verification — all on the final commit
c4c84facheck:docs-single-h1✓ check-docs-single-h1: 394 page(s) under content/docs/ carry no body-level # heading (1 subtree(s) excluded); self-test✓ 17 cases pass@objectstack/spec check:docs✅ 229 generated files in sync with packages/spec@objectstack/spec check:scripts-typecheckEXIT=0— and--listFilesconfirms all three edited TS files are in the programfile-description.test.tsTests 70 passed (70)pnpm lint(repo-wideeslint . --no-inline-config)LINT_EXIT=0— full run, not narrowedcheck:nul-bytes,check:cross-package-test-inputs,check:test-source-alias,check:doc-anchors,check:changeset-gate-self-testsEXIT=0eachThe full family derived by
node scripts/pm/dispatch-gates.mjs(27 root gates + 5 filtered) ran green earlier in the branch; the table above is the re-run on the final tree.Reverse verification (measured, ordinary direction). Replacing the
withHeadingsAtSectionLevelcall with the identity turns 9 of the 70 cases red — every level-1 case plus both corpus limbs, the first reporting exactly the 43 headings the issue measured onmain. The two cases about what the shift must not touch (already start at level 2,#NoSpace) stay green, as do the 61 cases the file already had. The mutation was confirmed on disk before the run (anchor hit count 1 → 0, injected line 0 → 1, blob2581ae65→615e62fb) and the restore leg proved bygit diff HEADempty. No build is involved: the test imports./lib/file-descriptionby relative path, so vitest transforms the source directly — nodist/can make this reading stale.Disclosures
check:generated— one limb NOT MEASURED here, and it is not this diff's. It reports✗ 1 of 14 artifact(s) stale: api-surface/, with its own reason:❌ packages/spec/dist holds no .d.ts declarations — the package is not built.api-surface/is derived from the built.d.ts(build-api-surface.ts: "the built dist — run after build"), and this worktree has nopackages/spec/dist. The limb this change owns,✓ check:docs content/docs/references/**, is green in that same run, and this commit's 42 files contain nopackages/spec/src/**, nopackage.jsonand no export change, so nothing here can moveapi-surface/. CI builds and owns it.check:doc-formula-expressions/check:doc-security-posturefirst read asPREREQUISITE NOT MET— an unbuilt@objectstack/formula/@objectstack/lint, which those gates label "Nothing was measured… NOT a finding". Discarded that reading, built both packages, re-ran:GATE1_EXIT=0,GATE2_EXIT=0.FileDescriptionContextliterals were missed by the first pass and caught bycheck:scripts-typecheck(TS2345), not by vitest — the runtime suite was green while the types were wrong. Fixed and re-run before the final commit.Changeset
None —
skip-changeset. This PR publishes nothing:packages/spec'sfilesarray does not includescripts/, andcontent/docs/**and rootscripts/**ship in no tarball. Nopackages/spec/src/**was touched, so the published surface is byte-identical. Per AGENTS.md, pure bug fixes do not require a changeset, and the workflow's own prescription calls "this PR edits a CI-internal script" the textbook label case.Epic
Part of the definition of done in #12243 — that epic's no page renders more than one
<h1>count includes these 38 pages, which is why this stayed a sub-issue of it even though the fix lives outside the epic's declaredapps/docs/**+content/docs/**territory (epic PM routing, comment 5412736502). #12250 remains open and is untouched here:content/docs/releases/**keeps its carve-out, and no file under it was edited.Generated by Claude Code