Repository navigation
finding(app-shell): ObjectView emits the deprecated view-level kanban alias groupField into options.kanban instead of the spec-canonical groupByField — into the one bag the alias fold is told not to reach #8193
Description
Found while implementing objectui#7773 (the plugin-view / plugin-list half of the groupField cleanup). Filed unassigned and NOT touched there — packages/app-shell is outside that dispatch's two packages. Measured on origin/main a915064e.
What
packages/app-shell/src/views/ObjectView.tsx:2414 builds the view-level kanban config for the list-view it hands down, and writes the deprecated legacy alias rather than the spec-canonical key:
packages/app-shell/src/views/ObjectView.tsx:2409-2415
...(() => {
const lane =
viewDef.kanban?.groupByField ||
viewDef.kanban?.groupField ||
detectStatusField(objectDef as any) ||
undefined;
return lane ? { groupBy: lane, groupField: lane } : {};
})(),
It reads the canonical groupByField first (correct), then emits groupField — never groupByField.
Why this is not the same defect as objectui#7773, and not inert
This is the view-level config, where groupField is a live legacy alias of the spec's groupByField, not the retired node-level key. So the write works today. Two measured facts make it worth its own card anyway:
-
Its sibling already migrated, and left a note saying one key is enough.
packages/app-shell/src/views/InterfaceListPage.tsx:157, indefaultKanbanFromObject:groupByFieldis the spec key. This used to emit the legacygroupFieldalongside it because ListView rendered off the alias only — that read-site now prefers the spec key, so one key is enough.It returns
{ groupByField: field }. TheObjectViewtwin twelve hundred lines away was not carried along, so the same producer surface speaks two different vocabularies for the same concept depending on which entry point you came through. -
The alias fold deliberately does not reach this bag, so the legacy spelling is what actually drives the lanes.
normalizeListViewSchemafoldskanban.groupFieldintogroupByField(packages/core/src/utils/normalize-list-view.ts:191), but only on the DECLARED path. Its own test pins the boundary as intentional —packages/core/src/utils/__tests__/normalize-list-view.test.ts:361'does not reach into the legacyoptions.*twin'.ObjectViewwrites intooptions.kanban, so nothing folds it, andListView.tsx:2495resolves the lane asgroupByField || groupField || detectStatusField(...)— landing on the alias because the canonical key was never written.
Net: a producer in this repo emits the deprecated spelling into the one bag where the migration machinery has been told not to look, and the alias read is load-bearing purely because of that.
Suggested shape
Emit groupByField (matching InterfaceListPage), leaving every alias READ untouched — ListView already prefers the canonical key, which is exactly the reason InterfaceListPage was able to drop the alias. Worth a pin so the two app-shell producers cannot fork again.
Boundary for whoever takes it
- ⛔ Do not touch any alias READ.
ListView.tsx:1536/:1890/:2165/:2494/:2495andObjectView.tsx(plugin-view):1283/:1313/:1323are the live view-level alias reads; stored metadata still authors the alias. - ⛔ Do not touch
packages/plugin-list/README.md:126/:186— view-level config examples, still valid. ⚠️ The same expression also writes a view-levelgroupBy, which is read only by the Kanban capability gate (ListView.tsx:1536/:1890,v.groupByField, v.groupField, v.groupBy). objectui'sKanbanConfigmirror declares onlygroupFieldandcardFieldsas local extensions on top of the spec config and is.passthrough(), sogroupByrides through undeclared at this level. I did NOT measure whether@objectstack/spec's own kanban config declaresgroupBy— check that before deciding whether thegroupByhalf is a second alias to fold or a legitimate key.- ⛔ Not objectui#7773 (
plugin-view/plugin-listnode-level write, in flight) and ⛔ not objectui#7772 (the page-block designer inspector). Both of those concern the RETIRED node-level key; this one is view-level and live.
Dedup
Searched before filing, with a positive control that fired in the same session (a query naming objectui#7773's defect returned objectui#7773 first). The query for this defect returned no card covering app-shell's ObjectView view-level alias emission. Closest neighbours, checked and not duplicates: objectui#2890 (the broad ListView legacy-vocabulary audit — this is one atomic producer site, the same relationship objectui#7773 has to it), objectui#7772 and objectui#7773 (node-level, retired key). state=all sweep.
Filed by the dev of objectui#7773, session session_01YBWFb5YgMU5dw8p2VKj16S.
Activity
os-justin commentedon Sep 7, 2026
Claim:sessionsession_01YBWFb5YgMU5dw8p2VKj16S· branchclaude/issue-8193-objectview-canonical-groupbyfieldPM 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.Hot-file serial cleared. This card was held until #8188 landed, since both touch
packages/app-shell/src/views/ObjectView.tsx. #8188 landed asd80447d4e, so the file is free. Nothing else in flight touches it — #7772 is inviews/metadata-admin/previews/, #8181 inviews/studio-design/, #8205 inproviders/.⭐ This card is unusually well-bounded and its author was scrupulous about the difference between what it measured and what it did not. Two things to carry:
The precedent is in-tree and explains itself.
InterfaceListPage.tsx:157(defaultKanbanFromObject) already migrated and left the reasoning in a comment — "that read-site now prefers the spec key, so one key is enough." TheObjectViewtwin twelve hundred lines away was simply not carried along. So this is following an established migration, ⛔ not deciding a new one.groupByhalf is explicitly UNMEASURED and is yours to settle first. The same expression also writes a view-levelgroupBy. The card says plainly: "I did NOT measure whether@objectstack/spec's own kanban config declaresgroupBy— check that before deciding whether thegroupByhalf is a second alias to fold or a legitimate key."⇒ Measure it at runtime against the spec, with a control that fires (the shape #8171 uses: a
safeParsethat accepts the key paired with the same strict schema refusing a bogus key by name). IfgroupByturns out to be a second alias, ⛔ do not fold it in this PR — report it and file it. One atomic producer site per card is what makes this one reviewable.⛔ The alias READS are off-limits —
ListView.tsx:1536/:1890/:2165/:2494/:2495and plugin-viewObjectView.tsx:1283/:1313/:1323. Stored metadata still authors the alias, andListViewalready prefers the canonical key, which is the whole reason the sibling could drop it.Generated by Claude Code
os-justin commentedon Sep 7, 2026
os-dev-report
{ "issue": 8193, "status": "done", "branch": "claude/issue-8193-objectview-canonical-groupbyfield", "pr": "https://github.com/objectstack-ai/objectui/pull/8227", "premise_still_valid": true, "summary": "ObjectView's view-level kanban producer now emits the spec-canonical `groupByField` instead of the deprecated `groupField`, matching the sibling `defaultKanbanFromObject` that migrated first. The inline IIFE became the exported `kanbanViewOptions`, the fifth member of the timeline/calendar/gantt/gallery `*ViewOptions` family, so both app-shell producers are observable and pinnable. ⚠️ THE FIX HAD TO BE TWO PARTS AND THE PM SHOULD REVIEW THE SECOND: measured on the real component, the producer change ALONE removes the Kanban toggle from every object view. ListView.availableViews consulted the `options.kanban` bag for the LEGACY spelling only (no canonical twin), while the render branch merges the bag and resolves `groupByField || groupField` — so the gate recognized strictly less than what renders. Emitting the canonical key made that latent hole a live regression. I added the canonical rung at ListView.tsx:2165 — the line the card fenced as an alias READ. I removed NO alias read; all three original rungs are intact and stored metadata still resolves `groupField`. This is objectui#5042 (map) and objectui#7544 (chart) a third time: the gate and the seam must answer one question. Zone 2 settled: the spec does NOT declare view-level `groupBy` — filed as objectui#8213, deliberately NOT folded here.", "tests": "BUILD: pnpm --filter '@object-ui/app-shell^...' --filter '@object-ui/plugin-list^...' build -> VERDICT command-exit 0. SUITES: pnpm exec vitest run packages/plugin-list/ packages/app-shell/ -> VERDICT command-exit 0, 'Test Files 711 passed (711) / Tests 7033 passed | 1 skipped (7034)'. TYPE-CHECK: pnpm --filter @object-ui/app-shell --filter @object-ui/plugin-list type-check -> VERDICT command-exit 0, 'Scope: 2 of 47 workspace projects', both echoed 'tsc --noEmit && tsc -p tsconfig.test.json' then 'Done' (script name is `type-check` with a hyphen; a `typecheck` filter would have matched zero scripts and exited 0). NEW PINS: 24 arms across the two files, all green. GATES: check:control-bytes, check:spec-symbols, check:self-import, check:unreferenced-sources, check:changeset-presence, check:changeset-no-major all exit 0; check:governed-queue-guard says NOT GOVERNED (5 paths, 0 matched). check:readme-exports exit 1 read as NOT MEASURED, not red — prerequisite failure, it needs every package's dist/*.d.ts and prints 'run pnpm build first' for 84 self-imports in packages this PR does not touch; it has its own CI workflow that builds first. CLAUSE-2 MEASUREMENT (real component, viewType grid so the 'allow the schema's own viewType' rung cannot mask it): options.kanban {groupBy,groupField} -> Kanban offered TRUE; {groupBy,groupByField} -> FALSE; neither key (negative control) -> FALSE; declared top-level groupByField (positive control) -> TRUE. Both controls fired. ZONE 2 MEASUREMENT vs KanbanConfigSchema from @objectstack/spec/ui, resolved 17.2.0, strict: declared keys are groupByField/summarizeField/columns; {groupByField,groupBy} -> unrecognized_keys ['groupBy']; CONTROL {groupByField,zzzBogusKey} -> unrecognized_keys ['zzzBogusKey']; CONTROL2 {groupByField} alone -> NO unrecognized_keys. Judged on key recognition, not on full-parse green, because the question was whether the key is declared. ABLATION, both legs committed-then-mutated, no dist leg needed since both pins import their subject by relative path (./ObjectView, ../ListView): (A) producer reverted to groupField — anchored fixed-string counts canonical 1->0 and legacy 0->1 measured in BOTH directions, pin went 6 failed / 9 passed, the ADR-0085 lane-RESOLUTION arms and spec controls staying green in both worlds as predicted (this defect never changed which field was chosen, only the key it was filed under); (B) canonical gate rung deleted — rung count 1->0 with the legacy rung still 1, pin went 2 failed / 7 passed, exactly the two canonical-bag arms, every legacy-bag/declared-path/refusal control green. Both restored under an EXIT trap with absolute paths and proven BY STATE: git hash-object equal to git rev-parse HEAD:PATH, empty git diff HEAD, empty git status. ANCHORS re-measured on ecf8e726e vs the card's a915064e: ObjectView producer claimed 2409-2415 measured 2476-2485 (drifted, objectui#8188 landed in between); ListView lane resolution claimed 2495 measured 2518 (drifted); InterfaceListPage 157, normalize-list-view 191, its pin test 361, and the capability gate 2165 all EXACT. All three Zone 2b premises hold.", "mcp_calls": "9 — two issue reads on 8193, one open-PR listing for claim overlap, two dedup searches, one issue create (8213), one PR create, one PR read-back, this comment", "open_questions": [ { "question": "The card fenced ListView.tsx:2165 as an alias READ that must not be touched, and I did touch it — adding the canonical `schema.options?.kanban?.groupByField` rung alongside the existing legacy one. Is that within the card's intent?", "options": [ "A — keep it (shipped). The fence's stated reason is that stored metadata still authors the alias, so alias reads must survive; nothing was removed, all three original rungs are intact, and the added rung makes the gate read the SPEC key rather than only the deprecated one. Without it the card's own fix removes the Kanban toggle from every object view — measured, both controls firing.", "B — split the gate rung into its own PR and hold this one. Keeps the card literally atomic, but the producer change cannot ship first without the regression, so the two would have to land in the opposite order or together anyway.", "C — emit both keys from the producer. Preserves the gate untouched, but re-introduces the deprecated spelling this card exists to remove, and contradicts the sibling's 'one key is enough' precedent." ], "recommendation": "A. It is an addition, not a removal, and it is the same repair objectui#5042 and objectui#7544 already made for map and chart when a gate and its render seam disagreed. Flagging it explicitly because it is a deviation from an explicit fence, not because I think it is wrong." } ], "out_of_scope_findings": [ "filed as #8213: the view-level `groupBy` ObjectView writes into options.kanban is a third lane spelling that the spec's strict KanbanConfigSchema refuses by name (measured, with a control that fires); it is read by ListView's projection collectors so retiring it needs a producer census — deliberately not folded here, exactly as the card directed" ] }Generated by Claude Code
os-justin commentedon Sep 7, 2026
os-dev-report
Supersedes the earlier
os-dev-reporton this card (CI follow-up round: the objectui#7322 alias-site pin).{ "issue": 8193, "status": "done", "branch": "claude/issue-8193-objectview-canonical-groupbyfield", "pr": "https://github.com/objectstack-ai/objectui/pull/8227", "premise_still_valid": true, "summary": "Unchanged from the first round, plus the CI fix. ObjectView's view-level kanban producer emits the spec-canonical `groupByField` instead of the deprecated `groupField`, via the newly exported `kanbanViewOptions` (fifth member of the timeline/calendar/gantt/gallery `*ViewOptions` family), and ListView's capability gate gained the canonical rung for the `options` bag — without which the producer change alone removes the Kanban toggle from every object view (measured, both controls firing). PM ruled A: the gate rung at ListView.tsx:2165 stays; it is not reverted. CI FOLLOW-UP: the one red test was `packages/types/src/__tests__/object-kanban-group-by-limit-7322.test.ts`, a verbatim source-text snapshot of the gate expression that stopped occurring once the fourth rung widened it. I verified the pin's SUBSTANCE independently before touching it: `ListView.tsx` still reads the view-level `groupField` alias at four sites (projection collectors :1536 and :1890, the preserved gate rung :2179, and the render branch's lane resolution :2535), and the other two pinned sites are untouched — so it was a stale literal, not a regression. Re-derived to match the four-rung expression verbatim; both alias rungs remain inside the pinned text, and I proved by ablation that it still reddens BY NAME when a real alias read is removed. ⚠️ ONE CORRECTION TO THE DISPATCH MESSAGE: the pin's `:362-363` note is CONDITIONAL — 'If a site stops reading `groupField`, the retirement's stated boundary has moved and the docblocks on both faces are wrong: re-derive, do not widen the tombstone on the way past.' That antecedent is FALSE here, so the docblock/tombstone half of the prescription does not apply; the boundary has not moved and nothing on either face is wrong. Reading it as an unconditional instruction would have had me rewrite correct docblocks and touch a tombstone this card must not touch. Only the re-derivation applied, and that is all I did. `origin/main` merged in (merge, never rebase).", "tests": "CI REPRODUCED LOCALLY THEN FIXED. objectui#7322 pin BEFORE (stale literal restored under a trap, anchored counts new-rung-in-pin 1->0 and old-single-line 0->1): VERDICT command-exit 1, 'Test Files 1 failed (1) / Tests 1 failed | 28 passed (29)', failing test named 'packages/plugin-list/src/ListView.tsx still reads the view-level alias' — matches the CI signature exactly. AFTER: VERDICT command-exit 0, 'Test Files 1 passed (1) / Tests 29 passed (29)'. ABLATION OF THE RE-DERIVED PIN (the bar: prove it CAN still fail): deleted the legacy `schema.options?.kanban?.groupField` rung from ListView.tsx, anchored count 1->0, pin went VERDICT command-exit 1, 'Tests 1 failed | 28 passed (29)', failing BY NAME on the same test title. So the re-derived literal is not vacuous. Both mutations restored BY STATE, never by exit code: git hash-object equal to git rev-parse HEAD:PATH (024dcf549… for the test file, 24a412030… for ListView.tsx), empty git diff HEAD, empty git status. MY OWN TWO PINS RE-RUN POST-MERGE alongside the 7322 pin: VERDICT command-exit 0, 'Test Files 3 passed (3) / Tests 53 passed (53)' — nothing already proved has regressed. PACKAGE I EDITED THIS ROUND: pnpm exec vitest run packages/types/ -> VERDICT command-exit 0, 'Test Files 137 passed (137) / Tests 2589 passed (2589)'; pnpm --filter @object-ui/types type-check green in the same chain. FIRST ROUND, STILL STANDING (pre-merge): app-shell + plugin-list 'Test Files 711 passed (711) / Tests 7033 passed | 1 skipped (7034)'; type-check green for both; dependency-closure build green. GATES re-run post-merge: check:changeset-presence exit 0 ('5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'), check:control-bytes exit 0. check:readme-exports still NOT MEASURED locally (prerequisite: needs every package's dist/*.d.ts, prints 'run pnpm build first' for 84 self-imports in packages this PR does not touch; it has its own CI workflow that builds first). NOT RE-RUN AFTER THE MERGE: the full app-shell + plugin-list suites — this round's diff touches only the types test file, and the merge is other people's already-landed main; left to CI.", "mcp_calls": "16 total — 10 in the first round, plus this round: two dedup searches (one returned 0, the other failed), one search retry (failed), one PR body update, one PR read-back, this comment. Two of those were rate-limit failures, see open_questions.", "open_questions": [ { "question": "RESOLVED BY PM — recorded for the trail. The card fenced ListView.tsx:2165 as an alias READ; I added the canonical rung there because measurement showed the producer change alone removes the Kanban capability. PM ruled A: keep it.", "options": ["A — keep the added gate rung (RULED, shipped)", "B — split it into its own PR", "C — emit both keys from the producer"], "recommendation": "A, as ruled. No alias read was removed; all three original rungs are intact and the pin ablation above re-proves the alias is still read." }, { "question": "A finding I could NOT file because the GitHub search endpoint is rate-limited for this account, so I could not run a dedup control — and an uncontrolled zero is not a reading. Handing it to the PM to file rather than filing it undeduped or dropping it silently. Should the verbatim source-text pin mechanism itself be reconsidered?", "options": [ "A — file a finding. `VIEW_LEVEL_ALIAS_SITES` in object-kanban-group-by-limit-7322.test.ts asserts substance ('this site still reads the view-level `groupField` alias') by matching a byte-exact snapshot of an unrelated file's source text, including indentation. Any legitimate reformat of that expression reddens it, and the fix is always in the test. This is the second time tonight a snapshot-of-elsewhere pin went stale on a legitimate edit — objectui#8212's ledger entries went stale when objectui#8186 landed a declaration; this one went stale on my rung.", "B — leave it. The mechanism fails SAFE (red, never silently green), and its verbatim-ness is partly the point: it forces a human to look at any edit to that expression, which is exactly what a tombstone boundary wants." ], "recommendation": "File it, but at low priority and with B stated in the body — the counter-argument is real and this is a cost-of-maintenance question, not a correctness bug. I did NOT rewrite the mechanism in this PR, per the dispatch instruction. Dedup search text I would have used: 'source text substring pin goes stale when an unrelated legitimate edit reformats the pinned expression' (returned total_count 0, but with NO control that fired, so that zero carries no information)." } ], "out_of_scope_findings": [ "filed as #8213: the view-level `groupBy` ObjectView writes into options.kanban is a third lane spelling that the spec's strict KanbanConfigSchema refuses by name (measured, with a control that fires); read by ListView's projection collectors so retiring it needs a producer census — deliberately not folded here, as the card directed", "NOT FILED, handed to the PM: the verbatim source-text pin mechanism in object-kanban-group-by-limit-7322.test.ts (see open_questions). Blocked on GitHub search rate-limiting, which made a dedup control impossible this round." ] }Generated by Claude Code