Skip to content

feat(spec)!: close the gantt/tree config .passthrough() windows and declare the ten renderer-read gantt keys (#15469) - #16052

Queued
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-15469-gantt-tree-config-close-passthrough
Queued

feat(spec)!: close the gantt/tree config .passthrough() windows and declare the ten renderer-read gantt keys (#15469)#16052
claude[bot] wants to merge 2 commits into
mainfrom
claude/issue-15469-gantt-tree-config-close-passthrough

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15469

Ruling (director seat, comment 5548640040 on #15469, 2026-09-05T02:09Z; maintainer verbatim 「同意」 to decision batch #41 item 2)

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.

Executed exactly that: both .passthrough() calls in packages/spec/src/ui/view.zod.ts are removed, the ten keys plugin-gantt read through the gantt window are declared at the types objectui measures, the tree window closes with no new key, and the four prose sites that said the window was open are rewritten. The ruled tree fallback (a dated close) was not needed — see Zone-2 reading (1), tree.

Zone-2 reading (1) — the undeclared read set per renderer, at objectui pin a472b07 (read-only checkout, clean, HEAD == .objectui-sha)

gantt — ten keys, from GANTT_CONFIG_EXTENSION_KEYS (packages/plugin-gantt/src/ObjectGantt.tsx:406), typed by the module-private GanttConfigExtensionFields (packages/types/src/zod/objectql.zod.ts:683) and documented on objectui's TS-side GanttConfig (packages/types/src/objectql.ts:174:322). Each is read by the renderer (site cited), and each is now declared as:

key declared type renderer read
borderColorField string ObjectGantt.tsx:858 createFieldColorResolver(fieldDefs[borderColorField])
lockField string ObjectGantt.tsx:1108 locked: lockField ? !!record[lockField] : undefined
objectField string ObjectGantt.tsx:1412:1423 detail href per row's own object
summaryExtent enum 'children' | 'self' GanttView.tsx:604 / :1841
defaultCollapsedDepth integer ≥ 0 (objectui: z.number(); the tree sibling defaultExpandedDepth in this file is already int().min(0), and a depth is an integer by construction) GanttView.tsx:594 / :1739
dependencyTypes boolean GanttView.tsx:635 / :1576 / :5106
timeZone string (IANA name; the renderer validates and warns) GanttView.tsx:318:329 makeTzShift
exportFileName string GanttView.tsx:3264
interactions closed object { move?, resize?, progress?, link? : boolean } (this gantt interactions block) GanttView.tsx:899:902
timeSegments closed object { dayStart?: string, bands: [{ key?, label, start, end, color? }], showMidnight?: boolean } (this gantt time-segments block / this gantt shift band) ObjectGantt.tsx:225shifts.ts:33:60

The nested objects are declared as strictObject sub-shapes, not z.object/z.unknown — per M3 a loose declaration would be the second de-facto contract the ruling closes (objectui's own mirror uses plain z.object for them; when objectui .extend()s the spec schema with its map, its versions win on that side — no break). dependencyField was measured and not declared: getGanttConfig takes the block WHOLE and reads dependencyField only on the flat top-level face (ObjectGantt.tsx:543), so it is not a GanttConfig key.

tree — zero keys. Re-derived over the whole plugin-tree package (ObjectTree.tsx, index.tsx; every schema.* read enumerated): getTreeConfig (ObjectTree.tsx:113) reads exactly parentField, labelField, fields, defaultExpandedDepth from the nested tree block (schema.tree || schema.filter?.tree) — all four already declared on TreeConfigSchema. The other reads (titleField, data, staticData, objectName, filter) are top-level object-tree component keys, not tree-block keys. So the tree window closes declaring nothing new and the ruled fallback is moot.

Zone-2 reading (2) — who authors the ten keys (objectstack f7db8f4fd, objectui a472b07)

  • objectstack examples/**, content/docs/**, skills/**, packages/**, apps/**, docs/**: every gantt: block found (examples/app-showcase/src/ui/views/task.view.ts, content/docs/ui/views.mdx, skills/objectstack-ui/rules/list-views.md, packages/lint/src/showcase-shape.fixtures.ts, packages/lint/src/validate-list-view-field-refs*.ts, packages/spec/src/kernel/functional-completeness.ts) authors none of the ten and no undeclared key; the only mentions of the unambiguous seven (borderColorFieldtimeSegments) are release-note prose (content/docs/releases/v15.mdx:151, :155, v16.mdx:634), the two comments this PR rewrites, and QA checklist prose. Zero in-repo authors ⇒ the changeset carries a release note, not a migration sentence.
  • objectui fixtures author the ten in its own tests/README/demo (test files per key: lockField 7, interactions 6, summaryExtent 5, defaultCollapsedDepth 4, borderColorField 3, objectField 3, timeZone 3, timeSegments 3, dependencyTypes 2, exportFileName 2; plugin-gantt/README.md and demo/main.tsx a few) — they keep parsing because the keys are now declared.
  • /home/user/hotcrm: absent in this container (not readable; reported, not measured).

Ledger answer (triage's open question, M4 — answered by reading packages/spec/scripts/lib/strictness-ledger.ts)

A .passthrough() site is not invisible to check:strictness-ledger: postureOf walks the method chain (since #5072) and records the site as passthrough in the counts artifact's posture table. It is not debt the ratchet retires either: the remaining-strip map, the bucket subtotals and the reverse pin count strip sites only, and passthrough is a non-strip posture — so an open-by-passthrough site never enters a row's strip count, never needs a Class verdict, and never moves the ratchet when closed or reopened. Its only ledger trace is the per-directory posture column and the row prose; hence the closure is written into the view.zod.ts row (dated, this card) and the counts regenerate: ui/ passthrough 5 → 3, strict 157 → 162 (two closures + three new nested strict sites), view.zod.ts sites 58 → 61, strip unchanged at 4.

What changed

  • packages/spec/src/ui/view.zod.ts — ten declared members with describes; both .passthrough() removed; docblock, tooltip-entry comment, the two "Forward-compatible" comments and the ListMapConfigSchema docblock rewritten (no prose in packages/spec, content/docs, skills/** or the ledger still says the window is open — swept for renderer-ahead / Forward-compatible / passthrough+gantt|tree).
  • Pins: new view-gantt-tree-config-closed-15469.test.ts (42 tests: 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 kept in the output, refused at a wrong type on its own path; nested surfaces closed and their aliases; ListView.gantt.colourField and .tree.labelFeild refused at the block through ListViewSchema and objects[0].listViews.schedule.gantt through ObjectStackDefinitionSchema, with positive controls). Flipped: view.test.ts ("passthrough unknown renderer fields" → declared + refused), view-strictness-batch18.test.ts (parent now closed), scripts/strictness-ledger.test.ts (both sites read strict; the 台账 AST 计数器把 strictObject(...).passthrough() 读成 strict —— postureOf() 在 helper 惯用法上提前返回、不走链 #5072 walk keeps a red control on a mutated copy of the live file; the file's passthrough set pinned empty).
  • Regenerated by tooling only: packages/spec/authorable-surface/ui.json (+10 ui/GanttConfig:*), content/docs/references/{ui/view,api/protocol,data/object}.mdx (the & Record intersection is gone from every gantt/tree cell), the strictness counts artifact.
  • docs/audits/2026-07-unknown-key-strictness-ledger.md row + scripts/lib/strictness-ledger.ts docblock (dated closure). ADRs untouched.
  • .changeset/gantt-tree-config-close-passthrough.md@objectstack/spec minor, **BREAKING** banner listing the ten, adr-0087: not-required (no-migration-prescription) (nothing renamed or retired; every declared key keeps parsing; the only newly refused input is a key no renderer ever read).

Verification

All under scripts/pm/os-verify-lock.sh (OS_VERIFY_LOCK_SLOT=issue-15469), exit codes captured before any pipe, verdicts quoted from the gates' own lines. Tree: 9c3106b9f (final commit; the union below was run on it — git rev-parse --short HEAD = 9c3106b9f at each run).

  • M1 probe, before/after (packages/spec/dist/ui/index.mjs, zod 4.4.3, two in-process controls): before — Gantt rejects unknown = false, Tree rejects unknown = false, calendar/timeline/map true, both controls true (the card's reading, reproduced on f7db8f4fd); after — all five true, gantt/tree refusing with Unrecognized key(s) on this gantt configuration: \bogus_key_xyz`/… this tree configuration …. **M2**: GanttConfigSchema.shapereachable through thelazySchemaProxy, 19 → 29 keys;TreeConfigSchema.shape` unchanged (4).
  • Build: pnpm --filter @objectstack/spec buildVERDICT command-exit 0 (.build-input-hash ← 32d237ea…); check:generated✓ All 15 generated artifacts are up to date. Reverse check of the build (rule: "read the rebuilt d.ts, not a cache"): the pre-edit probe on the pre-edit dist read passthrough behaviour and 19 keys; the post-edit probe on the rebuilt dist reads the refusal and 29 keys — same script, two builds, opposite readings.
  • Spec suite, complete: vitest run --maxWorkers=2 in four shards covering every file vitest.config.ts includes (src/**/*.test.ts + scripts/**/*.test.ts = 479 files; measured 147 + 82 + 246 + 4 = 479, the complement computed by find minus the shards): Test Files 147 passed (147) / Tests 4075 passed (ui, shared, scripts, top-level stack/compose; 314s), 82 passed / 2774 (every remaining file that transitively imports ui/view.zod.ts or mentions a gantt/tree fixture; 27s), 246 passed / 5966 (the rest; 36s), 4 passed / 65. Total 12,880 tests, 0 failures.
  • Typecheck: @objectstack/spectsc --noEmit exit 0 (its 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/**/*, so the new pin is in the program) and check:scripts-typecheck exit 0. Consumers (pnpm --filter '...@objectstack/spec' direction, downstream): @objectstack/lint (tsc --noEmit && check:test-typecheck: OK, on a built formula+lint closure) and @objectstack/sdui-parser (tsc --noEmit exit 0) measured green. @objectstack/runtime / @objectstack/rest declared to CI, narrowed with evidence: their build closures are 30 / 25 packages (29 / 24 turbo cache misses on a box three other devs were queued on), and the whole diff's type effect is confined to two nested view-config types that no package outside packages/spec names (grep -rn 'GanttConfig\b\|TreeConfig\b' over packages apps examples excluding spec: 0 hits) and no non-test source outside spec reads a key off (\.(gantt|tree)(\?\.|\.)KEY over the same roots: 0 hits) — there is no consumer expression for the index-signature removal to move.
  • Gates — derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths; from the merge base, re-derived after the regeneration commit: 75 → 97 families) and reconciled with --ran: 97 of 97 executed. 94 exit 0 (incl. check:api-surface, check:authorable-surface, check:docs, check:strictness-ledger, check:adr-0087-registration --base origin/main, check:changeset-no-major, check:empty-changeset, check:nul-bytes, check:closing-keyword-parity, check:type-check-coverage, check:dts-closure, check:published-files, check:doc-anchors, check:docs-single-h1, the lint-package check:doc-formula-expressions once formula+lint were built). NOT MEASURED locally (prerequisite exit 3, "nothing was measured", declared to CI): check:dual-build-cjs-loads and check:type-check-debt — both require the whole workspace built (turbo run build --filter='./packages/*' --filter='./packages/*/*'), which is CI's full-farm run. check:skill-examples (exit 1) is the third prerequisite case: it walks the client-SDK surface first and refuses on an unbuilt packages/client-react/dist ("a verdict now would be a FALSE GREEN") before reaching the skills + docs surface this diff could touch; that surface's closure is 35 packages, so it is declared to CI with the direct fact the gate would compile: the only gantt blocks under skills/** (objectstack-ui/rules/list-views.md:293) and content/docs/ui/views.mdx:329 author startDateField / endDateField / titleField / progressField / dependenciesField / parentField — all six in the declared set — and no tree block exists there.
  • eslint, narrowed and measured: population = the files ESLint's own config lints (eslint . --no-inline-config; no parserOptions.project, no typed rules — eslint.config.mjs:328 — so the verdict on an untouched file is a function of that file alone and this diff cannot move it); of the 13 changed paths, the 6 .ts files are the linted population (md/mdx/json are outside it, --print-config confirms none is ignored): --format jsonfiles linted: 6, errors: 0, warnings: 0, exit 0.
  • Control bytes: grep -naP over every touched file — no hits.
  • main moved 15 commits (f7db8f4fd2e3576503) during the run; git diff --name-only HEAD...origin/main touches none of this PR's files (checked by path), so no merge/regeneration round was owed under the seat's rule.

objectui follow-up (ui seat — not edited here)

When .objectui-sha moves past this change, objectui at a472b07 needs: (1) packages/plugin-gantt/src/ObjectGantt.blockPrecedence.test.tsx:266FLAT_GANTT_CONFIG_KEYS = Object.keys(GanttConfigSchema.shape)GANTT_CONFIG_EXTENSION_KEYS now duplicates the ten, so its "No duplicates" assertion (and the satisfies readonly KnownGanttConfigKey[] coverage pin at :275) should derive from the spec's shape alone; (2) packages/types/src/zod/objectql.zod.ts:683 GanttConfigExtensionFields is now a second declaration of ten keys the spec models — derive by reference (SpecGanttConfigSchema.shape.KEY) per the file's own #2231 discipline, and its .extend(GanttConfigExtensionFields) at :831 becomes redundant; (3) prose that still says the spec is loose: packages/types/src/objectql.ts:168/:222 ("GanttConfigSchema is $loose upstream"), packages/types/src/zod/objectql.zod.ts:338, packages/core/src/utils/__tests__/normalize-list-view.test.ts:351. None of these breaks the current pin: the pinned objectui builds against the spec version it declares.

Out of scope here (own rulings): #14471 is not addressed here; #14110 and objectui#7243 are out of scope here as well.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf


Generated by Claude Code

… the ten renderer-read gantt keys

`GanttConfigSchema` and `TreeConfigSchema` were `strictObject(...).passthrough()`:
the helper applied and undone, so an undeclared key parsed green on exactly two
view config blocks. Both `.passthrough()` calls are removed; the ten members
objectui's plugin-gantt read through the gantt window are declared at the types
the renderer reads (measured at objectui pin a472b07); plugin-tree's read set
was measured as the four keys already declared, so the tree close declares
nothing new.

Pins: the card's five-schema probe with two controls, every declared key
accepted at type and refused at a wrong one on its own path, the ListView and
ObjectStackDefinition doors refusing `gantt.colourField`, and the strictness
ledger gate test re-pointed (both sites read strict, mutated-copy red control).
The ledger row answers the "is passthrough visible to the ratchet" question by
reading the gate; changeset is a minor with the BREAKING banner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
…trictness counts for the closed gantt/tree blocks

Generated by `pnpm --filter @objectstack/spec build` (authorable-surface/ui.json,
the ten new `ui/GanttConfig:*` entries) and `check:generated --fix`
(content/docs/references/{ui/view,api/protocol,data/object}.mdx — the
`Record` intersection is gone from every gantt/tree cell, the ten members and
the two nested shapes are documented; the ledger counts artifact — ui/ strict
157 to 162, passthrough 5 to 3). Ledger row numbers corrected to match; the
door pin's object fixture uses the record `fields` form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:ui tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx (via GanttConfigSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 53cbad9f75572600ef43bb2a18071633fd6c0f68packageMentionDocs.

Which tree this was computed on

This run read content/docs from e59732cd13d1c1cc403614d43e16e231fde67111 — the merge of head 9c3106b9f3ff9059f2fa349963661b60eefb2431 into base 53cbad9f75572600ef43bb2a18071633fd6c0f68, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e59732cd13d1c1cc403614d43e16e231fde67111 && git checkout e59732cd13d1c1cc403614d43e16e231fde67111
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53cbad9f75572600ef43bb2a18071633fd6c0f68 9c3106b9f3ff9059f2fa349963661b60eefb2431 && git checkout -B drift-repro 53cbad9f75572600ef43bb2a18071633fd6c0f68 && git merge --no-ff 9c3106b9f3ff9059f2fa349963661b60eefb2431

node scripts/docs-audit/affected-docs.mjs --json 53cbad9f75572600ef43bb2a18071633fd6c0f68

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 53cbad9f75572600ef43bb2a18071633fd6c0f68 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

1 participant