things-api 0.14.0 → 0.16.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 (140) hide show
  1. package/README.md +4 -4
  2. package/dist/audit/schema.d.ts +8 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/commands/area.js +1 -1
  5. package/dist/cli/commands/install-skill.js +9 -2
  6. package/dist/cli/commands/install-skill.js.map +1 -1
  7. package/dist/cli/commands/project.js +1 -1
  8. package/dist/cli/commands/project.js.map +1 -1
  9. package/dist/cli/commands/reads.js +73 -4
  10. package/dist/cli/commands/reads.js.map +1 -1
  11. package/dist/cli/commands/repeat-flags.d.ts +11 -1
  12. package/dist/cli/commands/repeat-flags.js +23 -0
  13. package/dist/cli/commands/repeat-flags.js.map +1 -1
  14. package/dist/cli/commands/todo.js +50 -3
  15. package/dist/cli/commands/todo.js.map +1 -1
  16. package/dist/cli/commands/writes.js +202 -61
  17. package/dist/cli/commands/writes.js.map +1 -1
  18. package/dist/cli/glyphs.js +9 -10
  19. package/dist/cli/glyphs.js.map +1 -1
  20. package/dist/cli/help.js +18 -13
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/read-driver.d.ts +9 -3
  23. package/dist/cli/read-driver.js +3 -1
  24. package/dist/cli/read-driver.js.map +1 -1
  25. package/dist/cli/render.d.ts +33 -2
  26. package/dist/cli/render.js +60 -9
  27. package/dist/cli/render.js.map +1 -1
  28. package/dist/cli/skill.d.ts +24 -0
  29. package/dist/cli/skill.js +40 -0
  30. package/dist/cli/skill.js.map +1 -1
  31. package/dist/client.d.ts +88 -15
  32. package/dist/client.js +31 -5
  33. package/dist/client.js.map +1 -1
  34. package/dist/contracts.d.ts +13 -2
  35. package/dist/contracts.js +1 -1
  36. package/dist/contracts.js.map +1 -1
  37. package/dist/index.d.ts +5 -4
  38. package/dist/index.js +1 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/mcp/server.js +187 -36
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/model/entities.d.ts +97 -40
  43. package/dist/model/entities.js +0 -4
  44. package/dist/model/entities.js.map +1 -1
  45. package/dist/model/mappers.js +24 -43
  46. package/dist/model/mappers.js.map +1 -1
  47. package/dist/read/area-view.js +6 -6
  48. package/dist/read/area-view.js.map +1 -1
  49. package/dist/read/detail.js +51 -3
  50. package/dist/read/detail.js.map +1 -1
  51. package/dist/read/filter-contract.d.ts +1 -1
  52. package/dist/read/filter-contract.js +3 -0
  53. package/dist/read/filter-contract.js.map +1 -1
  54. package/dist/read/log-boundary.d.ts +46 -2
  55. package/dist/read/log-boundary.js +43 -2
  56. package/dist/read/log-boundary.js.map +1 -1
  57. package/dist/read/predicates.d.ts +13 -0
  58. package/dist/read/predicates.js +13 -0
  59. package/dist/read/predicates.js.map +1 -1
  60. package/dist/read/project-view.js +5 -5
  61. package/dist/read/project-view.js.map +1 -1
  62. package/dist/read/queries.d.ts +12 -0
  63. package/dist/read/queries.js +14 -0
  64. package/dist/read/queries.js.map +1 -1
  65. package/dist/read/search-rank.js +2 -2
  66. package/dist/read/search-rank.js.map +1 -1
  67. package/dist/read/shape.d.ts +30 -26
  68. package/dist/read/shape.js +130 -101
  69. package/dist/read/shape.js.map +1 -1
  70. package/dist/read/snapshot.js +2 -2
  71. package/dist/read/snapshot.js.map +1 -1
  72. package/dist/read/stage.d.ts +36 -18
  73. package/dist/read/stage.js +30 -13
  74. package/dist/read/stage.js.map +1 -1
  75. package/dist/read/views.d.ts +62 -0
  76. package/dist/read/views.js +134 -14
  77. package/dist/read/views.js.map +1 -1
  78. package/dist/surface-copy.d.ts +2 -2
  79. package/dist/surface-copy.js +2 -2
  80. package/dist/write/batch.d.ts +28 -3
  81. package/dist/write/batch.js +162 -46
  82. package/dist/write/batch.js.map +1 -1
  83. package/dist/write/clear-reminder.js +3 -1
  84. package/dist/write/clear-reminder.js.map +1 -1
  85. package/dist/write/clone.d.ts +4 -0
  86. package/dist/write/clone.js +531 -0
  87. package/dist/write/clone.js.map +1 -0
  88. package/dist/write/commands.js +196 -29
  89. package/dist/write/commands.js.map +1 -1
  90. package/dist/write/guards.d.ts +1 -1
  91. package/dist/write/guards.js +44 -12
  92. package/dist/write/guards.js.map +1 -1
  93. package/dist/write/make-repeating-project.d.ts +2 -3
  94. package/dist/write/make-repeating-project.js +2 -73
  95. package/dist/write/make-repeating-project.js.map +1 -1
  96. package/dist/write/move.js +53 -1
  97. package/dist/write/move.js.map +1 -1
  98. package/dist/write/operations.d.ts +134 -11
  99. package/dist/write/operations.js +5 -1
  100. package/dist/write/operations.js.map +1 -1
  101. package/dist/write/pipeline.d.ts +38 -0
  102. package/dist/write/pipeline.js +71 -6
  103. package/dist/write/pipeline.js.map +1 -1
  104. package/dist/write/pre-state.d.ts +41 -0
  105. package/dist/write/pre-state.js +71 -8
  106. package/dist/write/pre-state.js.map +1 -1
  107. package/dist/write/preserve-modified.d.ts +55 -0
  108. package/dist/write/preserve-modified.js +106 -0
  109. package/dist/write/preserve-modified.js.map +1 -0
  110. package/dist/write/promote-clone.d.ts +32 -0
  111. package/dist/write/promote-clone.js +620 -0
  112. package/dist/write/promote-clone.js.map +1 -0
  113. package/dist/write/reorder.js +49 -2
  114. package/dist/write/reorder.js.map +1 -1
  115. package/dist/write/resolution-timestamps.js +29 -7
  116. package/dist/write/resolution-timestamps.js.map +1 -1
  117. package/dist/write/reversibility.js +32 -14
  118. package/dist/write/reversibility.js.map +1 -1
  119. package/dist/write/scope-guard.js +2 -0
  120. package/dist/write/scope-guard.js.map +1 -1
  121. package/dist/write/undo.d.ts +12 -2
  122. package/dist/write/undo.js +145 -22
  123. package/dist/write/undo.js.map +1 -1
  124. package/dist/write/vectors/applescript.js +9 -0
  125. package/dist/write/vectors/applescript.js.map +1 -1
  126. package/dist/write/vectors/simulator.js +242 -10
  127. package/dist/write/vectors/simulator.js.map +1 -1
  128. package/dist/write/vectors/ui-certification.d.ts +1 -1
  129. package/dist/write/vectors/ui-certification.js +10 -3
  130. package/dist/write/vectors/ui-certification.js.map +1 -1
  131. package/dist/write/verify/delta.d.ts +55 -10
  132. package/dist/write/verify/delta.js +73 -10
  133. package/dist/write/verify/delta.js.map +1 -1
  134. package/package.json +1 -1
  135. package/schema/envelope.schema.json +25 -0
  136. package/skills/things-cli/SKILL.md +10 -7
  137. package/skills/things-cli/references/banner.md +1 -1
  138. package/skills/things-cli/references/contracts.md +17 -9
  139. package/skills/things-cli/references/{model.md → data-model.md} +5 -2
  140. package/skills/things-cli/references/ordering.md +2 -0
@@ -1,16 +1,16 @@
1
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
2
+ * The read-payload SHAPING transform: the token-economy rules RS1 and RS2, the
3
+ * universal item-DTO reshapes (RS4), and the RS5 lifecycle taxonomy — applied at
4
4
  * the JSON emit boundary of the read surfaces (the CLI `--json` read envelope,
5
5
  * src/cli/read-driver.ts, and the MCP read tool results, src/mcp/server.ts), the
6
6
  * same boundaries omit-empty runs at. Shaping runs BEFORE omit-empty. The
7
7
  * human-render path keeps the full, unshaped entities, so this is JSON-only.
8
8
  *
9
- * Both R6/R7 are deterministic BY VIEW KIND / SECTION — the emitter knows whether
9
+ * Both RS1/RS2 are deterministic BY VIEW KIND / SECTION — the emitter knows whether
10
10
  * it is inside a single-container view, a lifecycle bucket, or a mixed list —
11
11
  * never a per-item heuristic.
12
12
  *
13
- * ## R10 — the `stage` lifecycle taxonomy (every tier, every kind incl. detail)
13
+ * ## RS5 — the `stage` lifecycle taxonomy (every tier, every kind incl. detail)
14
14
  * The three former wire fields `start` / `logged` / `trashed` are DELETED from
15
15
  * every item and replaced by ONE derived `stage` ∈ `inbox | upcoming | anytime |
16
16
  * someday | logbook | trash` (src/read/stage.ts `deriveStage`, the single pure
@@ -18,11 +18,11 @@
18
18
  * bucket a view puts an item in). Today/evening membership is a SEPARATE
19
19
  * presence-keyed axis — `today: true` / `evening: true` (evening implies today) —
20
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
21
+ * - `stage` is DROPPED only where the enclosing node PROVABLY states it — the RS1
22
22
  * rule (drop only what the node provably says). That is the stage-PURE flat
23
23
  * views (inbox, `anytime`, someday, logbook, trash) and the stage-named card
24
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
25
+ * BY stage). The `anytime` catalogue is stage-PURE (RS5.2): every member is an
26
26
  * Anytime-view row (ANYTIME_SELF) — undated-active, arrived-active, or arrived
27
27
  * someday-scheduled — and an ARRIVED dated row derives `anytime` (Upcoming is
28
28
  * STRICTLY FUTURE, UPC1), so the field is redundant there.
@@ -32,12 +32,13 @@
32
32
  * non-redundant information — plus `today` (mixes upcoming + anytime-deadline
33
33
  * rows), search, changes, the projects/areas listings, the card NODE, and
34
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.
35
+ * - the former `todaySection` field is DELETED from the entity model entirely:
36
+ * it merely duplicated the presence-keyed `today`/`evening` markers
37
+ * (`todaySection: "evening"` `evening: true`; `"today"` `today && !evening`),
38
+ * which every consumer now reads directly (the human render, the write-verify
39
+ * delta, the today-view evening split).
39
40
  *
40
- * ## R12 — `when`, the derived TIME-AXIS position (replaces startDate + markers)
41
+ * ## RS7 — `when`, the derived TIME-AXIS position (replaces startDate + markers)
41
42
  * Today/evening membership and the scheduled/projected date collapse onto ONE
42
43
  * derived, presence-keyed field `when` (src/read/stage.ts `deriveWhen`): `"today"`
43
44
  * / `"evening"` (Today-view membership, from the SAME `today`/`evening` markers the
@@ -62,11 +63,11 @@
62
63
  * it). KEPT everywhere
63
64
  * else it is present — including the flat `upcoming`/`anytime`/`inbox`/`someday`
64
65
  * catalogues, search, changes (a deadline-pulled row reads `when: "today"` in the
65
- * mixed search/changes surfaces, informatively; note R13 re-files it to stage
66
+ * mixed search/changes surfaces, informatively; note RS8 re-files it to stage
66
67
  * `anytime` and the flat inbox/someday views now EXCLUDE it — it appears in the
67
68
  * `anytime` catalogue instead, `when: "today"` kept, stage dropped as pure).
68
69
  *
69
- * ## R13 — provisional Today members + GUI-faithful pulled-row membership
70
+ * ## RS8 — provisional Today members + GUI-faithful pulled-row membership
70
71
  * BANNER1 / BANNER1b (docs/lab/banner1-research.md). Two coupled facts:
71
72
  * - **`provisional: true`** — a presence-keyed marker on every Today member the GUI
72
73
  * pips / counts in the "You have N new to-dos" banner: a Today member NOT yet
@@ -83,35 +84,38 @@
83
84
  * key-implied `when`. The flat someday/inbox views EXCLUDE pulled rows and the anytime view
84
85
  * INCLUDES them (src/read/views.ts + predicates.ts DEADLINE_PULLED) — GUI fidelity.
85
86
  *
86
- * ## Universal item-DTO reshapes (R9 — EVERY tier, EVERY read kind incl. detail)
87
+ * ## Universal item-DTO reshapes (RS4 — EVERY tier, EVERY read kind incl. detail)
87
88
  * - **checklist nesting** — flat counts → presence-keyed `checklist: {open,total}`.
88
89
  * - **todos counts** — a project's flat leaf-action counts → presence-keyed
89
90
  * `todos: {open, total}` (omit when total 0).
90
- * - **repeating template/instance split (R11)** — the wire drops the
91
+ * - **repeating template/instance split (RS6)** — the wire drops the
91
92
  * `isTemplate`/`isInstance` discriminators; key presence carries the fact. A
92
93
  * TEMPLATE keeps a nested `repeating: {paused?, deadlined?, rule?,
93
94
  * latestInstance?}` — the series object (rule config + backward pointer +
94
95
  * state flags); presence MEANS template. The forward pointer `nextOccurrence`
95
- * moved to the top-level `when` (R12 — a template's projected date IS its time
96
+ * moved to the top-level `when` (RS7 — a template's projected date IS its time
96
97
  * position); `latestInstance` is detail-only (SL1). An INSTANCE keeps a flat
97
- * `instanceOf: <templateUuid>` and no `repeating`. A plain row keeps neither.
98
- * See {@link reshapeRepeatingWire}.
98
+ * `instanceOf: <templateUuid>` and no `repeating`; on a DETAIL read it also
99
+ * gains a sibling `repeats: {rule?, next?, paused?}` — its template's repeat
100
+ * context (the GUI's lower-corner caption), `rule` byte-consistent with the
101
+ * template card's `repeating.rule` and `next` the fixed-mode next occurrence. A
102
+ * plain row keeps neither. See {@link reshapeRepeatingWire}.
99
103
  * - **string tags** — `tags`/`inheritedTags` become plain arrays of names.
100
104
  * - **one project key** — a headed item's owning project (formerly
101
105
  * `headingProject`) is merged into `project`; `headingProject` never appears.
102
106
  *
103
- * ## R6 — no-redundant-ancestry (both tiers)
107
+ * ## RS1 — no-redundant-ancestry (both tiers)
104
108
  * project-view children drop `project`+`area` (heading-group members also drop
105
109
  * `heading`); area-view children/project-cards drop `area`; anytime/someday
106
110
  * section items drop `area`. Mixed lists keep every ref. (In the COMPACT tier the
107
- * `heading` ref is additionally dropped everywhere — R7.)
111
+ * `heading` ref is additionally dropped everywhere — RS2.)
108
112
  *
109
- * ## R7 — named detail tiers (compact | full)
113
+ * ## RS2 — named detail tiers (compact | full)
110
114
  * List contexts default to COMPACT; `detail`/`show` and `--full` / `full:true`
111
115
  * use FULL. Compact drops `created`/`modified`, the full `notes` string (a
112
116
  * presence-keyed `hasNotes: true` marks a row with notes), and the `heading` ref;
113
- * `status` is omitted when `open`. FULL keeps them but still applies R6, the
114
- * universal reshapes, and R10.
117
+ * `status` is omitted when `open`. FULL keeps them but still applies RS1, the
118
+ * universal reshapes, and RS5.
115
119
  */
116
120
  import { deriveStage, deriveWhen, whenIsProvisional } from "./stage.js";
117
121
  /** The DB-less default: assume every title round-trips (bare title, no uuid sibling). */
@@ -212,7 +216,7 @@ function reshapeTodos(o) {
212
216
  o["todos"] = { open, total };
213
217
  }
214
218
  /**
215
- * R11 — rewrite the internal `repeating` block into the wire's template/instance
219
+ * RS6 — rewrite the internal `repeating` block into the wire's template/instance
216
220
  * split, mutating `o` in place. The internal entity carries the full
217
221
  * RepeatingInfo (`isTemplate`/`isInstance`/`templateUuid`/…); the wire loses the
218
222
  * `isTemplate`/`isInstance` discriminators entirely and instead lets KEY
@@ -223,13 +227,21 @@ function reshapeTodos(o) {
223
227
  * template (an unadorned template emits `repeating: {}` — a bare `{}` is NOT
224
228
  * pruned by omit-empty, so the presence signal survives). The inner false
225
229
  * booleans are default-pruned (presence-keyed). The forward pointer
226
- * `nextOccurrence` moved OUT to the top-level `when` (R12 — a template's
230
+ * `nextOccurrence` moved OUT to the top-level `when` (RS7 — a template's
227
231
  * projected date IS its time position); `rule` and `latestInstance` stay
228
232
  * detail-only (populated by src/read/detail.ts on `entity.repeating`);
229
233
  * `latestInstance` is the backward pointer symmetric to `when`.
230
234
  * - **Instance** (`isInstance`) → a flat presence-keyed `instanceOf:
231
- * <templateUuid>` and NO `repeating` object. Presence of `instanceOf` MEANS
232
- * instance.
235
+ * <templateUuid>` (the instance marker + the write handle) and, on a DETAIL
236
+ * read, a sibling `repeats` object — the template's repeat CONTEXT joined onto
237
+ * the instance (`{rule?, next?, paused?}`), the GUI's lower-corner "Repeats on
238
+ * Aug 19" / "Repeats 1 day after completion" caption. `rule` is the SAME decoded
239
+ * shape a template card emits under `repeating.rule` (one recurrence vocabulary
240
+ * on the wire); `next` is the template's projected next occurrence, FIXED mode
241
+ * ONLY (absent for after-completion — no successor date exists yet); `paused`
242
+ * surfaces the template's paused flag. Populated by src/read/detail.ts's mirror
243
+ * join; absent when the template is unresolvable (dangling FK) or carries no
244
+ * caption. NO `repeating` object on an instance.
233
245
  * - **Plain** (neither) → neither key.
234
246
  */
235
247
  function reshapeRepeatingWire(o) {
@@ -247,7 +259,7 @@ function reshapeRepeatingWire(o) {
247
259
  if (r["rule"] != null)
248
260
  out["rule"] = r["rule"]; // detail read only
249
261
  // The SL1 "Show Latest" pick — detail-only; the backward pointer symmetric to
250
- // the top-level `when` (the forward pointer, R12).
262
+ // the top-level `when` (the forward pointer, RS7).
251
263
  if (typeof r["latestInstance"] === "string")
252
264
  out["latestInstance"] = r["latestInstance"];
253
265
  o["repeating"] = out;
@@ -255,27 +267,43 @@ function reshapeRepeatingWire(o) {
255
267
  else if (r["isInstance"] === true) {
256
268
  if (typeof r["templateUuid"] === "string")
257
269
  o["instanceOf"] = r["templateUuid"];
270
+ // The instance's TEMPLATE context (detail-only mirror join). Presence-keyed:
271
+ // the whole `repeats` object is omitted unless it carries at least one fact.
272
+ const ctx = r["repeats"];
273
+ if (ctx !== null && typeof ctx === "object") {
274
+ const c = ctx;
275
+ const out = {};
276
+ if (c["rule"] != null)
277
+ out["rule"] = c["rule"]; // same shape as a template card's `repeating.rule`
278
+ if (typeof c["next"] === "string")
279
+ out["next"] = c["next"]; // FIXED mode only
280
+ if (c["paused"] === true)
281
+ out["paused"] = true;
282
+ if (Object.keys(out).length > 0)
283
+ o["repeats"] = out;
284
+ }
258
285
  }
259
286
  }
260
- /** The R10 stage input read straight off a materialized task entity. */
287
+ /** The RS5 stage input read straight off a materialized task entity's `derived` bag. */
261
288
  function stageOf(s) {
262
289
  const repeating = s["repeating"];
263
290
  const isTemplate = repeating !== null &&
264
291
  typeof repeating === "object" &&
265
292
  repeating["isTemplate"] === true;
293
+ const d = (s["derived"] ?? {});
266
294
  return deriveStage({
267
- trashed: s["trashed"] === true,
268
- logged: s["logged"] === true,
269
- start: s["start"],
295
+ trashed: d["trashed"] === true,
296
+ logged: d["logged"] === true,
297
+ start: d["start"],
270
298
  startDate: s["startDate"] ?? null,
271
299
  repeating: { isTemplate },
272
300
  // The presence-keyed Today marker (stamped at materialize with the response
273
301
  // clock) discriminates an ARRIVED dated row (→ anytime) from a strictly-
274
- // future one (→ upcoming). Read BEFORE the marker is stripped downstream.
275
- today: s["today"] === true,
302
+ // future one (→ upcoming). Read from the `derived` substrate bag.
303
+ today: d["today"] === true,
276
304
  });
277
305
  }
278
- /** The R12 `when` input read straight off a materialized task entity (given its stage). */
306
+ /** The RS7 `when` input read straight off a materialized task entity (given its stage). */
279
307
  function whenOf(s, stage) {
280
308
  const repeating = s["repeating"];
281
309
  const isTemplate = repeating !== null &&
@@ -284,20 +312,21 @@ function whenOf(s, stage) {
284
312
  const nextOccurrence = isTemplate && typeof repeating["nextOccurrence"] === "string"
285
313
  ? repeating["nextOccurrence"]
286
314
  : null;
315
+ const d = (s["derived"] ?? {});
287
316
  return deriveWhen({
288
317
  stage,
289
318
  // The SAME presence-keyed markers stageOf reads — never re-derived, so a
290
319
  // `when` of today/evening can never disagree with Today-view membership.
291
- today: s["today"] === true,
292
- evening: s["evening"] === true,
320
+ today: d["today"] === true,
321
+ evening: d["evening"] === true,
293
322
  startDate: s["startDate"] ?? null,
294
323
  repeating: { isTemplate, nextOccurrence },
295
324
  });
296
325
  }
297
326
  /**
298
- * Shape ONE task entity (to-do or project): the universal reshapes, the R10/R12
299
- * stage/`when` rewrite, then the R6 ancestry drops, then — when `compact` — the
300
- * R7 default-pruning. A shallow copy is taken so unknown sibling keys
327
+ * Shape ONE task entity (to-do or project): the universal reshapes, the RS5/RS7
328
+ * stage/`when` rewrite, then the RS1 ancestry drops, then — when `compact` — the
329
+ * RS2 default-pruning. A shallow copy is taken so unknown sibling keys
301
330
  * (`changeKind` on a changes row, `match` on a search hit) pass through
302
331
  * untouched. Non-task values (areas, tags, refs, headings) are returned as-is.
303
332
  */
@@ -309,8 +338,8 @@ function shapeItem(src, drop, compact, promoter) {
309
338
  if (type !== "to-do" && type !== "project")
310
339
  return src; // not a shaped entity
311
340
  const stage = stageOf(s); // from the ORIGINAL fields, before any reshape
312
- const when = whenOf(s, stage); // R12 — derived from the same fields + markers
313
- // R13 (BANNER1 law L-B): a Today member is PROVISIONAL — the GUI pips it and
341
+ const when = whenOf(s, stage); // RS7 — derived from the same fields + markers
342
+ // RS8 (BANNER1 law L-B): a Today member is PROVISIONAL — the GUI pips it and
314
343
  // counts it in the "You have N new to-dos" banner — until the app MATERIALIZES
315
344
  // it (start:=1, startDate:=today). Presence-keyed marker, derived from the SAME
316
345
  // inputs as the stage/`when` axes (never an independent re-derivation): the row
@@ -321,9 +350,9 @@ function shapeItem(src, drop, compact, promoter) {
321
350
  // section, so no enclosing node implies it. The app rewrites start/startDate
322
351
  // when the user acknowledges the banner; that is a GUI-only side effect our
323
352
  // read cannot clear (watchers beware — see contract.md `provisional`).
324
- const provisional = whenIsProvisional(when, s["start"], s["startDate"] ?? null);
353
+ const provisional = whenIsProvisional(when, (s["derived"] ?? {})["start"], s["startDate"] ?? null);
325
354
  const o = { ...s };
326
- // R9 universal reshapes (every tier, every kind incl. detail).
355
+ // RS4 universal reshapes (every tier, every kind incl. detail).
327
356
  reshapeChecklist(o);
328
357
  reshapeTodos(o);
329
358
  flattenTags(o);
@@ -331,41 +360,32 @@ function shapeItem(src, drop, compact, promoter) {
331
360
  if (o["project"] == null && o["headingProject"] != null)
332
361
  o["project"] = o["headingProject"];
333
362
  delete o["headingProject"];
334
- // R10 the three lifecycle fields are replaced by the one derived `stage`.
335
- delete o["start"];
336
- delete o["logged"];
337
- delete o["trashed"];
338
- // R10.1 `todaySection` is retired from the wire (it duplicated `evening`);
339
- // the internal entity keeps it for the render / write-verify paths.
340
- delete o["todaySection"];
363
+ // The ENTIRE internal derivation substrate leaves the wire in ONE structural
364
+ // drop (one-vocabulary Batch 2, Option B): `start`/`logged`/`trashed` (RS5 —
365
+ // replaced by `stage`), `today`/`evening` (RS7 — replaced by `when`), and the
366
+ // raw `reminder` byte all live in the nested `o.derived` bag. §9n: the
367
+ // top-level consumer `reminder` is ALREADY the live-gated value (null once the
368
+ // byte is presentation-dead its `startDate` gone strictly past), gated at the
369
+ // mapper; a null key is pruned by omit-empty, so a stale reminder never reaches
370
+ // the wire without any drop here. `stage`/`when`/`provisional` are then stamped
371
+ // from the derivations above.
372
+ delete o["derived"];
341
373
  if (drop.stage !== true)
342
374
  o["stage"] = stage;
343
- // R12the today/evening marker KEYS are replaced by the derived `when` on
344
- // EVERY tier (they never appear on the wire); `when` is emitted unless the
345
- // enclosing context provably states the position (the today view's sections;
346
- // a card date-group — handled in rebucketChildren).
347
- delete o["today"];
348
- delete o["evening"];
349
- // §9n — a reminder byte is presentation-dead once its `startDate` goes strictly
350
- // past: the GUI hides the bell but never clears the byte. The materialize-time
351
- // `reminderLive` marker (mappers, via `reminderIsLive` under the response clock)
352
- // says whether the stored reminder still renders; drop the `reminder` key when
353
- // it does not, mirroring the GUI. The marker itself never rides the wire.
354
- if (o["reminderLive"] !== true)
355
- delete o["reminder"];
356
- delete o["reminderLive"];
375
+ // RS7`when` is emitted unless the enclosing context provably states the
376
+ // position (the today view's sections; a card date-group in rebucketChildren).
357
377
  if (drop.when !== true && when !== undefined)
358
378
  o["when"] = when;
359
- // R13 — the provisional banner marker (never dropped; presence-keyed).
379
+ // RS8 — the provisional banner marker (never dropped; presence-keyed).
360
380
  if (provisional)
361
381
  o["provisional"] = true;
362
382
  // The owning project's uuid scopes the heading round-trip (headings resolve
363
- // within their project). Captured BEFORE the R6 project-drop so a project-view
383
+ // within their project). Captured BEFORE the RS1 project-drop so a project-view
364
384
  // LOGBOOK row — whose `project` is dropped as redundant, yet which KEEPS its
365
385
  // heading ref (drop.keepHeading) — can still promote its `headingUuid` in the
366
386
  // project's scope.
367
387
  const projectUuid = refUuid(o["project"]);
368
- // R6 — drop redundant ancestry (both tiers).
388
+ // RS1 — drop redundant ancestry (both tiers).
369
389
  if (drop.project === true)
370
390
  delete o["project"];
371
391
  if (drop.area === true)
@@ -386,7 +406,7 @@ function shapeItem(src, drop, compact, promoter) {
386
406
  // `isRepeatingTemplate` marker, so re-emit it here as a flat presence-keyed
387
407
  // sibling of the `project` ref (never `false`), riding wherever `project`
388
408
  // rides. A heading-nested row already merged its owning project into `project`
389
- // above, so direct AND headed template children mark; the R6 project-drop
409
+ // above, so direct AND headed template children mark; the RS1 project-drop
390
410
  // above already removed `project` where the view implies it, so a project-view
391
411
  // child carries no orphaned marker. Both tiers — it is a correctness signal,
392
412
  // not detail. Only project refs ever carry the flag (area/heading never do).
@@ -396,13 +416,13 @@ function shapeItem(src, drop, compact, promoter) {
396
416
  flattenRef(o, "area", "areaUuid", "area", forceUuid, promoter);
397
417
  if (!compact || drop.keepHeading === true)
398
418
  flattenRef(o, "heading", "headingUuid", "heading", forceUuid, promoter, projectUuid);
399
- // R12 — FULL/DETAIL keep the raw `startDate` beside `when` as the SUBSTRATE
419
+ // RS7 — FULL/DETAIL keep the raw `startDate` beside `when` as the SUBSTRATE
400
420
  // (`startDate` = what is stored, `when` = where it sits). COMPACT drops it below
401
421
  // (the position `when` carries is what a list needs).
402
422
  if (!compact)
403
423
  return o;
404
- // R7 compact — default-pruning (absence = the default).
405
- delete o["startDate"]; // R12 — position lives in `when`; substrate is full-tier only
424
+ // RS2 compact — default-pruning (absence = the default).
425
+ delete o["startDate"]; // RS7 — position lives in `when`; substrate is full-tier only
406
426
  if (o["status"] === "open")
407
427
  delete o["status"];
408
428
  delete o["created"];
@@ -432,12 +452,12 @@ function withShapedItems(base, drop, compact, promoter) {
432
452
  }
433
453
  /**
434
454
  * Re-bucket a project's / area's / heading's live (non-logbook/trash) children
435
- * into the R10 card shape by their derived {@link deriveStage} — so the bucket an
455
+ * into the RS5 card shape by their derived {@link deriveStage} — so the bucket an
436
456
  * item lands in ALWAYS equals its `stage`:
437
457
  * - `anytime` — stage anytime, in encounter order;
438
- * - `upcoming` — stage upcoming, date-grouped `[{date, items}]` (a dated row under
458
+ * - `upcoming` — stage upcoming, day-grouped `[{when, items}]` (a dated row under
439
459
  * its `startDate`, a template under its `nextOccurrence`), date ASC; date-LESS
440
- * templates (after-completion / paused) form a trailing `{date: null, items}`
460
+ * templates (after-completion / paused) form a trailing `{when: null, items}`
441
461
  * group (explicit null per the `area: null` section precedent);
442
462
  * - `someday` — stage someday.
443
463
  * Items are already in view order (index / date+todayIndex) from the read layer,
@@ -477,7 +497,7 @@ function rebucketChildren(children, drop, compact, promoter) {
477
497
  datedByKey.set(date, []);
478
498
  datedOrder.push(date);
479
499
  }
480
- // R12 — inside a date-group the group states the date, so a member whose
500
+ // RS7 — inside a date-group the group states the date, so a member whose
481
501
  // `when` equals it drops it (every scheduled row and every projected
482
502
  // template does — that IS the group key).
483
503
  const shaped = shape(c);
@@ -495,12 +515,12 @@ function rebucketChildren(children, drop, compact, promoter) {
495
515
  }
496
516
  const upcoming = datedOrder
497
517
  .toSorted((a, b) => a.localeCompare(b))
498
- .map((date) => ({ date, items: datedByKey.get(date) }));
518
+ .map((date) => ({ when: date, items: datedByKey.get(date) }));
499
519
  if (restingTemplates.length > 0)
500
- upcoming.push({ date: null, items: restingTemplates });
520
+ upcoming.push({ when: null, items: restingTemplates });
501
521
  return { anytime, upcoming, someday };
502
522
  }
503
- /** Flatten an internal IsoDateGroup[] (`[{date, items}]`) to its items, in order. */
523
+ /** Flatten an internal IsoDateGroup[] (`[{when, items}]`) to its items, in order. */
504
524
  function flattenGroups(groups) {
505
525
  if (!Array.isArray(groups))
506
526
  return [];
@@ -515,7 +535,7 @@ function flattenGroups(groups) {
515
535
  /** Coerce an unknown value to an array (empty when absent). */
516
536
  const asArray = (v) => (Array.isArray(v) ? v : []);
517
537
  /**
518
- * The R6 ref drop for the un-headed BODY's four `children` bucket records (v2):
538
+ * The RS1 ref drop for the un-headed BODY's four `children` bucket records (v2):
519
539
  * every body child drops project/area (the card states them), the bucket-implied
520
540
  * stage (each of `anytime`/`upcoming`/`someday`/`logbook` is stage-pure), and the
521
541
  * heading ref — a body child is by construction un-headed (its `heading` is null),
@@ -524,7 +544,7 @@ const asArray = (v) => (Array.isArray(v) ? v : []);
524
544
  */
525
545
  const PROJECT_CHILD_DROP = { project: true, area: true, heading: true, stage: true };
526
546
  /**
527
- * The R6 ref drop for a HEADING's four `children` bucket records (v2): a headed
547
+ * The RS1 ref drop for a HEADING's four `children` bucket records (v2): a headed
528
548
  * child drops project/area (the card states them), the heading ref (its position
529
549
  * UNDER `headings[].children` states membership — structural, #362 / task item 6),
530
550
  * and the bucket-implied stage. Applied uniformly to the heading's live buckets
@@ -537,10 +557,10 @@ const AREA_CHILD_DROP = { area: true, stage: true };
537
557
  const AREA_PROJECTS_DROP = { area: true };
538
558
  /**
539
559
  * Anytime sidebar-section items: stage-PURE → drop area + the section-implied
540
- * stage (R10.2). Every Anytime-view member derives `anytime`: ANYTIME_SELF
560
+ * stage (RS5.2). Every Anytime-view member derives `anytime`: ANYTIME_SELF
541
561
  * (undated-active, arrived-active, arrived someday-scheduled) — an ARRIVED dated
542
562
  * row is Anytime, not Upcoming (Upcoming is STRICTLY FUTURE, UPC1) — AND, since
543
- * R13, the DEADLINE-PULLED undated Inbox/Someday rows the view now includes
563
+ * RS8, the DEADLINE-PULLED undated Inbox/Someday rows the view now includes
544
564
  * (BANNER1b), which derive `anytime` too (deriveStage step 2½: a Today-marked
545
565
  * undated row is a pull → anytime). Repeating templates are excluded from the
546
566
  * view (NOT_TEMPLATE), so no stage-`upcoming` row can appear here — still pure.
@@ -552,8 +572,8 @@ const SOMEDAY_SECTION_DROP = { area: true, stage: true };
552
572
  const NO_DROP = {};
553
573
  /**
554
574
  * The today view's two `children` bucket records: drop the key-implied `when`
555
- * (R12 — the `today`/`evening` bucket key states it) AND the bucket-implied
556
- * `stage` (R13). Every Today member now derives stage `anytime` by construction —
575
+ * (RS7 — the `today`/`evening` bucket key states it) AND the bucket-implied
576
+ * `stage` (RS8). Every Today member now derives stage `anytime` by construction —
557
577
  * an ARRIVED `startDate` (step 5) or a DEADLINE PULL (step 2½) both derive
558
578
  * `anytime`, and there are no future-dated or undated-someday Today members — so
559
579
  * both buckets are provably stage-PURE `anytime` and the field is redundant there
@@ -613,7 +633,7 @@ function stoppedMs(o) {
613
633
  * heading (HEADARC2-C anomaly) is NOT logged, so it rides `anytime` here — its
614
634
  * presence in a live bucket under an `archived` heading node is self-evident.
615
635
  * `drop` carries the container's ancestry drops (body vs heading); the day-block
616
- * `when` renames the internal date group's `date` (rebucketChildren) — `null` for
636
+ * key is `when` end-to-end (`rebucketChildren` builds it no rename) — `null` for
617
637
  * the resting block. No bucket is capped in the project view today, so every
618
638
  * `total` is absent (R1: an untruncated bucket never restates its length); the
619
639
  * `total?` argument keeps the record + day-block shape ready for PR 5's sweep and
@@ -623,7 +643,11 @@ function shapeContainerChildren(children, drop, compact, promoter) {
623
643
  const live = [];
624
644
  const logged = [];
625
645
  for (const c of asArray(children)) {
626
- if (c !== null && typeof c === "object" && c["logged"] === true)
646
+ // The logbook-boundary flag lives on the internal `derived` substrate bag.
647
+ const isLogged = c !== null &&
648
+ typeof c === "object" &&
649
+ c["derived"]?.["logged"] === true;
650
+ if (isLogged)
627
651
  logged.push(c);
628
652
  else
629
653
  live.push(c);
@@ -633,8 +657,9 @@ function shapeContainerChildren(children, drop, compact, promoter) {
633
657
  const logbook = shapeList(loggedSorted, drop, compact, promoter);
634
658
  return {
635
659
  anytime: bucketRecord(anytime),
636
- // The day-block ARRAY: `date` `when` (R3); `null` is the resting block (#V8).
637
- upcoming: upcoming.map((g) => ({ when: g.date, items: g.items })),
660
+ // The day-block ARRAY (`{when, items}`, `when: null` the resting block #V8);
661
+ // `rebucketChildren` already keys each block by `when`.
662
+ upcoming,
638
663
  someday: bucketRecord(someday),
639
664
  logbook: bucketRecord(logbook),
640
665
  };
@@ -664,7 +689,7 @@ function shapeProjectView(view, compact, promoter) {
664
689
  });
665
690
  });
666
691
  return {
667
- // The card NODE keeps everything but is still an item DTO (universal + R10 reshapes).
692
+ // The card NODE keeps everything but is still an item DTO (universal + RS5 reshapes).
668
693
  project: shapeItem(view["project"], NO_DROP, false, promoter),
669
694
  children: shapeContainerChildren(view["bodyChildren"], PROJECT_CHILD_DROP, compact, promoter),
670
695
  headings: headingContainers,
@@ -676,7 +701,7 @@ function shapeProjectView(view, compact, promoter) {
676
701
  * key; the area logbook is the bounded query `things logbook --area <ref>`, #346).
677
702
  * The same stage-derived bucketing as {@link shapeContainerChildren} minus the
678
703
  * logbook split: `anytime`/`someday` are `{items, total?}` records, `upcoming` is
679
- * the day-block ARRAY (R3, `date` `when`) with the trailing `{when: null, items}`
704
+ * the day-block ARRAY (R3, keyed by `when`) with the trailing `{when: null, items}`
680
705
  * resting block for date-less recurring templates (#V8). Inline `total` is stamped
681
706
  * downstream by {@link withAreaBucketTotals} (only `anytime` can be capped — the
682
707
  * `--area-limit` scope; the scheduled/someday direct to-dos always survive).
@@ -685,8 +710,9 @@ function shapeAreaChildren(members, drop, compact, promoter) {
685
710
  const { anytime, upcoming, someday } = rebucketChildren(members, drop, compact, promoter);
686
711
  return {
687
712
  anytime: bucketRecord(anytime),
688
- // The day-block ARRAY: `date` `when` (R3); `null` is the resting block (#V8).
689
- upcoming: upcoming.map((g) => ({ when: g.date, items: g.items })),
713
+ // The day-block ARRAY (`{when, items}`, `when: null` the resting block #V8);
714
+ // `rebucketChildren` already keys each block by `when`.
715
+ upcoming,
690
716
  someday: bucketRecord(someday),
691
717
  };
692
718
  }
@@ -803,7 +829,7 @@ function shapeUpcomingView(items, compact, promoter) {
803
829
  datedByKey.set(key, []);
804
830
  datedOrder.push(key);
805
831
  }
806
- // R12 — inside a dated block the block states the date, so a member whose
832
+ // RS7 — inside a dated block the block states the date, so a member whose
807
833
  // `when` equals it drops it (a scheduled row's when IS the key). A forecast
808
834
  // row has no `when`; a horizon-projected row whose `when` diverges keeps it.
809
835
  if (shaped !== null && typeof shaped === "object" && shaped["when"] === key) {
@@ -938,9 +964,9 @@ function shapeSections(sections, drop, compact, promoter) {
938
964
  * The flat, mixed-provenance list kinds mapped to their drop spec. Only the
939
965
  * stage-PURE catalogues (inbox/someday/logbook/trash; the section-based `anytime`
940
966
  * is pure too, handled via shapeSections below) drop the bucket-implied `stage`.
941
- * The mixed/derived surfaces (search/changes/projects) keep it. The global
967
+ * The mixed/derived surfaces (search/changes/deadlines/projects) keep it. The global
942
968
  * `upcoming` view is NOT here — it reshapes into `data.sections` day blocks
943
- * ({@link shapeUpcomingView}), keeping `stage` (R10.2: stage-mixed — future-dated
969
+ * ({@link shapeUpcomingView}), keeping `stage` (RS5.2: stage-mixed — future-dated
944
970
  * `upcoming` rows beside deadline-forecast `anytime`/`someday` ones).
945
971
  */
946
972
  const FLAT_LIST_DROP = new Map([
@@ -949,11 +975,13 @@ const FLAT_LIST_DROP = new Map([
949
975
  ["trash", { stage: true }],
950
976
  ["changes", NO_DROP],
951
977
  ["search", NO_DROP],
978
+ // deadlines is stage-MIXED (to-dos + projects, deadline-ordered) — keep `stage`.
979
+ ["deadlines", NO_DROP],
952
980
  ["projects", NO_DROP],
953
981
  ]);
954
982
  /**
955
- * Apply the universal reshapes + R6 + R7 + R10 to a read payload for one view
956
- * `kind`. `full` forces the FULL tier (R7 default-pruning off, everything else
983
+ * Apply the universal reshapes + RS1 + RS2 + RS5 to a read payload for one view
984
+ * `kind`. `full` forces the FULL tier (RS2 default-pruning off, everything else
957
985
  * applied); an unrecognized kind passes through unchanged. The input is never
958
986
  * mutated (shallow copies throughout), so the human-render path keeps the full
959
987
  * entities.
@@ -1031,9 +1059,10 @@ export function candidateRef(type, src) {
1031
1059
  if (project !== null)
1032
1060
  out.project = project;
1033
1061
  // stage/when only for the task kinds, and only when the source carries the
1034
- // materialized lifecycle substrate (`start`) — a thin uuid+title resolver row
1035
- // does not, so the keys stay absent (presence-keyed, like the wire).
1036
- if ((type === "to-do" || type === "project") && typeof s["start"] === "string") {
1062
+ // materialized lifecycle substrate (`derived.start`) — a thin uuid+title
1063
+ // resolver row does not, so the keys stay absent (presence-keyed, like the wire).
1064
+ if ((type === "to-do" || type === "project") &&
1065
+ typeof (s["derived"] ?? {})["start"] === "string") {
1037
1066
  const stage = stageOf(s);
1038
1067
  out.stage = stage;
1039
1068
  const when = whenOf(s, stage);