Skip to content

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

Closed

Description

@os-warren

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, through columns: 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-kanban document changes meaning: it was never judged by the kanban arm. 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.

⚠️ Direction check the implementing seat owes. 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[].items now parses green (it is not a declared member of a lane, and nothing refuses it);
  • a card with no title now 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

  1. The two schema-catalog fixtures needed groupBy and data: [] added during the retirement, because the object-kanban face refuses a purely static board (lanes carrying their own cards, no groupBy, no record source). Their rendered DOM is byte-identical afterwards — the sha256/tag/element pins in kanban-column-cards-6939.test.tsx pass 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 shape columns[].cards judging is about.
  2. SchemaRegistry (packages/types/src/registry.ts) has never carried an object-kanban entry, so with 'kanban' retired the kanban family has no entry in that map at all and ComponentType no longer offers any kanban node type. ⛔ Adding one widens ComponentType and 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

  1. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Unblocking measurement — the "widening" premise is falsified by the protocol

    PM seat (domain:spec, session session_01Jmxdo7bmeqCQHLSfmLVX9w). This card was held pm:blocked on the reading that declaring columns widens the object-kanban accept 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):

    我们的项目以 objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。

    The reading

    objectstack-ai/objectstack HEAD eabdd66f45f402eba0f8404a8a9de4a501fc83a6, packages/spec/src/ui/component.zod.ts:

    • :2656 export const ObjectKanbanPropsSchema = lazySchema(() => strictObject({ … })), bound at :3069 — 'object-kanban': ObjectKanbanPropsSchema.
    • :2670-2671:
      columns: z.array(z.unknown()).optional()
        .describe('Swimlane definitions ({ id, title } per `groupBy` value, or bare value strings) — NOT a field projection'),

    ⇒ columns is 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. Moving pm:blocked → pm:queue.

    ⚠️ But the shape is now a constraint, not a free choice

    The protocol's element type is z.unknown() and its describe states the shape in prose, as a union:

    { id, title } per groupBy value, or bare value strings

    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:

    1. a KanbanColumn that 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;
    2. a reading of what the ObjectUI renderer actually accepts today, so the declaration matches the reader rather than the card's title;
    3. ⛔ no narrowing of the zod twin below z.array(z.unknown()) without a spec card first.

    ⭐ The second held key is a different animal — object-kanban.dataSource

    The other object-kanban entry in MEMBER_PIN_EXEMPTIONS is dataSource. Measured: dataSource is not a member of ObjectKanbanPropsSchema at all. It is the shared component-level binding (PageComponentSchema.dataSource), which the kanban docblocks reference as the thing that can supply the object instead of objectName (:2668, :2681, :2695).

    That matters for objectui#8071's ledger: SchemaRenderer strips dataSource before 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). So object-kanban.dataSource is 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

    • Whether the bare-string arm of columns is authored anywhere in the protocol's own corpus or in this repo.
    • What ObjectUI's object-kanban renderer accepts for columns today.
    • Whether any other MEMBER_PIN_EXEMPTIONS entry is a cross-block binding misfiled as a block member.

    Generated by Claude Code

  2. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Claim: session session_01Jmxdo7bmeqCQHLSfmLVX9w (PM seat domain:spec, dispatching os-dev) · branch claude/issue-8913-object-kanban-columns-declared
    Clause-②: yes

    ⚠️ Corrected 2026-09-10 by this seat, after the dev delivered. This claim first declared Clause-②: no on the reading that declaring columns merely mirrors the protocol. check-clause2-carriers.mjs --pair 8989 exits 4 on that pairing (row C5): the diff carries 7 T1 widening tells — six new keys on the new lane zod object plus columns itself (packages/types/src/zod/objectql.zod.ts:1261) — against a no declaration. The dev hung needs:contract-review on both carriers by its own judgement of the diff, which was the right call and which no contradicted. ⛔ A provisional declaration is not a verdict, and the dev's diff outranks it. Re-declared yes; the label and the declaration now say the same thing.

    ⭐ The blocker is gone — measured, not argued

    This card sat pm:blocked on the reading that declaring columns widens the object-kanban accept set and therefore needs a maintainer ruling. It does not. Full measurement at 5618083792; the short form:

    objectstack-ai/objectstack HEAD eabdd66f45f402eba0f8404a8a9de4a501fc83a6, packages/spec/src/ui/component.zod.ts:2670-2671:

      columns: z.array(z.unknown()).optional()
        .describe('Swimlane definitions ({ id, title } per `groupBy` value, or bare value strings) — NOT a field projection'),

    inside ObjectKanbanPropsSchema (:2656), bound at :3069. The protocol already declares columns. Declaring it in objectui is mirroring, ⛔ not widening. Clause-②: no follows 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 its describe states the shape in prose, as a union:

    { id, title } per groupBy value, or bare value strings

    ⇒ 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:

    我们的项目以 objectstack 协议为准,文档应该以实际实现为准。协议不正确的应该先修改协议。

    (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:

    1. Read what the objectui renderer accepts today — measured, from the code, not from this card's title. The declaration must match the reader, and the card's title (columns: KanbanColumn[]) is a proposal, ⛔ not a finding.
    2. A KanbanColumn that 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.
    3. ⛔ Do not narrow the zod twin below 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.
    4. If any step forces you to choose between two defensible contracts, STOP and put it in open_questions rather 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's MEMBER_PIN_EXEMPTIONS, is not a member of ObjectKanbanPropsSchema at all. It is the shared component-level binding (PageComponentSchema.dataSource), which SchemaRenderer strips before spreading a node's keys as React props. The kanban docblocks reference it only as the thing that can supply the object instead of objectName (: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

    • ⛔ Never edit a shared primary checkout. /home/user/objectui is shallow with a stale origin/main — ⛔ never git fetch / git checkout there, ⛔ never cut a worktree from it. /home/user/objectstack is 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.
    • ⛔ Never git stash (refs/stash is shared across worktrees — your pop takes another agent's entry and reports success). Use a patch or a wip commit.
    • ⛔ Never kill a process by name; only a PID you recorded. ⛔ Never mutate anything under node_modules. ⛔ Never edit content/docs/releases/ — your input to release notes is the changeset.
    • ⛔ Never write the assignee and post no second Claim:; both are set for you above.
    • Capture every rc to a file before any pipe. Read lint logs for the literal word error, ⛔ 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.
    • ⛔ PR stays draft — no auto-merge, no enqueue, no merge, no flipping ready. Landing is this seat's act.
    • GitHub content in English (quote maintainer rulings verbatim, ⛔ untranslated).

    Generated by Claude Code

  3. self-assigned this
    on Sep 10, 2026
  4. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    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

  5. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Seat dispositions — the claim is corrected, and this card's TITLE is falsified

    PM seat (domain:spec @ objectui, session session_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10. Answering PR #8989's open_questions.

    ⛔ The blocking one — Q1, disposition A, done

    check-clause2-carriers.mjs --pair 8989 exited 4 (row C5): my claim comment declared Clause-②: no while the diff carries 7 T1 widening tells — six new keys on the new lane zod object plus columns itself (packages/types/src/zod/objectql.zod.ts:1261).

    Re-declared yes in the claim at 5619585748, with the correction visible in place. Gate re-run: rc 0, "both carriers agree."

    ⭐ The dev hung needs:contract-review on both carriers by its own judgement of the diff, overriding my provisional no. That was correct and my no was wrong. A provisional declaration only sets the conservative direction for dispatch tiering; it is ⛔ not a verdict, and the diff outranks it. ⛔ Option B (keep no + 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[]. KanbanColumn requires cards — it is the runtime lane, filled by bucketCardsIntoColumns before KanbanImpl ever sees one. As the authoring element it refuses:

    • 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;
    • the renderer, which reads col.cards || [] on both legs and materializes cards-less lanes from a picklist;
    • this repo's own typed corpus, which authors columns: [{ id, title }] under satisfies 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 optional cards / 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[].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 — the very thing the protocol forbids requiring. items stays accepted-and-dropped, and that verdict is now pinned.

    Q2 — the color tombstone: disposition A (leave it off)

    Adopted as the dev shipped it, and for its reason: carrying objectui#7664's retiredDeclarativeKanbanKey tombstone 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 (a schema-reference.md example teaching color on kanban columns) 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 because SchemaRenderer renders from the raw document via the structural validateSchema, ⛔ 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 across KanbanImpl, 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.ts resolves through the built .d.ts. An ablation on a packages/types declaration 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 touches packages/types needs a rebuild leg, or its green is not a reading.

    Next

    needs:contract-review stays 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

  6. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    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

  7. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Seat dispositions — rework round accepted, both open questions answered

    PM seat (domain:spec @ objectui, session session_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10. Head 5490145e75690d44af710109938b24e467352095.

    ⭐ 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 number arm is broken" on the reviewer's word — it ran its own probe against the real bucketCardsIntoColumns and got 1:r1, 2:r2, __uncolumned__:r1+r2 against a clean one:r1 string 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 builds knownIds from the raw col.id and filters Object.keys(groups), which are strings. new Set([1]).has('1') is false. 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 in localizeColumn — 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 shipped

    string[] | Lane[] on both faces, so a mixed array is refused. Its own probe of the object-first mix returned done: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 describe prose reads as two array shapes ("{id,title} per groupBy value, 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:

    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.

    ⇒ 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.json sets an empty paths, so cross-package type assertions resolve through the built declaration). ⛔ An ablation on packages/types without a rebuild leg measures the old artifact.

    Next

    ⚠️ CI is mid-flight at this head (6 of 34 still in_progress at 16:16Z) and the PR reads behind. ⛔ The behind will not be hand-cleared — this repo's merge queue rebuilds each PR on current main itself, and hand-merging base earlier today moved a head and cost a full review round on objectui#8934.

    needs:contract-review stays 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

  8. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Clause-② carriers cleared — PASS on record

    PM seat (domain:spec @ objectui, session session_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10.

    Review record: 5622034134 — ## Contract review, round 2 (delta), VERDICT: PASS, judged head 5490145e75690d44af710109938b24e467352095. Supersedes the round-1 REWORK at 5621251369 on the earlier head. Tier: 202 harness model stamps, one distinct value, claude-fable-5-1 = CONTRACT_REVIEW_TIER.

    Independence pair:

    Implemented-by: claude/issue-8913-object-kanban-columns-declared (mode:subagent)
    Reviewed-by: session_01Jmxdo7bmeqCQHLSfmLVX9w (contract-review subagent agent-a46660e37d5cfc529, context-isolated)
    

    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 8989 rc 0 with the current script ✅ · ③ 34 check runs, terminal: 31 success, 3 skipped, 0 non-green — every check, ⛔ not a required subset ✅.

    needs:contract-review stripped 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=0 and kept the final JSON. Last to land was Test (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 ObjectGridSchema.columns as an in-house precedent for string[] | Lane[]. ⚠️ Its line anchors had already moved by the time I checked (origin/main advanced between the two reads), so I re-derived it:

    packages/types/src/objectql.ts:684        columns?: string[] | ListColumn[];
    packages/types/src/zod/objectql.zod.ts:228  columns: z.union([z.array(z.string()), z.array(ListColumnSchema)]).optional()…
    packages/types/src/zod/objectql.zod.ts:651  columns: z.union([z.array(z.string()), z.array(ListColumnSchema)]).optional()…
    

    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: number dropped, with the mechanism the dev derived and the reviewer verified against source: the injection map reads groups[col.id] (JS coerces the property key) while the leftover sweep builds knownIds from the raw id and filters string Object.keys — new Set([1]).has('1') is false.
    • Mixed arrays refused on both faces and in both orders. The dev made the narrowness judgement itself, as asked; the reviewer upheld it and neither overturned this seat's disposition A.
    • Counts moved for the stated reason: counterfactual A went 7 → 5 failures because the numeric-id and mixed-array rows left the ACCEPT set. Counterfactual B reproduces the false green end to end — dist 1/0 with 0 errors, rebuild 0/1, then 13 error TS (8× TS2741 + 5× TS2322 on the same five fixtures), then restore.
    • ⭐ An honesty note in the record worth keeping: on the zod legs, dist is not on the pin's read path, so the dev's dist markers there are belt-and-braces rather than load-bearing. The reviewer said so instead of counting them as evidence.

    Carried forward, ⛔ not folded in

    • objectui#8993 — the bucketer double-buckets any non-string lane id. This PR closes the authored path only; ⚠️ the lanes the renderer materialises itself take id: opt.value straight 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 to domain:ui.
    • objectui#8990 — groupBy required 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.
    • One non-blocking nit: an intro line in the docs lacks the strict-twin qualifier.

    Generated by Claude Code

  9. os-warren commented on Sep 10, 2026

    @os-warren
    CollaboratorAuthor

    Release: branch claude/issue-8913-object-kanban-columns-declared · PR #8989 MERGED as 8693b8569d183f197815468da24054a040b3ec01 — verified on the tree, ⛔ not from an API field.

    Landing established by git merge-base --is-ancestor → rc 0 in a fresh clone, with the reversed probe as a negative control → rc 1. ⚠️ Field gap, ninth sample: commit 2026-09-10 16:32:49Z vs merged_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

    packages/types/src/zod/objectql.zod.ts:1277
      columns: z.union([z.array(z.string()), z.array(ObjectKanbanLaneSchema)]).optional()
    

    ⇒ the union of two array shapes, ⛔ not an array of a per-element union — so a mixed array is refused. And the number arm is gone, with the mechanism written into the declaration itself:

    ObjectKanbanLaneSchema.id: z.string().describe(
      'Lane id — matched against the groupBy value. STRING only: the bucketer builds knownIds from the
       raw col.id and compares it with Object.keys(groups), which are strings, so a numeric id buckets
       every card TWICE (objectui#8993)')
    

    ⭐ A declaration that says why it refuses and names the carrier card. Anyone who later wonders whether number was 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 13 error TS (8× TS2741 + 5× TS2322) and 7 zod failures, reproduced independently by the review. KanbanColumn requires cards; it is the runtime lane, filled by bucketCardsIntoColumns before KanbanImpl sees one.

    What shipped is the protocol's union: a bare value string, or a lane object with id / title and optional cards / 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/main advanced between the two reads): objectql.ts:684 columns?: string[] | ListColumn[], and two zod instances at zod/objectql.zod.ts:228 and :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:

    • The dev re-measured the review's own finding before acting on it, and produced the mechanism neither the review nor this seat had. It then diagnosed its own first-round misread exactly: "the renderer coerces with 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."
    • The reviewer stopped mid-round rather than judge ③ on a partial CI reading, then polled to 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.

    ⚠️ Carried forward

    • objectui#8993 — the bucketer double-buckets any non-string lane id. This PR closes the authored path only; the lanes the renderer materialises itself take id: opt.value straight 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.
    • objectui#8990 — groupBy required 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.columns stays in the console's MEMBER_PIN_EXEMPTIONS — objectui#8071's renderer-side member pin is a different obligation, and no pin was registered here.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions