things-api 0.9.0 → 0.10.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 (214) hide show
  1. package/README.md +30 -11
  2. package/dist/audit/log.js +56 -2
  3. package/dist/audit/log.js.map +1 -1
  4. package/dist/audit/schema.d.ts +26 -8
  5. package/dist/audit/schema.js +42 -1
  6. package/dist/audit/schema.js.map +1 -1
  7. package/dist/cli/commands/area.d.ts +26 -3
  8. package/dist/cli/commands/area.js +125 -70
  9. package/dist/cli/commands/area.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +1 -1
  11. package/dist/cli/commands/doctor.js +44 -5
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/mcp.js +23 -4
  14. package/dist/cli/commands/mcp.js.map +1 -1
  15. package/dist/cli/commands/project.d.ts +18 -1
  16. package/dist/cli/commands/project.js +69 -31
  17. package/dist/cli/commands/project.js.map +1 -1
  18. package/dist/cli/commands/reads.js +423 -157
  19. package/dist/cli/commands/reads.js.map +1 -1
  20. package/dist/cli/commands/repeat-flags.d.ts +20 -0
  21. package/dist/cli/commands/repeat-flags.js +76 -0
  22. package/dist/cli/commands/repeat-flags.js.map +1 -0
  23. package/dist/cli/commands/setup.js +2 -3
  24. package/dist/cli/commands/setup.js.map +1 -1
  25. package/dist/cli/commands/show.js +47 -24
  26. package/dist/cli/commands/show.js.map +1 -1
  27. package/dist/cli/commands/todo.d.ts +1 -1
  28. package/dist/cli/commands/todo.js +7 -6
  29. package/dist/cli/commands/todo.js.map +1 -1
  30. package/dist/cli/commands/writes.js +378 -148
  31. package/dist/cli/commands/writes.js.map +1 -1
  32. package/dist/cli/did-you-mean.d.ts +1 -1
  33. package/dist/cli/excess-args.d.ts +15 -0
  34. package/dist/cli/excess-args.js +51 -0
  35. package/dist/cli/excess-args.js.map +1 -0
  36. package/dist/cli/glyphs.d.ts +39 -2
  37. package/dist/cli/glyphs.js +91 -21
  38. package/dist/cli/glyphs.js.map +1 -1
  39. package/dist/cli/help.d.ts +54 -0
  40. package/dist/cli/help.js +355 -0
  41. package/dist/cli/help.js.map +1 -0
  42. package/dist/cli/main.js +32 -25
  43. package/dist/cli/main.js.map +1 -1
  44. package/dist/cli/period.d.ts +7 -0
  45. package/dist/cli/period.js +12 -0
  46. package/dist/cli/period.js.map +1 -1
  47. package/dist/cli/read-driver.d.ts +26 -19
  48. package/dist/cli/read-driver.js +67 -33
  49. package/dist/cli/read-driver.js.map +1 -1
  50. package/dist/cli/render.d.ts +92 -28
  51. package/dist/cli/render.js +318 -97
  52. package/dist/cli/render.js.map +1 -1
  53. package/dist/cli/resolve-invocation.d.ts +42 -4
  54. package/dist/cli/resolve-invocation.js +97 -14
  55. package/dist/cli/resolve-invocation.js.map +1 -1
  56. package/dist/cli/tag-filters.d.ts +59 -0
  57. package/dist/cli/tag-filters.js +57 -0
  58. package/dist/cli/tag-filters.js.map +1 -0
  59. package/dist/cli/verb-hint.d.ts +25 -0
  60. package/dist/cli/verb-hint.js +138 -0
  61. package/dist/cli/verb-hint.js.map +1 -0
  62. package/dist/cli/width.d.ts +135 -0
  63. package/dist/cli/width.js +313 -0
  64. package/dist/cli/width.js.map +1 -0
  65. package/dist/client.d.ts +137 -24
  66. package/dist/client.js +105 -16
  67. package/dist/client.js.map +1 -1
  68. package/dist/config.d.ts +11 -0
  69. package/dist/config.js +3 -0
  70. package/dist/config.js.map +1 -1
  71. package/dist/contracts.d.ts +87 -17
  72. package/dist/contracts.js +44 -1
  73. package/dist/contracts.js.map +1 -1
  74. package/dist/db/fingerprint.d.ts +12 -0
  75. package/dist/db/fingerprint.js +15 -1
  76. package/dist/db/fingerprint.js.map +1 -1
  77. package/dist/db/locate.js +1 -1
  78. package/dist/db/locate.js.map +1 -1
  79. package/dist/diagnose.d.ts +63 -0
  80. package/dist/diagnose.js +38 -1
  81. package/dist/diagnose.js.map +1 -1
  82. package/dist/index.d.ts +30 -4
  83. package/dist/index.js +35 -2
  84. package/dist/index.js.map +1 -1
  85. package/dist/mcp/server.d.ts +9 -1
  86. package/dist/mcp/server.js +691 -133
  87. package/dist/mcp/server.js.map +1 -1
  88. package/dist/model/entities.d.ts +31 -7
  89. package/dist/model/entities.js.map +1 -1
  90. package/dist/model/mappers.d.ts +7 -0
  91. package/dist/model/mappers.js +7 -2
  92. package/dist/model/mappers.js.map +1 -1
  93. package/dist/model/serialize.d.ts +28 -0
  94. package/dist/model/serialize.js +80 -0
  95. package/dist/model/serialize.js.map +1 -0
  96. package/dist/model/when-sugar.d.ts +47 -0
  97. package/dist/model/when-sugar.js +45 -0
  98. package/dist/model/when-sugar.js.map +1 -0
  99. package/dist/read/area-view.d.ts +8 -1
  100. package/dist/read/area-view.js +49 -13
  101. package/dist/read/area-view.js.map +1 -1
  102. package/dist/read/filter-contract.d.ts +117 -0
  103. package/dist/read/filter-contract.js +78 -0
  104. package/dist/read/filter-contract.js.map +1 -0
  105. package/dist/read/predicates.d.ts +30 -0
  106. package/dist/read/predicates.js +30 -0
  107. package/dist/read/predicates.js.map +1 -1
  108. package/dist/read/project-view.d.ts +12 -1
  109. package/dist/read/project-view.js +39 -8
  110. package/dist/read/project-view.js.map +1 -1
  111. package/dist/read/queries.d.ts +149 -6
  112. package/dist/read/queries.js +250 -31
  113. package/dist/read/queries.js.map +1 -1
  114. package/dist/read/sections.d.ts +51 -0
  115. package/dist/read/sections.js +37 -0
  116. package/dist/read/sections.js.map +1 -0
  117. package/dist/read/sidebar-order.js +2 -1
  118. package/dist/read/sidebar-order.js.map +1 -1
  119. package/dist/read/tags.d.ts +27 -3
  120. package/dist/read/tags.js +83 -12
  121. package/dist/read/tags.js.map +1 -1
  122. package/dist/read/truncation.d.ts +71 -0
  123. package/dist/read/{pagination.js → truncation.js} +103 -94
  124. package/dist/read/truncation.js.map +1 -0
  125. package/dist/read/views.d.ts +140 -16
  126. package/dist/read/views.js +264 -67
  127. package/dist/read/views.js.map +1 -1
  128. package/dist/surface-copy.d.ts +19 -0
  129. package/dist/surface-copy.js +32 -0
  130. package/dist/surface-copy.js.map +1 -1
  131. package/dist/sync-health.d.ts +78 -0
  132. package/dist/sync-health.js +312 -0
  133. package/dist/sync-health.js.map +1 -0
  134. package/dist/write/accessibility-probe.d.ts +12 -0
  135. package/dist/write/accessibility-probe.js +63 -0
  136. package/dist/write/accessibility-probe.js.map +1 -0
  137. package/dist/write/automation-probe.d.ts +7 -0
  138. package/dist/write/automation-probe.js +8 -1
  139. package/dist/write/automation-probe.js.map +1 -1
  140. package/dist/write/batch.js +4 -2
  141. package/dist/write/batch.js.map +1 -1
  142. package/dist/write/capabilities.d.ts +8 -0
  143. package/dist/write/capabilities.js +13 -6
  144. package/dist/write/capabilities.js.map +1 -1
  145. package/dist/write/commands.d.ts +2 -0
  146. package/dist/write/commands.js +562 -48
  147. package/dist/write/commands.js.map +1 -1
  148. package/dist/write/edit-checklist.js +3 -2
  149. package/dist/write/edit-checklist.js.map +1 -1
  150. package/dist/write/guards.d.ts +2 -7
  151. package/dist/write/guards.js +64 -6
  152. package/dist/write/guards.js.map +1 -1
  153. package/dist/write/heading.js +2 -0
  154. package/dist/write/heading.js.map +1 -1
  155. package/dist/write/lock.d.ts +40 -2
  156. package/dist/write/lock.js +91 -14
  157. package/dist/write/lock.js.map +1 -1
  158. package/dist/write/make-repeating-project.d.ts +4 -0
  159. package/dist/write/make-repeating-project.js +253 -0
  160. package/dist/write/make-repeating-project.js.map +1 -0
  161. package/dist/write/operations.d.ts +145 -1
  162. package/dist/write/operations.js +48 -0
  163. package/dist/write/operations.js.map +1 -1
  164. package/dist/write/pipeline.d.ts +19 -0
  165. package/dist/write/pipeline.js +109 -26
  166. package/dist/write/pipeline.js.map +1 -1
  167. package/dist/write/pre-state.d.ts +68 -3
  168. package/dist/write/pre-state.js +102 -3
  169. package/dist/write/pre-state.js.map +1 -1
  170. package/dist/write/reopen.js +3 -2
  171. package/dist/write/reopen.js.map +1 -1
  172. package/dist/write/reorder.js +27 -17
  173. package/dist/write/reorder.js.map +1 -1
  174. package/dist/write/repeat-rule.d.ts +22 -0
  175. package/dist/write/repeat-rule.js +258 -0
  176. package/dist/write/repeat-rule.js.map +1 -0
  177. package/dist/write/reversibility.js +49 -0
  178. package/dist/write/reversibility.js.map +1 -1
  179. package/dist/write/tag-refs.d.ts +47 -0
  180. package/dist/write/tag-refs.js +126 -0
  181. package/dist/write/tag-refs.js.map +1 -0
  182. package/dist/write/undo.d.ts +74 -5
  183. package/dist/write/undo.js +493 -80
  184. package/dist/write/undo.js.map +1 -1
  185. package/dist/write/vectors/registry.d.ts +17 -1
  186. package/dist/write/vectors/registry.js +18 -2
  187. package/dist/write/vectors/registry.js.map +1 -1
  188. package/dist/write/vectors/types.d.ts +119 -3
  189. package/dist/write/vectors/ui-certification.d.ts +48 -0
  190. package/dist/write/vectors/ui-certification.js +46 -0
  191. package/dist/write/vectors/ui-certification.js.map +1 -0
  192. package/dist/write/vectors/ui-drag.d.ts +188 -0
  193. package/dist/write/vectors/ui-drag.js +1095 -0
  194. package/dist/write/vectors/ui-drag.js.map +1 -0
  195. package/dist/write/vectors/ui-recipes.d.ts +70 -0
  196. package/dist/write/vectors/ui-recipes.js +558 -0
  197. package/dist/write/vectors/ui-recipes.js.map +1 -0
  198. package/dist/write/vectors/ui.d.ts +137 -0
  199. package/dist/write/vectors/ui.js +656 -0
  200. package/dist/write/vectors/ui.js.map +1 -0
  201. package/dist/write/verify/delta.d.ts +31 -3
  202. package/dist/write/verify/delta.js +22 -6
  203. package/dist/write/verify/delta.js.map +1 -1
  204. package/dist/write/verify/poller.js +1 -0
  205. package/dist/write/verify/poller.js.map +1 -1
  206. package/package.json +1 -1
  207. package/dist/cli/exit-codes.d.ts +0 -26
  208. package/dist/cli/exit-codes.js +0 -26
  209. package/dist/cli/exit-codes.js.map +0 -1
  210. package/dist/cli/output.d.ts +0 -42
  211. package/dist/cli/output.js +0 -16
  212. package/dist/cli/output.js.map +0 -1
  213. package/dist/read/pagination.d.ts +0 -104
  214. package/dist/read/pagination.js.map +0 -1
@@ -1,8 +1,11 @@
1
1
  import { decodeReminderTime, encodeReminderTime, reminderUrlToken, } from "../model/dates.js";
2
2
  import { byUuid } from "../read/detail.js";
3
- import { childTagTitles, computeReorderPre, emptyPreState, loadTarget, missingTagTitles, projectChildren, projectStatus, resolveArea, resolveHeading, resolveProject, resolveTag, trashedCount, } from "./pre-state.js";
3
+ import { childTagTitles, classifyProjectRepeat, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
4
+ import { resolveTagRefs } from "./tag-refs.js";
4
5
  import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
6
+ import { assertRepeatRule } from "./repeat-rule.js";
5
7
  import { escapeAppleScript } from "./vectors/applescript.js";
8
+ import { areaReorderSidebarRecipe, convertToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
6
9
  // ------------------------------------------------------------------ helpers
7
10
  function thingsUrl(command, params, token) {
8
11
  const parts = [];
@@ -99,6 +102,18 @@ function whenAssertions(when, todayIso) {
99
102
  function sortedTags(tags) {
100
103
  return [...tags].toSorted();
101
104
  }
105
+ /**
106
+ * Resolve tag refs (title or `parent/child` path — names only) into pre-state:
107
+ * the leaf titles to apply, plus the unknown (`missingTags`) refs the
108
+ * H-UNKNOWN-TAG guard refuses on. Tags apply BY NAME through the app's own
109
+ * vector, so the app resolves the name (duplicate names resolve app-side, as
110
+ * in the GUI) — the resolver is a pure existence check.
111
+ */
112
+ function applyTagRefs(db, pre, tags) {
113
+ const res = resolveTagRefs(db, tags);
114
+ pre.missingTags = res.missing;
115
+ pre.resolvedTagTitles = res.titles;
116
+ }
102
117
  /** Round-trip normalization: "6:5"-style inputs → canonical "06:05". */
103
118
  function normalizeReminder(time) {
104
119
  return decodeReminderTime(encodeReminderTime(time)) ?? time;
@@ -115,6 +130,17 @@ function whenWithReminder(when, reminder) {
115
130
  function containerGiven(ref) {
116
131
  return ref !== undefined && (ref.uuid !== undefined || ref.title !== undefined);
117
132
  }
133
+ /**
134
+ * The create-probe title for the duplicate / identity-replacement ops, whose
135
+ * new row inherits the pre-read target's title. Non-heading targets only (the
136
+ * heading-convert op uses the raw title, headings included); "" when the target
137
+ * did not resolve. Shared by the pre-read same-title capture and the delta so
138
+ * the excluded set and the probe title cannot drift apart.
139
+ */
140
+ function nonHeadingTitle(pre) {
141
+ const target = pre.target;
142
+ return target !== null && target.type !== "heading" ? target.title : "";
143
+ }
118
144
  // ----------------------------------------------------------------- commands
119
145
  const todoAdd = {
120
146
  op: "todo.add",
@@ -139,7 +165,8 @@ const todoAdd = {
139
165
  if (containerGiven(params.area))
140
166
  pre.destArea = resolveArea(db, params.area);
141
167
  if (params.tags !== undefined)
142
- pre.missingTags = missingTagTitles(db, params.tags);
168
+ applyTagRefs(db, pre, params.tags);
169
+ pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "to-do");
143
170
  return pre;
144
171
  },
145
172
  expectedDelta(pre, params, ctx) {
@@ -153,8 +180,9 @@ const todoAdd = {
153
180
  }
154
181
  if (params.deadline !== undefined)
155
182
  assert.push({ field: "deadline", equals: params.deadline });
156
- if (params.tags !== undefined)
157
- assert.push({ field: "tags", equals: sortedTags(params.tags) });
183
+ if (params.tags !== undefined) {
184
+ assert.push({ field: "tags", equals: sortedTags(pre.resolvedTagTitles) });
185
+ }
158
186
  if (params.checklistItems !== undefined) {
159
187
  assert.push({ field: "checklistTitles", equals: params.checklistItems });
160
188
  }
@@ -171,7 +199,12 @@ const todoAdd = {
171
199
  assert.push({ field: "area.uuid", equals: area.uuid });
172
200
  return {
173
201
  mode: "create",
174
- probe: { title: params.title, type: "to-do", sinceEpoch: ctx.nowEpoch - 2 },
202
+ probe: {
203
+ title: params.title,
204
+ type: "to-do",
205
+ sinceEpoch: ctx.nowEpoch - 2,
206
+ excludeUuids: pre.sameTitleUuids,
207
+ },
175
208
  assert,
176
209
  };
177
210
  },
@@ -184,7 +217,7 @@ const todoAdd = {
184
217
  notes: params.notes,
185
218
  when: params.when === undefined ? undefined : whenWithReminder(params.when, params.reminder),
186
219
  deadline: params.deadline,
187
- tags: params.tags?.join(","),
220
+ tags: params.tags === undefined ? undefined : pre.resolvedTagTitles.join(","),
188
221
  "checklist-items": params.checklistItems?.join("\n"),
189
222
  "list-id": container?.uuid,
190
223
  heading: pre.destHeading?.resolved?.title,
@@ -400,21 +433,22 @@ const todoSetTags = {
400
433
  preRead(db, params) {
401
434
  const pre = emptyPreState();
402
435
  pre.target = loadTarget(db, params.uuid);
403
- pre.missingTags = missingTagTitles(db, params.tags);
436
+ applyTagRefs(db, pre, params.tags);
404
437
  return pre;
405
438
  },
406
- expectedDelta(_pre, params) {
439
+ expectedDelta(pre, params) {
407
440
  return {
408
441
  mode: "update",
409
442
  uuid: params.uuid,
410
- assert: [{ field: "tags", equals: sortedTags(params.tags) }],
443
+ assert: [{ field: "tags", equals: sortedTags(pre.resolvedTagTitles) }],
411
444
  };
412
445
  },
413
- compile(params, vector, _pre, ctx) {
446
+ compile(params, vector, pre, ctx) {
447
+ const applied = pre.resolvedTagTitles;
414
448
  if (vector === "url-scheme") {
415
- return thingsUrl("update", { id: params.uuid, tags: params.tags.join(",") }, ctx.token);
449
+ return thingsUrl("update", { id: params.uuid, tags: applied.join(",") }, ctx.token);
416
450
  }
417
- return osa(`set tag names of to do id ${q(params.uuid)} to ${q(params.tags.join(", "))}`);
451
+ return osa(`set tag names of to do id ${q(params.uuid)} to ${q(applied.join(", "))}`);
418
452
  },
419
453
  };
420
454
  /** Normalize the string | spec union; decide whether states force the json form. */
@@ -516,6 +550,7 @@ const projectAdd = {
516
550
  const pre = emptyPreState();
517
551
  if (containerGiven(params.area))
518
552
  pre.destArea = resolveArea(db, params.area);
553
+ pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "project");
519
554
  return pre;
520
555
  },
521
556
  expectedDelta(pre, params, ctx) {
@@ -531,7 +566,12 @@ const projectAdd = {
531
566
  assert.push({ field: "area.uuid", equals: area.uuid });
532
567
  return {
533
568
  mode: "create",
534
- probe: { title: params.title, type: "project", sinceEpoch: ctx.nowEpoch - 2 },
569
+ probe: {
570
+ title: params.title,
571
+ type: "project",
572
+ sinceEpoch: ctx.nowEpoch - 2,
573
+ excludeUuids: pre.sameTitleUuids,
574
+ },
535
575
  assert,
536
576
  };
537
577
  },
@@ -602,24 +642,25 @@ const projectSetTags = {
602
642
  preRead(db, params) {
603
643
  const pre = emptyPreState();
604
644
  pre.target = loadTarget(db, params.uuid);
605
- pre.missingTags = missingTagTitles(db, params.tags);
645
+ applyTagRefs(db, pre, params.tags);
606
646
  return pre;
607
647
  },
608
- expectedDelta(_pre, params) {
648
+ expectedDelta(pre, params) {
609
649
  return {
610
650
  mode: "update",
611
651
  uuid: params.uuid,
612
- assert: [{ field: "tags", equals: sortedTags(params.tags) }],
652
+ assert: [{ field: "tags", equals: sortedTags(pre.resolvedTagTitles) }],
613
653
  };
614
654
  },
615
- compile(params, vector, _pre, ctx) {
655
+ compile(params, vector, pre, ctx) {
616
656
  // Both vectors validated on projects (A1 URL, A2 AppleScript). Full
617
657
  // replacement semantics mirror todo.set-tags; unknown tags are guarded
618
658
  // pre-write (the app silently drops them).
659
+ const applied = pre.resolvedTagTitles;
619
660
  if (vector === "url-scheme") {
620
- return thingsUrl("update-project", { id: params.uuid, tags: params.tags.join(",") }, ctx.token);
661
+ return thingsUrl("update-project", { id: params.uuid, tags: applied.join(",") }, ctx.token);
621
662
  }
622
- return osa(`set tag names of project id ${q(params.uuid)} to ${q(params.tags.join(", "))}`);
663
+ return osa(`set tag names of project id ${q(params.uuid)} to ${q(applied.join(", "))}`);
623
664
  },
624
665
  };
625
666
  const projectMove = {
@@ -695,17 +736,24 @@ const projectDuplicate = {
695
736
  preRead(db, params) {
696
737
  const pre = emptyPreState();
697
738
  pre.target = loadTarget(db, params.uuid);
739
+ pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
698
740
  return pre;
699
741
  },
700
742
  expectedDelta(pre, _params, ctx) {
701
743
  // E17: the copy carries the title, notes AND children; discover it with
702
- // the create probe (fresh creationDate, same as the to-do path E07).
744
+ // the create probe (fresh creationDate, same as the to-do path E07). The
745
+ // ORIGINAL shares the copy's title, so it is among the excluded pre-existing
746
+ // rows — discovery lands on the copy, never the source.
703
747
  const target = pre.target;
704
- const title = target !== null && target.type !== "heading" ? target.title : "";
705
748
  const notes = target !== null && target.type !== "heading" ? target.notes : "";
706
749
  return {
707
750
  mode: "create",
708
- probe: { title, type: "project", sinceEpoch: ctx.nowEpoch - 2 },
751
+ probe: {
752
+ title: nonHeadingTitle(pre),
753
+ type: "project",
754
+ sinceEpoch: ctx.nowEpoch - 2,
755
+ excludeUuids: pre.sameTitleUuids,
756
+ },
709
757
  assert: [{ field: "notes", equals: notes }],
710
758
  };
711
759
  },
@@ -861,7 +909,7 @@ const areaAdd = {
861
909
  preRead(db, params) {
862
910
  const pre = emptyPreState();
863
911
  if (params.tags !== undefined)
864
- pre.missingTags = missingTagTitles(db, params.tags);
912
+ applyTagRefs(db, pre, params.tags);
865
913
  pre.existingEntityUuids = db.prepare("SELECT uuid FROM TMArea WHERE title = ? COLLATE NOCASE").all(params.title).map((r) => r.uuid);
866
914
  return pre;
867
915
  },
@@ -871,9 +919,13 @@ const areaAdd = {
871
919
  entity: "area",
872
920
  title: params.title,
873
921
  excludeUuids: pre.existingEntityUuids,
922
+ // Assert the landed tag set on the created area — the app silently
923
+ // drops unknowns (guarded pre-write), so the created row must carry
924
+ // exactly the resolved titles (parity with the other tag-set ops).
925
+ ...(params.tags !== undefined && { assertTags: sortedTags(pre.resolvedTagTitles) }),
874
926
  };
875
927
  },
876
- compile(params, vector) {
928
+ compile(params, vector, pre) {
877
929
  if (vector !== "applescript")
878
930
  unsupportedVector(this.op, vector);
879
931
  const make = `make new area with properties {name:${q(params.title)}}`;
@@ -881,7 +933,7 @@ const areaAdd = {
881
933
  return osa(make);
882
934
  const payload = `tell application "Things3"\n` +
883
935
  ` ${make}\n` +
884
- ` set tag names of area ${q(params.title)} to ${q(params.tags.join(", "))}\n` +
936
+ ` set tag names of area ${q(params.title)} to ${q(pre.resolvedTagTitles.join(", "))}\n` +
885
937
  `end tell`;
886
938
  return { vector: "applescript", kind: "osascript", payload, redactedPayload: payload };
887
939
  },
@@ -966,17 +1018,23 @@ const todoDuplicate = {
966
1018
  preRead(db, params) {
967
1019
  const pre = emptyPreState();
968
1020
  pre.target = loadTarget(db, params.uuid);
1021
+ pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
969
1022
  return pre;
970
1023
  },
971
1024
  expectedDelta(pre, _params, ctx) {
972
1025
  // The copy carries the same title/notes with a fresh uuid + creationDate
973
- // (E07) — discover it with the create probe, assert copy fidelity.
1026
+ // (E07) — discover it with the create probe, assert copy fidelity. The
1027
+ // ORIGINAL shares the title, so it is excluded as a pre-existing row.
974
1028
  const target = pre.target;
975
- const title = target !== null && target.type !== "heading" ? target.title : "";
976
1029
  const notes = target !== null && target.type !== "heading" ? target.notes : "";
977
1030
  return {
978
1031
  mode: "create",
979
- probe: { title, type: "to-do", sinceEpoch: ctx.nowEpoch - 2 },
1032
+ probe: {
1033
+ title: nonHeadingTitle(pre),
1034
+ type: "to-do",
1035
+ sinceEpoch: ctx.nowEpoch - 2,
1036
+ excludeUuids: pre.sameTitleUuids,
1037
+ },
980
1038
  assert: [{ field: "notes", equals: notes }],
981
1039
  };
982
1040
  },
@@ -997,15 +1055,16 @@ const areaUpdate = {
997
1055
  const pre = emptyPreState();
998
1056
  pre.entityTarget = resolveArea(db, { title: params.target, uuid: params.target });
999
1057
  if (params.tags !== undefined)
1000
- pre.missingTags = missingTagTitles(db, params.tags);
1058
+ applyTagRefs(db, pre, params.tags);
1001
1059
  return pre;
1002
1060
  },
1003
1061
  expectedDelta(pre, params) {
1004
1062
  const assert = [];
1005
1063
  if (params.title !== undefined)
1006
1064
  assert.push({ field: "title", equals: params.title });
1007
- if (params.tags !== undefined)
1008
- assert.push({ field: "tags", equals: sortedTags(params.tags) });
1065
+ if (params.tags !== undefined) {
1066
+ assert.push({ field: "tags", equals: sortedTags(pre.resolvedTagTitles) });
1067
+ }
1009
1068
  return {
1010
1069
  mode: "entity-updated",
1011
1070
  entity: "area",
@@ -1021,7 +1080,7 @@ const areaUpdate = {
1021
1080
  if (params.title !== undefined)
1022
1081
  lines.push(`set name of area id ${id} to ${q(params.title)}`);
1023
1082
  if (params.tags !== undefined) {
1024
- lines.push(`set tag names of area id ${id} to ${q(params.tags.join(", "))}`);
1083
+ lines.push(`set tag names of area id ${id} to ${q(pre.resolvedTagTitles.join(", "))}`);
1025
1084
  }
1026
1085
  if (lines.length === 1)
1027
1086
  return osa(lines[0]);
@@ -1180,6 +1239,18 @@ function asDateBlock(varName, iso) {
1180
1239
  `set day of ${varName} to ${d}`,
1181
1240
  ];
1182
1241
  }
1242
+ /**
1243
+ * Local noon -> UTC instant, so the stored timestamp decodes back to the
1244
+ * requested local DATE in every timezone (P4d: json attrs honored exactly).
1245
+ * WITHOUT milliseconds: the app's json date parser rejects fractional
1246
+ * seconds — a `.000Z` timestamp fails the whole command (error modal, no
1247
+ * write; caught live by the e2e 2026-07-09 — P4d's validated shape was
1248
+ * second-precision).
1249
+ */
1250
+ function utcNoon(iso) {
1251
+ const [y, m, d] = iso.split("-").map(Number);
1252
+ return new Date(y ?? 0, (m ?? 1) - 1, d ?? 1, 12, 0, 0).toISOString().replace(/\.\d{3}Z$/, "Z");
1253
+ }
1183
1254
  const todoBackdate = {
1184
1255
  op: "todo.backdate",
1185
1256
  hazards: ["H-UNKNOWN-DESTINATION", "H-BACKDATE-OPEN"],
@@ -1223,7 +1294,7 @@ const todoAddLogged = {
1223
1294
  throw new RangeError("creationDate must not be after completionDate");
1224
1295
  }
1225
1296
  const pre = emptyPreState();
1226
- pre.sameTitleUuids = db.prepare("SELECT uuid FROM TMTask WHERE title = ? AND type = 0").all(params.title).map((r) => r.uuid);
1297
+ pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "to-do");
1227
1298
  return pre;
1228
1299
  },
1229
1300
  expectedDelta(pre, params) {
@@ -1250,18 +1321,6 @@ const todoAddLogged = {
1250
1321
  compile(params, vector, _pre, ctx) {
1251
1322
  if (vector !== "url-scheme")
1252
1323
  unsupportedVector(this.op, vector);
1253
- // Local noon -> UTC instant, so the stored timestamp decodes back to the
1254
- // requested local DATE in every timezone (P4d: json attrs honored exactly).
1255
- // WITHOUT milliseconds: the app's json date parser rejects fractional
1256
- // seconds — a `.000Z` timestamp fails the whole command (error modal, no
1257
- // write; caught live by the e2e 2026-07-09 — P4d's validated shape was
1258
- // second-precision).
1259
- const utcNoon = (iso) => {
1260
- const [y, m, d] = iso.split("-").map(Number);
1261
- return new Date(y ?? 0, (m ?? 1) - 1, d ?? 1, 12, 0, 0)
1262
- .toISOString()
1263
- .replace(/\.\d{3}Z$/, "Z");
1264
- };
1265
1324
  const payload = JSON.stringify([
1266
1325
  {
1267
1326
  type: "to-do",
@@ -1395,15 +1454,21 @@ const headingCreate = {
1395
1454
  preRead(db, params) {
1396
1455
  const pre = emptyPreState();
1397
1456
  pre.destProject = resolveProject(db, params.project);
1457
+ pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "heading");
1398
1458
  return pre;
1399
1459
  },
1400
1460
  expectedDelta(pre, params, ctx) {
1401
1461
  const project = pre.destProject?.resolved;
1402
- // A new type=2 row with this title under the project; disambiguate
1403
- // duplicate titles by newest creationDate (findCreated orders DESC).
1462
+ // A new type=2 row with this title under the project; a pre-existing
1463
+ // same-title heading is excluded so discovery cannot bind to it.
1404
1464
  return {
1405
1465
  mode: "create",
1406
- probe: { title: params.title, type: "heading", sinceEpoch: ctx.nowEpoch - 2 },
1466
+ probe: {
1467
+ title: params.title,
1468
+ type: "heading",
1469
+ sinceEpoch: ctx.nowEpoch - 2,
1470
+ excludeUuids: pre.sameTitleUuids,
1471
+ },
1407
1472
  assert: project !== undefined && project !== null
1408
1473
  ? [{ field: "project.uuid", equals: project.uuid }]
1409
1474
  : [],
@@ -1453,6 +1518,443 @@ const todoClearDatedReminder = {
1453
1518
  });
1454
1519
  },
1455
1520
  };
1521
+ // ------------------------------------------------------- ui (GUI) vector
1522
+ /** A compiled Accessibility recipe as a CompiledInvocation (no secrets). */
1523
+ function uiDrive(recipe) {
1524
+ const rendered = `ui-drive ${recipe.op} on ${recipe.targetUuid}: ${recipe.steps
1525
+ .map((s) => s.label)
1526
+ .join(" → ")}`;
1527
+ return { vector: "ui", kind: "ui-drive", payload: rendered, redactedPayload: rendered, recipe };
1528
+ }
1529
+ // Re-exported so make-repeating-project.ts and existing callers keep importing
1530
+ // the validator from commands.ts; the implementation + the full combination
1531
+ // matrix live in repeat-rule.ts.
1532
+ export { assertRepeatRule };
1533
+ /**
1534
+ * The extended-vocabulary fields of a rule as a recipe `extras` bag, including
1535
+ * ONLY the keys that are present (exactOptionalPropertyTypes: never set an
1536
+ * optional field to undefined). A bare `{ uuid, frequency, interval }` yields
1537
+ * `{}`, so the recipe drives exactly the certified two-control path.
1538
+ */
1539
+ function ruleExtras(params) {
1540
+ return {
1541
+ ...(params.afterCompletion !== undefined && { afterCompletion: params.afterCompletion }),
1542
+ ...(params.weekdays !== undefined && { weekdays: params.weekdays }),
1543
+ ...(params.monthly !== undefined && { monthly: params.monthly }),
1544
+ ...(params.yearly !== undefined && { yearly: params.yearly }),
1545
+ ...(params.ends !== undefined && { ends: params.ends }),
1546
+ ...(params.reminder !== undefined && { reminder: params.reminder }),
1547
+ ...(params.deadline !== undefined && { deadline: params.deadline }),
1548
+ ...(params.startDaysEarlier !== undefined && { startDaysEarlier: params.startDaysEarlier }),
1549
+ };
1550
+ }
1551
+ /** ui ops all guard existence/type + the H-UI-DRIVE acknowledgement. */
1552
+ const UI_HAZARDS = ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"];
1553
+ const todoMakeRepeating = {
1554
+ op: "todo.make-repeating",
1555
+ hazards: UI_HAZARDS,
1556
+ preRead(db, params) {
1557
+ assertRepeatRule(params);
1558
+ const pre = emptyPreState();
1559
+ pre.target = loadTarget(db, params.uuid);
1560
+ pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
1561
+ return pre;
1562
+ },
1563
+ expectedDelta(pre, _params, ctx) {
1564
+ // Identity REPLACEMENT (UI2-a): the original to-do uuid dies; a NEW
1565
+ // template row (type=0 with a recurrence rule) is born alongside a spawned
1566
+ // instance sharing its title. Discover the template with the create probe,
1567
+ // excluding the pre-existing same-title rows (incl. the dying original), and
1568
+ // pick the template (not the spawned instance) by asserting it IS one.
1569
+ return {
1570
+ mode: "create",
1571
+ probe: {
1572
+ title: nonHeadingTitle(pre),
1573
+ type: "to-do",
1574
+ sinceEpoch: ctx.nowEpoch - 2,
1575
+ excludeUuids: pre.sameTitleUuids,
1576
+ },
1577
+ assert: [{ field: "repeating.isTemplate", equals: true }],
1578
+ };
1579
+ },
1580
+ compile(params, vector) {
1581
+ if (vector !== "ui")
1582
+ unsupportedVector(this.op, vector);
1583
+ return uiDrive(makeRepeatingRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
1584
+ },
1585
+ };
1586
+ const todoRescheduleRepeat = {
1587
+ op: "todo.reschedule-repeat",
1588
+ hazards: UI_HAZARDS,
1589
+ preRead(db, params) {
1590
+ assertRepeatRule(params);
1591
+ const pre = emptyPreState();
1592
+ pre.target = loadTarget(db, params.uuid);
1593
+ return pre;
1594
+ },
1595
+ expectedDelta(_pre, params) {
1596
+ // Identity PRESERVED (UI2-b): the same template uuid, rule mutated in
1597
+ // place. Assert the decoded rule's frequency + interval; ALSO capture the
1598
+ // whole prior rule (+ deadline flag) so the undo can re-drive it faithfully.
1599
+ return {
1600
+ mode: "update",
1601
+ uuid: params.uuid,
1602
+ assert: [
1603
+ { field: "repeating.rule.unit", equals: params.frequency },
1604
+ { field: "repeating.rule.interval", equals: params.interval },
1605
+ ],
1606
+ capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
1607
+ };
1608
+ },
1609
+ compile(params, vector) {
1610
+ if (vector !== "ui")
1611
+ unsupportedVector(this.op, vector);
1612
+ return uiDrive(rescheduleRepeatRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
1613
+ },
1614
+ };
1615
+ const todoPauseRepeat = {
1616
+ op: "todo.pause-repeat",
1617
+ hazards: UI_HAZARDS,
1618
+ preRead(db, params) {
1619
+ const pre = emptyPreState();
1620
+ pre.target = loadTarget(db, params.uuid);
1621
+ return pre;
1622
+ },
1623
+ expectedDelta(_pre, params) {
1624
+ // Pause (UI2-c): rt1_instanceCreationPaused → 1; identity preserved.
1625
+ return {
1626
+ mode: "update",
1627
+ uuid: params.uuid,
1628
+ assert: [{ field: "repeating.paused", equals: true }],
1629
+ };
1630
+ },
1631
+ compile(params, vector) {
1632
+ if (vector !== "ui")
1633
+ unsupportedVector(this.op, vector);
1634
+ return uiDrive(pauseRepeatRecipe(params.uuid));
1635
+ },
1636
+ };
1637
+ const todoResumeRepeat = {
1638
+ op: "todo.resume-repeat",
1639
+ hazards: UI_HAZARDS,
1640
+ preRead(db, params) {
1641
+ const pre = emptyPreState();
1642
+ pre.target = loadTarget(db, params.uuid);
1643
+ return pre;
1644
+ },
1645
+ expectedDelta(_pre, params) {
1646
+ // Resume (UI2-c): rt1_instanceCreationPaused → 0; identity preserved.
1647
+ return {
1648
+ mode: "update",
1649
+ uuid: params.uuid,
1650
+ assert: [{ field: "repeating.paused", equals: false }],
1651
+ };
1652
+ },
1653
+ compile(params, vector) {
1654
+ if (vector !== "ui")
1655
+ unsupportedVector(this.op, vector);
1656
+ return uiDrive(resumeRepeatRecipe(params.uuid));
1657
+ },
1658
+ };
1659
+ // ------------------------------------------------- repeating-PROJECT ops
1660
+ // The project analogs of reschedule/pause/resume. Same recurrence codec and
1661
+ // Repeat dialog as the to-do ops (identical DB deltas), reached through the
1662
+ // project view's always-visible repeat bar (UIC2). No project.stop-repeat is
1663
+ // built — the project Stop then selecting the demoted project crashes Things
1664
+ // (CRASH1 / oddities §7 C5).
1665
+ const projectRescheduleRepeat = {
1666
+ op: "project.reschedule-repeat",
1667
+ hazards: UI_HAZARDS,
1668
+ preRead(db, params) {
1669
+ assertRepeatRule(params);
1670
+ const pre = emptyPreState();
1671
+ pre.target = loadTarget(db, params.uuid);
1672
+ return pre;
1673
+ },
1674
+ expectedDelta(_pre, params) {
1675
+ // Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
1676
+ // capture the prior rule (+ deadline flag) for the faithful undo.
1677
+ return {
1678
+ mode: "update",
1679
+ uuid: params.uuid,
1680
+ assert: [
1681
+ { field: "repeating.rule.unit", equals: params.frequency },
1682
+ { field: "repeating.rule.interval", equals: params.interval },
1683
+ ],
1684
+ capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
1685
+ };
1686
+ },
1687
+ compile(params, vector) {
1688
+ if (vector !== "ui")
1689
+ unsupportedVector(this.op, vector);
1690
+ return uiDrive(projectRescheduleRepeatRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
1691
+ },
1692
+ };
1693
+ const projectPauseRepeat = {
1694
+ op: "project.pause-repeat",
1695
+ hazards: UI_HAZARDS,
1696
+ preRead(db, params) {
1697
+ const pre = emptyPreState();
1698
+ pre.target = loadTarget(db, params.uuid);
1699
+ return pre;
1700
+ },
1701
+ expectedDelta(_pre, params) {
1702
+ // Pause (UIC2-a): rt1_instanceCreationPaused → 1; identity preserved.
1703
+ return {
1704
+ mode: "update",
1705
+ uuid: params.uuid,
1706
+ assert: [{ field: "repeating.paused", equals: true }],
1707
+ };
1708
+ },
1709
+ compile(params, vector) {
1710
+ if (vector !== "ui")
1711
+ unsupportedVector(this.op, vector);
1712
+ return uiDrive(projectPauseRepeatRecipe(params.uuid));
1713
+ },
1714
+ };
1715
+ const projectResumeRepeat = {
1716
+ op: "project.resume-repeat",
1717
+ hazards: UI_HAZARDS,
1718
+ preRead(db, params) {
1719
+ const pre = emptyPreState();
1720
+ pre.target = loadTarget(db, params.uuid);
1721
+ return pre;
1722
+ },
1723
+ expectedDelta(_pre, params) {
1724
+ // Resume (UIC2-a): rt1_instanceCreationPaused → 0; identity preserved.
1725
+ return {
1726
+ mode: "update",
1727
+ uuid: params.uuid,
1728
+ assert: [{ field: "repeating.paused", equals: false }],
1729
+ };
1730
+ },
1731
+ compile(params, vector) {
1732
+ if (vector !== "ui")
1733
+ unsupportedVector(this.op, vector);
1734
+ return uiDrive(projectResumeRepeatRecipe(params.uuid));
1735
+ },
1736
+ };
1737
+ // project.make-repeating: the pure-AX row-selection op (UIC4). The command
1738
+ // spec drives the area / someday cases; the area-less-anytime coercion is done
1739
+ // by the runMakeRepeatingProject orchestrator BEFORE this spec sees the target,
1740
+ // so by drive time the taxonomy is always area or someday. A DIRECT dispatch on
1741
+ // an anytime project is refused by H-PROJECT-REPEAT (with the orchestrator as
1742
+ // the remediation).
1743
+ const projectMakeRepeating = {
1744
+ op: "project.make-repeating",
1745
+ hazards: ["H-UNKNOWN-DESTINATION", "H-PROJECT-REPEAT", "H-UI-DRIVE"],
1746
+ preRead(db, params) {
1747
+ assertRepeatRule(params);
1748
+ const pre = emptyPreState();
1749
+ pre.target = loadTarget(db, params.uuid);
1750
+ pre.projectRepeat = classifyProjectRepeat(db, pre.target);
1751
+ pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
1752
+ return pre;
1753
+ },
1754
+ expectedDelta(pre, _params, ctx) {
1755
+ // Identity REPLACEMENT (UIC4-b): the original project uuid dies; a NEW
1756
+ // template project (with a recurrence rule) is born alongside a spawned
1757
+ // instance sharing its title. Pick the TEMPLATE by asserting it IS one
1758
+ // (area preserved), excluding the pre-existing same-title rows so discovery
1759
+ // cannot bind to the spawned instance or the dying original.
1760
+ return {
1761
+ mode: "create",
1762
+ probe: {
1763
+ title: nonHeadingTitle(pre),
1764
+ type: "project",
1765
+ sinceEpoch: ctx.nowEpoch - 2,
1766
+ excludeUuids: pre.sameTitleUuids,
1767
+ },
1768
+ assert: [{ field: "repeating.isTemplate", equals: true }],
1769
+ };
1770
+ },
1771
+ compile(params, vector, pre) {
1772
+ if (vector !== "ui")
1773
+ unsupportedVector(this.op, vector);
1774
+ const tax = pre.projectRepeat;
1775
+ if (tax === null || tax.kind === "refuse" || tax.kind === "anytime") {
1776
+ // Unreachable in practice: the guards block refuse/anytime before compile.
1777
+ throw new Error("project.make-repeating: no selectable-row taxonomy resolved (guard bypassed?)");
1778
+ }
1779
+ return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
1780
+ },
1781
+ };
1782
+ // project.create-repeating is delivered by the runCreateRepeatingProject
1783
+ // orchestrator (project.add THEN project.make-repeating); it has no single
1784
+ // atomic surface and is never dispatched directly through the pipeline.
1785
+ const CREATE_REPEATING_ORCHESTRATED = "project.create-repeating is delivered by the runCreateRepeatingProject orchestrator (create " +
1786
+ "the project, then promote it to a repeating series); it has no atomic surface and is never " +
1787
+ "dispatched directly through the pipeline";
1788
+ const projectCreateRepeating = {
1789
+ op: "project.create-repeating",
1790
+ hazards: [],
1791
+ preRead() {
1792
+ return emptyPreState();
1793
+ },
1794
+ expectedDelta() {
1795
+ throw new Error(CREATE_REPEATING_ORCHESTRATED);
1796
+ },
1797
+ compile() {
1798
+ throw new Error(CREATE_REPEATING_ORCHESTRATED);
1799
+ },
1800
+ };
1801
+ const todoConvertToProject = {
1802
+ op: "todo.convert-to-project",
1803
+ hazards: UI_HAZARDS,
1804
+ preRead(db, params) {
1805
+ const pre = emptyPreState();
1806
+ pre.target = loadTarget(db, params.uuid);
1807
+ // The new row is a PROJECT (type=1) — exclude pre-existing same-title
1808
+ // projects, not the source to-do (a different type the probe never sees).
1809
+ pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
1810
+ return pre;
1811
+ },
1812
+ expectedDelta(pre, _params, ctx) {
1813
+ // Identity REPLACEMENT (UI2-d): the to-do uuid dies; a NEW type=1 project
1814
+ // is born, notes preserved. Discover it (its uuid is returned).
1815
+ const target = pre.target;
1816
+ const notes = target !== null && target.type !== "heading" ? target.notes : "";
1817
+ return {
1818
+ mode: "create",
1819
+ probe: {
1820
+ title: nonHeadingTitle(pre),
1821
+ type: "project",
1822
+ sinceEpoch: ctx.nowEpoch - 2,
1823
+ excludeUuids: pre.sameTitleUuids,
1824
+ },
1825
+ assert: [{ field: "notes", equals: notes }],
1826
+ };
1827
+ },
1828
+ compile(params, vector) {
1829
+ if (vector !== "ui")
1830
+ unsupportedVector(this.op, vector);
1831
+ return uiDrive(convertToProjectRecipe("todo.convert-to-project", params.uuid));
1832
+ },
1833
+ };
1834
+ const headingConvertToProject = {
1835
+ op: "heading.convert-to-project",
1836
+ hazards: UI_HAZARDS,
1837
+ preRead(db, params) {
1838
+ const pre = emptyPreState();
1839
+ pre.target = loadTarget(db, params.uuid);
1840
+ // The new row is a PROJECT (type=1) carrying the former heading's title;
1841
+ // exclude pre-existing same-title projects. The heading uses its raw title
1842
+ // (headings are not covered by nonHeadingTitle).
1843
+ const title = pre.target !== null ? pre.target.title : "";
1844
+ pre.sameTitleUuids = sameTitleTaskUuids(db, title, "project");
1845
+ return pre;
1846
+ },
1847
+ expectedDelta(pre, _params, ctx) {
1848
+ // Identity REPLACEMENT (UI2-d): the heading uuid dies; a NEW type=1
1849
+ // project is born (promoted into the parent project's area, children
1850
+ // reparented). Discover the new project by its (former heading) title.
1851
+ const target = pre.target;
1852
+ const title = target !== null ? target.title : "";
1853
+ return {
1854
+ mode: "create",
1855
+ probe: {
1856
+ title,
1857
+ type: "project",
1858
+ sinceEpoch: ctx.nowEpoch - 2,
1859
+ excludeUuids: pre.sameTitleUuids,
1860
+ },
1861
+ assert: [],
1862
+ };
1863
+ },
1864
+ compile(params, vector) {
1865
+ if (vector !== "ui")
1866
+ unsupportedVector(this.op, vector);
1867
+ return uiDrive(convertToProjectRecipe("heading.convert-to-project", params.uuid));
1868
+ },
1869
+ };
1870
+ // -------------------------------------------------- sidebar AREA reorder
1871
+ /** The compile-time placement for area.reorder (resolved refs). */
1872
+ function sidebarPlacementOf(params, pre) {
1873
+ const ref = pre.destArea?.resolved;
1874
+ if (params.before !== undefined && ref != null) {
1875
+ return { kind: "before", uuid: ref.uuid, title: ref.title };
1876
+ }
1877
+ if (params.after !== undefined && ref != null) {
1878
+ return { kind: "after", uuid: ref.uuid, title: ref.title };
1879
+ }
1880
+ return { kind: params.position === "first" ? "first" : "last" };
1881
+ }
1882
+ const areaReorderSidebar = {
1883
+ op: "area.reorder",
1884
+ hazards: ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"],
1885
+ preRead(db, params) {
1886
+ const destinations = [params.before, params.after, params.position].filter((d) => d !== undefined);
1887
+ if (destinations.length !== 1) {
1888
+ throw new RangeError("pass exactly one destination: before, after, or position (first | last)");
1889
+ }
1890
+ if (params.position !== undefined &&
1891
+ params.position !== "first" &&
1892
+ params.position !== "last") {
1893
+ throw new RangeError(`invalid position "${params.position}" — expected first | last`);
1894
+ }
1895
+ const pre = emptyPreState();
1896
+ pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
1897
+ const ref = params.before ?? params.after;
1898
+ if (ref !== undefined)
1899
+ pre.destArea = resolveArea(db, { uuid: ref, title: ref });
1900
+ const target = pre.entityTarget.resolved;
1901
+ const dest = pre.destArea?.resolved;
1902
+ if (target != null && dest != null && target.uuid === dest.uuid) {
1903
+ throw new RangeError("the destination area is the area being moved");
1904
+ }
1905
+ // The sidebar drag addresses rows by their VISIBLE NAME — a duplicated
1906
+ // area title makes the row ambiguous, so refuse up front.
1907
+ for (const area of [target, dest]) {
1908
+ if (area == null)
1909
+ continue;
1910
+ const dup = db
1911
+ .prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
1912
+ .get(area.title);
1913
+ if (dup.n > 1) {
1914
+ throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas — the sidebar move addresses ` +
1915
+ "areas by their visible name; rename one first");
1916
+ }
1917
+ }
1918
+ pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
1919
+ return pre;
1920
+ },
1921
+ expectedDelta(pre, params) {
1922
+ const target = pre.entityTarget?.resolved?.uuid ?? "";
1923
+ const order = (pre.areaOrder ?? []).filter((u) => u !== target);
1924
+ const refUuid = pre.destArea?.resolved?.uuid;
1925
+ // Assert the implicated RELATIVE pair (index VALUES are never asserted —
1926
+ // a drag may renumber a neighbor, AXDRAG1-a); capture the FULL pre-order
1927
+ // for undo.
1928
+ let sequence;
1929
+ if (params.before !== undefined && refUuid !== undefined) {
1930
+ sequence = [target, refUuid];
1931
+ }
1932
+ else if (params.after !== undefined && refUuid !== undefined) {
1933
+ sequence = [refUuid, target];
1934
+ }
1935
+ else if (params.position === "first") {
1936
+ const first = order[0];
1937
+ sequence = first === undefined ? [target] : [target, first];
1938
+ }
1939
+ else {
1940
+ const last = order.at(-1);
1941
+ sequence = last === undefined ? [target] : [last, target];
1942
+ }
1943
+ return {
1944
+ mode: "ordering",
1945
+ key: "area-index",
1946
+ sequence,
1947
+ capture: pre.areaOrder ?? sequence,
1948
+ subject: target,
1949
+ };
1950
+ },
1951
+ compile(params, vector, pre) {
1952
+ if (vector !== "ui")
1953
+ unsupportedVector(this.op, vector);
1954
+ const target = pre.entityTarget?.resolved;
1955
+ return uiDrive(areaReorderSidebarRecipe({ uuid: target?.uuid ?? "", title: target?.title ?? "" }, sidebarPlacementOf(params, pre)));
1956
+ },
1957
+ };
1456
1958
  const trashEmpty = {
1457
1959
  op: "trash.empty",
1458
1960
  hazards: ["H-PERMANENT-DELETE"],
@@ -1508,5 +2010,17 @@ export const COMMANDS = {
1508
2010
  "heading.unarchive": headingUnarchive,
1509
2011
  "heading.create": headingCreate,
1510
2012
  "todo.clear-dated-reminder": todoClearDatedReminder,
2013
+ "todo.make-repeating": todoMakeRepeating,
2014
+ "todo.reschedule-repeat": todoRescheduleRepeat,
2015
+ "todo.pause-repeat": todoPauseRepeat,
2016
+ "todo.resume-repeat": todoResumeRepeat,
2017
+ "todo.convert-to-project": todoConvertToProject,
2018
+ "heading.convert-to-project": headingConvertToProject,
2019
+ "project.reschedule-repeat": projectRescheduleRepeat,
2020
+ "project.pause-repeat": projectPauseRepeat,
2021
+ "project.resume-repeat": projectResumeRepeat,
2022
+ "area.reorder": areaReorderSidebar,
2023
+ "project.make-repeating": projectMakeRepeating,
2024
+ "project.create-repeating": projectCreateRepeating,
1511
2025
  };
1512
2026
  //# sourceMappingURL=commands.js.map