Skip to content

test(types): watch the mirror-wider-than-declared direction with a fifth parity ledger - #7758

Merged
os-justin merged 6 commits into
mainfrom
claude/issue-7069-parity-mirror-wider-ledger
Sep 5, 2026
Merged

test(types): watch the mirror-wider-than-declared direction with a fifth parity ledger#7758
os-justin merged 6 commits into
mainfrom
claude/issue-7069-parity-mirror-wider-ledger

Conversation

@os-justin

@os-justin os-justin commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7069

The parity file watched two directions and, by construction, not a third: a mirrored,
data-shaped key whose zod face accepts a spelling the TypeScript face refuses. This adds
the reverse-inequality operator, a fifth ledger seeded at the measured debt, its
invariant, and a runtime leg bounding the part the type level cannot judge.

⛔ No mirror and no declaration moves. Every seeded entry is a disposition still to be
made, which is the shape #6058 landed UnmirroredDeclared with. The dispositions are
filed as #7759; the instrument limitation the audit found is filed as #7760.

Deliverable 1 — the audit, before the ledger

Triage's hard order. Every number below was re-derived on this branch's merged tree
(origin/main e546222b merged at 8e037c81), after PR #7726, PR #7731 and the later
siblings landed in this file; nothing is carried from before.

The population I scanned for

"Mirror wider than declared" has several producers. The instrument is one type-level
comparison — [z.input of the mirror slot] extends [the declared type], non-distributive,
over keys BOTH faces declare — so what it covers is a property of that comparison, not a
choice:

producer covered why
(b) z.string() / a wider enum against a narrower TS literal union both faces concrete; the comparison is exactly this question
(d) optionality differences (mirror optional, TS required) z.input of an optional carries undefined, so it enters the comparison
(a) z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]) against a TS twin spelling only the array see below — the input face is unknown, not the union
(c) z.any() against a narrower TS type any is assignable to everything, so the comparison is green by construction and can never report it
(c') z.unknown() against a narrower TS type excluded with (a); indistinguishable from it at the type level

The producer the card called systematic is the one the type level cannot judge, and
that is the audit's main finding.
base.zod.ts#SchemaNodeSchema is annotated with a
z.ZodType whose type argument is any, to break the recursion inside its own z.lazy,
and zod 4 defaults such a schema's INPUT parameter to unknown. So every slot spelled
through it reads unknown (or unknown[]) on the input face — wider than every
declaration BY DEFINITION, and silent about what the mirror accepts at runtime, where the
lazy union does validate. Comparing there reports the annotation, not the accept-set.
Filed as #7760.

Re-derived on the merged tree, two ways: the literal
z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]) spelling occurs at 61 sites
across 12 files under packages/types/src/zod/ — confirming the dispatch's count and
showing triage's "12" counted FILES. Structurally, after .extend() inheritance, 304
registered-mirror slots reach a recursion-breaking lazy node, because every component
mirror inherits the base's two schema-node slots.

The distribution

Denominator: 157 registered pairs (EXPECTED_MIRROR_PAIRS, the pin PR #7731 landed).

reading pairs keys
raw reverse-inequality signal, no exclusion 129
after excluding the unconstrained input face 36 54
— of those, CONCRETE face (a real accept-set gap) 18 27
— of those, SCHEMA-NODE (an unknown nested deeper) 21 27

The raw-signal figure was measured by mutating Unconstrained to a constant false on a
trap-guarded scratch tree (anchor counts before/after; restore proven by blob hash and an
empty git diff HEAD), not estimated. Of the 54 seeded keys, 12 across 8 pairs are
also in KnownDrift: those are DISJOINT vocabularies, where each face refuses something
the other admits, now measured from both sides.

Ledger sizes re-derived on the same tree, for the record: KnownDrift 42 entries / 63
keys, UnmirroredDeclared 14 / 96, RuntimeOnlyDeclared 7 / 24, the new
WiderThanDeclared 36 / 54.

Positive control per producer

Each is a synthetic pair the instrument must catch or must ignore, pinned in the file, not
run once by hand:

  • assertionWiderOperatorReportsAWidening — producer (b): mirror z.string(), declaration
    two literals. Reported.
  • assertionNarrowerOperatorIsBlindToAWidening and
    assertionUnmirroredOperatorIsBlindToAWidening — the SAME pair is silent under both
    operators that stood before. ⭐ This is the card's whole claim reduced to one pair, pinned
    rather than argued.
  • assertionUnconstrainedFaceIsExcludedButNotAWaiver — an unknown slot and an
    unknown[] slot are excluded while a concrete widening on the same pair is still
    reported, so the exclusion is a boundary and not a silencer.
  • assertionSchemaNodeFaceIsUnconstrained — the premise the exclusion rests on: the
    annotation really does erase that const's input face.

What the audit found that the card did not predict

The class the card said would outlive its instance has a live instance.
form.zod.ts#SliderSchema.defaultValue and .value are
z.union([z.number(), z.array(z.number())]) on the mirror against number[] on the
declaration — the exact DataTableSchema.toolbar shape #6881 measured and PR #7066 retired,
on a pair nothing had looked at. form.zod.ts#FormSchema.layout is the plainest instance of
producer (b): the mirror is z.enum(['vertical', 'horizontal', 'grid']) and the declaration
states the first two, so the third spelling parses green and tsc refuses it.
layout.zod.ts#ContainerSchema.maxWidth admits true where the declaration admits the false
literal alone.

⚠️ TableColumn.type is NOT in the ledger — the #5853 repair landed, and its absence is
the ratchet reading correctly rather than a miss. TableColumnSchema.cell is in, on the same
pair, as a different key in a different class.

⚠️ The instance the card names as open, DataTableSchema.rowActions (#6940), is also not
here.
That card is CLOSED and the key was repaired — the parity header records the repair
on the KnownDrift side — so the card's third example is stale, not missed.

Deliverable 2 — the ledger

  • WiderThanDeclaredKeys — the operator, dual to NarrowerThanDeclared, at the type
    level, in the same non-distributive tuple-wrapped shape. DeclaredKeys and not keyof D
    supplies the declared half, because BaseSchema's index signature resolves the declared
    type to any for a key the declaration does not really state, and any absorbs this
    direction as silently as the other.
  • WiderThanDeclared — the ledger, keyed by MirrorKey, seeded with all 36 pairs and
    their exact key sets, each entry naming its class and its wider shape, and the instance card
    where one exists.
  • assertionWiderMatchesLedger — the invariant, routed through the existing
    ReconcileAgainstLedger (its shape fits unchanged), written as an assignment to never so
    the compiler prints the offending PAIR.
  • assertionWiderLedgerRecordsEveryKey — the same reconciliation reported by KEY. Not a
    duplicate: the pair-level message locates the failure and says nothing about which key
    moved; the two together are the diagnosis, which the ablation below shows.
  • assertionNoVacuousWiderMeasurement — see the withdrawn design note.
  • assertionWiderLedgerKeysAreRegistered — every ledger key names a registered pair.
  • Header updated: the covered directions are now three, and the third section says where the
    measurement's hole is.

Runtime, where the type level cannot see

The dispatch asked for the type level first and runtime only where the type level is blind,
and to say which. It is blind to WHICH mirrors produce the unconstrained faces it excludes:
at the type level every unconstrained face looks alike, so a new recursive mirror widens the
blind region without changing one character the compiler reads. At runtime the mirrors are
values, so the region is enumerable. Three cases walk _zod.def from every registered
mirror and pin the SOURCES in both directions — not a count of slots, which moves with every
.extend().

The first spelling of that leg asserted the region had a single source,
SchemaNodeSchema. The walk refuted it on its first run
: the navigation, menu, tree,
action and filter-condition mirrors each carry the same z.ZodType ANY annotation — 10
such consts across 7 files — plus three lazy nodes with no exported const to name. The
claim was replaced by the measurement, and the file records that it was wrong, because the
wrong version is the intuitive one: the card, the triage and the dispatch all describe the
producer as if it were one const.

One design withdrawn, and why it is in the file

A recursive Unconstrained that descended into object properties would have excluded the
SCHEMA-NODE class properly instead of only its top two levels. It was written, measured and
withdrawn: on the deepest pairs it drove the whole WiderOf instantiation at a pair to
any, and any is assignable to never, so the invariant went silently green on exactly
the pairs with the most structure
— a guard that cannot fail, wearing the shape of a
stricter one. assertionNoVacuousWiderMeasurement exists because of that attempt and pins
the failure mode, so a future deeper predicate cannot land unnoticed. This is why the
SCHEMA-NODE entries are seeded and labelled rather than excluded.

Red-first proof

Re-run on the FINAL head 8e037c81. Trap-guarded, absolute paths, mutation proven on disk by
anchor counts before and after (⛔ never by an editor's exit code), restore proven by blob
hash against HEAD and an empty git diff HEAD. Baseline at HEAD: tsc exit 0.

leg mutation on-disk proof result
A — a widening appears on an UNLEDGERED pair AlertSchema.variant gains a third enum arm anchor before old=1 new=0, after old=0 new=1 tsc exit 1; Type '"data-display.zod.ts#AlertSchema"' is not assignable to type 'never' and Type '"variant"' is not assignable to type 'never'
B — a seeded entry goes stale 'layout' dropped from the FormSchema entry anchor before old=1 new=0, after old=0 new=1 tsc exit 1; names the pair and Type '"layout"'

Both restores: blob matches HEAD, git diff HEAD empty. Predicted direction was RED in
both legs, in opposite directions, and that is what was observed. No dist is involved —
tsc -p tsconfig.test.json reads the mirrors as sources in one program, so there is no build
step between the mutation and the measurement.

Changeset

check-changeset-presence.mjs counts the test file as source of a released package, so it
demanded a declaration and got the empty-frontmatter form, which that gate documents as
the explicit exemption. ⛔ No skip-changeset label — a phantom in this repo.

Nothing ships: tsc -p tsconfig.json --listFilesOnly lists 0 files under __tests__,
and tsc -p tsconfig.test.json --listFilesOnly does list the parity file, so the type-level
invariants are inside a program that runs.

Gates — exit codes captured before any pipe, heavy runs through the shared verify lock

All re-run on the final head 8e037c81:

gate exit
pnpm exec vitest run --maxWorkers=2 packages/types/ 0 — 120 files, 2068 tests
pnpm --filter @object-ui/types type-check 0
node scripts/check-changeset-presence.mjs 0
node scripts/check-changeset-fixed.mjs 0
node scripts/check-changeset-no-major.mjs 0
node scripts/check-changeset-overwrite.mjs 0
pnpm run check:control-bytes 0 — 6321 tracked text files
pnpm run check:spec-symbols 0
pnpm run check:published-tsconfig-exclude 0
pnpm --filter @object-ui/types lint 0 errors (268 pre-existing warnings)

The three new runtime cases were confirmed to EXECUTE under --reporter=verbose, not merely
to be collected. Control bytes were also self-scanned on the changed files outside the gate.

Governed-surface predicate on the final file list: 0 of 2 paths hit the register
ordinary queue landing applies.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s

…fth parity ledger

The parity file's four ledgers are blind by construction to a mirrored,
data-shaped key whose zod face accepts a spelling the TypeScript face refuses:
the forward comparison finds the declared type fits, and the unmirrored half
finds the key present in `.shape`. Adds the reverse-inequality operator, a
fifth ledger seeded at the measured debt, its invariant, recognition pins for
both blindnesses, and a runtime leg bounding the region the type level cannot
judge.

No mirror and no declaration moves: every seeded entry is a disposition still
to be made.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
… by pair

The pair-level assignment locates the failure; the symmetric difference names
the key that moved. Neither message alone is the diagnosis.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
The type level cannot report which recursion-breaking mirrors produce the
unconstrained input faces it excludes; at runtime the lazy nodes are reachable,
so the sources are enumerated and pinned in both directions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
…ity-mirror-wider-ledger

# Conflicts:
#	packages/types/src/__tests__/zod-mirror-parity.test.ts
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3186.7 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-BJOuTw-1.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.63KB 116.21KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.44KB 61.25KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.87KB 13.31KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.37KB 46.41KB
plugin-dashboard (index.js) 132.87KB 34.68KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.26KB 41.00KB
plugin-grid (index.js) 209.29KB 56.78KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.44KB 6.78KB
plugin-markdown (index.js) 13.93KB 4.81KB
plugin-report (index.js) 43.59KB 11.97KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.20KB 3.19KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@os-justin
os-justin marked this pull request as ready for review September 5, 2026 16:02
@os-justin
os-justin added this pull request to the merge queue Sep 5, 2026
Merged via the queue into main with commit 2760075 Sep 5, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-7069-parity-mirror-wider-ledger branch September 5, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants