docs(spec): assert an @example caption has a block beneath it - #17395
Conversation
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
ACCEPT — head
|
Fixes #16962
Clause-②: no — the diff is a documentation-generator invariant plus its tests and a changeset. No schema, accept set, or published runtime surface is touched;
packages/spec/src/**is untouched, and the assertion re-declared against the ACTUAL diff agrees with the claim comment's dispatch-time reading.The card's subject, and what turned out to be true
@example CAPTIONis declared to be "the caption of the fence beneath it".EXAMPLE_CAPTIONacts on that reading and promotes the tag into a bold lead-in. Nothing asserted the fence was there, and #15440 shipped two orphaned captions past that gap: the promotion still fired and the rows below collapsed into one run-on paragraph on two customer-facing pages.Every premise re-measured on this branch at
origin/main59db8a0:file-description.tsMODULE_MARKERSCOPE paragraphEXAMPLE_CAPTIONis/^@example[ \t]+(\S.*)$/gmand rewrites to**$1**@example CAPTIONlines inpackages/spec/src@examplepartitions exactly 199 captioned / 199 bare / 398 total, zero overlapThe population the card could not name is 12, not 199
⭐
renderFileDescriptionreads only the MODULE doc block.findModuleDocBlockreturnsnullfor any block that documents a symbol, so property-level@exampletags — the@example 'support_case'shape that made up the bulk of the card's 166 — never reach this renderer at all. Measured with the realfindModuleDocBlockover all 1321 files: 456 carry a module block, and those blocks hold 12@example CAPTIONlines. All 12 are followed by a fence or an indented block. Orphans today: 0.Independent confirmation, arrived at separately:
EXAMPLE_CAPTION's own doc comment already says "Twelve module headers write a caption over a fence", and #15443's changeset says "12 lines across 10 pages".Cross-check that the other 187 are outside the docs surface entirely:
@exampleappears 6 times in all ofcontent/docs/references/, and all 6 are email addresses (j***@example.com,user@example.com). Zero leaked tags. Lit controlEndpoints= 6 files; dark control = 0.⇒ The card asked for a predicate separating damage from harmless prose over a 166-line population. Scoped to what the renderer actually renders, no such predicate is needed: the population is 12, and the predicate is the contract itself.
Which of the three failure-fix routes — the second
First delete the construct that permits the error; then make the correct form the only spelling; only then add a check.
Route 1, delete the construct — rejected, with a measurement. The construct is
EXAMPLE_CAPTION's unconditional promotion, and deleting it is the card's "Remove" end. It would regress #14455 (the literal text@example Endpointsreturns to the page) and degrade 12 correct, live captions to prevent a defect with 0 live instances.Route 2, make the correct form the only spelling — TAKEN.
renderFileDescriptionnow refuses, before it emits anything, a prose-level@example CAPTIONwith no block beneath it. An unfenced caption fails the docs build, so the only spelling that can produce a page is the fenced one — the wrong page is not detected, it is impossible. This is the move the file already makes twice:findModuleDocBlockis written as what the generator will select "because a rule that makes the wrong page impossible needs no detector", andwithHeadingsAtSectionLevelalready refuses a heading it cannot renumber and names the source-side fix. The new refusal is the third instance of the same idiom.Route 3, add a check — NOT taken, and the difference is not cosmetic. The card's "Enforce" end was a gate over
packages/specdocblocks, which is what needs the predicate design and the controlled false-positive count. This PR adds no gate script, no scan surface, and no new CI family. The precondition lives inside the one function that publishes, is asked only of the 12 blocks that function renders, and asks only the question the contract already states.⛔ It never asks whether a run of prose is "really" a table. That shape-sniffing is what this module's header rejects and what the card put out of scope. What an author writes instead of a fence is not knowable from the text; whether a block is there is.
Both code kinds satisfy it: an
indentedblock is re-emitted as a fence by the render loop, so a caption above one captions a fence by the time a reader sees it. Judged onclassifyLines' verdict rather than on raw text, so a header illustrating the tag inside a fence is not refused for demonstrating the broken form.Ablation — two-sided, proven on disk
Under
trap restore EXIT INT TERMwith absolute paths; restore isgit checkout HEAD -- packages/spec/scripts/lib/file-description.ts.Exactly the four refusal cases go red and the five acceptance cases stay green — the direction a guard-deleting ablation must produce. No
distpreflight applies:file-description.test.tsimports./lib/file-descriptionby relative source path, so no build artifact sits in the resolution path.Verification
Run in the worktree, exit codes captured before any pipe.
pnpm --filter @objectstack/spec exec vitest run scripts/file-description.test.ts--reporter=verbosepnpm --filter @objectstack/spec run check:docspnpm --filter @objectstack/spec run typecheckpnpm check:nul-bytes·check:cross-package-test-inputs·check:test-source-aliascheck-empty-changeset·check-changeset-no-major·check-changeset-fixedpnpm check:published-files·check:doc-authoring·check:type-check-coveragedocs-audit/check-affected-docs.mjs·docs-audit/check-drift-comment.mjscheck:variant-docs·check:skill-refs·check:empty-statepnpm check:type-check-debtcheck:docsis the load-bearing one: it runsrenderFileDescriptionover every source in the package, so the new precondition was asked of the whole live corpus and fired zero times, and the 228 generated files came back byte-identical. The assertion is additive — it refuses or passes through, and it changes no output.check:type-check-debtexits 3, which its own text defines asPREREQUISITE NOT MET— "NOT a pass and NOT a finding, nothing was measured". It needs the whole./packages/*build closure on disk. Left to CI. ⛔ Not read as green and ⛔ not read as red.check:docswas NOT MEASURED on its first run too (exit 1,packages/spec/json-schema is missing); re-run afterpnpm --filter @objectstack/spec build, which regenerates it. Not in MERGE state, sogen:schemahad no stale anchor to roll back to.Type-check coverage, proven rather than assumed.
tsconfig.test.json'sincludestops atsrc, so it does NOT reach these files;scripts/**is the siblingtsconfig.scripts.jsonprogram thatcheck:scripts-typecheckinvokes.tsc -p tsconfig.scripts.json --listFiles= exit 0 over 915 files, with the edited lib present (1), the edited test present (1), and a fabricated path absent (0).Declared narrowing.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives 59 runnable commands for these paths, re-derived after the changeset existed (which added the 6 changeset families, all run above). The families above were run; the remainder are left to CI, including every family the tool itself marks NOT MEASURED, the 46 artifact rosters, the 11 wide-population families, and the 5 path-scheduled CI jobs. Repo-wide scans (pnpm lint) are CI's run.验收备注
Findings noted, not filed — neither has a carrier that would reach them.
renderFileDescriptionnever reads. This is a correction to the card's own analysis, delivered here and in the report rather than as a new issue — the card is the carrier, and it is about to be closed by this PR.#16960's subject (path:NNNanchors in prose) and the third card the triage seat filed for the shared gap are unaffected by this change either way. This PR deliberately does not widen toward them; the routes stay separate exactly as triage ruled.#16960 remains openand is not addressed here.Authored by Claude Code in session
session_01MkQhmuuJAVDjmeWNixwDDH, dispatched by thedomain:specexecution seat.Generated by Claude Code