docs(types): drop the shared @default 'row' on FlexLayoutProps.direction and state both consumers in prose - #8230
Conversation
…ction and state both consumers in prose `FlexLayoutProps.direction` is declared once (objectui#6151) so that `FlexSchema` and `StackSchema` name it from both sides, but its two consumers deliberately diverge on what they apply when the key is omitted: `flex.tsx` reads `schema.direction || 'row'`, and `stack.tsx` reads `schema.direction || 'col'` under its own `// Default to column for Stack`. The single published `@default 'row'` was therefore correct for `flex` and wrong for `stack` — whose registration agrees with its renderer (`defaultProps.direction: 'col'`), not with the tag. The renderers are the authority — they are what runs — so nothing but the docblock moved. The remedy is the one objectui#7361 already landed two members down on this same interface for `align`: remove the tag rather than pick a second conditionally-true value, and state the per-type values in prose so no parser reads a value only one consumer applies. The prose form is copied from `align`'s so two sibling members on one interface do not read as two different situations. The criterion is DIVERGENCE, not sharedness. `justify` is shared by the same two consumers and both read `|| 'start'`, so its tag is correct and stays. That control is now derived off disk in the pin instead of asserted from memory, so it turns red if those two reads ever part company. Row 3 of the objectui#7361 census lands in objectui#7361's own pin (`layout-default-jsdoc-7361.test.ts`) rather than a second file: every expected value is extracted from the renderer source, so the pin fails if either side moves. Card objectui#7734. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114Ytxr5sM1vdW19Y9WAx6E
Seat review — accepted. The control was upgraded, not just used
⭐ The best thing here is what happened to the
|
✅ 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
|
…Props docblock The sentence naming `justify` as the positive control — that a shared member carries an `@default` only when both consumers apply the same value — was sitting on `direction`'s member docblock. It is a claim about this interface, not about `direction`: on the interface docblock it reaches the reader of every member instead of one, and it restores the parity between `align` and `direction`, which are meant to read as two instances of one situation rather than as two situations. Re-voiced for its new home so it covers all three shared members at once: `align` and `direction` diverge and state their per-type values in prose, `justify` keeps its tag because `flex.tsx` and `stack.tsx` both read `|| 'start'`. Prose only. No assertion in `layout-default-jsdoc-7361.test.ts` was touched or loosened: the interface docblock sits ahead of `export interface`, which is where `interfaceBody()` starts its slice, so no member-docblock lookup can see the moved text. Card objectui#7734. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0114Ytxr5sM1vdW19Y9WAx6E
|
✅ 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
|
Addendum on
|
Fixes #7734
FlexLayoutProps.directionis declared once soFlexSchemaandStackSchemacan name it from both sides (objectui#6151), but its two consumers deliberately diverge on what they apply when the key is omitted. Every anchor below was re-located by content onorigin/main580b0fdf4— the card measured onb74a859and triage on0558e0f, so their line numbers were stale:flex.tsxschema.direction || 'row':16stack.tsxschema.direction || 'col':22, under its own// Default to column for Stackat:21The single published
@default 'row'was therefore correct forflexand wrong forstack— whose registration agrees with its renderer (defaultProps.direction: 'col',stack.tsx:130) and not with the tag.The remedy was already on this interface, two members down
objectui#7361 landed as
e546222b3(#7736) and settled the identical case foralign. It did not correct the value — it removed the tag and stated the per-type values in prose, on the stated ground that "one tag on a shared member cannot be right for both … the per-type values are stated in prose so no parser reads a value that is only conditionally true."This PR copies that prose form structurally, deliberately: two sibling members on one interface written in two different styles would read as though they were two different situations. After the second commit the two member docblocks are parallel again — same shape, same voice, neither carrying anything the other lacks.
directionis the sharper instance of the same defect, and the docblock says so.align's tag said'center', which neither renderer applies, so it could be dismissed as merely a wrong value.direction's'row'is whatflexgenuinely applies — the defect is that a shared member's tag is only conditionally true, not that its value is wrong.The control that keeps this from over-generalising
justifyis shared by the same two consumers on the same interface, and both read|| 'start'(flex.tsx:17,stack.tsx:23). Re-verified here on580b0fdf4: unchanged since triage measured, no drift. Its@default 'start'is correct and stays.The criterion is a divergent shared member must not carry a tag, not "shared members must not carry tags."
That criterion is stated once, on the
FlexLayoutPropsinterface docblock — it is a claim about this interface, so on the interface docblock it reaches the reader of every member rather than one member's reader, and it keepsalignanddirectionreading as two instances of one situation. It names all three shared members:alignanddirectiondiverge and state their per-type values in prose,justifykeeps its tag because both consumers agree.The same control also exists mechanically in the pin, where it was a literal
toEqual(["'start'"]). This PR derives it off disk instead, so it turns red if those two reads ever part company — at which pointjustifystops being a control and becomes a fourth instance of this defect.Where the pin lands, and why not a new file
Row 3 of objectui#7361's census goes into objectui#7361's own pin,
packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts, rather than a second card-numbered file. Same argument as the prose: rows 1-3 are one census, and the helpers that read both sides off disk already live there. Every expected value is extracted from the renderer source by a regex guarded by an explicit positive control, so the pin fails if either side moves.No pin assertion was touched or loosened by the relocation. The interface docblock sits ahead of
export interface FlexLayoutProps, which is exactly whereinterfaceBody()starts its slice, so no member-docblock lookup can see the moved text. All 15 assertions pass unchanged.Verification — every reading on the final commit
4317adc82pnpm --filter '@object-ui/types^...' build@object-ui/typeshas no internal workspace deps (@objectstack/spec,zodonly). pnpm answeredERR_PNPM_RECURSIVE_RUN_NO_SCRIPT None of the selected packages has a "build" script, which is the measurement, not a failurepnpm --filter @object-ui/types build✓ dist completeness: 1 package(s) complete (124 emitted files verified)pnpm exec vitest run --project unit packages/types/src/__tests__/Test Files 136 passed (136)·Tests 2561 passed (2561)--reporter=verboseTests 15 passed (15), including all fourrow 3 — FlexLayoutProps.directionassertions and the rewrittenFlexLayoutProps.justify keeps its tag — its two consumers AGREEpnpm --filter @object-ui/types type-checktsc --noEmit+tsconfig.examples.json+tsconfig.test.json)pnpm --filter @object-ui/types linterr 0 warn 0)check:control-bytespnpm check:control-bytes✅ check-control-bytes: OK (scanned 6549 tracked text file(s); skipped 85 binary).node scripts/check-changeset-presence.mjs✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)pnpm changeset:check✅ All workspace packages are in the changeset fixed group.·✅ No changeset declares a 'major' bump.pnpm lint:coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).Every verdict above is the line the gate printed itself; exit codes were captured before any pipe. The whole union was re-run on
4317adc82after the relocation commit — nothing here is carried over from the first commit's run.Coverage was measured, not assumed.
type-checkruns threetscinvocations, and only one of them contains the test file — so "typecheck is clean" could have been true while saying nothing about the new assertions. Counted with--listFiles:src/layout.tstsc --noEmittsc -p tsconfig.test.jsontsc -p tsconfig.examples.jsonBoth edited files are covered, via the test project.
Reverse verification — two legs, direction predicted before running
The pin claims it turns red if either side moves, so both sides were mutated. Predicted first: leg A ⇒ 2 red, leg B ⇒ 1 red. Re-run in full on
4317adc82, because leg A's mutation target is the very docblock the relocation commit edited.@default 'row'back into thedirectiondocblockgrep -c= 1; blob hash differs from theHEADblobTests 2 failed | 13 passed— exactlypublishes NO single-value @default block tagandno longer publishes the value only ONE of the two consumers appliesstack.tsxonto flex's fallback (|| 'col'→|| 'row')grep -c= 1, removed textgrep -c= 0Tests 1 failed | 14 passed— exactlythe two consumers really do divergeBoth legs matched the prediction. No rebuild was needed and none is claimed: this pin reads both sides as source text with
readFileSync— no module resolution, nodist— so the stale-artifact hazard that makes an unbuilt ablation silently green does not apply here.Each leg restored with
git checkout HEAD -- ABSOLUTE_PATH(absolute paths,HEADnamed so the mutated index cannot be the source), and the restore was proven, not assumed: both files'git hash-objectcompared byte-identical to theirHEADblob hashes, andgit diff HEADwas empty. A control run on the restored tree returnedTests 15 passed (15).Changeset:
@object-ui/types: patch— measured, not guessedpackages/typespublishesdist(files: ["dist", …], not private), andtsccarries JSDoc into the emitted declaration. After building:The published
.d.tsbytes change, so this ships something a consumer sees in their editor and every type-doc generator reads. It is a release, not an internal-only change — so a realpatch, not an empty-frontmatter "no release" declaration. This matches the precedent exactly: objectui#7361 shipped.changeset/layout-default-jsdoc-7361.mddeclaring'@object-ui/types': patchfor the same class of change, and that changeset is still pending release.Deliberately not touched
packages/types/src/zod/layout.zod.ts— the mirror's.default('lg')still contradicts the JSDocmaxWidth: 'xl'that objectui#7361 landed. That contradiction is objectui#7735's reason for existing (needs-user-decision, p2). Aligning it here would be a published runtime behaviour change smuggled into a documentation card. My own view, for objectui#7735 rather than for here: the mirror is the runtime face and the JSDoc is not, so "align them" is the wrong frame — that card should rule on which face is authoritative first, because moving the mirror to'xl'changes what every existing container without an explicitmaxWidthrenders as.flex.tsx/stack.tsx— evidence, not edit targets. Not one line, other than the transient leg-B ablation mutation above, which was restored and proven restored.@defaulttags inlayout.ts— they declare a default no renderer applies as a fallback at all, so they are not comparable rather than wrong. Different question, needs its own card. The block-tag census onlayout.tswent 23 → 22: exactly one tag removed.align's docblock — untouched throughout, including by the relocation commit.Finding filed
objectui#8229 —
flex.tsxdeclares two different defaults foralignin the same file:defaultProps.align: 'center'(:132) against its own renderer fallbackschema.align || 'start'(:18). This is where the tag objectui#7361 retired most likely came from, and it is the third face that card did not reconcile. Bounded census in the issue: one disagreement out of eight comparable renderer-vs-defaultPropspairs across both files. Unassigned, unlabelled, left for triage; not acted on here.Authored in Claude Code session
session_0114Ytxr5sM1vdW19Y9WAx6E.