Skip to content

docs content: the 38 generated reference pages still render two <h1> — the heading comes from a spec JSDoc header via build-docs.ts #12249

Description

@os-zhuang

One-liner

38 generated pages under content/docs/references/** still render two <h1>. The heading is not in the MDX — it comes from a JSDoc file header in packages/spec/src/**, copied verbatim into the page by packages/spec/scripts/build-docs.ts. #12236 could not fix them and carved the tree out of its new gate.

Measured on origin/main at 20b0fdb

pages under content/docs/** with a body-level `# ` heading   195
  of which generated (carry the AUTO-GENERATED marker)        38
  body `# ` headings inside those 38 pages                    43   (41 distinct texts)

Two examples, and where the text really lives:

content/docs/references/data/context-tokens.mdx:11   # Why this lives in `spec`
  <- packages/spec/src/data/context-tokens.zod.ts:10   * # Why this lives in `spec`

content/docs/references/kernel/*.mdx                 # <Name> Protocol
  <- the same JSDoc file header, rendered by renderFileDescription()

build-docs.ts line ~422 inserts fileDesc into the page with no heading-level
normalisation:

if (fileDesc) {
    md += `${fileDesc}\n\n`;
}

Why #12236 could not do it

  1. Hand-editing the 38 MDX files is reverted by the next generator run, and check:docs (pnpm --filter @objectstack/spec check:docs, a required step in Lint & Repo Gates) regenerates the tree and fails on any difference — so the hand-fix would have shipped CI-red.
  2. The real fix is in packages/**, which epic epic(docs-site): the site is technically un-indexable — fix robots/sitemap/canonical/OG first, then the keyword shape #12243 declares out of territory.

Options

  • A — normalise in the generator. Demote every ATX heading in fileDesc by one level at emit time in build-docs.ts. One place, fixes all 38 and every future one, regenerates 38 tracked files. Note ## TypeScript Usage is already emitted at level 2, so a plain #→## demotion lands the file-description headings as its siblings, which is the right level for them.
  • B — fix the JSDoc sources. 41 headings across packages/spec/src/**. Same output, distributed edit, nothing stops the next one.

A is the contract-first shape: the generator owns the page's heading levels, so the level rule belongs where the page is emitted, not in every comment that feeds it.

Acceptance

  • no page under content/docs/references/** contains a body-level # heading
  • pnpm --filter @objectstack/spec check:docs is green with the regenerated tree committed
  • the content/docs/references/** carve-out is deleted from scripts/check-docs-single-h1.mjs — that gate FAILS when an excluded tree becomes clean, so this is mechanical, not a thing to remember

Activity

  1. os-zhuang commented on Aug 25, 2026

    @os-zhuang
    ContributorAuthor

    Routing, from the epic PM for #12243.

    This one leaves the epic's declared territory: the duplicated heading originates in a packages/spec/src JSDoc file header that build-docs.ts copies verbatim, so the fix is in the generator or in the spec source — and the epic declared apps/docs/** + content/docs/** only. Per the repo's cross-seat rule, anything touching packages/spec belongs to the domain:spec seat, which is its single owner; the epic PM does not dispatch it.

    It stays a sub-issue of #12243 because the epic's definition of done says no page renders more than one <h1>, and these 38 pages are part of that count. Treat it as the epic's Blocked-by on the spec lane rather than as epic work.

    Note for whoever takes it: hand-editing the generated pages ships CI-red — check:docs regenerates the tree and fails on any hand-edit. The generator is the only place this can be fixed.

  2. self-assigned this
    on Aug 26, 2026
  3. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    Claim: session_01E5LFCYBJ3q2s6yW6oMLxwy (domain:spec PM seat R7, seat post #6017) — branch claude/issue-12249-single-h1-references.

    • File surface: packages/spec/scripts/build-docs.ts (option A — demote ATX headings in fileDesc at emit time), regenerated content/docs/references/** (38 pages, repo tooling only — never hand-edited, check:docs regenerates and fails on hand-edits per the epic PM's note), and scripts/check-docs-single-h1.mjs (delete the content/docs/references/** carve-out — acceptance per triage). No overlap with the other in-flight card ([finding] unregistered dogfood proof tag admin-platform-admin-standing — pre-existing check:liveness warning, recurrence of #10773's class #12216: proof-registry.mts).
    • Territory: sub-issue of epic epic(docs-site): the site is technically un-indexable — fix robots/sitemap/canonical/OG first, then the keyword shape #12243, but triage-designated to this seat — the epic declares packages/** outside its territory; the sub-issue link stays for the epic's checklist only.
    • Container & model: node scripts/pm/dispatch-gates.mjs --tier packages/spec/scripts/build-docs.ts scripts/check-docs-single-h1.mjs content/docs/references derived at origin/main 5d4d93a: no path-derived mandate. Content judgment (matches triage): JSDoc heading levels move at render time, no schema accept/reject behaviour changes → opus.
    • Clause-②: no (both limbs no).
    • Serial constraints: generated-tree card — any merge conflict in content/docs/references/** goes through bash scripts/pm/os-regen-merge.sh, never text-merge.

    Generated by Claude Code

  4. huangyiirene commented on Aug 26, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 12249,
      "verdict": "implemented",
      "status": "done",
      "branch": "claude/issue-12249-single-h1-references",
      "pr_number": 12551,
      "pr_url": "https://github.com/objectstack-ai/objectstack/pull/12551",
      "premise_still_valid": true,
      "head_commit": "c4c84fa",
      "diff_summary": "42 files, +453/-127. Option A as triaged. packages/spec/scripts/lib/file-description.ts: renderFileDescription now renumbers the rendered fragment so its SHALLOWEST heading sits at ctx.sectionLevel, moving every other heading with it (prose lines only). packages/spec/scripts/build-docs.ts: new PAGE_SECTION_LEVEL = 2 constant, used for its own '## TypeScript Usage' emission and injected as ctx.sectionLevel. packages/spec/scripts/file-description.test.ts: 9 new unit pins + 2 corpus limbs (70 tests total). scripts/check-docs-single-h1.mjs: the content/docs/references/** carve-out deleted, header prose and self-test reworked around the remaining releases/** entry. 38 regenerated pages under content/docs/references/**. No packages/spec/src/** JSDoc header touched (that was option B, rejected).",
      "pages_regenerated": 38,
      "carve_out_deleted": true,
      "checks_run": [
        "check:docs-single-h1 EXIT=0 - self-test 17 cases pass; corpus verdict line: 394 page(s) under content/docs/ carry no body-level '# ' heading (1 subtree(s) excluded). Judged population grew 356 -> 394.",
        "@objectstack/spec check:docs EXIT=0 - verdict line: 229 generated files in sync with packages/spec",
        "@objectstack/spec check:scripts-typecheck EXIT=0 - tsconfig.scripts.json; --listFiles confirms all three edited TS files are in the program",
        "vitest scripts/file-description.test.ts EXIT=0 - Tests 70 passed (70)",
        "pnpm lint (repo-wide eslint . --no-inline-config) LINT_EXIT=0 - FULL run, not narrowed",
        "check:nul-bytes EXIT=0; check:cross-package-test-inputs EXIT=0; check:test-source-alias EXIT=0; check:doc-anchors EXIT=0; check:changeset-gate-self-tests EXIT=0",
        "Full family from `node scripts/pm/dispatch-gates.mjs` (no path list passed): 27 root gates all PASS + 5 filtered gates - ran earlier in the branch; the list above is the re-run on the final commit c4c84fa",
        "REVERSE VERIFICATION: replacing the withHeadingsAtSectionLevel call with the identity turns 9 of 70 cases red - every level-1 case plus both corpus limbs, the first reporting exactly the 43 headings the issue measured on main. The two cases about what the shift must NOT touch (already-at-level-2, #NoSpace) stay green, as do the 61 pre-existing cases. Mutation confirmed on disk BEFORE the run: anchor grep count 1 -> 0, injected line 0 -> 1, blob 2581ae65 -> 615e62fb. Restore leg proved by `git diff HEAD` empty, trap-guarded with absolute paths. No build involved - the test imports ./lib/file-description by relative path, so vitest transforms source directly and no dist/ can make the reading stale."
      ],
      "premise_notes": "Premise VERIFIED on fresh origin/main (c0f5e8f, branch point). Re-measured independently: 38 pages under content/docs/references/** with a body-level '# ' heading, all 38 carrying the AUTO-GENERATED marker, 43 heading lines, 41 distinct texts - matching the issue body exactly. Rendering every packages/spec/src/**/*.zod.ts module header confirmed the same 38 sources. build-docs.ts line ~422 inserted fileDesc with no heading normalisation, as described. One measurement CHANGED the implementation shape versus the issue's suggested plain '#'->'##' demotion: 26 further described modules already start their headings at level 2 (105 headings), so a blanket +1 would have demoted those correctly-levelled headings and regenerated 26 extra pages. The shipped fix therefore renumbers relative to the block's shallowest heading, which only ever demotes and touches exactly the 38. Max heading depth in any level-1-bearing description is 2, so no overflow past level 6 (a shift that would is a loud throw).",
      "new_findings_filed": [],
      "out_of_scope_findings": [],
      "instrument_disclosures": [
        "check:generated reports '1 of 14 artifact(s) stale: api-surface/' with its own reason printed: '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 no packages/spec/dist. That limb is NOT MEASURED here, not red. The limb this change owns is green in the same run: 'check:docs content/docs/references/**'. This commit's 42 files contain no packages/spec/src/**, no package.json and no export change, so nothing in this diff can move api-surface/. CI builds and owns it.",
        "check:doc-formula-expressions and check:doc-security-posture first exited 1 with 'PREREQUISITE NOT MET' (unbuilt @objectstack/formula / @objectstack/lint) - the gates' own text says 'Nothing was measured... NOT a finding'. That reading was DISCARDED, both packages built, gates re-run: GATE1_EXIT=0, GATE2_EXIT=0.",
        "gen:docs itself refused to run against a missing packages/spec/json-schema tree rather than emitting pages from stale input. Ran gen:schema first, then gen:docs. Working as designed; recorded because the first regeneration attempt produced no output.",
        "Two FileDescriptionContext literals were missed by my first edit pass. vitest stayed GREEN (types are erased at runtime); check:scripts-typecheck caught them as TS2345. Fixed and both re-run before the final commit - so the reported 70/70 is on the corrected tree.",
        "LABEL WRITE USED THE DECLARED FALLBACK, not the additive endpoint. The additive POST /issues/12551/labels returned HTTP 403 ('GitHub access is not enabled for this session') from this dev seat, so skip-changeset was applied via read -> union -> whole-set write through MCP issue_write. Read-back after the labeler bot settled confirms labels = ['size/l','skip-changeset'] - the bot's size/l was preserved, nothing erased."
      ],
      "summary": "Fixed in the generator per option A. renderFileDescription renumbers a module header's headings so the fragment's shallowest one sits at the page's section level (2), which build-docs.ts now names as PAGE_SECTION_LEVEL and injects as context - the same seam, and the same argument, as fromCategory (#6484): a .zod.ts header cannot know what page it will be embedded in. The shift is scoped to prose lines using the renderer's existing classifyLines model, so a '# ' inside a fenced or indented code block is untouched; that is why it lives beside classifyLines rather than as a regex over the emitted string in build-docs.ts, since a fence-blind pass corrupts working snippets to satisfy a rule about HTML those lines never produce. Regenerated with repo tooling only: exactly 38 pages, and every one of the 89 changed lines is an ATX heading. The references carve-out is deleted from scripts/check-docs-single-h1.mjs and its self-test reworked so the dead-exclusion limb now guards the remaining releases/** entry; the gate's judged population grows 356 -> 394. No changeset: this PR publishes nothing (packages/spec 'files' excludes scripts/), so it carries skip-changeset instead. Draft PR, no auto-merge; CI convergence is the PM's to review.",
      "open_questions": []
    }

    Generated by Claude Code

  5. removed their assignment
    on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions