things-api 0.11.0 → 0.13.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 (208) hide show
  1. package/README.md +16 -5
  2. package/dist/audit/schema.d.ts +7 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/canonical-ref.d.ts +21 -0
  5. package/dist/cli/canonical-ref.js +23 -0
  6. package/dist/cli/canonical-ref.js.map +1 -0
  7. package/dist/cli/commands/area.d.ts +18 -11
  8. package/dist/cli/commands/area.js +81 -77
  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 +19 -4
  14. package/dist/cli/commands/install-skill.js +130 -52
  15. package/dist/cli/commands/install-skill.js.map +1 -1
  16. package/dist/cli/commands/mcp.js +21 -1
  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 +92 -48
  20. package/dist/cli/commands/project.js.map +1 -1
  21. package/dist/cli/commands/reads.d.ts +5 -1
  22. package/dist/cli/commands/reads.js +110 -31
  23. package/dist/cli/commands/reads.js.map +1 -1
  24. package/dist/cli/commands/setup.js +11 -4
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/show.js +75 -36
  27. package/dist/cli/commands/show.js.map +1 -1
  28. package/dist/cli/commands/todo.js +2 -2
  29. package/dist/cli/commands/todo.js.map +1 -1
  30. package/dist/cli/commands/writes.js +795 -144
  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 +44 -7
  34. package/dist/cli/did-you-mean.js.map +1 -1
  35. package/dist/cli/dry-run.d.ts +55 -0
  36. package/dist/cli/dry-run.js +92 -0
  37. package/dist/cli/dry-run.js.map +1 -0
  38. package/dist/cli/glyphs.d.ts +16 -2
  39. package/dist/cli/glyphs.js +60 -12
  40. package/dist/cli/glyphs.js.map +1 -1
  41. package/dist/cli/help.js +69 -13
  42. package/dist/cli/help.js.map +1 -1
  43. package/dist/cli/main.js +8 -1
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/move-hint.d.ts +3 -2
  46. package/dist/cli/move-hint.js +2 -2
  47. package/dist/cli/move-hint.js.map +1 -1
  48. package/dist/cli/read-driver.d.ts +53 -5
  49. package/dist/cli/read-driver.js +82 -15
  50. package/dist/cli/read-driver.js.map +1 -1
  51. package/dist/cli/ref-render.d.ts +14 -0
  52. package/dist/cli/ref-render.js +9 -0
  53. package/dist/cli/ref-render.js.map +1 -0
  54. package/dist/cli/render.d.ts +40 -14
  55. package/dist/cli/render.js +128 -55
  56. package/dist/cli/render.js.map +1 -1
  57. package/dist/cli/resolve-invocation.d.ts +7 -5
  58. package/dist/cli/resolve-invocation.js +16 -7
  59. package/dist/cli/resolve-invocation.js.map +1 -1
  60. package/dist/cli/skill-check.d.ts +12 -6
  61. package/dist/cli/skill-check.js +31 -14
  62. package/dist/cli/skill-check.js.map +1 -1
  63. package/dist/cli/skill.d.ts +8 -0
  64. package/dist/cli/skill.js +10 -0
  65. package/dist/cli/skill.js.map +1 -1
  66. package/dist/cli/verb-hint.js +1 -1
  67. package/dist/cli/verb-hint.js.map +1 -1
  68. package/dist/client.d.ts +158 -26
  69. package/dist/client.js +254 -38
  70. package/dist/client.js.map +1 -1
  71. package/dist/config.d.ts +66 -2
  72. package/dist/config.js +120 -12
  73. package/dist/config.js.map +1 -1
  74. package/dist/contracts.d.ts +187 -28
  75. package/dist/contracts.js +22 -1
  76. package/dist/contracts.js.map +1 -1
  77. package/dist/db/baselines/db-v26.js +4 -1
  78. package/dist/db/baselines/db-v26.js.map +1 -1
  79. package/dist/diagnose.d.ts +34 -2
  80. package/dist/diagnose.js +36 -1
  81. package/dist/diagnose.js.map +1 -1
  82. package/dist/index.d.ts +21 -8
  83. package/dist/index.js +26 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/mcp/server.d.ts +9 -0
  86. package/dist/mcp/server.js +790 -322
  87. package/dist/mcp/server.js.map +1 -1
  88. package/dist/model/entities.d.ts +57 -4
  89. package/dist/model/entities.js.map +1 -1
  90. package/dist/model/mappers.d.ts +2 -0
  91. package/dist/model/mappers.js +44 -2
  92. package/dist/model/mappers.js.map +1 -1
  93. package/dist/model/recurrence.d.ts +8 -1
  94. package/dist/model/recurrence.js.map +1 -1
  95. package/dist/model/serialize.js +15 -3
  96. package/dist/model/serialize.js.map +1 -1
  97. package/dist/read/area-filter.d.ts +76 -0
  98. package/dist/read/area-filter.js +58 -0
  99. package/dist/read/area-filter.js.map +1 -0
  100. package/dist/read/area-view.d.ts +22 -11
  101. package/dist/read/area-view.js +54 -28
  102. package/dist/read/area-view.js.map +1 -1
  103. package/dist/read/detail.js +15 -8
  104. package/dist/read/detail.js.map +1 -1
  105. package/dist/read/predicates.d.ts +18 -0
  106. package/dist/read/predicates.js +19 -0
  107. package/dist/read/predicates.js.map +1 -1
  108. package/dist/read/project-view.d.ts +91 -15
  109. package/dist/read/project-view.js +135 -37
  110. package/dist/read/project-view.js.map +1 -1
  111. package/dist/read/pseudo-area.d.ts +29 -0
  112. package/dist/read/pseudo-area.js +27 -0
  113. package/dist/read/pseudo-area.js.map +1 -0
  114. package/dist/read/queries.d.ts +175 -19
  115. package/dist/read/queries.js +376 -32
  116. package/dist/read/queries.js.map +1 -1
  117. package/dist/read/scope.d.ts +126 -0
  118. package/dist/read/scope.js +161 -0
  119. package/dist/read/scope.js.map +1 -0
  120. package/dist/read/search-rank.d.ts +2 -5
  121. package/dist/read/shape.d.ts +181 -0
  122. package/dist/read/shape.js +737 -0
  123. package/dist/read/shape.js.map +1 -0
  124. package/dist/read/show-target.d.ts +9 -1
  125. package/dist/read/show-target.js +77 -17
  126. package/dist/read/show-target.js.map +1 -1
  127. package/dist/read/stage.d.ts +199 -0
  128. package/dist/read/stage.js +125 -0
  129. package/dist/read/stage.js.map +1 -0
  130. package/dist/read/truncation.d.ts +7 -12
  131. package/dist/read/truncation.js +56 -21
  132. package/dist/read/truncation.js.map +1 -1
  133. package/dist/read/views.d.ts +68 -16
  134. package/dist/read/views.js +206 -52
  135. package/dist/read/views.js.map +1 -1
  136. package/dist/surface-copy.d.ts +16 -0
  137. package/dist/surface-copy.js +16 -0
  138. package/dist/surface-copy.js.map +1 -1
  139. package/dist/write/batch.d.ts +49 -10
  140. package/dist/write/batch.js +423 -71
  141. package/dist/write/batch.js.map +1 -1
  142. package/dist/write/commands.js +268 -55
  143. package/dist/write/commands.js.map +1 -1
  144. package/dist/write/guards.d.ts +1 -1
  145. package/dist/write/guards.js +71 -10
  146. package/dist/write/guards.js.map +1 -1
  147. package/dist/write/heading.d.ts +10 -1
  148. package/dist/write/heading.js +35 -5
  149. package/dist/write/heading.js.map +1 -1
  150. package/dist/write/make-repeating-project.d.ts +2 -2
  151. package/dist/write/make-repeating-project.js +9 -9
  152. package/dist/write/make-repeating-project.js.map +1 -1
  153. package/dist/write/move.d.ts +134 -0
  154. package/dist/write/move.js +1874 -0
  155. package/dist/write/move.js.map +1 -0
  156. package/dist/write/operations.d.ts +105 -19
  157. package/dist/write/operations.js +56 -7
  158. package/dist/write/operations.js.map +1 -1
  159. package/dist/write/opid.d.ts +31 -0
  160. package/dist/write/opid.js +30 -0
  161. package/dist/write/opid.js.map +1 -0
  162. package/dist/write/pipeline.d.ts +70 -3
  163. package/dist/write/pipeline.js +198 -33
  164. package/dist/write/pipeline.js.map +1 -1
  165. package/dist/write/pre-state.d.ts +172 -8
  166. package/dist/write/pre-state.js +467 -25
  167. package/dist/write/pre-state.js.map +1 -1
  168. package/dist/write/reorder.d.ts +23 -3
  169. package/dist/write/reorder.js +2106 -86
  170. package/dist/write/reorder.js.map +1 -1
  171. package/dist/write/reversibility.js +19 -7
  172. package/dist/write/reversibility.js.map +1 -1
  173. package/dist/write/scope-guard.d.ts +31 -0
  174. package/dist/write/scope-guard.js +162 -0
  175. package/dist/write/scope-guard.js.map +1 -0
  176. package/dist/write/undo.js +120 -13
  177. package/dist/write/undo.js.map +1 -1
  178. package/dist/write/vectors/applescript.js +16 -7
  179. package/dist/write/vectors/applescript.js.map +1 -1
  180. package/dist/write/vectors/shortcuts.js +1 -1
  181. package/dist/write/vectors/shortcuts.js.map +1 -1
  182. package/dist/write/vectors/simulator.js +83 -27
  183. package/dist/write/vectors/simulator.js.map +1 -1
  184. package/dist/write/vectors/types.d.ts +11 -0
  185. package/dist/write/vectors/ui-certification.d.ts +3 -3
  186. package/dist/write/vectors/ui-certification.js +41 -5
  187. package/dist/write/vectors/ui-certification.js.map +1 -1
  188. package/dist/write/vectors/ui-drag.d.ts +21 -1
  189. package/dist/write/vectors/ui-drag.js +138 -27
  190. package/dist/write/vectors/ui-drag.js.map +1 -1
  191. package/dist/write/vectors/ui-recipes.d.ts +21 -1
  192. package/dist/write/vectors/ui-recipes.js +166 -7
  193. package/dist/write/vectors/ui-recipes.js.map +1 -1
  194. package/dist/write/vectors/ui.d.ts +25 -1
  195. package/dist/write/vectors/ui.js +142 -33
  196. package/dist/write/vectors/ui.js.map +1 -1
  197. package/dist/write/verify/delta.d.ts +17 -1
  198. package/dist/write/verify/delta.js +106 -29
  199. package/dist/write/verify/delta.js.map +1 -1
  200. package/package.json +7 -1
  201. package/schema/envelope.schema.json +397 -0
  202. package/skills/things-cli/SKILL.md +36 -17
  203. package/skills/things-cli/references/banner.md +35 -0
  204. package/skills/things-cli/references/contracts.md +32 -9
  205. package/skills/things-cli/references/errors.md +57 -0
  206. package/skills/things-cli/references/gui.md +1 -1
  207. package/skills/things-cli/references/model.md +18 -7
  208. package/skills/things-cli/references/ordering.md +88 -0
@@ -1,11 +1,13 @@
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, classifyHeadingConvert, classifyProjectRepeat, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, 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, headingConvertToProjectRecipe, 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";
9
11
  import { buildRepeatingFingerprint } from "./verify/delta.js";
10
12
  // ------------------------------------------------------------------ helpers
11
13
  function thingsUrl(command, params, token) {
@@ -236,6 +238,14 @@ const todoAdd = {
236
238
  * `when=` CLEARS an existing reminder (R07/R20), so when the caller
237
239
  * re-schedules to today/evening/a date without addressing the reminder we
238
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.
239
249
  */
240
250
  function effectiveReminder(pre, params) {
241
251
  if (params.reminder !== undefined)
@@ -249,6 +259,8 @@ function effectiveReminder(pre, params) {
249
259
  const target = pre.target;
250
260
  if (target === null || target.type === "heading")
251
261
  return null;
262
+ if (!reminderIsLive(target.startDate, pre.todayIso))
263
+ return null;
252
264
  return target.reminder;
253
265
  }
254
266
  function assertNotesModesExclusive(params) {
@@ -275,9 +287,10 @@ function expectedNotes(pre, params) {
275
287
  const todoUpdate = {
276
288
  op: "todo.update",
277
289
  hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
278
- preRead(db, params) {
290
+ preRead(db, params, now) {
279
291
  assertNotesModesExclusive(params);
280
292
  const pre = emptyPreState();
293
+ pre.todayIso = localToday(now);
281
294
  pre.target = loadTarget(db, params.uuid);
282
295
  return pre;
283
296
  },
@@ -351,11 +364,15 @@ const todoMove = {
351
364
  ],
352
365
  preRead(db, params) {
353
366
  const container = containerGiven(params.project) || containerGiven(params.area) || params.heading !== undefined;
354
- if (params.inbox === true && (container || params.detach === true)) {
355
- 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");
356
370
  }
357
- if (params.detach === true && container) {
358
- 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");
359
376
  }
360
377
  if (containerGiven(params.project) && containerGiven(params.area)) {
361
378
  throw new RangeError("project and area are exclusive destinations");
@@ -374,6 +391,16 @@ const todoMove = {
374
391
  }
375
392
  }
376
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
+ }
377
404
  if (containerGiven(params.area))
378
405
  pre.destArea = resolveArea(db, params.area);
379
406
  return pre;
@@ -385,7 +412,7 @@ const todoMove = {
385
412
  assert.push({ field: "start", equals: "inbox" }, { field: "startDate", equals: null });
386
413
  return { mode: "update", uuid: params.uuid, assert };
387
414
  }
388
- if (params.detach === true) {
415
+ if (params.loose === true) {
389
416
  // P21/P22: empty list-id strips every container link; the schedule is
390
417
  // untouched (pin it — a silent de-schedule would be a contrary write).
391
418
  const target = pre.target;
@@ -393,6 +420,16 @@ const todoMove = {
393
420
  assert.push({ field: "project", equals: null }, { field: "area", equals: null }, { field: "heading", equals: null }, { field: "startDate", equals: startDate });
394
421
  return { mode: "update", uuid: params.uuid, assert };
395
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
+ }
396
433
  const heading = pre.destHeading?.resolved;
397
434
  const project = pre.destProject?.resolved;
398
435
  const area = pre.destArea?.resolved;
@@ -415,13 +452,20 @@ const todoMove = {
415
452
  unsupportedVector(this.op, vector);
416
453
  return osa(`move to do id ${q(params.uuid)} to list "Inbox"`);
417
454
  }
418
- if (params.detach === true) {
455
+ if (params.loose === true) {
419
456
  // Empty list-id = clear the container (P21/P22) — URL only; the other
420
457
  // vectors reject or silently ignore container removal (P10/P11, P26).
421
458
  if (vector !== "url-scheme")
422
459
  unsupportedVector(this.op, vector);
423
460
  return thingsUrl("update", { id: params.uuid, "list-id": "" }, ctx.token);
424
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
+ }
425
469
  const project = pre.destProject?.resolved;
426
470
  const area = pre.destArea?.resolved;
427
471
  if (vector === "url-scheme") {
@@ -604,9 +648,10 @@ const projectAdd = {
604
648
  const projectUpdate = {
605
649
  op: "project.update",
606
650
  hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
607
- preRead(db, params) {
651
+ preRead(db, params, now) {
608
652
  assertNotesModesExclusive(params);
609
653
  const pre = emptyPreState();
654
+ pre.todayIso = localToday(now);
610
655
  pre.target = loadTarget(db, params.uuid);
611
656
  return pre;
612
657
  },
@@ -680,8 +725,8 @@ const projectMove = {
680
725
  op: "project.move",
681
726
  hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE"],
682
727
  preRead(db, params) {
683
- if ((params.detach === true) === containerGiven(params.area)) {
684
- 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");
685
730
  }
686
731
  const pre = emptyPreState();
687
732
  pre.target = loadTarget(db, params.uuid);
@@ -696,7 +741,7 @@ const projectMove = {
696
741
  mode: "update",
697
742
  uuid: params.uuid,
698
743
  assert: [
699
- params.detach === true
744
+ params.noArea === true
700
745
  ? { field: "area", equals: null }
701
746
  : { field: "area.uuid", equals: pre.destArea?.resolved?.uuid ?? "" },
702
747
  ],
@@ -704,14 +749,14 @@ const projectMove = {
704
749
  },
705
750
  compile(params, vector, pre, ctx) {
706
751
  if (vector === "url-scheme") {
707
- // 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
708
753
  // surface: AppleScript rejects missing value/"" and json-null no-ops).
709
754
  return thingsUrl("update-project", {
710
755
  id: params.uuid,
711
- "area-id": params.detach === true ? "" : (pre.destArea?.resolved?.uuid ?? ""),
756
+ "area-id": params.noArea === true ? "" : (pre.destArea?.resolved?.uuid ?? ""),
712
757
  }, ctx.token);
713
758
  }
714
- if (params.detach === true)
759
+ if (params.noArea === true)
715
760
  unsupportedVector(this.op, vector);
716
761
  return osa(`set area of project id ${q(params.uuid)} to area id ` +
717
762
  q(pre.destArea?.resolved?.uuid ?? ""));
@@ -953,10 +998,13 @@ const areaAdd = {
953
998
  };
954
999
  const areaDelete = {
955
1000
  op: "area.delete",
956
- hazards: ["H-UNKNOWN-DESTINATION", "H-PERMANENT-DELETE"],
1001
+ hazards: ["H-UNKNOWN-DESTINATION", "H-AREA-NOT-EMPTY", "H-PERMANENT-DELETE"],
957
1002
  preRead(db, params) {
958
1003
  const pre = emptyPreState();
959
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
+ }
960
1008
  return pre;
961
1009
  },
962
1010
  expectedDelta(pre) {
@@ -1184,7 +1232,7 @@ const reorder = {
1184
1232
  preRead(db, params, now) {
1185
1233
  const pre = emptyPreState();
1186
1234
  let containerUuid = null;
1187
- if (params.scope === "project" || params.scope === "headings") {
1235
+ if (params.scope === "project") {
1188
1236
  pre.destProject = resolveProject(db, params.container ?? {});
1189
1237
  containerUuid = pre.destProject.resolved?.uuid ?? null;
1190
1238
  }
@@ -1192,6 +1240,20 @@ const reorder = {
1192
1240
  pre.destArea = resolveArea(db, params.container ?? {});
1193
1241
  containerUuid = pre.destArea.resolved?.uuid ?? null;
1194
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
+ }
1195
1257
  pre.reorder = computeReorderPre(db, params, containerUuid, now);
1196
1258
  return pre;
1197
1259
  },
@@ -1209,15 +1271,24 @@ const reorder = {
1209
1271
  compile(params, vector, pre) {
1210
1272
  if (vector !== "applescript")
1211
1273
  unsupportedVector(this.op, vector);
1212
- 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"
1213
1278
  ? `project id ${q(pre.destProject?.resolved?.uuid ?? "")}`
1214
1279
  : params.scope === "area"
1215
1280
  ? `area id ${q(pre.destArea?.resolved?.uuid ?? "")}`
1216
- : params.scope === "inbox"
1217
- ? `list "Inbox"`
1218
- : params.scope === "someday"
1219
- ? `list "Someday"`
1220
- : `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
+ : params.scope === "upcoming"
1290
+ ? `list "Upcoming"`
1291
+ : `list "Today"`;
1221
1292
  const wire = pre.reorder?.wireList ?? params.uuids;
1222
1293
  if (params.scope === "someday") {
1223
1294
  // The Someday handler STACKS each sent id above the list's current top
@@ -1365,7 +1436,7 @@ function headingChildren(db, headingUuid) {
1365
1436
  return todos;
1366
1437
  }
1367
1438
  const headingRename = {
1368
- op: "heading.rename",
1439
+ op: "project.rename-heading",
1369
1440
  hazards: ["H-UNKNOWN-DESTINATION"],
1370
1441
  preRead(db, params) {
1371
1442
  const pre = emptyPreState();
@@ -1388,7 +1459,7 @@ const headingRename = {
1388
1459
  },
1389
1460
  };
1390
1461
  const headingArchive = {
1391
- op: "heading.archive",
1462
+ op: "project.archive-heading",
1392
1463
  hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-CHILDREN"],
1393
1464
  preRead(db, params) {
1394
1465
  const pre = emptyPreState();
@@ -1441,7 +1512,7 @@ const headingArchive = {
1441
1512
  },
1442
1513
  };
1443
1514
  const headingUnarchive = {
1444
- op: "heading.unarchive",
1515
+ op: "project.unarchive-heading",
1445
1516
  hazards: ["H-UNKNOWN-DESTINATION"],
1446
1517
  preRead(db, params) {
1447
1518
  const pre = emptyPreState();
@@ -1461,8 +1532,8 @@ const headingUnarchive = {
1461
1532
  return osa(`set status of to do id ${q(params.uuid)} to open`);
1462
1533
  },
1463
1534
  };
1464
- const headingCreate = {
1465
- op: "heading.create",
1535
+ const headingAdd = {
1536
+ op: "project.add-heading",
1466
1537
  hazards: ["H-UNKNOWN-DESTINATION"],
1467
1538
  preRead(db, params) {
1468
1539
  const pre = emptyPreState();
@@ -1497,6 +1568,32 @@ const headingCreate = {
1497
1568
  });
1498
1569
  },
1499
1570
  };
1571
+ const projectMoveHeading = {
1572
+ op: "project.move-heading",
1573
+ hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-ORDER"],
1574
+ preRead(db, params) {
1575
+ const pre = emptyPreState();
1576
+ pre.destProject = resolveProject(db, params.project);
1577
+ pre.headingMove = computeHeadingMovePre(db, pre.destProject, params.headings, params.placement);
1578
+ return pre;
1579
+ },
1580
+ expectedDelta(pre) {
1581
+ // The reorder wire re-ranks the FULL heading list; verify the whole target
1582
+ // order (strictly ascending "index"). Children follow their heading (scf
1583
+ // P1), so no per-child assertion is needed.
1584
+ return { mode: "ordering", key: "index", sequence: pre.headingMove?.targetOrder ?? [] };
1585
+ },
1586
+ compile(_params, vector, pre) {
1587
+ // The same native private-reorder wire the old `reorder --scope headings`
1588
+ // used (experimental — gated by allow-experimental + the sdef canary),
1589
+ // fed the computed full order.
1590
+ if (vector !== "applescript")
1591
+ unsupportedVector(this.op, vector);
1592
+ const order = pre.headingMove?.targetOrder ?? [];
1593
+ return osa(`${PRIVATE_REORDER_COMMAND} project id ${q(pre.destProject?.resolved?.uuid ?? "")} ` +
1594
+ `with ids ${q(order.join(","))}`);
1595
+ },
1596
+ };
1500
1597
  const todoClearDatedReminder = {
1501
1598
  op: "todo.clear-dated-reminder",
1502
1599
  hazards: ["H-UNKNOWN-DESTINATION", "H-NO-REMINDER"],
@@ -1573,14 +1670,17 @@ const todoMakeRepeating = {
1573
1670
  pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
1574
1671
  return pre;
1575
1672
  },
1576
- expectedDelta(pre, _params, ctx) {
1673
+ expectedDelta(pre, params, ctx) {
1577
1674
  // Identity REPLACEMENT or preserve-as-instance (UI2-a / RSIM-R): a NEW
1578
1675
  // template row (type=0 with a recurrence rule) is born; the original uuid is
1579
1676
  // EITHER destroyed OR relinked as the current-occurrence instance. Discover
1580
1677
  // the template with the create probe (excluding the pre-existing same-title
1581
1678
  // rows), pick it by asserting it IS a template, then the `repeating` context
1582
1679
  // hardens discovery (restored time-bound, source-fingerprint tiebreak) and
1583
- // derives instance + source fate for the enriched result.
1680
+ // derives instance + source fate for the enriched result. `expectedRule`
1681
+ // makes the LANDED rule verifiable: a template minted with the wrong
1682
+ // frequency/interval (the interval-field race, oddities §8l) becomes a
1683
+ // verify-failed:mismatch instead of a silent ok.
1584
1684
  return {
1585
1685
  mode: "create",
1586
1686
  probe: {
@@ -1592,6 +1692,11 @@ const todoMakeRepeating = {
1592
1692
  repeating: {
1593
1693
  sourceUuid: pre.target.uuid,
1594
1694
  fingerprint: buildRepeatingFingerprint(pre.target),
1695
+ expectedRule: {
1696
+ type: params.afterCompletion === true ? "after-completion" : "fixed",
1697
+ unit: params.frequency,
1698
+ interval: params.interval,
1699
+ },
1595
1700
  },
1596
1701
  }),
1597
1702
  },
@@ -1615,12 +1720,22 @@ const todoRescheduleRepeat = {
1615
1720
  },
1616
1721
  expectedDelta(_pre, params) {
1617
1722
  // Identity PRESERVED (UI2-b): the same template uuid, rule mutated in
1618
- // place. Assert the decoded rule's frequency + interval; ALSO capture the
1619
- // whole prior rule (+ deadline flag) so the undo can re-drive it faithfully.
1723
+ // place. Assert the decoded rule's TYPE + frequency + interval; ALSO capture
1724
+ // the whole prior rule (+ deadline flag) so the undo can re-drive it
1725
+ // faithfully. Asserting `type` (fixed vs after-completion) is what makes a
1726
+ // fixed→after-completion conversion VERIFIABLE at all: in the field-report
1727
+ // incident (0½ item 1) both rules were weekly/interval-2, so unit+interval
1728
+ // never changed — only the type flipped, and without this assertion verify
1729
+ // could neither confirm the conversion landed nor tell the pre-drive
1730
+ // idempotency check that the target had not yet been reached.
1620
1731
  return {
1621
1732
  mode: "update",
1622
1733
  uuid: params.uuid,
1623
1734
  assert: [
1735
+ {
1736
+ field: "repeating.rule.type",
1737
+ equals: params.afterCompletion === true ? "after-completion" : "fixed",
1738
+ },
1624
1739
  { field: "repeating.rule.unit", equals: params.frequency },
1625
1740
  { field: "repeating.rule.interval", equals: params.interval },
1626
1741
  ],
@@ -1694,11 +1809,17 @@ const projectRescheduleRepeat = {
1694
1809
  },
1695
1810
  expectedDelta(_pre, params) {
1696
1811
  // Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
1697
- // capture the prior rule (+ deadline flag) for the faithful undo.
1812
+ // capture the prior rule (+ deadline flag) for the faithful undo. Assert the
1813
+ // rule TYPE too (fixed vs after-completion) so a conversion that leaves
1814
+ // unit+interval unchanged is still verifiable — see todo.reschedule-repeat.
1698
1815
  return {
1699
1816
  mode: "update",
1700
1817
  uuid: params.uuid,
1701
1818
  assert: [
1819
+ {
1820
+ field: "repeating.rule.type",
1821
+ equals: params.afterCompletion === true ? "after-completion" : "fixed",
1822
+ },
1702
1823
  { field: "repeating.rule.unit", equals: params.frequency },
1703
1824
  { field: "repeating.rule.interval", equals: params.interval },
1704
1825
  ],
@@ -1774,13 +1895,14 @@ const projectMakeRepeating = {
1774
1895
  pre.repeatSubtreeUuids = projectSubtreeUuids(db, pre.target.uuid);
1775
1896
  return pre;
1776
1897
  },
1777
- expectedDelta(pre, _params, ctx) {
1898
+ expectedDelta(pre, params, ctx) {
1778
1899
  // Identity REPLACEMENT or preserve-as-instance (UIC4-b / RSIM-R): a NEW
1779
1900
  // template project (with a recurrence rule) is born; the source project is
1780
1901
  // EITHER destroyed OR (when its subtree holds a nested repeater) relinked as
1781
1902
  // the current-occurrence instance. Pick the TEMPLATE by asserting it IS one,
1782
1903
  // excluding pre-existing same-title rows; the `repeating` context hardens
1783
1904
  // discovery and derives instance + source fate + childrenReplaced.
1905
+ // `expectedRule` makes the landed rule verifiable (interval-race guard, §8l).
1784
1906
  return {
1785
1907
  mode: "create",
1786
1908
  probe: {
@@ -1793,6 +1915,11 @@ const projectMakeRepeating = {
1793
1915
  sourceUuid: pre.target.uuid,
1794
1916
  fingerprint: buildRepeatingFingerprint(pre.target),
1795
1917
  subtreeUuids: pre.repeatSubtreeUuids ?? [],
1918
+ expectedRule: {
1919
+ type: params.afterCompletion === true ? "after-completion" : "fixed",
1920
+ unit: params.frequency,
1921
+ interval: params.interval,
1922
+ },
1796
1923
  },
1797
1924
  }),
1798
1925
  },
@@ -1810,23 +1937,23 @@ const projectMakeRepeating = {
1810
1937
  return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
1811
1938
  },
1812
1939
  };
1813
- // project.create-repeating is delivered by the runCreateRepeatingProject
1940
+ // project.add-repeating is delivered by the runAddRepeatingProject
1814
1941
  // orchestrator (project.add THEN project.make-repeating); it has no single
1815
1942
  // atomic surface and is never dispatched directly through the pipeline.
1816
- const CREATE_REPEATING_ORCHESTRATED = "project.create-repeating is delivered by the runCreateRepeatingProject orchestrator (create " +
1943
+ const ADD_REPEATING_ORCHESTRATED = "project.add-repeating is delivered by the runAddRepeatingProject orchestrator (create " +
1817
1944
  "the project, then promote it to a repeating series); it has no atomic surface and is never " +
1818
1945
  "dispatched directly through the pipeline";
1819
- const projectCreateRepeating = {
1820
- op: "project.create-repeating",
1946
+ const projectAddRepeating = {
1947
+ op: "project.add-repeating",
1821
1948
  hazards: [],
1822
1949
  preRead() {
1823
1950
  return emptyPreState();
1824
1951
  },
1825
1952
  expectedDelta() {
1826
- throw new Error(CREATE_REPEATING_ORCHESTRATED);
1953
+ throw new Error(ADD_REPEATING_ORCHESTRATED);
1827
1954
  },
1828
1955
  compile() {
1829
- throw new Error(CREATE_REPEATING_ORCHESTRATED);
1956
+ throw new Error(ADD_REPEATING_ORCHESTRATED);
1830
1957
  },
1831
1958
  };
1832
1959
  const todoConvertToProject = {
@@ -1863,7 +1990,7 @@ const todoConvertToProject = {
1863
1990
  },
1864
1991
  };
1865
1992
  const headingConvertToProject = {
1866
- op: "heading.convert-to-project",
1993
+ op: "project.promote-heading",
1867
1994
  hazards: UI_HAZARDS,
1868
1995
  preRead(db, params) {
1869
1996
  const pre = emptyPreState();
@@ -1901,11 +2028,76 @@ const headingConvertToProject = {
1901
2028
  // H-UNKNOWN-DESTINATION blocks a non-heading target before compile; a
1902
2029
  // refuse here means a parentless heading (or one absent from its project's
1903
2030
  // heading set) slipped past — fail loud rather than drive the wrong row.
1904
- throw new Error(`heading.convert-to-project: ${tax?.kind === "refuse" ? tax.detail : "no heading taxonomy resolved (guard bypassed?)"}`);
2031
+ throw new Error(`project.promote-heading: ${tax?.kind === "refuse" ? tax.detail : "no heading taxonomy resolved (guard bypassed?)"}`);
1905
2032
  }
1906
2033
  return uiDrive(headingConvertToProjectRecipe(tax.projectReveal, tax.ordinal));
1907
2034
  },
1908
2035
  };
2036
+ const projectMoveHeadingToProject = {
2037
+ op: "project.move-heading-to-project",
2038
+ hazards: UI_HAZARDS,
2039
+ preRead(db, params) {
2040
+ const pre = emptyPreState();
2041
+ pre.headingMoveToProject = classifyHeadingMoveToProject(db, params.project, params.heading, params.toProject);
2042
+ if (pre.headingMoveToProject.kind === "ok") {
2043
+ // Load the heading row so the verify oracle reads its (post-op) project FK.
2044
+ pre.target = loadTarget(db, pre.headingMoveToProject.pre.headingUuid);
2045
+ }
2046
+ return pre;
2047
+ },
2048
+ expectedDelta(pre) {
2049
+ // HEADXPROJ: a single-row change — the heading's `project` FK becomes the
2050
+ // destination; its children follow via their intact heading FK (no child
2051
+ // rewrite, no index churn), so no per-child assertion is needed.
2052
+ const tax = pre.headingMoveToProject;
2053
+ const headingUuid = tax?.kind === "ok" ? tax.pre.headingUuid : "";
2054
+ const destUuid = tax?.kind === "ok" ? tax.pre.destProjectUuid : "";
2055
+ return {
2056
+ mode: "update",
2057
+ uuid: headingUuid,
2058
+ assert: [{ field: "project.uuid", equals: destUuid }],
2059
+ };
2060
+ },
2061
+ compile(_params, vector, pre) {
2062
+ if (vector !== "ui")
2063
+ unsupportedVector(this.op, vector);
2064
+ const tax = pre.headingMoveToProject;
2065
+ if (tax === null || tax.kind === "refuse") {
2066
+ // The taxonomy refusals are surfaced by H-UNKNOWN-DESTINATION before
2067
+ // compile; reaching here with one means the guard was bypassed.
2068
+ throw new Error(`project.move-heading-to-project: ${tax?.kind === "refuse" ? tax.detail : "no taxonomy resolved (guard bypassed?)"}`);
2069
+ }
2070
+ return uiDrive(moveHeadingToProjectRecipe(tax.pre.sourceProjectUuid, tax.pre.headingTitle, tax.pre.destProjectTitle));
2071
+ },
2072
+ };
2073
+ const projectDissolveHeading = {
2074
+ op: "project.dissolve-heading",
2075
+ hazards: UI_HAZARDS,
2076
+ preRead(db, params) {
2077
+ const pre = emptyPreState();
2078
+ pre.target = loadTarget(db, params.uuid);
2079
+ pre.headingDissolve = classifyHeadingDissolve(db, pre.target);
2080
+ return pre;
2081
+ },
2082
+ expectedDelta(_pre, params) {
2083
+ // DISS1: the heading row is HARD-DELETED (removed from TMTask) while its
2084
+ // children become direct project children. The verify oracle is the heading
2085
+ // GONE — its children re-home is the DISS1-locked invariant, checked by the
2086
+ // op's tests (a childless heading dissolve verifies identically).
2087
+ return { mode: "gone", entity: "task", uuid: params.uuid };
2088
+ },
2089
+ compile(_params, vector, pre) {
2090
+ if (vector !== "ui")
2091
+ unsupportedVector(this.op, vector);
2092
+ const tax = pre.headingDissolve;
2093
+ if (tax === null || tax.kind === "refuse") {
2094
+ // The taxonomy refusals are surfaced by H-UNKNOWN-DESTINATION before
2095
+ // compile; reaching here with one means the guard was bypassed.
2096
+ throw new Error(`project.dissolve-heading: ${tax?.kind === "refuse" ? tax.detail : "no taxonomy resolved (guard bypassed?)"}`);
2097
+ }
2098
+ return uiDrive(dissolveHeadingRecipe(tax.pre.projectReveal, tax.pre.headingTitle));
2099
+ },
2100
+ };
1909
2101
  // -------------------------------------------------- sidebar AREA reorder
1910
2102
  /** The compile-time placement for area.reorder (resolved refs). */
1911
2103
  function sidebarPlacementOf(params, pre) {
@@ -1931,6 +2123,16 @@ const areaReorderSidebar = {
1931
2123
  params.position !== "last") {
1932
2124
  throw new RangeError(`invalid position "${params.position}" — expected first | last`);
1933
2125
  }
2126
+ // The reserved `loose` pseudo-area ref (the derived area-less view) has no
2127
+ // sidebar row, so it can be neither moved nor an anchor — refuse it by name
2128
+ // with a specific message rather than a generic no-such-area error, matching
2129
+ // the loose-pseudo-area write-refusal pattern (src/read/pseudo-area.ts).
2130
+ for (const ref of [params.target, params.before, params.after]) {
2131
+ if (ref !== undefined && isLooseRef(ref)) {
2132
+ throw new RangeError("the loose pseudo-area is a derived view — it has no sidebar row and cannot be " +
2133
+ "reordered");
2134
+ }
2135
+ }
1934
2136
  const pre = emptyPreState();
1935
2137
  pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
1936
2138
  const ref = params.before ?? params.after;
@@ -1941,17 +2143,25 @@ const areaReorderSidebar = {
1941
2143
  if (target != null && dest != null && target.uuid === dest.uuid) {
1942
2144
  throw new RangeError("the destination area is the area being moved");
1943
2145
  }
1944
- // The sidebar drag addresses rows by their VISIBLE NAME a duplicated
1945
- // area title makes the row ambiguous, so refuse up front.
2146
+ // The sidebar drag addresses rows by their VISIBLE NAME. A DUPLICATE area
2147
+ // title is now handled by positional disambiguation in the ui-vector driver
2148
+ // (ORDFIN2 AXDRAG3: the intended uuid's Nth same-titled row is grabbed by the
2149
+ // `(index, uuid)` ASC law, and the post-gesture DB assert + self-invert catch
2150
+ // a wrong grab) — so a uuid-targeted ref whose title is shared is NO LONGER
2151
+ // refused here. A duplicate NAME ref stays refused upstream (resolveArea
2152
+ // returns the ambiguity candidates). Only a SANITY CAP remains: too many
2153
+ // same-titled rows could make the positional grab loop unreliable, so refuse
2154
+ // rather than churn. (WIRED — lab-cert pending an AXDRAG4 VM sitting.)
2155
+ const SAME_TITLE_CAP = 8;
1946
2156
  for (const area of [target, dest]) {
1947
2157
  if (area == null)
1948
2158
  continue;
1949
2159
  const dup = db
1950
2160
  .prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
1951
2161
  .get(area.title);
1952
- if (dup.n > 1) {
1953
- throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas the sidebar move addresses ` +
1954
- "areas by their visible name; rename one first");
2162
+ if (dup.n > SAME_TITLE_CAP) {
2163
+ throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas (over the ${SAME_TITLE_CAP}-row ` +
2164
+ "positional-disambiguation cap) rename some, or reorder by a uniquely-titled anchor");
1955
2165
  }
1956
2166
  }
1957
2167
  pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
@@ -2044,22 +2254,25 @@ export const COMMANDS = {
2044
2254
  "project.set-tags": projectSetTags,
2045
2255
  "todo.backdate": todoBackdate,
2046
2256
  "todo.add-logged": todoAddLogged,
2047
- "heading.rename": headingRename,
2048
- "heading.archive": headingArchive,
2049
- "heading.unarchive": headingUnarchive,
2050
- "heading.create": headingCreate,
2257
+ "project.add-heading": headingAdd,
2258
+ "project.rename-heading": headingRename,
2259
+ "project.archive-heading": headingArchive,
2260
+ "project.unarchive-heading": headingUnarchive,
2261
+ "project.promote-heading": headingConvertToProject,
2262
+ "project.move-heading": projectMoveHeading,
2263
+ "project.move-heading-to-project": projectMoveHeadingToProject,
2264
+ "project.dissolve-heading": projectDissolveHeading,
2051
2265
  "todo.clear-dated-reminder": todoClearDatedReminder,
2052
2266
  "todo.make-repeating": todoMakeRepeating,
2053
2267
  "todo.reschedule-repeat": todoRescheduleRepeat,
2054
2268
  "todo.pause-repeat": todoPauseRepeat,
2055
2269
  "todo.resume-repeat": todoResumeRepeat,
2056
2270
  "todo.convert-to-project": todoConvertToProject,
2057
- "heading.convert-to-project": headingConvertToProject,
2058
2271
  "project.reschedule-repeat": projectRescheduleRepeat,
2059
2272
  "project.pause-repeat": projectPauseRepeat,
2060
2273
  "project.resume-repeat": projectResumeRepeat,
2061
2274
  "area.reorder": areaReorderSidebar,
2062
2275
  "project.make-repeating": projectMakeRepeating,
2063
- "project.create-repeating": projectCreateRepeating,
2276
+ "project.add-repeating": projectAddRepeating,
2064
2277
  };
2065
2278
  //# sourceMappingURL=commands.js.map