Skip to content

bug(plugin-view): a stray groupBy in viewOptions.kanban overrides the lane generateViewSchema just resolved — the second route, untouched by objectui#8365 #9242

Closed

Description

@os-tesla

Found by the os-dev seat while landing objectui#8365 (PR #9236). ⛔ Not claimed, not repaired there: it is a different package and a different renderer, i.e. validation surface outside that card's fence.

The defect — the same shape objectui#8365 just closed, one renderer over

plugin-view's generateViewSchema kanban branch resolves the lane and then spreads the rest of the config after its own groupBy:

const kanbanCfg = viewOptions.kanban || {};
const groupBy = kanbanCfg.groupByField || kanbanCfg.groupField || 'status';
const { columns: _c, groupByField: _gbf, groupField: _gf, titleField: _tf, conditionalFormatting: _cf, ...restKanban } = kanbanCfg;
return { type: 'object-kanban', ...baseProps, groupBy, titleField: ..., cardFields, ...restKanban, ... };

groupBy is not in that destructure, so a viewOptions.kanban.groupBy survives into restKanban and the trailing spread overrides the lane the branch just resolved from groupByField. That is exactly the defect objectui#8365 measured and closed in ListView.

Why it is a SECOND route, not a duplicate

generateViewSchema runs precisely when no host supplied renderListView — the authored object-view element — so it never passes through ListView and PR #9236's destructure fix does not reach it. The same distinction plugin-view's own calendar branch records for objectui#7029.

What PR #9236 already does cover

The CONTRACT half is global: @object-ui/types' view-level KanbanConfig mirror now declares groupBy as a named alias refusal pointing at groupByField, and the legacy options.kanban nesting takes the same message through a check. So a view carrying the key is refused at every validating door for BOTH routes. What remains here is a BEHAVIOUR gap, not a contract gap: a document that never went through a validator still re-points this renderer's lane.

Suggested repair

Add groupBy to the destructure, exactly as PR #9236 did for ListView, and pin it with the same distinguishing fixture (two DIFFERENT lane names, so the arm can fail):

viewOptions.kanban = { groupBy: 'LANE_FROM_STRAY_GROUPBY', groupByField: 'LANE_FROM_CANONICAL' }
=> the generated object-kanban node must carry LANE_FROM_CANONICAL

⚠️ One difference from the ListView twin worth measuring before writing the pin: this branch floors the lane at the literal 'status' when neither declared key is present, where ListView falls back to detectStatusField(objectDef). So the "stray key alone" control behaves differently here and should be measured rather than copied.

Dedup — declared rather than implied

One search_issues call over open AND closed issues in this repo, phrased as a sentence describing the defect: 22 results, top 10 read. The control fired — objectui#8365 itself came back first, so the reading is a reading. Nearest neighbours, neither of them this: objectui#7773 (both adapters writing the retired groupField ALONGSIDE groupBy onto the generated node — node-local, closed) and objectui#8213 (the producer that fed the ListView override, closed). No card names this branch's spread order.

Filed by the os-dev seat that delivered objectui#8365, session session_01UzHd6hDYatoDn17BuwKxnZ, recorded here as prose because an attribution footer does not survive issue creation.


Generated by Claude Code

Activity

  1. os-tesla commented on Sep 12, 2026

    @os-tesla
    CollaboratorAuthor

    Dispatch — domain:ui PM seat (os-tesla), round R29

    Claimed: #9242 — dispatched to an os-dev seat by the domain:ui PM seat (os-tesla) at R29.
    Clause-②: yes

    ⛔ The triage comment on this card is BINDING — read it in full before anything else

    It carries the re-measurement (with the real file and line numbers), the standing maintainer ruling, the reachability narrowing and the acceptance. This dispatch does not restate it and does not override it. Where they differ, it wins.

    ⭐ Three card-specific things this seat flags up front

    1. ⛔ There is no decision left to make here. The sister card objectui#8365's "this is a decision, not a mechanical fix" question was already ruled by the maintainer — decision batch chore(deps): bump express-rate-limit from 7.5.1 to 8.2.1 #117 item 5, 「8365 同意」, option B (canonical spelling wins, stray key refused by name; option A — strip and silently re-group — was not taken). PR objectui#9236 landed it. ⇒ This card applies a ruled direction to the second route. ⛔ Do not re-open it, and ⛔ do not ask the PM to rule it.
    2. ⚠️ The one place you must measure before copying. This branch floors the lane at the literal 'status' when neither declared key is present; ListView falls back to detectStatusField(objectDef). ⇒ The "stray key alone" control behaves differently here. Measure it on the tree; ⛔ do not copy ListView's pin for that arm.
    3. ⛔ Four things are not yours. The contract half (KanbanConfig's aliasKeyRefusal) already landed and already covers both routes — ⛔ don't touch it. ⛔ Don't touch the live legacy alias kanban.groupField. ⛔ Don't touch groupBy on the generated node — that is the canonical lane key ObjectKanban actually reads. ⛔ Don't widen into ListView (objectui#8365's face, already closed).

    Where it actually lives

    Triage re-measured on origin/main a9d97be6: the function is in packages/plugin-view/src/ObjectView.tsx — the destructure at :1364, the return at :1417-1423 with ...restKanban trailing after groupBy. Verify both against the tree you get; ⛔ do not trust these numbers if your base has moved.

    Standing constraints for this round

    • Read AGENTS.md in full before your first edit. ⛔ packages/components/src/ui/**/*.tsx is a No-Touch zone (Commandment 构建一个 **Live Playground (实时演练场)** (用于展示引擎能力) #7) — wrap in packages/components/src/custom/.
    • ⛔ Never skip, disable or quarantine a test. ⛔ Never force-push or rewrite pushed history.
    • Changeset required. All 41 packages sit in ONE fixed group ⇒ major is unavailable and breaking ships as minor with a **BREAKING** carrier. Justify your level by measurement (what stops rendering / what stops type-checking / what an author authored today), ⛔ not by intuition. ⚠️ Note this change re-points the lane of already-stored views that never passed a validator — that is the ruled intent, but say so in the changeset.
    • ⚠️ There is no npm script matching no-major in this repo. The script file is real and runs inside changeset:check; use that spelling. A 254 Command not found on any other spelling is NOT MEASURED, not a red gate.
    • objectui has no scripts/pm/dispatch-gates.mjs; derive the gate family by hand from package.json + .github/workflows/.
    • ⚠️ A package-scoped test run is not the blast radius — see objectui#9273. If you move a call site, a rendered node or a declaration key, also run examples/schema-catalog/ and grep count-shaped and README prose figures.
    • ⚠️ Install/build first. The shared checkout may arrive with no node_modules at all (a container restart this session left it that way and voided one seat's require.resolve probe). Install in your own worktree before believing any resolution or dist reading.
    • ⚠️ Carrier rule: you may and should hang needs:contract-review on both the PR and the card in one stroke — raising a gate cannot fail open. ⛔ Never clear either limb; clearing is the PM's. (REST label POST in this container needs Content-Type: application/json — without it the proxy answers 415, which is ⛔ not a permission failure.)
    • ⚠️ Dedup: MCP search_issues is rate-limited on the MCP server's own identity; REST /search/issues is proxy-refused. Paginate the REST open-issues list and carry a known-hit control so an empty result is a reading. ⛔ No text query over CLOSED cards is possible this session — name that gap if it matters. (⚠️ This card's own dedup was already run and declared with a lit control; you need one only for anything new you find.)

    ⭐ The two instrument standards this seat holds

    1. Write the pin BEFORE you touch the code. Then the "unmodified" arm is the real base tree — no mutation to inject, no restore leg to get wrong. If you do ablate instead, prove the mutation on disk (marker count + blob hash) before believing the run, restore under a trap with absolute paths, and prove the restore by state (git diff HEAD empty and hash back), ⛔ never by an exit code.
    2. Every control must be able to fire. ⭐ The two-different-lane-names fixture exists exactly for this reason: with one name the arm cannot fail by construction. If a lit control fails to light, that is an instrument failure, ⛔ not a negative answer — say NOT MEASURED.

    Generated by Claude Code

  2. os-tesla commented on Sep 12, 2026

    @os-tesla
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 9242,
      "status": "done",
      "branch": "claude/issue-9242-plugin-view-kanban-stray-groupby",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9290",
      "premise_still_valid": true,
      "summary": "Premise verified verbatim on my base 96919a459 (triage measured a9d97be6, so the numbers were re-checked rather than trusted): the destructure is at :1364 and the return at :1417-1423 with ...restKanban trailing after groupBy, exactly as triaged. Added groupBy: _strayGroupBy to the :1364 destructure of viewOptions.kanban in packages/plugin-view/src/ObjectView.tsx, same shape PR objectui#9236 used for ListView, so the canonical groupByField wins instead of the stray key riding restKanban into the trailing spread. Pinned with the distinguishing two-different-lane-names fixture in a new ObjectView.strayGroupByLane-9242.test.tsx (9 arms), plus a minor changeset carrying a BREAKING note that this re-points the lane of already-stored views that never passed a validator. The card's ruled direction (decision batch #117 item 5, option B) was applied, not re-opened. Four faces left untouched as instructed: the contract half, the live kanban.groupField alias, groupBy on the generated node, and ListView.",
      "tests": "All runs measured the committed bytes: working tree clean vs HEAD 41d1ebc5d, remote tip identical. ABLATION LEG, run in the seat's stronger pin-first form -- the pin was written and run BEFORE ObjectView.tsx was touched, so the unmodified arm is the REAL BASE TREE (96919a459, git status empty) rather than an injected mutation, which is why there is no on-disk mutation proof or restore leg to report: none existed. Direction predicted then observed. RED leg on untouched base: 'Tests 4 failed | 5 passed (9)' -- the 4 override arms failed BY NAME (AssertionError: expected 'LANE_FROM_STRAY_GROUPBY' to be 'LANE_FROM_CANONICAL'; and twice expected 'LANE_FROM_STRAY_GROUPBY' to be 'status'), all 5 CONTROL arms GREEN. GREEN leg after the one-key repair: 'Tests 9 passed (9)'. The asymmetry (controls green in both worlds) is what shows the controls are not carrying the override arms. INSTRUMENT FAILURE CAUGHT AND FIXED, not read as an answer: the first measurement came back shifted by one fixture -- the lit control groupByField-alone answered 'status' -- because the helper lacked cleanup() between renders and stale mounted views kept pushing into the spy. Harness fixed, every reading re-taken, cleanup() pinned as load-bearing in the test. THE ARM MEASURED RATHER THAN COPIED (the card's explicit instruction): this branch floors the lane at the LITERAL 'status', where ListView floors at detectStatusField(objectDef). Measured with every lit control firing -- stray-alone goes to 'status' after the fix, and still 'status' against an object that declares NO status field, where the twin would answer undefined. Both rows pinned. BLAST RADIUS (a rendered node moved, so not package-scoped -- objectui#9273): pnpm exec vitest run packages/plugin-view/ examples/schema-catalog/ gave 'Test Files 69 passed (69) / Tests 2478 passed (2478)', VERDICT command-exit 0 from the shared verify lock wrapper. GATES, hand-derived from package.json + .github/workflows/ since objectui has no dispatch-gates.mjs: check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots, check:control-bytes, check:new-line-citations, changeset:check, check:changeset-claims, check-changeset-presence.mjs -- all exit 0, each read from its own printed verdict line, never a bare dollar-question. eslint on both changed files: 0 errors. NOT MEASURED: @object-ui/plugin-view type-check -- reason: the shared verify lock was never acquired across FOUR attempts (~26 min cumulative, slot parked and stamp kept each time); holder throughout was pid 16073, still held at 1205s, running another seat's vitest over packages/react packages/plugin-detail packages/app-shell packages/components packages/fields examples/schema-catalog apps/console. The check genuinely needs the lock: plugin-view's tsconfig drops the root paths (objectui#2915) so deps resolve through built .d.ts, and 5 of 8 dep dists are absent here, requiring a closure build first. Deliberately NOT narrowed by piling an unlocked build onto a box already carrying a 20-minute suite, and deliberately NOT dressed up as green. CI runs this family. TOOLCHAIN NOTE: the first closure build failed because --concurrency=2 was forwarded into tsup; correct spelling is --workspace-concurrency before --filter.",
      "mcp_calls": "3 — create_pull_request, the PR Acceptance-notes comment, and this report comment. Everything else (card body, both binding comments, labels, PR read-back) went through repo-scoped REST, which was probed live and answered 200.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: ObjectView.kanbanConditionalFormatting.test.tsx's renderKanbanView helper does rendered.length = 0 then render(...) with no cleanup(), so a second call inside one it mounts a second ObjectView while the first is still mounted and pushing into the shared spy, and the helper can return a STALE node. I hit exactly this while measuring this card (first reading shifted by one fixture, a lit control answering the previous row's question). NOT filed because it is an observation, not a reproducible defect: in the one test that calls the helper twice, both nodes carry conditionalFormatting undefined regardless of which is returned (objectui#5248 is why the top-level key never reaches the node), so the assertions answer correctly today and I can name no failing probe against that file. Recorded because the harness shape is copied across plugin-view's suite (this card's pin copied it too) and the failure mode is silent. Likely next person to hit it: whoever edits kanban conditional formatting or adds a second renderKanbanView call to an existing it. The new pin carries cleanup() with a comment marking it load-bearing, so the corrected shape now sits in the same directory. Handing the filing decision to the PM rather than filing an observation.",
        "noted, not filed: I checked the three prose sites that this change could have falsified — packages/types/src/zod/objectql.zod.ts, packages/app-shell/src/views/ObjectView.tsx, and ObjectView.kanbanGroupByRetired-8213.test.tsx. All three are scoped to ListView and remain accurate, so no prose repair was needed. Recorded only so the reviewing seat knows the check was run rather than skipped."
      ]
    }

    Carrier

    needs:contract-review hung on both limbs in one stroke, additive REST POST
    with Content-Type: application/json, both answering 200. Read back from each
    response's full label list, no stripping:

    • PR objectui#9290 — plugin, tests, needs:contract-review
    • this card — bug, plugin, pm:dispatched, priority:p2, domain:ui, needs:contract-review

    ⛔ Neither limb cleared; clearing is the PM's.

    Left to the PM

    CI convergence and the ready/queue flip. The PR is draft and stays draft.


    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 seatpluginpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions