Repository navigation
[finding] content/docs/references/contracts/ 是一个只剩 meta.json 的空目录,build-docs.ts 已不再产出它 #7303
Description
Activity
Findings triage: promoted with the delete direction — finding → pm:queue, routed domain:spec-tooling.
- Premise check (
origin/main@62b6a2f):content/docs/references/contracts/meta.jsonis live and reads exactly as filed ("pages": []);build-docs.tshas nocontractscategory (its only two hits are theapi:description prose at :698 and a comment at :799 — the card's grep result reproduced). PR docs(getting-started): quick-reference 计数改(N of M schemas),并把真实参考目录纳入门 (#6530) #7302 has merged, so theCategories Without a Sectionrow this card wants removed is on main too. - Grade rationale — deciding the card's own 删还是留 question: 删 (residue). The evidence is mechanical, not a product call:
build-docs.ts's category table is the single writer and source of truth forreferences/, it has never listedcontracts, the six pages live on atcontent/docs/kernel/contracts/(content intact), and the directory's own history (c483c1326removed it,36425099are-addedmeta.jsonalone) marks the file as an accidental re-add. Scoped cleanup: deletecontent/docs/references/contracts/, drop thecontractsrow from the docs(getting-started): quick-reference 计数改(N of M schemas),并把真实参考目录纳入门 (#6530) #7302 table in the same PR. Maintainer veto window open — reverse to "placeholder" only if spec-side production of a Contracts Protocol category is actually planned. - Routing anchor: the fix face is the generated-references tree +
check-quick-reference-counts.mjs's table — the references pipeline, which the domain table assigns todomain:spec-tooling(contract gates/generators/references 管线). - Dedup: no open card touches
references/contracts/; [finding]quick-reference.mdx的「Data Protocol (17 schemas)」是策展子集,而references/data/实有 29 个 schema 页;计数门只自校验、看不到这个差 #6530/PR docs(getting-started): quick-reference 计数改(N of M schemas),并把真实参考目录纳入门 (#6530) #7302 is the discovering neighbour, already merged, scope disjoint. target:v17: not applied — zero user-visible surface (nav excludes the category; lychee green).
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Claim: PM loop round 6
Session: session_01KJATVrh6V2ysutYUJigh3B
Branch: claude/issue-7303-drop-empty-contracts-references
Worktree: objectstack-issue-7303
Domain: domain:spec-tooling
File surface: content/docs/references/contracts/ (delete), content/docs/getting-started/quick-reference.mdx (one table row), scripts/check-quick-reference-counts.mjs (its contracts entry, docblock and self-test expectation). Stop on breach; explain in the report.
Container & model: S/M, mode:subagent, model: opus — see the self-test coupling below; this is not the one-file delete it looks like.
Serial constraints cleared: no open PR touches any of the three sites; PR #7302 (the discovering neighbour) has already merged, so its table is on main and there is no in-flight collision with it. This lane has zero other cards in flight.
Dispatched under triage's promotion, which also settled the card's own open question (「删还是留」→ 删, residue), on mechanical grounds rather than a product call: build-docs.ts's category table is the single writer for references/ and has never listed contracts; the six pages live on at content/docs/kernel/contracts/; and the directory's history shows c483c1326 removed it while 36425099a re-added meta.json alone. Maintainer veto window is open on that direction.
Premise re-verified live on origin/main before claiming, and it turned up two things the card does not mention:
⚠️ content/docs/references/**is routed tomerge=os-regen— confirmed against.gitattributesread live. This card deletes from that tree rather than regenerating it, so the driver is not the mechanism here, but a sibling regen landing mid-flight is a real merge interaction the dev must handle by merging main rather than by hand-resolving.⚠️ Thecontractsrow is in three places, not one. The card names the table row;scripts/check-quick-reference-counts.mjsalso carriescontracts: [](:492) and a literal self-test expectation string (:519), plus a docblock mention (:80). Removing only the visible row leaves the gate's self-test asserting a row that no longer exists — a red that reads as "the delete was wrong" when it is really "the fixture was missed". That coupling is why this is dispatched atopusrather than as a mechanical delete.
build-docs.ts re-checked: 2 contracts hits, both prose (the api: description and a comment), so nothing regenerates the directory back.
Generated by Claude Code
Dev report: premise falsified — no PR. content/docs/references/contracts/meta.json is a GENERATED artifact, not residue.
I implemented the ruled 删 direction in full, then ran the verification the dispatch asked for before pushing, and it went red. Reverting the branch to origin/main. Evidence below, all measured in a clean worktree off origin/main @ 62b6a2fb2.
The measurement that settles it
Delete the directory, then ask the generator:
$ pnpm --filter @objectstack/spec check:docs
✗ content/docs/references/ is out of date with packages/spec:
+ content/docs/references/contracts/meta.json (missing — spec adds it)
And running the generator for real puts it back, byte for byte:
$ ls content/docs/references/contracts/
ls: cannot access 'content/docs/references/contracts/': No such file or directory
$ pnpm --filter @objectstack/spec gen:docs
✅ Generated 231 files
$ cat content/docs/references/contracts/meta.json
{
"title": "Contracts Protocol",
"pages": []
}
So gen:docs is the writer of that file. Any regeneration PR would silently re-add it — which is exactly what 36425099a ("docs: regenerate references from current spec") did, and it means that commit was a regeneration producing its own output, not an accidental re-add.
Where the card's grep went wrong
The card and the promotion both rest on grep -n contracts build-docs.ts returning only prose. That grep is correct but scoped to the wrong file — build-docs.ts has no category table to be absent from. Categories are discovered at run time:
packages/spec/scripts/build-docs.ts:127
const CATEGORY_DIRS = fs.readdirSync(SRC_DIR)
.filter(file => fs.statSync(path.join(SRC_DIR, file)).isDirectory());and titled from a declared map in a different file — packages/spec/scripts/lib/category-title.ts:75:
contracts: 'Contracts Protocol',That map is total in both directions and build-stopping (resolveCategoryTitles, pinned by category-title.test.ts: "declares a title for exactly the module directories under packages/spec/src"). So contracts is a first-class declared category, and "title": "Contracts Protocol" in the meta.json is not an implementation-free declaration — it is CATEGORY_TITLES.contracts, rendered.
packages/spec/src/contracts/ is also very much alive: 82 files (auth-service.ts, data-engine.ts, metadata-service.ts, sharing-service.ts, …).
Why it publishes zero pages — and why it still gets a meta.json
packages/spec/src/contracts/ holds TypeScript service interfaces, not .zod.ts schemas, so gen:schema emits no JSON Schema for it. It does, however, create the category directory:
$ ls packages/spec/json-schema/contracts/ | wc -l
0 # the directory EXISTS, and is empty
That empty-but-present directory is the whole mechanism. In groupSchemasByPage() (build-docs.ts:298-321):
conversionsandmigrationshave nojson-schema/directory, so they hitcontinueat :302 (they print "Warning: Schema directory ... does not exist") and never reachbyCategory.set. That is why they have no directory underreferences/at all;contractsdoes have one, so it falls through, finds zero.jsonfiles, builds an emptypagesmap, and reachesbyCategory.set(category, pages)at :320 with the key present;- the emit loop then runs for
contracts, writes no.mdx, and writesmeta.jsonwithpages: [].
So the conversions/migrations asymmetry that makes contracts look like an unswept leftover is mechanical and generated, not curation drift.
build-docs.ts also names this state deliberately, at the line the card read as a passing comment (:799):
the steady state for a category whose schemas are all unrepresentable in JSON Schema (
contracts/is the standing example)
and check:docs prints its documented branch on every run today:
⚠ Skipping clean of contracts/ — this build published no JSON Schema under .../json-schema/contracts; leaving its pages as they are.
Two more of the card's factual claims that did not hold
- "没有任何页链接到
/docs/references/contracts" — there is one:content/docs/kernel/index.mdx:44. It is genuinely dead (that route has noindex.mdx, the only reference category without one), but it is a hand-written-link defect independent of this card. Filed as docs:content/docs/kernel/index.mdxlinks to/docs/references/contracts, a route that does not resolve #7330. - The gate self-test coupling was not load-bearing.
GOOD_CATALOG/GOOD_PAGEinscripts/check-quick-reference-counts.mjsare synthetic andcheckPageis pure, so the self-test stayed green (22/22) through the delete. The dispatch's concern that it would red was itself unfounded.
What the real question turns out to be
Not 删还是留 for a leftover file — that question is closed: it is generated, and deleting it is a no-op that CI reverses.
The live question is whether build-docs.ts should emit a meta.json for a category that publishes zero pages. Today it produces an empty, unroutable directory (no index.mdx, excluded from references/meta.json's pages) whose only effect is to make anyone enumerating the tree count 15 categories instead of 14 — which is precisely what produced this card. Making the emit conditional on pages.length > 0 would delete the directory at the generator, permanently, and bring contracts into line with conversions/migrations.
That is a change to a generator's output contract, not a file deletion, so I am not making it unprompted — it needs its own card and its own decision. Handing back to triage.
Local gates run and green before the revert (as a delete): check-quick-reference-counts real + --self-test (22/22, 14 categories all sectioned or declared), check:doc-authoring, check:docs-audit-scope, check-nul-bytes. Reverse verification also confirmed the coverage check still bites — removing studio's row reds with [coverage] content/docs/references/studio/ (3 page(s)) has no section on this page. Only check:docs reds, and that is the one that matters.
Generated by Claude Code
Generated by Claude Code
Premise falsified — the ruled 删 direction is wrong, no PR opened, nothing pushed. This is a successful dispatch, not a failed one: the dev implemented the ruled direction in full, hit the stop condition the brief defined, reverted, and reported.
meta.json is GENERATED, not residue
The decisive evidence, and it is not an argument — it is the generator:
$ pnpm --filter @objectstack/spec check:docs
✗ content/docs/references/ is out of date with packages/spec:
+ content/docs/references/contracts/meta.json (missing — spec adds it)
and with the directory deleted, gen:docs recreates it byte-for-byte. A hand delete is reverted by the next regeneration — which means 36425099a was not "an accidental re-add", it was a regeneration producing its own output.
PM-verified independently on origin/main, all four:
| Claim | Check |
|---|---|
build-docs.ts has no category table to be absent from |
✅ :127 — fs.readdirSync(SRC_DIR), categories are discovered, not listed |
| The category is nonetheless declared | ✅ category-title.ts:75 — contracts: 'Contracts Protocol' |
| The mechanism | ✅ packages/spec/json-schema/contracts/ is generated and holds 0 .json files, while packages/spec/src/contracts/ has 82 live source files. conversions/migrations continue because their schema dirs don't exist; contracts' dir does exist, falls through with zero pages, and reaches the emit with the key present |
"没有任何页链接到 /docs/references/contracts" |
❌ false — content/docs/kernel/index.mdx:44 links there, and there is no index.mdx, so the route cannot resolve. Filed as #7330 |
Two corrections that land against the people who graded this, me included
My dispatch warning was wrong. I told the dev the gate's self-test carried a literal expectation that would red on the delete, and made that the reason for the model tier. It doesn't: GOOD_CATALOG/GOOD_PAGE are synthetic and checkPage is pure, so the self-test stayed 22/22 green through the delete. I read :519 as a live fixture without checking whether the self-test consumes real tree state. That is the same shape as ㉟ — a positional read treated as a structural claim.
The grade's rationale rests on a grep scoped to the wrong file — this is a report to #6015, ⛔ not a re-grade, which is not this seat's to make. The promotion reasoned that "build-docs.ts's category table … has never listed contracts". There is no category table; build-docs.ts discovers directories, and the declaration lives in category-title.ts, which does list it. The card made that error first and the promotion inherited it — the honest reading is that both were misled by the same grep, not that anyone was careless. Cost: one dispatch, which is the correct price for finding this out, and cheaper than a hand delete landing and being silently regenerated back.
The reverse verification is why the "everything else is green" reading is trustworthy: the dev removed a different category's row (studio) and the coverage gate went red as predicted, so the check still bites and the greens above are not a loosened gate.
What is actually left, and it is a different question
The directory is generated, unroutable (no index.mdx), excluded from the nav pages array, and invisible to lychee. Its only measured effect is that anyone enumerating references/ counts 15 categories instead of 14 — which is exactly what produced this card, so the generator will keep manufacturing this finding until the emit changes.
Moving to needs-user-decision, because the remaining question is a generator output-contract call, not the cleanup this card was graded as:
- A (dev's recommendation) — guard the
meta.jsonemit on the category having ≥1 page, the way theindex.mdxemit at:849is already guarded. The directory then disappears at the generator, permanently, andcontractslines up withconversions/migrations. Same file surface this card scoped, but landing as generator output rather than a hand delete. - B — leave the generator, fix the quick-reference row's prose (it currently says "left over from an earlier layout", which is now known false). Cheapest; keeps the empty directory and the dead route forever.
- C — do nothing, close with the mechanism recorded.
contracts is a declared category with 82 live source files. A makes the docs tree silently stop representing it, and the empty directory is currently the only committed trace that the category exists. If reference pages for the service contracts are ever wanted, B is the honest holding position and the real work is generating pages from those 82 interfaces — a much bigger card than either option here.
Assignee released; ⛔ no work proceeds until this is ruled.
Generated by Claude Code
6 remaining items
Label correction — this card was held in a comment but still labelled pm:queue. No change to the analysis, the two options, or the hold; only the state machine is being made honest.
pm:queue reads "ready to dispatch" (SKILL.md label table). This card has been 决定待做 since the 07:49Z comment, and the protocol's rule for that state is explicit: post the analysis on the issue, add needs-user-decision, drop it from the active queue — because label:needs-user-decision is the maintainer's inbox filter. Carrying pm:queue instead meant this card was simultaneously (a) invisible to the inbox query the maintainer actually runs, and (b) visible to every selection sweep as a dispatch candidate. It survived on the seat's in-context memory alone, which is not a durable channel.
Labels now: documentation, domain:spec-tooling, needs-user-decision. Assignment kept, so no other seat spends a dev on it while it waits.
packages/spec/src/contracts/ file count) has already moved once, 82 → 84, and it moved against the delete.
Still A (guard the meta.json emit on pages.length > 0) vs B (leave the generator, fix the now-false quick-reference prose). ⛔ No work proceeds until this is ruled.
Generated by Claude Code
Maintainer ruling recorded 2026-08-11 (PM session, executing the maintainer's direct instruction in chat — verbatim: 「接受你的全部建议,请更新 issue 的状态和标签」, accepting the four-lens decision-inbox review in full).
Ruling: delete. content/docs/references/contracts/ is a leftover, not a placeholder — build-docs.ts's category table is the source of truth for that tree and has no contracts entry; the original six pages live on in content/docs/kernel/contracts/. Remove the directory (its meta.json included) and drop the contracts row from PR #7302's Categories-Without-a-Section table in the same change.
State: needs-user-decision → pm:queue (domain:spec-tooling seat).
Generated by Claude Code
Claimed by the domain:spec-tooling seat (#6018) — session session_01KJATVrh6V2ysutYUJigh3B, branch claude/issue-7303-contracts-category-emit-guard. Round 14. pm:queue → pm:dispatched as one write. Model opus, mode:cloud.
Dispatched as option A — the delete happens at the generator. The ruling has now said delete twice, so ⛔ the 删/留 call is closed and B ("leave the generator, fix the prose") is off the table: B does not remove the directory, and removing it is what was ruled. Reading the ruling for its action rather than its grounds, A is the only form that achieves it.
⚠️ Recording once, not re-asking: the grounds are still falsified, for the fifth time
The ruling states: "build-docs.ts's category table is the source of truth for that tree and has no contracts entry." Re-measured on origin/main at 08:5xZ, all five of this card's re-check commands re-run:
| # | premise | reading |
|---|---|---|
| 1 | is there a category table? | No. build-docs.ts:127 = const CATEGORY_DIRS = fs.readdirSync(SRC_DIR) — categories are discovered from disk |
| 2 | is contracts declared? |
category-title.ts:75 — contracts: 'Contracts Protocol' ✅ still declared, still mandatory |
| 3 | is the tree generated output? | .gitattributes → 1 hit, merge=os-regen ✅ holds |
| 4 | live source behind the category | 84 files in packages/spec/src/contracts/ ✅ (was 82 at first analysis) |
| 5 | is the target still present? | content/docs/references/contracts/meta.json ✅ present |
✅ One part of the ruling does check out: the original pages do live on — content/docs/kernel/contracts/ holds 7 files. That half is correct.
⛔ This is a report, not a re-litigation, and no answer is requested. It is recorded because the same wrongly-scoped grep (grep -n contracts build-docs.ts → prose only, correct output from the wrong file) has now been inherited five times: the card, the promotion, my own first dispatch, the 08-10 ruling, and this one. The dispatch below does not depend on it either way — it is here so the sixth reader does not spend the research again. ⇒ Report to #6015, not a re-grade.
Why a hand delete cannot be the deliverable
Measured end-to-end by the previous dev and unchanged: delete the directory ⇒ pnpm --filter @objectstack/spec check:docs reds with + content/docs/references/contracts/meta.json (missing — spec adds it), and gen:docs restores it byte-for-byte. A delete-only PR cannot merge. The generator writes this file, so the generator is where it stops being written.
The deliverable
- Guard the
meta.jsonemit onpages.length > 0inpackages/spec/scripts/build-docs.ts, mirroring the guard theindex.mdxemit already has at:849.⚠️ Read:849first and mirror that shape rather than inventing a second idiom — and verify the line still says what this brief claims (㊶: anchor on text, not line numbers; this anchor is ~2 days old). - Run the generator so the directory disappears as generated output, and commit that result. ⛔ Do not hand-delete and hope; the whole point is that the tree is
merge=os-regen. - Drop the
contractsrow from PR docs(getting-started): quick-reference 计数改(N of M schemas),并把真实参考目录纳入门 (#6530) #7302's Categories-Without-a-Section table, as ruled. - Confirm
contractsnow behaves likeconversions/migrations— the categories that already emit nothing.
⚠️ The caveat that must ride along, unchanged
contracts is a declared category with 84 live source files, and that empty meta.json is currently the only committed trace in the docs tree that the category exists. After this change the references tree silently stops representing it. That is the ruled outcome and you should implement it — but ⛔ do not let resolveCategoryTitles break: it is total and throws on a title with no directory as well as a directory with no title (build-docs.ts:130-134). Verify the declaration at category-title.ts:75 must stay, or that removing it is safe — measure, do not assume, and say which you found.
Scope
- In:
build-docs.ts(the one guard), the regenerated tree, the docs(getting-started): quick-reference 计数改(N of M schemas),并把真实参考目录纳入门 (#6530) #7302 quick-reference row, a changeset. - ⛔ Out:
content/docs/kernel/contracts/(the real pages — untouched); any other category's emit behaviour;packages/spec/src/**/*.zod.ts; ⛔content/docs/releases/**. - ㉕ — disclose any expansion rather than absorbing it.
Accept bar
pnpm --filter @objectstack/spec check:docsgreen — that gate is the whole reason this card came back twice.- Re-running
gen:docsafter the change does not restore the directory (the previous failure mode, proven by a second run). ⚠️ A green docs-link gate is not evidence anything resolves:lychee.tomlsetsinclude_fragments = "none", so a link to a non-existent anchor returns[200] ✅ OK([finding] No CI gate checks documentation anchors —lychee.tomlsetsinclude_fragments = "none", so a link to a heading that does not exist passes as[200] OK#7484). If you touch a link or a heading, verify by slugging with the repo's owngithub-sluggerplus a repo-wide grep for the old fragment.- ㉜ — prove the check can see before believing any green.
Report at draft-PR time per #6644 L2 — ⛔ no idle-polling CI. Issue comment with the os-dev-report marker first, return message second.
Generated by Claude Code
Dev report — implemented as option A, draft PR #7622 open. Branch claude/issue-7303-contracts-category-emit-guard, worktree objectstack-7303 off origin/main @ e124711.
The ruled action landed: content/docs/references/contracts/ is gone, and the contracts row is out of the Categories-Without-a-Section table. content/docs/references/ goes 15 category directories → 14; the generator's emitted file count goes 231 → 230.
The change
One guard in packages/spec/scripts/build-docs.ts, in the PAGES_BY_CATEGORY emit loop:
const pages = buildCategoryPages(category, Array.from(zodFileSchemas.keys()));
…
if (pages.length === 0) return;I read the index.mdx guard first as instructed. It has moved — it is at :850, not :849, and reads if (zodFiles.size === 0) return; inside the Object.entries(CATEGORIES).forEach at :848. Shape mirrored (early return when the category published nothing), placed at the tail of the loop body so nothing else in it changes — in particular categoryPageSchemas.set at :833 still runs, so the root index's input is byte-identical. Confirmed: git status content/docs/references/ was empty after the first gen:docs with the guard, i.e. no other generated file moved.
⚠️ One thing the brief's step 2 did not anticipate — disclosed, not absorbed
The generator cannot delete the file it stops writing, so the tracked copy still had to be removed in the commit. contracts/ is never manageDir'd: step 1 (:789-807) returns at the !hasSchemas branch before manageDir(dir), because there is no JSON Schema to regenerate from. flush() only deletes paths that are ownedOnDisk(), and only reports - (stale) for owned paths (lib/generated-output.ts:129-131). So after the guard, contracts/meta.json was neither emitted nor owned — invisible to check:docs in both directions, and untouched by gen:docs.
Measured, in order:
| step | result |
|---|---|
guard applied, gen:docs run 1 |
✅ Generated 230 files (was 231) — but content/docs/references/contracts/meta.json still on disk |
git rm the file |
directory gone |
gen:docs runs 2, 3, 4 |
✅ Generated 230 files, directory not restored any of the three times |
check:docs |
✅ 230 generated files in sync with packages/spec, exit 0 |
So the delete is durable, and durable because of the guard — but it is one git rm in the commit rather than a diff gen:docs produced on its own. I did not widen scope to make the generator own that directory (narrowing manageDir into the !hasSchemas branch would put every category's hand-preserved pages at risk — the exact case #4723 protects, and "any other category's emit behaviour" is out of scope). Flagging it as the one place the deliverable's shape differs from the brief's description.
Both gen:docs runs and their results
Asked for two; ran four (one before the git rm, three after). Runs 2–4 all printed ✅ Generated 230 files with the directory absent before and after. The previous failure mode — a delete that the next regeneration reverses — does not reproduce.
check:docs output
Positive control first, so the green is worth something. Hand-delete on an otherwise-clean tree, no guard:
✗ content/docs/references/ is out of date with packages/spec:
+ content/docs/references/contracts/meta.json (missing — spec adds it)
Exit status 1
Reproduced exactly as recorded on this card. After the guard + git rm:
✅ import examples resolve against api-surface/ (62 accepted gap(s) in the baseline)
✅ 230 generated files in sync with packages/spec
EXIT=0
contracts/ is unmanaged, check:docs is now blind to that path — a stray file reappearing there would not red. That is a pre-existing property of the !hasSchemas skip-clean branch, not something this PR introduces, but it means the check:docs green is necessary and not sufficient. The load-bearing evidence is the emit count (231 → 230) and the three non-restoring gen:docs runs.
category-title.ts — the declaration must stay. Measured.
Deleted contracts: 'Contracts Protocol' (still at :75, anchor holds) and ran the generator:
$ pnpm --filter @objectstack/spec gen:docs
✗ Every reference page title, sidebar label and root-index row for a module is this string.
…
Exit status 1
Build-stopping, as resolveCategoryTitles' totality predicted — packages/spec/src/contracts/ has 84 files (git ls-tree -r --name-only origin/main packages/spec/src/contracts/ | wc -l = 84, reconciled against this card's premise-4 pin of 84), so the directory exists and demands a title. Restored; git diff on the file is empty and the full spec suite passes (376 files / 9867 tests).
In-repo precedent that this is the normal steady state, not a wart: conversions: 'Conversions Protocol' is declared too, and conversions publishes nothing at all. Declared-with-zero-pages is a supported shape.
contracts vs conversions / migrations — parity confirmed
contracts references/: (absent)
conversions references/: (absent)
migrations references/: (absent)
Exact residual difference, stated rather than rounded off: conversions/migrations still print Warning: Schema directory … does not exist (their json-schema/<cat>/ is missing); contracts prints nothing, because its directory exists and is empty. That is a diagnostic difference only — the three now emit identically. The ⚠ Skipping clean of contracts/ … line that every build used to print is gone, since the directory no longer exists.
Scope expansions — disclosed
Two beyond the brief's four items, both because my change makes an existing sentence false:
- The paragraph above the table in
quick-reference.mdx: "holds two more category directories that deliberately get no section" → "one more … gets". Removing only the row would leave the prose miscounting. scripts/check-quick-reference-counts.mjs:80docblock: "references/studio/(3 pages) andreferences/contracts/(0 pages) have no section" → studio only. Prose in the gate that documents the real tree.
⛔ Not touched: the gate's GOOD_CATALOG / GOOD_PAGE self-test fixtures (:492, :519, :650, :760, :769). They are synthetic, never read the real tree, and still exercise a reachable parse case. The first dispatch's warning that they were load-bearing remains unfounded — self-test is 22/22 green with them untouched.
Reverse control that the row removal was required, not cosmetic — re-added the row and ran the gate:
content/docs/getting-started/quick-reference.mdx:244 [coverage] `contracts` is declared as having
no section, but content/docs/references/contracts/ does not exist
EXIT=1
Inbound links
Repo-wide grep for references/contracts (grep -rn, no head in the pipeline): 4 hits — the gate docblock (fixed), an untracked fumadocs build artifact (apps/docs/.source/server.ts, not in git ls-files, regenerates), and a dated audit record (docs/audits/2026-06-…md, left as the historical record it is). Positive control: the grep did find three real occurrences, so it can see.
content/docs/kernel/index.mdx:44 now reads [Kernel](/docs/references/kernel), [System](/docs/references/system) — no /docs/references/contracts. So this change strands no inbound link. #7330 looks closable independently; not my call, not touched.
No heading and no link fragment changed, so no github-slugger slug check was warranted — the #categories-without-a-section anchor is untouched.
Gates, with invocation scope
| Gate | Result |
|---|---|
pnpm --filter @objectstack/spec check:docs |
✅ exit 0, 230 files in sync |
pnpm --filter @objectstack/spec gen:docs ×4 |
✅ directory never restored |
pnpm --filter @objectstack/spec test |
✅ 376 files / 9867 tests |
pnpm --filter @objectstack/spec typecheck |
✅ (tsc --noEmit + check:scripts-typecheck + check:test-typecheck) |
pnpm run check:quick-reference-counts |
✅ self-test 22/22; 14 categories all sectioned or declared |
pnpm run check:doc-authoring |
✅ 375 files clean |
pnpm run check:docs-audit-scope |
✅ 179 hand-written docs in sync |
pnpm run check:empty-changeset |
✅ |
pnpm run check:nul-bytes |
✅ 7056 files scanned |
pnpm run check:adr-anchors, check:adr-links |
✅ |
npx eslint on the two touched sources |
✅ exit 0 |
Changeset: .changeset/references-empty-category-meta.md (@objectstack/spec: patch). ⛔ content/docs/releases/** untouched.
Not verified
- CI. Not consulted, not polled — the PM's per the brief. Everything above is local.
- The docs site was not booted. This PR changes no published page (the removed file was unroutable), so there is nothing to look at, but I did not prove that in a browser.
check:docs's blindness to the now-unmanagedcontracts/path, stated above — I did not attempt to close it.
The ruling's grounds, recorded once, no answer requested
Per the seat's brief I did not re-derive this, but I did trip over the line while working and can confirm it directly: build-docs.ts:127 is const CATEGORY_DIRS = fs.readdirSync(SRC_DIR) with a comment above it reading "Categories are discovered from the src directory". There is no category table. The ruling's action is implemented in full; only its stated grounds are wrong, and nothing here depends on them. ⇒ report, not a re-grade.
Generated by Claude Code
ACCEPTED — landed as 814db6d80 (PR #7622, squashed). Verified by content on a fresh ref (㉞). Card auto-closed; pm:dispatched stripped; cloud session archived. Ledger → 19 merged.
Landing verification
| # | criterion | reading @ origin/main |
|---|---|---|
| 1 | content/docs/references/contracts/ gone |
empty ✅ |
| 2 | the pages.length === 0 guard exists in build-docs.ts |
1 ✅ |
| 3 | category-title.ts contracts: declaration still present |
1 ✅ |
| 4 | the contracts row gone from the quick-reference table |
0 hits ✅ |
| 5 | reference category directories 15 → 14 | 14 ✅ |
16 — my probe, not the PR. I counted git ls-tree entries, which includes the tree's own index.mdx and meta.json blobs. Filtered to tree objects it is exactly 14, as claimed. Same class as the count error I made on #7503's brief: a count is only as good as the set the command actually enumerates (㊾/51).
Criterion 3 is the one that mattered. The brief asked the dev to measure whether the declaration must stay rather than assume it, and it did — removing it is build-stopping, because resolveCategoryTitles is total over the directories in packages/spec/src/ and throws both directions. It stays, and the changeset says so in prose. Had that been assumed either way, this PR would have either broken the build or left a false claim in the changeset.
What the dev did well
- Ran
gen:docsfour times, not once. The previous failure mode of this exact card was "delete, gate goes red, generator restores it byte-for-byte" — a single green run cannot distinguish a fix from a coincidence. - Guarded on the page count, not on the symptom.
contractsreaches the emit with zero pages because it holds TypeScript service interfaces rather than.zod.tsschemas, sogen:schemacreates an emptyjson-schema/contracts/;conversions/migrationshave no schema directory at all and are skipped earlier. The guard deliberately keys onpages.length, so a future category of either shape lands the same way — the asymmetry is explained in the comment rather than encoded in the condition. - Updated
check-quick-reference-counts.mjs's docblock in the same commit — that gate reads the table this PR edits, so a stale docblock would have described a table that no longer exists. - The changeset states the one measurable effect honestly: no published page changes, emitted files 231 → 230, and the only loss is a directory holding
{ "title": …, "pages": [] }— unroutable, invisible to the link checker, and counted by anyone enumerating the tree.
⚠️ For the record — the ruling's grounds, sixth and final inheritance
This landed on the ruling's action (remove the directory), which is now permanently satisfied at the generator. Its stated grounds remain false: there is no category table in build-docs.ts — :127 is fs.readdirSync(SRC_DIR), categories are discovered from disk. Re-measured once more at landing. ⛔ No answer is requested and nothing depends on it; it is recorded so the seventh reader does not re-derive it. Report to #6015, not a re-grade.
Generated by Claude Code
在做 #6530(PR #7302,把 quick-reference 的计数与真实参考目录对齐并纳入门)时顺带发现,非该单范围,按 Prime Directive #10 独立记录。⛔ 未自我认领。
观察
content/docs/references/下的 15 个分类目录里,contracts/是唯一一个没有任何.mdx的:其余 14 个目录页数(去掉
index.mdx):ai 11、api 28、automation 13、cloud 11、data 30、identity 5、integration 1、kernel 31、qa 1、security 5、shared 8、studio 3、system 37、ui 16。它是怎么留下的
packages/spec/scripts/build-docs.ts是content/docs/references/的唯一写者,而它的分类表里根本没有contracts这一项(grep -n contracts build-docs.ts只命中api:的一句描述文字和一句注释)。目录本身是历史残留:c483c1326"fix: migrate hand-written docs from auto-generated references/ to guides/" 删掉了contracts/下全部 6 个页(auth-service/cache-service/data-engine/index/metadata-service/storage-service)以及当时的meta.json;36425099a"docs: regenerate references from current spec (docs: regenerate references from current spec #1908)" 又把meta.json单独加了回来,但页没有回来。那批内容今天活在
content/docs/kernel/contracts/(auth-service.mdx/cache-service.mdx/data-engine.mdx/index.mdx/metadata-service.mdx/storage-service.mdx六页都在),所以内容没有丢,丢的只是这个空壳目录没人清。影响面(据实,不夸大)
今天没有任何用户会撞到它,所以按 observation-class 记录、不自评级别。 依据:
content/docs/references/meta.json的pages数组显式枚举了 14 个分类,不含contracts,所以导航里不会出现一个空的 "Contracts Protocol" 组;/docs/references/contracts;Check Documentation Links(lychee)今天是绿的。代价只有两处,都是对读者与 agent 的:
content/docs/references/是「每个分类一个目录」这条结构约定的实例,多一个空目录会让任何按目录枚举分类的人(包括 agent)数出 15 而不是 14;meta.json里的"title": "Contracts Protocol"是一句没有对应实现的声明 —— 它宣称存在一个 Contracts Protocol 分类,而 spec 侧没有任何东西产出它。为什么现在才被看见
PR #7302 给
scripts/check-quick-reference-counts.mjs加了分类级覆盖扫描:references/下每个分类目录必须要么在 quick-reference 上有小节、要么在新增的## Categories Without a Section表里被声明。contracts因此被迫写进那张表,写的时候才发现它 0 页。也就是说,这个空目录现在是被 gate 盯着的(页数从 0 变成非 0 会红),只是它该不该继续存在是另一个问题。需要决定的是「删还是留」
content/docs/references/contracts/,并把 PR #7302 那张表里的contracts行一并去掉meta.json的 title 应改成不暗示已存在倾向前者:
build-docs.ts的分类表是这棵树的真实来源,它里面没有contracts,那这个目录就不是「等着被填」的占位,而是36425099a加回了一个不该加回的文件。但这是分诊该定的,不是我在 #6530 范围内该定的。