things-api 0.10.0 → 0.12.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 (239) hide show
  1. package/README.md +50 -13
  2. package/dist/audit/schema.d.ts +7 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/clock.d.ts +8 -0
  5. package/dist/cli/clock.js +24 -0
  6. package/dist/cli/clock.js.map +1 -0
  7. package/dist/cli/commands/area.d.ts +18 -11
  8. package/dist/cli/commands/area.js +61 -74
  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 +60 -0
  14. package/dist/cli/commands/install-skill.js +256 -0
  15. package/dist/cli/commands/install-skill.js.map +1 -0
  16. package/dist/cli/commands/mcp.js +9 -0
  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 +27 -34
  20. package/dist/cli/commands/project.js.map +1 -1
  21. package/dist/cli/commands/reads.d.ts +15 -2
  22. package/dist/cli/commands/reads.js +144 -44
  23. package/dist/cli/commands/reads.js.map +1 -1
  24. package/dist/cli/commands/setup.js +7 -2
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/show.js +38 -14
  27. package/dist/cli/commands/show.js.map +1 -1
  28. package/dist/cli/commands/todo.js +15 -9
  29. package/dist/cli/commands/todo.js.map +1 -1
  30. package/dist/cli/commands/writes.js +797 -174
  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 +26 -3
  34. package/dist/cli/did-you-mean.js.map +1 -1
  35. package/dist/cli/glyphs.d.ts +32 -2
  36. package/dist/cli/glyphs.js +83 -12
  37. package/dist/cli/glyphs.js.map +1 -1
  38. package/dist/cli/help.js +121 -16
  39. package/dist/cli/help.js.map +1 -1
  40. package/dist/cli/main.js +33 -2
  41. package/dist/cli/main.js.map +1 -1
  42. package/dist/cli/move-hint.d.ts +46 -0
  43. package/dist/cli/move-hint.js +196 -0
  44. package/dist/cli/move-hint.js.map +1 -0
  45. package/dist/cli/period.d.ts +2 -8
  46. package/dist/cli/period.js +44 -30
  47. package/dist/cli/period.js.map +1 -1
  48. package/dist/cli/read-driver.d.ts +43 -5
  49. package/dist/cli/read-driver.js +91 -15
  50. package/dist/cli/read-driver.js.map +1 -1
  51. package/dist/cli/render.d.ts +19 -8
  52. package/dist/cli/render.js +70 -27
  53. package/dist/cli/render.js.map +1 -1
  54. package/dist/cli/resolve-invocation.d.ts +2 -2
  55. package/dist/cli/resolve-invocation.js +3 -3
  56. package/dist/cli/resolve-invocation.js.map +1 -1
  57. package/dist/cli/skill-check.d.ts +27 -0
  58. package/dist/cli/skill-check.js +99 -0
  59. package/dist/cli/skill-check.js.map +1 -0
  60. package/dist/cli/skill.d.ts +70 -0
  61. package/dist/cli/skill.js +152 -0
  62. package/dist/cli/skill.js.map +1 -0
  63. package/dist/cli/tag-filters.d.ts +6 -2
  64. package/dist/cli/tag-filters.js +8 -6
  65. package/dist/cli/tag-filters.js.map +1 -1
  66. package/dist/cli/verb-hint.js +28 -3
  67. package/dist/cli/verb-hint.js.map +1 -1
  68. package/dist/cli/version.d.ts +4 -0
  69. package/dist/cli/version.js +18 -0
  70. package/dist/cli/version.js.map +1 -0
  71. package/dist/client.d.ts +171 -27
  72. package/dist/client.js +279 -40
  73. package/dist/client.js.map +1 -1
  74. package/dist/config.d.ts +66 -2
  75. package/dist/config.js +120 -12
  76. package/dist/config.js.map +1 -1
  77. package/dist/contracts.d.ts +186 -28
  78. package/dist/contracts.js +22 -1
  79. package/dist/contracts.js.map +1 -1
  80. package/dist/diagnose.d.ts +34 -2
  81. package/dist/diagnose.js +36 -1
  82. package/dist/diagnose.js.map +1 -1
  83. package/dist/index.d.ts +25 -9
  84. package/dist/index.js +22 -3
  85. package/dist/index.js.map +1 -1
  86. package/dist/mcp/server.d.ts +9 -0
  87. package/dist/mcp/server.js +1157 -684
  88. package/dist/mcp/server.js.map +1 -1
  89. package/dist/model/clock.d.ts +61 -0
  90. package/dist/model/clock.js +71 -0
  91. package/dist/model/clock.js.map +1 -0
  92. package/dist/model/dates.d.ts +26 -3
  93. package/dist/model/dates.js +84 -3
  94. package/dist/model/dates.js.map +1 -1
  95. package/dist/model/entities.d.ts +60 -3
  96. package/dist/model/entities.js.map +1 -1
  97. package/dist/model/mappers.d.ts +4 -2
  98. package/dist/model/mappers.js +66 -9
  99. package/dist/model/mappers.js.map +1 -1
  100. package/dist/model/recurrence.d.ts +8 -1
  101. package/dist/model/recurrence.js.map +1 -1
  102. package/dist/read/area-filter.d.ts +76 -0
  103. package/dist/read/area-filter.js +59 -0
  104. package/dist/read/area-filter.js.map +1 -0
  105. package/dist/read/area-view.d.ts +12 -12
  106. package/dist/read/area-view.js +58 -34
  107. package/dist/read/area-view.js.map +1 -1
  108. package/dist/read/detail.d.ts +1 -1
  109. package/dist/read/detail.js +26 -12
  110. package/dist/read/detail.js.map +1 -1
  111. package/dist/read/log-boundary.d.ts +1 -1
  112. package/dist/read/log-boundary.js +13 -3
  113. package/dist/read/log-boundary.js.map +1 -1
  114. package/dist/read/predicates.d.ts +18 -0
  115. package/dist/read/predicates.js +19 -0
  116. package/dist/read/predicates.js.map +1 -1
  117. package/dist/read/project-view.d.ts +32 -16
  118. package/dist/read/project-view.js +80 -45
  119. package/dist/read/project-view.js.map +1 -1
  120. package/dist/read/pseudo-area.d.ts +29 -0
  121. package/dist/read/pseudo-area.js +27 -0
  122. package/dist/read/pseudo-area.js.map +1 -0
  123. package/dist/read/queries.d.ts +108 -19
  124. package/dist/read/queries.js +212 -35
  125. package/dist/read/queries.js.map +1 -1
  126. package/dist/read/scope.d.ts +126 -0
  127. package/dist/read/scope.js +162 -0
  128. package/dist/read/scope.js.map +1 -0
  129. package/dist/read/search-rank.d.ts +2 -5
  130. package/dist/read/shape.d.ts +162 -0
  131. package/dist/read/shape.js +686 -0
  132. package/dist/read/shape.js.map +1 -0
  133. package/dist/read/show-target.d.ts +9 -1
  134. package/dist/read/show-target.js +45 -4
  135. package/dist/read/show-target.js.map +1 -1
  136. package/dist/read/snapshot.d.ts +1 -1
  137. package/dist/read/snapshot.js +9 -2
  138. package/dist/read/snapshot.js.map +1 -1
  139. package/dist/read/stage.d.ts +199 -0
  140. package/dist/read/stage.js +125 -0
  141. package/dist/read/stage.js.map +1 -0
  142. package/dist/read/truncation.d.ts +8 -6
  143. package/dist/read/truncation.js +35 -10
  144. package/dist/read/truncation.js.map +1 -1
  145. package/dist/read/views.d.ts +61 -20
  146. package/dist/read/views.js +240 -82
  147. package/dist/read/views.js.map +1 -1
  148. package/dist/surface-copy.d.ts +9 -0
  149. package/dist/surface-copy.js +9 -0
  150. package/dist/surface-copy.js.map +1 -1
  151. package/dist/write/accessibility-probe.d.ts +1 -1
  152. package/dist/write/accessibility-probe.js +8 -0
  153. package/dist/write/accessibility-probe.js.map +1 -1
  154. package/dist/write/automation-probe.d.ts +1 -1
  155. package/dist/write/automation-probe.js +8 -0
  156. package/dist/write/automation-probe.js.map +1 -1
  157. package/dist/write/batch.d.ts +49 -10
  158. package/dist/write/batch.js +423 -70
  159. package/dist/write/batch.js.map +1 -1
  160. package/dist/write/commands.d.ts +1 -1
  161. package/dist/write/commands.js +316 -66
  162. package/dist/write/commands.js.map +1 -1
  163. package/dist/write/guards.d.ts +1 -1
  164. package/dist/write/guards.js +94 -10
  165. package/dist/write/guards.js.map +1 -1
  166. package/dist/write/heading.d.ts +10 -1
  167. package/dist/write/heading.js +35 -5
  168. package/dist/write/heading.js.map +1 -1
  169. package/dist/write/make-repeating-project.d.ts +2 -2
  170. package/dist/write/make-repeating-project.js +9 -9
  171. package/dist/write/make-repeating-project.js.map +1 -1
  172. package/dist/write/move.d.ts +130 -0
  173. package/dist/write/move.js +1533 -0
  174. package/dist/write/move.js.map +1 -0
  175. package/dist/write/operations.d.ts +105 -19
  176. package/dist/write/operations.js +56 -7
  177. package/dist/write/operations.js.map +1 -1
  178. package/dist/write/opid.d.ts +31 -0
  179. package/dist/write/opid.js +30 -0
  180. package/dist/write/opid.js.map +1 -0
  181. package/dist/write/pipeline.d.ts +122 -3
  182. package/dist/write/pipeline.js +309 -52
  183. package/dist/write/pipeline.js.map +1 -1
  184. package/dist/write/planner.js +8 -1
  185. package/dist/write/planner.js.map +1 -1
  186. package/dist/write/pre-state.d.ts +197 -6
  187. package/dist/write/pre-state.js +427 -20
  188. package/dist/write/pre-state.js.map +1 -1
  189. package/dist/write/recurrence-rule-blob.d.ts +65 -0
  190. package/dist/write/recurrence-rule-blob.js +150 -0
  191. package/dist/write/recurrence-rule-blob.js.map +1 -0
  192. package/dist/write/reorder.d.ts +23 -3
  193. package/dist/write/reorder.js +1871 -85
  194. package/dist/write/reorder.js.map +1 -1
  195. package/dist/write/reversibility.js +19 -7
  196. package/dist/write/reversibility.js.map +1 -1
  197. package/dist/write/scope-guard.d.ts +31 -0
  198. package/dist/write/scope-guard.js +162 -0
  199. package/dist/write/scope-guard.js.map +1 -0
  200. package/dist/write/undo.js +120 -13
  201. package/dist/write/undo.js.map +1 -1
  202. package/dist/write/vectors/applescript.js +16 -7
  203. package/dist/write/vectors/applescript.js.map +1 -1
  204. package/dist/write/vectors/registry.d.ts +5 -7
  205. package/dist/write/vectors/registry.js +49 -2
  206. package/dist/write/vectors/registry.js.map +1 -1
  207. package/dist/write/vectors/shortcuts.js +1 -1
  208. package/dist/write/vectors/shortcuts.js.map +1 -1
  209. package/dist/write/vectors/simulator.d.ts +44 -0
  210. package/dist/write/vectors/simulator.js +1192 -0
  211. package/dist/write/vectors/simulator.js.map +1 -0
  212. package/dist/write/vectors/types.d.ts +46 -0
  213. package/dist/write/vectors/ui-certification.d.ts +10 -4
  214. package/dist/write/vectors/ui-certification.js +43 -10
  215. package/dist/write/vectors/ui-certification.js.map +1 -1
  216. package/dist/write/vectors/ui-drag.d.ts +21 -1
  217. package/dist/write/vectors/ui-drag.js +138 -27
  218. package/dist/write/vectors/ui-drag.js.map +1 -1
  219. package/dist/write/vectors/ui-recipes.d.ts +34 -1
  220. package/dist/write/vectors/ui-recipes.js +217 -6
  221. package/dist/write/vectors/ui-recipes.js.map +1 -1
  222. package/dist/write/vectors/ui.d.ts +40 -1
  223. package/dist/write/vectors/ui.js +196 -39
  224. package/dist/write/vectors/ui.js.map +1 -1
  225. package/dist/write/verify/delta.d.ts +127 -2
  226. package/dist/write/verify/delta.js +255 -16
  227. package/dist/write/verify/delta.js.map +1 -1
  228. package/dist/write/verify/poller.d.ts +7 -1
  229. package/dist/write/verify/poller.js +20 -1
  230. package/dist/write/verify/poller.js.map +1 -1
  231. package/package.json +24 -3
  232. package/schema/envelope.schema.json +383 -0
  233. package/skills/things-cli/SKILL.md +70 -0
  234. package/skills/things-cli/references/banner.md +35 -0
  235. package/skills/things-cli/references/contracts.md +62 -0
  236. package/skills/things-cli/references/errors.md +49 -0
  237. package/skills/things-cli/references/gui.md +11 -0
  238. package/skills/things-cli/references/model.md +60 -0
  239. package/skills/things-cli/references/ordering.md +71 -0
@@ -0,0 +1,686 @@
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); the resting-templates `{date: null}` group is
58
+ * unchanged (an unprojected template has no `when`).
59
+ * - `when` is DROPPED inside the `today` view's own sections (the section key states
60
+ * today/evening) and inside any card/heading `upcoming` DATE-GROUP for a member
61
+ * whose `when` equals the group's date (the group states it). KEPT everywhere
62
+ * else it is present — including the flat `upcoming`/`anytime`/`inbox`/`someday`
63
+ * catalogues, search, changes (a deadline-pulled row reads `when: "today"` in the
64
+ * mixed search/changes surfaces, informatively; note R13 re-files it to stage
65
+ * `anytime` and the flat inbox/someday views now EXCLUDE it — it appears in the
66
+ * `anytime` 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 own sections become stage-PURE →
81
+ * `stage` is DROPPED there (TODAY_SECTION_DROP), alongside the section-implied
82
+ * `when`. The flat someday/inbox views EXCLUDE pulled rows and the anytime view
83
+ * INCLUDES them (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
+ /**
117
+ * Fold the flat `tags` / `inheritedTags` arrays of `{title}` objects into plain
118
+ * arrays of tag NAMES (universal across tiers and kinds). Tag uuids were never
119
+ * on the wire; the title is the identity. A missing key is left missing; an
120
+ * empty array stays `[]` (omit-empty prunes it later).
121
+ */
122
+ function flattenTags(o) {
123
+ for (const key of ["tags", "inheritedTags"]) {
124
+ const v = o[key];
125
+ if (!Array.isArray(v))
126
+ continue;
127
+ o[key] = v.map((t) => t !== null && typeof t === "object" && "title" in t ? t["title"] : t);
128
+ }
129
+ }
130
+ /**
131
+ * Reshape the flat checklist counts (and, on a `detail` read, the items array)
132
+ * into ONE presence-keyed `checklist` object — universal across tiers and kinds.
133
+ * No checklist → the `checklist` key is absent entirely; otherwise
134
+ * `{open, total}`, plus `items` when the source carried them (detail reads).
135
+ */
136
+ function reshapeChecklist(o) {
137
+ const total = typeof o["checklistItemsCount"] === "number" ? o["checklistItemsCount"] : 0;
138
+ const open = typeof o["openChecklistItemsCount"] === "number" ? o["openChecklistItemsCount"] : 0;
139
+ const items = o["checklist"]; // ChecklistItem[] on a detail read; absent on list rows
140
+ delete o["checklistItemsCount"];
141
+ delete o["openChecklistItemsCount"];
142
+ delete o["checklist"];
143
+ const hasItems = Array.isArray(items) && items.length > 0;
144
+ if (total === 0 && !hasItems)
145
+ return; // no checklist → omit the key
146
+ const cl = { open, total };
147
+ if (Array.isArray(items))
148
+ cl["items"] = items;
149
+ o["checklist"] = cl;
150
+ }
151
+ /**
152
+ * Reshape a project's flat leaf-action counts into ONE presence-keyed `todos`
153
+ * object — universal across tiers and kinds, mirroring {@link reshapeChecklist}.
154
+ * The source columns are the app-maintained materialized child counts
155
+ * (`untrashedLeafActionsCount` / `openUntrashedLeafActionsCount`), which count
156
+ * to-do children only (headings and checklist items excluded by construction).
157
+ * No to-do children (total 0) → the `todos` key is absent entirely; otherwise
158
+ * `{open, total}`. A no-op on to-dos (they carry no such columns).
159
+ */
160
+ function reshapeTodos(o) {
161
+ const total = typeof o["untrashedLeafActionsCount"] === "number"
162
+ ? o["untrashedLeafActionsCount"]
163
+ : 0;
164
+ const open = typeof o["openUntrashedLeafActionsCount"] === "number"
165
+ ? o["openUntrashedLeafActionsCount"]
166
+ : 0;
167
+ delete o["untrashedLeafActionsCount"];
168
+ delete o["openUntrashedLeafActionsCount"];
169
+ if (total === 0)
170
+ return; // presence-keyed: no to-do children → omit the key
171
+ o["todos"] = { open, total };
172
+ }
173
+ /**
174
+ * R11 — rewrite the internal `repeating` block into the wire's template/instance
175
+ * split, mutating `o` in place. The internal entity carries the full
176
+ * RepeatingInfo (`isTemplate`/`isInstance`/`templateUuid`/…); the wire loses the
177
+ * `isTemplate`/`isInstance` discriminators entirely and instead lets KEY
178
+ * PRESENCE carry the fact:
179
+ *
180
+ * - **Template** (`isTemplate`) → a nested `repeating` object — the series state
181
+ * `{paused?, deadlined?, rule?, latestInstance?}`. Presence of `repeating` MEANS
182
+ * template (an unadorned template emits `repeating: {}` — a bare `{}` is NOT
183
+ * pruned by omit-empty, so the presence signal survives). The inner false
184
+ * booleans are default-pruned (presence-keyed). The forward pointer
185
+ * `nextOccurrence` moved OUT to the top-level `when` (R12 — a template's
186
+ * projected date IS its time position); `rule` and `latestInstance` stay
187
+ * detail-only (populated by src/read/detail.ts on `entity.repeating`);
188
+ * `latestInstance` is the backward pointer symmetric to `when`.
189
+ * - **Instance** (`isInstance`) → a flat presence-keyed `instanceOf:
190
+ * <templateUuid>` and NO `repeating` object. Presence of `instanceOf` MEANS
191
+ * instance.
192
+ * - **Plain** (neither) → neither key.
193
+ */
194
+ function reshapeRepeatingWire(o) {
195
+ const rep = o["repeating"];
196
+ delete o["repeating"];
197
+ if (rep === null || typeof rep !== "object")
198
+ return;
199
+ const r = rep;
200
+ if (r["isTemplate"] === true) {
201
+ const out = {}; // presence MEANS template — a bare {} survives omit-empty
202
+ if (r["paused"] === true)
203
+ out["paused"] = true;
204
+ if (r["deadlined"] === true)
205
+ out["deadlined"] = true;
206
+ if (r["rule"] != null)
207
+ out["rule"] = r["rule"]; // detail read only
208
+ // The SL1 "Show Latest" pick — detail-only; the backward pointer symmetric to
209
+ // the top-level `when` (the forward pointer, R12).
210
+ if (typeof r["latestInstance"] === "string")
211
+ out["latestInstance"] = r["latestInstance"];
212
+ o["repeating"] = out;
213
+ }
214
+ else if (r["isInstance"] === true) {
215
+ if (typeof r["templateUuid"] === "string")
216
+ o["instanceOf"] = r["templateUuid"];
217
+ }
218
+ }
219
+ /** The R10 stage input read straight off a materialized task entity. */
220
+ function stageOf(s) {
221
+ const repeating = s["repeating"];
222
+ const isTemplate = repeating !== null &&
223
+ typeof repeating === "object" &&
224
+ repeating["isTemplate"] === true;
225
+ return deriveStage({
226
+ trashed: s["trashed"] === true,
227
+ logged: s["logged"] === true,
228
+ start: s["start"],
229
+ startDate: s["startDate"] ?? null,
230
+ repeating: { isTemplate },
231
+ // The presence-keyed Today marker (stamped at materialize with the response
232
+ // clock) discriminates an ARRIVED dated row (→ anytime) from a strictly-
233
+ // future one (→ upcoming). Read BEFORE the marker is stripped downstream.
234
+ today: s["today"] === true,
235
+ });
236
+ }
237
+ /** The R12 `when` input read straight off a materialized task entity (given its stage). */
238
+ function whenOf(s, stage) {
239
+ const repeating = s["repeating"];
240
+ const isTemplate = repeating !== null &&
241
+ typeof repeating === "object" &&
242
+ repeating["isTemplate"] === true;
243
+ const nextOccurrence = isTemplate && typeof repeating["nextOccurrence"] === "string"
244
+ ? repeating["nextOccurrence"]
245
+ : null;
246
+ return deriveWhen({
247
+ stage,
248
+ // The SAME presence-keyed markers stageOf reads — never re-derived, so a
249
+ // `when` of today/evening can never disagree with Today-view membership.
250
+ today: s["today"] === true,
251
+ evening: s["evening"] === true,
252
+ startDate: s["startDate"] ?? null,
253
+ repeating: { isTemplate, nextOccurrence },
254
+ });
255
+ }
256
+ /**
257
+ * Shape ONE task entity (to-do or project): the universal reshapes, the R10/R12
258
+ * stage/`when` rewrite, then the R6 ancestry drops, then — when `compact` — the
259
+ * R7 default-pruning. A shallow copy is taken so unknown sibling keys
260
+ * (`changeKind` on a changes row, `match` on a search hit) pass through
261
+ * untouched. Non-task values (areas, tags, refs, headings) are returned as-is.
262
+ */
263
+ function shapeItem(src, drop, compact) {
264
+ if (src === null || typeof src !== "object")
265
+ return src;
266
+ const s = src;
267
+ const type = s["type"];
268
+ if (type !== "to-do" && type !== "project")
269
+ return src; // not a shaped entity
270
+ const stage = stageOf(s); // from the ORIGINAL fields, before any reshape
271
+ const when = whenOf(s, stage); // R12 — derived from the same fields + markers
272
+ // R13 (BANNER1 law L-B): a Today member is PROVISIONAL — the GUI pips it and
273
+ // counts it in the "You have N new to-dos" banner — until the app MATERIALIZES
274
+ // it (start:=1, startDate:=today). Presence-keyed marker, derived from the SAME
275
+ // inputs as the stage/`when` axes (never an independent re-derivation): the row
276
+ // is a Today member (via the today/evening markers `whenOf` reuses — so it can
277
+ // never disagree with the star) AND not yet materialized (BANNER1 L1:
278
+ // `start != active OR startDate IS NULL`). Templates are never Today members,
279
+ // so they never mark. NEVER dropped by any view/section — the banner is not a
280
+ // section, so no enclosing node implies it. The app rewrites start/startDate
281
+ // when the user acknowledges the banner; that is a GUI-only side effect our
282
+ // read cannot clear (watchers beware — see contract.md `provisional`).
283
+ const provisional = whenIsProvisional(when, s["start"], s["startDate"] ?? null);
284
+ const o = { ...s };
285
+ // R9 universal reshapes (every tier, every kind incl. detail).
286
+ reshapeChecklist(o);
287
+ reshapeTodos(o);
288
+ flattenTags(o);
289
+ reshapeRepeatingWire(o);
290
+ if (o["project"] == null && o["headingProject"] != null)
291
+ o["project"] = o["headingProject"];
292
+ delete o["headingProject"];
293
+ // R10 — the three lifecycle fields are replaced by the one derived `stage`.
294
+ delete o["start"];
295
+ delete o["logged"];
296
+ delete o["trashed"];
297
+ // R10.1 — `todaySection` is retired from the wire (it duplicated `evening`);
298
+ // the internal entity keeps it for the render / write-verify paths.
299
+ delete o["todaySection"];
300
+ if (drop.stage !== true)
301
+ o["stage"] = stage;
302
+ // R12 — the today/evening marker KEYS are replaced by the derived `when` on
303
+ // EVERY tier (they never appear on the wire); `when` is emitted unless the
304
+ // enclosing context provably states the position (the today view's sections;
305
+ // a card date-group — handled in rebucketChildren).
306
+ delete o["today"];
307
+ delete o["evening"];
308
+ // §9n — a reminder byte is presentation-dead once its `startDate` goes strictly
309
+ // past: the GUI hides the bell but never clears the byte. The materialize-time
310
+ // `reminderLive` marker (mappers, via `reminderIsLive` under the response clock)
311
+ // says whether the stored reminder still renders; drop the `reminder` key when
312
+ // it does not, mirroring the GUI. The marker itself never rides the wire.
313
+ if (o["reminderLive"] !== true)
314
+ delete o["reminder"];
315
+ delete o["reminderLive"];
316
+ if (drop.when !== true && when !== undefined)
317
+ o["when"] = when;
318
+ // R13 — the provisional banner marker (never dropped; presence-keyed).
319
+ if (provisional)
320
+ o["provisional"] = true;
321
+ // R6 — drop redundant ancestry (both tiers).
322
+ if (drop.project === true)
323
+ delete o["project"];
324
+ if (drop.area === true)
325
+ delete o["area"];
326
+ if (drop.heading === true)
327
+ delete o["heading"];
328
+ // R12 — FULL/DETAIL keep the raw `startDate` beside `when` as the SUBSTRATE
329
+ // (`startDate` = what is stored, `when` = where it sits). COMPACT drops it below
330
+ // (the position `when` carries is what a list needs).
331
+ if (!compact)
332
+ return o;
333
+ // R7 compact — default-pruning (absence = the default).
334
+ delete o["startDate"]; // R12 — position lives in `when`; substrate is full-tier only
335
+ if (o["status"] === "open")
336
+ delete o["status"];
337
+ delete o["created"];
338
+ delete o["modified"];
339
+ const notes = typeof o["notes"] === "string" ? o["notes"] : "";
340
+ delete o["notes"];
341
+ if (notes !== "")
342
+ o["hasNotes"] = true;
343
+ // The heading ref is compact-dropped everywhere (the GUI shows the project,
344
+ // never the heading, outside a project view). Full tier / detail keep it.
345
+ delete o["heading"];
346
+ return o;
347
+ }
348
+ /** Map a plain array of items with the item shaper. */
349
+ function shapeList(items, drop, compact) {
350
+ if (!Array.isArray(items))
351
+ return items;
352
+ return items.map((i) => shapeItem(i, drop, compact));
353
+ }
354
+ /** Copy `base` and overwrite `items` with the shaped list (avoids spread-in-map). */
355
+ function withShapedItems(base, drop, compact) {
356
+ const out = { ...base };
357
+ out["items"] = shapeList(base["items"], drop, compact);
358
+ return out;
359
+ }
360
+ /**
361
+ * Re-bucket a project's / area's / heading's live (non-logbook/trash) children
362
+ * into the R10 card shape by their derived {@link deriveStage} — so the bucket an
363
+ * item lands in ALWAYS equals its `stage`:
364
+ * - `anytime` — stage anytime, in encounter order;
365
+ * - `upcoming` — stage upcoming, date-grouped `[{date, items}]` (a dated row under
366
+ * its `startDate`, a template under its `nextOccurrence`), date ASC; date-LESS
367
+ * templates (after-completion / paused) form a trailing `{date: null, items}`
368
+ * group (explicit null per the `area: null` section precedent);
369
+ * - `someday` — stage someday.
370
+ * Items are already in view order (index / date+todayIndex) from the read layer,
371
+ * so encounter order within a date group preserves that ordering. Each item is
372
+ * then run through {@link shapeItem} with the section drop (ancestry + `stage`,
373
+ * since the bucket states it).
374
+ */
375
+ function rebucketChildren(children, drop, compact) {
376
+ const anytime = [];
377
+ const someday = [];
378
+ const datedByKey = new Map();
379
+ const datedOrder = [];
380
+ const restingTemplates = [];
381
+ const shape = (c) => shapeItem(c, drop, compact);
382
+ for (const raw of children) {
383
+ if (raw === null || typeof raw !== "object")
384
+ continue;
385
+ const c = raw;
386
+ const stage = stageOf(c);
387
+ if (stage === "anytime") {
388
+ anytime.push(shape(c));
389
+ }
390
+ else if (stage === "someday") {
391
+ someday.push(shape(c));
392
+ }
393
+ else if (stage === "upcoming") {
394
+ const repeating = c["repeating"];
395
+ const nextOcc = repeating != null && typeof repeating === "object"
396
+ ? (repeating["nextOccurrence"] ?? null)
397
+ : null;
398
+ const date = (c.startDate ?? null) !== null ? c.startDate : nextOcc;
399
+ if (date === null) {
400
+ restingTemplates.push(shape(c));
401
+ }
402
+ else {
403
+ if (!datedByKey.has(date)) {
404
+ datedByKey.set(date, []);
405
+ datedOrder.push(date);
406
+ }
407
+ // R12 — inside a date-group the group states the date, so a member whose
408
+ // `when` equals it drops it (every scheduled row and every projected
409
+ // template does — that IS the group key).
410
+ const shaped = shape(c);
411
+ if (shaped !== null && typeof shaped === "object" && shaped["when"] === date) {
412
+ delete shaped["when"];
413
+ }
414
+ datedByKey.get(date).push(shaped);
415
+ }
416
+ }
417
+ else {
418
+ // inbox / logbook / trash should not appear among a card's live children;
419
+ // route defensively to anytime rather than drop the row.
420
+ anytime.push(shape(c));
421
+ }
422
+ }
423
+ const upcoming = datedOrder
424
+ .toSorted((a, b) => a.localeCompare(b))
425
+ .map((date) => ({ date, items: datedByKey.get(date) }));
426
+ if (restingTemplates.length > 0)
427
+ upcoming.push({ date: null, items: restingTemplates });
428
+ return { anytime, upcoming, someday };
429
+ }
430
+ /** Flatten an internal IsoDateGroup[] (`[{date, items}]`) to its items, in order. */
431
+ function flattenGroups(groups) {
432
+ if (!Array.isArray(groups))
433
+ return [];
434
+ const out = [];
435
+ for (const g of groups) {
436
+ if (g !== null && typeof g === "object" && Array.isArray(g["items"])) {
437
+ out.push(...g["items"]);
438
+ }
439
+ }
440
+ return out;
441
+ }
442
+ /** Coerce an unknown value to an array (empty when absent). */
443
+ const asArray = (v) => (Array.isArray(v) ? v : []);
444
+ /** The R6 ref drop for every child bucket of a project view (unheaded members). */
445
+ const PROJECT_CHILD_DROP = { project: true, area: true, stage: true };
446
+ /** Heading-group members drop the heading ref too (the group states it). */
447
+ const HEADING_MEMBER_DROP = { project: true, area: true, heading: true, stage: true };
448
+ /** Area-view child-item buckets drop their area (the card states it) + the bucket-implied stage. */
449
+ const AREA_CHILD_DROP = { area: true, stage: true };
450
+ /** Area-view PROJECTS list: a mixed listing of the area's project rows — keep `stage`, drop area. */
451
+ const AREA_PROJECTS_DROP = { area: true };
452
+ /**
453
+ * Anytime sidebar-section items: stage-PURE → drop area + the section-implied
454
+ * stage (R10.2). Every Anytime-view member derives `anytime`: ANYTIME_SELF
455
+ * (undated-active, arrived-active, arrived someday-scheduled) — an ARRIVED dated
456
+ * row is Anytime, not Upcoming (Upcoming is STRICTLY FUTURE, UPC1) — AND, since
457
+ * R13, the DEADLINE-PULLED undated Inbox/Someday rows the view now includes
458
+ * (BANNER1b), which derive `anytime` too (deriveStage step 2½: a Today-marked
459
+ * undated row is a pull → anytime). Repeating templates are excluded from the
460
+ * view (NOT_TEMPLATE), so no stage-`upcoming` row can appear here — still pure.
461
+ */
462
+ const ANYTIME_SECTION_DROP = { area: true, stage: true };
463
+ /** Someday sidebar-section items: stage-PURE → drop area + the bucket-implied stage. */
464
+ const SOMEDAY_SECTION_DROP = { area: true, stage: true };
465
+ /** The card NODE / detail / mixed lists: keep every ref, `stage`, and `when`. */
466
+ const NO_DROP = {};
467
+ /**
468
+ * The today view's own sections: drop the section-implied `when` (R12) AND the
469
+ * section-implied `stage` (R13). Every Today member now derives stage `anytime`
470
+ * by construction — an ARRIVED `startDate` (step 5) or a DEADLINE PULL (step 2½)
471
+ * both derive `anytime`, and there are no future-dated or undated-someday Today
472
+ * members — so the Today sections are provably stage-PURE `anytime` and the field
473
+ * is redundant there (verified strict by the today-section purity property test
474
+ * in test/unit/stage.test.ts). `provisional` is NOT a drop — the banner is not a
475
+ * section, so nothing implies it.
476
+ */
477
+ const TODAY_SECTION_DROP = { when: true, stage: true };
478
+ /** Shape every collection bucket of a project view; the card node is left full + ancestry-intact. */
479
+ function shapeProjectView(view, compact) {
480
+ const cd = PROJECT_CHILD_DROP;
481
+ const hd = HEADING_MEMBER_DROP;
482
+ const shapeHeadingGroup = (g) => {
483
+ if (g === null || typeof g !== "object")
484
+ return g;
485
+ const grp = g;
486
+ const out = {};
487
+ // The heading NODE itself drops its `project` ref — the card states it.
488
+ if (grp["heading"] !== null && typeof grp["heading"] === "object") {
489
+ const h = { ...grp["heading"] };
490
+ delete h["project"];
491
+ out["heading"] = h;
492
+ }
493
+ else {
494
+ out["heading"] = grp["heading"];
495
+ }
496
+ const members = [
497
+ ...asArray(grp["items"]),
498
+ ...flattenGroups(grp["scheduled"]),
499
+ ...asArray(grp["someday"]),
500
+ ...asArray(grp["repeating"]),
501
+ ];
502
+ const { anytime, upcoming, someday } = rebucketChildren(members, hd, compact);
503
+ out["anytime"] = anytime;
504
+ out["upcoming"] = upcoming;
505
+ out["someday"] = someday;
506
+ return out;
507
+ };
508
+ const headings = Array.isArray(view["headings"])
509
+ ? view["headings"].map(shapeHeadingGroup)
510
+ : view["headings"];
511
+ const looseMembers = [
512
+ ...asArray(view["active"]),
513
+ ...flattenGroups(view["scheduled"]),
514
+ ...asArray(view["someday"]),
515
+ ...asArray(view["repeating"]),
516
+ ];
517
+ const { anytime, upcoming, someday } = rebucketChildren(looseMembers, cd, compact);
518
+ const out = { ...view };
519
+ delete out["active"];
520
+ delete out["scheduled"];
521
+ delete out["repeating"];
522
+ delete out["logged"];
523
+ // Trashed children live only in `things trash` — never a project-view bucket.
524
+ // Delete defensively in case an untyped source carries the old key.
525
+ delete out["trashed"];
526
+ // The project card NODE keeps everything (children derive their container from
527
+ // it), but is still an item DTO, so the universal + R10 reshapes apply.
528
+ out["project"] = shapeItem(view["project"], NO_DROP, false);
529
+ out["anytime"] = anytime;
530
+ out["upcoming"] = upcoming;
531
+ out["someday"] = someday;
532
+ out["headings"] = headings;
533
+ // A project keeps its in-context `logbook` (a project is a bounded object with
534
+ // a real done-state); trashed children live only in `things trash`.
535
+ out["logbook"] = shapeList(view["logged"], cd, compact);
536
+ return out;
537
+ }
538
+ /** Shape every collection bucket of an area view; the area node keeps its identity (tags folded). */
539
+ function shapeAreaView(view, compact) {
540
+ const looseMembers = [
541
+ ...asArray(view["active"]),
542
+ ...flattenGroups(view["scheduled"]),
543
+ ...asArray(view["someday"]),
544
+ ...asArray(view["repeating"]),
545
+ ];
546
+ const { anytime, upcoming, someday } = rebucketChildren(looseMembers, AREA_CHILD_DROP, compact);
547
+ const out = { ...view };
548
+ delete out["active"];
549
+ delete out["scheduled"];
550
+ delete out["repeating"];
551
+ // No `logbook` or `trash` bucket: an area's logbook is the bounded query
552
+ // `things logbook --area <ref>`, and trashed rows live only in `things trash`.
553
+ // Delete defensively in case an untyped source carries the old keys.
554
+ delete out["logged"];
555
+ delete out["trashed"];
556
+ out["area"] = shapeArea(view["area"]);
557
+ out["anytime"] = anytime;
558
+ // The projects list is a mixed listing of the area's project rows — keep stage.
559
+ out["projects"] = shapeList(view["projects"], AREA_PROJECTS_DROP, compact);
560
+ out["upcoming"] = upcoming;
561
+ out["someday"] = someday;
562
+ return out;
563
+ }
564
+ /** Fold an area entity's tags to string names in place (returns a shallow copy). */
565
+ function shapeArea(src) {
566
+ if (src === null || typeof src !== "object")
567
+ return src;
568
+ const o = { ...src };
569
+ flattenTags(o);
570
+ return o;
571
+ }
572
+ /** Shape the today/evening split (mixed list — keep refs + stage; drop the section-implied `when`). */
573
+ function shapeTodayView(view, compact) {
574
+ return {
575
+ ...view,
576
+ today: shapeList(view["today"], TODAY_SECTION_DROP, compact),
577
+ evening: shapeList(view["evening"], TODAY_SECTION_DROP, compact),
578
+ };
579
+ }
580
+ /** Shape sidebar sections (anytime/someday catalogues) with the section's drop spec. */
581
+ function shapeSections(sections, drop, compact) {
582
+ if (!Array.isArray(sections))
583
+ return sections;
584
+ return sections.map((s) => s === null || typeof s !== "object" ? s : withShapedItems(s, drop, compact));
585
+ }
586
+ /**
587
+ * The flat, mixed-provenance list kinds mapped to their drop spec. Only the
588
+ * stage-PURE catalogues (inbox/someday/logbook/trash; the section-based `anytime`
589
+ * is pure too, handled via shapeSections below) drop the bucket-implied `stage`.
590
+ * `upcoming` KEEPS it (R10.2): the Upcoming view is stage-mixed — it carries
591
+ * deadline-forecast stage-`anytime`/`someday` rows alongside future-dated
592
+ * stage-`upcoming` ones. The mixed/derived surfaces (search/changes/projects)
593
+ * keep it too.
594
+ */
595
+ const FLAT_LIST_DROP = new Map([
596
+ ["inbox", { stage: true }],
597
+ ["upcoming", NO_DROP],
598
+ ["logbook", { stage: true }],
599
+ ["trash", { stage: true }],
600
+ ["changes", NO_DROP],
601
+ ["search", NO_DROP],
602
+ ["projects", NO_DROP],
603
+ ]);
604
+ /**
605
+ * Apply the universal reshapes + R6 + R7 + R10 to a read payload for one view
606
+ * `kind`. `full` forces the FULL tier (R7 default-pruning off, everything else
607
+ * applied); an unrecognized kind passes through unchanged. The input is never
608
+ * mutated (shallow copies throughout), so the human-render path keeps the full
609
+ * entities.
610
+ */
611
+ export function shapeReadPayload(kind, data, full) {
612
+ // `detail` is the FULL record and drops no ancestry / stage / `when`.
613
+ if (kind === "detail")
614
+ return shapeItem(data, NO_DROP, false);
615
+ const compact = !full;
616
+ const flatDrop = FLAT_LIST_DROP.get(kind);
617
+ if (flatDrop !== undefined)
618
+ return shapeList(data, flatDrop, compact);
619
+ if (kind === "today" && data !== null && typeof data === "object") {
620
+ return shapeTodayView(data, compact);
621
+ }
622
+ if (kind === "anytime" && Array.isArray(data)) {
623
+ return shapeSections(data, ANYTIME_SECTION_DROP, compact); // stage-pure → drop stage
624
+ }
625
+ if (kind === "someday" && Array.isArray(data)) {
626
+ return shapeSections(data, SOMEDAY_SECTION_DROP, compact); // stage-pure → drop stage
627
+ }
628
+ if (kind === "area-view" && data !== null && typeof data === "object") {
629
+ return shapeAreaView(data, compact);
630
+ }
631
+ if (kind === "project-view" && data !== null && typeof data === "object") {
632
+ return shapeProjectView(data, compact);
633
+ }
634
+ // The `areas` listing carries Area entities whose tags fold to names.
635
+ if (kind === "areas" && Array.isArray(data))
636
+ return data.map(shapeArea);
637
+ // tags / legend / snapshot / diagnostics: not tag-carrying entity payloads.
638
+ return data;
639
+ }
640
+ /** The fixed cap on a listed candidate array; the error `message` states the total when it overflows. */
641
+ export const CANDIDATE_CAP = 8;
642
+ /** Read a container hint — a Ref `{title}` or a plain title string — to its non-empty title, else null. */
643
+ function candidateContainerTitle(v) {
644
+ if (typeof v === "string")
645
+ return v === "" ? null : v;
646
+ if (v !== null && typeof v === "object") {
647
+ const t = v["title"];
648
+ if (typeof t === "string" && t !== "")
649
+ return t;
650
+ }
651
+ return null;
652
+ }
653
+ /**
654
+ * Project ONE entity — a materialized to-do/project/area/heading, or a thin
655
+ * `{uuid, title}` resolver row — to the fixed {@link CandidateRef}. The SINGLE
656
+ * source every error-candidate emit flows through (the did-you-mean fallback and
657
+ * every not-found/ambiguous resolver), so the candidate shape can never vary by
658
+ * site. Reuses the wire's own {@link stageOf}/{@link whenOf} derivations — the
659
+ * lifecycle words are never re-derived here.
660
+ */
661
+ export function candidateRef(type, src) {
662
+ const s = (src ?? {});
663
+ const out = {
664
+ uuid: typeof s["uuid"] === "string" ? s["uuid"] : "",
665
+ title: typeof s["title"] === "string" ? s["title"] : "",
666
+ type,
667
+ };
668
+ const area = candidateContainerTitle(s["area"]);
669
+ if (area !== null)
670
+ out.area = area;
671
+ const project = candidateContainerTitle(s["project"]) ?? candidateContainerTitle(s["headingProject"]);
672
+ if (project !== null)
673
+ out.project = project;
674
+ // stage/when only for the task kinds, and only when the source carries the
675
+ // materialized lifecycle substrate (`start`) — a thin uuid+title resolver row
676
+ // does not, so the keys stay absent (presence-keyed, like the wire).
677
+ if ((type === "to-do" || type === "project") && typeof s["start"] === "string") {
678
+ const stage = stageOf(s);
679
+ out.stage = stage;
680
+ const when = whenOf(s, stage);
681
+ if (when !== undefined)
682
+ out.when = when;
683
+ }
684
+ return out;
685
+ }
686
+ //# sourceMappingURL=shape.js.map