Skip to content

finding(tooling): rule 2's tie test is symbol-agnostic, so a declaration can cite a retired spec symbol and pass because it references a different, live one #4607

Description

@yinlianghui

Observation-class finding, measured while implementing #4597 (PR #4606). Nothing is changed for it there — the fix is in scripts/check-spec-symbol-derivation.mjs's rule logic, which is #4592's surface, and #4597 is a comments-and-ledger card.

What was found

Rule 2 flags an exported declaration when its doc comment claims spec alignment and the declaration referencesSpec(...) is false. That tie test asks:

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

It does not ask whether the declaration references the symbol the comment actually cites. So a declaration that cites symbol A while referencing an unrelated symbol B satisfies the tie and is never flagged — even when A does not exist in the installed spec at all.

Live specimen (now fixed, but only by accident)

FeedItem in packages/types/src/views.ts carried:

/**
 * FeedItem — A single item in the unified activity feed.
 * Aligned with @objectstack/spec FeedItemSchema.
 */
export interface FeedItem {
  type: FeedItemType;   // imported from '@objectstack/spec/data'
  ...
}

FeedItemSchema was removed from @objectstack/spec/data in the 16.0.0 major, alongside MentionSchema, ReactionSchema, FieldChangeEntrySchema and RecordSubscriptionSchema (the spec's own CHANGELOG.md records the removal and points consumers at the data API over sys_comment / sys_activity). FeedItemType was deliberately kept.

So FeedItem cited a symbol the pinned spec does not export — exactly the defect #4597 is about — and sat in the same file as four declarations that were flagged, four lines from a section banner making the same claim. It was never in CLAIM_DEBT and never appeared in a single gate run, purely because its type member references the one feed symbol that survived.

PR #4606 rewrote that comment as in-file collateral, so the specimen is gone from the tree. The hole it demonstrates is not. Any declaration with a tie to any spec symbol currently has a free pass to cite a retired or nonexistent one.

Why it may matter

This is the same harm the guard's own header argues, reached by a different route: an agent reads "Aligned with X", goes looking for X, and cannot tell whether the protocol retired it, renamed it, or never had it. The tie test was built to answer "is there something behind this claim" — and a tie to a different symbol is not something behind this claim.

Note the asymmetry that makes it easy to miss: the more spec-integrated a declaration is, the weaker the check on its prose becomes. A fully hand-written fork gets its claim scrutinised; one that imports a single live spec type for one member does not.

Cheap to close, if it is worth closing

The information is already computed. findClaim returns symbols — the capitalised identifiers named right after the spec mention — and the runner already derives dangling from them against specNames. Today both are used only to sharpen the message for declarations that have already failed the tie test.

Two directions, neither costed here:

  1. Report dangling citations independently of the tie. If a claim names symbols and all of them are absent from the spec's export set, that is reportable whether or not the declaration references something else. This catches the FeedItem class directly and needs no new parsing.
  2. Make the tie symbol-aware. Require the tie to involve the cited symbol (or a name derived from it, e.g. XSchema / X), not merely any spec binding. Stricter, but likely to need an allowance for the legitimate type: FeedItemType shape, where the citation and the tie are genuinely different-but-related symbols.

Direction 1 looks like the honest, low-false-positive half; direction 2 is a judgement call about how much prose precision the guard should enforce.

Deliberately not established here

  • How many declarations this currently hides. Not measured. It needs a run of the claim scan with the referencesSpec filter removed, cross-referenced against the spec's export set — cheap, but it is the fix's own measurement and belongs with the fix.
  • Priority. No user hits this and no metadata is refused; it is a precision gap in a guard, which is why it is filed as an observation rather than a defect.

Refs #4592, #4597, PR #4606, #4588.


Generated by Claude Code

Activity

  1. self-assigned this
    on Aug 13, 2026
  2. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM — session session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4607-citation-aware-tie. Code work starts after PR #4606 lands (same rule-2 machinery + ledger block; it is armed now).

    PM ruling (delegated, veto window open) — make the tie test citation-aware, high-precision, and measure the hidden population with the fix:

    1. The fix: rule 2 currently passes a declaration whose claim CITES symbol X while the declaration ties to unrelated live symbol Y. Sharpen it: when findClaim extracts cited symbols and ALL of them are dangling (absent from the installed spec's export set — the dangling list it already computes), the declaration is flagged regardless of an incidental tie to a different symbol. Conservative on purpose: a claim citing at least one LIVE symbol stays governed by the existing tie test (a claim-vs-tie MISMATCH among live symbols is out of scope — note it, don't build it).
    2. Precision guards, with fixtures for each: (a) the FeedItem specimen (claim phrase + dangling citation + live tie to FeedItemType) → RED, verbatim both ways (the discrimination proof — the CURRENT gate must provably pass it); (b) provenance-style comments WITHOUT a claim phrase (the exact rewordings PR fix(types,i18n): doc comments stop citing retired @objectstack/spec symbols (#4597) #4606 just shipped — "authored against… retired in…", "its cited X went with the 16.0.0 feed removal") → GREEN, pinned against those live lines so the gate can never turn on the honest retirement-record idiom; (c) a claim citing a live symbol, tied to that symbol → green; (d) a claim citing a live symbol while tied to a different live one → green under this card (see 1).
    3. Measure the hidden population repo-wide with the sharpened rule — the count finding(tooling): rule 2's tie test is symbol-agnostic, so a declaration can cite a retired spec symbol and pass because it references a different, live one #4607 deliberately left to the fix. Disposition per instance: comment-only fixes in surfaces NOT owned by the in-flight finding(types): two competing SchemaNode declarations — core's interface vs types' union #4580-round2 seat (packages/types/base.ts, core type decls, react, and any label/description read-site package are ⛔ its surfaces — ledger those instead); everything else follows the finding(types,i18n): eight doc comments claim alignment with @objectstack/spec symbols the spec does not export #4597 pattern (retired-vs-never-existed measured before rewording, per the ledger comment's own new guidance). STOP condition: >10 instances needing non-mechanical judgment ⇒ report the census, fix none beyond fixtures.
    4. Governance unchanged: CLAIM_ALLOW/CLAIM_DEBT shapes and ratchets stay; new flags enter CLAIM_DEBT via --claim-ledger regeneration where not fixed; the ≤10 ALLOW pin must keep passing. Changeset: tooling-only ⇒ empty frontmatter unless a package's published .d.ts moves (comment fixes ⇒ patch per package, the fix(types,i18n): doc comments stop citing retired @objectstack/spec symbols (#4597) #4606 precedent). Never major.

    Mutual exclusion: ⛔ #4580-round2's in-flight surfaces (types/core/react + label-resolution sites); scripts/** is yours alone once #4606 lands; ⛔ content/docs/releases/**.


    Generated by Claude Code


    Generated by Claude Code

  3. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    CollaboratorAuthor

    PM step-7 复核: ACCEPT — with both open questions ruled.

    OQ1 — A, keep the sentence-scoped citation window in this PR. The false-positive class did not exist before symbols became load-bearing, was reproduced live (not hypothesized), and its containment is a measured no-op on the tree (20/20 findings unchanged) with its own discrimination tests. Shipping the rule without it would knowingly hand some future PR a red gate naming This/SDUI/NODE as spec symbols. Containment of a defect a change introduces belongs in that change.

    OQ2 — A, no follow-up instrument for the live-mismatch class. Measured surface: 2 declarations, both legitimate. An allowance built for zero known defects is the 38-entry ALLOW map #4592 already measured and rejected; the non-goal is documented in the header and pinned by fixture (d), which is the right amount of machinery.

    The census discipline here is the standard: the zero hidden population is a MEASUREMENT (finding-list diff, not gate-verdict diff, plus the 3-declaration exposed surface enumerated), the #4606 idioms are pinned green on the REAL tree with companion assertions so deleting the comments can't satisfy the pin, and the SPEC_NAMES stub being made faithful the moment it became load-bearing — with the reason written at the declaration — is exactly how test scaffolds should evolve. The KNOWN LIMITATION pinned as a test (PageNodeSchema's unterminated sentence) rather than fixed is honest, and #4610 records it for whoever next touches the extractor. The api.github.com-403-reads-as-empty-list false-green trap in shell CI pollers is noted for future dev briefs — the MCP read is the only authority.

    Flipping ready and arming auto-merge now.


    Generated by Claude Code


    Generated by Claude Code

  4. added a commit that references this issue on Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions