Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e 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

Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

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

Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

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 \u003e 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

Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

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

Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

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

Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

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

Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

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

Scope the schedule (gantt) lens to open work - #98

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work
Sep 1, 2026
Merged

Scope the schedule (gantt) lens to open work#98
os-warren merged 2 commits into
mainfrom
claude/issue-96-schedule-open-work

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#96

Scopes the schedule (gantt) lens to status IN ('open','in_progress'), mirroring what #91 did to by_unit, and extends #91's grouped-lens scope inventory to cover the groupByField lenses rather than adding a second mechanism beside it.

The card's diagnosis was checked in a browser first and does not hold. That is the main thing a reviewer should read before the rest: the gantt is not page-scoped, and no owner group was missing. The filter is still right, for the reason the triage comment gave — a schedule of finished work is not what this screen is for — but it is not repairing what the card thought it was, and neither the view comment nor the guard message claims it is.

What the browser actually showed

pnpm demo, seeded store, /_console/apps/ai.objectstack.duly/duly_task/view/schedule, before any edit. One page load issues two fetches of the same filtered set:

?populate=owner&top=100&select=…&filter=… → 100 rows, total 186, hasMore true ← the footer
?sort=visible_from&filter=… → 186 rows, total 186, hasMore false ← the chart

The chart is served by the non-grid fetch, which sends no top at all, and @objectstack/rest applies no cap when limit is absent — so it receives the whole result set. Counted off the DOM rather than inferred: 186 task rows + 12 group rows = 198 × 40px = the 7920px the task list scrolls. The before screenshot shows the same thing without arithmetic — the Dev Admin group row reads 1/31 → 12/31, and 12/31 is six months past the 100-row page's own max(due_date) of 6/30.

So on this console build the timeline range covered the true 2026-01-31 → 2026-12-31 span, all 12 owners with scheduled work had a group, and the 100 records · Showing first 100 records. More data may be available. footer was printed under a chart that was in fact complete. The footer is the ListView chrome reporting its own paged fetch; it describes neither the chart above it nor the store.

Filed upstream separately — see "Upstream" below. objectui#7189 is about useGroupedData grouping over a fetched page on a grid, which is a different code path from anything measured here, so this is not a gantt half of it.

The card's second open question, settled

Whether an owner group is actually being dropped on today's seed.

Enumerated by scrolling the virtualised task list end to end and collecting every gantt-task-row-__group__* row, against the owners the seed gives scheduled open work to (counted server-side over the whole store, not off the screen):

owner groups the lens rendersowners with scheduled open workmissing
before1212none
after1212none

Name for name, both directions: Ana Ferreira, Dev Admin, Elin Halvorsen, Greta Lindqvist, Ibrahim Chaudhry, Marek Dvorak, Owen Pryce, Priya Raman, Rosa Delgado, Sami Okonkwo, Tomas Bergh, Yuki Tanabe. No owner is missing after the change, so this card does not halt.

Worth recording because it is the near-miss: had the gantt consumed the paged fetch, Owen Pryce is the owner who would have vanished. His single scheduled task falls outside the 100-row page, and the page's 11 owner buckets are exactly the 12 less him.

After

33 rows (27 open + 6 in_progress), hasMore: false, 12 group rows, 33 records in the footer with the "more data may be available" line gone, and 33 + 12 = 45 × 40px = 1800px of task list.

beforeafter
beforeafter

Screenshots live on a throwaway branch (claude/issue-96-shots, an orphan commit holding the two PNGs), so no binaries enter this diff.

One side effect, stated because the file explicitly warns against it. The gantt block's comment says no filter here should be narrowed to make objectui#7203's month label look better. This narrowing moves it anyway: the toolbar formats min(visible_from) - 7d, so the label goes January 2026June 2026. It is a side effect and not a reason — the label is still wrong, only about a different month now — and objectui#7203 remains open and untouched by this PR.

The guard

Extends #91's walk instead of adding a second one, and keeps its idiom: pin the named decisions, inventory the rest. No rule that every grouped view must be filtered — that rule has now been measured firing on a healthy view twice (catalog_tree groups two levels with no filter and needs none; board groups bystatus, where a status scope would delete columns from the board).

The walk now reads both grouping mechanisms — the grid grouping block and the kanban/gantt/timelinegroupByField (the same three blocks the projection guard above it already inventories) — and each inventory line names which one it is about:

view duly_duty › listViews.catalog_tree · grid grouping: business_unit, owner · status scope: (none)
view duly_task › listViews.board · kanban.groupByField: status · status scope: (none)
view duly_task › listViews.by_unit · grid grouping: business_unit · status scope: in_progress, open
view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open

The two pins have different deletion conditions, which is recorded in the comment because it matters to whoever eventually removes this stopgap: by_unit is scoped because grid grouping is page-scoped, so it retires with objectui#7189; schedule is scoped because a gantt of finished work is not what the screen is for, and no platform fix retires that.

Proven red before the fix

The fix committed first, then src/views/task.view.ts alone restored to its pre-fix state through a script carrying an EXIT INT TERM restore trap, with the mutation confirmed on disk by anchored counts before the run (open-work filter rules present: 4by_unit's survives, schedule's is gone — and schedule scope block present: 0). npx vitest run test/metadata-bindings.test.ts exited 1 on exactly the two intended assertions, 53 others still passing:

 ❯ test/metadata-bindings.test.ts (55 tests | 2 failed)
× `schedule` carries the open-work status filter that makes it a schedule
× inventories every grouped lens and the scope it carries
- "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: in_progress, open"
+ "view duly_task › listViews.schedule · gantt.groupByField: owner · status scope: (none)"

The restore leg was verified byte-identical to the committed file (git diff --quiet HEAD -- src/views/task.view.ts), so the numbers above come from the tree this PR actually ships.

The residual, not overclaimed

This lens is not currently page-scoped, so there is no page-scoping to remove. What stays true:

  • The paged fetch is real, still runs, and the footer reads off it. Nothing in this repo decides which of the two the chart consumes, so a console change pointing the gantt at the paged one would make the card's mechanism real — and on a gantt it lands harder than on a grid, because the timeline range is derived from the rows in hand: the chart would draw the wrong span rather than merely omit bars.
  • Scoping keeps this deployment's result set inside a page either way. That is a size, not a mechanism.
  • Not a page-size raise, per the reasoning on The by_unit lens shows 151 finished tasks and pages out a whole business unit — scope it to open work #86: it moves the cliff instead of removing it, and on the evidence above the page size is not what holds this chart together.

Upstream

  • objectui#7189 — commented, not duplicated. Added the measurement that bounds its scope: the gantt does not share useGroupedData's page-scoping, so #7189 stays a grid card.
  • New objectui card for the two-fetch/footer mismatch measured here — a gantt lens issuing both a paged fetch (which feeds a footer that then misdescribes the chart) and an unbounded one (which has no ceiling at all). Number in the report; it is a distinct defect from #7189 rather than another instance of it.
  • objectui#7203 and objectui#7204 are the two rendering defects already recorded on this screen. Both remain open; out of scope here.

Gates

All four green on b3ea92f, the merged head this PR ships (origin/main advanced to #97 mid-task; merged in and re-run, since #97 edits the same test file):

✓ Validation passed (364ms)
Test Files 26 passed (26)
Tests 664 passed (664)
✓ Build complete (654ms)

pnpm typecheck printed no diagnostics. No changeset: this repo has no changeset mechanism, the four gates are the whole contract.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 11:43
A manager opens the schedule to see an overloaded fortnight before it
arrives; nothing about that question involves work that is already done.
On the demo seed 151 of the 186 scheduled tasks are `done`, so the lens
spent nearly all of its height drawing finished bars.
Extends #91's grouped-lens scope inventory rather than adding a second
mechanism: the walk now covers the kanban/gantt/timeline `groupByField`
lenses alongside the grid `grouping` block, names which mechanism each
line is about, and pins the two lenses whose scope is a decision. It is
still a pin-and-inventory, not a rule that every grouped view must be
filtered — that rule fires on `catalog_tree` and on `board`, both fine.
The card's diagnosis was checked in a browser first and does NOT hold:
the gantt is not page-scoped. Its chart is served by the non-grid fetch,
which sends no `top`, so it drew all 186 rows and all 12 owner groups
over the full span, while a separate `top=100` fetch fed the footer
beneath it. No owner group was missing before this change. The view
comment and the guard message say so, so that neither is later cited as
evidence for something that was not measured.
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. My triage asserted a mechanism and you falsified it; that is the right outcome and the report handles it correctly.

Gates on the merged head b3ea92f:validate 0, typecheck 0, test 0 (Test Files 26, Tests 664), build 0, and the filter verified present in dist/objectstack.json at /views/0/listViews/schedule.

The correction

I wrote in the triage that "both derivations run over the fetched page". That is wrong for the gantt. You measured, before editing anything, that one page load issues two fetches of the same set: a top=100 one that feeds only the ListView footer, and an unbounded one that feeds the chart. The chart drew all 186 rows and all 12 owner groups over the true 2026-01-31 → 2026-12-31 span, with 186 + 12 = 198 × 40px matching the 7920px the task list scrolled. So nothing was being dropped, and the card's second open question settles as no.

That also means the residual I asked you to state was not a description of today — and restating it as "what would happen if the chart ever consumed the paged fetch" is the honest form. Writing the falsification into the view comment and the guard's failure message is the part I would have missed: it stops either being cited later as evidence for something nobody measured.

The remedy survives because its justification never rested on the mechanism — 151 of 186 scheduled tasks are done, and a gantt of finished work answers no question anyone opens this screen with. The filter also removes a "more data may be available" footer that was printed under a complete chart, which is its own small lie.

And the near-miss is the detail that makes this more than an academic correction: Owen Pryce's single scheduled task falls outside the 100-row page, so had the chart consumed that fetch, he is precisely the owner who would have vanished. The hazard was real; the wiring just happened not to reach it.

Your two judgment calls

  1. Keep Fixes #96. The corrected diagnosis lives in the PR body, the view comment and the guard's message — three places that travel with the code. A card held open as a record is a card someone has to triage again later; git history is the record.
  2. Merging main mid-task and re-running all four gates on the merged head was right, and Collapse views.test.ts's binding stopgap into metadata-bindings.test.ts #97 touching the same test file is exactly when it matters.

The upstream finding is the better one

objectui#7210 — a gantt lens fetching its rows twice, once paged into a footer that then misdescribes the chart beside it, and once unbounded with no ceiling reachable from view metadata — is a more serious report than the card that produced it. Today that unbounded fetch is 186 rows; in a real deployment it is however many scheduled tasks exist, with nothing an author can write to bound it. Searching first and confirming it is not a duplicate of #7189/#7203/#7204 (and that #6419/#6460 are closed) is what makes it worth someone's time.

Commenting on #7189 to bound it to the grid rather than opening a gantt half was also right — the gantt does not share useGroupedData's page-scoping, and an issue that overstates its own scope gets fixed in the wrong place.

Extending #91's inventory rather than adding a second guard, and holding the line against a blanket "every grouped view must be filtered" rule, keeps that file coherent — this is the third card to measure that such a rule would fire on views that are fine.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 11:56
@os-warren
os-warren merged commit 75d086f into mainSep 1, 2026
1 check passed
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.

The schedule (gantt) lens is page-scoped at 100 rows, so its timeline range and owner groups are computed over a slice

1 participant

@os-warren