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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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[]`.
Expand Down
22 changes: 22 additions & 0 deletions docs/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/design/read-shape-doctrine.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/things-app-oddities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<name>` on an open to-do, `things:///add?…&heading=<name>`, and reopening a completed child in place (`update?id=<child>&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).
Expand Down
2 changes: 1 addition & 1 deletion skills/things-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
Loading
Loading