fix(types,plugin-tree): derive TreeViewConfig from the spec and drop the key spec 17.4.0 refuses (objectui#8841) - #9052
Conversation
…titleField `@object-ui/types` published `TreeViewConfig` as a hand-written interface — a copy of the protocol's `ListView.tree` block under a second name — and the copy declared a fifth key, `titleField`, that `@objectstack/spec@17.4.0` REFUSES on that block by name (`TreeConfigSchema` is a `strictObject` since spec #15469 closed the `.passthrough()` window 17.3.0 left open). This repo's published face therefore accepted what the contract rejects: an author who followed `@object-ui/types` was refused at publish. Both divergences are addressed: - the KEY — `titleField` is removed from the type, and `getTreeConfig`'s `?? schema.titleField` rung goes with it. That rung read the flattened NODE, never the block, and `titleField` is declared on neither face; it was also unreachable from both in-repo producers of an `object-tree` node, each of which floors `labelField` before the node is built. - the COPY — `TreeViewConfig` is now `NonNullable<SpecListView['tree']>`, a derivation rather than a rename of the spec's `TreeConfig`. The NAME is kept (a consumer census found nine referencing files and no `TreeConfig` free in the barrel); what is retired is the hand copy behind it, which is the half that could drift. The `Pick` interim objectui#8841 offered was conditional on the 17.4.0 bump not having landed. It had: `chore(deps): take the 17.4.0 @objectstack/* line` moved the lockfile to 17.4.0 four hours after this card was filed, so the plain alias is available and no dependency is bumped here. The three `labelField || titleField` dual-reads (plugin-view, plugin-list, app-shell) are kept as undeclared tolerant fallbacks so stored view records keep resolving and objectui#6557's pin stays green; retiring them is a follow-up. The console's canonical rung stays annotated `TreeViewConfig`; its legacy rung is deliberately left untyped rather than re-declared. Both census pins are re-pinned as PARITY WITH THE PROTOCOL rather than as a literal key list — a literal list is what let this drift through, since it was maintained alongside the type it was supposed to audit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewHead reviewed: Charter read at: objectstack Type arguments below are spelled in words — ① Derived judgments — the accept-set and public-face changes the diff implies, one by one
On the two-verdict conflict this card exists to close — my own reading. The CHANGES-REQUIRED record ( ② Semver grading against
|
Provenance — carriers cleared, landingClause-② contract review: Reviewer ran at Independence — NOT self-review, and this reviewer corrected the historical record while establishing it: ⭐ It states plainly that both are children of the same seat session and that independence rests on context isolation plus tier ( Landing pre-checks — all measured just now
|
Dequeued at 22:23:59Z — and this PR's own queue build was ENTIRELY GREEN
The measurement, and it does not name a failure in this PRThe queue built this PR on branch
So there is no failure signature attached to this PR to diagnose, re-run, or fix. The PR is still ⛔ What I am NOT claiming⛔ I am not calling this a flake, and ⛔ not attributing it to a batch-mate I cannot name. The dequeue notice says a grouped-queue failure can come from another PR in the same batch — that is the shape this fits, since a green build was dequeued for DispositionRe-queued as is, head unchanged. Grounds: the three landing pre-checks that gated this PR still hold on this head (contract review ⛔ Base is not hand-merged; the queue rebuilds on current Generated by Claude Code |
Fixes #8841
⛔ Draft. Landing is the seat's act — no enqueue, no ready flip.
OUTEROFINNER, never with angle brackets. GitHub's body sanitizer deletes tag-shaped fragments on save, and backticks and fenced code blocks do not protect them (AGENTS.md, GitHub 会改写你写进 issue/PR 正文的字节, case ①). The measured instance was a.d.tsbefore/after table whose two generic columns were both eaten and collapsed into the same string, so a table written to show a type change rendered as "nothing changed". The real source spellings are in the diff.@object-ui/typespublishedTreeViewConfigas a hand-written interface — a copy of the protocol'sListView.treeblock under a second name — and the copy declared a fifth key,titleField, that@objectstack/spec@17.4.0refuses on that block by name. This repo's published face accepted what the contract rejects, so an author who followed@object-ui/typeswas refused at publish. This aligns the repo to the spec (协议为基准) and derives the type instead of copying it.Two reviews at
CONTRACT_REVIEW_TIERon the merged head49f36c3b8fdisagreed, and the fix must not quietly overtake that.5602098291titleFielddeclared against the ruling's own criterion and refused by spec 17.4.0; F2 the hand copy is a rename of the spec'sTreeConfig5602172326The CHANGES-REQUIRED verdict named exactly this defect, at the exact file and key, and it shipped anyway. Both findings it raised are executed here — F1 as the key removal, F2 as the derivation. The card's acceptance (
5602180375) had already recorded on #8253 that "titleFieldwas declared on read-side evidence, and the console does not write it"; that record is what this PR settles.⭐ The PASS verdict was not wrong about what it measured. Its findings 1 and 3 are accurate and were re-derived here: the ruling's cited read is off the flattened node, not the block; and the readers census was satisfied by text rather than by a block read. Where it went wrong is one step up — it upheld a declaration on read-side evidence when the ruling had asked a write-side question, and the reads it upheld it on turn out to be untyped tolerance rather than contract.
The card says the pinned spec is 17.3.0 with
.passthrough(), "so the divergence is invisible to today's parity gate and becomes a refusal on the next spec bump", and it offers aPickinterim until the 17.4.0 bump.Measured on my own base (
681d3f10e):pnpm-lock.yamlalready pins@objectstack/spec@17.4.0, and there is no17.3.0left in it (grep -c 'spec@17.3.0' pnpm-lock.yaml→ 0). It landed at093af32e2chore(deps): take the 17.4.0 @objectstack/* line (#8783), 2026-09-09T17:29:57Z — 4h16m after PR #8828 merged (13:13Z) and 3h40m after the card was filed (13:49Z).git merge-base --is-ancestor 093af32e2 681d3f10e→ exit 0; control leg (the reverse direction) → exit 1; the clone is not shallow.Two consequences:
@object-ui/typesface has been accepting a key the installed protocol refuses since that bump.Pickinterim is not needed and would be wrong. Its own precondition ("until the 17.4.0 bump") is already satisfied, and aPickrestates the key list — which is what lettitleFieldthrough in the first place. This PR takes the plain derivation.⛔ No dependency is bumped here. Nothing needed bumping;
pnpm-lock.yamland everypackage.jsonare untouched, so PR #9032's hold is not contested.The two divergences, and which of them I fixed
Both — but they are fixed differently, and the difference is the point.
1. The key (
titleField) — removed. It is gone from the type, andgetTreeConfig's?? schema.titleFieldrung is gone with it.2. The copy — retired. The NAME is kept, deliberately. In
packages/types/src/views.ts,TreeViewConfigis now a one-line alias —export type TreeViewConfig =NonNullable OFSpecListView['tree'].The divergence that can drift is the hand copy, not the alias: once the declaration IS the spec's symbol, the next key the protocol adds, renames or retypes arrives here without an edit, and a key the protocol never declared cannot be added here at all. Renaming the published name back to
TreeConfigwould be a separate breaking removal from the barrel with no drift left to prevent — so per the card's ⛔ I did the consumer census rather than the rename:TreeViewConfigacross 4 packages (types,plugin-tree,plugin-view,app-shell); it is on the published barrel atpackages/types/src/index.ts.@object-ui/typesexports noTreeConfigtoday, so the name is free — the collision argument does not decide it either way.The reader census — measured on this base, before the rung was removed
titleFieldreads that touch the tree block or node, and what each is coupled to:TreeViewConfig?packages/plugin-tree/src/ObjectTree.tsxgetTreeConfigschema.titleFieldschema: any— and it reads the flattened NODE, not the block (nested.titleField: 0 hits)packages/plugin-view/src/ObjectView.tsx'tree'branchviewOptions.tree?.titleFieldviewOptionsresolves throughNamedListView['options'], which is Record OFstringTOanypackages/plugin-list/src/ListView.tsx'tree'branchtreeCfg.titleFieldtreeCfg = (schema as any).tree || …packages/app-shell/src/views/ObjectView.tsxconsole composition(viewDef.tree as TreeViewConfig | undefined)?.titleFieldTreeViewConfigExactly one of the four is compile-coupled. Proven by ablation, not by reading — restoring the cast on the committed tree:
(The error text is itself a second reading of the derivation: the resolved shape is exactly the protocol's four keys, no index signature.) Restore verified by blob equality against
HEADandgit diff HEADempty.My measurement agrees with the card's guidance, and adds a reason the card did not have:
ListView.treeread.titleFieldis declared on neither face — not onObjectTreeSchema(packages/types/src/objectql.ts, which declares exactlyparentField, labelField, fields, defaultExpandedDepth), not on its zod mirror (zod/objectql.zod.ts, same four), and not on the protocol's block. And it was unreachable from both in-repo producers of anobject-treenode —plugin-view's andplugin-list's'tree'branches each computelabelField: … || … || 'name'before the node is built, so the??chain never fell through to it. No test pinned its behaviour.any, so none of them had to change; keeping them means a view record already storingtree.titleFieldresolves exactly as before and objectui#6557's pin stays green (it does — see below). The console's canonical rung stays annotatedTreeViewConfig; ⛔ its legacy rung is deliberately not re-cast to a local{ titleField?: string }, because that would fossilise a renderer-side alias into a second contract — AGENTS.md #0.1, the defect this PR undoes. Retiring the three is a follow-up.The console criterion, re-measured by content (anchors had drifted)
The ruling's test is does the console write it. The card cites
CreateViewDialog.tsx:290-298; on this base thetreeslot is at :289-300 — the anchors moved, the content did not.packages/app-shell/src/views/CreateViewDialog.tsxtree: [ … ]collectsparentFieldalone.titleFieldappears 3× — at :202 (calendar), :217 (timeline), :239 (gantt). So the zero for thetreeslot is a reading about the tree slot, not about a blind instrument.subConfig[rf.key] = v), so nothing else writes it either.⇒ The console does not write
tree.titleField. The ruling's own answer was remove, and that is what this does.Measurements, each with a firing control
1. spec 17.4.0 — strictness and key set (installed artifact,
node_modules/@objectstack/spec, version read from its ownpackage.json=17.4.0):Through the real host path (
ListViewSchema, with a validcolumnsso the control is green):By content, the same artifact:
TreeConfigSchema = strictObject({…}, {parentField, labelField, fields, defaultExpandedDepth}), no.passthrough(); typed asz.ZodObjectparameterised by the four-key shape andz.core.$strict.2. spec 17.3.0 — the
.passthrough()claim (npm tarballobjectstack-spec-17.3.0.tgz, extracted to a scratch dir, ⛔ never intonode_modules; version read from the tarball's ownpackage.json=17.3.0):By content: the block ends⚠️ Both readings are positive values from a resolved import, not
}).passthrough()at 17.3.0 and})at 17.4.0. ⇒ Both halves of the card's claim confirmed.undefinedsymbol readings, so the uninterpretable-reading hazard the dispatch flagged does not arise here.3. The re-pinned census actually fails when the drift returns — ablation on the committed tree: mutate
views.tsso the alias becomes NonNullable OFSpecListView['tree']INTERSECTED WITH{ titleField?: string }→ prove it landed on disk (blob hash differs fromHEAD's) →pnpm --filter @object-ui/types build→ prove it reacheddist/views.d.tsby content → run:Restored, rebuilt, and verified: blob equals
HEAD's,git diff HEADempty, anddist/views.d.tsback to the pure alias by content.The census pins are re-pinned as PARITY, not as a literal list
A literal key list is what let this through:
DeclaredKeyandDECLAREDwere maintained beside the type they were supposed to audit, so whentitleFieldwas added the lists were updated to match and the pins agreed with the defect. A census cannot be total over something it also authors.packages/plugin-tree/src/ObjectTree.hostConfigExported-8253.test.ts— the literalDeclaredKeyunion is gone. In its place:EqualOF (TreeViewConfig, NonNullable OFSpecListView['tree']) — structural equality, notextends, a firing control requiring that comparison to sayfalsefor the near-miss that actually shipped, a namedtitleField-is-gone pin, and an explicit no-index-signature pin — that last one does not fall out of parity (at 17.3.0 both sides would carry[key: string]: unknownand parity would still hold), and it is the line that fires if the installed spec ever drops below 17.4.0.TreeConfigSchemaby content, assertstitleFieldis absent, and carries two firing controls (a key the protocol does declare is found; the schema accepts what it declares).packages/types/src/__tests__/tree-view-config-readers-8253.test.ts—DECLAREDstill exists (it.eachneeds a value, and a failure namedreads `labelField`beats one namedreads keys[1]), but it is no longer authored: it is checked againstTreeConfigSchema's own shape in both directions, with a control proving the comparison can fail. ThetitleFieldblock is rewritten from "stays DECLARED" to "is NOT declared — the reads that survive are tolerance", and it now records the three surviving dual-reads explicitly so the next session does not read "the key is gone" as "every read is gone".packages/plugin-view/src/__tests__/ObjectView.treeViewConfigDeclared-8253.test.ts— the "accepts every declared key" literal dropstitleField, and a new@ts-expect-errorcase pins that the host writing the entry inline is now refused for it. That is the diagnostic that moved: what used to be learned from a parse failure at publish is now learned at compile time.Documentation
packages/plugin-tree/README.mdclaimed the exported block "is the single declaration of that shape". It was false when written —ListViewSchema['tree']already published the same block derived — and it is the claim that made the hand copy look legitimate. Corrected to state that the protocol owns the block and thatTreeViewConfigderives from it. ThetitleFieldparagraph is replaced with the refusal, the reason, and the migration (write labelField).Changeset — level justified from this repo's own precedent, by content
@object-ui/typesminor. Removing a key from a published exported type breaks a producer that annotates against it, and this repo grades its own breaking changesminor:AGENTS.md: ⛔ never declaremajor— the fixed group would leave@objectstack's cadence; "objectui 自身的破坏性变更也标minor(在正文里写清 breaking 语义即可)", enforced byscripts/check-changeset-no-major.mjs.packages/types/CHANGELOG.md, the closest precedent by shape —f1d4748"Remove the retiredstriped/bordered/virtualScrolllist-view surface", a spec-side retirement propagated into this package as a key removal, shipped 17.6.0 → Minor Changes. Siblings in the same section:97abb24(Remove BaseFieldMetadata.indexed),9695da7,d2ce342,58b8346— all removals, all minor. The section also states the policy in the body: "BREAKING for hand-written form schemas — deliberately declaredminor."@object-ui/plugin-treeminor (a runtime rung is removed),@object-ui/plugin-viewand@object-ui/app-shellpatch (type-only / annotation-only).node scripts/check-changeset-presence.mjs→ rc 0, and it independently counts 4 released packages changed, matching the 4 entries.Verification (every
rccaptured to a file before any pipe)pnpm --filter '@object-ui/types...' buildturbo run build --filter='!@object-ui/site' --concurrency=2@object-ui/types/plugin-tree/plugin-view/app-shellplugin-list/console/corepackages/types/— 169 files, 3353 testspackages/plugin-tree/— 14 files, 62 testspackages/plugin-view/— 37 files, 316 teststitleFieldConvergence+relayRungCensus-7559— 32 testscheck:spec-symbolscheck:spec-floorscheck:doc-snippets/check:doc-examples/check:readme-exports/check:doc-fencescheck:changeset-presence/check:changeset-no-majorcheck:control-bytes/check:new-line-citations/check:phantom-depstypes/plugin-tree/plugin-view/app-shellcheck:spec-floorsexited 1 withno-artifactfor three unbuilt packages, andcheck:doc-examplesexited 2 (its documented "the gate could not run" code) for four unbuilt packages. Both are 0 on the fully built tree; the first readings are not verdicts about this diff.⭐ objectui#6557's
CONTROL: the tree's second view-declared rung (tree.titleField) still answerspasses — the tolerant fallback is intact, which is the whole reason it was kept.验收备注
@objectstack/specfloorpackages/typesdeclares is^17.3.0, while this type's strictness only exists from 17.4.0.check:spec-floorsis green because it asks whether the symbols exist at the floor, andTreeConfigdoes exist at 17.3.0 — it asks nothing about strictness. Under a 17.3.0 install the block regains[key: string]: unknownand the@ts-expect-errorpins would report "Unused '@ts-expect-error' directive" — loud, not silent, which is why I left it alone rather than smuggling a floor bump in. It would movepnpm-lock.yaml, which fix(core): raise declared @objectstack/spec floor to ^17.3.0 #9032 holds. Noted for the seat; ⛔ not touched.labelField || titleFielddual-reads are undeclared tolerance for a key the protocol refuses. Recorded here and in the test file; their retirement is the follow-up the card names.packages/plugin-view/src/ObjectView.tsx's'tree'branch ends with...(viewOptions.tree || {}), which spreads whatever the block carries onto the node — so a storedtitleFieldstill lands on theobject-treenode even though no face declares it there. Out of scope; it is the same tolerance layer as the three reads above.packages/types/src/zod/objectql.zod.tsandpackages/types/src/objectql.tswere read only.packages/app-shell/src/views/ObjectView.tsxis outside my nominal surface but is not held by any of the 13 open PRs (checked by content) and its edit is forced by the type change — see the ablation above.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code