Skip to content

fix(plugin-kanban): ObjectKanbanComponentProps.schema names both registered node types (objectui#7322 item ②) - #8153

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-7322-object-kanban-component-props
Sep 6, 2026
Merged

fix(plugin-kanban): ObjectKanbanComponentProps.schema names both registered node types (objectui#7322 item ②)#8153
os-justin merged 1 commit into
mainfrom
claude/issue-7322-object-kanban-component-props

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #7322 (item ②; item ① landed as PR #7774 on 2026-09-05)

ObjectKanbanRenderer is registered under two keys — 'object-kanban' (index.tsx:416)
and 'kanban' (:429), and kanban-plugin-dialect-authoritative-7664.test.ts already pins
that both resolve to the same renderer. The two keys have different declared node types:

key declared type type literal objectName / groupBy
'object-kanban' ObjectKanbanSchema (objectql.ts) 'object-kanban' required / required (after item ①)
'kanban' KanbanSchema (complex.ts, moved down by #7743) 'kanban' optional / optional

ObjectKanbanComponentProps.schema named KanbanSchema alone. The discriminants are
disjoint string literals, so no object-kanban node was assignable to the component that
renders it
, and the prop lied about half the nodes it serves.

The measurement that settled union-vs-single-type

The dispatch said measure first, choose second, and to stop and report if no single type can
serve both registrations. It can't — and the reason is not just the discriminant.
ObjectKanban reads thirteen keys off schema. Neither declaration covers them; the two
together cover twelve:

key(s) BaseSchema KanbanSchema ObjectKanbanSchema
objectName, groupBy, limit, cardFields yes yes
columns, cardTitle, swimlaneField, grouping yes
titleField yes
data, bind, className yes (data, className)
filter

Each arm is load-bearing, in both directions:

  • naming ObjectKanbanSchema alone (the original card's implied remedy) drops four
    declared reads and the 'kanban' registration — it is wrong in the other direction;
  • naming KanbanSchema alone (the status quo) drops titleField — which is exactly why
    ObjectKanban.tsx spelled that read (schema as any).titleField.

⇒ the honest type is the union, KanbanSchema | ObjectKanbanSchema. It claims exactly the
accept set the registry dispatches to this component and no more: an unregistered node type is
still turned away (pinned). This is the same land shape #7311 already took for the calendar
(ObjectCalendarComponentProps.schema: ObjectGridSchema | CalendarSchema).

filter (:310, the real $filter on the fetch) is declared by neither face and still
rides BaseSchema's index signature. Measured and reported, not changed here — this card
moves the prop, not the two published schema faces. See "Findings" below.

What the change costs the tree — the visible payoff

Five of the six in-package fixtures that mount an object-kanban board were escaping the prop
with as never. Four of them now carry a real satisfies ObjectKanbanSchema:

  • ObjectKanban.rejectedMoveRollback.test.tsx
  • ObjectKanban.requiredWhenPrompt.test.tsx
  • ObjectKanban.markedRefusalToast.test.tsx
  • ObjectKanban.navWidthDefault.test.tsx

The other two (overlayTitleI18n, overlayTitleNoProviderFallback) are static boards —
columns plus inline data, no fetch — so they author no objectName, which
ObjectKanbanSchema declares required. That requiredness is #7780's subject; their casts
stay, now carrying the reason and the card number instead of being silent.

Three as any casts drop out of ObjectKanban.tsx: titleField at :350 / :1024 (honest
now that the object-kanban arm declares it) and cardFields / cardTitle (already declared;
the casts were redundant). Measured with eslint on the same file before and after: 28 → 24
@typescript-eslint/no-explicit-any findings, i.e. exactly the four any tokens removed.

(schema as any).navigation at :789 stays: navigation is declared on neither face, so
removing the cast would change the spelling of an index-signature read and nothing else — the
#5903 disposition, restated.

index.tsx:395's schema: any — the "if and only if" answered NO

The dispatch authorised cleaning it up iff the measurement makes an honest type available
there. It does not, on two readings: ComponentRegistry.register's component parameter is
untyped, so an annotation there is a claim about registry dispatch that nothing enforces; and
registration.test.tsx:32 renders a deliberately minimal { type: 'object-kanban' }, which no
declared type accepts while #7780 is open. #5903 took the same disposition for the same reason
("the registered renderer still passes schema: any, so no runtime shape is turned away").

The pin, and the pin it amends

Newpackages/plugin-kanban/src/__tests__/object-kanban-component-props-7322.test.ts.
Suite 1 (compile-time, read by tsc -p tsconfig.test.json): both arms accepted, an
unregistered node type (ObjectGridSchema) refused, the discriminant equal to
'kanban' | 'object-kanban', with IsAny controls so a widening to any cannot pass. Suite 2
(runtime): the registered key set is extracted from index.tsx off disk and required to
equal the union's arms — a hand-written pair of strings is exactly what this defect survived
under, so a third registration or a re-key goes red naming the key, and the fix is to move the
prop rather than the test. Anti-vacuity: the extraction must be non-empty, must not pick up
'kanban-ui' / 'kanban-enhanced' (registered in the same file to other renderers), and every
key it finds must resolve to ObjectKanbanRenderer in the live registry.

Amendedkanban-plugin-dialect-authoritative-7664.test.ts, leg 3's first bullet, from
an Equal identity assertion between the prop's schema member and KanbanSchema, to
assignability. Identity was never what objectui#7664's ruling claimed; its words are that
"the four registered renderers' props still type-check against the declared schema" — the
same assignability form leg 3's own kanban-ui bullet already uses, and for the same reason.
Identity was merely the shape the prop happened to have while it named one arm, which was
itself this card's defect.

Verification

All on 25907cd70; every exit code captured before any pipe; build/test through the shared
verify lock.

command exit verdict
pnpm --filter @object-ui/plugin-kanban type-check (tsc --noEmit + tsconfig.test.json) 0 no error TS line
pnpm exec vitest run --maxWorkers=2 packages/plugin-kanban/ 0 Test Files 25 passed (25) · Tests 130 passed (130)
turbo run type-check --filter=@object-ui/runner --filter=@object-ui/app-shell 0 Tasks: 31 successful, 31 total (the two package-level consumers)
pnpm --filter @object-ui/plugin-kanban lint 0 ✖ 121 problems (0 errors, 121 warnings); 0 new on touched files
pnpm --filter @object-ui/plugin-kanban build 0 Declaration files built in 4378ms
node scripts/check-changeset-presence.mjs 0 ✅ 8 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-control-bytes.mjs 0 ✅ OK (scanned 6511 tracked text file(s))
pnpm check:spec-symbols 0 ✅ no comment cites a key its spec symbol does not declare
pnpm check:element-data-source-declaration 0 OK — 13 gate-consuming file(s) checked
pnpm check:unreferenced-sources 0 OK Every shipped source file … is reachable
pnpm check:vi-mock-specifiers / check:vi-mock-inherit / check:self-import / check:phantom-deps 0 all
pnpm check:sdui-registration-pins NOT MEASURED ❌ No console build to weigh at apps/console/dist/assets — a prerequisite complaint, not a red gate. Narrowing declared: this diff adds and removes no registration, which is that gate's whole subject. CI runs it with a real console build.

turbo ls --affected from the merge-base names 8 packages; the two with a real source-level
dependency on this one (runner, app-shell) are type-checked above, the rest are apps and
examples that reach this package only through plugin registration and are CI's.

Reverse verification at the package boundary

A widening is invisible unless the consumer really reads the rebuilt .d.ts. A scratch probe
assigning an ObjectKanbanSchema-typed value into the prop's schema member, importing
through dist/index:

  • against the rebuilt .d.tsexit 0;
  • with that one line in dist/ObjectKanban.d.ts reverted to the single arm (mutation proven by
    md5, restored and re-proven) → exit 2, TS2322: Type 'ObjectKanbanSchema' is not assignable to type 'KanbanSchema'.

So the green is a reading of this change, not of a cache.

Ablation — both halves of the new pin, red-first

Run from the committed state, mutation proven by blob hash on disk, restored under
trap … EXIT INT TERM with git checkout HEAD -- and git diff HEAD empty afterwards. No
dist is on the pin's own resolution path (../ObjectKanban and ../index are source imports;
tsconfig.test.json sets "paths": {} only for the @object-ui/* workspace deps, which this
change does not touch).

Leg A — prop reverted to the single arm (the union member replaced by schema: KanbanSchema;
anchors counted 1→0 and 0→1, blob 48bd449dfba3ea76).
Predicted red on the two new type legs and on the four satisfies fixtures. Observed:
tsc -p tsconfig.test.json exit 2, 6 errorsTS2344 at
object-kanban-component-props-7322.test.ts(77,43) and (89,41) (_AcceptsTheObjectKanbanNode,
_DiscriminantIsTheTwoKeys) plus TS2322 on all four fixtures. One refinement on the
prediction: the fixtures fail at the JSX prop assignment (TS2322), not at the satisfies
clause (TS1360) — the literals still satisfy ObjectKanbanSchema; it is the prop that stops
accepting them, which is the more exact statement of the defect.

Leg B — one registration re-keyed ('kanban''kanban2' in index.tsx; blob
4e354382c01425be). Predicted exactly one red assertion with the anti-vacuity legs
staying green. Observed: vitest exit 1, Tests 1 failed | 3 passed (4),
AssertionError: expected [ 'kanban2', 'object-kanban' ] to deeply equal [ 'kanban', 'object-kanban' ].
⚠️ Leg B's grep -c echo lines were mangled by quoting inside the script's heredoc and printed
no usable count; the mutation's arrival on disk is carried instead by the blob-hash inequality
and by the assertion message itself, which quotes the mutated key back. Two independent
readings, so the leg stands; the broken echo is a script defect, reported rather than papered
over.

Restore proven on both legs: blobs equal HEAD again, git diff HEAD empty.

Findings — reported, not filed, to avoid a fourth card on this family

The dispatch flagged that #7772 / #7773 / #7780 already sit on the object-kanban family and
this card must not duplicate them. Two measurements fall inside cards that already exist:

Not touched

The four view-level groupField alias sites (normalize-list-view.ts, ListView.tsx,
ObjectView.tsx), BaseSchema's index signature (#5155), and packages/types — read-only
here, as dispatched (slot 3/5 is editing complex.ts's ChatbotSchema region concurrently).
Nothing under content/docs/releases/.

Governance

node scripts/check-governed-queue-guard.mjs --test on this diff's paths:
✅ NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched.
Held draft with needs:contract-review per Clause-② (this widens a member of a prop type
exported from a published package). ⛔ Does not enqueue while that label is on it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…stered node types

`ObjectKanbanRenderer` is registered under two keys — `'object-kanban'` and
`'kanban'` — whose declared node types are `ObjectKanbanSchema` and
`KanbanSchema`. The prop named `KanbanSchema` alone, so no `object-kanban` node
was assignable to the component that renders it; the discriminants are disjoint
literals, so half the boards this component serves had no cast-free annotation.

Measured read set: `ObjectKanban` reads thirteen keys off `schema`; the two
declarations together cover twelve, and each arm is load-bearing (`titleField`
comes only from `ObjectKanbanSchema`, `columns` / `cardTitle` / `swimlaneField` /
`grouping` only from `KanbanSchema`). `filter` is declared by neither and still
rides `BaseSchema`'s index signature — reported, not changed here.

The prop is now the union of the two arms. Three schema-key `as any` casts drop
out of `ObjectKanban.tsx`; four of six in-package fixtures drop their `as never`
for `satisfies ObjectKanbanSchema` (the two that remain are static boards with
no `objectName`, objectui#7780). A new pin derives the registered key set from
`index.tsx` off disk and requires it to equal the union's arms, and the
objectui#7664 prop leg moves from identity to assignability — the form that
ruling's own words claim, and the form its `kanban-ui` leg already uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3186.6 KB 3191.4 KB
Main entry chunk (gzip) 143.5 KB 350 KB
Entry file index-CsIQvyOJ.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) 15.67KB 5.75KB
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) 5.13KB 2.35KB
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) 498.09KB 113.98KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 182.08KB 50.62KB
fields (index.js) 242.43KB 61.25KB
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) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
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.84KB 10.94KB
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) 11.71KB 4.29KB
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) 5.12KB 1.74KB
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) 15.16KB 3.68KB
plugin-calendar (index.js) 47.29KB 13.18KB
plugin-charts (index.js) 70.35KB 19.68KB
plugin-chatbot (index.js) 193.54KB 46.04KB
plugin-dashboard (index.js) 131.41KB 34.43KB
plugin-designer (index.js) 211.51KB 43.01KB
plugin-detail (index.js) 247.75KB 63.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 52.46KB 14.57KB
plugin-list (index.js) 113.34KB 27.72KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.48KB 20.81KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
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.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
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.74KB 1.41KB
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 (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

Copy link
Copy Markdown
Collaborator Author

Contract review — PASS — PR #8153 (objectui#7322 item ②), head 25907cd70

Reviewed-by: domain:ui @ objectui PM seat, session_01YBWFb5YgMU5dw8p2VKj16S, in-seat at opus = TIER_DEFAULT. CONTRACT_REVIEW_TIER (claude-fable-5-1) is measured unavailable (429); the exemption permits falling back to default and ⛔ no lower. ⛔ This review is not described as running at contract-review tier.

⭐ The union-vs-single-type question — re-measured by this seat, ⛔ not accepted from the report

This was the dispatch's central clause. I warned that the card's implied remedy ("point the prop at ObjectKanbanSchema") was not obviously correct because one component serves two registrations, and told the dev to measure before writing a type and to stop if no single type could serve both. It measured, and the answer is a union. I re-ran the cross-tabulation on origin/main = 21d7989fb and every cell matches:

key KanbanSchema ObjectKanbanSchema
titleField 0 1 ⇒ naming KanbanSchema alone drops it — which is exactly why it was (schema as any).titleField
columns · cardTitle · swimlaneField · grouping 1 each 0 each ⇒ naming ObjectKanbanSchema alone (the card's remedy) drops four declared reads
objectName · groupBy · limit · cardFields 1 each 1 each the control — item ①'s keys, disposition independently known
filter · navigation 0 0 on neither face — the out-of-scope finding

the union is provably the only honest type, and the card was wrong in both directions. The control row is what makes the other rows readings: a tabulation that answered "declared" for everything would show it here, and it does not — bind / className come back BaseSchema-only on the same instrument. ✅ The stop-and-report clause was correctly not triggered; each arm is load-bearing.

① Derived judgments

Direction: WIDENS ObjectKanbanComponentProps.schema. Additive for callers (more values accepted); for anyone reading props.schema it is a union, which is the honest description of what the component is actually handed.

⚠️ One caveat I record rather than gloss: the three as any casts drop out, but on the arm that does not declare a key the read still resolves through BaseSchema's [key: string]: any. So schema.titleField compiles on the KanbanSchema arm as any, and the union of that with string | undefined is any. ⇒ the honesty gain here is at the declaration and at the cast count, ⛔ not yet at the type of the read. That residue is objectui#5155's index-signature blind spot, which the dispatch fenced off and which this PR correctly does not touch. Saying so because "the casts are gone" could otherwise be read as "the reads are now typed", and they are not.

⭐⭐ The anti-vacuity pin is the best thing in this diff. _RejectsAnUnregisteredNode asserts that ObjectGridSchema — a sibling node type declared in the same file as ObjectKanbanSchema, registered to a different renderer — is not assignable to the union. That is the one leg that would go green if anyone later widened the prop to any or to a structural face. ⇒ a widening pinned only by "these two are accepted" is satisfied by any; this one is not. Correct instinct, correctly placed.

Reverse verification at the package boundary — a widening is invisible unless the consumer reads the rebuilt .d.ts. Probe assigns an ObjectKanbanSchema value through dist/index → exit 0; the same line in dist/ObjectKanban.d.ts reverted to the single arm (md5 7874c417…3f8384ec…, restored) → exit 2, TS2322. ✅ The green reads this change, not a cache.

② semver

@object-ui/plugin-kanban: minor, following objectui#5903's precedent bump for the same manoeuvre. ✅ major forbidden; Changeset Bump Policy green.

③ Boundary flags

  • Ablation leg B's on-disk anchor echo was VOID (heredoc quoting swallowed the count) and the dev said so rather than papering over it — the leg stands on two independent readings instead (blob-hash inequality, plus the assertion message quoting the mutated key kanban2 back). And it was ⛔ not re-run, with the reason stated: 「re-running until something lands is the defect one level up」. ⭐ That is the correct handling of a partially-void measurement, and the sentence is worth keeping.
  • Fixture triage done per fixture, ⛔ not as a batch. Four are fully assignable and move from as never to satisfies ObjectKanbanSchema (the strongest honest form — checks assignability without erasing the literal). Two keep their casts because they are static boards authoring no objectName, which ObjectKanbanSchema declares required — measured, not assumed (removing the casts produced TS2322 on the prop and TS1360 on the satisfies clause, naming objectName), and that requiredness is objectui#7780's open subject. The casts now carry the reason and the card number. ✅ Exactly right: silently rewriting those two would have fossilised a disputed requiredness into six fixtures.
  • The one pin amended, with its reason: objectui#7664's prop leg moves from an identity assertion (Equal against KanbanSchema) to assignability — the form that ruling's own words claim, and the form its own kanban-ui leg already used. ⇒ amending a pin whose assertion the change falsifies, in the direction the ruling actually states, ⛔ not deleting an inconvenient one.
  • Lint delta measured, not asserted: base and head ObjectKanban.tsx linted side by side in one invocation — 28 → 24 no-explicit-any findings, i.e. exactly the four any tokens the three removed casts contained. ⭐ A count that matches the diff arithmetic is a reading; "lint is green" would not have been.
  • check:sdui-registration-pins NOT MEASURED (no apps/console build) with a declared narrowing — the gate's whole subject is registrations and this diff adds and removes none. ✅ Correctly refusing to call a prerequisite failure a pass; CI measures it.
  • Out-of-scope findings reported as a COMMENT on objectui#7742 rather than as a fifth card, because the dispatch warned four cards already sit on this family and both keys (navigation at :789, filter at :310) fall inside finding(types,plugin-kanban): the ruled KanbanSchema carries three zero-read members (allowCollapse, cardTemplates, columnWidths) and the board reads an undeclared titleField — enforce-or-remove on the shape objectui#7664 declared #7742's stated scope. ⇒ declining to file is as much a discipline as filing. It also left a PREMISE NOTICE: this PR moves finding(types,plugin-kanban): the ruled KanbanSchema carries three zero-read members (allowCollapse, cardTemplates, columnWidths) and the board reads an undeclared titleField — enforce-or-remove on the shape objectui#7664 declared #7742's own premise (its titleField half now reads differently), so its taker must re-measure from 25907cd70. ⭐ Warning the next card that you moved its ground is rare and valuable.
  • ⭐ The report corrected its own mcp_calls count (posted 9, actual 11 — the two read-backs necessarily post-dated the comment) and explained why it did not edit the comment in place. Unprompted, on a number nobody would have checked.
  • open_questions: none. packages/types untouched; the four view-level groupField alias sites untouched; BaseSchema untouched. ✅ All three fences held.

Verdict

PASS. needs:contract-review is attached and read back on the PR ([plugin, plugin: kanban, tests, needs:contract-review], union exact, nothing stripped by the auto-labeler) — cleared in this stroke, with this comment as its provenance.

Not landing yet. CI unconverged: 32 enumerated against total_count: 32, 6 still in_progress (Test shards 1–4, Type Check, Live E2E), zero failures. ⭐ Lint read explicitly and green (completed_at 21:01:07Z). Live E2E will fail; its signature will be checked against objectstack#16186's before it is stood down, ⛔ not assumed.

This PR carries the closing keyword — item ① landed as PR #7774 and item ② is the last item — so objectui#7322 closes on merge and this seat strips pm:dispatched and clears the assignee in the same stroke.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Addendum to the review above — needs:contract-review is now cleared on the PR (labels read back: [plugin, plugin: kanban, tests]), as that comment said it would be in the same stroke.

Two things in the PR body that the os-dev-report did not carry, and that the review should record because both are conditional clauses of the dispatch being answered rather than skipped:

⭐ The index.tsx:395 schema: any clause was answered NO, on measurement. My dispatch authorised cleaning it up 「if and only if your measurement makes an honest type available there」. The dev's answer is that it does not, on two readings: ComponentRegistry.register's component parameter is untyped, so an annotation there would be a claim about registry dispatch that nothing enforces; and registration.test.tsx:32 renders a deliberately minimal { type: 'object-kanban' } that no declared type accepts while objectui#7780 is open. objectui#5903 took the same disposition for the same reason. ⇒ an "if and only if" resolved to no, with the reason, is the clause working — ⛔ not a step skipped. A dev that had cleaned it up to look thorough would have made a claim the registry does not enforce and broken a live test.

(schema as any).navigation at :789 also stays, and correctly: navigation is declared on neither face (I verified: 0 and 0), so removing that cast would change the spelling of an index-signature read and nothing else. That is objectui#5903's disposition restated. ⇒ it also narrows my own caveat above — the residual any I flagged is now confined to the keys no face declares, which is objectui#7742's ledger, ⛔ not this card's.

⭐ And the union shape has a house precedent I had not named in the dispatch: objectui#7311 already landed ObjectCalendarComponentProps.schema: ObjectGridSchema | CalendarSchema — the same manoeuvre on the calendar, the sibling card triage twice pointed at. ⇒ this is the family's established land shape, ⛔ not an invention.

Nothing in either changes the verdict: PASS, landing on convergence.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Landing — PR #8153 (objectui#7322 item ②), head 25907cd70, 2026-09-06T21:1xZ.

Pre-landing three:

① Contract review PASS (comment 5562152707) at opus, addendum 5562155867; needs:contract-review cleared with that as provenance. Head unchanged since the review.

CI converged, enumerated 32 against total_count: 3228 success · 3 skipped · 1 failure, ⛔ zero in_progress. ⭐ Lint read explicitly and green (completed_at 21:01:07Z).

The single failure is Live E2E (informational), and its signature was read and matched line for line (job 101557334744), ⛔ not assumed:

[live-backend] not ready after 300s — last 100 log lines:
  ⚠ AuthPlugin failed to load: The requested module '@better-auth/core/db'
    does not provide an export named 'createLocalAccountIssuer'
  WARN CORE: Core service missing, functionality may be degraded: auth
  … no such table: sys_organization / sys_position / sys_permission_set / sys_user
  ✓ Server is ready          ← 42 plugins, 132 seeded rows, 31 boot warnings
##[error]Process completed with exit code 1.

Identical to #8142's, #8145's and #8150's. Carded objectstack#16186; the red is by design per objectui#7689, whose triage ⛔ forbids reverting the pin to get green. ⛔ No re-run spent.

⚠️ And a mechanism argument alongside the signature match, ⛔ not instead of it: the step boots a published @objectstack/* backend and dies at readiness before any objectui code is exercised, so a prop-type change in plugin-kanban cannot reach it.

Ready flipped, auto-merge SQUASH enabled. ⚠️ The API echoes an empty method even when it applied — verify by queue ref and parent-count, ⛔ not by that echo. #8150 is ahead in the queue; this one stacks behind it.

⭐ Landing control established BEFORE the queue takes it, so the post-merge confirmation is a reading and ⛔ not a hopeful grep:

read branch origin/main
KanbanSchema | ObjectKanbanSchema in packages/plugin-kanban/src/ObjectKanban.tsx 1 0
ObjectKanban.tsx line count on main (instrument live?) 1064

⇒ the zero is a reading on a live file.

Fixes #7322 — item ① landed as PR #7774 and item ② is the last item — so the card closes on merge and this seat strips pm:dispatched and clears the assignee in the same stroke.


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

1 participant