things-api 0.18.0 → 0.19.1

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 (224) hide show
  1. package/README.md +18 -9
  2. package/deputy/AppIcon.icns +0 -0
  3. package/deputy/VERSION +1 -1
  4. package/deputy/helpers-Info.plist +4 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +3 -1
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
  10. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
  11. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  12. package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
  13. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
  14. package/deputy/reader/Info.plist +2 -0
  15. package/deputy/reader/entitlements.plist +30 -0
  16. package/deputy/reader/main.swift +129 -40
  17. package/deputy/src/server.swift +25 -2
  18. package/deputy/src/tcc.swift +58 -0
  19. package/dist/audit/schema.d.ts +30 -1
  20. package/dist/audit/schema.js.map +1 -1
  21. package/dist/capability.d.ts +322 -0
  22. package/dist/capability.js +706 -0
  23. package/dist/capability.js.map +1 -0
  24. package/dist/cli/commands/doctor.d.ts +13 -1
  25. package/dist/cli/commands/doctor.js +141 -5
  26. package/dist/cli/commands/doctor.js.map +1 -1
  27. package/dist/cli/commands/helpers.js +189 -51
  28. package/dist/cli/commands/helpers.js.map +1 -1
  29. package/dist/cli/commands/mcp.js +8 -0
  30. package/dist/cli/commands/mcp.js.map +1 -1
  31. package/dist/cli/commands/op-result.js +6 -0
  32. package/dist/cli/commands/op-result.js.map +1 -1
  33. package/dist/cli/commands/reads.js +47 -1
  34. package/dist/cli/commands/reads.js.map +1 -1
  35. package/dist/cli/commands/repeat-flags.js +34 -2
  36. package/dist/cli/commands/repeat-flags.js.map +1 -1
  37. package/dist/cli/commands/setup.d.ts +9 -0
  38. package/dist/cli/commands/setup.js +76 -83
  39. package/dist/cli/commands/setup.js.map +1 -1
  40. package/dist/cli/commands/writes.d.ts +1 -1
  41. package/dist/cli/commands/writes.js +311 -142
  42. package/dist/cli/commands/writes.js.map +1 -1
  43. package/dist/cli/dry-run.d.ts +1 -1
  44. package/dist/cli/dry-run.js +1 -1
  45. package/dist/cli/glyphs.d.ts +23 -4
  46. package/dist/cli/glyphs.js +46 -19
  47. package/dist/cli/glyphs.js.map +1 -1
  48. package/dist/cli/help.d.ts +1 -1
  49. package/dist/cli/help.js +49 -27
  50. package/dist/cli/help.js.map +1 -1
  51. package/dist/cli/interrupt.d.ts +54 -6
  52. package/dist/cli/interrupt.js +78 -12
  53. package/dist/cli/interrupt.js.map +1 -1
  54. package/dist/cli/main.js +8 -5
  55. package/dist/cli/main.js.map +1 -1
  56. package/dist/cli/period.d.ts +3 -1
  57. package/dist/cli/period.js +1 -1
  58. package/dist/cli/period.js.map +1 -1
  59. package/dist/cli/read-driver.js +26 -1
  60. package/dist/cli/read-driver.js.map +1 -1
  61. package/dist/cli/render.d.ts +31 -4
  62. package/dist/cli/render.js +127 -15
  63. package/dist/cli/render.js.map +1 -1
  64. package/dist/client.d.ts +23 -3
  65. package/dist/client.js +36 -13
  66. package/dist/client.js.map +1 -1
  67. package/dist/contracts.d.ts +8 -4
  68. package/dist/contracts.js +1 -1
  69. package/dist/contracts.js.map +1 -1
  70. package/dist/deputy/install.d.ts +234 -6
  71. package/dist/deputy/install.js +843 -31
  72. package/dist/deputy/install.js.map +1 -1
  73. package/dist/deputy/notices.js +2 -2
  74. package/dist/deputy/notices.js.map +1 -1
  75. package/dist/deputy/protocol.d.ts +59 -5
  76. package/dist/deputy/protocol.js +48 -11
  77. package/dist/deputy/protocol.js.map +1 -1
  78. package/dist/deputy/routing.d.ts +37 -0
  79. package/dist/deputy/routing.js +181 -27
  80. package/dist/deputy/routing.js.map +1 -1
  81. package/dist/deputy/wake.d.ts +61 -0
  82. package/dist/deputy/wake.js +149 -0
  83. package/dist/deputy/wake.js.map +1 -0
  84. package/dist/diagnose.d.ts +19 -4
  85. package/dist/diagnose.js +64 -8
  86. package/dist/diagnose.js.map +1 -1
  87. package/dist/direct-setup.d.ts +104 -0
  88. package/dist/direct-setup.js +658 -0
  89. package/dist/direct-setup.js.map +1 -0
  90. package/dist/host-access.d.ts +75 -0
  91. package/dist/host-access.js +177 -0
  92. package/dist/host-access.js.map +1 -0
  93. package/dist/index.d.ts +20 -5
  94. package/dist/index.js +14 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/mcp/server.d.ts +27 -1
  97. package/dist/mcp/server.js +308 -109
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/op-result.d.ts +9 -0
  100. package/dist/op-result.js +13 -1
  101. package/dist/op-result.js.map +1 -1
  102. package/dist/read/filter-contract.d.ts +1 -1
  103. package/dist/read/filter-contract.js +4 -0
  104. package/dist/read/filter-contract.js.map +1 -1
  105. package/dist/read/shape.js +4 -0
  106. package/dist/read/shape.js.map +1 -1
  107. package/dist/read/views.d.ts +29 -0
  108. package/dist/read/views.js +60 -0
  109. package/dist/read/views.js.map +1 -1
  110. package/dist/session-grant.d.ts +48 -0
  111. package/dist/session-grant.js +185 -0
  112. package/dist/session-grant.js.map +1 -0
  113. package/dist/surface-copy.d.ts +19 -3
  114. package/dist/surface-copy.js +19 -3
  115. package/dist/surface-copy.js.map +1 -1
  116. package/dist/wizard.d.ts +58 -0
  117. package/dist/wizard.js +174 -0
  118. package/dist/wizard.js.map +1 -0
  119. package/dist/write/availability.d.ts +1 -16
  120. package/dist/write/availability.js +10 -65
  121. package/dist/write/availability.js.map +1 -1
  122. package/dist/write/batch.d.ts +20 -0
  123. package/dist/write/batch.js +111 -21
  124. package/dist/write/batch.js.map +1 -1
  125. package/dist/write/capabilities.d.ts +9 -0
  126. package/dist/write/capabilities.js +2 -0
  127. package/dist/write/capabilities.js.map +1 -1
  128. package/dist/write/clear-reminder.js +1 -1
  129. package/dist/write/clear-reminder.js.map +1 -1
  130. package/dist/write/commands.d.ts +22 -7
  131. package/dist/write/commands.js +231 -254
  132. package/dist/write/commands.js.map +1 -1
  133. package/dist/write/failure-hints.d.ts +32 -7
  134. package/dist/write/failure-hints.js +9 -14
  135. package/dist/write/failure-hints.js.map +1 -1
  136. package/dist/write/guards.js +22 -4
  137. package/dist/write/guards.js.map +1 -1
  138. package/dist/write/lock.d.ts +16 -13
  139. package/dist/write/lock.js +55 -0
  140. package/dist/write/lock.js.map +1 -1
  141. package/dist/write/make-repeating-project.js +9 -13
  142. package/dist/write/make-repeating-project.js.map +1 -1
  143. package/dist/write/move.js +9 -12
  144. package/dist/write/move.js.map +1 -1
  145. package/dist/write/operations.d.ts +51 -29
  146. package/dist/write/operations.js +16 -0
  147. package/dist/write/operations.js.map +1 -1
  148. package/dist/write/opid.d.ts +72 -14
  149. package/dist/write/opid.js +152 -8
  150. package/dist/write/opid.js.map +1 -1
  151. package/dist/write/param-schema.d.ts +80 -0
  152. package/dist/write/param-schema.js +754 -0
  153. package/dist/write/param-schema.js.map +1 -0
  154. package/dist/write/pipeline.d.ts +110 -5
  155. package/dist/write/pipeline.js +288 -14
  156. package/dist/write/pipeline.js.map +1 -1
  157. package/dist/write/pre-state.d.ts +38 -16
  158. package/dist/write/pre-state.js +117 -22
  159. package/dist/write/pre-state.js.map +1 -1
  160. package/dist/write/promote-clone.js +658 -444
  161. package/dist/write/promote-clone.js.map +1 -1
  162. package/dist/write/reorder.js +25 -8
  163. package/dist/write/reorder.js.map +1 -1
  164. package/dist/write/repeat-collateral.d.ts +31 -0
  165. package/dist/write/repeat-collateral.js +153 -0
  166. package/dist/write/repeat-collateral.js.map +1 -0
  167. package/dist/write/repeat-rule.d.ts +28 -1
  168. package/dist/write/repeat-rule.js +54 -4
  169. package/dist/write/repeat-rule.js.map +1 -1
  170. package/dist/write/resolution-timestamps.d.ts +11 -0
  171. package/dist/write/resolution-timestamps.js +108 -40
  172. package/dist/write/resolution-timestamps.js.map +1 -1
  173. package/dist/write/reversibility.js +4 -0
  174. package/dist/write/reversibility.js.map +1 -1
  175. package/dist/write/template-mutation.d.ts +137 -0
  176. package/dist/write/template-mutation.js +598 -0
  177. package/dist/write/template-mutation.js.map +1 -0
  178. package/dist/write/undo.js +10 -20
  179. package/dist/write/undo.js.map +1 -1
  180. package/dist/write/update-fields.d.ts +151 -0
  181. package/dist/write/update-fields.js +427 -0
  182. package/dist/write/update-fields.js.map +1 -0
  183. package/dist/write/vectors/applescript.js +6 -10
  184. package/dist/write/vectors/applescript.js.map +1 -1
  185. package/dist/write/vectors/registry.d.ts +1 -1
  186. package/dist/write/vectors/registry.js +10 -2
  187. package/dist/write/vectors/registry.js.map +1 -1
  188. package/dist/write/vectors/shortcuts.js +2 -2
  189. package/dist/write/vectors/shortcuts.js.map +1 -1
  190. package/dist/write/vectors/simulator.js +15 -4
  191. package/dist/write/vectors/simulator.js.map +1 -1
  192. package/dist/write/vectors/types.d.ts +268 -1
  193. package/dist/write/vectors/ui-certification.js +63 -13
  194. package/dist/write/vectors/ui-certification.js.map +1 -1
  195. package/dist/write/vectors/ui-chord.d.ts +144 -0
  196. package/dist/write/vectors/ui-chord.js +428 -0
  197. package/dist/write/vectors/ui-chord.js.map +1 -0
  198. package/dist/write/vectors/ui-drag.d.ts +8 -4
  199. package/dist/write/vectors/ui-drag.js +2 -0
  200. package/dist/write/vectors/ui-drag.js.map +1 -1
  201. package/dist/write/vectors/ui-recipes.d.ts +70 -3
  202. package/dist/write/vectors/ui-recipes.js +482 -44
  203. package/dist/write/vectors/ui-recipes.js.map +1 -1
  204. package/dist/write/vectors/ui.d.ts +280 -10
  205. package/dist/write/vectors/ui.js +1068 -77
  206. package/dist/write/vectors/ui.js.map +1 -1
  207. package/dist/write/vectors/url-scheme.js +3 -0
  208. package/dist/write/vectors/url-scheme.js.map +1 -1
  209. package/dist/write/verify/delta.d.ts +62 -0
  210. package/dist/write/verify/delta.js +36 -2
  211. package/dist/write/verify/delta.js.map +1 -1
  212. package/dist/write/verify/poller.d.ts +4 -1
  213. package/dist/write/verify/poller.js +14 -0
  214. package/dist/write/verify/poller.js.map +1 -1
  215. package/package.json +2 -1
  216. package/schema/envelope.schema.json +6 -4
  217. package/scripts/build-helpers.sh +5 -0
  218. package/skills/things-cli/SKILL.md +5 -3
  219. package/skills/things-cli/references/contracts.md +1 -0
  220. package/skills/things-cli/references/data-model.md +3 -0
  221. package/skills/things-cli/references/errors.md +1 -1
  222. package/skills/things-cli/references/gui.md +1 -0
  223. package/skills/things-cli/references/repeating.md +6 -0
  224. package/skills/things-cli/references/tag-cleanup.md +64 -0
@@ -1,16 +1,17 @@
1
- import { decodeReminderTime, encodeReminderTime, localToday, reminderUrlToken, zonedWallInstant, } from "../model/dates.js";
1
+ import { localToday, zonedWallInstant } from "../model/dates.js";
2
2
  import { byUuid } from "../read/detail.js";
3
3
  import { manualLogDateEpoch, pendingLogCount } from "../read/log-boundary.js";
4
4
  import { isLooseRef } from "../read/pseudo-area.js";
5
- import { reminderIsLive } from "../read/stage.js";
6
- import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
5
+ import { RESOLUTION_TIMESTAMP_EXPECTED } from "../surface-copy.js";
6
+ import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, seriesRowUuids, trashedCount, } from "./pre-state.js";
7
+ import { assertNotesModesExclusive, normalizeReminder, updateAssertions, updateWireParams, whenAssertions, whenWithReminder, } from "./update-fields.js";
7
8
  import { resolveTagRefs } from "./tag-refs.js";
8
9
  import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
9
10
  import { assertRepeatRule } from "./repeat-rule.js";
10
11
  import { assessOffRuleFirst, deadlineDriveNext, deriveFixedAnchor } from "./repeat-anchor.js";
11
12
  import { expectedRuleAssertions } from "./repeat-asserts.js";
12
13
  import { escapeAppleScript } from "./vectors/applescript.js";
13
- import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
14
+ import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingChordRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, createNextCopyRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
14
15
  import { buildRepeatingFingerprint } from "./verify/delta.js";
15
16
  // ------------------------------------------------------------------ helpers
16
17
  function thingsUrl(command, params, token) {
@@ -65,65 +66,6 @@ function q(value) {
65
66
  function unsupportedVector(op, vector) {
66
67
  throw new Error(`${op} cannot be compiled for vector ${vector} (planner bug)`);
67
68
  }
68
- function whenAssertions(when, todayIso, opts = { mode: "add" }) {
69
- // Strict shape check: an unvalidated string used to flow straight into the
70
- // URL (e.g. "2026-07-20@09:30", the raw URL grammar) — the app would SET
71
- // date+reminder while verification asserted the literal string as the date,
72
- // reporting a false mismatch on a write that succeeded.
73
- if (when !== "today" &&
74
- when !== "evening" &&
75
- when !== "anytime" &&
76
- when !== "someday" &&
77
- !/^\d{4}-\d{2}-\d{2}$/.test(when)) {
78
- throw new RangeError(when.includes("@")
79
- ? `invalid when "${when}" — a reminder time is a separate parameter (reminder: "HH:mm"; CLI --reminder), not an @ suffix`
80
- : `invalid when "${when}" — expected today | evening | anytime | someday | YYYY-MM-DD`);
81
- }
82
- switch (when) {
83
- case "today":
84
- // Today's non-evening section: in Today (the `today` marker) AND NOT in the
85
- // evening sub-bucket (the presence-keyed `evening` marker absent — asserted
86
- // as null, which valuesEqual treats as absent). Gated to Today members under
87
- // the verify clock exactly as the retired `todaySection` was.
88
- //
89
- // The `startDate` assertion differs by op (field bug §0½.8):
90
- // - ADD mints a fresh Today item with no schedule history, so the app dates
91
- // it EXACTLY today — exact equality is right.
92
- // - UPDATE of an item ALREADY in Today whose `startDate` has already arrived
93
- // (past or today): the app PRESERVES that historical date rather than
94
- // rewriting the storage byte to today (arrived-date law). Assert the
95
- // arrived-date PREDICATE (non-null and <= today) so a preserved historical
96
- // date verifies, while an undated deadline-only pull (null startDate) is
97
- // still rejected — the item's Today membership then rests only on a
98
- // deadline, not on the requested schedule.
99
- return [
100
- { field: "start", equals: "active" },
101
- opts.mode === "update"
102
- ? { field: "startDate", satisfies: { predicate: "arrived-on-or-before", date: todayIso } }
103
- : { field: "startDate", equals: todayIso },
104
- { field: "today", equals: true },
105
- { field: "evening", equals: null },
106
- ];
107
- case "evening":
108
- // The This-Evening sub-bucket: the `evening` marker (which implies `today`).
109
- return [
110
- { field: "start", equals: "active" },
111
- { field: "startDate", equals: todayIso },
112
- { field: "evening", equals: true },
113
- ];
114
- case "anytime":
115
- return [
116
- { field: "start", equals: "active" },
117
- { field: "startDate", equals: null },
118
- ];
119
- case "someday":
120
- return [{ field: "start", equals: "someday" }];
121
- default:
122
- // Concrete date: assert only the date — start-state semantics differ
123
- // for past/today/future dates (only the date itself is invariant).
124
- return [{ field: "startDate", equals: when }];
125
- }
126
- }
127
69
  function sortedTags(tags) {
128
70
  return [...tags].toSorted();
129
71
  }
@@ -139,21 +81,28 @@ function applyTagRefs(db, pre, tags) {
139
81
  pre.missingTags = res.missing;
140
82
  pre.resolvedTagTitles = res.titles;
141
83
  }
142
- /** Round-trip normalization: "6:5"-style inputs → canonical "06:05". */
143
- function normalizeReminder(time) {
144
- return decodeReminderTime(encodeReminderTime(time)) ?? time;
145
- }
146
84
  /**
147
- * The URL `when` value with an optional reminder token appended through the
148
- * deterministic emitter (never a bare 1–11 hour oddity 2d).
85
+ * Is a destination container SUPPLIED? A PRESENCE check, not a duck-test (#580):
86
+ * the parameter schema (param-schema.ts) has already proven that a present ref is
87
+ * a `{uuid}`/`{title}` object, so anything else reaching here is an engine bug,
88
+ * not caller input — and it THROWS rather than reading as "no destination given",
89
+ * which is precisely how a bare-string `project` used to compile an Inbox capture
90
+ * with no placement and no placement assertion, then verify clean.
149
91
  */
150
- function whenWithReminder(when, reminder) {
151
- if (reminder === undefined || reminder === null)
152
- return when;
153
- return `${when}@${reminderUrlToken(reminder)}`;
92
+ export function containerGiven(ref) {
93
+ if (ref === undefined || ref === null)
94
+ return false;
95
+ assertContainerRef(ref);
96
+ return true;
154
97
  }
155
- function containerGiven(ref) {
156
- return ref !== undefined && (ref.uuid !== undefined || ref.title !== undefined);
98
+ /** Belt-and-braces: a present container reference must carry a uuid or a title. */
99
+ export function assertContainerRef(ref) {
100
+ if (typeof ref !== "object" || Array.isArray(ref)) {
101
+ throw new RangeError(`a container reference must be an object naming a uuid or a title — received ${JSON.stringify(ref)}`);
102
+ }
103
+ if (ref.uuid === undefined && ref.title === undefined) {
104
+ throw new RangeError("a container reference must name a uuid or a title — it names neither");
105
+ }
157
106
  }
158
107
  /**
159
108
  * The create-probe title for the duplicate / identity-replacement ops, whose
@@ -351,108 +300,35 @@ const todoAdd = {
351
300
  },
352
301
  };
353
302
  /**
354
- * The effective reminder a when-bearing update should leave behind. A bare
355
- * `when=` CLEARS an existing reminder (R07/R20), so when the caller
356
- * re-schedules to today/evening/a date without addressing the reminder we
357
- * auto-preserve the current one; an explicit null is the intentional clear.
358
- *
359
- * §9n: a reminder whose row's `startDate` is already strictly PAST is
360
- * presentation-dead (the GUI hides its bell; the byte lingers in the DB). We do
361
- * NOT auto-preserve such a stale byte — carrying it into the re-schedule would
362
- * RESURRECT a reminder the user believes gone. A LIVE reminder (startDate
363
- * today/future) is preserved as before. The liveness test is the SAME
364
- * {@link reminderIsLive} predicate the read side gates on, consulted against the
365
- * target's CURRENT `startDate` under the response clock.
303
+ * The to-do / project update pair. Both legs the URL parameters and the
304
+ * expected-delta assertions are DERIVED from the single exhaustive registry in
305
+ * update-fields.ts (the #491 exhaustive-map doctrine): the only per-verb
306
+ * difference is the URL command name, so neither verb can accept a field the
307
+ * other drops, and a field added to `UpdateFields` breaks compilation there
308
+ * until every leg handles it.
366
309
  */
367
- function effectiveReminder(pre, params) {
368
- if (params.reminder !== undefined)
369
- return params.reminder;
370
- const when = params.when;
371
- const schedulable = when === "today" ||
372
- when === "evening" ||
373
- (typeof when === "string" && /^\d{4}-\d{2}-\d{2}$/.test(when));
374
- if (!schedulable)
375
- return null;
376
- const target = pre.target;
377
- if (target === null || target.type === "heading")
378
- return null;
379
- if (!reminderIsLive(target.startDate, pre.todayIso))
380
- return null;
381
- // Read the RAW stored byte off the substrate (top-level `reminder` is
382
- // live-gated under the LOAD clock, which can differ from `pre.todayIso` under a
383
- // pinned THINGS_NOW). The `reminderIsLive` guard above applies liveness under
384
- // the injected clock explicitly, so the raw byte + this gate reproduce the
385
- // former semantics exactly.
386
- return target.derived.reminder;
387
- }
388
- function assertNotesModesExclusive(params) {
389
- if (params.notes !== undefined &&
390
- (params.appendNotes !== undefined || params.prependNotes !== undefined)) {
391
- throw new RangeError("notes (replace) is exclusive with appendNotes/prependNotes");
392
- }
393
- if (params.appendNotes !== undefined && params.prependNotes !== undefined) {
394
- throw new RangeError("appendNotes and prependNotes cannot be combined in one update");
395
- }
396
- }
397
- function expectedNotes(pre, params) {
398
- const current = pre.target !== null && pre.target.type !== "heading" ? pre.target.notes : "";
399
- // Separator semantics probed: newline-joined, no stray newline against an
400
- // empty note (E04/E05/E11/E12).
401
- if (params.appendNotes !== undefined) {
402
- return current === "" ? params.appendNotes : `${current}\n${params.appendNotes}`;
403
- }
404
- if (params.prependNotes !== undefined) {
405
- return current === "" ? params.prependNotes : `${params.prependNotes}\n${current}`;
406
- }
407
- return undefined;
310
+ function updateSpec(op, urlCommand) {
311
+ return {
312
+ op,
313
+ hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
314
+ preRead(db, params, now) {
315
+ assertNotesModesExclusive(params);
316
+ const pre = emptyPreState();
317
+ pre.todayIso = localToday(now);
318
+ pre.target = loadTarget(db, params.uuid);
319
+ return pre;
320
+ },
321
+ expectedDelta(pre, params, ctx) {
322
+ return { mode: "update", uuid: params.uuid, assert: updateAssertions(params, pre, ctx) };
323
+ },
324
+ compile(params, vector, pre, ctx) {
325
+ if (vector !== "url-scheme")
326
+ unsupportedVector(op, vector);
327
+ return thingsUrl(urlCommand, { id: params.uuid, ...updateWireParams(params, pre) }, ctx.token);
328
+ },
329
+ };
408
330
  }
409
- const todoUpdate = {
410
- op: "todo.update",
411
- hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
412
- preRead(db, params, now) {
413
- assertNotesModesExclusive(params);
414
- const pre = emptyPreState();
415
- pre.todayIso = localToday(now);
416
- pre.target = loadTarget(db, params.uuid);
417
- return pre;
418
- },
419
- expectedDelta(pre, params, ctx) {
420
- const assert = [];
421
- if (params.title !== undefined)
422
- assert.push({ field: "title", equals: params.title });
423
- if (params.notes !== undefined)
424
- assert.push({ field: "notes", equals: params.notes });
425
- const joined = expectedNotes(pre, params);
426
- if (joined !== undefined)
427
- assert.push({ field: "notes", equals: joined });
428
- if (params.when !== undefined) {
429
- assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
430
- const reminder = effectiveReminder(pre, params);
431
- assert.push({
432
- field: "reminder",
433
- equals: reminder === null ? null : normalizeReminder(reminder),
434
- });
435
- }
436
- if (params.deadline !== undefined)
437
- assert.push({ field: "deadline", equals: params.deadline });
438
- return { mode: "update", uuid: params.uuid, assert };
439
- },
440
- compile(params, vector, pre, ctx) {
441
- if (vector !== "url-scheme")
442
- unsupportedVector(this.op, vector);
443
- return thingsUrl("update", {
444
- id: params.uuid,
445
- title: params.title,
446
- notes: params.notes,
447
- "append-notes": params.appendNotes,
448
- "prepend-notes": params.prependNotes,
449
- when: params.when === undefined
450
- ? undefined
451
- : whenWithReminder(params.when, effectiveReminder(pre, params)),
452
- deadline: params.deadline === null ? "" : params.deadline,
453
- }, ctx.token);
454
- },
455
- };
331
+ const todoUpdate = updateSpec("todo.update", "update");
456
332
  function statusSpec(op, urlParam, asStatus, scriptStatus) {
457
333
  return {
458
334
  op,
@@ -826,6 +702,14 @@ const projectAdd = {
826
702
  const area = pre.destArea?.resolved;
827
703
  if (area !== undefined && area !== null)
828
704
  assert.push({ field: "area.uuid", equals: area.uuid });
705
+ // KNOWN SHALLOW SPOT (audited 2026-08-23, exhaustive-map sweep): the seeded
706
+ // children — `todos` and the structured `items` (headings + rich children) —
707
+ // contribute NO assertion, so a project born with an empty body still
708
+ // verifies `ok`. Unlike a to-do's `checklistItems` (asserted through
709
+ // `checklistTitles`) there is no child-list field on the delta vocabulary to
710
+ // compare against, and adding one needs probe evidence that the imported
711
+ // children are readable inside the verify window before a missing child may
712
+ // be called a failure. Tracked in docs/up-next.md; do NOT "fix" it blind.
829
713
  return {
830
714
  mode: "create",
831
715
  probe: {
@@ -873,55 +757,9 @@ const projectAdd = {
873
757
  }, ctx.token);
874
758
  },
875
759
  };
876
- const projectUpdate = {
877
- op: "project.update",
878
- hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
879
- preRead(db, params, now) {
880
- assertNotesModesExclusive(params);
881
- const pre = emptyPreState();
882
- pre.todayIso = localToday(now);
883
- pre.target = loadTarget(db, params.uuid);
884
- return pre;
885
- },
886
- expectedDelta(pre, params, ctx) {
887
- const assert = [];
888
- if (params.title !== undefined)
889
- assert.push({ field: "title", equals: params.title });
890
- if (params.notes !== undefined)
891
- assert.push({ field: "notes", equals: params.notes });
892
- const joined = expectedNotes(pre, params);
893
- if (joined !== undefined)
894
- assert.push({ field: "notes", equals: joined });
895
- if (params.when !== undefined) {
896
- assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
897
- // Projects carry the same reminderTime codec as to-dos (A3); a bare
898
- // when= clears an existing reminder unless auto-preserved.
899
- const reminder = effectiveReminder(pre, params);
900
- assert.push({
901
- field: "reminder",
902
- equals: reminder === null ? null : normalizeReminder(reminder),
903
- });
904
- }
905
- if (params.deadline !== undefined)
906
- assert.push({ field: "deadline", equals: params.deadline });
907
- return { mode: "update", uuid: params.uuid, assert };
908
- },
909
- compile(params, vector, pre, ctx) {
910
- if (vector !== "url-scheme")
911
- unsupportedVector(this.op, vector);
912
- return thingsUrl("update-project", {
913
- id: params.uuid,
914
- title: params.title,
915
- notes: params.notes,
916
- "append-notes": params.appendNotes,
917
- "prepend-notes": params.prependNotes,
918
- when: params.when === undefined
919
- ? undefined
920
- : whenWithReminder(params.when, effectiveReminder(pre, params)),
921
- deadline: params.deadline === null ? "" : params.deadline,
922
- }, ctx.token);
923
- },
924
- };
760
+ // Projects carry the same reminderTime codec and notes/schedule semantics as
761
+ // to-dos (A3/E18) — same registry, same legs, only the URL command differs.
762
+ const projectUpdate = updateSpec("project.update", "update-project");
925
763
  const projectSetTags = {
926
764
  op: "project.set-tags",
927
765
  hazards: ["H-UNKNOWN-DESTINATION", "H-UNKNOWN-TAG"],
@@ -1579,22 +1417,26 @@ const reorder = {
1579
1417
  };
1580
1418
  // ---- resolution-timestamp normalization (§5 of the plan) ------------------
1581
1419
  /**
1582
- * Parse an ISO date (`2025-01-15`) OR datetime (`2025-01-15T09:30[:ss]`) and
1583
- * resolve it to a single UTC instant in the effective `zone`. A date-only value
1584
- * lands at NOON in that zone (B-DATEONLY: noon decodes to the intended calendar
1585
- * date in every zone; midnight can slip a day). A datetime is read as wall-clock
1586
- * time in the effective zone. `zone` undefined = the process-local (app host)
1587
- * zone, which for a local CLI run IS the app's own zone.
1420
+ * Parse a date (`2025-01-15`) OR datetime (`2025-01-15T09:30[:ss]`, or the same
1421
+ * value spelled with a space `2025-01-15 09:30`) and resolve it to a single
1422
+ * UTC instant in the effective `zone`. Both separators are first-class
1423
+ * spellings of the SAME instant: the space form is what a human (or a shell
1424
+ * heredoc) types, the `T` form is what a serializer emits, and neither is
1425
+ * rewritten into the other. A date-only value lands at NOON in that zone
1426
+ * (B-DATEONLY: noon decodes to the intended calendar date in every zone;
1427
+ * midnight can slip a day). A datetime is read as wall-clock time in the
1428
+ * effective zone. `zone` undefined = the process-local (app host) zone, which
1429
+ * for a local CLI run IS the app's own zone.
1588
1430
  */
1589
1431
  export function resolveResolutionInstant(input, zone) {
1590
1432
  const dateOnly = /^(\d{4}-\d{2}-\d{2})$/.exec(input);
1591
1433
  if (dateOnly !== null)
1592
1434
  return zonedWallInstant(dateOnly[1], 12, 0, 0, zone);
1593
- const dt = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(input);
1435
+ const dt = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(input);
1594
1436
  if (dt !== null) {
1595
1437
  return zonedWallInstant(dt[1], Number(dt[2]), Number(dt[3]), dt[4] === undefined ? 0 : Number(dt[4]), zone);
1596
1438
  }
1597
- throw new RangeError(`invalid timestamp "${input}" — expected an ISO date (YYYY-MM-DD) or datetime (YYYY-MM-DDTHH:mm)`);
1439
+ throw new RangeError(`invalid timestamp "${input}" — expected ${RESOLUTION_TIMESTAMP_EXPECTED}`);
1598
1440
  }
1599
1441
  const pad2 = (n) => String(n).padStart(2, "0");
1600
1442
  /** The host-local calendar date (`YYYY-MM-DD`) an instant renders as — what the verify layer observes for stopped/created. */
@@ -1825,9 +1667,18 @@ const headingAdd = {
1825
1667
  });
1826
1668
  },
1827
1669
  };
1670
+ /**
1671
+ * project.move-heading — within-project heading order, on the ARROW-CHORD ui
1672
+ * vector (CHORDMH1, on the HEADORD1 law). Things 3.23 left the private reorder
1673
+ * command declared and inert, which took heading order offline entirely; the
1674
+ * replacement is the app's own keyboard affordance (⌘↑/⌘↓/⌘⌥↑/⌘⌥↓ on a selected
1675
+ * heading row), driven as a verified closed loop by src/write/vectors/ui-chord.ts.
1676
+ * This is the ONE reorder that leaves the `privateReorderIsNoOp` range — every
1677
+ * other scope still routes through its own gate.
1678
+ */
1828
1679
  const projectMoveHeading = {
1829
1680
  op: "project.move-heading",
1830
- hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-ORDER"],
1681
+ hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-ORDER", "H-UI-DRIVE"],
1831
1682
  preRead(db, params) {
1832
1683
  const pre = emptyPreState();
1833
1684
  pre.destProject = resolveProject(db, params.project);
@@ -1835,24 +1686,41 @@ const projectMoveHeading = {
1835
1686
  return pre;
1836
1687
  },
1837
1688
  expectedDelta(pre) {
1838
- // The reorder wire re-ranks the FULL heading list; verify the whole target
1839
- // order (strictly ascending "index"). Children follow their heading (scf
1840
- // P1), so no per-child assertion is needed.
1841
- return { mode: "ordering", key: "index", sequence: pre.headingMove?.targetOrder ?? [] };
1689
+ const move = pre.headingMove;
1690
+ return {
1691
+ mode: "ordering",
1692
+ key: "index",
1693
+ // The full end state: every heading of the project, strictly ascending.
1694
+ sequence: move?.targetOrder ?? [],
1695
+ // RRF1: the headings the caller did NOT name must hold their EXACT prior
1696
+ // rank — a chord is a single-row write, and this is what proves the one
1697
+ // that landed was the row the plan addressed (a heading row exposes no
1698
+ // title to read the selection back through).
1699
+ unchanged: move?.untouched ?? [],
1700
+ // Children follow their heading through an intact FK, un-renumbered and
1701
+ // un-touched (no userModificationDate bump — that is what `frozen` adds).
1702
+ frozen: (move?.children ?? []).map((c) => ({
1703
+ uuid: c.uuid,
1704
+ assert: [{ field: "heading.uuid", equals: c.heading }],
1705
+ })),
1706
+ ...(move?.targetOrder[0] !== undefined && { subject: move.targetOrder[0] }),
1707
+ };
1842
1708
  },
1843
- compile(_params, vector, pre) {
1844
- // The same native private-reorder wire the old `reorder --scope headings`
1845
- // used (experimental — gated by allow-experimental + the sdef canary),
1846
- // fed the computed full order.
1847
- if (vector !== "applescript")
1709
+ compile(params, vector, pre) {
1710
+ if (vector !== "ui")
1848
1711
  unsupportedVector(this.op, vector);
1849
- // #V11: send the MINIMAL front-cluster wire, not the full order — an archived
1850
- // heading that need not move stays out of it (untouched, HEADSORT H-UNSWEPT);
1851
- // any archived heading the target FORCES in (pre.headingMove.reopened) is
1852
- // reopened and disclosed. NEVER put un-targeted rows into the wire.
1853
- const order = pre.headingMove?.wire ?? [];
1854
- return osa(`${PRIVATE_REORDER_COMMAND} project id ${q(pre.destProject?.resolved?.uuid ?? "")} ` +
1855
- `with ids ${q(order.join(","))}`);
1712
+ const move = pre.headingMove;
1713
+ if (move === null || move.problems.length > 0) {
1714
+ // The membership/anchor refusals are surfaced by H-HEADING-ORDER
1715
+ // before compile; reaching here with one means the guard was bypassed.
1716
+ throw new Error(`project.move-heading: ${move === null ? "no heading move computed (guard bypassed?)" : move.problems.join("; ")}`);
1717
+ }
1718
+ // The movee set is EXACTLY what the caller named — never derived from the
1719
+ // untouched set, which fences a different (smaller) thing: a bystander whose
1720
+ // POSITION shifts is still a bystander no chord may move.
1721
+ return uiDrive(moveHeadingChordRecipe(pre.destProject?.resolved?.uuid ?? "", move.targetOrder, [
1722
+ ...params.headings,
1723
+ ]));
1856
1724
  },
1857
1725
  };
1858
1726
  const todoClearDatedReminder = {
@@ -1901,11 +1769,41 @@ function uiDrive(recipe) {
1901
1769
  // the validator from commands.ts; the implementation + the full combination
1902
1770
  // matrix live in repeat-rule.ts.
1903
1771
  export { assertRepeatRule };
1772
+ /**
1773
+ * The recipe extras for a MAKE-REPEATING drive: {@link ruleExtras} with the
1774
+ * "Next:" drive date deadline-adjusted, exactly as {@link reschedRuleExtras}
1775
+ * does for a reschedule.
1776
+ *
1777
+ * ONE MEANING FOR `next`, ONE PLACE THAT SHIFTS IT (NEXTPOP1). `next` is the
1778
+ * requested first-occurrence START — what `--when` means to a caller — in every
1779
+ * params bag, at every layer. A DEADLINED rule anchors the dialog on the
1780
+ * DEADLINE (YANCH1 #493), so the date the "Next:" control must carry is
1781
+ * `next + startDaysEarlier`; that conversion belongs to the compile, where the
1782
+ * dialog is, and nowhere else.
1783
+ *
1784
+ * It used to happen upstream too: the promote orchestrators shifted `next`
1785
+ * before handing the bag to this op, so the SAME field meant a START coming from
1786
+ * a caller and a DUE date coming from a composite. Everything downstream that
1787
+ * shifts — `assessOffRuleFirst`, via `assertRepeatRule` in preRead — then shifted
1788
+ * a second time, and a deadlined MONTHLY promote was refused before it ran with
1789
+ * a message about a date the caller never asked for (`--deadline 2026-08-20`
1790
+ * + `--when 2026-08-06` → "a first occurrence on 2026-08-20 would not hold",
1791
+ * measured in-lab). The orchestrators now pass `--when` through unshifted.
1792
+ */
1793
+ function makeRuleExtras(params) {
1794
+ const base = ruleExtras(params);
1795
+ const drive = deadlineDriveNext(params);
1796
+ return drive !== undefined ? { ...base, next: drive } : base;
1797
+ }
1904
1798
  /**
1905
1799
  * The extended-vocabulary fields of a rule as a recipe `extras` bag, including
1906
1800
  * ONLY the keys that are present (exactOptionalPropertyTypes: never set an
1907
1801
  * optional field to undefined). A bare `{ uuid, frequency, interval }` yields
1908
1802
  * `{}`, so the recipe drives exactly the certified two-control path.
1803
+ *
1804
+ * `next` is copied through VERBATIM — a START. Every compile that drives the
1805
+ * dialog wraps this with its own deadline shift ({@link makeRuleExtras},
1806
+ * {@link reschedRuleExtras}); this function never shifts (NEXTPOP1).
1909
1807
  */
1910
1808
  function ruleExtras(params) {
1911
1809
  return {
@@ -1948,15 +1846,31 @@ function reschedEffParams(params) {
1948
1846
  * `--when` is the scheduled START, but a deadlined rule anchors on the DEADLINE,
1949
1847
  * so the dialog's "Next:" field is driven with `when + startDaysEarlier` and the
1950
1848
  * app back-shifts the instance start to `--when`. The expectedDelta keeps the RAW
1951
- * `params.next` (the cursor asserts the START). make/add-repeating apply this shift
1952
- * upstream (promote-clone.ts) and pass an already-adjusted rule, so only the
1953
- * reschedule compile needs it here.
1849
+ * `params.next` (the cursor asserts the START). {@link makeRuleExtras} is the
1850
+ * make/add-repeating twin of this the shift lands in the compile on every verb
1851
+ * that drives the dialog, and nowhere upstream of it (NEXTPOP1).
1954
1852
  */
1955
1853
  function reschedRuleExtras(params) {
1956
1854
  const base = ruleExtras(reschedEffParams(params));
1957
1855
  const drive = deadlineDriveNext(params);
1958
1856
  return drive !== undefined ? { ...base, next: drive } : base;
1959
1857
  }
1858
+ /**
1859
+ * The rule-vocabulary keys this reschedule REQUESTED — the input to the
1860
+ * unexplained-delta check (CGRD1 guard 3): every decoded-rule field that moves
1861
+ * must be attributable to one of these, or to a mapped co-mover of one.
1862
+ *
1863
+ * Built from the EFFECTIVE params, the same bag the recipe drives, so a
1864
+ * `--when`-only reschedule that DERIVES a calendar anchor counts that anchor as
1865
+ * requested rather than as an unexplained move (RSPA1 / YANCH1 #493) — the drive
1866
+ * really does address those pop-ups. `uuid` is the target, not a rule field.
1867
+ */
1868
+ function requestedRuleKeys(params) {
1869
+ const eff = reschedEffParams(params);
1870
+ return Object.entries(eff)
1871
+ .filter(([key, value]) => key !== "uuid" && value !== undefined)
1872
+ .map(([key]) => key);
1873
+ }
1960
1874
  /** ui ops all guard existence/type + the H-UI-DRIVE acknowledgement. */
1961
1875
  const UI_HAZARDS = ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"];
1962
1876
  const todoMakeRepeating = {
@@ -2004,7 +1918,7 @@ const todoMakeRepeating = {
2004
1918
  compile(params, vector) {
2005
1919
  if (vector !== "ui")
2006
1920
  unsupportedVector(this.op, vector);
2007
- return uiDrive(makeRepeatingRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
1921
+ return uiDrive(makeRepeatingRecipe(params.uuid, params.frequency, params.interval, makeRuleExtras(params)));
2008
1922
  },
2009
1923
  };
2010
1924
  const todoRescheduleRepeat = {
@@ -2042,6 +1956,12 @@ const todoRescheduleRepeat = {
2042
1956
  includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
2043
1957
  }),
2044
1958
  capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
1959
+ // UNEXPLAINED-DELTA DETECTION (CGRD1 guard 3). The assertions above prove
1960
+ // the requested rule landed; this proves nothing ELSE did. A reschedule
1961
+ // rewrites an EXISTING rule, so there is a full pre-state to diff against —
1962
+ // which is why make-repeating / add-repeating do not carry it: they mint the
1963
+ // rule, so every field is new by construction and there is nothing to compare.
1964
+ collateral: { requested: requestedRuleKeys(params) },
2045
1965
  };
2046
1966
  },
2047
1967
  compile(params, vector) {
@@ -2072,6 +1992,56 @@ const todoPauseRepeat = {
2072
1992
  return uiDrive(pauseRepeatRecipe(params.uuid));
2073
1993
  },
2074
1994
  };
1995
+ /**
1996
+ * `Items ▸ Repeat ▸ Create Next Copy` (CNC1) — materialize the pending
1997
+ * occurrence and advance the series. The INTERNAL first leg of the
1998
+ * template-mutation composites (template-mutation.ts); it is not a user-facing
1999
+ * verb, because a bare "spawn one now" has no use we have been asked for and
2000
+ * every honest use of it mutates the row it mints.
2001
+ *
2002
+ * Verified as a CREATE: the minted row wears the template's own title, is a
2003
+ * to-do, carries a fresh (gesture wall-clock) creationDate, and is not one of
2004
+ * the rows the series already had — which is exactly the create-probe's
2005
+ * gauntlet, and it hands the composite back the minted uuid it needs. The one
2006
+ * field assertion is the FK: the discovered row must be an instance of THIS
2007
+ * template, so a same-titled row created by anything else in the window cannot
2008
+ * pass. The cursor advance is deliberately NOT asserted here — a create delta
2009
+ * asserts against the created row, and the composite re-reads the template
2010
+ * afterwards anyway to disclose when the next occurrence lands.
2011
+ */
2012
+ const todoCreateNextCopy = {
2013
+ op: "todo.create-next-copy",
2014
+ hazards: UI_HAZARDS,
2015
+ preRead(db, params) {
2016
+ const pre = emptyPreState();
2017
+ pre.target = loadTarget(db, params.uuid);
2018
+ // The series' existing rows — the template plus every instance it has
2019
+ // already spawned, all of which share its title. They are the create
2020
+ // probe's exclusion set, so "the new row" is unambiguous even on a series
2021
+ // whose occurrences all look alike.
2022
+ pre.sameTitleUuids = seriesRowUuids(db, params.uuid);
2023
+ return pre;
2024
+ },
2025
+ expectedDelta(pre, params, ctx) {
2026
+ const target = pre.target;
2027
+ const title = target !== null && target.type !== "heading" ? target.title : "";
2028
+ return {
2029
+ mode: "create",
2030
+ probe: {
2031
+ title,
2032
+ type: "to-do",
2033
+ sinceEpoch: ctx.nowEpoch - 2,
2034
+ excludeUuids: pre.sameTitleUuids,
2035
+ },
2036
+ assert: [{ field: "repeating.templateUuid", equals: params.uuid }],
2037
+ };
2038
+ },
2039
+ compile(params, vector) {
2040
+ if (vector !== "ui")
2041
+ unsupportedVector(this.op, vector);
2042
+ return uiDrive(createNextCopyRecipe(params.uuid));
2043
+ },
2044
+ };
2075
2045
  const todoResumeRepeat = {
2076
2046
  op: "todo.resume-repeat",
2077
2047
  hazards: UI_HAZARDS,
@@ -2131,6 +2101,12 @@ const projectRescheduleRepeat = {
2131
2101
  includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
2132
2102
  }),
2133
2103
  capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
2104
+ // UNEXPLAINED-DELTA DETECTION (CGRD1 guard 3). The assertions above prove
2105
+ // the requested rule landed; this proves nothing ELSE did. A reschedule
2106
+ // rewrites an EXISTING rule, so there is a full pre-state to diff against —
2107
+ // which is why make-repeating / add-repeating do not carry it: they mint the
2108
+ // rule, so every field is new by construction and there is nothing to compare.
2109
+ collateral: { requested: requestedRuleKeys(params) },
2134
2110
  };
2135
2111
  },
2136
2112
  compile(params, vector) {
@@ -2240,7 +2216,7 @@ const projectMakeRepeating = {
2240
2216
  // Unreachable in practice: the guards block refuse/anytime before compile.
2241
2217
  throw new Error("project.make-repeating: no selectable-row taxonomy resolved (guard bypassed?)");
2242
2218
  }
2243
- return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
2219
+ return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, makeRuleExtras(params)));
2244
2220
  },
2245
2221
  };
2246
2222
  // project.add-repeating is delivered by the runAddRepeatingProject
@@ -2612,6 +2588,7 @@ export const COMMANDS = {
2612
2588
  "todo.make-repeating": todoMakeRepeating,
2613
2589
  "todo.reschedule-repeat": todoRescheduleRepeat,
2614
2590
  "todo.pause-repeat": todoPauseRepeat,
2591
+ "todo.create-next-copy": todoCreateNextCopy,
2615
2592
  "todo.resume-repeat": todoResumeRepeat,
2616
2593
  "todo.convert-to-project": todoConvertToProject,
2617
2594
  "project.reschedule-repeat": projectRescheduleRepeat,