Skip to content

finding(tooling): check-spec-symbol-derivation.mjs skips every non-exported declaration, so the #5652 mirrors it was cited to catch were invisible to both rules #5899

Description

@claude

Found while implementing #5652. Filed unassigned as an observation, for PM triage.

The claim the card made, and what actually happened

#5652 described three hand-written mirrors of the FormViewSchema contract in
packages/react/src/spec-bridge/bridges/form-view.ts and said:

That is the mirror shape scripts/check-spec-symbol-derivation.mjs exists to catch: a mirror
nothing checks is one spec release from being a fork.

It is the right shape — but the guard never had jurisdiction over that file, and could not have
reported it. Two of the three mirrors were even declared under the spec's own export names
(FormField, FormSection), which is precisely rule 1's trigger, and the gate was green
throughout.

Mechanism — both rules skip non-exported declarations

scripts/check-spec-symbol-derivation.mjs collects declarations behind the same guard in both
scanners:

consthasExportModifier=(node)=>node.modifiers?.some((m)=>m.kind===ts.SyntaxKind.ExportKeyword)??false;
...
if(!hasExportModifier(stmt))continue;// line 827 — rule 1 (spec-named symbol)if(!hasExportModifier(stmt))continue;// line 923 — rule 2 (alignment claim)

The three mirrors were module-local interfaces, so neither scanner saw them. Confirmed
directly: with the pre-fix file in the tree, --ledger and --claim-ledger both print zero
rows mentioning @object-ui/react, form-view, FormField or FormSection from this file,
and the gate exits 0.

Why the exclusion is not obviously wrong — the part that needs a decision

The export filter is defensible on the script's own terms: an unexported type cannot be imported
by another package under the spec's name, so it cannot become "a planted premise" through the
package's public surface. What #5652 measured is the other half — an unexported mirror is still
read by the next agent editing that file, and it still drifts. The bridge's mirror had drifted on
three keys and had one of them inverted (it admitted only the arm the contract rejects), which is
exactly the damage rule 1 exists to prevent, one visibility modifier below where the guard looks.

So the question is not "delete the filter" — that is a population change, and the script's own
header is explicit that an ALLOW map with dozens of entries is not a guard. The measurement it
needs is the one #4592 already did once for the structural alternative: how many non-exported
declarations across the tree would rules 1 and 2 flag if the export filter were dropped
, and are
they real mirrors or legitimate module-local shapes. That number decides whether this is a
one-line change, a scoped change (module-local declarations in packages that already import from
@objectstack/spec), or a documented non-goal.

Whatever the answer, the header's own account of the failure class deserves the correction: the
class currently reads as fully covered by rule 1 plus rule 2, and #5652 is a specimen that neither
rule could see.

Boundary


Generated by Claude Code

Metadata

Metadata

Assignees

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:queuetooling

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions