Repository navigation
finding(types): 57 ZodDefault nodes still reachable from the published @object-ui/types/zod barrel after #7735 — batch #69's principle stops at the files it named, and the rest are imported by reference from @objectstack/spec #8317
Description
Activity
- addeddomain: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 8, 2026 分诊路由 —
domain:spec·needs-user-decision·priority:p2· type Task⛔ 本席是分诊席(
claude-opus-5):不认领、不派发、不写码、不合并、⛔ 不裁决决策箱卡(本会话claude-opus-5,CONTRACT_REVIEW_TIER要求 fable 层)。⭐ 卡自己判断准确 —— 「⛔ This is a ruling question, not a defect with an obvious repair」。⇒ 送决策箱。卡正文已含六项要素(事实、复现、两组测量、三条岔路及各自代价、以及「不裁」的声明),本席在此补齐决策箱家具:四棱卡面、推荐、强制置信缺口、维护者速读。
四棱卡面
① 项目长远合理性
批次 #69 的裁定是一句无条件的原则:「A validator validates; it does not write values into an author's document.」⇒ 一个只在部分键上为真的原则,长期看比没有原则更糟 —— 因为读者会以为它处处为真。
⭐ 而卡指出的关键是:如果没人裁,落地的是 (c) 且不写下来 —— 「the one outcome with no defender」。⇒ 长期合理性上,(b) 最优(原则处处为真),(a) 次之(在本仓的消费面上处处为真),(c)-未写下 最差。
② 实际业务拉动
⭐ 复现是实测的,且落在一条常见路径上:
safeValidateSchema({ type: 'object-view', objectName: 'account', navigation: {} }) ⇒ navigation: { mode: 'page', preventNavigation: false, openNewTab: false, size: 'auto' }四个作者没写的键,出现在
result.data里。 ⇒ #7735 当初立卡要解决的「一份授权文档、两种形状」在这些键上原样存活。⚠️ 但受害者未测量:⛔ 没有任何读数说明有人因此出过错。⇒ 这一棱不构成紧迫性,只构成「原则未兑现」。③ 防 AI 犯错
⭐ 这是最强的一棱。 一个 AI 作者写下一份元数据、拿回来一份多了几个键的文档 —— 而且有些键会多、有些不会,且无法从文档本身分辨。
⇒ 卡把代价说到位了:「an author cannot tell which key is which without reading the import graph」。⇒ ⭐ 要求作者读依赖图才能预测校验器的行为,是这条轴上最坏的形状。
⇒ (a) 在本仓消费面上消除这个不确定性;(b) 处处消除;(c) 把它固化并只是写下来 —— 写下来比不写好,⛔ 但作者仍要读那份说明才知道边界在哪。
④ 创业阶段不扩散
规模 (a) 57 处,本仓内, .removeDefault(),⭐ 已有一次先例:packages/types/src/zod/objectql.zod.ts:452SpecListViewSchema.shape.type.removeDefault()(b) ⚠️ 1546 处(packages/spec/src/**实测真实调用点;其中src/ui/**占 126)+ 另一个仓的发布列车 + 一个不属于本席位的裁定(c) 一段说明文字 ⇒ 维护者 2026-08-04「先专注于核心能力」与 2026-08-27「创业阶段…短期不考虑渐进」⇒ 强烈反对 (b) 作为当下动作,⛔ 不是反对它作为方向。
推荐 —— (a),且理由与卡的顾虑正面对上
⭐ 卡对 (a) 的最大顾虑是「a new divergence in a package whose whole job is not to diverge」。本席认为这条顾虑写重了,理由是一个卡自己给出的事实:
PR #8299 delivers that for the 41
.default()call sites written in this repo's own mirrors.⇒ ⭐⭐ 那 41 处的分叉已经存在,而且是批次 #69 刻意造成的。 也就是说:本仓的 parse 输出与 spec 的 parse 输出,从 #8299 落地那天起就已经不一致了。
⇒ (a) 不是「引入一种新的分叉」,是「把一次已被裁定的分叉补完」。 而现状 —— 41 处已分叉、57 处未分叉、两者混在同一个
safeValidateSchema的输出里 —— 是三种状态里唯一没有人主张过的那个。另外两条支持 (a) 的理由:
- ⭐ (a) 不封闭 (b)。 若 spec 日后采纳同一原则,那 57 个
.removeDefault()自动变成 no-op —— ⛔ 不需要回滚,不需要迁移。这是一个可逆的动作。 - 手法是现成的,不是发明的 ——
objectql.zod.ts:452已经这么做过一次。
⚠️ 同时必须做 (c) 的那一半:无论选哪条,边界都要写下来。⇒ 若选 (a),写下的是「本仓的 mirror 不作者化任何默认值,包括从 spec 导入的子 schema」——一句话,且从此不再有边界可写错。
⚠️ 强制置信缺口 —— 本推荐最可能错的两处-
⭐⭐ ⛔ 没有人测过是否有消费者「依赖」那些被塞进去的值。
safeValidateSchema今天会把navigation.mode = 'page'等写进result.data。若本仓或下游有代码读result.data.navigation.mode并指望它有值,(a) 会让它拿到undefined。
⇒⚠️ 这是 (a) 落地前必须补的读数,⛔ 不是可选的:对那 57 个键各扫一次消费点,带阳性对照。若命中非零,(a) 的代价评估作废,需重新权衡。 -
spec 侧的看法未征询。 本仓
.removeDefault()掉上游声明的默认值,在 spec 维护者看来是否算破坏契约,本席无从判断 —— 而@object-ui/types的职责恰恰是镜像它。
⇒⚠️ 若答案是「算」,那么 (a) 不可取,(b) 或 (c) 成为仅有的两条。
维护者速读
我们前不久定了一条规矩:校验器只负责检查,不许往用户写的文档里塞东西。
这条规矩兑现了一半。我们自己写的那 41 处已经改好了;但还有 57 处是从后端的
@objectstack/spec里整块引进来的,它们还在往文档里塞值。具体长这样 —— 用户只写了
navigation: {},拿回来的是:navigation: { mode: 'page', preventNavigation: false, openNewTab: false, size: 'auto' }四个他没写的键。
问题在于:现在有些键会被塞、有些不会,而从文档本身看不出来是哪些 —— 要弄清楚得去读依赖关系图。对写元数据的人(尤其是 AI)来说,这是最难对付的一种不确定。
三条路:
- A —— 在我们这边把这 57 处的默认值剥掉。 改动小(57 处,手法我们已经用过一次),当天可做,而且可逆:将来后端若也采纳这条规矩,这些改动会自动变成空操作。
⚠️ 代价:我们的校验结果会和后端的不完全一样 —— 但那已经发生了,前面那 41 处就是。 - B —— 推到后端去改。 最干净、一次解决所有人。
⚠️ 代价:那边有 1546 处,是另一个仓库的发布节奏,而且不是我们能替他们决定的。 - C —— 就把这条边界写清楚:我们自己的不塞,引进来的照旧塞,把这个不对称写进文档。
⚠️ 代价:规矩仍然只对一半的键成立,作者还是得查才知道。
我们建议 A,
⚠️ 但有一个前提要先测:现在有没有代码在指望那些被塞进去的值。如果有,A 的代价就变了,要重新算。请回一个字母:A · B · C。
定级与车道
priority:p2:⛔ 无运行期损害;⭐ 但一条已裁定的原则只对一半的键成立,而这个状态没有任何人主张过。⛔ 不抬 p1:无人受害的实测。domain:spec:落点是packages/types/src/zod/**的导入边界 —— 契约面,与 #8318 / #8498 / #8516 / #8517 同族。⛔ 邻卡
- finding(types): the zod layout mirror substitutes runtime
.default()values the renderers never apply — a parsedcontainerrenders a different width than an unparsed one #7735 / PR fix(types,components): the zod mirrors stop authoring defaults #8299 —— 批次 Redesign examples based on new JSON project specification #69 的裁定与 41 处交付;评审升级本卡的评论:issues/7735#issuecomment-5569648055。 - 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)—— 「三张声明面互不一致」的总卡。 ⚠️ 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 —— 同一次 finding(types): the zod layout mirror substitutes runtime.default()values the renderers never apply — a parsedcontainerrenders a different width than an unparsed one #7735 落地留下的另一半残留(16 个键的@default无渲染器读)。⭐ 本席本轮已路由,并在其上记录了一处必须重测的问题。两张建议同一人连读,⛔ 但不合并:本卡是校验器塞值,那张是文档标签失去指称。
Generated by Claude Code
- ⭐ (a) 不封闭 (b)。 若 spec 日后采纳同一原则,那 57 个
Ruling recorded — A, measure-first: the mirror strips the 57 imported defaults at its import boundary, after a consumer census of those keys; the boundary sentence is written once (director seat, decision batch #90, 2026-09-08)
Provenance (who / verbatim / where): maintainer, live PM chat with the director seat (
session_01TezFG8ZMrNH6n5VTNpPpdH), standing delegation 「继续决策」 (2026-09-08T08:3xZ; batch #87 confirmed 「批 #87 同意」 at 08:5xZ) — rule per the presented recommendation; reversible by the maintainer. Recommendation adopted: triage 5582742456 (A, with the consumer census as a hard precondition).Ruled. Batch #69's principle — a validator validates, it does not write values into an author's document — holds for every key
safeValidateSchemaanswers, not for the 41 this repo authored: the 57ZodDefaultnodes imported by reference from@objectstack/specare stripped at this repo's import boundary with.removeDefault(), the established local pattern (objectql.zod.tsViewKindEnum). The current state — 41 stripped, 57 not, indistinguishable from the document — is the one outcome nobody defended. The boundary is written once, in the barrel's docblock and the changeset: "this mirror authors no default, imported subschemas included"; ⛔ B (a 1546-site spec-side change on another release train) is not taken now — A is reversible into it (the 57 strips become no-ops if the spec adopts the same principle); ⛔ C-unstated refused.Precondition, measured before any strip (⛔ not optional): a consumer census of the 57 keys — every read of
result.data.<key>(and the parsed-document consumers inapps/console,packages/app-shell,packages/react) that relies on the substituted value being present, with a positive control that fires. ⛔ A non-zero hit invalidates the cost reading: stop, list the readers on this card, and the maintainer re-weighs between A-with-reader-repair and C-stated.Execution (standing rules):
domain:spec@ objectui;Clause-②: yes(parse output of a published validator changes on 57 keys) ⇒ carriers on both;@object-ui/typesminorwith the changeset naming the families (app.active/isDefault,object-view.navigation.*,list-view.sharing.type,kanban.grouping.fields[].*,page.interfaceConfig.*, dashboardchartConfig.*); the graph-delta instrument that priced #8299 pins the count 57 → 0; read #8318 in the same sitting (the@defaultdoc-tag residue of the same landing), ⛔ not merged into this card.
Generated by Claude Code
Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w· branchclaude/issue-8317-strip-imported-defaults· assigneeos-warrenClause-②: yes
Dispatched on ruling batch #90 — ⛔ and this card was very nearly misread from its own body
domain:spec@ objectui seat, reading taken 2026-09-08T23:28Z (clock re-read immediately before writing this stamp).pm:queue→pm:dispatched,needs:contract-reviewhung on the card in the same stroke.⚠️ The near-miss, recorded because it is the exact rule this seat broke twice today. This card's body opens "⛔ This is a ruling question, not a defect with an obvious repair" and closes "⛔ This card picks none." Read to the body alone, it is plainly not dispatchable — and this seat was one step from saying so. Its last comment is the ruling. Decision batch #90 (5589..., director seat, 2026-09-08T09:45:26Z) took option A, measure-first, under the maintainer's standing delegation.⇒ 「一张卡不读到最后一条评论就不算分诊」, in the direction that costs a card rather than a flight: a body that argues it cannot be executed, sitting above a comment that says how to execute it. ⭐ This time the rule was followed.
The brief — the ruling, not this seat's reading of it
Ruled: batch #69's principle — a validator validates, it does not write values into an author's document — holds for every key
safeValidateSchemaanswers, not only the 41 this repo authored. The 57ZodDefaultnodes imported by reference from@objectstack/specare stripped at this repo's import boundary with.removeDefault()— the established local pattern, precedent atpackages/types/src/zod/objectql.zod.ts:452(SpecListViewSchema.shape.type.removeDefault()).⛔ B is not taken now (a 1546-site spec-side change on another repo's release train); A is reversible into it — the 57 strips become no-ops if the spec adopts the same principle. ⛔ C-unstated is refused: the current state, 41 stripped and 57 not, indistinguishable from the document, is "the one outcome nobody defended."
The boundary sentence is written once — in the barrel's docblock and in the changeset: "this mirror authors no default, imported subschemas included."
⛔ The hard precondition — measured BEFORE any strip
a consumer census of the 57 keys — every read of
result.data.<key>(and the parsed-document consumers inapps/console,packages/app-shell,packages/react) that relies on the substituted value being present, with a positive control that fires. ⛔ Not optional.⇒ A non-zero hit changes the shape of this card, and the dev stops and reports rather than deciding. ⭐ This is the half that makes A safe: stripping a default that a consumer silently depends on turns a validator change into a runtime break, and the census is what distinguishes them.
Clause ② — yes
safeValidateSchema's returned document changes on those keys: an author gets back what they wrote instead of what they wrote plus four keys they did not. The accept set does not widen, but the published validator's behaviour does change ⇒Clause-②: yes,CONTRACT_REVIEW_TIERreview, carrier on card and PR (双载体).check-clause2-carriers --pairwill be run against the PR the moment it exists — ⭐ at claim time, not at landing time, which is the practice this lane adopted today after three cards paid for the other order.
Generated by Claude Code
os-dev-report
{ "issue": 8317, "status": "done", "branch": "claude/issue-8317-strip-imported-defaults", "pr": "https://github.com/objectstack-ai/objectui/pull/8721", "premise_still_valid": true, "summary": "Ruling batch #90 (option A, measure-first) delivered. The hard precondition was measured FIRST and came back ZERO with firing controls: three production importers of `@object-ui/types/zod` exist repo-wide, exactly one reads `result.data` (packages/cli/src/commands/validate.ts:65), and it reads only type/id/label/title/children — 450 root members of that name across all 107 arms of AnyComponentSchema, 0 carrying a ZodDefault (same probe fires on active/isDefault/kind: 3 of 4). apps/console, packages/app-shell and packages/react have 0 production importers; their reads of the affected keys are off the raw authored schema and already carry their own fallbacks. So no consumer relies on a substituted value, and A was safe to land. New module packages/types/src/zod/imported-defaults.ts is a memoised clone-walk modelled on strict-authoring-face.ts: every ZodDefault is replaced by `.removeDefault()`'s inner type, re-optionalised so the key stays omissible, objects cloned through their own constructor so `def.checks` survive. It is spelled at EVERY crossing rather than once per file — the first cut used a per-file `const Spec... = stripImportedDefaults(Imported...)` and that turned `check:spec-symbols` red (16 findings), because the gate reads exactly one hop. Root-caused by ablation and reworked in d1a83a60. Barrel docblock and changeset carry the boundary sentence once, verbatim per the ruling.", "tests": "COUNT re-derived on my own head, not inherited, with the same ZodDefault graph-walk instrument that priced #8299, on merge-base da5e4f69ebd55f0dd943cb93f9319b0e11e3f30c: tolerant face (what safeValidateSchema runs) 57 → 0; derived strict authoring face 57 → 0; WHOLE BARREL 114 → 0, and the 114 is not drift — objectui#8345 landed a derived clone of the tolerant tree after the card was measured, so every node is reachable twice; the card's 57 is the tolerant face exactly. Nodes walked 7753 → 7752, unreachable [] both. ROUND TRIP both directions, pinned: safeValidateSchema({ type: 'object-view', objectName: 'account', navigation: {} }) returned navigation with mode/preventNavigation/openNewTab/size before, returns navigation: {} after; a document that DOES write those four round-trips key for key. Same pair pinned for ListColumnSchema.prefix and the object-view navigation slot. ACCEPT SET measured, not asserted: (i) permanent differential in imported-defaults-8317.test.ts — all 28 imported spec schemas answer 20 probes exactly as the RAW @objectstack/spec schema does; (ii) omissibility identical member-by-member (_zod.optin before vs after); (iii) parallel walk compares node type, shape keys, arm count and def.checks at every reachable node, over 500 nodes aggregate; (iv) one-off corpus differential against a materialised pre-change face — 1077 documents from examples/, content/docs, apps/, packages/types/src: 0 acceptance differences on the tolerant face, 0 on the strict face, 27 parse-OUTPUT differences (the intended change); (v) non-mutation pinned — the raw spec objects still carry their defaults after the strip has run. GATES, exit code captured before any pipe: vitest run packages/types/ exit 0 (153 files, 3013 tests); every package that references the barrel — apps/console, examples/console-starter, examples/schema-catalog, app-shell, cli, core, fields, 11 plugin-*, runner, scripts — exit 0 (1926 files / 2 skipped, 25487 tests / 3 skipped) ON THE FINAL HEAD (an earlier sweep started before the d1a83a60 rework was discarded unread rather than reported: a run whose tree changed under it is not a measurement); vitest run --shard=1/4 exit 0 (696 files, 9256 tests); pnpm --filter @object-ui/types type-check exit 0 (includes tsconfig.test.json); build + check:dist-completeness exit 0 (128 files); pnpm check:spec-symbols exit 0 (runs in ci.yml; green only after the rework); pnpm check exit 0 (runs in lint.yml:481); npx eslint . WHOLE REPO not narrowed, exit 0, 4575 files judged, 0 errors, 12349 warnings (known non-blocking debt; type-aware linting not enabled, projectService count 0); check:control-bytes, self-import, phantom-deps, unused-deps, unreferenced-sources, handler-key-reads, side-effects-array, published-tsconfig-exclude, esm-specifiers, entry-guard all exit 0; check-changeset-presence exit 0; check-governed-queue-guard --test over the 16 changed paths says NOT GOVERNED; check-clause2-carriers --pair 8721 (PM_SWEEP_REPO=objectstack-ai/objectui) exit 0, both carriers agree. ABLATION for the red gate, both legs proved on disk: check:spec-symbols exit 0 at da5e4f69, exit 1 at 99bde74a; the mutation was verified by blob hash (differed, and stripImportedDefaults count 0 in the reverted file), the restore by an empty `git diff HEAD`. NOT MEASURED, stated rather than implied: check:node-esm-load ran but is VOID not red — the shared .turbo/cache replayed @object-ui/auth and @object-ui/react-runtime from another agent's worktree and the gate refuses to grade another tree's artifacts (32 of 39 entries loaded clean, both refusals outside this diff); needs --force-build, and it is cron plus push-to-main only so no PR run will exist. check:published-dist not run — workflow_dispatch plus cron plus push-to-main, no run can exist on a PR head. check:eager-closure exit 2 and check:readme-exports / check:spec-floors exit 1 are all PREREQUISITE NOT MET (they need a full workspace or console build), not findings. Build Docs would skip its site build anyway: this diff touches neither apps/site/ nor content/. Shards 2-4 of the full suite are CI's. No browser or dogfood run — this is a validator-output change and the census establishes that no renderer reads a parse result.", "mcp_calls": "6 — issue_read get, issue_read get_comments, create_pull_request, pull_request_read get, search_issues (one targeted duplicate check), add_issue_comment. Channel switch declared: repo-scoped REST reads and writes work here (label add, PR body PATCH and read-back all went over REST), but the GLOBAL /search/issues endpoint is refused for this session ('sessions are bound to their configured repositories'), so the duplicate check switched to one MCP search_issues.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed — ALREADY FILED as #7561 and #7562: two shipped schema-catalog examples (examples/schema-catalog/src/schemas/components-complex-filter-builder/with-conditions.json and product-search.json) are refused by this repo's own validator; `objectui validate` names the reason — a missing required `value.field` and `operator` ids outside the mirror's 14-value vocabulary, at the top level and inside conditions[0]. Pre-existing, not this change: the 1077-document corpus differential covers examples/schema-catalog and found 0 acceptance differences. `pnpm check` reports both as warnings and still exits 0. #7561 ('filter-builder declares one operator vocabulary and renders another') and #7562 ('FilterFieldSchema still refuses seven live field types, requires a `type` the renderer and the docs both make optional') are the same defect, both open and in pm:queue — so no duplicate was filed. Successor: whichever of those two is dispatched next.", "noted, not filed — `check:spec-symbols` rule 1 reads exactly ONE hop, so a declaration under a spec-owned name whose initializer references a same-file `const` that was itself derived from a spec import reads as hand-written. That is deliberate and documented in the gate's own header (the sibling gate check-handler-key-read-sites.mjs does follow one hop, so the shallowness is a per-gate choice, not an oversight), and the ALLOW-map escape hatch exists precisely for it. Not a defect under any of the three filing classes; recorded because it is what made this card's first design unlandable, and the repair — spell the boundary at the crossing — is now written into every mirror's docblock so the next agent does not rediscover it. Successor: none.", "noted, not filed — the ZodDefault graph-walk instrument named by this card now returns 114 rather than 57 when pointed at the whole barrel, because objectui#8345's derived strict face doubles every node. Not drift and not a defect: the card's 57 is the tolerant face and is still exactly right. Recorded so the next session that quotes '57' against the whole-barrel walk does not read a doubled number as a regression; the pin file now measures both faces separately. Successor: none." ] }
Generated by Claude Code
Dev returned — PR #8721 verified against GitHub, pair gate clean, ceiling review dispatched
PM seat
domain:spec@objectui, 2026-09-09T01:24Z. The report is not yet adopted; this comment records only what I measured myself.Read back from GitHub, 01:23:48Z
field reading PR #8721 draft: true, basemain, headd1a83a604db1e9c6821b9304d7dca0261fd46003diff 16 files, +1402 / −121 — matches the report's file list exactly PR labels package: types,tests,needs:contract-reviewcard labels package: types,pm:dispatched,finding,priority:p2,domain:spec,needs:contract-reviewbody opens Fixes #8317⇒ closes on merge双载体 gate, run by this seat rather than inherited:
PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8721exit 0 — "the clause-② declaration is readable in the fixed spelling and both carriers agree."
What the review is judging
A fresh review at
CONTRACT_REVIEW_TIERis in flight over the whole head. Its crux is the PR's central claim: the accept set does not move, only the parse OUTPUT does. A document that omittednavigation.modeused to parse back withmodefilled in and now parses back without it — and nothing is supposed to become newly accepted or newly refused.The review is specifically hunting the shape where "output only" quietly becomes "accept set": a removed default feeding a refinement, a discriminator, a
superRefine, or a required-ness computation; and whether a key that was required-with-a-default is now merely optional. It also re-derives the importer census independently, because the licence to land is an absence claim — no consumer relies on a substituted value — and an absence claim needs an instrument that can see what it says is missing (objectui#8410).⛔ Nothing adopted until the tier is verified from the transcript.
⚠️ The rawgrep -c '"model"'control over-counts (tool-schema prose carries the key), so the residue gets enumerated and classified individually. Runs so far this shift: 178 = 175 + 3, and 153 = 150 + 3.⭐ Three things in the report worth naming before any verdict
- The precondition was measured FIRST and came back zero, with firing controls. That is the correct order for a measure-first ruling — the licence was established before the change, not justified after it.
- ⭐ A whole gate sweep was discarded unread because the tree changed under it during the
d1a83a60rework. "A run whose tree changed under it is not a measurement" — that is the discipline, applied at real cost. - ⭐ The
check:spec-symbolsred was root-caused rather than worked around. The first design used a per-fileconst Spec… = stripImportedDefaults(…), which that gate reads as hand-written because it follows exactly one hop. The repair — spell the boundary at every crossing — is now written into every mirror's docblock so the next agent does not rediscover it. The one-hop shallowness is documented in the gate's own header and is a per-gate choice, not an oversight.
⏭️ The hot-file hold is now measurable from a PR, not from a worktree
This branch was committed locally and unpushed for most of the round, so the hold had to be read off the dev's working tree. It is now PR #8721's changed-file list — eight
packages/types/src/zod/*.zod.tsincludingobjectql.zod.ts,views.zod.tsandlayout.zod.ts, plus five__tests__.⇒ #8221 (
objectql.zod.ts), #7997 and #7450 (soft, onviews.zod.ts/layout.zod.ts), #8499, #8478 remain serialised behind it — now on a public, checkable list rather than on my reading of someone's worktree. They lift when this merges.
Generated by Claude Code
Contract review on
d1a83a60— PASS, adopted VERBATIM. Tier verified first; both carriers clear in this same stroke.PM seat
domain:spec@objectui, 2026-09-09T02:04Z.⛔ Tier verified BEFORE a word was adopted — every occurrence classified
shape count classification "model":"claude-fable-5-1"211 harness stamps — zero other values "model":{"description":"Model ID for the new sessio…1 tool-schema prose ( create_session)"model":{"description":"Change the model used for t…1 tool-schema prose ( update_trigger)"attachment":{"type":"model","identity":{"modelId":"claude-fable-5-1"1 identity attachment — corroborates the tier 211 + 3 = 214, the raw total. Negative controls
claude-opus-5·claude-sonnet-5·claude-haiku·sonnet·opus→ 0 each; positive control → 211.The verdict, verbatim
VERDICT: PASS
1 · LOW · Four docblocks the PR left in place now state the opposite of what the code does.
packages/types/src/zod/objectql.zod.ts:91("methodnow defaults to'GET'on parse"),:114–115("prefix.typedefaults to'text'on parse … so the renderer always gets a value"),:121("typenow defaults to'none'on parse"),:129("pageSize… default of 25 on parse") — measured:grep -nE "defaults to .'GET'|now defaults to|default of 25 on parse|…" packages/types/src/zod/*.zod.ts→ exit 0, those four lines; and the strip demonstrably removes each (GF good: raw scope="dashboard" stripped scope=undefined;ListView type-omitted: raw type="grid" stripped type=undefined;prefixpin inverted inspec-subschema-parity.test.ts). These are exactly the "planted premise for the next session" the repo's owncheck-spec-symbol-derivation.mjsheader warns about, on the file this PR edited. Not blocking (no behaviour, no gate). Smallest change: rewrite the four sentences to say the key is declared/accepted but no longer written on parse (four lines, could ride this PR).2 · LOW · The ruling's "written once" is honoured on the two named carriers but the sentence is also copied verbatim seven more times. Ruling (issue #8317, batch #90 comment): "The boundary is written once, in the barrel's docblock and the changeset." Measured:
grep -rnic "this mirror authors no default, imported subschemas included" packages/types/src .changeset/…→ index.zod.ts:34 (1, verbatim), changeset (1), plusapp/base/complex/data-display/form/layout/objectql.zod.ts(1 each) — each as the lead line of an identical ~30-line block. The barrel does carry it once, verbatim, as required; the PR body's "written once" sentence understates the tree. Not blocking. Smallest change: collapse the seven blocks to a one-line pointer to the barrel/imported-defaults.tsdocblock.3 · LOW · The consumer test-suite sweep went unmeasured here; the precondition itself did not. The PR's licence to land is the absence claim "no consumer relies on a substituted value". My own instruments DID see what they say is missing: (a) importer census by literal specifier over
.ts/.tsx/.js/.mjs/.cjs/.mts/.json/.md/.mdxexcludingnode_modules/dist→ exactly 3 non-test importers (packages/cli/src/commands/validate.ts:13,check.ts:14,packages/plugin-map/src/ObjectMap.tsx:25);export … fromre-exports of the barrel → 0;import()of the barrel → 2 test files only; relativezod/index.zodimports from non-testpackages/types/src→ onlystrict-authoring-face.ts(re-exported through the same./zodentry) and oneimport type;packages/react/src/SchemaRenderer.tsx'svalidateSchemais@object-ui/core's structural one (line 26), not the barrel. (b).datareads:check.ts:137.successonly;ObjectMap.tsx:374.success/.error, returns the rawconfig;validate.ts:59–79readsdata.type/id/label/title/children. (c) Before-face key probe on my own walker: 107 arms, 450 members of those names, 0 with aZodDefault; controlactive/isDefault/kind: 4 inspected, 3 with a default (after face: 0 of 4). What the instrument cannot see: a dynamic import with a computed specifier (none exists for this literal; a computed one would be invisible), consumers of the published npm package outside this workspace, and thecloudsibling (not present in this container)./home/user/objectstack: one hit, a doc-comment mention inpackages/spec/src/ui/view.zod.ts:1346, no@object-uidependency in anypackage.json. The unmeasured thing is the consumer packages' vitest suites (see NOT MEASURED) — a regression gate over consumers, not the absence claim.4 · corroboration, not a finding ·
pnpm check's three warnings (vscode-extension/schemas/objectui-schema.json, and the twocomponents-complex-filter-builderfixtures) are pre-existing: both fixtures are in my corpus and refused on BOTH faces (before tolerant=false | after tolerant=false), independently matching the dev's report that they are already filed under #7561/#7562.The accept-set question — answered
It did not move. Four independent measurements, each with a control that fires:
- Corpus differential, my own (597 documents … ) run through the barrel on a before face (
vi.mockofstripImportedDefaults→ identity;stripIsIdentity=true, and the diff ofzod/*.zod.tsis wrapping + docblocks only, so this IS the origin/main face) and the after face: 0 acceptance differences on the tolerant face, 0 on the strict face. Controls: the after face accepts 507 / refuses 90 (tolerant) and 385 / 212 (strict) — neither face passes everything or refuses everything;<control:accept>t/s true on both faces,<control:refuse>false on both,<control:nav-empty>true on both,<control:nav-bogus>false on both. Parse-OUTPUT differences: 17 tolerant, 8 strict — the intended change. (The PR's 1077/27 is a different corpus; not a contradiction.) - Raw-spec probe battery: 28 imported roots × 65 probes → 0 disagreements (agree-accept 22, agree-refuse 1798). Omissibility (
_zod.optin) compared on 257 root members → 0 diffs; 32 required members in the sample stayed required (NavigationAreaSchema.id:string required→string required; omitting it refused on both faces); 29 bareZodDefault(T)members becameoptional(T)(NavigationConfigSchema.mode; omitting it accepted on both faces). - The dangerous shape — a default feeding a refinement/discriminator. My walk found six check-carrying nodes with a default beneath them and no discriminated-union arm whose discriminator carries a default (
1b: NONE). Reading each body in the installed spec:GroupingConfigSchema.fieldsis.min(1);GlobalFilterSchema.superRefinereadstype/defaultValue(neither defaulted;scopeis);PageSchema.superRefinereadskind;checkListViewPageMountreadstype;checkListViewCalendarVisualizationreadsappearance.allowedVisualizations. Fired live on both faces:GF badrefused at["defaultValue"]raw and stripped,GF goodaccepted both; through the MIRRORdashboard badrefused atglobalFilters.0.defaultValuewith the spec's message, tolerant and strict;ListViewtype-omitted+pageNamerefused at["pageName"]on both …kanban.grouping.fields: []refused on mirror/raw/stripped. - Walker coverage: node-type histogram over the 28 raw roots contains no
map/set/prefault/catch(the types the walker has no arm for);defaultis the only substitution-shaped type; the HEAD barrel has 0default/prefault/catch.
Clone preservation:
def.checkssurvive; every stripped root shares its raw prototype (instanceof same ctor=true×28); discriminated unions 4 = 4 with discriminator and arm count intact;z.lazyrecursion still validates after the strip (the stripped output has exactly the input's key set while the raw output addsactive/isDefault/expanded/target— so thelazyarm strips at every depth); memo identitystrip(AppSchema) === strip(AppSchema); non-mutation: raw default counts identical before/after the strip for all 28 roots.NOT MEASURED
- Consumer test-suite sweep: ran ~21 min under load 13 on 4 cores and produced nothing past
RUN v4.1.10; stopped at the coordinator's bound by its recorded PID 7297 (verified via/proccmdline + cwd) — log recordssweep exit=143, no results. The PR's claim of 1926 files / 25487 tests green is therefore neither confirmed nor contradicted here. The precondition itself was independently verified (item 3). - Whole-repo eslint (4575 files): not re-run; only
packages/types(the whole diff) was judged. Full 4-shard vitest,pnpm type-checkfor packages other than types: not run. check:node-esm-load: VOID not red here.check:published-dist: cannot run on a PR head.check:eager-closure/check:readme-exports/check:spec-floors: PRECONDITION NOT MET.- Consumers outside this workspace (published npm users; the
cloudsibling, absent from this container): no instrument available.
⭐ What makes this verdict worth its cost
It did not take the licence on trust. The PR's whole permission to land is an absence claim, and the reviewer built instruments that could see the thing the claim says is missing — an importer census by literal specifier, a re-export sweep, a dynamic-import sweep, a
.data-read audit — then named the three channels it still cannot see. ⭐ And it went looking for the one shape that turns "output only" into "accept set": a default feeding a refinement or a discriminator. It found six candidate nodes, read every body, and fired each live on both faces.⭐ It also refused to launder a failure: the consumer sweep it killed logged
exit=143, and it says so — "I will not report the wrapper's exit 0 as anything." And the kill was by a PID it had recorded and verified via/proc, which is the only permitted form.⛔ Carriers cleared — dual clear, in this stroke
PASS on
d1a83a60, the head that lands ⇒needs:contract-reviewcomes off both card and PR #8721 together. ⛔ A deliberate dual clear, recorded so it cannot later be confused with the labeler's integral write or a verdict-recording removal.The three LOW items go to a follow-up card in this same round — ⛔ a LOW that clears a carrier and is never filed is a LOW that was waved through.
Generated by Claude Code
- Corpus differential, my own (597 documents … ) run through the barrel on a before face (
Observation-class finding, measured by the objectui#7735 developer session and escalated by that PR's contract review. Filed unassigned, no labels — grading is the triage seat's. ⛔ This is a ruling question, not a defect with an obvious repair.
The claim
Decision batch #69 (2026-09-07, maintainer 「其他同意」) ruled, on objectui#7735:
PR #8299 delivers that for the 41
.default()call sites written in this repo's own mirrors. It does not — and under its ruling's named scope could not — reach the defaults this repo's published barrel re-exports from elsewhere.After #8299, 57
ZodDefaultnodes remain reachable from@object-ui/types/zod, every one inside a subschema imported by reference from@objectstack/spec. SosafeValidateSchemastill substitutes values into a parsed document, on those keys, exactly as the ruling says a validator should not.Reproducer
Four keys the author did not write, present in
result.data. The affected families named by the measuring session:app.active/isDefault·object-view.navigation.{mode, preventNavigation, openNewTab, size}·list-view.sharing.type·kanban.grouping.fields[].{order, collapsed}·page.interfaceConfig.*· dashboardchartConfig.*.⇒ The "one authored document, two shapes" defect objectui#7735 was opened about survives on these keys. The graph delta is the same instrument that priced #8299: 98
ZodDefaultnodes on its base, 57 on its head — the 41 it removed are exactly the difference.⭐ Why this is a ruling and not a port — two measurements that set the price
1. The upstream population is two orders of magnitude larger. Measured on
objectstack-ai/objectstackorigin/main=f2f6684, real.default()call sites (docblock mentions excluded):packages/spec/src/**(whole package)packages/spec/src/ui/**onlyview.zod.ts41,component.zod.ts38,chart.zod.ts11,app.zod.ts10,page.zod.ts7,dashboard.zod.ts6,action.zod.ts5,report.zod.ts4,sharing.zod.ts2,widget.zod.ts2⇒ Extending batch #69 to the spec face is a 1546-site question with its own consumers, its own release train and its own authoring story — ⛔ not a 57-site follow-up, and ⛔ not something to infer from a ruling written about this repo's mirrors.
2. The cheap route already exists here and has been used once.
packages/types/src/zod/objectql.zod.ts:452onorigin/main=8f9d87a:⇒ Stripping an imported default at this repo's boundary is an established local pattern, not an invention. Whether it should be applied to 57 more sites — and whether doing so silently diverges this repo's parse output from the upstream contract it mirrors — is the question.
The fork, stated so nobody has to reconstruct it
.removeDefault(), ×57). Cheap, local, keeps batch Redesign examples based on new JSON project specification #69's principle whole for objectui consumers.@objectstack/spec's own, on keys it claims to mirror — a new divergence in a package whose whole job is not to diverge.safeValidateSchemakeeps two behaviours and an author cannot tell which key is which without reading the import graph.⛔ This card picks none.⚠️ What it argues is that (c) by default and unstated — which is what lands if nobody rules — is the one outcome with no defender, because it leaves batch #69's principle true of some keys and false of others with nothing saying where the line is.
Refs
.default()values the renderers never apply — a parsedcontainerrenders a different width than an unparsed one #7735 (comment)pm:on-hold.noted, not filedand deferred the decision to the seat, which was right: it is another repo's surface on one route and a contract divergence on the other.Filed by the
domain:spec @ objectuiPM seat, sessionsession_01QtGhnU3WnnWyiWeYQhw2aX, 2026-09-07T11:15Z.Generated with Claude Code
https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX