fix(plugin-kanban): read onCardMove from a React prop so the object-kanban arm can tombstone it - #9443
Conversation
…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
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
|
Contract reviewHead reviewed: ① Derived judgments — every accept-set and public-surface change in the diff, named and judged
⇒ No widening of any published accept set survives review. Every surface change is either the ruled narrowing or an additive optional prop. ② Semver
③ Boundary flagsThe dev reported
Acceptance legs, both verified rather than accepted on report:
VerdictPASS. Landing follows: both carriers stripped in one stroke with a provenance note, then ready → queue. Generated by Claude Code |
|
Clause-② carriers stripped — PASS. In-seat review record: comment Mechanical limb: ⭐ Landing note: Generated by Claude Code |
Fixes #9342
Executes the
domain:uiseat's option B ruling recorded on PR objectui#9338.KanbanRendererreadsonCardMovefrom an explicit React prop instead of offschema;ObjectKanbanpasseshandleCardMovethrough that prop; theobject-kanbanarm tombstones the key withhandlerKeyRefusal(..., '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:
onCardMoveon anobject-kanbandocument reaches nothing.
ObjectKanbansubstitutes its ownhandleCardMoveon the schema it hands down and declares noonCardMoveReact prop (its rest parameter is
void _props), so both channels are dead.That is the objectui#6124
'retired'disposition.check:handler-key-readsrefuses'retired'while a renderer stillreads 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
packages/plugin-kanban/src/index.tsxonCardMoveleaves theKanbanRendererProps.schemabag and becomes an explicit React prop, a sibling ofschema; the component reads the proppackages/plugin-kanban/src/ObjectKanban.tsxonCardMove={handleCardMove}as a JSX prop instead of a key on the schema literalpackages/types/src/objectql.tsObjectKanbanSchema.onCardMove?: neverpackages/types/src/zod/objectql.zod.tsonCardMove: handlerKeyRefusal('onCardMove', 'retired', 'Card move handler')scripts/check-handler-key-read-sites.mjsobject-kanban::ObjectKanbanSchema.onCardMoveledger row drainsREADME-KANBAN.md,packages/plugin-kanban/README.md,content/docs/api/schema-reference.md,content/docs/plugins/plugin-kanban.mdxThe read was named in
KanbanImpl.tsx; on disk it is inpackages/plugin-kanban/src/index.tsx(KanbanRendererPropsat:175,onCardMove={schema.onCardMove}in theKanbanRendererbody).KanbanImpl.tsxis the lazy board chunk and has taken
onCardMoveas an ordinary React prop allalong, 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:
Before, same command, same tree minus this change:
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-kanbanslice did drain two rows (39 to 37), butobjectui#9344 then added two —
tabs::TabsSchema.onTabChangeanddetail::DetailSchema.onTabChange, the pair a cast receiver had hidden from thecensus — putting the ledger back at 39 before this branch started. The
object-kanban::ObjectKanbanSchema.onCardMoverow is drained: noobject-kanban::row remains, asserted as a set inhandlerKeyDispositionsMeasured-7804.test.tsxsuite 4, and reachable handlerreads fall 61 to 60 in the same run.
Acceptance leg 2 — the ablation
Prediction, written before the run: restoring the
schema.onCardMovereadwhile leaving the arm tombstoned must turn
check:handler-key-readsred witha
retired-but-readfinding printingdeclares it RETIRED, but a renderer still reads it, and must redden the source-derived pinkanban-handler-slots-7664.test.tsx— whoseforwardedByKanbanRenderer()readsthe
schema.on*accesses out of theKanbanRendererbody — plus the newprop-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 toonCardMove={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):
Leg A —
check:handler-key-reads, exit 1, its own printed line: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:
Restore proven by state, not by re-running the test —
git checkout HEAD --against an absolute path, then:
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.mdtaught anobject-kanbandocument carrying"onCardMove": "(event) => ..."— a function spelled as a string, acceptedand 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-reviewis attached to both carriers in the same write as thecontract increment: the card and this PR. This narrows a published React props
surface (
KanbanRendererProps.schema.onCardMovegoes) and puts a new key onone (
onCardMovebecomes an explicit React prop).Changeset
minorwith a**BREAKING**carrier, verified against the repo's own rulerather than taken on faith: AGENTS.md forbids a
majorin any changeset becausethe
fixedgroup ships as one family whose major tracks@objectstack, andscripts/check-changeset-no-major.mjsenforces it (green here).fixedgroup holds 40 packages today. The card says 41 and AGENTS.md says39. The conclusion is unaffected —
majoris forbidden outright, not scored bygroup size — so this is recorded, not filed.
Acceptance notes — observed, not filed
fixed-group figure (39) and the card's (41) are both staleagainst 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'sRETIREDdocblock 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 sharedheavy-verify lock.
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=2Tasks: 16 successful, 16 totalpnpm lint(eslint . --no-inline-config, whole farm)Tasks: 47 successful, 47 total,0 errorspnpm check:handler-key-readsnode scripts/check-changeset-presence.mjs10 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-no-major.mjsNo changeset declares a major bump.node scripts/check-governed-queue-guard.mjs --testNOT GOVERNED — 16 path(s) checked against 5 governed surface(s); none matched.pnpm check:control-bytesOK (scanned 7603 tracked text file(s))pnpm check:readme-exports0 unbuilt)pnpm check:doc-snippets·check:doc-types·check:doc-examples·check:doc-example-ids·check:doc-fences·check:doc-example-readerspnpm check:action-forward-parity·check:dist-completeness·check:comment-mask-corpuspnpm check:changeset-claimscheck:readme-exportsandcheck:doc-snippetsboth refused to run on anunbuilt tree first (exit 2 / "run
pnpm buildfirst"). That is a prerequisitenot 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_011QreXiyMEqKLN4U5daMPVaGenerated by Claude Code