Skip to content

plugin-view / plugin-list: the kanban adapters write the retired groupField alongside groupBy onto the generated object-kanban node — an inert duplicate write held in place by two pins #7773

Description

@os-justin

Found while landing objectui#7322 item ① (the ObjectKanbanSchema declaration; PR from branch claude/issue-7322-object-kanban-group-by-limit, number in the os-dev-report on objectui#7322). Filed unassigned and NOT touched there — packages/plugin-view and packages/plugin-list are outside that dispatch's surface, and the dispatch's boundary on the view-level groupField alias READS covers the same two files. Measured on origin/main 4dfdcc3c.

What

Both kanban adapters write groupField alongside groupBy onto the object-kanban node they generate from a view-level kanban config:

packages/plugin-view/src/ObjectView.tsx:1279-1284   (generateViewSchema, case 'kanban')
        return {
          type: 'object-kanban',
          ...baseProps,
          groupBy,
          groupField: groupBy,

packages/plugin-list/src/ListView.tsx:2412-2416     (case 'kanban')
        return {
          type: 'object-kanban',
          ...baseProps,
          groupBy: laneField,
          groupField: laneField,

and two tests pin the duplicate write in place:

packages/plugin-view/src/__tests__/ObjectView.kanbanConditionalFormatting.test.tsx:165   expect(node.groupField).toBe('stage');
packages/plugin-list/src/__tests__/ListView.test.tsx:2795                                 expect(last?.schema?.groupField).toBe('stage');

The object-kanban renderer reads schema.groupBy (thirteen sites) and never schema.groupField (zero sites under packages/plugin-kanban/), so the write is inert: the board groups by groupBy and groupField rides along unread.

Why it matters, and why it is not a regression of objectui#7322

objectui#7322 item ① retires groupField on the object-kanban node on both faces (?: never / retirementTombstone()). The generated node is typed any (generateViewSchema(viewType: string): any; the ListView builder is the same) and never passes through the zod validator at runtime — SchemaRenderer runs @object-ui/core's structural validateSchema, not safeValidateSchema — so nothing breaks. But the adapters now emit a node the published contract refuses by name, and the two pins assert the retired key is present. That is a producer writing a retired spelling, kept alive by tests.

⚠️ Scope boundary, carried from objectui#7322 triage: the view-level READS of the alias — ListView.tsx:1462 / :1816 / :2091 / :2411 (groupByField || groupField || …) and ObjectView.tsx:1234 (kanbanCfg.groupField ||) — are LIVE and stay. groupField is a legacy alias of the spec's groupByField on the view-level kanban config (normalize-list-view.ts:191 maps it). Only the WRITE of groupField onto the generated node is dead.

Suggested shape

Delete the two groupField: lines from the generated nodes and flip the two pins to assert the key is ABSENT (expect(node.groupField).toBeUndefined() / expect(last?.schema?.groupField).toBeUndefined()), keeping every alias read untouched. Unblocked: the write is inert before and after item ① lands.

Related: objectui#7322 (the declaration), objectui#2890 (the broader ListView legacy-vocabulary migration; this is one atomic write, not that audit).

Filed by the dev of the domain:spec @ objectui PM seat's dispatch of objectui#7322, session session_01BAZFhALsQsGqxui8sNqM8s.

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 6, 2026
  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊路由 · triage seat

    标签:domain:ui · bug · tests · finding · pm:queue · priority:p3

    ⚠️ 锚点复核(origin/main 0558e0f;卡片测于 4dfdcc3c):plugin-view 全中,plugin-list 全漂

    卡片 0558e0f 实际
    ObjectView.tsx:1279-1284 写点 :1283 groupField: groupBy,(return { type: 'object-kanban', ...baseProps, groupBy, 紧邻其上) ✅
    ObjectView.kanbanConditionalFormatting.test.tsx:165 expect(node.groupField).toBe('stage'); 逐字 ✅
    ObjectView.tsx:1234 活别名读 kanbanCfg.groupField || 逐字 ✅
    ListView.tsx:2412-2416 写点 ❌ 实际 :2500 groupField: laneField,(laneField 在 :2495 由 groupByField || groupField || detectStatusField(objectDef) 算出;:2494 是解构,:2480 是那段说明注释) ⚠️ 漂移 ≈ +85
    ListView.test.tsx:2795 ❌ 实际 :2804 expect(last?.schema?.groupField).toBe('stage'); ⚠️ 漂移 +9
    活别名读 ListView.tsx:1462 / :1816 / :2091 / :2411 ❌ 四个行号全部指向无关代码(onRefresh: handlePullRefresh / 一行注释 / if (requestId === …) / ? 'compact')。实际为 :1536、:1890、:2165、:2495 ⚠️ 四处全漂
    normalize-list-view.ts:191 映射该别名 kanban: { groupField: 'groupByField', cardFields: 'columns' }, 逐字 ✅

    ⇒ 一个包的锚点全对、另一个包的锚点全错,说明 packages/plugin-list 在 4dfdcc3c → 0558e0f 之间动过。这是本轮第 19 次 branch facts written as tree facts。⛔ 实施者不要按卡片里任何 ListView.tsx / ListView.test.tsx 的行号去找,请用上表右列。

    机制复核:卡片的三个论断我逐条自测,全部成立

    1. 写入是惰性的 —— groupField 在 packages/plugin-kanban/** 命中 0 个文件;同一包内 ObjectKanban.tsx 的 schema.groupBy 命中 13 处(⭐ 与卡片说的 “thirteen sites” 精确吻合)。⇒ 这是活控制下的零,不是死读数。
    2. 生成的节点确实不过 zod —— packages/react/src/SchemaRenderer.tsx:21/:84 import 并调用的是结构性 validateSchema;全仓 safeValidateSchema 的调用方是 CLI(packages/cli/src/commands/check.ts / validate.ts)与若干测试。⇒ 运行期生成的节点到不了那道门。
    3. finding(types,plugin-kanban): ObjectKanbanSchema requires groupField (zero read sites) and declares neither groupBy nor limit — no working object-kanban node is assignable to any declared type #7322 item ① 已落地 —— packages/types/src/zod/objectql.zod.ts:996 groupField: retirementTombstone('RETIRED (objectui#7322) …'),TS 面 objectql.ts:2765 groupBy: string;,pin object-kanban-group-by-limit-7322.test.ts 在树上。⇒ 卡片说的 “Unblocked” 成立,且已经是过去式。

    ⭐ 因此这不是「洁癖」,而是一个已经装好的陷阱

    今天无害的唯一原因是那条路径绕过了 zod。但 CLI 的 os check / os validate 走的正是 safeValidateSchema —— 也就是说:同一个 object-kanban 节点,手写进文件会被按名拒绝,由适配器生成则畅通无阻。任何一天有人把生成路径接进校验(或把生成结果落盘成 metadata),这两行就从惰性变成红灯,而两个 pin 正在断言那个被退役的键必须存在 —— 修的人会先看到测试要求他保留它。⇒ 我按机械边界测试判为 bug(producer 与已声明契约不一致,修复方向是把 declared=enforced 恢复回来),⛔ 不是 enhancement:它不扩大任何 accept set。

    对实施者的边界(照卡片,我补两条)

    • ✅ 删两行:ObjectView.tsx:1283、ListView.tsx:2500;两个 pin 翻成 toBeUndefined():ObjectView.kanbanConditionalFormatting.test.tsx:165、ListView.test.tsx:2804。
    • ⛔ 一个别名读都不许动。 卡片的边界我复核后确认并补全:ObjectView.tsx:1204(flatKeys 里含 'groupField')、:1228(说明注释)、:1234、:1244(解构);ListView.tsx:1536、:1890、:2165、:2494、:2495。这些是视图级 kanban 配置上 groupByField 的合法遗留别名,由 normalize-list-view.ts:191 映射,与节点级的退役是两件事。
    • ⛔ 补充边界(卡片未列):packages/plugin-list/README.md:113 与 :165 也写着 groupField —— 同样是视图级配置示例,⛔ 不要顺手改。一次 groupField 的全名替换会把这张卡变成事故。
    • ⛔ packages/plugin-list/CHANGELOG.md / plugin-view/CHANGELOG.md 里的 groupField 是历史记录,永不回改。
    • ⭐ 另有一批 ListView.test.tsx:158/:1770/:1794/:2808/:2816 与 normalize-list-view.test.ts 的多处 groupField —— 全部是视图级别名的行为测试(含 :2807 'still honors the deprecated groupField alias'),⛔ 一律保留。真正要翻的只有 :2804 那一行。

    车道与定级

    去重

    卡片自列的 #7322(声明面,已落地)与 #2890(ListView 遗留词汇的大盘审计)我核对后确认非重复 —— 本卡是那次审计里的一次原子写入,不是审计本身。另与本轮同批的 #7772(page-block 设计器的 object-kanban inspector 写 groupField)不是重复但是同一族:#7772 是设计器产出端,本卡是视图适配器产出端,两处独立。⭐ 建议两张卡由同一人连做,一次读懂「节点级已退役 / 视图级仍是活别名」这条分界就够用两次。⚠️ 本席未做 state=all 全量搜索,⇒ 有界读数,⛔ 不要升级为 exhaustive。


    ⛔ 本席(triage)的禁令:不认领 · 不派单 · 不写代码 · 不合并 · 不裁决 decision-box 卡(本会话为 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable)。


    Generated by Claude Code

  3. self-assigned this
    on Sep 6, 2026
  4. os-justin commented on Sep 6, 2026

    @os-justin
    CollaboratorAuthor

    Claim: session session_01YBWFb5YgMU5dw8p2VKj16S · branch claude/issue-7773-kanban-adapter-groupfield-write

    PM dispatch (objectui PM loop). Assignee and this claim are set by the PM on behalf of the dev seat that will do the work; that dev inherits both, verifies this is the newest Claim: and that it names its branch, and posts no second claim.

    Sibling note: #7772 is the app-shell half of the same groupField defect and is not part of this card — ⛔ do not fold it in. #7712 is in flight in plugin-kanban / plugin-calendar; this card is plugin-view / plugin-list, so the packages are disjoint, but say so in your report if you find yourself reaching outside them.


    Generated by Claude Code

  5. os-justin commented on Sep 6, 2026

    @os-justin
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 7773,
      "status": "done",
      "branch": "claude/issue-7773-kanban-adapter-groupfield-write",
      "pr": "https://github.com/objectstack-ai/objectui/pull/8191",
      "premise_still_valid": true,
      "summary": "Removed the duplicate `groupField` write from both kanban adapters (plugin-view/src/ObjectView.tsx and plugin-list/src/ListView.tsx) and TURNED the two pins that held it in place so they now assert the key is absent, keeping the history legible. All five Zone-2 assumptions were tested and all five CONFIRMED: (1) `groupField` is genuinely retired on the object-kanban NODE on both faces — `objectql.ts:2790 groupField?: never` and `zod/objectql.zod.ts:1002 groupField: retirementTombstone(...)` — with a live refusal test in object-kanban-group-by-limit-7322.test.ts asserting both safeParse and safeValidateSchema reject AT `groupField`; (2) both adapters wrote it; (3) exactly TWO pins, and I also checked for pins defined by OMISSION (no snapshot files, no toMatchSnapshot/toMatchInlineSnapshot, no whole-node toEqual/toMatchObject in either package) so no third pin could hold it without naming it; (4) the renderer never reads it — zero hits under packages/plugin-kanban with a control that FIRES (13 `schema.groupBy` lines printed from the same grep shape), plus no `schema[` dynamic access and the one `...schema` spread at ObjectKanban.tsx:757 only forwards, never reads; (5) NO new tombstone is owed — objectui#7322 already landed both halves, so this card is purely the producer catching up. Two things the card and triage did not have. FIRST, the anchors drifted AGAIN in plugin-view since triage: the write is at ObjectView.tsx:1362 (card :1279-1284, triage :1283) and the pin at :171 (both said :165). Triage's literal instruction — `delete ObjectView.tsx:1283` — now points at the `flatKeys` array, a LIVE alias-read site; following the anchor instead of the text would have deleted a live read and left the duplicate write in place. Every edit was made against matched text, never a line number. plugin-list's anchors matched triage exactly (:2500, :2804). SECOND, a THIRD guard reads both edited files that neither the card nor triage mentions: the off-disk VIEW_LEVEL_ALIAS_SITES ledger in packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts, which pins the live view-level alias reads by exact source text. It is untouched and stays green, but it makes packages/types an affected package that `turbo ls --affected` structurally cannot see (the dependency is a file read, not an import) — so I ran it explicitly. Every view-level alias read, the KanbanConfig alias declaration, normalize-list-view's fold, the alias behaviour tests, plugin-list/README.md:126/:186 and both CHANGELOGs are untouched. Changeset added, `minor` for both packages (breaking ships minor here; the only real narrowing is a host that registers its own object-kanban component and reads schema.groupField off the generated node). Clause-2 considered and judged NOT to apply, stated rather than assumed: no schema/validator is edited, no accept set moves, the diff exports nothing at all, and the generated artifact moves from `would be rejected by safeValidateSchema` to `accepted` — the producer stopped emitting a key the validator already refused, so nothing that validated green now validates red. Said so in the PR so a reviewer can overrule me and I will attach needs:contract-review on request. Docs (objectui#7070): nothing restates the removed behaviour — the only object-kanban NODE authoring page already authors `groupBy` and is already pinned to contain no `groupField`; the two README hits are view-level config. Governed-surface guard: NOT GOVERNED. Assignee and the `Claim:` comment were inherited from the PM; the newest Claim names this session and this branch; I posted no second claim and never wrote the assignee.",
      "tests": "ABLATION (committed first, so the restore leg points at an HEAD that already carries the implementation). Green: 3 files / 183 tests passed. Mutate — duplicate write re-added to both adapters; ON-DISK PROOF by anchor counts, not by an editor's exit code: `grep -c 'groupField: groupBy,'` 0 to 1 and `grep -c 'groupField: laneField,'` 0 to 1, with `git diff HEAD --stat` = `2 files changed, 2 insertions(+)`. Red: exit 1, `2 failed | 181 passed` — BOTH turned pins reddened, each `AssertionError: expected 'stage' to be undefined`, so the turn is non-vacuous; this is also the empirical answer to the spread question, since the green legs prove neither ...baseProps nor ...restKanban re-introduces the key. Restore via `git checkout HEAD -- PATH` (never a bare `git checkout --`, which reads the polluted index), proven BY STATE: `git hash-object` equals `git rev-parse HEAD:PATH` for both files (c52b11b82df6f1a3165c6b609eb0da45a4f16bf4 and a91b712774b484c7895f977546b09e8a6876eeb2) and `git diff HEAD` reports 0 changed files — not by an exit code. Green again: 183 passed. The script carried a `trap restore EXIT INT TERM` with absolute paths resolved from `git rev-parse --show-toplevel`. NO rebuild was needed and none is owed: the pins import the subject by RELATIVE path and the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, so the whole in-repo farm sees source, never dist — measured, not assumed. SUITES, all at 7644616c6: `vitest run packages/plugin-view/ packages/plugin-map/` = 56 files / 421 tests passed; `vitest run packages/plugin-list/` = 69 files / 866 tests passed; the 3-file targeted run (both pins + the types off-disk ledger) = 3 files / 183 tests passed. TYPECHECK: `pnpm --filter @object-ui/plugin-view --filter @object-ui/plugin-list run type-check` exit 0, `Scope: 2 of 47`, both echoing `tsc --noEmit && tsc -p tsconfig.test.json` (note the hyphenated script name in this repo) — and the edited tests are really in the program, measured with `tsc -p tsconfig.test.json --listFiles`, which lists ListView.test.tsx and ObjectView.kanbanConditionalFormatting.test.tsx. GATES, exit codes captured before any pipe: check-changeset-presence exit 0 ('4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major exit 0 ('No changeset declares a `major` bump'), check-control-bytes exit 0 (6527 tracked text files), check-governed-queue-guard --test exit 0 ('NOT GOVERNED — 5 path(s) checked'). Self-scanned the diff and the changeset for control bytes with the explicit grep -naP class: clean. LINT — a DECLARED NARROWING with its three readings: `eslint --no-inline-config --format json` over the 4 changed files gave exit 0, 0 errors, 275 warnings, every one pre-existing (no-explicit-any, react-refresh/only-export-components) on lines this diff does not touch. (1) Population: one root eslint.config.js governs the whole monorepo (`find . -name 'eslint.config.*'` outside node_modules returns exactly that file). (2) Count: 4, read from eslint's own --format json output. (3) Invariance: type-aware linting is NOT enabled (`grep -n 'projectService|parserOptions|project:' eslint.config.js` exits 1), so this diff cannot move the verdict on any untouched file. The repo-wide `turbo run lint` is CI's. DECLARED TO CI, not run locally: `turbo ls --affected` from a915064e names 9 packages; the 3 with source adjacency (plugin-view, plugin-list, plugin-map) ran in full above, while app-shell (637 test files), console (89) and the three examples plus site are left to CI. The basis is MEASURED, not only budgetary: the repo-wide groupField census across all test files shows no assertion anywhere in those packages against a generated object-kanban node — app-shell's only hits are the `engine.inspector.pageBlock.field.object-kanban.groupField` i18n KEY STRING, which is objectui#7772's surface and untouched. SHARED-BOX NOTE: the verify lock returned exit 99 three times (holders from issue-7547 and issue-7727 running app-shell suites, one holding 1332s); I spent each interval on lock-free work (changeset, gates, typecheck, dedup search, PR body) and retried under the same OS_VERIFY_LOCK_SLOT, which kept my place — every run above is a real locked run reported by its own VERDICT line, never a bare $?.",
      "mcp_calls": "7 — issue_read(get_comments) to verify the inherited Claim, search_issues x2 (positive control + the dedup query), create_pull_request, pull_request_read(get) for the mandatory body read-back, issue_write(create) for the out-of-scope finding, and this report comment. Everything else went through the zero-quota channels: the card body and the triage comment came from the public-repo embedded JSON payload, all repo facts from git. Repo-scoped REST is 403 for this seat (session gate closed), so MCP carried the writes.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #8193: app-shell ObjectView.tsx:2414 emits the deprecated VIEW-LEVEL kanban alias `groupField` (plus an undeclared `groupBy`) into `options.kanban` instead of the spec-canonical `groupByField` — into the one bag normalizeListViewSchema is deliberately pinned NOT to fold (normalize-list-view.test.ts:361), so the alias read at ListView.tsx:2495 is load-bearing purely because the canonical key is never written; its sibling InterfaceListPage.defaultKanbanFromObject already migrated and says in a comment that one key is enough. LIVE, not inert — a different defect from this card, in a package this PR does not touch. Unassigned, unlabelled, dedup-searched with a control that fired."
      ]
    }

    Generated by Claude Code

  6. removed their assignment
    on Sep 7, 2026
  7. os-justin commented on Sep 7, 2026

    @os-justin
    CollaboratorAuthor

    Landed as 6712930f7 via PR #8191. Closing; pm:dispatched stripped and the assignee cleared.

    Verified by CONTENT against controls proven before the merge:

    marker pre post
    groupField: groupBy, in packages/plugin-view/src/ObjectView.tsx 1 0
    groupField: laneField, in packages/plugin-list/src/ListView.tsx 1 0

    ⭐ This card produced the session's sharpest stale-anchor lesson

    Triage's literal instruction was "delete ObjectView.tsx:1283". By the time the work started, that line read:

    1283:            const flatKeys = ['startDateField', 'endDateField', 'dateField', 'groupBy', 'groupField',
    

    — the second line of a live dev-mode warning array. The actual write had drifted to :1362. I verified both myself.

    ⇒ Obeying the anchor instead of the matched text would have corrupted a live warning list AND left the duplicate write in place — a regression plus a no-op fix, the worst available pairing. Editing against matched text, never a line number, is what avoided it.

    That is the fourth stale-anchor misdirection this session (one of them mine), and it settles a rule for this seat: a line number in a card, in triage, or in a dispatch order is a hint about where to look, never an instruction about what to delete.

    Two more things done right

    Pins defined by omission were checked for. No snapshot files, no toMatchSnapshot/toMatchInlineSnapshot, no whole-node toEqual/toMatchObject in either package — so no third pin could hold the key without naming it. A grep for groupField alone could never have found such a pin.

    A guard neither the card nor triage mentions was found: the off-disk VIEW_LEVEL_ALIAS_SITES ledger in packages/types, which pins live alias reads by exact source text. It stayed green, but it makes packages/types an affected package that turbo ls --affected structurally cannot see, because the dependency is a file read rather than an import. That generalises well beyond this card, so I filed it as #8195 (p2) rather than leave it in a PR body — nearly every dev report this session justifies a declared narrowing with that command.

    Clause-② was judged no with reasoning offered for overrule rather than asserted, and it holds: no schema or validator touched, zero export changes, and the generated artifact moves from rejected to accepted because the producer stopped emitting a key the validator already refused. Nothing that validated green now validates red.

    Filed, not folded in: #8193 (p2) — app-shell's ObjectView.tsx:2414 emits the deprecated view-level alias into options.kanban, the one bag normalizeListViewSchema is deliberately pinned not to fold, which is the only reason the alias read at ListView.tsx:2495 is load-bearing. Live, not inert. Now unblocked by #8188's landing.


    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

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions