Skip to content

fix(plugin-kanban): read onCardMove from a React prop so the object-kanban arm can tombstone it - #9443

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-9342-kanban-oncardmove-prop
Sep 14, 2026
Merged

fix(plugin-kanban): read onCardMove from a React prop so the object-kanban arm can tombstone it#9443
os-tesla merged 1 commit into
mainfrom
claude/issue-9342-kanban-oncardmove-prop

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #9342

Executes the domain:ui seat's option B ruling recorded on PR objectui#9338.
KanbanRenderer reads onCardMove from an explicit React prop instead of off
schema; ObjectKanban passes handleCardMove through that prop; the
object-kanban arm tombstones the key with handlerKeyRefusal(..., 'retired', ...).

Parent objectui#7804 stays open by its own instruction, and is not addressed
here
beyond this one row. Options A, C and D are recorded as out on the card
and are not re-proposed.

The standoff this breaks

Two things were both true and neither could be overruled:

  • MEASURED (objectui#7804): an authored onCardMove on an object-kanban
    document reaches nothing. ObjectKanban substitutes its own
    handleCardMove on the schema it hands down and declares no onCardMove
    React prop (its rest parameter is void _props), so both channels are dead.
    That is the objectui#6124 'retired' disposition.
  • GATE: check:handler-key-reads refuses 'retired' while a renderer still
    reads the key, because a tombstone "has no read site BY CONSTRUCTION".

What the gate cannot see is that the value at that read was substituted one hop
earlier. Moving the read is what makes both sides true at once.

What changed

File Change
packages/plugin-kanban/src/index.tsx onCardMove leaves the KanbanRendererProps.schema bag and becomes an explicit React prop, a sibling of schema; the component reads the prop
packages/plugin-kanban/src/ObjectKanban.tsx onCardMove={handleCardMove} as a JSX prop instead of a key on the schema literal
packages/types/src/objectql.ts ObjectKanbanSchema.onCardMove?: never
packages/types/src/zod/objectql.zod.ts onCardMove: handlerKeyRefusal('onCardMove', 'retired', 'Card move handler')
scripts/check-handler-key-read-sites.mjs the object-kanban::ObjectKanbanSchema.onCardMove ledger row drains
docs README-KANBAN.md, packages/plugin-kanban/README.md, content/docs/api/schema-reference.md, content/docs/plugins/plugin-kanban.mdx

⚠️ Correction to the dispatched file surface, declared rather than absorbed.
The read was named in KanbanImpl.tsx; on disk it is in
packages/plugin-kanban/src/index.tsx (KanbanRendererProps at :175,
onCardMove={schema.onCardMove} in the KanbanRenderer body). KanbanImpl.tsx
is the lazy board chunk and has taken onCardMove as an ordinary React prop all
along, so it needed no change. Followed the disk, per instruction.

Acceptance leg 1 — the gate goes green on that spelling

Its own printed line, on this branch:

OK  105 arm(s), 212 registration(s) (119 with an arm), 60 reachable handler read(s), 60 judged, 0 left unjudged on an arm with an unresolved spread, 38 exempted by ledger — every judged read is a declared member of its arm.

Before, same command, same tree minus this change:

OK  105 arm(s), 212 registration(s) (119 with an arm), 61 reachable handler read(s), 61 judged, 0 left unjudged on an arm with an unresolved spread, 39 exempted by ledger — every judged read is a declared member of its arm.

⚠️ The observed ledger count is 39 to 38, not the 36 the card predicts, and
the number reported here is the one measured.
The card's arithmetic
(39 to 37 to 36) is correct for the moment it was written and was overtaken:
objectui#7804's plugin-kanban slice did drain two rows (39 to 37), but
objectui#9344 then added two — tabs::TabsSchema.onTabChange and
detail::DetailSchema.onTabChange, the pair a cast receiver had hidden from the
census — putting the ledger back at 39 before this branch started. The
object-kanban::ObjectKanbanSchema.onCardMove row is drained: no
object-kanban:: row remains, asserted as a set in
handlerKeyDispositionsMeasured-7804.test.tsx suite 4, and reachable handler
reads fall 61 to 60 in the same run.

Acceptance leg 2 — the ablation

Prediction, written before the run: restoring the schema.onCardMove read
while leaving the arm tombstoned must turn check:handler-key-reads red with
a retired-but-read finding printing declares it RETIRED, but a renderer still reads it, and must redden the source-derived pin
kanban-handler-slots-7664.test.tsx — whose forwardedByKanbanRenderer() reads
the schema.on* accesses out of the KanbanRenderer body — plus the new
prop-versus-schema-bag leg beside it.

Outcome and the on-disk mutation proof are in the run block below.

Outcome: as predicted, both halves. Mutation onCardMove={onCardMove} back to
onCardMove={schema.onCardMove}, one line, arm left tombstoned.

On-disk proof of the mutation (a zero-hit editor still exits 0, so the edit is
observed, not inferred):

prop read  'onCardMove={onCardMove}'         count before: 1  after: 0
schema read 'onCardMove={schema.onCardMove}' count before: 0  after: 1
HEAD blob    4596106d3a4baac836715c5d5c76f0826c748d80
mutated blob 4bce990b53194eaebe96f152522d79c40a3d89ce   (differs, so the write landed)

Leg A — check:handler-key-reads, exit 1, its own printed line:

x  1 handler key(s) a registered renderer reads are not declared by their arm:

      'object-kanban'.onCardMove  read at packages/plugin-kanban/src/index.tsx:300
          registered in packages/plugin-kanban/src/index.tsx; ObjectKanbanSchema (objectql.zod.ts) declares it RETIRED, but a renderer still reads it.

Leg B — kanban-handler-slots-7664.test.tsx, exit 1: Tests 4 failed | 3 passed (7).
The four that redden are the source-derived ones, and the three that stay green
are what keep this from being a file that broke wholesale:

× ⭐ onCardMove travels the PROP and NOT the schema bag (objectui#9342)
× `'object-kanban'` (ObjectKanban) passes onQuickAdd through and supplies its OWN onCardClick and onCardMove
× the read site is measured, not listed: KanbanRenderer reads exactly these two off the document
× ⭐ two of the three are declared on the surviving `object-kanban` face — measured per key, not per prefix
    expected [ 'onCardClick', 'onCardMove', ...(1) ] to deeply equal [ 'onCardClick', 'onQuickAdd' ]

Restore proven by state, not by re-running the test — git checkout HEAD --
against an absolute path, then:

HEAD blob: 4596106d3a4baac836715c5d5c76f0826c748d80
disk blob: 4596106d3a4baac836715c5d5c76f0826c748d80   MATCH
git diff HEAD   -> empty
git status --porcelain -> empty

and the gate is green again on the restored tree, which is the lit control for
the red above.

Acceptance leg 3 — the doc repair

README-KANBAN.md taught an object-kanban document carrying
"onCardMove": "(event) => ..." — a function spelled as a string, accepted
and silently dropped
on the day it was written. It now says so, teaches the
React prop, and its prop table spells the key never.

Clause-② carriers

needs:contract-review is attached to both carriers in the same write as the
contract increment: the card and this PR. This narrows a published React props
surface (KanbanRendererProps.schema.onCardMove goes) and puts a new key on
one (onCardMove becomes an explicit React prop).

Changeset

minor with a **BREAKING** carrier, verified against the repo's own rule
rather than taken on faith: AGENTS.md forbids a major in any changeset because
the fixed group ships as one family whose major tracks @objectstack, and
scripts/check-changeset-no-major.mjs enforces it (green here).

⚠️ Two counts in the dispatch and in AGENTS.md disagree with the config: the
fixed group holds 40 packages today. The card says 41 and AGENTS.md says
39. The conclusion is unaffected — major is forbidden outright, not scored by
group size — so this is recorded, not filed.

Acceptance notes — observed, not filed

  • AGENTS.md's fixed-group figure (39) and the card's (41) are both stale
    against the config's 40. AGENTS.md is a governed surface and the drift is an
    instance of its own rule 完善设计器的每一个细节 #9; no PR or person is otherwise routed through that
    line, so there is no carrier — successor: none.
  • handler-keys-json-refusal-6124.test.ts's RETIRED docblock read "22 keys"
    while its own length assertion read 20, across objectui#8802's arm retirement.
    Repaired in place, because this change moves that very count.

Verification

All run on 43a6724, from the repository root, through this container's shared
heavy-verify lock.

Command Result
pnpm exec vitest run packages/types/ packages/plugin-kanban/ Test Files 240 passed (240) · Tests 4609 passed (4609)
pnpm exec turbo run type-check --filter=@object-ui/types --filter=@object-ui/plugin-kanban --filter=@object-ui/components --concurrency=2 Tasks: 16 successful, 16 total
pnpm lint (eslint . --no-inline-config, whole farm) Tasks: 47 successful, 47 total, 0 errors
pnpm check:handler-key-reads green, line quoted above
node scripts/check-changeset-presence.mjs 10 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)
node scripts/check-changeset-no-major.mjs No changeset declares a major bump.
node scripts/check-governed-queue-guard.mjs --test NOT GOVERNED — 16 path(s) checked against 5 governed surface(s); none matched.
pnpm check:control-bytes OK (scanned 7603 tracked text file(s))
pnpm check:readme-exports green on a fully built tree (0 unbuilt)
pnpm check:doc-snippets · check:doc-types · check:doc-examples · check:doc-example-ids · check:doc-fences · check:doc-example-readers all green
pnpm check:action-forward-parity · check:dist-completeness · check:comment-mask-corpus all green
pnpm check:changeset-claims report-only, no finding against this diff

⚠️ check:readme-exports and check:doc-snippets both refused to run on an
unbuilt tree first (exit 2 / "run pnpm build first"). That is a prerequisite
not met
, not a red gate, and it is recorded here because the two readings look
identical if you only read an exit code: both were re-run green after
turbo run build (Tasks: 44 successful, 44 total).

Everything else is CI's — the merge queue runs the whole farm.

Session, as prose so it survives an edit of this body:
https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa


Generated by Claude Code

…tombstone it

`KanbanRenderer` took `onCardMove` off the `schema` bag it was handed. On an
`object-kanban` document that read was already dead: `ObjectKanban` substitutes
its own `handleCardMove` one hop earlier and declares no `onCardMove` React
prop, so an authored value was accepted by `BaseSchema`'s passthrough, kept, and
then dropped.

The disposition was measured `'retired'` but could not be spelled:
`check:handler-key-reads` refuses a tombstone while a renderer still reads the
key off the document, because a tombstone has no read site by construction.

Move the READ. `KanbanRendererProps` declares `onCardMove` as an explicit React
prop, a sibling of `schema`; `ObjectKanban` passes `handleCardMove` through it;
`ObjectKanbanSchema` carries `?: never` on the TypeScript face and
`handlerKeyRefusal('onCardMove', 'retired', ...)` on the zod mirror. The key's
`KNOWN_UNDECLARED_READS` row drains.

BREAKING: narrows a published props surface. A host rendering `KanbanRenderer`
directly and writing the handler inside `schema` must move it to the prop.

Docs repaired here, because this change is what makes them wrong:
`README-KANBAN.md` taught an `object-kanban` document carrying
`"onCardMove": "(event) => ..."`, accepted and silently dropped until now.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3061.0 KB 3104.5 KB
Main entry chunk (gzip) 145.6 KB 350 KB
Entry file index-BtK6YH4f.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.84KB 130.47KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 213.54KB 59.33KB
fields (index.js) 247.89KB 62.50KB
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.92KB 14.22KB
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.27KB 65.55KB
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.55KB 57.83KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.68KB 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.36KB 20.78KB
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

Copy link
Copy Markdown
Collaborator Author

Bundle Analysis — the gate and the reason, recorded on the PR as the gate itself asks

Posted by the domain:ui PM seat. This is the record the enqueue rule requires on the pull request (⛔ not on the card) before a PR is queued carrying this red.

The gate: Bundle Analysis (.github/workflows/performance-budget.yml). It is a blocking check when it runs — scripts/dependabot-merge-gate.mjs classifies it under OPTIONAL_CONTEXTS, where "optional" is about the trigger-level path filter, ⛔ not about the check being advisory: present ⇒ must be success; absent ⇒ not waited for.

The reason it is red here, quoted from the gate's own output rather than paraphrased:

❌ chunk `ui-components`  387.9 KB measured / 389.6 KB ceiling
   (headroom 1.7 KB = 0.02x the 89.0 KB regression, under the 0.10x floor
   and held open by its declared 4289-byte allowance)
chunk `ui-components` was ALREADY declared exhausted before this run, and has
now lost a further 0.01x of a regression against the pinned figure.

and the gate's own instruction, verbatim:

⚠️ READ THIS BEFORE AUDITING YOUR OWN DIFF. This row's headroom is a standing debt that predates this change, and it moves under traffic that has nothing to do with the chunk — measured at three gzipped bytes across five unrelated merges. So this verdict is NOT an accusation that your diff spent the bytes…

⛔ There is therefore nothing here for this pull request to "fix", and the two edits that would turn this green are both forbidden: ⛔ never raise the ceiling, and ⛔ never raise the allowance. Paying the row down is the open decision on the chunk, ⛔ not a task for whichever change the queue happened to weigh — take it there, and say on this pull request that you did.

⇒ Taken there: the debt is tracked at objectui#9251 / objectui#9204. ⛔ No duplicate filed, and ⛔ nothing in this PR attempts to turn the row green.

The three conditions for carrying it red into the queue, each verified structurally this round:

  1. Red by design on a pushed branch ✓ — the gate says so in its own output above: nothing here for this PR to fix, and both green-making edits forbidden.
  2. Does not run on merge_group ✓ — the workflow's on: carries only push and pull_request; the sole merge_group mentions in the file are in a comment explaining it cannot hold the queue.
  3. This comment.

⚠️ Correction of this seat's own earlier record, so the bad sentence does not propagate: this seat previously wrote that the gate is "red on origin/main's own tip". That is withdrawnmain's tip often carries no Bundle Analysis run at all, because the push: trigger is path-filtered, and a bare zero there reads as a pass while meaning nothing of the kind. The durable statement is red on 7 of the last 7 main commits that actually ran it. The entry-chunk half of the gate is green throughout (144.5 KB against a 350 KB line); only the ui-components closure row is red.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Head reviewed: 43a672461d47ab39102e08946b8dd81d28cdbe7d
Seat: domain:ui (os-tesla) · default judgment tier — the contract-review tier and its downgrade fuse bind only the spec and skills seats, so this lane's clause-② review is default-tier in-seat review plus the gates.

Implemented-by: claude/issue-9342-kanban-oncardmove-prop
Reviewed-by: session_011QreXiyMEqKLN4U5daMPVa

① Derived judgments — every accept-set and public-surface change in the diff, named and judged

  1. KanbanRendererProps.schema.onCardMove REMOVED — narrows a published props surface. ✅ Correct. It is the ruling's substance, not a side effect. A direct host that wrote the handler inside schema breaks: TS error if typed, silent drop if not. Declared in the changeset with the migration spelled (schema={board} onCardMove={handler}).
  2. KanbanRendererProps.onCardMove ADDED as an optional top-level prop — widens the props surface, additively. ✅ Optional, so no existing host breaks on it.
  3. ObjectKanbanSchema.onCardMove?: never (TypeScript face) — narrows the document accept set. ✅ Correct, and this is the point: BaseSchema is .passthrough(), so the key was previously accepted and silently dropped. A key that reached nothing now fails loudly instead. Louder-than-before is the intended direction.
  4. zod mirror: handlerKeyRefusal('onCardMove', 'retired', …) — the same narrowing on the runtime face. ✅ Parity with the TS face is not asserted by prose: zod-mirror-parity.test.ts moves in the same diff. The 'retired' reading (not 'runtime-slot') is right — a slot keeps the member callable, which would publish a key the object-bound board drops, and this package's own quickAdd carve-out forbids that resolution in as many words.
  5. scripts/check-handler-key-read-sites.mjs — the gate that judges this change is itself in the diff. Scrutinised hardest, because a change that edits its own gate is the textbook way to fake a green. ✅ It is a TIGHTENING, not a weakening. The gate's logic is untouched; what changes is one data row drained from KNOWN_UNDECLARED_READS — the exemption that let this key pass as an accepted undeclared read. Removing an exemption can only make the gate stricter. And leaving it would have failed: the script's own staleExemptions() reddens a row that outlived its read. Draining it was mandatory, not discretionary.

⇒ No widening of any published accept set survives review. Every surface change is either the ruled narrowing or an additive optional prop.

② Semver

minor on @object-ui/types, @object-ui/plugin-kanban, @object-ui/components, carrying an explicit BREAKING header. ✅ Consistent with the declared change.

major is unavailable by convention, not preference: the fixed group ships as one family and scripts/check-changeset-no-major.mjs rejects a major outright (green on this head). The dev verified that against the script rather than taking the card's word — the card's "41 packages" is actually 40 in .changeset/config.json, and the conclusion is unaffected because the rule is a flat refusal, not a function of group size.

⚠️ Observed, not blocking: @object-ui/components is bumped for a change that touches only README-KANBAN.md under its src/. That is the repo's own presence gate doing its job (the file is inside a published package's source root), so a version ships for a doc repair. Correct per the gates as they stand; noted so it is not mistaken for over-declaration.

③ Boundary flags

The dev reported open_questions: [] — and, having read the diff, I agree none is open. Its five out_of_scope_findings are each answered here rather than left to lapse:

  • File surface — the dispatch named KanbanImpl.tsx as the read site; the read is at packages/plugin-kanban/src/index.tsx:271. ⭐ The dev is right and the error was mine. Verified independently at source: index.tsx:271 carried onCardMove={schema.onCardMove}; KanbanImpl.tsx has taken it as an ordinary React prop all along and needed no change. The dev followed the disk over my transcription, which is what the dispatch instructed. This is my third file-surface transcription error this shift — the pattern is now recorded at objectui#9445's neighbourhood and is mine to fix, not the devs'.
  • Ledger arithmetic — the card predicted 39 → 37 → 36; the observed drain is 39 → 38. ✅ Accepted as measured. The card's figure was correct when written and was overtaken: objectui#7804's slice drained two, then objectui#9344 added two back (tabs::TabsSchema.onTabChange, detail::DetailSchema.onTabChange, the pair a cast receiver had hidden). The acceptance condition was "report the number you actually observe, not the expected one", and the load-bearing half holds: zero object-kanban:: rows remain, asserted as a set rather than a count.
  • Fixed-group count (40 measured vs 41 in the card vs 39 in AGENTS.md) — ⛔ not this PR's to repair; AGENTS.md is a governed surface and nothing routes through the figure.
  • handler-keys-json-refusal-6124.test.ts docblock said 22 while its own assertion said 20 — repaired in place, correctly: this change moves that very count, so the docblock now points at the assertion instead of restating it.
  • PR objectui#8941 overlap on packages/types/src/objectql.ts — no conflict hit; the branch is cut from current main. Whoever lands second keeps both hunks.

Acceptance legs, both verified rather than accepted on report:

  • check:handler-key-reads green, quoting the gate's own printed line; the object-kanban::ObjectKanbanSchema.onCardMove row is drained.
  • Ablation: direction predicted in writing before the run (gate RED with declares it RETIRED, but a renderer still reads it, plus the source-derived pin reddening); outcome matched exactly — gate exit 1 with that literal string, pin 4 failed / 3 passed where the 3 are the lit control that the file did not break wholesale. Mutation proved on disk by blob hash (4596106d4bce990b), restore proved by state (git diff HEAD empty, blob back to 4596106d).

Verdict

PASS.

Landing follows: both carriers stripped in one stroke with a provenance note, then ready → queue.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Clause-② carriers stripped — PASS. In-seat review record: comment 5657172517, judged at head 43a672461d47ab39102e08946b8dd81d28cdbe7d (the current head; no push since). needs:contract-review removed from both carriers in this one stroke — card objectui#9342 and this PR — per 清标即落地.

Mechanical limb: PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 9443exit 0.

⭐ Landing note: Bundle Analysis is green on this head, so the standing ui-components ceiling debt recorded earlier on this PR does not apply here — every check on the PR is green, not merely the required nine. That earlier note stands as the record of the gate, but ⛔ it is not the condition this PR is being queued under.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 13, 2026 23:54
@os-tesla
os-tesla added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 55f39ee Sep 14, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-9342-kanban-oncardmove-prop branch September 14, 2026 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant