feat(types): the kanban arm declares the plugin dialect; the DeclarativeKanban* trio retires (objectui#7664) - #7743
Conversation
…ativeKanban* trio retires For an authored `type: 'kanban'` document two types were authoritative depending on who asked: `safeValidateSchema` honoured `DeclarativeKanbanSchema` while the renderer registered for the key consumed `@object-ui/plugin-kanban`'s own `KanbanSchema`, so a board could validate and render empty. Maintainer ruling (a) (2026-09-05, batch #41): the plugin dialect is authoritative. - `@object-ui/types` declares `KanbanSchema` / `KanbanColumn` / `KanbanCard` / `CardTemplate` / `ColumnWidthConfig` (member for member the plugin's shape), mirrors them in `zod/complex.zod.ts`, and `SchemaRegistry['kanban']` names the declaration; `plugin-kanban/src/types.ts` re-exports the five. - The retired dialect's inert keys (`draggable`, column `color`) are `?: never` tombstones refused by name; `onColumnAdd` / `onCardAdd` carried over. - The six `DeclarativeKanban*` exports retire (ADR-0049) with their two objectui#7645 pins; a 7664 pin on each side replaces them. The parity, handler-key and docs-retired ratchets are re-keyed; the docs page moves to the plugin dialect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…ban-plugin-dialect-authoritative # Conflicts: # packages/types/src/__tests__/zod-mirror-parity.test.ts
✅ 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 review — ⛔ VERDICT: REFUSE
⛔ A REFUSE is not cleared by the fix. The remediated diff comes back for a fresh isolated review. R1 — the blocking finding: this PR widens an accept set, in the direction #6124 ruled out
⭐ And the recorded rationale is contradicted by a read site the PR did not checkTwo places in this diff justify the deletion — the But onCardClick={schema.onCardClick}…in the same block as the ⇒ The argument that deleted
|
…ban-plugin-dialect-authoritative
…ban-plugin-dialect-authoritative
…ission The successor `'kanban'` arm dropped `onCardClick` instead of giving it a #6124 disposition. `BaseSchema` is `.passthrough()`, so a dropped key is not refused — it stops being judged and the value is KEPT. Measured on the built dist before this commit, `{ type: 'kanban', columns: [], onCardClick: { action: 'toast' } }` was ACCEPTED with `{"action":"toast"}` surviving into the parsed output, while the same document at `onCardMove` / `onQuickAdd` / `onColumnAdd` / `draggable` was REFUSED. After it, all five are REFUSED. The rationale that was recorded for the deletion — "the object-bound board owns the click" — is contradicted by the source. Measured per registration (`plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx`, new here): - `'kanban-ui'`: `KanbanRenderer` forwards `onCardClick`, `onCardMove` and `onQuickAdd` to the board by identity, from one block. Lit controls: the two keys already ruled runtime slots come out live on this probe. - `'kanban'` / `'object-kanban'`: `ObjectKanban` substitutes its own function for `onCardClick` — and for `onCardMove`, in the same object literal, with `onQuickAdd` arriving by identity as the lit control on this key. The two keys have identical reachability here, so that reading retires both or neither. - and the substitute CALLS the authored handler: `ObjectKanban` declares an `onCardClick` prop (there is no `onCardMove` prop), which `SchemaRenderer` supplies by spreading non-metadata schema keys. An `onCardClick` authored on a `type: 'kanban'` document runs. So the disposition is RUNTIME SLOT: callable on the TypeScript face, refused by name on the mirror. `RUNTIME_SLOT` 44 -> 45, `ALL_SITES` 66 -> 67; `RETIRED` stays 22 and the #7340 docs census stays 26. Why no ratchet caught it: the #6124 ledger is hand-written by key, so a substitution holds its length constant and a deleted key is invisible to it. The new file derives the required key set from the read site instead — it extracts the `schema.on*` reads out of `KanbanRenderer`'s body and requires each to be a declared arm member, which is red on exactly this deletion. Also in this commit, riding with the remediation: - the `@object-ui/plugin-kanban` changeset no longer claims the shape is "member for member what this package declared". Counted with the TypeScript parser against `origin/main`, it is that shape plus four members: three `?: never` tombstones, and `onCardClick`, which the plugin dialect never declared while its renderer read it. - the `@object-ui/types` changeset gains the accept-set bullet for the five handler arms. - the `draggable` / column `color` tombstone JSDoc cites the two-prong discriminator instead of arguing from inertness. Re-measured on `origin/main`: `schema-reference.md` opened its kanban example with `"draggable": true`, documented it in the property table, and authored a `color` on every one of its three columns. - `schema-reference.md` documents `onCardClick` again, and it joins the #7340 blanket-sweep control (six rows -> seven). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
✅ 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
|
⛔ CI red on
|
| reading | result |
|---|---|
Type Check on main (81a2eb1fb) |
success (14:40:40Z, and again at 14:26:34Z) |
Type Check on this PR's previous head bd1fc7111 |
success (14:11:52Z) |
Type Check on ecdaafaf0 |
failure |
| the failing project | packages/plugin-kanban — the package this remediation adds a test file to |
The range bd1fc7111...ecdaafaf0 adds packages/plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx (+251). Green base, green previous head, red once that file lands, in that package's own type-check. ⛔ Not a flake, not the base's, and no re-run is being spent on it — the two green baselines are what a re-run would have been buying.
Two distinct errors, not one
Cannot find **name** 'node:fs' — not "cannot find module". That is a bare specifier read in a type/value position, or a file needing node in its tsconfig types; the same tsconfig is green on main, so something in the diff put those specifiers where the compiler reads them as names. It must be diagnosed separately from the dataSource errors, and ⛔ neither may be silenced by loosening the source: making the provider prop optional or casting it away would be a type-surface change on a PR whose entire subject is that the type surface must be honest.
Four test shards were still in_progress when Type Check went red; they are owed a reading too.
State
⛔ needs:contract-review stays on both carriers. This remediated diff returns for a fresh isolated contract review regardless of CI — a REFUSE is never cleared by the fix. The R1 disposition (onCardClick runtime-slot vs retired) still has to be measured, not chosen; the ledger still owes an answer on what would catch a deleted key (a hand-written ledger cannot, which is how R1 got in); and objectui#7742 still owes its missing undeclared-read rows.
Generated by Claude Code
…age's own type-check
`Type Check` went red on the previous head, in `packages/plugin-kanban`'s own
`pnpm run type-check`, on the file the remediation added. Two distinct causes,
both reproduced with that exact command before and after:
- TS2591 ×3 (`node:fs` / `node:path` / `node:url`) — `tsconfig.test.json`
names `types`, which switches off automatic `@types/*` inclusion, and its
comment recorded the invariant the new file breaks: "nothing in these tests
touches Node globals". The file reads `../index.tsx` off disk on purpose —
that is what makes the guard derive the forwarded key set from the read
site instead of from a list. `node` is named alongside
`@testing-library/jest-dom`, exactly as plugin-calendar / -charts /
-chatbot / -dashboard / -detail / -gantt / -grid already do, and the
comment now says why. It stays OUT of `tsconfig.json`: package source ships
to browsers.
- TS2741 ×2 — `SchemaRendererProvider` requires `dataSource`. Spelled
`dataSource={undefined}`, the form three other call sites in this repo use
for a surface that fetches nothing; these boards author their lanes
statically, so it is the same runtime value the measurements were taken
with. ⛔ Not fixed by loosening the prop or casting the provider — the
subject of this PR is that the type surface must be honest.
`packages/plugin-kanban$ pnpm run type-check` exit 2 -> 0, and
`turbo run type-check` is 81/81 across the repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…ban-plugin-dialect-authoritative
✅ 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
|
✅ 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
|
R1 remediated at
|
| document | before | after |
|---|---|---|
{type:'kanban', columns:[], onCardClick:{action:'toast'}} |
ACCEPTED, and passthrough: onCardClick survives into the parsed output = {"action":"toast"} |
REFUSED / REFUSED, custom @ ["onCardClick"] |
onCardMove · onQuickAdd · onColumnAdd · draggable (controls) |
REFUSED | REFUSED, unchanged |
arm shape declares onCardClick |
false |
true |
The disposition was measured, not chosen: onCardClick is a runtime slot — callable on the TS face, refused by name on the mirror. Ledger deltas: RUNTIME_SLOT 44 → 45, ALL_SITES 66 → 67, RETIRED stays 22, the 7340 docs census stays 26.
⭐ The systemic answer, and it is bigger than this PR
The question this seat put to the remediation was "what would now catch a deleted key?" — because the re-key that let R1 through was a substitution, which holds the ledger's length constant. The answer is filed as objectui#7753: the #6124 handler-key ledger is hand-written by name, so it cannot see a deletion at all. The new derived guard closes this for the kanban arm only — 3 of the ledger's 67 sites. The other 64 reproduce the same failure with every gate green. And the card names the trap: ratcheting the count is the tempting fix and is precisely the one that fails on this very instance.
It corrected the review, and re-measured #7742 rather than pasting into it
- ⛔
bindis NOT an undeclared read — it is a liveBaseSchemamember atbase.ts:248. My REFUSE comment listed it among nine; that was wrong and it belongs on no count of undeclared reads. - finding(types,plugin-kanban): the ruled
KanbanSchemacarries three zero-read members (allowCollapse,cardTemplates,columnWidths) and the board reads an undeclaredtitleField— enforce-or-remove on the shape objectui#7664 declared #7742 gained five real rows the original census missed because it matchedschema.KEYbut not the(schema as any).KEYcast form:navigation(ObjectKanban.tsx:689),objectFields(index.tsx:200— an internal channelObjectKanbanwrites and no author writes), and thekanban-enhancedtrioonColumnToggle/enableVirtualScrolling/virtualScrollThreshold(index.tsx:338-340, on a registration with no declared arm at all). Posted as comment 5552759314 on that card, not as a new one, since it is the class finding(types,plugin-kanban): the ruledKanbanSchemacarries three zero-read members (allowCollapse,cardTemplates,columnWidths) and the board reads an undeclaredtitleField— enforce-or-remove on the shape objectui#7664 declared #7742 already owns. - Two counts re-measured down:
titleFieldis 2 real reads + 2 comment mentions (not 4),filter2 real + 1 comment (not 3).
The two gates the previous round declared NOT MEASURED are now measured
Rather than declaring the narrowing a second time, the seat built the 34-task prerequisite: check:doc-snippets exit 0 (456 of 456 blocks judged, 0 failed) and check:readme-exports exit 0 (421 self-imports judged, 421 real, 0 wrong-path, 0 fabricated).
Type Check — the failure I attributed to this PR, root-caused and closed
Reproduced with the exact job command (packages/plugin-kanban$ pnpm run type-check) at exit 2 with five errors — TS2591 ×3 on node:fs/node:path/node:url and TS2741 ×2 on a missing dataSource — both causes root-caused separately, now exit 0, with repo-wide turbo run type-check at 81 successful / 81 total.
f88304691 shows its checks cancelled because the newer push superseded them, so there is no shard result from that head to carry forward — including the "Type Check went green there" reading I posted earlier. The seat ran a 104-suite cross-package blast radius in its place (1735 tests, exit 0).
What is owed, precisely
- A fresh isolated contract review of the whole remediated diff — 25 files, not just the R1 hunk. ⛔ Not a re-read of the old verdict.
- CI to terminal on
1a5915f26: 24 success / 3 skipped / 0 failed, 5 still running as of this comment (Type Check + the four shards). - Only then the three landing checks and the queue.
Handing the review to the director seat. ⛔ Until it returns, this PR does not move.
Generated by Claude Code
Status refresh — CI is now terminal green; one item remains, and it is not this seat's to doUpdating the "what is owed" list from my park notice (5552874537) so whoever picks this up does not have to re-derive it.
⛔ A REFUSE is never cleared by the fix, so what is owed is a fresh review of the whole remediated diff — not a re-read of the REFUSE verdict, and not a spot-check of the R1 hunk. For the reviewer's convenience, the two things I would put at the top of an attack list — both are the remediation's own claims and neither has been independently verified:
And one correction the remediation made to my REFUSE that a reviewer should carry rather than re-derive: Generated by Claude Code |
|
Fresh contract review (clause ②) after the R1 REFUSE — PASS at head Landing next: governed test 0 of 25 paths, CI 29 success / 3 skipped / 0 red on Implemented-by: Generated by Claude Code |
…ban-plugin-dialect-authoritative Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3 # Conflicts: # packages/types/src/__tests__/zod-mirror-parity.test.ts
|
Removed from the merge queue at 16:17:28Z on a conflict in Generated by Claude Code |
…ssue-7664-kanban-plugin-dialect-authoritative
PM ruling — A: delete the two stale
|
…ban-plugin-dialect-authoritative
…larativeKanban pairs The mirror-wider-than-declared ledger (added by PR 7758) keys two of its 36 rows on pair names this branch retires: 'complex.zod.ts#DeclarativeKanbanColumnSchema': 'cards' 'complex.zod.ts#DeclarativeKanbanSchema': 'columns' `assertionWiderLedgerKeysAreRegistered` requires every ledger key to be a registered `MirrorKey`. After the re-key to `KanbanColumnSchema` / `KanbanSchema`, these two no longer are, and the merged tree is tsc-RED with exactly that one diagnostic. Deleting the rows is the ledger's own documented remedy 4 for a pair that has gone: "a key DISAPPEARED - the faces agree again; correct or delete the entry." Re-keying them to the new pair names instead was measured RED: `WiderOf` for both new pairs is `never`, because the re-key eliminated the drift those rows recorded. No coverage is lost - you cannot watch a pair that no longer exists. Redness inherited from two correct changes landing in sequence, not introduced here. Ruled by the domain:ui PM seat on PR 7743. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
New head pushed:
|
| commit | parents | how |
|---|---|---|
2702b7412 |
1a5915f26 (the reviewed commit) + 2760075ff (PR 7758) |
pre-existing; one five-line import block resolved as a union |
724a7a5ea |
2702b7412 + 554f2b6be |
auto-merged, zero conflicts |
b5de2f206 |
724a7a5ea + 2fcefb955 (current main, PR 7782) |
auto-merged, zero conflicts — mine, this run |
main moved once more while the ruling was being written (554f2b6be to 2fcefb955), so a third merge was taken. 2fcefb955 is now an ancestor of the head; re-checked after the last verification run and main had not moved again at push time.
Merge 3 measured as a union on my own head: the PR delta against 2fcefb955 stayed 25 files / 1470 insertions / 597 deletions across it, and main's three non-parity files (app.ts, app-hidden-catalogue-flag-7542.test.ts, the changeset) are byte-identical to main on the head. Its parity-file contribution — PR 7542 removing the app.zod.ts#AppComponentSchema KnownDrift entry and restating the header at 41 entries / 62 keys — is present and intact.
The only content change: the two authorised ledger rows
One commit, b115f08d0, one file, 4 deletions, 0 insertions. Located by content, not by line number (main's removal of 21 lines had shifted them from 1811 to 1789):
/** SCHEMA-NODE. */
'complex.zod.ts#DeclarativeKanbanColumnSchema': 'cards';
/** SCHEMA-NODE. */
'complex.zod.ts#DeclarativeKanbanSchema': 'columns';
Each row carried its own one-line SCHEMA-NODE doc comment; both went with the rows. WiderThanDeclared goes 36 rows to 34. Verified on my own head before deleting: the MIRRORS registry holds KanbanCardSchema / KanbanColumnSchema / KanbanSchema (lines 592-594) and no DeclarativeKanban* key at all, and complex.zod.ts on this head no longer exports the trio — the five surviving mentions are one doc paragraph plus the retiredDeclarativeKanbanKey refusal-message helper. The pairs are gone, which is the ledger's own documented remedy 4: "a key DISAPPEARED — the faces agree again; correct or delete the entry."
⛔ Nothing else was touched. No count, no other ledger row, no type, no file outside zod-mirror-parity.test.ts. Blob-level: every one of the PR's other 24 files is byte-identical between 724a7a5ea and the pushed head, and the PR's two deleted files are still absent.
tsc — the parity instrument, exit 0, zero diagnostics
$ pnpm exec tsc -p packages/types/tsconfig.test.json --noEmit
TSC_POST_EXIT=0
TSC_POST_DIAGNOSTIC_LINES=0
Before the deletion, on b5de2f206, the same command was exit 2 with exactly one diagnostic — zod-mirror-parity.test.ts(1884,11): error TS2344, assertionWiderLedgerKeysAreRegistered. Exit codes captured by redirecting to a file before any pipe.
Ratchet still lit after the deletion. Control on the pushed head: mutated 'complex.zod.ts#CarouselSchema': 'items' to 'itemsMUTANT', proven on disk (anchor count 1 to 0, mutant count 0 to 1, blob e87df8eb to 35279a18), and tsc went to exit 2 naming the pair twice:
zod-mirror-parity.test.ts(2020,14): error TS2322: Type '"complex.zod.ts#CarouselSchema"' is not assignable to type 'never'.
zod-mirror-parity.test.ts(2039,14): error TS2322: Type 'WiderLedgerKeyDrift' is not assignable to type 'never'.
— assertionWiderMatchesLedger and assertionWiderLedgerRecordsEveryKey. Restored under trap … EXIT INT TERM with an absolute path and git checkout HEAD -- pinned to HEAD: disk hash back to e87df8ebcc5c0a15f469d9523b15d49d0031e260, equal to the HEAD blob, mutant count 0, anchor count 1, git diff HEAD --stat empty, git status --porcelain empty.
Counts unmoved — re-measured on this head, not copied forward
RUNTIME_SLOT 45, RETIRED 22, unique ids 67, EXPECTED_MIRROR_PAIRS 159 — the same four figures the PASS was taken on. Named explicitly by the verbose runner:
✓ handler-keys-json-refusal-6124.test.ts > 67 sites are ledgered, 45 runtime slots + 22 retired, with no key filed twice
✓ zod-mirror-parity.test.ts > the registry population matches EXPECTED_MIRROR_PAIRS
✓ zod-mirror-parity.test.ts > both differences the header speaks of derive from the pin and the live ledgers
✓ zod-mirror-parity.test.ts > the split figures and the totals the header writes down equal the ledger
The header carries prose counts only for KnownDrift and UnmirroredDeclared; there is no written-down figure for WiderThanDeclared anywhere in the file, so the deletion left no stale prose — which is what those last two derivation tests independently confirm.
Everything else
| check | verdict |
|---|---|
pnpm --filter @object-ui/types build |
exit 0 — 124 emitted files, dist completeness OK |
pnpm exec eslint . |
exit 0 — 0 errors, 11943 warnings (repo-wide pre-existing baseline, unmoved) |
check-changeset-presence |
exit 0 — re-derives 25 changed files against merge-base 2fcefb955, 17 published source files of 2 released packages, 2 changesets |
check-changeset-fixed / -no-major / -overwrite |
exit 0 / exit 0 / exit 0 (2 added, 0 modified, 0 deleted) |
check-control-bytes |
exit 0 — 6347 tracked text files; plus a targeted raw-control-byte scan of the edited file, no match |
vitest, packages/types/ + packages/plugin-kanban/ |
exit 0 — 150 files / 2384 tests. |
b5de2f206 the same suites were green while tsc was red. The parity instrument is tsc, above.
PR is open and not a draft; no label added; not enqueued from this seat.
Generated by Claude Code
✅ 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
|
CI 在
|
| status / conclusion | 数量 |
|---|---|
completed / success |
29 |
completed / skipped |
3 |
in_progress |
0 |
| 任何非 success 终态 | 0 |
⛔ 本席没有读 check_suite.completed,那不是闸门读数;上面的分布是把 32 个 run 逐个取 (status, conclusion) 数出来的。19:03Z 那次读数还有 7 个 in_progress(含 4 个 Test shard 与 Type Check),现已全部落到 success。
② ⚠️ needs:contract-review 已重新挂上两个载体 —— 这是重挂,不是新发现
node scripts/pm/check-clause2-carriers.mjs --pair 7743 在重挂前读 exit 4,理由是它自己给的,原话摘要:
card #7664 declares
Clause-②: yeswhile NEITHER it nor its delivering open PR #7743 carriesneeds:contract-review— the gate WAS bound and cleared (last removal2026-09-05T16:13:21Z), but the PR's head has MOVED since: its head commit is dated2026-09-05T18:41:10Z. The review that cleared this gate judged a different tree, so the clear no longer covers what would land.
⇒ 这正是恢复规则点名的「head 后移或无结论才重挂」形状,而重挂是席位的动作、不是脚本的动作(脚本明说:从检查器里写标签等于自查放行)。本席据此把 needs:contract-review 加回 PR #7743 与 卡 #7664 两个载体。
重挂后复跑:
✓ check-clause2-carriers: PR #7743 / card #7664 — the clause-② declaration is readable in the fixed spelling and both carriers agree.
EXIT=0
⇒ 落地前检三条里的 ②③ 现在都是可核的绿;只剩 ①。
③ 唯一剩下的阻塞:总监席延展 PASS
⛔ 本席不延展自己的 PASS 跨新 head。 这条不是客套,是规则:1a5915f26 上的 PASS 由总监席出具,判的是那棵树;b115f08d0 之后又叠了一次 merge(b5de2f206,main 移到 2fcefb955 / PR #7782)和一次 4 行删除,是另一棵树。
按总监席在卡 #7664 上的常设承诺(comment 5553560768):
When the new head is pushed, post it here; this seat extends the PASS and re-arms auto-merge.
新 head 已按该承诺贴在两处(PR 与卡)。⇒ 等总监席:延展 PASS → 摘掉两个载体上的 needs:contract-review → 重新武装 auto-merge(本仓 squash-only,直接 PUT /pulls/7743/merge 返回 405「Changes must be made through the merge queue」,⛔ 不要试)。
auto_merge 现读 null,那不能当作「没入队」的证据 —— 排队中的 PR 这个字段本来就读 null,它回答的是另一个问题。入队与否的权威仪器是 timeline 上的 added_to_merge_queue / removed_from_merge_queue 事件;gh-readonly-queue ref 只能正面证明成员资格,不能反面证伪。这条是本席今天自己踩过并公开撤回过的,写在这里免得下一位重踩。
Generated by Claude Code
|
Contract review (clause ②) PASS extended to head Implemented-by: Generated by Claude Code |
Fixes #7664
Maintainer ruling (a) — comment 5548643216 on the card, decision batch #41, verbatim 「同意」: for an authored
type: 'kanban'document the plugin dialect is authoritative. This PR executes the ruling's list end to end.Clause-②: yes⇒needs:contract-review, parked for the director.⛔ This diff carries a REFUSE remediation and returns for a FRESH isolated review. The verdict is comment 5552425067; a REFUSE is not cleared by the fix.
needs:contract-reviewstays on both carriers.Contract-review remediation — R1: the arm widened an accept set
The finding. The successor
'kanban'arm droppedonCardClickoutright — no refusal arm, no?: never.BaseSchemais.passthrough(), so a dropped key is not refused: it stops being judged and the value is KEPT. Measured on the built dist, both doors (the arm's ownsafeParse, and the exportedsafeValidateSchemaatype: 'kanban'document actually reaches):bd1fc7111+ main{ type: 'kanban', columns: [], onCardClick: { action: 'toast' } }{"action":"toast"}custom, pathonCardClick)onCardMoveonQuickAddonColumnAdddraggableinvalid_type)invalid_type)The disposition was measured, not chosen —
plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx, new here, renders each of the four registrations with authored spies and mocked board chunks:onCardClickonCardMoveonQuickAdd'kanban-ui'(KanbanRenderer)'kanban'(ObjectKanbanRenderer)'object-kanban''kanban-enhanced'Two lit controls, so no zero here comes from a probe that could not have fired. On
'kanban-ui'the two keys this PR already kept as runtime slots come out live, besideonCardClick, off the same forward block inindex.tsx. On'kanban'the lit control isonQuickAdd, which arrives by identity — the schema-spread channel does reach the board on that key — whileonCardClickandonCardMoveare both replaced, in the same object literal of theKanbanRenderercall insideObjectKanban.tsx. ⇒ The rationale that was recorded for the deletion ("the object-bound board owns the click") would have deletedonCardMovejust as well.And a third channel decides it outright:
ObjectKanbanDECLARES anonCardClickprop (there is noonCardMoveprop), and its substitute wrapper calls it. Measured through the production path — a document authored withonCardClick, rendered throughSchemaRenderer, which spreads every non-metadata schema key as a React prop: the board is handed a function that is not the authored one, and invoking it runs the authored one. AnonCardClickauthored on atype: 'kanban'document runs. SoonCardClickis at least as live asonCardMoveon every channel measured, and the disposition is RUNTIME SLOT, not?: never.What that changed:
onCardClick?: (card: KanbanCard, event?: unknown) =a void return on the TypeScript face (unknownfor the event because this package declares zero dependencies and has no React types;KanbanImplnarrows it at the call site) +handlerKeyRefusal('onCardClick', 'runtime-slot', …)on the mirror.RUNTIME_SLOT44 → 45,ALL_SITES66 → 67;RETIREDstays 22 and the #7340 docs census stays 26.KnownDriftforKanbanSchemagainsonCardClick.Why no ratchet caught it, and what does now. The #6124 ledger is two hand-written arrays keyed by name; the re-key was a SUBSTITUTION (
onCardClickout,onQuickAddin) so its length never moved, and nothing derives its population from a read site. The new file closes that for this arm: it extracts theschema.on*reads out ofKanbanRenderer's body inindex.tsxand requires each to be a declared arm member carrying the RUNTIME SLOT guidance — red on exactly this deletion, and derived rather than listed. That covers 3 of the ledger's 67 sites; the class is filed as objectui#7753 with three options and a note that the tempting one (ratchet the count) is the one that fails on this instance.Also corrected, because the review found written rationale the source contradicted:
RUNTIME_SLOTcomment inhandler-keys-json-refusal-6124.test.tsand theKnownDriftcomment inzod-mirror-parity.test.ts— now state what the source does, including the substitution that applies identically toonCardMove;@object-ui/plugin-kanbanchangeset no longer claims the shape is "member for member what this package declared". Counted with the TypeScript parser againstorigin/main: 19 members on the plugin'sKanbanSchema, 6 onKanbanColumn, 7 onKanbanCard; the declarations here are that shape plus four — three?: nevertombstones, andonCardClick, which the plugin dialect never declared while its renderer read it;@object-ui/typeschangeset gains the accept-set bullet for all five handler arms;draggable/ columncolortombstone JSDoc cites the two-prong discriminator instead of arguing from inertness. Re-measured onorigin/main:schema-reference.mdopened its kanban example with"draggable": trueand carried a table row "Enable drag-and-drop between columns", and authored acoloron every one of its three columns with thecolumnsrow reading "each withid,title,color, andcards" ⇒ prong 2 holds for both. Prong 1 holds forcoloronly (classNameis the named live replacement);draggablehas none, which is why prong 2 is what carries it. The hazards differ and both are measured:KanbanColumndoes not extendBaseSchema, so its mirror is a plain object — an undeclared column key is accepted and stripped, whereas an undeclared board key is kept;schema-reference.mddocumentsonCardClickagain, and it joins the docs: three pages still teach retired handler keys as authorable props —onColumnAdd/onCardAdd(schema-reference),onSelectChange/onExpandChange(tree-view),onComplete(input-otp) — stale once #6124's tombstones land #7340 blanket-sweep control (six rows → seven).What changed
@object-ui/typescomplex.ts— the'kanban'arm declaresKanbanSchema/KanbanColumn/KanbanCard/CardTemplate/ColumnWidthConfig: the shapepackages/plugin-kanban/src/types.tsdeclared, plus the four members named above. The two runtime-computed card members keep their React types through the ambient namespace exactly asdata-display.tsalready spellsheaderIcon/rowStyle; the package still declares no React dependency (check:phantom-depsgreen).zod/complex.zod.ts— five mirrors (KanbanSchema,KanbanColumnSchema,KanbanCardSchema,CardTemplateSchema,ColumnWidthConfigSchema);ComplexSchema→AnyComponentSchema→safeValidateSchemaapply them.conditionalFormattingreuses the'object-kanban'arm's rule union (KanbanConditionalFormattingRuleSchema, newly exported fromobjectql.zod.ts);groupingisSpecGroupingConfigSchemaby reference, asObjectGallerySchemaspells it; the two runtime-computed card members pass through asz.any()(theheaderIconprecedent, objectui#6424).registry.ts—'kanban': KanbanSchema(PR fix(types): SchemaRegistry's kanban entry stops describing a component it cannot name #7662's transitionalBaseSchemaintersection re-pointed; theBaseSchemaimport tidied; the comment block rewritten — its "do not restore … without moving the renderer's dialect into a layer this package may depend on" named this route).plugin-kanban/src/types.ts— re-exports the five from@object-ui/types(a plain re-export, not an authority under the one-authority ratchet);InlineFieldDefinitionstays local.ObjectKanban.tsx's bare import becomesimport type. Behaviour unchanged.plugin-kanban/tsconfig.test.json—typesgainsnode, as plugin-calendar / -charts / -chatbot / -dashboard / -detail / -gantt / -grid already do. The new test reads../index.tsxoff disk on purpose: that is what makes its key set derived rather than listed. It stays OUT oftsconfig.json— package source ships to browsers.DeclarativeKanban*exports and both objectui#7645 pin files (their docblocks disclosed it);_KeyKeptcarried forward into the new pin.EXPECTED_MIRROR_PAIRS = 159on top of PR test(types): derive the zod-mirror-parity population instead of writing it down #7731's derived population, merged here;KnownDriftKanbanSchema: onCardMove | onCardClick | onQuickAdd;KanbanSchemaadded toSPEC_DERIVED_PAIRS; the rule union excluded with its reason), handler-keys-6124 (owner rename plusonCardClickrestored andonQuickAddadded ⇒ 45 + 22 = 67), component-docs-retired-handler-keys-7340 (CONTROL re-keyed and extended to 7; census 26 unchanged because theonColumnAdd/onCardAddtombstones carry over onto the successor arm), one-authority baseline history note.api/schema-reference.mdkanban section moves to the plugin dialect (in-page anchor and the import line follow;check-doc-linksgreen);api/index.md, both READMEs,examples/zod-validation-example.ts.@object-ui/typesminor with a BREAKING banner;@object-ui/plugin-kanbanpatch.majornot declared (check-changeset-no-majorgreen).The two numbers the dispatch said not to inherit
complex.tsand pinned in the new test: the plugin dialect's own body is 19 live members (typeis the one the ruling did not count), and the arm declared here is 20 live — those 19 plusonCardClick— plus 3 tombstones (draggable,onColumnAdd,onCardAdd). The twentieth is the R1 remediation: measured onorigin/main,plugin-kanban/src/types.tscontains zero occurrences of the name whileKanbanRendererforwards it, so copying that dialect member for member reproduced its undeclared read.How the ruling's refusal pin was read
The ruling's pin says "a
columns/cardsboard is refused". Read literally that contradicts the ruled shape: the plugin dialect DECLAREScolumns[].cards[]— the two catalog entries are static boards of exactly that shape and render every card (examples/schema-catalog/test/kanban-column-cards-6939.test.tsx, still green). So the refusal keys on what the retired dialect had and this one does not: the board'sdraggableand a column'scolor. The retired CARD keys (labels,assignees,dueDate,priority,content) are deliberately NOT refused: a card is an open record (bucketCardsIntoColumnspushes raw records into lanes), so those are legitimate field names.Read census, re-measured
Over
packages/plugin-kanban/src(non-test), matchingschema.KEYand the(schema as any).KEYcast form the first census missed, with every hit classified code-vs-comment by reading the line. Reads not named byKanbanSchema:titleField(2 real reads plus 2 comment mentions),filter(2 real plus 1 comment — objectui#7712),navigation(1),objectFields(1, an internal channelObjectKanbanwrites, never authored), and the'kanban-enhanced'trioonColumnToggle/enableVirtualScrolling/virtualScrollThreshold(1 each, on a registration with no declared arm at all).bindis not undeclared — it is a live member ofBaseSchema, which this arm extends. Zero-read declared members:allowCollapse,cardTemplates,columnWidths(typealso reads zero and is not a finding — the registry dispatches on it). The five missing rows and both corrections are posted to objectui#7742;onCardClickis recorded there as the row this PR closes, so it does not die when the card does.Verification — all at
1a5915f26(this head, after theorigin/mainmerge)Exit codes captured before any pipe; verdicts quoted from the instruments.
tsconfig.tsbuildinforemoved withdist, ortscemits nothing and the old dist answers).packages/plugin-kanban$ pnpm run type-check— the exact command theType Checkjob runs — reproduced red (exit 2, 5 errors) on the previous head and exit 0 here. Repo-wideturbo run type-check⇒ 81 successful, 81 total.tsc -p packages/types/tsconfig.test.json --noEmit, not vitest: exit 0, zero diagnostic lines.packages/types/ packages/plugin-kanban/ examples/schema-catalog/⇒ 169 files, 4240 tests passed.onCardClickmutated back to?: never; mutation proved on disk (live signature 1→0,neverform 0→1);tsc -p packages/types/tsconfig.test.jsonexit 2 with exactly two diagnostics, both naming the ablation — TS2344 athandler-keys-json-refusal-6124.test.ts(549,10)(the newKeepsFunctionline) and TS2322 atzod-mirror-parity.test.ts(1555,14)(theKnownDriftentry stops being an allowed drift once the faces agree). Restored under atrap … EXIT INT TERMwith an absolute path, proved bygit hash-object== the HEAD blob84392c98f…and an emptygit diff HEAD. The second ablation was not re-run because it was already taken: the new derived guard's red-first run on the unfixed head reportedonCardClickasdeclared: false, guidance: falsebesideonCardMoveandonQuickAddattrue.eslint .over the repo: exit 0 — 0 errors, warnings only (all pre-existingno-explicit-any, none on a line this diff adds).changeset:check,check-changeset-presence("17 source file(s) of 2 released package(s) changed, and this change declares 2 changeset(s)"),check-changeset-overwrite,check:control-bytes("scanned 6318 tracked text file(s)"),check:doc-fences,check:doc-types,check:doc-links("Links are valid across 17 scan roots"),check:vi-mock-specifiers,check:vi-mock-inherit,check:published-tsconfig-exclude,check-type-check-coverage,check:phantom-deps,check:self-import,check:dist-completeness.diston disk) was built here:check:doc-snippets⇒ exit 0, "456 of 456 block(s) judged, 0 failed";check:readme-exports⇒ exit 0, "421 self-imports judged (421 real, 0 wrong-path, 0 fabricated)".Refs: objectui#6172 · objectui#7645 · objectui#7651 (closed not_planned) · objectui#6973 (closed completed) · objectui#7322 · objectui#7712 · objectui#7742 · objectui#7753 · PR #7643 · PR #7662 · PR #7731 · PR #7732.
Session:
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3