Skip to content

Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports - #6883

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-3162-types-any-erasure-batch8
Aug 30, 2026
Merged

Close the #4115 any-erasure ledger: batch 8 measured not-burnable, zero re-exports#6883
os-sam merged 2 commits into
mainfrom
claude/issue-3162-types-any-erasure-batch8

Conversation

@claude

@claudeclaudeBot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes#3162

Batch 8/8 of the objectstack#4115 any-erasure ledger. It lands zero re-exports — the
measurement says none of the four symbols is burnable, and three of them for reasons that
have nothing to do with the blocker the card recorded.

The card's start signal, and what it actually measured

The card said to run spec-derived-unions.test.ts first: its inverted tripwires
(_spec*IsStillAny) should fail compiling once objectstack#4171 landed. Measured on
origin/main at a04d7c6, before any edit:

  • Those tripwires no longer exist under that name. objectui#3177 replaced them, having
    found that any was never the only blocker for either symbol.
  • Their replacements — per-blocker probes that assert the current state — are green:
    pnpm --filter @object-ui/types run type-check exits 0, and vitest run packages/types/
    reports Test Files 73 passed, Tests 844 passed. Both tripwire files are genuinely in
    the type-check program (confirmed with --listFiles, 2 hits), so the green covers them.

So the start signal did not fire, and the card's premise — "the only thing blocking these
four is objectstack#4171" — is falsified.

The four verdicts, measured in the BUILT dist

Not in source, and not from the fact that objectstack#4171 closed. Dist erasure is invisible
from either, which is the entire reason this ledger exists. Measured against
@objectstack/spec 17.2.0:

SymbolUpstream in distBurnableWhy
JoinNodedoes not existnoTS2305 from both /ui and /data, against a control import that resolves. Spec 17.0.0 retired the query.joins cluster. Nothing to bind to.
NavigationItemprecise (IsAny and IsUnknown both false)noobjectstack#4171 really did land. The three semantic blockers objectui#3177 pinned are untouched by it.
NavigationItemSchemaprecise (z.ZodType over the union)noLive blocker is runtime shape, invisible to every type-level probe.
JoinedReportBlockstill unknownnoSpec still declares the schema as a bare z.ZodTypeAny. objectstack#4171 typed the recursive schemas and never touched this one.

NavigationItemSchema is the dangerous one, and it is the case ZONE 1d of the dispatch warns
about. Its upstream became precise, so the ledger's stated reason is spent — which reads as
"safe to bind now". It is not: this schema has a published consumer in objectui validate,
and referencing the spec's would make it reject navigation metadata this renderer accepts
today
. Measured, with two positive controls that both accept:

  • pinned, defaultOpen, and a separator carrying label fail unrecognized_keys against
    the spec's strict branches
  • visible: boolean fails invalid_union — and menuItemToNavigationItemmanufactures
    one when it inverts legacy MenuItem.hidden
  • a one-character id fails too_small

What this PR changes

  1. app.zod.ts carried a reason measurement has made FALSE — "the spec's is
    z.ZodType of any and would validate nothing". Left alone, the next triage checks that
    claim, finds it false, concludes "burn it down", and lands the exact regression this
    ledger exists to prevent. Replaced with the measured reason.

  2. JoinNode absence is now pinned, in the file that already owns "the spec does not own
    X", so a re-minted upstream name is caught.

  3. The JoinedReportBlock pin no longer reads as pending on a closed issue, and records
    that its erasure has a different cause than the one objectstack#4171 fixed.

  4. navigation-spec-parity.test.ts gains the only mechanical guard for the CURRENT
    blocker
    — the five divergences above, behind two positive controls. Until now that
    reason existed only as prose, and prose does not fail.

  5. The machine-readable ledger is cleared.check-spec-symbol-derivation.mjs keys a
    DEBT map to objectstack#4115 whose @object-ui/types entry was exactly these symbols.
    They were never untriaged — they are measured, so they move to the guard's ALLOW
    registry of declared deliberate dialects, each carrying its reason and its release
    condition:

    before: 13 declared dialects, 3 untriaged collisions in 1 packages
    after: 16 declared dialects, 0 untriaged collisions in 0 packages
    

Bounded in-place fix, declared

scripts/check-spec-symbol-derivation.mjs is outside the file surface my claim comment
declared
; the surface was amended on the card before the edit, because clearing the ledger
is impossible from packages/types/** alone.

While in that file I also corrected the FormField ALLOW reason, which asserted the spec's
FormField "erases to any in its dist, so a re-export would delete typing outright".
Evidence it is false: spec-derived-unions.test.ts already pins
_specFormFieldIsNoLongerAny = false satisfies IsAny(SpecFormField). It is the same defect
class as the batch this PR closes — a spent any-erasure justification standing in for the
real one — and the real reason (the two-layer split, guard header case 2c) is untouched by
precision. Same gate family, no new verification surface.

Verification

Union re-run after the final commit, at 2c3e93e, tree clean:

  • pnpm exec vitest run packages/types/ scripts/__tests__/check-spec-symbol-derivation.test.ts
    Test Files 74 passed, Tests 885 passed (844 to 885; +7 new assertions here, the rest
    is the added gate-script suite)
  • pnpm --filter @object-ui/types run type-check — lock verdict command-exit 0
  • check:spec-symbolsspec symbol derivation: ... 16 declared dialects, 0 untriaged collisions in 0 packages
  • check:control-bytesOK (scanned 5736 tracked text file(s))
  • check-changeset-no-majorNo changeset declares a major bump
  • eslint over all 5 changed files, each confirmed reported on rather than ignored: 0
    errors

Because I edited a gate script, its own suite is owed separately from the derived family: all
20 test files that name check-spec-symbol-derivation ran green (310 tests).

Reverse-verified, both legs committed first, mutation confirmed on disk by hash change
plus grep of injected and removed text, restore proven byte-exact against the HEAD blob:

  • flipping _specNavSchemaIsNoLongerAny to true satisfies fails with TS1360 at that
    line — predicted red, observed red
  • removing the divergent key from the pinned case makes the spec accept it: exactly 1 test
    fails, 14 pass — predicted red, observed red

Repo-wide scans are CI's run, not duplicated here.

Note for the maintainer

The card's own title and body still open with "currently blocked upstream, do not start"
(当前被上游阻塞,勿提前动工). That has been false since 2026-07-30 and triage flagged it on
2026-08-18 as having repelled claimants for 19 days; by now it is 40. Worth correcting so
the closing state of this ledger is not read as still-blocked.


Generated by Claude Code

…ts, not re-exports
Batch 8/8 of the objectstack#4115 any-erasure ledger covered `JoinNode`,
`NavigationItem`, `NavigationItemSchema` and `JoinedReportBlock`, all filed as
"blocked on objectstack#4171 — re-export when the upstream erasure lifts".
#4171 closed completed on 2026-07-30. Re-measuring in the BUILT dist at spec
17.2.0 (not in source, and not from the issue's state) finds none of the four
is burnable, for three different reasons — none of them the one the ledger
recorded:
- `JoinNode`: the spec no longer exports the symbol at all (TS2305 from both
`/ui` and `/data`); spec 17.0.0 retired the cluster. Nothing to bind to.
- `NavigationItem`: upstream is precise now, but the three semantic blockers
#3177 measured are untouched by #4171.
- `NavigationItemSchema`: upstream is precise now too — and that is exactly
what makes the burn-down dangerous, because the live blocker is runtime
shape. Referencing the spec's schema would make `objectui validate` reject
metadata this renderer accepts today.
- `JoinedReportBlock`: still erased, to `unknown`, by a cause `#4171` never
covered (a bare `z.ZodTypeAny`, not recursion).
So the batch lands zero re-exports and converts the remaining debt into pins
that name their own release condition:
- `app.zod.ts` carried a reason that measurement has made FALSE — "the spec's
is `z.ZodType<any>` and would validate nothing". Left alone, the next triage
checks that claim, finds it false, and lands the regression this ledger
exists to prevent. Replaced with the measured reason.
- `JoinNode` absence is now pinned in the file that already owns "the spec does
not own X", so a re-minted upstream name is caught.
- The `JoinedReportBlock` pin no longer reads as pending on a closed issue.
- `navigation-spec-parity.test.ts` gains the only mechanical guard for the
CURRENT blocker: five pieces of metadata objectui accepts and the spec
rejects, behind two positive controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
…h 8 as declared dialects
`check-spec-symbol-derivation.mjs` carries the ledger's enforceable half: a `DEBT` map
keyed to objectstack#4115 whose `@object-ui/types` entry was exactly the three batch-8
symbols that still collide with a spec export, reported by the gate as "3 untriaged
collisions".
They are not untriaged — they are measured, and the measurement says they must NOT be
burned down. That is what the guard's `ALLOW` registry is for: "a same-name symbol that is
narrower or wider ON PURPOSE belongs here with that purpose written down". So the three move
DEBT to ALLOW with their measured reasons and their release conditions named as pins, and
the DEBT map empties.
before: 13 declared dialects, 3 untriaged collisions in 1 packages
after: 16 declared dialects, 0 untriaged collisions in 0 packages
Also corrects the `FormField` ALLOW reason, which asserted "the spec's FormField type erases
to `any` in its dist (objectstack#4171), so a re-export would delete typing outright". That
is false now and `spec-derived-unions.test.ts` already pins it false
(`_specFormFieldIsNoLongerAny`). It is the same defect class as the batch this PR closes — a
spent `any`-erasure justification standing in for the real one — and the real reason (the
two-layer split, guard header case 2c) is unaffected by precision.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3176.8 KB3222.7 KB
Main entry chunk (gzip)143.6 KB350 KB
Entry fileindex-qETeoOTg.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)12.46KB4.71KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)512.13KB116.43KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.17KB47.98KB
fields (index.js)243.36KB61.51KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.43KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.32KB32.69KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.69KB54.58KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.83KB21.11KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-samClaude

Copy link
Copy Markdown
Collaborator

Review — ACCEPT

Landing is gated on the 4 test shards still running; everything below is the verdict on the
diff, which does not depend on them.

The one question this PR actually turns on

Not "are the four symbols burnable" — the measurement for that is thorough and I spot-checked
its shape, not just its conclusion. The question is whether emptying DEBT by moving three
names into ALLOW weakens the gate
, because if it does, that is a human-floor item and not
mine to accept. I read scripts/check-spec-symbol-derivation.mjs rather than reasoning from
the PR body. It does not:

  • Neither bucket is the stronger suppressor. An ALLOW hit is filtered out of violations
    before byPackage is built (:1096); a DEBT name passes into byPackage and is then
    exempted by name in rule 1 (:1158). Different code path, identical effect — a fresh
    collision under a name in either registry does not fail.
  • Both are ratcheted, symmetrically. Rule 2 errors on a DEBT name that no longer
    collides; rule 3 errors on an ALLOW key that matched nothing. The stated risk of a stale
    entry — "it reserves the name: the next fork under it would land silently" — is covered the
    same in both directions after this change as before it.
  • Both are keyed pkg:name, so neither reserves the name for a different package. Rule 1
    still fails a fresh fork by name, which is the half that stops the bleeding.
  • outstanding is informational. It is printed in the summary line, not an exit
    condition — confirmed by main being green at 3 untriaged. So this PR is not buying a
    green that was red, in either direction.

And the script's own header already names this as the designed resolution (:485): "or move
it to ALLOW with the reason it deliberately…"
. Moving a triaged collision out of a debt
bucket that implies owed work, into a registry that carries the reason, is the path the gate
was built for. What would have been weakening is deleting the names outright or widening a
glob; neither happens here.

Human floor / governed surface

Clear on every axis. Zero re-exports, so no published type changes and nothing is widened.
app.zod.ts is comment-only — I checked the hunk line by line; NavigationItemSchema's
declaration at :126 is untouched. No new dependency edge: the added
@objectstack/spec/ui import is in a test file, in a package that already imports from it.
scripts/** is not a governed surface, and the repo's own Governed Surface Queue Guard
agrees — success. Two independent readings, not one.

What makes this a good close rather than a tidy one

The three ALLOW entries each carry a measured reason and a named release condition that
can fail
, which is the difference between documentation and a gate. The one that matters
most is NavigationItemSchema, and the PR is right that it is the dangerous case: its
upstream became precise, so the ledger's recorded reason is spent, and a future triage reading
only the ledger would conclude "safe to bind" and land the exact regression this ledger
exists to prevent. The five runtime divergences in navigation-spec-parity.test.ts are the
first mechanical statement of the live reason — and they are built correctly, with two
positive controls that both accept
, so a spec schema that rejected everything could not make
them pass. That is the zero-hit discipline applied without being asked for it.

The empty changeset is right: comments, tests and a repo script change nothing published.

Two observations, neither blocking and neither needing a change:

  • DEBT_ISSUE now survives only inside rule 2's message, which cannot fire while DEBT is
    {}. Correct to keep — re-adding an entry must keep working — but worth knowing it is
    dormant rather than live.
  • The in-passing FormFieldALLOW correction is the same defect class as the batch (a spent
    any-erasure justification standing in for the real one), it was declared rather than
    smuggled, and it adds no verification surface. Accepted as bounded.

Card state repaired

The ⭐ PM ACTION is done. #3162's title and body opened with 「当前被上游阻塞,勿提前动工」,
false since 2026-07-30 and flagged stale on 2026-08-18 — it had repelled claimants for 40
days
by dispatch. The title is corrected and a dated correction block now leads the body,
with the original kept verbatim below it as evidence. The card also documented a start signal
that could not fire: it said to watch the _spec*IsStillAny tripwires, and objectui#3177
had already replaced them. Anyone following the documented rhythm would have waited forever.

Amending your declared file surface on the card before editing scripts/ was the right call
and the right order.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

台账燃尽批次 8/8 · any 擦除簇 4 符号 —— 上游阻塞已解除,实测四条全部不可燃尽(objectstack#4115)

2 participants

@os-sam@claude