fix(types,components,core)!: stack, flex and grid gap are each one of their renderer's steps, on both faces and the registrations; one enumeration pin for every layout spacing key (objectui#11474) - #11489
Conversation
…r renderers' steps on both faces `StackSchema.gap`, `FlexSchema.gap` (and the authored `flex` bag, which holds the member by reference) and `GridSchema.gap` were `z.number()` / `number`. Each renderer maps a closed set of steps; any other number drew no gap rule. `grid` builds an arbitrary-value class at runtime for an unmapped number, and no compiled stylesheet defines it. - zod mirror: one shared helper for "a literal union of the renderer's steps with a refusal naming them", used by `container.padding` and the three gaps. - TS face: literal unions; `StackSchema` declares its own `gap` through an `Omit` over `FlexLayoutProps`, which carries no index signature. - registrations: each `gap` input is a closed enum in `container.padding`'s object form. The renderers' branches are unchanged. - core builders: `GridBuilder.gap` / `FlexBuilder.gap` take the declared set. - one generalised enumeration pin replaces the container-only one. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
…pages; one minor changeset Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
|
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: Inputs: card objectui#11474 (body and its three comments: triage Check-runs on the head, read at 2026-10-02T17:25Z: 39 ① Derived judgmentsEach accept-set and public-surface change the diff implies, named right or wrong. The three mapped sets were re-derived by reading the renderers at the head, not taken from the report:
No derived judgment in the diff is wrong. ② Semver level
② fails on the two unnoted entries. The remedy is prose-only and frontmatter-untouched (the ③ Boundary flagsDev flags (the report's
Implemented-by: VERDICT: FAIL Generated by Claude Code |
… make false `.changeset/6151-stack-schema-omit-collapse.md` read `gap` as one `number` member declared once on `FlexLayoutProps`; `StackSchema` now declares its own literal union over an `Omit` of it. `.changeset/11441-retire-nav-responsive-grid-layout.md` said `grid` accepts a `gap` number; it now accepts one of ten steps. Each gets an append-only dated note in the house form naming objectui#11474 and the changeset that ships. Claude-Session: https://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC Co-authored-by: Claude <noreply@anthropic.com>
✅ 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: Re-review after one patch round. Inputs: card objectui#11474 (body and its three comments: triage What the patch round touched, read off the compare, not the dev's list: one commit, Check-runs on the head, read twice. At 2026-10-02T17:37:52Z: 31 ① Derived judgmentsCarried from ② Semver levelThe changeset The The two notes, re-read sentence by sentence against the diff and the head.
The sweep, in full, over all 2658 pending bodies at the head (not the gate's list, which resolves file names, not meaning, and names 16 entries, none of them the two above). Patterns read: the word ② passes. The changeset declares every narrowing the diff publishes, ③ Boundary flagsCarried from
Implemented-by: VERDICT: PASS |
Fixes #11474
Clause-②: no
Why
no(a narrowing):stack.gap,flex.gapandgrid.gapshrink from every number to each renderer's mapped set, and theflex/griddescribes stop advertising "0-8". Nothing widens. The changeset isminorand states the breaking authoring meaning, because objectui never declaresmajor(AGENTS.md section 9, version alignment).Dispatched by the
domain:spec @ objectuiseat (objectui#10217) under triage's family direction (comment5955082167) and the seat's claim (comment5956674102), sessionhttps://claude.ai/code/session_01XvhGmGAP79ZB8swnkapxPC. This is the family's last step after objectui#10286 (container.maxWidth) and objectui#11424 (container.padding).The accept-set change
50c73fed0)1db960ca)StackSchema.gapz.number()FlexSchema.gap, and the authored bag (properties.gap, the same schema object by reference)z.number(), describe "Tailwind scale 0-8"GridSchema.gapz.number(), describe "Tailwind scale 0-8"FlexLayoutProps.gap(shared byFlexSchemaand the bag typeFlexBlockNode)number@default 2keptStackSchema.gapnumber, inherited fromFlexLayoutPropsOmitofgapoverFlexLayoutProps(no index signature there, so no member is erased;stack-schema-emitted-members.test.tsmeasures the emitted declaration and stays green)GridSchema.gapnumberstack/flex/gridregistration,gapinputtype: 'number'type: 'enum',{ label, value }entries with numeric values, incontainer.padding's object formGridBuilder.gap()/FlexBuilder.gap()in@object-ui/corenumberNonNullableof the member)gap === Nbranches;grid'sGAPSmap and its runtime-built fallbackcontainer.paddingmoved onto the same shared helper (rendererSpacingStepsinlayout.zod.ts, the dispatch's suggested route: the spelling now repeats four times). Its refusal text is byte-identical to base (compared against the builtdist), and its describe is unchanged.The three refusals as stored (read from the built
distat head):gapon astackis one of 0, 1, 2, 3, 4, 5, 6, 8, 10 (objectui#11474): those are the steps the renderer maps to a gap class, and0means none. Any other number drew NO gap class at all, not even the default2, so it is refused here rather than rendered with no gap. Pick the step you meant from that set."4; it is refused here instead. ..."Measurements (the dispatch's zone 2)
H1, the three sets, derived by rendering (a throwaway probe through the real
SchemaRendererand registry at base50c73fed0, candidates 0 to 32 plus -1, 0.5, 1.5, 2.5, 9.5):stackdraws a gap class for 0 to 6, 8, 10 and none for 7, 9, 11 to 32 or any fraction.flex(flat or bag) draws one for 0 to 8 and none for 9 upward.griddrawsgap-0togap-6,gap-8,gap-10,gap-12from its map and a runtime-builtgap-[N*0.25rem]for everything else (gap-[2.25rem]for 9,gap-[-0.25rem]for -1). At base, every candidate on all four spellings parsed green onsafeValidateSchemaand onStrictAnyComponentSchema. H1 holds as the dispatch stated it.H3,
grid.gapmeasured first: closed. Instrument: both Tailwind entry sheets compiled exactly as their builds compile them (postcssplus@tailwindcss/postcss,fromset to the entry so its@sourcelines resolve):apps/console/src/index.css(482,775 bytes) andpackages/components/src/index.css, the sheet the package ships asstyle.css(158,414 bytes). Control: every mapped grid class (gap-0...gap-12),md:gap-10andgap-1.5are present in both sheets. The arbitrary-value lookup is live:max-w-[16rem]andpx-[0.3rem]are found in both by the same matcher. Reading: all 28 runtime-built classes (for 7, 9, 11, 13 to 32, -1, 0.5, 1.5, 2.5, 9.5) are absent from both. So an unmapped grid gap reaches no rule, andgrid.gapcloses like the other two. The pin re-derives this against the package's own sheet on every run. The console reading was taken once and nothing re-derives it.H2, the flex bag. Closing
FlexSchema.gapflowed into the bag and intoFlexBlockNodewith no edit there.{ type: 'flex', properties: { gap: 9 } }is refused atproperties.gapon both faces with the set in the issue.{ type: 'flex', gap: 9 }stays refused by name towardproperties.gap, as every flatflexprop already was.tscrefuses{ type: 'flex', properties: { gap: 9 } }typed asFlexBlockNode(a compile-time pin under the package'stype-check).H4, the enumeration. Derived by the new pin: every registration in
@object-ui/componentswithisContainer, and every input on it whose values are numbers. It findsaspect-ratio.ratio,container.padding,flex.gap,grid.columns,grid.smColumns,grid.mdColumns,grid.lgColumns,grid.xlColumns,grid.gapandstack.gap. Rendering classifies four as spacing keys:container.padding,flex.gap,grid.gap,stack.gap. One other open member of the family is reported, ⛔ not closed here: see "Reported, not closed" below.cardhas no numeric input, and no layout node has a responsivegapobject.H5, corpus.
git grepof everygapliteral in tracked files (CHANGELOGs and the lockfile excluded), triaged by node. No corpus document authors an unmappedgapon astack,flexorgrid. The off-set literals are inline CSS (styleobjects incontent/docs/guide/react-pages.md, an old changeset, two plugin demos,DatasetReportRenderer.tsx),DashboardConfigSchema.gap(a different schema), and a prose comment in the schema-catalog test. The catalog values (0, 1, 2, 3, 4, 6, 8) sit inside every set. The same sweep over the objectstack checkout (examples/**,packages/*/src) found inline CSS only. Re-judged by running:examples/schema-catalog(40 files, includingsafe-validate-corpus-6318.test.ts),pnpm check:doc-snippets,pnpm check:doc-examplesandpnpm check:skill-examples, all green.The pins
packages/components/src/__tests__/layout-spacing-sets-11474.test.tsxgeneralisescontainer-padding-set-11424.test.tsx, which is deleted. Its assertions are held here in general form; the absent-key control now compares with the registration's default step instead of a literal class list. It enumerates as above. A candidate counts as MAPPED when the spacing utilities its value draws are all rules in the package's compiled stylesheet. Each spacing key is then held to three things. Its declaration accepts exactly that set on the tolerant and strict faces, at the authored spelling: flat, or thepropertiesbag when the node refuses the key flat; the spelling is derived from the declaration. Its registration input is a closed enum of exactly that set. An absent key draws exactly what the registration's default step draws. An unmapped number draws no spacing rule, so nothing is rounded or clamped. The non-spacing numeric inputs are classified and held to nothing else. Lit controls: the stylesheet reader sees a variant, an escaped dot and an arbitrary value; the enumeration findscontainer.padding.packages/types/src/__tests__/layout-gap-sets-11474.test.ts: per key and per face, unmapped numbers (including a fraction and -1) are refused at the key with codeinvalid_value, the set inissue.valuesand in the message. Every mapped step parses, and an absent key parses. The flatflexspelling stays refused by name. The describes state the set and no longer say "0-8". Compile-time pins check that the TS faces refuse 7 onstack, 9 and 10 onflex(node,FlexLayoutPropsandFlexBlockNode), and 9 ongrid, and that each zod and TS pair states one set.flex-properties-bag-11276.test.tspinnedgap: '4'in the bag asinvalid_type. A literal union judges by value, so it is nowinvalid_valueat the same path. The assertion's purpose (the bag keeps the mirror's verdict, at its own path) is unchanged, so the code was updated in place.container-padding-set-11424.test.tsandcontainer.tsxonly had their pointer to the deleted file repointed.Ablations (committed implementation,
node ../objectstack/scripts/ablation-replace.mjswrap mode, trap-armed restore, each restore proven by blob equal to HEAD and an emptygit diff HEAD)stackregistration drops10from its enumstack.gap registration enum: expected [ +0, 1, 2, 3, 4, 5, 6, 8 ] to deeply equal [ +0, 1, 2, 3, 4, 5, 6, 8, 10 ]stack.tsxgains agap === 7branchstack.gap on the tolerant face: expected [ +0, 1, 2, 3, 4, 5, 6, 8, 10 ] to deeply equal [ +0, 1, 2, 3, 4, 5, 6, 7, 8, 10 ]STACK_GAP_STEPSgains 7stack.gap)grid'sGAPSmap drops10(10 then buildsgap-[2.5rem])grid.gap(1 failed, 11 passed)StackSchema.gapgains 7tsc -p tsconfig.test.jsonred@ts-expect-errorandtruenot assignable tofalsein the new types pin, plus the existingzod-mirror-parity.test.tsparity pinFirst attempts at A1 and A2 were no-ops. The tool refused both before running anything: A1 was a delete passed as an empty replacement, and A2's anchor was a substring of its replacement. Both were re-run with corrected anchors as A1b and A2b above.
Gates at head
1db960capnpm --filter '@object-ui/components^...' --filter @object-ui/components run build(9 packages), thenturbo run build --filter='./packages/*' --concurrency=2(39 tasks) for the doc gates.type-check(script name echoed):@object-ui/types0,@object-ui/core0,@object-ui/components0. The test tsconfigs include the new pins (--listFilesOnly).packages/types/334 files, 8840 passed.packages/core/193 files, 3844 passed, 27 skipped.packages/components/353 passed, 1 skipped; 3584 tests passed.examples/schema-catalog/40 files, 2259 passed.packages/sdui-parser/20 files, 289 passed. Registry-reading suites (9 files, 584 passed): consolecomponent-input-union-specimens,ga-honoured-inputs-author-reach,html-tier-manifest,public-contract,registry-inputs-spec-parity; app-shellwidget-dom-leak-sweep; layoutcontainment-declared-slot-9910; plugin-designerdesignerRegistrationInputs-11434;scripts/__tests__/check-component-surface-parity.test.ts.check:component-surface-parity,check:doc-types,check:doc-snippets(776 of 776 judged, 0 failed),check:doc-examples,check:skill-examples,check:doc-fences,check:new-line-citations,check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check-changeset-no-major.mjs,check-changeset-presence.mjs,check:i18n-designer-parity,check:i18n-keys,check:designer-field-key-parity,check:registry-bare-names,check:unreferenced-sources,check:test-path-roots.manifestFromConfigsplusvalidateTree):stackgap 7,flexgap 9,gridgap 9 andcontainerpadding 9 each answerinvalid-enumnaming the set. Controlsstack4,flex8,grid12 andcontainer8 answer nothing.eslint --format jsonover the 11 changed.ts/.tsxfiles. All 11 are linted by the root config (isPathIgnoredfalse for each), and the JSON reports 11 files with 0 errors. The 9 warnings are all on untouched lines (no-explicit-any,react-refresh). The config enables no type-aware linting, so this diff cannot move a verdict on an untouched file. The repo-widepnpm lintbelongs to CI.NOT MEASURED:
check:sdui-registration-pins. Reason: it weighsapps/console/dist/assets, which needs a full console build. It pins that the registration keys named by thesideEffectsarrays survive bundling. This diff moves nosideEffectsarray and no registration key, only an input's type. CI runs it.Reported, not closed
grid.columns(and the four flatsmColumnstoxlColumnsinputs, and the responsive object's values). This is the same family shape: declaredz.number()/number, while the renderer maps 1 to 12 through itsGRID_COLS*maps. Readings through the realSchemaRenderer:columns: 13drawsgrid-cols-1 sm:grid-cols-2, so the md count is silently dropped.columns: 0andcolumns: -1drawgrid-cols-2, a substituted value.columns: { md: 13 }drawsgrid-cols-1. All four parse green on both faces. The dispatch says report, ⛔ not close; the report names it for the seat.Acceptance notes (observations, not filed)
examples/schema-catalog/test/layout-props-conversion.test.tsxkeeps its own hand-writtenGAP_LADDERandCONTAINER_PADDINGsets. They agree with the derived sets today and could now read the declarations instead. That is unexercised drift, so it is noted here and not filed. Carrier: none.grid.tsx's gap line still carries the comment "Fallback for arbitrary values if not in map", which no longer describes a reachable authored case and never produced a compiled rule. It sits on a renderer branch line, which the claim's file surface excludes. Carrier: none.Generated by Claude Code