Repository navigation
test(plugin-kanban): characterize the cross-column landing position (objectui#8826 Q3) - #8975
Merged
os-steve merged 1 commit intoSep 10, 2026
Conversation
…objectui#8826 Q3)
Answers the third, explicitly unmeasured question on the card: on a
cross-column move, is the landing POSITION within the target column also
lost? It is, on both data ownerships.
`KanbanImpl.handleDragEnd` computes a landing index, splices the card in at
it, and hands the index to `onCardMove`. `ObjectKanban.handleCardMove`
discards it (`void _newIndex`) and `persistCardMove` writes a body of exactly
the groupBy column. The index reaches a parameter and stops there.
The two ownerships differ in WHEN the loss becomes visible, which the card did
not anticipate:
- internal data: the optimistic write re-renders the board, which re-buckets
and re-syncs the mirror from the prop, so the card leaves the dropped slot
immediately -- a visible snap, not a later silent revert.
- external data: the optimistic write is skipped, nothing re-renders, and the
dropped slot survives on screen until the parent's next reflow.
Characterization only -- zero behaviour change. The recorder captures the
`columns` prop on every board render, driven through the real `onDragEnd`, and
carries both a lit positive control (the drop does produce a new snapshot,
with the membership change in it) and a dead negative control (a drag that
ends on nothing records neither snapshot nor write).
The changeset declares an empty frontmatter: the package publishes `dist` only,
so a file under `src/__tests__/` releases nothing.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-steve
marked this pull request as ready for review
September 10, 2026 13:32
os-steve
deleted the
claude/issue-8826-crosscolumn-landing-position-probe
branch
September 10, 2026 13:50
akarma-synetal
pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Sep 17, 2026
…nowhere (objectui#8826) (objectstack-ai#9640) Fixes objectstack-ai#8826 Ruled on the card as batch objectstack-ai#141 item 3, letter **B**, maintainer 「同意」: the board stops claiming a capability it does not have. The fence the ruling asked this body to carry, verbatim: 「order persistence is not a capability of this board; a spec card would be its entry」. ## What changed `KanbanImpl`'s `handleDragEnd`: - **Same-column drop** — the branch no longer reorders local state. The card returns to its position. Nothing is persisted, so nothing is shown as persisted. - **Cross-column move** — the membership change stays and is still persisted through `persistCardMove`. The landing index is still computed and still handed to `onCardMove`, where it is still discarded; what changed is that it is no longer *painted*. The branch appends to the target column instead of splicing at the slot the card was released over, so the board claims membership and says nothing about position. No new prop, no new persistence path, no new authorable key — `Clause-②: no` holds, and this is a strict narrowing of a claim rather than a widening of any surface. Two README bullets advertised in-column reordering as a feature. They now say what is true. Letter C was refused for formalising the false success; leaving it in the docs would have been letter C by the back door. ##⚠️ One mechanism claim in the ruling was falsified — the file, not the mechanism The ruling's execution note reads 「`ObjectKanban.tsx`: the same-column branch of `handleDragEnd` …」. Re-derived by symbol (`git grep -nw handleDragEnd`): **`ObjectKanban.tsx` has no `handleDragEnd`**. The drag handler and the optimistic reorder live in `KanbanImpl.tsx`; `ObjectKanban.tsx` owns `handleCardMove`, whose `_newIndex` parameter and `void _newIndex` discard are exactly where the ruling says they are and are **unchanged by this branch**. Everything the ruling asked for is done, one file over from where it says. `KanbanImpl`'s mirror-sync timing (objectui#8534 / PR objectstack-ai#8825) is untouched. The mirror itself stays — the cross-column optimistic write still needs it, which is now what defends it in that card's pin. ## The two pins — both legs, in one file `packages/plugin-kanban/src/__tests__/sameColumnDropIsNotClaimed-8826.test.tsx`, on the recorder objectui#8826 was measured with (a module mock of `KanbanImpl` capturing the `columns` prop per render, plus a capture of the board's real `onDragEnd`): - **LEG 1** — a same-column drop leaves DOM order equal to the DATA order, on **both** data ownerships, records no new `columns` snapshot and reaches no `update`. - **LEG 2, the lit control** — a cross-column move still emits the new membership snapshot and still writes it, with an exact-body assertion (`update('task', 'a', { status: 'in_progress' })`). Without this leg, LEG 1 would read green on a board whose drag path was broken outright. The fixture puts two cards in the source column and asks for their reverse order, so "did not move" is distinguishable from "nothing was driven". ## Reverse verification — two arms, mutation proven on disk, restore proven by hash Run from the committed fix (`1f1ec56`); every arm restores with `git checkout HEAD --` under a trap with absolute paths, and each restore is verified by the blob hash plus an empty `git diff HEAD`, never by an exit code. | arm | mutation | result | |---|---|---| | **same-column** | re-add the optimistic reorder this card removed | **exit 1 — 3 failed / 5 passed.** The failures are exactly LEG 1 (internal), LEG 1 (external) and the sibling control in `columnsReachDomSameCommit-8534`. **LEG 2 stayed green**, so the instrument distinguishes the defect from a dead board. | | **cross-column** | splice at the drop index again | **exit 1 — 1 failed / 9 passed.** The single failure is the external-path assertion in `crossColumnLandingPosition-8826`; LEG 1 and everything else stayed green. | `KanbanImpl.tsx` HEAD blob `5cd6f3b0eaf34acafa223271f8619b4de09366e1`; mutated blobs `7a93f0098ec53e68f839bf8fb4fc8823040b3575` and `3f9b95415364fc026770ba297b4c30a76f2c3dea`; restored blob reproduces the HEAD blob byte for byte on both arms, and the tree was verified clean afterwards. The mutation scripts assert their anchor is present exactly once before writing, so a no-op edit aborts instead of passing as a run. ## Verification All run at `1f1ec56`, exit codes captured to files before being read, never through a pipe. Heavy runs serialized through the shared verify lock with `OS_VERIFY_LOCK_SLOT=objectui-8826`, verdicts read from its printed `VERDICT command-exit` line. | check | exit | reading | |---|---|---| | `pnpm exec vitest run packages/plugin-kanban/` (repo ROOT) | 0 | 56 files / 358 tests passed | | `pnpm --filter @object-ui/plugin-kanban type-check` | 0 | both legs — `tsc --noEmit` and `tsc -p tsconfig.test.json`, the second being what covers the test files | | `pnpm --filter @object-ui/plugin-kanban lint` | 0 | 0 errors; this diff contributes 0 of the package's 216 pre-existing warnings | | `check:changeset-presence` | 0 | 1 changeset declared for 5 published source files across 2 packages | | `check:doc-snippets` · `check:doc-examples` · `check:readme-exports` | 0 · 0 · 0 | these three read package READMEs, and all three first exited non-zero as PREREQUISITE NOT MET; re-run to a real 0 after the scoped build the gates name | | `check:doc-types` · `check:doc-fences` · `check:doc-example-ids` | 0 | — | | `check:control-bytes` · `check:new-line-citations` · `check:test-path-roots` · `check:unreferenced-sources` | 0 | — | | `check:vi-mock-specifiers` · `-inherit` · `-override-shape` | 0 | run because this diff adds two `vi.mock` call sites | | `check:changeset-claims` · `check:changeset-no-major` · `check:handler-key-reads` | 0 | — | | `check-governed-queue-guard --test` | 0 | NOT GOVERNED — 4 paths checked against 5 governed surfaces, none matched | A dependency-closure build (`--filter '@object-ui/plugin-kanban^...' build`) and the doc gates' own 35-task scoped build were both run to a real 0 first; the type-check and the three README/doc gates were failing only for want of them. ## Acceptance notes - **Q3 was already answered, contrary to the brief.** The dispatch carried the card's question 3 as unmeasured. It was answered and merged on 2026-09-10 by PR objectstack-ai#8975, whose characterization pin is in the tree. Re-derived rather than cited: that instrument was run on the pristine base at the start of this work and passed, so the reading stands — the cross-column landing position is lost on both ownerships. The fix surface is therefore the wider one, and this branch covers both halves. - **What "appended" is, and is not.** The board cannot compute the target column's record-order position from the `columns` prop alone: each column's cards are a record-ordered subsequence, and nothing carries their global interleaving. Appending is therefore the only slot the board can offer without claiming to know where the round-trip will sort the card — it is not itself a promise that the card will stay last. On the ownership that re-renders, the prop-driven re-sync corrects it inside the same commit; on the external ownership, the card sits at the end of the target column until the parent reflows. What is removed is the claim that the user's drop slot was honoured.⚠️ The objectstack-ai#8975 fixture cannot distinguish "appended" from "record order" — its moved card is last in the record array either way — and its assertion now says so in place. - **noted, not filed — `KanbanEnhanced.tsx` carries a byte-for-byte twin of the removed reorder.** Left alone deliberately: it has zero non-test importers, its registry key was retired by objectui#8257, and the package `exports` publishes only `.` and `./style.css`, so it is unreachable from outside this package. Dead code is not a fileable class. Successor: none — no open PR touches this package. - **noted, not filed — the exported `useQuickAddReorder` hook keeps its own local-order state.** It is a published export with no in-repo consumer and is not on this board's drag path. Successor: none. - **Deviation from the claimed file surface, declared.** The claim named `packages/plugin-kanban/src/`. This branch also touches `packages/plugin-kanban/README.md`, `packages/components/src/renderers/complex/README-KANBAN.md` and `.changeset/`. The changeset is mechanically required. The two README bullets are the docs half of this very behaviour change (AGENTS.md commandment objectstack-ai#2), and each was a one-line claim of the exact capability the ruling removes; shipping them unchanged would leave the board advertising in-column reordering in print. --- _Generated by [Claude Code](https://claude.ai/code/session_012EpHzwH4wTy5sd7ibkD2yq)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #8826 — answers that card's question 3 only. The card stays OPEN.
What this is
objectui#8826 measured that a SAME-column reorder is persisted nowhere. Its third question — on a CROSS-column move, is the landing POSITION within the target column also lost? — was filed explicitly unmeasured, with the instruction to answer it before touching anything, because the fix surface depends on the answer.
This PR is that measurement, frozen as a characterization test.
Zero behaviour change. No persistence added. No optimistic update removed.
KanbanImpl's mirror-sync timing untouched — that belongs to objectui#8534 / PR #8825 and has already landed. The card's questions 1 and 2 are deliberately not pre-empted; they stay with the maintainer.The answer: the position is lost too — on BOTH data ownerships
KanbanImpl.handleDragEnd's cross-column branch does compute a landing index, does splice the card in at it, and does hand that index toonCardMoveas its fourth argument.ObjectKanban.handleCardMovereceives it as_newIndexand discards it outright (void _newIndex);persistCardMovewrites a body of exactly thegroupBycolumn. The index reaches a parameter and stops there — no sink, no write key, no storage.⭐ The part the card did not anticipate: the two ownerships answer differently
columnssnapshot after the dropbacklog: OR in_progress:Xray,Yankee,Alpha— record orderact()data, e.g. ListView)The optimistic write that re-renders the board is itself gated on ownership. On the internal path it fires,
KanbanRendererre-buckets, thecolumnsidentity changes and the mirror re-syncs — so the loss is a visible snap on the spot, which is worse than the delayed silent revert the card describes for the same-column case. On the external path the optimistic write is skipped, nothing re-renders, and the local move stands until the parent reflows — that is the card's silent-revert signature exactly.⇒ Position is lost on both. They differ only in when the user sees it happen.
(The table's column-order cells are written with the word
ORwhere a vertical bar would be, because a bar is the markdown table cell separator. The test's own constants use the bar.)The readings
Fixture is built so the two orders cannot coincide: the moved card is last in the record array and is dropped first in the target column. A fixture where the dropped card also happened to sort first would read green either way.
Records:
Xray(in_progress),Yankee(in_progress),Alpha(backlog). DropAlphaontoXray= slot 0 of "In Progress".backlog:Alpha/in_progress:Xray,Yankeein_progress:Alpha,Xray,Yankee→ never recorded, on either pathin_progress:Xray,Yankee,Alpha; DOM['Xray','Yankee','Alpha']['Alpha','Xray','Yankee']update('task', 'a', { status: 'in_progress' }), exact body, no positional keyLit positive control (the card's own): the cross-column drop does produce a new
columnssnapshot carrying the membership change ⇒ the instrument is connected, so "the drop order never appears" is a reading and not a dead wire.Dead negative control: a drag that ends on nothing records 0 snapshots and makes 0 writes ⇒ the instrument does not fire spuriously.
Ablation — proving the pin discriminates
A characterization pin is green on
mainby construction, so its value is that it goes RED when the behaviour moves. Mutation applied at the discard anchor inObjectKanban.tsx: honour the index by hoisting the record to the front of the source array when the card was dropped into slot 0. Deliberately crude — not a candidate fix, only enough of a behaviour change to prove the pin can see one.Run against
513fe387d.ObjectKanban.tsx2a6d42aac9aca111797d4ab4386cf28df46f50ed33c2b6f2217c407b7565691c7140ad1bff33a2c12a6d42aac9aca111797d4ab4386cf28df46f50edRestore proven two ways, not by an exit code: the on-disk blob hash reproduces the HEAD blob byte for byte, and
git diff HEADis empty. The mutation was proven to reach the disk before the RED arm ran (anchor counts and a changed blob hash), so a no-op edit could not have been mistaken for a passing run.The two failures are exactly the two measurement assertions; the two controls, the external-path asymmetry and the wire assertion stay green — the mutation only moves internal-path ordering. That failure profile, not just the red, is what says the pin is aimed at the right thing.
Gates
pnpm --filter @object-ui/plugin-kanban type-check(both legs:tsc --noEmit+tsconfig.test.json)pnpm --filter @object-ui/plugin-kanban lintpnpm exec vitest run packages/plugin-kanban/(from repo ROOT)check-control-bytes.mjscheck-changeset-presence.mjscheck-governed-queue-guard.mjs --test(both paths)check:doc-typescheck:doc-snippetscheck:doc-examplescheck:vi-mock-specifiers/-inherit/-override-shapeThe
vi-mocktrio is not from the dispatched list — this diff adds twovi.mockcall sites, and those three gates read exactly that, so they were run as implicated.The doc gates first exited 2 (
PREREQUISITE NOT MET, packages unbuilt); the scoped build the gate names was run and both were re-run to a real 0. Exit 2 was not read as a pass.Changeset carries an empty frontmatter: the package publishes
distonly, so a file undersrc/__tests__/releases nothing. That is the gate's own documented pass for a test-only change, not a workaround.Acceptance notes
scripts/check-doc-example-types.mjshas no CI job ("0 hits across all of.github/andturbo.json"). It has one:.github/workflows/doc-snippet-types.ymlrunsnode scripts/check-doc-example-types.mjsas a real job step, onpull_request,push,merge_groupandworkflow_dispatch. The gate's own workflow filename does not carry its name, which is the likely reason a filename-shaped search missed it; that same workflow's header even documents the gate as a sibling. Consequence: the "nothing in CI will tell you" warning attached to theUNGATED_EXAMPLESledger is wrong — CI does tell you. Noted, not filed: this is a correction to a dispatch brief, not a repo defect, and no PR or file carries it. Successor: none.ObjectKanban.tsx:1081-1083; on today'smainit is at:1224-1226. Re-derived by anchor, as instructed. The mechanism is unchanged:_newIndexis a declared parameter,void _newIndexis the discard, andhandleCardMoveearly-returns onfromColumnId === toColumnId. Noted, not filed — line numbers in an issue body are not a defect.packages/plugin-kanban/at all, PR chore(deps): lucide-react 1.31.0 -> 1.43.0, with the one retired spelling repaired #8941, and only itspackage.json(a dependency bump). No open PR touches this package's source or tests. No overlap.columnsReachDomSameCommit-8534.test.tsxsnapshots the DOM per commit at theKanbanImplseam, a different instrument, and it never drives a cross-column drop. Thecolumns-prop recorder was therefore built here as a module mock, matching the card's description so the readings are directly comparable.What the maintainer now needs in order to rule Q1 and Q2
The measurement changes the shape of question 2. The card framed today's behaviour as "let the user think it worked, then silently undo it" and offered a snap-back as the honest-but-dumber alternative. On the internal-data path the snap-back already happens — immediately, unprompted, with no animation or message. So:
KanbanImplalready computes and forwards it. Persisting in-column order would need a sink and a storage contract, but it would not need new plumbing to learn where the card was dropped.🤖 Generated with Claude Code
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Generated by Claude Code