Skip to content

chore(tooling): a spec-alignment claim is judged by the symbols it cites, not by incidental ties (#4607) - #4609

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4607-citation-aware-tie
Aug 13, 2026
Merged

chore(tooling): a spec-alignment claim is judged by the symbols it cites, not by incidental ties (#4607)#4609
yinlianghui merged 1 commit into
mainfrom
claude/issue-4607-citation-aware-tie

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4607

The hole

Rule 2 of check:spec-symbols flags an exported declaration whose doc comment claims
@objectstack/spec alignment while the declaration references nothing spec-bound. That
tie test was symbol-AGNOSTIC — it asked only

does this declaration reference any identifier bound to a @objectstack/spec import?

so a claim about symbol X passed on an incidental reference to an unrelated symbol Y.

FeedItem (packages/types/src/views.ts) was the live specimen: it cited FeedItemSchema,
removed from @objectstack/spec/data in the 16.0.0 major, and never appeared in a single
gate run — purely because one member is typed FeedItemType, the one feed symbol that
removal kept. It sat four lines from a section banner making the same claim, in the same
file as four declarations that were flagged, and it was found by reading the file
rather than by any run of the script.

The asymmetry is what made it expensive: the more spec-integrated a declaration was, the
weaker the check on its prose became. A fully hand-written fork got its claim scrutinised;
one importing a single live spec type for one member did not.

The rule

When a claim NAMES symbols and the installed spec exports NONE of them, the declaration
is flagged whatever else it references. A tie to a DIFFERENT symbol is not something
behind THIS claim.

Deliberately narrow, per the ruling recorded on #4607:

  • a claim naming at least one live symbol stays governed by the existing tie test,
    unchanged;
  • a claim-vs-tie mismatch among live symbols is a documented non-goal — it needs a
    name-relatedness allowance for the legitimate type: FeedItemType shape, where citation
    and tie are genuinely different-but-related symbols, and that is a different instrument;
  • given no spec export set to check against, nothing can be known to dangle and the rule
    stays out of the way entirely, rather than manufacturing a verdict from ignorance of the
    spec.

The failure message keeps its existing shape — the dangling list was already rendered
("…and names X, which @objectstack/spec does not export"). What changed is that an
incidental live tie no longer suppresses the finding.

Discrimination proof, verbatim both ways

One fixture (the specimen: claim phrase, citation of a dangling symbol, live tie to a
different symbol via import + member use), one spec-name map faithful to the pinned
17.0.0-rc.6 (FeedItemType live, FeedItemSchema absent — both verified against the
installed package), run through both scanners:

CURRENT gate (origin/main 92876f097): 0 finding(s)
SHARPENED gate (this branch): 1 finding(s)
  FeedItem  line 7  claim: "Aligned with"  dangling: [FeedItemSchema]

The current gate provably passes it. Both halves are pinned in the suite, and the green
half of the pair is byte-identical to the red one except for which symbol the claim cites
(FEED_ITEM_SPECIMEN.replace('FeedItemSchema', 'FeedItemType')) — so the discrimination is
the citation, not the fixture.

Reverse verification (rule reverted by patch + git checkout, tests kept, restored and
sha256-verified identical — no git stash):

× (a) flags the specimen, and names the symbol the spec has dropped
    AssertionError: expected [] to have a length of 1 but got +0
× takes cited symbols from the mention's own sentence, not the next one
    AssertionError: expected [ 'ActionSchema', 'Open' ] to deeply equal [ 'ActionSchema' ]
 Test Files  1 failed (1)
      Tests  2 failed | 32 passed (34)

Exactly the two tests that pin this change go red; every #4592 must-not-change fixture and
every new precision guard stays green without the fix, which is what makes them guards
rather than restatements of the rule.

Population census, verbatim

Both scanners over the identical file set, diffing raw finding lists rather than the gate
verdict (the verdict hides anything already in the CLAIM_DEBT ledger):

files scanned:              1251
spec export names:          4834
findings, CURRENT rule:     20
findings, SHARPENED rule:   20
NEWLY FLAGGED (population): 0
NO LONGER FLAGGED:          0

The hidden population is zero. So that zero is a measurement and not an absence of
firing, here is the surface the sharpened rule newly examines — every exported declaration
carrying a claim that the old tie test waved through:

Declarations carrying a spec-alignment claim AND passing the old tie test: 3
  cites no symbol at all:      0   (rule cannot fire - unchanged)
  every cited symbol LIVE:     2   (tie test governs - unchanged, non-goal (d))
  mixed live + dangling:       1   (tie test governs - unchanged)
  ALL cited symbols dangling:  0   [the population this card measures]

  all-cited-live      ActionDef        packages/core/src/actions/ActionRunner.ts:112  cites: [ActionSchema]  dangling: []
  all-cited-live      ActionType       packages/types/src/ui-action.ts:126            cites: [ActionType]    dangling: []
  mixed               PageNodeSchema   packages/types/src/layout.ts:533               cites: [PageSchema, This, SDUI, NODE]  dangling: [This, SDUI, NODE]

Dispositions: none needed. No instance requires a reword, a CLAIM_DEBT entry, or a
CLAIM_ALLOW entry — the population is empty, so the 10-instance STOP condition is not
approached. PR #4606 removed the only known specimen as in-file collateral; this card
closes the hole it demonstrated and confirms the tree has no second one. Nothing under
#4580-round2's in-flight surfaces was read for a verdict or edited: the only files this PR
touches are scripts/** plus one changeset.

Two things the change exposed

Making symbols load-bearing surfaced two defects that were invisible while it only
decorated a failure message. Both are fixed here because both are ways this change could
have gone wrong, not scope growth:

  1. The citation window scraped the next sentence. ActionDef reads "…mirroring
    @objectstack/spec's ActionSchema. Open key set on a data bag is correct" — and
    Open was reported as a cited symbol. Harmless as message decoration; as a rule input
    it is a new false-positive class: a claim whose only "citations" are prose words is
    all-dangling, so a green declaration with a live tie would go red with a nonsense
    message. Measured and reproduced before fixing. Symbols are now taken from the
    mention's own sentence — the same discipline the claim/mention pairing already applies —
    and the truncation is a measured no-op on the tree (census above is post-containment;
    ActionDef moved from mixed to all-cited-live with no verdict change). It must not
    fire on ListView.navigation, where the . is a member separator; pinned.

  2. The suite's spec-name stub omitted ListView. The real spec exports it from
    @objectstack/spec/ui (verified). While specNames was decoration the omission changed
    nothing; the moment it decides whether the tie test applies, two long-standing green
    fixtures went red for a reason existing nowhere but that map. The map is now faithful to
    rc.6 for every name the fixtures touch, with the reason written at the declaration.

Residual, pinned as a known limitation rather than fixed: a claim sentence that never
terminates still donates prose words (PageNodeSchema cites PageSchema, This, SDUI, NODE). It costs nothing today — PageSchema is live, so the tie test governs — and
tightening it further means deciding what a citation looks like, which is a different
instrument. See the open question below.

The #4606 idiom stays green — pinned against the real tree

PR #4606's rewordings name the dead symbol on purpose; that provenance is the remedy this
guard exists to produce, so a rule that turned on it would punish its own fix. Pinned two
ways:

  • copies of both idioms as fixtures — "Local shape — authored against the protocol's
    PluralRuleSchema, retired in 17.0.0-rc.6…" and "its cited FeedItemSchema went with
    the 16.0.0 feed removal";
  • and the real files, because a copy can drift from what shipped:
    packages/types/src/views.ts and packages/i18n/src/utils/spec-formatters.ts are
    scanned as they exist on main and must return no finding, plus a check that they still
    carry the load-bearing provenance phrases so the pin cannot be satisfied by deleting the
    comments. Both green. What makes them green is that they claim nothing: no alignment
    phrase sits next to a @objectstack/spec mention, so there is no claim to have anything
    behind.

Governance — unchanged, both sides

before after
CLAIM_ALLOW 2 declared deliberate copies 2
CLAIM_DEBT 18 unbacked claims in 5 packages 18 in 5
rule 1 ALLOW / DEBT 13 dialects, 3 collisions in 1 package unchanged

No ledger regeneration was needed (nothing new to ledger), CLAIM_PATTERNS was not
widened, and all three ratchets are untouched. Gate output is identical before and after:

✅  spec symbol derivation: 1251 files scanned against 4834 spec export names; 13 declared dialects, 3 untriaged collisions in 1 packages.
✅  spec alignment claims: 2 declared deliberate copies, 18 unbacked claims in 5 packages.

Verification

  • pnpm exec vitest run --maxWorkers=2 scripts/42 files, 957 tests passed (943 before;
    14 added).
  • pnpm type-check:scripts — green.
  • eslint on both changed files — clean, 0 problems.
  • Gate battery, all exit 0: check-{control-bytes,phantom-dependencies,changeset-presence,changeset-no-major,changeset-fixed,type-check-coverage,lint-coverage,spec-symbol-derivation,doc-links}.mjs.
  • Control-byte self-scan over every touched file including the untracked changeset — clean.
  • No packages/** file changed, so no .d.ts moves and no per-package patch is owed;
    check-changeset-presence agrees ("0 of them under the src/ of a package the release
    covers"). The changeset is empty-frontmatter, tooling-only, never major.

Open question for the PM

The sentence-scoped citation window (item 1 above) is a narrowing I made inside this
card because the false-positive class it contains is one this change would otherwise have
introduced. If you would rather it were a separate instrument change, it is a self-contained
15-line hunk in findClaim plus three tests and can be split out — the sharpened tie rule
does not depend on it, only its precision does.


Generated by Claude Code

…tes, not by incidental ties (#4607)

Rule 2 of `check:spec-symbols` flags an exported declaration whose doc comment
claims `@objectstack/spec` alignment while the declaration references nothing
spec-bound. That tie test was symbol-AGNOSTIC — it asked whether the declaration
referenced ANY spec-bound identifier — so a claim about symbol X passed on an
incidental reference to an unrelated symbol Y.

`FeedItem` (packages/types/src/views.ts) was the live specimen: it cited
`FeedItemSchema`, removed from `@objectstack/spec/data` in the 16.0.0 major, and
never appeared in a single gate run purely because one member is typed
`FeedItemType` — the one feed symbol the removal kept. Measured on
origin/main@92876f097, the scanner returns 0 findings for that shape; it returns
the finding after this change.

The tie is now judged against the symbols the claim CITES: when a claim names
symbols and the installed spec exports none of them, the declaration is flagged
whatever else it references. Deliberately narrow — a claim naming at least one
LIVE symbol stays governed by the tie test unchanged, since a claim-vs-tie
mismatch among live symbols needs a name-relatedness allowance for the
legitimate `type: FeedItemType` shape, and that is a different instrument.

Making `symbols` load-bearing exposed two things that were harmless while it only
decorated a failure message:

  - the citation window scraped the capitalised opening words of the NEXT
    sentence and reported them as cited symbols (`ActionDef` cites
    `ActionSchema`, and prose `Open`). Symbols are now taken from the mention's
    own sentence, the same discipline the claim/mention pairing already applies;
  - the test suite's spec-name stub omitted `ListView`, which the spec really
    exports, which made two green fixtures red for a reason existing nowhere but
    that map.

Measured repo-wide: the hidden population is zero. 20 findings before, 20 after,
0 newly flagged, 0 no longer flagged; CLAIM_ALLOW 2 and CLAIM_DEBT 18 in 5
packages, unchanged both sides.

Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3

Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 7:22pm

Request Review

@github-actions github-actions Bot added the tests label Aug 13, 2026
@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 19:37
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit f69127f Aug 13, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4607-citation-aware-tie branch August 13, 2026 19:37
yinlianghui pushed a commit that referenced this pull request Aug 13, 2026
…anode-round2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants