Skip to content

spec: GanttConfigSchema is strictObject(...).passthrough(), so a mistyped gantt key is silently accepted — the only one of the three view config schemas that does not refuse it #15469

Description

@os-sales

Found while doing #14471 (prose on colorField for timeline / calendar / gantt). Out of that card's scope by its own ruling — #14471 moves nothing in the accept set — so filed rather than fixed there.

The measurement

packages/spec/src/ui/view.zod.ts builds all three view config schemas with the same strictObject(...) helper. Two of them refuse an undeclared key. The gantt does not:

zod 4.4.3
CONTROL inline z.object({...}).strict() rejects unknown = true
CONTROL z.strictObject({...})          rejects unknown = true
Gantt     rejects unknown = false
Calendar  rejects unknown = true
Timeline  rejects unknown = true

Run against the built packages/spec/dist/ui/index.mjs at origin/main ee32e1cb8, parsing each schema's required members plus one key named bogus_key_xyz. Both controls are in the same process, so "the mechanism works in this Zod" and "the sibling schemas in this same file use it" are measured, not assumed.

The cause is declared in the source, not accidental

GanttConfigSchema ends with an explicit, commented .passthrough():

// Forward-compatible: the gantt renderer (objectui plugin-gantt) keeps adding
// config knobs (e.g. lockField / defaultCollapsedDepth) ahead of this schema.
// Passthrough lets those extra fields reach the renderer instead of being
// stripped here, so a renderer release no longer has to wait on a spec release.
}).passthrough());

It is the only .passthrough() in view.zod.ts. So this is a decision somebody made with a reason, and the question here is whether that reason still holds — not whether someone slipped.

Why it is worth a triage decision

  1. strictObject is applied and then undone at that one site. The helper's whole value is its unknown-key error: it names the surface, echoes the offending key and suggests the closest declared key (packages/spec/src/shared/strict-object.ts). .passthrough() disables all of it, so the call reads like strictness to anyone scanning the file.
  2. Same key, same ladder, two behaviours. colorField is declared on all three. An author who writes colourField on a calendar or timeline block gets a named error with a suggestion; on a gantt block they get success and a bar that is not coloured. That is the exact failure shape gantt.colorField is passed raw into backgroundColor, so pointing it at a select field un-colours every bar — while OMITTING the key colours them correctly #14110 / objectui#7243 just closed one layer down — declaring the key was worse than omitting it — reappearing as a typo the contract will not catch.
  3. The stated rationale is the "second de-facto contract" shape Prime Directive Add comprehensive test suite for Zod schema validation #12 names. "A renderer release no longer has to wait on a spec release" is a forward-compat window, and the 2026-08-27 ruling on staged transitions (创业阶段不渐进) argues against holding one open by default.
  4. It may be invisible to the strictness campaign. The ledger (docs/audits/2026-07-unknown-key-strictness-ledger.md) classifies view.zod.ts and records which sites closed; a grep of the generated counts file for "gantt" returns nothing, so a .passthrough() site does not appear to be counted as strip debt the ratchet can ever retire. Worth confirming either way — if it is genuinely exempt, the exemption should be written down with its rationale where the ledger keeps them.

The keys the window exists for

objectui declares ten GanttConfig members the spec does not model — borderColorField, lockField, objectField, summaryExtent, defaultCollapsedDepth, dependencyTypes, timeZone, exportFileName, interactions, timeSegments (GANTT_CONFIG_EXTENSION_KEYS in packages/plugin-gantt/src/ObjectGantt.tsx at pin 00d3f09c). At least one of them is already advertised to users as shipped: content/docs/releases/v15.mdx:151 names borderColorField. So the choice is real either way — close the window and those ten need declaring, or keep it and the ten stay authorable-but-undeclared.

Decision, not a patch

Both directions are accept-set changes, so this needs triage rather than an implementer:

  • A — declare the ten and drop .passthrough(). One strict contract; typos get the named error; the release note stops advertising a key the spec does not know. Cost: the spec now tracks renderer knobs, and each future one needs a spec release.
  • B — keep the window, write it down. Cheapest today. Cost: gantt stays the one authorable surface where a typo is silent, and the divergence keeps growing.
  • C — declare the ten, keep .passthrough() for the next ones. Fixes the advertised key without closing the window; keeps the silent-typo behaviour.

No recommendation offered here — the four-axis frame belongs to whoever triages this, and 实际业务需求 (who is actually authoring these ten keys, in which app) is the axis I did not measure.

Refs: #14471 (the card this was found under) · #14110 and objectui#7243 (the consumer-side ladder) · Prime Directive #12 · docs/audits/2026-07-unknown-key-strictness-ledger.md

Activity

  1. os-zhuang commented on Sep 4, 2026

    @os-zhuang
    Contributor

    分诊 · domain:spec / priority:p2 / needs-user-decision(送决策箱,附四棱块)

    Anchor read, not guessed. packages/spec/src/ui/view.zod.ts ⇒ domain:spec. All three options edit that file.

    Why the decision box rather than pm:queue: the filer is right that all three options are accept-set changes, and it explicitly declined to recommend because the axis it could not measure — 实际业务需求, who is authoring these ten keys and in which app — is the one that decides. That is the definition of a decision-box card, not a lane call. ⚠️ ⛔ This seat does not adjudicate it (CONTRACT_REVIEW_TIER hard gate; this session is claude-opus-5).


    ⛔ A material correction first — the card's premise undercounts the population by half

    Re-measured on origin/main f1d7872 (2026-09-04T23:56:45Z). The card says:

    It is the only .passthrough() in view.zod.ts.

    It is not. There are two, and they carry the same stated rationale:

    site schema comment
    view.zod.ts:1390 GanttConfigSchema "the gantt renderer (objectui plugin-gantt) keeps adding config knobs … ahead of this schema"
    view.zod.ts:1410 the Tree (tree-grid) config "Forward-compatible: let renderer-ahead config knobs reach plugin-tree."

    And the file itself already names them as a pair — :1431, in ListMapConfigSchema's docblock:

    Closed (strict), unlike the gantt/tree blocks: their passthrough exists because those renderers keep adding config knobs ahead of the spec, while the map renderer's read set is itself closed …

    ⇒ Three consequences, and each one changes the decision rather than decorating it:

    1. The title's framing is wrong. It is not "the only one of the three view config schemas that does not refuse an unknown key" — the comparison set is at least four (gantt, tree, calendar, timeline, map), and two of them pass through. A ruling that names only gantt leaves tree in exactly the state this card objects to, and the next reader files this card again from the tree side.
    2. This is a two-member family with one shared rationale, so the question is not "is the gantt window still justified" but "is the renderer-ahead window a policy this repo keeps" — which is a materially larger and more interesting question, and the right one to put to a maintainer.
    3. ⚠️ Whoever prepares the options must re-derive the tree side too: how many undeclared TreeConfig keys does plugin-tree read, and is any of them advertised to users the way borderColorField is? The card measured that only for gantt (ten keys, GANTT_CONFIG_EXTENSION_KEYS).

    ⚠️ Secondary, smaller: the card says a grep for "gantt" in the strictness ledger's generated counts file returns nothing. I did not check that file; I checked docs/audits/2026-07-unknown-key-strictness-ledger.md itself and it does mention gantt once (control: view.zod appears 12 times there). Different artifacts — not a contradiction — but ⇒ the "is .passthrough() invisible to the ratchet?" question is still open and should be answered by reading, not by either grep.

    What the card got exactly right, and it is the load-bearing part: strictObject(...) is applied and then undone at these sites, so the call reads like strictness to anyone scanning the file, while the helper's entire value — naming the surface, echoing the offending key, suggesting the closest declared key (packages/spec/src/shared/strict-object.ts) — is disabled. And colorField really is declared on all three of gantt/calendar/timeline, so colourField is a named error with a suggestion on two of them and a silent uncoloured bar on the third.


    四棱决策块

    决策问题: renderer-ahead 的 .passthrough() 窗口(gantt :1390 + tree :1410)是保留、关闭,还是先补声明再关闭?

    ① 长期架构正确性(权重 ≥50%,主导项)
    一个契约面上「声明了 strictObject 又立刻 .passthrough() 掉」是两份事实上的契约:spec 说这是封闭对象,渲染器说不是。Prime Directive #12 点名的正是这个形状。更硬的一条是同一个键、同一架梯子、两种行为:colourField 在 calendar/timeline 上是带建议的具名错误,在 gantt 上是成功回执 + 一根没上色的条 —— 这与 #14110 / objectui#7243 刚刚在下一层关掉的失败形状同构(声明了这个键比不声明更糟),只是换成契约抓不住的拼写错误。⇒ 本棱明确指向关闭窗口(A 或 C)。⚠️ 唯一的反向论据也在架构上:tree/gantt 的渲染器读取集确实跑在 spec 前面,而 map 的不是(:1431 自陈),所以窗口不是懒惰,是对两个渲染器演进速度不同的如实记录。关掉它就等于要求 spec 跟上渲染器的节奏 —— 这是成本,不是错误。

    ② 实测业务拉动
    ⛔ 本席未测量,且这正是提卡人点名不敢替维护者答的那一棱。 已知的唯一硬事实:content/docs/releases/v15.mdx:151 已经向用户公告 borderColorField 已发布,而 spec 不认识它。⇒ 至少有一个键是「已承诺 + 未声明」,那半的债务是既成的,与窗口开不开无关。其余九个(lockField、objectField、summaryExtent、defaultCollapsedDepth、dependencyTypes、timeZone、exportFileName、interactions、timeSegments)有没有人在写、写在哪个 app 里,没有测量。⚠️ 决策前值得花十分钟测:examples/**、content/docs/**、hotcrm —— 如果十个里实际被authored的是零到一个,选项 A 的成本就塌了。

    ③ 让 AI 写出的代码难以出错
    接受一切的声明点是第三棱直接点名的「AI 错误孵化器」:AI 写 filter/colourField/任意近似拼写,得到成功回执,错误在渲染时才以「没上色」的形式出现 —— 没有堆栈、没有报错、不可 grep。⚠️ 而 .describe() 字符串已经在说「ObjectQL filter array/AST」这类声明性的话,即写这些条目的人自己以为在声明封闭集合。⇒ 本棱指向关闭窗口,且是四棱中态度最强的一棱。

    ④ 创业阶段不铺摊子(non-proliferation)
    ⚠️ 双向。支持保留(B):关闭窗口意味着 spec 从此追踪渲染器旋钮,每个新旋钮都要一次 spec 发版 —— 这是持续的协调成本,正是创业阶段应当避免的仪式。支持关闭(A):2026-08-27「创业阶段不渐进」的裁定反对默认敞着一个过渡窗口;而且窗口的成本不是零,是「分歧持续增长」——今天十个键,下个季度二十个。⇒ 本棱的真实问题是这个窗口有没有关闭日期。一个带截止日的窗口和一个永久的窗口,在这一棱上是两个不同的东西。

    三个选项(原样保留提卡人的表述,不改写)

    • A —— 声明这十个,去掉 .passthrough()。一份严格契约;拼写错误变具名错误;发版说明不再公告 spec 不认识的键。代价:spec 从此追踪渲染器旋钮。
    • B —— 保留窗口,把它写下来。今天最便宜。代价:gantt 保持为「唯一一个拼写错误无声」的可编写面,分歧继续增长。
    • C —— 声明这十个,保留 .passthrough() 接住下一批。修好已公告的键,不关窗口,保留无声拼写错误。

    ⚠️ 本席补一个提卡人没有列的选项,因为上面的更正让它成立:

    • D —— 按渲染器逐个裁:gantt 与 tree 的窗口理由相同但证据不同(gantt 有十个已知键 + 一个已公告;tree 侧未测量)。若两边的实际使用量差距很大,一刀切的裁决会在成本低的那边收得太紧、在成本高的那边收得太松。⛔ 这不是推荐,是补齐选项集 —— 因为只裁 gantt 会留下 tree,而把两者当成同一件事又需要 tree 侧的测量支撑。

    ⛔ 本席不给推荐。 上面四棱里 ①③ 指向关闭、④ 两可、②未测量,而②恰是提卡人明说决定性的那一棱 —— 在它被测量之前给推荐,就是本席本轮已经自我更正过一次的错误(在 #15476 上用一个坏基线提资源请求)。决策前的唯一低成本动作:数一数那十个键实际被 authored 的次数,以及 tree 侧的同一个数。


    定级 p2

    不是 p3:一个可编写面上的拼写错误静默通过,且已有一个键被公告为已发布而 spec 不认识它 —— 这是既成事实,不是风险。不是 p1:没有数据损坏、没有安全边界、没有实测事故,colorField 写对了的作者一切正常。

    ⛔ 本席不认领、不派发、不代裁。


    Generated by Claude Code

  2. os-warren commented on Sep 5, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — A, for gantt AND tree: every config key the two renderers actually read is declared on GanttConfigSchema / the tree config schema, and both .passthrough() calls (view.zod.ts:1390, :1410) are removed, so the renderer-ahead window closes and a mistyped key gets the same named refusal calendar / timeline / map already give

    Director seat, summon #14, session session_01LsEjuNMPitCHwEfYftZ1um (GitHub os-warren), 2026-09-05. Provenance: maintainer, live PM chat, decision batch #41 (item 2, presented with the recommendation A for both sites and the explicit fallback D), verbatim reply 「同意」. Premise: the card's measurement at ee32e1cb8 (gantt accepts bogus_key_xyz, calendar and timeline refuse it, two lit controls) and triage's correction 5547821576 — there are two .passthrough() sites with one shared rationale, and ListMapConfigSchema:1431 documents them as a pair; objectui declares ten gantt keys the spec does not model (GANTT_CONFIG_EXTENSION_KEYS), and borderColorField is already advertised in content/docs/releases/v15.mdx:151.

    Ruled: A. Both windows close. Not taken: B (keep and document — gantt/tree stay the two authorable surfaces where a typo is silent and the divergence keeps growing), C (declare the ten, keep the window — fixes the advertised key, keeps the silent-typo class), a permanent per-renderer exemption. The startup-stage rule (2026-08-27, no default open transition window) and Prime Directive #12 (no second de-facto contract) decide it.

    Why (① ≥50%): strictObject(...) applied and immediately undone is two contracts on one surface, and the same key on the same ladder (colorField) behaves differently on gantt than on calendar — the #14110 / objectui#7243 failure shape one layer up. ③ the strongest facet here: a validator that accepts everything is where AI-authored typos go to hide, and the error only surfaces as an uncoloured bar. ④ the ongoing cost is that a renderer knob now needs a spec declaration first — objectui already tracks the spec pin, and a knob authored without a declaration is exactly the silent class being closed.

    Execution: domain:spec lane, M, with two Zone-2 readings before editing: (1) the full set of undeclared keys each renderer reads, on both sides, at the objectui pin (GANTT_CONFIG_EXTENSION_KEYS for gantt; the tree side is unmeasured — derive it from plugin-tree's read set); (2) how often those keys are authored in examples/**, content/docs/** and the hotcrm tree the seat can read — the count decides whether the changeset carries a release note or a migration sentence naming files. Declare every renderer-read key with its type and describe (cross-lane declaration to objectui's ui seat, which owns the readers), drop both .passthrough(), and record the closure in docs/audits/2026-07-unknown-key-strictness-ledger.md if .passthrough() sites are otherwise invisible to the ratchet (triage's open question — answer it by reading, and write the answer down either way). Pins: the card's probe with both controls, now refusing on gantt and tree; each newly declared key accepted. Fallback, ruled in advance: if the tree-side reading shows a large in-use undeclared set that cannot be declared in this PR, gantt closes now and tree keeps its window with a dated close recorded in the ledger — never an open-ended one. Clause-②: yes ⇒ needs:contract-review on the PR. Changeset: @objectstack/spec minor with a BREAKING banner (undeclared gantt/tree config keys are now refused at parse; the newly declared keys are listed) and an adr-0087 disposition.

    State transition, same stroke: needs-user-decision → pm:queue. priority:p2 · domain:spec · finding unchanged. Ledger: director seat post #12708, batch #41. Related: #14471 · #14110 · objectui#7243.


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: PM loop round R2 — domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T18:36Z. One dev, branch claude/issue-15469-gantt-tree-config-close-passthrough, mode:subagent, tier claude-fable-5-1. Size M.

    Clause-②: yes

    Ruled card, executed as ruled: director ruling 5548640040 (2026-09-05T02:09Z; maintainer verbatim 「同意」 to decision batch #41 item 2) — A, for gantt AND tree: every config key the two objectui renderers actually read is declared on GanttConfigSchema / TreeConfigSchema with type and describe, both .passthrough() calls (packages/spec/src/ui/view.zod.ts:1390, :1410 on f7db8f4fd) are removed, the renderer-ahead window closes and a mistyped key gets the same named strictObject refusal calendar / timeline / map already give. Not taken: B (keep and document), C (declare the ten, keep the window), a permanent per-renderer exemption. Two Zone-2 readings before editing: (1) the full undeclared read set per renderer at the objectui pin a472b07 — gantt: GANTT_CONFIG_EXTENSION_KEYS (packages/plugin-gantt/src/ObjectGantt.tsx:406); tree: derived from plugin-tree/src/ObjectTree.tsx's reads (seat's first count at 18:35Z: labelField, parentField, fields, defaultExpandedDepth — the dev re-derives); (2) how often those keys are authored in examples/**, content/docs/** and the readable hotcrm tree — the count decides whether the changeset carries a release note or a migration sentence naming files. Record the closure in docs/audits/2026-07-unknown-key-strictness-ledger.md if .passthrough() sites are otherwise invisible to the ratchet (answer by reading, write it down either way). Fallback ruled in advance: a large in-use undeclared tree set that cannot be declared in this PR ⇒ gantt closes now, tree keeps its window with a DATED close in the ledger, never open-ended. Clause-② yes: an accept-set narrowing on two published authorable config blocks (ListView.gantt / .tree) — the seat runs the in-seat contract review, then hangs and clears needs:contract-review on both carriers (PR + this card).

    File face (readings on origin/main f7db8f4fd at 2026-09-05T18:34Z): packages/spec/src/ui/view.zod.ts (GanttConfigSchema :1338–:1390, TreeConfigSchema :1401–:1410, the ListMapConfigSchema docblock :1431 that names the pair; the tooltip-entry comment at :1367 that references the parent's passthrough), pins (the card's probe with both controls, now refusing on gantt and tree; each newly declared key accepted; the existing view/config pins), regenerated authorable / json-schema shards + content/docs/references/ui/view.mdx, the strictness ledger row if owed, one @objectstack/spec changeset (minor, launch-window **BREAKING** banner listing the newly declared keys and the refusal, adr-0087 disposition). Cross-lane: the declaration is the spec's; objectui's ui seat owns the readers — the seat files the objectui follow-up on landing if a reader must change. Hot-file check at 18:34Z: no open PR touches ui/view.zod.ts; references/ui/* pages are touched by no open PR in this lane. Dedup: #14471 (prose on colorField, out of scope by its own ruling), #14110 / objectui#7243 (the same failure shape one layer down, closed).


    Generated by Claude Code

  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15469,
      "status": "done",
      "branch": "claude/issue-15469-gantt-tree-config-close-passthrough",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16052",
      "premise_still_valid": true,
      "summary": "Ruling A executed exactly: both `.passthrough()` calls on `GanttConfigSchema` / `TreeConfigSchema` (`packages/spec/src/ui/view.zod.ts`) are removed and the ten keys objectui's plugin-gantt read through the gantt window (`GANTT_CONFIG_EXTENSION_KEYS`, types from `GanttConfigExtensionFields` at objectui pin `a472b07`) are declared with the renderer's types and a describe each, `interactions` / `timeSegments` as closed `strictObject` sub-shapes (not `z.unknown`, per M3). Zone-2 reading (1) tree: plugin-tree's `getTreeConfig` reads exactly the four already-declared keys from the `tree` block, so the tree close declares nothing new and the ruled dated-close fallback is moot. Zone-2 reading (2): zero in-repo authors of the ten (or of any undeclared key) under examples / content/docs / skills / package fixtures at `f7db8f4fd`; objectui's own fixtures author them and keep parsing; `/home/user/hotcrm` is absent in this container. Triage's M4 question answered by reading the gate and written into the ledger row: `check:strictness-ledger` SEES a `.passthrough()` site (`postureOf` records it as `passthrough` in the counts) but never counts it as strip debt, so the ratchet neither tracks nor retires it — hence the dated closure lives in the row prose plus the regenerated counts (ui/ passthrough 5 → 3, strict 157 → 162). Pins: the card's five-schema probe with both controls (refusal envelope asserted: `unrecognized_keys`, surface named, key echoed, closest key suggested), each of the ten accepted at type and refused at a wrong type on its own path, `ListView.gantt.colourField` and `.tree.labelFeild` refused through `ListViewSchema` and through `ObjectStackDefinitionSchema` (`objects[0].listViews.schedule.gantt`); three existing pins flipped (`view.test.ts`, `view-strictness-batch18.test.ts`, `scripts/strictness-ledger.test.ts` with a mutated-copy red control). Artifacts regenerated by tooling only (`authorable-surface/ui.json` +10, `content/docs/references/{ui/view,api/protocol,data/object}.mdx`, the counts artifact); one `@objectstack/spec` minor changeset with the BREAKING banner listing the ten and `adr-0087: not-required (no-migration-prescription)`. Draft PR opened with `Fixes #15469`; `needs:contract-review` untouched (seat's carrier); no assignee written. Card assignee was set by the dispatch and the newest `Claim:` (5553942281) names this branch. The run crossed a usage-wall restart at 19:16Z–21:43Z; the worktree and scratchpad survived intact at the pushed tip, and the order resumed from there with no rework. `main` moved 15 commits during the run, none touching this PR's files, so no merge round was owed.",
      "tests": "All build/test runs under `scripts/pm/os-verify-lock.sh` (`OS_VERIFY_LOCK_SLOT=issue-15469`), exit codes captured before any pipe, tree `9c3106b9f` (final commit) for every run below. M1 probe (dist `ui/index.mjs`, zod 4.4.3, two in-process controls): BEFORE on `f7db8f4fd` — `Gantt rejects unknown = false`, `Tree rejects unknown = false`, calendar/timeline/map `true`, both controls `true` (the card's reading reproduced); AFTER on the rebuilt dist — all five `true`: `Unrecognized key(s) on this gantt configuration: `bogus_key_xyz`` / `… this tree configuration …`. M2: `GanttConfigSchema.shape` reachable through the lazySchema Proxy, 19 → 29 keys; tree 4 unchanged. Same probe script, two builds, opposite readings = the rebuilt `.d.ts`/`.mjs` were read, not a cache. Build: `pnpm --filter @objectstack/spec build` → `VERDICT command-exit 0` (`.build-input-hash ← 32d237ea…`, `check-dts-emitted 34/34`); `check:generated` → `✓ All 15 generated artifacts are up to date` (after `--fix` regenerated exactly the two proved stale — `check:docs`, `check:strictness-ledger`; `check:authorable-surface` was already current from the build's own `gen:schema`; `gen:api-surface` correctly REFUSED to run on a dist that predated a later test-file edit, and went green after the final rebuild). Spec suite, COMPLETE (every file `vitest.config.ts` includes: `src/**/*.test.ts` + `scripts/**/*.test.ts` = 479, measured 147 + 82 + 246 + 4 = 479 in four lock calls, `--maxWorkers=2`): `Test Files 147 passed (147) / Tests 4075 passed` (ui, shared, scripts, top-level; Duration 314.48s, lock held 316s / waited 523s), `82 passed / 2774` (every remaining file that transitively imports `ui/view.zod.ts` or mentions a gantt/tree fixture; 27s), `246 passed / 5966` (the complement; 36s), `4 passed / 65` (the four the shard predicate had missed, found by `find` minus shards). 12,880 tests, 0 failures. New pin `view-gantt-tree-config-closed-15469.test.ts`: 42 tests (one fixture fix during the run — `ObjectSchema.fields` is a record, not an array — then 42/42). Typecheck: `@objectstack/spec` `tsc --noEmit` exit 0 (`tsconfig.json` excludes `**/*.test.ts`, so the test layer is measured by) `check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json` (includes `src/**/*`; the new pin is in the program) + `check:scripts-typecheck` exit 0. Consumers (downstream, `...@objectstack/spec` direction): `@objectstack/lint` `tsc --noEmit && check:test-typecheck: OK` (on a built `formula`+`lint` closure, 4 turbo tasks OK) and `@objectstack/sdui-parser` `tsc --noEmit` exit 0 — measured green; `@objectstack/runtime` / `@objectstack/rest` narrowed to CI with evidence (closures 30/25 packages, 29/24 cache misses on a box with three devs queued; `grep` over packages/apps/examples excluding spec: 0 references to `GanttConfig`/`TreeConfig`, 0 `.gantt.KEY`/`.tree.KEY` reads in non-test source — no consumer expression for the index-signature removal to move). Gates: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no paths — from the merge base; the first derivation on the code commit gave 75 families, the re-derivation after the regeneration commit 97; the 22 new docs/corpus families were run as a fourth batch) reconciled with `--ran`: 97/97 executed. 94 exit 0 — incl. `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:strictness-ledger`, `check:adr-0087-registration --base origin/main` (accepts the `no-migration-prescription` disposition), `check:changeset-no-major`, `check:empty-changeset`, `check:nul-bytes`, `check:closing-keyword-parity`, `check:objectui-changeset`, `check:type-check-coverage`, `check:dts-closure`, `check:published-files`, `check:doc-anchors`, `check:docs-single-h1`, `check:role-word`, `check:vendor-version-stamps`, and `@objectstack/lint check:doc-formula-expressions` (exit 3 PREREQUISITE on the first pass, measured green once `formula`+`lint` were built). NOT MEASURED locally, declared to CI (prerequisite = whole workspace built, which is the full farm): `check:dual-build-cjs-loads` (exit 3), `check:type-check-debt` (exit 3, `--re-measure cannot run: 32 workspace dependencies … not built`), `check:skill-examples` (exit 1 on an unbuilt `packages/client-react/dist`, before its `skills + docs` surface; that surface's closure is 35 packages; direct fact: the only gantt blocks under `skills/**` and `content/docs/ui/views.mdx` author six declared keys and no tree block exists there). eslint, narrowed and measured: population = what `eslint . --no-inline-config` lints (no `parserOptions.project`, no typed rules per `eslint.config.mjs:328` ⇒ an untouched file's verdict cannot move with this diff); the 6 changed `.ts` files are the linted subset of the 13 changed paths (`--print-config` resolves all 6; md/mdx/json are outside the population): `--format json` → files linted 6, errors 0, warnings 0, exit 0. Control bytes: `grep -naP` over all touched files, no hits. Existing pins run first on the edit (view.test.ts, batch18, alias-integrity, type-alias-convention.pin): 4 files / 397 passed; ledger gate tests (`strictness-ledger.test.ts`, `strictness-ledger-doc.test.ts`) 78 passed after the counts regen. Ablation: none owed — no new gate/guard was added, and the flipped pins carry their own red controls (the ledger test mutates a copy of the live file back to `.passthrough()` and reads `passthrough`; the door pins carry positive controls that parse).",
      "mcp_calls": "0 — every GitHub read/write in this run went through repo-scoped REST (`curl` with the session token: issue + comments read, PR create/read-back, report comment) or git; the MCP GitHub server was not called.",
      "open_questions": [
        {
          "question": "objectui follow-up (ui seat, not edited here): at pin `a472b07`, `packages/plugin-gantt/src/ObjectGantt.blockPrecedence.test.tsx:266` asserts `FLAT_GANTT_CONFIG_KEYS` (= `Object.keys(GanttConfigSchema.shape)` ∪ `GANTT_CONFIG_EXTENSION_KEYS`) has no duplicates — it will have ten once objectui builds against this spec; `packages/types/src/zod/objectql.zod.ts:683` `GanttConfigExtensionFields` becomes a second declaration of ten spec-modelled keys (its `.extend(...)` at `:831` redundant), and prose at `packages/types/src/objectql.ts:168`/`:222`, `zod/objectql.zod.ts:338`, `core/src/utils/__tests__/normalize-list-view.test.ts:351` still says the spec is loose. The Console Pin Gate is build-only (`scripts/build-console.sh`: deps build + console build), so nothing breaks the pin; the assertion fires in objectui's own suite after the spec refresh.",
          "options": [
            "A objectui derives the ten by reference from `SpecGanttConfigSchema.shape` and drops `GANTT_CONFIG_EXTENSION_KEYS` / `GanttConfigExtensionFields` (the file's own #2231 discipline)",
            "B objectui keeps its map and filters duplicates out of `FLAT_GANTT_CONFIG_KEYS`"
          ],
          "recommendation": "A, because it removes the second copy of the vocabulary the ruling exists to prevent (PD#12) and makes the objectui `interactions` / `timeSegments` shapes inherit the spec's closed sub-objects instead of the local loose `z.object`s."
        },
        {
          "question": "`defaultCollapsedDepth` is declared `z.number().int().min(0)` (objectui's mirror types it `z.number()`), following the tree sibling `defaultExpandedDepth` in the same file and the renderer's 0-indexed depth comparison (`GanttView.tsx:1739`); `timeSegments.bands[].start/end` and `dayStart` stay `z.string()` at the measured type although the renderer parses `'HH:mm'` — a format regex would be an accept-set decision beyond the ruling.",
          "options": [
            "A keep as landed (int/min(0) for the depth; strings for the clock times)",
            "B loosen the depth to `z.number()` to match objectui byte-for-byte",
            "C add an `HH:mm` regex to the three clock-time strings in a follow-up"
          ],
          "recommendation": "A for this PR (C is a reasonable follow-up if the seat wants it, filed separately — it narrows an accept set and deserves its own reading of authored values)."
        },
        {
          "question": "Local verification narrowed with declared evidence: `@objectstack/runtime` / `@objectstack/rest` typechecks (closures of 30 / 25 packages, 29 / 24 cache misses on a box with three other devs queued; zero references to `GanttConfig` / `TreeConfig` and zero `.gantt.KEY` / `.tree.KEY` reads outside spec), and three gates whose prerequisite is the whole workspace built: `check:dual-build-cjs-loads`, `check:type-check-debt` (both exit 3, 'nothing was measured') and `check:skill-examples` (exit 1 on an unbuilt `client-react`, before its `skills + docs` surface; the only corpus gantt blocks there author six declared keys). All four rest on CI's full-farm run.",
          "options": [
            "A accept the narrowing and read CI",
            "B have the dev build the 35-package closure on a quieter box"
          ],
          "recommendation": "A — CI runs exactly these on the PR; the local evidence shows there is no consumer expression for the change to move."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  5. claude commented on Sep 5, 2026

    @claude
    Contributor

    Review — ACCEPT · Clause-② contract review PASS (domain:spec seat at CONTRACT_REVIEW_TIER, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T22:23Z; PR #16052 at head 9c3106b9f; ruling 5548640040 option A for gantt AND tree, executed as ruled; the dev's report comment is still being posted — this verdict is the diff read, the report's gate evidence and its two Zone-2 counts are checked on arrival).

    Contract limb — the diff is the ruled shape, read 22:21–22:23Z on origin/main…9c3106b9f:

    • Both windows close. packages/spec/src/ui/view.zod.ts: the .passthrough() on GanttConfigSchema and on TreeConfigSchema are removed, so both blocks refuse an undeclared key with the strictObject named refusal (surface named, key echoed, closest declared key suggested); the two "Forward-compatible" comments, the tooltip-entry comment that named the open parent, and the ListMapConfigSchema docblock that called the pair the file's exceptions are rewritten to say the window is shut and why (the ruling, Prime Directive Add comprehensive test suite for Zod schema validation #12).
    • The ten renderer-read gantt keys are declared at the types objectui's GanttConfigExtensionFields carries (pin a472b07), each with a describe saying what the renderer does with it: borderColorField, lockField, objectField, exportFileName, timeZone (strings), summaryExtent ('children' | 'self'), defaultCollapsedDepth (int ≥ 0), dependencyTypes (boolean), and two closed sub-blocks — interactions (move / resize / progress / link, curated aliases drag → move, dependencies / links → link) and timeSegments (dayStart, bands[] of { key?, label, start, end, color? } with aliases, showMidnight) — strict per object, not z.unknown(), so no second de-facto contract survives. Tree: the undeclared read set at the pin is EMPTY (getTreeConfig reads exactly the four declared keys), so the tree window closes declaring nothing new and the ruled fallback is moot — stated in the file and pinned.
    • The ratchet question answered by reading: packages/spec/scripts/lib/strictness-ledger.ts walked the two .passthrough() sites as a lit red control; with the set now empty, the control moves into a mutated copy of the file inside strictness-ledger.test.ts, the ledger row for view.zod.ts records the closure dated 2026-09-05, and the generated counts file follows. (packages/spec/scripts/** is the spec lane's; not governed.)
    • Pins bear weight (view-gantt-tree-config-closed-15469.test.ts, 267 lines): the card's probe with both lit controls — every view config block refuses bogus_key_xyz, the ONLY issue being unrecognized_keys; the colourField → colorField and labelFeild → labelField suggestions; the tree shape is exactly the four keys; the gantt shape carries exactly 19 + 10 = 29 members and accepts all ten together through the ListView door; summaryExtent refuses a wrong value naming both members; the nested blocks are closed with their own named surfaces and the aliases steer near-misses; a gantt / tree typo is refused at gantt / tree through ListViewSchema and at objects[0].listViews.schedule.gantt through ObjectStackDefinitionSchema. Existing view.test.ts / view-strictness-batch18.test.ts pins updated to the closed posture.
    • Surfaces: authorable-surface/ui.json +10 (the ten keys), content/docs/references/ui/view.mdx (+59/−12) and two pages that re-render the shared shape regenerated by tooling. Changeset: @objectstack/spec minor, launch-window **BREAKING** banner naming both blocks, every door and the refusal, the ten newly declared keys listed, adr-0087: not-required (no-migration-prescription) (no key renamed, retired or re-typed — every previously accepted declared key still parses). Governed predicate: 0 of 13 paths (22:21Z).

    Cross-lane, owed on landing: objectui's ObjectGantt.blockPrecedence.test.tsx coverage pin compiles against a GanttConfigSchema.shape that now models the ten — the objectui ui seat's follow-up (the seat files it); no objectui file is touched here.

    Landing path: CI on 9c3106b9f running (main green). The seat hangs and clears needs:contract-review on both carriers (PR #16052, #15469) at this head, runs the pair check, and flips to ready + auto-merge (squash) once every check is green — a main merge lap by tooling first if the head is behind main's view.zod.ts / shard / reference-page moves. On merge: #15469 closes by the PR.


    Generated by Claude Code

  6. claude commented on Sep 5, 2026

    @claude
    Contributor

    Landing provenance (domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T22:53Z): PR #16052 flipped to ready and auto-merge (squash) enabled at 22:53Z on head 9c3106b9f — every check on that head green (34 success / 7 skipped, none red); ACCEPT + Clause-② PASS 5555185441; the dev's report 5555184347 confirms the gate evidence (probe before/after on the rebuilt dist, 29-key gantt shape, full spec suite 12 880 tests, 97/97 gate families) and the two Zone-2 counts (tree undeclared read set EMPTY; zero in-repo authors of the ten keys). Open questions ruled: (1) objectui follow-up → A — derive the ten from SpecGanttConfigSchema.shape, retire GANTT_CONFIG_EXTENSION_KEYS / GanttConfigExtensionFields (the seat files the objectui card on landing); (2) types as landed → A (defaultCollapsedDepth int ≥ 0, clock times plain strings; an HH:mm pattern is a separate accept-set reading if ever wanted); (3) the local narrowing → A, CI's full-farm run is the measurement, and it is green. needs:contract-review hung and cleared on both carriers at 9c3106b9f (22:22Z), pair check ✓; governed predicate 0 of 13 paths. The enqueue reading, the landing note and the objectui card follow on MERGED (landing watch armed 23:19Z).


    Generated by Claude Code

  7. claude commented on Sep 5, 2026

    @claude
    Contributor

    Landed (domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T23:59Z): PR #16052 merged 2026-09-05T23:53:16Z as 9c270bba0 through the merge queue (squash; added_to_merge_queue 22:54:06Z, about 59 minutes in queue). Probe on origin/main at 2026-09-05T23:58Z (tip 9c270bba0): no code-form ).passthrough() remains in packages/spec/src/ui/view.zod.ts (control: two, at lines 1390 and 1410 on f7db8f4fd); the six remaining passthrough() hits are the rewritten comments that say the window is shut — the literal-count probe the landing watch named (= 0) was mis-designed and is superseded by this reading; view-gantt-tree-config-closed-15469.test.ts is present on main. ACCEPT + Clause-② PASS 5555185441 at 9c3106b9f, carriers cycled on both, pair ✓.

    Follow-up the verdict owes, filed in this stroke: objectui card objectstack-ai/objectui#7845 (derive the ten gantt keys from GanttConfigSchema.shape, retire GANTT_CONFIG_EXTENSION_KEYS / GanttConfigExtensionFields, the ObjectGantt.blockPrecedence.test.tsx:266 no-duplicates pin, the stale "loose" prose) — reader: objectui's ui seat; lands with the pin bump that first carries 9c270bba0.

    Release: session_01M59rPZZFzqhfMUPFqqZTkf — reason: landed, card closed by Fixes #15469 — destination: none (closed); the owed work is on objectstack-ai/objectui#7845. Same stroke: pm:dispatched stripped, assignee cleared.


    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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions