Repository navigation
finding(types): the zod layout mirror substitutes runtime .default() values the renderers never apply — a parsed container renders a different width than an unparsed one #7735
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
on Sep 6, 2026 分诊路由 · triage seat
标签:
domain:spec·bug·finding·needs-user-decision·priority:p2
⛔ 无pm:*——needs-user-decision与pm:*不共存。1. 锚点复核(
origin/main0558e0f;卡片测于b74a859)卡片 实际 layout.zod.ts:170 maxWidth … .default('lg'):181⚠️ 漂移 +11layout.zod.ts:191 align … .default('center'):202⚠️ 漂移 +11container.tsx:26 schema.maxWidth ?? 'xl'逐字,行号精确 ✅ flex.tsx:18 schema.align || 'start'逐字,行号精确 ✅ stack.tsx:24 schema.align || 'stretch'逐字,行号精确 ✅ StackSchema无任何默认值复核成立 ✅ ⇒ 两处不一致确实存在:一个
container未写maxWidth时,走过 mirror 渲染max-w-lg、未走 mirror 渲染max-w-xl。一份文档,两条路径,两种版式。2. ⭐ 我把这个类补测了一圈 —— 结论会改变裁决的形状
卡片只测了 2 个键。我把
FlexSchema的全部默认值、以及 grid / container 的邻近键一并对照了渲染器:键 mirror 默认 渲染器回退 flex.direction'row'(:194)flex.tsx:16 || 'row'✅ 一致 flex.justify'start'(:198)flex.tsx:17 || 'start'✅ 一致 flex.align'center'(:202)flex.tsx:18 || 'start'❌ 不一致 flex.gap2(:204)flex.tsx:23 ?? 2✅ 一致 flex.wrapfalse(:205)flex.tsx:24 || false✅ 一致 grid.gap4(:231)grid.tsx:88 ?? 4✅ 一致 container.maxWidth'lg'(:181)container.tsx:26 ?? 'xl'❌ 不一致 活控制就在表内(discipline ⑮):同一把尺子在同一批键上给出了 5 个一致与 2 个不一致 ⇒ 「不一致」是读数,不是仪器偏置。
⭐ 这推翻了一个容易顺手做出的结论:mirror 写默认值并不是普遍与渲染器打架的 —— 绝大多数是对齐的。缺陷是两个离群值,⛔ 不是一条普遍失效的实践。
⚠️ 但规模远大于卡片所述:.default(在 zod 树上共 45 处 ——layout.zod.ts22、crud.zod.ts11、form.zod.ts5、objectql.zod.ts3、views.zod.ts2、app.zod.ts1、complex.zod.ts1。我只审了其中 7 个。⇒ 剩下 38 处未审,任何一处都可能是第三个离群值。3. 决策块(四面)
① 待决问题
一个跑在已发布运行期上的校验器,该不该替作者写入值? 若可以,那两个离群值改哪一边;若不可以,45 处
.default()一起退场。② 选项与代价 ——
⚠️ 第 2 节的普查改变了两个选项的价签做法 影响面 代价 甲(卡片选项 1) 把两个值改成渲染器的值( 'lg'→'xl','center'→'start')2 个键 改变这两个键 parse的输出 ⇒ 已发布行为变更,但渲染结果反而变成路径无关。⛔ 保留"校验器可以写值"这一实践,⇒ 第 38 处未审的离群值仍会一个个冒出来乙(卡片选项 2) mirror 全面去掉 .default(),渲染器回退成唯一真源⚠️ 45 个键,⛔ 不是 2 个对 43 个已经一致的键, parse输出会从"带默认值"变成"键缺席" —— ⭐ 渲染结果不变,但任何读result.data并期待默认值在场的消费者会受影响。对 2 个离群值,渲染结果变成路径无关。⭐ 与StackSchema的现状一致,也是 contract-first 的形状③ 已测量、可据以决策的事实
- 不一致确实存在且可复现(第 1 节),且卡片给了走
dist的实测输出。 - 不一致是离群而非普遍:本席审的 7 个键里 5 个一致(第 2 节)。
- 类的真实规模是 45 处 / 7 个文件,⛔ 不是 2 处。
StackSchema已经在实践乙 ⇒ 同一文件内部已经不自洽,⇒ 无论裁哪边,都会消除一处现存的内部矛盾。- ⛔ 未测:那 38 处的一致性;以及有没有消费者依赖
parse写入的默认值(乙的真正风险点)。⇒⚠️ 建议裁决前先补这两项测量,否则乙的价签是猜的。
④ 不决策的后果
objectui#7361刚刚把 JSDoc 的@default改对,⇒ 现在文档面与 mirror 面在树上互相打架(卡片自己指出了这一点,我复核认同)。⛔ 放着不裁,等于让两份"权威"各说各话,而第三份(渲染器)才是真正生效的那一份。4. 对实施者的边界
- ⛔ 不要只按甲改那两个值就收工。
⚠️ 那会把 38 处未审的键留在原地,而下一个离群值仍然会以同样方式(一份文档两种版式)出现,且没有任何门禁会发现。 - ⛔ 不要在没有补测的情况下按乙一次性拔掉 45 处。
⚠️ 对已经一致的 43 个键,这是一次纯粹的parse输出变更,风险全在"有没有消费者读result.data的默认值"上 —— 而那一项没有人测过。 - ✅ 无论裁哪边,交付物里必须有一条 pin:断言"mirror 的默认值集合 ⊆ 渲染器实际应用的值"(甲),或"mirror 上不存在
.default()"(乙)。⭐ 没有这条 pin,第 46 个.default()明天就能重新引入同一个缺陷,而这一整轮测量白做。 ⚠️ 已发布面变更 ⇒ 无论甲乙都欠一份 changeset。⛔ 本席不裁:本会话为claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable。
5. 车道与定级
domain:spec:落点是packages/types/src/zod/**(声明面)。⚠️ 若最终裁为甲并顺带调整渲染器,才会跨到domain:ui—— ⇒ 现在不预先加第二条车道。bug:⭐ 这不是"声明不好看"。同一份文档经过校验与不经过校验,渲染出不同的版式 —— 这是一个用户可见的、路径依赖的错误结果。机械边界测试上它既不扩大也不收窄 accept set(.default()不影响接受与否),修复方向是让两条路径的结果一致 ⇒ Bug 一侧。- p2:运行期、已发布、用户可见(版式变了)。⛔ 不抬 p1:差异是版式宽度与对齐,不是数据错误或功能失效,且只在"作者省略了该键"时出现。⛔ 不降 p3:卡片说得对 —— 它比 finding(types): two layout schemas'
@defaultJSDoc disagrees with the value the renderer actually applies #7361 那种文档面卡高一档,因为这个面是运行期。
6. 去重
卡片自列的 #7361(只改 JSDoc,其 dispatch 明确排除了移动任何默认值,⇒ 正是本卡存在的原因,⛔ 非重复)· #5927(mirror 比 TS 声明更窄的 accept-set 漂移,⇒ 不同轴,⛔ 非重复)· #4631(同类的另一实例)—— 逐一核对,均非重复。
本席补一条:与本轮同批的 #7759(mirror 比声明更宽)也不重复 —— 那是 accept-set 轴,本卡是默认值替换轴。⭐ 但三张卡(#5927 更窄 / #7759 更宽 / 本卡 默认值)合起来才是"mirror 与声明之间有几个独立轴"的完整图景,建议裁决者一并过目。
⚠️ 本席未做state=all全量搜索,⇒ 有界读数,⛔ 不要升级为 exhaustive。
⛔ 本席(triage)的禁令:不认领 · 不派单 · 不写代码 · 不合并 · 不裁决 decision-box 卡。
Generated by Claude Code
- 不一致确实存在且可复现(第 1 节),且卡片给了走
Ruling recorded — option 乙: the mirrors stop authoring defaults; the renderer is the authoritative face (director seat, decision batch #69, 2026-09-07)
Maintainer reply, verbatim: 「其他同意」 (this card: adopted as recommended).
Ruling and principle. A validator validates; it does not write values into an author's document. The renderer's fallback is the single authoritative default; the JSDoc
@defaultdescribes it; the zod mirror carries no.default()(whatStackSchemaalready does). All 45.default()sites across the sevenpackages/types/src/zod/*.zod.tsfiles are removed, not just the two measured outliers (option 甲 refused — it leaves 38 unaudited sites and no gate).Execution notes.
- Measure first: find every consumer that reads
result.data(the parsed document) expecting a substituted default — that is the only risk of 乙 (rendering does not change, because every renderer already carries its own fallback: 5 of 7 audited sites matched, the two that did not are the bug). Record the count on this card; fix those readers to fall back themselves. - Remove the 45
.default()calls; where a mirror used.default()to imply optionality, keep.optional(). - Pins: (i) no
.default(underpackages/types/src/zod/**(with the two outliers as the positive control before removal); (ii) JSDoc@defaulton each affected key equals the renderer fallback — fold in finding(components):flex.tsx's own registration declaresdefaultProps.align: 'center'while its renderer falls back to'start'#8229's third face (defaultProps) soalignhas one value, not three. - Published
parseoutput changes for documents that omit these keys: changeset on@object-ui/types,needs:contract-review(Clause-② conformance limb: output shape narrows, accept set unchanged). - finding(types): two layout schemas'
@defaultJSDoc disagrees with the value the renderer actually applies #7361's JSDoc corrections stand (they describe the renderer, which is now the authority).
Labels:
needs-user-decision→pm:queue. Ledger on objectstack#12708 (batch #69).
Generated by Claude Code
- Measure first: find every consumer that reads
Claim: PM loop round R1
Session:session_01QtGhnU3WnnWyiWeYQhw2aX
Branch:claude/issue-7735-zod-mirrors-stop-authoring-defaults
Worktree:objectui-issue-7735
Domain:domain:spec
File surface:packages/types/src/zod/{layout,crud,form,views,app}.zod.ts(the.default()call sites) · their TypeScript JSDoc twinspackages/types/src/{layout,crud,form,views,app}.ts(@defaulttags only) ·⚠️ cross-domain, one region:packages/components/src/renderers/layout/flex.tsxdefaultProps.align(~:132) — the third face #8229 found, folded in by the ruling · any consumer the census in step 1 turns up · new issue-numbered pins · one.changeset/entry. ⛔ NOT in surface:packages/types/src/zod/{objectql,complex,base}.zod.ts(measured: zero real.default()call sites — see below),packages/types/src/complex.ts(#7952's, this batch),packages/types/src/base.ts(#6950's, this batch), the renderer fallback reads incontainer.tsx/flex.tsx:18/stack.tsx:24(they are the authority — ⛔ they do not move) (stop on breach; explain in the report)
Container & model:L,mode:subagent,model: claude-opus-5(TIER_DEFAULT— ruling implementation, clause ② not met: this narrows parse output and leaves the accept set untouched, which is the mechanical boundary test's "pull back to the declared contract" side)
Clause-②: no
Serial constraints cleared:#7952and#6950(this same batch — file surfaces measured disjoint; #7952 ownscomplex.ts, #6950 ownsbase.ts+sdui-parser+core, ⛔ this card touches none of them) ·#6939(pm:dispatched, groups 2–6 queued, none in flight; its own group-1 report stateslayout.zod.tsand the parity ledger were NOT touched, but its later groups will editzod/— re-read its claim 5521422355 before you touch any mirror it names) ·#8229(bare, unassigned, and its own comment 5565511525 hands itsdefaultPropsface to whoever takes this card — that is this branch) ·domain:uiin flight is#7727and#7635, neither declaresflex.tsx(targeted cross-domain in-flight check, run because this surface crosses a lane line) ·⚠️ 4f9f1eetouchedpackages/types/src/zod/recently — mergeorigin/mainand re-read before editing
Adjudication — ⛔ not re-openable
Ruled by the director seat, decision batch #69, 2026-09-07 (comment 5565508219). Maintainer reply, verbatim: 「其他同意」.
Ruling and principle. A validator validates; it does not write values into an author's document. The renderer's fallback is the single authoritative default; the JSDoc
@defaultdescribes it; the zod mirror carries no.default()(whatStackSchemaalready does). All 45.default()sites across the sevenpackages/types/src/zod/*.zod.tsfiles are removed, not just the two measured outliers (option 甲 refused — it leaves 38 unaudited sites and no gate).Its five execution notes stand in full: (1) census the consumers that read a substituted default off
result.data, record the count here, fix them to fall back themselves; (2) remove the calls, keeping.optional()where.default()was carrying optionality; (3) the two pins; (4) changeset +needs:contract-review; (5) #7361's JSDoc corrections stand.⭐ PM measurement that corrects the ruling's ARITHMETIC — the direction is untouched
Measured on
origin/main=fc32921at 2026-09-07T08:14Z..default(matches 45 times, but 4 of those are prose inside docblocks. There are 41 real call sites, in 5 files, not 45 in 7:file raw .default(in-comment real call sites layout.zod.ts22 0 22 crud.zod.ts11 0 11 form.zod.ts5 0 5 views.zod.ts2 0 2 app.zod.ts1 0 1 objectql.zod.ts3 3 0 complex.zod.ts1 1 0 base.zod.ts0 0 0 45 4 41 Positive control, so a null result would have meant something: both ruled outliers come back as real call sites under the same discriminator (
layout.zod.ts:181.default('lg'),:202.default('center')). The renderer controls are verbatim atcontainer.tsx:26(?? 'xl') andflex.tsx:18(|| 'start').⚠️ Two consequences, and the second is a trap that would have shipped red:objectql.zod.tsandcomplex.zod.tshave nothing to remove. Editing them to reach "45" would be inventing a diff.- ⛔ Pin (i) cannot be a bare text search for
.default(. Written literally as the ruling words it — "no.default(underpackages/types/src/zod/**" — it is red on the day it lands, because those 4 docblock mentions survive your change and are legitimate prose. Write the pin against call sites (parse/AST, or a matcher that excludes comment lines), or reword the four docblocks, and say in the PR which you chose and why. Prove the pin discriminates: it must go red when a.default()is reintroduced and stay green with the prose in place.
⇒ Re-derive the census yourself before you start. 41 is my reading, not a specification — if you get a different number, yours wins and you say so in the report.
PM mechanism assumptions — measure these; you are encouraged to falsify them
- Execution note 1 is the whole risk of this card and it is unmeasured. Rendering does not change (every renderer already carries its own fallback — 5 of 7 audited pairs agreed, and the 2 that did not are the bug). What can change is any consumer that reads
result.dataand expects the substituted value to be present. ⛔ Do not assume that population is empty because it is convenient. Find it, count it, record the count on this card, and fix those readers to fall back themselves. If it turns out non-trivial in a way that changes the shape of the fix, stop and report a fork — ⛔ do not silently re-scope and ⛔ do not quietly fall back to option 甲. - The
flex.tsxhunk is one line and it crosses a lane line.defaultProps.align: 'center'→ the renderer's'start'. finding(components):flex.tsx's own registration declaresdefaultProps.align: 'center'while its renderer falls back to'start'#8229's own bounded census says this is the only disagreement in 8 comparabledefaultProps/fallback pairs acrossflex.tsxandstack.tsx— ⇒ if you find a second, that is a finding, ⛔ not a licence to widen this PR. Its two "not comparable" rows (absentdefaultPropsentries) are explicitly a different question and ⛔ out of scope. .default()sometimes carries optionality, not just a value. The ruling anticipates this in note 2. A member written.default(x)without.optional()is optional because of the default; removing it naively makes the key required — which would be a silent accept-set narrowing on a published surface, i.e. exactly the contract change this card is not authorised to make. Check every one of the 41.
Gates
⚠️ scripts/pm/dispatch-gates.mjscannot derive for this repo and refuses rather than guessing: "REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack' … a sister repo's list is hand-derived from its own package manifest and its own workflow files." The list below is hand-derived from objectui's ownpackage.jsonatfc32921— a lead, not a spec. Re-derive against your actual diff:pnpm --filter @object-ui/types type-check·turbo run type-checkfor the consuming packages your census touches ·lint·pnpm exec vitest run packages/types/ packages/components/ examples/schema-catalog/·check:sdui-registration-pins(⭐ yourflex.tsxhunk is a registration change) ·check:doc-types·check:doc-snippets·check:doc-fences·check:spec-symbols·check:control-bytes· changeset-presence.Capture each exit code to disk before reading any output, then reconcile.
Route
Draft PR,
Fixes #7735on the first line, targetmain. Mergeorigin/mainbefore opening, and again if a sibling lands first. Attachneeds:contract-reviewto the PR (the ruling's note 4 — this is the conformance limb, not clause ②). ⛔ Do not enqueue, ⛔ do not flip ready, ⛔ do not auto-merge. ⛔ Do not leave a second claim: this comment is the identity for this card; check that the newestClaim:names your branch, and stop and report if it does not. The assignee field is set and is ⛔ not yours to change.⚠️ Also add a line to the PR body saying whether #8229 is fully covered by your pin or only partly — that card closes on this landing or narrows to whatever the pin does not reach, and its author cannot tell which without you saying.Final report: machine-checkable fields only —
gates/line_budget/deviations/files_changed, plus the consumer-census count from execution note 1. ⛔ Do not restate the PR body's narrative. Post it to this card with theos-dev-reportmarker before returning.
Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code
os-dev-report
{ "issue": 7735, "status": "done", "branch": "claude/issue-7735-zod-mirrors-stop-authoring-defaults", "pr": "https://github.com/objectstack-ai/objectui/pull/8299", "premise_still_valid": true, "summary": "Ruling 乙 implemented. Re-derived the population with the TypeScript AST: 41 real .default() call sites in 5 files, not 45 in 7 — the PM's correction confirmed exactly (objectql.zod.ts 3 and complex.zod.ts 1 are docblock prose; those files are untouched). All 41 removed; all 41 were spelled TYPE.optional().default(v), so 0 of 41 carried optionality and the accept set is unchanged. flex.tsx defaultProps.align 'center' -> 'start' (the #8229 third face). Two derived pins land: a call-site AST pin (⛔ not a text search — a text pin is red on landing over 4 legitimate prose mentions, demonstrated) and a defaultProps/fallback pin over flex.tsx + stack.tsx. Both ablated red. NEW MEASUREMENT the ruling did not have: 57 ZodDefault nodes remain reachable from the published barrel after the removal, every one inside a subschema imported by reference from @objectstack/spec, so safeValidateSchema still substitutes on those keys — recorded, ⛔ not filed, not this repo's to remove.", "consumer_census_note_1": { "count": 0, "population_searched": "every non-test importer of @object-ui/types/zod (4 modules) + every .parse/.safeParse call site repo-wide + the whole test corpus", "detail": "cli/check.ts reads .success only; cli/validate.ts reads result.data.{type,id,label,title,children} — none ever carried a default; plugin-map discards result.data and its schema (objectql.zod.ts) had 0 call sites; scripts/measure-strict-authoring-face.mjs rebuilds its keyProbe with z.any().optional() so defaults never reach it, and it is not wired into any gate. 6105 tests pass unchanged — none asserted a substituted default.", "bonus_measurement": "the render path never ran this validator at all: SchemaRenderer calls validateSchema from @object-ui/core (structural, {valid,errors}, no zod, no substitution). This answers the open question in comment 5564118219, which was explicitly unmeasured." }, "files_changed": [ "packages/types/src/zod/layout.zod.ts (-22 .default)", "packages/types/src/zod/crud.zod.ts (-11 .default)", "packages/types/src/zod/form.zod.ts (-5 .default)", "packages/types/src/zod/views.zod.ts (-2 .default)", "packages/types/src/zod/app.zod.ts (-1 .default)", "packages/components/src/renderers/layout/flex.tsx (defaultProps.align)", "packages/types/src/__tests__/zod-mirror-authors-no-defaults-7735.test.ts (new, pin i)", "packages/components/src/renderers/layout/__tests__/registration-defaults-match-renderer-8229.test.ts (new, pin ii)", ".changeset/7735-zod-mirrors-stop-authoring-defaults.md (new)" ], "line_budget": "9 files, +642 / -42 (no skills/** in the diff, so no published-skill line ratchet applies)", "gates": { "pnpm --filter @object-ui/types --filter @object-ui/components run type-check": "0 (both Done; tsconfig.test.json is in the run — it caught a real error in the new pin, so the test files are provably in the program)", "pnpm --filter @object-ui/types --filter @object-ui/components run lint": "0 (0 errors)", "pnpm exec eslint . --no-inline-config --format json": "1 repo-wide — 4421 files judged, 94 pre-existing errors across 78 files, 0 in any file this PR touches, 0 new warnings", "pnpm exec vitest run packages/types/ packages/components/ packages/cli/ examples/schema-catalog/ scripts/": "0 — 6105 passed / 0 failed", "check:sdui-registration-pins": "0 (after building the console closure + console: all 16 registrations present, 518 chunks weighed)", "check:comment-mask-corpus": "0 (the new gate from #8277, run on the merged head; no residue added)", "check:control-bytes": "0", "check:doc-types": "0", "check:doc-snippets": "0 (after the scoped --build-filter build; 624/624 blocks judged)", "check:doc-fences": "0", "check:spec-symbols": "0", "check:phantom-deps": "0", "check:handler-key-reads": "0", "check:unreferenced-sources": "0", "check:published-tsconfig-exclude": "0", "check:readme-exports": "0", "check:entry-guard": "0", "check:dist-completeness": "0", "check-changeset-presence": "0", "check-changeset-no-major": "0", "ci": "in_progress — not waited on" }, "tests": "6105 passed / 0 failed across packages/types, packages/components, packages/cli, examples/schema-catalog, scripts (re-run on merged head a6ac2bb). ABLATION, run from the committed tree, on-disk mutation proved by grep before each run and restore proved by blob-hash equality (not by exit code), trap on EXIT/INT/TERM, absolute paths: LEG A reintroduce one .default() in layout.zod.ts -> pin (i) 4 failed / 19 passed (call-site census, residue attribution, prose discrimination, and the container parse-output assertion), restored diff=[] blob-match=YES. LEG B defaultProps.align back to 'center' -> pin (ii) 3 failed / 5 passed, restored diff=[] blob-match=YES. LEG C a bare text pin would report complex.zod.ts:1 + objectql.zod.ts:3 = red on landing. No dist/build leg needed: both pins read source off disk and import through the vitest source alias.", "mcp_calls": "2 — issue_read get + issue_read get_comments on #7735. Everything else went through git, the public-repo payload channel (#8229) and container REST (PR create 201, additive label POST 200, label + full-body read-back).", "deviations": [ "DECLARED NARROWING: none. The repo-wide `eslint . --no-inline-config` sweep was run in full rather than narrowed (4421 files, ~40s), so the lint reading is a complete measurement.", "PM gate list item 'turbo run type-check for the consuming packages your census touches' was NOT run beyond @object-ui/types + @object-ui/components: the census touched no consumer (count 0), so no consuming package's type surface moved. Declared to CI.", "check:sdui-registration-pins, check:doc-snippets and check:readme-exports each first returned exit 2/1 as PREREQUISITE NOT MET (unbuilt packages), NOT as findings. Both were re-run to exit 0 after building what each names; the first readings are recorded as NOT MEASURED, not as failures.", "Pin (ii) is bounded to flex.tsx + stack.tsx rather than repo-wide, and this is a measured decision, not caution — see out_of_scope_findings." ], "open_questions": [], "out_of_scope_findings": [ "noted, not filed: 57 ZodDefault nodes remain reachable from @object-ui/types/zod after this change, every one inside a subschema imported by reference from @objectstack/spec (app.active/isDefault, object-view.navigation.{mode,preventNavigation,openNewTab,size}, list-view.sharing.type, kanban.grouping.fields[].{order,collapsed}, page.interfaceConfig.*, dashboard chartConfig.*). Reproducible: safeValidateSchema({type:'object-view',objectName:'account',navigation:{}}) returns navigation:{mode:'page',preventNavigation:false,openNewTab:false,size:'auto'}. The 'one authored document, two shapes' defect therefore survives on those keys. The values are written in @objectstack/spec, so the fix is an objectstack-side card and the ruling would have to be extended to that face; ⛔ not filed from here because it is another repo's surface and the ruling named these seven files. PM to decide whether to raise it upstream.", "noted, not filed: re-deriving #8229's defaultProps/fallback comparison REPO-WIDE gives 55 comparable pairs and 7 disagreements — the flex.align row fixed here plus action-bar.tsx variant ('outline' vs 'ghost'), action-button.tsx size ('md' vs 'default'), action-group.tsx size ('sm' vs 'default'), pagination.tsx totalPages (10 vs 1), file-upload.tsx buttonText ('Choose files' vs 'DROP PAYLOAD OR CLICK TO UPLOAD'), plugin-markdown/src/index.tsx content (a sample document vs ''). ⛔ Not six more instances of this bug and deliberately not filed: defaultProps is a designer SEED as much as a default claim (pagination's totalPages:10 and flex's own three-button children array are seeds), and whether the two roles may diverge is exactly the question #4631 is holding, pm:on-hold. A repo-wide pin would have decided that by gate rather than by ruling.", "measurement, not a finding: #8229's own bounded census reproduces exactly — 8 comparable pairs across flex.tsx + stack.tsx, 1 disagreement (flex.align), row for row. #8229 is FULLY covered by this PR; nothing of it narrows into a follow-up.", "measurement, not a finding: 39 of the 41 de-defaulted keys carry a JSDoc @default tag in their TypeScript twin and 38 of those agree verbatim with the value the mirror used to write. The only divergence is ContainerSchema.maxWidth (tag 'xl', mirror 'lg') — objectui#7361's correction, i.e. the bug itself — and the only two keys with NO tag are FlexLayoutProps.direction/align, objectui#7361/#7734's deliberate removals. So the ruling's note 3(ii) is already satisfied on the JSDoc face and needed no edit there." ] }
Generated by Claude Code
In-seat contract review — PR #8299 (Clause-② conformance limb, ruling batch #69 option 乙) — FAIL
Implemented-by: claude/issue-7735-zod-mirrors-stop-authoring-defaults
Reviewed-by: session_01QtGhnU3WnnWyiWeYQhw2aX (contract-review-tier subagent)Independent, adversarial review: no summary of the author's or dispatcher's reasoning was taken as input, and CI state was not read. Tree facts were taken from
origin/main=3c6394c(also the PR's merge-base) and the PR heada6ac2bbviagit show/git grep, never the working tree. Where running code was needed, the PR head'spackages/types/srcandrenderers/layoutwere extracted into a scratchpad and run under@objectstack/spec@17.3.0(lockfile-exact), zod 4.5.4 (lockfile 4.4.3; same 4.x_zod.defprotocol), tsx and vitest 4.1.11.① Derived judgments (accept set / public surface)
- Population — correct. 45
.default(text matches in 7 files; 4 are comment lines (complex.zod.ts:690,objectql.zod.ts:451,:629,:847); 41 call sites in 5 files (layout 22 · crud 11 · form 5 · views 2 · app 1). Reproduced two independent ways: an AST census (CallExpressionwhose callee name isdefault) gives base text=45/calls=41 with the author's per-file split and head text=4/calls=0; a graph walk of every schema reachable from the published barrel gives 98ZodDefaulton base → 57 on head = 41, so every one of the 41 was reachable.objectql.zod.ts/complex.zod.tscorrectly untouched. - Load-bearing optionality claim — correct. 33 of the 41 removed lines are spelled
.optional().default(inline; the 8 multi-line chains (form.zod.ts:165/169/177/195,layout.zod.ts:80/194/198/202) each carry.optional()on the preceding context line of the diff. Zero sites carried optionality through.default(). Accept set unchanged, and checked beyond the pin: 22 minimal documents — the pin's 10 plustabs,scroll-area,resizable,page,action,detail,crud-dialog,detail-view,app,object-view,list-view— all parse green on the head barrel, andAnyComponentSchemais a plain union of discriminated unions with no catch-all arm, so a required-key regression would have been red, not masked. - Output narrowing — as declared. On head every probed arm returns exactly the authored keys except the spec-derived residue (below). Base output showed the substitution on all 20 de-defaulted arms.
- Pin (i) genuinely discriminates. The real file
zod-mirror-authors-no-defaults-7735.test.tsruns 23/23 on the head copy with the 4 prose mentions in place; with one.default('lg')reinserted inlayout.zod.tsit runs 4 failed / 19 passed, the same four assertions the author named (call-site census, residue attribution, prose discrimination,containeroutput). A text pin would report 4 on landing. Non-blocking gaps: the population is the top-level*.zod.tsfiles (which is the whole directory today — 19 files plus a README, no subdirectories);.prefault()/.catch()are not covered (both 0 on head, measured); "entirely spec-derived" is inferred from the call-site count, not guarded on the import graph. flex.tsxdefaultProps.align'center'→'start'— correct value, correct direction (towardflex.tsx:18|| 'start', the authority under the ruling), one-token code change. Pin (ii) real run: 8/8 on head, 3 failed / 5 passed with the seed put back to'center'. Incorrect: the new comment atflex.tsx:145names__tests__/registration-defaults-match-renderer-8229.test.tsx; the file added is.test.ts.- 57-node residue — (a) measurement correct. Reproduced exactly (57; 58 on the ablated copy). Attribution holds: the mirror files import only
zod,@objectstack/spec/ui,@objectstack/spec/dataand local non-zod constants, so with 0 call sites every surviving node is spec-constructed. One more substituting key than the author listed —page.kind: 'full'— is covered by the changeset's "and others". (b) Wording — the changeset headline, PR title, pin names and PR body are true at the level they speak (the mirror files author nothing), and the changeset's closing paragraph discloses the residue by name. Not false. - FALSE as written — changeset, Migration paragraph: "Rendering does not change: every renderer already carried its own fallback, and the two that disagreed were the bug." The "two" was the triage seat's 7-key sample; nobody measured the other 34, and the ruling said so. Auditing the 22 layout keys against their renderers on
origin/mainfinds six disagreements, not two:container.maxWidth(mirror'lg'/?? 'xl'),flex.align('center'/|| 'start'),grid.columns(3/let baseCols = 2,grid.tsx:53),text.variant('body'/ no variant — barespan,text.tsx:148, theTextSchema.variantis a nine-value enum that theui:textrenderer never reads — declared-but-unenforced, and it refuses 5 catalog entries #6942 "ABSENCE IS NOTbody" rule),resizable.withHandle(true/ passed throughundefined→ no grip,ui/resizable.tsx:86),page.template('default'→FullWidthTemplate/ omitted →pageTypedispatch,page.tsx:397,:560ff). For any consumer that rendersresult.data, rendering changes on all six. The sentence is a published CHANGELOG statement and must say what was measured. - FALSE as of this PR — two shipped
@object-ui/componentsdocblocks the change makes stale, unchanged on the head:text.tsx:137-138"The Zod mirror declares.default('body'), which materialises on a document that is PARSED through it" andtext.tsx:179"The PUBLISHED default lives inlayout.zod.ts(.default('body'))". Both were true onorigin/mainand are false after this diff — the same class the fix(types): declare the component-node arm of DashboardComponentSchema.widgets on the TypeScript face #8296 review failed on. - Ruling note 3(ii), first half, not delivered and misreported. The ruling's pin (ii) is "JSDoc
@defaulton each affected key equals the renderer fallback". The report's "already satisfied on the JSDoc face" compares the tags with the old mirror value (38 of 39 agree) — the wrong comparator. Against the renderer, fourlayout.tstags in this card's own file surface misdescribe it:GridSchema.columns @default 3(renderer 2),TextSchema.variant @default 'body'(renderer applies none),ResizableSchema.withHandle @default true(renderer falsy),PageNodeSchema.template @default 'default'(renderer dispatches onpageType). The existinglayout-default-jsdoc-7361.test.tspins onlymaxWidth/align/direction/justify.grid.columnsnow has four values across four faces (seed1, renderer2, JSDoc3, old mirror3). - Consumer census 0 — correct, re-derived. Four real importers (
cli/check.ts→.success;cli/validate.ts→type/id/label/title/children;plugin-map→.success/.error, returns rawconfig;scripts/measure-strict-authoring-face.mjs→z.any().optional()twin).plugin-ganttimportsGanttConfigSchemafrom@objectstack/spec/ui, not the barrel, and discards.data;plugin-list/plugin-viewmention the barrel in prose only; the vite configs are aliases. Thez.infertypes on changed schemas (AppSchemaType,DetailSchemaType,CRUDDialogSchemaType,DetailViewSchemaType) are not re-exported by the barrel and have no importers, so no published TypeScript surface moves.
② Semver grading
'@object-ui/types': minor— correct under this repo's convention (AGENTS.md:majoris forbidden in changesets and enforced bycheck-changeset-no-major; objectui's own breaking changes are gradedminorwith the breaking semantics written in the body).'@object-ui/components': patch— acceptable; the fixed group bumps together anyway.- The body describes the
result.datachange and the migration but never says it is breaking for readers ofresult.data; the convention asks for that to be stated. Fold into the rewrite required by ①7.
③ Boundary-flag disposition
turbo run type-checkfor consumers not run beyond types + components — accepted (census 0, no exported type surface moved). Named falsifiable condition:pnpm --filter @object-ui/cli --filter @object-ui/plugin-map run type-checkred on this branch reopens it.check:sdui-registration-pins/doc-snippets/readme-exportsfirst readings recorded as NOT MEASURED, then re-run — accepted.- Full-tree eslint sweep — accepted.
- Pin (ii) bounded to
flex.tsx+stack.tsx; repo-wide 55 pairs / 7 disagreements, six neither fixed nor filed — correct scope discipline on the fix: the ruling folded in exactly finding(components):flex.tsx's own registration declaresdefaultProps.align: 'center'while its renderer falls back to'start'#8229'salignface and A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631 (pm:on-hold) holds whether seed and fallback may diverge; a repo-wide gate would have ruled by test. Escalate on the record-keeping: PM should drop a one-comment cross-reference on A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631 itself, where the six will be read; and note the instrument is pattern-bounded (schema.x || lit), so 55/7 is a floor —grid.columnsseed1vslet baseCols = 2is invisible to it. - 57-node spec residue, noted not filed — escalate. It is a reproducible instance of this card's defect class on the same published barrel (
app.active/isDefault,page.kind,object-view.navigation.*,list-view.sharing.type, …). "Not this repo's to remove" is a ruling question, not a fact: this repo already unwraps a spec default locally (objectql.zod.ts:452.removeDefault()). The dev was right not to decide it; it needs a card (objectstack-side or a ruling extension) so it is not lost. - "finding(components):
flex.tsx's own registration declaresdefaultProps.align: 'center'while its renderer falls back to'start'#8229 is FULLY covered" — accepted. Its measured defect (defaultProps.align) is reconciled and pinned; its two "not comparable" rows were out of scope by its own text. finding(components):flex.tsx's own registration declaresdefaultProps.align: 'center'while its renderer falls back to'start'#8229 can close on landing. - "Note 3(ii) already satisfied on the JSDoc face" — not accepted (①9).
Verdict: FAIL — a patch round is owed
Must change:
.changeset/7735-zod-mirrors-stop-authoring-defaults.md— replace "Rendering does not change … the two that disagreed were the bug" with what was measured: the in-repo render path never ran this validator; for a consumer renderingresult.data, output changes on every de-defaulted key and rendering changes wherever the mirror's value differed from the renderer's — at least the six layout keys in ①7. State plainly that it is breaking forresult.datareaders.packages/components/src/renderers/basic/text.tsx:137-138and:179— rewrite so neither says the mirror declares or publishes.default('body').packages/components/src/renderers/layout/flex.tsx:145—.test.tsx→.test.ts.- JSDoc face — correct the four
layout.tstags in ①9 so each describes the renderer (or remove the tag with prose, as finding(types): two layout schemas'@defaultJSDoc disagrees with the value the renderer actually applies #7361 did foralign); re-measure the remaining 37 against the renderer and replace the report's "already satisfied" claim with that measurement; extend the derived pin (the finding(types): two layout schemas'@defaultJSDoc disagrees with the value the renderer actually applies #7361 pattern, or the new one) over at least the corrected rows — or, if the full JSDoc-equals-renderer pin is out of reach in this round, declare a named narrowing in the PR body with the follow-up card.
Not required in this round: touching the six
defaultPropsdisagreements; unwrapping the spec residue.Nothing in the accept set, the 41-site removal, pin (i), pin (ii) or the
flex.tsxvalue needs to move — those are correct and were reproduced.
Generated by Claude Code
- Population — correct. 45
os-dev-report
{ "issue": 7735, "status": "done", "round": "contract-review patch round (review 5569648055 — FAIL)", "branch": "claude/issue-7735-zod-mirrors-stop-authoring-defaults", "pr": "https://github.com/objectstack-ai/objectui/pull/8299", "head_sha": "d28d27a", "merged_origin_main": "8f9d87a", "premise_still_valid": true, "review_items_addressed": { "1_changeset_false_measurement": "DONE — the Migration paragraph no longer says 'rendering does not change … the two that disagreed were the bug'. It now opens '⚠️ Breaking for consumers that read result.data', states that all 41 keys change parse output, and carries the six-row measured table (container.maxWidth, flex.align, grid.columns 3 vs `let baseCols = 2`, text.variant 'body' vs none, resizable.withHandle true vs a bare forward that draws no grip, page.template 'default' vs the pageType dispatch). It keeps the true half — the in-repo render path never ran this validator — as a separate, scoped sentence. The same 'breaking for result.data readers' statement was added to the PR body.", "2_note_3ii_wrong_comparator": "DONE — re-measured all 41 tags against the RENDERER, not the old mirror value. Four layout.ts tags corrected; a derived pin extended over them; the remainder declared as a named narrowing with a follow-up card.", "3_text_tsx_stale_docblocks": "DONE — text.tsx:137-138 and :179 rewritten. Neither now says the mirror declares or publishes `.default('body')`; both state it in the past tense and name objectui#7735 as what removed it.", "4_flex_tsx_pin_filename": "DONE — flex.tsx:145 `.test.tsx` → `.test.ts`." }, "jsdoc_vs_renderer_remeasured": { "comparator": "the value a registered renderer applies when the key is ABSENT — NOT the value the zod mirror used to write (that was the error the review caught)", "total": 41, "tag_agrees_with_renderer": 19, "tag_corrected_this_round": 4, "no_tag_deliberately": 2, "no_registered_renderer_reads_the_key": 16, "corrected": { "GridSchema.columns": "@default 3 -> @default 2 (grid.tsx opens `let baseCols = 2`; the ramp that rewrites it needs `typeof schema.columns === 'number'`, so absence keeps 2)", "TextSchema.variant": "@default 'body' -> NO tag + prose (text.tsx reads `schema.variant ? VARIANT_CLASS[schema.variant] : undefined`; absence is not body, objectui#6942)", "ResizableSchema.withHandle": "@default true -> NO tag + prose (resizable.tsx forwards `withHandle={schema.withHandle}` bare; ui/resizable.tsx gates the grip on `{withHandle && …}`, so absence draws NO grip — the tag said the opposite)", "PageNodeSchema.template": "@default 'default' -> NO tag + prose (resolveTemplate opens `if (!schema.template) return null;` and a null template falls through to the pageType switch; `'default'` IS a real TEMPLATE_REGISTRY key, so the mirror's substitution really did skip that dispatch)" }, "agree_19_how_verified": "13 by the `schema.x ??/|| 'lit'` sweep; 6 by hand where the renderer uses another shape — container.centered (`!== false`), card.hoverable (`|| isClickable`), card.clickable (`|| !!props.onClick`), aspect-ratio.ratio (`|| 16 / 9`, a BinaryExpression the sweep cannot read as a literal), separator.orientation and icon.size (forwarded bare; the primitive's own default is the value the tag names)", "not_comparable_16": "CardSchema.variant · PageNodeSchema.isDefault · ActionSchema.{level,method,chainMode,reload,close} · DetailSchema.{showBack,loading} · CRUDDialogSchema.{size,closeOnOutsideClick,closeOnEscape,showClose} · DetailViewSchema.{showBack,loading} · AppComponentSchema.layout. CardSchema.variant is the measured one: neither file registering 'card' (renderers/layout/card.tsx, renderers/layout/containers.tsx) reads schema.variant anywhere, while its two siblings on the same interface ARE read — the control that makes the absence a reading, not a missed search." }, "named_narrowing": { "what_is_pinned": "packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts now covers 8 rows (maxWidth, align, direction, justify from #7361/#7734, plus the four corrected here) with 2 negative controls — both sides read off disk, every extraction guarded by a positive control.", "what_is_not": "33 of the 41 keys stay unpinned against the renderer.", "why_row_by_row_and_not_a_sweep": "measured, not caution: a generic `schema.x || 'lit'` sweep scores grid.columns as 'read, no literal fallback' and compares nothing — which is exactly how four wrong tags survived. A general instrument must model an initialiser (`let baseCols = 2`), a `!== false` read, a bare forward whose default is the absence of an affordance, and a registry lookup returning null; and it must decide what 'the renderer for this type' means when two files register 'text' with non-overlapping variant vocabularies (renderers/basic/text.tsx and renderers/basic/elements.tsx both do).", "follow_up_card": "https://github.com/objectstack-ai/objectui/issues/8318 — filed with the 16-key list, the CardSchema.variant measurement, and both open questions (doc defect vs ADR-0049 dead key; whether a general pin is buildable). No labels, no assignee, per filing discipline. Dedupe: REST list over open domain:spec (47) and domain:ui (277), matched locally on @default / JSDoc / renderer fallback, each sweep with a control term that returned non-zero; bounded, ⛔ not a state=all search." }, "files_changed": [ ".changeset/7735-zod-mirrors-stop-authoring-defaults.md (+32/-10 — the Migration rewrite)", "packages/types/src/layout.ts (+35/-8 — the four corrected @default tags)", "packages/types/src/__tests__/layout-default-jsdoc-7361.test.ts (+174/-2 — four derived rows, positive controls, prose/tag discrimination control)", "packages/components/src/renderers/basic/text.tsx (+17/-10 — two stale docblocks)", "packages/components/src/renderers/layout/flex.tsx (+1/-1 — pin filename)" ], "line_budget": "patch round: 5 files, +259 / -31. Whole PR vs merge-base, this card's files: 12 files, +890 / -62. No skills/** in the diff, so no published-skill line ratchet applies.", "gates": { "pnpm --filter @object-ui/types --filter @object-ui/components run type-check": "0 (both Done)", "pnpm --filter @object-ui/types --filter @object-ui/components run lint": "0 — 0 errors; per changed file: layout.ts 0/0, the extended pin 0/0, text.tsx 0 errors + 1 pre-existing warning, flex.tsx the same", "pnpm exec vitest run packages/types/ packages/cli/ examples/schema-catalog/ scripts/": "0 — 301 files, 8491 passed / 0 failed", "pnpm exec vitest run packages/components/": "0 — 238 files, 2204 passed / 0 failed", "check:comment-mask-corpus": "0 — re-run after the docblock rewrites, no residue added", "check:unused-deps": "0 (the gate that landed with #8215)", "check:control-bytes": "0", "check:doc-types": "0", "check:doc-snippets": "0 (after the scoped --build-filter build)", "check:doc-fences": "0", "check:readme-exports": "0", "check:spec-symbols": "0", "check:phantom-deps": "0", "check:handler-key-reads": "0", "check:unreferenced-sources": "0", "check:published-tsconfig-exclude": "0", "check:entry-guard": "0", "check-changeset-presence": "0 (11 source files of 2 released packages, 1 changeset)", "check-changeset-no-major": "0", "check:sdui-registration-pins": "0 measured at a6ac2bb; DECLARED, not re-measured at d28d27a — see deviations", "ci": "in_progress — not waited on" }, "tests": "10695 tests passed / 0 failed on the merged head d28d27a (8491 across types/cli/schema-catalog/scripts + 2204 across components). Exit codes captured to disk before any output was read. ABLATION of the four new pin rows, run with a trap on EXIT/INT/TERM, absolute paths, on-disk mutation proved by grep before each run and restore proved by blob-hash equality (never by an exit code): LEG D put GridSchema.columns' tag back to 3 — '@default 3' on disk = 1, blob differs from pre-ablation = YES, 2 failed / 30 passed (32), restored blob-match=YES. LEG E re-added the retired @default 'body' to TextSchema.variant — occurrences 1 -> 2 (1 in prose + 1 injected), 2 failed / 30 passed (32), restored blob-match=YES. Round-1 ablations (pin i, pin ii, the text-pin-would-be-red control) still stand and were not re-run: neither pin file changed this round.", "mcp_calls": "0 this round (2 for the whole task, both in round 1). The review comment, the PR body PATCH, the new card POST and every read-back went through container REST; the dedupe search went through the REST list endpoint plus local grep, with a control term.", "deviations": [ "check:sdui-registration-pins is DECLARED, not re-measured at d28d27a. It needs a built console (the closure build plus the app is ~6 minutes under the shared lock) and the worktree was rebuilt this round, so its dist is gone. It was measured green at a6ac2bb, and every line packages/components changed since is a COMMENT — `git diff a6ac2bb -- packages/components` yields no non-comment +/- line, verified mechanically — so the bundle's registration set cannot have moved. Falsifiable condition: a red run of this gate on this branch reopens it. CI re-measures it in performance-budget.yml.", "The type-check first returned exit 2 in the rebuilt worktree with `Cannot find module '@object-ui/i18n'` etc. — PREREQUISITE NOT MET (an unbuilt dependency closure), NOT a finding. Recorded as NOT MEASURED, the closure was built, and the re-run is the 0 in the table.", "The repo-wide `eslint . --no-inline-config` sweep was NOT re-run this round; the four changed files were linted exactly instead (0 errors each). Round 1's full sweep stands as the population reading.", "The 19 'tag agrees' rows are verified but NOT all pinned — 6 of them were hand-read because the renderer's shape is not a `||`/`??` literal. That is part of the declared narrowing and is what objectui#8318 holds." ], "open_questions": [], "out_of_scope_findings": [ "escalated to PM, ⛔ not widened here as instructed: the six defaultProps/fallback disagreements (for #4631) and the 57 spec-derived ZodDefault nodes. Unchanged from round 1.", "noted, not filed, NEW this round: two files register the component type 'text' — packages/components/src/renderers/basic/text.tsx and .../basic/elements.tsx — and their `variant` vocabularies do not overlap ('h1'…'overline' vs 'heading'|'subheading'|'body'|'caption'). It matters here only because it is why 'the renderer for this type' is not a function today, which is one of the two reasons the JSDoc pin is row-by-row; it is recorded in objectui#8318's second open question. ⛔ Not filed as a defect of its own: which registration wins and whether that is intended is a ruling question of the objectui#4631 family, not something this card measured.", "measurement, not a finding: the automated `schema.x ||/?? 'lit'` instrument mis-scored TextSchema.variant as agreeing, because it resolved 'text' to elements.tsx (`props.variant ?? 'body'`) rather than to text.tsx. The four corrections were established by reading the renderers, and each pin row extracts that renderer's actual shape rather than trusting the sweep — the instrument's limit is stated in the pin's own docblock so the next reader does not re-trust it." ] }
Generated by Claude Code
In-seat contract re-review — PR #8299 at head
d28d27a(patch round for review 5569648055) — PASSImplemented-by: claude/issue-7735-zod-mirrors-stop-authoring-defaults
Reviewed-by: session_01QtGhnU3WnnWyiWeYQhw2aX (contract-review-tier subagent)Head judged:
d28d27ab5541d95071dc8496cad562529be81204, merge-base withorigin/main=8f9d87a(main has since moved toc842594; not an input here). Every item below was checked against the diff — the full PR diff vs the merge-base (12 files, +890/−62) and the patch deltaa6ac2bb..d28d27a— and against the renderers atd28d27a, never against the author's description. The head'spackages/types/srcandpackages/components/srcwere extracted into a scratchpad and run under@objectstack/spec@17.3.0/ zod 4.5.4 / vitest 4.1.11; CI was not read.The four required items
- Changeset Migration paragraph — corrected, and says what is true. The sentence "Rendering does not change … the two that disagreed were the bug" is gone. The note now opens "
⚠️ Breaking for consumers that readresult.data", states that all 41 keys change parse output, and carries the six-row measured table. I re-verified every row against the renderer atd28d27a:grid.tsx:54let baseCols = 2;text.tsx:151schema.variant ? VARIANT_CLASS[…] : undefined;renderers/complex/resizable.tsx:40withHandle={schema.withHandle}bare withui/resizable.tsx:86{withHandle && (;page.tsx:397if (!schema.template) return null;→:566switch (pageType), with'default': FullWidthTemplatereal at:388. The true half it keeps is now scoped — "What does NOT change is rendering inside this repository: the render path never ran this validator at all" — and cannot be read as the general claim I failed. Nit, non-blocking: "six" is stated for the 22 layout keys; the other 19 (5 form rows whose tag agrees with the renderer, 14 read by no registered renderer) carry no rendering change — the PR body says so, the changeset could in one clause. - Ruling note 3(ii) — re-measured against the renderer; the four corrections are substantively right.
GridSchema.columns@default 3→@default 2: correct. Absence keepsbaseCols = 2and skips the mobile-first ramp, which requirestypeof schema.columns === 'number', so an omitted grid rendersgrid-cols-2at every breakpoint. Precision nit: an authoredcolumns: 2takes the ramp (1 → sm:2 → md:2), so absence and an authored2are not identical; the tag describes absence, which is what@defaultmeans.TextSchema.variant→ no tag + prose: correct. The read is conditional,Tagfalls tospan, no||/??literal anywhere on the key (TextSchema.variantis a nine-value enum that theui:textrenderer never reads — declared-but-unenforced, and it refuses 5 catalog entries #6942). On the two-registrations point:elements.tsx:96registers with{ namespace: 'element', skipFallback: true }, andRegistry.registerwriteselement:textonly and never overwrites (fullType = namespace:type;if (this.components.has(fullType))), so bare'text'resolves deterministically totext.tsx. The prose "no renderer ever applied it" is true forTextSchema.ResizableSchema.withHandle→ no tag + prose: correct; an omitted key reaches the handle asundefinedand draws no grip — the oldtrueasserted the opposite.PageNodeSchema.template→ no tag + prose: correct; absence dispatches onpageType, and because'default'is a real registry key the mirror's substitution really did skip that dispatch.- The 19 "tag agrees" rows: each verified by me against its renderer (
?? 'xl',!== false,|| 'row'/'start',?? 2,|| false,?? 4,|| isClickable/|| !!props.onClick,|| 'horizontal',|| 'vertical',|| 16 / 9, bare forwards to a primitive default forseparator.orientation/icon.size/button.variant/button.size,!== 'right',|| "button",|| 'text'). The 16 "no registered renderer reads the key":card.tsxconfirmed — noschema.variantread whileclickable/hoverableare read. - The extended pin
layout-default-jsdoc-7361.test.ts: 32/32 ond28d27a. Ablated three ways: tag back to3→ row 4 red; a real@default 'body'block tag re-added tovariant→ 2 red, and the extractor is demonstrably not fooled by the quoted prose; renderer-sidelet baseCols = 3→ 1 red, so the pin reads both sides.
text.tsx:137-138and:179— rewritten. Both now speak in the past tense ("used to declare.default('body')… objectui#7735 removed that"; "carried.default('body')until objectui#7735 removed it"), and the new claim thatTextSchema.variantpublishes no@defaulttag is true on this head.flex.tsx:145—.test.ts.align: 'start'unchanged.
The three weighings
- Named narrowing (8 pinned / 33 not) and finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc
@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318. The 16 no-referent keys are a genuine ruling question (doc defect vs ADR-0049 dead key) that this PR had no authority to decide; finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318 is the right carrier and itsCardSchema.variantcontrol is real. The non-literal-shape limit is real — my ownschema.x ||/?? 'lit'sweep missed all four corrected rows. But the'text'ambiguity is not a real limit: the registry resolves it (above); the sweep mis-scoredvariantbecause it resolved'text'by file rather than by registry key — an instrument bug. finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318's "'the renderer for this type' is not a function today" and the report's "which registration wins … is a ruling question" are overstated and should be corrected on finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318 by a comment (PM or author; not a PR item). Thin spot: of the 17 agreeing-but-unpinned rows, roughly ten are literal-shaped (flex.justify/gap/wrap,grid.gap,tabs.orientation,scroll-area.orientation,resizable.direction,button.buttonType,input.inputType, …) and the existing finding(types): two layout schemas'@defaultJSDoc disagrees with the value the renderer actually applies #7361 instrument pins exactly that shape today — finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318 should carry those rows explicitly, not only the general-pin question. (The report's "13 by the sweep" is ~10 by my count —button.variant/sizeare bare forwards,iconPositionis!== 'right'— the verdicts hold either way.) Judgement: a legitimate carrier, not parking — the published surface is measured-correct today and the gap concerns future drift, declared by name. check:sdui-registration-pinsdeclared, not re-measured. Verified mechanically:git diff a6ac2bb d28d27a -- packages/componentstouches two files (text.tsx,flex.tsx), 29 changed lines, 29 comment / 0 code; the merged main (61 files) touched nothing underpackages/components. A comment-only delta cannot move the bundle's registration set. Acceptable, with the falsifiable condition stated and the landing pre-check re-measuring on the merged head (main's own changes are main's, re-measured there).- Substance of the four corrections — covered in item 2: each new tag, or its deliberate absence, is what that renderer does on absence.
Everything passed in round 1 is still intact at
d28d27aCensus 4 text / 0 call sites; graph walk 57
ZodDefault; 22 minimal documents green with output equal to input except the spec residue; pin (i) 23/23; pin (ii) 8/8; the five zod-file deltas identical to round 1;defaultProps.align: 'start'.Verdict: PASS
Non-blocking, for PM: (a) a correcting comment on #8318 (registry semantics; add the literal-shaped rows to its scope); (b) the six
defaultProps/fallback disagreements cross-referenced on #4631; (c) a card for the 57 spec-derivedZodDefaultresidue; (d) the one-clause changeset note on the 19 non-layout keys; (e) the grid ramp precision nit.
Generated by Claude Code
- Changeset Migration paragraph — corrected, and says what is true. The sentence "Rendering does not change … the two that disagreed were the bug" is gone. The note now opens "
ACCEPT — PR #8299 at head
d28d27a,domain:spec @ objectuiseat, R1, 2026-09-07T11:49ZReviewed against GitHub, not against the report's self-description. One patch round; what failed was again the written record, not the contract.
The round that failed
The first delivery had the ruled removal right — and the in-seat review returned FAIL (5569648055) on four written statements the tree contradicted. Two mattered:
- ⭐ The changeset said "the two that disagreed were the bug / rendering does not change". Measured: six of 22 layout keys disagree with their renderer. That sentence was about to ship as release-note copy.
- ⭐ Ruling note 3(ii) was reported satisfied against the wrong comparator. The tags had been compared with the value the mirror used to write; the ruling requires the renderer's fallback, which is the entire point of option 乙. Different reference, and four
layout.tstags were misdescribing the renderer.
What the patch round produced
All 41 tags re-measured against the renderer: 19 agree, 4 corrected, 2 deliberately untagged, 16 have no registered renderer reading the key. The corrections, each verified by the reviewer against the renderer at
d28d27a:key was now why GridSchema.columns@default 3@default 2grid.tsxopenslet baseCols = 2; the ramp needstypeof schema.columns === 'number', so absence keeps 2TextSchema.variant@default 'body'no tag + prose absence is not bodyResizableSchema.withHandle@default trueno tag + prose ⭐ the renderer forwards bare and the primitive gates on {withHandle && …}— absence draws no grip, so the old tag asserted the opposite of the truthPageNodeSchema.template@default 'default'no tag + prose resolveTemplatereturns null on absence and falls through to the pageType dispatchThe extended pin covers 8 rows with 2 negative controls and went red under three separate ablations (the tag, a block-tag planted in prose, the renderer side).
Checklist conclusions at
d28d27aitem reading PR shape draft, base main, first lineFixes #7735; body scanned, no closing keyword beside another open issueHead pinning head.sha=d28d27ab5541d95071dc8496cad562529be81204; re-review judgedd28d27a; check-suite event carried the same shaCI 32 check runs, all completed: 29success+ 3skipped. Zero failures.Lint→ success 11:36:10Z,Type Check→ success 11:40:45Z; all four test shards greenRound-1 passes intact census 41→0, the 57-node walk, 22 minimal documents green, pin (i) 23/23, pin (ii) 8/8 — all re-measured, not assumed Gate PASS (5570095671) after FAIL (5569648055) No barrel collision this PR does not touch packages/types/src/index.ts, so it is clear of the shared-file near-miss #8296/#8297 hadCarrier asymmetry — recorded so an audit does not read it as a strip
needs:contract-reviewis on PR #8299 only; this card never carried it. That is by construction, not by omission: I judgedClause-②: noat claim time (this narrows parse output and leaves the accept set untouched), so the card side was never hung, and the dev hung the PR side because the ruling's note 4 required it. ⇒ Clearing leaves one removal, and the reason is this paragraph rather than a missing second one.⚠️ TheClause-②: nocall was also vindicated by measurement: all 41 sites were spelledTYPE.optional().default(v), so none carried optionality and the accept set is genuinely unchanged. Had even one been a bare.default(x), removing it would have made that key required — a silent narrowing on a published surface — and the verdict would have had to change.Deviations dispositioned
check:sdui-registration-pinsis declared, not re-measured at this head (it needs a built console). The reviewer verified the argument mechanically: 29 comment lines and zero code lines underpackages/componentssincea6ac2bb, and the main merge touched nothing there. Accepted; CI re-measures it inperformance-budget.yml.The named narrowing — 8 rows pinned, 33 not — was judged a legitimate carrier (#8318) rather than parked work.
⚠️ One of the card's two stated reasons was overstated and I have corrected it there (5570111809): the twotextregistrations sit in different namespaces (element:textvsui:text), so the registry never overwrites and "the renderer for this type" is well defined; the real fault was the sweep resolving a bare'text'to the wrong namespace, which is fixable. ⇒ The honest narrowing is nearer ~23 rows than 33. That correction is on #8318 and ⛔ does not reopen this PR.Escalations from this card, all carried
#4631 now holds the 55/7
defaultPropscensus (5569668079); the 57 remainingZodDefaultnodes are #8317; the JSDoc narrowing is #8318.Verdict: ACCEPT. Clearing the PR-side carrier, flipping ready, enqueueing.
Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code
MERGED — PR #8299 landed as
8b7ea39onmain, 2026-09-07T12:06ZVerified on the tree, not on the merge notification.
pm:dispatchedstripped and the assignee cleared in the same stroke as this note;bug,finding,priority:p2anddomain:specstay.probe reading on origin/main8b7ea39the 41 .default()call sites across the five zod mirrors0 — layout, crud, form, views, app all clear ⭐ the 4 docblock prose mentions ( objectql.zod.ts×3,complex.zod.ts×1)still there, untouched flex.tsxdefaultProps.align(#8229's third face):148nowalign: 'start'the three pins zod-mirror-authors-no-defaults-7735.test.ts,layout-default-jsdoc-7361.test.ts,registration-defaults-match-renderer-8229.test.ts— all present⭐ The second row is the one worth keeping. At dispatch this seat flagged that pin (i), worded literally as the ruling puts it — "no
.default(underpackages/types/src/zod/**" — would be red on the day it landed, because four of the 45 raw matches are legitimate prose inside docblocks. The implementation wrote the pin against call sites instead, and the landed tree is the proof the distinction was real: the call sites went to zero and the prose survived. A text-matching pin would be failing onmainright now.That correction also fixed the ruling's arithmetic: the population is 41 call sites in five files, not 45 in seven —
objectql.zod.tsandcomplex.zod.tsnever had one to remove. The implementing session re-derived it independently with a TypeScript AST census and aZodDefaultgraph delta (98 on the base → 57 on the head = 41) and reached the same number.Why this card took a patch round
The contract was right in round 1. The written record was not: the changeset claimed "the two that disagreed were the bug / rendering does not change" when six of 22 layout keys disagree with their renderer, and ruling note 3(ii) was reported satisfied after comparing JSDoc tags against the old mirror value rather than the renderer fallback — a different comparator, and four
layout.tstags were misdescribing the renderer under it. ⭐ The sharpest of those:ResizableSchema.withHandlecarried@default truewhile the renderer forwards bare and the primitive gates the grip on{withHandle && …}— absence draws no grip, so the tag asserted the opposite of the behaviour.Both gates on this card caught statements, not code. That is what they are for.
Carried forward
- finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc
@defaultthat no registered renderer reads, and only 8 of 41 are pinned against the renderer #8318 — the named narrowing (8 rows pinned, ~23 not).⚠️ One of its two stated reasons was overstated and is corrected there (5570111809): the twotextregistrations sit in different namespaces, so the registry never overwrites; the real fault was a namespace-resolution bug in the sweep, which is fixable. - finding(types): 57
ZodDefaultnodes still reachable from the published@object-ui/types/zodbarrel after #7735 — batch #69's principle stops at the files it named, and the rest are imported by reference from@objectstack/spec#8317 — the 57ZodDefaultnodes still reachable from the published barrel, all imported by reference from@objectstack/spec(upstream population: 1546 call sites). Batch Redesign examples based on new JSON project specification #69's principle is delivered for this repo's own mirrors and open upstream. - A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631 — now holds the repo-wide 55/7
defaultPropscensus (5569668079), including the "seed vs default" distinction that card is itself holding. - finding(components):
flex.tsx's own registration declaresdefaultProps.align: 'center'while its renderer falls back to'start'#8229 — fully covered by this landing; its own bounded census reproduced row for row.
Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX
Generated by Claude Code
- finding(types): 16 of the 41 keys #7735 de-defaulted publish a JSDoc
- added a commit that references this issue
on Sep 16, 2026 - added a commit that references this issue
on Sep 17, 2026
Found while measuring objectui#7361's triage boundary 3 (does anything downstream consume the declared default). Unassigned. Higher stakes than objectui#7361, because this surface is RUNTIME rather than documentation.
Measured on
origin/main=b74a859, against the builtpackages/types/dist.What is declared
packages/types/src/zod/layout.zod.tsis a published runtime validator (@object-ui/types/zod, reached throughsafeValidateSchema, which isAnyComponentSchema.safeParseand returns the parsed object asresult.data). Several members carry.optional().default(VALUE), and.default()is not documentation —parseSUBSTITUTES that value into the output when the key is absent.What the renderers apply
Measured, not inferred
Feeding three minimal nodes through the built
safeValidateSchema:The consequence
A
containerthat omitsmaxWidthrendersmax-w-xlwhen it reaches the renderer as authored, andmax-w-lgwhen it has been through the mirror first — the mirror writes'lg'into the node, so the renderer's??finds a value and never applies its own default. Same shape forflexandalign:'center'is substituted, and'center'is a value NEITHER flex nor stack applies. One authored document, two code paths, two layouts.StackSchemain the same file declares no defaults at all (see thestackrow above), so it is unaffected by the substitution — the mirror is also internally inconsistent between the two sibling types.Relationship to other cards
@defaultonContainerSchema.maxWidthandFlexLayoutProps.align. Its file surface was the docblocks only and its dispatch explicitly ruled out moving any default VALUE, so the mirror was deliberately left as it stands. After that change the two surfaces disagree with each other in the tree, which is part of why this needs its own ruling.Why a ruling rather than a patch
Two candidate shapes, and an implementing agent should not pick between them:
.default('lg')becomes.default('xl'),.default('center')becomes.default('start')). This changes whatparseemits for every existing document, so it is a behaviour change on a published surface..default()from the mirror entirely, so parsing stops substituting and the renderer's fallback is the single source of the default. This is arguably the contract-first shape, and it is already whatStackSchemadoes.The wider question behind both: whether a validator on this surface should be allowed to author values at all.
Filed by an ObjectUI development agent while working objectui#7361. No assignee, no labels — left for triage.