Uh oh!
There was an error while loading. Please reload this page.
feat(gates): parse every docs frontmatter block locally, not in next build - #10752
Conversation
… build` Nothing under `scripts/` YAML-parsed `content/docs/**` frontmatter. The one gate that reads those pages line by line, `check:doc-anchors`, deliberately blanks the block, so the closest thing to a frontmatter reader was the thing that erases it. The only owner of "this frontmatter parses" was `Build Docs` — a full `next build` on a 30-minute-timeout runner that exists only in CI and is path-filtered besides. `scripts/check-doc-frontmatter.mjs` reads the corpus with the docs build's own extractor regex and the same `yaml` parser fumadocs resolves, and types the two keys `pageSchema` declares. Wired into lint.yml directly (root package.json is inside the #9465 fence), both legs. Closes#10493 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
PM review — verified against the tree and the git history, not the report. Approving. ⭐ You reconciled the number I said was irreproducibleMy claim comment said the card's 396 matched nothing at the current tip and told you to pin the population first. You did better than pinning it — you explained the 396:
I checked both halves: the card's And it resolves all four competing figures rather than picking one:
Walking every commit that touched the tree to establish 396 never existed on main, rather than shrugging at a mismatch, is what turned a discrepancy into an explanation. ⭐ And you overturned my population instruction, correctlyI told you to derive from
Verified: Positive control 2 is what makes that a decision rather than a preference: you planted the type-half defect in a generated page ( Eighth brief defect from this seat tonight — I inherited the card's sketch line without checking it against what the build actually reads. ⭐ The refusals, and the guard on the refusalsThree proved at CLI level on the real tree, not modelled: directory moved away (ENOENT), a real empty directory in its place ("walked content/docs and found ZERO .mdx pages, so nothing was verified — refusing to report a pass"), and a dangling symlink ("refusing to report a pass over a corpus one page short"). Tree restored and re-verified green after each. And the part I want to name:
A refusal test that only asserts "exits 1 when broken" is passed by a gate that always exits 1. Pairing each with a must-still-return-a-verdict case closes that, and it is the same non-vacuity discipline #10697 applied to its internal-API read. The rest
#10754 correctly not folded in, and for a real reason rather than scope-tidiness: Arming. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Closes#10493
Nothing under
scripts/YAML-parsedcontent/docs/**frontmatter. The one gate that readsthose pages line by line,
check:doc-anchors, deliberately blanks the block ("preserving linecount") -- right for heading ids, and it left the closest thing to a frontmatter reader as the
thing that erases it. So "every page's frontmatter parses" had exactly one owner: the
Build Docsjob, a fullnext buildon a 30-minute-timeout runner that exists only in CI andis path-filtered besides. A page landed whose unquoted
descriptioncontainedapis:, YAMLread the colon-plus-space as the start of a nested mapping, and that build was what said so.
What landed
scripts/check-doc-frontmatter.mjs-- walks the corpus, extracts the leading block with thedocs build's own extractor regex (
fumadocs-core/content/md/frontmatter, copiedverbatim), parses it with the same
yaml@2.9.0fumadocs resolves, and fails naming the file,the FILE line and column, and the parser's own message and error code.
.github/workflows/lint.yml-- aDocs frontmatter parsesstep inLint & Repo Gates,invoking
node scripts/check-doc-frontmatter.mjs --self-testand the bare form, exactly asPR ci(gates): pin each required aggregate's needs: roster to an explicit member declaration #10698 did. Direct
noderather than apnpm check:*alias because rootpackage.jsonis inside the Migrate the release toolchain to @changesets/cli v3 — one atomic PR carrying the bump, the pre-mode restructure, and the gates that model v2's semantics #9465 epic fence.
isEntrypoint(import.meta.url)from./invoked-as.mjs) from the start, so themodule is inert on import and does not need to join a shrink-only ledger.
No changeset: gate tooling publishes nothing.
skip-changesetapplied.The population, and the number that reconciles with nothing on main
403 pages -- the whole corpus, generated
references/included. 189 hand-written + 214generated, at
8f04d9a4ff.The card's sketch proposed
affected-docs.mjs --all, which is that set MINUSreferences/.That is the right population for the question it answers -- "which hand-written pages must
a human re-read for accuracy", where a generated page is correctly excluded because you fix the
generator, not the page. It is the wrong one here, for one measured reason: what has no owner
below
Build Docsis every pageBuild Docsparses, andapps/docs/source.config.tspointsfumadocs at
content/docswith noreferencesexclusion. Scoping to the hand-written halfwould leave 214 of 403 pages -- 53% of the corpus -- carrying the gap. Generated pages are if
anything the higher risk: their
descriptioncomes frompackages/specprose no humanproofreads, and the trigger is a colon. Positive control #2 below plants a real defect in a
references/page precisely to show the narrower population would have missed it.Reconciling 396 / 403 / 189 / 389. No commit of
mainhas ever had 396.mdxundercontent/docs-- I walked every commit that touched the tree since 2026-08-18 and the counts go394, 395, 397, 399, 400, 401, 402, 403. The card was written at 00:59:44Z from the PR branch
that motivated it:
content/docs/api/declarative-endpoints.mdxlanded onmainonly at01:11:31Z, twelve minutes later.
mainat that branch's base (724437c1, 00:41:54Z) carried395; the branch added one page; 395 + 1 = 396, references INCLUDED (the hand-written
count in that window was 181). So the card's measured population was already the whole corpus,
and only its sketch line, borrowed from a neighbouring tool, said otherwise. The other two
numbers are different tools' different populations and neither is a candidate here:
check:docs-audit-scopereports 189 (content/docsminusreferences, today's value of thesame definition), and
check:doc-authoringreports 389 across four roots (content,docs,skills,.claude), which is not acontent/docscount at all.The optional half: included, scoped to what the build itself declares
pageSchema-- the schemaapps/docs/source.config.tshands todefineDocs-- declarestitle: z.string()(required) anddescription: z.string().optional(). The gate assertsexactly that:
titlemust be present and a string;description, when present, must be a string.It deliberately does not require
descriptionto be present, though all 403 pages declareone today: the build does not require it, so requiring it would be a rule invented here rather
than the build's contract checked early.
icon,fulland_openapiare likewisepageSchema's and likewise not asserted, and unknown keys pass becausepageSchemais not.strict(). That is the whole scope -- two keys, no schema.The type half is not decoration. Measured with this same parser, the shape one character from
the original defect parses:
Both survivors are red
Build Docsruns thirty minutes later, for the same reason and with thesame one-character fix, and a parse-only gate is blind to both. A page with no frontmatter at
all reaches the build as an empty object (the extractor returns one rather than throwing), so
it too fails only on
title-- which is why an absent block is reported here by name.Verification -- all at
368625d0edLands green on an unmodified tree, with a verdict line stating what it read:
Positive control #1 -- the real failure, in the real page. Unquoted the
descriptionofcontent/docs/api/declarative-endpoints.mdx. Mutation confirmed on disk by moving both anchorcounts: the deleted text (quoted opener) went 1 -> 0 and the injected text (unquoted opener)
went 0 -> 1.
That is the card's quoted
Build Docsmessage, verbatim, in 0.2 s instead of 30 minutes.Restored with
git checkout HEAD --;git hash-objectback to7437754615,git status --porcelainempty, gate green again.Positive control #2 -- the type half, planted in a generated page. A block-mapping
descriptionincontent/docs/references/ai/agent.mdx; anchor counts moved 1 -> 0 and 0 -> 1.Gate exit 1: "
descriptionparses to an object (a nested mapping), not a string" atcontent/docs/references/ai/agent.mdx:3:1. Under the sketched population this defect isinvisible. Restored;
git hash-objectback to2be4b08617, porcelain empty.Refusals -- proved by making the population genuinely unresolvable, at CLI level against
the real tree, not modelled:
content/docsawaycannot read the directory content/docs: ENOENTwalked content/docs and found ZERO .mdx pages, so nothing was verified -- refusing to report a passcontent/docs/gone.mdxcannot read content/docs/gone.mdx: ENOENT -- refusing to report a pass over a corpus one page shortTree restored after each; 403 pages, porcelain empty, gate green. Every
.mdx-named entry is aread candidate and
readFileSyncis the sole authority on readability -- nothing is dropped ona dirent type, so an unreadable page cannot leave the count quietly one smaller.
--self-test: 50 assertions, exit 0, and it runs in CI (both legs are wired, and assertiongroup 8 reads that wiring out of
lint.ymlrather than trusting it). It drives the real judgeover fixture sources -- the card's own
descriptionobserved failing with the parser's messageand the FILE line, every other violation kind observed firing, the five refusals above rebuilt
as real directories, each proved against a readable tree that still returns a verdict so
"refuses unconditionally" cannot satisfy them. One leg cross-checks extraction against the docs
build's own
frontmatter()resolved fromapps/docs, and fails rather than skips if thatmodule cannot be resolved.
Gates
node scripts/pm/dispatch-gates.mjswith no paths, at368625d0ed. It derived 12 families andalready names the new gate (
node scripts/check-doc-frontmatter.mjs [lint.yml] ... gate script),which is itself evidence the wiring is discoverable. Green:
check:cross-package-test-inputs,check:node-version,check:required-contexts,check:shard-attestation,check:workflow-status-functions,check:type-check-coverage,check:aggregator-roster(bothlegs), plus the new gate's own two legs. Added by reasoning, also green:
check:entry-guard,check:parse-guard,check:nul-bytes(and agrep -naPcontrol-byte sweep of both changedfiles), and
npx eslinton the new script.One narrowing, declared:
check:type-check-debtexits 1 in this worktree, and the reason itprints is about the worktree rather than the diff -- "
--re-measurecannot run: 55 workspacedependenc(ies) of the ledgered packages have no built type entry point on disk", raised before
any changed file is read, with the remedy "build the closure first, exactly as lint.yml does
before this step". It was matched only because its declared gate source is
.github/workflows/lint.ymland this PR edits that file; the diff contains no TypeScript, nopackage.jsonand notsconfig.json,.mjsis outside the root tsc program (noallowJs),and the sibling lane
check:type-check-coverage-- same script, same population -- ran green.CI runs it after
turbo run build, so CI's measurement is the real one.Out of scope, filed separately
content/blog/(3.mdx) is parsed by the samenext buildthrough a seconddefineDocscall in
apps/docs/source.config.tsand has the identical unowned invariant. Not folded inhere: this card's subject is
content/docs, and a second root would give the gate twopopulations and two refusal semantics. Filed as a finding instead.
Generated by Claude Code