feat(types): App/Dashboard/Page twins take the spec by reference, like their zod mirrors - #10273
Conversation
…like their mirrors Each hand-written TypeScript twin now extends Omit over the spec's exported input type (App / Dashboard / Page) using the SAME as-const exclusion array its zod mirror's specFieldsExcept call reads (APP_SPEC_EXCLUDED, DASHBOARD_SPEC_EXCLUDED, PAGE_SPEC_EXCLUDED), so the published validator and the published type project one spec surface. Dashboard header and Page slots are withheld on the type side only (hand-written members not assignable to the spec's; already ledgered drift). contextSelectors is declared on the App twin over the mirror's own element schema. The MirroredUndeclared rows for the three pairs go to zero; spec tombstones now surface as optional members typed undefined, so authoring one is a compile error. Types-only: no runtime change, no mirror narrowed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S
…ns-spec-by-reference
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsChecked independently in a detached worktree at this head, against the pinned
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…forward-compat with spec main) objectstack main retires PageSchema.assignedProfiles (retiredKey, input type undefined), so the hand-written string[] member made PageNodeSchema fail its extends clause under the Spec Main Shape Gate (TS2430). The key is now omitted beside the shared list on the TypeScript face only; the member stays string[]. Its retirement is objectui#9409's decision. Pinned in the 9736 test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S
…ns-spec-by-reference
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S
✅ Console Performance Budget
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
Size Limits
|
|
CI blocker, not this PR's:
Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgmentsDelta over the PASS at
② Semver level
③ Boundary flagsNone new. There are no changes to Implemented-by: VERDICT: PASS Generated by Claude Code |
… navigation item type objectstack#19789 widened the spec's navigation item union with `doc`, and `NAV_TYPE_META` is a Record keyed by that spec-derived union, so objectui stopped compiling against @objectstack/spec built from objectstack main (the Spec Main Shape Gate, TS2741 on NavigationDesigner). - `doc` entry: `appDesigner.navTypeDoc`, `bg-blue-100 text-blue-700`, `BookOpen`. - The map is typed `Record<NavigationItemType | 'doc', ...>`: the pinned spec 17.4.0 has no `doc`, so a plain `doc:` key is an excess property there. The `| 'doc'` goes at the pin bump that ships `doc`. - `appDesigner.navTypeDoc` fallback in useDesignerTranslation and all ten locale packs. - Guard test: every discriminant of the installed spec's NavigationItemSchema renders a row with a resolved type badge (spec subset of map, not equality). - `doc` is deliberately NOT in QUICK_ADD_TYPES: an empty doc item fails the spec's book-or-doc requirement; authoring it is objectui#10188. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC (cherry picked from commit 6f970da)
✅ Console Performance Budget
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
Size Limits
|
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 oneas constarray. The mirror'sspecFieldsExceptcall and the twin both read that array. This is a types-only change: no runtime code changes and no mirror is narrowed.What changed
AppComponentSchema(app.ts)BaseSchema, plus Omit over specAppof(typeof APP_SPEC_EXCLUDED)[number]DashboardComponentSchema(complex.ts)BaseSchema, plus Omit over specDashboardofDASHBOARD_SPEC_EXCLUDEDplus'header'headerPageNodeSchema(layout.ts)BaseSchema, plus Omit over specPageofPAGE_SPEC_EXCLUDEDplus'slots'plus'assignedProfiles'slots,assignedProfileszod/{app,complex,layout}.zod.ts, each inline list becameexport const X_SPEC_EXCLUDED = [...] as const, andspecFieldsExceptnow reads that array. The members are byte-identical, so the mirror's accept set does not change. The arrays are not re-exported fromzod/index.zod.ts, which uses named re-exports. The twins read them withimport type.zod-mirror-parity's closed-population census now lists them inEXCLUSIONS, because they are key-name tuples, not schemas.headerand Pageslotsare hand-written members that are not assignable to the spec's member. Forheader,actionUrlis optional where the spec requires it,labelisstringagainstI18nLabel, andactionTypeis an open string. Forslots,SchemaNodeadmitsnulland primitives. Both are already ledgered inKnownDriftandWiderThanDeclared. 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'sobject,template,variables,isDefault,aria,kind) are assignable to the spec's member, so they override it without an omission. PageassignedProfilesis withheld the same way, for forward compatibility: objectstackmainretires it (retiredKey), so the hand-writtenstring[], 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 commit8e11943.)contextSelectorsis inAPP_SPEC_EXCLUDEDbecause the mirror re-adds it with its own element. The ruling names it among the admitted keys, so the App twin now declarescontextSelectorsas an optional array whose element is thez.inputof the mirror'sAppContextSelectorSchema. 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.MirroredUndeclaredrows forapp.zod.ts#AppComponentSchema(20 keys),complex.zod.ts#DashboardComponentSchema(11) andlayout.zod.ts#PageNodeSchema(10) are deleted. Each deletion leaves a note where its row stood.assertionMirroredUndeclaredMatchesLedgerandassertionMirroredUndeclaredRecordsEveryKeystill reconcile every pair, and the census sentence moves to the derived figures. No other ledger (KnownDrift/UnmirroredDeclared/WiderThanDeclared) moved.twins-spec-by-reference-9736.test.ts:never, with lit and negative controls.Equal, not by assignment, becauseBaseSchema's index signature would make an assignment vacuous.@ts-expect-erroron the tombstones Appversionand DashboardrefreshInterval, plus the mirror's parse refusal on the same literal.headerandslotsmembers keep their own types.dashboard-aria-retired-contract-twins.test.tswas rewritten. It pinned thatariais not declared. After this changeariais declared, but only as the spec tombstone typedundefined. The pin now asserts that, and it adds the@ts-expect-errorthat its old header said could not stick.Measured deviations from the dispatch's assumptions
?: undefined(thez.inputofz.never().optional()), not as?: never. For the accept set underexactOptionalPropertyTypes: falsethey are equivalent: authoring a value is a compile error.@objectstack/spec17.4.0,Pagecarries 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 head1d24a9ef9plus the changeset commit. The merge brought in only theobjectql*files from sibling work, and the@object-ui/typestype-check and vitest were re-run after it.pnpm --filter @object-ui/types type-checkexited 0. It chainstsc --noEmit, thentsconfig.examples.json, thentsconfig.test.json.pnpm exec vitest run packages/types/ apps/console/src/__tests__/registry-inputs-spec-parity.test.tsreported 215 files and 5126 tests passed, exit 0, on the merged head..d.ts: a probe file inplugin-dashboard/srcauthoredrefreshInterval: 5on aDashboardComponentSchema.tscwent red withTS2322 Type 'number' is not assignable to type 'undefined'. The probe was then removed andgit statuswas clean..tsfiles: 0 errors. The 14 warnings are all pre-existingno-explicit-anyhits 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-sourcesandcheck:published-tsconfig-excludeall exited 0.Changeset: minor
.changeset/9736-twins-spec-by-reference.mddeclares@object-ui/typesasminor. 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 semanticsminor, nevermajor.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.mdsaysBaseSchema's index signature means an existingaria: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.origin/mainwas merged before this PR opened, and the gates above re-ran on the merged head.Session:
https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3SGenerated by Claude Code