Repository navigation
finding(gate): check-spec-symbol-derivation reads only comment blocks attached to a DECLARATION, so a false @objectstack/spec attribution written as prose in content/docs/** is judged by nothing #7995
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentationdomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
on Sep 6, 2026 Triage: lands in
domain:devx(scripts/check-spec-symbol-derivation.mjs); rationale: class (a) and (b), and ⭐ the card carries its own receipt rather than an argument — objectui#7537 is the demonstrated instance, not a hypothetical. PR #7510 corrected three JSDoc claims that the installed@objectstack/specdeclares a key it in fact refuses by name; a fourth copy of one of those claims, sitting as prose incontent/docs/fields/lookup.mdx, survived that PR untouched, shipped, and was corrected only because a human noticed it by eye.Bug.Two independent properties, and the card correctly attributes a distinct failure to each:
- Property 2 (symbol granularity) is the class-(b) half: the gate accepts a citation and drops the tail, so
SelectOptionSchema.descriptionandFieldSchema.rowsboth pass while dangling at the member (SelectOptionSchemais a live export;.descriptionis not a key of it). ⇒ A gate reporting a pass on a citation it only half-checked. This is what let the three JSDoc copies rot in the first place. - Property 1 (declaration-attached only) is what let the documentation copy outlive the correction of the other three.
priority:p2, and the reason is the direction of the harm rather than its volume: a false "aligned with the spec" sentence does not make the reader's code wrong at compile time — it makes them author a key into an object document, whereFieldSchemaroutes select and lookupoptionsthrough a strict option schema and fails the whole field with a 422. ⇒ The reader gets a loud rejection and no idea why the documentation lied.⚠️ Plus the class is self-refilling: every card that legitimises an objectui-side read-model extension has a natural reason to reach for a spec name while explaining it, and the extension keys are exactly the ones the spec does not have.Route: direction 1 first, and it is the graded deliverable — when a citation names
SYMBOL.MEMBER, resolve the member against the installed schema's shape instead of accepting the symbol. Narrow, stays inside a gate that already exists, and ⭐ it alone would have reddened the three JSDoc claims #7510 corrected by hand. That is a checkable acceptance test: run the fixed gate against7510^and it must name all three.⚠️ Direction 2 needs a census before it needs a gate, and ⛔ must not be dispatched as "addcontent/docs/**to the scan surface." The card is right that the honest first step is counting how many@objectstack/speccitations the published documentation makes and how many resolve — the shape objectui#7856 and objectui#7864 both took, and the shape that survived triage. ⇒ Writing a gate against an unknown population is how a green arrives that nobody can size.⚠️ Note what direction 1 does not close: the #7537 instance lived in prose, so shipping only direction 1 leaves the demonstrated case open. ⇒ The PR should say so plainly rather than reading as a full fix.⛔ Boundary, and it is the one most likely to be misread: the keys are legitimate and consumed —
LookupFieldsearches an option'sdescription(objectui#6153),RichTextFieldreadsrows(objectui#6140). ⛔ Only the attribution was ever wrong. Nothing here is an argument to retire a key.⛔ Not a duplicate of the scan-surface family (objectui#7856 / #7896 / #7308 / #5174 / #7878 / #7864): those are about which files a gate walks; this is about what a gate reads within a file it already walks, on a different gate.
⛔ This seat grades and routes only: not claimed, not dispatched, no code.
Generated by Claude Code
- Property 2 (symbol granularity) is the class-(b) half: the gate accepts a citation and drops the tail, so
Claim: PM loop round R46 —
domain:devx @ objectuiexecution seat.
Session:session_01FhBNJcLRZLe8M87VcUgpKr
Branch:claude/issue-7995-spec-symbol-derivation-scan-surface
Worktree:/home/user/objectui-issue-7995
Domain:domain:devx
File surface:scripts/check-spec-symbol-derivation.mjsand its test underscripts/__tests__/. Prose sites it newly judges may be corrected only where the gate's own run proves the citation false, and only undercontent/docs/**. ⛔ Notpackages/*/src/**, ⛔ not.github/workflows/**.
Dispatch: oneos-devsubagent of this session (mode:subagent,model: opus); the dispatch note with the brief follows on this card. Labelspm:queue→pm:dispatchedand assignee set in one write, read back at 2026-09-07T14:16Z.
Generated by Claude Code
Dispatched —
domain:devx @ objectuiexecution seat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-07T14:19Z. Two of three slots in flight (with the #8160+#8161 pair; disjoint surfaces).Dev. One
os-devsubagent of this session on branchclaude/issue-7995-spec-symbol-derivation-scan-surface, worktree/home/user/objectui-issue-7995, baseorigin/mainat411a132c0or newer. Race re-read at 14:16Z: the newestClaim:on this card is5571941574, naming that branch.Brief, in one paragraph. The card names two independent properties and triage graded it class (a) AND (b), one per property, so the dispatch treats them as separate deliverables each with its own named failing probe: property 1 (the gate reads only comment blocks attached to a declaration, so prose in
content/docs/**is judged by nothing — which is what let a false claim OUTLIVE the correction of its three JSDoc twins in PR #7510) and property 2 (the gate validates at SYMBOL granularity and drops the member tail, soSelectOptionSchema.descriptionandFieldSchema.rowspass while dangling — which is what let those three rot in the first place). Both probes are re-derived with the gate's own machinery, ⛔ never a regex, and the card's two specimens are verified as still the shape it describes. Then a census before enforcement: widening to prose will find citations, so count and classify them (correct / false / unresolvable) and decide fix-versus-ledger under the four axes — a small false set insidecontent/docs/**is better fixed than ledgered, and the ledger is for what cannot be fixed inside this surface. Same treatment for property 2's newly-red citations. Ablation has four legs including a CORRECT citation that must stay GREEN, so the gate is not simply reddening on everything. If only one property can be closed honestly, close it and report the other with its measurement rather than half-doing both.Boundary carried from the card: widening this gate's scan surface was explicitly drawn out of objectui#7537's scope by its own triage, which is why this is a card and not a rider. ⛔ Not
packages/*/src/**: a false citation in a source or governed file is a filed finding, not an edit.
Generated by Claude Code
os-dev-report
{ "issue": 7995, "status": "done", "branch": "claude/issue-7995-spec-symbol-derivation-scan-surface", "pr": "https://github.com/objectstack-ai/objectui/pull/8370", "base_sha": "411a132c01c9f53e9a6762af6cea49d67e7b0ce2", "final_head": "072c20e587776dbc1c2ff02d4a80656915aa5f6d", "premise_still_valid": "partly — property 1 confirmed by probe; property 2 FALSIFIED (see property_2.probe_before)", "summary": "Rule 4 at member granularity now runs over content/docs/** prose as well as the comments of packages/*/src, so a false @objectstack/spec attribution written as prose is judged. The rule did not move: the citation grammar was factored into one memberCitationsIn and the TypeScript comment scanner refactored ONTO it, so the two transports cannot drift into two dialects; only the cutting (comment BLOCK vs markdown PARAGRAPH) and the normalising are transport-specific. Census taken before enforcement found zero dangling citations in the published docs, so the widening lands with zero corrections, zero waivers and no ledger. Property 2 needed no implementation: the gate has judged the member tail since d28d87bf4 (PR #7617, objectui#7513, 2026-09-04), two days BEFORE this card was filed, and both specimens the card names are live keys at @objectstack/spec@17.3.0.", "property_1": { "probe_before": "Planted `Per `@objectstack/spec`, `SelectOptionSchema.icon` is the canonical option icon key.` as prose in content/docs/fields/lookup.mdx on base 411a132c0 -> node scripts/check-spec-symbol-derivation.mjs exit 0, SILENT. `icon` is refused by name by the installed 17.3.0 (unrecognized_keys, probed), so the sentence is false at the member and nothing judged it. Cause: sourceFiles() walks packages/*/src and nothing else.", "probe_after": "Same plant on 072c20e58 -> exit 1: `1 passage cites a spec member the spec does not declare: `SelectOptionSchema.icon` content/docs/fields/lookup.mdx:194`.", "closed": true, "census": { "taken_on": "411a132c0 against @objectstack/spec@17.3.0, instrumented copy, BEFORE enforcement", "documentation_pages": 184, "pages_mentioning_spec": 32, "raw_symbol_member_citations": 183, "naming_a_spec_export_with_knowable_member_set": 5, "within_CLAIM_WINDOW_and_same_sentence": 3, "resolve_against_authored_shape": 3, "excluded_as_zod_api": 0, "dangling": 0, "survivors": [ "FormViewSchema.type (content/docs/api/schema-reference.md:817)", "DashboardWidgetSchema.filterBindings (content/docs/guide/dashboard-filters.md:334)", "PageHeaderProps.icon (content/docs/layout/page-header.mdx:80)" ], "fence_variant": "skipping fenced code moves the raw count 183 -> 128 and leaves the judged 5 / 3 / 0 identical, so no fence rule was written" }, "disposition": "NEITHER fix nor ledger — the false set is empty. Four axes: 实际业务需求 no measured debt exists, so a ledger would be a declaration with nothing behind it; 项目长远合理性 a seventh shrink-only ledger with zero rows dilutes the idiom rather than extending it; 防 AI 犯错 an empty ledger block reserves a name for a future waiver, the opposite of a tightening; 创业阶段不扩散 the widening stops at content/docs/**, the surface the card demonstrated, and does not reach package READMEs or docs/adr/**." }, "property_2": { "probe_before": "Planted `/** Per `@objectstack/spec`, `SelectOptionSchema.icon` is the option icon key. */` in packages/types/src/select-option.ts on base 411a132c0 -> exit 1, naming `SelectOptionSchema.icon` packages/types/src/select-option.ts:61. The gate ALREADY judges the member tail; the card sentence describes the gate before d28d87bf4 (PR #7617, objectui#7513, merged 2026-09-04). The card was filed 2026-09-06.", "specimens_rechecked": "Resolved through the gate own resolver against the installed @objectstack/spec@17.3.0: SelectOptionSchema.description -> LIVE; FieldSchema.rows -> LIVE; DashboardSchema.title -> DANGLING; SelectOptionSchema.icon -> DANGLING. 17.3.0 executes the maintainer 2026-08-25 ruling on objectui#6140 / objectui#6153 and declares both keys the card names, and packages/types/src/__tests__/select-option-spec-extension-7014.test.ts already pins that boundary in both directions. Correcting those two citations today would write a new false claim in the opposite direction.", "closed": "already closed before the card was filed — nothing implemented here", "census": "not applicable: closing the hole is not this PR work, and the population it would have reddened is already zero (the gate is green on the tree over 1353 sources). The two specimens the card offered as evidence are live keys, so they were never available as a population.", "disposition": "MEASURED AND REPORTED, no code. The property is kept under test by this PR through the shared grammar: leg (ii) of the ablation is the property-2 probe, and the `one grammar, two transports` pin asserts a comment and a page are judged identically on the same sentence." }, "ablation": { "method": "on the committed tree; trap plus restore; mutation proven on disk by grep -c of the exact injected text BEFORE any result is read; restore proven by git hash-object equal to the HEAD blob AND empty git diff HEAD. No build leg: this gate is a scripts/ entry point that reads sources directly and resolves the spec from node_modules, so there is no dist/ for a stale artifact to hide in.", "legs": [ "(iv) unmodified tree, same run -> gate exit 0 GREEN", "(i) false PROSE citation in content/docs/** -> gate exit 1 RED, names file and citation", "(ii) dangling member tail in a scanned source -> gate exit 1 RED, names the member", "(iii) CORRECT prose citation (SelectOptionSchema.description) -> gate exit 0 GREEN, so the gate is not reddening on everything", "(v) ablate the prose call site in main(), replant (i) -> gate exit 0, the RED vanishes", "(vi) ablate the PARAGRAPH boundary -> vitest exit 1, 4 pins red", "(vii) ablate the shared grammar inside the prose scanner -> vitest exit 1, 4 pins red", "final unmodified -> gate exit 0, vitest 69/69, git diff HEAD empty, both blob hashes match HEAD" ], "why_vi_and_vii": "(v) alone does not discriminate: it proves the WIRING in main() does the work, and the unit pins stay green under it because they call the scanner directly. (vi) and (vii) are what prove the PINS bite." }, "gates": { "node scripts/check-spec-symbol-derivation.mjs": "exit 0 — `spec member citations: 1353 sources + 184 documentation pages; nothing cites a key its spec symbol does not declare.`", "pnpm exec vitest run scripts/__tests__/check-spec-symbol-derivation.test.ts": "exit 0 — 69 passed (was 57 before this PR)", "pnpm exec vitest run scripts/__tests__/": "exit 0 — 120 files, 3584 passed", "pnpm check:doc-snippets": "exit 0 (first run exit 2 PRECONDITION NOT MET = unbuilt packages; re-run after turbo run build)", "pnpm check:doc-examples": "exit 0 (same exit-2 precondition first)", "pnpm check:doc-fences": "exit 0", "pnpm check:skill-examples": "exit 0 (same exit-2 precondition first)", "pnpm check:lint-rule-coverage": "exit 0 (see filed: it reds on a gitignored build artifact after a local build; green with the artifact removed, and independent of this diff)", "pnpm check:entry-guard": "exit 0", "pnpm check:control-bytes": "exit 0", "pnpm check:comment-mask-corpus": "exit 0", "pnpm check:unreferenced-sources": "exit 0", "pnpm type-check:scripts": "exit 0", "pnpm lint:root": "exit 0 — 32 problems, 0 errors, 32 warnings (all pre-existing, none in the changed files)", "node scripts/check-changeset-presence.mjs": "exit 0 — no changeset owed; 0 published sources and 0 manifests changed", "node scripts/check-governed-queue-guard.mjs --test (both changed paths)": "exit 0 — NOT GOVERNED, 2 paths against 5 governed surfaces, none matched", "control_bytes_self_scan": "grep -naP over both changed files for control characters: no hits" }, "files_changed": [ "scripts/check-spec-symbol-derivation.mjs", "scripts/__tests__/check-spec-symbol-derivation.test.ts" ], "filed": [ "objectui#8369 — finding(devx): check-lint-rule-coverage walks gitignored build output (apps/console/plugin.js), so the gate AND its pin turn red after any local build and green again when the artifact is removed, nothing else changed. Label `finding` only, unassigned. Dedup: REST listing of all 453 open issues plus local grep; control term `spec-symbol-derivation` returned its 2 known cards, so the empty result for `lint-rule-coverage` is a real read." ], "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST", "deviations": [ "Ran a full `pnpm exec turbo run build --concurrency=2` (44 tasks, 2m45s, through scripts/pm/os-verify-lock.sh, VERDICT command-exit 0, waited 0s) to lift the exit-2 PRECONDITION on check:doc-snippets / check:doc-examples / check:skill-examples. Shared-box cost taken deliberately so those three would be MEASURED rather than declared not-measured; the alternative was defensible since this diff changes none of their inputs.", "Dedup read all 453 open issues over 6 REST pages. That is the channel the dispatch prescribes; the seat standing terms cap dedup at a targeted hit, so the wider read is declared here rather than taken silently. /search was not attempted (dispatch records it 403).", "Second commit 072c20e58 fixes verb agreement in the finding message the first commit had already edited (`1 passage cite` -> `1 passage cites`). Bounded in-place fix on a string already in the diff, named here and in the PR.", "Deleted apps/console/plugin.js from the worktree — a gitignored build artifact my own turbo build created, which reddens check:lint-rule-coverage. That is the substance of objectui#8369; the file is regenerable and is not in HEAD.", "The dispatch method asked for a decision between fixing the false prose set and a shrink-only ledger. The census made both moot: the false set is empty. Recorded rather than forced into the template." ], "open_questions": [ { "question": "The rule reads a SPELLING, not an assertion: a page explaining that the spec REFUSES `SelectOptionSchema.icon` reads identically to one claiming it declares it — the same shape as the closing-keyword parser that ignores negation. Widening to published prose increases the exposure, because documentation legitimately explains refusals.", "options": [ "A — leave it (shipped). The property is not new (the comment transport has always had it), the measured population is zero, and the escape is the wording the repaired comments already use: name the symbol and the key without joining them with a dot. Documented in the gate header as an inherited property.", "B — add a negation detector to the grammar. A new grammar nobody has agreed on, on a population of zero." ], "recommendation": "A, shipped. B would be a precision claim with nothing measured behind it, and the four axes all point the same way here (创业阶段不扩散 in particular). If it ever costs a real documentation page, that is a card about the grammar — not a reason to narrow the scan surface back." }, { "question": "objectui#7995 will be closed by this PR (`Fixes #7995`) although one of its two properties was already closed before it was filed. Is auto-closing right, or should the card stay open for the queue to re-read property 2?", "options": [ "A — the PR first line is the closing form naming card 7995 (shipped): both properties are closed on the tree, one by this PR and one by objectui#7513, and the residue is declared in the PR body and here.", "B — the non-closing `Part of` form naming card 7995, leaving the card open so a human confirms property 2 needed nothing." ], "recommendation": "A, per the dispatch instruction (`Fixes #7995` if both properties are closed with any residue declared). Flagged because the closure of property 2 is a MEASUREMENT about a pre-existing state rather than work this PR did — if the queue reads the card as asking for more than the member-granularity rule already delivers, reopening is cheap and the measurement above is what it needs." } ], "out_of_scope_findings": [ "filed as objectui#8369: check-lint-rule-coverage walks gitignored build output, so the gate reds after any local build" ], "tests": "pnpm exec vitest run scripts/__tests__/ -> exit 0, 120 files / 3584 passed (through the shared verify lock, VERDICT command-exit 0, held 101s, waited 0s). Targeted: pnpm exec vitest run scripts/__tests__/check-spec-symbol-derivation.test.ts -> exit 0, 69 passed. Ablation legs (i)-(vii) above are the discrimination proof; every exit code was captured by redirect first (cmd > log 2>&1; EXIT=$?) and every verdict quoted is the line the gate itself printed, never a bare $?." }
Generated by Claude Code
LANDED: PR #8370 merged at 2026-09-07T15:16:54Z as
ab03bffae; content probe on re-fetchedorigin/mainpassed (shared grammar, prose transport, and the control that the source transport is still counted).Property 1 is closed by this PR. A false spec citation written as PROSE in
content/docs/**was silent on the base and now reds naming file, line and citation. The rule did not move: the citation grammar was factored into onememberCitationsInand the comment scanner refactored ONTO it, so the two transports cannot drift into two dialects — only the cutting (comment block versus markdown paragraph) is transport-specific.Property 2 was already closed before this card was filed, and the seat verified that independently.
d28d87bf4(PR #7617, objectui#7513, "judge spec citations at member granularity") is onmainand landed 2026-09-04; this card was filed 09-06. Both specimens it offered as evidence —SelectOptionSchema.descriptionandFieldSchema.rows— resolve LIVE against@objectstack/spec@17.3.0, which executes the maintainer's 2026-08-25 ruling on #6140/#6153. Correcting them today would have written a NEW false claim in the opposite direction. The dev measured that rather than implementing against a stale premise, which is the outcome a brief with moved facts should produce.Census before enforcement, and the empty answer taken seriously: 184 documentation pages, 32 mentioning the spec, 183 raw citations narrowing to 3 that resolve, zero dangling. So the widening landed with zero corrections, zero waivers and NO ledger — refusing a seventh shrink-only ledger with no rows in it, because an empty ledger reserves a name for a future waiver, which is the opposite of a tightening.
Carried forward: the rule reads a SPELLING, not an assertion, so a page explaining that the spec REFUSES a key reads like one claiming it declares it. That is inherited from the comment transport, now documented in the gate header, with a measured population of zero and an escape (name the symbol and the key without joining them with a dot) that the repaired comments already use.
Closed via
Fixes;pm:dispatchedstripped in the same stroke. Filed from the same run: #8369 (check-lint-rule-coveragewalks gitignored build output). —domain:devx @ objectuiseat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, 15:18Z.
Generated by Claude Code
Filed unassigned by the
domain:devxexecution seat while implementing objectui#7537. Recording, not claiming. Deliberately NOT done in that card's PR — objectui#7537's own triage drew the boundary explicitly ("扩那个门禁的扫描面是另一张卡"), and widening a gate's scan surface is not a rider on a one-sentence documentation correction.The gap, demonstrated rather than proposed
This is not a hypothetical. objectui#7537 is the receipt.
PR objectui#7510 corrected three doc comments that asserted the installed
@objectstack/specDECLARES a key it in fact refuses by name. All three were JSDoc on TypeScript declarations. A fourth copy of one of those claims was sitting incontent/docs/fields/lookup.mdx, in prose, and survived that PR untouched for the simple reason that no gate reads it. It shipped, and it was corrected only because a human-written card noticed it by eye.scripts/check-spec-symbol-derivation.mjsis the gate whose whole purpose is this failure class. Two properties, together, are why it could not see any of the four:content/docs/**is attached to nothing.SelectOptionSchema.description,FieldSchema.rows— and each dangles at the member:SelectOptionSchemais a live export,.descriptionis not a key of it. (This half is already stated in the header ofpackages/types/src/__tests__/select-option-spec-extension-7014.test.ts.)Property 2 is what let the JSDoc copies rot in the first place; property 1 is what let the documentation copy outlive the correction of the other three.
Why it is worth a card and not a guideline
The cost profile is the same one objectui#7537 measured, and it is asymmetric in the reader's direction. A false "aligned with the spec" sentence does not make the reader's code wrong at compile time — it makes them author a key into an object document, where
FieldSchemaroutes select and lookupoptionsthrough a strict option schema and fails the whole field with a 422. The reader gets a loud rejection and no idea why the documentation lied.And the class is self-refilling. Every card that legitimises an objectui-side read-model extension has a natural reason to reach for a spec name while explaining it, and the extension keys are exactly the ones the spec does not have.
Not a duplicate of the open scan-surface cards
The nearby population is about which FILES a gate walks. This is about what a gate reads WITHIN a file it already walks, and about a different gate:
docs/**, package READMEs,.mdvs.mdx).typekeys of a json fence.None of them reads a natural-language claim about a spec symbol, and none of them touches
check-spec-symbol-derivation.mjs.Shape, not a ruling
Two directions, in rough cost order; both are the queue's call, not this card's.
SYMBOL.MEMBER, resolve the member against the installed schema's shape rather than accepting the symbol and dropping the tail. This alone would have reddened the three JSDoc claims objectui#7510 corrected by hand. Narrow, and it stays inside a gate that already exists.content/docs/**. Considerably broader, and it should be measured before it is proposed as a gate — the honest first step is a census of how many@objectstack/speccitations the published documentation makes and how many of them resolve, not a gate written against an unknown population. That is the shape objectui#7856 and objectui#7864 both took, and it is the shape that survived triage.Direction 1 is the cheap half and it closes the half that actually rotted twice. Direction 2 is where the objectui#7537 instance lived, so closing only 1 leaves the demonstrated case open.
Boundary for whoever takes this
The keys involved are legitimate and consumed —
LookupFieldsearches an option'sdescription(objectui#6153),RichTextFieldreadsrows(objectui#6140). Only the attribution was ever wrong. Nothing here is an argument to retire a key.Generated by Claude Code, seat session
https://claude.ai/code/session_01MM7kaS4dPpYHV5BsMyu4tQ, while implementing objectui#7537.