Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fetch the fields the grouped grids group by, and guard it - #85

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection
Sep 1, 2026
Merged

Fetch the fields the grouped grids group by, and guard it#85
os-warren merged 1 commit into
mainfrom
claude/issue-76-grouped-view-projection

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#76

Both grouped grids bucketed by a field they never fetched. by_unit rendered one(empty) group holding all 186 rows; duty's grouped lens collapsed on both levels. The rows were right, nothing errored, and all four gates were green.

Cause, re-measured on this branch's own boot

The grid builds its query projection from columnsalone. Captured off the page load at 19a0306:

GET /api/v1/data/duly_task?select=id,subject,status,due_date,period_key,owner,source
→ business_unit: undefined (on all 186 rows)

business_unit was the grouping field and not a column, so it was never requested, so buildSegmentLabel took its first line — if (value === undefined || …) return '(empty)' — for every row. Read off the shipped console bundle, $select is [id, ...columns, ...fields referenced by action / conditional-formatting predicates]; grouping is not in that union.

Upstream: objectstack-ai/objectui#7179 — the projection should union the grouping fields rather than making authors mirror them in columns. Not waiting on it, and this is not a wart: on a by-X view the X column is worth showing anyway.

1. The views

  • task.view.ts › by_unit — the shared six plusbusiness_unit ([...columns, …], the same idiom stalled already uses). The other five task lenses keep the shared const untouched.
  • duty.view.ts › catalog_tree — its own list gains business_unit and owner, listed in the grouping's own order so the row reads the way the hierarchy nests.

2. The guard — the actual deliverable

test/metadata-bindings.test.ts gains a second, independent stopgap, deleted when objectui#7179 lands and not with the reference walk above it (that one goes with objectstack#14105 / #14107 / #14108).

Why the existing guard could not catch this: it resolves business_unit against duly_task's schema and it resolves — a real, populated, correctly-typed lookup. Every binding was valid. The defect is a relationship between two config keys, not a dangling reference.

Proven red first. On 19a0306 with the views unfixed, npx vitest run test/metadata-bindings.test.ts exited 1:

AssertionError: a grouped grid buckets rows by a field its `columns` do not carry …
+ "view duly_task › listViews.by_unit · grouping.fields[0].field groups by \"business_unit\",
columns: subject, status, due_date, period_key, owner, source"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[0].field groups by \"business_unit\", …"
+ "view duly_duty › listViews.catalog_tree · grouping.fields[1].field groups by \"owner\", …"

The transcript is recorded in the test's own comment, and synthetic fixtures pin both directions permanently (fires / does not fire, multi-level, bare-string column shorthand).

One measured deviation from the card's scope

The card asks the guard to cover kanban-grouping too, on the reading that the kanban board is "fine by luck" because its group field happens to be displayed. It is not luck, and status is not one of the board's columns. Ablated against a booted app one leg at a time, each mutation confirmed on disk and reverted through a restore trap:

legmutationresulting select
Aboard kanban.groupByFieldbusiness_unit…,source,business_unit,status
BA + business_unit added to kanban.columnsunchanged from A
Cschedule gantt.groupByFieldbusiness_unit…,due_date,business_unit
Drecent gains timeline.groupByField: business_unit…,business_unit,due_date

So kanban / gantt / timelinegroupByField are each unioned into the projection by their own adapter — the grid's grouping block is the one that is not, which is exactly the gap objectui#7179 names. The guard therefore covers grouping.fields[] only; widening it to three keys that measurably work would be the same mistake the card warns against for sort. The other group keys are still inventoried by the walk, so adding one fails a tripwire and lands in front of a human. Legs A/B also pin where a field must go: kanban.columns is the card face (cardFields), never the projection.

3. sort — left alone, as instructed

No measurement showed a sort failing, and sorting is applied server-side ($orderby in the same request), so a sort field outside columns is a different mechanism. Not touched, not guarded.

Browser verification

objectstack dev on port 3100 from this worktree, #75 seed (186 tasks, 31 duties, 5 business units), logged in as admin.

Before — one (empty) bucket holding every row:

before by_unit

before catalog_tree

After — by_unit, one group per unit, counts 33 + 3 + 46 + 18 = 100 = the visible row total (the seed's fifth unit has no rows on page 1 of 100):

after by_unit groups

after by_unit rows

After — duty's lens grouping on both levels (Ardenline Group → Nadia Ilves, Northgate Operations → Marek Dvorak 2 / Sami Okonkwo 3):

after catalog_tree

Post-fix requests: select=id,subject,status,due_date,period_key,owner,source,business_unit and select=id,name,form,frequency,business_unit,owner,source,status. Zero (empty) labels on either lens, no console errors, no 4xx.

The screenshots live on the throwaway branch claude/issue-76-evidence so no binaries enter this diff or main — delete it whenever.

Gates

All four green at df6b114 (this PR's 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
pnpm build exit 0 dist/objectstack.json 276.2 KB

No changeset: this repo has no .changeset/ directory in git log --all, no @changesets/* dependency and no changeset script — creating one would mint a mechanism nothing here reads.

Generated by Claude Code


Generated by Claude Code

`by_unit` rendered one `(empty)` group holding all 186 rows, and `duty`'s
grouped lens collapsed on both of its levels. The rows were right; only the
grouping did nothing, and nothing errored.
The grid builds its query projection from `columns` alone — measured on the
seeded app, the request was
`select=id,subject,status,due_date,period_key,owner,source`, so
`business_unit` arrived `undefined` and the renderer's `buildSegmentLabel`
returned `(empty)` for every row. Both grouped lenses now carry their own
grouping fields as columns; the lenses that want the shared six are
untouched.
Filed upstream as objectstack-ai/objectui#7179 — the projection should union
the grouping fields rather than making authors mirror them. Not waiting on
it: on a by-X view the X column is worth showing anyway.
The guard is the deliverable. `test/metadata-bindings.test.ts` gains a second,
independent stopgap: a grouped grid must carry its grouping fields in
`columns`. The existing walk cannot see this — it resolves `business_unit`
against the schema and it RESOLVES; the defect is a relationship between two
config keys, not a dangling reference. Proven red on 19a0306 before the fix,
with the transcript recorded in the test.
Scope is measured, not assumed: ablating each group key one at a time against
a booted app showed `kanban` / `gantt` / `timeline` `groupByField` are each
unioned into the projection by their own adapter, so the guard covers
`grouping.fields[]` only and inventories the rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

Reviewed — merging. The deviation from my scope is a correction to my card, and it is right.

Gates, re-run by me on df6b114 in a clean review worktree:validate 0, typecheck 0, test 0 (Test Files 20 passed, Tests 545 passed), build 0.

I wrote in scope item 2 that "the kanban groupByFields are fine by luck: status (board) and owner (assignments) both happen to be displayed columns." That was wrong on both halvesstatus is not one of the board's columns, and they are not fine by luck. Four browser ablation legs with the captured select= string in each show kanban, gantt and timeline each union their groupByField into the projection through their own adapter, needing no column at all; leg B further shows kanban.columns is the card face and has nothing to do with the fetch.

So the grid's grouping block is the only projection-blind path. Scoping the guard to grouping.fields[] and inventorying the other three behind a tripwire is exactly right: widening it would have made it fire on views that measurably work, which is the mistake item 3 warns about one paragraph earlier. Good catch that my own card contained the error item 3 was written to prevent.

Two more things worth recording:

  • Red-first was proven on 19a0306, not assertedexpected [ …(3) ] to deeply equal [] naming by_unit grouping.fields[0] and catalog_tree grouping.fields[0] and [1], with the other 42 tests in the file still passing so only the new assertion moved. And the guard is filed for deletion against objectui#7179, not against the #14105/#14107/#14108 walk — two stopgaps with different expiry dates, correctly kept apart.
  • The browser acceptance is the real proof. Before: by_unit one (empty) group holding all 186 rows, catalog_tree(empty) nested inside (empty). After: Northgate Operations 33 / Northgate Plant 3 / Northgate Quality 46 / Riverside Plant 18, each labelled with the unit's name, and catalog_tree grouping on both levels. Screenshots kept on a throwaway branch so no binaries enter the diff — the right call.

I have posted your kanban/gantt/timeline measurement to objectui#7179, since I had told the platform team those were worth checking and you have now checked them.

#86 (page-scoped group counts) is a real finding and correctly out of scope here — this card's own acceptance treated the counts as page-scoped, so fixing it would have been scope creep. Triaging it separately.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:10
@os-warren
os-warren merged commit 0f0ec49 into mainSep 1, 2026
1 check passed
os-warren added a commit that referenced this pull request Sep 1, 2026
`by_unit` carried no filter, so it rendered all 186 tasks — 151 of them
`done` — against a `top=100` request. The grid groups client-side over the
fetched page and computes its per-group counts over that same array, so
every group header read a page slice as a total and one of the five
business units had no group on the screen at all, with nothing saying a
unit was missing.
Measured in a browser on the seeded app, same server and session, with
only this filter reverted:
before Operations 33 · Plant 3 · Quality 46 · Riverside 18
= 100, "Showing first 100 records", Central Office ABSENT
after Central Office 1 · Operations 7 · Plant 1 · Quality 18 ·
Riverside 6 = 33 records, five groups, no truncation
It is also the better lens on its merits — nobody wants a by-unit
breakdown of work that finished six months ago. Not a bigger page size,
which moves the cliff instead of removing it.
The residual is stated rather than papered over: this stays structurally
page-scoped, so the view's `description` says the dashboard is the
authoritative by-unit surface and this lens is for browsing. That
description is served but NOT rendered today — the console's ObjectView
relay never copies a per-view `description` onto the ListView schema —
filed as objectstack-ai/objectui#7199 and said plainly in the view
comment so nobody believes the caveat is in front of users yet.
The guard pins the decision and inventories the other grouped grids, in
the same file and idiom as #85's — `catalog_tree` is measured at 31
duties and deliberately not required to carry a filter.
Upstream: objectstack-ai/objectui#7189.
Closes#86
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-warren