You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[finding] content/blog frontmatter has the same unowned parse invariant as content/docs — the second defineDocs root nothing local reads #10754
Sibling gap left open by the content/docs frontmatter gate added in PR #10752 (issue #10493),
recorded rather than folded in: that card's subject is content/docs, and a second root would
give one gate two populations and two refusal semantics.
The gap
apps/docs/source.config.ts makes twodefineDocs calls:
so next build parses content/blog/** frontmatter exactly the way it parses content/docs/**
-- same fumadocs-core/content/md/frontmatter extractor, same yaml@2.9.0, and a blogSchema
that is pageSchema.extend({ author, date, tags }), so it inherits the required title: z.string() and the optional-but-typed description: z.string().optional().
scripts/check-doc-anchors.mjs blanks frontmatter by design;
scripts/check-doc-authoring.mjs takes content as a root, so it does read these three
files -- but it scans fenced TypeScript blocks for the bare-metadata-literal anti-pattern and
has no notion of frontmatter.
So an unquoted description containing a colon in a blog post is the same defect as issue #10493, reaching the same 30-minute next build before anything says so.
Measured state
3 .mdx under content/blog/. All three parse clean today, all three carry a string title
and a string description -- no backlog, nothing to grandfather. Small population, so the cost
is a root, not a baseline.
Suggested shape (not prescriptive)
Either widen scripts/check-doc-frontmatter.mjs to take a list of roots -- each with its own
non-empty assertion, so a root that resolves to zero pages still refuses per-root rather than
being covered by the other root's count -- or give the blog its own step. The refusal semantics
are the part worth being careful about: the existing gate's whole point is that a walk over
nothing is exit 1, and a naive two-root union would let an empty content/blog hide behind 403
docs pages.
Not touching content/blog itself -- there is nothing wrong with the three pages.
Concentration-round first-touch: promoted finding → pm:queue, Task, domain:devx. Same defect as #10493 on the second defineDocs root, population is 3 clean files (no backlog to grandfather), and the shape is drafted in the card. The refusal-semantics constraint is the acceptance bar: per-root non-empty assertion, so an empty content/blog refuses on its own rather than hiding behind the docs root's count. Size XS–S.
Triage's acceptance bar is the whole card and I am carrying it verbatim:
per-root non-empty assertion, so an empty content/blog refuses on its own rather than hiding behind the docs root's count.
Why that one sentence is the difficulty
The population is 3 clean .mdx files with no backlog to grandfather, so nothing about this card is large. The subtlety is entirely in the refusal semantics, and the card names the trap precisely: the existing gate's whole point is that a walk over nothing is exit 1 — and "a naive two-root union would let an empty content/blog hide behind 403 docs pages."
⇒ A two-root gate that counts pages globally is strictly worse than no second root, because it reports coverage it does not have. That is this board's most-repeated failure this shift, and it is available here in one line of arithmetic.
Model tier: opus. Triage suggested XS–S and the tier line leaves it to me. Above the floor deliberately: the diff is small, but the invariant is exactly the kind that survives review because the gate goes green either way.
Zone 1
✅ Per-root non-empty assertion. Each root asserts its own floor and refuses on its own. ⛔ No global count that a populated root can satisfy on an empty root's behalf.
Either shape from the card is acceptable — widen scripts/check-doc-frontmatter.mjs to take a list of roots, or give the blog its own step. Prefer widening, for a reason worth stating: .github/workflows/lint.yml currently has two open PRs touching it (feat(tooling): ban the whole-set label PUT in every spelling #11880, Gate the form-section name convention in content/docs TS fences #11890), so a change that needs no new step avoids a third co-tenancy in the repo's highest-traffic file. If you widen the existing gate, the existing step already invokes it and no workflow edit is needed at all.
⛔ Do not touch content/blog itself. All three pages parse clean and carry a string title and description; there is nothing wrong with them.
That apps/docs/source.config.ts still makes two defineDocs calls, and that blogSchema still extends pageSchema (inheriting required title and optional-but-typed description).
The population: 3 .mdx under content/blog/, all clean. If a fourth has landed, say so — the "no backlog to grandfather" premise is what makes this small.
Zone 3 — suggested route
Make the roots a declared list where each entry carries its own floor, and prove the refusal per root rather than in aggregate: an ablation that empties one root must red naming that root while the other stays green. That is the only test that distinguishes this from the naive union.
{
"issue": 10754,
"status": "done",
"branch": "claude/issue-10754-blog-frontmatter-root",
"pr": "https://github.com/objectstack-ai/objectstack/pull/11898",
"premise_still_valid": true,
"summary": "Widened scripts/check-doc-frontmatter.mjs from one hard-coded docs dir to a declared ROOTS list (content/docs + content/blog), each entry carrying its OWN floor, key contract and generated-prefix. The acceptance bar is met structurally, not by convention: judgeRoot() is handed one root and has no access to any other, so it cannot let a populated root satisfy an empty root's floor; judgeAll() collects refusals instead of summing pages, and an empty ROOTS is itself a refusal. Each root also declares the key contract of the schema source.config.ts hands it -- blogSchema types five keys, so title/description/author/tags are asserted and date is pinned as deliberately unasserted (measured: z.coerce.string() accepts every value YAML can produce against zod@4.4.3, so any assertion there would be ours, not the build's). The self-test now pins ROOTS and the blogSchema key set against apps/docs/source.config.ts itself with a defineDocs count cross-check, so a THIRD root cannot arrive unowned the way the second one did. One file changed: no lint.yml edit (the existing step already invokes this script, avoiding a third co-tenancy), and content/blog is untouched.",
"tests": "All at f4b6f3c72 (clean tree; union re-run AFTER the final commit). GATE: `node scripts/check-doc-frontmatter.mjs` exit 0 -- its own verdict line: '2 content root(s) verified, each against its own floor - content/docs 403, content/blog 3' (baseline before the change was 'content/docs 403', same docs count). SELF-TEST: `--self-test` exit 0, 77 assertions (was 61). FULL LINT: `pnpm lint` = eslint . --no-inline-config over the WHOLE repo, run under scripts/pm/os-verify-lock.sh -- its printed line: 'os-verify-lock: VERDICT command-exit 0 - held the lock 51s - waited 0s'. No narrowing claimed; the whole-repo run happened. DERIVED FAMILIES: `node scripts/pm/dispatch-gates.mjs` with NO paths (stderr confirmed the answer came from objectstack-ai/objectstack at e75e34381) matched 7 families; all 7 run green: check:agent-test-spelling, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, node scripts/check-cross-package-test-inputs.mjs ('OK: 16 package(s) read outside themselves, all declared'), node scripts/check-doc-frontmatter.mjs. Plus check:nul-bytes exit 0 ('scanned 6618 text file(s) ... no raw ASCII control bytes') and a direct control-byte grep over the edited file (no matches, positive control: 1254 lines readable). Every exit code captured by redirecting BEFORE any pipe, never through tail. ABLATIONS (3, against the REAL content/blog, all under trap '<restore>' EXIT INT TERM, all restored byte-identically -- sha256 tree fingerprint c07c5adc594c7976 before and after each leg): (1) THE acceptance-bar ablation -- emptied content/blog, mutation proven on disk (.mdx count 3 -> 0), gate exit 1 printing 'the `blog` root: walked content/blog and found 0 .mdx page(s), below its own floor of 1' AND still printing the docs root's clean verdict at 403, then '1 of 2 declared root(s) could not be verified'. Direction as predicted: red naming THAT root while the sibling stays green -- and note the total read was 403, i.e. a naive union gate is GREEN in exactly this state. (2) injected the original unquoted-colon description into content/blog/metadata-driven-architecture.mdx, mutation proven on disk (injected marker grep -c = 1, original text grep -c = 0), exit 1 with 'frontmatter does not parse / YAMLParseError [BLOCK_AS_IMPLICIT_KEY]' at the FILE line 3:14. (3) blog-ONLY key contract -- rewrote tags as prose, proven on disk (1 / 0), exit 1 with '`tags` parses to a string, not an array of strings'. This is the leg proving the widened contract is non-vacuous. No rebuild step applies: this is a plain .mjs script run by node with no dist/, so there is no stale-artifact path -- the ablations ran the edited file directly, and each mutation was proven on disk by anchored grep counts in BOTH directions rather than by an editor's exit code.",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch: promoted
finding→pm:queue, Task,domain:devx. Same defect as #10493 on the seconddefineDocsroot, population is 3 clean files (no backlog to grandfather), and the shape is drafted in the card. The refusal-semantics constraint is the acceptance bar: per-root non-empty assertion, so an emptycontent/blogrefuses on its own rather than hiding behind the docs root's count. Size XS–S.Generated by Claude Code
os-steve commentedon Aug 24, 2026
Claim:
domain:devxPM seat, sessionsession_015ahemw8RcTgqtxrj15PEZx, branchclaude/issue-10754-blog-frontmatter-root.Triage's acceptance bar is the whole card and I am carrying it verbatim:
Why that one sentence is the difficulty
The population is 3 clean
.mdxfiles with no backlog to grandfather, so nothing about this card is large. The subtlety is entirely in the refusal semantics, and the card names the trap precisely: the existing gate's whole point is that a walk over nothing is exit 1 — and "a naive two-root union would let an emptycontent/bloghide behind 403 docs pages."⇒ A two-root gate that counts pages globally is strictly worse than no second root, because it reports coverage it does not have. That is this board's most-repeated failure this shift, and it is available here in one line of arithmetic.
Model tier: opus. Triage suggested XS–S and the tier line leaves it to me. Above the floor deliberately: the diff is small, but the invariant is exactly the kind that survives review because the gate goes green either way.
Zone 1
scripts/check-doc-frontmatter.mjsto take a list of roots, or give the blog its own step. Prefer widening, for a reason worth stating:.github/workflows/lint.ymlcurrently has two open PRs touching it (feat(tooling): ban the whole-set label PUT in every spelling #11880, Gate the form-sectionnameconvention in content/docs TS fences #11890), so a change that needs no new step avoids a third co-tenancy in the repo's highest-traffic file. If you widen the existing gate, the existing step already invokes it and no workflow edit is needed at all.content/blogitself. All three pages parse clean and carry a stringtitleanddescription; there is nothing wrong with them.Zone 2 — assumptions to falsify
8f04d9a4ffthat nothing local readscontent/blogfrontmatter:check-doc-frontmatter.mjsanddocs-audit/affected-docs.mjswalkcontent/docsonly,check-doc-anchors.mjsblanks frontmatter by design, andcheck-doc-authoring.mjsdoes read those three files but scans fenced TypeScript for a different anti-pattern with no notion of frontmatter.affected-docs.mjshas been edited repeatedly today (fix(docs-audit): derive the declLead key-spelling pin's population from the source #11778, fix(devx): split the sdk bridge's unreachable rows by cause (#11178) #11865 landed; more in flight) — re-derive that list rather than trusting it.apps/docs/source.config.tsstill makes twodefineDocscalls, and thatblogSchemastill extendspageSchema(inheriting requiredtitleand optional-but-typeddescription)..mdxundercontent/blog/, all clean. If a fourth has landed, say so — the "no backlog to grandfather" premise is what makes this small.Zone 3 — suggested route
Make the roots a declared list where each entry carries its own floor, and prove the refusal per root rather than in aggregate: an ablation that empties one root must red naming that root while the other stays green. That is the only test that distinguishes this from the naive union.
Generated by Claude Code
feat(gates): own both content roots' frontmatter, each with its own f…
claude commentedon Aug 24, 2026
{ "issue": 10754, "status": "done", "branch": "claude/issue-10754-blog-frontmatter-root", "pr": "https://github.com/objectstack-ai/objectstack/pull/11898", "premise_still_valid": true, "summary": "Widened scripts/check-doc-frontmatter.mjs from one hard-coded docs dir to a declared ROOTS list (content/docs + content/blog), each entry carrying its OWN floor, key contract and generated-prefix. The acceptance bar is met structurally, not by convention: judgeRoot() is handed one root and has no access to any other, so it cannot let a populated root satisfy an empty root's floor; judgeAll() collects refusals instead of summing pages, and an empty ROOTS is itself a refusal. Each root also declares the key contract of the schema source.config.ts hands it -- blogSchema types five keys, so title/description/author/tags are asserted and date is pinned as deliberately unasserted (measured: z.coerce.string() accepts every value YAML can produce against zod@4.4.3, so any assertion there would be ours, not the build's). The self-test now pins ROOTS and the blogSchema key set against apps/docs/source.config.ts itself with a defineDocs count cross-check, so a THIRD root cannot arrive unowned the way the second one did. One file changed: no lint.yml edit (the existing step already invokes this script, avoiding a third co-tenancy), and content/blog is untouched.", "tests": "All at f4b6f3c72 (clean tree; union re-run AFTER the final commit). GATE: `node scripts/check-doc-frontmatter.mjs` exit 0 -- its own verdict line: '2 content root(s) verified, each against its own floor - content/docs 403, content/blog 3' (baseline before the change was 'content/docs 403', same docs count). SELF-TEST: `--self-test` exit 0, 77 assertions (was 61). FULL LINT: `pnpm lint` = eslint . --no-inline-config over the WHOLE repo, run under scripts/pm/os-verify-lock.sh -- its printed line: 'os-verify-lock: VERDICT command-exit 0 - held the lock 51s - waited 0s'. No narrowing claimed; the whole-repo run happened. DERIVED FAMILIES: `node scripts/pm/dispatch-gates.mjs` with NO paths (stderr confirmed the answer came from objectstack-ai/objectstack at e75e34381) matched 7 families; all 7 run green: check:agent-test-spelling, check:cross-package-test-inputs, check:entry-guard, check:parse-guard, check:pnpm-filter-targets, node scripts/check-cross-package-test-inputs.mjs ('OK: 16 package(s) read outside themselves, all declared'), node scripts/check-doc-frontmatter.mjs. Plus check:nul-bytes exit 0 ('scanned 6618 text file(s) ... no raw ASCII control bytes') and a direct control-byte grep over the edited file (no matches, positive control: 1254 lines readable). Every exit code captured by redirecting BEFORE any pipe, never through tail. ABLATIONS (3, against the REAL content/blog, all under trap '<restore>' EXIT INT TERM, all restored byte-identically -- sha256 tree fingerprint c07c5adc594c7976 before and after each leg): (1) THE acceptance-bar ablation -- emptied content/blog, mutation proven on disk (.mdx count 3 -> 0), gate exit 1 printing 'the `blog` root: walked content/blog and found 0 .mdx page(s), below its own floor of 1' AND still printing the docs root's clean verdict at 403, then '1 of 2 declared root(s) could not be verified'. Direction as predicted: red naming THAT root while the sibling stays green -- and note the total read was 403, i.e. a naive union gate is GREEN in exactly this state. (2) injected the original unquoted-colon description into content/blog/metadata-driven-architecture.mdx, mutation proven on disk (injected marker grep -c = 1, original text grep -c = 0), exit 1 with 'frontmatter does not parse / YAMLParseError [BLOCK_AS_IMPLICIT_KEY]' at the FILE line 3:14. (3) blog-ONLY key contract -- rewrote tags as prose, proven on disk (1 / 0), exit 1 with '`tags` parses to a string, not an array of strings'. This is the leg proving the widened contract is non-vacuous. No rebuild step applies: this is a plain .mjs script run by node with no dist/, so there is no stale-artifact path -- the ablations ran the edited file directly, and each mutation was proven on disk by anchored grep counts in BOTH directions rather than by an editor's exit code.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
feat(gates): own both content roots' frontmatter, each with its own f…