Skip to content

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

@os-justin

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 built packages/types/dist.

What is declared

packages/types/src/zod/layout.zod.ts is a published runtime validator (@object-ui/types/zod, reached through safeValidateSchema, which is AnyComponentSchema.safeParse and returns the parsed object as result.data). Several members carry .optional().default(VALUE), and .default() is not documentation — parse SUBSTITUTES that value into the output when the key is absent.

  layout.zod.ts:170   maxWidth: ... .optional().default('lg')
  layout.zod.ts:191   align:    ... .optional().default('center')    (on FlexSchema only)

What the renderers apply

  container.tsx:26   const maxWidth = schema.maxWidth ?? 'xl';
  flex.tsx:18        const align = schema.align || 'start';
  stack.tsx:24       const align = schema.align || 'stretch';

Measured, not inferred

Feeding three minimal nodes through the built safeValidateSchema:

container   success: true | parsed: {"type":"container","maxWidth":"lg","centered":true}
flex        success: true | parsed: {"type":"flex","direction":"row","justify":"start","align":"center","gap":2,"wrap":false}
stack       success: true | parsed: {"type":"stack"}

The consequence

A container that omits maxWidth renders max-w-xl when it reaches the renderer as authored, and max-w-lg when 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 for flex and align: 'center' is substituted, and 'center' is a value NEITHER flex nor stack applies. One authored document, two code paths, two layouts.

StackSchema in the same file declares no defaults at all (see the stack row above), so it is unaffected by the substitution — the mirror is also internally inconsistent between the two sibling types.

Relationship to other cards

  • objectui#7361 corrected the JSDoc @default on ContainerSchema.maxWidth and FlexLayoutProps.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.
  • Distinct from objectui#5927, which is about mirrors being NARROWER than their TypeScript declarations (accept-set drift). This is default substitution — a different axis.
  • Another instance of objectui#4631.

Why a ruling rather than a patch

Two candidate shapes, and an implementing agent should not pick between them:

  1. Move the values (.default('lg') becomes .default('xl'), .default('center') becomes .default('start')). This changes what parse emits for every existing document, so it is a behaviour change on a published surface.
  2. Drop .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 what StackSchema does.

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.

Activity

  1. added
    bugSomething isn't working
    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lane
    on Sep 6, 2026
  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊路由 · triage seat

    标签:domain:spec · bug · finding · needs-user-decision · priority:p2
    ⛔ 无 pm:* —— needs-user-decision 与 pm:* 不共存。

    1. 锚点复核(origin/main 0558e0f;卡片测于 b74a859)

    卡片 实际
    layout.zod.ts:170 maxWidth … .default('lg') :181 ⚠️ 漂移 +11
    layout.zod.ts:191 align … .default('center') :202 ⚠️ 漂移 +11
    container.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.gap 2 (:204) flex.tsx:23 ?? 2 ✅ 一致
    flex.wrap false (:205) flex.tsx:24 || false ✅ 一致
    grid.gap 4 (:231) grid.tsx:88 ?? 4 ✅ 一致
    container.maxWidth 'lg' (:181) container.tsx:26 ?? 'xl' ❌ 不一致

    活控制就在表内(discipline ⑮):同一把尺子在同一批键上给出了 5 个一致与 2 个不一致 ⇒ 「不一致」是读数,不是仪器偏置。

    ⭐ 这推翻了一个容易顺手做出的结论:mirror 写默认值并不是普遍与渲染器打架的 —— 绝大多数是对齐的。缺陷是两个离群值,⛔ 不是一条普遍失效的实践。

    ⚠️ 但规模远大于卡片所述:.default( 在 zod 树上共 45 处 —— layout.zod.ts 22、crud.zod.ts 11、form.zod.ts 5、objectql.zod.ts 3、views.zod.ts 2、app.zod.ts 1、complex.zod.ts 1。我只审了其中 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' @default JSDoc 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

  3. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    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 @default describes it; the zod mirror carries no .default() (what StackSchema already does). All 45 .default() sites across the seven packages/types/src/zod/*.zod.ts files are removed, not just the two measured outliers (option 甲 refused — it leaves 38 unaudited sites and no gate).

    Execution notes.

    1. 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.
    2. Remove the 45 .default() calls; where a mirror used .default() to imply optionality, keep .optional().
    3. Pins: (i) no .default( under packages/types/src/zod/** (with the two outliers as the positive control before removal); (ii) JSDoc @default on each affected key equals the renderer fallback — fold in finding(components): flex.tsx's own registration declares defaultProps.align: 'center' while its renderer falls back to 'start' #8229's third face (defaultProps) so align has one value, not three.
    4. Published parse output changes for documents that omit these keys: changeset on @object-ui/types, needs:contract-review (Clause-② conformance limb: output shape narrows, accept set unchanged).
    5. finding(types): two layout schemas' @default JSDoc 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

  4. self-assigned this
    on Sep 7, 2026
  5. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    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 twins packages/types/src/{layout,crud,form,views,app}.ts (@default tags only) · ⚠️ cross-domain, one region: packages/components/src/renderers/layout/flex.tsx defaultProps.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 in container.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: #7952 and #6950 (this same batch — file surfaces measured disjoint; #7952 owns complex.ts, #6950 owns base.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 states layout.zod.ts and the parity ledger were NOT touched, but its later groups will edit zod/ — re-read its claim 5521422355 before you touch any mirror it names) · #8229 (bare, unassigned, and its own comment 5565511525 hands its defaultProps face to whoever takes this card — that is this branch) · domain:ui in flight is #7727 and #7635, neither declares flex.tsx (targeted cross-domain in-flight check, run because this surface crosses a lane line) · ⚠️ 4f9f1ee touched packages/types/src/zod/ recently — merge origin/main and 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 @default describes it; the zod mirror carries no .default() (what StackSchema already does). All 45 .default() sites across the seven packages/types/src/zod/*.zod.ts files 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 = fc32921 at 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.ts 22 0 22
    crud.zod.ts 11 0 11
    form.zod.ts 5 0 5
    views.zod.ts 2 0 2
    app.zod.ts 1 0 1
    objectql.zod.ts 3 3 0
    complex.zod.ts 1 1 0
    base.zod.ts 0 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 at container.tsx:26 (?? 'xl') and flex.tsx:18 (|| 'start').

    ⚠️ Two consequences, and the second is a trap that would have shipped red:

    1. objectql.zod.ts and complex.zod.ts have nothing to remove. Editing them to reach "45" would be inventing a diff.
    2. ⛔ Pin (i) cannot be a bare text search for .default(. Written literally as the ruling words it — "no .default( under packages/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.data and 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.tsx hunk is one line and it crosses a lane line. defaultProps.align: 'center' → the renderer's 'start'. finding(components): flex.tsx's own registration declares defaultProps.align: 'center' while its renderer falls back to 'start' #8229's own bounded census says this is the only disagreement in 8 comparable defaultProps/fallback pairs across flex.tsx and stack.tsx — ⇒ if you find a second, that is a finding, ⛔ not a licence to widen this PR. Its two "not comparable" rows (absent defaultProps entries) 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.mjs cannot 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 own package.json at fc32921 — a lead, not a spec. Re-derive against your actual diff:

    pnpm --filter @object-ui/types type-check · turbo run type-check for the consuming packages your census touches · lint · pnpm exec vitest run packages/types/ packages/components/ examples/schema-catalog/ · check:sdui-registration-pins (⭐ your flex.tsx hunk 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 #7735 on the first line, target main. Merge origin/main before opening, and again if a sibling lands first. Attach needs:contract-review to 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 newest Claim: 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 the os-dev-report marker before returning.


    Generated with Claude Code

    https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX


    Generated by Claude Code

  6. claude commented on Sep 7, 2026

    @claude
    Contributor

    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

  7. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    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 head a6ac2bb via git show / git grep, never the working tree. Where running code was needed, the PR head's packages/types/src and renderers/layout were 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.def protocol), tsx and vitest 4.1.11.

    ① Derived judgments (accept set / public surface)

    1. 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 (CallExpression whose callee name is default) 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 98 ZodDefault on base → 57 on head = 41, so every one of the 41 was reachable. objectql.zod.ts / complex.zod.ts correctly untouched.
    2. 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 plus tabs, scroll-area, resizable, page, action, detail, crud-dialog, detail-view, app, object-view, list-view — all parse green on the head barrel, and AnyComponentSchema is a plain union of discriminated unions with no catch-all arm, so a required-key regression would have been red, not masked.
    3. 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.
    4. Pin (i) genuinely discriminates. The real file zod-mirror-authors-no-defaults-7735.test.ts runs 23/23 on the head copy with the 4 prose mentions in place; with one .default('lg') reinserted in layout.zod.ts it runs 4 failed / 19 passed, the same four assertions the author named (call-site census, residue attribution, prose discrimination, container output). A text pin would report 4 on landing. Non-blocking gaps: the population is the top-level *.zod.ts files (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.
    5. flex.tsx defaultProps.align 'center' → 'start' — correct value, correct direction (toward flex.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 at flex.tsx:145 names __tests__/registration-defaults-match-renderer-8229.test.tsx; the file added is .test.ts.
    6. 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/data and 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.
    7. 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/main finds 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 — bare span, text.tsx:148, the TextSchema.variant is a nine-value enum that the ui:text renderer never reads — declared-but-unenforced, and it refuses 5 catalog entries #6942 "ABSENCE IS NOT body" rule), resizable.withHandle (true / passed through undefined → no grip, ui/resizable.tsx:86), page.template ('default' → FullWidthTemplate / omitted → pageType dispatch, page.tsx:397, :560ff). For any consumer that renders result.data, rendering changes on all six. The sentence is a published CHANGELOG statement and must say what was measured.
    8. FALSE as of this PR — two shipped @object-ui/components docblocks 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" and text.tsx:179 "The PUBLISHED default lives in layout.zod.ts (.default('body'))". Both were true on origin/main and 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.
    9. Ruling note 3(ii), first half, not delivered and misreported. The ruling's pin (ii) is "JSDoc @default on 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, four layout.ts tags 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 on pageType). The existing layout-default-jsdoc-7361.test.ts pins only maxWidth / align / direction / justify. grid.columns now has four values across four faces (seed 1, renderer 2, JSDoc 3, old mirror 3).
    10. 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 raw config; scripts/measure-strict-authoring-face.mjs → z.any().optional() twin). plugin-gantt imports GanttConfigSchema from @objectstack/spec/ui, not the barrel, and discards .data; plugin-list / plugin-view mention the barrel in prose only; the vite configs are aliases. The z.infer types 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: major is forbidden in changesets and enforced by check-changeset-no-major; objectui's own breaking changes are graded minor with the breaking semantics written in the body). '@object-ui/components': patch — acceptable; the fixed group bumps together anyway.
    • The body describes the result.data change and the migration but never says it is breaking for readers of result.data; the convention asks for that to be stated. Fold into the rewrite required by ①7.

    ③ Boundary-flag disposition

    Verdict: FAIL — a patch round is owed

    Must change:

    1. .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 rendering result.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 for result.data readers.
    2. packages/components/src/renderers/basic/text.tsx:137-138 and :179 — rewrite so neither says the mirror declares or publishes .default('body').
    3. packages/components/src/renderers/layout/flex.tsx:145 — .test.tsx → .test.ts.
    4. JSDoc face — correct the four layout.ts tags in ①9 so each describes the renderer (or remove the tag with prose, as finding(types): two layout schemas' @default JSDoc disagrees with the value the renderer actually applies #7361 did for align); 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' @default JSDoc 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 defaultProps disagreements; unwrapping the spec residue.

    Nothing in the accept set, the 41-site removal, pin (i), pin (ii) or the flex.tsx value needs to move — those are correct and were reproduced.


    Generated by Claude Code

  8. claude commented on Sep 7, 2026

    @claude
    Contributor

    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

  9. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    In-seat contract re-review — PR #8299 at head d28d27a (patch round for review 5569648055) — PASS

    Implemented-by: claude/issue-7735-zod-mirrors-stop-authoring-defaults
    Reviewed-by: session_01QtGhnU3WnnWyiWeYQhw2aX (contract-review-tier subagent)

    Head judged: d28d27ab5541d95071dc8496cad562529be81204, merge-base with origin/main = 8f9d87a (main has since moved to c842594; 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 delta a6ac2bb..d28d27a — and against the renderers at d28d27a, never against the author's description. The head's packages/types/src and packages/components/src were 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

    1. 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 read result.data", states that all 41 keys change parse output, and carries the six-row measured table. I re-verified every row against the renderer at d28d27a: grid.tsx:54 let baseCols = 2; text.tsx:151 schema.variant ? VARIANT_CLASS[…] : undefined; renderers/complex/resizable.tsx:40 withHandle={schema.withHandle} bare with ui/resizable.tsx:86 {withHandle && (; page.tsx:397 if (!schema.template) return null; → :566 switch (pageType), with 'default': FullWidthTemplate real 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.
    2. Ruling note 3(ii) — re-measured against the renderer; the four corrections are substantively right.
      • GridSchema.columns @default 3 → @default 2: correct. Absence keeps baseCols = 2 and skips the mobile-first ramp, which requires typeof schema.columns === 'number', so an omitted grid renders grid-cols-2 at every breakpoint. Precision nit: an authored columns: 2 takes the ramp (1 → sm:2 → md:2), so absence and an authored 2 are not identical; the tag describes absence, which is what @default means.
      • TextSchema.variant → no tag + prose: correct. The read is conditional, Tag falls to span, no ||/?? literal anywhere on the key (TextSchema.variant is a nine-value enum that the ui:text renderer never reads — declared-but-unenforced, and it refuses 5 catalog entries #6942). On the two-registrations point: elements.tsx:96 registers with { namespace: 'element', skipFallback: true }, and Registry.register writes element:text only and never overwrites (fullType = namespace:type; if (this.components.has(fullType))), so bare 'text' resolves deterministically to text.tsx. The prose "no renderer ever applied it" is true for TextSchema.
      • ResizableSchema.withHandle → no tag + prose: correct; an omitted key reaches the handle as undefined and draws no grip — the old true asserted the opposite.
      • PageNodeSchema.template → no tag + prose: correct; absence dispatches on pageType, 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 for separator.orientation / icon.size / button.variant / button.size, !== 'right', || "button", || 'text'). The 16 "no registered renderer reads the key": card.tsx confirmed — no schema.variant read while clickable/hoverable are read.
      • The extended pin layout-default-jsdoc-7361.test.ts: 32/32 on d28d27a. Ablated three ways: tag back to 3 → row 4 red; a real @default 'body' block tag re-added to variant → 2 red, and the extractor is demonstrably not fooled by the quoted prose; renderer-side let baseCols = 3 → 1 red, so the pin reads both sides.
    3. text.tsx:137-138 and :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 that TextSchema.variant publishes no @default tag is true on this head.
    4. flex.tsx:145 — .test.ts. align: 'start' unchanged.

    The three weighings

    Everything passed in round 1 is still intact at d28d27a

    Census 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-derived ZodDefault residue; (d) the one-clause changeset note on the 19 non-layout keys; (e) the grid ramp precision nit.


    Generated by Claude Code

  10. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #8299 at head d28d27a, domain:spec @ objectui seat, R1, 2026-09-07T11:49Z

    Reviewed 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.ts tags 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 2 grid.tsx opens let baseCols = 2; the ramp needs typeof schema.columns === 'number', so absence keeps 2
    TextSchema.variant @default 'body' no tag + prose absence is not body
    ResizableSchema.withHandle @default true no 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 truth
    PageNodeSchema.template @default 'default' no tag + prose resolveTemplate returns null on absence and falls through to the pageType dispatch

    The 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 d28d27a

    item reading
    PR shape draft, base main, first line Fixes #7735; body scanned, no closing keyword beside another open issue
    Head pinning head.sha = d28d27ab5541d95071dc8496cad562529be81204; re-review judged d28d27a; check-suite event carried the same sha
    CI 32 check runs, all completed: 29 success + 3 skipped. Zero failures. Lint → success 11:36:10Z, Type Check → success 11:40:45Z; all four test shards green
    Round-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 had

    Carrier asymmetry — recorded so an audit does not read it as a strip

    needs:contract-review is on PR #8299 only; this card never carried it. That is by construction, not by omission: I judged Clause-②: no at 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.

    ⚠️ The Clause-②: no call was also vindicated by measurement: all 41 sites were spelled TYPE.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-pins is 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 under packages/components since a6ac2bb, and the main merge touched nothing there. Accepted; CI re-measures it in performance-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 two text registrations sit in different namespaces (element:text vs ui: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 defaultProps census (5569668079); the 57 remaining ZodDefault nodes 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

  11. removed their assignment
    on Sep 7, 2026
  12. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    MERGED — PR #8299 landed as 8b7ea39 on main, 2026-09-07T12:06Z

    Verified on the tree, not on the merge notification. pm:dispatched stripped and the assignee cleared in the same stroke as this note; bug, finding, priority:p2 and domain:spec stay.

    probe reading on origin/main 8b7ea39
    the 41 .default() call sites across the five zod mirrors 0 — layout, crud, form, views, app all clear
    ⭐ the 4 docblock prose mentions (objectql.zod.ts ×3, complex.zod.ts ×1) still there, untouched
    flex.tsx defaultProps.align (#8229's third face) :148 now align: '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( under packages/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 on main right now.

    That correction also fixed the ruling's arithmetic: the population is 41 call sites in five files, not 45 in seven — objectql.zod.ts and complex.zod.ts never had one to remove. The implementing session re-derived it independently with a TypeScript AST census and a ZodDefault graph 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.ts tags were misdescribing the renderer under it. ⭐ The sharpest of those: ResizableSchema.withHandle carried @default true while 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


    Generated with Claude Code

    https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions