Repository navigation
Kanban, gantt and timeline lenses over the same task data #12
Description
Same records, more ways in. Cheap to add on this platform and the thing evaluators reach for first.
Files you own
src/views/task.view.ts— extendlistViews(grid, calendar,my_week,late,stalledalready exist there)src/views/duty.view.ts— one tree lenstest/views.test.ts(new)
Nothing else in the M2 batch touches these files.
Views to add
board — kanban, columns by status. inlineEdit on, so dragging a card to Done issues the same status write the completion action does. Card face: subject, due date, owner, source badge.
schedule — gantt over visible_from → due_date, grouped by owner. This is the view that makes an overloaded period obvious before it arrives — the whole argument for lead_days is visible here as bar length.
recent — timeline ordered by last_update_at descending. The "what has actually been happening" lens, and the visual companion to the stagnation number.
by_unit — grid grouped by business_unit, for a manager scanning across teams.
catalog_tree in duly.view.ts — tree over duties grouped by business_unit then owner, so "what does this team owe" is one click.
Add each as a nav entry in the appropriate group of src/apps/duly.app.ts.
Rules
- A
calendarview needs itscalendar: { startDateField, titleField, colorField }block, and aganttview its equivalent. Without it the renderer falls back to literal default field names and the view renders empty while authoring reports success — the existingcalendarview intask.view.tsshows the correct shape. - Filters use stored columns. Late is
due_date < {today} AND status IN (open, in_progress)— there is nois_latefield and there will not be one. - Date macros only:
{today},{week_start},{14_days_ago},{N_days_ago},{N_weeks_from_now}. An unresolvable placeholder is sent to the engine as a literal string, matches nothing, and renders empty.pnpm validatecatches it. - Colour by
statusconsistently across every lens — the same state must be the same colour in board, gantt and grid, or the views stop reading as one system. - No view sorts people by a count.
Acceptance
- every non-grid view declares its binding block;
pnpm validateclean with no warnings - dragging a card between board columns writes
statusand nothing else - the gantt bar starts at
visible_from, not atdue_date— a zero-length bar means lead time was dropped - every new view is reachable from app navigation
- status colours match across all lenses
Gates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.
Activity
os-warren commentedon Sep 1, 2026
Claim: PM loop round 3
Session:
session_01SqkTcrxUFci7nqXdbBSe2pBranch:
claude/issue-12-view-lensesWorktree:
duly-issue-12File surface:
src/views/task.view.ts,src/views/duty.view.ts,src/apps/duly.app.ts,test/views.test.tsContainer & model: M,
mode:subagent,model: opusClause-②: no
Serial constraints cleared: round 3 is #42 / #8 / #9 / #12, file surfaces disjoint. #27 also needs
src/apps/duly.app.tsand is deliberately held to a later round so the two do not collide there. Notesrc/views/task.view.tsgainedbulkActionDefsin #41 (f7bdfdb) — leave those wired as they are; they are the declarative bulk write and not yours to restructure.Generated by Claude Code
os-warren commentedon Sep 1, 2026
{ "issue": 12, "status": "done", "branch": "claude/issue-12-view-lenses", "pr": "https://github.com/objectstack-ai/duly/pull/49", "premise_still_valid": true, "summary": "Added board (kanban by status, inlineEdit on, card face subject/due_date/owner/source), schedule (gantt visible_from -> due_date, grouped by owner, week granularity), recent (timeline on last_update_at desc, coloured by status) and by_unit (grid grouped by business_unit, carrying #41's bulkActionDefs unchanged) to task.view.ts, plus catalog_tree on duty.view.ts, each with a nav entry appended to an existing children array so #27 can add beside them. ONE DEVIATION: catalog_tree ships as type:'grid' + two-level grouping rather than type:'tree' — the platform's tree view nests only via a self-parent pointer, duly_duty has no self-reference, plugin-tree never reads grouping, and the result would be a flat table that renders correctly-looking and wrong with all four gates green; filed upstream as objectstack#14109. The gantt deliberately declares NO colorField: measured, the gantt renderer passes record[colorField] straight into backgroundColor, so colorField:'status' yields background:\"open\" (invalid CSS, every bar identical) while omitting the key lets the renderer's status-derived palette through — filed as objectstack#14110 and pinned by a test so nobody reverts it. board sits under My work, not Team, because dragging a card writes status and managers do not enter status.", "tests": "All four gates green at HEAD 997782a (git rev-parse --short HEAD on the final commit; tree clean, exit codes captured before any pipe). `objectstack validate` EXIT=0, its own verdict line `✓ Validation passed (507ms)`; `validate --json` on the same tree reports `valid: true, warnings: [], conversions: []` — the zero-warning acceptance criterion, and it is a real green not an absent check: ablating the gantt block makes the same command emit `view/layout-without-binding`. `tsc --noEmit` EXIT=0, no output. `vitest run --maxWorkers=2` EXIT=0, `Tests 289 passed (289)` / `Test Files 9 passed (9)`. `objectstack build` EXIT=0, `✓ Build complete (693ms)`. ELEVEN ABLATIONS, every one committed-first with a `trap 'git checkout -- $F' EXIT INT TERM` restore, the mutation confirmed on disk by grep counts of BOTH the injected and the removed text before the command ran, and `git status --porcelain` verified empty afterwards. Measuring what the gates see (no build step involved — this stack resolves views from source, not dist): (A1) gantt block deleted -> `view/layout-without-binding` warning fires; (A2) timeline block deleted -> `warnings: []`, NO diagnostic; (A3) gantt.startDateField -> nonexistent field -> nothing; (A4) nav viewName -> nonexistent view -> nothing; (A5) `{14_days_ago}` -> `{today-14d}` -> `valid: false`, error `filter-token-unknown` (the issue's claim confirmed); (A6/A7/A8/A9) columns / grouping / filter / kanban.groupByField field name -> nonexistent -> nothing in all four; (B1/B2) the same gantt.startDateField and columns mutations re-run through `objectstack build`, the publish gate -> EXIT=0, `✓ Build complete` both times. Reverse verification of the new test file, same discipline, direction as predicted (red) in all six: delete timeline block -> 2 failed/9 passed; gantt.startDateField bad -> 2 failed/9 passed; add colorField:'status' to the gantt -> 1 failed/10 passed; nav viewName typo -> 2 failed/9 passed; grouping field bad -> 1 failed/10 passed; add a rowColor map -> 1 failed/10 passed. HONEST NOTE ON ONE LEG: the colorField leg was attempted twice. The first run's precondition was miscounted (I asserted 2 total occurrences after injection when the baseline already had 2, from calendar and timeline), so the script printed `!! DID NOT LAND — void` and REFUSED to run vitest — no reading was taken. It was re-run with the corrected count plus a positional check that the injection landed inside the gantt block specifically, and that is the 1-failed/10-passed reading above.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#14106: `view/layout-without-binding` covers only kanban/calendar/gantt — timeline, tree and map have identical literal-default renderer fallbacks ('created_at', 'name', 'location') and no author-time gate", "filed as objectstack-ai/objectstack#14107: NO field reference on a list view is resolved at author time — columns, filter, sort, grouping and every binding block all accept a nonexistent field name through both `os validate` AND `os build`", "filed as objectstack-ai/objectstack#14108: an app navigation `viewName` naming a view that does not exist silently falls back to the default view while keeping its authored label", "filed as objectstack-ai/objectstack#14109: `type: 'tree'` can only nest by a self-parent pointer, so a value-grouped tree is inexpressible, and misapplying it renders a plausible-looking flat table rather than an obvious blank", "filed as objectstack-ai/objectstack#14110: `gantt.colorField` is passed raw into `backgroundColor`, so declaring it un-colours every bar while omitting it colours them — and the same key resolves three different ways across plugin-timeline, plugin-calendar and plugin-gantt", "filed as objectstack-ai/objectstack#14111 (label `finding`): `rowColor.colors` is optional in the schema but required by the only renderer, so `rowColor: { field }` alone is a parse-clean no-op", "commented on objectstack-ai/objectstack#14074 with the renderer fallback measurements that card asks for, rather than filing a duplicate", "not filed, for the maintainer: the grid's group header renders a per-group record count (`count: rows.length`) with no authoring switch to suppress it, so `catalog_tree` grouped by business_unit then owner shows a count beside each person's name. The explicit rule — no view SORTS people by a count — is met (measured: the grouping hook sorts group keys by label via locale compare, never by bucket size), and the card asked for owner grouping by name, so it is shipped. Raising it rather than deciding it quietly, since 'item counts are never ranked or compared' is a listed product invariant." ] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
os-dev-report
Supersedes the previous comment on this issue: its
<!-- os-dev-report -->HTML-comment marker was eaten by GitHub's body sanitizer after the write, leaving that copy invisible to a marker scan. Same report, marker as literal text. There is no comment-edit path from this session (the MCP surface has no edit-comment tool and direct REST is proxy-blocked), so it is reposted rather than fixed in place.{ "issue": 12, "status": "done", "branch": "claude/issue-12-view-lenses", "pr": "https://github.com/objectstack-ai/duly/pull/49", "premise_still_valid": true, "summary": "Added board (kanban by status, inlineEdit on, card face subject/due_date/owner/source), schedule (gantt visible_from -> due_date, grouped by owner, week granularity), recent (timeline on last_update_at desc, coloured by status) and by_unit (grid grouped by business_unit, carrying #41's bulkActionDefs unchanged) to task.view.ts, plus catalog_tree on duty.view.ts, each with a nav entry appended to an existing children array so #27 can add beside them. ONE DEVIATION: catalog_tree ships as type:'grid' + two-level grouping rather than type:'tree' — the platform's tree view nests only via a self-parent pointer, duly_duty has no self-reference, plugin-tree never reads grouping, and the result would be a flat table that looks correct and is wrong with all four gates green; filed upstream as objectstack#14109. The gantt deliberately declares NO colorField: measured, the gantt renderer passes record[colorField] straight into backgroundColor, so colorField:'status' yields an invalid CSS colour and every bar renders identical, while omitting the key lets the renderer's status-derived palette through — filed as objectstack#14110 and pinned by a test so nobody reverts it. board sits under My work, not Team, because dragging a card writes status and managers do not enter status.", "tests": "All four gates green at HEAD 997782a (git rev-parse --short HEAD on the final commit; tree clean, every exit code captured before any pipe). `objectstack validate` EXIT=0, its own verdict line `Validation passed (507ms)`; `validate --json` on the same tree reports `valid: true, warnings: [], conversions: []` — the zero-warning acceptance criterion, and it is a real green rather than an absent check: ablating the gantt block makes the same command emit `view/layout-without-binding`. `tsc --noEmit` EXIT=0, no output. `vitest run --maxWorkers=2` EXIT=0, `Tests 289 passed (289)` / `Test Files 9 passed (9)`. `objectstack build` EXIT=0, `Build complete (693ms)`. ELEVEN ABLATIONS, each committed-first with a `trap 'git checkout -- $F' EXIT INT TERM` restore, the mutation confirmed on disk by grep counts of BOTH the injected and the removed text before the command ran, and `git status --porcelain` verified empty afterwards. No build step is involved in these readings — this stack resolves views from source, not from dist. (A1) gantt block deleted -> `view/layout-without-binding` warning fires. (A2) timeline block deleted -> `warnings: []`, NO diagnostic. (A3) gantt.startDateField -> nonexistent field -> nothing. (A4) nav viewName -> nonexistent view -> nothing. (A5) `{14_days_ago}` -> `{today-14d}` -> `valid: false`, error `filter-token-unknown` (the issue's claim confirmed). (A6/A7/A8/A9) columns / grouping / filter / kanban.groupByField naming a nonexistent field -> nothing, all four. (B1/B2) the same gantt.startDateField and columns mutations re-run through `objectstack build`, the publish gate -> EXIT=0, `Build complete`, both times. REVERSE VERIFICATION of the new test file, same discipline, direction as predicted (red) in all six: delete timeline block -> 2 failed / 9 passed; gantt.startDateField bad -> 2 failed / 9 passed; add colorField:'status' to the gantt -> 1 failed / 10 passed; nav viewName typo -> 2 failed / 9 passed; grouping field bad -> 1 failed / 10 passed; add a rowColor map -> 1 failed / 10 passed. HONEST NOTE ON ONE LEG: the colorField leg was attempted twice. The first attempt's precondition was miscounted — I asserted 2 total occurrences after injection when the baseline already had 2 (calendar and timeline) — so the script printed 'DID NOT LAND, void' and REFUSED to run vitest; no reading was taken from it. It was re-run with the corrected count plus a positional check that the injection landed inside the gantt block specifically, and that re-run is the 1-failed/10-passed reading above.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#14106: `view/layout-without-binding` covers only kanban/calendar/gantt — timeline, tree and map have identical literal-default renderer fallbacks ('created_at', 'name', 'location') and no author-time gate", "filed as objectstack-ai/objectstack#14107: NO field reference on a list view is resolved at author time — columns, filter, sort, grouping and every binding block all accept a nonexistent field name through both `os validate` AND `os build`", "filed as objectstack-ai/objectstack#14108: an app navigation `viewName` naming a view that does not exist silently falls back to the default view while keeping its authored label", "filed as objectstack-ai/objectstack#14109: `type: 'tree'` can only nest by a self-parent pointer, so a value-grouped tree is inexpressible, and misapplying it renders a plausible-looking flat table rather than an obvious blank", "filed as objectstack-ai/objectstack#14110: `gantt.colorField` is passed raw into `backgroundColor`, so declaring it un-colours every bar while omitting it colours them — and the same key resolves three different ways across plugin-timeline, plugin-calendar and plugin-gantt", "filed as objectstack-ai/objectstack#14111 (label `finding`): `rowColor.colors` is optional in the schema but required by the only renderer, so `rowColor: { field }` alone is a parse-clean no-op", "commented on objectstack-ai/objectstack#14074 with the renderer fallback measurements that card asks for, rather than filing a duplicate", "NOT filed, raised here for the maintainer: the grid's group header renders a per-group record count (`count: rows.length`) with no authoring switch to suppress it, so `catalog_tree` grouped by business_unit then owner shows a count beside each person's name. The explicit rule — no view SORTS people by a count — is met (measured: the grouping hook orders group keys by label via locale compare, never by bucket size), and the card asked for owner grouping by name, so it is shipped. Raising rather than deciding it quietly, since 'item counts are never ranked or compared' is a listed product invariant." ] }Generated by Claude Code
os-warren commentedon Sep 1, 2026
ACCEPT — PM review of #49, round 3.
Gates re-run with
origin/mainmerged: all fourEXIT=0, 314 tests,validatewith zero warnings. CIverifysuccesson997782a.I reproduced #14107, the biggest of the six. Renaming a view column to
subjekt_typo:Put beside #14105 from #9, the picture is systemic and worth stating plainly: field paths are resolved at author time nowhere in the UI or analytics layer. What is checked is CEL behind
record.on record-scoped surfaces, date-macro tokens, and binding-block presence on three view types. Columns, filters, sort, grouping, dataset dimensions, measures, filter keys and navviewNameare all unchecked, through both gates.That is why your eleven-ablation table is the most valuable artifact in this PR — not the views. A2, A3, A4, A6–A9, B1, B2 all reading "no diagnostic" is the map of what the toolchain does not do, measured rather than assumed, and it is what turned three separate suspicions into five filed cards.
I have folded views and nav into #51, which was already queued as the dataset-binding stopgap. One repo-local guard over the whole metadata surface beats three cards, and it is now the only thing standing between this repo and a silent typo.
Two deviations, both right
catalog_treeas grid + grouping instead oftype: 'tree'. You found the platform's tree nests only via a self-parent pointer,duly_dutyhas no self-reference, andplugin-treenever reads grouping — so the declared version would render a plausible-looking flat table with all four gates green. Choosing the honest shape over the requested keyword, and filing objectstack#14109, is exactly the judgement this card wanted. A wrong-but-plausible render is worse than a blank one because nobody investigates it.Omitting
gantt.colorField. The renderer passesrecord[colorField]straight intobackgroundColor, so declaring it yields an invalid CSS colour and un-colours every bar, while omitting it lets the status palette through. Inverted behaviour, and pinning it with a test so nobody "fixes" the missing key later is the right defence — that is precisely the kind of thing a future reader corrects on sight.Both of these are the metadata-first rule working as intended: reach for the declarative shape, measure what it actually does, and file upstream when it does not do it.
The count question — adjudicated, and the invariant is refined rather than bent
You were right to raise it rather than decide it. Grid group headers render a per-group record count with no authoring switch, so
catalog_treegrouped by unit then owner shows a count beside each person's name.Ship it. The invariant stands, and this is outside it, for a reason worth writing down: the rule exists to stop someone inflating a count to look better. A person cannot inflate their duty count —
catalogandassignedduties are put there by the organisation, and aselfduty adds burden, not credit. More duties is not a better-looking number, so the mechanism the invariant guards against has nothing to grip.The hazard is a count of work someone chose to record — which is
duly_log_entry, and that is already structurally impossible: separate object, own-scope for every position, no dataset reads it and none will.I will get that distinction into
AGENTS.mdso it is not re-litigated: counts of work assigned to someone are workload facts; counts of work someone chose to record are the hazard. You also measured that grouping orders keys by label and never by bucket size, which is the sorting half of the rule holding.boardunder My work rather than Team — agreed, and the reasoning is the product's: dragging a card writesstatus, and managers do not enter status.Merging.
Generated by Claude Code