fix(types): lift the flex example's direction/gap out of the inert props envelope - #7729
Merged
Merged
Conversation
…rops` envelope
`compositeExample` in `packages/types/examples/data-display-examples.json`
authored its layout configuration as `props: { direction: 'col', gap: 4 }`.
`SchemaRenderer` hoists `properties.*` onto the node and spreads `props` as
React props instead, so a renderer declared `({ schema })` — the ordinary
component-renderer shape, which `flex.tsx` has — never reads the envelope. The
example rendered with the default `row` direction and the default gap while
presenting itself as a column with `gap: 4`, and nothing complained: BaseSchema
is `.passthrough()`, so every gate accepted the spelling.
Both faces of `FlexSchema` declare the two keys at node level (`zod/layout.zod.ts`
as an enum + a number, `layout.ts` via `FlexLayoutProps`), so the fix is the lift,
not a rename of `props` to `properties`.
The pin is structural rather than acceptance-shaped for the same passthrough
reason: acceptance cannot separate "lifted" from "still under `props`, admitted
unexamined". It reads the fixture from disk, parses the node through both
`FlexSchema` and the published `LayoutSchema` union, and asserts the parsed
values are the authored ones rather than the schema defaults, plus a walk over
every `type`-carrying object in the fixture with a positive control so the zero
is a reading.
Deliberately untouched: the three counter-example sites in
`skills/objectui/rules/protocol.md` and `skills/objectui/guides/schema-expressions.md`
are marked-wrong teaching material, verified intact and left alone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
…x-props-envelope-lift
`readFixture(): any` tripped `@typescript-eslint/no-explicit-any` and AGENTS.md commandment #6. The document really is arbitrary JSON, so it is read as a `{ [key: string]: unknown }` and reached through an `objectAt` helper that refuses a non-object loudly rather than letting a renamed fixture key read as `undefined` and quietly weaken every assertion below it. No assertion changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
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-justin
marked this pull request as ready for review
September 5, 2026 12:39
This was referenced Sep 5, 2026
Merged
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 #6751
compositeExampleinpackages/types/examples/data-display-examples.jsonauthored its layout configuration inside apropsenvelope:{ "type": "flex", "id": "user-profile-card", "props": { "direction": "col", "gap": 4 } }SchemaRendererhoistsproperties.*onto the node and spreadspropsas React props instead, so a renderer declared({ schema })— the ordinary component-renderer shape, whichflex.tsxhas — never reads the envelope. The example therefore rendered with the defaultrowdirection and the default gap, while presenting itself as a column withgap: 4. Nothing complained, becauseBaseSchemais.passthrough()and every gate accepted the spelling.The fix is the lift, not a rename
Both faces of
FlexSchemadeclare the two keys at NODE level, so the envelope has no legitimate reading here andpropsis not renamed toproperties— that would add a second spelling for something the schema already declares:packages/types/src/zod/layout.zod.ts:179—direction: z.enum(['row','col','row-reverse','col-reverse']),gap: z.number()packages/types/src/layout.ts:309—FlexSchema extends BaseSchema, FlexLayoutProps, andFlexLayoutPropsdeclaresdirectionandgapNo schema is widened by this PR.
The pin is structural, not acceptance-shaped
.passthrough()is exactly why acceptance cannot close this: the broken document parsed GREEN through every schema in the package and would keep doing so. What separates the two states is the parsed VALUE, so that is what the pin asserts.FlexSchema.parse(node).direction.gap'props' in parsedpropsenvelope'row'(schema default)2(schema default)true'col'4falsepackages/types/src/__tests__/flex-props-envelope-lift-6751.test.tsreads the fixture from disk (so a fixture edit is measured, not remembered) and asserts, in six cases:FlexSchemawith the AUTHORED values, and nopropssurvives on the parsed node;LayoutSchemaunion entry point, which coversflex;typereports nopropsenvelope — zero hits;propsnode injected, reports exactly that node. Without it the zero in (3) could come from a walker that never reports anything;properties(the bag the renderer DOES hoist) nor theelement:*namespace (which readspropsby design, throughreadProps) is flagged, so the zero is aboutpropson component-renderer types specifically;id, the four children and their types, and the avatar child's own keys.The walk is scoped to this one fixture deliberately. See the fence below.
The fence: three counter-examples read, and left alone
The card names three same-shaped occurrences that are DELIBERATE teaching material and must not be "fixed". All three are intact on this branch point, and this PR edits none of them — a repo-wide "no node carries
props" assertion would have made the teaching material fail, which is why (3) above is fixture-scoped:skills/objectui/rules/protocol.md:110cardcarryingprops: { title }, still immediately followed by the measurement table at 130-137 whose row for it reads "no header element at all"skills/objectui/guides/schema-expressions.md:35card, under the comment "Evaluated, then dropped -- renders an empty card"skills/objectui/guides/schema-expressions.md:485text, under the comment "Worse — evaluated inside the envelope, then discarded: renders nothing", with its cross markThe other half of the card (
skills/objectui/guides/page-builder.md'sstatisticnode) had already landed onmainbefore this branch: that file matches"props"on 0 lines, with the control term"statistic"matching on 1 line in the same file, so the zero is a reading rather than a miss.Changeset: declared as releasing nothing
.changeset/6751-flex-props-envelope-lift.mduses the empty-frontmatter form, which is the explicit exemption rather than a workaround, andnode scripts/check-changeset-presence.mjsaccepts it on that basis. The reasoning it records:packages/types/package.jsonfilesis["dist","README.md","CHANGELOG.md","LICENSE"]—examples/is not published, and the gate agrees (it counts 3 changed files and 1 of them as published source);packages/types/tsconfig.jsonexcludes**/__tests__/**by directory, so the pin never reachesdist. Confirmed bycheck:published-tsconfig-exclude.⇒ no published behaviour changes.
Ablation — the pin can fail, proven on the shipping tree
Fix committed first, then the fixture mutated, then restored — trap-guarded, absolute paths, and verified by content rather than by an exit code. Run at
8c4d206:The direction the ablation went is the ordinary one — RED — and it goes red on three legs, two of them value assertions and one the structural walk. The three cases that stay GREEN under the mutation are the two controls and the untouched-neighbours check, which is what they are for: they do not depend on the lift.
Choosing
f96a781as the mutation source is the card's own recipe and needed no adjustment even though this branch was cut fromadb2a86: the fixture blob is byte-identical at both refs.Gates
All at
8c4d206, which is the pushed head. Exit codes captured before any pipe.pnpm exec vitest run --maxWorkers=2 packages/types/pnpm --filter @object-ui/types type-checknode scripts/check-changeset-presence.mjsnode scripts/check-changeset-fixed.mjsnode scripts/check-changeset-no-major.mjsnode scripts/check-changeset-overwrite.mjspnpm run check:control-bytespnpm run check:spec-symbolspnpm run check:unreferenced-sourcespnpm run check:published-tsconfig-excludepnpm run check:self-importHeavy runs went through the container's shared verify lock; the verdict line read
VERDICT command-exit 0each time.The type-check genuinely covers the pin, rather than merely being green beside it.
packages/types'stype-checkistsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json, and the root program excludes**/__tests__/**— so the coverage claim was measured rather than assumed:tsc -p tsconfig.test.json --listFilesOnlylists 572 program inputs, and the pin is one of them.Lint, as a declared narrowing. The repo-wide
eslint . --no-inline-configrun is CI's; this branch ran the narrowed one and offers the three readings that make the narrowing a measurement rather than a skip: (a) exactly one file in the diff is lintable at all — the other two are a.jsonfixture and a.mdchangeset; (b)eslint --format jsonon it reportsfiles linted: 1, errors: 0, warnings: 0; (c)eslint.config.jsdeclares noprojectService, noparserOptionsand noproject, so type-aware linting is not enabled and nothing in this diff can move the verdict on any file it does not touch. The first pass caught one real warning —@typescript-eslint/no-explicit-anyon the fixture reader, also a commandment #6 violation — fixed by reading the document as JSON ({ [key: string]: unknown }) through anobjectAthelper that refuses a non-object loudly, instead ofany. No assertion changed.origin/mainwas merged into this branch before the PR was opened, which brought in #7718 (#6972) — it edits the same fixture in a different region (examples.markdownlosingsanitize) and merged clean; every number above is from after that merge.Generated by Claude Code