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.
- package/README.md +2 -2
- package/dist/cli/commands/area.d.ts +5 -5
- package/dist/cli/commands/area.js +12 -7
- package/dist/cli/commands/area.js.map +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 +21 -12
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.js +6 -2
- package/dist/cli/commands/show.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 +116 -70
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/glyphs.d.ts +9 -2
- package/dist/cli/glyphs.js +11 -5
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/read-driver.d.ts +39 -9
- package/dist/cli/read-driver.js +37 -16
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +25 -26
- package/dist/cli/render.js +57 -56
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +92 -35
- package/dist/client.js +61 -18
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +23 -33
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +158 -144
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/dates.d.ts +26 -0
- package/dist/model/dates.js +46 -4
- package/dist/model/dates.js.map +1 -1
- package/dist/read/area-filter.d.ts +1 -1
- package/dist/read/area-filter.js +5 -4
- package/dist/read/area-filter.js.map +1 -1
- package/dist/read/area-view.d.ts +0 -11
- package/dist/read/area-view.js +1 -5
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/project-view.d.ts +34 -19
- package/dist/read/project-view.js +26 -20
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/scope.d.ts +1 -1
- package/dist/read/scope.js +5 -4
- package/dist/read/scope.js.map +1 -1
- package/dist/read/shape.d.ts +73 -14
- package/dist/read/shape.js +459 -151
- package/dist/read/shape.js.map +1 -1
- package/dist/read/truncation.d.ts +75 -2
- package/dist/read/truncation.js +69 -46
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +19 -18
- package/dist/read/views.js +5 -6
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +6 -0
- package/dist/surface-copy.js +6 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.js +0 -1
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +25 -1
- package/dist/write/commands.js +256 -121
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.js +27 -11
- package/dist/write/guards.js.map +1 -1
- package/dist/write/move.d.ts +26 -5
- package/dist/write/move.js +313 -2
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +49 -19
- package/dist/write/operations.js +2 -2
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.js +25 -4
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +48 -28
- package/dist/write/pre-state.js +134 -13
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.js +3 -3
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +34 -0
- package/dist/write/resolution-timestamps.js +286 -0
- package/dist/write/resolution-timestamps.js.map +1 -0
- package/dist/write/reversibility.js +5 -5
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.js +0 -5
- package/dist/write/scope-guard.js.map +1 -1
- package/dist/write/undo.d.ts +0 -6
- package/dist/write/undo.js +44 -4
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +14 -4
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +0 -8
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +16 -2
- package/dist/write/verify/delta.js +21 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +2 -78
- package/skills/things-cli/SKILL.md +7 -5
- package/skills/things-cli/references/contracts.md +3 -4
- package/skills/things-cli/references/model.md +8 -8
- package/skills/things-cli/references/ordering.md +16 -7
package/dist/write/commands.js
CHANGED
|
@@ -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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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.
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
|
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 ${
|
|
1322
|
-
`set month of ${varName} to ${
|
|
1323
|
-
`set day of ${varName} to ${
|
|
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
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
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
|
|
1339
|
-
|
|
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
|
-
|
|
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,
|