Found the hard way on #10215 / PR #10333: a new page landed with a frontmatter description that is an unquoted YAML scalar containing apis: . YAML reads the colon-plus-space as the start of a nested mapping, so the page's frontmatter does not parse:
./content/docs/api/declarative-endpoints.mdx
YAMLParseError: Nested mappings are not allowed in compact mappings at line 2, column 14:
description: Expose your app to systems outside the platform by declaring an ap…
^
Every local gate the card named passed on that exact file — check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:role-word, check:published-readme-links. The only thing that caught it was Build Docs (ci.yml, pnpm --filter @objectstack/docs build), a full next build on a 30-minute-timeout runner that exists only in CI.
Why nothing local sees it, measured rather than assumed
- No script under
scripts/ or scripts/docs-audit/ YAML-parses content/docs/** frontmatter. The seven scripts that import yaml parse workflows, changesets and pnpm-workspace.yaml — none of them docs frontmatter. - The one gate that reads these files line by line deliberately avoids it:
scripts/check-doc-anchors.mjs carries a helper whose docblock is "Blank out a leading YAML frontmatter block, preserving line count." It is right to do so for its own purpose (heading ids), but it means the closest thing to a frontmatter reader is the thing that erases it.
So the invariant "every content/docs/**/*.mdx has parseable frontmatter" has no owner below the full site build.
Why it is worth a gate rather than a shrug
- The failure is deterministic and the fix is one character-class — quoting the scalar. Paying for it with a
next build on the merge gate is the whole cost of the loop for a defect a 30ms parse names exactly. - The trigger is ordinary prose. Any
description mentioning a metadata key by name (apis:, fields:, views:) or writing any word: word reproduces it. Documentation about a metadata platform is the corpus most likely to write a colon. Build Docs is path-filtered (needs.filter.outputs.docs). It is the right gate for the real build, but it is late and coarse; the parse is neither.- It would land green and empty. Measured just now with the same
yaml 2.9.0 the docs build resolves: 396/396content/docs/**/*.mdx frontmatter blocks parse clean. There is no backlog to grandfather and no baseline to carry — the same "cheapest possible moment to take a directory in" argument check-doc-authoring's own header makes about docs/.
Same shape as #7484 (a documentation invariant that CI reported [200] OK on until a script was written for it), which is the precedent for how this repo closes this class.
Sketch
A check:doc-frontmatter that walks the same population affected-docs.mjs --all already defines (content/docs/**/*.mdx minus references/**), parses the leading --- block with yaml, and fails naming the file, the line and the parser's own message. Worth deciding at the same time whether it should assert the two keys every page carries (title, description) are present and are strings, since a missing title is the neighbouring silent defect — that part is a judgement call, the parse itself is not.
Not fixed on #10215 — that card is documentation-only and this is a scripts/ + workflow change. Unassigned for triage.
Found the hard way on #10215 / PR #10333: a new page landed with a frontmatter
descriptionthat is an unquoted YAML scalar containingapis:. YAML reads the colon-plus-space as the start of a nested mapping, so the page's frontmatter does not parse:Every local gate the card named passed on that exact file —
check:doc-anchors,check:doc-authoring,check:docs-audit-scope,check:docs-redirects,check:role-word,check:published-readme-links. The only thing that caught it wasBuild Docs(ci.yml,pnpm --filter @objectstack/docs build), a fullnext buildon a 30-minute-timeout runner that exists only in CI.Why nothing local sees it, measured rather than assumed
scripts/orscripts/docs-audit/YAML-parsescontent/docs/**frontmatter. The seven scripts that importyamlparse workflows, changesets andpnpm-workspace.yaml— none of them docs frontmatter.scripts/check-doc-anchors.mjscarries a helper whose docblock is "Blank out a leading YAML frontmatter block, preserving line count." It is right to do so for its own purpose (heading ids), but it means the closest thing to a frontmatter reader is the thing that erases it.So the invariant "every
content/docs/**/*.mdxhas parseable frontmatter" has no owner below the full site build.Why it is worth a gate rather than a shrug
next buildon the merge gate is the whole cost of the loop for a defect a 30ms parse names exactly.descriptionmentioning a metadata key by name (apis:,fields:,views:) or writing anyword: wordreproduces it. Documentation about a metadata platform is the corpus most likely to write a colon.Build Docsis path-filtered (needs.filter.outputs.docs). It is the right gate for the real build, but it is late and coarse; the parse is neither.yaml2.9.0 the docs build resolves: 396/396content/docs/**/*.mdxfrontmatter blocks parse clean. There is no backlog to grandfather and no baseline to carry — the same "cheapest possible moment to take a directory in" argumentcheck-doc-authoring's own header makes aboutdocs/.Same shape as #7484 (a documentation invariant that CI reported
[200] OKon until a script was written for it), which is the precedent for how this repo closes this class.Sketch
A
check:doc-frontmatterthat walks the same populationaffected-docs.mjs --allalready defines (content/docs/**/*.mdxminusreferences/**), parses the leading---block withyaml, and fails naming the file, the line and the parser's own message. Worth deciding at the same time whether it should assert the two keys every page carries (title,description) are present and are strings, since a missingtitleis the neighbouring silent defect — that part is a judgement call, the parse itself is not.Not fixed on #10215 — that card is documentation-only and this is a
scripts/+ workflow change. Unassigned for triage.