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.
- package/README.md +2 -2
- package/dist/cli/commands/area.js +1 -1
- package/dist/cli/commands/project.js +1 -1
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +73 -4
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/todo.js +1 -1
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +20 -0
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/glyphs.js +9 -10
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +4 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/read-driver.d.ts +7 -1
- package/dist/cli/read-driver.js +3 -1
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +33 -2
- package/dist/cli/render.js +60 -9
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +35 -3
- package/dist/client.js +25 -4
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +13 -2
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +56 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +61 -39
- package/dist/model/entities.js +0 -4
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +24 -43
- package/dist/model/mappers.js.map +1 -1
- package/dist/read/area-view.js +6 -6
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.js +2 -2
- package/dist/read/detail.js.map +1 -1
- package/dist/read/filter-contract.d.ts +1 -1
- package/dist/read/filter-contract.js +3 -0
- package/dist/read/filter-contract.js.map +1 -1
- package/dist/read/log-boundary.d.ts +46 -2
- package/dist/read/log-boundary.js +43 -2
- package/dist/read/log-boundary.js.map +1 -1
- package/dist/read/predicates.d.ts +13 -0
- package/dist/read/predicates.js +13 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.js +5 -5
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/search-rank.js +2 -2
- package/dist/read/search-rank.js.map +1 -1
- package/dist/read/shape.d.ts +5 -4
- package/dist/read/shape.js +53 -50
- package/dist/read/shape.js.map +1 -1
- package/dist/read/snapshot.js +2 -2
- package/dist/read/snapshot.js.map +1 -1
- package/dist/read/stage.d.ts +31 -13
- package/dist/read/stage.js +28 -11
- package/dist/read/stage.js.map +1 -1
- package/dist/read/views.d.ts +62 -0
- package/dist/read/views.js +131 -11
- package/dist/read/views.js.map +1 -1
- package/dist/write/clear-reminder.js +3 -1
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.js +106 -25
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.js +11 -7
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.js +45 -0
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +4 -3
- package/dist/write/operations.js +1 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.js +3 -3
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +41 -0
- package/dist/write/pre-state.js +64 -7
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +49 -2
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/resolution-timestamps.js +7 -7
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +5 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.js +2 -0
- package/dist/write/scope-guard.js.map +1 -1
- package/dist/write/undo.d.ts +1 -1
- package/dist/write/undo.js +19 -12
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +9 -0
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/simulator.js +210 -9
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/verify/delta.d.ts +55 -10
- package/dist/write/verify/delta.js +73 -10
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +25 -0
- package/skills/things-cli/SKILL.md +9 -7
- package/skills/things-cli/references/{model.md → data-model.md} +3 -1
- package/skills/things-cli/references/ordering.md +2 -0
package/dist/write/commands.js
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
1518
|
-
throw new RangeError("nothing to set: give
|
|
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.
|
|
1583
|
+
if (params.completedAt !== undefined) {
|
|
1527
1584
|
assert.push({
|
|
1528
1585
|
field: "stoppedDate",
|
|
1529
|
-
equals: hostLocalDate(resolveResolutionInstant(params.
|
|
1586
|
+
equals: hostLocalDate(resolveResolutionInstant(params.completedAt, ctx.zone)),
|
|
1530
1587
|
});
|
|
1531
1588
|
}
|
|
1532
|
-
if (params.
|
|
1589
|
+
if (params.createdAt !== undefined) {
|
|
1533
1590
|
assert.push({
|
|
1534
1591
|
field: "createdDate",
|
|
1535
|
-
equals: hostLocalDate(resolveResolutionInstant(params.
|
|
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.
|
|
1545
|
-
statements.push(...asDateBlockFromInstant("compDate", resolveResolutionInstant(params.
|
|
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.
|
|
1548
|
-
statements.push(...asDateBlockFromInstant("createDate", resolveResolutionInstant(params.
|
|
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
|