feat(types,plugin-view,plugin-tree): export the host tree view config as TreeViewConfig (objectui#8253) - #8828
Conversation
…ig as `TreeViewConfig` (objectui#8253) `tree` is a host-composition-only view type (objectui#5321 ruling B): on neither authored union, reached only when a host passes a `views` prop. The per-view `tree` block that path reads had no exported type — its only description was the module-local, non-exported `interface TreeConfig` in `ObjectTree.tsx`. The live host is the console, which stores view records and passes them as `views`, so a real consumer wrote this block against nothing and a misspelled `parentFeild` was admitted by the entry's `[key: string]: any`, stored, and never reported. Ruled option (a) on objectui#8253 (decision batch #78, 2026-09-07, maintainer 「同意」): `packages/types` exports the config, the module-local copy becomes an import of it, and there is no second copy. - `types/src/views.ts` declares `TreeViewConfig`; the barrel re-exports it. - `plugin-tree` imports it; its resolver's `ResolvedTreeConfig` is `Pick`ed off the one declaration and only adds requiredness for the two floored keys. - `plugin-view` declares `ObjectViewProps.views[n].tree` as `TreeViewConfig` — a narrowing on a face whose index signature already admitted every spelling. - `titleField` stays declared rather than deleted: the console's create-view dialog does not offer it, but the console's own composition reads it by name and objectui#6557 pins that rung as live behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…r the +27-line declaration (objectui#8253) `UNGATED_EXAMPLES` addresses each block by `file:LINE symbol`, so declaring `views[n].tree` in `ObjectViewProps` — 27 lines above the component docblock — moved all three ObjectView rows out from under their keys. `check:doc-examples` reported them as three UNDECLARED FAILURES plus three STALE LEDGER ROWs; the blocks and their reasons are unchanged, only the addresses. Same maintenance objectui#8466 performs on its own row in this ledger. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
…(objectui#8253) `check:spec-symbols` records a per-package DEBT ledger of local declarations that reuse a `@objectstack/spec` export name — "a local declaration under a spec export's name is read by the next agent as the spec's own definition". `@object-ui/plugin-tree` carried one row, `TreeConfig`, and this card's export retires that declaration outright, so the gate now fails the other way: a DEBT entry that no longer collides. Measured on the installed `@objectstack/spec@17.3.0`: `TreeConfig` appears 38 times in its dist, `TreeViewConfig` zero. So the ruling's suggested name is not merely a rename — it is the one that leaves the ledger empty for this package. 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 review at
|
Contract review at
|
Fixes #8253
Implements the recorded ruling — option (a): export the host
treeview config type (director seat, decision batch #78, 2026-09-07, maintainer 「同意」). ⛔ Not re-opened, not re-routed.What was wrong
treeis a host-composition-only view type (objectui#5321 ruling B): it is a member of neitherObjectViewSchema.defaultViewTypenorNamedListView.type, so no authored document reaches that branch — it runs only when a host passes aviewsprop. On that path a per-viewtreeblock is read at four sites, and its only description anywhere was the module-local, non-exportedinterface TreeConfiginplugin-tree/src/ObjectTree.tsx.The live host is the console: it stores view records and passes them as
views, and its create-view dialog offerstree. So a real consumer wrote this block with no type to write it against, and a misspelledparentFeildwas admitted by the views entry's[key: string]: any, stored, read by nobody and reported by nothing.The widening statement (强制条款②), in strict/lenient terms
Names that become reachable to a consumer that could not reach them before: exactly one —
TreeViewConfig, exported from@object-ui/types(declared insrc/views.ts, re-exported from the barrel, present indist/index.d.ts). Nothing else is added, renamed, retyped or removed on any published face.Does anything's value validation change? Yes — in the narrowing direction only, on exactly one key.
The brief's direction rule was checked against this face rather than assumed, and this card is the narrowing case, not the retirement case:
ObjectViewProps.views[n]closes with[key: string]: any. On such a face a declaration cannot widen — every spelling was already admitted, so no value that used to be refused becomes accepted. It can only narrow, by adding validation where there was none.tree?: TreeViewConfigdoes exactly that: a host composing the entry as an object literal now getsparentFeildreported as an excess property instead of silently dropped.ObjectView.treeViewConfigDeclared-8253.test.ts) — without the second, "it narrows" would be indistinguishable from "the index signature was removed", which would break every host.?: neverand no deletion-absorption problem arises.app-shell/src/views/ObjectView.tsxbuildsmergedViewsasviews.map((v: any) => …)over stored view records, so it arrives typedany[]. The diagnostic reaches a host that composes the entry inline against the declared prop type. Typing the stored-record path is an app-shell change and was recorded, not smuggled in here.titleField: declared, not deleted — and that was a measurementThe ruling put this key to a test: declare it if the console writes it, else remove the read. The measurement came back split, and the READ side decides.
CreateViewDialog.tsx'streeslot collectsparentFieldalone.titleFieldis what its calendar / timeline / gantt slots collect — so the zero is about the tree slot, not a broken instrument.app-shell/src/views/ObjectView.tsx(viewDef.tree?.titleField), with a comment naming it.plugin-view/src/ObjectView.tsx(viewOptions.tree?.titleField) andplugin-list/src/ListView.tsx(treeCfg.titleField).app-shell/src/views/ObjectView.titleFieldConvergence.test.tsx: "CONTROL: the tree's second view-declared rung (tree.titleField) still answers", landed by objectui#6557 precisely so a later edit could not collapse the tree's two view-declared rungs into one.⇒ Deleting the read would have reversed a recorded ruling and reddened its pin — a maintainer's call, not a rider on this card. Declaring it makes declared = enforced at all four read sites at once.
labelFieldstays canonical and wins wherever both are present; ⛔ it is not extended into a tolerant dual-read.Note the ruling's own scope,
ObjectTree.tsx:116-120, contains thetitleFieldread (at :118 on this head, cited as :119 in the ruling), so "exactly the keys that range reads" is five keys, not four.Unplanned finding: the export pays off a recorded spec-name-collision debt
check:spec-symbolskeeps a per-package DEBT ledger of local declarations that reuse a@objectstack/specexport name.@object-ui/plugin-treecarried exactly one row:TreeConfig. Retiring that declaration made the gate fail the other way (a debt row that no longer collides), and the row is deleted here.Measured on the installed
@objectstack/spec@17.3.0dist:TreeConfigappears 38 times,TreeViewConfigzero. The ruling's suggested name is therefore not merely a rename — it is the one that leaves this package's ledger empty.The pin, and what it would catch
Two files, two halves, neither redundant:
plugin-tree/src/ObjectTree.hostConfigExported-8253.test.ts— reachability through the published surface. The import says@object-ui/types, and that is load-bearing: this package'stsconfig.test.jsonsets"paths": {}so the specifier resolves through the workspace dependency'sexportsmap topackages/types/dist/index.d.ts, the way a published consumer resolves it. A pin importing'../../types/src/views'would never touchdistor theexportsmap and would stay green through a barrel that had stopped re-exporting the name. It pins: non-vacuity (IsAnyfalse), thekeyofcensus in both directions (which also refuses an index signature — one here would re-open the whole hole), all-keys-optional, each key's type, and@ts-expect-erroronparentFeild.types/src/__tests__/tree-view-config-readers-8253.test.ts— the derived half a type-level pin cannot see: the private interface is gone, the reader imports the exported name,ResolvedTreeConfigisPicked off the one declaration rather than hand-written beside it, all four reader sites exist, and thetitleFieldmeasurement above is pinned with its firing control. It cannot live in plugin-tree (no"types": ["node"]there) and cannot import@object-ui/types(no self-link inpackages/types/node_modules).What they catch: un-exporting the name, dropping the barrel re-export, breaking the
exportsmap, re-deriving a private copy in the renderer, adding an index signature, adding a key with no reader, removing a key from under one, or hand-writing a twin of the resolved type.@object-ui/typesto be built — a staledist/*.d.tslies in both directions. Stated in both headers.Verification
Ablation — delete the export, prove the mutation on disk before reading any result
Target: the barrel re-export in
packages/types/src/index.ts. Mutation, rebuild and restore ran in one shell under atrap … EXIT INT TERM.Restored via
git checkout HEAD -- ABSOLUTE_PATHand verified by blob hash againstHEAD, not by an exit code.distwas rebuilt afterwards so no mutated artifact survives.Non-vacuity — the red is a change of state, not the resting colour
pnpm --filter @object-ui/plugin-tree type-checkThe console call-site diagnostic, compiled against built
distThe README fence extracted verbatim and compiled with
tsc --strictresolving through theexportsmap:That is the class-(c) defect from the triage comment, now a compile error. The probe file was removed under a trap (
git statusclean).Gates — every exit code captured before any pipe (
cmd > file; rc=$?), at final head49f36c3b8check:control-bytes·check:spec-symbols·check:doc-examples·check:doc-snippetscheck:readme-exports·check:doc-fences·check:doc-types·check:doc-example-readerscheck:phantom-deps·check:unused-deps·check:self-importcheck:published-tsconfig-exclude·check:unreferenced-sourcestype-check(types + plugin-tree + plugin-view)lint(types + plugin-tree + plugin-view)check:doc-snippetsreports 637 of 637 blocks judged, 0 failed — that is the ruling's "the plugin README's example type-checks against the exported type", measured.check:readme-exports,check:doc-snippetsandcheck:doc-examplesfirst came back red for one shared reason — 22 unbuilt packages ("run the build first"), a prerequisite miss, not a verdict. All three were re-run green after building the 34 packages the snippet gate's own--build-filternames.Lint is a declared narrowing, and here is its evidence. (i) The config has no type-aware linting (
grep -nE "projectService|project:" eslint.config.js→ exit 1; firing controlparser→ 1 hit), so this diff cannot move any untouched file's verdict. (ii) File count read fromeslint --format json: 8 files. (iii) Same five shared files linted at BASE and at HEAD with the same instrument: 1 error / 84 warnings at both — the ratchet is flat, and the three new test files contribute 0 / 0. That lone "error" is an artefact of my stricter--no-inline-config; it lands on a deliberate documented/* eslint-disable no-restricted-imports */, and the repo's ownpnpm lint(turbo run lint→ per-packageeslint .) honours it and exits 0.Tests
packages/types/+packages/plugin-tree/packages/plugin-view/apps/console/(full)apps/console/src/__tests__/registry-inputs-spec-parity.test.ts— run explicitly, because a whole-suite pass is not a reading for one filescripts/__tests__/check-doc-example-types.test.ts+…-shared-reader.test.ts(owed for touching a gate script)Base→main pre-flight
git diff --name-only BASE origin/main→ 39 files moved on main since this branch's base, so the control can fire (verified:packages/types/src/complex.tsis in the window). Intersection with the six source files my pins read off disk: 0. The empty result is a reading, not a vacuum.验收备注
ObjectTree.tsx:119'sschema.titleFieldread againstObjectTreeSchema, which does not declare it. Answer: the key is now declared on the host config (TreeViewConfig.titleField) on the evidence in the table above.ObjectTreeSchema— the authored node face — is deliberately left alone: that is a different surface, and adding a key there would widen authored metadata, which no ruling authorises.ObjectTree.getTreeConfigreadstitleFieldoff the node (schema.titleField) but not off the nested block (nested.titleField). On every real host path this is invisible, becauseObjectView/ListView/ app-shell all spread the block flat onto the node before the renderer sees it. Closing it would mean adding an accepted spelling — a widening — so it is left exactly as measured. ⛔ Not filed: no reproducible defect and no contract violated.ResolvedTreeConfigis not a second copy. Every key name and type isPicked offTreeViewConfig; it adds only requiredness, and only for the two keys the resolver floors (labelField→'name',fields→[]). A structurally-equal hand-written twin would type-check fine and defeat the export, so the derivation is pinned by shape.kanbanarm #8799, feat(types): declare ObjectCalendarSchema.colorField and .allDayField (#8466) #8807, finding(types): 27 published.describe()schema descriptions inpackages/types/src/zod/**cite source line addresses — the authorable surface carries a citation nothing reads #8478); the instrument was verified firing (self-intersection = 18). But measuring feat(types): declare ObjectCalendarSchema.colorField and .allDayField (#8466) #8807's actual branch shows it is wider than the list I was given: it also touchesscripts/check-doc-example-types.mjs, which this PR touches too. The hunks are ~190 lines apart and non-overlapping — feat(types): declare ObjectCalendarSchema.colorField and .allDayField (#8466) #8807 re-keys oneobjectql.tsrow, this PR re-keys threeplugin-view/src/ObjectView.tsxrows — and both are the same maintenance that line-keyed ledger demands of any PR inserting a line above an@example. Mine is not deferrable: feat(types): declare ObjectCalendarSchema.colorField and .allDayField (#8466) #8807 does not touchplugin-view/src/ObjectView.tsx, so only a PR that moves those lines can re-key those rows. Flagging rather than deciding.维护者速读(草稿)
改了什么 — 把宿主写的 per-view
tree配置块从「一个藏在渲染器里、没导出的私有 interface」提升为@object-ui/types的正式导出类型TreeViewConfig,渲染器改为 import 它(⛔ 不留第二份副本),并在ObjectViewProps.views条目上声明tree?: TreeViewConfig。为什么改 — console 今天就在存这份配置、并让用户反复改写它。可它没有任何类型可对着写:用户把
parentField拼成parentFeild,配置照存不误,没人读、也没有任何提示。「被存下来、由非作者再写一遍」的东西就是契约,契约必须有类型 —— 这是 #8253 的裁决原话。风险与代价(含回滚) — 风险很低,方向是收紧而非放宽。
那个 views 条目本来就带一个开放的索引签名(index signature,值类型 any),所以以前能通过的值现在没有一个被拒绝,变化只是多了一处校验;索引签名原样保留,宿主的其它自定义键照旧。
回滚方式是 revert 本 PR 的三个 commit;没有数据迁移,也没有任何运行期行为改变(titleField 的读取原样保留,理由见上表)。附带收益:plugin-tree 与上游 spec 的那笔命名冲突欠账就此还清。
席位意见 — (留空,待 contract-review 席位定稿)
你要做的 — 确认两件事即可:①
titleField保留声明而非删除读取(依据是 #6557 已有的 pin 与 console 自身的读取,删它等于推翻旧裁决);② PM 需要为上面那条scripts/check-doc-example-types.mjs与 #8807 的先后顺序做个安排。⛔ Do not enqueue.
Clause-②: yes— this enlarges the published type face, so an in-seat contract-review-tier PASS is required first.needs:contract-reviewis hung on both carriers (card #8253 and this PR); a legitimate clear removes both together.🤖 Generated with Claude Code
https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
Generated by Claude Code