feat(types): ComponentInput.of — the coarse member kind, with readers on day one - #8097
Merged
Merged
Conversation
… on day one
`type: 'array'` said a value was a list and stopped there, so a member that
drifted from `@objectstack/spec` was invisible to every layer that reads a
declaration. `page:header.actions` is the measured cost: the contract declares
`z.array(z.string())` ("Action IDs"), the renderer read the members as
`ActionDef` objects, and the repo-wide parity gate stayed green for the whole
life of the drift because both sides carried the key and neither could say what
was inside it.
`ComponentInput` gains an optional `of`, carrying `type`'s coarse-kind
vocabulary one level down: the elements of an `array`, or the values of an
`object` used as a map. Three readers ship in this change, which is the bar
objectui#5905 set for a new key on this interface:
- the repo-wide parity gate compares every declared `of` against the member
kind `ComponentPropsMap[type]` accepts, one-directionally, with the same
exemption discipline as its three existing directions;
- `sdui-parser`'s `validateTree` reports a member that fits no declared kind
(`member-type-mismatch`, naming the offending positions);
- the codegen narrows the generated `sdui-intrinsics.d.ts` element type.
Fifteen keys across ten blocks declare one, each derived by probing the member
position with one value of each coarse kind and declaring only where exactly one
was accepted. `of` is a KIND and never a value domain, so the 2026-08-17
expression-ceiling ruling on `ComponentInput.type` is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
os-steve
marked this pull request as ready for review
September 6, 2026 15:25
os-steve
enabled auto-merge
September 6, 2026 15:25
Contributor
✅ 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
|
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Sep 6, 2026
…ponent-input-member-shape # Conflicts: # apps/console/src/__tests__/registry-inputs-spec-parity.test.ts
Contributor
✅ 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
|
os-steve
enabled auto-merge
September 6, 2026 16:46
Contributor
✅ 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
|
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8067
Gives
ComponentInputa machine-readable member-shape slot —of— and teaches the repo-wide parity gate to compare it. Cut fromorigin/mainat3686a44cbbf66d85879b89edd7b4911ab0389162.The slot, and why this shape
ComponentInput.ofcarriestype's coarse-kind vocabulary, one level down: the ELEMENTS of anarray, or the VALUES of anobjectused as a map. One kind, or an array of them for a member contract that is a union, with exactlytype's semantics — a member passes when any declared arm accepts it.Three things decided the design:
of, notitems/element/shape. All three promise a sub-schema. This deliberately is not one: it names a KIND and stops.of: 'object'says the elements are objects, never which keys they carry. That is the axis the card's cost story lives on — specz.array(z.string())vs a renderer readingActionDefobjects — and nothing wider was needed to catch it.descriptionIS the publication face's expression ceiling today, and SPEC IS THE SOLE JUDGE OF VALUES") deferred givingComponentInputreal CONSTRAINT slots —min/max/step, two sources of truth free to drift.ofadds no value domain, so it is not that direction reopened: it extends the one axis that ruling already blesses to the one position that had no way to state it.page:header.maxVisibleis still a bare'number'arm;of: 'string'still says nothing about WHICH strings.ofis read throughinputTypeArms, the accessortype's arms already go through — no secondArray.isArraybranch to get subtly wrong on the union form.Readers on day one — the #5905 bar
Three, all in this PR:
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts) — a new MEMBER DIRECTION compares every declaredofarm against the member kindComponentPropsMap[type]actually accepts, by probing the member position and reusing the existingrefusesKindjudgement. One-directional (widening only), with the same exemption discipline as the file's three existing directions.sdui-parser'svalidateTree— a member that fits no declared kind is reported asmember-type-mismatch, one diagnostic naming every offending position. It runs only once the CONTAINER verdict cleared, so one mistake draws one report.sdui-intrinsics.d.tsnarrows the element type.page:header'sactionsisstring[]where it wasunknown[].Consumers touched — all four, and what happened to each
gen-manifest.ts→sdui.manifest.jsonof(canonicalized exactly liketype);undefinedstaysundefinedrather than going throughcanonicalizeInputType, whose no-arms fallback is'string'— otherwise every array in every manifest would claim string members. Entries withoutofserialize byte-identically.sdui-intrinsics.d.tsofto narrow the emitted element / value type.validate.ts(unknown-propwalk)offor the member check. An input with noofis validated exactly as before.page.tsx:462(JSX-page prop whitelist)manifestFromConfigs, so it inherits 1–3. Verified by the console suite (89 files, 1015 tests).Fifteen declarations, DERIVED not chosen
Every container key's member position was probed with one value of each coarse kind, and
ofwas written only where exactly one kind was accepted. That makes each declaration underivable by taste, and un-widenable by construction. Fifteen keys, ten blocks:element:record_picker.sort,object-grid.searchableFields,page:accordion.items,page:header.actions,page:tabs.items,record:activity.types,record:details.sections/.fields/.hideFields,record:path.stages,record:quick_actions.actionNames/.requiredPermissions,record:related_list.columns/.filter/.actions.Seventeen array keys whose contract is
z.array(z.unknown())accept every kind, so there is nothing to declare. One key —record:highlights.fields— accepts a member UNION (a field name or an inline field object) and is deliberately left undeclared and pinned with its reason inMULTI_KIND_MEMBER_CONTRACTS: picking one arm is the narrowing this repo leaves un-gated as noise, and picking both would advertise a shape only a per-block pin can vouch for.Ablation + paired positive control
One run, three legs, on the committed tree (
60ba53df046475d62287847732092e4b39902ba3).The mutation is the exact drift the card describes —
page:header.actionsmembers declared as objects, which is what the renderer read for the whole life of the drift. The gate names the key. No rebuild can hide the mutation:apps/consolealiases@object-ui/componentstopackages/components/src(workspaceAliasesinapps/console/vite.config.ts), so the gate reads that source file directly. The restore is proved by blob hash equalling the HEAD blob and by a zero-bytegit diff HEAD, not by an exit code; the script carries atrap ... EXIT INT TERMrestore with an absolute path.The gate's ability to red is also pinned PERMANENTLY, not only by this one-off run —
the member judge reds on the drift that started this — page:header.actions, by nameassertsspecMemberVerdict('page:header', 'actions', 'array', { id: 'clone' })isrefuses-kindwhile'Account'isaccepts, with the container-level control that the ARRAY itself is fine.Other verification
pnpm exec vitest run packages/sdui-parser/ packages/types/— 142 files, 2565 passedpnpm exec vitest run packages/plugin-detail/ packages/plugin-grid/— 244 files, 2214 passedpnpm exec vitest run packages/components/— 234 files, 2159 passedpnpm exec vitest run apps/console/— 89 files, 1015 passedtype-checkon all six changed packages — all Donecheck:control-bytesOK (6472 files) ·check:sdui-registration-pinsOK (16 registrations, 518 chunks) ·check:element-data-source-declarationOK ·check:unreferenced-sourcesOK ·check:readme-exportsOK ·check:doc-snippetsOK (556/556) ·check:doc-typesOK ·check:doc-fencesOK ·check:changeset-presenceOK ·check:changeset-no-majorOKPins this legitimately moved
The
#7493/#5905retirement census pins recorded "five writable keys" and "the serializer forwards exactly six". A sixth live key and a seventh forwarded key is what this PR adds, so those equalities were updated to nameofexplicitly (they stay EQUALITIES — the load-bearing half is that the three retired keys must not reappear). The eight Zod tombstone messages that enumerate the forwarded list gainedof, and two pins on their literal text moved with them.Scope
No change to
@objectstack/spec. The "types NARROWER than the contract" half stays un-gated (#4971) andofinherits that asymmetry unchanged. Per-block pins are not made mandatory — that is #8068, which touches the same gate file in a different section; whoever lands second mergesmain.Merged
main— re-measured on the merged baseSister card #8068 (PR #8094) landed while this was open, touching the same file.
mainwas merged in as0ff0f6ac9738ed482d6fbd5a3d30ac3f82168198(merge ofd327b9c883401fa0b841f245469b04dcaa379f5c—mainhad moved on pastc546cdfdby the time of the merge). No rebase, no amend, no force-push.The conflict, and how it was resolved. One hunk: both cards inserted a module-level block immediately before the single
describe. Both were taken in sequence — the MEMBER DIRECTION (#8067) then the MEMBER-PIN DIRECTION (#8068) — and neither side's part was refactored. Theitblocks inside thedescribeauto-merged.Two prose repairs were needed because the merge made cross-references false, and nothing else changed:
page:header.actions就是那个没被钉住的第四个 #8068's opening paragraph said "the LIMIT note at the top of this file says so in as many words: aninputsentry of typearray/objectdeclares no member shape". That note now reads the opposite half —ofcarries the member KIND. The paragraph now says "the four directions above", quotes the note as it currently reads, and states in one line thatofis a declaration rather than a pin, so it neither satisfies nor exempts anything in that direction.page:header.actions就是那个没被钉住的第四个 #8068 quotes ("publish their members in prose and are pinned by per-block tests next to their renderers") and now points at the MEMBER-PIN DIRECTION for the half that makes naming that pin mandatory.Re-measured ablation — three legs, on the merged base
The file grew from 144 to 181 tests across the merge; the branch's old 144-test figures are superseded, not carried over.
The semantic interaction, measured rather than assumed
Answer: no interaction. Neither number moved, and nothing was adjusted.
ofis a DECLARATION, not a pin, and #8068'sstructuredInputsderives its population frominputTypeArms(input.type)— the CONTAINER arms — never frominput.of. That prediction was verified two independent ways:origin/main's own copy of the file.MEMBER_PIN_EXEMPTIONSis the identical 58-key set on both sides (ledger identical: True,added by me: none,removed by me: none) andMEMBER_PIN_EXEMPTION_CEILINGis still 58.ofdeclarations from the four registration files (of:declaration count 15 → 0, all four blobs proved different from their HEAD blobs) and re-derived the population. It is 77 array/object-armed inputs with the declarations present, and 77 with all of them stripped — and the two ID sets are identical line for line, not merely the same size. All four files restored to their HEAD blob hashes, with a zero-bytegit diff HEADover the set.So #8068's fourth direction judges exactly the population it measured, against the ledger and ceiling it shipped. No exemption was deleted and the ceiling was not touched — converting those entries to pins is #8071's job.
Checks re-run on the merged head
pnpm exec vitest run packages/sdui-parser/ packages/types/— 144 files, 2660 passedpnpm exec vitest run packages/plugin-detail/ packages/plugin-grid/ apps/console/— see the run table in the report comment on registry-inputs-spec-parity 对 array/object 类 key 降级成「只比名字」——ComponentInput没有承载成员形状的槽位,page:header.actions因此漂移了整整一个契约周期 #8067type-checkon the six changed packages🤖 Generated with Claude Code
https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
Generated by Claude Code