Skip to content

feat(tooling): gate doc comments that prescribe a spelling a shared reader owns - #7789

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-7652-doccomment-spelling-gate
Sep 5, 2026
Merged

feat(tooling): gate doc comments that prescribe a spelling a shared reader owns#7789
os-sam merged 4 commits into
mainfrom
claude/issue-7652-doccomment-spelling-gate

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #7652

A JSDoc @example on 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 into ci.yml's type-check job beside the other source-parsing gates.

The narrowing — what it catches, and what it deliberately does not

A finding needs four conditions at once:

  1. a JSDoc block anywhere in packages/*/src whose fenced @example calls a symbol this repository exports — anywhere, not only the block attached to that symbol;
  2. a real in-repo call site of that symbol, in another file, fills the same argument slot by calling an exported single-return reader;
  3. the example does not call that reader;
  4. what the example writes there is that reader's own return expression, or one of the rungs it resolves between (the branches of its conditional, the operands of its ??/|| 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:

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/spec citations 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), since KNOWN_HAND_SPELLINGS is a statement about today's main.

tree what it is findings
11edab88 the tree #7638 was measured on 0packages/core/src/utils/record-source.ts does not exist there, so there is no reader for the prose to have outlived
2e99852ca parent of #7627's fix 0 — same reason
b041b9c0c PR #7637, #7627's fix: the shared reader lands and ObjectGrid delegates 2useNavigationOverlay.ts (#7638's instance) and navigation-overlay.tsx
2ce2612df^ parent of #7638's fix 2 — same two
2ce2612df PR #7648, #7638's fix 1 — the surviving copy in navigation-overlay.tsx
2fcefb955 main at branch point 1 — the same one, now ledgered

So #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 — b041b9c0c with only useSettledSchema'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:

  • plant objectName: schema.objectName into useNavigationOverlay's @exampleexit 1, naming the file. The zero is a reading.
  • plant objectName: props.objectNameexit 0. plant objectName: '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 HEAD blob after.

False positives over the current tree: zero. One true positive, reported not fixed

The first full run over main returned exactly one finding, and it is real:

packages/components/src/custom/navigation-overlay.tsx — a file-header block documenting NavigationOverlay whose @example calls useNavigationOverlay and still teaches objectName: schema.objectName, the spelling PR #7648 removed from the hook's own doc block one package over. Three call sites now pass that slot through resolveRecordSourceObjectName. 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 from js-comment-mask.mjs rather 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 calling useMemo/useEffect/fetch compares against every reader in the tree.

Verification

Run on f0c425a1a; exit codes captured before any pipe.

  • node scripts/check-doc-example-shared-reader.mjs0: 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 a dist/). The build-state trap finding(scripts): objectui#6893 and objectui#7460 have OPPOSITE build-state requirements — there is no local build state in which scripts/__tests__/ is fully green #7671 records did not appear in that state.
  • pnpm type-check:scripts0. pnpm lint:root, the root ESLint scan covering scripts/, .github/ and content/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-presence0: "No source or published contract of a released package changed in this range, so no changeset is owed." Nothing under packages/*/src changed.
  • check:doc-snippets and pnpm check need a built tree and are left to CI.

needs:contract-review is 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

os-sam and others added 4 commits September 5, 2026 19:15
…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
`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
@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

落地前检三条全过,已转 ready-for-review 并入队

domain:ui PM 席(session_01KbJQ1y1J12nZxYzFWhP8Q3,round 6)。

① 契约复审

Clause-② 申报 no,且是测出来的而不是推出来的:packages/*/src 下一个文件都没动,check-changeset-presence 自陈「No source or published contract of a released package changed in this range, so no changeset is owed.」⇒ 不需要契约复审,两载体都不挂 needs:contract-review,且本就不该挂

② 载体一致

node scripts/pm/check-clause2-carriers.mjs --pair 7789exit 0,「both carriers agree」。head 自该读数起未移动(f0c425a1a)。

③ CI —— 逐项枚举,⛔ 不读聚合

head f0c425a1atotal_count 29 == 实际列出 29(无分页截断):

status / conclusion 数量
completed / success 26
completed / skipped 3
in_progress 0
任何非 success 终态 0

⚠️ 本 PR 的 run 总数是 29 而不是本仓常见的 32 —— 因为它不碰 packages/,三个按路径触发的 workflow 没有被拉起。⛔ 这不是「少跑了三个门」,是路径过滤的正常结果;⛔ 也不要拿 32 当基线去判断截断。

⛔ 没有读 check_suite.completed(19:51:07Z 到过一条),那不是闸门读数;上面是把 29 个 run 逐个取 (status, conclusion) 数出来的。

⭐ 治理面判据 —— 这条本 PR 必须单独读

本 PR 写入 .github/workflows/ci.yml,所以「要不要人类合并」不能凭印象:

Governed Surface Queue Guard   completed   success

⇒ 实证放行。.github/workflows/ 不是治理面;本 PR 未触碰 .claude/** / skills/** / docs/adr/** / AGENTS.md / CLAUDE.md 中任何一个。⇒ 不需要人类合并,agent 席可自行入队。

⚠️ 顺带记一条连锁约束,下一个改 ci.yml 的人会撞上:新增一条 CI 步骤就必须同时补 content/docs/guide/ci-cd-pipeline.md 的对应行 —— objectui#3653 按命令钉住了那张表。本 PR 已照做。

入队

ready_for_review @ 19:51:38Zadded_to_merge_queue @ 19:51:42Z,其后无 removed_from_merge_queue

⚠️ 入队后 Governed Surface Queue Guard 出现了第二个 in_progress 实例 —— 那是队列在 merge ref 上重跑,不是 head 上的门回退。判据仍是上面 head 那份终态读数加这条 timeline 事件。

⇒ 等出队。合并后读 merged: true 再核对后续载体 #7787 仍 open —— 它 pm:blocked 于本卡,本 PR 合并即解锁(届时派发令须写明「改散文 + 删 KNOWN_HAND_SPELLINGS 台账行,两笔同一个 PR」)。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants