diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf412a..90b6b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +- **DOCS — the read-view ordering contract is now documented, and the skill is swept for the read-shape doctrine's flat shapes.** Fifth and final PR of the read-shape doctrine (`docs/design/read-shape-doctrine.md`, ruling #C6) — docs only, no code/behavior change. + - `docs/contract.md` gains a **"Read views — shapes and orderings"** section: every view's `data` shape + the exact ordering of each kept bucket, flagging the two presentation-derived orders (`trash` recency, `search` relevance) as such vs. the real user-controlled axes. (The envelope JSON Schema models only the envelope layer — the `data` payload is an open coverage boundary — so the bucket orderings are documented in prose, per that boundary.) + - The `things-cli` skill (`SKILL.md`, `references/model.md`, `references/contracts.md`) is swept for the flat shapes landed across PRs 1–4: the flat `today` `items[]` + `meta.counts` (no `badge`/`sections`), the project card's flat `items` + heading catalog + flat `logbook` (no `anytime`/`upcoming`/`someday`/`logbookHeadings`), the area card's flat `items` + `projects`, and the corrected `stage`-drop / heading-ref rules. + - `docs/things-app-oddities.md` §5o lede corrected: "the desktop GUI is stricter" → platform-accurate "the GUI is stricter" (the observation was iOS; macOS presumed but unverified — the residual flagged in #373). + - **CHANGE (breaking, ALPHA) — the `area show` / `get_area` view dissolves its direct-to-do buckets into ONE flat `items[]`; `projects[]` is kept.** Fourth of five PRs implementing the read-shape doctrine (`docs/design/read-shape-doctrine.md` §3.13). An area has TWO order axes (project sidebar rank, direct-to-do index), so it keeps two flat lists. - **`data.view.items`** — ONE flat list of every direct to-do in area `index` order, each row carrying `stage` (KEPT — the list is stage-mixed) and `when`, `area` dropped (the card states it). The `anytime` / `upcoming` / `someday` direct-to-do buckets are GONE. Area direct to-dos are never headed nor project-nested, so they are fully self-describing. - **`data.view.projects`** — UNCHANGED (the area's child-project rows in sidebar order, mixed-stage, someday/scheduled projects included). This is the DISTINCT order axis; the `projects` / someday-projects / direct / upcoming / loose render variants collapse to TTY projections of `projects[]` + `items[]`. diff --git a/docs/contract.md b/docs/contract.md index 507ecb5..351be44 100644 --- a/docs/contract.md +++ b/docs/contract.md @@ -103,6 +103,28 @@ The contract reserves a small vocabulary, and each word means exactly one thing **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.) The flat `logbook` is ONE `stopDate DESC` list of ALL swept children — of open headings, un-headed, AND archived headings — each carrying its `heading` ref; the archived heading itself is a `headings[]` catalog entry with `archived`, and an odd OPEN child a Put-Back stranded under it appears in `logbook` with its live `stage` kept (so the bucket is stage-mixed). (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). +## Read views — shapes and orderings + +Every read view puts its rows in a documented order. Most are a real **user-controlled axis** (a drag order, a sidebar rank, a schedule) that a consumer can rely on and round-trip; two are **presentation-derived** (a recency or a relevance rank the app computes, flagged below) — stable and sensible, but not something the user arranged. The read-shape doctrine ([docs/design/read-shape-doctrine.md](design/read-shape-doctrine.md)) governs which buckets exist: JSON models semantic state, the terminal projects GUI placement, so a view is a flat `items[]` (with each row self-describing via `stage`/`when`/refs) unless a bucket encodes an ORDER AXIS the rows themselves do not carry. + +| view (`kind`) | `data` shape | ordering | axis | +|---|---|---|---| +| `inbox` | `{ items }` | `index ASC` | user (drag order) | +| `today` | `{ items }` + `meta.counts {dueOrOverdue, other}` | `startBucket ASC, COALESCE(todayIndexReferenceDate, startDate, deadline) DESC, todayIndex ASC, uuid ASC` — Today-proper and This-Evening members interleaved, each row's `when` marking which | mixed (index axis + entry-cohort recency) | +| `upcoming` | `{ items }` | `COALESCE(startDate, deadline) ASC, todayIndex ASC, uuid` (resting templates trail with no `when`) | schedule | +| `logbook` | `{ items }` | `stopDate DESC` (completion recency) | recency (semantic) | +| `trash` | `{ items }` | `userModificationDate DESC` | **presentation-derived** (recency) | +| `search` | `{ items }` | relevance rank (`compareSearchMatches`) | **presentation-derived** (relevance) | +| `changes` | `{ items }` | `userModificationDate DESC` | recency | +| `projects` | `{ items }` | sidebar: area rank → active-first → drag order (`--later` appends scheduled/someday per group) | user (sidebar) | +| `areas` | `{ items }` | `TMArea.index` (sidebar rank) | user (sidebar) | +| `anytime` / `someday` | `{ sections: [{area, items}] }` | sections by area **sidebar rank** (loose/null-area first); within a section, drag order then project-then-children | user (sidebar rank is the order axis the `sections` bucket earns its place by) | +| `project-view` | `{ view: { project, headings, items, logbook, openChildrenWhileResolved, openChildrenUnderArchivedHeading } }` | `headings` = heading `index ASC` (every heading, live + archived); `items` (all live children, headed + unheaded) = child `index ASC`; `logbook` (all swept children) = `stopDate DESC`, open odd children null-last | user (child + heading index); `logbook` recency | +| `area-view` | `{ view: { area, projects, items } }` | `projects` = sidebar rank (active-first within group); `items` (direct to-dos) = `index ASC` — the two DISTINCT order axes an area carries | user (sidebar + index) | +| `detail` / `show` | `{ item }` | single entity (no ordering) | — | + +The two **presentation-derived** orders (`trash` recency, `search` relevance) are appropriate for their views but are NOT a user arrangement — do not treat them as a stable drag order. Every other order above is a real axis. A view's rows are complete unless `meta.truncation.truncated` is `true`. (`project-view` / `area-view` retain per-block caps — see `meta.truncation.blocks`.) + ## The error-code registry Every `error.code` is a member of the `ErrorCode` union in `src/contracts.ts` — the compiler is the registry, so no surface can emit a code that is not listed here. Meanings are frozen at v1.0 (a documented code's meaning never changes); new codes may be added after v1.0, which is non-breaking. Two of the members are template-literal families whose suffix is minted in the write layer; their enumerated suffixes are listed in the two notes below the table. diff --git a/docs/design/read-shape-doctrine.md b/docs/design/read-shape-doctrine.md index 05b25aa..3a9e897 100644 --- a/docs/design/read-shape-doctrine.md +++ b/docs/design/read-shape-doctrine.md @@ -1,6 +1,6 @@ # Read-shape doctrine — JSON models semantic state; TTY projects GUI placement -**Status: RATIFIED.** This is the doctrine of record for every primary read view's JSON `data` payload, plus the per-view verdicts it produces. The maintainer's rulings on the contentious calls (§4) are final and are baked into the per-view verdicts (§3). The doctrine is being implemented across five sequential PRs (§6); a per-view verdict marked **landed** already ships, one marked **planned** is ratified design the code is catching up to. +**Status: RATIFIED — fully implemented.** This is the doctrine of record for every primary read view's JSON `data` payload, plus the per-view verdicts it produces. The maintainer's rulings on the contentious calls (§4) are final and are baked into the per-view verdicts (§3). All five sequential PRs (§6) have landed — every per-view verdict below now ships. This document extends [api-doctrine.md](api-doctrine.md) (the design canon) and is the *why/what* behind the contract change it drives. It obeys api-doctrine §1 (the ten-sentence grammar): every target shape still fits an existing `data` wrapper (`item` / `items` / `sections` / `view`) — no sixth container is invented. @@ -249,6 +249,6 @@ Ordered so each step is independently green and self-merged before the next (ALP 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?}]` (all headings, live + swept archived); `logbookHeadings` dissolves; single flat `logbook` bucket (stopDate DESC) absorbing archived-heading children, each with its `heading` ref, `stage` kept (mixed). All HEADARC2 TTY invariants preserved (byte-stable — the library keeps its structured logged-region grouping for the projection). **Landed.** 4. **`mg/area-view-dissolve` — area-view dissolve (§3.13).** Direct `anytime`/`upcoming`/`someday` → one flat `items[]` (index order, `stage`/`when` per row); `projects[]` KEPT (the distinct sidebar-rank order axis). TTY + truncation byte-stable (the library keeps its structured direct-to-do grouping; the per-block `area` cap on the open/current rows is preserved and mirrored onto the flat `items[]`). **Landed.** -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. +5. **`mg/ordering-contract-docs` — ordering contract + skill sweep (ruling #C6).** Documented every kept bucket's ordering in [../contract.md](../contract.md) (a "Read views — shapes and orderings" table; the envelope schema models only the envelope layer, so the `data` bucket orderings live in prose there, per the schema's coverage boundary); flagged the presentation-derived orders (trash recency, search relevance) as such; swept [contracts.md](contracts.md) and the skill (`SKILL.md`, `references/model.md`/`contracts.md`) for the flat shapes across all five PRs; fixed the §5o "desktop GUI is stricter" lede to platform-accurate wording. **Landed.** Living-doc updates ride each PR (per AGENTS.md): [../contract.md](../contract.md), [contracts.md](contracts.md), `CHANGELOG.md` (Unreleased, breaking), and the capability-matrix if a read verdict changes. diff --git a/docs/things-app-oddities.md b/docs/things-app-oddities.md index 8afd088..36c19fe 100644 --- a/docs/things-app-oddities.md +++ b/docs/things-app-oddities.md @@ -164,7 +164,7 @@ While an unanswered Automation consent dialog is up (or after one was dismissed The [§5b](#5b-adding-an-open-child-silently-reopens-a-resolved-project) reopen law (an open child added/moved into a *resolved project* silently reopens the project) applies identically to **archived (completed, `status=3`) HEADINGS**: `things:///update?…&heading=` on an open to-do, `things:///add?…&heading=`, and reopening a completed child in place (`update?id=&completed=false`) each flip the archived heading back open — a single-row write, `status 3→0` + `stopDate→NULL`, **byte-identical to the AppleScript un-archive op** (`set status … to open`, [heading-research](../lab/heading-research.md) P10b-b2). The reopen is heading-ONLY: the heading's swept (completed) children stay resolved (status/stopDate untouched). A corollary: an archived heading **cannot** hold an open child — any write that would produce one reopens the heading, so the odd (archived-heading + open-child) state is not reachable on these headless surfaces. Automation-relevant because `resolveHeadingRef` (our `todo move/add --heading` resolver) has no status filter and resolves archived headings by name AND uuid — but the reopen makes that SAFE (the destination becomes visible; no stranding under an invisible section, unlike the trashed-child hazards §6½/§6¾). Also note the `heading=` name-match (oddity 2c) INCLUDES archived headings — an archived heading is matched, not treated as missing. *(HEADARC-1a/1b/2ii/3/4, 2026-08-03, [lab/headarc-archived-heading-moves.md](../lab/headarc-archived-heading-moves.md) — all four headless surfaces reopen with the identical byte delta.)* **The "not headlessly reachable" corollary is boundary-tight, not absolute — HEADARC2-C (2026-08-03, [lab/headarc2-residual-captures.md](../lab/headarc2-residual-captures.md)):** GUI **Put Back** of a *trashed* open child restores it in place under an archived heading WITHOUT reopening (§6¾ heading analog below), so the odd state IS reachable — just never via a headless surface we ship (our `todo.restore` un-traps to the Inbox, severing the FK). **Rendering (HEADARC2-A/B, macOS matches the maintainer's iOS recollection — no delta):** an archived heading is invisible in the default project view and, under "Show N logged items", renders as a GROUPED section header with its swept children nested beneath it; while a heading is still OPEN, its swept child sits in the flat project-level logged toggle carrying a muted HEADING sublabel (the in-project toggle labels the heading; the global Logbook labels the project). -**Maintainer-observed GUI constraints (iOS, Things 3.22.12; macOS presumed identical but unverified — distinct from the VM-probed 3.22.11 evidence above; recorded here per the version-stamping policy, which forbids editing the immutable campaign docs):** the desktop GUI is STRICTER than every headless surface around an archived/log-swept heading, and the headless permissiveness self-heals. +**Maintainer-observed GUI constraints (iOS, Things 3.22.12; macOS presumed identical but unverified — distinct from the VM-probed 3.22.11 evidence above; recorded here per the version-stamping policy, which forbids editing the immutable campaign docs):** the GUI is STRICTER than every headless surface around an archived/log-swept heading, and the headless permissiveness self-heals (the observed surface was iOS; the macOS/desktop GUI is presumed identical but unverified, per the attribution above). - **(a) No add-after-archive in the GUI.** The GUI REFUSES dragging any to-do INTO an archived / log-swept heading — there is no way to file a new child under it by hand. The headless surfaces are MORE permissive: `update?…&heading=` / `add?…&heading=` land the child AND reopen the heading (HEADARC-1a/1b above), so no invisible-section stranding results. - **(b) Drag-out is one-way to the active bucket.** Dragging a child OUT of an archived / logged heading is allowed only TOWARD the active bucket; it reparents the to-do to the project ROOT and REOPENS it (`status→0`), matching our `todo.restore`'s Put-Back-to-Inbox severing intent. - **(c) Un-completing a child cascades upward.** Un-completing (reopening) a child of an archived heading cascades up — the HEADING unarchives — matching the HEADARC-4-odd byte evidence (reopening a completed child of a completed heading reopens the heading too). diff --git a/skills/things-cli/SKILL.md b/skills/things-cli/SKILL.md index 28a9893..1ca47c3 100644 --- a/skills/things-cli/SKILL.md +++ b/skills/things-cli/SKILL.md @@ -24,7 +24,7 @@ version: 0.0.0-dev Reads decompose an item's position onto two derived, presence-keyed words (they REPLACED the old `start`/`logged`/`trashed`/`todaySection` fields): -- **`stage`** — the sidebar BUCKET: `inbox | upcoming | anytime | someday | logbook | trash`. Read view membership off it directly (a completed row is `logbook`, a trashed row `trash`, regardless of any other hint). Dropped inside a section that already states it; present in mixed lists, `search`, `changes`, and `detail`. +- **`stage`** — the sidebar BUCKET: `inbox | upcoming | anytime | someday | logbook | trash`. Read view membership off it directly (a completed row is `logbook`, a trashed row `trash`, regardless of any other hint). Dropped only where a view provably states it — the stage-pure flat views (`inbox`/`anytime`/`someday`/`logbook`/`trash`) and the `today` view; KEPT everywhere it is stage-mixed — `upcoming`, `search`, `changes`, the projects/areas listings, the project/area card `items` (and the project `logbook`), and `detail`. - **`when`** — the TIME POSITION: `today | evening | a future ISO date`, or absent. `evening` implies today; someday is a bucket (→ `stage`), never a `when`. A due deadline pulls an undated row into Today (`when: "today"`); the app re-files it into Anytime, so it derives `stage: "anytime"` (it leaves the Inbox/Someday list). - **`provisional: true`** marks a Today member the app has not yet materialized (the "N new to-dos" banner / `•` pip); see [references/banner.md](references/banner.md). diff --git a/skills/things-cli/references/contracts.md b/skills/things-cli/references/contracts.md index 25a313d..e61d40f 100644 --- a/skills/things-cli/references/contracts.md +++ b/skills/things-cli/references/contracts.md @@ -6,8 +6,9 @@ The apiVersion-stable contracts that hold regardless of which binary version you Every `--json` response is an envelope `{ apiVersion, ok, kind, data, meta }`: -- Results are in `.data`, in exactly one of four wrappers named by `kind`: `.data.item` (one entity), `.data.view` (an area/project card), `.data.items` (a flat list), or `.data.sections` (a list split into named sections). Never `.items` at the top level. Item UUIDs are in `.uuid`, not `.id`; emitted UUIDs are always full. -- **Container refs and `type`.** A row's `area`/`project`/`heading` is a bare **title string**; a flat sibling `areaUuid`/`projectUuid`/`headingUuid` appears **only when that title would not resolve back** to the exact same item. **To act on a ref, pass `.areaUuid // .area`** (same for project/heading). For unattended pipelines or stored refs, use `--full` and key on uuids (the full tier always emits the `*Uuid` siblings). **Absent `type` = to-do** — `type` is present only for a `project`, `heading`, `area`, or `tag` ROW/candidate (a `project show` heading GROUP node drops it — its slot already states it is a heading, and it emits a presence-keyed **`archived`** — the archive timestamp, present iff archived — instead of any status/stage; a heading is archive/unarchive only, never completed/canceled). An archived heading stays among the live headings until the logbook sweep, then groups under the logged region. +- Results are in `.data`, in exactly one of four wrappers named by `kind`: `.data.item` (one entity), `.data.view` (an area/project card), `.data.items` (a flat list), or `.data.sections` (a list split into named sections — only the area-grouped `anytime`/`someday`). Never `.items` at the top level. Item UUIDs are in `.uuid`, not `.id`; emitted UUIDs are always full. +- **The composite cards are flat.** A `project show` `view` is `{ project, headings, items, logbook, openChildrenWhileResolved?, openChildrenUnderArchivedHeading? }`: `items` is ONE flat list of every live child (index order, each carrying `stage`/`when` and, when under a heading, its `heading` ref); `headings` is the flat catalog of EVERY heading `{uuid, title, archived?}` in index order; `logbook` is one flat `stopDate DESC` list of all logged children (each with its `heading` ref). An `area show` `view` is `{ area, projects, items }`: `items` is the flat direct-to-dos (index order), `projects` the child-project rows (sidebar order). Reconstruct a heading's members by filtering `items` (or `logbook`) on the `heading` ref. There are no `anytime`/`upcoming`/`someday` card buckets and no `logbookHeadings` — the terminal re-derives the GUI's grouped layout from these flat rows. +- **Container refs and `type`.** A row's `area`/`project`/`heading` is a bare **title string**; a flat sibling `areaUuid`/`projectUuid`/`headingUuid` appears **only when that title would not resolve back** to the exact same item. **To act on a ref, pass `.areaUuid // .area`** (same for project/heading). For unattended pipelines or stored refs, use `--full` and key on uuids (the full tier always emits the `*Uuid` siblings). **Absent `type` = to-do** — `type` is present only for a `project`, `heading`, `area`, or `tag` ROW/candidate. A `headings[]` catalog entry drops `type` (its slot states it is a heading) and emits a presence-keyed **`archived`** (the archive timestamp, present iff archived) instead of any status/stage — a heading is archive/unarchive only, never completed/canceled. Every heading, live or archived, is a catalog entry; a swept archived heading's logged children ride the flat `logbook` (with their `heading` ref). - Check `meta.truncation.truncated` before concluding "no match" or "that's everything": it is `true` exactly when any row was hidden (`shown < total`, or any grouped block capped). Raise `--limit`/`--all` or narrow the filter rather than assuming you saw everything. - List/search rows are compact **summaries**: their `tags` field is not necessarily the complete effective set, and placement can be partial. Use `things show --json` when notes, checklist, placement, or inherited/effective tags matter. diff --git a/skills/things-cli/references/model.md b/skills/things-cli/references/model.md index ef8dd3f..3786884 100644 --- a/skills/things-cli/references/model.md +++ b/skills/things-cli/references/model.md @@ -17,7 +17,7 @@ Entities, relationships, and how the sidebar views are computed over them, as ex - A to-do has at most ONE container, and may have none — standalone to-dos (no project, no area) are normal and appear at the top level of Anytime/Someday/Upcoming. Moving changes the container; completing or trashing does not. - The **Inbox is a state, not a container**: "in the Inbox" means untriaged — no container AND no schedule. Filing or scheduling an inbox to-do moves it out (filing promotes it to Anytime); moving a to-do TO the Inbox clears both its container and its schedule. -- A heading is a section label inside one project — not a task. It cannot be scheduled or tagged, and its lifecycle is **archive/unarchive ONLY** — a heading is never completed or canceled (it has no canceled state). In a `project show` view a heading GROUP node therefore carries no `status`/`stage`; it emits a presence-keyed **`archived`** (the ISO archive timestamp) when archived, absent when open. An archived heading stays in place among the live headings until the logbook sweep passes it, then it moves into the logged region as a grouped section with its children (mirroring how a completed to-do stays checked in place until it is swept). Deleting/archiving a heading affects only the label, per the operation's contract. +- A heading is a section label inside one project — not a task. It cannot be scheduled or tagged, and its lifecycle is **archive/unarchive ONLY** — a heading is never completed or canceled (it has no canceled state). In a `project show` view every heading (live and archived) is an entry in the flat `headings` catalog `{uuid, title, archived?}` — it carries no `status`/`stage`, and emits a presence-keyed **`archived`** (the ISO archive timestamp) when archived, absent when open. A heading's members are not nested under it: they ride the flat `items` (live children) and `logbook` (logged children) rows as a `heading` ref, so you reconstruct a heading's contents by filtering those lists on the ref. Deleting/archiving a heading affects only the label, per the operation's contract. - Tag inheritance flows downward: area → project → (through heading) → to-do. A to-do's *effective* tags = own tags ∪ project tags ∪ area tags. List output distinguishes own vs inherited tags. - **Status**: open → completed or canceled (both land in the Logbook) or trashed (Trash; restorable until emptied). Reopen brings a logged item back. - **when** (`today | evening | anytime | someday | YYYY-MM-DD`) controls which view an item appears in; **deadline** is an independent due date shown alongside the item; **reminder** is a time-of-day alert attached to a dated when. @@ -41,7 +41,7 @@ Entities, relationships, and how the sidebar views are computed over them, as ex Reads decompose an item's position onto two derived, presence-keyed words (they REPLACED the old `start`/`startDate`/`logged`/`trashed`/`todaySection` wire fields, which no longer appear): -- **`stage`** — the sidebar BUCKET: `inbox | upcoming | anytime | someday | logbook | trash`. Read view membership off it directly. It is dropped inside a section/catalogue that already states it (the stage-pure `inbox`/`anytime`/`someday`/`logbook`/`trash` lists and the `today` view) and kept everywhere it is not implied (the mixed `upcoming` catalogue, `search`, `changes`, the projects/areas listings, and `detail`). +- **`stage`** — the sidebar BUCKET: `inbox | upcoming | anytime | someday | logbook | trash`. Read view membership off it directly. It is dropped inside a section/catalogue that provably states it (the stage-pure `inbox`/`anytime`/`someday`/`logbook`/`trash` lists and the `today` view) and kept everywhere it is stage-mixed (the mixed `upcoming` catalogue, `search`, `changes`, the projects/areas listings, the project/area card `items` and the project `logbook`, and `detail`). - **`when`** — the TIME POSITION: `today | evening | a future ISO date`, or absent (unscheduled and not in Today). `evening` implies today. Someday is a bucket, never a `when`. The `today` view is one flat `data.items` list (no `sections`); each row's `when` marks Today-proper vs This-Evening, and the whole-view count rides `meta.counts` (`{dueOrOverdue, other}`). - The two are DIFFERENT facts. A due deadline pulls an UNDATED row into Today: it reads `when: "today"` and derives `stage: "anytime"` — the app re-files a deadline-pulled Inbox/Someday row into Anytime at pull time (R13/BANNER1b), so it drops out of the Inbox/Someday lists and joins Anytime while its `when` reads `today`. Completed/canceled → `stage: "logbook"`, trashed → `stage: "trash"`, regardless of any other hint. - `provisional: true` marks a Today member the app has not yet materialized — see [banner.md](banner.md). @@ -53,7 +53,7 @@ Every row comes back at one of two densities, selected by view kind + flag, neve - **Compact** (the list default) keeps identity + structural + non-default facts. A field at its default is OMITTED, so absence = the default: no `status` = open, no `checklist` = none, no `todos` = no child to-dos, no `when` = not in Today and unscheduled, no `provisional` = materialized. The full `notes` string is dropped and replaced by presence-keyed `hasNotes: true`; `startDate`, `created`, and `modified` are dropped. - **Full** (`show`/`detail`, or a list forced with `--full`) is the whole record — the complete `notes`, the raw `startDate` substrate behind `when`, the checklist `items` array, and timestamps. - Compact rows still carry the useful summaries: `checklist:{open,total}` on a to-do, `todos:{open,total}` on a project (app-maintained leaf-action counts — never headings, checklist items, or trashed rows), and `match:{field,text}` on a `search` hit whose match was NOT the title (`field` ∈ `heading | notes | checklist`). -- **Container absence rule:** inside a single-container node (a project/area card, an `anytime`/`someday` section, a heading group) an item omits any ancestry the node already states, so absent `project`/`area`/`heading` there means *inherited from the enclosing node*. A mixed list (`inbox`/`today`/`search`/`changes`) still names each row's own `project`/`area` (the `heading` ref is compact-dropped outside a project view; `--full` keeps it). **Two-view sublabel asymmetry:** a swept child of an OPEN heading in a `project show` logged region KEEPS its `heading` ref (the in-project logged toggle labels the HEADING) even in compact — but the same row in the GLOBAL `logbook` view is labeled by its PROJECT instead. A swept child of an ARCHIVED heading is not a flat logged row; it nests under its heading's group (which states the heading, so the child drops the ref). +- **Container absence rule:** inside a single-container node (a project/area card, an `anytime`/`someday` section) an item omits any ancestry the node already states, so absent `project`/`area` there means *inherited from the enclosing node*. A mixed list (`inbox`/`today`/`search`/`changes`) still names each row's own `project`/`area` (the `heading` ref is compact-dropped outside a project view; `--full` keeps it). **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, because membership is a per-row attribute, not a bucket — so a swept child of an ARCHIVED heading is a flat `logbook` row carrying its `heading` ref (no more nested group). **Two-view sublabel asymmetry:** that in-project `logbook` row's `heading` labels the HEADING, while the same row in the GLOBAL `logbook` view is labeled by its PROJECT instead. - **Container ref shape:** a container ref is a bare **title string** (`"area": "Family"`, `"project": "Groceries"`, `"heading": "Backlog"`). A flat sibling `areaUuid` / `projectUuid` / `headingUuid` (the full uuid) rides alongside **only when the bare title would not resolve back** to that exact item — a duplicate title in the same resolution scope, or a title that is itself a valid uuid prefix. **To act on a ref, pass `.areaUuid // .area`** (same for project/heading): the uuid when present, else the title. For unattended pipelines or stored refs, use `--full` and key on the uuids — the full tier emits every `*Uuid` sibling unconditionally. A row whose container project is a repeating template carries a flat `projectIsTemplate: true` (the JSON twin of the TTY `↻` glyph) — acting on such a row edits the blueprint, affecting future occurrences, so target the intended copy via `projectUuid` (a same-titled occurrence exists alongside the hidden template). - **`type` is presence-keyed:** **absent `type` = to-do.** A row omits `type` when it is a to-do; `type` is present for a `project`, `heading`, `area`, or `tag` ROW (including in the error `candidates` shape). This is scoped to ROWS/candidates — a positional keyed sub-object whose kind is fixed by its slot (a `project show` heading GROUP node) drops `type` too, since its position already states it is a heading.