Skip to content

test(types): split the unmirrored ledger by remedy — 23 runtime-only callbacks (#6152) - #6181

Merged
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-6152-callback-subset
Aug 24, 2026
Merged

test(types): split the unmirrored ledger by remedy — 23 runtime-only callbacks (#6152)#6181
yinlianghui-tw merged 1 commit into
mainfrom
claude/issue-6152-callback-subset

Conversation

@yinlianghui-tw

Copy link
Copy Markdown
Collaborator

Part of #6152 — implements the PM's option A ruling (#6152 (comment)). This lands the reclassification only; the other 98 keys, every mirror, KnownDrift and NarrowerThanDeclared are untouched.

What changed

UnmirroredDeclared recorded 121 declared-but-unmirrored keys. 23 are callback-shaped (on*), and mirroring is the wrong remedy for every one of them. They move, whole and still pinned, into a new RuntimeOnlyDeclared ledger.

This is a RECLASSIFICATION, and nothing here was repaired. Nothing was mirrored, no declaration was removed, nothing was waived. The same 23 facts are still measured and still reconciled — under a different remedy. 98 is the mirroring debt; 98 + 23 is what "121" used to mean. That statement is written into the seed comment of both ledgers and into the file header, so 121 cannot be re-cited as the mirroring debt the way #6141's numbers were.

Round 1's measurement (comment above) falsified the card's premise in three places, and the ruling followed the measurement:

  • Mirroring enforces nothing. All 23 are already function-only on the shipped dist/*.d.ts, so the only mirror spelling available is z.function() — which no serialized authored document can satisfy. It would move 23 rows off a ledger while adding validator surface no authored payload can populate: visible debt converted into invisible non-enforcement, the exact shape this guard family exists to close.
  • Narrowing is not available.plugin-calendar: an authored onEventClick reaches CalendarView through the spread and throws an UNCAUGHT error on click — needs a contract decision, not a strip #4453 narrowed a key that accepted string OR a function; there is no string branch left here to remove.
  • Removing the declarations would break shipped renderers. 21 of 23 are read off schema.* in renderer source, fed by sibling components that synthesise a schema-shaped object in code and render it through SchemaRenderer.

⚠️ The 16/7 split is recorded, because the 23 did not arrive the same way

73 on* keys are already mirrored repo-wide (56 z.function(), 11 z.string(), 3 z.any(), 3 named). So the mirrors do not omit callbacks by policy:

grouppairsreading
16 OVERSIGHTSDataTableSchema (mirror already carries 4 callbacks), FormSchema (3), DetailViewSchema (1, in the other dialect)the omitted keys sit in the same object literals as the included ones
7 POLICYObjectFormSchema 5, ObjectGridSchema 1, ObjectViewSchema 1objectql.zod.ts has zero on* and zero z.function(), and says so at :164successMessage applies "when no onSuccess handler is given"

Reclassifying all 23 is still right (same remedy), but a later reader must not infer uniform intent from uniform treatment.

The new ledger also records the standing prior art that reached this verdict independently for 3 of the 23 — docs/audits/2026-07-objectview-detailview-schema.md:148 and :237-241 ("live state and callbacks, not metadata", "must never enter a serializable protocol", "a props bag wearing a schema's clothes") and #4650's two-layer split — plus the evidence the declaration was already not trusted: DetailView.tsx:1402 reads a declared key through an (schema as any) cast at its only read site.

The new category is a ratchet, not a waiver

One measurement, two ledgers on the recorded side, reconciled through the union at RecordedUnmirrored. Four new pins keep the split honest, and all four were ablated (mutation proven on disk by anchored counts, restored by git checkout HEAD -- and verified byte-identical):

ablationtscwhat fired
a callback key recorded in neither ledger (what a NEW one looks like)EXIT 2assertionUnmirroredMatchesLedgerType '"data-display.zod.ts#DataTableSchema"' is not assignable to type 'never'
a runtime-only key the measurement no longer produces (STALE)EXIT 2assertionUnmirroredMatchesLedgerType '"objectql.zod.ts#ObjectGridSchema"' is not assignable to type 'never'
the callback misfiled into the ordinary halfEXIT 2assertionNoCallbackShapedKeyInUnmirroredDeclared (:1066) — the union is deliberately blind to which half, so this pin is what catches it
a non-callback key moved into the runtime-only halfEXIT 2assertionRuntimeOnlyIsCallbackShapedOnly (:1080) — this is what stops the category becoming a bucket
one key recorded in both halvesEXIT 2assertionLedgerHalvesAreDisjoint (:1096) + …NoCallbackShapedKey… (:1066)

The blindness of the union is not a gap discovered later — it is pinned as assertionSplitLedgerIsBlindToWhichHalf, next to the three synthetic-pair pins for the split ledger's growth and shrink directions.

Verification — all at 1df712b5f

Heavy steps serialized through the shared verify lock; exit codes captured before any pipe.

BUILD pnpm --filter @object-ui/types build -> VERDICT command-exit 0
RESOLUTION ts.resolveModuleName('@object-ui/types') from packages/plugin-form/src/ObjectForm.tsx
[Bundler] -> packages/types/dist/index.d.ts
[Node16] -> packages/types/dist/index.d.ts
[negative] '@object-ui/does-not-exist' -> UNRESOLVED (correct)
TSC tsc -p tsconfig.test.json -> TSC_EXIT=0 (green BEFORE the change too)
TYPE-CHECK pnpm --filter @object-ui/types type-check (all three projects) -> TYPECHECK_EXIT=0
VITEST (root) RUN v4.1.10 /home/user/objectui-issue-6152
Test Files 3 passed (3) / Tests 27 passed (27) VITEST_EXIT=0
ESLINT eslint . --no-inline-config -f json: 3686 files, 89 errors — all pre-existing in
files this diff does not touch; zod-mirror-parity.test.ts errorCount=0 warningCount=0
GATES check-control-bytes 0 · check-changeset-presence 0 · check-changeset-no-major 0 ·
check-changeset-fixed 0

Ledger counts re-derived mechanically (brace-matched interface, comments stripped), not inherited:

before UnmirroredDeclared 16 entries / 121 keys / 23 callback-shaped
after UnmirroredDeclared 16 entries / 98 keys / 0 callback-shaped
RuntimeOnlyDeclared 6 entries / 23 keys / 23 callback-shaped
KnownDrift 12 entries / 17 keys (untouched)

98 + 23 = 121: the 23 are visible and countable in the new category, so the reclassification is distinguishable from a waiver by measurement rather than by claim.

Out of scope, deliberately

No mirror edit · no declaration removed · the other 98 keys · KnownDrift · NarrowerThanDeclared · #6175 (ObjectGrid column persistence, filed separately and referenced but not addressed here) · the onColumnReorder/onColumnsReorder near-duplicate, recorded in the ledger as an open ruling · the 3 spec-derived pairs beyond what reclassification mechanically required (ObjectViewSchema's other ten keys stay in UnmirroredDeclared and stay routed to #2231).

One observation, recorded in the ledger rather than acted on: ObjectFormSchema.submitHandler is handler-shaped but not on*, so the /^on[A-Z]/ reclassification did not take it. It stays in UnmirroredDeclared until someone measures it.

Test-only; the changeset declares an empty frontmatter, so nothing releases.


Generated by Claude Code

…callbacks (#6152)
`UnmirroredDeclared` recorded 121 declared-but-unmirrored keys. 23 of them are
callback-shaped (`on*`) and mirroring is the wrong remedy for every one: measured on
the shipped `dist/*.d.ts` they are already function-only, so the only available mirror
spelling is `z.function()` — which no serialized authored document can satisfy. Mirroring
them would move 23 rows off a ledger while enforcing nothing, converting visible debt
into invisible non-enforcement.
Narrowing is not available either (there is no string branch left to remove, unlike the
#4453 precedent), and removing the declarations would break shipped renderers: 21 of the
23 are read off `schema.*` in renderer source, fed by sibling components that synthesize
a schema-shaped object in code and render it through `SchemaRenderer`.
So they are RECLASSIFIED, not fixed: a new `RuntimeOnlyDeclared` ledger holds all 23,
`UnmirroredDeclared` goes 121 -> 98 keys, and the two are reconciled against ONE
measurement through the union at `RecordedUnmirrored`. The move is stated in the seed
comments of both ledgers, including the 16/7 oversight-vs-policy split, so 121 is not
silently re-citable as the mirroring debt.
The new category is a ratchet, not a waiver, pinned in both directions on synthetic
pairs: a fresh callback-shaped unmirrored key reddens until filed, a recorded one that
leaves the measurement fails as STALE. Shape pins refuse a callback key in
`UnmirroredDeclared` and a non-callback key in `RuntimeOnlyDeclared`, and a disjointness
pin refuses double-filing.
No published type, no mirror and no runtime behaviour changes.
Part of #6152
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3221.2 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-CMfHbhSB.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)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
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)505.15KB114.53KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)168.48KB46.47KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
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)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
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)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.14KB61.94KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)201.05KB54.38KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
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)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
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)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
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)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.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.49KB2.14KB
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

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.

2 participants

@yinlianghui-tw@claude