Repository navigation
fix(types): the flex bag's child list is typed as nodes on the TypeScript face (objectui#11564) - #11594
Conversation
…ript face (objectui#11564) `FlexBlockNode` read its bag's `children` off `FLEX_BAG_CHILDREN`, whose list arm is `z.array(z.unknown())`, so the list, the form a `flex` node is authored in, took any entry on the TypeScript face while a single child was judged as a node. The zod faces need the `unknown`: the spec page walk judges each list entry there, once, at its real path, on both faces. `FlexBlockNode` now reads the bag's `children` off the flat `FlexSchema` mirror's own member, by reference (`SchemaNode | SchemaNode[]`, the slot every node slot takes); every other member is still the arm's. The zod arm does not move. The divergence is recorded in the mirror-parity ledger's `EXCLUSIONS` row for `FlexBlockSchema` and pinned in `flex-bag-children-list-11564.test.ts`. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
… is typed as nodes (objectui#11564) The comment said the bag's child list is `unknown[]`, which is no longer true. The nested `flex` node keeps its `satisfies FlexBlockNode`: as a list entry it also matches the post-hoist `FlexSchema`, whose index signature admits any `properties` until objectui#8347. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…t is checked as nodes (objectui#11564) The comment said the bag's child list is `unknown[]`, which is no longer true. The code is unchanged: the nested `flex` keeps `satisfies FlexBlockNode`, because as a list entry it also matches the post-hoist `FlexSchema`, whose index signature admits any `properties` until that signature is removed. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ 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: PR #11594 (card #11564), head ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
Gates on the head, as read at 2026-10-04T04:36Z: 31 check-runs ② Semver level
③ Boundary flagsDev report
Open question (the 11468 entry's sentence " Out-of-scope findings (two, carrier none), each answered:
Nothing escalated to the maintainer. Ordering holds: objectui#8347 stays Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11564
Clause-②: yes
On the TypeScript face, the
flexbag's child LIST (properties.children: [ … ]) wasunknown[]. That made it the one node slot theBaseSchemaindex-signature removal (objectui#8347) would have left unchecked.FlexBlockNodenow types it as nodes. This is the card's option B, the seat's choice under the director's delegation (claim5976113446). The zod arm does not move. objectui#8347 is not addressed here and remains open; it lands after this.The premise, verified first (both zod faces)
Before any edit, the probe went through each face's published validator (
dist/zod/index.zod.js):safeValidateSchema(tolerant) andStrictAnyComponentSchema(strict), atbdc9049ed.{ type: 'text', contnt: 'x' }unrecognized_keysatproperties.children.0typeinvalid_unionatproperties.children.0.typeelement:textbag misspellingproperties.children.0.properties…(missingcontent,unrecognized_keysforcontnt)typeproperties.children.0.typetypeproperties.children.0.properties.children.0.typeunrecognized_keysatproperties.childrentypeinvalid_unionatproperties.children(SchemaNodeSchema)null, boolean)So the walk judges each list entry once, at its real path, on both faces, as the
FLEX_BAG_CHILDRENdocblock says. The single child is judged bySchemaNodeSchema.flex-properties-bag-11276.test.tsalready pins this on both faces ("judged once, at its real path"), so the new pin does not repeat it.What changes
packages/types/src/authoring-nodes.ts:FlexBlockNodereads a newFlexBlockShape. That isFlexBlockSchema's closed shape with ONE member replaced: the bag'schildrenis the flatFlexSchemamirror's ownchildrenmember, by reference (SchemaNode | SchemaNode[], which is also whatFlexLayoutPropsdeclares). Every other member of the node and of the bag is still the arm's. It is still an interface (the objectui#11466 cycle), and it declares nothing of its own. If the arm'spropertiesstops being an optional zod object, the shape becomesneverand everyflexliteral stops compiling; it does not silently lose the bag.packages/types/src/__tests__/flex-bag-children-list-11564.test.ts(new, permanent):Equalrows: the bag'schildrenequalsSchemaNode | SchemaNode[] | undefined, the mirror's member, andFlexLayoutProps['children'].unknown[], plus rows showing it is the ONLY member that differs, on the node and in the bag.@ts-expect-errorrows: the f02 shape with a closed node (element:textbag misspelling) refused inside the list (with the arm-typed reading as a lit control), an undeclaredtype, a nested array, and an untyped object.flex).zod-mirror-parity.test.tskeys every divergence ledger (KnownDrift,WiderThanDeclared,WIDER_ARMS, …) by REGISTERED pair, andFlexBlockSchemais not one: it sits inEXCLUSIONS, because its TS face is derived. The divergence is therefore recorded in thatEXCLUSIONSrow's reason, with its comment pointing at the pin that re-derives it. (EXCLUSIONSreasons are only checked for presence and length.).changeset/11564-flex-bag-children-list.md:@object-ui/typesminor, the narrowing, the migration, and what does not move.packages/types/examples/dashboard.tsand thepackages/types/README.mdcomposition example both said the list isunknown[]. Each keeps itssatisfies FlexBlockNode, for the reason measured as probe A below.The dispatch's hypotheses, measured
SchemaNodefor both arms. That is the slot type objectui#11466 gave every node slot (BaseSchema.children, the zodNodeSlotZodType), and the single-child arm already read it (the input ofSchemaNodeSchema).DeclaredNode | stringis the PROP's root type and is not used: a child slot admits primitives on both zod faces.SchemaNodeadmitsstring | number | boolean | null | undefined, and so do both zod faces. What B refuses today is narrower, and only whereFlexBlockNodeitself is the target type: an undeclared or absenttype, a nested array, a closed node's misspelling (element:textand the other spec-derived nodes), and a value statically wider thanSchemaNode. At aSchemaNode/DeclaredNodeslot B is inert today, measured with tsc. A nested-flexbag misspelling, an undeclaredtypein the list, and a nested array all compile there (probes A, B, C), because the post-hoistFlexSchemaarm's index signature absorbsproperties. Census, by TypeScript AST over the tracked files:.ts/.tsxfilesflextype: 'flex'object literalsproperties.children: FlexBlockNode, both greenauthoring-nodes-bag-carriers-11468.test.ts,examples/dashboard.ts)safeValidateSchema(unknown),toEqual, zod fixtures)examples/schema-catalogJSON files withtype: 'flex'are typed as anExamplewhose schema isunknown(the type argument is spelled in words here because GitHub strips angle-bracket shapes), so no node type checks them; they are judged at runtime.FlexBlockNode, and no skill fence changes.error TS, atc5140c1. Zero sites turned red, so no consumer fix was needed. No site puts a non-node in the list on purpose:renderChildrenhands a nested array toSchemaRendererwhole.EXCLUSIONSreason.The card's acceptance pin: f02 under the P1 mutation (one-off, not committed)
For P1,
BaseSchema's[key: string]: any;was removed throughablation-replace.mjs(WRAP mode, anchor hit 1 to 0, blobc0d9ad447f8dto86201e15c7b2). Each mutated tree was restored and proved: blob back to the HEAD blob,git diff HEADempty, status clean. The probe is a scratch file with one verdict per line, read off tsc.c5140c1, signature presentbdc9049ed+ P1text+contnt)DeclaredNodeelement:textbag misspelling in the listThe first mutated run was a no-op. Its replacement text was a substring of the anchor, so the tool's count check refused before running the probe, and it restored and proved both files. The rerun used a marker replacement (0 to 1) and is the run cited above.
Reverse verification
The new pin file was compiled against BASE's
authoring-nodes.ts, in a detached comparison worktree, so this tree was never mutated. It went red with 7 errors: the threeEqualrows (TS2344) and four now-unused@ts-expect-error(TS2578, on the f02 closed-node row, undeclaredtype, nested array and untyped rows). Against HEAD it gives exit 0. The direction is red, as expected.Gates
pnpm --filter @object-ui/types type-check9107906pnpm --filter @object-ui/types test9107906turbo run type-check(TURBO_FORCE, 8 programs above)c5140c1schema-builder.test.ts+ 3 schema-catalog flex test filesc5140c1check:doc-snippets9107906check:doc-examples9107906@examplecompiles, or fails exactly as its ledger row declares."check:doc-types9107906check:skill-examples9107906check:readme-exports9107906check:spec-symbols9107906check:changeset-claims(report-only)9107906check:pending-changeset-literals9107906check:control-bytes9107906changeset:check9107906majorcheck-changeset-presence9107906type-check:coverage9107906check:new-line-citations9107906eslint .form)9107906Between
c5140c1and9107906onlypackages/types/README.mdchanged (git diff --stat), and no tsconfig program reads it.On the eslint row, the narrowing is a measurement: the 4 files are the diff's TS files, read from
--format json, andeslint.config.jsdeclares noparserOptions.projectorprojectService, so linting is not type-aware and cannot move a verdict in an untouched file. Repo-wide lint is CI's.Acceptance notes
.changeset/11468-bag-carrier-node-types.md, callsFlexBlockNode"theFlexBlockSchemaarm's input". After this change one bag member is read off the flat mirror instead. This release's 11564 entry says so, so the CHANGELOG reads coherently. Left unedited; the alternative is a dated note in the style of that entry's objectui#11440 note.flex, and thepage:containers through the same walk), and the renderer cannot draw it as a list. No producer of that shape was measured, and the render outcome is NOT MEASURED. Observation only; carrier: none.ablation-replace.mjsrefuses a replacement that occurs inside its own anchor (the replacement count cannot rise). That failure is loud and safe, and a marker replacement avoids it. Observation; carrier: none.The draft is the dev's terminal deliverable. Flipping it to ready and queueing it are the seat's actions.
Generated by Claude Code