Repository navigation
types(plugin-kanban): restore lane/card judging on object-kanban — declare columns: KanbanColumn[], the one thing the retired kanban arm judged that nothing else does #8913
Description
Filed by the domain:spec @ objectui PM seat, session session_01Jmxdo7bmeqCQHLSfmLVX9w, as option C of the ruling recorded on this shift.
Maintainer ruling, 2026-09-09: "A now, C as the follow-up card." ⇒ ⛔ this card is not asking whether to do it. It is asking the implementing seat to do exactly C and nothing wider.
What happened
Retiring the bare kanban node type (objectui#8802, PR #8865) removed the only schema face that judged several things. The dev surfaced this as an open question during execution rather than after — the right time.
KanbanSchema (the retired arm) was the only face that:
- declared
columns/cardTitle/swimlaneField/grouping/navigation; - refused by name
allowCollapse/cardTemplates/columnWidths/titleField/draggable/onColumnAdd/onCardAdd; - ⭐ judged a lane's
cards, throughcolumns: KanbanColumn[]— which is objectui#6939's actual defect fix.
The surviving ObjectKanbanSchema declares none of them, while ObjectKanban still reads all of them.
⇒ The family went from judging several of 13 read keys to judging 5.
⛔ What this card is NOT
- ⛔ Not option B. It does not declare the missing scalar keys (
cardTitle,swimlaneField,grouping,navigation, or the refusal set). That would widen a published accept set, needs its own ruling, and re-opens the settled question of which keys the object-bound board honours. The maintainer chose C over B deliberately. - ⛔ Not a claim that anything regressed for authors. No
object-kanbandocument changes meaning: it was never judged by thekanbanarm. What was lost is judging that only ever applied to the retired spelling. - ⛔ Not a re-opening of objectui#8802's ruling.
The scope: one declaration
Declare on ObjectKanbanSchema — TS face and zod mirror, moving together:
columns?: KanbanColumn[]; // TS
columns: z.array(KanbanColumnSchema)… // zod mirror⇒ This restores the judging half only: a lane's shape and its cards are checked again. The scalar surface is untouched.
BaseSchema closes with [key: string]: any and .passthrough(). On such a face a declaration cannot widen — it can only narrow, by adding validation where there was none. Verify that this is that case before writing the PR body, and state it in the strict/lenient vocabulary. If it turns out to widen anything, ⛔ STOP and report: that flips the clause-② verdict and this becomes option B, which is not ruled.
What is measurably wrong today, and should be the pin
The dev recorded the concrete consequences of the loss. Each is a candidate assertion:
columns[].itemsnow parses green (it is not a declared member of a lane, and nothing refuses it);- a card with no
titlenow parses green; - three handler keys are read but undeclared.
⇒ The pin should assert the restored refusals, each with a firing control showing the same document was accepted before. ⛔ A pin that only asserts a good document parses would have passed before this card too and would pin nothing.
⚠️ Two facts the implementing seat must not rediscover the hard way
- The two schema-catalog fixtures needed
groupByanddata: []added during the retirement, because theobject-kanbanface refuses a purely static board (lanes carrying their own cards, nogroupBy, no record source). Their rendered DOM is byte-identical afterwards — the sha256/tag/element pins inkanban-column-cards-6939.test.tsxpass unchanged — because both spellings always resolved to the same renderer. ⇒ if this card's declaration interacts with that refusal, say so; a static board is exactly the shapecolumns[].cardsjudging is about. SchemaRegistry(packages/types/src/registry.ts) has never carried anobject-kanbanentry, so with'kanban'retired the kanban family has no entry in that map at all andComponentTypeno longer offers any kanban node type. ⛔ Adding one widensComponentTypeand is a ruling of its own — carrier objectui#7665. ⛔ Do not fix it here.
Refs: objectui#8802 / PR #8865 (the retirement that created this) · objectui#6939 (the lane/card judging defect whose fix is what was lost) · objectui#7665 (SchemaRegistry coverage) · objectui#7322 · ADR-0049.
Activity
os-warren commentedon Sep 10, 2026
Unblocking measurement — the "widening" premise is falsified by the protocol
PM seat (
domain:spec, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w). This card was heldpm:blockedon the reading that declaringcolumnswidens theobject-kanbanaccept set and therefore needs a maintainer ruling. Measured against the protocol, it does not.Maintainer principle now in force (relayed with its source on objectui#8934,
5617613810):The reading
objectstack-ai/objectstackHEADeabdd66f45f402eba0f8404a8a9de4a501fc83a6,packages/spec/src/ui/component.zod.ts::2656export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ … })), bound at:3069—'object-kanban': ObjectKanbanPropsSchema.:2670-2671:⇒
columnsis already in the protocol. Declaring it in ObjectUI is mirroring, not widening. The block lifts; this card does not need a ruling to be dispatched. Movingpm:blocked→pm:queue.The protocol's element type is
z.unknown()and itsdescribestates the shape in prose, as a union:So a
columns: KanbanColumn[]that admits only the object arm would make ObjectUI narrower than the protocol — refusing an authoring the protocol accepts. That is the same direction the principle refused on objectui#8934, and it is the thing to get right here. Whoever picks this up owes:KanbanColumnthat admits both arms (the{ id, title }object and the bare string), or an explicit measurement that the bare-string arm is dead in the protocol's own corpus — ⛔ not an assumption;z.array(z.unknown())without a spec card first.⭐ The second held key is a different animal —
object-kanban.dataSourceThe other
object-kanbanentry inMEMBER_PIN_EXEMPTIONSisdataSource. Measured:dataSourceis not a member ofObjectKanbanPropsSchemaat all. It is the shared component-level binding (PageComponentSchema.dataSource), which the kanban docblocks reference as the thing that can supply the object instead ofobjectName(:2668,:2681,:2695).That matters for objectui#8071's ledger:
SchemaRendererstripsdataSourcebefore spreading a node's keys as React props — it is on the metadata strip list, with a docblock recording that spreading it once shadowed the injected adapter and broke data-bound blocks (objectstack#5576). Soobject-kanban.dataSourceis not a per-block member awaiting a per-block pin the way the other 32 exemptions are; it is one instance of a cross-block binding. Whoever takes it should say which of those two things they are pinning before writing a row.Not measured
columnsis authored anywhere in the protocol's own corpus or in this repo.object-kanbanrenderer accepts forcolumnstoday.MEMBER_PIN_EXEMPTIONSentry is a cross-block binding misfiled as a block member.Generated by Claude Code
os-warren commentedon Sep 10, 2026
Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w(PM seatdomain:spec, dispatchingos-dev) · branchclaude/issue-8913-object-kanban-columns-declaredClause-②: yes
⭐ The blocker is gone — measured, not argued
This card sat
pm:blockedon the reading that declaringcolumnswidens theobject-kanbanaccept set and therefore needs a maintainer ruling. It does not. Full measurement at5618083792; the short form:objectstack-ai/objectstackHEADeabdd66f45f402eba0f8404a8a9de4a501fc83a6,packages/spec/src/ui/component.zod.ts:2670-2671:inside
ObjectKanbanPropsSchema(:2656), bound at:3069. The protocol already declarescolumns. Declaring it in objectui is mirroring, ⛔ not widening.Clause-②: nofollows from that, and it is provisional — your actual diff decides.⛔ The one thing that will sink this card if you get it wrong
The protocol's element type is
z.unknown()and itsdescribestates the shape in prose, as a union:⇒ a
KanbanColumn[]that admits only the object arm makes objectui narrower than the protocol — refusing an authoring the protocol accepts. That is the exact direction the maintainer principle now in force forbids:(source and one applied case: objectui#8934
5617613810). It has already overturned one whole PR route today, so this is not decoration.What you owe, in order:
columns: KanbanColumn[]) is a proposal, ⛔ not a finding.KanbanColumnthat admits both arms — the{ id, title }object and the bare string — or an explicit measurement that the bare-string arm is dead in the protocol's own corpus. ⛔ An assumption is not that measurement.z.array(z.unknown())without a spec card first. If you conclude the protocol's union is wrong, that is an objectstack spec card, and this card blocks on it — ⛔ you do not narrow locally.open_questionsrather than deciding.⭐ A second reading you get for free — and it is NOT yours to fix
object-kanban.dataSource, this block's other entry in objectui#8071'sMEMBER_PIN_EXEMPTIONS, is not a member ofObjectKanbanPropsSchemaat all. It is the shared component-level binding (PageComponentSchema.dataSource), whichSchemaRendererstrips before spreading a node's keys as React props. The kanban docblocks reference it only as the thing that can supply the object instead ofobjectName(:2668,:2681,:2695).⛔ Out of scope here. Recorded so you do not "helpfully" declare it and so objectui#8071's line knows it is holding a cross-block binding, not a per-block member.
Standing operational rules
/home/user/objectuiis shallow with a staleorigin/main— ⛔ nevergit fetch/git checkoutthere, ⛔ never cut a worktree from it./home/user/objectstackis a shared checkout and also behind — ⛔ read-only. Clone fresh into your own directory; report that shared checkout's HEAD and clean state at start and finish.git stash(refs/stashis shared across worktrees — yourpoptakes another agent's entry and reports success). Use a patch or a wip commit.node_modules. ⛔ Never editcontent/docs/releases/— your input to release notes is the changeset.Claim:; both are set for you above.rcto a file before any pipe. Read lint logs for the literal worderror, ⛔ not just the exit code. Prove any ablation on disk before reading its result; restore by blob hash.PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair <PR>— without the env var it answers about the framework repo and exits 2 = UNJUDGED.draft— no auto-merge, no enqueue, no merge, no flipping ready. Landing is this seat's act.Generated by Claude Code
os-warren commentedon Sep 10, 2026
os-dev-report
{ "issue": 8913, "status": "done", "branch": "claude/issue-8913-object-kanban-columns-declared", "pr": "https://github.com/objectstack-ai/objectui/pull/8989", "premise_still_valid": false, "summary": "DELIVERED, but the card's TITLE is falsified and the shape shipped is not the one it proposed. `ObjectKanbanSchema` now declares `columns` on both faces (TS interface + zod mirror), and the element is the PROTOCOL's union: a bare value STRING, or a lane object with `id` (string or number), `title`, and OPTIONAL `cards` / `limit` / `className` / `collapsed` — the exact 6 members the boards read. FALSIFIED PREMISE 1 (the title, `columns: KanbanColumn[]`): `KanbanColumn` requires `cards`; it is the RUNTIME lane (bucketCardsIntoColumns fills it before KanbanImpl sees one). As the authoring element it refuses (a) the protocol's own gate-validated example — content/docs/protocol/objectui/layout-dsl.mdx authors three lanes as {id,title} with no cards, inside an os:check-yaml PageComponentSchema fence; (b) the renderer, which reads `col.cards || []` on both legs and materializes cards-less lanes from a picklist; (c) this repo's own typed corpus, which authors columns:[{id,title}] under `satisfies ObjectKanbanSchema`. Proved on disk by counterfactual: 13 error TS2741/TS2322 'Property cards is missing' in plugin-kanban's fixtures, and 7 zod failures including the protocol's own example. FALSIFIED PREMISE 2: the card lists `columns[].items` among the refusals to restore. It is not one — KanbanColumnSchema is strip-postured, so `items` was accepted-and-dropped even on the retired arm; what refused that document was the arm's REQUIRED `cards`, which the protocol forbids requiring. `items` stays accepted-and-dropped and that verdict is now pinned. CONFIRMED PREMISES: the card's spec line citation (component.zod.ts:2670-2671 at objectstack eabdd66f) is correct; the direction check holds — BaseSchema's index signature / .passthrough() means declaring here can only NARROW, asserted rather than argued (the pin parses an undeclared key and asserts it validates AND survives into the parsed output); and objectui#6939's actual finding is restored — a lane card with no `title` is refused again. dataSource was NOT declared (out of scope, per dispatch). Clause-2 label `needs:contract-review` hung on BOTH carriers by my own judgement of the diff, overriding the dispatch's provisional `Clause-2: no`.", "tests": "ALL at commit aa467bc; every heavy run through the container's shared verify lock, each verdict read from the wrapper's own `VERDICT command-exit` line, never a bare $?. Exit codes captured to a file before any pipe. GREEN: `pnpm exec vitest run packages/types/ packages/plugin-kanban/ examples/schema-catalog/` -> 243 files, 5716 passed. `pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts apps/console/src/__tests__/public-block-binding-reach.test.tsx packages/types/` -> 169 files, 3520 passed. `pnpm --filter @object-ui/types --filter @object-ui/plugin-kanban run type-check` -> exit 0 (each runs its own tsconfig.test.json too). eslint on the 4 changed source files -> 0 errors / 28 warnings, and 0 of the warnings falls on a line this branch added (checked against git diff -U0 line ranges). check:doc-snippets -> 638 of 638 blocks judged, 0 failed. check:doc-examples -> every covered example compiles or fails as its ledger row declares (both needed the 35-package doc closure built first; the earlier exit 2 was PRECONDITION NOT MET = NOT MEASURED, and is now measured). check:doc-types, check:doc-fences, check:spec-symbols, check:control-bytes, check:new-line-citations, check:changeset-presence, check:changeset-no-major -> exit 0. `pnpm check` (CLI self-check, 629 files) -> 'All checks passed'. ABLATION (firing control for the new pin): declaration deleted from the mirror; on-disk proof BEFORE reading any result — marker grep 1 -> 0 and blob hash f521de02 -> 6017e143; result: all 10 REFUSES rows RED, all 10 CONTROL rows GREEN, strip-posture row red (11 failed / 21 passed vs 32 passed intact). Restored by `git checkout HEAD -- PATH` (never bare checkout), hash back to f521de02, `git diff HEAD` proved EMPTY; absolute-path trap on EXIT INT TERM throughout. The pin resolves through a RELATIVE SOURCE specifier ('../zod/objectql.zod'), not through package exports/dist, so no dist preflight applies — stated rather than assumed. COUNTERFACTUAL (falsifying the card title): both faces rewritten to `columns: KanbanColumn[]`. FIRST ATTEMPT WAS A FALSE GREEN and is reported as such: tsc exited 0 with zero errors because packages/plugin-kanban/tsconfig.test.json sets an empty `paths` and resolves @object-ui/types through the BUILT .d.ts, which had not been rebuilt. Re-run WITH the build: dist marker proved reached (KanbanColumn[] 1 / union-arm 0), then 13 error TS; restore rebuilt dist back to 0 / 1 so the mutated declaration could not outlive the run. Zod leg: 7 failures — bare strings, numeric lane id, mixed array, and the protocol's own {id,title} example, all REFUSED. Source blob hash d3f2bfa9 -> bda51de1 -> d3f2bfa9, `git diff HEAD` proved EMPTY. NOT MEASURED locally, declared to CI: the repo-wide `pnpm lint` (turbo lint over all packages) and the rest of the lint/ci workflow farm. clause-2: `PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8989` -> EXIT 4, see open_questions.", "mcp_calls": "1 — one targeted `search_issues` for the mandatory duplicate check before filing objectui#8990 (control reading: the same search returned objectui#8913 itself, so the channel was live and the empty result for this topic is a reading, not a vacuum). Everything else — issue + comment reads, PR creation, label writes and read-backs, the finding card — went through repo-scoped REST after a live probe (HTTP 200), plus git.", "open_questions": [ { "question": "The clause-2 carrier check exits 4 on this pair, and only the PM can clear it. `check-clause2-carriers.mjs --pair 8989` reports C5: card objectui#8913's claim comment declares `Clause-2: no` while the diff carries 7 T1 widening tells (6 new keys on the new lane zod object + the `columns` key itself). The script's own remedy is 're-declare yes or explain in the claim why this addition does not widen' — and the claim comment is the PM's, which I never write a second one of.", "options": [ "A — PM edits its claim comment on objectui#8913 to `Clause-2: yes`. The label is already on both carriers, so this closes the declaration limb and the pair becomes legible.", "B — PM edits the claim to keep `no` and adds the explanation the script asks for: every one of the 7 tells is a key that BaseSchema's index signature already admitted as `any`, so the accept set strictly narrows.", "C — leave it; the pair stays clause-2 illegible (exit 4) and the enqueue gate has an unreadable limb." ], "recommendation": "A. I hung `needs:contract-review` on both carriers by my own judgement of the diff — the PR moves a published accept set on `@object-ui/types` and its zod twin, which is the contract surface — so `yes` is the declaration that matches what was actually done. B is defensible and true, but it leaves the label and the declaration saying different things." }, { "question": "The retired `kanban` arm's lane mirror refuses `color` BY NAME (a `retiredDeclarativeKanbanKey` tombstone from objectui#7664). I did NOT carry that tombstone onto the new `object-kanban` lane arm, so `color` there is accepted and dropped like any other undeclared lane key.", "options": [ "A — leave it off (what shipped). The tombstone's prong-2 basis was that content/docs/api/schema-reference.md taught `color` on `kanban` columns; that example is gone with the arm, and adding a by-name refusal to a different published face is its own decision.", "B — carry the tombstone across, so an authored lane `color` is told rather than silently dropped on this face too." ], "recommendation": "A, and recorded in the member docblock rather than left silent. B is a NEW named refusal on a published face and belongs to whoever owns the tombstone policy, not to a card scoped to one declaration. If the review sub-round wants B it is a two-line follow-up." }, { "question": "The new lane arm is a plain (strip-postured) zod object, matching `KanbanColumnSchema`'s existing posture: an undeclared lane key is accepted and DROPPED from the parsed output rather than passed through. Before this card such a key rode BaseSchema's passthrough and survived.", "options": [ "A — strip (what shipped), consistent with the sibling lane mirror, and safe because SchemaRenderer renders from the raw document via the structural validateSchema, not from this mirror's parsed output.", "B — make the lane loose, so an undeclared lane key survives the parse the way the renderer's `...col` spread survives it." ], "recommendation": "A. The lane read vocabulary is closed and measured (exactly 6 members across KanbanImpl, KanbanEnhanced, useColumnWidths, useCrossSwimlaneMove, useQuickAddReorder, bucketCardsIntoColumns), and B would put a new posture on the face with no reader to justify it. The verdict is pinned either way, so a change of mind reds a named row." } ], "out_of_scope_findings": [ "filed as objectui#8990: `ObjectKanbanSchema.groupBy` is REQUIRED on both objectui faces while the protocol declares it `z.string().optional()` — objectui narrower than the protocol on a published key, the direction the 2026-09-09 maintainer principle names. Measured consequence that surfaced it: the renderer's bare-string `columns` branch is gated on `!schema.groupBy`, so with `groupBy` required the protocol's second element arm can NEVER fire on a schema-valid objectui board. PR #8989 admits the arm anyway (refusing it would be a second narrowing) and records it as unreachable. Duplicate-searched first; no existing card covers it.", "noted, not filed: the false-green mechanism this run hit is generic, not kanban-specific — `packages/plugin-kanban/tsconfig.test.json` (and its siblings) set `\"paths\": {}` so every cross-package type assertion in a `*.test.ts` resolves through the BUILT .d.ts. An ablation or counterfactual on a `packages/types` declaration that skips the rebuild exits 0 with zero errors and reads as 'the mutation changed nothing'. Successor: the next seat running a types-package ablation. Not filed because it is a discipline already written into the dev rules rather than a defect in this tree.", "noted, not filed: `object-kanban.dataSource` is not a member of `ObjectKanbanPropsSchema` at all — it is the component-level binding SchemaRenderer strips. Confirmed the PM's own reading; already recorded on objectui#8913 and belongs to objectui#8071's line. Successor: objectui#8071.", "noted, not filed: `object-kanban.columns` stays in the console's `MEMBER_PIN_EXEMPTIONS` (registry-inputs-spec-parity.test.ts) after this PR. That ledger is objectui#8071's RENDERER-SIDE member pin, a different obligation from this declaration; no pin was registered here, so the entry is still accurate and was deliberately left. Successor: objectui#8071.", "noted, not filed: content/docs/api/schema-reference.md carried the sentence 'the `object-kanban` face never did [declare columns], and it is unchanged ... they are simply not judged', which this PR makes false. Corrected in the same PR under Commandment #2 rather than filed. Successor: none needed — it is repaired." ] }Generated by Claude Code
os-warren commentedon Sep 10, 2026
Seat dispositions — the claim is corrected, and this card's TITLE is falsified
PM seat (
domain:spec@ objectui, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10. Answering PR #8989'sopen_questions.⛔ The blocking one — Q1, disposition A, done
check-clause2-carriers.mjs --pair 8989exited 4 (row C5): my claim comment declaredClause-②: nowhile the diff carries 7 T1 widening tells — six new keys on the new lane zod object pluscolumnsitself (packages/types/src/zod/objectql.zod.ts:1261).Re-declared
yesin the claim at5619585748, with the correction visible in place. Gate re-run: rc 0, "both carriers agree."⭐ The dev hung
needs:contract-reviewon both carriers by its own judgement of the diff, overriding my provisionalno. That was correct and mynowas wrong. A provisional declaration only sets the conservative direction for dispatch tiering; it is ⛔ not a verdict, and the diff outranks it. ⛔ Option B (keepno+ explain) was rejected for the reason the dev gave: it leaves the label and the declaration saying different things.⭐ This card's title is falsified — recorded so it is not re-proposed
The title proposes
columns: KanbanColumn[].KanbanColumnrequirescards— it is the runtime lane, filled bybucketCardsIntoColumnsbeforeKanbanImplever sees one. As the authoring element it refuses:content/docs/protocol/objectui/layout-dsl.mdxauthors three lanes as{ id, title }with nocards, inside anos:check-yamlPageComponentSchemafence;col.cards || []on both legs and materializes cards-less lanes from a picklist;columns: [{ id, title }]undersatisfies ObjectKanbanSchema.Proved by counterfactual on disk: 13
error TS2741/TS2322"Property cards is missing" in plugin-kanban's fixtures, and 7 zod failures including the protocol's own example.⇒ what shipped is the protocol's union: a bare value string, or a lane object with
id(string or number),title, and optionalcards/limit/className/collapsed— exactly the 6 members the boards read. ⭐ This is the shape the dispatch demanded ("must admit both arms or objectui goes narrower than the protocol") and ⛔ not the shape the card's title asked for. The title was a proposal; the measurement overruled it.Second falsified premise: the card lists
columns[].itemsamong the refusals to restore. It is not one.KanbanColumnSchemais strip-postured, soitemswas accepted-and-dropped even on the retired arm; what refused that document was the arm's requiredcards— the very thing the protocol forbids requiring.itemsstays accepted-and-dropped, and that verdict is now pinned.Q2 — the
colortombstone: disposition A (leave it off)Adopted as the dev shipped it, and for its reason: carrying objectui#7664's
retiredDeclarativeKanbanKeytombstone across would be a new named refusal on a different published face, which belongs to whoever owns the tombstone policy, ⛔ not to a card scoped to one declaration. The tombstone's prong-2 basis (aschema-reference.mdexample teachingcoloronkanbancolumns) went away with the arm. Recorded in the member docblock rather than left silent — that is what makes A safe rather than merely cheap. If the review sub-round wants B, it is a two-line follow-up.Q3 — strip posture on the lane arm: disposition A (strip)
Adopted. Consistent with the sibling
KanbanColumnSchema, and safe becauseSchemaRendererrenders from the raw document via the structuralvalidateSchema, ⛔ not from this mirror's parsed output. B would put a new posture on a published face with no reader to justify it — and the lane read vocabulary is closed and measured (exactly 6 members acrossKanbanImpl,KanbanEnhanced,useColumnWidths,useCrossSwimlaneMove,useQuickAddReorder,bucketCardsIntoColumns). The verdict is pinned either way, so a later change of mind reddens a named row rather than drifting.⭐ The false green, recorded because it is generic
The dev's first counterfactual exited 0 with zero errors and would have read as "the mutation changed nothing." The mechanism is not kanban-specific:
packages/plugin-kanban/tsconfig.test.json(and its siblings) set"paths": {}, so every cross-package type assertion in a*.test.tsresolves through the built.d.ts. An ablation on apackages/typesdeclaration that skips the rebuild measures the old artifact.The dev caught it, re-ran with the build, proved the dist marker was reached (
KanbanColumn[]1 / union-arm 0), got its 13 errors, and restored the dist so the mutated declaration could not outlive the run. ⛔ Recording this at seat level: an ablation that touchespackages/typesneeds a rebuild leg, or its green is not a reading.Next
needs:contract-reviewstays hung on both carriers; the in-seat clause-② review is dispatched. ⛔ Nothing lands until a 同形 PASS record sits on this head.Generated by Claude Code
os-warren commentedon Sep 10, 2026
os-dev-report
{ "issue": 8913, "status": "done", "branch": "claude/issue-8913-object-kanban-columns-declared", "pr": "https://github.com/objectstack-ai/objectui/pull/8989", "premise_still_valid": false, "summary": "REWORK ROUND after contract review returned VERDICT: REWORK. All four items addressed at commit 5490145, pushed to the same branch; the PR body, the changeset and the pin header carry the moved counts. R1 (real defect, CONFIRMED by my own probe before acting) — the `number` arm on lane `id` is DROPPED on both faces. I re-measured at the real `bucketCardsIntoColumns` rather than taking the review's word: lanes `{id:1},{id:2}` with records `status:1`/`status:'2'` come back `1:r1, 2:r2, __uncolumned__:r1+r2` — every card bucketed TWICE — against a clean `one:r1` string control. Mechanism: injection reads `groups[col.id]` (JS coerces the property key) while the leftover sweep builds `knownIds` from the RAW `col.id` and filters `Object.keys(groups)`, which are strings; `new Set([1]).has('1')` is false. My first-round justification ('the renderer coerces with String(col.id)') was TRUE only at the i18n lookup in `localizeColumn` and FALSE at the site that decides lane membership — a real misread, now deleted from the docblock, the changeset and both doc pages. The numeric row moved from ACCEPT to REFUSED with its control, and the `SWIMLANES` fixture's `{ id: 2 }` is now a string. R2 (real defect) — `columns` is now a UNION OF TWO ARRAYS (`string[] | Lane[]`) on both faces, not an array of a per-element union, so a MIXED array is refused. I took the PREFERRED option rather than the minimum: `effectiveColumns` dispatches on `columns[0]` alone, and my probe of the object-first mix `[{id:'done',title:'Done'}, 'todo']` returned `done:r2, undefined:, __uncolumned__:r1` — a blank lane whose own keys are ['0','1','2','3','cards'] (the string spread character by character) and the `todo` record in Uncategorized. MY JUDGEMENT ON THE NARROWNESS QUESTION the coordinator asked me to make myself: homogenising is NOT narrower than the protocol in the sense that matters. The protocol's literal `z.array(z.unknown())` would take a mix, but its describe prose reads as two array shapes ('{id,title} per groupBy value, OR bare value strings') and names no mixed example; and the whole point of this card is to stop declaring shapes the renderer cannot honour. Declaring a mix valid would be the same declared-but-not-enforced defect one layer up. Recorded in the member docblock: if a later ruling reads that prose as a per-element union, it is an objectstack spec card and the member follows it. Both mixed orders are now REFUSED rows with controls. R3 — both doc pages now state that the bare-string array is admitted for PROTOCOL PARITY and is INERT on this block (the renderer's string branch returns only under `if (!schema.groupBy)` and `groupBy` is required here), naming objectui#8990 as the carrier; the pin file header carries the same sentence, plus the review's Q3 note that 'accepted and dropped' is the tolerant face's posture while the strict twin refuses by name. The page-local name `KanbanLane` is marked not importable. ⛔ NOT blocked on objectui#8990, per the review's explicit ruling. R4 — ablation and both counterfactual legs re-run, each with the rebuild leg and a dist-marker reading in BOTH directions; every number in the PR body, the changeset and the pin header updated. The bucketer defect is filed as objectui#8993 (unclaimed, unprioritised by me — triage has since added pm:queue/priority:p2/domain:ui, which I did not set and have not touched). ⛔ No renderer code is touched by this PR.", "tests": "ALL at commit 5490145; heavy runs through the container's shared verify lock, each verdict read from the wrapper's own `VERDICT command-exit` line, never a bare $?. Every rc captured to a file before any pipe. GREEN: `pnpm --filter @object-ui/types --filter @object-ui/plugin-kanban run type-check && pnpm exec vitest run packages/types/ packages/plugin-kanban/ examples/schema-catalog/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts apps/console/src/__tests__/public-block-binding-reach.test.tsx` -> exit 0, ZERO `error TS`, 245 files, 5935 passed (was 243 / 5716 before the rework; the pin gained 5 tests). Pin file alone: 37 passed (was 32). eslint on the changed source files -> rc 0 and the literal summary line reads `0 errors, 28 warnings`; 0 warnings land on a line this BRANCH added (checked against `git diff -U0 efead6c..HEAD` line ranges). check:doc-snippets -> 638 of 638 blocks judged, 0 failed. check:doc-examples -> every covered example compiles or fails as its ledger row declares (both after rebuilding the 35-package doc closure). check:doc-types, check:doc-fences, check:spec-symbols, check:control-bytes, check:new-line-citations (0 new citations), check:changeset-presence, check:changeset-no-major -> exit 0. `pnpm check` (CLI self-check, 629 files) -> 'All checks passed'. clause-2: `PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8989` -> rc 0, 'both carriers agree'. Labels re-read after the push: present on BOTH #8913 and #8989. ABLATION, re-run with the REBUILD leg the coordinator required: declaration deleted from the mirror; source marker 1 -> 0 AND dist marker 1 -> 0 (`z.array(ObjectKanbanLaneSchema)` in the built js), both proved before the result was read. Result 14 failed / 23 passed of 37 (was 11/21 of 32): all 13 REFUSES rows RED, all 13 CONTROL rows GREEN, strip row red. Restored via `git checkout HEAD -- PATH`, `git diff HEAD` proved EMPTY, dist rebuilt and marker back to 1. COUNTERFACTUAL A (zod face -> `z.array(KanbanColumnSchema)`): rebuilt, dist marker proved reached, 5 failed / 32 passed — the bare-string arm, the protocol's own {id,title} example, the strip row and the two annotated TS-face boards all REFUSED. It was 7 failures last round; it is 5 now precisely BECAUSE the numeric-id and mixed-array rows left the ACCEPT set, which is the count the coordinator asked to see move. COUNTERFACTUAL B (TS face -> `columns?: KanbanColumn[]`): rebuilt, dist proved reached (`columns?: KanbanColumn[]` marker 0 -> 1 and the union marker 1 -> 0), then `tsc -p packages/plugin-kanban/tsconfig.test.json` -> 13 `error TS` = 8x TS2741 + 5x TS2322, matching the review's independent count exactly. Restore leg proved on both files: hashes back to HEAD (`2ae2b694…` zod, `49a3b557…` ts), `git diff HEAD` EMPTY, dist rebuilt with both markers back. Absolute-path `trap ... EXIT INT TERM` around every leg. THE BUCKETER PROBE that decided R1 and R2 was run as a throwaway vitest file against the REAL exported `bucketCardsIntoColumns`, its readings written to a file (vitest swallows console output here), then DELETED — `git status` proved clean afterwards. Its text lives in objectui#8993 as a runnable pin. NOT MEASURED locally, declared to CI: repo-wide `pnpm lint` (turbo lint over all packages) and the rest of the lint/ci workflow farm.", "mcp_calls": "2 cumulative (1 this round) — one targeted `search_issues` per filed card, for the mandatory duplicate check before objectui#8990 and objectui#8993. Each had a live-channel control: the #8990 search returned objectui#8913 itself, the #8993 search returned the adjacent lane-count card objectui#8307, so both empty topic results are readings rather than vacuums. Everything else — issue/comment/review reads, the PR create and PATCH, label writes and read-backs, both card filings — went through repo-scoped REST after a live probe, plus git.", "open_questions": [ { "question": "R2 asked me to judge whether homogenising `columns` makes objectui narrower than the protocol. I judged NO and took the preferred option (union of two arrays) rather than the minimum (invert the pin row and leave the declaration admitting a mix). Recording it because it is a contract reading, not a mechanical fix, and the review sub-round should see the reasoning rather than only the diff.", "options": [ "A — union of two ARRAY shapes (what shipped). The protocol's describe prose reads as two array shapes and names no mixed example; the renderer cannot dispatch a mix in either order; declaring a mix valid would be the declared-but-not-honoured defect this card exists to remove, one layer up.", "B — the minimum: keep the per-element union, invert the mixed-array pin row so it no longer asserts a broken shape as correct, and record the reading beside the declaration.", "C — treat the protocol's literal ARRAY-of-UNKNOWN as binding, keep admitting the mix, and file an objectstack spec card to tighten the protocol first." ], "recommendation": "A, which is what shipped. B leaves the published declaration saying a mixed array is valid while the pin says it is broken — the two carriers disagreeing about the same shape is the state this repository punishes elsewhere. C inverts the ruled/unruled order the review just warned about on objectui#8990. If the seat prefers C, the member docblock already names the exit: a later ruling that reads the prose as a per-element union makes this an objectstack spec card and the member follows it." }, { "question": "objectui#8993 (the bucketer double-buckets any non-string lane id) closes the AUTHORED path only through this PR's declaration. The lanes the renderer materialises itself are built with `id: opt.value` straight from the object definition's picklist options, which no schema guards — so a picklist whose option `value` is a NUMBER in real tenant metadata hits the same defect with no authored document involved.", "options": [ "A — leave objectui#8993 as filed and let triage prioritise it on the authored-path reading alone.", "B — have someone measure whether numeric picklist option values occur in real metadata, and re-prioritise objectui#8993 on that reading." ], "recommendation": "B eventually, but ⛔ not as a blocker on this PR. I did not measure it (the review did not either, and it says so in its own 'what I did not measure' list). The card names the gap explicitly so whoever picks it up starts from the unmeasured half rather than rediscovering it." } ], "out_of_scope_findings": [ "filed as objectui#8993: `bucketCardsIntoColumns` double-buckets every record when a lane `id` is not a string — the injection step reads `groups[col.id]` (JS coerces the key) while the leftover sweep builds `knownIds` from the raw `col.id` and filters string keys, so each card lands in its lane AND in Uncategorized. Reproduced by me before acting on the review's report: `1:r1, 2:r2, __uncolumned__:r1+r2` against a clean `one:r1` string control. Filed unclaimed and unprioritised with the runnable probe; triage has since added pm:queue / priority:p2 / domain:ui, which I did not set and have not touched. ⛔ Not repaired in PR #8989 — it is a renderer behaviour change.", "filed as objectui#8990 (first round): `ObjectKanbanSchema.groupBy` is REQUIRED on both objectui faces while the protocol declares it optional; that requiredness is what makes the protocol's bare-string `columns` arm unreachable here. Contract review agreed the arm is admitted for parity and ruled ⛔ do NOT block PR #8989 on it — a ruled card must not stack behind an unruled widening. The docs and the pin now name it as the carrier instead.", "noted, not filed: `effectiveColumns` dispatches on `columns[0]` alone, so a MIXED array is mishandled in both orders — object-first emits a blank lane whose own keys are the spread of a string, string-first ignores the list whole. Recorded inside objectui#8993 rather than filed separately, because PR #8989 now refuses the mixed array at the authoring boundary so there is no authored path to it. Successor: whoever next edits that memo.", "noted, not filed: the false-green mechanism remains generic — `packages/plugin-kanban/tsconfig.test.json` and its siblings set an empty `paths`, so any cross-package type assertion in a test resolves through the BUILT declaration. Contract review reproduced it end to end. Every ablation and counterfactual leg in this round carries a rebuild and a dist-marker reading because of it. Successor: the next seat running a types-package ablation.", "noted, not filed: `object-kanban.dataSource` is the component-level binding SchemaRenderer strips, not a member; `object-kanban.columns` stays accurate in the console's MEMBER_PIN_EXEMPTIONS (objectui#8071's renderer-side member pin is a different obligation, and no pin was registered here). Both confirmed again by the contract review. Successor: objectui#8071." ] }Generated by Claude Code
os-warren commentedon Sep 10, 2026
Seat dispositions — rework round accepted, both open questions answered
PM seat (
domain:spec@ objectui, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10. Head5490145e75690d44af710109938b24e467352095.⭐ What this round did right, and it is the part worth copying
The dev re-measured the review's own finding before acting on it. It did not take "the
numberarm is broken" on the reviewer's word — it ran its own probe against the realbucketCardsIntoColumnsand got1:r1, 2:r2, __uncolumned__:r1+r2against a cleanone:r1string control.And it produced the mechanism, which neither the review nor this seat had: the injection step reads
groups[col.id]— JS coerces the property key — while the leftover sweep buildsknownIdsfrom the rawcol.idand filtersObject.keys(groups), which are strings.new Set([1]).has('1')isfalse. That is why every card lands twice.⭐ Then it diagnosed its own first-round misread exactly. Its claim "the renderer coerces with
String(col.id)" was true — at the i18n lookup inlocalizeColumn— and false at the site that decides lane membership. ⛔ Not "I was wrong", but "I was right about a different line than the one that matters." That is the useful shape of a correction, and it is now deleted from the docblock, the changeset and both doc pages rather than quietly patched.Q1 — homogenising
columns: disposition A, as shippedstring[] | Lane[]on both faces, so a mixed array is refused. Its own probe of the object-first mix returneddone:r2, undefined:, __uncolumned__:r1— a blank lane whose own keys are['0','1','2','3','cards'], the string spread character by character.I asked the dev to make the narrowness judgement itself rather than take mine, and I adopt its reasoning: the protocol's
describeprose reads as two array shapes ("{id,title}pergroupByvalue, or bare value strings") and names no mixed example; the renderer cannot dispatch a mix in either order; and declaring a mix valid would be the same declared-but-not-honoured defect this card exists to remove, one layer up.⛔ B rejected for the reason the dev gave: it leaves the published declaration saying a mixed array is valid while the pin says it is broken — two carriers disagreeing about the same shape is precisely the state this repo punishes elsewhere. ⛔ C rejected: it inverts the ruled/unruled order the review just warned about on objectui#8990. The exit is recorded in the member docblock — a later ruling reading that prose as a per-element union makes it an objectstack spec card, and the member follows it.
Q2 — objectui#8993's unmeasured half: B eventually, ⛔ not a blocker here
Adopted as recommended. ⭐ And this is the more important half of that card:
⇒ this PR's declaration closes the authored path only. Neither the dev nor the review measured whether numeric picklist option values occur in real metadata, and both said so. objectui#8993 names the gap explicitly so whoever takes it starts from the unmeasured half rather than rediscovering it. ⛔ Not a condition on this PR.
R4 — the counts moved, and for the right reason
Ablation 14 failed / 23 passed of 37 (was 11/21 of 32) — all 13 REFUSES rows red, all 13 CONTROL rows green. Counterfactual A went 7 failures → 5 ⭐ precisely because the numeric-id and mixed-array rows left the ACCEPT set — which is the count I asked to see move, moving for the stated reason rather than drifting. Counterfactual B reproduces 13
error TS= 8× TS2741 + 5× TS2322, matching the review's independent count exactly.Every leg carries a rebuild and a dist-marker reading in both directions, because of the false-green mechanism this card surfaced (
tsconfig.test.jsonsets an emptypaths, so cross-package type assertions resolve through the built declaration). ⛔ An ablation onpackages/typeswithout a rebuild leg measures the old artifact.Next
in_progressat 16:16Z) and the PR readsbehind. ⛔ Thebehindwill not be hand-cleared — this repo's merge queue rebuilds each PR on currentmainitself, and hand-merging base earlier today moved a head and cost a full review round on objectui#8934.needs:contract-reviewstays hung on both carriers. The delta review is dispatched against this head; ⛔ nothing lands until a 同形 PASS record sits on it and every check is terminal and green.Generated by Claude Code
os-warren commentedon Sep 10, 2026
Clause-② carriers cleared — PASS on record
PM seat (
domain:spec@ objectui, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10.Review record:
5622034134—## Contract review, round 2 (delta),VERDICT: PASS, judged head5490145e75690d44af710109938b24e467352095. Supersedes the round-1 REWORK at5621251369on the earlier head. Tier: 202 harnessmodelstamps, one distinct value,claude-fable-5-1=CONTRACT_REVIEW_TIER.Independence pair:
SELF-REVIEW — both are subagents of the one dispatching session. ⛔ Recorded as such, never an independent clearance.
Landing pre-checks, re-run by this seat: ① the PASS record above, on this head ✅ · ②
--pair 8989rc 0 with the current script ✅ · ③ 34 check runs, terminal: 31 success, 3 skipped, 0 non-green — every check, ⛔ not a required subset ✅.needs:contract-reviewstripped from both carriers in the same stroke as this comment.⭐ The reviewer waited rather than judged
It stopped mid-round with an interim status rather than rule ③ on a partial reading, then polled the check-runs API every 30s until
pending=0and kept the final JSON. Last to land wasTest (shard 2/4)at 16:29:03Z — 2m12s after the point at which the earlier reading would have been taken. ⛔ That is the discipline the previous round on objectui#7997 was missing, where a dev reported green 2m36s before its own shard 2 went red.The precedent for the union shape — verified here by content, ⛔ not by the cited line
The record cites⚠️ Its line anchors had already moved by the time I checked (
ObjectGridSchema.columnsas an in-house precedent forstring[] | Lane[].origin/mainadvanced between the two reads), so I re-derived it:Negative control: a name that cannot exist reads 0 in the same file. ⇒ the precedent is real and stronger than cited — two zod instances, not one — and it is exactly the union-of-arrays shape this PR adopted, ⛔ not an array of a per-element union.
What the round settled
id: numberdropped, with the mechanism the dev derived and the reviewer verified against source: the injection map readsgroups[col.id](JS coerces the property key) while the leftover sweep buildsknownIdsfrom the raw id and filters stringObject.keys—new Set([1]).has('1')isfalse.error TS(8× TS2741 + 5× TS2322 on the same five fixtures), then restore.Carried forward, ⛔ not folded in
id: opt.valuestraight from picklist options, which no schema guards, so numeric option values in real tenant metadata hit the same defect with no authored document involved. Neither the dev nor the reviewer measured whether those occur. Triaged elsewhere todomain:ui.groupByrequired here vs optional in the protocol, which is what makes the bare-string arm unreachable. ⛔ Explicitly not blocked on: a ruled card must not stack behind an unruled widening.Generated by Claude Code
os-warren commentedon Sep 10, 2026
Release:branchclaude/issue-8913-object-kanban-columns-declared· PR #8989 MERGED as8693b8569d183f197815468da24054a040b3ec01— verified on the tree, ⛔ not from an API field.Landing established by⚠️ Field gap, ninth sample: commit
git merge-base --is-ancestor→ rc 0 in a fresh clone, with the reversed probe as a negative control → rc 1.2026-09-10 16:32:49Zvsmerged_at 2026-09-10T16:50:26Z— 17m37s. Today's nine merges span 17m31s–17m44s.Verified by BEHAVIOUR on
origin/main, not only by the commit being present⇒ the union of two array shapes, ⛔ not an array of a per-element union — so a mixed array is refused. And the
numberarm is gone, with the mechanism written into the declaration itself:⭐ A declaration that says why it refuses and names the carrier card. Anyone who later wonders whether
numberwas an oversight finds the answer at the refusal.What this card actually landed vs what its title proposed
⛔ The title was falsified and is not what shipped.
columns: KanbanColumn[]would have refused the protocol's own gate-validated example, the renderer, and this repo's typed corpus — proved by counterfactual at 13error TS(8× TS2741 + 5× TS2322) and 7 zod failures, reproduced independently by the review.KanbanColumnrequirescards; it is the runtime lane, filled bybucketCardsIntoColumnsbeforeKanbanImplsees one.What shipped is the protocol's union: a bare value string, or a lane object with
id/titleand optionalcards/limit/className/collapsed— the six members the boards read.In-house precedent, re-derived by this seat by content after the review's line anchors had already moved (
origin/mainadvanced between the two reads):objectql.ts:684columns?: string[] | ListColumn[], and two zod instances atzod/objectql.zod.ts:228and:651. Negative control 0. ⇒ the shape is the house idiom, not an invention.Review trail
Round 1 REWORK (
5621251369) — caught two arms that were broken at runtime while the pin was about to freeze them as correct. Round 2 PASS (5622034134) at 202 harness stamps, one distinct value. Both declared SELF-REVIEW.⭐ Two behaviours worth carrying forward:
String(col.id)" was true at the i18n lookup and false at the site that decides lane membership. ⛔ Not "I was wrong" but "I was right about a different line than the one that matters."pending=0. The last check landed 2m12s after the earlier reading point. The inverse failure had happened hours before on objectui#7997, where a dev reported green 2m36s before its own shard went red.id: opt.valuestraight from picklist options, which no schema guards. Whether numeric option values occur in real tenant metadata is unmeasured by everyone and is the card's more important half.groupByrequired here vs optional in the protocol, which is what makes the bare-string arm unreachable. ⛔ This PR deliberately did not block on it: a ruled card must not stack behind an unruled widening.object-kanban.columnsstays in the console'sMEMBER_PIN_EXEMPTIONS— objectui#8071's renderer-side member pin is a different obligation, and no pin was registered here.Generated by Claude Code