Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- **CHANGE (breaking, ALPHA) — the `project show` / `get_project` view dissolves its live child buckets into ONE flat `items[]`.** Second of five PRs implementing the read-shape doctrine (`docs/design/read-shape-doctrine.md`, §3.12): a heading's membership is a per-row attribute, not a bucket.
- **`data.view` shape (breaking).** The unheaded `anytime` / `upcoming` / `someday` buckets AND the per-heading stage sub-buckets are GONE. In their place, `data.view.items` is ONE flat list of every LIVE child (unheaded and headed) in project `index` order — each row carrying `stage` (KEPT; the list is stage-mixed) and `when`, and each HEADED row carrying its `heading` ref (a flat title + a project-scoped `headingUuid` when the title would not round-trip, kept even in compact). Reconstruct a heading's members by filtering `items` on `heading`. An unheaded row omits `heading`.
- **`headings[]` is now the memberless live-heading catalog** — `[{heading:{uuid,title,archived?}}]` in index order (incl. empty headings; an archived-but-unswept heading still appears here carrying `archived`). Its per-heading member sub-buckets are gone (membership rides the row refs). PR 3 will unwrap it to `[{uuid,title,archived?}]`, merge swept archived headings in, and dissolve `logbookHeadings`.
- **Unchanged this PR:** the flat `logbook` rows, the `logbookHeadings` archived-heading groups, `openChildrenWhileResolved` / `openChildrenUnderArchivedHeading`, and the project card node.
- **TTY byte-stable.** `project show` output is byte-for-byte identical (verified by before/after capture on a rich fixture + the full render suite). The library retains its structured render grouping (it owns the clock + `todayIndex`) to project the GUI-faithful layout; only the wire flattened. (`src/read/project-view.ts` adds `items`, `src/read/shape.ts` `shapeProjectView`.) Tests: `test/unit/shape.test.ts` (flat items[] + memberless catalog + heading refs), `test/cli/e2e.test.ts` (CLI wire shape), `test/unit/stage.test.ts` (per-row stage property), `test/mcp/server.test.ts` (get_project). Docs: `docs/design/read-shape-doctrine.md`, `docs/contract.md`, `docs/design/contracts.md`. No new `apiVersion`; no new error code.

- **CHANGE (breaking, ALPHA) — the `today` view dissolves its Today/This-Evening `sections` into one flat `items[]`, and its count moves to `meta.counts`.** First of five PRs implementing the ratified read-shape doctrine (`docs/design/read-shape-doctrine.md`): JSON models semantic state, the TTY projects GUI placement. The Today-proper / This-Evening split is GUI placement recoverable per row from `when`, so it is no longer a wire bucket:
- **`data` shape (breaking).** `today`'s `data` is now `{ items }` — one flat list of Today members in comparator order (Today-proper and This-Evening interleaved by their SQL position), each row carrying its `when` (`"today"` / `"evening"`) and `provisional`; `stage` is still dropped (every Today member is stage-pure `anytime`, R13). The former `data.sections` (`[{key:"today"},{key:"evening"}]`) is gone — read `data.items` and split on `when === "evening"`. The `when` field, previously section-dropped, is KEPT on every today row.
- **Count → `meta.counts` (breaking).** The former `data.badge {dueOrOverdue, other}` moves to `meta.counts {dueOrOverdue, other}` (the app's sidebar count over OPEN members: due/overdue by deadline vs. the rest), so `data` stays pure domain rows. Over MCP the today count rides the result's metadata block alongside `truncation`.
Expand Down
2 changes: 1 addition & 1 deletion docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ The contract reserves a small vocabulary, and each word means exactly one thing
| `match` | inside a `search` hit | Match provenance — WHERE the query matched when it was NOT the title. `{field, text}` with `field` ∈ `"heading"` \| `"notes"` \| `"checklist"` and `text` the matched heading title, a bounded notes snippet (~80 chars centered on the hit), or the matched checklist item's title. **PRESENCE-KEYED: a title match carries NO `match`** (absence = matched where you'd expect). At most one annotation; when several fields match, precedence is title (none) > heading > notes > checklist. A heading match credits the parent PROJECT, a checklist match the parent TO-DO (the GUI has no bare heading/checklist row; checklist-item uuids appear on no surface). Rides the compact tier (a non-default fact). |
| compact / full | read TIER (not a wire field) | The data DENSITY a row is returned at. Compact (the list default) keeps identity + structural + non-default facts; full (`detail`/`show`, or a list forced with `--full` / `full: true`) is the whole record. Selected by view kind + the flag — never a caller-supplied field list. |

**Two reading rules the tiers add — absence is meaningful.** (1) *Inside a single-container view* — a project or area card, an `anytime`/`someday` section — an item omits any ancestry its enclosing node already states (a project-view child carries no `project`/`area`; an area-section item no `area`; a heading-group member no `heading`), so **absence of a container field there means inherited from the enclosing node**, not "no container". This is the opposite of a mixed list (`inbox`, `today`, `search`, `changes`), where every row still names its own `project`/`area` (the `heading` ref is compact-dropped everywhere — the GUI shows the project, never the heading, outside a project view; `--full` keeps it). ONE deliberate exception, the two-view sublabel asymmetry (HEADARC2-B): a project-view `logbook` row — a swept child of an OPEN heading — KEEPS its `heading` ref even in compact (with a project-scoped `headingUuid` when the title would not round-trip), because the in-project logged toggle labels the HEADING; the GLOBAL `logbook` view (`read_view logbook`) labels the PROJECT instead — same row, different sublabel by view. A swept child of an ARCHIVED heading is not here at all: it nests under its heading in `logbookHeadings` (the group header states the heading, so the child drops the ref). (2) *In the compact tier* a field at its default is omitted, so **absence = the default**: no `status` means `open`, no `checklist` means none, no `todos` means no child to-dos, no `repeating` means not a repeating template and no `instanceOf` means not an instance (a plain row carries neither — R11), no `when` means the item is neither in Today nor scheduled forward, and `startDate` (the raw substrate behind `when`) is dropped from compact entirely (full/detail only); the full `notes` string is dropped and a presence-keyed `hasNotes: true` marks a row that has notes, and `created`/`modified` are dropped (read them from `detail`, or pass `--full`). The `stage` word (which replaced `start`/`logged`/`trashed`) is itself dropped inside a view or card section that already states it (see the `stage` glossary row).
**Two reading rules the tiers add — absence is meaningful.** (1) *Inside a single-container view* — a project or area card, an `anytime`/`someday` section — an item omits any ancestry its enclosing node already states (a project-view child carries no `project`/`area`; an area-section item no `area`), so **absence of a container field there means inherited from the enclosing node**, not "no container". This is the opposite of a mixed list (`inbox`, `today`, `search`, `changes`), where every row still names its own `project`/`area`. The `heading` ref is compact-dropped in mixed lists (the GUI shows the project, never the heading, outside a project view; `--full` keeps it), but the **project view is the exception where a heading ref is self-describing state**: a project-view row (the flat `items[]`, and a `logbook` row) that lives under a heading KEEPS its `heading` ref even in compact — a flat title with a project-scoped `headingUuid` when the title would not round-trip — because a heading's membership is now a per-row attribute, not a bucket, and reconstructing a heading's members means filtering `items` on `heading`. An unheaded project-view row omits `heading`. (The two-view sublabel asymmetry, HEADARC2-B: a `logbook` row's heading labels the HEADING in-project, while the GLOBAL `logbook` view labels the PROJECT instead — same row, different sublabel by view.) A swept child of an ARCHIVED heading is not in the flat `logbook`: it nests under its heading in `logbookHeadings` (the group header states the heading, so the child drops the ref). (2) *In the compact tier* a field at its default is omitted, so **absence = the default**: no `status` means `open`, no `checklist` means none, no `todos` means no child to-dos, no `repeating` means not a repeating template and no `instanceOf` means not an instance (a plain row carries neither — R11), no `when` means the item is neither in Today nor scheduled forward, and `startDate` (the raw substrate behind `when`) is dropped from compact entirely (full/detail only); the full `notes` string is dropped and a presence-keyed `hasNotes: true` marks a row that has notes, and `created`/`modified` are dropped (read them from `detail`, or pass `--full`). The `stage` word (which replaced `start`/`logged`/`trashed`) is itself dropped inside a view or card section that already states it (see the `stage` glossary row).

## The error-code registry

Expand Down
4 changes: 2 additions & 2 deletions docs/design/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Changing the host's system **timezone** is safe: it relabels wall clocks but lea

Two shaping rules run at the read emit boundary — the CLI `--json` reads AND the MCP read tools — in [`src/read/shape.ts`](../../src/read/shape.ts), BEFORE omit-empty (below). They are deterministic BY VIEW KIND, never per-item heuristics, so both surfaces inherit the identical shape; the human render keeps the full unshaped entities.

**R6 — no-redundant-ancestry.** An item never states a fact its enclosing node already states. In a **project-view** every child (in any bucket, incl. heading-group members) drops `project` and `area`; a heading-group member additionally drops `heading` (the card and the group state them). In an **area-view** every child item and project card drops `area`; project-child items keep `project`. In an **anytime/someday** section (`{area, items}`) items drop `area` (including the explicit `area: null` section), keeping `project`/`heading`. **Mixed-provenance lists keep every ref** — `inbox`, `today`, `upcoming`, `logbook`, `trash`, `search`, `changes`, `projects` — since those pull from many containers (in the COMPACT tier the `heading` ref is additionally dropped from these, per R7 below). The invariant that makes the drop lossless: the entity `area` is the EFFECTIVE area, and a project/heading child carries `area = NULL` in the DB, so its effective area resolves THROUGH its container to exactly the card's area (the sidebar grouper buckets by the same effective area). **Reading rule: absence of a container field INSIDE a container view = inherited from the enclosing node**, the opposite of its meaning in a mixed list (where absence means "no container").
**R6 — no-redundant-ancestry.** An item never states a fact its enclosing node already states. In a **project-view** every child drops `project` and `area` (the card states them) — but a HEADED child KEEPS its `heading` ref (the read-shape doctrine §3.12: the live children are one flat `items[]` in index order, so a heading's membership rides the row ref, not a bucket; the ref is a flat title + project-scoped `headingUuid` when the title would not round-trip, kept even in compact). An unheaded project-view child carries no `heading`. In an **area-view** every child item and project card drops `area`; project-child items keep `project`. In an **anytime/someday** section (`{area, items}`) items drop `area` (including the explicit `area: null` section), keeping `project`/`heading`. **Mixed-provenance lists keep every ref** — `inbox`, `today`, `upcoming`, `logbook`, `trash`, `search`, `changes`, `projects` — since those pull from many containers (in the COMPACT tier the `heading` ref is additionally dropped from these, per R7 below). The invariant that makes the drop lossless: the entity `area` is the EFFECTIVE area, and a project/heading child carries `area = NULL` in the DB, so its effective area resolves THROUGH its container to exactly the card's area (the sidebar grouper buckets by the same effective area). **Reading rule: absence of a container field INSIDE a container view = inherited from the enclosing node**, the opposite of its meaning in a mixed list (where absence means "no container").

**R7 — named detail tiers (compact | full).** Every list context — `items`, `sections`, and the collection arrays inside a `view` — returns a COMPACT line-item; `detail`/`show` and a `--full` (CLI) / `full: true` (MCP `read_view` / `search` / `changes_since` / `get_project` / `get_area` / `list_collections`) request return the FULL record. Compact = identity + structural facts + non-default facts: `status` (omit when `open`), `created`/`modified` (always dropped — get them from `detail`), the full `notes` string dropped for a presence-keyed `hasNotes: true` marker (absent = no notes), and the `heading` ref dropped everywhere (the GUI shows the project, never the heading, outside a project view). `--full` restores per-row density (incl. full `notes` and `heading`) but R6 still applies (ancestry redundancy is not tier-dependent). **Compact reading rule: absence = the default.**

Expand Down Expand Up @@ -178,7 +178,7 @@ Kept even when "empty" (absence would be lossy, so these are always present on t
- **Identity keys** — always present: `uuid`, `type`, and the name (`title`). An untitled to-do still carries `title: ""`.
- **Booleans** — a real `false` is meaningful, never omitted where a value is emitted: an area's `visible`. (The former `logged`/`trashed` item booleans are gone — R10 folds them into the one derived `stage`; the former `today`/`evening` markers are gone too — R12 folds them into the one derived `when`; and R11 made `repeating` a presence-keyed TEMPLATE-only object whose inner `paused`/`deadlined` are default-pruned false booleans — the `isTemplate`/`isInstance` discriminators no longer exist on the wire.)
- **Numeric counts** — a `0` is meaningful, never omitted by omit-empty: `openChildrenWhileResolved`, and the `today` view's `meta.counts` (`dueOrOverdue`/`other`, which live on `meta`, not `data`). (The checklist counters and the project leaf-action counters are no longer flat wire fields — the reshapes fold them into the presence-keyed `checklist` and `todos` objects above, each absent when its total is 0.)
- **Structural scaffolding** — the view shape that *carries* entities is not itself an entity and is never pruned, so its lists/markers survive empty: the `today` view's flat `items[]`, the `project`/`area` card sections (`anytime`, `headings`, `upcoming`, `someday`, `logbook`, `trash`, `projects`), and a sidebar section's `area: null` — the load-bearing "top-level / loose block" marker. Only the entities *inside* the scaffolding are pruned. (This is why omit-empty is scoped to recognized entity shapes, not a blanket deep prune: a to-do's `area: null` means "no area" and is dropped, but a section's `area: null` is a discriminant and is kept — same key, opposite meaning.)
- **Structural scaffolding** — the view shape that *carries* entities is not itself an entity and is never pruned, so its lists/markers survive empty: the `today` view's flat `items[]`, the project card sections (`items`, `headings`, `logbook`, `logbookHeadings`), the area card sections (`anytime`, `upcoming`, `someday`, `projects`), and a sidebar section's `area: null` — the load-bearing "top-level / loose block" marker. Only the entities *inside* the scaffolding are pruned. (This is why omit-empty is scoped to recognized entity shapes, not a blanket deep prune: a to-do's `area: null` means "no area" and is dropped, but a section's `area: null` is a discriminant and is kept — same key, opposite meaning.)

Omitted when empty, per entity:

Expand Down
6 changes: 4 additions & 2 deletions docs/design/read-shape-doctrine.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ Order: sidebar (area rank → active-first → drag order); `--later` appends sc

Order: `TMArea.index` (sidebar rank). This listing is the natural home for the area-rank axis §3.3/§3.4/§3.10 depend on.

### 3.12 `project-view` (`data: { view }`) — **planned (PRs 2–3)**
### 3.12 `project-view` (`data: { view }`) — **PR 2 landed (children dissolve); PR 3 planned (headings catalog + logbook flatten)**

PR 2 has dissolved the unheaded + per-heading stage sub-buckets into one flat `items[]` (index order, each headed row carrying its `heading` ref); `headings[]` is now the memberless catalog of live-heading nodes (`[{heading:{uuid,title,archived?}}]`), and `logbook` / `logbookHeadings` are unchanged pending PR 3 (which unwraps `headings[]` to `[{uuid,title,archived?}]`, merges swept archived headings in, and dissolves `logbookHeadings` into the flat `logbook`).

Current buckets: `project` (card node) · `anytime[]` · `upcoming[{date,items}]` · `someday[]` (all UNHEADED) · `headings[{heading:{uuid,title,archived?}, anytime[], upcoming[{date,items}], someday[]}]` (live heading groups with per-heading stage sub-buckets) · `logbook[]` (flat swept rows) · `logbookHeadings[{heading, items[]}]` (archived-heading groups) · `openChildrenWhileResolved` · `openChildrenUnderArchivedHeading`.

Expand Down Expand Up @@ -244,7 +246,7 @@ Two orders are purely presentation-derived (trash recency, search rank) and are
Ordered so each step is independently green and self-merged before the next (ALPHA-CONTRACT: break freely, no shims). Each PR carries its own contract + schema regen + CHANGELOG, per-view snapshot/fixture updates, JSON-shape regression tests, and (PRs 2–4) TTY byte-stability tests.

1. **`mg/today-dissolve` — today dissolve (§3.1) + this doctrine doc.** `sections` → flat `items[]` + `when`; `badge` → `meta.counts`; the "badge" vocabulary purge; the TTY redesign (clean header + counts at top). **Landed.**
2. **`mg/project-children-dissolve` — project-view children dissolve (§3.12).** Unheaded + per-heading stage sub-buckets → one flat `items[]` in project index order, every row carrying `stage`/`when`/`heading` ref. TTY reconstructs the current GUI-faithful rendering from row attributes (byte-stable).
2. **`mg/project-children-dissolve` — project-view children dissolve (§3.12).** Unheaded + per-heading stage sub-buckets → one flat `items[]` in project index order, every row carrying `stage`/`when`/`heading` ref; `headings[]` becomes the memberless live-heading catalog. TTY byte-stable (the library retains the structured groups for the GUI-faithful projection — it owns the clock + `todayIndex`; the wire is flat). **Landed.**
3. **`mg/headings-catalog-logbook-flatten` — headings catalog + logbook flatten (§3.12, rulings #C3/#C3a/#C4).** `headings[]` → index-ordered catalog `[{uuid,title,archived?}]`; `logbookHeadings` dissolves; single flat `logbook` bucket (stopDate DESC) absorbing archived-heading children. All HEADARC2 TTY invariants preserved (byte-stable).
4. **`mg/area-view-dissolve` — area-view dissolve (§3.13).** Direct `anytime`/`upcoming`/`someday` → one flat `items[]`; `projects[]` KEPT (sidebar-rank axis). TTY byte-stable.
5. **`mg/ordering-contract-docs` — ordering contract + skill sweep (ruling #C6).** Document every kept bucket's ordering in [../contract.md](../contract.md) (+ envelope schema description strings); flag presentation-derived orders as such; sweep [contracts.md](contracts.md) and the skill for the new shapes across all five PRs; fix the §5o "the desktop GUI is stricter" lede to platform-accurate wording.
Expand Down
Loading
Loading