Skip to content

test(types): extend the zod-mirror anti-drift guard from BaseSchema to the whole mirror population - #5929

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-5684-zod-mirror-drift-guard
Aug 24, 2026
Merged

test(types): extend the zod-mirror anti-drift guard from BaseSchema to the whole mirror population#5929
os-zhuang merged 4 commits into
mainfrom
claude/issue-5684-zod-mirror-drift-guard

Conversation

@claude

@claude claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes #5684


The anti-drift construction in objectui#5680 was built for a derived parity check for BaseSchema alone. This extends that same construction — not a second mechanism beside it — to the whole mirror population in packages/types/src/zod/.

What a mirror is, and why nothing saw the class

A mirror restates a TypeScript declaration by hand. When the declaration widens and the mirror does not follow, the published validator refuses a spelling the published types invite and the renderer implements: declared != enforced on a shipped surface (@object-ui/types/zod). Two instances were found independently before anything looked for the class — #4605 (corrected by #5680) and #5186 — and both were latent until someone tripped over them.

The enumeration, with its control

The population unit is a pair, not a file: an exported zod const and the TS declaration it restates. The directory has 216 exported consts across 17 files.

bucket count
registered pairs (mirror + declaration) 163
excluded, with a reason 51
total accounted for 214 + 2 barrel consts = 216

Controls, classified by the same pass rather than by hand:

A second name-derived pairing failed the same way and is worth naming because it changes a number: the const FieldConstraintsSchema mirrors FieldValidationRules, not the like-named legacy FieldConstraints in field-types.ts (the flat dialect #5186 withdrew). Under the wrong pairing it reported 5 drifted keys; under the right one it reports none#5186's correction is holding exactly. A name-derived population would have published a 5-key phantom as a finding.

Note on a gate that cannot substitute for this one: check-spec-symbol-derivation's green is not coverage. #5899 measured that it skips every non-exported declaration, so it has been green on mirrors it cannot see. It is not cited as evidence anywhere here.

What the guard does

packages/types/src/__tests__/zod-mirror-parity.test.ts:

  1. One derived construction, 163 pairs. It reads each mirror's own .shape and asks, per key, whether the mirror would refuse the declared type. No key list to maintain — a hand-written key list is the same artefact the drift keeps producing.
  2. A closed population. A runtime census reads the directory off disk and fails when an exported const is in neither the registry nor EXCLUSIONS. A mirror added later cannot join the directory silently.
  3. Non-vacuity for all 163 at once. assertionNoVacuousEntry rejects an entry whose key union has degenerated to never or to bare string — the .passthrough() failure mode types(zod): widen the BaseSchema mirror to match its TypeScript declaration #5680 measured, where a pin came out as never while five keys were demonstrably narrow. assertionEveryPairOverlaps rejects a pairing that compares nothing.
  4. A two-way ratchet, not a waiver. The 17 pairs with drift today are pinned to their exact drifted key sets: new drift on a listed mirror turns red, and so does a listed entry once its drift is corrected.

base-schema-zod-mirror-parity.test.ts keeps all its #4605 runtime pins and no longer restates the construction; BaseSchema is one registered row, and its six-key pin travelled across by name.

The drift the guard caught: 17 of 163

Every one is declared != enforced on a published surface. They are not corrected here, and that is a deliberate call: each key is a two-way question — widen the mirror, or narrow the declaration under ADR-0049 — and #4605 answered it for BaseSchema only by measuring what the renderer actually reads. Widening 17 mirrors to match their declarations without that measurement is consumer-side tolerance (AGENTS.md #0.1) and would publish spellings that render nothing. The full measured table, with both sides printed from the checker, is #5927.

  • Strict widenings (mirror simply behind): ButtonGroupSchema.variant/size, ObjectChartSchema.chartType, FormSchema.validationMode, SelectSchema.defaultValue/value, DataTableSchema.selectable, ViewSwitcherSchema.defaultView/activeView/views.
  • Disjoint vocabularies (one side is dead): FormSchema.mode (disabled|read|edit vs create|edit|view), HeaderBarSchema.variant (floating vs transparent), FilterFieldSchema.operators (is_empty vs is_null), DataTableSchema.rowActions (boolean vs any[]), CalendarSchema.defaultValue/value, ChatbotSchema.body — where the two sides do not differ in width but in meaning.
  • Optionality: ComboboxSchema.options and CommandSchema.groups are REQUIRED by the mirror and optional in the declaration.
  • Structural / spec-derived, for Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231's triage: DashboardComponentSchema, DashboardWidgetSchema (here the mirror is the stricter side — the declaration says unknown), FilterBuilderSchema, CRUDSchema, FormSchema.fields.
  • A deliberate divergence, kept expressible: PageNodeSchema.pageType. PageVisualizationAlias (src/layout.ts) documents it: the TS side retains five visualization names as a sanctioned local extension while the mirror takes the spec's vocabulary by reference, and the spec repudiates them. Widening the mirror would re-add spellings the spec rejects. The card's hard clause — a guard that forbids a decision the repo has already made is a defect — is honoured by ledgering it with its reason instead of "correcting" it.

Exclusions (51), by reason

reason count
no .shape to read — z.lazy / z.ZodType<any> (recursive; accepts any, so it cannot be narrower) 14
a union OVER the mirrors, not an object — z.discriminatedUnion / z.union 12
a bare vocabulary — z.enum; checked where a mirrored KEY declares it 11
spec-owned BY REFERENCE — nothing restated, so nothing to drift 11
an index signature, not a declared key set — z.record 2
the name collision above (StylePropsSchema) 1

Plus, singled out because it is the exact phantom-assertion trap this file otherwise defends against: objectql.zod.ts#ListViewSchema is excluded because the declaration is derived FROM the mirrorListViewSchema = ListViewInferred & ListViewRuntimeProps and ListViewInferred = z.input<typeof ListViewSchema>. Asserting parity there would be true no matter what either side said.

Verification

All at 8c57c5118, the pushed head — which now carries a merge of origin/main, so the local tree is the tree CI evaluates (see the diagnosis section below). Exit codes captured before any pipe.

pnpm --filter @object-ui/types type-check     TYPECHECK_EXIT=0
  > tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json
pnpm exec vitest run --maxWorkers=2 packages/types/
  Test Files  53 passed (53)        Tests  577 passed (577)   VITEST_EXIT=0
pnpm --filter @object-ui/types lint           LINT_EXIT=0
  ✖ 253 problems (0 errors, 253 warnings)     [all pre-existing; none in the new file]
node scripts/check-changeset-presence.mjs     EXIT=0
  ✅ ... Every one of them has an EMPTY frontmatter — declared as releasing nothing
node scripts/check-changeset-no-major.mjs     EXIT=0   ✅ No changeset declares a `major` bump.
node scripts/check-changeset-fixed.mjs        EXIT=0   ✅ All workspace packages are in the changeset fixed group.
node scripts/check-control-bytes.mjs          EXIT=0   ✅ OK (scanned 4930 tracked text file(s); skipped 85 binary).
node scripts/check-lint-coverage.mjs          EXIT=0   ✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
node scripts/check-type-check-coverage.mjs    EXIT=0   ✅ 45/46 via `type-check` ... ✅ 41/41 packages compile their tests

Lint narrowing, declared with its three pieces. ① The universe came from eslint's own flat config (eslint . inside the package), not from a guess about which files count. ② --format json reports 114 files linted, 0 errors, 253 pre-existing warnings. ③ eslint.config.js configures no type-aware linting — no projectService, no parserOptions.project, no project: — so no rule's verdict on a file outside this diff can depend on this diff. The repo-wide farm is CI's run.

No stale-dist hazard, and it is structural rather than lucky. @object-ui/types has no workspace dependencies (pnpm --filter '@object-ui/types^...'"No projects matched the filters"); the guard resolves ../zod/*.zod.js to sibling TypeScript source, and @objectstack/spec is an unmodified npm package. Nothing on the resolution path is a workspace dist/, so no mutation below could have been masked by one — and legs A and B confirm it empirically, mutating .ts sources and being seen with no build step.

Reverse verification — direction predicted before each run

Every leg proved its mutation on disk by grepping injected and removed text (never an editor exit code), and the script carried a trap … EXIT INT TERM restore.

leg mutation on disk predicted observed
A widen AlertSchema.variant (TS) + 'info', mirror untouched injected 1 / removed 0 RED, naming data-display.zod.ts#AlertSchema TSC_EXIT=2; tsc printed Type '"data-display.zod.ts#AlertSchema"' is not assignable to type 'never'
B widen TooltipSchema.delayDuration (TS) to number | string, mirror untouched injected 1 / removed 0 RED, naming overlay.zod.ts#TooltipSchema TSC_EXIT=2; tsc printed Type '"overlay.zod.ts#TooltipSchema"' is not assignable to type 'never'
D ablate the construction so it always resolves never injected 1 / removed 0 RED reversed — the 17 ledgered pairs stop matching their entries TSC_EXIT=2; LedgerMismatch named all 17
E blind the census reader (readdirSync[]) injected 1 / removed 0 the non-vacuity test RED, others vacuously green VITEST_EXIT=1; 2 failed, 2 passed
control none — restored tree git diff HEAD --stat empty GREEN TSC_EXIT=0; LedgerMismatch => never

Two legs are worth reading closely rather than skimming:

  • Leg D is the discriminating one. Blinding the construction does not go quiet — it turns red from the other side, because the ledger pins exact key sets and a blinded guard measures never for all 17. A guard that could be neutered into silence would be worth little; this one fails when it stops seeing.
  • Leg E did not match my prediction, and the deviation is reported rather than smoothed over. I predicted one red (the non-vacuity test) and three vacuous greens. Two went red: the non-vacuity test and "no map entry names a const that no longer exists", because with an empty on-disk set every registry entry reads as stale. That is a second, independent channel against a blind reader — better than predicted, but it was not predicted.

Legs that do not discriminate, named as required: none of the four is silent under its mutation. Leg A and leg B use the same assertion (assertionDriftMatchesLedger) and so do not distinguish which of the two mirrors is at fault from the tsc message alone — the entry name comes from resolving LedgerMismatch, which is why it is exported.

Diagnosis — the first CI red, stated as a measurement

The first push was green locally and red in CI on the same SHA, TS2344 on assertionDriftMatchesLedger. That divergence, not the error, was the thing to explain. Three candidates were put to me and all three are refuted by measurement:

candidate measurement verdict
local tree != pushed commit fresh git worktree add at the pushed SHA, git status --porcelain empty, pnpm install --frozen-lockfile, pnpm --filter @object-ui/types type-check -> CLEAN_TYPECHECK_EXIT=0 refuted — a clean checkout of the pushed SHA is green
node_modules / spec drift see the version table below refuted
census order / environment dependence the failing assertion is type-level, over static literals; the runtime census cannot reach it. Leg E already showed the reader is not blind, and a clean-environment run is green refuted

The spec-version candidate, re-derived from non-stale sources

This one deserved care, because my first reading of a version came from /home/user/objectui — the shared checkout, whose HEAD is someone else's — which is exactly the trap that produced a wrong answer for a sibling task. Every figure below comes from this branch's own worktree or from git show <sha>:pnpm-lock.yaml, never from the shared tree:

source value
installed node_modules/@objectstack/spec/package.json 17.2.0
installed symlink target @objectstack+spec@17.2.0_ai@7.0.65_zod@4.4.3_
lockfile at 3d50d4c3b (the SHA CI called red) 17.2.0
lockfile at the merged head 17.2.0
lockfile at origin/main 17.2.0
git diff --stat 3d50d4c3b..<merge> -- pnpm-lock.yaml empty

One version, every commit involved, and the merge does not touch the lockfile. CI installs with --frozen-lockfile from that same file, so its resolution is identical by construction — not by inspection of a log. The spec version could not have differed.

What did differ

CI checks out the PR's merge ref, not the branch head. actions/checkout@v7 under on: pull_request builds refs/pull/N/merge, so the guard was measured against a base this branch never held. main advanced 0fce2ef81..2074a596d while the branch was open, and one of those commits is 18a8e7db7"fix(types): retire DashboardComponentSchema.aria — spec-tombstoned, renderer-dead" (#5830 / #5855). Its diff deletes aria?: { … } from the TS declaration.

Remove aria from the declaration and it leaves the comparison, so it is no longer drifted — and the ledger entry recording it as drifted goes stale. That is the ratchet's second direction firing on its first real occasion, and it is the behaviour this file was built to have: a ledgered drift corrected elsewhere must not stay recorded. Nothing on the branch changed; the base did.

The controlled experiment that proves it

Environment held completely fixed — same node_modules, @objectstack/spec@17.2.0, tsc 6.0.3, node v22.22.2 — reverting only packages/types/src/complex.ts to its pre-merge content, verified on disk both ways:

on-disk: 'aria?: {' present in reverted file (expect >=1): 1
on-disk: retirement comment present (expect 0):            0
TSC_EXIT=2
  zod-mirror-parity.test.ts(559,14): error TS2322:
    Type '"complex.zod.ts#DashboardComponentSchema"' is not assignable to type 'never'.
  dashboard-aria-retired-contract-twins.test.ts(52,11): error TS2322: ...
RESTORE -> 'aria?: {' present (expect 0): 0 · git diff HEAD --stat: [] · TSC_EXIT=0

One file's content flips the verdict with every other input fixed. The second error is independent corroboration: #5855's own guard fails on the same revert, so two guards written by different authors agree that aria's presence is the variable.

What the fix changes

  1. The ledger entry is corrected to the re-measured set, never narrowed: 'aria' | 'header' | 'widgets' | 'globalFilters' -> 'header' | 'widgets' | 'globalFilters'. Re-measuring all 163 pairs on the merged tree gives 17 drifted pairs, one key fewer, and that is the only entry that moved.
  2. The failure now names the pair. Expect< Equal< …, never > > reports only Type 'false' does not satisfy the constraint 'true' — naming neither pair nor key, which is what sent a CI failure to the compiler API to diagnose. assertionDriftMatchesLedger is now an assignment to never: identical teeth, legible failure. Verified on the live failure before correcting it, and re-proven by legs A and B, which now name their pair in the tsc message itself.
  3. The spec dependency is written down. The candidate was wrong here but the underlying property is real and worth recording: DashboardComponentSchema is spec-derived (BaseSchema.extend(SpecDashboardFields.shape)). 10 of the 163 registered pairs are built from a spec schema, and 3 of the 17 ledgered drifts are among them (DashboardComponentSchema, DashboardWidgetSchema, PageNodeSchema). For those, a spec bump can change a measured drift set with nothing in this repo changing. SPEC_DERIVED_PAIRS records the set, the assertion's docblock points at it as the first thing to check when the named pair is in it, and a test re-derives the set from the mirror sources so the list cannot rot. Ablated: dropping one entry fails that test alone (1 failed, 4 passed).

Nothing was narrowed, nothing was @ts-expect-error-ed, and CI was not re-run in hope. Both runs are reported: the clean checkout of the original SHA (green — which is what refutes the three candidates) and the full local union on the merged head (green — the table above).

Scope

Guard, its tests, exclusions, ledger, changeset. No mirror and no declaration is edited. #4605 and #5186 are regression evidence and are not re-litigated. The opposite drift direction — mirror wider than the declaration, #5853's class — is explicitly outside this construction and documented as such in NarrowerThanDeclared's docblock.

Follow-ups filed unassigned: #5927 (the 17 drifted pairs, needing a per-key ruling) and #5928 (the StyleProps name collision).


Generated by Claude Code


Generated by Claude Code

…or population

objectui#5680 built a derived parity construction for `BaseSchema` alone: read the
mirror's own `.shape` and compare each key against the TS declaration it restates, so
a widening that forgets the mirror turns red with no key list to maintain. The sibling
mirrors in `packages/types/src/zod/` had nothing equivalent, and the class already had
two confirmed instances (objectui#4605, #5186).

That one construction now covers 163 registered mirror/declaration pairs. A runtime
census reads the directory off disk and fails when an exported const is in neither the
registry nor the reasoned exclusion list, so a mirror added later cannot join the
population silently. 17 pairs carry measured drift today and are pinned to their exact
drifted key sets as a two-way ratchet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3230.7 KB 3990.2 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-BL8yIXhK.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) 10.13KB 3.77KB
app-shell (runtime-config.js) 13.57KB 4.78KB
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) 504.18KB 114.10KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 164.55KB 45.67KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
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) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.21KB 44.67KB
plugin-dashboard (index.js) 133.32KB 34.42KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 243.38KB 61.72KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.86KB 27.22KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 20.74KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 52.40KB 17.45KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
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) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
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 (index.js) 3.88KB 1.85KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (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) 3.40KB 1.71KB
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

claude added 2 commits August 24, 2026 02:08
…me the pair on failure

CI checks out the PR's MERGE with `main`, not the branch head, so the guard was
measured against a base this branch never held. objectui#5855 retired
`DashboardComponentSchema.aria` from the TS declaration as spec-tombstoned and
renderer-dead; that key therefore dropped out of the comparison and the ledger
entry recording it as drifted went stale. The two-way ratchet fired exactly as
designed — a ledgered drift corrected elsewhere must not stay recorded.

Measured again on the merged tree: 17 of 163 pairs drift, one key fewer than
before, and that is the only entry that moved.

`assertionDriftMatchesLedger` is now an assignment to `never` rather than
`Expect< Equal< …, never > >`. Identical teeth, but the failure prints the
offending pair instead of `Type 'false' does not satisfy the constraint 'true'`,
which named neither the pair nor the key and sent one CI failure to the compiler
API to diagnose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3230.7 KB 3990.2 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-BL8yIXhK.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) 10.13KB 3.77KB
app-shell (runtime-config.js) 13.57KB 4.78KB
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) 504.18KB 114.10KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 164.55KB 45.67KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
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) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.21KB 44.67KB
plugin-dashboard (index.js) 133.32KB 34.42KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 243.38KB 61.72KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.86KB 27.22KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 20.74KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 52.40KB 17.45KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
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) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
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 (index.js) 3.88KB 1.85KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (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) 3.40KB 1.71KB
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

…eads legibly

Ten of the 163 registered pairs are built FROM an @objectstack/spec schema, so one
side of their comparison moves when the spec moves — and three of the seventeen
ledgered drifts are among them (DashboardComponentSchema, DashboardWidgetSchema,
PageNodeSchema). A spec bump can therefore change a measured drift set with nothing
in this repo changing. That firing is correct — a vocabulary the spec widened or
withdrew is exactly what wants triage — but it should not read as a mystery.

SPEC_DERIVED_PAIRS writes the dependency down, the assertion's docblock points at
it as the first thing to check when the named pair is in it, and a test re-derives
the set from the mirror sources so the list cannot rot.

This was NOT the cause of the CI red it documents: the lockfile pinned
@objectstack/spec@17.2.0 across every commit involved and the merge did not touch
the lockfile. Recorded because it is a real property of the ledger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3230.7 KB 3990.2 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-BL8yIXhK.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) 10.13KB 3.77KB
app-shell (runtime-config.js) 13.57KB 4.78KB
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) 504.18KB 114.10KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 164.55KB 45.67KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
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.55KB 0.62KB
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 (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
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) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.21KB 44.67KB
plugin-dashboard (index.js) 133.32KB 34.42KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 243.38KB 61.72KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.86KB 27.22KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 20.74KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 52.40KB 17.45KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
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) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
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 (index.js) 3.88KB 1.85KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
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 (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) 3.40KB 1.71KB
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

@claude

claude Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

Green at 8c57c5118 — 19 checks, 0 failing, 0 pending. Type Check, the job that was red, is completed success.

Short version of the diagnosis (full detail, with the measurement tables, is in the PR body):

The divergence, not the error, was the thing to explain, and all three offered candidates are refuted by measurement:

  • local tree != pushed commit — a fresh worktree at the pushed SHA with pnpm install --frozen-lockfile type-checks green.
  • node_modules / @objectstack/spec drift — the installed package reports 17.2.0, and the lockfile pins 17.2.0 at the red SHA, at the merged head, and on main, with git diff --stat … -- pnpm-lock.yaml empty across the merge. With --frozen-lockfile CI's resolution is identical by construction. (My first reading of a version did come from the shared /home/user/objectui checkout and was wrong for exactly the reason flagged — every figure above is re-derived from this branch's own worktree and from git show <sha>:pnpm-lock.yaml.)
  • census order / environment dependence — the failing assertion is type-level over static literals; the runtime census cannot reach it.

What actually differed: CI builds refs/pull/N/merge, not the branch head. main advanced while the branch was open, and 18a8e7db7 retired DashboardComponentSchema.aria from the TS declaration (#5830 / #5855). Removing the key from the declaration removes it from the comparison, so a key this ledger recorded as drifted was no longer drifted — the entry went stale and the ratchet fired in its second direction. Nothing on the branch changed; the base did.

Proven by controlled experiment, environment held completely fixed (same node_modules, spec 17.2.0, tsc 6.0.3, node v22.22.2), reverting only packages/types/src/complex.ts to its pre-merge content:

'aria?: {' present in reverted file: 1   retirement comment: 0
TSC_EXIT=2  ->  Type '"complex.zod.ts#DashboardComponentSchema"' is not assignable to type 'never'
             +  dashboard-aria-retired-contract-twins.test.ts also red   <- #5855's OWN guard agrees
RESTORE -> 'aria?: {' present: 0 · git diff HEAD --stat: [] · TSC_EXIT=0

Three things changed, none of them the assertion's teeth:

  1. The ledger entry corrected to the re-measured set — 'aria' | 'header' | 'widgets' | 'globalFilters' -> 'header' | 'widgets' | 'globalFilters'. Re-measuring all 163 pairs: 17 drifted, one key fewer, and that is the only entry that moved.
  2. The failure now names the pair. Expect< Equal< …, never > > printed only Type 'false' does not satisfy the constraint 'true', which is what sent a CI red to the compiler API to diagnose. It is now an assignment to never — identical teeth, legible failure — verified on the live failure and re-proven by ablation legs A and B.
  3. The spec dependency is written down. The candidate was wrong here, but the property is real: DashboardComponentSchema is spec-derived, and 10 of 163 registered pairs are built from a spec schema, 3 of them among the 17 ledgered drifts. SPEC_DERIVED_PAIRS records the set, the assertion's docblock names it as the first thing to check, and a test re-derives it from the mirror sources so it cannot rot (ablated: dropping one entry fails that test alone).

Nothing narrowed, nothing @ts-expect-error-ed, CI not re-run in hope. Re-verified with the full local union on the merged head (type-check, 577 tests, lint 0 errors, six gates) plus the clean-checkout run. Still draft — the PM lands it.

Follow-up note: #5927's table listed aria as drifted; it is corrected there in a comment.


Generated by Claude Code


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 24, 2026 02:50
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit 3d2f6ab Aug 24, 2026
23 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5684-zod-mirror-drift-guard branch August 24, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants