Skip to content

Kanban, gantt and timeline lenses over the same task data #12

Closed

Description

@os-warren

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 — extend listViews (grid, calendar, my_week, late, stalled already exist there)
  • src/views/duty.view.ts — one tree lens
  • test/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 calendar view needs its calendar: { startDateField, titleField, colorField } block, and a gantt view its equivalent. Without it the renderer falls back to literal default field names and the view renders empty while authoring reports success — the existing calendar view in task.view.ts shows the correct shape.
  • Filters use stored columns. Late is due_date < {today} AND status IN (open, in_progress) — there is no is_late field 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 validate catches it.
  • Colour by status consistently 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 validate clean with no warnings
  • dragging a card between board columns writes status and nothing else
  • the gantt bar starts at visible_from, not at due_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

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

    @os-warren
    CollaboratorAuthor

    Claim: PM loop round 3
    Session: session_01SqkTcrxUFci7nqXdbBSe2p
    Branch: claude/issue-12-view-lenses
    Worktree: duly-issue-12
    File surface: src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts
    Container & model: M, mode:subagent, model: opus
    Clause-②: no
    Serial constraints cleared: round 3 is #42 / #8 / #9 / #12, file surfaces disjoint. #27 also needs src/apps/duly.app.ts and is deliberately held to a later round so the two do not collide there. Note src/views/task.view.ts gained bulkActionDefs in #41 (f7bdfdb) — leave those wired as they are; they are the declarative bulk write and not yours to restructure.


    Generated by Claude Code

  3. os-warren commented on Sep 1, 2026

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

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    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

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ACCEPT — PM review of #49, round 3.

    Gates re-run with origin/main merged: all four EXIT=0, 314 tests, validate with zero warnings. CI verify success on 997782a.

    I reproduced #14107, the biggest of the six. Renaming a view column to subjekt_typo:

    pnpm validate  EXIT=0   (no mention of the typo)
    pnpm build     EXIT=0
    

    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 nav viewName are 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_tree as grid + grouping instead of type: 'tree'. You found the platform's tree nests only via a self-parent pointer, duly_duty has no self-reference, and plugin-tree never 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 passes record[colorField] straight into backgroundColor, 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_tree grouped 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 — catalog and assigned duties are put there by the organisation, and a self duty 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.md so 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.

    board under My work rather than Team — agreed, and the reasoning is the product's: dragging a card writes status, and managers do not enter status.

    Merging.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions