things-api 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +50 -13
  2. package/dist/audit/schema.d.ts +7 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/clock.d.ts +8 -0
  5. package/dist/cli/clock.js +24 -0
  6. package/dist/cli/clock.js.map +1 -0
  7. package/dist/cli/commands/area.d.ts +18 -11
  8. package/dist/cli/commands/area.js +61 -74
  9. package/dist/cli/commands/area.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +1 -2
  11. package/dist/cli/commands/doctor.js +11 -1
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/install-skill.d.ts +60 -0
  14. package/dist/cli/commands/install-skill.js +256 -0
  15. package/dist/cli/commands/install-skill.js.map +1 -0
  16. package/dist/cli/commands/mcp.js +9 -0
  17. package/dist/cli/commands/mcp.js.map +1 -1
  18. package/dist/cli/commands/project.d.ts +1 -0
  19. package/dist/cli/commands/project.js +27 -34
  20. package/dist/cli/commands/project.js.map +1 -1
  21. package/dist/cli/commands/reads.d.ts +15 -2
  22. package/dist/cli/commands/reads.js +144 -44
  23. package/dist/cli/commands/reads.js.map +1 -1
  24. package/dist/cli/commands/setup.js +7 -2
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/show.js +38 -14
  27. package/dist/cli/commands/show.js.map +1 -1
  28. package/dist/cli/commands/todo.js +15 -9
  29. package/dist/cli/commands/todo.js.map +1 -1
  30. package/dist/cli/commands/writes.js +797 -174
  31. package/dist/cli/commands/writes.js.map +1 -1
  32. package/dist/cli/did-you-mean.d.ts +14 -3
  33. package/dist/cli/did-you-mean.js +26 -3
  34. package/dist/cli/did-you-mean.js.map +1 -1
  35. package/dist/cli/glyphs.d.ts +32 -2
  36. package/dist/cli/glyphs.js +83 -12
  37. package/dist/cli/glyphs.js.map +1 -1
  38. package/dist/cli/help.js +121 -16
  39. package/dist/cli/help.js.map +1 -1
  40. package/dist/cli/main.js +33 -2
  41. package/dist/cli/main.js.map +1 -1
  42. package/dist/cli/move-hint.d.ts +46 -0
  43. package/dist/cli/move-hint.js +196 -0
  44. package/dist/cli/move-hint.js.map +1 -0
  45. package/dist/cli/period.d.ts +2 -8
  46. package/dist/cli/period.js +44 -30
  47. package/dist/cli/period.js.map +1 -1
  48. package/dist/cli/read-driver.d.ts +43 -5
  49. package/dist/cli/read-driver.js +91 -15
  50. package/dist/cli/read-driver.js.map +1 -1
  51. package/dist/cli/render.d.ts +19 -8
  52. package/dist/cli/render.js +70 -27
  53. package/dist/cli/render.js.map +1 -1
  54. package/dist/cli/resolve-invocation.d.ts +2 -2
  55. package/dist/cli/resolve-invocation.js +3 -3
  56. package/dist/cli/resolve-invocation.js.map +1 -1
  57. package/dist/cli/skill-check.d.ts +27 -0
  58. package/dist/cli/skill-check.js +99 -0
  59. package/dist/cli/skill-check.js.map +1 -0
  60. package/dist/cli/skill.d.ts +70 -0
  61. package/dist/cli/skill.js +152 -0
  62. package/dist/cli/skill.js.map +1 -0
  63. package/dist/cli/tag-filters.d.ts +6 -2
  64. package/dist/cli/tag-filters.js +8 -6
  65. package/dist/cli/tag-filters.js.map +1 -1
  66. package/dist/cli/verb-hint.js +28 -3
  67. package/dist/cli/verb-hint.js.map +1 -1
  68. package/dist/cli/version.d.ts +4 -0
  69. package/dist/cli/version.js +18 -0
  70. package/dist/cli/version.js.map +1 -0
  71. package/dist/client.d.ts +171 -27
  72. package/dist/client.js +279 -40
  73. package/dist/client.js.map +1 -1
  74. package/dist/config.d.ts +66 -2
  75. package/dist/config.js +120 -12
  76. package/dist/config.js.map +1 -1
  77. package/dist/contracts.d.ts +186 -28
  78. package/dist/contracts.js +22 -1
  79. package/dist/contracts.js.map +1 -1
  80. package/dist/diagnose.d.ts +34 -2
  81. package/dist/diagnose.js +36 -1
  82. package/dist/diagnose.js.map +1 -1
  83. package/dist/index.d.ts +25 -9
  84. package/dist/index.js +22 -3
  85. package/dist/index.js.map +1 -1
  86. package/dist/mcp/server.d.ts +9 -0
  87. package/dist/mcp/server.js +1157 -684
  88. package/dist/mcp/server.js.map +1 -1
  89. package/dist/model/clock.d.ts +61 -0
  90. package/dist/model/clock.js +71 -0
  91. package/dist/model/clock.js.map +1 -0
  92. package/dist/model/dates.d.ts +26 -3
  93. package/dist/model/dates.js +84 -3
  94. package/dist/model/dates.js.map +1 -1
  95. package/dist/model/entities.d.ts +60 -3
  96. package/dist/model/entities.js.map +1 -1
  97. package/dist/model/mappers.d.ts +4 -2
  98. package/dist/model/mappers.js +66 -9
  99. package/dist/model/mappers.js.map +1 -1
  100. package/dist/model/recurrence.d.ts +8 -1
  101. package/dist/model/recurrence.js.map +1 -1
  102. package/dist/read/area-filter.d.ts +76 -0
  103. package/dist/read/area-filter.js +59 -0
  104. package/dist/read/area-filter.js.map +1 -0
  105. package/dist/read/area-view.d.ts +12 -12
  106. package/dist/read/area-view.js +58 -34
  107. package/dist/read/area-view.js.map +1 -1
  108. package/dist/read/detail.d.ts +1 -1
  109. package/dist/read/detail.js +26 -12
  110. package/dist/read/detail.js.map +1 -1
  111. package/dist/read/log-boundary.d.ts +1 -1
  112. package/dist/read/log-boundary.js +13 -3
  113. package/dist/read/log-boundary.js.map +1 -1
  114. package/dist/read/predicates.d.ts +18 -0
  115. package/dist/read/predicates.js +19 -0
  116. package/dist/read/predicates.js.map +1 -1
  117. package/dist/read/project-view.d.ts +32 -16
  118. package/dist/read/project-view.js +80 -45
  119. package/dist/read/project-view.js.map +1 -1
  120. package/dist/read/pseudo-area.d.ts +29 -0
  121. package/dist/read/pseudo-area.js +27 -0
  122. package/dist/read/pseudo-area.js.map +1 -0
  123. package/dist/read/queries.d.ts +108 -19
  124. package/dist/read/queries.js +212 -35
  125. package/dist/read/queries.js.map +1 -1
  126. package/dist/read/scope.d.ts +126 -0
  127. package/dist/read/scope.js +162 -0
  128. package/dist/read/scope.js.map +1 -0
  129. package/dist/read/search-rank.d.ts +2 -5
  130. package/dist/read/shape.d.ts +162 -0
  131. package/dist/read/shape.js +686 -0
  132. package/dist/read/shape.js.map +1 -0
  133. package/dist/read/show-target.d.ts +9 -1
  134. package/dist/read/show-target.js +45 -4
  135. package/dist/read/show-target.js.map +1 -1
  136. package/dist/read/snapshot.d.ts +1 -1
  137. package/dist/read/snapshot.js +9 -2
  138. package/dist/read/snapshot.js.map +1 -1
  139. package/dist/read/stage.d.ts +199 -0
  140. package/dist/read/stage.js +125 -0
  141. package/dist/read/stage.js.map +1 -0
  142. package/dist/read/truncation.d.ts +8 -6
  143. package/dist/read/truncation.js +35 -10
  144. package/dist/read/truncation.js.map +1 -1
  145. package/dist/read/views.d.ts +61 -20
  146. package/dist/read/views.js +240 -82
  147. package/dist/read/views.js.map +1 -1
  148. package/dist/surface-copy.d.ts +9 -0
  149. package/dist/surface-copy.js +9 -0
  150. package/dist/surface-copy.js.map +1 -1
  151. package/dist/write/accessibility-probe.d.ts +1 -1
  152. package/dist/write/accessibility-probe.js +8 -0
  153. package/dist/write/accessibility-probe.js.map +1 -1
  154. package/dist/write/automation-probe.d.ts +1 -1
  155. package/dist/write/automation-probe.js +8 -0
  156. package/dist/write/automation-probe.js.map +1 -1
  157. package/dist/write/batch.d.ts +49 -10
  158. package/dist/write/batch.js +423 -70
  159. package/dist/write/batch.js.map +1 -1
  160. package/dist/write/commands.d.ts +1 -1
  161. package/dist/write/commands.js +316 -66
  162. package/dist/write/commands.js.map +1 -1
  163. package/dist/write/guards.d.ts +1 -1
  164. package/dist/write/guards.js +94 -10
  165. package/dist/write/guards.js.map +1 -1
  166. package/dist/write/heading.d.ts +10 -1
  167. package/dist/write/heading.js +35 -5
  168. package/dist/write/heading.js.map +1 -1
  169. package/dist/write/make-repeating-project.d.ts +2 -2
  170. package/dist/write/make-repeating-project.js +9 -9
  171. package/dist/write/make-repeating-project.js.map +1 -1
  172. package/dist/write/move.d.ts +130 -0
  173. package/dist/write/move.js +1533 -0
  174. package/dist/write/move.js.map +1 -0
  175. package/dist/write/operations.d.ts +105 -19
  176. package/dist/write/operations.js +56 -7
  177. package/dist/write/operations.js.map +1 -1
  178. package/dist/write/opid.d.ts +31 -0
  179. package/dist/write/opid.js +30 -0
  180. package/dist/write/opid.js.map +1 -0
  181. package/dist/write/pipeline.d.ts +122 -3
  182. package/dist/write/pipeline.js +309 -52
  183. package/dist/write/pipeline.js.map +1 -1
  184. package/dist/write/planner.js +8 -1
  185. package/dist/write/planner.js.map +1 -1
  186. package/dist/write/pre-state.d.ts +197 -6
  187. package/dist/write/pre-state.js +427 -20
  188. package/dist/write/pre-state.js.map +1 -1
  189. package/dist/write/recurrence-rule-blob.d.ts +65 -0
  190. package/dist/write/recurrence-rule-blob.js +150 -0
  191. package/dist/write/recurrence-rule-blob.js.map +1 -0
  192. package/dist/write/reorder.d.ts +23 -3
  193. package/dist/write/reorder.js +1871 -85
  194. package/dist/write/reorder.js.map +1 -1
  195. package/dist/write/reversibility.js +19 -7
  196. package/dist/write/reversibility.js.map +1 -1
  197. package/dist/write/scope-guard.d.ts +31 -0
  198. package/dist/write/scope-guard.js +162 -0
  199. package/dist/write/scope-guard.js.map +1 -0
  200. package/dist/write/undo.js +120 -13
  201. package/dist/write/undo.js.map +1 -1
  202. package/dist/write/vectors/applescript.js +16 -7
  203. package/dist/write/vectors/applescript.js.map +1 -1
  204. package/dist/write/vectors/registry.d.ts +5 -7
  205. package/dist/write/vectors/registry.js +49 -2
  206. package/dist/write/vectors/registry.js.map +1 -1
  207. package/dist/write/vectors/shortcuts.js +1 -1
  208. package/dist/write/vectors/shortcuts.js.map +1 -1
  209. package/dist/write/vectors/simulator.d.ts +44 -0
  210. package/dist/write/vectors/simulator.js +1192 -0
  211. package/dist/write/vectors/simulator.js.map +1 -0
  212. package/dist/write/vectors/types.d.ts +46 -0
  213. package/dist/write/vectors/ui-certification.d.ts +10 -4
  214. package/dist/write/vectors/ui-certification.js +43 -10
  215. package/dist/write/vectors/ui-certification.js.map +1 -1
  216. package/dist/write/vectors/ui-drag.d.ts +21 -1
  217. package/dist/write/vectors/ui-drag.js +138 -27
  218. package/dist/write/vectors/ui-drag.js.map +1 -1
  219. package/dist/write/vectors/ui-recipes.d.ts +34 -1
  220. package/dist/write/vectors/ui-recipes.js +217 -6
  221. package/dist/write/vectors/ui-recipes.js.map +1 -1
  222. package/dist/write/vectors/ui.d.ts +40 -1
  223. package/dist/write/vectors/ui.js +196 -39
  224. package/dist/write/vectors/ui.js.map +1 -1
  225. package/dist/write/verify/delta.d.ts +127 -2
  226. package/dist/write/verify/delta.js +255 -16
  227. package/dist/write/verify/delta.js.map +1 -1
  228. package/dist/write/verify/poller.d.ts +7 -1
  229. package/dist/write/verify/poller.js +20 -1
  230. package/dist/write/verify/poller.js.map +1 -1
  231. package/package.json +24 -3
  232. package/schema/envelope.schema.json +383 -0
  233. package/skills/things-cli/SKILL.md +70 -0
  234. package/skills/things-cli/references/banner.md +35 -0
  235. package/skills/things-cli/references/contracts.md +62 -0
  236. package/skills/things-cli/references/errors.md +49 -0
  237. package/skills/things-cli/references/gui.md +11 -0
  238. package/skills/things-cli/references/model.md +60 -0
  239. package/skills/things-cli/references/ordering.md +71 -0
@@ -1,11 +1,14 @@
1
- import { decodeReminderTime, encodeReminderTime, reminderUrlToken, } from "../model/dates.js";
1
+ import { decodeReminderTime, encodeReminderTime, localToday, reminderUrlToken, } from "../model/dates.js";
2
2
  import { byUuid } from "../read/detail.js";
3
- import { childTagTitles, classifyProjectRepeat, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
3
+ import { isLooseRef } from "../read/pseudo-area.js";
4
+ import { reminderIsLive } from "../read/stage.js";
5
+ import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
4
6
  import { resolveTagRefs } from "./tag-refs.js";
5
7
  import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
6
8
  import { assertRepeatRule } from "./repeat-rule.js";
7
9
  import { escapeAppleScript } from "./vectors/applescript.js";
8
- import { areaReorderSidebarRecipe, convertToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
10
+ import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
11
+ import { buildRepeatingFingerprint } from "./verify/delta.js";
9
12
  // ------------------------------------------------------------------ helpers
10
13
  function thingsUrl(command, params, token) {
11
14
  const parts = [];
@@ -152,6 +155,12 @@ const todoAdd = {
152
155
  "H-REMINDER-SCOPE",
153
156
  ],
154
157
  preRead(db, params) {
158
+ if (containerGiven(params.project) && containerGiven(params.area)) {
159
+ throw new RangeError("project and area are exclusive destinations");
160
+ }
161
+ if (params.heading !== undefined && !containerGiven(params.project)) {
162
+ throw new RangeError("heading requires a project destination");
163
+ }
155
164
  const pre = emptyPreState();
156
165
  if (containerGiven(params.project)) {
157
166
  pre.destProject = resolveProject(db, params.project);
@@ -229,6 +238,14 @@ const todoAdd = {
229
238
  * `when=` CLEARS an existing reminder (R07/R20), so when the caller
230
239
  * re-schedules to today/evening/a date without addressing the reminder we
231
240
  * auto-preserve the current one; an explicit null is the intentional clear.
241
+ *
242
+ * §9n: a reminder whose row's `startDate` is already strictly PAST is
243
+ * presentation-dead (the GUI hides its bell; the byte lingers in the DB). We do
244
+ * NOT auto-preserve such a stale byte — carrying it into the re-schedule would
245
+ * RESURRECT a reminder the user believes gone. A LIVE reminder (startDate
246
+ * today/future) is preserved as before. The liveness test is the SAME
247
+ * {@link reminderIsLive} predicate the read side gates on, consulted against the
248
+ * target's CURRENT `startDate` under the response clock.
232
249
  */
233
250
  function effectiveReminder(pre, params) {
234
251
  if (params.reminder !== undefined)
@@ -242,6 +259,8 @@ function effectiveReminder(pre, params) {
242
259
  const target = pre.target;
243
260
  if (target === null || target.type === "heading")
244
261
  return null;
262
+ if (!reminderIsLive(target.startDate, pre.todayIso))
263
+ return null;
245
264
  return target.reminder;
246
265
  }
247
266
  function assertNotesModesExclusive(params) {
@@ -268,9 +287,10 @@ function expectedNotes(pre, params) {
268
287
  const todoUpdate = {
269
288
  op: "todo.update",
270
289
  hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
271
- preRead(db, params) {
290
+ preRead(db, params, now) {
272
291
  assertNotesModesExclusive(params);
273
292
  const pre = emptyPreState();
293
+ pre.todayIso = localToday(now);
274
294
  pre.target = loadTarget(db, params.uuid);
275
295
  return pre;
276
296
  },
@@ -344,11 +364,21 @@ const todoMove = {
344
364
  ],
345
365
  preRead(db, params) {
346
366
  const container = containerGiven(params.project) || containerGiven(params.area) || params.heading !== undefined;
347
- if (params.inbox === true && (container || params.detach === true)) {
348
- throw new RangeError("inbox is exclusive with project/area/heading/detach");
367
+ const detachFamily = params.loose === true || params.noHeading === true;
368
+ if (params.inbox === true && (container || detachFamily)) {
369
+ throw new RangeError("inbox is exclusive with project/area/heading and --no-heading/--loose");
349
370
  }
350
- if (params.detach === true && container) {
351
- throw new RangeError("detach is exclusive with project/area/heading destinations");
371
+ if (params.loose === true && (container || params.noHeading === true)) {
372
+ throw new RangeError("--loose is exclusive with project/area/heading and --no-heading");
373
+ }
374
+ if (params.noHeading === true && container) {
375
+ throw new RangeError("--no-heading is exclusive with project/area/heading destinations");
376
+ }
377
+ if (containerGiven(params.project) && containerGiven(params.area)) {
378
+ throw new RangeError("project and area are exclusive destinations");
379
+ }
380
+ if (params.heading !== undefined && !containerGiven(params.project)) {
381
+ throw new RangeError("heading requires a project destination");
352
382
  }
353
383
  const pre = emptyPreState();
354
384
  pre.target = loadTarget(db, params.uuid);
@@ -361,6 +391,16 @@ const todoMove = {
361
391
  }
362
392
  }
363
393
  }
394
+ // --no-heading re-asserts the CURRENT project as the container: resolve the
395
+ // target's own project (direct or via its heading) so compile/delta pin it.
396
+ if (params.noHeading === true) {
397
+ const t = pre.target;
398
+ const current = t !== null && t.type === "to-do" ? (t.project ?? t.headingProject ?? null) : null;
399
+ if (current !== null) {
400
+ pre.destProject = { resolved: { uuid: current.uuid, title: current.title }, matches: 1 };
401
+ pre.destProjectStatus = projectStatus(db, current.uuid);
402
+ }
403
+ }
364
404
  if (containerGiven(params.area))
365
405
  pre.destArea = resolveArea(db, params.area);
366
406
  return pre;
@@ -372,7 +412,7 @@ const todoMove = {
372
412
  assert.push({ field: "start", equals: "inbox" }, { field: "startDate", equals: null });
373
413
  return { mode: "update", uuid: params.uuid, assert };
374
414
  }
375
- if (params.detach === true) {
415
+ if (params.loose === true) {
376
416
  // P21/P22: empty list-id strips every container link; the schedule is
377
417
  // untouched (pin it — a silent de-schedule would be a contrary write).
378
418
  const target = pre.target;
@@ -380,6 +420,16 @@ const todoMove = {
380
420
  assert.push({ field: "project", equals: null }, { field: "area", equals: null }, { field: "heading", equals: null }, { field: "startDate", equals: startDate });
381
421
  return { mode: "update", uuid: params.uuid, assert };
382
422
  }
423
+ if (params.noHeading === true) {
424
+ // Leave the heading, keep the project: re-assert the current project as
425
+ // the container with no heading (lands in the unheaded block).
426
+ const project = pre.destProject?.resolved;
427
+ assert.push({ field: "heading", equals: null });
428
+ if (project !== undefined && project !== null) {
429
+ assert.push({ field: "project.uuid", equals: project.uuid });
430
+ }
431
+ return { mode: "update", uuid: params.uuid, assert };
432
+ }
383
433
  const heading = pre.destHeading?.resolved;
384
434
  const project = pre.destProject?.resolved;
385
435
  const area = pre.destArea?.resolved;
@@ -402,13 +452,20 @@ const todoMove = {
402
452
  unsupportedVector(this.op, vector);
403
453
  return osa(`move to do id ${q(params.uuid)} to list "Inbox"`);
404
454
  }
405
- if (params.detach === true) {
455
+ if (params.loose === true) {
406
456
  // Empty list-id = clear the container (P21/P22) — URL only; the other
407
457
  // vectors reject or silently ignore container removal (P10/P11, P26).
408
458
  if (vector !== "url-scheme")
409
459
  unsupportedVector(this.op, vector);
410
460
  return thingsUrl("update", { id: params.uuid, "list-id": "" }, ctx.token);
411
461
  }
462
+ if (params.noHeading === true) {
463
+ // Re-assert the CURRENT project as the list with no heading param — the
464
+ // to-do drops its heading FK and lands in the project's unheaded block.
465
+ if (vector !== "url-scheme")
466
+ unsupportedVector(this.op, vector);
467
+ return thingsUrl("update", { id: params.uuid, "list-id": pre.destProject?.resolved?.uuid ?? "" }, ctx.token);
468
+ }
412
469
  const project = pre.destProject?.resolved;
413
470
  const area = pre.destArea?.resolved;
414
471
  if (vector === "url-scheme") {
@@ -591,9 +648,10 @@ const projectAdd = {
591
648
  const projectUpdate = {
592
649
  op: "project.update",
593
650
  hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
594
- preRead(db, params) {
651
+ preRead(db, params, now) {
595
652
  assertNotesModesExclusive(params);
596
653
  const pre = emptyPreState();
654
+ pre.todayIso = localToday(now);
597
655
  pre.target = loadTarget(db, params.uuid);
598
656
  return pre;
599
657
  },
@@ -667,8 +725,8 @@ const projectMove = {
667
725
  op: "project.move",
668
726
  hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE"],
669
727
  preRead(db, params) {
670
- if ((params.detach === true) === containerGiven(params.area)) {
671
- throw new RangeError("project.move needs exactly one of area / detach");
728
+ if ((params.noArea === true) === containerGiven(params.area)) {
729
+ throw new RangeError("project.move needs exactly one of area / --no-area");
672
730
  }
673
731
  const pre = emptyPreState();
674
732
  pre.target = loadTarget(db, params.uuid);
@@ -683,7 +741,7 @@ const projectMove = {
683
741
  mode: "update",
684
742
  uuid: params.uuid,
685
743
  assert: [
686
- params.detach === true
744
+ params.noArea === true
687
745
  ? { field: "area", equals: null }
688
746
  : { field: "area.uuid", equals: pre.destArea?.resolved?.uuid ?? "" },
689
747
  ],
@@ -691,14 +749,14 @@ const projectMove = {
691
749
  },
692
750
  compile(params, vector, pre, ctx) {
693
751
  if (vector === "url-scheme") {
694
- // P23 (move) / P24 (empty area-id = detach — URL is the ONLY detach
752
+ // P23 (move) / P24 (empty area-id = leave the area — URL is the ONLY
695
753
  // surface: AppleScript rejects missing value/"" and json-null no-ops).
696
754
  return thingsUrl("update-project", {
697
755
  id: params.uuid,
698
- "area-id": params.detach === true ? "" : (pre.destArea?.resolved?.uuid ?? ""),
756
+ "area-id": params.noArea === true ? "" : (pre.destArea?.resolved?.uuid ?? ""),
699
757
  }, ctx.token);
700
758
  }
701
- if (params.detach === true)
759
+ if (params.noArea === true)
702
760
  unsupportedVector(this.op, vector);
703
761
  return osa(`set area of project id ${q(params.uuid)} to area id ` +
704
762
  q(pre.destArea?.resolved?.uuid ?? ""));
@@ -706,7 +764,7 @@ const projectMove = {
706
764
  };
707
765
  const todoRestore = {
708
766
  op: "todo.restore",
709
- hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE"],
767
+ hazards: ["H-UNKNOWN-DESTINATION", "H-TEMPLATE-CHILD-RESTORE", "H-REPEAT-SCHEDULE"],
710
768
  preRead(db, params) {
711
769
  const pre = emptyPreState();
712
770
  pre.target = loadTarget(db, params.uuid);
@@ -940,10 +998,13 @@ const areaAdd = {
940
998
  };
941
999
  const areaDelete = {
942
1000
  op: "area.delete",
943
- hazards: ["H-UNKNOWN-DESTINATION", "H-PERMANENT-DELETE"],
1001
+ hazards: ["H-UNKNOWN-DESTINATION", "H-AREA-NOT-EMPTY", "H-PERMANENT-DELETE"],
944
1002
  preRead(db, params) {
945
1003
  const pre = emptyPreState();
946
1004
  pre.entityTarget = resolveArea(db, { title: params.target, uuid: params.target });
1005
+ if (pre.entityTarget.resolved !== null) {
1006
+ pre.areaMembers = areaMemberCounts(db, pre.entityTarget.resolved.uuid);
1007
+ }
947
1008
  return pre;
948
1009
  },
949
1010
  expectedDelta(pre) {
@@ -1171,7 +1232,7 @@ const reorder = {
1171
1232
  preRead(db, params, now) {
1172
1233
  const pre = emptyPreState();
1173
1234
  let containerUuid = null;
1174
- if (params.scope === "project" || params.scope === "headings") {
1235
+ if (params.scope === "project") {
1175
1236
  pre.destProject = resolveProject(db, params.container ?? {});
1176
1237
  containerUuid = pre.destProject.resolved?.uuid ?? null;
1177
1238
  }
@@ -1179,6 +1240,20 @@ const reorder = {
1179
1240
  pre.destArea = resolveArea(db, params.container ?? {});
1180
1241
  containerUuid = pre.destArea.resolved?.uuid ?? null;
1181
1242
  }
1243
+ if (params.scope === "container-day") {
1244
+ // The container may be a project OR an area (DAYORD-b); resolve whichever
1245
+ // the ref names so the compile can pick the right `project id`/`area id`
1246
+ // specifier. The planner always passes a resolved uuid.
1247
+ const asProject = resolveProject(db, params.container ?? {});
1248
+ if (asProject.resolved !== null) {
1249
+ pre.destProject = asProject;
1250
+ containerUuid = asProject.resolved.uuid;
1251
+ }
1252
+ else {
1253
+ pre.destArea = resolveArea(db, params.container ?? {});
1254
+ containerUuid = pre.destArea.resolved?.uuid ?? null;
1255
+ }
1256
+ }
1182
1257
  pre.reorder = computeReorderPre(db, params, containerUuid, now);
1183
1258
  return pre;
1184
1259
  },
@@ -1196,15 +1271,22 @@ const reorder = {
1196
1271
  compile(params, vector, pre) {
1197
1272
  if (vector !== "applescript")
1198
1273
  unsupportedVector(this.op, vector);
1199
- const specifier = params.scope === "project" || params.scope === "headings"
1274
+ const containerDaySpecifier = pre.destProject?.resolved != null
1275
+ ? `project id ${q(pre.destProject.resolved.uuid)}`
1276
+ : `area id ${q(pre.destArea?.resolved?.uuid ?? "")}`;
1277
+ const specifier = params.scope === "project"
1200
1278
  ? `project id ${q(pre.destProject?.resolved?.uuid ?? "")}`
1201
1279
  : params.scope === "area"
1202
1280
  ? `area id ${q(pre.destArea?.resolved?.uuid ?? "")}`
1203
- : params.scope === "inbox"
1204
- ? `list "Inbox"`
1205
- : params.scope === "someday"
1206
- ? `list "Someday"`
1207
- : `list "Today"`;
1281
+ : params.scope === "container-day"
1282
+ ? containerDaySpecifier
1283
+ : params.scope === "inbox"
1284
+ ? `list "Inbox"`
1285
+ : params.scope === "someday"
1286
+ ? `list "Someday"`
1287
+ : params.scope === "tomorrow"
1288
+ ? `list "Tomorrow"`
1289
+ : `list "Today"`;
1208
1290
  const wire = pre.reorder?.wireList ?? params.uuids;
1209
1291
  if (params.scope === "someday") {
1210
1292
  // The Someday handler STACKS each sent id above the list's current top
@@ -1352,7 +1434,7 @@ function headingChildren(db, headingUuid) {
1352
1434
  return todos;
1353
1435
  }
1354
1436
  const headingRename = {
1355
- op: "heading.rename",
1437
+ op: "project.rename-heading",
1356
1438
  hazards: ["H-UNKNOWN-DESTINATION"],
1357
1439
  preRead(db, params) {
1358
1440
  const pre = emptyPreState();
@@ -1375,7 +1457,7 @@ const headingRename = {
1375
1457
  },
1376
1458
  };
1377
1459
  const headingArchive = {
1378
- op: "heading.archive",
1460
+ op: "project.archive-heading",
1379
1461
  hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-CHILDREN"],
1380
1462
  preRead(db, params) {
1381
1463
  const pre = emptyPreState();
@@ -1428,7 +1510,7 @@ const headingArchive = {
1428
1510
  },
1429
1511
  };
1430
1512
  const headingUnarchive = {
1431
- op: "heading.unarchive",
1513
+ op: "project.unarchive-heading",
1432
1514
  hazards: ["H-UNKNOWN-DESTINATION"],
1433
1515
  preRead(db, params) {
1434
1516
  const pre = emptyPreState();
@@ -1448,8 +1530,8 @@ const headingUnarchive = {
1448
1530
  return osa(`set status of to do id ${q(params.uuid)} to open`);
1449
1531
  },
1450
1532
  };
1451
- const headingCreate = {
1452
- op: "heading.create",
1533
+ const headingAdd = {
1534
+ op: "project.add-heading",
1453
1535
  hazards: ["H-UNKNOWN-DESTINATION"],
1454
1536
  preRead(db, params) {
1455
1537
  const pre = emptyPreState();
@@ -1484,6 +1566,32 @@ const headingCreate = {
1484
1566
  });
1485
1567
  },
1486
1568
  };
1569
+ const projectMoveHeading = {
1570
+ op: "project.move-heading",
1571
+ hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-ORDER"],
1572
+ preRead(db, params) {
1573
+ const pre = emptyPreState();
1574
+ pre.destProject = resolveProject(db, params.project);
1575
+ pre.headingMove = computeHeadingMovePre(db, pre.destProject, params.headings, params.placement);
1576
+ return pre;
1577
+ },
1578
+ expectedDelta(pre) {
1579
+ // The reorder wire re-ranks the FULL heading list; verify the whole target
1580
+ // order (strictly ascending "index"). Children follow their heading (scf
1581
+ // P1), so no per-child assertion is needed.
1582
+ return { mode: "ordering", key: "index", sequence: pre.headingMove?.targetOrder ?? [] };
1583
+ },
1584
+ compile(_params, vector, pre) {
1585
+ // The same native private-reorder wire the old `reorder --scope headings`
1586
+ // used (experimental — gated by allow-experimental + the sdef canary),
1587
+ // fed the computed full order.
1588
+ if (vector !== "applescript")
1589
+ unsupportedVector(this.op, vector);
1590
+ const order = pre.headingMove?.targetOrder ?? [];
1591
+ return osa(`${PRIVATE_REORDER_COMMAND} project id ${q(pre.destProject?.resolved?.uuid ?? "")} ` +
1592
+ `with ids ${q(order.join(","))}`);
1593
+ },
1594
+ };
1487
1595
  const todoClearDatedReminder = {
1488
1596
  op: "todo.clear-dated-reminder",
1489
1597
  hazards: ["H-UNKNOWN-DESTINATION", "H-NO-REMINDER"],
@@ -1560,12 +1668,17 @@ const todoMakeRepeating = {
1560
1668
  pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
1561
1669
  return pre;
1562
1670
  },
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.
1671
+ expectedDelta(pre, params, ctx) {
1672
+ // Identity REPLACEMENT or preserve-as-instance (UI2-a / RSIM-R): a NEW
1673
+ // template row (type=0 with a recurrence rule) is born; the original uuid is
1674
+ // EITHER destroyed OR relinked as the current-occurrence instance. Discover
1675
+ // the template with the create probe (excluding the pre-existing same-title
1676
+ // rows), pick it by asserting it IS a template, then the `repeating` context
1677
+ // hardens discovery (restored time-bound, source-fingerprint tiebreak) and
1678
+ // derives instance + source fate for the enriched result. `expectedRule`
1679
+ // makes the LANDED rule verifiable: a template minted with the wrong
1680
+ // frequency/interval (the interval-field race, oddities §8l) becomes a
1681
+ // verify-failed:mismatch instead of a silent ok.
1569
1682
  return {
1570
1683
  mode: "create",
1571
1684
  probe: {
@@ -1573,6 +1686,17 @@ const todoMakeRepeating = {
1573
1686
  type: "to-do",
1574
1687
  sinceEpoch: ctx.nowEpoch - 2,
1575
1688
  excludeUuids: pre.sameTitleUuids,
1689
+ ...(pre.target !== null && {
1690
+ repeating: {
1691
+ sourceUuid: pre.target.uuid,
1692
+ fingerprint: buildRepeatingFingerprint(pre.target),
1693
+ expectedRule: {
1694
+ type: params.afterCompletion === true ? "after-completion" : "fixed",
1695
+ unit: params.frequency,
1696
+ interval: params.interval,
1697
+ },
1698
+ },
1699
+ }),
1576
1700
  },
1577
1701
  assert: [{ field: "repeating.isTemplate", equals: true }],
1578
1702
  };
@@ -1594,12 +1718,22 @@ const todoRescheduleRepeat = {
1594
1718
  },
1595
1719
  expectedDelta(_pre, params) {
1596
1720
  // 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.
1721
+ // place. Assert the decoded rule's TYPE + frequency + interval; ALSO capture
1722
+ // the whole prior rule (+ deadline flag) so the undo can re-drive it
1723
+ // faithfully. Asserting `type` (fixed vs after-completion) is what makes a
1724
+ // fixed→after-completion conversion VERIFIABLE at all: in the field-report
1725
+ // incident (0½ item 1) both rules were weekly/interval-2, so unit+interval
1726
+ // never changed — only the type flipped, and without this assertion verify
1727
+ // could neither confirm the conversion landed nor tell the pre-drive
1728
+ // idempotency check that the target had not yet been reached.
1599
1729
  return {
1600
1730
  mode: "update",
1601
1731
  uuid: params.uuid,
1602
1732
  assert: [
1733
+ {
1734
+ field: "repeating.rule.type",
1735
+ equals: params.afterCompletion === true ? "after-completion" : "fixed",
1736
+ },
1603
1737
  { field: "repeating.rule.unit", equals: params.frequency },
1604
1738
  { field: "repeating.rule.interval", equals: params.interval },
1605
1739
  ],
@@ -1673,11 +1807,17 @@ const projectRescheduleRepeat = {
1673
1807
  },
1674
1808
  expectedDelta(_pre, params) {
1675
1809
  // Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
1676
- // capture the prior rule (+ deadline flag) for the faithful undo.
1810
+ // capture the prior rule (+ deadline flag) for the faithful undo. Assert the
1811
+ // rule TYPE too (fixed vs after-completion) so a conversion that leaves
1812
+ // unit+interval unchanged is still verifiable — see todo.reschedule-repeat.
1677
1813
  return {
1678
1814
  mode: "update",
1679
1815
  uuid: params.uuid,
1680
1816
  assert: [
1817
+ {
1818
+ field: "repeating.rule.type",
1819
+ equals: params.afterCompletion === true ? "after-completion" : "fixed",
1820
+ },
1681
1821
  { field: "repeating.rule.unit", equals: params.frequency },
1682
1822
  { field: "repeating.rule.interval", equals: params.interval },
1683
1823
  ],
@@ -1749,14 +1889,18 @@ const projectMakeRepeating = {
1749
1889
  pre.target = loadTarget(db, params.uuid);
1750
1890
  pre.projectRepeat = classifyProjectRepeat(db, pre.target);
1751
1891
  pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
1892
+ if (pre.target !== null)
1893
+ pre.repeatSubtreeUuids = projectSubtreeUuids(db, pre.target.uuid);
1752
1894
  return pre;
1753
1895
  },
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.
1896
+ expectedDelta(pre, params, ctx) {
1897
+ // Identity REPLACEMENT or preserve-as-instance (UIC4-b / RSIM-R): a NEW
1898
+ // template project (with a recurrence rule) is born; the source project is
1899
+ // EITHER destroyed OR (when its subtree holds a nested repeater) relinked as
1900
+ // the current-occurrence instance. Pick the TEMPLATE by asserting it IS one,
1901
+ // excluding pre-existing same-title rows; the `repeating` context hardens
1902
+ // discovery and derives instance + source fate + childrenReplaced.
1903
+ // `expectedRule` makes the landed rule verifiable (interval-race guard, §8l).
1760
1904
  return {
1761
1905
  mode: "create",
1762
1906
  probe: {
@@ -1764,6 +1908,18 @@ const projectMakeRepeating = {
1764
1908
  type: "project",
1765
1909
  sinceEpoch: ctx.nowEpoch - 2,
1766
1910
  excludeUuids: pre.sameTitleUuids,
1911
+ ...(pre.target !== null && {
1912
+ repeating: {
1913
+ sourceUuid: pre.target.uuid,
1914
+ fingerprint: buildRepeatingFingerprint(pre.target),
1915
+ subtreeUuids: pre.repeatSubtreeUuids ?? [],
1916
+ expectedRule: {
1917
+ type: params.afterCompletion === true ? "after-completion" : "fixed",
1918
+ unit: params.frequency,
1919
+ interval: params.interval,
1920
+ },
1921
+ },
1922
+ }),
1767
1923
  },
1768
1924
  assert: [{ field: "repeating.isTemplate", equals: true }],
1769
1925
  };
@@ -1779,23 +1935,23 @@ const projectMakeRepeating = {
1779
1935
  return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
1780
1936
  },
1781
1937
  };
1782
- // project.create-repeating is delivered by the runCreateRepeatingProject
1938
+ // project.add-repeating is delivered by the runAddRepeatingProject
1783
1939
  // orchestrator (project.add THEN project.make-repeating); it has no single
1784
1940
  // 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 " +
1941
+ const ADD_REPEATING_ORCHESTRATED = "project.add-repeating is delivered by the runAddRepeatingProject orchestrator (create " +
1786
1942
  "the project, then promote it to a repeating series); it has no atomic surface and is never " +
1787
1943
  "dispatched directly through the pipeline";
1788
- const projectCreateRepeating = {
1789
- op: "project.create-repeating",
1944
+ const projectAddRepeating = {
1945
+ op: "project.add-repeating",
1790
1946
  hazards: [],
1791
1947
  preRead() {
1792
1948
  return emptyPreState();
1793
1949
  },
1794
1950
  expectedDelta() {
1795
- throw new Error(CREATE_REPEATING_ORCHESTRATED);
1951
+ throw new Error(ADD_REPEATING_ORCHESTRATED);
1796
1952
  },
1797
1953
  compile() {
1798
- throw new Error(CREATE_REPEATING_ORCHESTRATED);
1954
+ throw new Error(ADD_REPEATING_ORCHESTRATED);
1799
1955
  },
1800
1956
  };
1801
1957
  const todoConvertToProject = {
@@ -1832,7 +1988,7 @@ const todoConvertToProject = {
1832
1988
  },
1833
1989
  };
1834
1990
  const headingConvertToProject = {
1835
- op: "heading.convert-to-project",
1991
+ op: "project.promote-heading",
1836
1992
  hazards: UI_HAZARDS,
1837
1993
  preRead(db, params) {
1838
1994
  const pre = emptyPreState();
@@ -1842,6 +1998,7 @@ const headingConvertToProject = {
1842
1998
  // (headings are not covered by nonHeadingTitle).
1843
1999
  const title = pre.target !== null ? pre.target.title : "";
1844
2000
  pre.sameTitleUuids = sameTitleTaskUuids(db, title, "project");
2001
+ pre.headingConvert = classifyHeadingConvert(db, pre.target);
1845
2002
  return pre;
1846
2003
  },
1847
2004
  expectedDelta(pre, _params, ctx) {
@@ -1861,10 +2018,82 @@ const headingConvertToProject = {
1861
2018
  assert: [],
1862
2019
  };
1863
2020
  },
1864
- compile(params, vector) {
2021
+ compile(_params, vector, pre) {
1865
2022
  if (vector !== "ui")
1866
2023
  unsupportedVector(this.op, vector);
1867
- return uiDrive(convertToProjectRecipe("heading.convert-to-project", params.uuid));
2024
+ const tax = pre.headingConvert;
2025
+ if (tax === null || tax.kind === "refuse") {
2026
+ // H-UNKNOWN-DESTINATION blocks a non-heading target before compile; a
2027
+ // refuse here means a parentless heading (or one absent from its project's
2028
+ // heading set) slipped past — fail loud rather than drive the wrong row.
2029
+ throw new Error(`project.promote-heading: ${tax?.kind === "refuse" ? tax.detail : "no heading taxonomy resolved (guard bypassed?)"}`);
2030
+ }
2031
+ return uiDrive(headingConvertToProjectRecipe(tax.projectReveal, tax.ordinal));
2032
+ },
2033
+ };
2034
+ const projectMoveHeadingToProject = {
2035
+ op: "project.move-heading-to-project",
2036
+ hazards: UI_HAZARDS,
2037
+ preRead(db, params) {
2038
+ const pre = emptyPreState();
2039
+ pre.headingMoveToProject = classifyHeadingMoveToProject(db, params.project, params.heading, params.toProject);
2040
+ if (pre.headingMoveToProject.kind === "ok") {
2041
+ // Load the heading row so the verify oracle reads its (post-op) project FK.
2042
+ pre.target = loadTarget(db, pre.headingMoveToProject.pre.headingUuid);
2043
+ }
2044
+ return pre;
2045
+ },
2046
+ expectedDelta(pre) {
2047
+ // HEADXPROJ: a single-row change — the heading's `project` FK becomes the
2048
+ // destination; its children follow via their intact heading FK (no child
2049
+ // rewrite, no index churn), so no per-child assertion is needed.
2050
+ const tax = pre.headingMoveToProject;
2051
+ const headingUuid = tax?.kind === "ok" ? tax.pre.headingUuid : "";
2052
+ const destUuid = tax?.kind === "ok" ? tax.pre.destProjectUuid : "";
2053
+ return {
2054
+ mode: "update",
2055
+ uuid: headingUuid,
2056
+ assert: [{ field: "project.uuid", equals: destUuid }],
2057
+ };
2058
+ },
2059
+ compile(_params, vector, pre) {
2060
+ if (vector !== "ui")
2061
+ unsupportedVector(this.op, vector);
2062
+ const tax = pre.headingMoveToProject;
2063
+ if (tax === null || tax.kind === "refuse") {
2064
+ // The taxonomy refusals are surfaced by H-UNKNOWN-DESTINATION before
2065
+ // compile; reaching here with one means the guard was bypassed.
2066
+ throw new Error(`project.move-heading-to-project: ${tax?.kind === "refuse" ? tax.detail : "no taxonomy resolved (guard bypassed?)"}`);
2067
+ }
2068
+ return uiDrive(moveHeadingToProjectRecipe(tax.pre.sourceProjectUuid, tax.pre.headingTitle, tax.pre.destProjectTitle));
2069
+ },
2070
+ };
2071
+ const projectDissolveHeading = {
2072
+ op: "project.dissolve-heading",
2073
+ hazards: UI_HAZARDS,
2074
+ preRead(db, params) {
2075
+ const pre = emptyPreState();
2076
+ pre.target = loadTarget(db, params.uuid);
2077
+ pre.headingDissolve = classifyHeadingDissolve(db, pre.target);
2078
+ return pre;
2079
+ },
2080
+ expectedDelta(_pre, params) {
2081
+ // DISS1: the heading row is HARD-DELETED (removed from TMTask) while its
2082
+ // children become direct project children. The verify oracle is the heading
2083
+ // GONE — its children re-home is the DISS1-locked invariant, checked by the
2084
+ // op's tests (a childless heading dissolve verifies identically).
2085
+ return { mode: "gone", entity: "task", uuid: params.uuid };
2086
+ },
2087
+ compile(_params, vector, pre) {
2088
+ if (vector !== "ui")
2089
+ unsupportedVector(this.op, vector);
2090
+ const tax = pre.headingDissolve;
2091
+ if (tax === null || tax.kind === "refuse") {
2092
+ // The taxonomy refusals are surfaced by H-UNKNOWN-DESTINATION before
2093
+ // compile; reaching here with one means the guard was bypassed.
2094
+ throw new Error(`project.dissolve-heading: ${tax?.kind === "refuse" ? tax.detail : "no taxonomy resolved (guard bypassed?)"}`);
2095
+ }
2096
+ return uiDrive(dissolveHeadingRecipe(tax.pre.projectReveal, tax.pre.headingTitle));
1868
2097
  },
1869
2098
  };
1870
2099
  // -------------------------------------------------- sidebar AREA reorder
@@ -1892,6 +2121,16 @@ const areaReorderSidebar = {
1892
2121
  params.position !== "last") {
1893
2122
  throw new RangeError(`invalid position "${params.position}" — expected first | last`);
1894
2123
  }
2124
+ // The reserved `loose` pseudo-area ref (the derived area-less view) has no
2125
+ // sidebar row, so it can be neither moved nor an anchor — refuse it by name
2126
+ // with a specific message rather than a generic no-such-area error, matching
2127
+ // the loose-pseudo-area write-refusal pattern (src/read/pseudo-area.ts).
2128
+ for (const ref of [params.target, params.before, params.after]) {
2129
+ if (ref !== undefined && isLooseRef(ref)) {
2130
+ throw new RangeError("the loose pseudo-area is a derived view — it has no sidebar row and cannot be " +
2131
+ "reordered");
2132
+ }
2133
+ }
1895
2134
  const pre = emptyPreState();
1896
2135
  pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
1897
2136
  const ref = params.before ?? params.after;
@@ -1902,17 +2141,25 @@ const areaReorderSidebar = {
1902
2141
  if (target != null && dest != null && target.uuid === dest.uuid) {
1903
2142
  throw new RangeError("the destination area is the area being moved");
1904
2143
  }
1905
- // The sidebar drag addresses rows by their VISIBLE NAME a duplicated
1906
- // area title makes the row ambiguous, so refuse up front.
2144
+ // The sidebar drag addresses rows by their VISIBLE NAME. A DUPLICATE area
2145
+ // title is now handled by positional disambiguation in the ui-vector driver
2146
+ // (ORDFIN2 AXDRAG3: the intended uuid's Nth same-titled row is grabbed by the
2147
+ // `(index, uuid)` ASC law, and the post-gesture DB assert + self-invert catch
2148
+ // a wrong grab) — so a uuid-targeted ref whose title is shared is NO LONGER
2149
+ // refused here. A duplicate NAME ref stays refused upstream (resolveArea
2150
+ // returns the ambiguity candidates). Only a SANITY CAP remains: too many
2151
+ // same-titled rows could make the positional grab loop unreliable, so refuse
2152
+ // rather than churn. (WIRED — lab-cert pending an AXDRAG4 VM sitting.)
2153
+ const SAME_TITLE_CAP = 8;
1907
2154
  for (const area of [target, dest]) {
1908
2155
  if (area == null)
1909
2156
  continue;
1910
2157
  const dup = db
1911
2158
  .prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
1912
2159
  .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");
2160
+ if (dup.n > SAME_TITLE_CAP) {
2161
+ throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas (over the ${SAME_TITLE_CAP}-row ` +
2162
+ "positional-disambiguation cap) rename some, or reorder by a uniquely-titled anchor");
1916
2163
  }
1917
2164
  }
1918
2165
  pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
@@ -2005,22 +2252,25 @@ export const COMMANDS = {
2005
2252
  "project.set-tags": projectSetTags,
2006
2253
  "todo.backdate": todoBackdate,
2007
2254
  "todo.add-logged": todoAddLogged,
2008
- "heading.rename": headingRename,
2009
- "heading.archive": headingArchive,
2010
- "heading.unarchive": headingUnarchive,
2011
- "heading.create": headingCreate,
2255
+ "project.add-heading": headingAdd,
2256
+ "project.rename-heading": headingRename,
2257
+ "project.archive-heading": headingArchive,
2258
+ "project.unarchive-heading": headingUnarchive,
2259
+ "project.promote-heading": headingConvertToProject,
2260
+ "project.move-heading": projectMoveHeading,
2261
+ "project.move-heading-to-project": projectMoveHeadingToProject,
2262
+ "project.dissolve-heading": projectDissolveHeading,
2012
2263
  "todo.clear-dated-reminder": todoClearDatedReminder,
2013
2264
  "todo.make-repeating": todoMakeRepeating,
2014
2265
  "todo.reschedule-repeat": todoRescheduleRepeat,
2015
2266
  "todo.pause-repeat": todoPauseRepeat,
2016
2267
  "todo.resume-repeat": todoResumeRepeat,
2017
2268
  "todo.convert-to-project": todoConvertToProject,
2018
- "heading.convert-to-project": headingConvertToProject,
2019
2269
  "project.reschedule-repeat": projectRescheduleRepeat,
2020
2270
  "project.pause-repeat": projectPauseRepeat,
2021
2271
  "project.resume-repeat": projectResumeRepeat,
2022
2272
  "area.reorder": areaReorderSidebar,
2023
2273
  "project.make-repeating": projectMakeRepeating,
2024
- "project.create-repeating": projectCreateRepeating,
2274
+ "project.add-repeating": projectAddRepeating,
2025
2275
  };
2026
2276
  //# sourceMappingURL=commands.js.map