feat(tooling): gate doc comments that prescribe a spelling a shared reader owns - #7789
Conversation
…eader owns (objectui#7652) A JSDoc `@example` on an exported hook is copied by call sites. When the ruling it encoded moves, the prose stays and seeds every later copy — so fixing the call sites without fixing the doc comment re-seeds them. That class cost two cards and three copied call sites (objectui#7627, objectui#7638), and objectui#7617's `check-spec-symbol-derivation` was credited with covering it twice while its rule 4 judges `@objectstack/spec` citations at member granularity and says nothing about prose prescribing a local spelling. The gate fires on four conditions at once: the `@example` calls the symbol it documents, a real in-repo call site fills the same argument slot by calling an exported single-`return` reader, the example does not, and what the example writes there is that reader's own return expression or one of the rungs it resolves between. That last condition is what keeps it off the literals and placeholders an example legitimately carries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…symbol it calls The first run of the widened scan found a live instance the narrower rule was blind to: `packages/components/src/custom/navigation-overlay.tsx` carries a FILE-HEADER block documenting `NavigationOverlay` whose `@example` calls `useNavigationOverlay` and still teaches `objectName: schema.objectName` — the spelling objectui#7638 was filed about and PR #7648 removed from the hook's own doc block one package over. Different file, different package, a block attached to no declaration: nothing in that fix pointed at it. Comment spans come from `js-comment-mask.mjs` rather than a regex, since this is a gate whose entire subject is comment text. Comparison is restricted to symbols this repository exports, because once every block is read an example calling `useMemo`/`useEffect`/`fetch` compares against every reader in the tree. The instance is reported, not fixed: objectui#7787 decides the prose, and it is carried as `KNOWN_HAND_SPELLINGS`' only row so the gate lands provably green rather than bundled with a change to what it judges. A row whose defect is gone fails the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…comment-spelling-gate
`tsconfig.scripts.json` type-checks `scripts/**/*.ts`, and `Statement` has no `declarationList` — TS2339 at the fixture, caught by `pnpm type-check:scripts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
落地前检三条全过,已转 ready-for-review 并入队
① 契约复审Clause-② 申报 ② 载体一致
③ CI —— 逐项枚举,⛔ 不读聚合head
⛔ 没有读 ⭐ 治理面判据 —— 这条本 PR 必须单独读本 PR 写入 ⇒ 实证放行。
入队
⇒ 等出队。合并后读 Generated by Claude Code |
Fixes #7652
A JSDoc
@exampleon an exported hook is not decoration: call sites copy it. When the ruling it encoded moves, the prose stays and seeds every later copy — so fixing the call sites without fixing the doc comment re-seeds them. Measured cost before this gate: two cards and three copied call sites (#7627, #7638), with nothing in CI able to see either.scripts/check-doc-example-shared-reader.mjs+ its pin, wired intoci.yml'stype-checkjob beside the other source-parsing gates.The narrowing — what it catches, and what it deliberately does not
A finding needs four conditions at once:
packages/*/srcwhose fenced@examplecalls a symbol this repository exports — anywhere, not only the block attached to that symbol;returnreader;??/||chain), written with the reader's own parameter spellings.Condition 4 is what keeps it off ordinary examples. A literal, a placeholder or a locally-named variable is exactly what an example is for; spelling out the body of the reader the callers delegate to is not.
It does not answer, on purpose:
@example(a@paramline prescribing a spelling is the same defect; finding(core): the six per-blockdata.provider === 'object'folds could collapse onto one shared reader — the seventh now lives innormalizeListViewSchema#7627's did),//comments,content/docs/**(that ischeck-doc-snippet-types/check-doc-component-types), a reader nobody calls yet, and whether the reader the call sites use is the right one.Why not
check-spec-symbol-derivation.mjs#7652 records that it was credited with this class twice — in #7638's card body and then in the dispatch that repeated it. Re-measured here rather than repeated: that script is also being changed by an open draft (#7685), and its rule 4 judges
@objectstack/speccitations at member granularity, which says nothing about prose prescribing a local spelling. New file.Measured on the trees that carried the instances
Run with the gate at this branch's head against detached checkouts of real history, exit codes captured before any pipe. Findings shown are pre-ledger (
raw), sinceKNOWN_HAND_SPELLINGSis a statement about today'smain.11edab88packages/core/src/utils/record-source.tsdoes not exist there, so there is no reader for the prose to have outlived2e99852cab041b9c0cObjectGriddelegatesuseNavigationOverlay.ts(#7638's instance) andnavigation-overlay.tsx2ce2612df^2ce2612dfnavigation-overlay.tsx2fcefb955mainat branch pointSo #7638's instance is caught on the real, unmutated tree for the whole four-commit window it was live: this gate would have reddened CI on PR #7637, before #7638 was filed.
#7627's instance is not visible on the tree that carried it, and the reason is measurable rather than argued: the shared reader was created by the same commit that fixed the prose, so at
b041b9c0c^there was nothing for the doc comment to be disagreeing with. Reconstructing the re-seeding state —b041b9c0cwith onlyuseSettledSchema's doc block reverted to its parent, the mutation confirmed on disk by blob hash and by counting the removed and injected text — the gate reports it, naming the whole ladder as the hand-spelled rung.The lit control, and a control on the control
On this branch's tree, in the very doc block whose zero the gate reports:
objectName: schema.objectNameintouseNavigationOverlay's@example→ exit 1, naming the file. The zero is a reading.objectName: props.objectName→ exit 0. plantobjectName: 'Accounts'→ exit 0.The second pair is the control on the control: same line, same doc block, same edit size, differing only in what the example prescribes. Without it, "the plant reddens it" would only prove the gate reacts to edits. All three mutations were confirmed on disk before each reading and the file restored to its
HEADblob after.False positives over the current tree: zero. One true positive, reported not fixed
The first full run over
mainreturned exactly one finding, and it is real:packages/components/src/custom/navigation-overlay.tsx— a file-header block documentingNavigationOverlaywhose@examplecallsuseNavigationOverlayand still teachesobjectName: schema.objectName, the spelling PR #7648 removed from the hook's own doc block one package over. Three call sites now pass that slot throughresolveRecordSourceObjectName. Different file, different package, a block attached to no declaration: nothing in that fix pointed at it.It is not fixed here — #7652 fenced the prose fixes out of this PR. Filed as #7787 and carried as
KNOWN_HAND_SPELLINGS' only row, an allowlist that only shrinks: a row must name a card, and a row whose defect is gone fails the gate rather than sitting there as a waiver for nothing.That instance is also why the scan reads every
/**block rather than only JSDoc attached to the symbol it calls — the narrower first version was green over it. Comment spans come fromjs-comment-mask.mjsrather than a regex, since this is a gate whose entire subject is comment text; comparison is restricted to first-party exports, because once every block is read an example callinguseMemo/useEffect/fetchcompares against every reader in the tree.Verification
Run on
f0c425a1a; exit codes captured before any pipe.node scripts/check-doc-example-shared-reader.mjs— 0: 77 documented symbols, 3944 call sites, 11 slots where a call site delegates to a shared reader.scripts/__tests__/(whole directory) — 0, on a completely unbuilt tree (0 of 40 packages with adist/). The build-state trap finding(scripts): objectui#6893 and objectui#7460 have OPPOSITE build-state requirements — there is no local build state in whichscripts/__tests__/is fully green #7671 records did not appear in that state.pnpm type-check:scripts— 0.pnpm lint:root, the root ESLint scan coveringscripts/,.github/andcontent/— 0 (0 errors, 34 pre-existing warnings, none in the changed files).check-entry-guard,check-control-bytes,check-lint-coverage,check:doc-fences,check:doc-types,check-doc-links,check-changeset-fixed— all 0.check-changeset-presence— 0: "No source or published contract of a released package changed in this range, so no changeset is owed." Nothing underpackages/*/srcchanged.check:doc-snippetsandpnpm checkneed a built tree and are left to CI.needs:contract-reviewis deliberately absent: a CI gate and its pin, no accept-set change and no published surface.🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Generated by Claude Code