Skip to content

chore(tooling): spec-symbol gate sees renamed hand-copies via their own alignment claims (#4592) - #4599

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4592-spec-symbol-rename-blindspot
Aug 13, 2026
Merged

chore(tooling): spec-symbol gate sees renamed hand-copies via their own alignment claims (#4592)#4599
yinlianghui merged 1 commit into
mainfrom
claude/issue-4592-spec-symbol-rename-blindspot

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4592

scripts/check-spec-symbol-derivation.mjs matches BY NAME, so a hand copy that was renamed away from the spec's symbol has nothing for it to match. ViewNavigationConfig (#4588) was exactly that — the spec's six navigation keys, hand-written, drifted on mode, under the comment "Aligned with @objectstack/spec ListView.navigation" — and it passed every CI run until a manual census found it.

This adds rule 2 to the same file: flag an exported declaration whose doc comment claims alignment with @objectstack/spec while the declaration references nothing spec-bound. That prose claim is the one thing both known instances carried.

Surface: scripts/** plus its test plus one changeset. Three files, no package source touched.


Phase 1 — the census, before any gate edit

A one-off structural scan (scratchpad only, not wired to CI, not in this diff): for every exported object-shaped @objectstack/spec type, compare key sets against local exported interfaces and type-literals under packages/*/src, and report every match with >= 4 shared keys and >= 0.80 Jaccard overlap whose local name differs from the spec symbol's.

scanned 1251 local files (1072 exported object shapes with >= 4 keys)
against 1687 spec object TYPES (of 4834 spec export names)

── HITS: renamed, non-derived, >= 0.8 overlap (38) ──
── EXCLUDED: structurally derived from the spec (0) ──
── EXCLUDED: local name IS a spec export name — the existing name gate sees these (1) ──
── NEAR-MISS BAND 0.6–0.8 (30) ──

The 38 hits, in full:

local file:line spec symbol overlap / shared keys
MarketplacePackageTranslation app-shell/.../marketplaceApi.ts:89 PackageTranslation 1.00 / 5
LocalizedPackage app-shell/.../usePackageL10n.ts:79 PackageTranslation 0.80 / 4
ApprovalActionAttachmentLite app-shell/.../useRecordApprovals.ts:120 ApprovalActionAttachment 1.00 / 5
ApprovalActionLite app-shell/.../useRecordApprovals.ts:134 ApprovalActionRow 0.93 / 14
ContextSelectorDef app-shell/src/layout/ContextSelectors.tsx:56 AppContextSelector 1.00 / 6
RawActionParam app-shell/.../resolveActionParams.ts:77 ActionParamParsed 1.00 / 16
FormSectionSpec app-shell/.../SchemaForm.tsx:423 FormSection 0.80 / 8
FormFieldSpec app-shell/.../SchemaForm.tsx:436 FormFieldInput 0.89 / 25
DecisionEdge app-shell/.../flow-decision-edges.ts:38 FlowEdge 1.00 / 7
FlowDesignerEdge app-shell/.../flow-canvas-layout.ts:129 FlowEdge 1.00 / 7
SimEdge app-shell/.../flow-sim-types.ts:28 FlowEdge 1.00 / 7
AuthSocialProvider auth/src/types.ts:160 AuthProviderInfo 1.00 / 4
PreviewModeOptions auth/src/types.ts:403 PreviewModeConfig 1.00 / 6
AuthOrganization auth/src/types.ts:429 Organization 0.86 / 6
ResultDialogSpec core/src/actions/ActionRunner.ts:506 ActionResultDialogTranslation 0.80 / 4
ElementDataSourceConfig core/.../element-data-source.ts:75 ElementDataSource 1.00 / 5
ResolvedResponsiveConfig core/.../ResponsiveProtocol.ts:47 ResponsiveConfig 1.00 / 4
ResolvedSharingConfig core/.../SharingProtocol.ts:54 SharingConfig 1.00 / 6
DashboardFilterDef core/.../dashboard-filters.ts:34 GlobalFilter 0.80 / 8
MetadataLayered data-objectstack/.../metadata-client.ts:243 GetMetaItemLayeredResponse 0.88 / 15
FileValueView fields/.../file-value.ts:35 ApprovalActionAttachment 0.83 / 5
AppShellBranding layout/src/AppShell.tsx:13 AppBranding 0.80 / 4
SpecResponsiveConfig mobile/.../useResponsiveConfig.ts:26 ResponsiveConfig 1.00 / 4
NormalizedAxis plugin-charts/.../normalizeChartSchema.ts:122 ChartAxis 1.00 / 9
AgentCapabilities plugin-chatbot/src/useAgents.ts:23 AiAgentCapabilities 1.00 / 4
BulkResult plugin-grid/.../useBulkExecutor.ts:27 MetadataBulkResult 1.00 / 4
SavedMappingEntry plugin-grid/src/savedMapping.ts:6 ImportFieldMapping 1.00 / 4
CacheManager types/src/data-protocol.ts:1420 ICacheService 0.83 / 5
BatchTransactionOperation types/src/data.ts:185 CrossObjectBatchOperation 1.00 / 4
ImportFieldMappingEntry types/src/data.ts:872 FieldMappingEntry 0.83 / 5
ImportRequestOptions types/src/data.ts:884 CreateImportJobRequest 0.81 / 13
ImportRecordsResult types/src/data.ts:938 ImportResponse 1.00 / 10
CreateImportJobResult types/src/data.ts:960 CreateImportJobResponse 1.00 / 5
ImportJobProgressInfo types/src/data.ts:978 ImportJobProgress 1.00 / 18
ImportJobSummaryInfo types/src/data.ts:1024 ImportJobSummary 1.00 / 13
ImportJobUndoResult types/src/data.ts:1046 UndoImportJobResponse 1.00 / 6
ListImportJobsOptions types/src/data.ts:1062 ListImportJobsRequest 1.00 / 4
BPMNEdge types/src/designer.ts:264 FlowEdge 0.86 / 6

Both known instances are correctly absent, as the ruling predicted: #4588's ViewNavigationConfig is now export type ViewNavigationConfig = NavigationConfig and #4550's sibling is a spec-derived alias, so neither is a hand-written shape any more.

Phase 2 — the decision the census forced

The structural gate was NOT built. 38 standing exemptions is not a guard, it is a second copy of the codebase, and nearly all 38 are legitimately distinct layers (ApprovalActionLite is a projection, SimEdge is the simulator's own node, BulkResult is four counters). That is far outside the "single digits" the ruling set as the condition for building it.

A second, unplanned result made the case stronger: the structural instrument is blind to the largest sub-class. Eight declarations claim alignment with spec symbols the installed spec does not export at all — DateFormatSchema, NumberFormatSchema, PluralRuleSchema, LocaleConfigSchema, FieldChangeEntrySchema, MentionSchema, ReactionSchema, RecordSubscriptionSchema. A key-set comparison has nothing on the other side to compare against; only the prose says anything at all. Filed as #4597.

The prose heuristic was built, with CLAIM_ALLOW (reasoned, per-entry) and a shrink-only CLAIM_DEBT ledger, mirroring the file's existing ALLOW + DEBT shape, and --claim-ledger regenerating the ledger so it is never hand-maintained.

Red-first: the discrimination proof

Run over an identical tree — the real repo plus one fixture package — with predictions written before the runs. The fixture is #4588's declaration renamed (ViewNavigationConfigFixture), six keys, mode required where the spec publishes it input-optional, under the canonical-sounding comment.

Current gate (origin/main), predicted PASS:

✅  spec symbol derivation: 1257 files scanned against 4834 spec export names; 13 declared dialects, 3 untriaged collisions in 1 packages.
EXIT=0

Hardened gate, identical tree, predicted RED:

❌  a spec-alignment claim has nothing behind it:

    • @object-ui/fixture claims spec alignment on 1 declaration that references nothing from the spec:
        `ViewNavigationConfigFixture`  packages/fixture/src/renamed-copy.ts:5  (claim: "Aligned with")
      Back the claim or drop it: derive the declaration (`z.infer< typeof SpecX >`,
      `SpecAuthoredInput< … >`, or import the spec type), add a CLAIM_ALLOW entry saying why
      the copy is deliberate, or delete the sentence — a canonical-sounding comment with
      nothing behind it is a planted premise for the next session, not stale documentation.
EXIT=1

The dangling-symbol annotation, on a fixture citing ReactionSchema:

        `ReactionFixture`  packages/fixture/src/dangling.ts:5  (claim: "Aligned with")
            …and names `ReactionSchema`, which @objectstack/spec does not export.

Green cases, all five on the same tree, exit 0: a z.infer derivation, a SpecAuthoredInput derivation, a similarly-shaped local type with no claim, a claim phrase belonging to a different sentence than the spec mention, and a renderer carrying an alignment claim. Plus, from the test suite: an interface that merely uses a spec type on a member, an unexported declaration, and a licence banner that must not donate its prose to the first declaration.

Reverse verification (patch + git checkout + sha256-verified restore; no git stash). With the fix removed, the new suite cannot even run:

Error: process.exit unexpectedly called with "0"
 ❯ scripts/check-spec-symbol-derivation.mjs:663:11
 Test Files  1 failed (1)      Tests  no tests

— which also proves the new invokedDirectly guard is load-bearing rather than cosmetic. Restore verified: scripts/check-spec-symbol-derivation.mjs: OK against the pre-revert sha256.

False-positive analysis — two predictions failed, and the heuristic changed both times

This is the part worth reviewing. The first pattern set flagged 51 declarations. Three precision rules brought that to 27, and two of them exist because a fixture I predicted green came out red:

  1. Attached comment only, not the licence banner. Reading all leading comment ranges let a banner supply the claim phrase for whichever declaration sat first.
  2. Proximity window (60 chars) — mis-predicted, caught by fixture E. ChartDataSeries reads "positionally aligned with the chart's categories" and separately explains what @objectstack/spec/ui owns. A bare co-occurrence test flags a model citizen.
  3. Same sentence, not just nearby — the window alone was NOT enough. My compressed fixture put the two 53 characters apart and it flagged anyway. Requiring no sentence boundary between claim and mention fixed it, and the tree's own ratchet immediately confirmed the win by reporting SPEC_ACTION_KEYS as a now-stale ledger entry — a second real false positive removed by the same rule.
  4. A blank line breaks attachment. A file whose banner is its only comment donated it to the first declaration; caught by a test that failed on the first run.
  5. Renderers are not shapes. Functions, classes and arrow-const components are skipped, on the judgement the existing AuthProvider / ListView / UserFilters ALLOW entries already make. Suppresses 9 sites.

Where the 27 remaining sit: essentially none is a false positive in the sense that the flag is wrong — every one is a spec-alignment claim with no compile-time tie. They split into deliberate-and-reasoned (CLAIM_ALLOW) and untriaged (CLAIM_DEBT).

CLAIM_ALLOW — 2 entries, both with the reason already written at the declaration

  • @object-ui/app-shell:MarketplacePackageTranslation — the app-shell bundle must not pull in @objectstack/spec for five translatable marketplace strings. A bundle-size duplication is a decision, and the comment says so rather than claiming the copy IS the spec's type.
  • @object-ui/core:ElementDataSourceConfigfilter is typed unknown rather than the spec's FilterCondition because three legitimately different filter shapes reach a renderer. The comment names the diverging key and its reason.

Deliberately kept to 2. RecordLookupBinding reads as a candidate but its own comment says "import it once a published ^16 release…" — that is temporary, so the ledger is the honest home, not a permanent exemption. The remaining 25 sit on surfaces this card does not own (#4580 holds packages/types), and triaging them is the owning seat's call, not a scripts/** card's. A test pins CLAIM_ALLOW at <= 10 entries so it cannot quietly become the 38-entry map this PR declined to build.

CLAIM_DEBT — 27 symbols, shrink-only, mechanically regenerated

@object-ui/types 17, @object-ui/i18n 4, @object-ui/core 2, and one each in app-shell, mobile, plugin-view, react. Every entry is --claim-ledger output verbatim; the ratchet fails the build if one stops matching, so it cannot outlive what it excuses. Eight of them are the dangling-citation cases (#4597) and one is a true renamed hand-copy (#4598).

Verification

  • node scripts/check-{control-bytes,phantom-dependencies,changeset-presence,changeset-no-major,changeset-fixed,type-check-coverage,lint-coverage,spec-symbol-derivation,doc-links}.mjs — all 9 PASS.
  • The hardened gate on the real tree, exit 0: 13 declared dialects, 3 untriaged collisions (rule 1 output byte-identical to origin/main) and 2 declared deliberate copies, 27 unbacked claims in 7 packages.
  • pnpm exec vitest run --maxWorkers=2 scripts/42 files, 943 tests, all pass (20 of them new).
  • pnpm type-check:scripts — exit 0. pnpm exec eslint on both touched files — 0 problems.
  • Control-byte self-scan grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over every touched file — clean.

One incidental catch worth flagging to reviewers: scripts-type-check.test.ts pins that no file in the scripts tsconfig program imports an @object-ui package, and it looks for the import text — so a fixture string inside a template literal tripped it and would have moved a CI step below the workspace build for no real dependency. The fixture now uses a relative specifier, with a comment saying why, since the helper is recognised by name rather than by module.

Changeset

Empty-frontmatter (.changeset/spec-symbol-rename-blindspot-4592.md) — tooling only, releases nothing, per the documented exemption.

Out of scope, filed unassigned

Neither is fixed here: #4592 owns scripts/**, and those surfaces belong to other seats.


Generated by Claude Code

…wn alignment claims (#4592)

`check:spec-symbols` matches BY NAME, so a hand copy that was RENAMED away
from the spec's symbol has nothing for it to match. `ViewNavigationConfig`
(#4588) was exactly that — the spec's six navigation keys, hand-written,
drifted on `mode`, under the comment "Aligned with @objectstack/spec
ListView.navigation" — and it passed every CI run until a manual census
found it.

Rule 2 flags an exported declaration whose doc comment claims alignment
with `@objectstack/spec` while the declaration references nothing
spec-bound, with the governance the file already uses: a reasoned
CLAIM_ALLOW map (2 entries) and a shrink-only CLAIM_DEBT ledger (27
symbols, regenerated by --claim-ledger).

The structural alternative was built as a one-off census and measured
before being rejected: 38 sites at >= 0.80 key overlap with a spec object
type, nearly all legitimately distinct layers, and blind to the largest
sub-class — eight declarations cite spec symbols the spec does not export.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@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 5:11pm

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 17:25
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit a5eb827 Aug 13, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4592-spec-symbol-rename-blindspot branch August 13, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants