things-api 0.17.0 → 0.19.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 (277) hide show
  1. package/README.md +16 -9
  2. package/deputy/AppIcon.icns +0 -0
  3. package/deputy/VERSION +1 -0
  4. package/deputy/helpers-Info.plist +27 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +22 -0
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +128 -0
  10. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +27 -0
  11. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  12. package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
  13. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +137 -0
  14. package/deputy/reader/Info.plist +22 -0
  15. package/deputy/reader/entitlements.plist +45 -0
  16. package/deputy/reader/main.swift +580 -0
  17. package/deputy/src/main.swift +133 -0
  18. package/deputy/src/osascript.swift +102 -0
  19. package/deputy/src/server.swift +373 -0
  20. package/deputy/src/sqlite.swift +121 -0
  21. package/deputy/src/tcc.swift +58 -0
  22. package/dist/audit/schema.d.ts +13 -1
  23. package/dist/audit/schema.js.map +1 -1
  24. package/dist/capability.d.ts +160 -0
  25. package/dist/capability.js +416 -0
  26. package/dist/capability.js.map +1 -0
  27. package/dist/cli/commands/doctor.js +181 -2
  28. package/dist/cli/commands/doctor.js.map +1 -1
  29. package/dist/cli/commands/helpers.d.ts +17 -0
  30. package/dist/cli/commands/helpers.js +275 -0
  31. package/dist/cli/commands/helpers.js.map +1 -0
  32. package/dist/cli/commands/op-result.d.ts +15 -0
  33. package/dist/cli/commands/op-result.js +45 -0
  34. package/dist/cli/commands/op-result.js.map +1 -0
  35. package/dist/cli/commands/reads.js +47 -1
  36. package/dist/cli/commands/reads.js.map +1 -1
  37. package/dist/cli/commands/setup.d.ts +9 -0
  38. package/dist/cli/commands/setup.js +63 -83
  39. package/dist/cli/commands/setup.js.map +1 -1
  40. package/dist/cli/commands/writes.d.ts +1 -1
  41. package/dist/cli/commands/writes.js +262 -109
  42. package/dist/cli/commands/writes.js.map +1 -1
  43. package/dist/cli/dry-run.d.ts +1 -1
  44. package/dist/cli/dry-run.js +1 -1
  45. package/dist/cli/glyphs.d.ts +23 -4
  46. package/dist/cli/glyphs.js +46 -19
  47. package/dist/cli/glyphs.js.map +1 -1
  48. package/dist/cli/help.d.ts +1 -1
  49. package/dist/cli/help.js +53 -27
  50. package/dist/cli/help.js.map +1 -1
  51. package/dist/cli/helpers-check.d.ts +10 -0
  52. package/dist/cli/helpers-check.js +59 -0
  53. package/dist/cli/helpers-check.js.map +1 -0
  54. package/dist/cli/main.js +22 -1
  55. package/dist/cli/main.js.map +1 -1
  56. package/dist/cli/period.d.ts +3 -1
  57. package/dist/cli/period.js +1 -1
  58. package/dist/cli/period.js.map +1 -1
  59. package/dist/cli/read-driver.js +26 -1
  60. package/dist/cli/read-driver.js.map +1 -1
  61. package/dist/cli/render.d.ts +31 -4
  62. package/dist/cli/render.js +127 -15
  63. package/dist/cli/render.js.map +1 -1
  64. package/dist/client.d.ts +22 -2
  65. package/dist/client.js +52 -15
  66. package/dist/client.js.map +1 -1
  67. package/dist/config.d.ts +31 -0
  68. package/dist/config.js +9 -0
  69. package/dist/config.js.map +1 -1
  70. package/dist/contracts.d.ts +6 -2
  71. package/dist/contracts.js +1 -1
  72. package/dist/contracts.js.map +1 -1
  73. package/dist/db/baselines/db-v26.js +8 -4
  74. package/dist/db/baselines/db-v26.js.map +1 -1
  75. package/dist/db/baselines/db-v27.d.ts +42 -0
  76. package/dist/db/baselines/db-v27.js +47 -0
  77. package/dist/db/baselines/db-v27.js.map +1 -0
  78. package/dist/db/baselines/index.js +2 -1
  79. package/dist/db/baselines/index.js.map +1 -1
  80. package/dist/db/locate.d.ts +4 -1
  81. package/dist/db/locate.js +3 -1
  82. package/dist/db/locate.js.map +1 -1
  83. package/dist/db/schema.d.ts +1 -1
  84. package/dist/db/schema.js +9 -0
  85. package/dist/db/schema.js.map +1 -1
  86. package/dist/deputy/bridge-worker.d.ts +1 -0
  87. package/dist/deputy/bridge-worker.js +79 -0
  88. package/dist/deputy/bridge-worker.js.map +1 -0
  89. package/dist/deputy/bridge.d.ts +14 -0
  90. package/dist/deputy/bridge.js +70 -0
  91. package/dist/deputy/bridge.js.map +1 -0
  92. package/dist/deputy/client.d.ts +19 -0
  93. package/dist/deputy/client.js +104 -0
  94. package/dist/deputy/client.js.map +1 -0
  95. package/dist/deputy/db-facade.d.ts +18 -0
  96. package/dist/deputy/db-facade.js +71 -0
  97. package/dist/deputy/db-facade.js.map +1 -0
  98. package/dist/deputy/files.d.ts +1 -0
  99. package/dist/deputy/files.js +16 -0
  100. package/dist/deputy/files.js.map +1 -0
  101. package/dist/deputy/install.d.ts +319 -0
  102. package/dist/deputy/install.js +1239 -0
  103. package/dist/deputy/install.js.map +1 -0
  104. package/dist/deputy/notice.d.ts +6 -0
  105. package/dist/deputy/notice.js +28 -0
  106. package/dist/deputy/notice.js.map +1 -0
  107. package/dist/deputy/notices.d.ts +31 -0
  108. package/dist/deputy/notices.js +94 -0
  109. package/dist/deputy/notices.js.map +1 -0
  110. package/dist/deputy/osa.d.ts +21 -0
  111. package/dist/deputy/osa.js +72 -0
  112. package/dist/deputy/osa.js.map +1 -0
  113. package/dist/deputy/protocol.d.ts +140 -0
  114. package/dist/deputy/protocol.js +143 -0
  115. package/dist/deputy/protocol.js.map +1 -0
  116. package/dist/deputy/routing.d.ts +84 -0
  117. package/dist/deputy/routing.js +422 -0
  118. package/dist/deputy/routing.js.map +1 -0
  119. package/dist/deputy/shortcuts-exec.d.ts +10 -0
  120. package/dist/deputy/shortcuts-exec.js +50 -0
  121. package/dist/deputy/shortcuts-exec.js.map +1 -0
  122. package/dist/diagnose.d.ts +57 -2
  123. package/dist/diagnose.js +105 -3
  124. package/dist/diagnose.js.map +1 -1
  125. package/dist/direct-setup.d.ts +78 -0
  126. package/dist/direct-setup.js +484 -0
  127. package/dist/direct-setup.js.map +1 -0
  128. package/dist/host-access.d.ts +63 -0
  129. package/dist/host-access.js +165 -0
  130. package/dist/host-access.js.map +1 -0
  131. package/dist/index.d.ts +31 -5
  132. package/dist/index.js +23 -1
  133. package/dist/index.js.map +1 -1
  134. package/dist/mcp/server.d.ts +27 -1
  135. package/dist/mcp/server.js +300 -97
  136. package/dist/mcp/server.js.map +1 -1
  137. package/dist/model/mappers.d.ts +12 -3
  138. package/dist/model/mappers.js +6 -1
  139. package/dist/model/mappers.js.map +1 -1
  140. package/dist/model/recurrence.d.ts +5 -1
  141. package/dist/model/recurrence.js +19 -1
  142. package/dist/model/recurrence.js.map +1 -1
  143. package/dist/model/template-projection.d.ts +26 -0
  144. package/dist/model/template-projection.js +113 -0
  145. package/dist/model/template-projection.js.map +1 -0
  146. package/dist/op-result.d.ts +55 -0
  147. package/dist/op-result.js +161 -0
  148. package/dist/op-result.js.map +1 -0
  149. package/dist/read/detail.js +7 -1
  150. package/dist/read/detail.js.map +1 -1
  151. package/dist/read/filter-contract.d.ts +1 -1
  152. package/dist/read/filter-contract.js +4 -0
  153. package/dist/read/filter-contract.js.map +1 -1
  154. package/dist/read/queries.js +8 -1
  155. package/dist/read/queries.js.map +1 -1
  156. package/dist/read/shape.js +4 -0
  157. package/dist/read/shape.js.map +1 -1
  158. package/dist/read/views.d.ts +35 -4
  159. package/dist/read/views.js +120 -18
  160. package/dist/read/views.js.map +1 -1
  161. package/dist/session-grant.d.ts +48 -0
  162. package/dist/session-grant.js +170 -0
  163. package/dist/session-grant.js.map +1 -0
  164. package/dist/surface-copy.d.ts +8 -0
  165. package/dist/surface-copy.js +8 -0
  166. package/dist/surface-copy.js.map +1 -1
  167. package/dist/sync-health.js +6 -2
  168. package/dist/sync-health.js.map +1 -1
  169. package/dist/wizard.d.ts +58 -0
  170. package/dist/wizard.js +167 -0
  171. package/dist/wizard.js.map +1 -0
  172. package/dist/write/accessibility-probe.js +4 -2
  173. package/dist/write/accessibility-probe.js.map +1 -1
  174. package/dist/write/automation-probe.js +5 -1
  175. package/dist/write/automation-probe.js.map +1 -1
  176. package/dist/write/availability.d.ts +1 -1
  177. package/dist/write/availability.js +10 -6
  178. package/dist/write/availability.js.map +1 -1
  179. package/dist/write/batch.d.ts +20 -0
  180. package/dist/write/batch.js +94 -15
  181. package/dist/write/batch.js.map +1 -1
  182. package/dist/write/capabilities.d.ts +9 -0
  183. package/dist/write/capabilities.js +2 -0
  184. package/dist/write/capabilities.js.map +1 -1
  185. package/dist/write/clear-reminder.js +1 -1
  186. package/dist/write/clear-reminder.js.map +1 -1
  187. package/dist/write/commands.d.ts +12 -1
  188. package/dist/write/commands.js +170 -237
  189. package/dist/write/commands.js.map +1 -1
  190. package/dist/write/experimental.d.ts +19 -0
  191. package/dist/write/experimental.js +50 -0
  192. package/dist/write/experimental.js.map +1 -1
  193. package/dist/write/failure-hints.d.ts +3 -1
  194. package/dist/write/failure-hints.js.map +1 -1
  195. package/dist/write/guards.js +19 -2
  196. package/dist/write/guards.js.map +1 -1
  197. package/dist/write/lock.d.ts +16 -13
  198. package/dist/write/lock.js +55 -0
  199. package/dist/write/lock.js.map +1 -1
  200. package/dist/write/make-repeating-project.js +9 -13
  201. package/dist/write/make-repeating-project.js.map +1 -1
  202. package/dist/write/move.js +14 -6
  203. package/dist/write/move.js.map +1 -1
  204. package/dist/write/operations.d.ts +64 -25
  205. package/dist/write/operations.js +7 -0
  206. package/dist/write/operations.js.map +1 -1
  207. package/dist/write/opid.d.ts +14 -0
  208. package/dist/write/opid.js +22 -0
  209. package/dist/write/opid.js.map +1 -1
  210. package/dist/write/param-schema.d.ts +124 -0
  211. package/dist/write/param-schema.js +747 -0
  212. package/dist/write/param-schema.js.map +1 -0
  213. package/dist/write/pipeline.d.ts +78 -2
  214. package/dist/write/pipeline.js +249 -4
  215. package/dist/write/pipeline.js.map +1 -1
  216. package/dist/write/pre-state.d.ts +8 -0
  217. package/dist/write/pre-state.js +62 -17
  218. package/dist/write/pre-state.js.map +1 -1
  219. package/dist/write/promote-clone.js +635 -367
  220. package/dist/write/promote-clone.js.map +1 -1
  221. package/dist/write/reorder.js +95 -29
  222. package/dist/write/reorder.js.map +1 -1
  223. package/dist/write/repeat-anchor.d.ts +30 -0
  224. package/dist/write/repeat-anchor.js +166 -1
  225. package/dist/write/repeat-anchor.js.map +1 -1
  226. package/dist/write/repeat-rule.d.ts +28 -1
  227. package/dist/write/repeat-rule.js +64 -4
  228. package/dist/write/repeat-rule.js.map +1 -1
  229. package/dist/write/resolution-timestamps.d.ts +11 -0
  230. package/dist/write/resolution-timestamps.js +83 -38
  231. package/dist/write/resolution-timestamps.js.map +1 -1
  232. package/dist/write/reversibility.js +4 -0
  233. package/dist/write/reversibility.js.map +1 -1
  234. package/dist/write/template-mutation.d.ts +137 -0
  235. package/dist/write/template-mutation.js +451 -0
  236. package/dist/write/template-mutation.js.map +1 -0
  237. package/dist/write/undo.js +10 -20
  238. package/dist/write/undo.js.map +1 -1
  239. package/dist/write/update-fields.d.ts +151 -0
  240. package/dist/write/update-fields.js +427 -0
  241. package/dist/write/update-fields.js.map +1 -0
  242. package/dist/write/vectors/applescript.js +10 -16
  243. package/dist/write/vectors/applescript.js.map +1 -1
  244. package/dist/write/vectors/registry.d.ts +1 -1
  245. package/dist/write/vectors/registry.js +10 -2
  246. package/dist/write/vectors/registry.js.map +1 -1
  247. package/dist/write/vectors/session-reachability.d.ts +55 -0
  248. package/dist/write/vectors/session-reachability.js +56 -8
  249. package/dist/write/vectors/session-reachability.js.map +1 -1
  250. package/dist/write/vectors/shortcuts.js +7 -16
  251. package/dist/write/vectors/shortcuts.js.map +1 -1
  252. package/dist/write/vectors/simulator.d.ts +16 -1
  253. package/dist/write/vectors/simulator.js +68 -12
  254. package/dist/write/vectors/simulator.js.map +1 -1
  255. package/dist/write/vectors/types.d.ts +124 -1
  256. package/dist/write/vectors/ui-recipes.d.ts +19 -0
  257. package/dist/write/vectors/ui-recipes.js +158 -38
  258. package/dist/write/vectors/ui-recipes.js.map +1 -1
  259. package/dist/write/vectors/ui.d.ts +89 -0
  260. package/dist/write/vectors/ui.js +504 -49
  261. package/dist/write/vectors/ui.js.map +1 -1
  262. package/dist/write/verify/delta.d.ts +21 -0
  263. package/dist/write/verify/delta.js.map +1 -1
  264. package/dist/write/verify/poller.d.ts +13 -0
  265. package/dist/write/verify/poller.js +38 -0
  266. package/dist/write/verify/poller.js.map +1 -1
  267. package/package.json +9 -1
  268. package/schema/envelope.schema.json +5 -3
  269. package/scripts/build-helpers.sh +109 -0
  270. package/scripts/deputy-cert-setup.sh +60 -0
  271. package/skills/things-cli/SKILL.md +12 -5
  272. package/skills/things-cli/references/contracts.md +1 -0
  273. package/skills/things-cli/references/data-model.md +3 -0
  274. package/skills/things-cli/references/errors.md +5 -1
  275. package/skills/things-cli/references/gui.md +1 -0
  276. package/skills/things-cli/references/repeating.md +75 -0
  277. package/skills/things-cli/references/tag-cleanup.md +64 -0
@@ -1,16 +1,16 @@
1
- import { decodeReminderTime, encodeReminderTime, localToday, reminderUrlToken, zonedWallInstant, } from "../model/dates.js";
1
+ import { localToday, zonedWallInstant } from "../model/dates.js";
2
2
  import { byUuid } from "../read/detail.js";
3
3
  import { manualLogDateEpoch, pendingLogCount } from "../read/log-boundary.js";
4
4
  import { isLooseRef } from "../read/pseudo-area.js";
5
- import { reminderIsLive } from "../read/stage.js";
6
- import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
5
+ import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, seriesRowUuids, trashedCount, } from "./pre-state.js";
6
+ import { assertNotesModesExclusive, normalizeReminder, updateAssertions, updateWireParams, whenAssertions, whenWithReminder, } from "./update-fields.js";
7
7
  import { resolveTagRefs } from "./tag-refs.js";
8
8
  import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
9
9
  import { assertRepeatRule } from "./repeat-rule.js";
10
- import { deadlineDriveNext } from "./repeat-anchor.js";
10
+ import { assessOffRuleFirst, deadlineDriveNext, deriveFixedAnchor } from "./repeat-anchor.js";
11
11
  import { expectedRuleAssertions } from "./repeat-asserts.js";
12
12
  import { escapeAppleScript } from "./vectors/applescript.js";
13
- import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
13
+ import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, createNextCopyRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
14
14
  import { buildRepeatingFingerprint } from "./verify/delta.js";
15
15
  // ------------------------------------------------------------------ helpers
16
16
  function thingsUrl(command, params, token) {
@@ -65,65 +65,6 @@ function q(value) {
65
65
  function unsupportedVector(op, vector) {
66
66
  throw new Error(`${op} cannot be compiled for vector ${vector} (planner bug)`);
67
67
  }
68
- function whenAssertions(when, todayIso, opts = { mode: "add" }) {
69
- // Strict shape check: an unvalidated string used to flow straight into the
70
- // URL (e.g. "2026-07-20@09:30", the raw URL grammar) — the app would SET
71
- // date+reminder while verification asserted the literal string as the date,
72
- // reporting a false mismatch on a write that succeeded.
73
- if (when !== "today" &&
74
- when !== "evening" &&
75
- when !== "anytime" &&
76
- when !== "someday" &&
77
- !/^\d{4}-\d{2}-\d{2}$/.test(when)) {
78
- throw new RangeError(when.includes("@")
79
- ? `invalid when "${when}" — a reminder time is a separate parameter (reminder: "HH:mm"; CLI --reminder), not an @ suffix`
80
- : `invalid when "${when}" — expected today | evening | anytime | someday | YYYY-MM-DD`);
81
- }
82
- switch (when) {
83
- case "today":
84
- // Today's non-evening section: in Today (the `today` marker) AND NOT in the
85
- // evening sub-bucket (the presence-keyed `evening` marker absent — asserted
86
- // as null, which valuesEqual treats as absent). Gated to Today members under
87
- // the verify clock exactly as the retired `todaySection` was.
88
- //
89
- // The `startDate` assertion differs by op (field bug §0½.8):
90
- // - ADD mints a fresh Today item with no schedule history, so the app dates
91
- // it EXACTLY today — exact equality is right.
92
- // - UPDATE of an item ALREADY in Today whose `startDate` has already arrived
93
- // (past or today): the app PRESERVES that historical date rather than
94
- // rewriting the storage byte to today (arrived-date law). Assert the
95
- // arrived-date PREDICATE (non-null and <= today) so a preserved historical
96
- // date verifies, while an undated deadline-only pull (null startDate) is
97
- // still rejected — the item's Today membership then rests only on a
98
- // deadline, not on the requested schedule.
99
- return [
100
- { field: "start", equals: "active" },
101
- opts.mode === "update"
102
- ? { field: "startDate", satisfies: { predicate: "arrived-on-or-before", date: todayIso } }
103
- : { field: "startDate", equals: todayIso },
104
- { field: "today", equals: true },
105
- { field: "evening", equals: null },
106
- ];
107
- case "evening":
108
- // The This-Evening sub-bucket: the `evening` marker (which implies `today`).
109
- return [
110
- { field: "start", equals: "active" },
111
- { field: "startDate", equals: todayIso },
112
- { field: "evening", equals: true },
113
- ];
114
- case "anytime":
115
- return [
116
- { field: "start", equals: "active" },
117
- { field: "startDate", equals: null },
118
- ];
119
- case "someday":
120
- return [{ field: "start", equals: "someday" }];
121
- default:
122
- // Concrete date: assert only the date — start-state semantics differ
123
- // for past/today/future dates (only the date itself is invariant).
124
- return [{ field: "startDate", equals: when }];
125
- }
126
- }
127
68
  function sortedTags(tags) {
128
69
  return [...tags].toSorted();
129
70
  }
@@ -139,21 +80,28 @@ function applyTagRefs(db, pre, tags) {
139
80
  pre.missingTags = res.missing;
140
81
  pre.resolvedTagTitles = res.titles;
141
82
  }
142
- /** Round-trip normalization: "6:5"-style inputs → canonical "06:05". */
143
- function normalizeReminder(time) {
144
- return decodeReminderTime(encodeReminderTime(time)) ?? time;
145
- }
146
83
  /**
147
- * The URL `when` value with an optional reminder token appended through the
148
- * deterministic emitter (never a bare 1–11 hour oddity 2d).
84
+ * Is a destination container SUPPLIED? A PRESENCE check, not a duck-test (#580):
85
+ * the parameter schema (param-schema.ts) has already proven that a present ref is
86
+ * a `{uuid}`/`{title}` object, so anything else reaching here is an engine bug,
87
+ * not caller input — and it THROWS rather than reading as "no destination given",
88
+ * which is precisely how a bare-string `project` used to compile an Inbox capture
89
+ * with no placement and no placement assertion, then verify clean.
149
90
  */
150
- function whenWithReminder(when, reminder) {
151
- if (reminder === undefined || reminder === null)
152
- return when;
153
- return `${when}@${reminderUrlToken(reminder)}`;
91
+ export function containerGiven(ref) {
92
+ if (ref === undefined || ref === null)
93
+ return false;
94
+ assertContainerRef(ref);
95
+ return true;
154
96
  }
155
- function containerGiven(ref) {
156
- return ref !== undefined && (ref.uuid !== undefined || ref.title !== undefined);
97
+ /** Belt-and-braces: a present container reference must carry a uuid or a title. */
98
+ export function assertContainerRef(ref) {
99
+ if (typeof ref !== "object" || Array.isArray(ref)) {
100
+ throw new RangeError(`a container reference must be an object naming a uuid or a title — received ${JSON.stringify(ref)}`);
101
+ }
102
+ if (ref.uuid === undefined && ref.title === undefined) {
103
+ throw new RangeError("a container reference must name a uuid or a title — it names neither");
104
+ }
157
105
  }
158
106
  /**
159
107
  * The create-probe title for the duplicate / identity-replacement ops, whose
@@ -351,108 +299,35 @@ const todoAdd = {
351
299
  },
352
300
  };
353
301
  /**
354
- * The effective reminder a when-bearing update should leave behind. A bare
355
- * `when=` CLEARS an existing reminder (R07/R20), so when the caller
356
- * re-schedules to today/evening/a date without addressing the reminder we
357
- * auto-preserve the current one; an explicit null is the intentional clear.
358
- *
359
- * §9n: a reminder whose row's `startDate` is already strictly PAST is
360
- * presentation-dead (the GUI hides its bell; the byte lingers in the DB). We do
361
- * NOT auto-preserve such a stale byte — carrying it into the re-schedule would
362
- * RESURRECT a reminder the user believes gone. A LIVE reminder (startDate
363
- * today/future) is preserved as before. The liveness test is the SAME
364
- * {@link reminderIsLive} predicate the read side gates on, consulted against the
365
- * target's CURRENT `startDate` under the response clock.
302
+ * The to-do / project update pair. Both legs the URL parameters and the
303
+ * expected-delta assertions are DERIVED from the single exhaustive registry in
304
+ * update-fields.ts (the #491 exhaustive-map doctrine): the only per-verb
305
+ * difference is the URL command name, so neither verb can accept a field the
306
+ * other drops, and a field added to `UpdateFields` breaks compilation there
307
+ * until every leg handles it.
366
308
  */
367
- function effectiveReminder(pre, params) {
368
- if (params.reminder !== undefined)
369
- return params.reminder;
370
- const when = params.when;
371
- const schedulable = when === "today" ||
372
- when === "evening" ||
373
- (typeof when === "string" && /^\d{4}-\d{2}-\d{2}$/.test(when));
374
- if (!schedulable)
375
- return null;
376
- const target = pre.target;
377
- if (target === null || target.type === "heading")
378
- return null;
379
- if (!reminderIsLive(target.startDate, pre.todayIso))
380
- return null;
381
- // Read the RAW stored byte off the substrate (top-level `reminder` is
382
- // live-gated under the LOAD clock, which can differ from `pre.todayIso` under a
383
- // pinned THINGS_NOW). The `reminderIsLive` guard above applies liveness under
384
- // the injected clock explicitly, so the raw byte + this gate reproduce the
385
- // former semantics exactly.
386
- return target.derived.reminder;
387
- }
388
- function assertNotesModesExclusive(params) {
389
- if (params.notes !== undefined &&
390
- (params.appendNotes !== undefined || params.prependNotes !== undefined)) {
391
- throw new RangeError("notes (replace) is exclusive with appendNotes/prependNotes");
392
- }
393
- if (params.appendNotes !== undefined && params.prependNotes !== undefined) {
394
- throw new RangeError("appendNotes and prependNotes cannot be combined in one update");
395
- }
396
- }
397
- function expectedNotes(pre, params) {
398
- const current = pre.target !== null && pre.target.type !== "heading" ? pre.target.notes : "";
399
- // Separator semantics probed: newline-joined, no stray newline against an
400
- // empty note (E04/E05/E11/E12).
401
- if (params.appendNotes !== undefined) {
402
- return current === "" ? params.appendNotes : `${current}\n${params.appendNotes}`;
403
- }
404
- if (params.prependNotes !== undefined) {
405
- return current === "" ? params.prependNotes : `${params.prependNotes}\n${current}`;
406
- }
407
- return undefined;
309
+ function updateSpec(op, urlCommand) {
310
+ return {
311
+ op,
312
+ hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
313
+ preRead(db, params, now) {
314
+ assertNotesModesExclusive(params);
315
+ const pre = emptyPreState();
316
+ pre.todayIso = localToday(now);
317
+ pre.target = loadTarget(db, params.uuid);
318
+ return pre;
319
+ },
320
+ expectedDelta(pre, params, ctx) {
321
+ return { mode: "update", uuid: params.uuid, assert: updateAssertions(params, pre, ctx) };
322
+ },
323
+ compile(params, vector, pre, ctx) {
324
+ if (vector !== "url-scheme")
325
+ unsupportedVector(op, vector);
326
+ return thingsUrl(urlCommand, { id: params.uuid, ...updateWireParams(params, pre) }, ctx.token);
327
+ },
328
+ };
408
329
  }
409
- const todoUpdate = {
410
- op: "todo.update",
411
- hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
412
- preRead(db, params, now) {
413
- assertNotesModesExclusive(params);
414
- const pre = emptyPreState();
415
- pre.todayIso = localToday(now);
416
- pre.target = loadTarget(db, params.uuid);
417
- return pre;
418
- },
419
- expectedDelta(pre, params, ctx) {
420
- const assert = [];
421
- if (params.title !== undefined)
422
- assert.push({ field: "title", equals: params.title });
423
- if (params.notes !== undefined)
424
- assert.push({ field: "notes", equals: params.notes });
425
- const joined = expectedNotes(pre, params);
426
- if (joined !== undefined)
427
- assert.push({ field: "notes", equals: joined });
428
- if (params.when !== undefined) {
429
- assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
430
- const reminder = effectiveReminder(pre, params);
431
- assert.push({
432
- field: "reminder",
433
- equals: reminder === null ? null : normalizeReminder(reminder),
434
- });
435
- }
436
- if (params.deadline !== undefined)
437
- assert.push({ field: "deadline", equals: params.deadline });
438
- return { mode: "update", uuid: params.uuid, assert };
439
- },
440
- compile(params, vector, pre, ctx) {
441
- if (vector !== "url-scheme")
442
- unsupportedVector(this.op, vector);
443
- return thingsUrl("update", {
444
- id: params.uuid,
445
- title: params.title,
446
- notes: params.notes,
447
- "append-notes": params.appendNotes,
448
- "prepend-notes": params.prependNotes,
449
- when: params.when === undefined
450
- ? undefined
451
- : whenWithReminder(params.when, effectiveReminder(pre, params)),
452
- deadline: params.deadline === null ? "" : params.deadline,
453
- }, ctx.token);
454
- },
455
- };
330
+ const todoUpdate = updateSpec("todo.update", "update");
456
331
  function statusSpec(op, urlParam, asStatus, scriptStatus) {
457
332
  return {
458
333
  op,
@@ -826,6 +701,14 @@ const projectAdd = {
826
701
  const area = pre.destArea?.resolved;
827
702
  if (area !== undefined && area !== null)
828
703
  assert.push({ field: "area.uuid", equals: area.uuid });
704
+ // KNOWN SHALLOW SPOT (audited 2026-08-23, exhaustive-map sweep): the seeded
705
+ // children — `todos` and the structured `items` (headings + rich children) —
706
+ // contribute NO assertion, so a project born with an empty body still
707
+ // verifies `ok`. Unlike a to-do's `checklistItems` (asserted through
708
+ // `checklistTitles`) there is no child-list field on the delta vocabulary to
709
+ // compare against, and adding one needs probe evidence that the imported
710
+ // children are readable inside the verify window before a missing child may
711
+ // be called a failure. Tracked in docs/up-next.md; do NOT "fix" it blind.
829
712
  return {
830
713
  mode: "create",
831
714
  probe: {
@@ -873,55 +756,9 @@ const projectAdd = {
873
756
  }, ctx.token);
874
757
  },
875
758
  };
876
- const projectUpdate = {
877
- op: "project.update",
878
- hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
879
- preRead(db, params, now) {
880
- assertNotesModesExclusive(params);
881
- const pre = emptyPreState();
882
- pre.todayIso = localToday(now);
883
- pre.target = loadTarget(db, params.uuid);
884
- return pre;
885
- },
886
- expectedDelta(pre, params, ctx) {
887
- const assert = [];
888
- if (params.title !== undefined)
889
- assert.push({ field: "title", equals: params.title });
890
- if (params.notes !== undefined)
891
- assert.push({ field: "notes", equals: params.notes });
892
- const joined = expectedNotes(pre, params);
893
- if (joined !== undefined)
894
- assert.push({ field: "notes", equals: joined });
895
- if (params.when !== undefined) {
896
- assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
897
- // Projects carry the same reminderTime codec as to-dos (A3); a bare
898
- // when= clears an existing reminder unless auto-preserved.
899
- const reminder = effectiveReminder(pre, params);
900
- assert.push({
901
- field: "reminder",
902
- equals: reminder === null ? null : normalizeReminder(reminder),
903
- });
904
- }
905
- if (params.deadline !== undefined)
906
- assert.push({ field: "deadline", equals: params.deadline });
907
- return { mode: "update", uuid: params.uuid, assert };
908
- },
909
- compile(params, vector, pre, ctx) {
910
- if (vector !== "url-scheme")
911
- unsupportedVector(this.op, vector);
912
- return thingsUrl("update-project", {
913
- id: params.uuid,
914
- title: params.title,
915
- notes: params.notes,
916
- "append-notes": params.appendNotes,
917
- "prepend-notes": params.prependNotes,
918
- when: params.when === undefined
919
- ? undefined
920
- : whenWithReminder(params.when, effectiveReminder(pre, params)),
921
- deadline: params.deadline === null ? "" : params.deadline,
922
- }, ctx.token);
923
- },
924
- };
759
+ // Projects carry the same reminderTime codec and notes/schedule semantics as
760
+ // to-dos (A3/E18) — same registry, same legs, only the URL command differs.
761
+ const projectUpdate = updateSpec("project.update", "update-project");
925
762
  const projectSetTags = {
926
763
  op: "project.set-tags",
927
764
  hazards: ["H-UNKNOWN-DESTINATION", "H-UNKNOWN-TAG"],
@@ -1921,16 +1758,39 @@ function ruleExtras(params) {
1921
1758
  };
1922
1759
  }
1923
1760
  /**
1924
- * Recipe extras for a RESCHEDULE, with the "Next:" drive date deadline-adjusted
1925
- * (YANCH1 #493): `--when` is the scheduled START, but a deadlined rule anchors on
1926
- * the DEADLINE, so the dialog's "Next:" field is driven with `when +
1927
- * startDaysEarlier` and the app back-shifts the instance start to `--when`. The
1928
- * expectedDelta keeps the RAW `params.next` (the cursor asserts the START).
1929
- * make/add-repeating apply this shift upstream (promote-clone.ts) and pass an
1930
- * already-adjusted rule, so only the reschedule compile needs it here.
1761
+ * The reschedule's EFFECTIVE rule params: the requested params PLUS the calendar
1762
+ * anchor DERIVED from `--when` when none was given explicitly (YANCH1 #493 the
1763
+ * SAME derive-and-drive make/add-repeating apply upstream in promote-clone.ts).
1764
+ * The anchor is derived from the deadline-shifted drive date (`deadlineDriveNext`
1765
+ * = when + startDaysEarlier) so a deadlined rule's anchor names the DUE date,
1766
+ * exactly as make/add. A rule-only reschedule (no `--when`) or an explicit anchor
1767
+ * flag leaves params UNCHANGED (deriveFixedAnchor returns an empty patch).
1768
+ *
1769
+ * This is the single source of truth used by BOTH the compile (`reschedRuleExtras`
1770
+ * → the recipe DRIVES the anchor pop-ups) AND expectedDelta (`expectedRuleAssertions`
1771
+ * ASSERTS the same anchor), so the drive vocabulary and the verify vocabulary
1772
+ * derive IDENTICALLY. Without it a `--when`-only reschedule left the anchor at the
1773
+ * dialog's untouched default (yearly January 1, monthly 1st, weekly Sunday) — the
1774
+ * DRIVE never touched the anchor pop-ups while the reschedule silently kept the old
1775
+ * placement (the RSPA1 live failure: a yearly `--when`-only reschedule whose anchor
1776
+ * was never driven).
1777
+ */
1778
+ function reschedEffParams(params) {
1779
+ return { ...params, ...deriveFixedAnchor(params, deadlineDriveNext(params)) };
1780
+ }
1781
+ /**
1782
+ * Recipe extras for a RESCHEDULE: the derived-anchor effective params (so the
1783
+ * recipe drives the weekly/monthly/yearly anchor pop-ups for a `--when`-only
1784
+ * reschedule), with the "Next:" drive date deadline-adjusted (YANCH1 #493) —
1785
+ * `--when` is the scheduled START, but a deadlined rule anchors on the DEADLINE,
1786
+ * so the dialog's "Next:" field is driven with `when + startDaysEarlier` and the
1787
+ * app back-shifts the instance start to `--when`. The expectedDelta keeps the RAW
1788
+ * `params.next` (the cursor asserts the START). make/add-repeating apply this shift
1789
+ * upstream (promote-clone.ts) and pass an already-adjusted rule, so only the
1790
+ * reschedule compile needs it here.
1931
1791
  */
1932
1792
  function reschedRuleExtras(params) {
1933
- const base = ruleExtras(params);
1793
+ const base = ruleExtras(reschedEffParams(params));
1934
1794
  const drive = deadlineDriveNext(params);
1935
1795
  return drive !== undefined ? { ...base, next: drive } : base;
1936
1796
  }
@@ -2002,11 +1862,22 @@ const todoRescheduleRepeat = {
2002
1862
  // the post-drive verify catches a wrong-anchor landing. A shallow unit+interval
2003
1863
  // subset false-noop'd a reschedule that changed only the monthly anchor /
2004
1864
  // deadline / ends (#491). ALSO capture the whole prior rule (+ deadline flag)
2005
- // so the undo can re-drive it faithfully.
1865
+ // so the undo can re-drive it faithfully. The DERIVED-anchor effective params
1866
+ // (reschedEffParams) are asserted — the SAME bag the compile drives — so a
1867
+ // `--when`-only reschedule verifies the derived anchor the drive lands (RSPA1).
1868
+ const eff = reschedEffParams(params);
2006
1869
  return {
2007
1870
  mode: "update",
2008
1871
  uuid: params.uuid,
2009
- assert: expectedRuleAssertions(params, { includeCursor: true }),
1872
+ // includeCursor asserts the first-occurrence cursor == --when. For an OFF-RULE
1873
+ // first (explicit anchor ≠ --when) the cursor lands on the next RULE-ALIGNED
1874
+ // occurrence, not --when (DACON1 cell DC4: --when Oct 16 with an Oct-16 due
1875
+ // anchor cursors to the next aligned start, 2029-10-02), so asserting it would
1876
+ // false-fail an honored off-rule reschedule — drop the cursor assertion there
1877
+ // and verify the rule anchor only.
1878
+ assert: expectedRuleAssertions(eff, {
1879
+ includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
1880
+ }),
2010
1881
  capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
2011
1882
  };
2012
1883
  },
@@ -2038,6 +1909,56 @@ const todoPauseRepeat = {
2038
1909
  return uiDrive(pauseRepeatRecipe(params.uuid));
2039
1910
  },
2040
1911
  };
1912
+ /**
1913
+ * `Items ▸ Repeat ▸ Create Next Copy` (CNC1) — materialize the pending
1914
+ * occurrence and advance the series. The INTERNAL first leg of the
1915
+ * template-mutation composites (template-mutation.ts); it is not a user-facing
1916
+ * verb, because a bare "spawn one now" has no use we have been asked for and
1917
+ * every honest use of it mutates the row it mints.
1918
+ *
1919
+ * Verified as a CREATE: the minted row wears the template's own title, is a
1920
+ * to-do, carries a fresh (gesture wall-clock) creationDate, and is not one of
1921
+ * the rows the series already had — which is exactly the create-probe's
1922
+ * gauntlet, and it hands the composite back the minted uuid it needs. The one
1923
+ * field assertion is the FK: the discovered row must be an instance of THIS
1924
+ * template, so a same-titled row created by anything else in the window cannot
1925
+ * pass. The cursor advance is deliberately NOT asserted here — a create delta
1926
+ * asserts against the created row, and the composite re-reads the template
1927
+ * afterwards anyway to disclose when the next occurrence lands.
1928
+ */
1929
+ const todoCreateNextCopy = {
1930
+ op: "todo.create-next-copy",
1931
+ hazards: UI_HAZARDS,
1932
+ preRead(db, params) {
1933
+ const pre = emptyPreState();
1934
+ pre.target = loadTarget(db, params.uuid);
1935
+ // The series' existing rows — the template plus every instance it has
1936
+ // already spawned, all of which share its title. They are the create
1937
+ // probe's exclusion set, so "the new row" is unambiguous even on a series
1938
+ // whose occurrences all look alike.
1939
+ pre.sameTitleUuids = seriesRowUuids(db, params.uuid);
1940
+ return pre;
1941
+ },
1942
+ expectedDelta(pre, params, ctx) {
1943
+ const target = pre.target;
1944
+ const title = target !== null && target.type !== "heading" ? target.title : "";
1945
+ return {
1946
+ mode: "create",
1947
+ probe: {
1948
+ title,
1949
+ type: "to-do",
1950
+ sinceEpoch: ctx.nowEpoch - 2,
1951
+ excludeUuids: pre.sameTitleUuids,
1952
+ },
1953
+ assert: [{ field: "repeating.templateUuid", equals: params.uuid }],
1954
+ };
1955
+ },
1956
+ compile(params, vector) {
1957
+ if (vector !== "ui")
1958
+ unsupportedVector(this.op, vector);
1959
+ return uiDrive(createNextCopyRecipe(params.uuid));
1960
+ },
1961
+ };
2041
1962
  const todoResumeRepeat = {
2042
1963
  op: "todo.resume-repeat",
2043
1964
  hazards: UI_HAZARDS,
@@ -2080,11 +2001,22 @@ const projectRescheduleRepeat = {
2080
2001
  // capture the prior rule (+ deadline flag) for the faithful undo. Assert the
2081
2002
  // FULL requested rule (#491) via expectedRuleAssertions — same completeness as
2082
2003
  // todo.reschedule-repeat (a shallow unit+interval subset false-noop'd an
2083
- // anchor/deadline/ends-only reschedule).
2004
+ // anchor/deadline/ends-only reschedule). The DERIVED-anchor effective params
2005
+ // (reschedEffParams) are asserted — the SAME bag the compile drives — so a
2006
+ // `--when`-only reschedule verifies the derived anchor the drive lands (RSPA1).
2007
+ const eff = reschedEffParams(params);
2084
2008
  return {
2085
2009
  mode: "update",
2086
2010
  uuid: params.uuid,
2087
- assert: expectedRuleAssertions(params, { includeCursor: true }),
2011
+ // includeCursor asserts the first-occurrence cursor == --when. For an OFF-RULE
2012
+ // first (explicit anchor ≠ --when) the cursor lands on the next RULE-ALIGNED
2013
+ // occurrence, not --when (DACON1 cell DC4: --when Oct 16 with an Oct-16 due
2014
+ // anchor cursors to the next aligned start, 2029-10-02), so asserting it would
2015
+ // false-fail an honored off-rule reschedule — drop the cursor assertion there
2016
+ // and verify the rule anchor only.
2017
+ assert: expectedRuleAssertions(eff, {
2018
+ includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
2019
+ }),
2088
2020
  capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
2089
2021
  };
2090
2022
  },
@@ -2567,6 +2499,7 @@ export const COMMANDS = {
2567
2499
  "todo.make-repeating": todoMakeRepeating,
2568
2500
  "todo.reschedule-repeat": todoRescheduleRepeat,
2569
2501
  "todo.pause-repeat": todoPauseRepeat,
2502
+ "todo.create-next-copy": todoCreateNextCopy,
2570
2503
  "todo.resume-repeat": todoResumeRepeat,
2571
2504
  "todo.convert-to-project": todoConvertToProject,
2572
2505
  "project.reschedule-repeat": projectRescheduleRepeat,