things-api 0.11.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 (192) 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/commands/area.d.ts +18 -11
  5. package/dist/cli/commands/area.js +55 -67
  6. package/dist/cli/commands/area.js.map +1 -1
  7. package/dist/cli/commands/doctor.d.ts +1 -2
  8. package/dist/cli/commands/doctor.js +11 -1
  9. package/dist/cli/commands/doctor.js.map +1 -1
  10. package/dist/cli/commands/install-skill.d.ts +19 -4
  11. package/dist/cli/commands/install-skill.js +101 -52
  12. package/dist/cli/commands/install-skill.js.map +1 -1
  13. package/dist/cli/commands/mcp.js +9 -0
  14. package/dist/cli/commands/mcp.js.map +1 -1
  15. package/dist/cli/commands/project.d.ts +1 -0
  16. package/dist/cli/commands/project.js +20 -28
  17. package/dist/cli/commands/project.js.map +1 -1
  18. package/dist/cli/commands/reads.js +100 -28
  19. package/dist/cli/commands/reads.js.map +1 -1
  20. package/dist/cli/commands/show.js +32 -8
  21. package/dist/cli/commands/show.js.map +1 -1
  22. package/dist/cli/commands/todo.js +1 -1
  23. package/dist/cli/commands/todo.js.map +1 -1
  24. package/dist/cli/commands/writes.js +769 -143
  25. package/dist/cli/commands/writes.js.map +1 -1
  26. package/dist/cli/did-you-mean.d.ts +14 -3
  27. package/dist/cli/did-you-mean.js +26 -3
  28. package/dist/cli/did-you-mean.js.map +1 -1
  29. package/dist/cli/glyphs.d.ts +16 -2
  30. package/dist/cli/glyphs.js +60 -12
  31. package/dist/cli/glyphs.js.map +1 -1
  32. package/dist/cli/help.js +67 -12
  33. package/dist/cli/help.js.map +1 -1
  34. package/dist/cli/main.js +1 -1
  35. package/dist/cli/main.js.map +1 -1
  36. package/dist/cli/move-hint.d.ts +3 -2
  37. package/dist/cli/move-hint.js +2 -2
  38. package/dist/cli/move-hint.js.map +1 -1
  39. package/dist/cli/read-driver.d.ts +43 -5
  40. package/dist/cli/read-driver.js +81 -15
  41. package/dist/cli/read-driver.js.map +1 -1
  42. package/dist/cli/render.d.ts +19 -8
  43. package/dist/cli/render.js +59 -25
  44. package/dist/cli/render.js.map +1 -1
  45. package/dist/cli/resolve-invocation.d.ts +2 -2
  46. package/dist/cli/resolve-invocation.js +3 -3
  47. package/dist/cli/resolve-invocation.js.map +1 -1
  48. package/dist/cli/skill-check.d.ts +12 -6
  49. package/dist/cli/skill-check.js +31 -14
  50. package/dist/cli/skill-check.js.map +1 -1
  51. package/dist/cli/skill.d.ts +8 -0
  52. package/dist/cli/skill.js +10 -0
  53. package/dist/cli/skill.js.map +1 -1
  54. package/dist/cli/verb-hint.js +1 -1
  55. package/dist/cli/verb-hint.js.map +1 -1
  56. package/dist/client.d.ts +140 -19
  57. package/dist/client.js +253 -38
  58. package/dist/client.js.map +1 -1
  59. package/dist/config.d.ts +66 -2
  60. package/dist/config.js +120 -12
  61. package/dist/config.js.map +1 -1
  62. package/dist/contracts.d.ts +176 -28
  63. package/dist/contracts.js +22 -1
  64. package/dist/contracts.js.map +1 -1
  65. package/dist/diagnose.d.ts +34 -2
  66. package/dist/diagnose.js +36 -1
  67. package/dist/diagnose.js.map +1 -1
  68. package/dist/index.d.ts +19 -8
  69. package/dist/index.js +19 -3
  70. package/dist/index.js.map +1 -1
  71. package/dist/mcp/server.d.ts +9 -0
  72. package/dist/mcp/server.js +733 -304
  73. package/dist/mcp/server.js.map +1 -1
  74. package/dist/model/entities.d.ts +46 -3
  75. package/dist/model/entities.js.map +1 -1
  76. package/dist/model/mappers.d.ts +2 -0
  77. package/dist/model/mappers.js +41 -2
  78. package/dist/model/mappers.js.map +1 -1
  79. package/dist/model/recurrence.d.ts +8 -1
  80. package/dist/model/recurrence.js.map +1 -1
  81. package/dist/read/area-filter.d.ts +76 -0
  82. package/dist/read/area-filter.js +59 -0
  83. package/dist/read/area-filter.js.map +1 -0
  84. package/dist/read/area-view.d.ts +11 -11
  85. package/dist/read/area-view.js +50 -28
  86. package/dist/read/area-view.js.map +1 -1
  87. package/dist/read/detail.js +15 -8
  88. package/dist/read/detail.js.map +1 -1
  89. package/dist/read/predicates.d.ts +18 -0
  90. package/dist/read/predicates.js +19 -0
  91. package/dist/read/predicates.js.map +1 -1
  92. package/dist/read/project-view.d.ts +31 -15
  93. package/dist/read/project-view.js +72 -39
  94. package/dist/read/project-view.js.map +1 -1
  95. package/dist/read/pseudo-area.d.ts +29 -0
  96. package/dist/read/pseudo-area.js +27 -0
  97. package/dist/read/pseudo-area.js.map +1 -0
  98. package/dist/read/queries.d.ts +105 -18
  99. package/dist/read/queries.js +185 -30
  100. package/dist/read/queries.js.map +1 -1
  101. package/dist/read/scope.d.ts +126 -0
  102. package/dist/read/scope.js +162 -0
  103. package/dist/read/scope.js.map +1 -0
  104. package/dist/read/search-rank.d.ts +2 -5
  105. package/dist/read/shape.d.ts +162 -0
  106. package/dist/read/shape.js +686 -0
  107. package/dist/read/shape.js.map +1 -0
  108. package/dist/read/show-target.d.ts +9 -1
  109. package/dist/read/show-target.js +45 -4
  110. package/dist/read/show-target.js.map +1 -1
  111. package/dist/read/stage.d.ts +199 -0
  112. package/dist/read/stage.js +125 -0
  113. package/dist/read/stage.js.map +1 -0
  114. package/dist/read/truncation.d.ts +7 -5
  115. package/dist/read/truncation.js +33 -8
  116. package/dist/read/truncation.js.map +1 -1
  117. package/dist/read/views.d.ts +49 -9
  118. package/dist/read/views.js +193 -40
  119. package/dist/read/views.js.map +1 -1
  120. package/dist/surface-copy.d.ts +9 -0
  121. package/dist/surface-copy.js +9 -0
  122. package/dist/surface-copy.js.map +1 -1
  123. package/dist/write/batch.d.ts +49 -10
  124. package/dist/write/batch.js +423 -71
  125. package/dist/write/batch.js.map +1 -1
  126. package/dist/write/commands.js +266 -55
  127. package/dist/write/commands.js.map +1 -1
  128. package/dist/write/guards.d.ts +1 -1
  129. package/dist/write/guards.js +71 -10
  130. package/dist/write/guards.js.map +1 -1
  131. package/dist/write/heading.d.ts +10 -1
  132. package/dist/write/heading.js +35 -5
  133. package/dist/write/heading.js.map +1 -1
  134. package/dist/write/make-repeating-project.d.ts +2 -2
  135. package/dist/write/make-repeating-project.js +9 -9
  136. package/dist/write/make-repeating-project.js.map +1 -1
  137. package/dist/write/move.d.ts +130 -0
  138. package/dist/write/move.js +1533 -0
  139. package/dist/write/move.js.map +1 -0
  140. package/dist/write/operations.d.ts +105 -19
  141. package/dist/write/operations.js +56 -7
  142. package/dist/write/operations.js.map +1 -1
  143. package/dist/write/opid.d.ts +31 -0
  144. package/dist/write/opid.js +30 -0
  145. package/dist/write/opid.js.map +1 -0
  146. package/dist/write/pipeline.d.ts +70 -3
  147. package/dist/write/pipeline.js +198 -33
  148. package/dist/write/pipeline.js.map +1 -1
  149. package/dist/write/pre-state.d.ts +162 -8
  150. package/dist/write/pre-state.js +385 -20
  151. package/dist/write/pre-state.js.map +1 -1
  152. package/dist/write/reorder.d.ts +23 -3
  153. package/dist/write/reorder.js +1871 -85
  154. package/dist/write/reorder.js.map +1 -1
  155. package/dist/write/reversibility.js +19 -7
  156. package/dist/write/reversibility.js.map +1 -1
  157. package/dist/write/scope-guard.d.ts +31 -0
  158. package/dist/write/scope-guard.js +162 -0
  159. package/dist/write/scope-guard.js.map +1 -0
  160. package/dist/write/undo.js +120 -13
  161. package/dist/write/undo.js.map +1 -1
  162. package/dist/write/vectors/applescript.js +16 -7
  163. package/dist/write/vectors/applescript.js.map +1 -1
  164. package/dist/write/vectors/shortcuts.js +1 -1
  165. package/dist/write/vectors/shortcuts.js.map +1 -1
  166. package/dist/write/vectors/simulator.js +83 -27
  167. package/dist/write/vectors/simulator.js.map +1 -1
  168. package/dist/write/vectors/types.d.ts +11 -0
  169. package/dist/write/vectors/ui-certification.d.ts +3 -3
  170. package/dist/write/vectors/ui-certification.js +41 -5
  171. package/dist/write/vectors/ui-certification.js.map +1 -1
  172. package/dist/write/vectors/ui-drag.d.ts +21 -1
  173. package/dist/write/vectors/ui-drag.js +138 -27
  174. package/dist/write/vectors/ui-drag.js.map +1 -1
  175. package/dist/write/vectors/ui-recipes.d.ts +21 -1
  176. package/dist/write/vectors/ui-recipes.js +166 -7
  177. package/dist/write/vectors/ui-recipes.js.map +1 -1
  178. package/dist/write/vectors/ui.d.ts +25 -1
  179. package/dist/write/vectors/ui.js +142 -33
  180. package/dist/write/vectors/ui.js.map +1 -1
  181. package/dist/write/verify/delta.d.ts +17 -1
  182. package/dist/write/verify/delta.js +106 -29
  183. package/dist/write/verify/delta.js.map +1 -1
  184. package/package.json +7 -1
  185. package/schema/envelope.schema.json +383 -0
  186. package/skills/things-cli/SKILL.md +34 -17
  187. package/skills/things-cli/references/banner.md +35 -0
  188. package/skills/things-cli/references/contracts.md +30 -9
  189. package/skills/things-cli/references/errors.md +49 -0
  190. package/skills/things-cli/references/gui.md +1 -1
  191. package/skills/things-cli/references/model.md +15 -6
  192. package/skills/things-cli/references/ordering.md +71 -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,22 @@ 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
+ : `list "Today"`;
1221
1290
  const wire = pre.reorder?.wireList ?? params.uuids;
1222
1291
  if (params.scope === "someday") {
1223
1292
  // The Someday handler STACKS each sent id above the list's current top
@@ -1365,7 +1434,7 @@ function headingChildren(db, headingUuid) {
1365
1434
  return todos;
1366
1435
  }
1367
1436
  const headingRename = {
1368
- op: "heading.rename",
1437
+ op: "project.rename-heading",
1369
1438
  hazards: ["H-UNKNOWN-DESTINATION"],
1370
1439
  preRead(db, params) {
1371
1440
  const pre = emptyPreState();
@@ -1388,7 +1457,7 @@ const headingRename = {
1388
1457
  },
1389
1458
  };
1390
1459
  const headingArchive = {
1391
- op: "heading.archive",
1460
+ op: "project.archive-heading",
1392
1461
  hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-CHILDREN"],
1393
1462
  preRead(db, params) {
1394
1463
  const pre = emptyPreState();
@@ -1441,7 +1510,7 @@ const headingArchive = {
1441
1510
  },
1442
1511
  };
1443
1512
  const headingUnarchive = {
1444
- op: "heading.unarchive",
1513
+ op: "project.unarchive-heading",
1445
1514
  hazards: ["H-UNKNOWN-DESTINATION"],
1446
1515
  preRead(db, params) {
1447
1516
  const pre = emptyPreState();
@@ -1461,8 +1530,8 @@ const headingUnarchive = {
1461
1530
  return osa(`set status of to do id ${q(params.uuid)} to open`);
1462
1531
  },
1463
1532
  };
1464
- const headingCreate = {
1465
- op: "heading.create",
1533
+ const headingAdd = {
1534
+ op: "project.add-heading",
1466
1535
  hazards: ["H-UNKNOWN-DESTINATION"],
1467
1536
  preRead(db, params) {
1468
1537
  const pre = emptyPreState();
@@ -1497,6 +1566,32 @@ const headingCreate = {
1497
1566
  });
1498
1567
  },
1499
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
+ };
1500
1595
  const todoClearDatedReminder = {
1501
1596
  op: "todo.clear-dated-reminder",
1502
1597
  hazards: ["H-UNKNOWN-DESTINATION", "H-NO-REMINDER"],
@@ -1573,14 +1668,17 @@ const todoMakeRepeating = {
1573
1668
  pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
1574
1669
  return pre;
1575
1670
  },
1576
- expectedDelta(pre, _params, ctx) {
1671
+ expectedDelta(pre, params, ctx) {
1577
1672
  // Identity REPLACEMENT or preserve-as-instance (UI2-a / RSIM-R): a NEW
1578
1673
  // template row (type=0 with a recurrence rule) is born; the original uuid is
1579
1674
  // EITHER destroyed OR relinked as the current-occurrence instance. Discover
1580
1675
  // the template with the create probe (excluding the pre-existing same-title
1581
1676
  // rows), pick it by asserting it IS a template, then the `repeating` context
1582
1677
  // hardens discovery (restored time-bound, source-fingerprint tiebreak) and
1583
- // derives instance + source fate for the enriched result.
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.
1584
1682
  return {
1585
1683
  mode: "create",
1586
1684
  probe: {
@@ -1592,6 +1690,11 @@ const todoMakeRepeating = {
1592
1690
  repeating: {
1593
1691
  sourceUuid: pre.target.uuid,
1594
1692
  fingerprint: buildRepeatingFingerprint(pre.target),
1693
+ expectedRule: {
1694
+ type: params.afterCompletion === true ? "after-completion" : "fixed",
1695
+ unit: params.frequency,
1696
+ interval: params.interval,
1697
+ },
1595
1698
  },
1596
1699
  }),
1597
1700
  },
@@ -1615,12 +1718,22 @@ const todoRescheduleRepeat = {
1615
1718
  },
1616
1719
  expectedDelta(_pre, params) {
1617
1720
  // 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.
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.
1620
1729
  return {
1621
1730
  mode: "update",
1622
1731
  uuid: params.uuid,
1623
1732
  assert: [
1733
+ {
1734
+ field: "repeating.rule.type",
1735
+ equals: params.afterCompletion === true ? "after-completion" : "fixed",
1736
+ },
1624
1737
  { field: "repeating.rule.unit", equals: params.frequency },
1625
1738
  { field: "repeating.rule.interval", equals: params.interval },
1626
1739
  ],
@@ -1694,11 +1807,17 @@ const projectRescheduleRepeat = {
1694
1807
  },
1695
1808
  expectedDelta(_pre, params) {
1696
1809
  // Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
1697
- // 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.
1698
1813
  return {
1699
1814
  mode: "update",
1700
1815
  uuid: params.uuid,
1701
1816
  assert: [
1817
+ {
1818
+ field: "repeating.rule.type",
1819
+ equals: params.afterCompletion === true ? "after-completion" : "fixed",
1820
+ },
1702
1821
  { field: "repeating.rule.unit", equals: params.frequency },
1703
1822
  { field: "repeating.rule.interval", equals: params.interval },
1704
1823
  ],
@@ -1774,13 +1893,14 @@ const projectMakeRepeating = {
1774
1893
  pre.repeatSubtreeUuids = projectSubtreeUuids(db, pre.target.uuid);
1775
1894
  return pre;
1776
1895
  },
1777
- expectedDelta(pre, _params, ctx) {
1896
+ expectedDelta(pre, params, ctx) {
1778
1897
  // Identity REPLACEMENT or preserve-as-instance (UIC4-b / RSIM-R): a NEW
1779
1898
  // template project (with a recurrence rule) is born; the source project is
1780
1899
  // EITHER destroyed OR (when its subtree holds a nested repeater) relinked as
1781
1900
  // the current-occurrence instance. Pick the TEMPLATE by asserting it IS one,
1782
1901
  // excluding pre-existing same-title rows; the `repeating` context hardens
1783
1902
  // discovery and derives instance + source fate + childrenReplaced.
1903
+ // `expectedRule` makes the landed rule verifiable (interval-race guard, §8l).
1784
1904
  return {
1785
1905
  mode: "create",
1786
1906
  probe: {
@@ -1793,6 +1913,11 @@ const projectMakeRepeating = {
1793
1913
  sourceUuid: pre.target.uuid,
1794
1914
  fingerprint: buildRepeatingFingerprint(pre.target),
1795
1915
  subtreeUuids: pre.repeatSubtreeUuids ?? [],
1916
+ expectedRule: {
1917
+ type: params.afterCompletion === true ? "after-completion" : "fixed",
1918
+ unit: params.frequency,
1919
+ interval: params.interval,
1920
+ },
1796
1921
  },
1797
1922
  }),
1798
1923
  },
@@ -1810,23 +1935,23 @@ const projectMakeRepeating = {
1810
1935
  return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
1811
1936
  },
1812
1937
  };
1813
- // project.create-repeating is delivered by the runCreateRepeatingProject
1938
+ // project.add-repeating is delivered by the runAddRepeatingProject
1814
1939
  // orchestrator (project.add THEN project.make-repeating); it has no single
1815
1940
  // 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 " +
1941
+ const ADD_REPEATING_ORCHESTRATED = "project.add-repeating is delivered by the runAddRepeatingProject orchestrator (create " +
1817
1942
  "the project, then promote it to a repeating series); it has no atomic surface and is never " +
1818
1943
  "dispatched directly through the pipeline";
1819
- const projectCreateRepeating = {
1820
- op: "project.create-repeating",
1944
+ const projectAddRepeating = {
1945
+ op: "project.add-repeating",
1821
1946
  hazards: [],
1822
1947
  preRead() {
1823
1948
  return emptyPreState();
1824
1949
  },
1825
1950
  expectedDelta() {
1826
- throw new Error(CREATE_REPEATING_ORCHESTRATED);
1951
+ throw new Error(ADD_REPEATING_ORCHESTRATED);
1827
1952
  },
1828
1953
  compile() {
1829
- throw new Error(CREATE_REPEATING_ORCHESTRATED);
1954
+ throw new Error(ADD_REPEATING_ORCHESTRATED);
1830
1955
  },
1831
1956
  };
1832
1957
  const todoConvertToProject = {
@@ -1863,7 +1988,7 @@ const todoConvertToProject = {
1863
1988
  },
1864
1989
  };
1865
1990
  const headingConvertToProject = {
1866
- op: "heading.convert-to-project",
1991
+ op: "project.promote-heading",
1867
1992
  hazards: UI_HAZARDS,
1868
1993
  preRead(db, params) {
1869
1994
  const pre = emptyPreState();
@@ -1901,11 +2026,76 @@ const headingConvertToProject = {
1901
2026
  // H-UNKNOWN-DESTINATION blocks a non-heading target before compile; a
1902
2027
  // refuse here means a parentless heading (or one absent from its project's
1903
2028
  // 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?)"}`);
2029
+ throw new Error(`project.promote-heading: ${tax?.kind === "refuse" ? tax.detail : "no heading taxonomy resolved (guard bypassed?)"}`);
1905
2030
  }
1906
2031
  return uiDrive(headingConvertToProjectRecipe(tax.projectReveal, tax.ordinal));
1907
2032
  },
1908
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));
2097
+ },
2098
+ };
1909
2099
  // -------------------------------------------------- sidebar AREA reorder
1910
2100
  /** The compile-time placement for area.reorder (resolved refs). */
1911
2101
  function sidebarPlacementOf(params, pre) {
@@ -1931,6 +2121,16 @@ const areaReorderSidebar = {
1931
2121
  params.position !== "last") {
1932
2122
  throw new RangeError(`invalid position "${params.position}" — expected first | last`);
1933
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
+ }
1934
2134
  const pre = emptyPreState();
1935
2135
  pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
1936
2136
  const ref = params.before ?? params.after;
@@ -1941,17 +2141,25 @@ const areaReorderSidebar = {
1941
2141
  if (target != null && dest != null && target.uuid === dest.uuid) {
1942
2142
  throw new RangeError("the destination area is the area being moved");
1943
2143
  }
1944
- // The sidebar drag addresses rows by their VISIBLE NAME a duplicated
1945
- // 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;
1946
2154
  for (const area of [target, dest]) {
1947
2155
  if (area == null)
1948
2156
  continue;
1949
2157
  const dup = db
1950
2158
  .prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
1951
2159
  .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");
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");
1955
2163
  }
1956
2164
  }
1957
2165
  pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
@@ -2044,22 +2252,25 @@ export const COMMANDS = {
2044
2252
  "project.set-tags": projectSetTags,
2045
2253
  "todo.backdate": todoBackdate,
2046
2254
  "todo.add-logged": todoAddLogged,
2047
- "heading.rename": headingRename,
2048
- "heading.archive": headingArchive,
2049
- "heading.unarchive": headingUnarchive,
2050
- "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,
2051
2263
  "todo.clear-dated-reminder": todoClearDatedReminder,
2052
2264
  "todo.make-repeating": todoMakeRepeating,
2053
2265
  "todo.reschedule-repeat": todoRescheduleRepeat,
2054
2266
  "todo.pause-repeat": todoPauseRepeat,
2055
2267
  "todo.resume-repeat": todoResumeRepeat,
2056
2268
  "todo.convert-to-project": todoConvertToProject,
2057
- "heading.convert-to-project": headingConvertToProject,
2058
2269
  "project.reschedule-repeat": projectRescheduleRepeat,
2059
2270
  "project.pause-repeat": projectPauseRepeat,
2060
2271
  "project.resume-repeat": projectResumeRepeat,
2061
2272
  "area.reorder": areaReorderSidebar,
2062
2273
  "project.make-repeating": projectMakeRepeating,
2063
- "project.create-repeating": projectCreateRepeating,
2274
+ "project.add-repeating": projectAddRepeating,
2064
2275
  };
2065
2276
  //# sourceMappingURL=commands.js.map