things-api 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +16 -5
  2. package/dist/audit/schema.d.ts +7 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/canonical-ref.d.ts +21 -0
  5. package/dist/cli/canonical-ref.js +23 -0
  6. package/dist/cli/canonical-ref.js.map +1 -0
  7. package/dist/cli/commands/area.d.ts +18 -11
  8. package/dist/cli/commands/area.js +81 -77
  9. package/dist/cli/commands/area.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +1 -2
  11. package/dist/cli/commands/doctor.js +11 -1
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/install-skill.d.ts +19 -4
  14. package/dist/cli/commands/install-skill.js +130 -52
  15. package/dist/cli/commands/install-skill.js.map +1 -1
  16. package/dist/cli/commands/mcp.js +21 -1
  17. package/dist/cli/commands/mcp.js.map +1 -1
  18. package/dist/cli/commands/project.d.ts +1 -0
  19. package/dist/cli/commands/project.js +92 -48
  20. package/dist/cli/commands/project.js.map +1 -1
  21. package/dist/cli/commands/reads.d.ts +5 -1
  22. package/dist/cli/commands/reads.js +110 -31
  23. package/dist/cli/commands/reads.js.map +1 -1
  24. package/dist/cli/commands/setup.js +11 -4
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/show.js +75 -36
  27. package/dist/cli/commands/show.js.map +1 -1
  28. package/dist/cli/commands/todo.js +2 -2
  29. package/dist/cli/commands/todo.js.map +1 -1
  30. package/dist/cli/commands/writes.js +795 -144
  31. package/dist/cli/commands/writes.js.map +1 -1
  32. package/dist/cli/did-you-mean.d.ts +14 -3
  33. package/dist/cli/did-you-mean.js +44 -7
  34. package/dist/cli/did-you-mean.js.map +1 -1
  35. package/dist/cli/dry-run.d.ts +55 -0
  36. package/dist/cli/dry-run.js +92 -0
  37. package/dist/cli/dry-run.js.map +1 -0
  38. package/dist/cli/glyphs.d.ts +16 -2
  39. package/dist/cli/glyphs.js +60 -12
  40. package/dist/cli/glyphs.js.map +1 -1
  41. package/dist/cli/help.js +69 -13
  42. package/dist/cli/help.js.map +1 -1
  43. package/dist/cli/main.js +8 -1
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/move-hint.d.ts +3 -2
  46. package/dist/cli/move-hint.js +2 -2
  47. package/dist/cli/move-hint.js.map +1 -1
  48. package/dist/cli/read-driver.d.ts +53 -5
  49. package/dist/cli/read-driver.js +82 -15
  50. package/dist/cli/read-driver.js.map +1 -1
  51. package/dist/cli/ref-render.d.ts +14 -0
  52. package/dist/cli/ref-render.js +9 -0
  53. package/dist/cli/ref-render.js.map +1 -0
  54. package/dist/cli/render.d.ts +40 -14
  55. package/dist/cli/render.js +128 -55
  56. package/dist/cli/render.js.map +1 -1
  57. package/dist/cli/resolve-invocation.d.ts +7 -5
  58. package/dist/cli/resolve-invocation.js +16 -7
  59. package/dist/cli/resolve-invocation.js.map +1 -1
  60. package/dist/cli/skill-check.d.ts +12 -6
  61. package/dist/cli/skill-check.js +31 -14
  62. package/dist/cli/skill-check.js.map +1 -1
  63. package/dist/cli/skill.d.ts +8 -0
  64. package/dist/cli/skill.js +10 -0
  65. package/dist/cli/skill.js.map +1 -1
  66. package/dist/cli/verb-hint.js +1 -1
  67. package/dist/cli/verb-hint.js.map +1 -1
  68. package/dist/client.d.ts +158 -26
  69. package/dist/client.js +254 -38
  70. package/dist/client.js.map +1 -1
  71. package/dist/config.d.ts +66 -2
  72. package/dist/config.js +120 -12
  73. package/dist/config.js.map +1 -1
  74. package/dist/contracts.d.ts +187 -28
  75. package/dist/contracts.js +22 -1
  76. package/dist/contracts.js.map +1 -1
  77. package/dist/db/baselines/db-v26.js +4 -1
  78. package/dist/db/baselines/db-v26.js.map +1 -1
  79. package/dist/diagnose.d.ts +34 -2
  80. package/dist/diagnose.js +36 -1
  81. package/dist/diagnose.js.map +1 -1
  82. package/dist/index.d.ts +21 -8
  83. package/dist/index.js +26 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/mcp/server.d.ts +9 -0
  86. package/dist/mcp/server.js +790 -322
  87. package/dist/mcp/server.js.map +1 -1
  88. package/dist/model/entities.d.ts +57 -4
  89. package/dist/model/entities.js.map +1 -1
  90. package/dist/model/mappers.d.ts +2 -0
  91. package/dist/model/mappers.js +44 -2
  92. package/dist/model/mappers.js.map +1 -1
  93. package/dist/model/recurrence.d.ts +8 -1
  94. package/dist/model/recurrence.js.map +1 -1
  95. package/dist/model/serialize.js +15 -3
  96. package/dist/model/serialize.js.map +1 -1
  97. package/dist/read/area-filter.d.ts +76 -0
  98. package/dist/read/area-filter.js +58 -0
  99. package/dist/read/area-filter.js.map +1 -0
  100. package/dist/read/area-view.d.ts +22 -11
  101. package/dist/read/area-view.js +54 -28
  102. package/dist/read/area-view.js.map +1 -1
  103. package/dist/read/detail.js +15 -8
  104. package/dist/read/detail.js.map +1 -1
  105. package/dist/read/predicates.d.ts +18 -0
  106. package/dist/read/predicates.js +19 -0
  107. package/dist/read/predicates.js.map +1 -1
  108. package/dist/read/project-view.d.ts +91 -15
  109. package/dist/read/project-view.js +135 -37
  110. package/dist/read/project-view.js.map +1 -1
  111. package/dist/read/pseudo-area.d.ts +29 -0
  112. package/dist/read/pseudo-area.js +27 -0
  113. package/dist/read/pseudo-area.js.map +1 -0
  114. package/dist/read/queries.d.ts +175 -19
  115. package/dist/read/queries.js +376 -32
  116. package/dist/read/queries.js.map +1 -1
  117. package/dist/read/scope.d.ts +126 -0
  118. package/dist/read/scope.js +161 -0
  119. package/dist/read/scope.js.map +1 -0
  120. package/dist/read/search-rank.d.ts +2 -5
  121. package/dist/read/shape.d.ts +181 -0
  122. package/dist/read/shape.js +737 -0
  123. package/dist/read/shape.js.map +1 -0
  124. package/dist/read/show-target.d.ts +9 -1
  125. package/dist/read/show-target.js +77 -17
  126. package/dist/read/show-target.js.map +1 -1
  127. package/dist/read/stage.d.ts +199 -0
  128. package/dist/read/stage.js +125 -0
  129. package/dist/read/stage.js.map +1 -0
  130. package/dist/read/truncation.d.ts +7 -12
  131. package/dist/read/truncation.js +56 -21
  132. package/dist/read/truncation.js.map +1 -1
  133. package/dist/read/views.d.ts +68 -16
  134. package/dist/read/views.js +206 -52
  135. package/dist/read/views.js.map +1 -1
  136. package/dist/surface-copy.d.ts +16 -0
  137. package/dist/surface-copy.js +16 -0
  138. package/dist/surface-copy.js.map +1 -1
  139. package/dist/write/batch.d.ts +49 -10
  140. package/dist/write/batch.js +423 -71
  141. package/dist/write/batch.js.map +1 -1
  142. package/dist/write/commands.js +268 -55
  143. package/dist/write/commands.js.map +1 -1
  144. package/dist/write/guards.d.ts +1 -1
  145. package/dist/write/guards.js +71 -10
  146. package/dist/write/guards.js.map +1 -1
  147. package/dist/write/heading.d.ts +10 -1
  148. package/dist/write/heading.js +35 -5
  149. package/dist/write/heading.js.map +1 -1
  150. package/dist/write/make-repeating-project.d.ts +2 -2
  151. package/dist/write/make-repeating-project.js +9 -9
  152. package/dist/write/make-repeating-project.js.map +1 -1
  153. package/dist/write/move.d.ts +134 -0
  154. package/dist/write/move.js +1874 -0
  155. package/dist/write/move.js.map +1 -0
  156. package/dist/write/operations.d.ts +105 -19
  157. package/dist/write/operations.js +56 -7
  158. package/dist/write/operations.js.map +1 -1
  159. package/dist/write/opid.d.ts +31 -0
  160. package/dist/write/opid.js +30 -0
  161. package/dist/write/opid.js.map +1 -0
  162. package/dist/write/pipeline.d.ts +70 -3
  163. package/dist/write/pipeline.js +198 -33
  164. package/dist/write/pipeline.js.map +1 -1
  165. package/dist/write/pre-state.d.ts +172 -8
  166. package/dist/write/pre-state.js +467 -25
  167. package/dist/write/pre-state.js.map +1 -1
  168. package/dist/write/reorder.d.ts +23 -3
  169. package/dist/write/reorder.js +2106 -86
  170. package/dist/write/reorder.js.map +1 -1
  171. package/dist/write/reversibility.js +19 -7
  172. package/dist/write/reversibility.js.map +1 -1
  173. package/dist/write/scope-guard.d.ts +31 -0
  174. package/dist/write/scope-guard.js +162 -0
  175. package/dist/write/scope-guard.js.map +1 -0
  176. package/dist/write/undo.js +120 -13
  177. package/dist/write/undo.js.map +1 -1
  178. package/dist/write/vectors/applescript.js +16 -7
  179. package/dist/write/vectors/applescript.js.map +1 -1
  180. package/dist/write/vectors/shortcuts.js +1 -1
  181. package/dist/write/vectors/shortcuts.js.map +1 -1
  182. package/dist/write/vectors/simulator.js +83 -27
  183. package/dist/write/vectors/simulator.js.map +1 -1
  184. package/dist/write/vectors/types.d.ts +11 -0
  185. package/dist/write/vectors/ui-certification.d.ts +3 -3
  186. package/dist/write/vectors/ui-certification.js +41 -5
  187. package/dist/write/vectors/ui-certification.js.map +1 -1
  188. package/dist/write/vectors/ui-drag.d.ts +21 -1
  189. package/dist/write/vectors/ui-drag.js +138 -27
  190. package/dist/write/vectors/ui-drag.js.map +1 -1
  191. package/dist/write/vectors/ui-recipes.d.ts +21 -1
  192. package/dist/write/vectors/ui-recipes.js +166 -7
  193. package/dist/write/vectors/ui-recipes.js.map +1 -1
  194. package/dist/write/vectors/ui.d.ts +25 -1
  195. package/dist/write/vectors/ui.js +142 -33
  196. package/dist/write/vectors/ui.js.map +1 -1
  197. package/dist/write/verify/delta.d.ts +17 -1
  198. package/dist/write/verify/delta.js +106 -29
  199. package/dist/write/verify/delta.js.map +1 -1
  200. package/package.json +7 -1
  201. package/schema/envelope.schema.json +397 -0
  202. package/skills/things-cli/SKILL.md +36 -17
  203. package/skills/things-cli/references/banner.md +35 -0
  204. package/skills/things-cli/references/contracts.md +32 -9
  205. package/skills/things-cli/references/errors.md +57 -0
  206. package/skills/things-cli/references/gui.md +1 -1
  207. package/skills/things-cli/references/model.md +18 -7
  208. package/skills/things-cli/references/ordering.md +88 -0
@@ -0,0 +1,737 @@
1
+ /**
2
+ * The read-payload SHAPING transform: the token-economy rules R6 and R7, the
3
+ * universal item-DTO reshapes (R9), and the R10 lifecycle taxonomy — applied at
4
+ * the JSON emit boundary of the read surfaces (the CLI `--json` read envelope,
5
+ * src/cli/read-driver.ts, and the MCP read tool results, src/mcp/server.ts), the
6
+ * same boundaries omit-empty runs at. Shaping runs BEFORE omit-empty. The
7
+ * human-render path keeps the full, unshaped entities, so this is JSON-only.
8
+ *
9
+ * Both R6/R7 are deterministic BY VIEW KIND / SECTION — the emitter knows whether
10
+ * it is inside a single-container view, a lifecycle bucket, or a mixed list —
11
+ * never a per-item heuristic.
12
+ *
13
+ * ## R10 — the `stage` lifecycle taxonomy (every tier, every kind incl. detail)
14
+ * The three former wire fields `start` / `logged` / `trashed` are DELETED from
15
+ * every item and replaced by ONE derived `stage` ∈ `inbox | upcoming | anytime |
16
+ * someday | logbook | trash` (src/read/stage.ts `deriveStage`, the single pure
17
+ * derivation reused by the card bucketing so `stage` can never disagree with the
18
+ * bucket a view puts an item in). Today/evening membership is a SEPARATE
19
+ * presence-keyed axis — `today: true` / `evening: true` (evening implies today) —
20
+ * derived in the mapper with the Today view's own two-arm predicate.
21
+ * - `stage` is DROPPED only where the enclosing node PROVABLY states it — the R6
22
+ * rule (drop only what the node provably says). That is the stage-PURE flat
23
+ * views (inbox, `anytime`, someday, logbook, trash) and the stage-named card
24
+ * sub-buckets (anytime/upcoming/someday/logbook/trash, which the bucketer splits
25
+ * BY stage). The `anytime` catalogue is stage-PURE (R10.2): every member is an
26
+ * Anytime-view row (ANYTIME_SELF) — undated-active, arrived-active, or arrived
27
+ * someday-scheduled — and an ARRIVED dated row derives `anytime` (Upcoming is
28
+ * STRICTLY FUTURE, UPC1), so the field is redundant there.
29
+ * `stage` is KEPT on the stage-MIXED or derived surfaces: the `upcoming` view
30
+ * carries deadline-forecast stage-`anytime`/`someday` rows alongside its
31
+ * future-dated stage-`upcoming` ones — dropping `stage` there would delete
32
+ * non-redundant information — plus `today` (mixes upcoming + anytime-deadline
33
+ * rows), search, changes, the projects/areas listings, the card NODE, and
34
+ * detail.
35
+ * - the former `todaySection` field is RETIRED from the wire entirely (R10.1):
36
+ * `todaySection: "evening"` merely duplicated the `evening: true` marker. It
37
+ * remains an internal entity field (the human render and the write-verify delta
38
+ * still read it); shaping deletes it from the JSON copy.
39
+ *
40
+ * ## R12 — `when`, the derived TIME-AXIS position (replaces startDate + markers)
41
+ * Today/evening membership and the scheduled/projected date collapse onto ONE
42
+ * derived, presence-keyed field `when` (src/read/stage.ts `deriveWhen`): `"today"`
43
+ * / `"evening"` (Today-view membership, from the SAME `today`/`evening` markers the
44
+ * mapper stamps — never re-derived, so `when` can never disagree with the star), a
45
+ * FUTURE ISO date (a strictly-future scheduled row, or a template's projected next
46
+ * occurrence), or absent (unscheduled + not in Today; also an unprojected template
47
+ * and every logged/trashed row). The doctrine line: **`stage` enumerates the
48
+ * sidebar BUCKETS an item lives in; `when` enumerates its TIME POSITIONS (today |
49
+ * evening | a future date).** Someday is deliberately NOT a `when` value (it is a
50
+ * bucket → stage). Tier/drop rules:
51
+ * - the former `today`/`evening` marker KEYS are DELETED from the wire on EVERY
52
+ * tier — `when` carries the fact; the markers stay internal (they feed `when`).
53
+ * - the raw `startDate` is DELETED in COMPACT (a list needs the position, not the
54
+ * substrate) and KEPT in FULL/DETAIL beside `when` — different facts: `startDate`
55
+ * = what is stored, `when` = where it sits.
56
+ * - a template's `repeating.nextOccurrence` is GONE from the wire — `when` replaces
57
+ * it (same fact, one word); an unprojected template has no `when`.
58
+ * - `when` is KEPT on every row it is present on. (The read-shape doctrine
59
+ * flattened the card `upcoming` date-groups into the project-/area-view flat
60
+ * `items[]`, so no enclosing node states the position anymore.) It rides the
61
+ * today view's flat `items[]` (Today-proper vs This-Evening), the project-/area-
62
+ * view `items[]`, and the flat `upcoming`/`anytime`/`inbox`/`someday` catalogues,
63
+ * search, changes (a deadline-pulled row reads `when: "today"` in the mixed
64
+ * search/changes surfaces, informatively; note R13 re-files it to stage `anytime`
65
+ * and the flat inbox/someday views now EXCLUDE it — it appears in the `anytime`
66
+ * catalogue instead, `when: "today"` kept, stage dropped as pure).
67
+ *
68
+ * ## R13 — provisional Today members + GUI-faithful pulled-row membership
69
+ * BANNER1 / BANNER1b (docs/lab/banner1-research.md). Two coupled facts:
70
+ * - **`provisional: true`** — a presence-keyed marker on every Today member the GUI
71
+ * pips / counts in the "You have N new to-dos" banner: a Today member NOT yet
72
+ * materialized (`start != active OR startDate IS NULL`, BANNER1 L1). Derived from
73
+ * the SAME today/evening markers + fields the stage/`when` axes use (never
74
+ * re-derived). Emitted on EVERY tier, NEVER dropped (the banner is not a section,
75
+ * so no node implies it). Absent on non-Today rows and on materialized ones.
76
+ * Read-only: the app clears it by materializing the row on banner-OK, a GUI-only
77
+ * side effect our read cannot perform (watchers beware).
78
+ * - **stage `anytime` for a deadline pull** — a due-deadline pull re-files an undated
79
+ * Inbox/Someday row into Anytime (deriveStage step 2½, L-A). So EVERY Today member
80
+ * derives stage `anytime`, and the today view's flat `items[]` is stage-PURE →
81
+ * `stage` is DROPPED there (TODAY_ITEM_DROP) while `when` is KEPT. The flat
82
+ * someday/inbox views EXCLUDE pulled rows and the anytime view INCLUDES them
83
+ * (src/read/views.ts + predicates.ts DEADLINE_PULLED) — GUI fidelity.
84
+ *
85
+ * ## Universal item-DTO reshapes (R9 — EVERY tier, EVERY read kind incl. detail)
86
+ * - **checklist nesting** — flat counts → presence-keyed `checklist: {open,total}`.
87
+ * - **todos counts** — a project's flat leaf-action counts → presence-keyed
88
+ * `todos: {open, total}` (omit when total 0).
89
+ * - **repeating template/instance split (R11)** — the wire drops the
90
+ * `isTemplate`/`isInstance` discriminators; key presence carries the fact. A
91
+ * TEMPLATE keeps a nested `repeating: {paused?, deadlined?, rule?,
92
+ * latestInstance?}` — the series object (rule config + backward pointer +
93
+ * state flags); presence MEANS template. The forward pointer `nextOccurrence`
94
+ * moved to the top-level `when` (R12 — a template's projected date IS its time
95
+ * position); `latestInstance` is detail-only (SL1). An INSTANCE keeps a flat
96
+ * `instanceOf: <templateUuid>` and no `repeating`. A plain row keeps neither.
97
+ * See {@link reshapeRepeatingWire}.
98
+ * - **string tags** — `tags`/`inheritedTags` become plain arrays of names.
99
+ * - **one project key** — a headed item's owning project (formerly
100
+ * `headingProject`) is merged into `project`; `headingProject` never appears.
101
+ *
102
+ * ## R6 — no-redundant-ancestry (both tiers)
103
+ * project-view children drop `project`+`area` (heading-group members also drop
104
+ * `heading`); area-view children/project-cards drop `area`; anytime/someday
105
+ * section items drop `area`. Mixed lists keep every ref. (In the COMPACT tier the
106
+ * `heading` ref is additionally dropped everywhere — R7.)
107
+ *
108
+ * ## R7 — named detail tiers (compact | full)
109
+ * List contexts default to COMPACT; `detail`/`show` and `--full` / `full:true`
110
+ * use FULL. Compact drops `created`/`modified`, the full `notes` string (a
111
+ * presence-keyed `hasNotes: true` marks a row with notes), and the `heading` ref;
112
+ * `status` is omitted when `open`. FULL keeps them but still applies R6, the
113
+ * universal reshapes, and R10.
114
+ */
115
+ import { deriveStage, deriveWhen, whenIsProvisional } from "./stage.js";
116
+ /** The DB-less default: assume every title round-trips (bare title, no uuid sibling). */
117
+ const ALWAYS_ROUND_TRIPS = { roundTrips: () => true };
118
+ /** The uuid of a `{uuid,title}` container Ref, or undefined for a non-object / string. */
119
+ function refUuid(v) {
120
+ if (v !== null && typeof v === "object" && typeof v["uuid"] === "string") {
121
+ return v["uuid"];
122
+ }
123
+ return undefined;
124
+ }
125
+ /**
126
+ * Whether a still-unflattened container Ref carries the repeating-TEMPLATE mark.
127
+ * Only project/container refs ever set it (area/heading refs never do — see
128
+ * entities.Ref), so this reads true only for a template PROJECT container.
129
+ */
130
+ function refIsTemplate(v) {
131
+ return v !== null && typeof v === "object" && v["isRepeatingTemplate"] === true;
132
+ }
133
+ /**
134
+ * Flatten ONE container ref `o[key]` from a `{uuid,title}` object to its bare
135
+ * TITLE string, adding a flat sibling `o[uuidKey]` = the full uuid ONLY when the
136
+ * round-trip law demands it: `forceUuid` (the FULL/detail tier — uuid siblings
137
+ * unconditional) OR the bare title does not resolve back to this exact entity
138
+ * (`!promoter.roundTrips`). A null/absent ref, or one already flattened to a
139
+ * string, is left untouched. The container's `isRepeatingTemplate` marker (a
140
+ * TTY-render disambiguator on the internal entity) does not survive the flatten
141
+ * — the human render reads the unshaped entity. {@link shapeItem} re-emits that
142
+ * fact for the JSON container PROJECT as the flat presence-keyed sibling
143
+ * `projectIsTemplate: true` BEFORE this flatten runs.
144
+ */
145
+ function flattenRef(o, key, uuidKey, kind, forceUuid, promoter, projectUuid) {
146
+ const ref = o[key];
147
+ if (ref === null || typeof ref !== "object")
148
+ return; // absent, or already a bare string
149
+ const r = ref;
150
+ const uuid = typeof r["uuid"] === "string" ? r["uuid"] : "";
151
+ const title = typeof r["title"] === "string" ? r["title"] : "";
152
+ o[key] = title;
153
+ if (forceUuid || !promoter.roundTrips(kind, title, uuid, projectUuid))
154
+ o[uuidKey] = uuid;
155
+ }
156
+ /**
157
+ * Fold the flat `tags` / `inheritedTags` arrays of `{title}` objects into plain
158
+ * arrays of tag NAMES (universal across tiers and kinds). Tag uuids were never
159
+ * on the wire; the title is the identity. A missing key is left missing; an
160
+ * empty array stays `[]` (omit-empty prunes it later).
161
+ */
162
+ function flattenTags(o) {
163
+ for (const key of ["tags", "inheritedTags"]) {
164
+ const v = o[key];
165
+ if (!Array.isArray(v))
166
+ continue;
167
+ o[key] = v.map((t) => t !== null && typeof t === "object" && "title" in t ? t["title"] : t);
168
+ }
169
+ }
170
+ /**
171
+ * Reshape the flat checklist counts (and, on a `detail` read, the items array)
172
+ * into ONE presence-keyed `checklist` object — universal across tiers and kinds.
173
+ * No checklist → the `checklist` key is absent entirely; otherwise
174
+ * `{open, total}`, plus `items` when the source carried them (detail reads).
175
+ */
176
+ function reshapeChecklist(o) {
177
+ const total = typeof o["checklistItemsCount"] === "number" ? o["checklistItemsCount"] : 0;
178
+ const open = typeof o["openChecklistItemsCount"] === "number" ? o["openChecklistItemsCount"] : 0;
179
+ const items = o["checklist"]; // ChecklistItem[] on a detail read; absent on list rows
180
+ delete o["checklistItemsCount"];
181
+ delete o["openChecklistItemsCount"];
182
+ delete o["checklist"];
183
+ const hasItems = Array.isArray(items) && items.length > 0;
184
+ if (total === 0 && !hasItems)
185
+ return; // no checklist → omit the key
186
+ const cl = { open, total };
187
+ if (Array.isArray(items))
188
+ cl["items"] = items;
189
+ o["checklist"] = cl;
190
+ }
191
+ /**
192
+ * Reshape a project's flat leaf-action counts into ONE presence-keyed `todos`
193
+ * object — universal across tiers and kinds, mirroring {@link reshapeChecklist}.
194
+ * The source columns are the app-maintained materialized child counts
195
+ * (`untrashedLeafActionsCount` / `openUntrashedLeafActionsCount`), which count
196
+ * to-do children only (headings and checklist items excluded by construction).
197
+ * No to-do children (total 0) → the `todos` key is absent entirely; otherwise
198
+ * `{open, total}`. A no-op on to-dos (they carry no such columns).
199
+ */
200
+ function reshapeTodos(o) {
201
+ const total = typeof o["untrashedLeafActionsCount"] === "number"
202
+ ? o["untrashedLeafActionsCount"]
203
+ : 0;
204
+ const open = typeof o["openUntrashedLeafActionsCount"] === "number"
205
+ ? o["openUntrashedLeafActionsCount"]
206
+ : 0;
207
+ delete o["untrashedLeafActionsCount"];
208
+ delete o["openUntrashedLeafActionsCount"];
209
+ if (total === 0)
210
+ return; // presence-keyed: no to-do children → omit the key
211
+ o["todos"] = { open, total };
212
+ }
213
+ /**
214
+ * R11 — rewrite the internal `repeating` block into the wire's template/instance
215
+ * split, mutating `o` in place. The internal entity carries the full
216
+ * RepeatingInfo (`isTemplate`/`isInstance`/`templateUuid`/…); the wire loses the
217
+ * `isTemplate`/`isInstance` discriminators entirely and instead lets KEY
218
+ * PRESENCE carry the fact:
219
+ *
220
+ * - **Template** (`isTemplate`) → a nested `repeating` object — the series state
221
+ * `{paused?, deadlined?, rule?, latestInstance?}`. Presence of `repeating` MEANS
222
+ * template (an unadorned template emits `repeating: {}` — a bare `{}` is NOT
223
+ * pruned by omit-empty, so the presence signal survives). The inner false
224
+ * booleans are default-pruned (presence-keyed). The forward pointer
225
+ * `nextOccurrence` moved OUT to the top-level `when` (R12 — a template's
226
+ * projected date IS its time position); `rule` and `latestInstance` stay
227
+ * detail-only (populated by src/read/detail.ts on `entity.repeating`);
228
+ * `latestInstance` is the backward pointer symmetric to `when`.
229
+ * - **Instance** (`isInstance`) → a flat presence-keyed `instanceOf:
230
+ * <templateUuid>` and NO `repeating` object. Presence of `instanceOf` MEANS
231
+ * instance.
232
+ * - **Plain** (neither) → neither key.
233
+ */
234
+ function reshapeRepeatingWire(o) {
235
+ const rep = o["repeating"];
236
+ delete o["repeating"];
237
+ if (rep === null || typeof rep !== "object")
238
+ return;
239
+ const r = rep;
240
+ if (r["isTemplate"] === true) {
241
+ const out = {}; // presence MEANS template — a bare {} survives omit-empty
242
+ if (r["paused"] === true)
243
+ out["paused"] = true;
244
+ if (r["deadlined"] === true)
245
+ out["deadlined"] = true;
246
+ if (r["rule"] != null)
247
+ out["rule"] = r["rule"]; // detail read only
248
+ // The SL1 "Show Latest" pick — detail-only; the backward pointer symmetric to
249
+ // the top-level `when` (the forward pointer, R12).
250
+ if (typeof r["latestInstance"] === "string")
251
+ out["latestInstance"] = r["latestInstance"];
252
+ o["repeating"] = out;
253
+ }
254
+ else if (r["isInstance"] === true) {
255
+ if (typeof r["templateUuid"] === "string")
256
+ o["instanceOf"] = r["templateUuid"];
257
+ }
258
+ }
259
+ /** The R10 stage input read straight off a materialized task entity. */
260
+ function stageOf(s) {
261
+ const repeating = s["repeating"];
262
+ const isTemplate = repeating !== null &&
263
+ typeof repeating === "object" &&
264
+ repeating["isTemplate"] === true;
265
+ return deriveStage({
266
+ trashed: s["trashed"] === true,
267
+ logged: s["logged"] === true,
268
+ start: s["start"],
269
+ startDate: s["startDate"] ?? null,
270
+ repeating: { isTemplate },
271
+ // The presence-keyed Today marker (stamped at materialize with the response
272
+ // clock) discriminates an ARRIVED dated row (→ anytime) from a strictly-
273
+ // future one (→ upcoming). Read BEFORE the marker is stripped downstream.
274
+ today: s["today"] === true,
275
+ });
276
+ }
277
+ /** The R12 `when` input read straight off a materialized task entity (given its stage). */
278
+ function whenOf(s, stage) {
279
+ const repeating = s["repeating"];
280
+ const isTemplate = repeating !== null &&
281
+ typeof repeating === "object" &&
282
+ repeating["isTemplate"] === true;
283
+ const nextOccurrence = isTemplate && typeof repeating["nextOccurrence"] === "string"
284
+ ? repeating["nextOccurrence"]
285
+ : null;
286
+ return deriveWhen({
287
+ stage,
288
+ // The SAME presence-keyed markers stageOf reads — never re-derived, so a
289
+ // `when` of today/evening can never disagree with Today-view membership.
290
+ today: s["today"] === true,
291
+ evening: s["evening"] === true,
292
+ startDate: s["startDate"] ?? null,
293
+ repeating: { isTemplate, nextOccurrence },
294
+ });
295
+ }
296
+ /**
297
+ * Shape ONE task entity (to-do or project): the universal reshapes, the R10/R12
298
+ * stage/`when` rewrite, then the R6 ancestry drops, then — when `compact` — the
299
+ * R7 default-pruning. A shallow copy is taken so unknown sibling keys
300
+ * (`changeKind` on a changes row, `match` on a search hit) pass through
301
+ * untouched. Non-task values (areas, tags, refs, headings) are returned as-is.
302
+ */
303
+ function shapeItem(src, drop, compact, promoter) {
304
+ if (src === null || typeof src !== "object")
305
+ return src;
306
+ const s = src;
307
+ const type = s["type"];
308
+ if (type !== "to-do" && type !== "project")
309
+ return src; // not a shaped entity
310
+ const stage = stageOf(s); // from the ORIGINAL fields, before any reshape
311
+ const when = whenOf(s, stage); // R12 — derived from the same fields + markers
312
+ // R13 (BANNER1 law L-B): a Today member is PROVISIONAL — the GUI pips it and
313
+ // counts it in the "You have N new to-dos" banner — until the app MATERIALIZES
314
+ // it (start:=1, startDate:=today). Presence-keyed marker, derived from the SAME
315
+ // inputs as the stage/`when` axes (never an independent re-derivation): the row
316
+ // is a Today member (via the today/evening markers `whenOf` reuses — so it can
317
+ // never disagree with the star) AND not yet materialized (BANNER1 L1:
318
+ // `start != active OR startDate IS NULL`). Templates are never Today members,
319
+ // so they never mark. NEVER dropped by any view/section — the banner is not a
320
+ // section, so no enclosing node implies it. The app rewrites start/startDate
321
+ // when the user acknowledges the banner; that is a GUI-only side effect our
322
+ // read cannot clear (watchers beware — see contract.md `provisional`).
323
+ const provisional = whenIsProvisional(when, s["start"], s["startDate"] ?? null);
324
+ const o = { ...s };
325
+ // R9 universal reshapes (every tier, every kind incl. detail).
326
+ reshapeChecklist(o);
327
+ reshapeTodos(o);
328
+ flattenTags(o);
329
+ reshapeRepeatingWire(o);
330
+ if (o["project"] == null && o["headingProject"] != null)
331
+ o["project"] = o["headingProject"];
332
+ delete o["headingProject"];
333
+ // R10 — the three lifecycle fields are replaced by the one derived `stage`.
334
+ delete o["start"];
335
+ delete o["logged"];
336
+ delete o["trashed"];
337
+ // R10.1 — `todaySection` is retired from the wire (it duplicated `evening`);
338
+ // the internal entity keeps it for the render / write-verify paths.
339
+ delete o["todaySection"];
340
+ if (drop.stage !== true)
341
+ o["stage"] = stage;
342
+ // R12 — the today/evening marker KEYS are replaced by the derived `when` on
343
+ // EVERY tier (they never appear on the wire); `when` is emitted on every row it
344
+ // is present on (since the read-shape doctrine flattened the card date-groups,
345
+ // no enclosing node states the position anymore — the flat lists carry it).
346
+ delete o["today"];
347
+ delete o["evening"];
348
+ // §9n — a reminder byte is presentation-dead once its `startDate` goes strictly
349
+ // past: the GUI hides the bell but never clears the byte. The materialize-time
350
+ // `reminderLive` marker (mappers, via `reminderIsLive` under the response clock)
351
+ // says whether the stored reminder still renders; drop the `reminder` key when
352
+ // it does not, mirroring the GUI. The marker itself never rides the wire.
353
+ if (o["reminderLive"] !== true)
354
+ delete o["reminder"];
355
+ delete o["reminderLive"];
356
+ if (drop.when !== true && when !== undefined)
357
+ o["when"] = when;
358
+ // R13 — the provisional banner marker (never dropped; presence-keyed).
359
+ if (provisional)
360
+ o["provisional"] = true;
361
+ // The owning project's uuid scopes the heading round-trip (headings resolve
362
+ // within their project). Captured BEFORE the R6 project-drop so a project-view
363
+ // LOGBOOK row — whose `project` is dropped as redundant, yet which KEEPS its
364
+ // heading ref (drop.keepHeading) — can still promote its `headingUuid` in the
365
+ // project's scope.
366
+ const projectUuid = refUuid(o["project"]);
367
+ // R6 — drop redundant ancestry (both tiers).
368
+ if (drop.project === true)
369
+ delete o["project"];
370
+ if (drop.area === true)
371
+ delete o["area"];
372
+ if (drop.heading === true)
373
+ delete o["heading"];
374
+ // Absent `type` = to-do — omit it on to-do rows (project/heading keep theirs).
375
+ if (o["type"] === "to-do")
376
+ delete o["type"];
377
+ // Flatten the surviving container refs to bare TITLE strings, adding a flat
378
+ // `*Uuid` sibling per the round-trip law (FULL tier: unconditional; compact:
379
+ // only when the title would not resolve back to this entity). The `heading`
380
+ // ref is compact-dropped below (except drop.keepHeading), so it is flattened
381
+ // on the FULL tier OR when a logbook row explicitly keeps it.
382
+ const forceUuid = !compact;
383
+ // The container PROJECT's repeating-TEMPLATE fact — the JSON twin of the TTY ↻
384
+ // glyph (src/cli/render.ts). flattenRef discards the internal ref's
385
+ // `isRepeatingTemplate` marker, so re-emit it here as a flat presence-keyed
386
+ // sibling of the `project` ref (never `false`), riding wherever `project`
387
+ // rides. A heading-nested row already merged its owning project into `project`
388
+ // above, so direct AND headed template children mark; the R6 project-drop
389
+ // above already removed `project` where the view implies it, so a project-view
390
+ // child carries no orphaned marker. Both tiers — it is a correctness signal,
391
+ // not detail. Only project refs ever carry the flag (area/heading never do).
392
+ if (refIsTemplate(o["project"]))
393
+ o["projectIsTemplate"] = true;
394
+ flattenRef(o, "project", "projectUuid", "project", forceUuid, promoter);
395
+ flattenRef(o, "area", "areaUuid", "area", forceUuid, promoter);
396
+ if (!compact || drop.keepHeading === true)
397
+ flattenRef(o, "heading", "headingUuid", "heading", forceUuid, promoter, projectUuid);
398
+ // R12 — FULL/DETAIL keep the raw `startDate` beside `when` as the SUBSTRATE
399
+ // (`startDate` = what is stored, `when` = where it sits). COMPACT drops it below
400
+ // (the position `when` carries is what a list needs).
401
+ if (!compact)
402
+ return o;
403
+ // R7 compact — default-pruning (absence = the default).
404
+ delete o["startDate"]; // R12 — position lives in `when`; substrate is full-tier only
405
+ if (o["status"] === "open")
406
+ delete o["status"];
407
+ delete o["created"];
408
+ delete o["modified"];
409
+ const notes = typeof o["notes"] === "string" ? o["notes"] : "";
410
+ delete o["notes"];
411
+ if (notes !== "")
412
+ o["hasNotes"] = true;
413
+ // The heading ref is compact-dropped everywhere (the GUI shows the project,
414
+ // never the heading, outside a project view). Full tier / detail keep it; a
415
+ // project-view logbook row (drop.keepHeading) keeps it too — flattened above.
416
+ if (drop.keepHeading !== true)
417
+ delete o["heading"];
418
+ return o;
419
+ }
420
+ /** Map a plain array of items with the item shaper. */
421
+ function shapeList(items, drop, compact, promoter) {
422
+ if (!Array.isArray(items))
423
+ return items;
424
+ return items.map((i) => shapeItem(i, drop, compact, promoter));
425
+ }
426
+ /** Copy `base` and overwrite `items` with the shaped list (avoids spread-in-map). */
427
+ function withShapedItems(base, drop, compact, promoter) {
428
+ const out = { ...base };
429
+ out["items"] = shapeList(base["items"], drop, compact, promoter);
430
+ return out;
431
+ }
432
+ /** Coerce an unknown value to an array (empty when absent). */
433
+ const asArray = (v) => (Array.isArray(v) ? v : []);
434
+ /**
435
+ * The flat project-view `items[]` rows (read-shape doctrine §3.12): drop
436
+ * project/area (the card states them) but KEEP `stage` (the list is stage-MIXED:
437
+ * anytime + upcoming + someday + closed-unswept), `when`, and the `heading` ref.
438
+ * The heading ref is kept + flattened even in compact (keepHeading), scoped to
439
+ * the owning project — flat title + `headingUuid` when the title would not
440
+ * round-trip — so a consumer reconstructs a heading's members from the rows.
441
+ */
442
+ const PROJECT_ITEM_DROP = { project: true, area: true, keepHeading: true };
443
+ /**
444
+ * Project-view LOGBOOK (flat logged) rows (read-shape doctrine §3.12 / #C4): drop
445
+ * project/area (the card states them) but KEEP `stage` and the `heading` ref
446
+ * (drop.keepHeading). The bucket is stage-MIXED since it absorbs the children of
447
+ * SWEPT ARCHIVED headings too — mostly `logbook`, but it can hold the odd OPEN
448
+ * child a Put-Back stranded under an archived heading (HEADARC2-C, stage
449
+ * `anytime`) — so `stage` is not provably implied and is kept. The heading ref is
450
+ * the GUI hint (the in-project logged toggle labels the HEADING; the global
451
+ * Logbook labels the PROJECT — the two-view asymmetry, HEADARC2-B), flat title +
452
+ * project-scoped `headingUuid`.
453
+ */
454
+ const PROJECT_LOGBOOK_DROP = {
455
+ project: true,
456
+ area: true,
457
+ keepHeading: true,
458
+ };
459
+ /**
460
+ * The flat area-view `items[]` rows (read-shape doctrine §3.13): drop `area` (the
461
+ * card states it) but KEEP `stage` (the list is stage-MIXED — anytime + upcoming
462
+ * + someday + closed-unswept) and `when`. Area direct to-dos are never headed and
463
+ * never project-nested, so nothing else to drop.
464
+ */
465
+ const AREA_ITEM_DROP = { area: true };
466
+ /** Area-view PROJECTS list: a mixed listing of the area's project rows — keep `stage`, drop area. */
467
+ const AREA_PROJECTS_DROP = { area: true };
468
+ /**
469
+ * Anytime sidebar-section items: stage-PURE → drop area + the section-implied
470
+ * stage (R10.2). Every Anytime-view member derives `anytime`: ANYTIME_SELF
471
+ * (undated-active, arrived-active, arrived someday-scheduled) — an ARRIVED dated
472
+ * row is Anytime, not Upcoming (Upcoming is STRICTLY FUTURE, UPC1) — AND, since
473
+ * R13, the DEADLINE-PULLED undated Inbox/Someday rows the view now includes
474
+ * (BANNER1b), which derive `anytime` too (deriveStage step 2½: a Today-marked
475
+ * undated row is a pull → anytime). Repeating templates are excluded from the
476
+ * view (NOT_TEMPLATE), so no stage-`upcoming` row can appear here — still pure.
477
+ */
478
+ const ANYTIME_SECTION_DROP = { area: true, stage: true };
479
+ /** Someday sidebar-section items: stage-PURE → drop area + the bucket-implied stage. */
480
+ const SOMEDAY_SECTION_DROP = { area: true, stage: true };
481
+ /** The card NODE / detail / mixed lists: keep every ref, `stage`, and `when`. */
482
+ const NO_DROP = {};
483
+ /**
484
+ * The today view's flat `items[]`: drop the view-implied `stage` (R13) but KEEP
485
+ * `when`. Every Today member derives stage `anytime` by construction — an ARRIVED
486
+ * `startDate` (step 5) or a DEADLINE PULL (step 2½) both derive `anytime`, and
487
+ * there are no future-dated or undated-someday Today members — so the today view
488
+ * is provably stage-PURE `anytime` and the field is redundant (verified strict by
489
+ * the today purity property test in test/unit/stage.test.ts). `when` is KEPT: the
490
+ * flat list interleaves Today-proper (`when: "today"`) and This-Evening
491
+ * (`when: "evening"`) members, so each row must carry which render section it
492
+ * belongs to — the split is derived from `when`, not a wire bucket. `provisional`
493
+ * is NOT a drop — the banner pip is per-row, nothing implies it.
494
+ */
495
+ const TODAY_ITEM_DROP = { stage: true };
496
+ /**
497
+ * Shape a heading catalog entry (a `headings[]` node — the flat `{uuid,title,
498
+ * archived?}` catalog, read-shape doctrine §3.12 / #C3). The type is implied by
499
+ * position, and a heading has no open/canceled/completed vocabulary the reader
500
+ * needs — so:
501
+ * - DROP `type` (positional: this slot is always a heading; the "absent type =
502
+ * to-do" convention is scoped to ROWS/candidates, never this catalog entry);
503
+ * - DROP `project` (the card states it);
504
+ * - REPLACE `status` with the presence-keyed `archived` (the stopDate, an ISO
505
+ * date-time following the `stopped`/logged-row convention) — emitted ONLY when
506
+ * the heading is archived (status "completed"), OMITTED when open. The entry
507
+ * carries only whether-and-when it was archived; sweptness (past the logbook
508
+ * boundary) is not on the wire — the TTY derives it (#C3a).
509
+ */
510
+ function shapeHeadingNode(src) {
511
+ if (src === null || typeof src !== "object")
512
+ return src;
513
+ const h = { ...src };
514
+ delete h["project"];
515
+ delete h["type"];
516
+ const isArchived = h["status"] !== undefined && h["status"] !== "open";
517
+ const stopped = h["stopped"];
518
+ delete h["status"];
519
+ delete h["stopped"];
520
+ // Presence-keyed `archived` — the ISO archive timestamp (past-participle twin of
521
+ // `stopped`/`created`/`modified`), full-datetime serialization like `stopped`.
522
+ if (isArchived && stopped != null)
523
+ h["archived"] = stopped;
524
+ return h;
525
+ }
526
+ /** Stopped-DESC comparator for the flat logbook (open odd children — no stopDate — sort last). */
527
+ function byStoppedDesc(a, b) {
528
+ const t = (x) => {
529
+ const s = x !== null && typeof x === "object" ? x["stopped"] : null;
530
+ return s instanceof Date ? s.getTime() : -Infinity;
531
+ };
532
+ return t(b) - t(a);
533
+ }
534
+ /**
535
+ * Shape a project view (read-shape doctrine §3.12 / #C3 / #C4). The live children
536
+ * are ONE flat `items[]` in project index order — each row carrying `stage`,
537
+ * `when`, and its `heading` ref (flat title + project-scoped `headingUuid`) — so a
538
+ * consumer reconstructs a heading's members by filtering `items` on `heading`.
539
+ * `headings[]` is the flat catalog `[{uuid,title,archived?}]` of EVERY heading
540
+ * (live + swept archived) in index order — the ORDER axis. `logbook` is one flat
541
+ * `stopDate DESC` list of ALL swept children — of open headings, un-headed, AND
542
+ * archived headings — each carrying its `heading` ref; `logbookHeadings` is gone
543
+ * (its rows folded in, the archived heading itself now a catalog entry with
544
+ * `archived`). The card node is left full + ancestry-intact.
545
+ */
546
+ function shapeProjectView(view, compact, promoter) {
547
+ // The heading catalog: every heading node (live + swept archived), index order,
548
+ // flattened to `{uuid,title,archived?}`. Membership rides the flat item/logbook
549
+ // row `heading` refs; archived-ness reads off `archived` here.
550
+ const headings = Array.isArray(view["headingCatalog"])
551
+ ? view["headingCatalog"].map(shapeHeadingNode)
552
+ : [];
553
+ // The flat logbook: swept children of open/un-headed headings (`logged`) PLUS
554
+ // the children of swept archived headings (`loggedHeadings`), merged into ONE
555
+ // stopDate-DESC list (open odd children last), each carrying its heading ref.
556
+ const loggedRows = [
557
+ ...asArray(view["logged"]),
558
+ ...asArray(view["loggedHeadings"]).flatMap((g) => g !== null && typeof g === "object" ? asArray(g["items"]) : []),
559
+ ].toSorted(byStoppedDesc);
560
+ const out = { ...view };
561
+ delete out["active"];
562
+ delete out["scheduled"];
563
+ delete out["repeating"];
564
+ delete out["someday"];
565
+ delete out["logged"];
566
+ delete out["loggedHeadings"];
567
+ delete out["headingCatalog"];
568
+ // Trashed children live only in `things trash` — never a project-view bucket.
569
+ // Delete defensively in case an untyped source carries the old key.
570
+ delete out["trashed"];
571
+ // The project card NODE keeps everything (children derive their container from
572
+ // it), but is still an item DTO, so the universal + R10 reshapes apply.
573
+ out["project"] = shapeItem(view["project"], NO_DROP, false, promoter);
574
+ // The flat live children — stage/when/heading kept, project/area dropped (the
575
+ // card states them). The heading ref is kept + flattened even in compact
576
+ // (keepHeading), project-scoped like the logbook rows.
577
+ out["items"] = shapeList(view["items"], PROJECT_ITEM_DROP, compact, promoter);
578
+ out["headings"] = headings;
579
+ // A project keeps its in-context `logbook` (a project is a bounded object with a
580
+ // real done-state); trashed children live only in `things trash`. The flat
581
+ // rows KEEP stage (mixed) + their heading ref (PROJECT_LOGBOOK_DROP).
582
+ out["logbook"] = shapeList(loggedRows, PROJECT_LOGBOOK_DROP, compact, promoter);
583
+ return out;
584
+ }
585
+ /**
586
+ * Shape an area view (read-shape doctrine §3.13). The direct to-dos dissolve into
587
+ * ONE flat `items[]` in index order — each row carrying `stage` + `when`, `area`
588
+ * dropped (the card states it). The stage/date sub-buckets (anytime/upcoming/
589
+ * someday) are gone. `projects[]` is KEPT — the area's child-project sidebar rank
590
+ * is a DISTINCT order axis from the direct-to-do index, so it earns its own list
591
+ * (mixed-stage, someday/scheduled projects included; the render split is TTY-only).
592
+ * The area node keeps its identity (tags folded).
593
+ */
594
+ function shapeAreaView(view, compact, promoter) {
595
+ const out = { ...view };
596
+ delete out["active"];
597
+ delete out["scheduled"];
598
+ delete out["repeating"];
599
+ delete out["someday"];
600
+ // No `logbook` or `trash` bucket: an area's logbook is the bounded query
601
+ // `things logbook --area <ref>`, and trashed rows live only in `things trash`.
602
+ // Delete defensively in case an untyped source carries the old keys.
603
+ delete out["logged"];
604
+ delete out["trashed"];
605
+ out["area"] = shapeArea(view["area"]);
606
+ // The flat direct to-dos — stage/when kept, area dropped.
607
+ out["items"] = shapeList(view["items"], AREA_ITEM_DROP, compact, promoter);
608
+ // The projects list is a mixed listing of the area's project rows — keep stage,
609
+ // sidebar order (the distinct order axis).
610
+ out["projects"] = shapeList(view["projects"], AREA_PROJECTS_DROP, compact, promoter);
611
+ return out;
612
+ }
613
+ /** Fold an area entity's tags to string names in place (returns a shallow copy). */
614
+ function shapeArea(src) {
615
+ if (src === null || typeof src !== "object")
616
+ return src;
617
+ const o = { ...src };
618
+ flattenTags(o);
619
+ return o;
620
+ }
621
+ /** Shape sidebar sections (anytime/someday catalogues) with the section's drop spec. */
622
+ function shapeSections(sections, drop, compact, promoter) {
623
+ if (!Array.isArray(sections))
624
+ return sections;
625
+ return sections.map((s) => s === null || typeof s !== "object" ? s : withShapedItems(s, drop, compact, promoter));
626
+ }
627
+ /**
628
+ * The flat, mixed-provenance list kinds mapped to their drop spec. Only the
629
+ * stage-PURE catalogues (inbox/someday/logbook/trash; the section-based `anytime`
630
+ * is pure too, handled via shapeSections below) drop the bucket-implied `stage`.
631
+ * `upcoming` KEEPS it (R10.2): the Upcoming view is stage-mixed — it carries
632
+ * deadline-forecast stage-`anytime`/`someday` rows alongside future-dated
633
+ * stage-`upcoming` ones. The mixed/derived surfaces (search/changes/projects)
634
+ * keep it too.
635
+ */
636
+ const FLAT_LIST_DROP = new Map([
637
+ ["inbox", { stage: true }],
638
+ ["upcoming", NO_DROP],
639
+ ["logbook", { stage: true }],
640
+ ["trash", { stage: true }],
641
+ ["changes", NO_DROP],
642
+ ["search", NO_DROP],
643
+ ["projects", NO_DROP],
644
+ ]);
645
+ /**
646
+ * Apply the universal reshapes + R6 + R7 + R10 to a read payload for one view
647
+ * `kind`. `full` forces the FULL tier (R7 default-pruning off, everything else
648
+ * applied); an unrecognized kind passes through unchanged. The input is never
649
+ * mutated (shallow copies throughout), so the human-render path keeps the full
650
+ * entities.
651
+ */
652
+ export function shapeReadPayload(kind, data, full, promoter) {
653
+ // The ref-promotion oracle drives the round-trip law for flat container refs.
654
+ // Absent (a DB-less unit shaping): assume every title round-trips — bare
655
+ // titles, no uuid siblings. Production always passes the client's promoter.
656
+ const p = promoter ?? ALWAYS_ROUND_TRIPS;
657
+ // `detail` is the FULL record and drops no ancestry / stage / `when`.
658
+ if (kind === "detail")
659
+ return shapeItem(data, NO_DROP, false, p);
660
+ const compact = !full;
661
+ const flatDrop = FLAT_LIST_DROP.get(kind);
662
+ if (flatDrop !== undefined)
663
+ return shapeList(data, flatDrop, compact, p);
664
+ // The today view: one flat `items[]` of Today members (Today-proper + This
665
+ // Evening interleaved in comparator order); drop the view-implied `stage`,
666
+ // KEEP `when` (it carries the render section). The `counts` aggregate rides
667
+ // `meta.counts`, not `data`. The wire wrapper is `{ items }` (read-driver).
668
+ if (kind === "today" && data !== null && typeof data === "object") {
669
+ return shapeList(data["items"], TODAY_ITEM_DROP, compact, p);
670
+ }
671
+ if (kind === "anytime" && Array.isArray(data)) {
672
+ return shapeSections(data, ANYTIME_SECTION_DROP, compact, p); // stage-pure → drop stage
673
+ }
674
+ if (kind === "someday" && Array.isArray(data)) {
675
+ return shapeSections(data, SOMEDAY_SECTION_DROP, compact, p); // stage-pure → drop stage
676
+ }
677
+ if (kind === "area-view" && data !== null && typeof data === "object") {
678
+ return shapeAreaView(data, compact, p);
679
+ }
680
+ if (kind === "project-view" && data !== null && typeof data === "object") {
681
+ return shapeProjectView(data, compact, p);
682
+ }
683
+ // The `areas` listing carries Area entities whose tags fold to names.
684
+ if (kind === "areas" && Array.isArray(data))
685
+ return data.map(shapeArea);
686
+ // tags / legend / snapshot / diagnostics: not tag-carrying entity payloads.
687
+ return data;
688
+ }
689
+ /** The fixed cap on a listed candidate array; the error `message` states the total when it overflows. */
690
+ export const CANDIDATE_CAP = 8;
691
+ /** Read a container hint — a Ref `{title}` or a plain title string — to its non-empty title, else null. */
692
+ function candidateContainerTitle(v) {
693
+ if (typeof v === "string")
694
+ return v === "" ? null : v;
695
+ if (v !== null && typeof v === "object") {
696
+ const t = v["title"];
697
+ if (typeof t === "string" && t !== "")
698
+ return t;
699
+ }
700
+ return null;
701
+ }
702
+ /**
703
+ * Project ONE entity — a materialized to-do/project/area/heading, or a thin
704
+ * `{uuid, title}` resolver row — to the fixed {@link CandidateRef}. The SINGLE
705
+ * source every error-candidate emit flows through (the did-you-mean fallback and
706
+ * every not-found/ambiguous resolver), so the candidate shape can never vary by
707
+ * site. Reuses the wire's own {@link stageOf}/{@link whenOf} derivations — the
708
+ * lifecycle words are never re-derived here.
709
+ */
710
+ export function candidateRef(type, src) {
711
+ const s = (src ?? {});
712
+ const out = {
713
+ uuid: typeof s["uuid"] === "string" ? s["uuid"] : "",
714
+ title: typeof s["title"] === "string" ? s["title"] : "",
715
+ };
716
+ // Absent `type` = to-do — emit it only for the other kinds.
717
+ if (type !== "to-do")
718
+ out.type = type;
719
+ const area = candidateContainerTitle(s["area"]);
720
+ if (area !== null)
721
+ out.area = area;
722
+ const project = candidateContainerTitle(s["project"]) ?? candidateContainerTitle(s["headingProject"]);
723
+ if (project !== null)
724
+ out.project = project;
725
+ // stage/when only for the task kinds, and only when the source carries the
726
+ // materialized lifecycle substrate (`start`) — a thin uuid+title resolver row
727
+ // does not, so the keys stay absent (presence-keyed, like the wire).
728
+ if ((type === "to-do" || type === "project") && typeof s["start"] === "string") {
729
+ const stage = stageOf(s);
730
+ out.stage = stage;
731
+ const when = whenOf(s, stage);
732
+ if (when !== undefined)
733
+ out.when = when;
734
+ }
735
+ return out;
736
+ }
737
+ //# sourceMappingURL=shape.js.map