feat(spec)!: close the gantt/tree config .passthrough() windows and declare the ten renderer-read gantt keys (#15469) - #16052
Conversation
… 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
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Fixes #15469
Ruling (director seat, comment 5548640040 on #15469, 2026-09-05T02:09Z; maintainer verbatim 「同意」 to decision batch #41 item 2)
Executed exactly that: both
.passthrough()calls inpackages/spec/src/ui/view.zod.tsare 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-privateGanttConfigExtensionFields(packages/types/src/zod/objectql.zod.ts:683) and documented on objectui's TS-sideGanttConfig(packages/types/src/objectql.ts:174–:322). Each is read by the renderer (site cited), and each is now declared as:borderColorFieldstringObjectGantt.tsx:858createFieldColorResolver(fieldDefs[borderColorField])lockFieldstringObjectGantt.tsx:1108locked: lockField ? !!record[lockField] : undefinedobjectFieldstringObjectGantt.tsx:1412–:1423detail href per row's own objectsummaryExtentenum 'children' | 'self'GanttView.tsx:604/:1841defaultCollapsedDepthinteger ≥ 0(objectui:z.number(); the tree siblingdefaultExpandedDepthin this file is alreadyint().min(0), and a depth is an integer by construction)GanttView.tsx:594/:1739dependencyTypesbooleanGanttView.tsx:635/:1576/:5106timeZonestring(IANA name; the renderer validates and warns)GanttView.tsx:318–:329makeTzShiftexportFileNamestringGanttView.tsx:3264interactions{ move?, resize?, progress?, link? : boolean }(this gantt interactions block)GanttView.tsx:899–:902timeSegments{ dayStart?: string, bands: [{ key?, label, start, end, color? }], showMidnight?: boolean }(this gantt time-segments block/this gantt shift band)ObjectGantt.tsx:225→shifts.ts:33–:60The nested objects are declared as
strictObjectsub-shapes, notz.object/z.unknown— per M3 a loose declaration would be the second de-facto contract the ruling closes (objectui's own mirror uses plainz.objectfor them; when objectui.extend()s the spec schema with its map, its versions win on that side — no break).dependencyFieldwas measured and not declared:getGanttConfigtakes the block WHOLE and readsdependencyFieldonly on the flat top-level face (ObjectGantt.tsx:543), so it is not aGanttConfigkey.tree — zero keys. Re-derived over the whole
plugin-treepackage (ObjectTree.tsx,index.tsx; everyschema.*read enumerated):getTreeConfig(ObjectTree.tsx:113) reads exactlyparentField,labelField,fields,defaultExpandedDepthfrom the nestedtreeblock (schema.tree || schema.filter?.tree) — all four already declared onTreeConfigSchema. The other reads (titleField,data,staticData,objectName,filter) are top-levelobject-treecomponent keys, nottree-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, objectuia472b07)examples/**,content/docs/**,skills/**,packages/**,apps/**,docs/**: everygantt: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 (borderColorField…timeSegments) 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.plugin-gantt/README.mdanddemo/main.tsxa 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 tocheck:strictness-ledger:postureOfwalks the method chain (since #5072) and records the site aspassthroughin 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 countstripsites only, andpassthroughis a non-strip posture — so an open-by-passthrough site never enters a row's strip count, never needs aClassverdict, 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 theview.zod.tsrow (dated, this card) and the counts regenerate:ui/passthrough 5 → 3, strict 157 → 162 (two closures + three new nested strict sites),view.zod.tssites 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 theListMapConfigSchemadocblock rewritten (no prose inpackages/spec,content/docs,skills/**or the ledger still says the window is open — swept forrenderer-ahead/Forward-compatible/passthrough+gantt|tree).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.colourFieldand.tree.labelFeildrefused at the block throughListViewSchemaandobjects[0].listViews.schedule.ganttthroughObjectStackDefinitionSchema, 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 readstrict; 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).packages/spec/authorable-surface/ui.json(+10ui/GanttConfig:*),content/docs/references/{ui/view,api/protocol,data/object}.mdx(the& Recordintersection is gone from every gantt/tree cell), the strictness counts artifact.docs/audits/2026-07-unknown-key-strictness-ledger.mdrow +scripts/lib/strictness-ledger.tsdocblock (dated closure). ADRs untouched..changeset/gantt-tree-config-close-passthrough.md—@objectstack/specminor,**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=9c3106b9fat each run).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/maptrue, both controlstrue(the card's reading, reproduced onf7db8f4fd); after — all fivetrue, gantt/tree refusing withUnrecognized key(s) on this gantt configuration: \bogus_key_xyz`/… this tree configuration …. **M2**:GanttConfigSchema.shapereachable through thelazySchemaProxy, 19 → 29 keys;TreeConfigSchema.shape` unchanged (4).pnpm --filter @objectstack/spec build—VERDICT 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 readpassthroughbehaviour and 19 keys; the post-edit probe on the rebuilt dist reads the refusal and 29 keys — same script, two builds, opposite readings.vitest run --maxWorkers=2in four shards covering every filevitest.config.tsincludes (src/**/*.test.ts+scripts/**/*.test.ts= 479 files; measured 147 + 82 + 246 + 4 = 479, the complement computed byfindminus 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 importsui/view.zod.tsor mentions a gantt/tree fixture; 27s),246 passed / 5966(the rest; 36s),4 passed / 65. Total 12,880 tests, 0 failures.@objectstack/spec—tsc --noEmitexit 0 (itstsconfig.jsonexcludes**/*.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(includessrc/**/*, so the new pin is in the program) andcheck:scripts-typecheckexit 0. Consumers (pnpm --filter '...@objectstack/spec'direction, downstream):@objectstack/lint(tsc --noEmit && check:test-typecheck: OK, on a builtformula+lintclosure) and@objectstack/sdui-parser(tsc --noEmitexit 0) measured green.@objectstack/runtime/@objectstack/restdeclared 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 outsidepackages/specnames (grep -rn 'GanttConfig\b\|TreeConfig\b'overpackages apps examplesexcluding spec: 0 hits) and no non-test source outside spec reads a key off (\.(gantt|tree)(\?\.|\.)KEYover the same roots: 0 hits) — there is no consumer expression for the index-signature removal to move.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-packagecheck:doc-formula-expressionsonceformula+lintwere built). NOT MEASURED locally (prerequisite exit 3, "nothing was measured", declared to CI):check:dual-build-cjs-loadsandcheck: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 unbuiltpackages/client-react/dist("a verdict now would be a FALSE GREEN") before reaching theskills + docssurface 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 underskills/**(objectstack-ui/rules/list-views.md:293) andcontent/docs/ui/views.mdx:329authorstartDateField/endDateField/titleField/progressField/dependenciesField/parentField— all six in the declared set — and no tree block exists there.eslint . --no-inline-config; noparserOptions.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.tsfiles are the linted population (md/mdx/json are outside it,--print-configconfirms none is ignored):--format json→files linted: 6, errors: 0, warnings: 0, exit 0.grep -naPover every touched file — no hits.mainmoved 15 commits (f7db8f4fd→2e3576503) during the run;git diff --name-only HEAD...origin/maintouches 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-shamoves past this change, objectui ata472b07needs: (1)packages/plugin-gantt/src/ObjectGantt.blockPrecedence.test.tsx:266—FLAT_GANTT_CONFIG_KEYS=Object.keys(GanttConfigSchema.shape)∪GANTT_CONFIG_EXTENSION_KEYSnow duplicates the ten, so its "No duplicates" assertion (and thesatisfies readonly KnownGanttConfigKey[]coverage pin at:275) should derive from the spec's shape alone; (2)packages/types/src/zod/objectql.zod.ts:683GanttConfigExtensionFieldsis 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:831becomes redundant; (3) prose that still says the spec is loose:packages/types/src/objectql.ts:168/:222("GanttConfigSchemais$looseupstream"),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