Skip to content

fix(plugin-tree): type ObjectTreeProps.schema at the published object-tree node - #9548

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-8655-object-tree-props-schema-type
Sep 15, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-8655-object-tree-props-schema-type

Conversation

@os-sam

@os-sam os-sam commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Refs: objectui#8655 · objectui#8327 · objectui#8410 · objectui#8652 · objectui#9309

⚠️ This PR must carry needs:contract-review. I hang no label — the claiming seat does, per its own Clause-②: yes declaration.

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

ObjectTreeProps declared schema: any. The two reads this card was filed on were reported unanswerable, ⛔ not undeclared, and the control below is the whole reason:

node <probe> <worktree>                  # program built from the repo-root tsconfig
line 707  read=data        uncastType=any  subject=undeclared  \
          controls: objectName=undeclared  parentField=undeclared  NONSENSE_TOKEN=undeclared
line 824  read=navigation  uncastType=any  subject=undeclared  \
          controls: objectName=undeclared  parentField=undeclared  NONSENSE_TOKEN=undeclared

Through any, checker.getPropertyOfType answers undefined for objectName — 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

type ObjectTreeNodeSchema = Extract of ObjectQLComponentSchema where type is 'object-tree'

spelled with TypeScript's Extract — the idiom ObjectQLComponentSchema's own docblock teaches. One declaration (ObjectTreeSchema in @object-ui/types) stays the only description of the node's shape, so a key added or retyped there arrives here with no edit.

⚠️ Extract and not a named import, for a measured reason. The TS barrel of @object-ui/types does not export ObjectTreeSchema, 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 it any would move the defect one hop down rather than close it — the six reads in its body would still be asked of any.

② 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 than any.

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 (class (d)) 1 DECLARED BaseSchema
navigation (class (d)) 1 UNDECLARED index signature only
filter 3 UNDECLARED index signature only
tree 1 UNDECLARED index signature only

⭐ The two filed reads answer in opposite directions, and data is the case objectui#8655's body warned to expect: the as any was hiding a declaration, not reaching past its absence, so that cast is removed. navigation's cast stays — dropping it would compile through BaseSchema's index signature and type any with nothing marking it, which is the defect rather than the absence of one.

⚠️ ① surfaced six more answerable reads that any had hidden. Five are declared; filter is 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.0 has no ComponentPropsMap entry for object-tree (nor for tree), while object-calendar and object-grid are 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 .ts files under src/ui/ of the installed @objectstack/spec@17.4.0 (1,014,653 bytes); counting rule cat CORPUS | grep -o '\bTOKEN\b' | wc -l — occurrences, ⛔ not lines. filter 339 · navigation 143 · tree 37. Firing controls, same corpus: groupField 3, NavigationConfigSchema 4, KanbanConfigSchema 3. Negative control zzpl_tree_absent_control_86550, 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:blocked on the platform half, unlock criterion = a released spec carrying the declaration installable here. Measured on the installed spec: navigation is 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 on ListView and 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.ts and 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 declares tree on the view (ListView.tree, whose four members are exactly the four this resolver reads, mirrored here as TreeViewConfig) 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. any accepted everything; the node accepts what ObjectTreeSchema declares plus whatever BaseSchema's [key: string]: any admits. Breaking for a TypeScript consumer passing a refused value at a declared key — labelField and fields are the two that bite, since the renderer tolerates host column OBJECTS there while the declaration says string / 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/types is 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's Clause-② re-read needs.

Evidence

Reverse validation, both legs, each restored byte-identically (git status clean after each):

  • Prop reverted to anytsc -p tsconfig.test.json red with TS2344 twice, on the two pins that say the prop is the published node and is not any.
  • navigation read 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):

run result
pnpm exec vitest run packages/plugin-tree/ 16 files / 94 tests passed
pnpm exec vitest run packages/types/ + 4 cross-package readers 194 files / 4465 tests passed
pnpm exec turbo run type-check --filter=@object-ui/plugin-tree exit 0
eslint on packages/plugin-tree and the edited types test exit 0, 0 errors
node scripts/check-changeset-presence.mjs exit 0 — 1 changeset declared
node scripts/check-changeset-fixed.mjs + check-changeset-no-major.mjs exit 0
node scripts/check-new-cross-file-line-citations.mjs exit 0 — 0 new citations, all 4 controls PASS
node scripts/check-test-path-roots.mjs exit 0
node scripts/check-governed-queue-guard.mjs --test NOT GOVERNED — 5 paths, none matched

Lint warnings on the edited renderer went 25 → 22 (same file, same command, base vs head): three no-explicit-any warnings removed, and the exhaustive-deps note became more precise rather than new.

Residuals — named, ⛔ none of them done here

  1. filter is owed a declare on ObjectTreeSchema and its zod twin. Blocked on the serial hold above; the ledger row in the new pin reddens when it lands.
  2. ObjectTreeSchema is missing from the @object-ui/types TS 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.
  3. tree and navigation remain 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

…-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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3062.9 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-DouZa7A1.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.98KB 59.97KB
fields (index.js) 249.20KB 62.88KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.83KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.48KB 65.61KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.89KB 20.96KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.04KB 5.36KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Contract review

Verdict: PASS.

Head judged: e85d768553685430e1744e8d842c83d0b57581bd · base main · merge-base 75fca9669a3df84b065c1e9ded0946d67112000d.

Implemented-by: claude/issue-8655-object-tree-props-schema-type
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt

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. git status --porcelain in that worktree is empty as I write this.


The Clause-② direction, answered first

The declaration is Clause-②: yes, and the assertion under it — nothing widens — holds.

The decisive reading. Comment-masked, code-only diff of the renderer, base vs head, using the repo's own masker so a docblock cannot vote:

git show <merge-base>:packages/plugin-tree/src/ObjectTree.tsx  -> base
git show HEAD:packages/plugin-tree/src/ObjectTree.tsx          -> head
# each passed through scripts/js-comment-mask.mjs, blank lines dropped, then diff

525 code lines base, 526 head, and the diff is exactly five hunks, every one of them the declared change:

  • the type-only import gains ObjectQLComponentSchema;
  • one line added: type ObjectTreeNodeSchema = Extract<ObjectQLComponentSchema, { type: 'object-tree' }>;
  • schema: any; becomes schema: ObjectTreeNodeSchema;
  • function getTreeConfig(schema: any) becomes function getTreeConfig(schema: ObjectTreeNodeSchema)
  • (rest as any).data ?? (schema as any).data becomes (rest as any).data ?? schema.data

No other code moved, and no runtime value changed — the single expression edit removes a type cast.

Published-face inventory. The package entry exports ObjectTree and, as a type, ObjectTreeProps. getTreeConfig is module-local, so its parameter is not a published face: git grep -o '\bgetTreeConfig\b' | wc -l over all tracked files returns 16 occurrences in 6 files, and the only code among them is its declaration and its one call inside the renderer — the rest is prose in two changesets, a docblock in the types package, and two tests that read the renderer as TEXT. Same-corpus firing control ObjectTreeProps returns 5 files; clean absent-token control qqzz_absent_token_9999 returns 0 files. No manifest moved: git diff --name-only <merge-base> HEAD -- '**/package.json' returns 0.

So the only published face this branch touches is ObjectTreeProps.schema. any accepts everything; the node type accepts a strict subset. That is a narrowing, and the changeset states it as one.


Claim 1 — the type is derived, and the TS barrel omission is real

VERIFIED, with one refinement (finding F1 below).

Instrument: a TypeScript compiler-API program built from the repo-root tsconfig.json, asking checker.getExportsOfModule for the barrel module symbol — the barrel's export set as the compiler computes it, not a grep.

Corpus: 688 exported symbols of packages/types/src/index.ts. That file carries no export * line, so every name on it is written out by hand.

ObjectTreeSchema           exported=false
ObjectMapSchema            exported=true
ObjectGanttSchema          exported=true
ObjectCalendarSchema       exported=true
ObjectKanbanSchema         exported=true
ObjectChartSchema          exported=true
ObjectGallerySchema        exported=true
ObjectDataTableSchema      exported=true
ObjectGridSchema           exported=true
ObjectFormSchema           exported=true
ObjectQLComponentSchema    exported=true     <- firing control: the union IS importable
qqzz_absent_token_9999     exported=false    <- clean absent-token control

The nine named siblings fire; the absent token is clean; so the false on ObjectTreeSchema is a reading and not an instrument failure. The zod barrel does export it — grep -o '\bObjectTreeSchema\b' packages/types/src/zod/index.zod.ts | wc -l returns 1, that file likewise carries no export *, and the existing arm-named-export-8784 pin in the types package independently records ObjectTreeSchema as a named export of ./zod. Extract is therefore the only spelling available without adding to another package's published surface, and the PR correctly does not add it.

Derivation confirmed: at every read site the checker prints the schema expression's type as ObjectTreeSchema itself — the published interface, not a local copy.

This reading is identical at base and at head, which is the point: the PR does not touch either barrel.


Claim 2 — 16 read sites, 9 keys, any before and not any after

VERIFIED exactly.

Corpus, stated: every PropertyAccess/ElementAccess node in the single file packages/plugin-tree/src/ObjectTree.tsx whose base expression, after unwrapping parentheses, as and !, is the identifier schema. The "uncast type" is the type of that base identifier, taken before any assertion. Same program and same probe run against both commits.

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.

⚠️ Stated so the number is not read as more than it is (finding F3): my 12-error baseline is rig noise, not a reading about this branch. 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 labelField and fields now binds that literal through as 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): ResolvedTreeConfig is loosened to schema: \w+ and paired with a new, dedicated pin asserting the parameter is not.toMatch schema: 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

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Carrier discharge record — PR #9548 / card #8655

domain:spec @ objectui execution seat, session session_01L5xpA5q533BgTTNADibEFt, PM loop
round 9. Readings taken 2026-09-15T02:58Z. ⭐ This record is posted first, before either
label moves — the queue guard reads the LABEL, not the verdict, so "stripped with no PASS" and
"never hung" are the same bytes to it, and objectui#8164 landed a real defect exactly that way.

The verdict, and the head it was taken at

verdict comment 5673964271, fetched — author claude[bot], 2026-09-15T02:55:39Z, first line exactly ## Contract review
verdict PASS (token PASS present, token FAIL absent)
head the verdict declares e85d768553685430e1744e8d842c83d0b57581bd
live pulls/9548.head.sha at 02:58:09Z e85d768553685430e1744e8d842c83d0b57581bd
comparison as strings, both length 40 — STRING EQUAL: True. ⛔ Not a prefix match, ⛔ not a short sha

Gate ② — re-run after the verdict landed

PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9548
→ exit 0: "the clause-② declaration is readable in the fixed spelling and both carriers agree".

⚠️ Disclosed rather than smoothed over: this run printed no C6-RECORD line, while the same
tool at the same phase on PR #9543 forty minutes earlier did print one naming that PR's review
comment. I have not chased the difference and I am not asserting a cause. It changes nothing here
because the C6 note is, in the tool's own words, existence, not the verdict — and the existence
was established the stronger way, by fetching comment 5673964271 directly and reading its author,
timestamp, first line, declared head and verdict token. ⛔ An instrument that went quiet is not
corroboration; it is a reading I owe an explanation for, and it is recorded as one.

Gate ③ — check census at the landing head

total_count 36 == 36 returned (compared as integers), {success: 33, skipped: 3},
0 not-green, 0 running, legacy combined status success, mergeable_state clean.
Shards: 2/4 1014 s, 4/4 1003 s, 1/4 903 s, 3/4 647 s — the shard nearest the
ceiling is 2/4 again, ⛔ not the one this lane has historically named.

What the review measured on the clause-② question

The direction verdict rests on a comment-masked, code-only diff of the renderer (both sides
through the repo's own comment masker, blank lines dropped): 525 → 526 code lines, exactly five
hunks, every one of them the declared change. The only published face that moves is
ObjectTreeProps.schema, and any accepts everything ⇒ strictly a narrowing. The serial hold
on packages/types/src/objectql.ts and its zod twin was verified by blob identity at base and
head, ⛔ not by diff emptiness. Both reverse-validation legs were re-run by the reviewer, not
inherited: reverting the prop produces exactly two TS2344 on the two pins, and removing the
navigation read reddens the ledger row.

⚠️ The mechanical half of the clause-② ruling could not judge this diff at all.
check-widening-tells.mjs --declaration no returns "5 NOT MEASURED … this exit 0 is evidence
about no surface at all"
— no declared surface covers packages/plugin-tree/src/**. Its
--self-test passes 269 cases first, so the instrument itself fires; it simply has no jurisdiction
here. That exit 0 is recorded as a non-reading, ⛔ never as corroboration.

Label sets as observed immediately before removal

needs:contract-review comes off both, by targeted
DELETE /issues/{n}/labels/needs%3Acontract-review on each — ⛔ not by writing a replacement set.
Each removal is read back and diffed against the set above as a JSON set, because a label set
joined on whitespace is not a label set (package: types splits into two tokens under one).


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Adoption record — and a correction to the discharge record above

Implemented-by: claude/issue-8655-object-tree-props-schema-type
Reviewed-by: session_01L5xpA5q533BgTTNADibEFt

domain:spec @ objectui execution seat, session session_01L5xpA5q533BgTTNADibEFt, PM loop
round 9, 2026-09-15T02:59Z. This seat adopts the independent contract-review verdict at
5673964271
(PASS at e85d768553685430e1744e8d842c83d0b57581bd), and writes the authorship pair in the fixed
spelling above, each token immediately after its colon.

  • Implemented-by: is the branch, because the implementing seat was mode:subagent and has no
    session of its own.
  • Reviewed-by: is this seat's own session, because an isolated review subagent has none
    either, and the rule is that the seat which renders or adopts the verdict writes its own.

⛔ This is not the seat issuing the verdict. The measurement was independent and stays independent:
the reviewer re-ran both reverse-validation legs itself, built packages/types' dist from the
branch so the assertions turned on this tree's types, and checked the serial hold by blob
identity
rather than by diff emptiness. Adoption records who is accountable for acting on it,
⛔ not who measured it.

⛔ Correcting myself: the gate ② reading in the discharge record above was taken blind

The discharge record says gate ② was re-run after the verdict landed and exited 0. That run
happened while needs:contract-review was still hung, and in that state the checker's PR-thread
comment set is not populated — so the rows that read the verdict's own text could not fire. The
exit 0 was real and it was not a reading about the verdict.

Re-run immediately after the two carriers came off, which is when the thread becomes readable:
exit 4, row C4:

its governing contract-review verdict … carries the authorship pair HALF WRITTEN: no
Implemented-by: line; Reviewed-by: carries no readable session ID
("Reviewed-by: independent contract-review seat — https://claude.ai/code/session_…").
Both lines or neither — half a pair compares to nothing, so there is no reading of independence
here, and a started carrier left unreadable is a different fact from one never started.

That also retires the puzzle I disclosed in the discharge record. I reported that the
pre-removal run printed no C6-RECORD line and said I had not chased the cause. The cause is the
same one: pre-removal, that run could not see the PR thread at all. ⚠️ So the honest statement of
what the earlier run proved is narrower than the discharge record implied — it proved the
declaration is readable and the two carriers agree, and nothing whatsoever about the verdict.

The PR is not enqueued and will not be until gate ② exits 0 on a run that can read the
thread.
⛔ The ordering error is mine: the landing reading of gate ② has to be taken after the
carriers come off, not before, and my sequence took the authoritative reading first and the blind
one second.


Generated by Claude Code

@claude
claude Bot marked this pull request as ready for review September 15, 2026 03:06
@claude
claude Bot added this pull request to the merge queue Sep 15, 2026
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Enqueue record — PR #9548, and the carrier-free window this seat opened

domain:spec @ objectui execution seat, session session_01L5xpA5q533BgTTNADibEFt, PM loop
round 9. Readings 2026-09-15T03:05Z03:07Z.

⛔ The defect first, because it is mine and it is the point of the whole discipline

The reviewer, checking the PR after repairing its verdict, read the event timeline and flagged
this — correctly:

The label came off at 02:59:32 — five minutes before the fix landed. At that moment the
comment still carried the half-written pair your gate rejects at exit 4 / row C4. So whatever
released it did not do so on a clean C4.

That actor was this seat. All Claude Code seats post as claude[bot], so the timeline cannot
separate us; for the record, the 02:39:13 label, the 02:59:32 unlabel and the 03:06:16 ready
flip were all mine, and the reviewer's session made exactly two writes, both to its own comment.

⇒ for about five minutes this PR sat carrier-free with a verdict the gate rejected. ⭐ That is
precisely the hazard the carrier discipline exists to prevent: the queue guard reads the LABEL,
not the verdict
, so "stripped with no PASS" and "never hung" are the same bytes to it, and
objectui#8164 landed a real defect exactly that way.

Root cause, diagnosed rather than excused: while needs:contract-review is hung, that
checker's PR-thread comment set is not populated, so every row that reads the verdict's own text is
silent. My sequence took that blind run as the landing reading and the authoritative run second.
The landing reading of gate ② has to be taken after the carriers come off. Nothing about the
outcome here was harmed — the PR did not become mergeable during the window and the verdict was
always PASS — but the ordering was wrong and the next such window might not be harmless.

The three gates, on runs that could read the thread

verdict comment 5673964271
declares e85d768553685430e1744e8d842c83d0b57581bd; live pulls/9548.head.sha at 03:06Z is the
same. Compared as strings, both length 40 — equal. Verdict token PASS present, FAIL absent.

--pair 9548 → exit 0, and this time with the C6-RECORD line naming comment
5673964271 and this head. ⚠️ Before the verdict's authorship pair was repaired the same command
exited 4 on row C4: "Both lines or neither — half a pair compares to nothing, so there is
no reading of independence here."
The repair was made by the reviewer in its own comment, ⛔ not by
this seat editing an independent verdict; it now parses to
Implemented-by: claude/issue-8655-object-tree-props-schema-type (the dev's branch, a
mode:subagent dev having no session) and Reviewed-by: session_01L5xpA5q533BgTTNADibEFt (the seat
that renders the verdict writes its own session).

My dispatch caused that too. I wrote the review order from memory instead of copying the
fixed spelling from references/contract-review.md, which carries it in two lines. That is the rule
this seat enforces on every dispatch order it writes — copy the template, ⛔ never compose from
memory — and I broke it on three review dispatches in one round. The other two were corrected in
flight before they posted.

Before the ready flip: total_count 36 == 36 returned, {success: 33, skipped: 3}, 0
not-green, 0 running. After it, the 37th check (Governed Surface Queue Guard, 03:06:24Z
03:06:39Z, 15 s) appeared and the census was re-taken: total_count 37 == 37,
{success: 34, skipped: 3}, 0 not-green, 0 running, legacy combined status success,
mergeable_state clean, draft false.

Landing

  • POST …/ccr/ready_for_review{"draft":false}, read back on GET /pulls/9548 and on the
    timeline as ready_for_review at 03:06:16Z.
  • PUT …/ccr/auto_merge {"merge_method":"SQUASH"}{"enabled":true,"merge_method":"squash"}.
  • Verified on the timeline: added_to_merge_queue at 03:07:05Z. (auto_merge reads back
    null once queued — consumed by the enqueue, so its absence is not evidence of failure.)

What rides forward, ⛔ not discharged by this merge


Generated by Claude Code

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants