things-api 0.13.0 → 0.14.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 (104) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/area.d.ts +5 -5
  3. package/dist/cli/commands/area.js +12 -7
  4. package/dist/cli/commands/area.js.map +1 -1
  5. package/dist/cli/commands/project.js +1 -1
  6. package/dist/cli/commands/project.js.map +1 -1
  7. package/dist/cli/commands/reads.js +21 -12
  8. package/dist/cli/commands/reads.js.map +1 -1
  9. package/dist/cli/commands/show.js +6 -2
  10. package/dist/cli/commands/show.js.map +1 -1
  11. package/dist/cli/commands/todo.js +1 -1
  12. package/dist/cli/commands/todo.js.map +1 -1
  13. package/dist/cli/commands/writes.js +116 -70
  14. package/dist/cli/commands/writes.js.map +1 -1
  15. package/dist/cli/glyphs.d.ts +9 -2
  16. package/dist/cli/glyphs.js +11 -5
  17. package/dist/cli/glyphs.js.map +1 -1
  18. package/dist/cli/read-driver.d.ts +39 -9
  19. package/dist/cli/read-driver.js +37 -16
  20. package/dist/cli/read-driver.js.map +1 -1
  21. package/dist/cli/render.d.ts +25 -26
  22. package/dist/cli/render.js +57 -56
  23. package/dist/cli/render.js.map +1 -1
  24. package/dist/client.d.ts +92 -35
  25. package/dist/client.js +61 -18
  26. package/dist/client.js.map +1 -1
  27. package/dist/contracts.d.ts +23 -33
  28. package/dist/contracts.js +1 -1
  29. package/dist/contracts.js.map +1 -1
  30. package/dist/index.d.ts +5 -4
  31. package/dist/index.js +2 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/mcp/server.js +158 -144
  34. package/dist/mcp/server.js.map +1 -1
  35. package/dist/model/dates.d.ts +26 -0
  36. package/dist/model/dates.js +46 -4
  37. package/dist/model/dates.js.map +1 -1
  38. package/dist/read/area-filter.d.ts +1 -1
  39. package/dist/read/area-filter.js +5 -4
  40. package/dist/read/area-filter.js.map +1 -1
  41. package/dist/read/area-view.d.ts +0 -11
  42. package/dist/read/area-view.js +1 -5
  43. package/dist/read/area-view.js.map +1 -1
  44. package/dist/read/project-view.d.ts +34 -19
  45. package/dist/read/project-view.js +26 -20
  46. package/dist/read/project-view.js.map +1 -1
  47. package/dist/read/scope.d.ts +1 -1
  48. package/dist/read/scope.js +5 -4
  49. package/dist/read/scope.js.map +1 -1
  50. package/dist/read/shape.d.ts +73 -14
  51. package/dist/read/shape.js +459 -151
  52. package/dist/read/shape.js.map +1 -1
  53. package/dist/read/truncation.d.ts +75 -2
  54. package/dist/read/truncation.js +69 -46
  55. package/dist/read/truncation.js.map +1 -1
  56. package/dist/read/views.d.ts +19 -18
  57. package/dist/read/views.js +5 -6
  58. package/dist/read/views.js.map +1 -1
  59. package/dist/surface-copy.d.ts +6 -0
  60. package/dist/surface-copy.js +6 -0
  61. package/dist/surface-copy.js.map +1 -1
  62. package/dist/write/batch.js +0 -1
  63. package/dist/write/batch.js.map +1 -1
  64. package/dist/write/commands.d.ts +25 -1
  65. package/dist/write/commands.js +256 -121
  66. package/dist/write/commands.js.map +1 -1
  67. package/dist/write/guards.js +27 -11
  68. package/dist/write/guards.js.map +1 -1
  69. package/dist/write/move.d.ts +26 -5
  70. package/dist/write/move.js +313 -2
  71. package/dist/write/move.js.map +1 -1
  72. package/dist/write/operations.d.ts +49 -19
  73. package/dist/write/operations.js +2 -2
  74. package/dist/write/operations.js.map +1 -1
  75. package/dist/write/pipeline.js +25 -4
  76. package/dist/write/pipeline.js.map +1 -1
  77. package/dist/write/pre-state.d.ts +48 -28
  78. package/dist/write/pre-state.js +134 -13
  79. package/dist/write/pre-state.js.map +1 -1
  80. package/dist/write/reorder.js +3 -3
  81. package/dist/write/reorder.js.map +1 -1
  82. package/dist/write/resolution-timestamps.d.ts +34 -0
  83. package/dist/write/resolution-timestamps.js +286 -0
  84. package/dist/write/resolution-timestamps.js.map +1 -0
  85. package/dist/write/reversibility.js +5 -5
  86. package/dist/write/reversibility.js.map +1 -1
  87. package/dist/write/scope-guard.js +0 -5
  88. package/dist/write/scope-guard.js.map +1 -1
  89. package/dist/write/undo.d.ts +0 -6
  90. package/dist/write/undo.js +44 -4
  91. package/dist/write/undo.js.map +1 -1
  92. package/dist/write/vectors/applescript.js +14 -4
  93. package/dist/write/vectors/applescript.js.map +1 -1
  94. package/dist/write/vectors/url-scheme.js +0 -8
  95. package/dist/write/vectors/url-scheme.js.map +1 -1
  96. package/dist/write/verify/delta.d.ts +16 -2
  97. package/dist/write/verify/delta.js +21 -2
  98. package/dist/write/verify/delta.js.map +1 -1
  99. package/package.json +1 -1
  100. package/schema/envelope.schema.json +2 -78
  101. package/skills/things-cli/SKILL.md +7 -5
  102. package/skills/things-cli/references/contracts.md +3 -4
  103. package/skills/things-cli/references/model.md +8 -8
  104. package/skills/things-cli/references/ordering.md +16 -7
@@ -1,4 +1,4 @@
1
- import { decodeReminderTime, encodeReminderTime, localToday, reminderUrlToken, } from "../model/dates.js";
1
+ import { decodeReminderTime, encodeReminderTime, localToday, reminderUrlToken, zonedWallInstant, } from "../model/dates.js";
2
2
  import { byUuid } from "../read/detail.js";
3
3
  import { isLooseRef } from "../read/pseudo-area.js";
4
4
  import { reminderIsLive } from "../read/stage.js";
@@ -144,6 +144,68 @@ function nonHeadingTitle(pre) {
144
144
  const target = pre.target;
145
145
  return target !== null && target.type !== "heading" ? target.title : "";
146
146
  }
147
+ /** True when an add carries a resolution-timestamp flag (routes it through json). */
148
+ function addHasTimestamps(p) {
149
+ return p.createdAt !== undefined || p.completedAt !== undefined;
150
+ }
151
+ /**
152
+ * Validate the resolution-timestamp flags on an add (shape, chronology, and the
153
+ * schedule contradiction). Instant ordering is zone-independent (both values
154
+ * share one effective zone), so the chronology check resolves against the host
155
+ * zone. Throws RangeError on a bad combination.
156
+ */
157
+ function assertAddTimestamps(p) {
158
+ if (p.createdAt !== undefined)
159
+ resolveResolutionInstant(p.createdAt);
160
+ if (p.completedAt !== undefined)
161
+ resolveResolutionInstant(p.completedAt);
162
+ if (p.createdAt !== undefined &&
163
+ p.completedAt !== undefined &&
164
+ resolveResolutionInstant(p.createdAt).getTime() >
165
+ resolveResolutionInstant(p.completedAt).getTime()) {
166
+ throw new RangeError("--created-at must not be after --completed-at");
167
+ }
168
+ if (p.completedAt !== undefined && (p.when !== undefined || p.reminder !== undefined)) {
169
+ throw new RangeError("--completed-at creates a resolved (Logbook) item, which has no active schedule — drop --when/--reminder");
170
+ }
171
+ if (p.reminder !== undefined && p.createdAt !== undefined) {
172
+ throw new RangeError("--reminder is not available with --created-at");
173
+ }
174
+ }
175
+ /** A `things:///json` import payload for a single created to-do/project. */
176
+ function addJsonUrl(type, attributes, token) {
177
+ return thingsUrl("json", { data: JSON.stringify([{ type, attributes }]) }, token);
178
+ }
179
+ /** The completion/creation json attributes for a timestamped add. */
180
+ function addDateAttributes(p, zone) {
181
+ const attrs = {};
182
+ if (p.createdAt !== undefined) {
183
+ attrs["creation-date"] = jsonTimestamp(resolveResolutionInstant(p.createdAt, zone));
184
+ }
185
+ if (p.completedAt !== undefined) {
186
+ attrs["completed"] = true;
187
+ attrs["completion-date"] = jsonTimestamp(resolveResolutionInstant(p.completedAt, zone));
188
+ }
189
+ return attrs;
190
+ }
191
+ /** The completion/creation delta assertions for a timestamped add. */
192
+ function addDateAssertions(p, zone) {
193
+ const assert = [];
194
+ if (p.completedAt !== undefined) {
195
+ assert.push({ field: "status", equals: "completed" });
196
+ assert.push({
197
+ field: "stoppedDate",
198
+ equals: hostLocalDate(resolveResolutionInstant(p.completedAt, zone)),
199
+ });
200
+ }
201
+ if (p.createdAt !== undefined) {
202
+ assert.push({
203
+ field: "createdDate",
204
+ equals: hostLocalDate(resolveResolutionInstant(p.createdAt, zone)),
205
+ });
206
+ }
207
+ return assert;
208
+ }
147
209
  // ----------------------------------------------------------------- commands
148
210
  const todoAdd = {
149
211
  op: "todo.add",
@@ -161,6 +223,7 @@ const todoAdd = {
161
223
  if (params.heading !== undefined && !containerGiven(params.project)) {
162
224
  throw new RangeError("heading requires a project destination");
163
225
  }
226
+ assertAddTimestamps(params);
164
227
  const pre = emptyPreState();
165
228
  if (containerGiven(params.project)) {
166
229
  pre.destProject = resolveProject(db, params.project);
@@ -182,13 +245,18 @@ const todoAdd = {
182
245
  const assert = [];
183
246
  if (params.notes !== undefined)
184
247
  assert.push({ field: "notes", equals: params.notes });
185
- if (params.when !== undefined)
186
- assert.push(...whenAssertions(params.when, ctx.todayIso));
187
- if (params.reminder !== undefined) {
188
- assert.push({ field: "reminder", equals: normalizeReminder(params.reminder) });
248
+ // A born-resolved (completedAt) item has no active schedule — skip when/reminder.
249
+ if (params.completedAt === undefined) {
250
+ if (params.when !== undefined)
251
+ assert.push(...whenAssertions(params.when, ctx.todayIso));
252
+ if (params.reminder !== undefined) {
253
+ assert.push({ field: "reminder", equals: normalizeReminder(params.reminder) });
254
+ }
255
+ if (params.deadline !== undefined) {
256
+ assert.push({ field: "deadline", equals: params.deadline });
257
+ }
189
258
  }
190
- if (params.deadline !== undefined)
191
- assert.push({ field: "deadline", equals: params.deadline });
259
+ assert.push(...addDateAssertions(params, ctx.zone));
192
260
  if (params.tags !== undefined) {
193
261
  assert.push({ field: "tags", equals: sortedTags(pre.resolvedTagTitles) });
194
262
  }
@@ -221,6 +289,30 @@ const todoAdd = {
221
289
  if (vector !== "url-scheme")
222
290
  unsupportedVector(this.op, vector);
223
291
  const container = pre.destProject?.resolved ?? pre.destArea?.resolved;
292
+ // Timestamped add: the only at-creation backdating surface is things:///json
293
+ // (the plain add URL drops date params — oddity 2g); route through it.
294
+ if (addHasTimestamps(params)) {
295
+ const attrs = { title: params.title };
296
+ if (params.notes !== undefined)
297
+ attrs["notes"] = params.notes;
298
+ Object.assign(attrs, addDateAttributes(params, ctx.zone));
299
+ if (params.completedAt === undefined) {
300
+ if (params.when !== undefined)
301
+ attrs["when"] = params.when;
302
+ if (params.deadline !== undefined)
303
+ attrs["deadline"] = params.deadline;
304
+ }
305
+ if (params.tags !== undefined)
306
+ attrs["tags"] = pre.resolvedTagTitles;
307
+ if (params.checklistItems !== undefined)
308
+ attrs["checklist-items"] = params.checklistItems;
309
+ if (container?.uuid !== undefined)
310
+ attrs["list-id"] = container.uuid;
311
+ if (pre.destHeading?.resolved?.title !== undefined) {
312
+ attrs["heading"] = pre.destHeading.resolved.title;
313
+ }
314
+ return addJsonUrl("to-do", attrs, ctx.token);
315
+ }
224
316
  return thingsUrl("add", {
225
317
  title: params.title,
226
318
  notes: params.notes,
@@ -604,6 +696,14 @@ const projectAdd = {
604
696
  op: "project.add",
605
697
  hazards: ["H-UNKNOWN-DESTINATION"],
606
698
  preRead(db, params) {
699
+ assertAddTimestamps(params);
700
+ // §5b (B-PROJ-JSON.2): a completed-project json import silently reverts to
701
+ // OPEN if any child spec is unresolved. Seed to-dos are always open, so a
702
+ // born-resolved project cannot carry them.
703
+ if (params.completedAt !== undefined && params.todos !== undefined && params.todos.length > 0) {
704
+ throw new RangeError("--completed-at cannot seed child to-dos: a completed-project import reverts to open " +
705
+ "unless every child is resolved (§5b) — create the project resolved, then add logged children");
706
+ }
607
707
  const pre = emptyPreState();
608
708
  if (containerGiven(params.area))
609
709
  pre.destArea = resolveArea(db, params.area);
@@ -614,10 +714,14 @@ const projectAdd = {
614
714
  const assert = [];
615
715
  if (params.notes !== undefined)
616
716
  assert.push({ field: "notes", equals: params.notes });
617
- if (params.when !== undefined)
618
- assert.push(...whenAssertions(params.when, ctx.todayIso));
619
- if (params.deadline !== undefined)
620
- assert.push({ field: "deadline", equals: params.deadline });
717
+ if (params.completedAt === undefined) {
718
+ if (params.when !== undefined)
719
+ assert.push(...whenAssertions(params.when, ctx.todayIso));
720
+ if (params.deadline !== undefined) {
721
+ assert.push({ field: "deadline", equals: params.deadline });
722
+ }
723
+ }
724
+ assert.push(...addDateAssertions(params, ctx.zone));
621
725
  const area = pre.destArea?.resolved;
622
726
  if (area !== undefined && area !== null)
623
727
  assert.push({ field: "area.uuid", equals: area.uuid });
@@ -635,6 +739,24 @@ const projectAdd = {
635
739
  compile(params, vector, pre, ctx) {
636
740
  if (vector !== "url-scheme")
637
741
  unsupportedVector(this.op, vector);
742
+ if (addHasTimestamps(params)) {
743
+ const attrs = { title: params.title };
744
+ if (params.notes !== undefined)
745
+ attrs["notes"] = params.notes;
746
+ Object.assign(attrs, addDateAttributes(params, ctx.zone));
747
+ if (params.completedAt === undefined) {
748
+ if (params.when !== undefined)
749
+ attrs["when"] = params.when;
750
+ if (params.deadline !== undefined)
751
+ attrs["deadline"] = params.deadline;
752
+ if (params.todos !== undefined) {
753
+ attrs["items"] = params.todos.map((t) => ({ type: "to-do", attributes: { title: t } }));
754
+ }
755
+ }
756
+ if (pre.destArea?.resolved?.uuid !== undefined)
757
+ attrs["area-id"] = pre.destArea.resolved.uuid;
758
+ return addJsonUrl("project", attrs, ctx.token);
759
+ }
638
760
  return thingsUrl("add-project", {
639
761
  title: params.title,
640
762
  notes: params.notes,
@@ -1229,7 +1351,7 @@ const tagUpdate = {
1229
1351
  const reorder = {
1230
1352
  op: "reorder",
1231
1353
  hazards: ["H-UNKNOWN-DESTINATION", "H-REORDER-SCOPE"],
1232
- preRead(db, params, now) {
1354
+ preRead(db, params, now, zone) {
1233
1355
  const pre = emptyPreState();
1234
1356
  let containerUuid = null;
1235
1357
  if (params.scope === "project") {
@@ -1254,18 +1376,36 @@ const reorder = {
1254
1376
  containerUuid = pre.destArea.resolved?.uuid ?? null;
1255
1377
  }
1256
1378
  }
1257
- pre.reorder = computeReorderPre(db, params, containerUuid, now);
1379
+ // The `reorder` op IS the pure-native private index wire — the ONE surface
1380
+ // runReorder dispatches here (strategy === "native"). So the LOGSORT ORD-13
1381
+ // permit is unconditionally offered (`admitResolved: true`); computeReorderPre
1382
+ // gates it internally on `key === "index"`, so day-axis native re-ranks
1383
+ // (today/container-day/tomorrow → todayIndex) still refuse resolved movees.
1384
+ // Every bounce/move/day-axis orchestrator calls computeReorderPre WITHOUT the
1385
+ // flag, so a resolved movee reaching an uncertified protocol stays refused.
1386
+ pre.reorder = computeReorderPre(db, params, containerUuid, now, { admitResolved: true, zone });
1258
1387
  return pre;
1259
1388
  },
1260
1389
  expectedDelta(pre, params) {
1261
1390
  // Verify the REQUESTED sequence (strictly ascending ranks). The wire
1262
1391
  // list pins the unrequested tail too, but the caller's contract is the
1263
1392
  // requested prefix; tail members are covered by pre-rank tripwires.
1393
+ // LOGSORT ORD-13 byte-lock: any admitted UNSWEPT-resolved movee must read
1394
+ // back index-only — status still closed, stoppedDate intact, umd unbumped
1395
+ // (a reopen would flip all three). Frozen assertions carry that into verify.
1396
+ const frozen = (pre.reorder?.resolvedMembers ?? []).map((m) => ({
1397
+ uuid: m.uuid,
1398
+ assert: [
1399
+ { field: "status", equals: m.status },
1400
+ { field: "stoppedDate", equals: m.stoppedDate },
1401
+ ],
1402
+ }));
1264
1403
  return {
1265
1404
  mode: "ordering",
1266
1405
  key: pre.reorder?.key ??
1267
1406
  (params.scope === "today" || params.scope === "evening" ? "todayIndex" : "index"),
1268
1407
  sequence: params.uuids,
1408
+ ...(frozen.length > 0 && { frozen }),
1269
1409
  };
1270
1410
  },
1271
1411
  compile(params, vector, pre) {
@@ -1311,117 +1451,108 @@ const reorder = {
1311
1451
  return osa(`${PRIVATE_REORDER_COMMAND} ${specifier} with ids ${q(wire.join(","))}`);
1312
1452
  },
1313
1453
  };
1314
- /** Locale-proof AppleScript date literal: local noon on an ISO date. */
1315
- function asDateBlock(varName, iso) {
1316
- const [y, m, d] = iso.split("-").map(Number);
1454
+ // ---- resolution-timestamp normalization (§5 of the plan) ------------------
1455
+ /**
1456
+ * Parse an ISO date (`2025-01-15`) OR datetime (`2025-01-15T09:30[:ss]`) and
1457
+ * resolve it to a single UTC instant in the effective `zone`. A date-only value
1458
+ * lands at NOON in that zone (B-DATEONLY: noon decodes to the intended calendar
1459
+ * date in every zone; midnight can slip a day). A datetime is read as wall-clock
1460
+ * time in the effective zone. `zone` undefined = the process-local (app host)
1461
+ * zone, which for a local CLI run IS the app's own zone.
1462
+ */
1463
+ export function resolveResolutionInstant(input, zone) {
1464
+ const dateOnly = /^(\d{4}-\d{2}-\d{2})$/.exec(input);
1465
+ if (dateOnly !== null)
1466
+ return zonedWallInstant(dateOnly[1], 12, 0, 0, zone);
1467
+ const dt = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(input);
1468
+ if (dt !== null) {
1469
+ return zonedWallInstant(dt[1], Number(dt[2]), Number(dt[3]), dt[4] === undefined ? 0 : Number(dt[4]), zone);
1470
+ }
1471
+ throw new RangeError(`invalid timestamp "${input}" — expected an ISO date (YYYY-MM-DD) or datetime (YYYY-MM-DDTHH:mm)`);
1472
+ }
1473
+ const pad2 = (n) => String(n).padStart(2, "0");
1474
+ /** The host-local calendar date (`YYYY-MM-DD`) an instant renders as — what the verify layer observes for stopped/created. */
1475
+ function hostLocalDate(instant) {
1476
+ return `${instant.getFullYear()}-${pad2(instant.getMonth() + 1)}-${pad2(instant.getDate())}`;
1477
+ }
1478
+ /**
1479
+ * The host-local calendar date a resolution-timestamp value resolves to — the
1480
+ * value the verify layer observes for `stoppedDate`/`createdDate`. Shared with
1481
+ * the multi-leg orchestrators so their synthesized deltas match the op's.
1482
+ */
1483
+ export function resolutionDeltaDate(input, zone) {
1484
+ return hostLocalDate(resolveResolutionInstant(input, zone));
1485
+ }
1486
+ /**
1487
+ * An instant as a second-precision UTC timestamp (`…Thh:mm:ssZ`) for the json
1488
+ * import. WITHOUT milliseconds: the app's json date parser rejects fractional
1489
+ * seconds — a `.000Z` stamp fails the whole command (oddity 2h; P4d's validated
1490
+ * shape was second-precision).
1491
+ */
1492
+ function jsonTimestamp(instant) {
1493
+ return instant.toISOString().replace(/\.\d{3}Z$/, "Z");
1494
+ }
1495
+ /**
1496
+ * A locale-proof AppleScript date literal built from an instant's HOST-LOCAL
1497
+ * wall-clock components — AS `current date` mutation lives in the host zone, so
1498
+ * setting those components reproduces exactly this instant regardless of the
1499
+ * effective zone the noon was computed in.
1500
+ */
1501
+ function asDateBlockFromInstant(varName, instant) {
1317
1502
  return [
1318
1503
  `set ${varName} to current date`,
1319
- `set time of ${varName} to 12 * hours`,
1504
+ `set time of ${varName} to ${instant.getHours()} * hours + ${instant.getMinutes()} * minutes + ${instant.getSeconds()}`,
1320
1505
  `set day of ${varName} to 1`,
1321
- `set year of ${varName} to ${y}`,
1322
- `set month of ${varName} to ${m}`,
1323
- `set day of ${varName} to ${d}`,
1506
+ `set year of ${varName} to ${instant.getFullYear()}`,
1507
+ `set month of ${varName} to ${instant.getMonth() + 1}`,
1508
+ `set day of ${varName} to ${instant.getDate()}`,
1324
1509
  ];
1325
1510
  }
1326
- /**
1327
- * Local noon -> UTC instant, so the stored timestamp decodes back to the
1328
- * requested local DATE in every timezone (P4d: json attrs honored exactly).
1329
- * WITHOUT milliseconds: the app's json date parser rejects fractional
1330
- * seconds — a `.000Z` timestamp fails the whole command (error modal, no
1331
- * write; caught live by the e2e 2026-07-09 — P4d's validated shape was
1332
- * second-precision).
1333
- */
1334
- function utcNoon(iso) {
1335
- const [y, m, d] = iso.split("-").map(Number);
1336
- return new Date(y ?? 0, (m ?? 1) - 1, d ?? 1, 12, 0, 0).toISOString().replace(/\.\d{3}Z$/, "Z");
1511
+ /** Shared spec for the kind-agnostic `set-dates` op (to-do / project). */
1512
+ function setDatesSpec(op, addressor) {
1513
+ return {
1514
+ op,
1515
+ hazards: ["H-UNKNOWN-DESTINATION", "H-BACKDATE-OPEN"],
1516
+ preRead(db, params) {
1517
+ if (params.completionDate === undefined && params.creationDate === undefined) {
1518
+ throw new RangeError("nothing to set: give completionDate and/or creationDate");
1519
+ }
1520
+ const pre = emptyPreState();
1521
+ pre.target = loadTarget(db, params.uuid);
1522
+ return pre;
1523
+ },
1524
+ expectedDelta(_pre, params, ctx) {
1525
+ const assert = [];
1526
+ if (params.completionDate !== undefined) {
1527
+ assert.push({
1528
+ field: "stoppedDate",
1529
+ equals: hostLocalDate(resolveResolutionInstant(params.completionDate, ctx.zone)),
1530
+ });
1531
+ }
1532
+ if (params.creationDate !== undefined) {
1533
+ assert.push({
1534
+ field: "createdDate",
1535
+ equals: hostLocalDate(resolveResolutionInstant(params.creationDate, ctx.zone)),
1536
+ });
1537
+ }
1538
+ return { mode: "update", uuid: params.uuid, assert };
1539
+ },
1540
+ compile(params, vector, _pre, ctx) {
1541
+ if (vector !== "applescript")
1542
+ unsupportedVector(this.op, vector);
1543
+ const statements = [];
1544
+ if (params.completionDate !== undefined) {
1545
+ statements.push(...asDateBlockFromInstant("compDate", resolveResolutionInstant(params.completionDate, ctx.zone)), `set completion date of ${addressor} id ${q(params.uuid)} to compDate`);
1546
+ }
1547
+ if (params.creationDate !== undefined) {
1548
+ statements.push(...asDateBlockFromInstant("createDate", resolveResolutionInstant(params.creationDate, ctx.zone)), `set creation date of ${addressor} id ${q(params.uuid)} to createDate`);
1549
+ }
1550
+ return osaBlock(statements);
1551
+ },
1552
+ };
1337
1553
  }
1338
- const todoBackdate = {
1339
- op: "todo.backdate",
1340
- hazards: ["H-UNKNOWN-DESTINATION", "H-BACKDATE-OPEN"],
1341
- preRead(db, params) {
1342
- if (params.completionDate === undefined && params.creationDate === undefined) {
1343
- throw new RangeError("nothing to backdate: give completionDate and/or creationDate");
1344
- }
1345
- const pre = emptyPreState();
1346
- pre.target = loadTarget(db, params.uuid);
1347
- return pre;
1348
- },
1349
- expectedDelta(_pre, params) {
1350
- const assert = [];
1351
- if (params.completionDate !== undefined) {
1352
- assert.push({ field: "stoppedDate", equals: params.completionDate });
1353
- }
1354
- if (params.creationDate !== undefined) {
1355
- assert.push({ field: "createdDate", equals: params.creationDate });
1356
- }
1357
- return { mode: "update", uuid: params.uuid, assert };
1358
- },
1359
- compile(params, vector) {
1360
- if (vector !== "applescript")
1361
- unsupportedVector(this.op, vector);
1362
- const statements = [];
1363
- if (params.completionDate !== undefined) {
1364
- statements.push(...asDateBlock("compDate", params.completionDate), `set completion date of to do id ${q(params.uuid)} to compDate`);
1365
- }
1366
- if (params.creationDate !== undefined) {
1367
- statements.push(...asDateBlock("createDate", params.creationDate), `set creation date of to do id ${q(params.uuid)} to createDate`);
1368
- }
1369
- return osaBlock(statements);
1370
- },
1371
- };
1372
- const todoAddLogged = {
1373
- op: "todo.add-logged",
1374
- hazards: [],
1375
- preRead(db, params) {
1376
- if (params.creationDate !== undefined &&
1377
- params.creationDate.localeCompare(params.completionDate) > 0) {
1378
- throw new RangeError("creationDate must not be after completionDate");
1379
- }
1380
- const pre = emptyPreState();
1381
- pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "to-do");
1382
- return pre;
1383
- },
1384
- expectedDelta(pre, params) {
1385
- const assert = [
1386
- { field: "status", equals: "completed" },
1387
- { field: "stoppedDate", equals: params.completionDate },
1388
- ];
1389
- if (params.creationDate !== undefined) {
1390
- assert.push({ field: "createdDate", equals: params.creationDate });
1391
- }
1392
- if (params.notes !== undefined)
1393
- assert.push({ field: "notes", equals: params.notes });
1394
- return {
1395
- mode: "create",
1396
- probe: {
1397
- title: params.title,
1398
- type: "to-do",
1399
- sinceEpoch: 0,
1400
- excludeUuids: pre.sameTitleUuids,
1401
- },
1402
- assert,
1403
- };
1404
- },
1405
- compile(params, vector, _pre, ctx) {
1406
- if (vector !== "url-scheme")
1407
- unsupportedVector(this.op, vector);
1408
- const payload = JSON.stringify([
1409
- {
1410
- type: "to-do",
1411
- attributes: {
1412
- title: params.title,
1413
- ...(params.notes !== undefined && { notes: params.notes }),
1414
- completed: true,
1415
- "completion-date": utcNoon(params.completionDate),
1416
- ...(params.creationDate !== undefined && {
1417
- "creation-date": utcNoon(params.creationDate),
1418
- }),
1419
- },
1420
- },
1421
- ]);
1422
- return thingsUrl("json", { data: payload }, ctx.token);
1423
- },
1424
- };
1554
+ const todoSetDates = setDatesSpec("todo.set-dates", "to do");
1555
+ const projectSetDates = setDatesSpec("project.set-dates", "project");
1425
1556
  /** Children of a heading (open ones drive the archive policies). */
1426
1557
  function headingChildren(db, headingUuid) {
1427
1558
  const rows = db
@@ -1589,7 +1720,11 @@ const projectMoveHeading = {
1589
1720
  // fed the computed full order.
1590
1721
  if (vector !== "applescript")
1591
1722
  unsupportedVector(this.op, vector);
1592
- const order = pre.headingMove?.targetOrder ?? [];
1723
+ // #V11: send the MINIMAL front-cluster wire, not the full order an archived
1724
+ // heading that need not move stays out of it (untouched, HEADSORT H-UNSWEPT);
1725
+ // any archived heading the target FORCES in (pre.headingMove.reopened) is
1726
+ // reopened and disclosed. NEVER put un-targeted rows into the wire.
1727
+ const order = pre.headingMove?.wire ?? [];
1593
1728
  return osa(`${PRIVATE_REORDER_COMMAND} project id ${q(pre.destProject?.resolved?.uuid ?? "")} ` +
1594
1729
  `with ids ${q(order.join(","))}`);
1595
1730
  },
@@ -2252,8 +2387,6 @@ export const COMMANDS = {
2252
2387
  "project.reopen": projectReopen,
2253
2388
  "project.restore": projectRestore,
2254
2389
  "project.set-tags": projectSetTags,
2255
- "todo.backdate": todoBackdate,
2256
- "todo.add-logged": todoAddLogged,
2257
2390
  "project.add-heading": headingAdd,
2258
2391
  "project.rename-heading": headingRename,
2259
2392
  "project.archive-heading": headingArchive,
@@ -2271,6 +2404,8 @@ export const COMMANDS = {
2271
2404
  "project.reschedule-repeat": projectRescheduleRepeat,
2272
2405
  "project.pause-repeat": projectPauseRepeat,
2273
2406
  "project.resume-repeat": projectResumeRepeat,
2407
+ "todo.set-dates": todoSetDates,
2408
+ "project.set-dates": projectSetDates,
2274
2409
  "area.reorder": areaReorderSidebar,
2275
2410
  "project.make-repeating": projectMakeRepeating,
2276
2411
  "project.add-repeating": projectAddRepeating,