things-api 0.10.0 → 0.12.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 +50 -13
- package/dist/audit/schema.d.ts +7 -0
- 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 +18 -11
- package/dist/cli/commands/area.js +61 -74
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -2
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +60 -0
- package/dist/cli/commands/install-skill.js +256 -0
- package/dist/cli/commands/install-skill.js.map +1 -0
- package/dist/cli/commands/mcp.js +9 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +1 -0
- package/dist/cli/commands/project.js +27 -34
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +15 -2
- package/dist/cli/commands/reads.js +144 -44
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.js +7 -2
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +38 -14
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +15 -9
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +797 -174
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +14 -3
- package/dist/cli/did-you-mean.js +26 -3
- package/dist/cli/did-you-mean.js.map +1 -1
- package/dist/cli/glyphs.d.ts +32 -2
- package/dist/cli/glyphs.js +83 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +121 -16
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +33 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +46 -0
- package/dist/cli/move-hint.js +196 -0
- package/dist/cli/move-hint.js.map +1 -0
- package/dist/cli/period.d.ts +2 -8
- package/dist/cli/period.js +44 -30
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.d.ts +43 -5
- package/dist/cli/read-driver.js +91 -15
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +19 -8
- package/dist/cli/render.js +70 -27
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +2 -2
- package/dist/cli/resolve-invocation.js +3 -3
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/skill-check.d.ts +27 -0
- package/dist/cli/skill-check.js +99 -0
- package/dist/cli/skill-check.js.map +1 -0
- package/dist/cli/skill.d.ts +70 -0
- package/dist/cli/skill.js +152 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/cli/tag-filters.d.ts +6 -2
- package/dist/cli/tag-filters.js +8 -6
- package/dist/cli/tag-filters.js.map +1 -1
- package/dist/cli/verb-hint.js +28 -3
- package/dist/cli/verb-hint.js.map +1 -1
- 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/client.d.ts +171 -27
- package/dist/client.js +279 -40
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +66 -2
- package/dist/config.js +120 -12
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +186 -28
- package/dist/contracts.js +22 -1
- package/dist/contracts.js.map +1 -1
- package/dist/diagnose.d.ts +34 -2
- package/dist/diagnose.js +36 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +25 -9
- package/dist/index.js +22 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +1157 -684
- 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 +60 -3
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +4 -2
- package/dist/model/mappers.js +66 -9
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/recurrence.d.ts +8 -1
- package/dist/model/recurrence.js.map +1 -1
- package/dist/read/area-filter.d.ts +76 -0
- package/dist/read/area-filter.js +59 -0
- package/dist/read/area-filter.js.map +1 -0
- package/dist/read/area-view.d.ts +12 -12
- package/dist/read/area-view.js +58 -34
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.d.ts +1 -1
- package/dist/read/detail.js +26 -12
- package/dist/read/detail.js.map +1 -1
- 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 +18 -0
- package/dist/read/predicates.js +19 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +32 -16
- package/dist/read/project-view.js +80 -45
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/pseudo-area.d.ts +29 -0
- package/dist/read/pseudo-area.js +27 -0
- package/dist/read/pseudo-area.js.map +1 -0
- package/dist/read/queries.d.ts +108 -19
- package/dist/read/queries.js +212 -35
- package/dist/read/queries.js.map +1 -1
- package/dist/read/scope.d.ts +126 -0
- package/dist/read/scope.js +162 -0
- package/dist/read/scope.js.map +1 -0
- package/dist/read/search-rank.d.ts +2 -5
- package/dist/read/shape.d.ts +162 -0
- package/dist/read/shape.js +686 -0
- package/dist/read/shape.js.map +1 -0
- package/dist/read/show-target.d.ts +9 -1
- package/dist/read/show-target.js +45 -4
- package/dist/read/show-target.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/stage.d.ts +199 -0
- package/dist/read/stage.js +125 -0
- package/dist/read/stage.js.map +1 -0
- package/dist/read/truncation.d.ts +8 -6
- package/dist/read/truncation.js +35 -10
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +61 -20
- package/dist/read/views.js +240 -82
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +9 -0
- package/dist/surface-copy.js +9 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/accessibility-probe.d.ts +1 -1
- package/dist/write/accessibility-probe.js +8 -0
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.d.ts +1 -1
- package/dist/write/automation-probe.js +8 -0
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -70
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.d.ts +1 -1
- package/dist/write/commands.js +316 -66
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +94 -10
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.d.ts +10 -1
- package/dist/write/heading.js +35 -5
- package/dist/write/heading.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +2 -2
- package/dist/write/make-repeating-project.js +9 -9
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/move.d.ts +130 -0
- package/dist/write/move.js +1533 -0
- package/dist/write/move.js.map +1 -0
- package/dist/write/operations.d.ts +105 -19
- package/dist/write/operations.js +56 -7
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +31 -0
- package/dist/write/opid.js +30 -0
- package/dist/write/opid.js.map +1 -0
- package/dist/write/pipeline.d.ts +122 -3
- package/dist/write/pipeline.js +309 -52
- 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 +197 -6
- package/dist/write/pre-state.js +427 -20
- 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/reorder.d.ts +23 -3
- package/dist/write/reorder.js +1871 -85
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/reversibility.js +19 -7
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.d.ts +31 -0
- package/dist/write/scope-guard.js +162 -0
- package/dist/write/scope-guard.js.map +1 -0
- package/dist/write/undo.js +120 -13
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +16 -7
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +5 -7
- package/dist/write/vectors/registry.js +49 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +1 -1
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +44 -0
- package/dist/write/vectors/simulator.js +1192 -0
- package/dist/write/vectors/simulator.js.map +1 -0
- package/dist/write/vectors/types.d.ts +46 -0
- package/dist/write/vectors/ui-certification.d.ts +10 -4
- package/dist/write/vectors/ui-certification.js +43 -10
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-drag.d.ts +21 -1
- package/dist/write/vectors/ui-drag.js +138 -27
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +34 -1
- package/dist/write/vectors/ui-recipes.js +217 -6
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +40 -1
- package/dist/write/vectors/ui.js +196 -39
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +127 -2
- package/dist/write/verify/delta.js +255 -16
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +7 -1
- package/dist/write/verify/poller.js +20 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +24 -3
- package/schema/envelope.schema.json +383 -0
- package/skills/things-cli/SKILL.md +70 -0
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +62 -0
- package/skills/things-cli/references/errors.md +49 -0
- package/skills/things-cli/references/gui.md +11 -0
- package/skills/things-cli/references/model.md +60 -0
- package/skills/things-cli/references/ordering.md +71 -0
package/dist/write/commands.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { decodeReminderTime, encodeReminderTime, reminderUrlToken, } from "../model/dates.js";
|
|
1
|
+
import { decodeReminderTime, encodeReminderTime, localToday, reminderUrlToken, } from "../model/dates.js";
|
|
2
2
|
import { byUuid } from "../read/detail.js";
|
|
3
|
-
import {
|
|
3
|
+
import { isLooseRef } from "../read/pseudo-area.js";
|
|
4
|
+
import { reminderIsLive } from "../read/stage.js";
|
|
5
|
+
import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
|
|
4
6
|
import { resolveTagRefs } from "./tag-refs.js";
|
|
5
7
|
import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
|
|
6
8
|
import { assertRepeatRule } from "./repeat-rule.js";
|
|
7
9
|
import { escapeAppleScript } from "./vectors/applescript.js";
|
|
8
|
-
import { areaReorderSidebarRecipe, convertToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
|
|
10
|
+
import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
|
|
11
|
+
import { buildRepeatingFingerprint } from "./verify/delta.js";
|
|
9
12
|
// ------------------------------------------------------------------ helpers
|
|
10
13
|
function thingsUrl(command, params, token) {
|
|
11
14
|
const parts = [];
|
|
@@ -152,6 +155,12 @@ const todoAdd = {
|
|
|
152
155
|
"H-REMINDER-SCOPE",
|
|
153
156
|
],
|
|
154
157
|
preRead(db, params) {
|
|
158
|
+
if (containerGiven(params.project) && containerGiven(params.area)) {
|
|
159
|
+
throw new RangeError("project and area are exclusive destinations");
|
|
160
|
+
}
|
|
161
|
+
if (params.heading !== undefined && !containerGiven(params.project)) {
|
|
162
|
+
throw new RangeError("heading requires a project destination");
|
|
163
|
+
}
|
|
155
164
|
const pre = emptyPreState();
|
|
156
165
|
if (containerGiven(params.project)) {
|
|
157
166
|
pre.destProject = resolveProject(db, params.project);
|
|
@@ -229,6 +238,14 @@ const todoAdd = {
|
|
|
229
238
|
* `when=` CLEARS an existing reminder (R07/R20), so when the caller
|
|
230
239
|
* re-schedules to today/evening/a date without addressing the reminder we
|
|
231
240
|
* auto-preserve the current one; an explicit null is the intentional clear.
|
|
241
|
+
*
|
|
242
|
+
* §9n: a reminder whose row's `startDate` is already strictly PAST is
|
|
243
|
+
* presentation-dead (the GUI hides its bell; the byte lingers in the DB). We do
|
|
244
|
+
* NOT auto-preserve such a stale byte — carrying it into the re-schedule would
|
|
245
|
+
* RESURRECT a reminder the user believes gone. A LIVE reminder (startDate
|
|
246
|
+
* today/future) is preserved as before. The liveness test is the SAME
|
|
247
|
+
* {@link reminderIsLive} predicate the read side gates on, consulted against the
|
|
248
|
+
* target's CURRENT `startDate` under the response clock.
|
|
232
249
|
*/
|
|
233
250
|
function effectiveReminder(pre, params) {
|
|
234
251
|
if (params.reminder !== undefined)
|
|
@@ -242,6 +259,8 @@ function effectiveReminder(pre, params) {
|
|
|
242
259
|
const target = pre.target;
|
|
243
260
|
if (target === null || target.type === "heading")
|
|
244
261
|
return null;
|
|
262
|
+
if (!reminderIsLive(target.startDate, pre.todayIso))
|
|
263
|
+
return null;
|
|
245
264
|
return target.reminder;
|
|
246
265
|
}
|
|
247
266
|
function assertNotesModesExclusive(params) {
|
|
@@ -268,9 +287,10 @@ function expectedNotes(pre, params) {
|
|
|
268
287
|
const todoUpdate = {
|
|
269
288
|
op: "todo.update",
|
|
270
289
|
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
|
|
271
|
-
preRead(db, params) {
|
|
290
|
+
preRead(db, params, now) {
|
|
272
291
|
assertNotesModesExclusive(params);
|
|
273
292
|
const pre = emptyPreState();
|
|
293
|
+
pre.todayIso = localToday(now);
|
|
274
294
|
pre.target = loadTarget(db, params.uuid);
|
|
275
295
|
return pre;
|
|
276
296
|
},
|
|
@@ -344,11 +364,21 @@ const todoMove = {
|
|
|
344
364
|
],
|
|
345
365
|
preRead(db, params) {
|
|
346
366
|
const container = containerGiven(params.project) || containerGiven(params.area) || params.heading !== undefined;
|
|
347
|
-
|
|
348
|
-
|
|
367
|
+
const detachFamily = params.loose === true || params.noHeading === true;
|
|
368
|
+
if (params.inbox === true && (container || detachFamily)) {
|
|
369
|
+
throw new RangeError("inbox is exclusive with project/area/heading and --no-heading/--loose");
|
|
349
370
|
}
|
|
350
|
-
if (params.
|
|
351
|
-
throw new RangeError("
|
|
371
|
+
if (params.loose === true && (container || params.noHeading === true)) {
|
|
372
|
+
throw new RangeError("--loose is exclusive with project/area/heading and --no-heading");
|
|
373
|
+
}
|
|
374
|
+
if (params.noHeading === true && container) {
|
|
375
|
+
throw new RangeError("--no-heading is exclusive with project/area/heading destinations");
|
|
376
|
+
}
|
|
377
|
+
if (containerGiven(params.project) && containerGiven(params.area)) {
|
|
378
|
+
throw new RangeError("project and area are exclusive destinations");
|
|
379
|
+
}
|
|
380
|
+
if (params.heading !== undefined && !containerGiven(params.project)) {
|
|
381
|
+
throw new RangeError("heading requires a project destination");
|
|
352
382
|
}
|
|
353
383
|
const pre = emptyPreState();
|
|
354
384
|
pre.target = loadTarget(db, params.uuid);
|
|
@@ -361,6 +391,16 @@ const todoMove = {
|
|
|
361
391
|
}
|
|
362
392
|
}
|
|
363
393
|
}
|
|
394
|
+
// --no-heading re-asserts the CURRENT project as the container: resolve the
|
|
395
|
+
// target's own project (direct or via its heading) so compile/delta pin it.
|
|
396
|
+
if (params.noHeading === true) {
|
|
397
|
+
const t = pre.target;
|
|
398
|
+
const current = t !== null && t.type === "to-do" ? (t.project ?? t.headingProject ?? null) : null;
|
|
399
|
+
if (current !== null) {
|
|
400
|
+
pre.destProject = { resolved: { uuid: current.uuid, title: current.title }, matches: 1 };
|
|
401
|
+
pre.destProjectStatus = projectStatus(db, current.uuid);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
364
404
|
if (containerGiven(params.area))
|
|
365
405
|
pre.destArea = resolveArea(db, params.area);
|
|
366
406
|
return pre;
|
|
@@ -372,7 +412,7 @@ const todoMove = {
|
|
|
372
412
|
assert.push({ field: "start", equals: "inbox" }, { field: "startDate", equals: null });
|
|
373
413
|
return { mode: "update", uuid: params.uuid, assert };
|
|
374
414
|
}
|
|
375
|
-
if (params.
|
|
415
|
+
if (params.loose === true) {
|
|
376
416
|
// P21/P22: empty list-id strips every container link; the schedule is
|
|
377
417
|
// untouched (pin it — a silent de-schedule would be a contrary write).
|
|
378
418
|
const target = pre.target;
|
|
@@ -380,6 +420,16 @@ const todoMove = {
|
|
|
380
420
|
assert.push({ field: "project", equals: null }, { field: "area", equals: null }, { field: "heading", equals: null }, { field: "startDate", equals: startDate });
|
|
381
421
|
return { mode: "update", uuid: params.uuid, assert };
|
|
382
422
|
}
|
|
423
|
+
if (params.noHeading === true) {
|
|
424
|
+
// Leave the heading, keep the project: re-assert the current project as
|
|
425
|
+
// the container with no heading (lands in the unheaded block).
|
|
426
|
+
const project = pre.destProject?.resolved;
|
|
427
|
+
assert.push({ field: "heading", equals: null });
|
|
428
|
+
if (project !== undefined && project !== null) {
|
|
429
|
+
assert.push({ field: "project.uuid", equals: project.uuid });
|
|
430
|
+
}
|
|
431
|
+
return { mode: "update", uuid: params.uuid, assert };
|
|
432
|
+
}
|
|
383
433
|
const heading = pre.destHeading?.resolved;
|
|
384
434
|
const project = pre.destProject?.resolved;
|
|
385
435
|
const area = pre.destArea?.resolved;
|
|
@@ -402,13 +452,20 @@ const todoMove = {
|
|
|
402
452
|
unsupportedVector(this.op, vector);
|
|
403
453
|
return osa(`move to do id ${q(params.uuid)} to list "Inbox"`);
|
|
404
454
|
}
|
|
405
|
-
if (params.
|
|
455
|
+
if (params.loose === true) {
|
|
406
456
|
// Empty list-id = clear the container (P21/P22) — URL only; the other
|
|
407
457
|
// vectors reject or silently ignore container removal (P10/P11, P26).
|
|
408
458
|
if (vector !== "url-scheme")
|
|
409
459
|
unsupportedVector(this.op, vector);
|
|
410
460
|
return thingsUrl("update", { id: params.uuid, "list-id": "" }, ctx.token);
|
|
411
461
|
}
|
|
462
|
+
if (params.noHeading === true) {
|
|
463
|
+
// Re-assert the CURRENT project as the list with no heading param — the
|
|
464
|
+
// to-do drops its heading FK and lands in the project's unheaded block.
|
|
465
|
+
if (vector !== "url-scheme")
|
|
466
|
+
unsupportedVector(this.op, vector);
|
|
467
|
+
return thingsUrl("update", { id: params.uuid, "list-id": pre.destProject?.resolved?.uuid ?? "" }, ctx.token);
|
|
468
|
+
}
|
|
412
469
|
const project = pre.destProject?.resolved;
|
|
413
470
|
const area = pre.destArea?.resolved;
|
|
414
471
|
if (vector === "url-scheme") {
|
|
@@ -591,9 +648,10 @@ const projectAdd = {
|
|
|
591
648
|
const projectUpdate = {
|
|
592
649
|
op: "project.update",
|
|
593
650
|
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
|
|
594
|
-
preRead(db, params) {
|
|
651
|
+
preRead(db, params, now) {
|
|
595
652
|
assertNotesModesExclusive(params);
|
|
596
653
|
const pre = emptyPreState();
|
|
654
|
+
pre.todayIso = localToday(now);
|
|
597
655
|
pre.target = loadTarget(db, params.uuid);
|
|
598
656
|
return pre;
|
|
599
657
|
},
|
|
@@ -667,8 +725,8 @@ const projectMove = {
|
|
|
667
725
|
op: "project.move",
|
|
668
726
|
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE"],
|
|
669
727
|
preRead(db, params) {
|
|
670
|
-
if ((params.
|
|
671
|
-
throw new RangeError("project.move needs exactly one of area /
|
|
728
|
+
if ((params.noArea === true) === containerGiven(params.area)) {
|
|
729
|
+
throw new RangeError("project.move needs exactly one of area / --no-area");
|
|
672
730
|
}
|
|
673
731
|
const pre = emptyPreState();
|
|
674
732
|
pre.target = loadTarget(db, params.uuid);
|
|
@@ -683,7 +741,7 @@ const projectMove = {
|
|
|
683
741
|
mode: "update",
|
|
684
742
|
uuid: params.uuid,
|
|
685
743
|
assert: [
|
|
686
|
-
params.
|
|
744
|
+
params.noArea === true
|
|
687
745
|
? { field: "area", equals: null }
|
|
688
746
|
: { field: "area.uuid", equals: pre.destArea?.resolved?.uuid ?? "" },
|
|
689
747
|
],
|
|
@@ -691,14 +749,14 @@ const projectMove = {
|
|
|
691
749
|
},
|
|
692
750
|
compile(params, vector, pre, ctx) {
|
|
693
751
|
if (vector === "url-scheme") {
|
|
694
|
-
// P23 (move) / P24 (empty area-id =
|
|
752
|
+
// P23 (move) / P24 (empty area-id = leave the area — URL is the ONLY
|
|
695
753
|
// surface: AppleScript rejects missing value/"" and json-null no-ops).
|
|
696
754
|
return thingsUrl("update-project", {
|
|
697
755
|
id: params.uuid,
|
|
698
|
-
"area-id": params.
|
|
756
|
+
"area-id": params.noArea === true ? "" : (pre.destArea?.resolved?.uuid ?? ""),
|
|
699
757
|
}, ctx.token);
|
|
700
758
|
}
|
|
701
|
-
if (params.
|
|
759
|
+
if (params.noArea === true)
|
|
702
760
|
unsupportedVector(this.op, vector);
|
|
703
761
|
return osa(`set area of project id ${q(params.uuid)} to area id ` +
|
|
704
762
|
q(pre.destArea?.resolved?.uuid ?? ""));
|
|
@@ -706,7 +764,7 @@ const projectMove = {
|
|
|
706
764
|
};
|
|
707
765
|
const todoRestore = {
|
|
708
766
|
op: "todo.restore",
|
|
709
|
-
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE"],
|
|
767
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-TEMPLATE-CHILD-RESTORE", "H-REPEAT-SCHEDULE"],
|
|
710
768
|
preRead(db, params) {
|
|
711
769
|
const pre = emptyPreState();
|
|
712
770
|
pre.target = loadTarget(db, params.uuid);
|
|
@@ -940,10 +998,13 @@ const areaAdd = {
|
|
|
940
998
|
};
|
|
941
999
|
const areaDelete = {
|
|
942
1000
|
op: "area.delete",
|
|
943
|
-
hazards: ["H-UNKNOWN-DESTINATION", "H-PERMANENT-DELETE"],
|
|
1001
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-AREA-NOT-EMPTY", "H-PERMANENT-DELETE"],
|
|
944
1002
|
preRead(db, params) {
|
|
945
1003
|
const pre = emptyPreState();
|
|
946
1004
|
pre.entityTarget = resolveArea(db, { title: params.target, uuid: params.target });
|
|
1005
|
+
if (pre.entityTarget.resolved !== null) {
|
|
1006
|
+
pre.areaMembers = areaMemberCounts(db, pre.entityTarget.resolved.uuid);
|
|
1007
|
+
}
|
|
947
1008
|
return pre;
|
|
948
1009
|
},
|
|
949
1010
|
expectedDelta(pre) {
|
|
@@ -1171,7 +1232,7 @@ const reorder = {
|
|
|
1171
1232
|
preRead(db, params, now) {
|
|
1172
1233
|
const pre = emptyPreState();
|
|
1173
1234
|
let containerUuid = null;
|
|
1174
|
-
if (params.scope === "project"
|
|
1235
|
+
if (params.scope === "project") {
|
|
1175
1236
|
pre.destProject = resolveProject(db, params.container ?? {});
|
|
1176
1237
|
containerUuid = pre.destProject.resolved?.uuid ?? null;
|
|
1177
1238
|
}
|
|
@@ -1179,6 +1240,20 @@ const reorder = {
|
|
|
1179
1240
|
pre.destArea = resolveArea(db, params.container ?? {});
|
|
1180
1241
|
containerUuid = pre.destArea.resolved?.uuid ?? null;
|
|
1181
1242
|
}
|
|
1243
|
+
if (params.scope === "container-day") {
|
|
1244
|
+
// The container may be a project OR an area (DAYORD-b); resolve whichever
|
|
1245
|
+
// the ref names so the compile can pick the right `project id`/`area id`
|
|
1246
|
+
// specifier. The planner always passes a resolved uuid.
|
|
1247
|
+
const asProject = resolveProject(db, params.container ?? {});
|
|
1248
|
+
if (asProject.resolved !== null) {
|
|
1249
|
+
pre.destProject = asProject;
|
|
1250
|
+
containerUuid = asProject.resolved.uuid;
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
pre.destArea = resolveArea(db, params.container ?? {});
|
|
1254
|
+
containerUuid = pre.destArea.resolved?.uuid ?? null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1182
1257
|
pre.reorder = computeReorderPre(db, params, containerUuid, now);
|
|
1183
1258
|
return pre;
|
|
1184
1259
|
},
|
|
@@ -1196,15 +1271,22 @@ const reorder = {
|
|
|
1196
1271
|
compile(params, vector, pre) {
|
|
1197
1272
|
if (vector !== "applescript")
|
|
1198
1273
|
unsupportedVector(this.op, vector);
|
|
1199
|
-
const
|
|
1274
|
+
const containerDaySpecifier = pre.destProject?.resolved != null
|
|
1275
|
+
? `project id ${q(pre.destProject.resolved.uuid)}`
|
|
1276
|
+
: `area id ${q(pre.destArea?.resolved?.uuid ?? "")}`;
|
|
1277
|
+
const specifier = params.scope === "project"
|
|
1200
1278
|
? `project id ${q(pre.destProject?.resolved?.uuid ?? "")}`
|
|
1201
1279
|
: params.scope === "area"
|
|
1202
1280
|
? `area id ${q(pre.destArea?.resolved?.uuid ?? "")}`
|
|
1203
|
-
: params.scope === "
|
|
1204
|
-
?
|
|
1205
|
-
: params.scope === "
|
|
1206
|
-
? `list "
|
|
1207
|
-
:
|
|
1281
|
+
: params.scope === "container-day"
|
|
1282
|
+
? containerDaySpecifier
|
|
1283
|
+
: params.scope === "inbox"
|
|
1284
|
+
? `list "Inbox"`
|
|
1285
|
+
: params.scope === "someday"
|
|
1286
|
+
? `list "Someday"`
|
|
1287
|
+
: params.scope === "tomorrow"
|
|
1288
|
+
? `list "Tomorrow"`
|
|
1289
|
+
: `list "Today"`;
|
|
1208
1290
|
const wire = pre.reorder?.wireList ?? params.uuids;
|
|
1209
1291
|
if (params.scope === "someday") {
|
|
1210
1292
|
// The Someday handler STACKS each sent id above the list's current top
|
|
@@ -1352,7 +1434,7 @@ function headingChildren(db, headingUuid) {
|
|
|
1352
1434
|
return todos;
|
|
1353
1435
|
}
|
|
1354
1436
|
const headingRename = {
|
|
1355
|
-
op: "
|
|
1437
|
+
op: "project.rename-heading",
|
|
1356
1438
|
hazards: ["H-UNKNOWN-DESTINATION"],
|
|
1357
1439
|
preRead(db, params) {
|
|
1358
1440
|
const pre = emptyPreState();
|
|
@@ -1375,7 +1457,7 @@ const headingRename = {
|
|
|
1375
1457
|
},
|
|
1376
1458
|
};
|
|
1377
1459
|
const headingArchive = {
|
|
1378
|
-
op: "
|
|
1460
|
+
op: "project.archive-heading",
|
|
1379
1461
|
hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-CHILDREN"],
|
|
1380
1462
|
preRead(db, params) {
|
|
1381
1463
|
const pre = emptyPreState();
|
|
@@ -1428,7 +1510,7 @@ const headingArchive = {
|
|
|
1428
1510
|
},
|
|
1429
1511
|
};
|
|
1430
1512
|
const headingUnarchive = {
|
|
1431
|
-
op: "
|
|
1513
|
+
op: "project.unarchive-heading",
|
|
1432
1514
|
hazards: ["H-UNKNOWN-DESTINATION"],
|
|
1433
1515
|
preRead(db, params) {
|
|
1434
1516
|
const pre = emptyPreState();
|
|
@@ -1448,8 +1530,8 @@ const headingUnarchive = {
|
|
|
1448
1530
|
return osa(`set status of to do id ${q(params.uuid)} to open`);
|
|
1449
1531
|
},
|
|
1450
1532
|
};
|
|
1451
|
-
const
|
|
1452
|
-
op: "heading
|
|
1533
|
+
const headingAdd = {
|
|
1534
|
+
op: "project.add-heading",
|
|
1453
1535
|
hazards: ["H-UNKNOWN-DESTINATION"],
|
|
1454
1536
|
preRead(db, params) {
|
|
1455
1537
|
const pre = emptyPreState();
|
|
@@ -1484,6 +1566,32 @@ const headingCreate = {
|
|
|
1484
1566
|
});
|
|
1485
1567
|
},
|
|
1486
1568
|
};
|
|
1569
|
+
const projectMoveHeading = {
|
|
1570
|
+
op: "project.move-heading",
|
|
1571
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-ORDER"],
|
|
1572
|
+
preRead(db, params) {
|
|
1573
|
+
const pre = emptyPreState();
|
|
1574
|
+
pre.destProject = resolveProject(db, params.project);
|
|
1575
|
+
pre.headingMove = computeHeadingMovePre(db, pre.destProject, params.headings, params.placement);
|
|
1576
|
+
return pre;
|
|
1577
|
+
},
|
|
1578
|
+
expectedDelta(pre) {
|
|
1579
|
+
// The reorder wire re-ranks the FULL heading list; verify the whole target
|
|
1580
|
+
// order (strictly ascending "index"). Children follow their heading (scf
|
|
1581
|
+
// P1), so no per-child assertion is needed.
|
|
1582
|
+
return { mode: "ordering", key: "index", sequence: pre.headingMove?.targetOrder ?? [] };
|
|
1583
|
+
},
|
|
1584
|
+
compile(_params, vector, pre) {
|
|
1585
|
+
// The same native private-reorder wire the old `reorder --scope headings`
|
|
1586
|
+
// used (experimental — gated by allow-experimental + the sdef canary),
|
|
1587
|
+
// fed the computed full order.
|
|
1588
|
+
if (vector !== "applescript")
|
|
1589
|
+
unsupportedVector(this.op, vector);
|
|
1590
|
+
const order = pre.headingMove?.targetOrder ?? [];
|
|
1591
|
+
return osa(`${PRIVATE_REORDER_COMMAND} project id ${q(pre.destProject?.resolved?.uuid ?? "")} ` +
|
|
1592
|
+
`with ids ${q(order.join(","))}`);
|
|
1593
|
+
},
|
|
1594
|
+
};
|
|
1487
1595
|
const todoClearDatedReminder = {
|
|
1488
1596
|
op: "todo.clear-dated-reminder",
|
|
1489
1597
|
hazards: ["H-UNKNOWN-DESTINATION", "H-NO-REMINDER"],
|
|
@@ -1560,12 +1668,17 @@ const todoMakeRepeating = {
|
|
|
1560
1668
|
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
|
|
1561
1669
|
return pre;
|
|
1562
1670
|
},
|
|
1563
|
-
expectedDelta(pre,
|
|
1564
|
-
// Identity REPLACEMENT (UI2-a
|
|
1565
|
-
// template row (type=0 with a recurrence rule) is born
|
|
1566
|
-
//
|
|
1567
|
-
// excluding the pre-existing same-title
|
|
1568
|
-
// pick
|
|
1671
|
+
expectedDelta(pre, params, ctx) {
|
|
1672
|
+
// Identity REPLACEMENT or preserve-as-instance (UI2-a / RSIM-R): a NEW
|
|
1673
|
+
// template row (type=0 with a recurrence rule) is born; the original uuid is
|
|
1674
|
+
// EITHER destroyed OR relinked as the current-occurrence instance. Discover
|
|
1675
|
+
// the template with the create probe (excluding the pre-existing same-title
|
|
1676
|
+
// rows), pick it by asserting it IS a template, then the `repeating` context
|
|
1677
|
+
// hardens discovery (restored time-bound, source-fingerprint tiebreak) and
|
|
1678
|
+
// derives instance + source fate for the enriched result. `expectedRule`
|
|
1679
|
+
// makes the LANDED rule verifiable: a template minted with the wrong
|
|
1680
|
+
// frequency/interval (the interval-field race, oddities §8l) becomes a
|
|
1681
|
+
// verify-failed:mismatch instead of a silent ok.
|
|
1569
1682
|
return {
|
|
1570
1683
|
mode: "create",
|
|
1571
1684
|
probe: {
|
|
@@ -1573,6 +1686,17 @@ const todoMakeRepeating = {
|
|
|
1573
1686
|
type: "to-do",
|
|
1574
1687
|
sinceEpoch: ctx.nowEpoch - 2,
|
|
1575
1688
|
excludeUuids: pre.sameTitleUuids,
|
|
1689
|
+
...(pre.target !== null && {
|
|
1690
|
+
repeating: {
|
|
1691
|
+
sourceUuid: pre.target.uuid,
|
|
1692
|
+
fingerprint: buildRepeatingFingerprint(pre.target),
|
|
1693
|
+
expectedRule: {
|
|
1694
|
+
type: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1695
|
+
unit: params.frequency,
|
|
1696
|
+
interval: params.interval,
|
|
1697
|
+
},
|
|
1698
|
+
},
|
|
1699
|
+
}),
|
|
1576
1700
|
},
|
|
1577
1701
|
assert: [{ field: "repeating.isTemplate", equals: true }],
|
|
1578
1702
|
};
|
|
@@ -1594,12 +1718,22 @@ const todoRescheduleRepeat = {
|
|
|
1594
1718
|
},
|
|
1595
1719
|
expectedDelta(_pre, params) {
|
|
1596
1720
|
// Identity PRESERVED (UI2-b): the same template uuid, rule mutated in
|
|
1597
|
-
// place. Assert the decoded rule's frequency + interval; ALSO capture
|
|
1598
|
-
// whole prior rule (+ deadline flag) so the undo can re-drive it
|
|
1721
|
+
// place. Assert the decoded rule's TYPE + frequency + interval; ALSO capture
|
|
1722
|
+
// the whole prior rule (+ deadline flag) so the undo can re-drive it
|
|
1723
|
+
// faithfully. Asserting `type` (fixed vs after-completion) is what makes a
|
|
1724
|
+
// fixed→after-completion conversion VERIFIABLE at all: in the field-report
|
|
1725
|
+
// incident (0½ item 1) both rules were weekly/interval-2, so unit+interval
|
|
1726
|
+
// never changed — only the type flipped, and without this assertion verify
|
|
1727
|
+
// could neither confirm the conversion landed nor tell the pre-drive
|
|
1728
|
+
// idempotency check that the target had not yet been reached.
|
|
1599
1729
|
return {
|
|
1600
1730
|
mode: "update",
|
|
1601
1731
|
uuid: params.uuid,
|
|
1602
1732
|
assert: [
|
|
1733
|
+
{
|
|
1734
|
+
field: "repeating.rule.type",
|
|
1735
|
+
equals: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1736
|
+
},
|
|
1603
1737
|
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1604
1738
|
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1605
1739
|
],
|
|
@@ -1673,11 +1807,17 @@ const projectRescheduleRepeat = {
|
|
|
1673
1807
|
},
|
|
1674
1808
|
expectedDelta(_pre, params) {
|
|
1675
1809
|
// Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
|
|
1676
|
-
// capture the prior rule (+ deadline flag) for the faithful undo.
|
|
1810
|
+
// capture the prior rule (+ deadline flag) for the faithful undo. Assert the
|
|
1811
|
+
// rule TYPE too (fixed vs after-completion) so a conversion that leaves
|
|
1812
|
+
// unit+interval unchanged is still verifiable — see todo.reschedule-repeat.
|
|
1677
1813
|
return {
|
|
1678
1814
|
mode: "update",
|
|
1679
1815
|
uuid: params.uuid,
|
|
1680
1816
|
assert: [
|
|
1817
|
+
{
|
|
1818
|
+
field: "repeating.rule.type",
|
|
1819
|
+
equals: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1820
|
+
},
|
|
1681
1821
|
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1682
1822
|
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1683
1823
|
],
|
|
@@ -1749,14 +1889,18 @@ const projectMakeRepeating = {
|
|
|
1749
1889
|
pre.target = loadTarget(db, params.uuid);
|
|
1750
1890
|
pre.projectRepeat = classifyProjectRepeat(db, pre.target);
|
|
1751
1891
|
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "project");
|
|
1892
|
+
if (pre.target !== null)
|
|
1893
|
+
pre.repeatSubtreeUuids = projectSubtreeUuids(db, pre.target.uuid);
|
|
1752
1894
|
return pre;
|
|
1753
1895
|
},
|
|
1754
|
-
expectedDelta(pre,
|
|
1755
|
-
// Identity REPLACEMENT (UIC4-b):
|
|
1756
|
-
// template project (with a recurrence rule) is born
|
|
1757
|
-
//
|
|
1758
|
-
//
|
|
1759
|
-
//
|
|
1896
|
+
expectedDelta(pre, params, ctx) {
|
|
1897
|
+
// Identity REPLACEMENT or preserve-as-instance (UIC4-b / RSIM-R): a NEW
|
|
1898
|
+
// template project (with a recurrence rule) is born; the source project is
|
|
1899
|
+
// EITHER destroyed OR (when its subtree holds a nested repeater) relinked as
|
|
1900
|
+
// the current-occurrence instance. Pick the TEMPLATE by asserting it IS one,
|
|
1901
|
+
// excluding pre-existing same-title rows; the `repeating` context hardens
|
|
1902
|
+
// discovery and derives instance + source fate + childrenReplaced.
|
|
1903
|
+
// `expectedRule` makes the landed rule verifiable (interval-race guard, §8l).
|
|
1760
1904
|
return {
|
|
1761
1905
|
mode: "create",
|
|
1762
1906
|
probe: {
|
|
@@ -1764,6 +1908,18 @@ const projectMakeRepeating = {
|
|
|
1764
1908
|
type: "project",
|
|
1765
1909
|
sinceEpoch: ctx.nowEpoch - 2,
|
|
1766
1910
|
excludeUuids: pre.sameTitleUuids,
|
|
1911
|
+
...(pre.target !== null && {
|
|
1912
|
+
repeating: {
|
|
1913
|
+
sourceUuid: pre.target.uuid,
|
|
1914
|
+
fingerprint: buildRepeatingFingerprint(pre.target),
|
|
1915
|
+
subtreeUuids: pre.repeatSubtreeUuids ?? [],
|
|
1916
|
+
expectedRule: {
|
|
1917
|
+
type: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1918
|
+
unit: params.frequency,
|
|
1919
|
+
interval: params.interval,
|
|
1920
|
+
},
|
|
1921
|
+
},
|
|
1922
|
+
}),
|
|
1767
1923
|
},
|
|
1768
1924
|
assert: [{ field: "repeating.isTemplate", equals: true }],
|
|
1769
1925
|
};
|
|
@@ -1779,23 +1935,23 @@ const projectMakeRepeating = {
|
|
|
1779
1935
|
return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
|
|
1780
1936
|
},
|
|
1781
1937
|
};
|
|
1782
|
-
// project.
|
|
1938
|
+
// project.add-repeating is delivered by the runAddRepeatingProject
|
|
1783
1939
|
// orchestrator (project.add THEN project.make-repeating); it has no single
|
|
1784
1940
|
// atomic surface and is never dispatched directly through the pipeline.
|
|
1785
|
-
const
|
|
1941
|
+
const ADD_REPEATING_ORCHESTRATED = "project.add-repeating is delivered by the runAddRepeatingProject orchestrator (create " +
|
|
1786
1942
|
"the project, then promote it to a repeating series); it has no atomic surface and is never " +
|
|
1787
1943
|
"dispatched directly through the pipeline";
|
|
1788
|
-
const
|
|
1789
|
-
op: "project.
|
|
1944
|
+
const projectAddRepeating = {
|
|
1945
|
+
op: "project.add-repeating",
|
|
1790
1946
|
hazards: [],
|
|
1791
1947
|
preRead() {
|
|
1792
1948
|
return emptyPreState();
|
|
1793
1949
|
},
|
|
1794
1950
|
expectedDelta() {
|
|
1795
|
-
throw new Error(
|
|
1951
|
+
throw new Error(ADD_REPEATING_ORCHESTRATED);
|
|
1796
1952
|
},
|
|
1797
1953
|
compile() {
|
|
1798
|
-
throw new Error(
|
|
1954
|
+
throw new Error(ADD_REPEATING_ORCHESTRATED);
|
|
1799
1955
|
},
|
|
1800
1956
|
};
|
|
1801
1957
|
const todoConvertToProject = {
|
|
@@ -1832,7 +1988,7 @@ const todoConvertToProject = {
|
|
|
1832
1988
|
},
|
|
1833
1989
|
};
|
|
1834
1990
|
const headingConvertToProject = {
|
|
1835
|
-
op: "
|
|
1991
|
+
op: "project.promote-heading",
|
|
1836
1992
|
hazards: UI_HAZARDS,
|
|
1837
1993
|
preRead(db, params) {
|
|
1838
1994
|
const pre = emptyPreState();
|
|
@@ -1842,6 +1998,7 @@ const headingConvertToProject = {
|
|
|
1842
1998
|
// (headings are not covered by nonHeadingTitle).
|
|
1843
1999
|
const title = pre.target !== null ? pre.target.title : "";
|
|
1844
2000
|
pre.sameTitleUuids = sameTitleTaskUuids(db, title, "project");
|
|
2001
|
+
pre.headingConvert = classifyHeadingConvert(db, pre.target);
|
|
1845
2002
|
return pre;
|
|
1846
2003
|
},
|
|
1847
2004
|
expectedDelta(pre, _params, ctx) {
|
|
@@ -1861,10 +2018,82 @@ const headingConvertToProject = {
|
|
|
1861
2018
|
assert: [],
|
|
1862
2019
|
};
|
|
1863
2020
|
},
|
|
1864
|
-
compile(
|
|
2021
|
+
compile(_params, vector, pre) {
|
|
1865
2022
|
if (vector !== "ui")
|
|
1866
2023
|
unsupportedVector(this.op, vector);
|
|
1867
|
-
|
|
2024
|
+
const tax = pre.headingConvert;
|
|
2025
|
+
if (tax === null || tax.kind === "refuse") {
|
|
2026
|
+
// H-UNKNOWN-DESTINATION blocks a non-heading target before compile; a
|
|
2027
|
+
// refuse here means a parentless heading (or one absent from its project's
|
|
2028
|
+
// heading set) slipped past — fail loud rather than drive the wrong row.
|
|
2029
|
+
throw new Error(`project.promote-heading: ${tax?.kind === "refuse" ? tax.detail : "no heading taxonomy resolved (guard bypassed?)"}`);
|
|
2030
|
+
}
|
|
2031
|
+
return uiDrive(headingConvertToProjectRecipe(tax.projectReveal, tax.ordinal));
|
|
2032
|
+
},
|
|
2033
|
+
};
|
|
2034
|
+
const projectMoveHeadingToProject = {
|
|
2035
|
+
op: "project.move-heading-to-project",
|
|
2036
|
+
hazards: UI_HAZARDS,
|
|
2037
|
+
preRead(db, params) {
|
|
2038
|
+
const pre = emptyPreState();
|
|
2039
|
+
pre.headingMoveToProject = classifyHeadingMoveToProject(db, params.project, params.heading, params.toProject);
|
|
2040
|
+
if (pre.headingMoveToProject.kind === "ok") {
|
|
2041
|
+
// Load the heading row so the verify oracle reads its (post-op) project FK.
|
|
2042
|
+
pre.target = loadTarget(db, pre.headingMoveToProject.pre.headingUuid);
|
|
2043
|
+
}
|
|
2044
|
+
return pre;
|
|
2045
|
+
},
|
|
2046
|
+
expectedDelta(pre) {
|
|
2047
|
+
// HEADXPROJ: a single-row change — the heading's `project` FK becomes the
|
|
2048
|
+
// destination; its children follow via their intact heading FK (no child
|
|
2049
|
+
// rewrite, no index churn), so no per-child assertion is needed.
|
|
2050
|
+
const tax = pre.headingMoveToProject;
|
|
2051
|
+
const headingUuid = tax?.kind === "ok" ? tax.pre.headingUuid : "";
|
|
2052
|
+
const destUuid = tax?.kind === "ok" ? tax.pre.destProjectUuid : "";
|
|
2053
|
+
return {
|
|
2054
|
+
mode: "update",
|
|
2055
|
+
uuid: headingUuid,
|
|
2056
|
+
assert: [{ field: "project.uuid", equals: destUuid }],
|
|
2057
|
+
};
|
|
2058
|
+
},
|
|
2059
|
+
compile(_params, vector, pre) {
|
|
2060
|
+
if (vector !== "ui")
|
|
2061
|
+
unsupportedVector(this.op, vector);
|
|
2062
|
+
const tax = pre.headingMoveToProject;
|
|
2063
|
+
if (tax === null || tax.kind === "refuse") {
|
|
2064
|
+
// The taxonomy refusals are surfaced by H-UNKNOWN-DESTINATION before
|
|
2065
|
+
// compile; reaching here with one means the guard was bypassed.
|
|
2066
|
+
throw new Error(`project.move-heading-to-project: ${tax?.kind === "refuse" ? tax.detail : "no taxonomy resolved (guard bypassed?)"}`);
|
|
2067
|
+
}
|
|
2068
|
+
return uiDrive(moveHeadingToProjectRecipe(tax.pre.sourceProjectUuid, tax.pre.headingTitle, tax.pre.destProjectTitle));
|
|
2069
|
+
},
|
|
2070
|
+
};
|
|
2071
|
+
const projectDissolveHeading = {
|
|
2072
|
+
op: "project.dissolve-heading",
|
|
2073
|
+
hazards: UI_HAZARDS,
|
|
2074
|
+
preRead(db, params) {
|
|
2075
|
+
const pre = emptyPreState();
|
|
2076
|
+
pre.target = loadTarget(db, params.uuid);
|
|
2077
|
+
pre.headingDissolve = classifyHeadingDissolve(db, pre.target);
|
|
2078
|
+
return pre;
|
|
2079
|
+
},
|
|
2080
|
+
expectedDelta(_pre, params) {
|
|
2081
|
+
// DISS1: the heading row is HARD-DELETED (removed from TMTask) while its
|
|
2082
|
+
// children become direct project children. The verify oracle is the heading
|
|
2083
|
+
// GONE — its children re-home is the DISS1-locked invariant, checked by the
|
|
2084
|
+
// op's tests (a childless heading dissolve verifies identically).
|
|
2085
|
+
return { mode: "gone", entity: "task", uuid: params.uuid };
|
|
2086
|
+
},
|
|
2087
|
+
compile(_params, vector, pre) {
|
|
2088
|
+
if (vector !== "ui")
|
|
2089
|
+
unsupportedVector(this.op, vector);
|
|
2090
|
+
const tax = pre.headingDissolve;
|
|
2091
|
+
if (tax === null || tax.kind === "refuse") {
|
|
2092
|
+
// The taxonomy refusals are surfaced by H-UNKNOWN-DESTINATION before
|
|
2093
|
+
// compile; reaching here with one means the guard was bypassed.
|
|
2094
|
+
throw new Error(`project.dissolve-heading: ${tax?.kind === "refuse" ? tax.detail : "no taxonomy resolved (guard bypassed?)"}`);
|
|
2095
|
+
}
|
|
2096
|
+
return uiDrive(dissolveHeadingRecipe(tax.pre.projectReveal, tax.pre.headingTitle));
|
|
1868
2097
|
},
|
|
1869
2098
|
};
|
|
1870
2099
|
// -------------------------------------------------- sidebar AREA reorder
|
|
@@ -1892,6 +2121,16 @@ const areaReorderSidebar = {
|
|
|
1892
2121
|
params.position !== "last") {
|
|
1893
2122
|
throw new RangeError(`invalid position "${params.position}" — expected first | last`);
|
|
1894
2123
|
}
|
|
2124
|
+
// The reserved `loose` pseudo-area ref (the derived area-less view) has no
|
|
2125
|
+
// sidebar row, so it can be neither moved nor an anchor — refuse it by name
|
|
2126
|
+
// with a specific message rather than a generic no-such-area error, matching
|
|
2127
|
+
// the loose-pseudo-area write-refusal pattern (src/read/pseudo-area.ts).
|
|
2128
|
+
for (const ref of [params.target, params.before, params.after]) {
|
|
2129
|
+
if (ref !== undefined && isLooseRef(ref)) {
|
|
2130
|
+
throw new RangeError("the loose pseudo-area is a derived view — it has no sidebar row and cannot be " +
|
|
2131
|
+
"reordered");
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1895
2134
|
const pre = emptyPreState();
|
|
1896
2135
|
pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
|
|
1897
2136
|
const ref = params.before ?? params.after;
|
|
@@ -1902,17 +2141,25 @@ const areaReorderSidebar = {
|
|
|
1902
2141
|
if (target != null && dest != null && target.uuid === dest.uuid) {
|
|
1903
2142
|
throw new RangeError("the destination area is the area being moved");
|
|
1904
2143
|
}
|
|
1905
|
-
// The sidebar drag addresses rows by their VISIBLE NAME
|
|
1906
|
-
//
|
|
2144
|
+
// The sidebar drag addresses rows by their VISIBLE NAME. A DUPLICATE area
|
|
2145
|
+
// title is now handled by positional disambiguation in the ui-vector driver
|
|
2146
|
+
// (ORDFIN2 AXDRAG3: the intended uuid's Nth same-titled row is grabbed by the
|
|
2147
|
+
// `(index, uuid)` ASC law, and the post-gesture DB assert + self-invert catch
|
|
2148
|
+
// a wrong grab) — so a uuid-targeted ref whose title is shared is NO LONGER
|
|
2149
|
+
// refused here. A duplicate NAME ref stays refused upstream (resolveArea
|
|
2150
|
+
// returns the ambiguity candidates). Only a SANITY CAP remains: too many
|
|
2151
|
+
// same-titled rows could make the positional grab loop unreliable, so refuse
|
|
2152
|
+
// rather than churn. (WIRED — lab-cert pending an AXDRAG4 VM sitting.)
|
|
2153
|
+
const SAME_TITLE_CAP = 8;
|
|
1907
2154
|
for (const area of [target, dest]) {
|
|
1908
2155
|
if (area == null)
|
|
1909
2156
|
continue;
|
|
1910
2157
|
const dup = db
|
|
1911
2158
|
.prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
|
|
1912
2159
|
.get(area.title);
|
|
1913
|
-
if (dup.n >
|
|
1914
|
-
throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas
|
|
1915
|
-
"
|
|
2160
|
+
if (dup.n > SAME_TITLE_CAP) {
|
|
2161
|
+
throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas (over the ${SAME_TITLE_CAP}-row ` +
|
|
2162
|
+
"positional-disambiguation cap) — rename some, or reorder by a uniquely-titled anchor");
|
|
1916
2163
|
}
|
|
1917
2164
|
}
|
|
1918
2165
|
pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
|
|
@@ -2005,22 +2252,25 @@ export const COMMANDS = {
|
|
|
2005
2252
|
"project.set-tags": projectSetTags,
|
|
2006
2253
|
"todo.backdate": todoBackdate,
|
|
2007
2254
|
"todo.add-logged": todoAddLogged,
|
|
2008
|
-
"heading
|
|
2009
|
-
"heading
|
|
2010
|
-
"heading
|
|
2011
|
-
"heading
|
|
2255
|
+
"project.add-heading": headingAdd,
|
|
2256
|
+
"project.rename-heading": headingRename,
|
|
2257
|
+
"project.archive-heading": headingArchive,
|
|
2258
|
+
"project.unarchive-heading": headingUnarchive,
|
|
2259
|
+
"project.promote-heading": headingConvertToProject,
|
|
2260
|
+
"project.move-heading": projectMoveHeading,
|
|
2261
|
+
"project.move-heading-to-project": projectMoveHeadingToProject,
|
|
2262
|
+
"project.dissolve-heading": projectDissolveHeading,
|
|
2012
2263
|
"todo.clear-dated-reminder": todoClearDatedReminder,
|
|
2013
2264
|
"todo.make-repeating": todoMakeRepeating,
|
|
2014
2265
|
"todo.reschedule-repeat": todoRescheduleRepeat,
|
|
2015
2266
|
"todo.pause-repeat": todoPauseRepeat,
|
|
2016
2267
|
"todo.resume-repeat": todoResumeRepeat,
|
|
2017
2268
|
"todo.convert-to-project": todoConvertToProject,
|
|
2018
|
-
"heading.convert-to-project": headingConvertToProject,
|
|
2019
2269
|
"project.reschedule-repeat": projectRescheduleRepeat,
|
|
2020
2270
|
"project.pause-repeat": projectPauseRepeat,
|
|
2021
2271
|
"project.resume-repeat": projectResumeRepeat,
|
|
2022
2272
|
"area.reorder": areaReorderSidebar,
|
|
2023
2273
|
"project.make-repeating": projectMakeRepeating,
|
|
2024
|
-
"project.
|
|
2274
|
+
"project.add-repeating": projectAddRepeating,
|
|
2025
2275
|
};
|
|
2026
2276
|
//# sourceMappingURL=commands.js.map
|