Repository navigation
Both grouped grids group by fields they never fetch — "By business unit" is one (empty) bucket holding every row #76
Description
Found by browser verification on merged main (19a0306) with the #75 seed loaded — 186 tasks across 5 business units.
The manager's org lens is broken, and it is broken in the way that reads as a data problem rather than a bug.
Measured
/_console/apps/ai.objectstack.duly/duly_task/view/by_unit renders one collapsible group:
Business unit
▾ (empty) 100
Every row is inside it. The rows themselves are correct — right records, right count, right sort. Only the grouping did nothing.
The data is fine. Straight from the store:
select business_unit, count(*) from duly_task group by 1
9a_MClo_4ByhoRgj 61
Q2UJSlOy4ICnWmTg 86
b2usyCHDAho0owsx 31
K36wAueKC2W1fz1k 7
BUZwsjpNFv-cbyZA 1 → 186 rows, 0 nulls
And the dashboard's Not moving, by business unit chart groups the same field on the same data correctly, rendering Northgate Operations and Northgate Quality as separate bars.
Cause
The grid builds its query projection from columns alone. Captured on the page load:
GET /api/v1/data/duly_task?populate=owner&top=100&select=id,subject,status,due_date,period_key,…
→ record keys: id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined
src/views/task.view.ts shares one columns const across every task lens:
const columns = [
{ field: 'subject' }, { field: 'status' }, { field: 'due_date' },
{ field: 'period_key' }, { field: 'owner' }, { field: 'source' },
];business_unit is not in it, so it is never requested, so it is undefined on arrival, so objectui's buildSegmentLabel hits its first line — if (value === undefined || …) return '(empty)' — for all 186 rows.
Both grouped views are affected, and duty is affected twice
| View | Groups by | In its columns? |
|---|---|---|
task.view.ts → by_unit |
business_unit |
❌ |
duty.view.ts → grouped lens |
business_unit |
❌ |
duty.view.ts → grouped lens |
owner |
❌ |
duty's columns are name, form, frequency, source, status — neither grouping field is among them, so that view collapses on both levels.
The kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns.
Filed upstream
objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than requiring authors to mirror them in columns. Two config keys that are separate in the schema imply grouping by a non-displayed field is supported, and it silently is not.
That is the real fix and it is not ours. Do not wait for it — the workaround here is also just better UI.
Scope
-
Add the grouping fields to the columns of both grouped views. On a by-unit view the unit column is worth showing anyway, so this is not a wart; on
duty's grouped lens, addbusiness_unitandowner. Keep the sharedcolumnsconst for the lenses that want the current six, and give the grouped lenses their own list rather than widening every view to carry a column only one of them needs. -
Add a test that fails when a view groups or kanban-groups by a field its
columnsdo not carry. This is the actual deliverable — the fix above is three lines and the guard is what stops it coming back. It belongs intest/metadata-bindings.test.tsalongside the other binding guards, and it must be written so it would have failed on19a0306: assert that before you fix the views, and say so in the test's comment.Note carefully why the existing guard did not catch this, because it is the interesting part:
test/metadata-bindings.test.tsresolvesbusiness_unitagainstduly_task's schema, and it resolves — it is a real, populated, correctly-typed lookup. Every binding was valid. The bug is a relationship between two config keys, not a dangling reference, and no amount of reference checking sees it. -
Leave
sortalone unless you can measure a failure. Sorting is applied server-side through the query, not client-side over the projection, so asortfield outsidecolumnsis very likely fine —stalledsorts bylast_update_at,schedulebyvisible_from, and both are outside the shared six. If you can show a sort silently not happening, that is a second finding and its own card; do not fold an unmeasured guess into this one, and do not widen the test tosortwithout evidence.
Acceptance
by_unitrenders one group per business unit, each labelled with the unit's name, counts summing to the visible row total- the
dutygrouped lens groups on both levels - a test that fails on
19a0306and passes after - verified in a browser against the seeded data, not only in tests — a screenshot of the grouped view in the PR
Gates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.
Activity
os-warren commentedon Sep 1, 2026
Claiming this issue.
session_01SqkTcrxUFci7nqXdbBSe2pclaude/issue-76-grouped-view-projection../duly-issue-76offmain(19a0306)Scope as filed: (1) give the two grouped lenses their own column lists carrying their grouping fields, (2) add the config-key-relationship guard to
test/metadata-bindings.test.ts, proven red on19a0306first, (3) leavesortalone. Referencing objectstack-ai/objectui#7179 in the code comment; not filing a duplicate and not waiting for it.Generated by Claude Code
os-warren commentedon Sep 1, 2026
{ "issue": 76, "status": "done", "branch": "claude/issue-76-grouped-view-projection", "pr": "https://github.com/objectstack-ai/duly/pull/85", "premise_still_valid": true, "summary": "Both grouped grids now carry their grouping fields as columns: task by_unit gets business_unit ([...columns, ...], the idiom stalled already uses), duty catalog_tree gets business_unit and owner in the grouping's own order. The shared six stay untouched for the other lenses, and objectui#7179 is referenced in both code comments. The deliverable is the guard: test/metadata-bindings.test.ts gains a second, independent stopgap (deleted with #7179, NOT with the #14105/#14107/#14108 walk) asserting a grouped grid carries its grouping fields in columns, proven red on 19a0306 first with the transcript recorded in the test's comment. ONE MEASURED DEVIATION from scope item 2: the card asks the guard to cover kanban-grouping on the reading that the board is 'fine by luck because status is a displayed column' — status is NOT one of the board's columns, and four browser ablation legs show kanban/gantt/timeline groupByField are each unioned into the $select by their own adapter, while the grid's grouping block is the only projection-blind one. Widening the guard to those three would fire on views that measurably work, which is the mistake item 3 warns about for sort, so the guard covers grouping.fields[] and INVENTORIES the rest behind a tripwire. Nothing was changed for sort. No changeset (repo has no changesets tooling; PM confirmed mid-task).", "tests": "All four gates run in the worktree at final commit df6b114 (= PR head): `pnpm validate` exit 0 (one expected warning: hierarchy-security is enterprise-edition, AGENTS.md rule 7), `pnpm typecheck` exit 0, `pnpm test` exit 0 — 20 files / 545 tests passed, `pnpm build` exit 0 (dist/objectstack.json 276.2 KB). Exit codes captured directly, never through a pipe. GitHub Actions `verify` on df6b114: completed / success. RED-FIRST: on 19a0306 with the views unfixed, `npx vitest run test/metadata-bindings.test.ts` exited 1 — 'expected [ …(3) ] to deeply equal []' naming by_unit grouping.fields[0], catalog_tree grouping.fields[0] and [1]; 42 other tests in the file passed, so only the new assertion moved. ABLATION (4 legs, browser, own dev server on :3100 with the #75 seed, admin login): the ObjectStack dev server compiles the artifact from source and hot-restarts, so each leg's rebuild is that restart, waited on by polling /_console/ for 200 before measuring; each mutation was confirmed ON DISK by anchored grep counts of the injected AND removed text (printed as 'injected=1 removed=0'), never by the editor's exit code, and each leg was independently confirmed at runtime by the changed `select` in the captured request. Leg A: board kanban.groupByField -> business_unit gives select=id,subject,due_date,owner,source,BUSINESS_UNIT,status (the group field is fetched with no column). Leg B: + business_unit in kanban.columns changes nothing (kanban.columns is the card face). Leg C: schedule gantt.groupByField -> business_unit gives select=...,due_date,BUSINESS_UNIT. Leg D: recent gains timeline.groupByField gives select=...,BUSINESS_UNIT,due_date. Every leg restored through a `trap '<git checkout -- src/views/task.view.ts>' EXIT INT TERM`; the tree was verified byte-identical to HEAD afterwards (`git diff --stat` empty) and the shipped PR is 3 files / +286 / -1, so no mutation leaked. BROWSER ACCEPTANCE: before — by_unit rendered one `(empty)` group holding all 186 rows and catalog_tree rendered `(empty)` nested in `(empty)` (31); after — by_unit renders Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, summing to 100, the visible row total, each labelled with the unit's NAME, and catalog_tree groups on both levels (Ardenline Group -> Nadia Ilves, Northgate Operations -> Marek Dvorak 2 / Sami Okonkwo 3). Zero `(empty)` labels, no page errors, no 4xx. Screenshots (before + after, both lenses) are embedded in the PR body from the throwaway branch claude/issue-76-evidence, so no binaries enter the diff or main.", "open_questions": [], "out_of_scope_findings": [ "filed as #86: grouped grid counts are page-scoped — by_unit groups the fetched 100 rows, so one of the five business units has no group at all and the header counts (33/3/46/18) are slices of the store's 61/86/31/7/1; labelled `finding`, unassigned, not fixed here (#76's own acceptance already treats the counts as page-scoped)" ] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
(Re-post: GitHub's body sanitizer ate the HTML-comment marker on the previous comment, which would have made this report invisible to the PM's scan. Same report, literal marker, one angle-bracketed fragment reworded so it survives. The comment above is superseded.)
{ "issue": 76, "status": "done", "branch": "claude/issue-76-grouped-view-projection", "pr": "https://github.com/objectstack-ai/duly/pull/85", "premise_still_valid": true, "summary": "Both grouped grids now carry their grouping fields as columns: task by_unit gets business_unit (spread of the shared six plus one, the idiom stalled already uses), duty catalog_tree gets business_unit and owner in the grouping's own order. The shared six stay untouched for the other lenses, and objectui#7179 is referenced in both code comments. The deliverable is the guard: test/metadata-bindings.test.ts gains a second, independent stopgap (deleted with #7179, NOT with the #14105/#14107/#14108 walk) asserting a grouped grid carries its grouping fields in columns, proven red on 19a0306 first with the transcript recorded in the test's comment. ONE MEASURED DEVIATION from scope item 2: the card asks the guard to cover kanban-grouping on the reading that the board is 'fine by luck because status is a displayed column' — status is NOT one of the board's columns, and four browser ablation legs show kanban/gantt/timeline groupByField are each unioned into the select by their own adapter, while the grid's grouping block is the only projection-blind one. Widening the guard to those three would fire on views that measurably work, which is the mistake item 3 warns about for sort, so the guard covers grouping.fields[] and INVENTORIES the rest behind a tripwire. Nothing was changed for sort. No changeset (repo has no changesets tooling; PM confirmed mid-task).", "tests": "All four gates run in the worktree at final commit df6b114 (= PR head): `pnpm validate` exit 0 (one expected warning: hierarchy-security is enterprise-edition, AGENTS.md rule 7), `pnpm typecheck` exit 0, `pnpm test` exit 0 — 20 files / 545 tests passed, `pnpm build` exit 0 (dist/objectstack.json 276.2 KB). Exit codes captured directly, never through a pipe. GitHub Actions `verify` on df6b114: completed / success. RED-FIRST: on 19a0306 with the views unfixed, `npx vitest run test/metadata-bindings.test.ts` exited 1 — 'expected [ …(3) ] to deeply equal []' naming by_unit grouping.fields[0], catalog_tree grouping.fields[0] and [1]; the other 42 tests in the file passed, so only the new assertion moved. ABLATION (4 legs, browser, own dev server on :3100 with the #75 seed, admin login): the ObjectStack dev server compiles the artifact from source and hot-restarts, so each leg's rebuild is that restart, waited on by polling /_console/ for 200 before measuring; each mutation was confirmed ON DISK by anchored grep counts of the injected AND removed text (printed as 'injected=1 removed=0'), never by the editor's exit code, and each leg was independently confirmed at runtime by the changed select in the captured request. Leg A: board kanban.groupByField to business_unit gives select=id,subject,due_date,owner,source,BUSINESS_UNIT,status (the group field is fetched with no column). Leg B: plus business_unit in kanban.columns changes nothing (kanban.columns is the card face). Leg C: schedule gantt.groupByField to business_unit gives select=...,due_date,BUSINESS_UNIT. Leg D: recent gains timeline.groupByField gives select=...,BUSINESS_UNIT,due_date. Every leg restored through a bash trap on EXIT INT TERM running `git checkout -- src/views/task.view.ts`; the tree was verified byte-identical to HEAD afterwards (`git diff --stat` empty) and the shipped PR is 3 files / +286 / -1, so no mutation leaked. BROWSER ACCEPTANCE: before — by_unit rendered one `(empty)` group holding all 186 rows and catalog_tree rendered `(empty)` nested in `(empty)` (31); after — by_unit renders Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, summing to 100, the visible row total, each labelled with the unit's NAME, and catalog_tree groups on both levels (Ardenline Group then Nadia Ilves; Northgate Operations then Marek Dvorak 2 / Sami Okonkwo 3). Zero `(empty)` labels, no page errors, no 4xx. Screenshots (before and after, both lenses) are embedded in the PR body from the throwaway branch claude/issue-76-evidence, so no binaries enter the diff or main.", "open_questions": [], "out_of_scope_findings": [ "filed as #86: grouped grid counts are page-scoped — by_unit groups the fetched 100 rows, so one of the five business units has no group at all and the header counts (33/3/46/18) are slices of the store's 61/86/31/7/1; labelled `finding`, unassigned, not fixed here (#76's own acceptance already treats the counts as page-scoped)" ] }Generated by Claude Code