fix(plugin-tree): type ObjectTreeProps.schema at the published object-tree node - #9548
Conversation
…-tree node
`ObjectTreeProps` declared `schema: any`, so the two reads objectui#8655 was
filed on were structurally UNANSWERABLE: `checker.getPropertyOfType` through
`any` returns `undefined` for a key the node certainly declares exactly as it
does for a nonsense token, so "undeclared" and "unknown" were the same answer.
The card's order is therefore type first, re-measure second, classify third.
The prop and `getTreeConfig`'s parameter are now
`Extract< ObjectQLComponentSchema, { type: 'object-tree' } >` — derived off the
published union, never a second declaration of the node's shape.
Re-measured with the checker, the two filed reads answer in opposite
directions: `data` is DECLARED on `BaseSchema`, so its `as any` is removed;
`navigation` is genuinely undeclared and its cast stays, marking the read
instead of hiding it. Six more reads became answerable that `any` had hidden —
five declared, and `filter` not.
Classified against the objectui#8327 family ruling, each key on its own
reading: `navigation` is objectui#8652's, ruled option B and blocked, so it is
ledgered and not ruled; `filter` is declare-by-mirror-alignment and is NOT
executed here because the two files that carry it are held by an in-flight
branch; `tree` is a protocol VIEW-level block declared on zero element faces,
so declaring it on the node would fork the contract, and retiring the read
needs a producer census a text search cannot make.
Refs: objectui#8655, objectui#8327, objectui#8410, objectui#8652
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5xpA5q533BgTTNADibEFt
✅ 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 reviewVerdict: PASS. Head judged: Implemented-by: claude/issue-8655-object-tree-props-schema-type I did not write this PR and took no figure from its body. Everything below was re-measured in a private detached worktree at that head, with the commands shown and the corpus of every number stated. Where my reading is stronger or weaker than the body's, both are given. The
|
| read sites | distinct keys | sites whose UNCAST type is any |
|
|---|---|---|---|
base 75fca9669a |
16 | 9 | 16 / 16 |
head e85d768553 |
16 | 9 | 0 / 16 |
The nine keys, both commits: data defaultExpandedDepth fields filter labelField navigation objectName parentField tree.
Claim 3 — the per-key verdicts
VERIFIED exactly, all nine rows, by checker.getPropertyOfType against the node type.
| key | sites | verdict | declared on |
|---|---|---|---|
objectName |
5 | DECLARED | ObjectTreeSchema |
parentField |
1 | DECLARED | ObjectTreeSchema |
labelField |
1 | DECLARED | ObjectTreeSchema |
fields |
2 | DECLARED | ObjectTreeSchema |
defaultExpandedDepth |
1 | DECLARED | ObjectTreeSchema |
data |
1 | DECLARED | BaseSchema |
navigation |
1 | UNDECLARED | index signature only |
filter |
3 | UNDECLARED | index signature only |
tree |
1 | UNDECLARED | index signature only |
Site counts sum to 16 and no key is mixed. The two class-(d) reads do answer in opposite directions, and the branch acts accordingly: the cast on data is removed, the cast on navigation is kept.
The control that makes the three UNDECLARED rows readings rather than silence — the same instrument, the same type, asked in one pass:
getPropertyOfType(objectName) -> DECLARED
getPropertyOfType(parentField) -> DECLARED
getPropertyOfType(labelField) -> DECLARED
getPropertyOfType(fields) -> DECLARED
getPropertyOfType(defaultExpandedDepth) -> DECLARED
getPropertyOfType(data) -> DECLARED
getPropertyOfType(navigation) -> undefined
getPropertyOfType(filter) -> undefined
getPropertyOfType(tree) -> undefined
getPropertyOfType(qqzz_absent_token_9999) -> undefined
Asked of the base prop type the same instrument answers undefined for all ten, nonsense token and objectName alike — which is the branch's stated reason for typing first and classifying second, and it reproduces.
Claim 4 — the installed spec has no element entry for this node
VERIFIED exactly.
Corpus: Object.keys(ComponentPropsMap) read off the runtime export of the installed @objectstack/spec (subpath /ui), version confirmed 17.4.0 from its own package.json. Runtime rather than a source grep, so an entry arriving by spread cannot hide.
entries = 45
object-tree present=false
tree present=false
object-calendar present=true <- firing control
object-grid present=true <- firing control
object-kanban present=true <- firing control
qqzz_absent_token_9999 present=false <- clean absent-token control
45 entries, both subjects absent, two named controls present. The screening corpus the body publishes also checks out independently: ls <spec>/src/ui/*.ts | wc -l returns 18 files and cat <spec>/src/ui/*.ts | wc -c returns 1,014,653 bytes, exactly as stated.
Claim 5 — the filter declare is determined and NOT executed
VERIFIED, by blob identity rather than by diff emptiness.
git rev-parse <merge-base>:packages/types/src/objectql.ts -> 9c9a69491c2bd570102cbf33727a9b955978e01a
git rev-parse HEAD:packages/types/src/objectql.ts -> 9c9a69491c2bd570102cbf33727a9b955978e01a
git rev-parse <merge-base>:packages/types/src/zod/objectql.zod.ts -> 8521112d2952d83c5783543048395f6650bac086
git rev-parse HEAD:packages/types/src/zod/objectql.zod.ts -> 8521112d2952d83c5783543048395f6650bac086
Both byte-identical to base — the same blob object, which is an identity check and not an equal-length one. git diff --name-only <merge-base> HEAD -- packages/types returns exactly one path, and it is a test. The whole branch is 5 files: one changeset, one new pin, two modified tests, one renderer.
The serial hold is real. I fetched PR #9540 rather than citing it unread: it is open, draft, on branch claude/issue-9309-object-gallery-filter-typed-unknown, and its file list is exactly four entries — a changeset, a new test, and modified packages/types/src/objectql.ts and packages/types/src/zod/objectql.zod.ts. So the two files the filter declare would need are genuinely held, and stopping was the declared handling.
I also confirmed the verdict it is deferring is live: the ObjectTreeSchema interface in the mirror declares objectName, parentField, labelField, fields and defaultExpandedDepth, and no filter.
Claim 6 — both reverse-validation legs
VERIFIED, both legs re-run by me, worktree restored and clean.
Leg B — remove the navigation read. Deleted the single line reading that key off the schema, then ran the new pin file:
pnpm exec vitest run packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts
Tests 1 failed | 16 passed (17)
AssertionError: navigation is ledgered but no longer read - the exception is a hole:
expected [ 'tree', 'filter', 'fields', ...(5) ] to include 'navigation'
16 passed / 1 failed and the failure message verbatim, on the pin named "the ledger is not stale: every carve-out is STILL READ". Restored with git checkout --; git diff --quiet HEAD then reports the tree identical to the head under review.
Leg A — revert the prop to any. tsc -p packages/plugin-tree/tsconfig.test.json, head vs ablation, as a differential:
total error TS lines |
of which TS2344 | |
|---|---|---|
| head, unmodified | 12 | 0 |
prop reverted to any |
14 | 2 |
The two errors that appear and are absent at head, by diffing the sorted error sets, are both error TS2344: Type 'false' does not satisfy the constraint 'true', and they land on the two compile-time pins named _PropIsThePublishedNode and _PropIsNotAnyAnyMore — the rows that assert the prop IS the published node and is NOT any. Exactly two, exactly those. Restored; tree identical to head.
tsconfig.test.json sets paths: {} so @object-ui/* resolves through built dist, and I could only supply sibling dist trees built at a different commit; the 12 are TS2305/TS2307/TS2322 about @object-ui/components, @object-ui/test-support and @object-ui/plugin-detail, none of them TS2344, and identical across both runs. I built packages/types' own dist from this tree so the types the assertions actually turn on come from the branch under review. The differential is sound; the absolute green the author reports is consistent with it but is not something my rig could reproduce.
Claim 7 — CI at this head
VERIFIED exactly. GET /repos/objectstack-ai/objectui/commits/e85d768553685430e1744e8d842c83d0b57581bd/check-runs?per_page=100, compared as integers rather than by eye:
total_count = 36
len(check_runs) = 36
EQUAL AS NUMBERS = True
completed/success = 33
completed/skipped = 3
non-green or not-completed = 0
The count is not truncated by the page size. The three skipped are dependabot, Test (coverage) and an unexpanded matrix template row Test (coverage shard ${{ matrix.shard }}/4) — none of them contract-relevant; all four real Test (shard n/4) legs, Type Check, Lint, Line Citation Gate and the four changeset gates are among the 33 successes.
Head re-confirmed as e85d768553685430e1744e8d842c83d0b57581bd at the end of the review; it did not move under me.
The two test edits — repaired at the matcher, not by flipping a declaration
Checked specifically for a test weakened to go green. Neither is.
- The renderer test that feeds host column objects at
labelFieldandfieldsnow binds that literal throughas unknown as ObjectTreeProps['schema']with the off-contract shape named in the comment. The runtime assertions below it are untouched; only the input literal is annotated so it compiles under the narrower prop. This is the correct direction — the declaration was not widened to admit the shape. - The types-package control that used to match
function getTreeConfig(schema: any): ResolvedTreeConfigis loosened toschema: \w+and paired with a new, dedicated pin asserting the parameter isnot.toMatchschema: any. Net assertion strength is preserved: the spelling that would make the census unanswerable is named and refused rather than merely no longer required. A false positive in a matcher repaired in the matcher.
Independent gate: node scripts/check-changeset-presence.mjs in the worktree exits 0 — "4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)". The changeset declares '@object-ui/plugin-tree': minor, no major.
Findings
F1 — minor, precision; no change required to land. The "nine siblings" figure is a sub-population, not the arm population, and the stronger true statement is available. Measured over the corpus "the arms written on type ObjectQLComponentSchema = …": there are 12 arms, of which 11 are on the TS barrel and exactly one is not — ObjectTreeSchema. The two arms the body's list omits, ListViewSchema and ObjectViewSchema, are both exported. Every one of the nine named is exported, so nothing stated is false; but "nine siblings are on it" understates a 12/11/1 reading in which this node is the sole omission. Suggest the residual card for the barrel addition carry 12/11/1 rather than "nine".
F2 — informational, for the PM rather than the author. The mechanical half of the clause-② ruling cannot judge this diff, and its own output says so rather than passing it: check-widening-tells.mjs --declaration no over this branch's diff (its --self-test passes 269 cases first, so the instrument fires) returns "5 changed file(s) — 0 judged against a declared surface … 5 NOT MEASURED. ⛔ NOTHING on this diff was examined for widening tells, so this exit 0 is evidence about no surface at all." No declared surface covers packages/plugin-tree/src/** or packages/types/src/__tests__/**. I am recording that exit 0 as a non-reading, not as corroboration; the direction verdict above rests on the comment-masked code diff and the checker, which are readings.
F3 — informational, scoped to my rig. Leg A is reported as a differential over a noisy baseline, for the reason given under Claim 6. It does not weaken the verdict: the two TS2344 appear only under the ablation and only on the two pins that assert the prop's identity.
F4 — nit. The changeset sentence "ObjectTreeProps.schema is the published object-tree node instead of any, and getTreeConfig's parameter with it" sits under an accept-set heading, but getTreeConfig is module-local and is not part of the published surface. Harmless, and the direction is narrowing either way.
None of F1–F4 is a widening the Clause-② line fails to cover, a verdict that failed to reproduce, a control that could not fire, a selection published as a measurement, a pin deleted where it should have been reversed, or a test weakened to go green.
PASS. Labels, draft state and landing untouched — they are the PM's.
Generated by Claude Code
Carrier discharge record — PR #9548 / card #8655
The verdict, and the head it was taken at
Gate ② — re-run after the verdict landed
Gate ③ — check census at the landing head
What the review measured on the clause-② questionThe direction verdict rests on a comment-masked, code-only diff of the renderer (both sides
Label sets as observed immediately before removal
Generated by Claude Code |
Adoption record — and a correction to the discharge record aboveImplemented-by: claude/issue-8655-object-tree-props-schema-type
⛔ This is not the seat issuing the verdict. The measurement was independent and stays independent: ⛔ Correcting myself: the gate ② reading in the discharge record above was taken blindThe discharge record says gate ② was re-run after the verdict landed and exited 0. That run Re-run immediately after the two carriers came off, which is when the thread becomes readable:
⭐ That also retires the puzzle I disclosed in the discharge record. I reported that the ⇒ The PR is not enqueued and will not be until gate ② exits 0 on a run that can read the Generated by Claude Code |
Enqueue record — PR #9548, and the carrier-free window this seat opened
⛔ The defect first, because it is mine and it is the point of the whole disciplineThe reviewer, checking the PR after repairing its verdict, read the event timeline and flagged
That actor was this seat. All Claude Code seats post as ⇒ for about five minutes this PR sat carrier-free with a verdict the gate rejected. ⭐ That is Root cause, diagnosed rather than excused: while The three gates, on runs that could read the thread① verdict comment ② ⭐ My dispatch caused that too. I wrote the review order from memory instead of copying the ③ Before the ready flip: Landing
What rides forward, ⛔ not discharged by this merge
Generated by Claude Code |
Refs: objectui#8655 · objectui#8327 · objectui#8410 · objectui#8652 · objectui#9309
needs:contract-review. I hang no label — the claiming seat does, per its ownClause-②: yesdeclaration.Drafted by an ObjectUI development agent seat. Every figure below was re-derived on this branch and each one carries the command and the CORPUS that produced it.
The order, and why it is not negotiable
ObjectTreePropsdeclaredschema: any. The two reads this card was filed on were reported unanswerable, ⛔ not undeclared, and the control below is the whole reason:Through
any,checker.getPropertyOfTypeanswersundefinedforobjectName— a key this node certainly declares — exactly as it does for a token verified absent from the tree. So "undeclared" and "unknown" were the same byte, and any verdict recorded then would have been an assumption. ⇒ ① type, ② re-measure, ③ classify.① Where the type comes from — DERIVED, not hand-written
spelled with TypeScript's
Extract— the idiomObjectQLComponentSchema's own docblock teaches. One declaration (ObjectTreeSchemain@object-ui/types) stays the only description of the node's shape, so a key added or retyped there arrives here with no edit.Extractand not a named import, for a measured reason. The TS barrel of@object-ui/typesdoes not exportObjectTreeSchema, while its nine siblings (ObjectMapSchema,ObjectGanttSchema,ObjectCalendarSchema,ObjectKanbanSchema,ObjectChartSchema,ObjectGallerySchema,ObjectDataTableSchema,ObjectGridSchema,ObjectFormSchema) are all on it; the ZOD barrel does export it, repaired by objectui#8784. ⛔ That omission is not repaired here — it is a published-surface addition on another package. Reported, not smuggled.getTreeConfig's parameter is typed in the same stroke: it is handed the same prop object, and leaving itanywould move the defect one hop down rather than close it — the six reads in its body would still be asked ofany.② The checker reading after the type — every read, one row each
Resolved by taking the static type of the schema expression before any cast and calling
checker.getPropertyOfType. ⛔ Never a grep (objectui#8410). 16 read sites, 9 distinct keys, and after ① every site's uncast type is the node rather thanany.objectNameObjectTreeSchemaparentFieldObjectTreeSchemalabelFieldObjectTreeSchemafieldsObjectTreeSchemadefaultExpandedDepthObjectTreeSchemadata(class (d))BaseSchemanavigation(class (d))filtertree⭐ The two filed reads answer in opposite directions, and
datais the case objectui#8655's body warned to expect: theas anywas hiding a declaration, not reaching past its absence, so that cast is removed.navigation's cast stays — dropping it would compile throughBaseSchema's index signature and typeanywith nothing marking it, which is the defect rather than the absence of one.anyhad hidden. Five are declared;filteris not. Reported, ⛔ not narrowed away.③ Classification — family ruling, per key, each with its own reading
One reading governs all three, measured rather than assumed: the installed
@objectstack/spec@17.4.0has noComponentPropsMapentry forobject-tree(nor fortree), whileobject-calendarandobject-gridare present and a verified-absent token is not. So no key can be confirmed on "the element schema this node maps to" — there is none. Each verdict therefore names the schema the spec does declare the key on.Screening, published with its corpus: the 18
.tsfiles undersrc/ui/of the installed@objectstack/spec@17.4.0(1,014,653 bytes); counting rulecat CORPUS | grep -o '\bTOKEN\b' | wc -l— occurrences, ⛔ not lines.filter339 ·navigation143 ·tree37. Firing controls, same corpus:groupField3,NavigationConfigSchema4,KanbanConfigSchema3. Negative controlzzpl_tree_absent_control_8655→ 0, verified zero in this corpus before use. All three subjects screen non-zero ⇒ each was confirmed on a schema with the checker, per triage.navigation— ⛔ NOT RULED HERE. objectui#8652's family: maintainer ruled option B (declare on the PLATFORM element schemas first, then mirror),pm:blockedon the platform half, unlock criterion = a released spec carrying the declaration installable here. Measured on the installed spec:navigationis declared on exactly one of 45 element entries,object-grid— so the unlock has not happened. Ledgered by name, read untouched.filter— DECLARE (align the mirror), and ⛔ NOT EXECUTED on this branch. The spec declares it onListViewand on four comparable element faces (object-grid,object-kanban,object-calendar,object-metric); this repo's mirror declares it on all eight sibling node schemas and omits it on this one alone. ⛔ Blocked by a declared serial constraint:packages/types/src/objectql.tsand its zod twin are held by an in-flight branch (objectui#9309), whose PR is open and modifies both. The declared handling for that breach is to stop and report, so the ledger row reddens the day the key is declared.tree— ⛔ declare is OFF THE TABLE; this card does not retire it either. The spec declarestreeon the view (ListView.tree, whose four members are exactly the four this resolver reads, mirrored here asTreeViewConfig) and on zero of 45 element faces. Putting a view-level block on a node schema forks the contract rather than mirroring it. Retiring the read is the other exit and it needs a producer census this card cannot soundly make: producers deliver these keys by SPREADING the block flat, and a text census is structurally blind to a key arriving through a spread — objectui#8651 measured exactly that and had to withdraw a retirement. ⇒ routed, rung untouched.Accept-set change, stated plainly
NARROWS
@object-ui/plugin-tree's published props type.anyaccepted everything; the node accepts whatObjectTreeSchemadeclares plus whateverBaseSchema's[key: string]: anyadmits. Breaking for a TypeScript consumer passing a refused value at a declared key —labelFieldandfieldsare the two that bite, since the renderer tolerates host column OBJECTS there while the declaration saysstring/string[]. This package's own test carried that shape; it now says so at the mount instead of compiling silently, ⛔ rather than the declaration being widened to make it compile.⭐ Nothing in
@object-ui/typesis declared, widened or narrowed here — the only file touched in that package is a test. Step ③ landed exactly one declare verdict and it is not executed, so nothing on any published schema widens on this branch. That is the input the claiming seat'sClause-②re-read needs.Evidence
Reverse validation, both legs, each restored byte-identically (
git statusclean after each):any→tsc -p tsconfig.test.jsonred with TS2344 twice, on the two pins that say the prop is the published node and is notany.navigationread removed → the runtime ledger row fails: "navigation is ledgered but no longer read — the exception is a hole". 16 passed, 1 failed.Runs (heavy ones serialised through the shared verify lock):
pnpm exec vitest run packages/plugin-tree/pnpm exec vitest run packages/types/ + 4 cross-package readerspnpm exec turbo run type-check --filter=@object-ui/plugin-treeeslintonpackages/plugin-treeand the edited types testnode scripts/check-changeset-presence.mjsnode scripts/check-changeset-fixed.mjs+check-changeset-no-major.mjsnode scripts/check-new-cross-file-line-citations.mjsnode scripts/check-test-path-roots.mjsnode scripts/check-governed-queue-guard.mjs --testLint warnings on the edited renderer went 25 → 22 (same file, same command, base vs head): three
no-explicit-anywarnings removed, and theexhaustive-depsnote became more precise rather than new.Residuals — named, ⛔ none of them done here
filteris owed a declare onObjectTreeSchemaand its zod twin. Blocked on the serial hold above; the ledger row in the new pin reddens when it lands.ObjectTreeSchemais missing from the@object-ui/typesTS barrel while its nine siblings are on it and its zod twin is on the zod barrel. A published-surface addition on another package; wants its own card.treeandnavigationremain undeclared reads by decision, each ledgered with its reason and each asserted still-read so the exemption cannot rot.Landing
⛔ Stays draft. Not enqueued, no auto-merge, ready-for-review not flipped — landing is the claiming seat's.
Generated by Claude Code