Skip to content

Both grouped grids group by fields they never fetch — "By business unit" is one (empty) bucket holding every row #76

Closed

Description

@os-warren

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

  1. 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, add business_unit and owner. Keep the shared columns const 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.

  2. Add a test that fails when a view groups or kanban-groups by a field its columns do not carry. This is the actual deliverable — the fix above is three lines and the guard is what stops it coming back. It belongs in test/metadata-bindings.test.ts alongside the other binding guards, and it must be written so it would have failed on 19a0306: 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.ts resolves business_unit against duly_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.

  3. Leave sort alone unless you can measure a failure. Sorting is applied server-side through the query, not client-side over the projection, so a sort field outside columns is very likely fine — stalled sorts by last_update_at, schedule by visible_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 to sort without evidence.

Acceptance

  • by_unit renders one group per business unit, each labelled with the unit's name, counts summing to the visible row total
  • the duty grouped lens groups on both levels
  • a test that fails on 19a0306 and 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

  1. self-assigned this
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    Claiming this issue.

    • Session: session_01SqkTcrxUFci7nqXdbBSe2p
    • Branch: claude/issue-76-grouped-view-projection
    • Worktree: ../duly-issue-76 off main (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 on 19a0306 first, (3) leave sort alone. Referencing objectstack-ai/objectui#7179 in the code comment; not filing a duplicate and not waiting for it.


    Generated by Claude Code

  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor
    {
      "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

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions