Skip to content

bug(plugin-kanban): 同列内拖拽重排**从不被持久化到任何地方** —— 用户看见它动了,下一次 prop 变化时它悄悄退回去 #8826

Closed

Description

@os-zhuang

由 domain:ui @ objectui 派发席开出。证据是 objectui#8534 / PR #8825 为了选机制而做的那次测量的副产品 —— 那次测量本来是问「prop 会不会反映重排」,顺带把这个缺口量清楚了。此前没有任何人量过它。⛔ 未认领。

实测

domain:ui 的 dev 在真实 ObjectKanban 上通过真实 onDragEnd 驱动,用一个记录器捕获板子每次被渲染时收到的 columns prop:

读数 结果
亮对照 —— 跨列移动(已知会变) backlog:Beta | in_progress:Alpha —— 出现了新的 prop 快照 ⇒ 仪器接通
被测 —— 同列重排 prop 快照:backlog: → backlog: → backlog:Alpha,Beta,从不是 backlog:Beta,Alpha。DOM 显示 [Beta, Alpha],update() 被调用 0 次

⇒ DOM 变了,而没有任何东西被通知。

两条独立的代码路径印证该读数(派发席复测其一)

packages/plugin-kanban/src/ObjectKanban.tsx
:1081      _newIndex: number,
:1083      void _newIndex;          ← 顺序索引被显式丢弃
  • handleDragEnd 的同列分支不调用任何回调;
  • handleCardMove 在 fromColumnId === toColumnId 时提前返回,并丢弃 newIndex。

⇒ 卡片顺序的真相只存在于组件本地 state 里。 列成员会回流(那正是点亮上面对照的东西),顺序从不。

后果

用户在一列内把卡片拖到新位置 ⇒ 它当场就动了(乐观更新,这一半是好的)⇒ 但没有任何东西被持久化 ⇒ 下一次 prop 变化(刷新、轮询、任何一次数据回流)会把它退回原样。

⚠️ 形状像数据丢失:用户做了一个动作、看见了它生效、之后它在没有任何提示的情况下消失。⛔ 不报错、不提示、不回滚动画。

⇒ 定级 p2:无数据损坏(顺序本来就没被存过),但用户可见、静默、且违背用户刚刚看到的反馈。⛔ 不抬 p1:没有落库的错值,重排本来就从未被声称会持久化。

⚠️ 这是产品缺口,不是回归

⛔ 长期如此,与 PR #8825 无关。 #8825 刻意原样保留了它 —— 动它正是那张卡的裁决明令围死的「不得为了同帧属性而破坏乐观重排」。⇒ ⛔ 承接者不要把本卡读成 #8825 的后续修复。

待判定(⛔ 派发席不代决)

  1. 同列顺序应不应该被持久化? 若应该,存到哪 —— 一个显式的排序字段?视图级的顺序覆盖?
  2. 若不应该,那么乐观更新就不该发生:今天的行为是「让用户以为成功了,然后悄悄撤销」,而一个当场就弹回、或者根本不允许同列拖拽的界面,虽然更笨但诚实。⚠️ 这一条是本卡真正的分叉点。
  3. 跨列移动会回流成员但不回流顺序 —— 落到目标列的哪个位置,是不是也在丢?(⚠️ 未测,承接者需自己量。)

⚠️ 承接约束

  • ⛔ 不要通过删掉乐观更新来「修」它,除非那正是第 2 问的裁决结果 —— 那会把一个静默缺陷换成一个明显更差的交互。
  • ⛔ 不要顺手动 KanbanImpl 的镜子同步时机:那是 objectui#8534 / PR fix(plugin-kanban): align the columns mirror during render, not in an effect #8825 的,已落地。
  • 先答第 3 问再动手:若跨列的落点顺序也在丢,那么修复面比本卡正文写的宽。

Dedup

⚠️ 派发席没有为本卡跑带亮对照的查重 ⇒ 上面没有「未见重复」的断言,承接者请自行查重。已知相邻:objectui#8534 / PR #8825(证据来源)· objectui#4138(被拒绝移动的回滚)。

来源

objectui#8534 · PR #8825 的 验收备注 第三条与其机制测量 · ObjectKanban.tsx:1079-1083

Activity

  1. self-assigned this
    on Sep 10, 2026
  2. os-steve commented on Sep 10, 2026

    @os-steve
    Collaborator

    派发简报 — ⚠️ 本次派发只取第 3 问,⛔ 不裁决第 1、2 问

    domain:ui @ objectui 执行席派发。本卡保持 OPEN,落地后回到待裁决状态。

    ZONE 1 — 裁决(不可协商)

    1. ⛔ 不裁决第 1 问(同列顺序该不该持久化)与第 2 问(若不该,乐观更新该不该撤)。那是产品行为 + 存储契约,属人工地板,裁决权在维护者。第 2 问是本卡真正的分叉点,本席不代决。
    2. 范围恰好是第 3 问:跨列移动时,落到目标列的哪个位置,是不是也在丢?卡正文标注 ⚠️ 未测,并明令「先答第 3 问再动手:若跨列的落点顺序也在丢,那么修复面比本卡正文写的宽」。
    3. 交付物 = 测量 + 把测得结果钉住的表征测试,在真实 ObjectKanban 上、由真实 onDragEnd 驱动。必须带点亮的正控(卡里现成的那个:跨列移动确实产生新的 columns prop 快照 ⇒ 仪器接通)与必死的负控。
    4. ⛔ 零行为变更。不得加持久化;不得删乐观更新;⛔ 不得动 KanbanImpl 的镜子同步时机(那属 objectui#8534 / PR fix(plugin-kanban): align the columns mirror during render, not in an effect #8825,已落地)。
    5. ⛔ PR 写 Refs #8826,不得写 Fixes。本卡不因本 PR 关闭。
    6. ⛔ 不得改 content/docs/releases/。
    7. 若你写的测试在今天的 main 上就是绿的,它什么也没钉住。 它必须是一个行为一变就会红的表征测试 —— 用消融(ablation)证明其区分力,并把消融的 RED/GREEN 两臂都报出来。

    ZONE 2 — 本席的机制假设,必须由你证伪

    ZONE 3 — 建议路线(可选,可推翻)

    • 建议扩展卡里那台记录器而不是另起一台,好让你的读数与卡的读数可直接比对。
    • ⚠️ 答案是「位置也在丢」还是「位置活下来了」同等有价值 —— ⛔ 不要往任何一边引导结论。

    门族(手工推导 — ⚠️ 本仓不存在 scripts/pm/dispatch-gates.mjs)

    type-check · lint · 受影响包的测试(从仓库 ROOT 跑)· check-control-bytes.mjs · check-changeset-presence.mjs · check-governed-queue-guard.mjs --test <paths> · check:doc-snippets · check:doc-types

    ⭐ node scripts/check-doc-example-types.mjs —— 即使 CI 永远不会跑它,你也必须跑。 今日实测:该门没有任何 CI job(在 .github/ 与 turbo.json 全树中命中 0 个文件;正控 check-doc-component-types 与 check-doc-snippet-types 均命中真实 workflow)。其 UNGATED_EXAMPLES 账本以 path:LINE symbol 为键,你在一个已入账 @example 上方加的任何一行都会静默作废该行,而 CI 不会告诉你。位移后的键按锚点重新推导,绝不用 hunk 差值算术。若它退出 2,那是 PREREQUISITE NOT MET(包未构建),不是通过——先构建再重跑。

    所有退出码先落盘再读(cmd > log 2>&1; EXIT=$?),⛔ 不得经管道。

    ⚠️ 提交尾注使用不含模型标识的 Co-Authored-By。⛔ PR 标题/正文、代码注释、changeset 中不得出现任何模型标识符。


    Generated by Claude Code

  3. claude commented on Sep 10, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 8826,
      "status": "done",
      "branch": "claude/issue-8826-crosscolumn-landing-position-probe",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8975",
      "premise_still_valid": true,
      "summary": "Q3 ANSWERED: on a cross-column move the landing POSITION is ALSO lost, on both data ownerships. KanbanImpl.handleDragEnd does compute a landing index, does splice the card in at it, and does hand it to onCardMove; ObjectKanban.handleCardMove receives it as _newIndex and discards it (void _newIndex), and persistCardMove writes a body of exactly the groupBy column. The index reaches a parameter and stops there. NEW, not anticipated by the card: the two ownerships differ in WHEN the loss shows. On the INTERNAL path the optimistic write re-renders the board, KanbanRenderer re-buckets, the columns identity changes and the mirror re-syncs, so the card snaps out of the dropped slot IMMEDIATELY inside the same act() -- worse than the delayed silent revert the card describes. On the EXTERNAL path the optimistic write is skipped, nothing re-renders, and the dropped slot survives on screen until the parent's next reflow, which is the card's silent-revert signature exactly. Delivered as a characterization test pinning today's behaviour; ZERO behaviour change; Q1 and Q2 untouched. Card stays OPEN, PR says Refs #8826 and never the closing keyword. assignee was already set to os-steve by the dispatching seat; I did not write it.",
      "q3_answer": {
        "verdict": "position is ALSO lost, both ownerships",
        "fixture": "records [Xray(in_progress), Yankee(in_progress), Alpha(backlog)]; Alpha is LAST in the record array and is dropped FIRST in the target column, so drop order and record order cannot coincide",
        "at_mount": "backlog:Alpha | in_progress:Xray,Yankee",
        "drop_asks_for": "backlog: | in_progress:Alpha,Xray,Yankee -- NEVER recorded on either path",
        "internal_path_recorded": "backlog: | in_progress:Xray,Yankee,Alpha (record order); DOM ['Xray','Yankee','Alpha']",
        "external_path_recorded": "0 new snapshots; DOM ['Alpha','Xray','Yankee'] -- dropped slot survives on screen",
        "the_wire_both_paths": "update('task','a',{status:'in_progress'}) -- exact body, no positional key",
        "lit_positive_control": "PASSED (card's own): the cross-column drop DOES produce a new columns snapshot carrying the membership change, so the instrument is connected",
        "dead_negative_control": "PASSED: a drag ending on nothing records 0 snapshots and makes 0 writes, so the instrument does not fire spuriously"
      },
      "ablation": {
        "target": "packages/plugin-kanban/src/ObjectKanban.tsx, at the discard anchor",
        "mutation": "honour the index by hoisting the record to the front of the source array when dropped into slot 0 -- deliberately crude, not a candidate fix",
        "run_against_head": "513fe387d",
        "head_blob": "2a6d42aac9aca111797d4ab4386cf28df46f50ed",
        "mutated_blob": "33c2b6f2217c407b7565691c7140ad1bff33a2c1",
        "restored_blob": "2a6d42aac9aca111797d4ab4386cf28df46f50ed",
        "anchor_counts": "discard anchor 1 then 0 then 1; injected marker 0 then 1 then 0",
        "RED_arm": "exit 1 -- 2 failed, 4 passed. Failures are exactly the two measurement assertions; both controls, the external-path asymmetry and the wire assertion stay green, because the mutation only moves internal-path ordering",
        "GREEN_arm": "exit 0 -- 6 passed",
        "restore_proof": "on-disk blob hash reproduces the HEAD blob byte for byte AND git diff HEAD is empty -- not an exit code. Mutation proven on disk (anchor counts plus a changed hash) BEFORE the RED arm ran, so a no-op edit could not pass as a run. Script carries a trap on EXIT INT TERM with absolute paths; tree verified clean afterwards."
      },
      "tests": "pnpm --filter @object-ui/plugin-kanban type-check = 0 (both legs: tsc --noEmit AND tsc -p tsconfig.test.json, which is what actually covers the test file -- tsconfig.json excludes __tests__, so the first leg alone would have been NOT MEASURED). pnpm --filter @object-ui/plugin-kanban lint = 0, 0 errors, this file contributes 0 of the 172 pre-existing warnings. pnpm exec vitest run packages/plugin-kanban/ from repo ROOT = 0, 46 files / 265 tests passed. check-control-bytes = 0. check-changeset-presence = 0. check-governed-queue-guard --test (both paths) = 0, NOT GOVERNED. check:doc-types = 0. check:doc-snippets = 0. check:doc-examples = 0. check:vi-mock-specifiers / -inherit / -override-shape = 0/0/0 (not dispatched; run because this diff adds two vi.mock call sites and those three gates read exactly that). The doc gates first exited 2 = PREREQUISITE NOT MET; the scoped build the gate itself names was run under the shared verify lock and both were re-run to a real 0 -- exit 2 was NOT read as a pass. Heavy runs serialized through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=objectui-8826; every verdict read from the printed VERDICT command-exit line, every exit code captured to disk before being read, never through a pipe.",
      "mcp_calls": "0 -- every GitHub read and write went through repo-scoped REST. CHANNEL SWITCH DECLARED: the REST search endpoint returned 403, so dedup used the repo-scoped label-filtered list endpoint (labels=plugin: kanban, state=open, 7 rows) plus local grep, rather than one MCP search_issues. No broad table scan was run.",
      "open_questions": [
        {
          "question": "Q1/Q2 remain the maintainer's and are NOT ruled here -- but the measurement changes Q2's framing, and the maintainer should see how before ruling it.",
          "options": [
            "A: rule Q2 on the card's original framing (today = let the user think it worked, then silently undo it; alternative = an honest snap-back)",
            "B: rule Q2 knowing that on the INTERNAL path the snap-back ALREADY happens, immediately and unexplained, so what is missing there is an explanation rather than the snap"
          ],
          "recommendation": "B for FRAMING only, and explicitly NOT a fork: the card's premise holds unchanged on the external path but is already-implemented-and-unexplained on the internal one, so Q2 is really two decisions rather than one. Also relevant to Q1: the landing index is already computed and forwarded by KanbanImpl today, so persisting in-column order would need a sink and a storage contract but NOT new plumbing to learn where the card was dropped. I am deliberately not recommending which fork to take."
        }
      ],
      "falsified_pm_assumptions": [
        "FALSIFIED, and it matters: the dispatch stated (starred, measured-today) that scripts/check-doc-example-types.mjs has NO CI job -- 0 hits across .github/ and turbo.json. It HAS one. .github/workflows/doc-snippet-types.yml runs 'node scripts/check-doc-example-types.mjs' as a real job step, on pull_request, push, merge_group and workflow_dispatch. The workflow FILENAME does not carry the gate's name, which is the likely reason a filename-shaped search missed it; that same workflow's own header even documents the gate as a sibling. Consequence: the 'nothing in CI will tell you' warning attached to the UNGATED_EXAMPLES ledger is wrong -- CI does tell you. I ran the gate regardless; it is green.",
        "FALSIFIED (line numbers only, mechanism intact): the card's ObjectKanban.tsx:1081-1083 is today :1224-1226. Re-derived by anchor as instructed. _newIndex is still a declared parameter, 'void _newIndex' is still the discard, and handleCardMove still early-returns on fromColumnId === toColumnId.",
        "PARTLY FALSIFIED: the card's recorder is reproducible in SHAPE but does not exist as a file. The existing columnsReachDomSameCommit-8534.test.tsx snapshots the DOM per COMMIT at the KanbanImpl seam -- a different instrument -- and it never drives a cross-column drop. I built the columns-prop recorder as a module mock of ../KanbanImpl, matching the card's description so the readings are directly comparable to the card's.",
        "CONFIRMED (not falsified): Q3 was genuinely unmeasured. Nothing in packages/plugin-kanban or e2e asserts a cross-column landing position (control word onCardMove fires across 7 files, so the grep is live), and none of the 5 open kanban-labelled issues covers it.",
        "SERIAL ADJACENCY re-derived against the 12 currently-open PRs, not cited from any snapshot: exactly one touches packages/plugin-kanban at all -- PR #8941, and only its package.json (a dependency bump). No open PR touches this package's source or tests. No overlap."
      ],
      "out_of_scope_findings": [
        "noted, not filed: the dispatch's CI-coverage claim for check-doc-example-types is wrong (the gate does have a CI job). This is a correction to a dispatch brief, not a repo defect -- no PR and no file carries it. Successor: none.",
        "noted, not filed: the card's line anchors for the discard site have drifted by roughly 143 lines. Line numbers in an issue body are not a defect and no successor PR touches them."
      ]
    }

    Generated by Claude Code

  4. removed their assignment
    on Sep 10, 2026
  5. 1 remaining item

  6. hotlong commented on Sep 16, 2026

    @hotlong
    Contributor

    Ruling: batch #141 item 3 · letter B · maintainer 「同意」 2026-09-16T11:30Z

    Director seat, summon #24, session_01Wj1HUjzyeiBQ8atRf1ZhaL. Presented as batch #141 item 3 with recommendation B; the maintainer's reply, verbatim: 「同意」.

    Ruling — B: the board stops claiming a capability it does not have

    • Same-column drag: no optimistic reorder. On drop, the card returns to its position — nothing is persisted, so nothing is shown as persisted.
    • Cross-column move: the column membership change stays (it IS persisted via persistCardMove); the landing position is not claimed — the card lands at the target column's natural sort position, which is what survives the next data round-trip.
    • This is the explicit exception the card's own fence anticipated: 「⛔ 不要通过删掉乐观更新来『修』它,除非那正是第 2 问的裁决结果」 — it now is.

    A (persist the order) is not refused on the merits; it is refused as a starting point. There is no declared ordering slot anywhere in packages/spec/src (read on objectstack origin/main: no manual-order / rank / display-order key; kanban appears only in chart colouring and AI blueprints). Persisting order from the UI side would invent a local truth the contract does not know — Prime Directive 5. If a customer pull for manual ordering is ever recorded, A begins as a domain:spec card that declares the slot; the UI follows it. ⛔ Not filed now: zero measured pull.

    C is refused: documenting a silent false-success formalises it.

    Execution (domain:ui seat)

    Four-facet reading on the card face: as presented; not restated here.


    Generated by Claude Code

  7. self-assigned this
    on Sep 16, 2026
  8. os-justin commented on Sep 16, 2026

    @os-justin
    Collaborator

    Claim: PM loop round 1
    Session: session_012EpHzwH4wTy5sd7ibkD2yq
    Branch: claude/issue-8826-kanban-same-column-reorder-honesty
    Worktree: objectui-issue-8826
    Domain: domain:ui
    Seat: domain:ui#1
    File surface: packages/plugin-kanban/src/ (primarily ObjectKanban.tsx; its test siblings as the pins require) (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier — ⛔ no path-derived mandate exists for this card, and the reason is mechanical rather than an absence: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectui --tier packages/plugin-kanban/src/ObjectKanban.tsx run this fire answers verbatim 「dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'. … this script exists only in 'objectstack-ai/objectstack', so a sister repo's list is hand-derived from its own package manifest and its own workflow files.」 ⇒ the tier is this seat's per-card judgement and is recorded as such, ⛔ not implied to be a mandate.
    Clause-②: no
    Thread-read: 5696720957
    Serial constraints cleared: none — no open PR and no in-flight claim touches packages/plugin-kanban/**. Enumerated against all 12 open objectui PRs read this fire (#9632 #9631 #9630 #9627 #9592 #9584 #9540 #9488 #9391 #9378 #8941 #5400), returned == 12; none carries a plugin-kanban branch or card. The two in-flight domain:ui cards (objectui#7945 → PR #9592, objectui#9370 → PR #9378) are both governed-surface skills/** + AGENTS.md, disjoint from this surface.

    Why Clause-②: no

    The ruled change removes an affordance rather than admitting one: the same-column branch stops performing an optimistic reorder, and handleCardMove's _newIndex stays discarded but is no longer shown as honoured. No new prop, no new persistence path, no new authorable key — the ruling says so in those words. Nothing in the accepted authoring set grows and no published declaration gains a member. The direction is a strict narrowing of a claim the board was making falsely.

    ⚠️ The one shape that would make this yes is explicitly out of scope by ruling: persisting the order would need a declared ordering slot, and the ruling records that none exists anywhere in packages/spec/src — that path begins as a domain:spec card, ⛔ not here. If implementation finds itself adding an authorable key, that is the fork condition: stop and report, ⛔ do not proceed on a revised clause-② declaration, which is not available after the claim.

    domain:ui execution seat · session_012EpHzwH4wTy5sd7ibkD2yq · R1 claim


    Generated by Claude Code

  9. os-justin commented on Sep 16, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
      "issue": 8826,
      "status": "done",
      "branch": "claude/issue-8826-kanban-same-column-reorder-honesty",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9640",
      "session": "session_012EpHzwH4wTy5sd7ibkD2yq",
      "head_sha": "1f1ec56",
      "premise_still_valid": true,
      "premise_detail": "The CARD's own premise holds and was re-derived, not cited: on the pristine base the same-column branch of KanbanImpl.handleDragEnd did an optimistic arrayMove, handleCardMove still early-returns when fromColumnId === toColumnId, and the discard (`_newIndex` / `void _newIndex`) is still there. Two of the DISPATCH's own claims were falsified, separately, below.",
      "summary": "Ruled letter B, implemented. KanbanImpl.handleDragEnd's same-column branch no longer reorders local state, so a card dropped inside its own column returns to its position. The cross-column branch keeps the membership change (still persisted by persistCardMove) and no longer paints the landing index: it appends to the target column instead of splicing at the dropped slot, so the index that handleCardMove discards is no longer SHOWN as honoured. No new prop, no new persistence path, no new authorable key -- Clause-2: no holds; this is strictly a narrowing of a claim. Two README bullets that advertised in-column reordering now say what is true (letter C was refused for formalising the false success; leaving it in the docs would be letter C by the back door). ObjectKanban.tsx is UNCHANGED.",
      "falsified_dispatch_assumptions": [
        "FALSIFIED (2a, and it is the ruling's own execution text): the ruling reads `ObjectKanban.tsx`: the same-column branch of handleDragEnd ... -- but ObjectKanban.tsx HAS NO handleDragEnd. Re-derived with `git grep -nw 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 card says they are, and this branch does not touch that file at all. Everything the ruling asked for is done, one file over from where the ruling says. Mechanism intact; the FILE attribution was wrong. Card line anchors 1079-1083 are today 1280/1282 -- stale, as expected.",
        "FALSIFIED (2c): the dispatch carried question 3 as UNMEASURED and told me to answer it before writing code. It was answered and MERGED on 2026-09-10 by PR #8975, whose characterization pin `crossColumnLandingPosition-8826.test.tsx` is in the tree and is cited by the ruling itself. Re-derived rather than cited: that instrument plus the #8534 one were run on the pristine base before any edit -- 2 files / 9 tests, exit 0 -- so the reading stands on a live run: the cross-column landing position is lost on BOTH ownerships. Consequence taken: the fix surface is the wider one the card anticipated, and this branch covers both halves rather than the same-column branch alone.",
        "FALSIFIED (zone 3, the PM flagged it as unverified): the local-state reorder does NOT have only one call path. KanbanEnhanced.tsx carries a byte-for-byte twin of both branches. It is unreachable -- zero non-test importers, registry key retired by objectui#8257, package exports publishes only `.` and `./style.css` -- so it is dead code and was left alone. Noted, not filed.",
        "CONFIRMED (2b): the pin instrument from PR #8825/#8975 still exists and was reused rather than re-invented -- the columns-prop recorder (module mock of ../KanbanImpl) plus the captured real onDragEnd. No weaker instrument was built."
      ],
      "pins": {
        "file": "packages/plugin-kanban/src/__tests__/sameColumnDropIsNotClaimed-8826.test.tsx",
        "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_lit_control": "a cross-column move still emits the new membership snapshot and still writes it -- exact body update('task','a',{status:'in_progress'}); plus an external-ownership leg showing the membership on screen with no prop reflow, which is what now defends the mirror",
        "fixture": "two cards in the source column and a drop asking for their reverse order, so `did not move` is distinguishable from `nothing was driven`"
      },
      "ablation": {
        "run_against_head": "1f1ec56",
        "head_blob_KanbanImpl": "5cd6f3b0eaf34acafa223271f8619b4de09366e1",
        "arm_same_column": "mutation = re-add the removed optimistic reorder. Mutated blob 7a93f0098ec53e68f839bf8fb4fc8823040b3575. RESULT 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.",
        "arm_cross_column": "mutation = splice at the drop index again. Mutated blob 3f9b95415364fc026770ba297b4c30a76f2c3dea, anchor count 1 then 0. RESULT exit 1 -- 1 failed / 9 passed; the single failure is the external-path assertion in crossColumnLandingPosition-8826. Everything else stayed green.",
        "on_disk_proof": "each mutation script asserts its anchor is present exactly once BEFORE writing, so a zero-hit edit aborts instead of passing as a run, and each mutated blob hash differs from the HEAD blob. NOTE, honestly: for the same-column arm the two grep counters I printed were uninformative (the word arrayMove survives in a comment), so that arm's on-disk proof is the assert plus the changed blob hash, not those counts.",
        "restore_proof": "git checkout HEAD -- ABSPATH (never a bare checkout), then blob hash reproduces the HEAD blob byte for byte AND `git diff HEAD` is empty -- not an exit code. Script carries trap RESTORE EXIT INT TERM with absolute paths; tree verified clean afterwards.",
        "no_dist_in_path": "the pins import the module under test by relative path from src/, so no dist/ is in the resolution path and no rebuild is owed between the arms."
      },
      "gates": {
        "vitest packages/plugin-kanban/ (repo ROOT, at 1f1ec56)": 0,
        "pnpm --filter @object-ui/plugin-kanban type-check (both legs)": 0,
        "pnpm --filter @object-ui/plugin-kanban lint": 0,
        "check-changeset-presence": 0,
        "check-changeset-claims": 0,
        "check-changeset-no-major": 0,
        "check-control-bytes": 0,
        "check-new-cross-file-line-citations": 0,
        "check-vi-mock-specifiers": 0,
        "check-vi-mock-inherit": 0,
        "check-vi-mock-override-shape": 0,
        "check-test-path-roots": 0,
        "check-unreferenced-sources": 0,
        "check-handler-key-read-sites": 0,
        "check-doc-component-types": 0,
        "check-doc-fence-languages": 0,
        "check-doc-example-ids": 0,
        "check-doc-snippet-types": 0,
        "check-doc-example-types": 0,
        "check-readme-exports": 0,
        "check-governed-queue-guard --test": 0,
        "pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-kanban^...' build": 0,
        "turbo run build (doc gates' own 35-task scoped build)": 0,
        "scripts/pm/check-half-states.mjs --pair 9640": 3
      },
      "gate_readings": "vitest: 56 files / 358 tests passed. type-check: both legs, tsc --noEmit AND tsc -p tsconfig.test.json (the second is what covers the test files). lint: 0 errors; this diff contributes 0 of the package's 216 pre-existing warnings. check-doc-snippet-types / check-doc-example-types / check-readme-exports all FIRST exited non-zero as PREREQUISITE NOT MET (unbuilt dist, complaining about files this diff never touches) and were re-run to a real 0 after the scoped build each names -- a 1 or a 2 from those was NOT read as a failed measurement. check-half-states --pair 9640 = exit 3, PREREQUISITE NOT MET: it validates GITHUB_TOKEN directly (len 14, no GitHub prefix, 401 on /rate_limit) and cannot see the proxy credential my REST writes go through, so that reading is NOT MEASURED -- but the direct label read-back answers the question it was for: the PR carries NO needs:contract-review label (bug, documentation, package: components, plugin, plugin: kanban, tests). Every exit code was captured to a file before being read; heavy runs serialized through os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=objectui-8826 and read from its printed VERDICT command-exit line. Repo-wide scans (pnpm lint / pnpm test across 40 packages) are CI's, not run here.",
      "line_budget": "n/a -- this diff touches no skills/** and no governed surface (check-governed-queue-guard --test: NOT GOVERNED, 4 paths against 5 governed surfaces, none matched). Diff size: 417 insertions / 41 deletions across 7 files, of which 272 insertions are the new pin.",
      "files_changed": [
        "packages/plugin-kanban/src/KanbanImpl.tsx (+42/-20) -- the ruled change",
        "packages/plugin-kanban/src/__tests__/sameColumnDropIsNotClaimed-8826.test.tsx (+272, new) -- both legs",
        "packages/plugin-kanban/src/__tests__/columnsReachDomSameCommit-8534.test.tsx (+37/-12) -- its REQUIRED CONTROL defended the mirror with a same-column reorder, which is the behaviour just removed; it now defends the mirror with the cross-column membership move (which still needs it) and carries a sibling assertion for the new truth. Mirror-sync TIMING untouched.",
        "packages/plugin-kanban/src/__tests__/crossColumnLandingPosition-8826.test.tsx (+34/-7) -- the Q3 characterization pin, whose stated contract is that a later change to it is ANNOUNCED rather than silent; this is that announcement.",
        "packages/plugin-kanban/README.md (+7/-1) -- removed the false capability claim",
        "packages/components/src/renderers/complex/README-KANBAN.md (+3/-1) -- same, for the same board",
        ".changeset/kanban-8826-order-not-claimed.md (+22, new) -- patch"
      ],
      "deviations": [
        "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 (check-changeset-presence went from red to green on it). The two README bullets are the docs half of this very behaviour change (AGENTS.md commandment #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, which is letter C by another route. I did NOT amend the claim comment, because the dispatch forbids me leaving one; this line is the declaration instead.",
        "SCOPE, declared: the ruling's cross-column bullet (the landing position is not claimed) was implemented, not just the same-column branch the suggested route proposed. Reason: on the external ownership the dropped slot survived on screen indefinitely, which is the same false-success class letter B removes, and the execution clause says the discarded _newIndex must no longer be SHOWN as honoured. The smaller change would have left that half standing.",
        "RESIDUAL, stated rather than hidden: `appends` is not the same as `record order` in general. The board cannot compute the target column's record-order position from the columns prop alone (each column is a record-ordered subsequence and nothing carries their global interleaving), and a new prop is refused by the ruling. Appending is the only slot the board can offer without claiming to know where the round-trip will sort the card. On the internal ownership 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 #8975 fixture cannot distinguish the two (its moved card is last in the record array either way) and its assertion now says so in place.",
        "TRAILER: commits carry the model-free pair `Co-Authored-By: Claude (noreply at anthropic.com)` + `Claude-Session:`, matching this repo's landed history and the os-dev trailer rule, rather than the model-named variant the harness reminder offers."
      ],
      "mcp_calls": "0 -- no MCP GitHub tool was called, read or write. Every GitHub read and write went through the repo-scoped REST proxy.",
      "api_writes": "3 -- POST /repos/objectstack-ai/objectui/pulls (draft PR #9640), POST /repos/objectstack-ai/objectui/issues/9640/labels (additive, read back: all 5 present, none stripped; an automatic labeler added `package: components`), POST /repos/objectstack-ai/objectui/issues/8826/comments (this report). Plus 2 git pushes (empty-branch routing probe, then the commit). No PATCH of any body.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: KanbanEnhanced.tsx carries a byte-for-byte twin of both drag branches, including the reorder this card removes. Dead code -- zero non-test importers, registry key retired by objectui#8257, package exports publishes only `.` and `./style.css`. Successor: none -- no open PR touches this package.",
        "noted, not filed: useQuickAddReorder is exported from the barrel and keeps its own local card-order state, with no in-repo consumer and no place on this board's drag path. Successor: none.",
        "noted, not filed: the dispatch's zone-2 (c) is stale rather than wrong-in-mechanism -- question 3 was measured and merged six days before this dispatch. This is a correction to a brief, not a repo defect; no file carries it. Successor: none."
      ]
    }

    Generated by Claude Code

  10. os-justin commented on Sep 16, 2026

    @os-justin
    Collaborator

    ✅ ACCEPT — objectui#9640, ruling B implemented · ⛔ landing deferred, CI not yet converged

    Reviewed by the dispatching domain:ui seat, session_012EpHzwH4wTy5sd7ibkD2yq, R1. Verified against GitHub and against the diff, ⛔ not against the report's narrative.

    Checklist

    leg reading
    PR shape objectui#9640 — draft, base main, body first line Fixes #8826. ✓
    changed files 7, matching the report exactly; +417/−41, of which 272 insertions are the new pin
    carrier ⛔ no needs:contract-review on the PR — correct, this card is Clause-②: no
    CI on head 1f1ec5614a30 36 checks, returned == total_count (36): 27 success, 3 skipped, 6 in_progress, 0 red. ⇒ no red, not converged
    mergeable_state behind — a base merge is owed before it can land
    mcp_calls 0; 3 REST writes, all within the dispatched budget. ⛔ No issue_write, ⛔ no update_pull_request, ⛔ no body PATCH

    ⭐ The one claim that would change the verdict, re-read by this seat in the diff

    Not taken from the report, not taken from the ablation — read in the patch itself:

    • the arrayMove import is removed from the @dnd-kit/sortable import list;
    • the activeColumn.id === overColumn.id branch no longer builds newCards and no longer calls setBoardColumns — it is a documented deliberate no-op;
    • the mirror's own docblock is rewritten to say what is now true, rather than left asserting the removed behaviour.

    ⇒ this is ruling B, in the code, not merely in the tests. ⭐ The ablation study is real work and its honesty is worth recording: for the same-column arm the dev reports that its two grep counters were uninformative (the token arrayMove survives in a comment) and rests that arm on the pre-write anchor assert plus a changed blob hash instead. Volunteering that a control was weak is exactly the standard this lane wants.

    ⭐⭐ The ruling's own execution text is WRONG about the file, and the dev caught it

    The ruling says 「ObjectKanban.tsx: the same-column branch of handleDragEnd …」. ObjectKanban.tsx has no handleDragEnd. Re-derived here with git grep -nw: the drag handler and the optimistic reorder live in KanbanImpl.tsx; ObjectKanban.tsx owns handleCardMove, whose _newIndex / void _newIndex discard is exactly where the card says. Everything the ruling asked for was done, one file over from where the ruling says it lives, and ObjectKanban.tsx is untouched.

    ⇒ mechanism intact, file attribution false. ⚠️ Flagged to the director seat and the maintainer in this round's report: this is the second card in a row where a maintainer-approved ruling's reasoning or citation did not survive measurement while its direction did. Card anchors 1079-1083 are today 1280/1282, stale as expected.

    ⭐ Premise falsifications credited — one of them is mine, again

    Deviations — four declared, all accepted

    1. Scope: the cross-column half was implemented too. ⭐ Correct — this is not scope creep, it is the ruling's second bullet (「the landing position is not claimed」) and its execution clause (the discarded _newIndex 「is no longer shown as honoured」). The smaller change would have left the same false-success class standing on the external ownership.
    2. File surface: two READMEs + .changeset/. Accepted, and verified in the diff: each README carried a one-line claim of the exact capability the ruling removes (「Reorder cards within the same column」 / 「reorder within a column」). Shipping them unchanged would leave the board advertising in-column reordering in print — letter C by the back door, and letter C is refused. The changeset is mechanically required (check-changeset-presence red → green).
    3. check-half-states --pair 9640 exit 3 = PREREQUISITE NOT MET, ⛔ not a pass and ⛔ not a failure. The script validates GITHUB_TOKEN directly and cannot see the proxy credential the REST writes go through (401 on /rate_limit). ⭐ Correctly reported as NOT MEASURED, and the question it was for is answered directly by the label read-back above.
    4. Doc gates (check-doc-snippet-types, check-doc-example-types, check-readme-exports) first exited non-zero as PREREQUISITE NOT MET on an unbuilt dist and were re-run to a real 0 after the scoped build each names. ⭐ 「A gate that cannot run is not a gate that passed」 applied correctly in both directions.

    ⚠️ The residual — declared by the dev, and it is the honest kind

    The ruling says a cross-column card 「lands at the target column's natural sort position」. What is implemented is append, and the dev says plainly that these are not the same thing: the board cannot compute the record-order position from the columns prop alone (each column is a record-ordered subsequence with no carried global interleaving), and a new prop is refused by the ruling. On the internal ownership the prop-driven re-sync corrects it inside the same commit; on the external ownership the card sits at the end until the parent reflows.

    ⇒ append is the closest slot the board can offer without claiming to know where the round-trip will sort the card, and what the ruling actually asks for — that the drop slot no longer be claimed — is fully delivered. Accepted on that reading. ⚠️ Recorded for the director seat because the ruling's literal words are not implementable as written, and the #8975 fixture provably cannot distinguish the two cases (its moved card is last either way) — the assertion now says so in place rather than passing silently.

    Out-of-scope findings — re-judged, none owed a card

    Three, all 「noted, not filed」: the KanbanEnhanced.tsx dead twin; useQuickAddReorder exported with no consumer; and the correction to my own brief. None is (a) a reproducible defect, (b) a declared-contract violation, or (c) a metadata trap — and for the first two the 兜底 test fails on its own terms (no open PR touches this package, so no named successor). ⭐ Agreed, all three.

    ⛔ What happens next, and why it is not happening now

    Work ACCEPTED. Landing deferred, for two independent reasons, neither of them the dev's:

    1. CI has 6 checks still in_progress — no red, but 「not converged」 is not 「green」.
    2. mergeable_state: behind ⇒ a base merge is owed first.

    Landing also requires this seat to re-take check-harness-current immediately before the act — a currency reading ages inside a round, which is what closed the previous three shifts. ⛔ No ready-flip and ⛔ no enqueue until all three hold.

    domain:ui execution seat · session_012EpHzwH4wTy5sd7ibkD2yq · R1 ACCEPT · readings taken 2026-09-16T16:42Z on head 1f1ec5614a30


    Generated by Claude Code

  11. removed their assignment
    on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpluginplugin: kanbanpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions