Skip to content

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

@os-zhuang

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:

A validator validates; it does not write values into an author's document.

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 ZodDefault nodes remain reachable from @object-ui/types/zod, every one inside a subschema imported by reference from @objectstack/spec. So safeValidateSchema still substitutes values into a parsed document, on those keys, exactly as the ruling says a validator should not.

Reproducer

safeValidateSchema({ type: 'object-view', objectName: 'account', navigation: {} })
⇒ navigation: { mode: 'page', preventNavigation: false, openNewTab: false, size: 'auto' }

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.* · dashboard chartConfig.*.

⇒ 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 ZodDefault nodes 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/objectstack origin/main = f2f6684, real .default() call sites (docblock mentions excluded):

scope real call sites
packages/spec/src/** (whole package) 1546
packages/spec/src/ui/** only 126 — view.zod.ts 41, component.zod.ts 38, chart.zod.ts 11, app.zod.ts 10, page.zod.ts 7, dashboard.zod.ts 6, action.zod.ts 5, report.zod.ts 4, sharing.zod.ts 2, widget.zod.ts 2

⇒ 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:452 on origin/main = 8f9d87a:

const ViewKindEnum = SpecListViewSchema.shape.type.removeDefault();

⇒ 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

  • (a) Strip at this repo's import boundary (.removeDefault(), ×57). Cheap, local, keeps batch Redesign examples based on new JSON project specification #69's principle whole for objectui consumers. ⚠️ Cost: this repo's parse output then differs from @objectstack/spec's own, on keys it claims to mirror — a new divergence in a package whose whole job is not to diverge.
  • (b) Raise it upstream as a spec-side ruling. Principled and one answer for everyone. ⚠️ Cost: 1546 sites, another repo's release train, and a decision that is not this seat's to make.
  • (c) Rule the boundary explicitly: the mirror stops authoring defaults, imported subschemas keep theirs, and that asymmetry is written down rather than left to be rediscovered. ⚠️ Cost: safeValidateSchema keeps 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

Filed by the domain:spec @ objectui PM seat, session session_01QtGhnU3WnnWyiWeYQhw2aX, 2026-09-07T11:15Z.


Generated with Claude Code

https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX

Activity

  1. added theissue type on Sep 8, 2026
  2. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    ContributorAuthor

    分诊路由 — 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:452 SpecListViewSchema.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) 的理由:

    1. ⭐ (a) 不封闭 (b)。 若 spec 日后采纳同一原则,那 57 个 .removeDefault() 自动变成 no-op —— ⛔ 不需要回滚,不需要迁移。这是一个可逆的动作。
    2. 手法是现成的,不是发明的 —— objectql.zod.ts:452 已经这么做过一次。

    ⚠️ 同时必须做 (c) 的那一半:无论选哪条,边界都要写下来。⇒ 若选 (a),写下的是「本仓的 mirror 不作者化任何默认值,包括从 spec 导入的子 schema」——一句话,且从此不再有边界可写错。


    ⚠️ 强制置信缺口 —— 本推荐最可能错的两处

    1. ⭐⭐ ⛔ 没有人测过是否有消费者「依赖」那些被塞进去的值。
      safeValidateSchema 今天会把 navigation.mode = 'page' 等写进 result.data。若本仓或下游有代码读 result.data.navigation.mode 并指望它有值,(a) 会让它拿到 undefined。
      ⇒ ⚠️ 这是 (a) 落地前必须补的读数,⛔ 不是可选的:对那 57 个键各扫一次消费点,带阳性对照。若命中非零,(a) 的代价评估作废,需重新权衡。

    2. 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 同族。

    ⛔ 邻卡


    Generated by Claude Code

  3. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    ContributorAuthor

    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 safeValidateSchema answers, not for the 41 this repo authored: the 57 ZodDefault nodes imported by reference from @objectstack/spec are stripped at this repo's import boundary with .removeDefault(), the established local pattern (objectql.zod.ts ViewKindEnum). 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 in apps/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/types minor with the changeset naming the families (app.active / isDefault, object-view.navigation.*, list-view.sharing.type, kanban.grouping.fields[].*, page.interfaceConfig.*, dashboard chartConfig.*); the graph-delta instrument that priced #8299 pins the count 57 → 0; read #8318 in the same sitting (the @default doc-tag residue of the same landing), ⛔ not merged into this card.


    Generated by Claude Code

  4. os-warren commented on Sep 8, 2026

    @os-warren
    Collaborator

    Claim: session session_01Jmxdo7bmeqCQHLSfmLVX9w · branch claude/issue-8317-strip-imported-defaults · assignee os-warren

    Clause-②: 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-review hung 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 safeValidateSchema answers, not only the 41 this repo authored. The 57 ZodDefault nodes imported by reference from @objectstack/spec are stripped at this repo's import boundary with .removeDefault() — the established local pattern, precedent at packages/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 in apps/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_TIER review, carrier on card and PR (双载体).

    check-clause2-carriers --pair will 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

  5. self-assigned this
    on Sep 8, 2026
  6. os-warren commented on Sep 9, 2026

    @os-warren
    Collaborator

    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

  7. os-warren commented on Sep 9, 2026

    @os-warren
    Collaborator

    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, base main, head d1a83a604db1e9c6821b9304d7dca0261fd46003
    diff 16 files, +1402 / −121 — matches the report's file list exactly
    PR labels package: types, tests, needs:contract-review
    card labels package: types, pm:dispatched, finding, priority:p2, domain:spec, needs:contract-review
    body 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 8721
    

    exit 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_TIER is 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 omitted navigation.mode used to parse back with mode filled 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 raw grep -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

    1. 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.
    2. ⭐ A whole gate sweep was discarded unread because the tree changed under it during the d1a83a60 rework. "A run whose tree changed under it is not a measurement" — that is the discipline, applied at real cost.
    3. ⭐ The check:spec-symbols red was root-caused rather than worked around. The first design used a per-file const 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.ts including objectql.zod.ts, views.zod.ts and layout.zod.ts, plus five __tests__.

    ⇒ #8221 (objectql.zod.ts), #7997 and #7450 (soft, on views.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

  8. os-warren commented on Sep 9, 2026

    @os-warren
    Collaborator

    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 ("method now defaults to 'GET' on parse"), :114–115 ("prefix.type defaults to 'text' on parse … so the renderer always gets a value"), :121 ("type now 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; prefix pin inverted in spec-subschema-parity.test.ts). These are exactly the "planted premise for the next session" the repo's own check-spec-symbol-derivation.mjs header 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), plus app/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.ts docblock.

    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/.mdx excluding node_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 … from re-exports of the barrel → 0; import() of the barrel → 2 test files only; relative zod/index.zod imports from non-test packages/types/src → only strict-authoring-face.ts (re-exported through the same ./zod entry) and one import type; packages/react/src/SchemaRenderer.tsx's validateSchema is @object-ui/core's structural one (line 26), not the barrel. (b) .data reads: check.ts:137 .success only; ObjectMap.tsx:374 .success/.error, returns the raw config; validate.ts:59–79 reads data.type/id/label/title/children. (c) Before-face key probe on my own walker: 107 arms, 450 members of those names, 0 with a ZodDefault; control active/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 the cloud sibling (not present in this container). /home/user/objectstack: one hit, a doc-comment mention in packages/spec/src/ui/view.zod.ts:1346, no @object-ui dependency in any package.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 two components-complex-filter-builder fixtures) 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.mock of stripImportedDefaults → identity; stripIsIdentity=true, and the diff of zod/*.zod.ts is 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 bare ZodDefault(T) members became optional(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.fields is .min(1); GlobalFilterSchema.superRefine reads type/defaultValue (neither defaulted; scope is); PageSchema.superRefine reads kind; checkListViewPageMount reads type; checkListViewCalendarVisualization reads appearance.allowedVisualizations. Fired live on both faces: GF bad refused at ["defaultValue"] raw and stripped, GF good accepted both; through the MIRROR dashboard bad refused at globalFilters.0.defaultValue with the spec's message, tolerant and strict; ListView type-omitted+pageName refused 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); default is the only substitution-shaped type; the HEAD barrel has 0 default/prefault/catch.

    Clone preservation: def.checks survive; every stripped root shares its raw prototype (instanceof same ctor=true ×28); discriminated unions 4 = 4 with discriminator and arm count intact; z.lazy recursion still validates after the strip (the stripped output has exactly the input's key set while the raw output adds active/isDefault/expanded/target — so the lazy arm strips at every depth); memo identity strip(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 /proc cmdline + cwd) — log records sweep 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-check for 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 cloud sibling, 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-review comes 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

  9. removed their assignment
    on Sep 9, 2026
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

    domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanefindingpackage: typespriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions