Commit 1bbaa16
feat(types): App/Dashboard/Page twins take the spec by reference, like their zod mirrors (#10273)
Fixes #9736
Clause-②: yes
Executes ruling `5732099575` (batch #167 item 4, letter 甲; the
maintainer agreed with 「其他同意」 at 2026-09-18T15:17Z). The three
hand-written TypeScript twins now take the spec by reference, the same
way their zod mirrors do. The exclusion list is hoisted to one `as
const` array. The mirror's `specFieldsExcept` call and the twin both
read that array. This is a types-only change: no runtime code changes
and no mirror is narrowed.
## What changed
| twin | extends | twin-only omission |
|---|---|---|
| `AppComponentSchema` (`app.ts`) | `BaseSchema`, plus Omit over spec
`App` of `(typeof APP_SPEC_EXCLUDED)[number]` | none |
| `DashboardComponentSchema` (`complex.ts`) | `BaseSchema`, plus Omit
over spec `Dashboard` of `DASHBOARD_SPEC_EXCLUDED` plus `'header'` |
`header` |
| `PageNodeSchema` (`layout.ts`) | `BaseSchema`, plus Omit over spec
`Page` of `PAGE_SPEC_EXCLUDED` plus `'slots'` plus `'assignedProfiles'`
| `slots`, `assignedProfiles` |
- **Hoist only.** In `zod/{app,complex,layout}.zod.ts`, each inline list
became `export const X_SPEC_EXCLUDED = [...] as const`, and
`specFieldsExcept` now reads that array. The members are byte-identical,
so the mirror's accept set does not change. The arrays are not
re-exported from `zod/index.zod.ts`, which uses named re-exports. The
twins read them with `import type`. `zod-mirror-parity`'s
closed-population census now lists them in `EXCLUSIONS`, because they
are key-name tuples, not schemas.
- **Twin-only omissions.** Only the compiler's collisions are omitted,
and each one has a docblock line saying why. Dashboard `header` and Page
`slots` are hand-written members that are **not assignable** to the
spec's member. For `header`, `actionUrl` is optional where the spec
requires it, `label` is `string` against `I18nLabel`, and `actionType`
is an open string. For `slots`, `SchemaNode` admits `null` and
primitives. Both are already ledgered in `KnownDrift` and
`WiderThanDeclared`. Both are withheld on the type side only, and
neither twin member was widened. The other twin-declared members
(`icon`, `branding`, `active`, `hidden`, `requiredPermissions`, and the
page's `object`, `template`, `variables`, `isDefault`, `aria`, `kind`)
are assignable to the spec's member, so they override it without an
omission. Page `assignedProfiles` is withheld the same way, for forward
compatibility: objectstack `main` retires it (`retiredKey`), so the
hand-written `string[]`, left unchanged, would fail the extends clause
under the Spec Main Shape Gate. Its retirement is objectui#9409 (on
hold). *(Added by the PM seat after commit `8e11943`.)*
- **`contextSelectors`** is in `APP_SPEC_EXCLUDED` because the mirror
re-adds it with its own element. The ruling names it among the admitted
keys, so the App twin now declares `contextSelectors` as an optional
array whose element is the `z.input` of the mirror's
`AppContextSelectorSchema`. That takes the mirror's own element by
reference, and the generic is spelled out in words here because GitHub
strips angle-bracket shapes. Without that declaration the App row could
not reach zero.
- **Ledger.** The `MirroredUndeclared` rows for
`app.zod.ts#AppComponentSchema` (20 keys),
`complex.zod.ts#DashboardComponentSchema` (11) and
`layout.zod.ts#PageNodeSchema` (10) are deleted. Each deletion leaves a
note where its row stood. `assertionMirroredUndeclaredMatchesLedger` and
`assertionMirroredUndeclaredRecordsEveryKey` still reconcile every pair,
and the census sentence moves to the derived figures. No other ledger
(`KnownDrift` / `UnmirroredDeclared` / `WiderThanDeclared`) moved.
- **Pins.** `twins-spec-by-reference-9736.test.ts`:
- For each twin, the set of spec keys minus declared keys is `never`,
with lit and negative controls.
- Admitted keys have the spec's own member type. This is checked with
`Equal`, not by assignment, because `BaseSchema`'s index signature would
make an assignment vacuous.
- `@ts-expect-error` on the tombstones App `version` and Dashboard
`refreshInterval`, plus the mirror's parse refusal on the same literal.
- The `header` and `slots` members keep their own types.
- `dashboard-aria-retired-contract-twins.test.ts` was rewritten. It
pinned that `aria` is *not declared*. After this change `aria` is
declared, but only as the spec tombstone typed `undefined`. The pin now
asserts that, and it adds the `@ts-expect-error` that its old header
said could not stick.
## Measured deviations from the dispatch's assumptions
- Tombstones surface as `?: undefined` (the `z.input` of
`z.never().optional()`), not as `?: never`. For the accept set under
`exactOptionalPropertyTypes: false` they are equivalent: authoring a
value is a compile error.
- On `@objectstack/spec` 17.4.0, `Page` carries **no** tombstone. The
Page pin therefore covers the admitted half only, and the test says so.
## Evidence
All readings below were taken on the merged head `a1cc88e32`, except
where a line says otherwise. The consumer type-check, the reverse
verification and the check scripts ran on the pre-merge head `1d24a9ef9`
plus the changeset commit. The merge brought in only the `objectql*`
files from sibling work, and the `@object-ui/types` type-check and
vitest were re-run after it.
- `pnpm --filter @object-ui/types type-check` exited 0. It chains `tsc
--noEmit`, then `tsconfig.examples.json`, then `tsconfig.test.json`.
- `pnpm exec vitest run packages/types/
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` reported
215 files and 5126 tests passed, exit 0, on the merged head.
- Consumer type-check (the downstream direction: every package whose
source names one of the three interfaces) passed for all 10 packages:
app-shell, components, core, layout, plugin-dashboard, plugin-designer,
plugin-list, react, runner and example-schema-catalog. **Zero consumer
fixes were needed.** This ran after a turbo build of their dependency
closure (32 tasks).
- Reverse verification that consumers read the rebuilt `.d.ts`: a probe
file in `plugin-dashboard/src` authored `refreshInterval: 5` on a
`DashboardComponentSchema`. `tsc` went red with `TS2322 Type 'number' is
not assignable to type 'undefined'`. The probe was then removed and `git
status` was clean.
- eslint on the 9 changed `.ts` files: 0 errors. The 14 warnings are all
pre-existing `no-explicit-any` hits on lines this diff does not touch.
- `check-changeset-presence`, `check:spec-symbols`,
`check:new-line-citations` (0 new), `check:control-bytes`,
`check:installed-pin-claims`, `check:changeset-claims`,
`check:pending-changeset-literals`, `check:published-dist`,
`check:unreferenced-sources` and `check:published-tsconfig-exclude` all
exited 0.
## Changeset: minor
`.changeset/9736-twins-spec-by-reference.md` declares `@object-ui/types`
as `minor`. The published type surface gains members. One authoring
spelling (a tombstone key) moves from a parse-time refusal to a
compile-time refusal, and this repo marks breaking semantics `minor`,
never `major`.
## Acceptance notes
- `check:changeset-claims` (report-only) lists 39 pending changesets
that name files this diff touches. One of them now reads false:
`.changeset/dashboard-aria-member-retired-5830.md` says `BaseSchema`'s
index signature means an existing `aria:` line still compiles. After
this PR it does not compile. That text is another change's pending
declaration and is left untouched here. This PR's changeset states the
new behaviour explicitly. Carrier: the release seat, or whoever next
edits that changeset.
- Serial siblings objectui#10084 and objectui#10066 and PR
objectui#10260 touch disjoint files. `origin/main` was merged before
this PR opened, and the gates above re-ran on the merged head.
Session: `https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S`
---
_Generated by [Claude
Code](https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 1dbb993 commit 1bbaa16
10 files changed
Lines changed: 411 additions & 99 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
Lines changed: 27 additions & 19 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
35 | 37 | | |
36 | 38 | | |
37 | 39 | | |
| |||
43 | 45 | | |
44 | 46 | | |
45 | 47 | | |
46 | | - | |
47 | | - | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
56 | 57 | | |
57 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
58 | 66 | | |
59 | 67 | | |
60 | 68 | | |
| |||
Lines changed: 146 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
0 commit comments