things-api 0.9.0 → 0.11.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 +57 -12
- 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/clock.d.ts +8 -0
- package/dist/cli/clock.js +24 -0
- package/dist/cli/clock.js.map +1 -0
- package/dist/cli/commands/area.d.ts +26 -3
- package/dist/cli/commands/area.js +129 -75
- 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/install-skill.d.ts +45 -0
- package/dist/cli/commands/install-skill.js +207 -0
- package/dist/cli/commands/install-skill.js.map +1 -0
- 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 +74 -35
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +15 -2
- package/dist/cli/commands/reads.js +459 -165
- 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 +8 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +53 -30
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.d.ts +1 -1
- package/dist/cli/commands/todo.js +20 -13
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +388 -161
- 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 +55 -2
- package/dist/cli/glyphs.js +114 -21
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +54 -0
- package/dist/cli/help.js +405 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.js +64 -26
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +45 -0
- package/dist/cli/move-hint.js +196 -0
- package/dist/cli/move-hint.js.map +1 -0
- package/dist/cli/period.d.ts +9 -8
- package/dist/cli/period.js +56 -30
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +26 -19
- package/dist/cli/read-driver.js +77 -33
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +92 -28
- package/dist/cli/render.js +333 -103
- 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/skill-check.d.ts +21 -0
- package/dist/cli/skill-check.js +82 -0
- package/dist/cli/skill-check.js.map +1 -0
- package/dist/cli/skill.d.ts +62 -0
- package/dist/cli/skill.js +142 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tag-filters.d.ts +63 -0
- package/dist/cli/tag-filters.js +59 -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 +163 -0
- package/dist/cli/verb-hint.js.map +1 -0
- package/dist/cli/version.d.ts +4 -0
- package/dist/cli/version.js +18 -0
- package/dist/cli/version.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 +160 -24
- package/dist/client.js +132 -19
- 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 +97 -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 +36 -5
- package/dist/index.js +38 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -1
- package/dist/mcp/server.js +1038 -436
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/clock.d.ts +61 -0
- package/dist/model/clock.js +71 -0
- package/dist/model/clock.js.map +1 -0
- package/dist/model/dates.d.ts +26 -3
- package/dist/model/dates.js +84 -3
- package/dist/model/dates.js.map +1 -1
- package/dist/model/entities.d.ts +48 -10
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +9 -2
- package/dist/model/mappers.js +32 -9
- 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 +55 -17
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.d.ts +1 -1
- package/dist/read/detail.js +11 -4
- package/dist/read/detail.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/log-boundary.d.ts +1 -1
- package/dist/read/log-boundary.js +13 -3
- package/dist/read/log-boundary.js.map +1 -1
- 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 +45 -12
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/queries.d.ts +152 -7
- package/dist/read/queries.js +277 -36
- 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/snapshot.d.ts +1 -1
- package/dist/read/snapshot.js +9 -2
- package/dist/read/snapshot.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 +147 -22
- package/dist/read/views.js +282 -80
- 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 +71 -0
- package/dist/write/accessibility-probe.js.map +1 -0
- package/dist/write/automation-probe.d.ts +8 -1
- package/dist/write/automation-probe.js +16 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.js +5 -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 +3 -1
- package/dist/write/commands.js +602 -49
- 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 +87 -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 +73 -2
- package/dist/write/pipeline.js +218 -43
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.js +8 -1
- package/dist/write/planner.js.map +1 -1
- package/dist/write/pre-state.d.ts +105 -3
- package/dist/write/pre-state.js +144 -3
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/recurrence-rule-blob.d.ts +65 -0
- package/dist/write/recurrence-rule-blob.js +150 -0
- package/dist/write/recurrence-rule-blob.js.map +1 -0
- 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 +15 -1
- package/dist/write/vectors/registry.js +65 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +44 -0
- package/dist/write/vectors/simulator.js +1136 -0
- package/dist/write/vectors/simulator.js.map +1 -0
- package/dist/write/vectors/types.d.ts +154 -3
- package/dist/write/vectors/ui-certification.d.ts +54 -0
- package/dist/write/vectors/ui-certification.js +43 -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 +83 -0
- package/dist/write/vectors/ui-recipes.js +610 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -0
- package/dist/write/vectors/ui.d.ts +152 -0
- package/dist/write/vectors/ui.js +704 -0
- package/dist/write/vectors/ui.js.map +1 -0
- package/dist/write/verify/delta.d.ts +141 -4
- package/dist/write/verify/delta.js +199 -21
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +7 -1
- package/dist/write/verify/poller.js +21 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +18 -3
- package/skills/things-cli/SKILL.md +53 -0
- package/skills/things-cli/references/contracts.md +41 -0
- package/skills/things-cli/references/gui.md +11 -0
- package/skills/things-cli/references/model.md +51 -0
- 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,12 @@
|
|
|
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, classifyHeadingConvert, classifyProjectRepeat, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, 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, headingConvertToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
|
|
9
|
+
import { buildRepeatingFingerprint } from "./verify/delta.js";
|
|
6
10
|
// ------------------------------------------------------------------ helpers
|
|
7
11
|
function thingsUrl(command, params, token) {
|
|
8
12
|
const parts = [];
|
|
@@ -99,6 +103,18 @@ function whenAssertions(when, todayIso) {
|
|
|
99
103
|
function sortedTags(tags) {
|
|
100
104
|
return [...tags].toSorted();
|
|
101
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve tag refs (title or `parent/child` path — names only) into pre-state:
|
|
108
|
+
* the leaf titles to apply, plus the unknown (`missingTags`) refs the
|
|
109
|
+
* H-UNKNOWN-TAG guard refuses on. Tags apply BY NAME through the app's own
|
|
110
|
+
* vector, so the app resolves the name (duplicate names resolve app-side, as
|
|
111
|
+
* in the GUI) — the resolver is a pure existence check.
|
|
112
|
+
*/
|
|
113
|
+
function applyTagRefs(db, pre, tags) {
|
|
114
|
+
const res = resolveTagRefs(db, tags);
|
|
115
|
+
pre.missingTags = res.missing;
|
|
116
|
+
pre.resolvedTagTitles = res.titles;
|
|
117
|
+
}
|
|
102
118
|
/** Round-trip normalization: "6:5"-style inputs → canonical "06:05". */
|
|
103
119
|
function normalizeReminder(time) {
|
|
104
120
|
return decodeReminderTime(encodeReminderTime(time)) ?? time;
|
|
@@ -115,6 +131,17 @@ function whenWithReminder(when, reminder) {
|
|
|
115
131
|
function containerGiven(ref) {
|
|
116
132
|
return ref !== undefined && (ref.uuid !== undefined || ref.title !== undefined);
|
|
117
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* The create-probe title for the duplicate / identity-replacement ops, whose
|
|
136
|
+
* new row inherits the pre-read target's title. Non-heading targets only (the
|
|
137
|
+
* heading-convert op uses the raw title, headings included); "" when the target
|
|
138
|
+
* did not resolve. Shared by the pre-read same-title capture and the delta so
|
|
139
|
+
* the excluded set and the probe title cannot drift apart.
|
|
140
|
+
*/
|
|
141
|
+
function nonHeadingTitle(pre) {
|
|
142
|
+
const target = pre.target;
|
|
143
|
+
return target !== null && target.type !== "heading" ? target.title : "";
|
|
144
|
+
}
|
|
118
145
|
// ----------------------------------------------------------------- commands
|
|
119
146
|
const todoAdd = {
|
|
120
147
|
op: "todo.add",
|
|
@@ -126,6 +153,12 @@ const todoAdd = {
|
|
|
126
153
|
"H-REMINDER-SCOPE",
|
|
127
154
|
],
|
|
128
155
|
preRead(db, params) {
|
|
156
|
+
if (containerGiven(params.project) && containerGiven(params.area)) {
|
|
157
|
+
throw new RangeError("project and area are exclusive destinations");
|
|
158
|
+
}
|
|
159
|
+
if (params.heading !== undefined && !containerGiven(params.project)) {
|
|
160
|
+
throw new RangeError("heading requires a project destination");
|
|
161
|
+
}
|
|
129
162
|
const pre = emptyPreState();
|
|
130
163
|
if (containerGiven(params.project)) {
|
|
131
164
|
pre.destProject = resolveProject(db, params.project);
|
|
@@ -139,7 +172,8 @@ const todoAdd = {
|
|
|
139
172
|
if (containerGiven(params.area))
|
|
140
173
|
pre.destArea = resolveArea(db, params.area);
|
|
141
174
|
if (params.tags !== undefined)
|
|
142
|
-
|
|
175
|
+
applyTagRefs(db, pre, params.tags);
|
|
176
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "to-do");
|
|
143
177
|
return pre;
|
|
144
178
|
},
|
|
145
179
|
expectedDelta(pre, params, ctx) {
|
|
@@ -153,8 +187,9 @@ const todoAdd = {
|
|
|
153
187
|
}
|
|
154
188
|
if (params.deadline !== undefined)
|
|
155
189
|
assert.push({ field: "deadline", equals: params.deadline });
|
|
156
|
-
if (params.tags !== undefined)
|
|
157
|
-
assert.push({ field: "tags", equals: sortedTags(
|
|
190
|
+
if (params.tags !== undefined) {
|
|
191
|
+
assert.push({ field: "tags", equals: sortedTags(pre.resolvedTagTitles) });
|
|
192
|
+
}
|
|
158
193
|
if (params.checklistItems !== undefined) {
|
|
159
194
|
assert.push({ field: "checklistTitles", equals: params.checklistItems });
|
|
160
195
|
}
|
|
@@ -171,7 +206,12 @@ const todoAdd = {
|
|
|
171
206
|
assert.push({ field: "area.uuid", equals: area.uuid });
|
|
172
207
|
return {
|
|
173
208
|
mode: "create",
|
|
174
|
-
probe: {
|
|
209
|
+
probe: {
|
|
210
|
+
title: params.title,
|
|
211
|
+
type: "to-do",
|
|
212
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
213
|
+
excludeUuids: pre.sameTitleUuids,
|
|
214
|
+
},
|
|
175
215
|
assert,
|
|
176
216
|
};
|
|
177
217
|
},
|
|
@@ -184,7 +224,7 @@ const todoAdd = {
|
|
|
184
224
|
notes: params.notes,
|
|
185
225
|
when: params.when === undefined ? undefined : whenWithReminder(params.when, params.reminder),
|
|
186
226
|
deadline: params.deadline,
|
|
187
|
-
tags: params.tags
|
|
227
|
+
tags: params.tags === undefined ? undefined : pre.resolvedTagTitles.join(","),
|
|
188
228
|
"checklist-items": params.checklistItems?.join("\n"),
|
|
189
229
|
"list-id": container?.uuid,
|
|
190
230
|
heading: pre.destHeading?.resolved?.title,
|
|
@@ -317,6 +357,12 @@ const todoMove = {
|
|
|
317
357
|
if (params.detach === true && container) {
|
|
318
358
|
throw new RangeError("detach is exclusive with project/area/heading destinations");
|
|
319
359
|
}
|
|
360
|
+
if (containerGiven(params.project) && containerGiven(params.area)) {
|
|
361
|
+
throw new RangeError("project and area are exclusive destinations");
|
|
362
|
+
}
|
|
363
|
+
if (params.heading !== undefined && !containerGiven(params.project)) {
|
|
364
|
+
throw new RangeError("heading requires a project destination");
|
|
365
|
+
}
|
|
320
366
|
const pre = emptyPreState();
|
|
321
367
|
pre.target = loadTarget(db, params.uuid);
|
|
322
368
|
if (containerGiven(params.project)) {
|
|
@@ -400,21 +446,22 @@ const todoSetTags = {
|
|
|
400
446
|
preRead(db, params) {
|
|
401
447
|
const pre = emptyPreState();
|
|
402
448
|
pre.target = loadTarget(db, params.uuid);
|
|
403
|
-
|
|
449
|
+
applyTagRefs(db, pre, params.tags);
|
|
404
450
|
return pre;
|
|
405
451
|
},
|
|
406
|
-
expectedDelta(
|
|
452
|
+
expectedDelta(pre, params) {
|
|
407
453
|
return {
|
|
408
454
|
mode: "update",
|
|
409
455
|
uuid: params.uuid,
|
|
410
|
-
assert: [{ field: "tags", equals: sortedTags(
|
|
456
|
+
assert: [{ field: "tags", equals: sortedTags(pre.resolvedTagTitles) }],
|
|
411
457
|
};
|
|
412
458
|
},
|
|
413
|
-
compile(params, vector,
|
|
459
|
+
compile(params, vector, pre, ctx) {
|
|
460
|
+
const applied = pre.resolvedTagTitles;
|
|
414
461
|
if (vector === "url-scheme") {
|
|
415
|
-
return thingsUrl("update", { id: params.uuid, tags:
|
|
462
|
+
return thingsUrl("update", { id: params.uuid, tags: applied.join(",") }, ctx.token);
|
|
416
463
|
}
|
|
417
|
-
return osa(`set tag names of to do id ${q(params.uuid)} to ${q(
|
|
464
|
+
return osa(`set tag names of to do id ${q(params.uuid)} to ${q(applied.join(", "))}`);
|
|
418
465
|
},
|
|
419
466
|
};
|
|
420
467
|
/** Normalize the string | spec union; decide whether states force the json form. */
|
|
@@ -516,6 +563,7 @@ const projectAdd = {
|
|
|
516
563
|
const pre = emptyPreState();
|
|
517
564
|
if (containerGiven(params.area))
|
|
518
565
|
pre.destArea = resolveArea(db, params.area);
|
|
566
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "project");
|
|
519
567
|
return pre;
|
|
520
568
|
},
|
|
521
569
|
expectedDelta(pre, params, ctx) {
|
|
@@ -531,7 +579,12 @@ const projectAdd = {
|
|
|
531
579
|
assert.push({ field: "area.uuid", equals: area.uuid });
|
|
532
580
|
return {
|
|
533
581
|
mode: "create",
|
|
534
|
-
probe: {
|
|
582
|
+
probe: {
|
|
583
|
+
title: params.title,
|
|
584
|
+
type: "project",
|
|
585
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
586
|
+
excludeUuids: pre.sameTitleUuids,
|
|
587
|
+
},
|
|
535
588
|
assert,
|
|
536
589
|
};
|
|
537
590
|
},
|
|
@@ -602,24 +655,25 @@ const projectSetTags = {
|
|
|
602
655
|
preRead(db, params) {
|
|
603
656
|
const pre = emptyPreState();
|
|
604
657
|
pre.target = loadTarget(db, params.uuid);
|
|
605
|
-
|
|
658
|
+
applyTagRefs(db, pre, params.tags);
|
|
606
659
|
return pre;
|
|
607
660
|
},
|
|
608
|
-
expectedDelta(
|
|
661
|
+
expectedDelta(pre, params) {
|
|
609
662
|
return {
|
|
610
663
|
mode: "update",
|
|
611
664
|
uuid: params.uuid,
|
|
612
|
-
assert: [{ field: "tags", equals: sortedTags(
|
|
665
|
+
assert: [{ field: "tags", equals: sortedTags(pre.resolvedTagTitles) }],
|
|
613
666
|
};
|
|
614
667
|
},
|
|
615
|
-
compile(params, vector,
|
|
668
|
+
compile(params, vector, pre, ctx) {
|
|
616
669
|
// Both vectors validated on projects (A1 URL, A2 AppleScript). Full
|
|
617
670
|
// replacement semantics mirror todo.set-tags; unknown tags are guarded
|
|
618
671
|
// pre-write (the app silently drops them).
|
|
672
|
+
const applied = pre.resolvedTagTitles;
|
|
619
673
|
if (vector === "url-scheme") {
|
|
620
|
-
return thingsUrl("update-project", { id: params.uuid, tags:
|
|
674
|
+
return thingsUrl("update-project", { id: params.uuid, tags: applied.join(",") }, ctx.token);
|
|
621
675
|
}
|
|
622
|
-
return osa(`set tag names of project id ${q(params.uuid)} to ${q(
|
|
676
|
+
return osa(`set tag names of project id ${q(params.uuid)} to ${q(applied.join(", "))}`);
|
|
623
677
|
},
|
|
624
678
|
};
|
|
625
679
|
const projectMove = {
|
|
@@ -665,7 +719,7 @@ const projectMove = {
|
|
|
665
719
|
};
|
|
666
720
|
const todoRestore = {
|
|
667
721
|
op: "todo.restore",
|
|
668
|
-
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE"],
|
|
722
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-TEMPLATE-CHILD-RESTORE", "H-REPEAT-SCHEDULE"],
|
|
669
723
|
preRead(db, params) {
|
|
670
724
|
const pre = emptyPreState();
|
|
671
725
|
pre.target = loadTarget(db, params.uuid);
|
|
@@ -695,17 +749,24 @@ const projectDuplicate = {
|
|
|
695
749
|
preRead(db, params) {
|
|
696
750
|
const pre = emptyPreState();
|
|
697
751
|
pre.target = loadTarget(db, params.uuid);
|
|
752
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
|
|
698
753
|
return pre;
|
|
699
754
|
},
|
|
700
755
|
expectedDelta(pre, _params, ctx) {
|
|
701
756
|
// 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).
|
|
757
|
+
// the create probe (fresh creationDate, same as the to-do path E07). The
|
|
758
|
+
// ORIGINAL shares the copy's title, so it is among the excluded pre-existing
|
|
759
|
+
// rows — discovery lands on the copy, never the source.
|
|
703
760
|
const target = pre.target;
|
|
704
|
-
const title = target !== null && target.type !== "heading" ? target.title : "";
|
|
705
761
|
const notes = target !== null && target.type !== "heading" ? target.notes : "";
|
|
706
762
|
return {
|
|
707
763
|
mode: "create",
|
|
708
|
-
probe: {
|
|
764
|
+
probe: {
|
|
765
|
+
title: nonHeadingTitle(pre),
|
|
766
|
+
type: "project",
|
|
767
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
768
|
+
excludeUuids: pre.sameTitleUuids,
|
|
769
|
+
},
|
|
709
770
|
assert: [{ field: "notes", equals: notes }],
|
|
710
771
|
};
|
|
711
772
|
},
|
|
@@ -861,7 +922,7 @@ const areaAdd = {
|
|
|
861
922
|
preRead(db, params) {
|
|
862
923
|
const pre = emptyPreState();
|
|
863
924
|
if (params.tags !== undefined)
|
|
864
|
-
|
|
925
|
+
applyTagRefs(db, pre, params.tags);
|
|
865
926
|
pre.existingEntityUuids = db.prepare("SELECT uuid FROM TMArea WHERE title = ? COLLATE NOCASE").all(params.title).map((r) => r.uuid);
|
|
866
927
|
return pre;
|
|
867
928
|
},
|
|
@@ -871,9 +932,13 @@ const areaAdd = {
|
|
|
871
932
|
entity: "area",
|
|
872
933
|
title: params.title,
|
|
873
934
|
excludeUuids: pre.existingEntityUuids,
|
|
935
|
+
// Assert the landed tag set on the created area — the app silently
|
|
936
|
+
// drops unknowns (guarded pre-write), so the created row must carry
|
|
937
|
+
// exactly the resolved titles (parity with the other tag-set ops).
|
|
938
|
+
...(params.tags !== undefined && { assertTags: sortedTags(pre.resolvedTagTitles) }),
|
|
874
939
|
};
|
|
875
940
|
},
|
|
876
|
-
compile(params, vector) {
|
|
941
|
+
compile(params, vector, pre) {
|
|
877
942
|
if (vector !== "applescript")
|
|
878
943
|
unsupportedVector(this.op, vector);
|
|
879
944
|
const make = `make new area with properties {name:${q(params.title)}}`;
|
|
@@ -881,7 +946,7 @@ const areaAdd = {
|
|
|
881
946
|
return osa(make);
|
|
882
947
|
const payload = `tell application "Things3"\n` +
|
|
883
948
|
` ${make}\n` +
|
|
884
|
-
` set tag names of area ${q(params.title)} to ${q(
|
|
949
|
+
` set tag names of area ${q(params.title)} to ${q(pre.resolvedTagTitles.join(", "))}\n` +
|
|
885
950
|
`end tell`;
|
|
886
951
|
return { vector: "applescript", kind: "osascript", payload, redactedPayload: payload };
|
|
887
952
|
},
|
|
@@ -966,17 +1031,23 @@ const todoDuplicate = {
|
|
|
966
1031
|
preRead(db, params) {
|
|
967
1032
|
const pre = emptyPreState();
|
|
968
1033
|
pre.target = loadTarget(db, params.uuid);
|
|
1034
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
|
|
969
1035
|
return pre;
|
|
970
1036
|
},
|
|
971
1037
|
expectedDelta(pre, _params, ctx) {
|
|
972
1038
|
// The copy carries the same title/notes with a fresh uuid + creationDate
|
|
973
|
-
// (E07) — discover it with the create probe, assert copy fidelity.
|
|
1039
|
+
// (E07) — discover it with the create probe, assert copy fidelity. The
|
|
1040
|
+
// ORIGINAL shares the title, so it is excluded as a pre-existing row.
|
|
974
1041
|
const target = pre.target;
|
|
975
|
-
const title = target !== null && target.type !== "heading" ? target.title : "";
|
|
976
1042
|
const notes = target !== null && target.type !== "heading" ? target.notes : "";
|
|
977
1043
|
return {
|
|
978
1044
|
mode: "create",
|
|
979
|
-
probe: {
|
|
1045
|
+
probe: {
|
|
1046
|
+
title: nonHeadingTitle(pre),
|
|
1047
|
+
type: "to-do",
|
|
1048
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1049
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1050
|
+
},
|
|
980
1051
|
assert: [{ field: "notes", equals: notes }],
|
|
981
1052
|
};
|
|
982
1053
|
},
|
|
@@ -997,15 +1068,16 @@ const areaUpdate = {
|
|
|
997
1068
|
const pre = emptyPreState();
|
|
998
1069
|
pre.entityTarget = resolveArea(db, { title: params.target, uuid: params.target });
|
|
999
1070
|
if (params.tags !== undefined)
|
|
1000
|
-
|
|
1071
|
+
applyTagRefs(db, pre, params.tags);
|
|
1001
1072
|
return pre;
|
|
1002
1073
|
},
|
|
1003
1074
|
expectedDelta(pre, params) {
|
|
1004
1075
|
const assert = [];
|
|
1005
1076
|
if (params.title !== undefined)
|
|
1006
1077
|
assert.push({ field: "title", equals: params.title });
|
|
1007
|
-
if (params.tags !== undefined)
|
|
1008
|
-
assert.push({ field: "tags", equals: sortedTags(
|
|
1078
|
+
if (params.tags !== undefined) {
|
|
1079
|
+
assert.push({ field: "tags", equals: sortedTags(pre.resolvedTagTitles) });
|
|
1080
|
+
}
|
|
1009
1081
|
return {
|
|
1010
1082
|
mode: "entity-updated",
|
|
1011
1083
|
entity: "area",
|
|
@@ -1021,7 +1093,7 @@ const areaUpdate = {
|
|
|
1021
1093
|
if (params.title !== undefined)
|
|
1022
1094
|
lines.push(`set name of area id ${id} to ${q(params.title)}`);
|
|
1023
1095
|
if (params.tags !== undefined) {
|
|
1024
|
-
lines.push(`set tag names of area id ${id} to ${q(
|
|
1096
|
+
lines.push(`set tag names of area id ${id} to ${q(pre.resolvedTagTitles.join(", "))}`);
|
|
1025
1097
|
}
|
|
1026
1098
|
if (lines.length === 1)
|
|
1027
1099
|
return osa(lines[0]);
|
|
@@ -1180,6 +1252,18 @@ function asDateBlock(varName, iso) {
|
|
|
1180
1252
|
`set day of ${varName} to ${d}`,
|
|
1181
1253
|
];
|
|
1182
1254
|
}
|
|
1255
|
+
/**
|
|
1256
|
+
* Local noon -> UTC instant, so the stored timestamp decodes back to the
|
|
1257
|
+
* requested local DATE in every timezone (P4d: json attrs honored exactly).
|
|
1258
|
+
* WITHOUT milliseconds: the app's json date parser rejects fractional
|
|
1259
|
+
* seconds — a `.000Z` timestamp fails the whole command (error modal, no
|
|
1260
|
+
* write; caught live by the e2e 2026-07-09 — P4d's validated shape was
|
|
1261
|
+
* second-precision).
|
|
1262
|
+
*/
|
|
1263
|
+
function utcNoon(iso) {
|
|
1264
|
+
const [y, m, d] = iso.split("-").map(Number);
|
|
1265
|
+
return new Date(y ?? 0, (m ?? 1) - 1, d ?? 1, 12, 0, 0).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1266
|
+
}
|
|
1183
1267
|
const todoBackdate = {
|
|
1184
1268
|
op: "todo.backdate",
|
|
1185
1269
|
hazards: ["H-UNKNOWN-DESTINATION", "H-BACKDATE-OPEN"],
|
|
@@ -1223,7 +1307,7 @@ const todoAddLogged = {
|
|
|
1223
1307
|
throw new RangeError("creationDate must not be after completionDate");
|
|
1224
1308
|
}
|
|
1225
1309
|
const pre = emptyPreState();
|
|
1226
|
-
pre.sameTitleUuids = db
|
|
1310
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "to-do");
|
|
1227
1311
|
return pre;
|
|
1228
1312
|
},
|
|
1229
1313
|
expectedDelta(pre, params) {
|
|
@@ -1250,18 +1334,6 @@ const todoAddLogged = {
|
|
|
1250
1334
|
compile(params, vector, _pre, ctx) {
|
|
1251
1335
|
if (vector !== "url-scheme")
|
|
1252
1336
|
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
1337
|
const payload = JSON.stringify([
|
|
1266
1338
|
{
|
|
1267
1339
|
type: "to-do",
|
|
@@ -1395,15 +1467,21 @@ const headingCreate = {
|
|
|
1395
1467
|
preRead(db, params) {
|
|
1396
1468
|
const pre = emptyPreState();
|
|
1397
1469
|
pre.destProject = resolveProject(db, params.project);
|
|
1470
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, params.title, "heading");
|
|
1398
1471
|
return pre;
|
|
1399
1472
|
},
|
|
1400
1473
|
expectedDelta(pre, params, ctx) {
|
|
1401
1474
|
const project = pre.destProject?.resolved;
|
|
1402
|
-
// A new type=2 row with this title under the project;
|
|
1403
|
-
//
|
|
1475
|
+
// A new type=2 row with this title under the project; a pre-existing
|
|
1476
|
+
// same-title heading is excluded so discovery cannot bind to it.
|
|
1404
1477
|
return {
|
|
1405
1478
|
mode: "create",
|
|
1406
|
-
probe: {
|
|
1479
|
+
probe: {
|
|
1480
|
+
title: params.title,
|
|
1481
|
+
type: "heading",
|
|
1482
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1483
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1484
|
+
},
|
|
1407
1485
|
assert: project !== undefined && project !== null
|
|
1408
1486
|
? [{ field: "project.uuid", equals: project.uuid }]
|
|
1409
1487
|
: [],
|
|
@@ -1453,6 +1531,469 @@ const todoClearDatedReminder = {
|
|
|
1453
1531
|
});
|
|
1454
1532
|
},
|
|
1455
1533
|
};
|
|
1534
|
+
// ------------------------------------------------------- ui (GUI) vector
|
|
1535
|
+
/** A compiled Accessibility recipe as a CompiledInvocation (no secrets). */
|
|
1536
|
+
function uiDrive(recipe) {
|
|
1537
|
+
const rendered = `ui-drive ${recipe.op} on ${recipe.targetUuid}: ${recipe.steps
|
|
1538
|
+
.map((s) => s.label)
|
|
1539
|
+
.join(" → ")}`;
|
|
1540
|
+
return { vector: "ui", kind: "ui-drive", payload: rendered, redactedPayload: rendered, recipe };
|
|
1541
|
+
}
|
|
1542
|
+
// Re-exported so make-repeating-project.ts and existing callers keep importing
|
|
1543
|
+
// the validator from commands.ts; the implementation + the full combination
|
|
1544
|
+
// matrix live in repeat-rule.ts.
|
|
1545
|
+
export { assertRepeatRule };
|
|
1546
|
+
/**
|
|
1547
|
+
* The extended-vocabulary fields of a rule as a recipe `extras` bag, including
|
|
1548
|
+
* ONLY the keys that are present (exactOptionalPropertyTypes: never set an
|
|
1549
|
+
* optional field to undefined). A bare `{ uuid, frequency, interval }` yields
|
|
1550
|
+
* `{}`, so the recipe drives exactly the certified two-control path.
|
|
1551
|
+
*/
|
|
1552
|
+
function ruleExtras(params) {
|
|
1553
|
+
return {
|
|
1554
|
+
...(params.afterCompletion !== undefined && { afterCompletion: params.afterCompletion }),
|
|
1555
|
+
...(params.weekdays !== undefined && { weekdays: params.weekdays }),
|
|
1556
|
+
...(params.monthly !== undefined && { monthly: params.monthly }),
|
|
1557
|
+
...(params.yearly !== undefined && { yearly: params.yearly }),
|
|
1558
|
+
...(params.ends !== undefined && { ends: params.ends }),
|
|
1559
|
+
...(params.reminder !== undefined && { reminder: params.reminder }),
|
|
1560
|
+
...(params.deadline !== undefined && { deadline: params.deadline }),
|
|
1561
|
+
...(params.startDaysEarlier !== undefined && { startDaysEarlier: params.startDaysEarlier }),
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
/** ui ops all guard existence/type + the H-UI-DRIVE acknowledgement. */
|
|
1565
|
+
const UI_HAZARDS = ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"];
|
|
1566
|
+
const todoMakeRepeating = {
|
|
1567
|
+
op: "todo.make-repeating",
|
|
1568
|
+
hazards: UI_HAZARDS,
|
|
1569
|
+
preRead(db, params) {
|
|
1570
|
+
assertRepeatRule(params);
|
|
1571
|
+
const pre = emptyPreState();
|
|
1572
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1573
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
|
|
1574
|
+
return pre;
|
|
1575
|
+
},
|
|
1576
|
+
expectedDelta(pre, _params, ctx) {
|
|
1577
|
+
// Identity REPLACEMENT or preserve-as-instance (UI2-a / RSIM-R): a NEW
|
|
1578
|
+
// template row (type=0 with a recurrence rule) is born; the original uuid is
|
|
1579
|
+
// EITHER destroyed OR relinked as the current-occurrence instance. Discover
|
|
1580
|
+
// the template with the create probe (excluding the pre-existing same-title
|
|
1581
|
+
// rows), pick it by asserting it IS a template, then the `repeating` context
|
|
1582
|
+
// hardens discovery (restored time-bound, source-fingerprint tiebreak) and
|
|
1583
|
+
// derives instance + source fate for the enriched result.
|
|
1584
|
+
return {
|
|
1585
|
+
mode: "create",
|
|
1586
|
+
probe: {
|
|
1587
|
+
title: nonHeadingTitle(pre),
|
|
1588
|
+
type: "to-do",
|
|
1589
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1590
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1591
|
+
...(pre.target !== null && {
|
|
1592
|
+
repeating: {
|
|
1593
|
+
sourceUuid: pre.target.uuid,
|
|
1594
|
+
fingerprint: buildRepeatingFingerprint(pre.target),
|
|
1595
|
+
},
|
|
1596
|
+
}),
|
|
1597
|
+
},
|
|
1598
|
+
assert: [{ field: "repeating.isTemplate", equals: true }],
|
|
1599
|
+
};
|
|
1600
|
+
},
|
|
1601
|
+
compile(params, vector) {
|
|
1602
|
+
if (vector !== "ui")
|
|
1603
|
+
unsupportedVector(this.op, vector);
|
|
1604
|
+
return uiDrive(makeRepeatingRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
|
|
1605
|
+
},
|
|
1606
|
+
};
|
|
1607
|
+
const todoRescheduleRepeat = {
|
|
1608
|
+
op: "todo.reschedule-repeat",
|
|
1609
|
+
hazards: UI_HAZARDS,
|
|
1610
|
+
preRead(db, params) {
|
|
1611
|
+
assertRepeatRule(params);
|
|
1612
|
+
const pre = emptyPreState();
|
|
1613
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1614
|
+
return pre;
|
|
1615
|
+
},
|
|
1616
|
+
expectedDelta(_pre, params) {
|
|
1617
|
+
// Identity PRESERVED (UI2-b): the same template uuid, rule mutated in
|
|
1618
|
+
// place. Assert the decoded rule's frequency + interval; ALSO capture the
|
|
1619
|
+
// whole prior rule (+ deadline flag) so the undo can re-drive it faithfully.
|
|
1620
|
+
return {
|
|
1621
|
+
mode: "update",
|
|
1622
|
+
uuid: params.uuid,
|
|
1623
|
+
assert: [
|
|
1624
|
+
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1625
|
+
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1626
|
+
],
|
|
1627
|
+
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
1628
|
+
};
|
|
1629
|
+
},
|
|
1630
|
+
compile(params, vector) {
|
|
1631
|
+
if (vector !== "ui")
|
|
1632
|
+
unsupportedVector(this.op, vector);
|
|
1633
|
+
return uiDrive(rescheduleRepeatRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
|
|
1634
|
+
},
|
|
1635
|
+
};
|
|
1636
|
+
const todoPauseRepeat = {
|
|
1637
|
+
op: "todo.pause-repeat",
|
|
1638
|
+
hazards: UI_HAZARDS,
|
|
1639
|
+
preRead(db, params) {
|
|
1640
|
+
const pre = emptyPreState();
|
|
1641
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1642
|
+
return pre;
|
|
1643
|
+
},
|
|
1644
|
+
expectedDelta(_pre, params) {
|
|
1645
|
+
// Pause (UI2-c): rt1_instanceCreationPaused → 1; identity preserved.
|
|
1646
|
+
return {
|
|
1647
|
+
mode: "update",
|
|
1648
|
+
uuid: params.uuid,
|
|
1649
|
+
assert: [{ field: "repeating.paused", equals: true }],
|
|
1650
|
+
};
|
|
1651
|
+
},
|
|
1652
|
+
compile(params, vector) {
|
|
1653
|
+
if (vector !== "ui")
|
|
1654
|
+
unsupportedVector(this.op, vector);
|
|
1655
|
+
return uiDrive(pauseRepeatRecipe(params.uuid));
|
|
1656
|
+
},
|
|
1657
|
+
};
|
|
1658
|
+
const todoResumeRepeat = {
|
|
1659
|
+
op: "todo.resume-repeat",
|
|
1660
|
+
hazards: UI_HAZARDS,
|
|
1661
|
+
preRead(db, params) {
|
|
1662
|
+
const pre = emptyPreState();
|
|
1663
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1664
|
+
return pre;
|
|
1665
|
+
},
|
|
1666
|
+
expectedDelta(_pre, params) {
|
|
1667
|
+
// Resume (UI2-c): rt1_instanceCreationPaused → 0; identity preserved.
|
|
1668
|
+
return {
|
|
1669
|
+
mode: "update",
|
|
1670
|
+
uuid: params.uuid,
|
|
1671
|
+
assert: [{ field: "repeating.paused", equals: false }],
|
|
1672
|
+
};
|
|
1673
|
+
},
|
|
1674
|
+
compile(params, vector) {
|
|
1675
|
+
if (vector !== "ui")
|
|
1676
|
+
unsupportedVector(this.op, vector);
|
|
1677
|
+
return uiDrive(resumeRepeatRecipe(params.uuid));
|
|
1678
|
+
},
|
|
1679
|
+
};
|
|
1680
|
+
// ------------------------------------------------- repeating-PROJECT ops
|
|
1681
|
+
// The project analogs of reschedule/pause/resume. Same recurrence codec and
|
|
1682
|
+
// Repeat dialog as the to-do ops (identical DB deltas), reached through the
|
|
1683
|
+
// project view's always-visible repeat bar (UIC2). No project.stop-repeat is
|
|
1684
|
+
// built — the project Stop then selecting the demoted project crashes Things
|
|
1685
|
+
// (CRASH1 / oddities §7 C5).
|
|
1686
|
+
const projectRescheduleRepeat = {
|
|
1687
|
+
op: "project.reschedule-repeat",
|
|
1688
|
+
hazards: UI_HAZARDS,
|
|
1689
|
+
preRead(db, params) {
|
|
1690
|
+
assertRepeatRule(params);
|
|
1691
|
+
const pre = emptyPreState();
|
|
1692
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1693
|
+
return pre;
|
|
1694
|
+
},
|
|
1695
|
+
expectedDelta(_pre, params) {
|
|
1696
|
+
// Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
|
|
1697
|
+
// capture the prior rule (+ deadline flag) for the faithful undo.
|
|
1698
|
+
return {
|
|
1699
|
+
mode: "update",
|
|
1700
|
+
uuid: params.uuid,
|
|
1701
|
+
assert: [
|
|
1702
|
+
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1703
|
+
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1704
|
+
],
|
|
1705
|
+
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
1706
|
+
};
|
|
1707
|
+
},
|
|
1708
|
+
compile(params, vector) {
|
|
1709
|
+
if (vector !== "ui")
|
|
1710
|
+
unsupportedVector(this.op, vector);
|
|
1711
|
+
return uiDrive(projectRescheduleRepeatRecipe(params.uuid, params.frequency, params.interval, ruleExtras(params)));
|
|
1712
|
+
},
|
|
1713
|
+
};
|
|
1714
|
+
const projectPauseRepeat = {
|
|
1715
|
+
op: "project.pause-repeat",
|
|
1716
|
+
hazards: UI_HAZARDS,
|
|
1717
|
+
preRead(db, params) {
|
|
1718
|
+
const pre = emptyPreState();
|
|
1719
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1720
|
+
return pre;
|
|
1721
|
+
},
|
|
1722
|
+
expectedDelta(_pre, params) {
|
|
1723
|
+
// Pause (UIC2-a): rt1_instanceCreationPaused → 1; identity preserved.
|
|
1724
|
+
return {
|
|
1725
|
+
mode: "update",
|
|
1726
|
+
uuid: params.uuid,
|
|
1727
|
+
assert: [{ field: "repeating.paused", equals: true }],
|
|
1728
|
+
};
|
|
1729
|
+
},
|
|
1730
|
+
compile(params, vector) {
|
|
1731
|
+
if (vector !== "ui")
|
|
1732
|
+
unsupportedVector(this.op, vector);
|
|
1733
|
+
return uiDrive(projectPauseRepeatRecipe(params.uuid));
|
|
1734
|
+
},
|
|
1735
|
+
};
|
|
1736
|
+
const projectResumeRepeat = {
|
|
1737
|
+
op: "project.resume-repeat",
|
|
1738
|
+
hazards: UI_HAZARDS,
|
|
1739
|
+
preRead(db, params) {
|
|
1740
|
+
const pre = emptyPreState();
|
|
1741
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1742
|
+
return pre;
|
|
1743
|
+
},
|
|
1744
|
+
expectedDelta(_pre, params) {
|
|
1745
|
+
// Resume (UIC2-a): rt1_instanceCreationPaused → 0; identity preserved.
|
|
1746
|
+
return {
|
|
1747
|
+
mode: "update",
|
|
1748
|
+
uuid: params.uuid,
|
|
1749
|
+
assert: [{ field: "repeating.paused", equals: false }],
|
|
1750
|
+
};
|
|
1751
|
+
},
|
|
1752
|
+
compile(params, vector) {
|
|
1753
|
+
if (vector !== "ui")
|
|
1754
|
+
unsupportedVector(this.op, vector);
|
|
1755
|
+
return uiDrive(projectResumeRepeatRecipe(params.uuid));
|
|
1756
|
+
},
|
|
1757
|
+
};
|
|
1758
|
+
// project.make-repeating: the pure-AX row-selection op (UIC4). The command
|
|
1759
|
+
// spec drives the area / someday cases; the area-less-anytime coercion is done
|
|
1760
|
+
// by the runMakeRepeatingProject orchestrator BEFORE this spec sees the target,
|
|
1761
|
+
// so by drive time the taxonomy is always area or someday. A DIRECT dispatch on
|
|
1762
|
+
// an anytime project is refused by H-PROJECT-REPEAT (with the orchestrator as
|
|
1763
|
+
// the remediation).
|
|
1764
|
+
const projectMakeRepeating = {
|
|
1765
|
+
op: "project.make-repeating",
|
|
1766
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-PROJECT-REPEAT", "H-UI-DRIVE"],
|
|
1767
|
+
preRead(db, params) {
|
|
1768
|
+
assertRepeatRule(params);
|
|
1769
|
+
const pre = emptyPreState();
|
|
1770
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1771
|
+
pre.projectRepeat = classifyProjectRepeat(db, pre.target);
|
|
1772
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
|
|
1773
|
+
if (pre.target !== null)
|
|
1774
|
+
pre.repeatSubtreeUuids = projectSubtreeUuids(db, pre.target.uuid);
|
|
1775
|
+
return pre;
|
|
1776
|
+
},
|
|
1777
|
+
expectedDelta(pre, _params, ctx) {
|
|
1778
|
+
// Identity REPLACEMENT or preserve-as-instance (UIC4-b / RSIM-R): a NEW
|
|
1779
|
+
// template project (with a recurrence rule) is born; the source project is
|
|
1780
|
+
// EITHER destroyed OR (when its subtree holds a nested repeater) relinked as
|
|
1781
|
+
// the current-occurrence instance. Pick the TEMPLATE by asserting it IS one,
|
|
1782
|
+
// excluding pre-existing same-title rows; the `repeating` context hardens
|
|
1783
|
+
// discovery and derives instance + source fate + childrenReplaced.
|
|
1784
|
+
return {
|
|
1785
|
+
mode: "create",
|
|
1786
|
+
probe: {
|
|
1787
|
+
title: nonHeadingTitle(pre),
|
|
1788
|
+
type: "project",
|
|
1789
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1790
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1791
|
+
...(pre.target !== null && {
|
|
1792
|
+
repeating: {
|
|
1793
|
+
sourceUuid: pre.target.uuid,
|
|
1794
|
+
fingerprint: buildRepeatingFingerprint(pre.target),
|
|
1795
|
+
subtreeUuids: pre.repeatSubtreeUuids ?? [],
|
|
1796
|
+
},
|
|
1797
|
+
}),
|
|
1798
|
+
},
|
|
1799
|
+
assert: [{ field: "repeating.isTemplate", equals: true }],
|
|
1800
|
+
};
|
|
1801
|
+
},
|
|
1802
|
+
compile(params, vector, pre) {
|
|
1803
|
+
if (vector !== "ui")
|
|
1804
|
+
unsupportedVector(this.op, vector);
|
|
1805
|
+
const tax = pre.projectRepeat;
|
|
1806
|
+
if (tax === null || tax.kind === "refuse" || tax.kind === "anytime") {
|
|
1807
|
+
// Unreachable in practice: the guards block refuse/anytime before compile.
|
|
1808
|
+
throw new Error("project.make-repeating: no selectable-row taxonomy resolved (guard bypassed?)");
|
|
1809
|
+
}
|
|
1810
|
+
return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
|
|
1811
|
+
},
|
|
1812
|
+
};
|
|
1813
|
+
// project.create-repeating is delivered by the runCreateRepeatingProject
|
|
1814
|
+
// orchestrator (project.add THEN project.make-repeating); it has no single
|
|
1815
|
+
// atomic surface and is never dispatched directly through the pipeline.
|
|
1816
|
+
const CREATE_REPEATING_ORCHESTRATED = "project.create-repeating is delivered by the runCreateRepeatingProject orchestrator (create " +
|
|
1817
|
+
"the project, then promote it to a repeating series); it has no atomic surface and is never " +
|
|
1818
|
+
"dispatched directly through the pipeline";
|
|
1819
|
+
const projectCreateRepeating = {
|
|
1820
|
+
op: "project.create-repeating",
|
|
1821
|
+
hazards: [],
|
|
1822
|
+
preRead() {
|
|
1823
|
+
return emptyPreState();
|
|
1824
|
+
},
|
|
1825
|
+
expectedDelta() {
|
|
1826
|
+
throw new Error(CREATE_REPEATING_ORCHESTRATED);
|
|
1827
|
+
},
|
|
1828
|
+
compile() {
|
|
1829
|
+
throw new Error(CREATE_REPEATING_ORCHESTRATED);
|
|
1830
|
+
},
|
|
1831
|
+
};
|
|
1832
|
+
const todoConvertToProject = {
|
|
1833
|
+
op: "todo.convert-to-project",
|
|
1834
|
+
hazards: UI_HAZARDS,
|
|
1835
|
+
preRead(db, params) {
|
|
1836
|
+
const pre = emptyPreState();
|
|
1837
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1838
|
+
// The new row is a PROJECT (type=1) — exclude pre-existing same-title
|
|
1839
|
+
// projects, not the source to-do (a different type the probe never sees).
|
|
1840
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
|
|
1841
|
+
return pre;
|
|
1842
|
+
},
|
|
1843
|
+
expectedDelta(pre, _params, ctx) {
|
|
1844
|
+
// Identity REPLACEMENT (UI2-d): the to-do uuid dies; a NEW type=1 project
|
|
1845
|
+
// is born, notes preserved. Discover it (its uuid is returned).
|
|
1846
|
+
const target = pre.target;
|
|
1847
|
+
const notes = target !== null && target.type !== "heading" ? target.notes : "";
|
|
1848
|
+
return {
|
|
1849
|
+
mode: "create",
|
|
1850
|
+
probe: {
|
|
1851
|
+
title: nonHeadingTitle(pre),
|
|
1852
|
+
type: "project",
|
|
1853
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1854
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1855
|
+
},
|
|
1856
|
+
assert: [{ field: "notes", equals: notes }],
|
|
1857
|
+
};
|
|
1858
|
+
},
|
|
1859
|
+
compile(params, vector) {
|
|
1860
|
+
if (vector !== "ui")
|
|
1861
|
+
unsupportedVector(this.op, vector);
|
|
1862
|
+
return uiDrive(convertToProjectRecipe("todo.convert-to-project", params.uuid));
|
|
1863
|
+
},
|
|
1864
|
+
};
|
|
1865
|
+
const headingConvertToProject = {
|
|
1866
|
+
op: "heading.convert-to-project",
|
|
1867
|
+
hazards: UI_HAZARDS,
|
|
1868
|
+
preRead(db, params) {
|
|
1869
|
+
const pre = emptyPreState();
|
|
1870
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1871
|
+
// The new row is a PROJECT (type=1) carrying the former heading's title;
|
|
1872
|
+
// exclude pre-existing same-title projects. The heading uses its raw title
|
|
1873
|
+
// (headings are not covered by nonHeadingTitle).
|
|
1874
|
+
const title = pre.target !== null ? pre.target.title : "";
|
|
1875
|
+
pre.sameTitleUuids = sameTitleTaskUuids(db, title, "project");
|
|
1876
|
+
pre.headingConvert = classifyHeadingConvert(db, pre.target);
|
|
1877
|
+
return pre;
|
|
1878
|
+
},
|
|
1879
|
+
expectedDelta(pre, _params, ctx) {
|
|
1880
|
+
// Identity REPLACEMENT (UI2-d): the heading uuid dies; a NEW type=1
|
|
1881
|
+
// project is born (promoted into the parent project's area, children
|
|
1882
|
+
// reparented). Discover the new project by its (former heading) title.
|
|
1883
|
+
const target = pre.target;
|
|
1884
|
+
const title = target !== null ? target.title : "";
|
|
1885
|
+
return {
|
|
1886
|
+
mode: "create",
|
|
1887
|
+
probe: {
|
|
1888
|
+
title,
|
|
1889
|
+
type: "project",
|
|
1890
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1891
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1892
|
+
},
|
|
1893
|
+
assert: [],
|
|
1894
|
+
};
|
|
1895
|
+
},
|
|
1896
|
+
compile(_params, vector, pre) {
|
|
1897
|
+
if (vector !== "ui")
|
|
1898
|
+
unsupportedVector(this.op, vector);
|
|
1899
|
+
const tax = pre.headingConvert;
|
|
1900
|
+
if (tax === null || tax.kind === "refuse") {
|
|
1901
|
+
// H-UNKNOWN-DESTINATION blocks a non-heading target before compile; a
|
|
1902
|
+
// refuse here means a parentless heading (or one absent from its project's
|
|
1903
|
+
// heading set) slipped past — fail loud rather than drive the wrong row.
|
|
1904
|
+
throw new Error(`heading.convert-to-project: ${tax?.kind === "refuse" ? tax.detail : "no heading taxonomy resolved (guard bypassed?)"}`);
|
|
1905
|
+
}
|
|
1906
|
+
return uiDrive(headingConvertToProjectRecipe(tax.projectReveal, tax.ordinal));
|
|
1907
|
+
},
|
|
1908
|
+
};
|
|
1909
|
+
// -------------------------------------------------- sidebar AREA reorder
|
|
1910
|
+
/** The compile-time placement for area.reorder (resolved refs). */
|
|
1911
|
+
function sidebarPlacementOf(params, pre) {
|
|
1912
|
+
const ref = pre.destArea?.resolved;
|
|
1913
|
+
if (params.before !== undefined && ref != null) {
|
|
1914
|
+
return { kind: "before", uuid: ref.uuid, title: ref.title };
|
|
1915
|
+
}
|
|
1916
|
+
if (params.after !== undefined && ref != null) {
|
|
1917
|
+
return { kind: "after", uuid: ref.uuid, title: ref.title };
|
|
1918
|
+
}
|
|
1919
|
+
return { kind: params.position === "first" ? "first" : "last" };
|
|
1920
|
+
}
|
|
1921
|
+
const areaReorderSidebar = {
|
|
1922
|
+
op: "area.reorder",
|
|
1923
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"],
|
|
1924
|
+
preRead(db, params) {
|
|
1925
|
+
const destinations = [params.before, params.after, params.position].filter((d) => d !== undefined);
|
|
1926
|
+
if (destinations.length !== 1) {
|
|
1927
|
+
throw new RangeError("pass exactly one destination: before, after, or position (first | last)");
|
|
1928
|
+
}
|
|
1929
|
+
if (params.position !== undefined &&
|
|
1930
|
+
params.position !== "first" &&
|
|
1931
|
+
params.position !== "last") {
|
|
1932
|
+
throw new RangeError(`invalid position "${params.position}" — expected first | last`);
|
|
1933
|
+
}
|
|
1934
|
+
const pre = emptyPreState();
|
|
1935
|
+
pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
|
|
1936
|
+
const ref = params.before ?? params.after;
|
|
1937
|
+
if (ref !== undefined)
|
|
1938
|
+
pre.destArea = resolveArea(db, { uuid: ref, title: ref });
|
|
1939
|
+
const target = pre.entityTarget.resolved;
|
|
1940
|
+
const dest = pre.destArea?.resolved;
|
|
1941
|
+
if (target != null && dest != null && target.uuid === dest.uuid) {
|
|
1942
|
+
throw new RangeError("the destination area is the area being moved");
|
|
1943
|
+
}
|
|
1944
|
+
// The sidebar drag addresses rows by their VISIBLE NAME — a duplicated
|
|
1945
|
+
// area title makes the row ambiguous, so refuse up front.
|
|
1946
|
+
for (const area of [target, dest]) {
|
|
1947
|
+
if (area == null)
|
|
1948
|
+
continue;
|
|
1949
|
+
const dup = db
|
|
1950
|
+
.prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
|
|
1951
|
+
.get(area.title);
|
|
1952
|
+
if (dup.n > 1) {
|
|
1953
|
+
throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas — the sidebar move addresses ` +
|
|
1954
|
+
"areas by their visible name; rename one first");
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
|
|
1958
|
+
return pre;
|
|
1959
|
+
},
|
|
1960
|
+
expectedDelta(pre, params) {
|
|
1961
|
+
const target = pre.entityTarget?.resolved?.uuid ?? "";
|
|
1962
|
+
const order = (pre.areaOrder ?? []).filter((u) => u !== target);
|
|
1963
|
+
const refUuid = pre.destArea?.resolved?.uuid;
|
|
1964
|
+
// Assert the implicated RELATIVE pair (index VALUES are never asserted —
|
|
1965
|
+
// a drag may renumber a neighbor, AXDRAG1-a); capture the FULL pre-order
|
|
1966
|
+
// for undo.
|
|
1967
|
+
let sequence;
|
|
1968
|
+
if (params.before !== undefined && refUuid !== undefined) {
|
|
1969
|
+
sequence = [target, refUuid];
|
|
1970
|
+
}
|
|
1971
|
+
else if (params.after !== undefined && refUuid !== undefined) {
|
|
1972
|
+
sequence = [refUuid, target];
|
|
1973
|
+
}
|
|
1974
|
+
else if (params.position === "first") {
|
|
1975
|
+
const first = order[0];
|
|
1976
|
+
sequence = first === undefined ? [target] : [target, first];
|
|
1977
|
+
}
|
|
1978
|
+
else {
|
|
1979
|
+
const last = order.at(-1);
|
|
1980
|
+
sequence = last === undefined ? [target] : [last, target];
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
mode: "ordering",
|
|
1984
|
+
key: "area-index",
|
|
1985
|
+
sequence,
|
|
1986
|
+
capture: pre.areaOrder ?? sequence,
|
|
1987
|
+
subject: target,
|
|
1988
|
+
};
|
|
1989
|
+
},
|
|
1990
|
+
compile(params, vector, pre) {
|
|
1991
|
+
if (vector !== "ui")
|
|
1992
|
+
unsupportedVector(this.op, vector);
|
|
1993
|
+
const target = pre.entityTarget?.resolved;
|
|
1994
|
+
return uiDrive(areaReorderSidebarRecipe({ uuid: target?.uuid ?? "", title: target?.title ?? "" }, sidebarPlacementOf(params, pre)));
|
|
1995
|
+
},
|
|
1996
|
+
};
|
|
1456
1997
|
const trashEmpty = {
|
|
1457
1998
|
op: "trash.empty",
|
|
1458
1999
|
hazards: ["H-PERMANENT-DELETE"],
|
|
@@ -1508,5 +2049,17 @@ export const COMMANDS = {
|
|
|
1508
2049
|
"heading.unarchive": headingUnarchive,
|
|
1509
2050
|
"heading.create": headingCreate,
|
|
1510
2051
|
"todo.clear-dated-reminder": todoClearDatedReminder,
|
|
2052
|
+
"todo.make-repeating": todoMakeRepeating,
|
|
2053
|
+
"todo.reschedule-repeat": todoRescheduleRepeat,
|
|
2054
|
+
"todo.pause-repeat": todoPauseRepeat,
|
|
2055
|
+
"todo.resume-repeat": todoResumeRepeat,
|
|
2056
|
+
"todo.convert-to-project": todoConvertToProject,
|
|
2057
|
+
"heading.convert-to-project": headingConvertToProject,
|
|
2058
|
+
"project.reschedule-repeat": projectRescheduleRepeat,
|
|
2059
|
+
"project.pause-repeat": projectPauseRepeat,
|
|
2060
|
+
"project.resume-repeat": projectResumeRepeat,
|
|
2061
|
+
"area.reorder": areaReorderSidebar,
|
|
2062
|
+
"project.make-repeating": projectMakeRepeating,
|
|
2063
|
+
"project.create-repeating": projectCreateRepeating,
|
|
1511
2064
|
};
|
|
1512
2065
|
//# sourceMappingURL=commands.js.map
|