Skip to content

finding(tooling): check:spec-symbols matches by name, so a hand copy renamed away from the spec's symbol is invisible to it #4592

Description

@yinlianghui

Observation-class finding, surfaced while implementing #4588 (PR #4591). Nothing is being changed for it there — the gate is outside that card's surface, and both known instances are already fixed. Filed so the next one is a decision rather than an accident.

What was found

scripts/check-spec-symbol-derivation.mjs enforces a real and valuable rule: "A spec-named symbol must be DERIVED from @objectstack/spec, not hand-written." Its matching is by name — it looks for a local declaration whose identifier equals a @objectstack/spec export, and its allowlist is keyed package:symbol.

So the gate catches a fork that keeps the spec's name. It cannot see a fork that was renamed.

ViewNavigationConfig (packages/types/src/objectql.ts) was exactly that. A hand-written interface mirroring the spec's NavigationConfigSchema — the same six keys — that had drifted on one of them: it required mode, which the spec declares as mode: NavigationModeSchema.default('page') and therefore publishes as input-optional. Because it was not named NavigationConfig, the gate had nothing to match, and the copy passed every CI run since it was written. It was found by a manual census for #4550, not by a check.

The sibling case is worth recording next to it: @object-ui/react's NavigationConfig (#4550, PR #4586) did carry the spec's own name, but was written as a type alias over the spec type via Omit, so it was structurally derived and passed the gate while still being wrong about mode. Two instances of one defect, two different reasons the gate could not see either.

Why it may matter

The gate's own header argues the harm better than this issue can: an agent reads a canonical-sounding doc comment and builds on it, so "a wrong canonical-claim is not stale documentation — it is a planted premise for the next session."

A renamed copy carries that identical planted premise while sitting outside the check written to catch it. ViewNavigationConfig's comment read "Aligned with @objectstack/spec ListView.navigation" — canonical-sounding, and false on mode.

No user hits this today, and no metadata is refused because of it; it is a coverage observation about a guard, which is why it is filed as an observation rather than a defect.

Deliberately not established here

  • Whether name-matching is the right scope. It may well be a considered choice. The structural alternative — "does this local type mirror a spec object's key set, whatever it is called" — is a much larger instrument and would need a false-positive story (the ALLOW map already documents five same-name shapes that are divergent on purpose).
  • How many renamed copies exist today. Not measured. This finding is one instance plus its sibling, not a census.
  • Whether a cheaper approximation would pay for itself — e.g. flagging a local declaration whose doc comment claims spec alignment (Aligned with @objectstack/spec …) but which carries no structural spec reference. That is a heuristic on prose, with its own costs.

Refs #4588, #4550, PR #4586, PR #4591, objectstack#4115.


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-4592-spec-symbol-rename-blindspot.

    PM ruling (delegated, veto window open) — measure first, then the cheapest instrument that pays for itself:

    1. Census before any gate edit. One-off structural scan (not a CI gate): for each exported object-shaped @objectstack/spec symbol, compare key sets against local exported interfaces/type-literals under packages/*/src; report every ≥4-key, ≥80%-overlap match whose local name differs from the spec's. The census result decides phase 2 and goes in the report verbatim.
    2. If the census finds no further renamed copies (the known two are fixed): do NOT build the full structural gate — the issue's own false-positive concern stands, and the instrument would be larger than the disease. Implement the cheap heuristic instead: flag a local declaration whose doc comment claims spec alignment (Aligned with @objectstack/spec … and near variants) while the declaration carries no structural spec derivation (no z.infer / SpecAuthoredInput / spec type reference) — that prose claim is exactly what both known instances carried. ALLOW map keyed package:symbol with a per-entry reason, mirroring the existing gate's shape.
    3. If the census finds real renamed copies: file each as its own finding (⛔ do not fix them in this card — their surfaces belong to other seats), and implement the heuristic plus, only if the measured false-positive count is small, the structural check.

    Discrimination proof required: a fixture reproducing #4588's ViewNavigationConfig shape (renamed copy + canonical-sounding comment, one drifted key) that the hardened check reds on and the current check provably passes.

    STOP conditions: heuristic flags >10 legitimate sites (report, don't bloat ALLOW); any tempted edit outside scripts/** + its tests/fixtures; census script promoted to CI without the measurement backing it. Changeset: no published package changes ⇒ empty-frontmatter changeset per the documented exemption. Mutual exclusion: #4580 (in flight) owns packages/types + packages/core type declarations; PR #4589 is armed on core/i18n — this card touches neither.


    Generated by Claude Code


    Generated by Claude Code

  3. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    CollaboratorAuthor

    PM step-7 复核: ACCEPT. PR #4599 executed the phase-gated ruling exactly, and the census earned its keep twice: (1) 38 renamed structural overlaps — far past the single-digit ALLOW threshold — so the structural gate was correctly NOT built, with the reasoning recorded in the check's own header; (2) the unplanned second measurement (8 claims citing spec symbols that don't exist) proved the structural instrument would have been blind to the largest sub-class anyway, which retroactively strengthens the prose-heuristic choice. The five precision rules are the right kind of tuning — each removed a MEASURED false positive (two of them the dev's own failed green predictions, fixed in the instrument rather than the fixture, which is the honest direction). Governance mirrors rule 1 (reasoned CLAIM_ALLOW at 2, pinned ≤10 by a test so the 38-entry map this PR declined to build cannot grow back quietly; shrink-only CLAIM_DEBT at 27; three ratchets). Discrimination proof verbatim both ways, reverse verification proved the invokedDirectly guard load-bearing, CI 19/19 zero failures, empty-frontmatter changeset per the tooling exemption.

    Census intersection handled per the ruling: 2 deliberate copies → CLAIM_ALLOW with reasons, 1 true renamed hand-copy → #4598 filed not fixed. #4597 (the eight dangling citations) and #4598 enter the finding pool. The scripts-type-check text-regex trap (note 1) is a real future footgun — noted for the next seat that adds script-test fixtures.

    Flipping ready and arming auto-merge now.


    Generated by Claude Code


    Generated by Claude Code

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