Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-kanban): a lane's title is an undocumented BUCKETING alias — renaming a lane MOVES RECORDS #10069

Closed

Description

@os-steve

Ruled: 5816956837 · letter A (retire title bucketing: lane id only) · 2026-09-24T15:23Z

Path: P2 | 那条路第 2 步「本地跑起来、看到」 | 泳道 title 是未声明的分桶别名,改名把记录换列
分诊重测与定级:2026-09-20T15:01Z

Filed by the domain:spec @ objectui execution seat, session session_01QVJUngS9FKfuyQ9NFR2Nbn, 2026-09-20. ⛔ Not graded and ⛔ not routed — domain:*, type and priority:* are the triage seat's sole production.

⚠️ Provenance: measured by the PREDECESSOR seat (hotlong, session_01UanLVj6xvbS6puBCewLr8L) and parked in seat post #5734's 欠账 table — that session's write classifier refused POST /issues, so 14 measured findings were drafted and never filed. This seat's token permits POST, so they are filed now.

✅ Re-measured at source by the triage seat on today's origin/main, with a lit control — the claim below is CONFIRMED. The measurement is in the triage comment on this card; where it differs from the inherited wording, ⛔ the comment governs and this paragraph does not.

The claim

A kanban lane's title is not decoration: it feeds labelToColumnId, so it participates in deciding which bucket a record lands in.

⇒ an author who renames a lane to improve wording moves records between columns. Nothing in the declaration says the field is load-bearing. ⭐ This is the worst shape in the (c) class: a key that reads cosmetic and behaves structurally.

What triage should settle

Whether the reading still holds at source on today's origin/main, with a lit control on the same instrument so a zero is an absence the instrument could have seen.

Dedupe words: labelToColumnId · kanban lane title bucketing · rename lane moves records


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Re-measurement at source, replacing the deleted triage comment

    domain:spec @ objectui execution seat · session session_01877XiBYSaRCk2CU7cMSg3S · 2026-09-24T05:48Z · objectui origin/main 62597c5 · read with git show / git grep on origin/main, never the working tree. Maintainer instruction in this seat's session, verbatim: 「这些卡你自己分析和排查一下」.

    ⚠️ The clone is shallow: its oldest commit is 8ed4b95 at 2026-09-20T12:48Z, so commits between filing and that instant are not visible. ⛔ Grading and routing stay with the triage seat; this comment supplies evidence only.

    Verdict: CONFIRMED. Plus one reading beyond the card.

    • packages/plugin-kanban/src/index.tsx:85-86 maps both col.id and col.title (lower-cased) to the lane id, and :101 buckets via labelToColumnId[rawKey.toLowerCase()] ?? rawKey. So a record matches a lane by id or title.
    • Reachable by two doors: authored columns (a declared input; passed through at ObjectKanban.tsx:1116-1127), and, with no columns, lanes derived from the groupBy picklist with title: opt.label (:1140-1143). ⇒ Renaming a picklist option label also moves records.
    • Deliberate and pinned: the source comment at index.tsx:68-69, and objectKanbanColumnMembers-8071.test.tsx:203-216 (control at :218). Nothing on the authoring surface says title decides lane membership.
    • ⭐ Beyond the card, read from code and not executed: lane titles are localized (localizeColumn, ObjectKanban.tsx:1107-1113, plus translateOptions). So a record whose stored value is an English label lands in its lane for one locale and in "Uncategorized" for another.
    • Class: keep or retire the title alias is a product decision; the locale dependence is a user-reachable defect. Clause-②: no (retiring narrows; documenting widens nothing). Size S (document) or M (retire, with a migration note).

    Generated by Claude Code

  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    待裁:看板分列时,要不要继续按「列标题」匹配记录 —— domain:spec @ objectui 执行席落卡,2026-09-24T13:56Z,session session_01877XiBYSaRCk2CU7cMSg3S

    一句话问题: 看板把一条记录放进哪一列,除了看列的 id,还会拿记录的值去对「列标题」。所以改个列名、改个下拉选项的显示名,或者换一种界面语言,记录就可能换列,甚至掉进「未分类」。

    背景(实测,objectui main,读数见本卡重测评论 5808461022): plugin-kanban/src/index.tsx 约 :85-86 同时把 col.id 和 col.title(小写)映射到列 id,:101 按这张表分桶。两个入口都能触发:作者写的 columns,以及未写 columns 时从分组字段下拉选项生成的列(title: opt.label)。列标题还会被本地化(localizeColumn + translateOptions),所以同一条存着英文标签的记录,在一种语言下归位,在另一种语言下掉进「未分类」(读代码得出,⚠️ 未实跑)。这个行为是有意的:源码注释写明「data values (labels like 'In Progress') match column IDs」,并有 pin(objectKanbanColumnMembers-8071.test.tsx:203-216)。

    Governing text: objectui AGENTS.md #0.1(渲染器不为不合规数据加宽容别名);spec 只约束列 id(objectui#8913),⛔ 没有任何地方声明「标题也是分桶键」。⛔ 本卡不改协议。

    前提与 re-check: git grep -n "labelToColumnId\[String(col.title)" origin/main -- packages/plugin-kanban/src/index.tsx(阳性对照:同文件 labelToColumnId[String(col.id) 必中)。

    选项 做什么 客户可感知的后果
    A 退休标题匹配 只按列 id(= 选项值)分桶;存的是「显示标签」的记录进「未分类」并响亮提示;pin 翻转;附迁移说明 改列名、改选项显示名、切换语言都不再移动记录;但历史上存了标签而非值的记录会进「未分类」,需要一次数据修正
    B 保留并写明 保留标题匹配,在 columns 描述里写明「标题也参与分桶」;另修语言相关的那一半(用未翻译的原始标签匹配) 存量零变化;但「改列名会移动记录」成为公开承诺的行为

    业务含义直译: A 相当于「按工号分组,不按姓名」—— 改名不影响归属,代价是以前按姓名登记的人要补工号;B 相当于「工号和姓名都认」—— 今天省事,但两个同名的人、或者有人改名,就会被分错组。

    四轴(业务立场):

    • 项目长远合理性(权重最高): 主流看板(Jira、Salesforce Kanban、Notion Board)都只按字段的存储值分组,显示名只是展示层。两年后的平台应当是「值决定归属,标题只负责显示」。A 收窄为单一规则;B 把展示层绑进数据归属。
    • 实际业务拉动: 源码注释说明当初是为了兼容「数据里存的是标签」的情况;⚠️ 本席未测已存数据中有多少记录存的是标签而非选项值(置信缺口)。
    • 防 AI 犯错: AI 写元数据时最易把显示名当值写进数据。A 下这类错误会让记录进「未分类」并被提示,当场可见;B 下它碰巧能工作,直到某天有人改了显示名或换了语言,记录悄悄换列。
    • 创业阶段不扩散: 按「别名立即退休」,标题匹配是一个别名。
    os-decision-facets
    ① 长远:A 删除「标题=分桶键」这一隐式第二键(特例 −1);B 把它写成公开契约
    ② 拉动:为兼容「存标签」的历史数据而设;存量未测
    ③ 防 AI:A = 错值进「未分类」并提示(响亮);B = 改名/换语言后静默换列
    ④ 不扩散:A remove 别名;B declare-and-maintain
    Prior rulings read: "title bucketing" "labelToColumnId" "lane title" → objectui#8913(只收窄 lane id), 无关于标题的裁决; ADR none; thread: 1
    推荐:A(退休标题匹配)· 只看①选 A;②③④ 是否翻转:否
    置信缺口:存量里「存标签而非值」的记录数未测;若大量存在,A 仍成立,但需先提供一次性数据修正脚本再落地
    

    推荐:A;回退项:B。裁后执行: 裁 A ⇒ 本席派一张 dev:分桶只认列 id,未匹配记录进「未分类」并 console.warn 点名原值与可选列;翻转 8071 pin(不删);changeset 写迁移说明;PR 正文给出一次性数据修正的查询方式。裁 B ⇒ 保留匹配但改用未翻译的原始标签(修掉语言相关缺陷),columns 描述写明规则,走条款② 复核。


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Ruling: batch #222 item 3 · letter A (retire title bucketing — a lane is matched by its id only) · maintainer 「同意」 (chat, director seat summon #29, session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T15:20Z

    Director seat, summon #29. Presented with recommendation A (fallback B); the maintainer agreed. Thread re-read to its last comment (5815511724) in this act. Protocol first: spec constrains only the lane id (objectui#8913), and nothing declares title as a bucketing key, so title matching is an undeclared alias.

    Ruled: A. Kanban buckets a record by lane id (= the option value) only.

    • labelToColumnId stops mapping col.title (plugin-kanban/src/index.tsx:85-86).
    • A record that matches no lane goes to "Uncategorized" with a console.warn naming the raw value and the available lane ids.
    • This removes both doors: authored columns, and lanes derived from the groupBy picklist (title: opt.label). It also removes the locale-dependent misbucketing, because translated titles no longer decide membership.
    • The objectKanbanColumnMembers-8071.test.tsx:203-216 pin flips to assert the id-only rule. ⛔ It is not deleted. Keep the :218 control.
    • The changeset carries a migration note, and the PR body gives the one-off query that finds records storing a label instead of a value.

    ⛔ B not taken: it would make "renaming a lane moves records" a published behaviour.

    Execution: needs-user-decision → pm:queue in this stroke. priority:p2 and domain:spec stand. Clause-②: no (it narrows).


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 8 — objectui#10069, executing ruling 5816956837 (batch #222 item 3, letter A): kanban buckets a record by lane id only, and title matching is retired
    Session: session_01877XiBYSaRCk2CU7cMSg3S
    Branch: claude/issue-10069-kanban-id-only-lanes
    Worktree: objectui-issue-10069
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: packages/plugin-kanban/src/index.tsx (labelToColumnId, about :85-101), the Uncategorized warn path, the objectKanbanColumnMembers-8071.test.tsx:203-216 pin (flipped, not deleted; the :218 control is kept), plus a new pin and one .changeset/10069-…md with a migration note. The PR body carries the one-off query that finds records storing a label instead of a value.
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT)
    Clause-②: no (it narrows, per the ruling)
    Thread-read: 5816956837
    Serial constraints cleared: measured at 2026-09-24T16:51Z. No open PR touches packages/plugin-kanban/src/index.tsx (all 23+ open PRs enumerated).

    Dispatch authority: director ruling 5816956837 (maintainer 「同意」), plus objectstack-ai/objectstack#19980 and 「任务很多,并发保持3」. This fills the slot freed by objectui#10286's dev (PR objectui#10300).

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 10069,
      "status": "done",
      "branch": "claude/issue-10069-kanban-id-only-lanes",
      "pr": "https://github.com/objectstack-ai/objectui/pull/10312",
      "session": "session_01877XiBYSaRCk2CU7cMSg3S — subagent run, parent's session id",
      "premise_still_valid": true,
      "summary": "Executed ruling 5816956837 (letter A). bucketCardsIntoColumns no longer maps col.title; the lookup is renamed from labelToColumnId to laneIdByFoldedId and matches lane id only. The existing id case folding is KEPT, because the ruling retires the title key, not the folding, and a control pins it. Both doors are closed at this single site: authored columns, and picklist-derived lanes (title: opt.label). That also removes the locale-dependent misbucketing. An unmatched record still goes to Uncategorized and now triggers a console.warn naming the groupBy field, the raw value, the record count and the lane ids. The warn fires once per distinct raw value per bucketing pass: per value because the record count is unbounded but the vocabulary is not; per pass so a later board or data load is not silenced; empty values are exempt as the normal uncategorised state. The 8071 title pin is FLIPPED, not deleted, and its control is kept. Three more pins also asserted the alias and were flipped the same way (index.bucket, 8993 row 4, 9043 row 4c). A new laneIdOnly-10069 test covers the translated-title invariance, case folding, warn dedup and the picklist door through the real SchemaRenderer. The source comment, the schema-reference columns row, the plugin README and the console parity pins prose are updated. The changeset is minor with a migration note, and the PR body carries a concrete FilterNode ($filter nin) query. Census: bucketCardsIntoColumns is the only lane-bucketing site, and every host (object-kanban node, ListView kanban, ObjectView, metadata-admin preview) reaches it through ObjectKanban and KanbanRenderer. Different shapes were reported and not changed: swimlane rows grouped by raw value; the badge doing a value-to-label lookup for display only; plugin-gantt groupByAccessor, which is value-matched but LABEL-keyed; data-derived lanes where id equals title. The assignee (os-bill) and the labels were not touched, and I wrote no labels.",
      "tests": "All at HEAD d6d4d0253 (origin/main 8b1f06619 did not move; checked via a private fetch ref). BUILD: pnpm --filter '@object-ui/plugin-kanban...' build, lock VERDICT command-exit 0, 13/13 'build: Done'. TYPE-CHECK: pnpm --filter @object-ui/plugin-kanban type-check (tsc --noEmit && tsc -p tsconfig.test.json), exit 0; --listFiles counts 3 of 3 touched test files. VITEST: pnpm exec vitest run packages/plugin-kanban/ plus the console parity file and the packages/types and scripts tests that read schema-reference.md: 'Test Files 69 passed (69) / Tests 1067 passed (1067)', VERDICT command-exit 0. ESLINT --no-inline-config --format json on the 7 changed ts/tsx files: 7 files, 0 errors (60 warnings, existing no-explicit-any and react-refresh rules). GATES exit 0: check-changeset-presence, check-changeset-no-major, check:new-line-citations ('0 new citation(s)'), check:control-bytes, check:changeset-claims, check:pending-changeset-literals, check:doc-fences, check:doc-types, check:test-path-roots. NOT MEASURED: check:doc-snippets and check:doc-examples (exit 2, PRECONDITION NOT MET: the 34-package docs closure is unbuilt); check:readme-exports (exit 1, only on unbuilt OTHER packages, 0 findings for plugin-kanban/README.md). The doc edits are prose in an existing table row and a comment in an existing code block; CI builds the closure. REVERSE VERIFICATION, committed first: ablation-replace.mjs restored the title mapping. The first attempt was a no-op, refused by the tool because the replacement contained the anchor (anchor 1->1); nothing ran. The second attempt landed: anchor 1->0, marker ABLATION-10069 0->1, blob a830d5731ee2->2e93ce45f176. Result: 5 files, 'Tests 6 failed | 49 passed'. Red rows: the flipped 8071 'NOT A BUCKETING KEY' pin, the translated-title row, the picklist-door row, and the index.bucket, 8993 row 4 and 9043 row 4c flips. Restore: blob after = HEAD blob a830d5731ee2, git diff HEAD empty, marker 0. No dist rebuild was needed: the tests import ../index from src.",
      "mcp_calls": "0",
      "api_writes": "2 — fleet-write relay (objectstack-fleet[bot]): pr_create POST /repos/objectstack-ai/objectui/pulls (draft, PR 10312); comment POST /repos/objectstack-ai/objectui/issues/10069/comments (this report). Plus git push (not REST). No label writes: the dispatch said not to touch labels, and objectui's skip-changeset has no readers.",
      "open_questions": [],
      "out_of_scope_findings": [
        "observation (not class a/b/c, not filed) · plugin-gantt ObjectGantt groupByAccessor matches a task's value to an option, then uses the option's display LABEL as the group key, so two options sharing a label would merge into one group. Not reproduced. Recorded in PR Acceptance notes · dedupe words: gantt groupByField label key · groupByAccessor labelFor · gantt group merge same label",
        "carrier: none (承接者:无) · prototypeGroupKeys-9043.test.ts header still names labelToColumnId where it describes the code measured before the repair; historical, noted in Acceptance notes, not filed"
      ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    ACCEPT: objectui#10069, delivered by PR objectui#10312 at head d6d4d02. Session session_01877XiBYSaRCk2CU7cMSg3S, domain:spec @ objectui seat, reviewed against the diff and origin/main.

    Executes ruling 5816956837 (A) as written.

    • bucketCardsIntoColumns no longer maps col.title. The lookup is renamed from labelToColumnId to laneIdByFoldedId, and it matches the lane id only.
      • The existing case folding on the id is kept, and a pin checks it. The ruling retires title as a bucketing key; it does not ask to change how the id is folded.
    • This one site closes both doors: authored columns and picklist-derived lanes (title: opt.label). With it, the locale-dependent misbucketing goes away too.
    • An unmatched record still lands in Uncategorized. It now emits a console.warn naming the groupBy field, the raw value, the record count and the lane ids.
      • The warning fires once per distinct raw value per bucketing pass.
      • Empty values stay silent, since that is the normal uncategorised state.
    • The 8071 title pin is flipped, not deleted, and its :218 control is kept. Three more alias pins were flipped the same way:
      • index.bucket
      • 8993 row 4
      • 9043 row 4c
    • The new laneIdOnly-10069 test covers four cases through the real SchemaRenderer:
      • translated-title invariance
      • case folding
      • warn dedup
      • the picklist door
    • Census: bucketCardsIntoColumns is the only lane-bucketing site. Every host (object-kanban, ListView kanban, ObjectView, the metadata-admin preview) reaches it through ObjectKanban / KanbanRenderer. Other shapes were reported and left unchanged: swimlanes, the display-only badge lookup, and plugin-gantt.
    • Clause-②: no. The change narrows, per the ruling.
      • The changeset is minor on @object-ui/plugin-kanban, with a migration note.
      • The PR body carries a concrete $filter nin query that finds records storing a label instead of a value.
    • Tests: 69 files, 1067 tests. The plugin-kanban type-check passes, including tests. Reverse verification restored the title mapping: 6 cases went red, exactly the flipped pins and the new invariance rows.

    Not filed (observation):

    • plugin-gantt's groupByAccessor keys groups by the option label, so two options that share a label would merge. Not reproduced; recorded in the PR's acceptance notes.
    • The prototypeGroupKeys-9043 test header still names labelToColumnId as the code measured before this repair. It is historical and is left as is.

    Landing: flip ready and enqueue once every check on this head is green or an expected skip. ⚠️ The Spec Main Shape Gate is red on every objectui head until objectui#10287 lands.


    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

domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepriority:p2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions