things-api 0.14.0 → 0.15.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 (103) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/area.js +1 -1
  3. package/dist/cli/commands/project.js +1 -1
  4. package/dist/cli/commands/project.js.map +1 -1
  5. package/dist/cli/commands/reads.js +73 -4
  6. package/dist/cli/commands/reads.js.map +1 -1
  7. package/dist/cli/commands/todo.js +1 -1
  8. package/dist/cli/commands/todo.js.map +1 -1
  9. package/dist/cli/commands/writes.js +20 -0
  10. package/dist/cli/commands/writes.js.map +1 -1
  11. package/dist/cli/glyphs.js +9 -10
  12. package/dist/cli/glyphs.js.map +1 -1
  13. package/dist/cli/help.js +4 -2
  14. package/dist/cli/help.js.map +1 -1
  15. package/dist/cli/read-driver.d.ts +7 -1
  16. package/dist/cli/read-driver.js +3 -1
  17. package/dist/cli/read-driver.js.map +1 -1
  18. package/dist/cli/render.d.ts +33 -2
  19. package/dist/cli/render.js +60 -9
  20. package/dist/cli/render.js.map +1 -1
  21. package/dist/client.d.ts +35 -3
  22. package/dist/client.js +25 -4
  23. package/dist/client.js.map +1 -1
  24. package/dist/contracts.d.ts +13 -2
  25. package/dist/contracts.js +1 -1
  26. package/dist/contracts.js.map +1 -1
  27. package/dist/index.d.ts +4 -3
  28. package/dist/index.js +1 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/mcp/server.js +56 -6
  31. package/dist/mcp/server.js.map +1 -1
  32. package/dist/model/entities.d.ts +61 -39
  33. package/dist/model/entities.js +0 -4
  34. package/dist/model/entities.js.map +1 -1
  35. package/dist/model/mappers.js +24 -43
  36. package/dist/model/mappers.js.map +1 -1
  37. package/dist/read/area-view.js +6 -6
  38. package/dist/read/area-view.js.map +1 -1
  39. package/dist/read/detail.js +2 -2
  40. package/dist/read/detail.js.map +1 -1
  41. package/dist/read/filter-contract.d.ts +1 -1
  42. package/dist/read/filter-contract.js +3 -0
  43. package/dist/read/filter-contract.js.map +1 -1
  44. package/dist/read/log-boundary.d.ts +46 -2
  45. package/dist/read/log-boundary.js +43 -2
  46. package/dist/read/log-boundary.js.map +1 -1
  47. package/dist/read/predicates.d.ts +13 -0
  48. package/dist/read/predicates.js +13 -0
  49. package/dist/read/predicates.js.map +1 -1
  50. package/dist/read/project-view.js +5 -5
  51. package/dist/read/project-view.js.map +1 -1
  52. package/dist/read/search-rank.js +2 -2
  53. package/dist/read/search-rank.js.map +1 -1
  54. package/dist/read/shape.d.ts +5 -4
  55. package/dist/read/shape.js +53 -50
  56. package/dist/read/shape.js.map +1 -1
  57. package/dist/read/snapshot.js +2 -2
  58. package/dist/read/snapshot.js.map +1 -1
  59. package/dist/read/stage.d.ts +31 -13
  60. package/dist/read/stage.js +28 -11
  61. package/dist/read/stage.js.map +1 -1
  62. package/dist/read/views.d.ts +62 -0
  63. package/dist/read/views.js +131 -11
  64. package/dist/read/views.js.map +1 -1
  65. package/dist/write/clear-reminder.js +3 -1
  66. package/dist/write/clear-reminder.js.map +1 -1
  67. package/dist/write/commands.js +106 -25
  68. package/dist/write/commands.js.map +1 -1
  69. package/dist/write/guards.js +11 -7
  70. package/dist/write/guards.js.map +1 -1
  71. package/dist/write/move.js +45 -0
  72. package/dist/write/move.js.map +1 -1
  73. package/dist/write/operations.d.ts +4 -3
  74. package/dist/write/operations.js +1 -0
  75. package/dist/write/operations.js.map +1 -1
  76. package/dist/write/pipeline.js +3 -3
  77. package/dist/write/pipeline.js.map +1 -1
  78. package/dist/write/pre-state.d.ts +41 -0
  79. package/dist/write/pre-state.js +64 -7
  80. package/dist/write/pre-state.js.map +1 -1
  81. package/dist/write/reorder.js +49 -2
  82. package/dist/write/reorder.js.map +1 -1
  83. package/dist/write/resolution-timestamps.js +7 -7
  84. package/dist/write/resolution-timestamps.js.map +1 -1
  85. package/dist/write/reversibility.js +5 -0
  86. package/dist/write/reversibility.js.map +1 -1
  87. package/dist/write/scope-guard.js +2 -0
  88. package/dist/write/scope-guard.js.map +1 -1
  89. package/dist/write/undo.d.ts +1 -1
  90. package/dist/write/undo.js +19 -12
  91. package/dist/write/undo.js.map +1 -1
  92. package/dist/write/vectors/applescript.js +9 -0
  93. package/dist/write/vectors/applescript.js.map +1 -1
  94. package/dist/write/vectors/simulator.js +210 -9
  95. package/dist/write/vectors/simulator.js.map +1 -1
  96. package/dist/write/verify/delta.d.ts +55 -10
  97. package/dist/write/verify/delta.js +73 -10
  98. package/dist/write/verify/delta.js.map +1 -1
  99. package/package.json +1 -1
  100. package/schema/envelope.schema.json +25 -0
  101. package/skills/things-cli/SKILL.md +9 -7
  102. package/skills/things-cli/references/{model.md → data-model.md} +3 -1
  103. package/skills/things-cli/references/ordering.md +2 -0
@@ -1,5 +1,6 @@
1
1
  import { decodeReminderTime, encodeReminderTime, localToday, reminderUrlToken, zonedWallInstant, } from "../model/dates.js";
2
2
  import { byUuid } from "../read/detail.js";
3
+ import { manualLogDateEpoch, pendingLogCount } from "../read/log-boundary.js";
3
4
  import { isLooseRef } from "../read/pseudo-area.js";
4
5
  import { reminderIsLive } from "../read/stage.js";
5
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";
@@ -62,7 +63,7 @@ function q(value) {
62
63
  function unsupportedVector(op, vector) {
63
64
  throw new Error(`${op} cannot be compiled for vector ${vector} (planner bug)`);
64
65
  }
65
- function whenAssertions(when, todayIso) {
66
+ function whenAssertions(when, todayIso, opts = { mode: "add" }) {
66
67
  // Strict shape check: an unvalidated string used to flow straight into the
67
68
  // URL (e.g. "2026-07-20@09:30", the raw URL grammar) — the app would SET
68
69
  // date+reminder while verification asserted the literal string as the date,
@@ -78,16 +79,35 @@ function whenAssertions(when, todayIso) {
78
79
  }
79
80
  switch (when) {
80
81
  case "today":
82
+ // Today's non-evening section: in Today (the `today` marker) AND NOT in the
83
+ // evening sub-bucket (the presence-keyed `evening` marker absent — asserted
84
+ // as null, which valuesEqual treats as absent). Gated to Today members under
85
+ // the verify clock exactly as the retired `todaySection` was.
86
+ //
87
+ // The `startDate` assertion differs by op (field bug §0½.8):
88
+ // - ADD mints a fresh Today item with no schedule history, so the app dates
89
+ // it EXACTLY today — exact equality is right.
90
+ // - UPDATE of an item ALREADY in Today whose `startDate` has already arrived
91
+ // (past or today): the app PRESERVES that historical date rather than
92
+ // rewriting the storage byte to today (arrived-date law). Assert the
93
+ // arrived-date PREDICATE (non-null and <= today) so a preserved historical
94
+ // date verifies, while an undated deadline-only pull (null startDate) is
95
+ // still rejected — the item's Today membership then rests only on a
96
+ // deadline, not on the requested schedule.
81
97
  return [
82
98
  { field: "start", equals: "active" },
83
- { field: "startDate", equals: todayIso },
84
- { field: "todaySection", equals: "today" },
99
+ opts.mode === "update"
100
+ ? { field: "startDate", satisfies: { predicate: "arrived-on-or-before", date: todayIso } }
101
+ : { field: "startDate", equals: todayIso },
102
+ { field: "today", equals: true },
103
+ { field: "evening", equals: null },
85
104
  ];
86
105
  case "evening":
106
+ // The This-Evening sub-bucket: the `evening` marker (which implies `today`).
87
107
  return [
88
108
  { field: "start", equals: "active" },
89
109
  { field: "startDate", equals: todayIso },
90
- { field: "todaySection", equals: "evening" },
110
+ { field: "evening", equals: true },
91
111
  ];
92
112
  case "anytime":
93
113
  return [
@@ -304,8 +324,11 @@ const todoAdd = {
304
324
  }
305
325
  if (params.tags !== undefined)
306
326
  attrs["tags"] = pre.resolvedTagTitles;
307
- if (params.checklistItems !== undefined)
308
- attrs["checklist-items"] = params.checklistItems;
327
+ if (params.checklistItems !== undefined) {
328
+ // §9y: things:///json rejects a bare STRING array wholesale (silent
329
+ // no-op) — emit the OBJECT-array shape the app accepts.
330
+ attrs["checklist-items"] = checklistItemsJsonAttr(params.checklistItems.map((title) => ({ title })));
331
+ }
309
332
  if (container?.uuid !== undefined)
310
333
  attrs["list-id"] = container.uuid;
311
334
  if (pre.destHeading?.resolved?.title !== undefined) {
@@ -353,7 +376,12 @@ function effectiveReminder(pre, params) {
353
376
  return null;
354
377
  if (!reminderIsLive(target.startDate, pre.todayIso))
355
378
  return null;
356
- return target.reminder;
379
+ // Read the RAW stored byte off the substrate (top-level `reminder` is
380
+ // live-gated under the LOAD clock, which can differ from `pre.todayIso` under a
381
+ // pinned THINGS_NOW). The `reminderIsLive` guard above applies liveness under
382
+ // the injected clock explicitly, so the raw byte + this gate reproduce the
383
+ // former semantics exactly.
384
+ return target.derived.reminder;
357
385
  }
358
386
  function assertNotesModesExclusive(params) {
359
387
  if (params.notes !== undefined &&
@@ -396,7 +424,7 @@ const todoUpdate = {
396
424
  if (joined !== undefined)
397
425
  assert.push({ field: "notes", equals: joined });
398
426
  if (params.when !== undefined) {
399
- assert.push(...whenAssertions(params.when, ctx.todayIso));
427
+ assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
400
428
  const reminder = effectiveReminder(pre, params);
401
429
  assert.push({
402
430
  field: "reminder",
@@ -601,6 +629,21 @@ const todoSetTags = {
601
629
  },
602
630
  };
603
631
  /** Normalize the string | spec union; decide whether states force the json form. */
632
+ /**
633
+ * The `checklist-items` value `things:///json` accepts: an array of
634
+ * `{type:"checklist-item",attributes:{title[,completed]}}` OBJECTS. A bare
635
+ * STRING array is rejected wholesale — the whole import silently no-ops, no row,
636
+ * no error (§9y; RESID1 R-JSONPAR RAW-a..d). `completed` is emitted only when a
637
+ * spec carries it, so an at-creation add (all items open) yields the minimal
638
+ * `{title}` attributes the probe validated (OBJ/OBJ3), while the checklist
639
+ * replace path forwards its per-item state.
640
+ */
641
+ function checklistItemsJsonAttr(specs) {
642
+ return specs.map((s) => ({
643
+ type: "checklist-item",
644
+ attributes: s.completed === undefined ? { title: s.title } : { title: s.title, completed: s.completed },
645
+ }));
646
+ }
604
647
  function checklistSpecs(items) {
605
648
  const specs = items.map((i) => typeof i === "string"
606
649
  ? { title: i, completed: false }
@@ -647,10 +690,7 @@ const todoReplaceChecklist = {
647
690
  operation: "update",
648
691
  id: params.uuid,
649
692
  attributes: {
650
- "checklist-items": specs.map((s) => ({
651
- type: "checklist-item",
652
- attributes: { title: s.title, completed: s.completed },
653
- })),
693
+ "checklist-items": checklistItemsJsonAttr(specs),
654
694
  },
655
695
  },
656
696
  ]);
@@ -787,7 +827,7 @@ const projectUpdate = {
787
827
  if (joined !== undefined)
788
828
  assert.push({ field: "notes", equals: joined });
789
829
  if (params.when !== undefined) {
790
- assert.push(...whenAssertions(params.when, ctx.todayIso));
830
+ assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
791
831
  // Projects carry the same reminderTime codec as to-dos (A3); a bare
792
832
  // when= clears an existing reminder unless auto-preserved.
793
833
  const reminder = effectiveReminder(pre, params);
@@ -1390,6 +1430,13 @@ const reorder = {
1390
1430
  // Verify the REQUESTED sequence (strictly ascending ranks). The wire
1391
1431
  // list pins the unrequested tail too, but the caller's contract is the
1392
1432
  // requested prefix; tail members are covered by pre-rank tripwires.
1433
+ // TODWIRE: on the `today` scope the wire is now MINIMAL — only the named
1434
+ // block gets fresh (front, monotonic) `todayIndex`; the unnamed tail keeps
1435
+ // its cohort-interleaved (non-monotonic) `todayIndex`, so the FULL
1436
+ // `params.uuids` is NOT strictly-ascending after the write. Verify the
1437
+ // `todayWire` (the named block) instead — it lands at the visible front in
1438
+ // wire order with fresh ascending values, and the untouched tail rides the
1439
+ // pre-rank tripwires. Every other scope verifies the requested prefix.
1393
1440
  // LOGSORT ORD-13 byte-lock: any admitted UNSWEPT-resolved movee must read
1394
1441
  // back index-only — status still closed, stoppedDate intact, umd unbumped
1395
1442
  // (a reopen would flip all three). Frozen assertions carry that into verify.
@@ -1400,11 +1447,14 @@ const reorder = {
1400
1447
  { field: "stoppedDate", equals: m.stoppedDate },
1401
1448
  ],
1402
1449
  }));
1450
+ const sequence = params.scope === "today" && pre.reorder?.todayWire != null
1451
+ ? pre.reorder.todayWire
1452
+ : params.uuids;
1403
1453
  return {
1404
1454
  mode: "ordering",
1405
1455
  key: pre.reorder?.key ??
1406
1456
  (params.scope === "today" || params.scope === "evening" ? "todayIndex" : "index"),
1407
- sequence: params.uuids,
1457
+ sequence,
1408
1458
  ...(frozen.length > 0 && { frozen }),
1409
1459
  };
1410
1460
  },
@@ -1429,7 +1479,14 @@ const reorder = {
1429
1479
  : params.scope === "upcoming"
1430
1480
  ? `list "Upcoming"`
1431
1481
  : `list "Today"`;
1432
- const wire = pre.reorder?.wireList ?? params.uuids;
1482
+ // TODWIRE the `today` scope sends the MINIMAL visible-order wire (names only
1483
+ // what must move; unnamed rows keep their entry cohorts + visible positions),
1484
+ // not the OLD full `wireList` that fused every cohort (MOVPLC/ORD-20). Every
1485
+ // other native scope keeps the full wire (their `index`/day axes do not
1486
+ // re-stamp `tiRef`, so a full re-rank is non-damaging).
1487
+ const wire = (params.scope === "today" ? pre.reorder?.todayWire : undefined) ??
1488
+ pre.reorder?.wireList ??
1489
+ params.uuids;
1433
1490
  if (params.scope === "someday") {
1434
1491
  // The Someday handler STACKS each sent id above the list's current top
1435
1492
  // (the current top itself never moves), with OPPOSITE stack directions
@@ -1514,8 +1571,8 @@ function setDatesSpec(op, addressor) {
1514
1571
  op,
1515
1572
  hazards: ["H-UNKNOWN-DESTINATION", "H-BACKDATE-OPEN"],
1516
1573
  preRead(db, params) {
1517
- if (params.completionDate === undefined && params.creationDate === undefined) {
1518
- throw new RangeError("nothing to set: give completionDate and/or creationDate");
1574
+ if (params.completedAt === undefined && params.createdAt === undefined) {
1575
+ throw new RangeError("nothing to set: give completedAt and/or createdAt");
1519
1576
  }
1520
1577
  const pre = emptyPreState();
1521
1578
  pre.target = loadTarget(db, params.uuid);
@@ -1523,16 +1580,16 @@ function setDatesSpec(op, addressor) {
1523
1580
  },
1524
1581
  expectedDelta(_pre, params, ctx) {
1525
1582
  const assert = [];
1526
- if (params.completionDate !== undefined) {
1583
+ if (params.completedAt !== undefined) {
1527
1584
  assert.push({
1528
1585
  field: "stoppedDate",
1529
- equals: hostLocalDate(resolveResolutionInstant(params.completionDate, ctx.zone)),
1586
+ equals: hostLocalDate(resolveResolutionInstant(params.completedAt, ctx.zone)),
1530
1587
  });
1531
1588
  }
1532
- if (params.creationDate !== undefined) {
1589
+ if (params.createdAt !== undefined) {
1533
1590
  assert.push({
1534
1591
  field: "createdDate",
1535
- equals: hostLocalDate(resolveResolutionInstant(params.creationDate, ctx.zone)),
1592
+ equals: hostLocalDate(resolveResolutionInstant(params.createdAt, ctx.zone)),
1536
1593
  });
1537
1594
  }
1538
1595
  return { mode: "update", uuid: params.uuid, assert };
@@ -1541,11 +1598,11 @@ function setDatesSpec(op, addressor) {
1541
1598
  if (vector !== "applescript")
1542
1599
  unsupportedVector(this.op, vector);
1543
1600
  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`);
1601
+ if (params.completedAt !== undefined) {
1602
+ statements.push(...asDateBlockFromInstant("compDate", resolveResolutionInstant(params.completedAt, ctx.zone)), `set completion date of ${addressor} id ${q(params.uuid)} to compDate`);
1546
1603
  }
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`);
1604
+ if (params.createdAt !== undefined) {
1605
+ statements.push(...asDateBlockFromInstant("createDate", resolveResolutionInstant(params.createdAt, ctx.zone)), `set creation date of ${addressor} id ${q(params.uuid)} to createDate`);
1549
1606
  }
1550
1607
  return osaBlock(statements);
1551
1608
  },
@@ -2356,6 +2413,29 @@ const trashEmpty = {
2356
2413
  return osa("empty trash");
2357
2414
  },
2358
2415
  };
2416
+ const logNow = {
2417
+ op: "log-now",
2418
+ hazards: [],
2419
+ preRead(db, _params, now, zone) {
2420
+ const pre = emptyPreState();
2421
+ // The resolved-but-unlogged census the verb will move (the disclosed count),
2422
+ // and the pre-op boundary stamp the delta compares against.
2423
+ pre.logNow = {
2424
+ pending: pendingLogCount(db, now, zone),
2425
+ manualLogDatePre: manualLogDateEpoch(db),
2426
+ };
2427
+ return pre;
2428
+ },
2429
+ expectedDelta(pre) {
2430
+ const ln = pre.logNow ?? { pending: 0, manualLogDatePre: null };
2431
+ return { mode: "logged-now", pending: ln.pending, manualLogDatePre: ln.manualLogDatePre };
2432
+ },
2433
+ compile(_params, vector) {
2434
+ if (vector !== "applescript")
2435
+ unsupportedVector(this.op, vector);
2436
+ return osa("log completed now");
2437
+ },
2438
+ };
2359
2439
  export const COMMANDS = {
2360
2440
  "todo.add": todoAdd,
2361
2441
  "todo.update": todoUpdate,
@@ -2409,5 +2489,6 @@ export const COMMANDS = {
2409
2489
  "area.reorder": areaReorderSidebar,
2410
2490
  "project.make-repeating": projectMakeRepeating,
2411
2491
  "project.add-repeating": projectAddRepeating,
2492
+ "log-now": logNow,
2412
2493
  };
2413
2494
  //# sourceMappingURL=commands.js.map