things-api 0.9.0 → 0.10.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 +30 -11
- package/dist/audit/log.js +56 -2
- package/dist/audit/log.js.map +1 -1
- package/dist/audit/schema.d.ts +26 -8
- package/dist/audit/schema.js +42 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/area.d.ts +26 -3
- package/dist/cli/commands/area.js +125 -70
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -1
- package/dist/cli/commands/doctor.js +44 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/mcp.js +23 -4
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +18 -1
- package/dist/cli/commands/project.js +69 -31
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +423 -157
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/repeat-flags.d.ts +20 -0
- package/dist/cli/commands/repeat-flags.js +76 -0
- package/dist/cli/commands/repeat-flags.js.map +1 -0
- package/dist/cli/commands/setup.js +2 -3
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +47 -24
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +1 -1
- package/dist/cli/commands/todo.js +7 -6
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +378 -148
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +1 -1
- package/dist/cli/excess-args.d.ts +15 -0
- package/dist/cli/excess-args.js +51 -0
- package/dist/cli/excess-args.js.map +1 -0
- package/dist/cli/glyphs.d.ts +39 -2
- package/dist/cli/glyphs.js +91 -21
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +54 -0
- package/dist/cli/help.js +355 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.js +32 -25
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +7 -0
- package/dist/cli/period.js +12 -0
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +26 -19
- package/dist/cli/read-driver.js +67 -33
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +92 -28
- package/dist/cli/render.js +318 -97
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +42 -4
- package/dist/cli/resolve-invocation.js +97 -14
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/tag-filters.d.ts +59 -0
- package/dist/cli/tag-filters.js +57 -0
- package/dist/cli/tag-filters.js.map +1 -0
- package/dist/cli/verb-hint.d.ts +25 -0
- package/dist/cli/verb-hint.js +138 -0
- package/dist/cli/verb-hint.js.map +1 -0
- package/dist/cli/width.d.ts +135 -0
- package/dist/cli/width.js +313 -0
- package/dist/cli/width.js.map +1 -0
- package/dist/client.d.ts +137 -24
- package/dist/client.js +105 -16
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +87 -17
- package/dist/contracts.js +44 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/fingerprint.d.ts +12 -0
- package/dist/db/fingerprint.js +15 -1
- package/dist/db/fingerprint.js.map +1 -1
- package/dist/db/locate.js +1 -1
- package/dist/db/locate.js.map +1 -1
- package/dist/diagnose.d.ts +63 -0
- package/dist/diagnose.js +38 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +30 -4
- package/dist/index.js +35 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -1
- package/dist/mcp/server.js +691 -133
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +31 -7
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +7 -0
- package/dist/model/mappers.js +7 -2
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/serialize.d.ts +28 -0
- package/dist/model/serialize.js +80 -0
- package/dist/model/serialize.js.map +1 -0
- package/dist/model/when-sugar.d.ts +47 -0
- package/dist/model/when-sugar.js +45 -0
- package/dist/model/when-sugar.js.map +1 -0
- package/dist/read/area-view.d.ts +8 -1
- package/dist/read/area-view.js +49 -13
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/filter-contract.d.ts +117 -0
- package/dist/read/filter-contract.js +78 -0
- package/dist/read/filter-contract.js.map +1 -0
- package/dist/read/predicates.d.ts +30 -0
- package/dist/read/predicates.js +30 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +12 -1
- package/dist/read/project-view.js +39 -8
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +149 -6
- package/dist/read/queries.js +250 -31
- package/dist/read/queries.js.map +1 -1
- package/dist/read/sections.d.ts +51 -0
- package/dist/read/sections.js +37 -0
- package/dist/read/sections.js.map +1 -0
- package/dist/read/sidebar-order.js +2 -1
- package/dist/read/sidebar-order.js.map +1 -1
- package/dist/read/tags.d.ts +27 -3
- package/dist/read/tags.js +83 -12
- package/dist/read/tags.js.map +1 -1
- package/dist/read/truncation.d.ts +71 -0
- package/dist/read/{pagination.js → truncation.js} +103 -94
- package/dist/read/truncation.js.map +1 -0
- package/dist/read/views.d.ts +140 -16
- package/dist/read/views.js +264 -67
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +19 -0
- package/dist/surface-copy.js +32 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/sync-health.d.ts +78 -0
- package/dist/sync-health.js +312 -0
- package/dist/sync-health.js.map +1 -0
- package/dist/write/accessibility-probe.d.ts +12 -0
- package/dist/write/accessibility-probe.js +63 -0
- package/dist/write/accessibility-probe.js.map +1 -0
- package/dist/write/automation-probe.d.ts +7 -0
- package/dist/write/automation-probe.js +8 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.js +4 -2
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +8 -0
- package/dist/write/capabilities.js +13 -6
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/commands.d.ts +2 -0
- package/dist/write/commands.js +562 -48
- package/dist/write/commands.js.map +1 -1
- package/dist/write/edit-checklist.js +3 -2
- package/dist/write/edit-checklist.js.map +1 -1
- package/dist/write/guards.d.ts +2 -7
- package/dist/write/guards.js +64 -6
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.js +2 -0
- package/dist/write/heading.js.map +1 -1
- package/dist/write/lock.d.ts +40 -2
- package/dist/write/lock.js +91 -14
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +4 -0
- package/dist/write/make-repeating-project.js +253 -0
- package/dist/write/make-repeating-project.js.map +1 -0
- package/dist/write/operations.d.ts +145 -1
- package/dist/write/operations.js +48 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/pipeline.d.ts +19 -0
- package/dist/write/pipeline.js +109 -26
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +68 -3
- package/dist/write/pre-state.js +102 -3
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reopen.js +3 -2
- package/dist/write/reopen.js.map +1 -1
- package/dist/write/reorder.js +27 -17
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +22 -0
- package/dist/write/repeat-rule.js +258 -0
- package/dist/write/repeat-rule.js.map +1 -0
- package/dist/write/reversibility.js +49 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/tag-refs.d.ts +47 -0
- package/dist/write/tag-refs.js +126 -0
- package/dist/write/tag-refs.js.map +1 -0
- package/dist/write/undo.d.ts +74 -5
- package/dist/write/undo.js +493 -80
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +17 -1
- package/dist/write/vectors/registry.js +18 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/types.d.ts +119 -3
- package/dist/write/vectors/ui-certification.d.ts +48 -0
- package/dist/write/vectors/ui-certification.js +46 -0
- package/dist/write/vectors/ui-certification.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +188 -0
- package/dist/write/vectors/ui-drag.js +1095 -0
- package/dist/write/vectors/ui-drag.js.map +1 -0
- package/dist/write/vectors/ui-recipes.d.ts +70 -0
- package/dist/write/vectors/ui-recipes.js +558 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +137 -0
- package/dist/write/vectors/ui.js +656 -0
- package/dist/write/vectors/ui.js.map +1 -0
- package/dist/write/verify/delta.d.ts +31 -3
- package/dist/write/verify/delta.js +22 -6
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.js +1 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/exit-codes.d.ts +0 -26
- package/dist/cli/exit-codes.js +0 -26
- package/dist/cli/exit-codes.js.map +0 -1
- package/dist/cli/output.d.ts +0 -42
- package/dist/cli/output.js +0 -16
- package/dist/cli/output.js.map +0 -1
- package/dist/read/pagination.d.ts +0 -104
- package/dist/read/pagination.js.map +0 -1
package/dist/write/commands.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { decodeReminderTime, encodeReminderTime, reminderUrlToken, } from "../model/dates.js";
|
|
2
2
|
import { byUuid } from "../read/detail.js";
|
|
3
|
-
import { childTagTitles, computeReorderPre, emptyPreState, loadTarget,
|
|
3
|
+
import { childTagTitles, classifyProjectRepeat, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
|
|
4
|
+
import { resolveTagRefs } from "./tag-refs.js";
|
|
4
5
|
import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
|
|
6
|
+
import { assertRepeatRule } from "./repeat-rule.js";
|
|
5
7
|
import { escapeAppleScript } from "./vectors/applescript.js";
|
|
8
|
+
import { areaReorderSidebarRecipe, convertToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
|
|
6
9
|
// ------------------------------------------------------------------ helpers
|
|
7
10
|
function thingsUrl(command, params, token) {
|
|
8
11
|
const parts = [];
|
|
@@ -99,6 +102,18 @@ function whenAssertions(when, todayIso) {
|
|
|
99
102
|
function sortedTags(tags) {
|
|
100
103
|
return [...tags].toSorted();
|
|
101
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve tag refs (title or `parent/child` path — names only) into pre-state:
|
|
107
|
+
* the leaf titles to apply, plus the unknown (`missingTags`) refs the
|
|
108
|
+
* H-UNKNOWN-TAG guard refuses on. Tags apply BY NAME through the app's own
|
|
109
|
+
* vector, so the app resolves the name (duplicate names resolve app-side, as
|
|
110
|
+
* in the GUI) — the resolver is a pure existence check.
|
|
111
|
+
*/
|
|
112
|
+
function applyTagRefs(db, pre, tags) {
|
|
113
|
+
const res = resolveTagRefs(db, tags);
|
|
114
|
+
pre.missingTags = res.missing;
|
|
115
|
+
pre.resolvedTagTitles = res.titles;
|
|
116
|
+
}
|
|
102
117
|
/** Round-trip normalization: "6:5"-style inputs → canonical "06:05". */
|
|
103
118
|
function normalizeReminder(time) {
|
|
104
119
|
return decodeReminderTime(encodeReminderTime(time)) ?? time;
|
|
@@ -115,6 +130,17 @@ function whenWithReminder(when, reminder) {
|
|
|
115
130
|
function containerGiven(ref) {
|
|
116
131
|
return ref !== undefined && (ref.uuid !== undefined || ref.title !== undefined);
|
|
117
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* The create-probe title for the duplicate / identity-replacement ops, whose
|
|
135
|
+
* new row inherits the pre-read target's title. Non-heading targets only (the
|
|
136
|
+
* heading-convert op uses the raw title, headings included); "" when the target
|
|
137
|
+
* did not resolve. Shared by the pre-read same-title capture and the delta so
|
|
138
|
+
* the excluded set and the probe title cannot drift apart.
|
|
139
|
+
*/
|
|
140
|
+
function nonHeadingTitle(pre) {
|
|
141
|
+
const target = pre.target;
|
|
142
|
+
return target !== null && target.type !== "heading" ? target.title : "";
|
|
143
|
+
}
|
|
118
144
|
// ----------------------------------------------------------------- commands
|
|
119
145
|
const todoAdd = {
|
|
120
146
|
op: "todo.add",
|
|
@@ -139,7 +165,8 @@ const todoAdd = {
|
|
|
139
165
|
if (containerGiven(params.area))
|
|
140
166
|
pre.destArea = resolveArea(db, params.area);
|
|
141
167
|
if (params.tags !== undefined)
|
|
142
|
-
|
|
168
|
+
applyTagRefs(db, pre, params.tags);
|
|
169
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "to-do");
|
|
143
170
|
return pre;
|
|
144
171
|
},
|
|
145
172
|
expectedDelta(pre, params, ctx) {
|
|
@@ -153,8 +180,9 @@ const todoAdd = {
|
|
|
153
180
|
}
|
|
154
181
|
if (params.deadline !== undefined)
|
|
155
182
|
assert.push({ field: "deadline", equals: params.deadline });
|
|
156
|
-
if (params.tags !== undefined)
|
|
157
|
-
assert.push({ field: "tags", equals: sortedTags(
|
|
183
|
+
if (params.tags !== undefined) {
|
|
184
|
+
assert.push({ field: "tags", equals: sortedTags(pre.resolvedTagTitles) });
|
|
185
|
+
}
|
|
158
186
|
if (params.checklistItems !== undefined) {
|
|
159
187
|
assert.push({ field: "checklistTitles", equals: params.checklistItems });
|
|
160
188
|
}
|
|
@@ -171,7 +199,12 @@ const todoAdd = {
|
|
|
171
199
|
assert.push({ field: "area.uuid", equals: area.uuid });
|
|
172
200
|
return {
|
|
173
201
|
mode: "create",
|
|
174
|
-
probe: {
|
|
202
|
+
probe: {
|
|
203
|
+
title: params.title,
|
|
204
|
+
type: "to-do",
|
|
205
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
206
|
+
excludeUuids: pre.sameTitleUuids,
|
|
207
|
+
},
|
|
175
208
|
assert,
|
|
176
209
|
};
|
|
177
210
|
},
|
|
@@ -184,7 +217,7 @@ const todoAdd = {
|
|
|
184
217
|
notes: params.notes,
|
|
185
218
|
when: params.when === undefined ? undefined : whenWithReminder(params.when, params.reminder),
|
|
186
219
|
deadline: params.deadline,
|
|
187
|
-
tags: params.tags
|
|
220
|
+
tags: params.tags === undefined ? undefined : pre.resolvedTagTitles.join(","),
|
|
188
221
|
"checklist-items": params.checklistItems?.join("\n"),
|
|
189
222
|
"list-id": container?.uuid,
|
|
190
223
|
heading: pre.destHeading?.resolved?.title,
|
|
@@ -400,21 +433,22 @@ const todoSetTags = {
|
|
|
400
433
|
preRead(db, params) {
|
|
401
434
|
const pre = emptyPreState();
|
|
402
435
|
pre.target = loadTarget(db, params.uuid);
|
|
403
|
-
|
|
436
|
+
applyTagRefs(db, pre, params.tags);
|
|
404
437
|
return pre;
|
|
405
438
|
},
|
|
406
|
-
expectedDelta(
|
|
439
|
+
expectedDelta(pre, params) {
|
|
407
440
|
return {
|
|
408
441
|
mode: "update",
|
|
409
442
|
uuid: params.uuid,
|
|
410
|
-
assert: [{ field: "tags", equals: sortedTags(
|
|
443
|
+
assert: [{ field: "tags", equals: sortedTags(pre.resolvedTagTitles) }],
|
|
411
444
|
};
|
|
412
445
|
},
|
|
413
|
-
compile(params, vector,
|
|
446
|
+
compile(params, vector, pre, ctx) {
|
|
447
|
+
const applied = pre.resolvedTagTitles;
|
|
414
448
|
if (vector === "url-scheme") {
|
|
415
|
-
return thingsUrl("update", { id: params.uuid, tags:
|
|
449
|
+
return thingsUrl("update", { id: params.uuid, tags: applied.join(",") }, ctx.token);
|
|
416
450
|
}
|
|
417
|
-
return osa(`set tag names of to do id ${q(params.uuid)} to ${q(
|
|
451
|
+
return osa(`set tag names of to do id ${q(params.uuid)} to ${q(applied.join(", "))}`);
|
|
418
452
|
},
|
|
419
453
|
};
|
|
420
454
|
/** Normalize the string | spec union; decide whether states force the json form. */
|
|
@@ -516,6 +550,7 @@ const projectAdd = {
|
|
|
516
550
|
const pre = emptyPreState();
|
|
517
551
|
if (containerGiven(params.area))
|
|
518
552
|
pre.destArea = resolveArea(db, params.area);
|
|
553
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "project");
|
|
519
554
|
return pre;
|
|
520
555
|
},
|
|
521
556
|
expectedDelta(pre, params, ctx) {
|
|
@@ -531,7 +566,12 @@ const projectAdd = {
|
|
|
531
566
|
assert.push({ field: "area.uuid", equals: area.uuid });
|
|
532
567
|
return {
|
|
533
568
|
mode: "create",
|
|
534
|
-
probe: {
|
|
569
|
+
probe: {
|
|
570
|
+
title: params.title,
|
|
571
|
+
type: "project",
|
|
572
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
573
|
+
excludeUuids: pre.sameTitleUuids,
|
|
574
|
+
},
|
|
535
575
|
assert,
|
|
536
576
|
};
|
|
537
577
|
},
|
|
@@ -602,24 +642,25 @@ const projectSetTags = {
|
|
|
602
642
|
preRead(db, params) {
|
|
603
643
|
const pre = emptyPreState();
|
|
604
644
|
pre.target = loadTarget(db, params.uuid);
|
|
605
|
-
|
|
645
|
+
applyTagRefs(db, pre, params.tags);
|
|
606
646
|
return pre;
|
|
607
647
|
},
|
|
608
|
-
expectedDelta(
|
|
648
|
+
expectedDelta(pre, params) {
|
|
609
649
|
return {
|
|
610
650
|
mode: "update",
|
|
611
651
|
uuid: params.uuid,
|
|
612
|
-
assert: [{ field: "tags", equals: sortedTags(
|
|
652
|
+
assert: [{ field: "tags", equals: sortedTags(pre.resolvedTagTitles) }],
|
|
613
653
|
};
|
|
614
654
|
},
|
|
615
|
-
compile(params, vector,
|
|
655
|
+
compile(params, vector, pre, ctx) {
|
|
616
656
|
// Both vectors validated on projects (A1 URL, A2 AppleScript). Full
|
|
617
657
|
// replacement semantics mirror todo.set-tags; unknown tags are guarded
|
|
618
658
|
// pre-write (the app silently drops them).
|
|
659
|
+
const applied = pre.resolvedTagTitles;
|
|
619
660
|
if (vector === "url-scheme") {
|
|
620
|
-
return thingsUrl("update-project", { id: params.uuid, tags:
|
|
661
|
+
return thingsUrl("update-project", { id: params.uuid, tags: applied.join(",") }, ctx.token);
|
|
621
662
|
}
|
|
622
|
-
return osa(`set tag names of project id ${q(params.uuid)} to ${q(
|
|
663
|
+
return osa(`set tag names of project id ${q(params.uuid)} to ${q(applied.join(", "))}`);
|
|
623
664
|
},
|
|
624
665
|
};
|
|
625
666
|
const projectMove = {
|
|
@@ -695,17 +736,24 @@ const projectDuplicate = {
|
|
|
695
736
|
preRead(db, params) {
|
|
696
737
|
const pre = emptyPreState();
|
|
697
738
|
pre.target = loadTarget(db, params.uuid);
|
|
739
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
|
|
698
740
|
return pre;
|
|
699
741
|
},
|
|
700
742
|
expectedDelta(pre, _params, ctx) {
|
|
701
743
|
// E17: the copy carries the title, notes AND children; discover it with
|
|
702
|
-
// the create probe (fresh creationDate, same as the to-do path E07).
|
|
744
|
+
// the create probe (fresh creationDate, same as the to-do path E07). The
|
|
745
|
+
// ORIGINAL shares the copy's title, so it is among the excluded pre-existing
|
|
746
|
+
// rows — discovery lands on the copy, never the source.
|
|
703
747
|
const target = pre.target;
|
|
704
|
-
const title = target !== null && target.type !== "heading" ? target.title : "";
|
|
705
748
|
const notes = target !== null && target.type !== "heading" ? target.notes : "";
|
|
706
749
|
return {
|
|
707
750
|
mode: "create",
|
|
708
|
-
probe: {
|
|
751
|
+
probe: {
|
|
752
|
+
title: nonHeadingTitle(pre),
|
|
753
|
+
type: "project",
|
|
754
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
755
|
+
excludeUuids: pre.sameTitleUuids,
|
|
756
|
+
},
|
|
709
757
|
assert: [{ field: "notes", equals: notes }],
|
|
710
758
|
};
|
|
711
759
|
},
|
|
@@ -861,7 +909,7 @@ const areaAdd = {
|
|
|
861
909
|
preRead(db, params) {
|
|
862
910
|
const pre = emptyPreState();
|
|
863
911
|
if (params.tags !== undefined)
|
|
864
|
-
|
|
912
|
+
applyTagRefs(db, pre, params.tags);
|
|
865
913
|
pre.existingEntityUuids = db.prepare("SELECT uuid FROM TMArea WHERE title = ? COLLATE NOCASE").all(params.title).map((r) => r.uuid);
|
|
866
914
|
return pre;
|
|
867
915
|
},
|
|
@@ -871,9 +919,13 @@ const areaAdd = {
|
|
|
871
919
|
entity: "area",
|
|
872
920
|
title: params.title,
|
|
873
921
|
excludeUuids: pre.existingEntityUuids,
|
|
922
|
+
// Assert the landed tag set on the created area — the app silently
|
|
923
|
+
// drops unknowns (guarded pre-write), so the created row must carry
|
|
924
|
+
// exactly the resolved titles (parity with the other tag-set ops).
|
|
925
|
+
...(params.tags !== undefined && { assertTags: sortedTags(pre.resolvedTagTitles) }),
|
|
874
926
|
};
|
|
875
927
|
},
|
|
876
|
-
compile(params, vector) {
|
|
928
|
+
compile(params, vector, pre) {
|
|
877
929
|
if (vector !== "applescript")
|
|
878
930
|
unsupportedVector(this.op, vector);
|
|
879
931
|
const make = `make new area with properties {name:${q(params.title)}}`;
|
|
@@ -881,7 +933,7 @@ const areaAdd = {
|
|
|
881
933
|
return osa(make);
|
|
882
934
|
const payload = `tell application "Things3"\n` +
|
|
883
935
|
` ${make}\n` +
|
|
884
|
-
` set tag names of area ${q(params.title)} to ${q(
|
|
936
|
+
` set tag names of area ${q(params.title)} to ${q(pre.resolvedTagTitles.join(", "))}\n` +
|
|
885
937
|
`end tell`;
|
|
886
938
|
return { vector: "applescript", kind: "osascript", payload, redactedPayload: payload };
|
|
887
939
|
},
|
|
@@ -966,17 +1018,23 @@ const todoDuplicate = {
|
|
|
966
1018
|
preRead(db, params) {
|
|
967
1019
|
const pre = emptyPreState();
|
|
968
1020
|
pre.target = loadTarget(db, params.uuid);
|
|
1021
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
|
|
969
1022
|
return pre;
|
|
970
1023
|
},
|
|
971
1024
|
expectedDelta(pre, _params, ctx) {
|
|
972
1025
|
// The copy carries the same title/notes with a fresh uuid + creationDate
|
|
973
|
-
// (E07) — discover it with the create probe, assert copy fidelity.
|
|
1026
|
+
// (E07) — discover it with the create probe, assert copy fidelity. The
|
|
1027
|
+
// ORIGINAL shares the title, so it is excluded as a pre-existing row.
|
|
974
1028
|
const target = pre.target;
|
|
975
|
-
const title = target !== null && target.type !== "heading" ? target.title : "";
|
|
976
1029
|
const notes = target !== null && target.type !== "heading" ? target.notes : "";
|
|
977
1030
|
return {
|
|
978
1031
|
mode: "create",
|
|
979
|
-
probe: {
|
|
1032
|
+
probe: {
|
|
1033
|
+
title: nonHeadingTitle(pre),
|
|
1034
|
+
type: "to-do",
|
|
1035
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1036
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1037
|
+
},
|
|
980
1038
|
assert: [{ field: "notes", equals: notes }],
|
|
981
1039
|
};
|
|
982
1040
|
},
|
|
@@ -997,15 +1055,16 @@ const areaUpdate = {
|
|
|
997
1055
|
const pre = emptyPreState();
|
|
998
1056
|
pre.entityTarget = resolveArea(db, { title: params.target, uuid: params.target });
|
|
999
1057
|
if (params.tags !== undefined)
|
|
1000
|
-
|
|
1058
|
+
applyTagRefs(db, pre, params.tags);
|
|
1001
1059
|
return pre;
|
|
1002
1060
|
},
|
|
1003
1061
|
expectedDelta(pre, params) {
|
|
1004
1062
|
const assert = [];
|
|
1005
1063
|
if (params.title !== undefined)
|
|
1006
1064
|
assert.push({ field: "title", equals: params.title });
|
|
1007
|
-
if (params.tags !== undefined)
|
|
1008
|
-
assert.push({ field: "tags", equals: sortedTags(
|
|
1065
|
+
if (params.tags !== undefined) {
|
|
1066
|
+
assert.push({ field: "tags", equals: sortedTags(pre.resolvedTagTitles) });
|
|
1067
|
+
}
|
|
1009
1068
|
return {
|
|
1010
1069
|
mode: "entity-updated",
|
|
1011
1070
|
entity: "area",
|
|
@@ -1021,7 +1080,7 @@ const areaUpdate = {
|
|
|
1021
1080
|
if (params.title !== undefined)
|
|
1022
1081
|
lines.push(`set name of area id ${id} to ${q(params.title)}`);
|
|
1023
1082
|
if (params.tags !== undefined) {
|
|
1024
|
-
lines.push(`set tag names of area id ${id} to ${q(
|
|
1083
|
+
lines.push(`set tag names of area id ${id} to ${q(pre.resolvedTagTitles.join(", "))}`);
|
|
1025
1084
|
}
|
|
1026
1085
|
if (lines.length === 1)
|
|
1027
1086
|
return osa(lines[0]);
|
|
@@ -1180,6 +1239,18 @@ function asDateBlock(varName, iso) {
|
|
|
1180
1239
|
`set day of ${varName} to ${d}`,
|
|
1181
1240
|
];
|
|
1182
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Local noon -> UTC instant, so the stored timestamp decodes back to the
|
|
1244
|
+
* requested local DATE in every timezone (P4d: json attrs honored exactly).
|
|
1245
|
+
* WITHOUT milliseconds: the app's json date parser rejects fractional
|
|
1246
|
+
* seconds — a `.000Z` timestamp fails the whole command (error modal, no
|
|
1247
|
+
* write; caught live by the e2e 2026-07-09 — P4d's validated shape was
|
|
1248
|
+
* second-precision).
|
|
1249
|
+
*/
|
|
1250
|
+
function utcNoon(iso) {
|
|
1251
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
1252
|
+
return new Date(y ?? 0, (m ?? 1) - 1, d ?? 1, 12, 0, 0).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1253
|
+
}
|
|
1183
1254
|
const todoBackdate = {
|
|
1184
1255
|
op: "todo.backdate",
|
|
1185
1256
|
hazards: ["H-UNKNOWN-DESTINATION", "H-BACKDATE-OPEN"],
|
|
@@ -1223,7 +1294,7 @@ const todoAddLogged = {
|
|
|
1223
1294
|
throw new RangeError("creationDate must not be after completionDate");
|
|
1224
1295
|
}
|
|
1225
1296
|
const pre = emptyPreState();
|
|
1226
|
-
pre.sameTitleUuids = db
|
|
1297
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "to-do");
|
|
1227
1298
|
return pre;
|
|
1228
1299
|
},
|
|
1229
1300
|
expectedDelta(pre, params) {
|
|
@@ -1250,18 +1321,6 @@ const todoAddLogged = {
|
|
|
1250
1321
|
compile(params, vector, _pre, ctx) {
|
|
1251
1322
|
if (vector !== "url-scheme")
|
|
1252
1323
|
unsupportedVector(this.op, vector);
|
|
1253
|
-
// Local noon -> UTC instant, so the stored timestamp decodes back to the
|
|
1254
|
-
// requested local DATE in every timezone (P4d: json attrs honored exactly).
|
|
1255
|
-
// WITHOUT milliseconds: the app's json date parser rejects fractional
|
|
1256
|
-
// seconds — a `.000Z` timestamp fails the whole command (error modal, no
|
|
1257
|
-
// write; caught live by the e2e 2026-07-09 — P4d's validated shape was
|
|
1258
|
-
// second-precision).
|
|
1259
|
-
const utcNoon = (iso) => {
|
|
1260
|
-
const [y, m, d] = iso.split("-").map(Number);
|
|
1261
|
-
return new Date(y ?? 0, (m ?? 1) - 1, d ?? 1, 12, 0, 0)
|
|
1262
|
-
.toISOString()
|
|
1263
|
-
.replace(/\.\d{3}Z$/, "Z");
|
|
1264
|
-
};
|
|
1265
1324
|
const payload = JSON.stringify([
|
|
1266
1325
|
{
|
|
1267
1326
|
type: "to-do",
|
|
@@ -1395,15 +1454,21 @@ const headingCreate = {
|
|
|
1395
1454
|
preRead(db, params) {
|
|
1396
1455
|
const pre = emptyPreState();
|
|
1397
1456
|
pre.destProject = resolveProject(db, params.project);
|
|
1457
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "heading");
|
|
1398
1458
|
return pre;
|
|
1399
1459
|
},
|
|
1400
1460
|
expectedDelta(pre, params, ctx) {
|
|
1401
1461
|
const project = pre.destProject?.resolved;
|
|
1402
|
-
// A new type=2 row with this title under the project;
|
|
1403
|
-
//
|
|
1462
|
+
// A new type=2 row with this title under the project; a pre-existing
|
|
1463
|
+
// same-title heading is excluded so discovery cannot bind to it.
|
|
1404
1464
|
return {
|
|
1405
1465
|
mode: "create",
|
|
1406
|
-
probe: {
|
|
1466
|
+
probe: {
|
|
1467
|
+
title: params.title,
|
|
1468
|
+
type: "heading",
|
|
1469
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1470
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1471
|
+
},
|
|
1407
1472
|
assert: project !== undefined && project !== null
|
|
1408
1473
|
? [{ field: "project.uuid", equals: project.uuid }]
|
|
1409
1474
|
: [],
|
|
@@ -1453,6 +1518,443 @@ const todoClearDatedReminder = {
|
|
|
1453
1518
|
});
|
|
1454
1519
|
},
|
|
1455
1520
|
};
|
|
1521
|
+
// ------------------------------------------------------- ui (GUI) vector
|
|
1522
|
+
/** A compiled Accessibility recipe as a CompiledInvocation (no secrets). */
|
|
1523
|
+
function uiDrive(recipe) {
|
|
1524
|
+
const rendered = `ui-drive ${recipe.op} on ${recipe.targetUuid}: ${recipe.steps
|
|
1525
|
+
.map((s) => s.label)
|
|
1526
|
+
.join(" → ")}`;
|
|
1527
|
+
return { vector: "ui", kind: "ui-drive", payload: rendered, redactedPayload: rendered, recipe };
|
|
1528
|
+
}
|
|
1529
|
+
// Re-exported so make-repeating-project.ts and existing callers keep importing
|
|
1530
|
+
// the validator from commands.ts; the implementation + the full combination
|
|
1531
|
+
// matrix live in repeat-rule.ts.
|
|
1532
|
+
export { assertRepeatRule };
|
|
1533
|
+
/**
|
|
1534
|
+
* The extended-vocabulary fields of a rule as a recipe `extras` bag, including
|
|
1535
|
+
* ONLY the keys that are present (exactOptionalPropertyTypes: never set an
|
|
1536
|
+
* optional field to undefined). A bare `{ uuid, frequency, interval }` yields
|
|
1537
|
+
* `{}`, so the recipe drives exactly the certified two-control path.
|
|
1538
|
+
*/
|
|
1539
|
+
function ruleExtras(params) {
|
|
1540
|
+
return {
|
|
1541
|
+
...(params.afterCompletion !== undefined && { afterCompletion: params.afterCompletion }),
|
|
1542
|
+
...(params.weekdays !== undefined && { weekdays: params.weekdays }),
|
|
1543
|
+
...(params.monthly !== undefined && { monthly: params.monthly }),
|
|
1544
|
+
...(params.yearly !== undefined && { yearly: params.yearly }),
|
|
1545
|
+
...(params.ends !== undefined && { ends: params.ends }),
|
|
1546
|
+
...(params.reminder !== undefined && { reminder: params.reminder }),
|
|
1547
|
+
...(params.deadline !== undefined && { deadline: params.deadline }),
|
|
1548
|
+
...(params.startDaysEarlier !== undefined && { startDaysEarlier: params.startDaysEarlier }),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
/** ui ops all guard existence/type + the H-UI-DRIVE acknowledgement. */
|
|
1552
|
+
const UI_HAZARDS = ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"];
|
|
1553
|
+
const todoMakeRepeating = {
|
|
1554
|
+
op: "todo.make-repeating",
|
|
1555
|
+
hazards: UI_HAZARDS,
|
|
1556
|
+
preRead(db, params) {
|
|
1557
|
+
assertRepeatRule(params);
|
|
1558
|
+
const pre = emptyPreState();
|
|
1559
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1560
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
|
|
1561
|
+
return pre;
|
|
1562
|
+
},
|
|
1563
|
+
expectedDelta(pre, _params, ctx) {
|
|
1564
|
+
// Identity REPLACEMENT (UI2-a): the original to-do uuid dies; a NEW
|
|
1565
|
+
// template row (type=0 with a recurrence rule) is born alongside a spawned
|
|
1566
|
+
// instance sharing its title. Discover the template with the create probe,
|
|
1567
|
+
// excluding the pre-existing same-title rows (incl. the dying original), and
|
|
1568
|
+
// pick the template (not the spawned instance) by asserting it IS one.
|
|
1569
|
+
return {
|
|
1570
|
+
mode: "create",
|
|
1571
|
+
probe: {
|
|
1572
|
+
title: nonHeadingTitle(pre),
|
|
1573
|
+
type: "to-do",
|
|
1574
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1575
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1576
|
+
},
|
|
1577
|
+
assert: [{ field: "repeating.isTemplate", equals: true }],
|
|
1578
|
+
};
|
|
1579
|
+
},
|
|
1580
|
+
compile(params, vector) {
|
|
1581
|
+
if (vector !== "ui")
|
|
1582
|
+
unsupportedVector(this.op, vector);
|
|
1583
|
+
return uiDrive(makeRepeatingRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
|
|
1584
|
+
},
|
|
1585
|
+
};
|
|
1586
|
+
const todoRescheduleRepeat = {
|
|
1587
|
+
op: "todo.reschedule-repeat",
|
|
1588
|
+
hazards: UI_HAZARDS,
|
|
1589
|
+
preRead(db, params) {
|
|
1590
|
+
assertRepeatRule(params);
|
|
1591
|
+
const pre = emptyPreState();
|
|
1592
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1593
|
+
return pre;
|
|
1594
|
+
},
|
|
1595
|
+
expectedDelta(_pre, params) {
|
|
1596
|
+
// Identity PRESERVED (UI2-b): the same template uuid, rule mutated in
|
|
1597
|
+
// place. Assert the decoded rule's frequency + interval; ALSO capture the
|
|
1598
|
+
// whole prior rule (+ deadline flag) so the undo can re-drive it faithfully.
|
|
1599
|
+
return {
|
|
1600
|
+
mode: "update",
|
|
1601
|
+
uuid: params.uuid,
|
|
1602
|
+
assert: [
|
|
1603
|
+
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1604
|
+
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1605
|
+
],
|
|
1606
|
+
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
1607
|
+
};
|
|
1608
|
+
},
|
|
1609
|
+
compile(params, vector) {
|
|
1610
|
+
if (vector !== "ui")
|
|
1611
|
+
unsupportedVector(this.op, vector);
|
|
1612
|
+
return uiDrive(rescheduleRepeatRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
|
|
1613
|
+
},
|
|
1614
|
+
};
|
|
1615
|
+
const todoPauseRepeat = {
|
|
1616
|
+
op: "todo.pause-repeat",
|
|
1617
|
+
hazards: UI_HAZARDS,
|
|
1618
|
+
preRead(db, params) {
|
|
1619
|
+
const pre = emptyPreState();
|
|
1620
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1621
|
+
return pre;
|
|
1622
|
+
},
|
|
1623
|
+
expectedDelta(_pre, params) {
|
|
1624
|
+
// Pause (UI2-c): rt1_instanceCreationPaused → 1; identity preserved.
|
|
1625
|
+
return {
|
|
1626
|
+
mode: "update",
|
|
1627
|
+
uuid: params.uuid,
|
|
1628
|
+
assert: [{ field: "repeating.paused", equals: true }],
|
|
1629
|
+
};
|
|
1630
|
+
},
|
|
1631
|
+
compile(params, vector) {
|
|
1632
|
+
if (vector !== "ui")
|
|
1633
|
+
unsupportedVector(this.op, vector);
|
|
1634
|
+
return uiDrive(pauseRepeatRecipe(params.uuid));
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
const todoResumeRepeat = {
|
|
1638
|
+
op: "todo.resume-repeat",
|
|
1639
|
+
hazards: UI_HAZARDS,
|
|
1640
|
+
preRead(db, params) {
|
|
1641
|
+
const pre = emptyPreState();
|
|
1642
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1643
|
+
return pre;
|
|
1644
|
+
},
|
|
1645
|
+
expectedDelta(_pre, params) {
|
|
1646
|
+
// Resume (UI2-c): rt1_instanceCreationPaused → 0; identity preserved.
|
|
1647
|
+
return {
|
|
1648
|
+
mode: "update",
|
|
1649
|
+
uuid: params.uuid,
|
|
1650
|
+
assert: [{ field: "repeating.paused", equals: false }],
|
|
1651
|
+
};
|
|
1652
|
+
},
|
|
1653
|
+
compile(params, vector) {
|
|
1654
|
+
if (vector !== "ui")
|
|
1655
|
+
unsupportedVector(this.op, vector);
|
|
1656
|
+
return uiDrive(resumeRepeatRecipe(params.uuid));
|
|
1657
|
+
},
|
|
1658
|
+
};
|
|
1659
|
+
// ------------------------------------------------- repeating-PROJECT ops
|
|
1660
|
+
// The project analogs of reschedule/pause/resume. Same recurrence codec and
|
|
1661
|
+
// Repeat dialog as the to-do ops (identical DB deltas), reached through the
|
|
1662
|
+
// project view's always-visible repeat bar (UIC2). No project.stop-repeat is
|
|
1663
|
+
// built — the project Stop then selecting the demoted project crashes Things
|
|
1664
|
+
// (CRASH1 / oddities §7 C5).
|
|
1665
|
+
const projectRescheduleRepeat = {
|
|
1666
|
+
op: "project.reschedule-repeat",
|
|
1667
|
+
hazards: UI_HAZARDS,
|
|
1668
|
+
preRead(db, params) {
|
|
1669
|
+
assertRepeatRule(params);
|
|
1670
|
+
const pre = emptyPreState();
|
|
1671
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1672
|
+
return pre;
|
|
1673
|
+
},
|
|
1674
|
+
expectedDelta(_pre, params) {
|
|
1675
|
+
// Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
|
|
1676
|
+
// capture the prior rule (+ deadline flag) for the faithful undo.
|
|
1677
|
+
return {
|
|
1678
|
+
mode: "update",
|
|
1679
|
+
uuid: params.uuid,
|
|
1680
|
+
assert: [
|
|
1681
|
+
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1682
|
+
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1683
|
+
],
|
|
1684
|
+
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
1685
|
+
};
|
|
1686
|
+
},
|
|
1687
|
+
compile(params, vector) {
|
|
1688
|
+
if (vector !== "ui")
|
|
1689
|
+
unsupportedVector(this.op, vector);
|
|
1690
|
+
return uiDrive(projectRescheduleRepeatRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
|
|
1691
|
+
},
|
|
1692
|
+
};
|
|
1693
|
+
const projectPauseRepeat = {
|
|
1694
|
+
op: "project.pause-repeat",
|
|
1695
|
+
hazards: UI_HAZARDS,
|
|
1696
|
+
preRead(db, params) {
|
|
1697
|
+
const pre = emptyPreState();
|
|
1698
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1699
|
+
return pre;
|
|
1700
|
+
},
|
|
1701
|
+
expectedDelta(_pre, params) {
|
|
1702
|
+
// Pause (UIC2-a): rt1_instanceCreationPaused → 1; identity preserved.
|
|
1703
|
+
return {
|
|
1704
|
+
mode: "update",
|
|
1705
|
+
uuid: params.uuid,
|
|
1706
|
+
assert: [{ field: "repeating.paused", equals: true }],
|
|
1707
|
+
};
|
|
1708
|
+
},
|
|
1709
|
+
compile(params, vector) {
|
|
1710
|
+
if (vector !== "ui")
|
|
1711
|
+
unsupportedVector(this.op, vector);
|
|
1712
|
+
return uiDrive(projectPauseRepeatRecipe(params.uuid));
|
|
1713
|
+
},
|
|
1714
|
+
};
|
|
1715
|
+
const projectResumeRepeat = {
|
|
1716
|
+
op: "project.resume-repeat",
|
|
1717
|
+
hazards: UI_HAZARDS,
|
|
1718
|
+
preRead(db, params) {
|
|
1719
|
+
const pre = emptyPreState();
|
|
1720
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1721
|
+
return pre;
|
|
1722
|
+
},
|
|
1723
|
+
expectedDelta(_pre, params) {
|
|
1724
|
+
// Resume (UIC2-a): rt1_instanceCreationPaused → 0; identity preserved.
|
|
1725
|
+
return {
|
|
1726
|
+
mode: "update",
|
|
1727
|
+
uuid: params.uuid,
|
|
1728
|
+
assert: [{ field: "repeating.paused", equals: false }],
|
|
1729
|
+
};
|
|
1730
|
+
},
|
|
1731
|
+
compile(params, vector) {
|
|
1732
|
+
if (vector !== "ui")
|
|
1733
|
+
unsupportedVector(this.op, vector);
|
|
1734
|
+
return uiDrive(projectResumeRepeatRecipe(params.uuid));
|
|
1735
|
+
},
|
|
1736
|
+
};
|
|
1737
|
+
// project.make-repeating: the pure-AX row-selection op (UIC4). The command
|
|
1738
|
+
// spec drives the area / someday cases; the area-less-anytime coercion is done
|
|
1739
|
+
// by the runMakeRepeatingProject orchestrator BEFORE this spec sees the target,
|
|
1740
|
+
// so by drive time the taxonomy is always area or someday. A DIRECT dispatch on
|
|
1741
|
+
// an anytime project is refused by H-PROJECT-REPEAT (with the orchestrator as
|
|
1742
|
+
// the remediation).
|
|
1743
|
+
const projectMakeRepeating = {
|
|
1744
|
+
op: "project.make-repeating",
|
|
1745
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-PROJECT-REPEAT", "H-UI-DRIVE"],
|
|
1746
|
+
preRead(db, params) {
|
|
1747
|
+
assertRepeatRule(params);
|
|
1748
|
+
const pre = emptyPreState();
|
|
1749
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1750
|
+
pre.projectRepeat = classifyProjectRepeat(db, pre.target);
|
|
1751
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
|
|
1752
|
+
return pre;
|
|
1753
|
+
},
|
|
1754
|
+
expectedDelta(pre, _params, ctx) {
|
|
1755
|
+
// Identity REPLACEMENT (UIC4-b): the original project uuid dies; a NEW
|
|
1756
|
+
// template project (with a recurrence rule) is born alongside a spawned
|
|
1757
|
+
// instance sharing its title. Pick the TEMPLATE by asserting it IS one
|
|
1758
|
+
// (area preserved), excluding the pre-existing same-title rows so discovery
|
|
1759
|
+
// cannot bind to the spawned instance or the dying original.
|
|
1760
|
+
return {
|
|
1761
|
+
mode: "create",
|
|
1762
|
+
probe: {
|
|
1763
|
+
title: nonHeadingTitle(pre),
|
|
1764
|
+
type: "project",
|
|
1765
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1766
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1767
|
+
},
|
|
1768
|
+
assert: [{ field: "repeating.isTemplate", equals: true }],
|
|
1769
|
+
};
|
|
1770
|
+
},
|
|
1771
|
+
compile(params, vector, pre) {
|
|
1772
|
+
if (vector !== "ui")
|
|
1773
|
+
unsupportedVector(this.op, vector);
|
|
1774
|
+
const tax = pre.projectRepeat;
|
|
1775
|
+
if (tax === null || tax.kind === "refuse" || tax.kind === "anytime") {
|
|
1776
|
+
// Unreachable in practice: the guards block refuse/anytime before compile.
|
|
1777
|
+
throw new Error("project.make-repeating: no selectable-row taxonomy resolved (guard bypassed?)");
|
|
1778
|
+
}
|
|
1779
|
+
return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
|
|
1780
|
+
},
|
|
1781
|
+
};
|
|
1782
|
+
// project.create-repeating is delivered by the runCreateRepeatingProject
|
|
1783
|
+
// orchestrator (project.add THEN project.make-repeating); it has no single
|
|
1784
|
+
// atomic surface and is never dispatched directly through the pipeline.
|
|
1785
|
+
const CREATE_REPEATING_ORCHESTRATED = "project.create-repeating is delivered by the runCreateRepeatingProject orchestrator (create " +
|
|
1786
|
+
"the project, then promote it to a repeating series); it has no atomic surface and is never " +
|
|
1787
|
+
"dispatched directly through the pipeline";
|
|
1788
|
+
const projectCreateRepeating = {
|
|
1789
|
+
op: "project.create-repeating",
|
|
1790
|
+
hazards: [],
|
|
1791
|
+
preRead() {
|
|
1792
|
+
return emptyPreState();
|
|
1793
|
+
},
|
|
1794
|
+
expectedDelta() {
|
|
1795
|
+
throw new Error(CREATE_REPEATING_ORCHESTRATED);
|
|
1796
|
+
},
|
|
1797
|
+
compile() {
|
|
1798
|
+
throw new Error(CREATE_REPEATING_ORCHESTRATED);
|
|
1799
|
+
},
|
|
1800
|
+
};
|
|
1801
|
+
const todoConvertToProject = {
|
|
1802
|
+
op: "todo.convert-to-project",
|
|
1803
|
+
hazards: UI_HAZARDS,
|
|
1804
|
+
preRead(db, params) {
|
|
1805
|
+
const pre = emptyPreState();
|
|
1806
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1807
|
+
// The new row is a PROJECT (type=1) — exclude pre-existing same-title
|
|
1808
|
+
// projects, not the source to-do (a different type the probe never sees).
|
|
1809
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
|
|
1810
|
+
return pre;
|
|
1811
|
+
},
|
|
1812
|
+
expectedDelta(pre, _params, ctx) {
|
|
1813
|
+
// Identity REPLACEMENT (UI2-d): the to-do uuid dies; a NEW type=1 project
|
|
1814
|
+
// is born, notes preserved. Discover it (its uuid is returned).
|
|
1815
|
+
const target = pre.target;
|
|
1816
|
+
const notes = target !== null && target.type !== "heading" ? target.notes : "";
|
|
1817
|
+
return {
|
|
1818
|
+
mode: "create",
|
|
1819
|
+
probe: {
|
|
1820
|
+
title: nonHeadingTitle(pre),
|
|
1821
|
+
type: "project",
|
|
1822
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1823
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1824
|
+
},
|
|
1825
|
+
assert: [{ field: "notes", equals: notes }],
|
|
1826
|
+
};
|
|
1827
|
+
},
|
|
1828
|
+
compile(params, vector) {
|
|
1829
|
+
if (vector !== "ui")
|
|
1830
|
+
unsupportedVector(this.op, vector);
|
|
1831
|
+
return uiDrive(convertToProjectRecipe("todo.convert-to-project", params.uuid));
|
|
1832
|
+
},
|
|
1833
|
+
};
|
|
1834
|
+
const headingConvertToProject = {
|
|
1835
|
+
op: "heading.convert-to-project",
|
|
1836
|
+
hazards: UI_HAZARDS,
|
|
1837
|
+
preRead(db, params) {
|
|
1838
|
+
const pre = emptyPreState();
|
|
1839
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1840
|
+
// The new row is a PROJECT (type=1) carrying the former heading's title;
|
|
1841
|
+
// exclude pre-existing same-title projects. The heading uses its raw title
|
|
1842
|
+
// (headings are not covered by nonHeadingTitle).
|
|
1843
|
+
const title = pre.target !== null ? pre.target.title : "";
|
|
1844
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, title, "project");
|
|
1845
|
+
return pre;
|
|
1846
|
+
},
|
|
1847
|
+
expectedDelta(pre, _params, ctx) {
|
|
1848
|
+
// Identity REPLACEMENT (UI2-d): the heading uuid dies; a NEW type=1
|
|
1849
|
+
// project is born (promoted into the parent project's area, children
|
|
1850
|
+
// reparented). Discover the new project by its (former heading) title.
|
|
1851
|
+
const target = pre.target;
|
|
1852
|
+
const title = target !== null ? target.title : "";
|
|
1853
|
+
return {
|
|
1854
|
+
mode: "create",
|
|
1855
|
+
probe: {
|
|
1856
|
+
title,
|
|
1857
|
+
type: "project",
|
|
1858
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1859
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1860
|
+
},
|
|
1861
|
+
assert: [],
|
|
1862
|
+
};
|
|
1863
|
+
},
|
|
1864
|
+
compile(params, vector) {
|
|
1865
|
+
if (vector !== "ui")
|
|
1866
|
+
unsupportedVector(this.op, vector);
|
|
1867
|
+
return uiDrive(convertToProjectRecipe("heading.convert-to-project", params.uuid));
|
|
1868
|
+
},
|
|
1869
|
+
};
|
|
1870
|
+
// -------------------------------------------------- sidebar AREA reorder
|
|
1871
|
+
/** The compile-time placement for area.reorder (resolved refs). */
|
|
1872
|
+
function sidebarPlacementOf(params, pre) {
|
|
1873
|
+
const ref = pre.destArea?.resolved;
|
|
1874
|
+
if (params.before !== undefined && ref != null) {
|
|
1875
|
+
return { kind: "before", uuid: ref.uuid, title: ref.title };
|
|
1876
|
+
}
|
|
1877
|
+
if (params.after !== undefined && ref != null) {
|
|
1878
|
+
return { kind: "after", uuid: ref.uuid, title: ref.title };
|
|
1879
|
+
}
|
|
1880
|
+
return { kind: params.position === "first" ? "first" : "last" };
|
|
1881
|
+
}
|
|
1882
|
+
const areaReorderSidebar = {
|
|
1883
|
+
op: "area.reorder",
|
|
1884
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"],
|
|
1885
|
+
preRead(db, params) {
|
|
1886
|
+
const destinations = [params.before, params.after, params.position].filter((d) => d !== undefined);
|
|
1887
|
+
if (destinations.length !== 1) {
|
|
1888
|
+
throw new RangeError("pass exactly one destination: before, after, or position (first | last)");
|
|
1889
|
+
}
|
|
1890
|
+
if (params.position !== undefined &&
|
|
1891
|
+
params.position !== "first" &&
|
|
1892
|
+
params.position !== "last") {
|
|
1893
|
+
throw new RangeError(`invalid position "${params.position}" — expected first | last`);
|
|
1894
|
+
}
|
|
1895
|
+
const pre = emptyPreState();
|
|
1896
|
+
pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
|
|
1897
|
+
const ref = params.before ?? params.after;
|
|
1898
|
+
if (ref !== undefined)
|
|
1899
|
+
pre.destArea = resolveArea(db, { uuid: ref, title: ref });
|
|
1900
|
+
const target = pre.entityTarget.resolved;
|
|
1901
|
+
const dest = pre.destArea?.resolved;
|
|
1902
|
+
if (target != null && dest != null && target.uuid === dest.uuid) {
|
|
1903
|
+
throw new RangeError("the destination area is the area being moved");
|
|
1904
|
+
}
|
|
1905
|
+
// The sidebar drag addresses rows by their VISIBLE NAME — a duplicated
|
|
1906
|
+
// area title makes the row ambiguous, so refuse up front.
|
|
1907
|
+
for (const area of [target, dest]) {
|
|
1908
|
+
if (area == null)
|
|
1909
|
+
continue;
|
|
1910
|
+
const dup = db
|
|
1911
|
+
.prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
|
|
1912
|
+
.get(area.title);
|
|
1913
|
+
if (dup.n > 1) {
|
|
1914
|
+
throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas — the sidebar move addresses ` +
|
|
1915
|
+
"areas by their visible name; rename one first");
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
|
|
1919
|
+
return pre;
|
|
1920
|
+
},
|
|
1921
|
+
expectedDelta(pre, params) {
|
|
1922
|
+
const target = pre.entityTarget?.resolved?.uuid ?? "";
|
|
1923
|
+
const order = (pre.areaOrder ?? []).filter((u) => u !== target);
|
|
1924
|
+
const refUuid = pre.destArea?.resolved?.uuid;
|
|
1925
|
+
// Assert the implicated RELATIVE pair (index VALUES are never asserted —
|
|
1926
|
+
// a drag may renumber a neighbor, AXDRAG1-a); capture the FULL pre-order
|
|
1927
|
+
// for undo.
|
|
1928
|
+
let sequence;
|
|
1929
|
+
if (params.before !== undefined && refUuid !== undefined) {
|
|
1930
|
+
sequence = [target, refUuid];
|
|
1931
|
+
}
|
|
1932
|
+
else if (params.after !== undefined && refUuid !== undefined) {
|
|
1933
|
+
sequence = [refUuid, target];
|
|
1934
|
+
}
|
|
1935
|
+
else if (params.position === "first") {
|
|
1936
|
+
const first = order[0];
|
|
1937
|
+
sequence = first === undefined ? [target] : [target, first];
|
|
1938
|
+
}
|
|
1939
|
+
else {
|
|
1940
|
+
const last = order.at(-1);
|
|
1941
|
+
sequence = last === undefined ? [target] : [last, target];
|
|
1942
|
+
}
|
|
1943
|
+
return {
|
|
1944
|
+
mode: "ordering",
|
|
1945
|
+
key: "area-index",
|
|
1946
|
+
sequence,
|
|
1947
|
+
capture: pre.areaOrder ?? sequence,
|
|
1948
|
+
subject: target,
|
|
1949
|
+
};
|
|
1950
|
+
},
|
|
1951
|
+
compile(params, vector, pre) {
|
|
1952
|
+
if (vector !== "ui")
|
|
1953
|
+
unsupportedVector(this.op, vector);
|
|
1954
|
+
const target = pre.entityTarget?.resolved;
|
|
1955
|
+
return uiDrive(areaReorderSidebarRecipe({ uuid: target?.uuid ?? "", title: target?.title ?? "" }, sidebarPlacementOf(params, pre)));
|
|
1956
|
+
},
|
|
1957
|
+
};
|
|
1456
1958
|
const trashEmpty = {
|
|
1457
1959
|
op: "trash.empty",
|
|
1458
1960
|
hazards: ["H-PERMANENT-DELETE"],
|
|
@@ -1508,5 +2010,17 @@ export const COMMANDS = {
|
|
|
1508
2010
|
"heading.unarchive": headingUnarchive,
|
|
1509
2011
|
"heading.create": headingCreate,
|
|
1510
2012
|
"todo.clear-dated-reminder": todoClearDatedReminder,
|
|
2013
|
+
"todo.make-repeating": todoMakeRepeating,
|
|
2014
|
+
"todo.reschedule-repeat": todoRescheduleRepeat,
|
|
2015
|
+
"todo.pause-repeat": todoPauseRepeat,
|
|
2016
|
+
"todo.resume-repeat": todoResumeRepeat,
|
|
2017
|
+
"todo.convert-to-project": todoConvertToProject,
|
|
2018
|
+
"heading.convert-to-project": headingConvertToProject,
|
|
2019
|
+
"project.reschedule-repeat": projectRescheduleRepeat,
|
|
2020
|
+
"project.pause-repeat": projectPauseRepeat,
|
|
2021
|
+
"project.resume-repeat": projectResumeRepeat,
|
|
2022
|
+
"area.reorder": areaReorderSidebar,
|
|
2023
|
+
"project.make-repeating": projectMakeRepeating,
|
|
2024
|
+
"project.create-repeating": projectCreateRepeating,
|
|
1511
2025
|
};
|
|
1512
2026
|
//# sourceMappingURL=commands.js.map
|