things-api 0.11.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 +16 -5
- package/dist/audit/schema.d.ts +7 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/area.d.ts +18 -11
- package/dist/cli/commands/area.js +55 -67
- 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 +19 -4
- package/dist/cli/commands/install-skill.js +101 -52
- package/dist/cli/commands/install-skill.js.map +1 -1
- 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 +20 -28
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.js +100 -28
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/show.js +32 -8
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +1 -1
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +769 -143
- 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 +16 -2
- package/dist/cli/glyphs.js +60 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +67 -12
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +3 -2
- package/dist/cli/move-hint.js +2 -2
- package/dist/cli/move-hint.js.map +1 -1
- package/dist/cli/read-driver.d.ts +43 -5
- package/dist/cli/read-driver.js +81 -15
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +19 -8
- package/dist/cli/render.js +59 -25
- 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 +12 -6
- package/dist/cli/skill-check.js +31 -14
- package/dist/cli/skill-check.js.map +1 -1
- package/dist/cli/skill.d.ts +8 -0
- package/dist/cli/skill.js +10 -0
- package/dist/cli/skill.js.map +1 -1
- package/dist/cli/verb-hint.js +1 -1
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/client.d.ts +140 -19
- package/dist/client.js +253 -38
- 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 +176 -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 +19 -8
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +733 -304
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +46 -3
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +2 -0
- package/dist/model/mappers.js +41 -2
- 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 +11 -11
- package/dist/read/area-view.js +50 -28
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.js +15 -8
- package/dist/read/detail.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 +31 -15
- package/dist/read/project-view.js +72 -39
- 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 +105 -18
- package/dist/read/queries.js +185 -30
- 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/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 +7 -5
- package/dist/read/truncation.js +33 -8
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +49 -9
- package/dist/read/views.js +193 -40
- 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/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -71
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.js +266 -55
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +71 -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 +70 -3
- package/dist/write/pipeline.js +198 -33
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +162 -8
- package/dist/write/pre-state.js +385 -20
- package/dist/write/pre-state.js.map +1 -1
- 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/shortcuts.js +1 -1
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +83 -27
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +11 -0
- package/dist/write/vectors/ui-certification.d.ts +3 -3
- package/dist/write/vectors/ui-certification.js +41 -5
- 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 +21 -1
- package/dist/write/vectors/ui-recipes.js +166 -7
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +25 -1
- package/dist/write/vectors/ui.js +142 -33
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -1
- package/dist/write/verify/delta.js +106 -29
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +7 -1
- package/schema/envelope.schema.json +383 -0
- package/skills/things-cli/SKILL.md +34 -17
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +30 -9
- package/skills/things-cli/references/errors.md +49 -0
- package/skills/things-cli/references/gui.md +1 -1
- package/skills/things-cli/references/model.md +15 -6
- package/skills/things-cli/references/ordering.md +71 -0
package/dist/write/commands.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
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, headingConvertToProjectRecipe, 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";
|
|
9
11
|
import { buildRepeatingFingerprint } from "./verify/delta.js";
|
|
10
12
|
// ------------------------------------------------------------------ helpers
|
|
11
13
|
function thingsUrl(command, params, token) {
|
|
@@ -236,6 +238,14 @@ const todoAdd = {
|
|
|
236
238
|
* `when=` CLEARS an existing reminder (R07/R20), so when the caller
|
|
237
239
|
* re-schedules to today/evening/a date without addressing the reminder we
|
|
238
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.
|
|
239
249
|
*/
|
|
240
250
|
function effectiveReminder(pre, params) {
|
|
241
251
|
if (params.reminder !== undefined)
|
|
@@ -249,6 +259,8 @@ function effectiveReminder(pre, params) {
|
|
|
249
259
|
const target = pre.target;
|
|
250
260
|
if (target === null || target.type === "heading")
|
|
251
261
|
return null;
|
|
262
|
+
if (!reminderIsLive(target.startDate, pre.todayIso))
|
|
263
|
+
return null;
|
|
252
264
|
return target.reminder;
|
|
253
265
|
}
|
|
254
266
|
function assertNotesModesExclusive(params) {
|
|
@@ -275,9 +287,10 @@ function expectedNotes(pre, params) {
|
|
|
275
287
|
const todoUpdate = {
|
|
276
288
|
op: "todo.update",
|
|
277
289
|
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
|
|
278
|
-
preRead(db, params) {
|
|
290
|
+
preRead(db, params, now) {
|
|
279
291
|
assertNotesModesExclusive(params);
|
|
280
292
|
const pre = emptyPreState();
|
|
293
|
+
pre.todayIso = localToday(now);
|
|
281
294
|
pre.target = loadTarget(db, params.uuid);
|
|
282
295
|
return pre;
|
|
283
296
|
},
|
|
@@ -351,11 +364,15 @@ const todoMove = {
|
|
|
351
364
|
],
|
|
352
365
|
preRead(db, params) {
|
|
353
366
|
const container = containerGiven(params.project) || containerGiven(params.area) || params.heading !== undefined;
|
|
354
|
-
|
|
355
|
-
|
|
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");
|
|
356
370
|
}
|
|
357
|
-
if (params.
|
|
358
|
-
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");
|
|
359
376
|
}
|
|
360
377
|
if (containerGiven(params.project) && containerGiven(params.area)) {
|
|
361
378
|
throw new RangeError("project and area are exclusive destinations");
|
|
@@ -374,6 +391,16 @@ const todoMove = {
|
|
|
374
391
|
}
|
|
375
392
|
}
|
|
376
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
|
+
}
|
|
377
404
|
if (containerGiven(params.area))
|
|
378
405
|
pre.destArea = resolveArea(db, params.area);
|
|
379
406
|
return pre;
|
|
@@ -385,7 +412,7 @@ const todoMove = {
|
|
|
385
412
|
assert.push({ field: "start", equals: "inbox" }, { field: "startDate", equals: null });
|
|
386
413
|
return { mode: "update", uuid: params.uuid, assert };
|
|
387
414
|
}
|
|
388
|
-
if (params.
|
|
415
|
+
if (params.loose === true) {
|
|
389
416
|
// P21/P22: empty list-id strips every container link; the schedule is
|
|
390
417
|
// untouched (pin it — a silent de-schedule would be a contrary write).
|
|
391
418
|
const target = pre.target;
|
|
@@ -393,6 +420,16 @@ const todoMove = {
|
|
|
393
420
|
assert.push({ field: "project", equals: null }, { field: "area", equals: null }, { field: "heading", equals: null }, { field: "startDate", equals: startDate });
|
|
394
421
|
return { mode: "update", uuid: params.uuid, assert };
|
|
395
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
|
+
}
|
|
396
433
|
const heading = pre.destHeading?.resolved;
|
|
397
434
|
const project = pre.destProject?.resolved;
|
|
398
435
|
const area = pre.destArea?.resolved;
|
|
@@ -415,13 +452,20 @@ const todoMove = {
|
|
|
415
452
|
unsupportedVector(this.op, vector);
|
|
416
453
|
return osa(`move to do id ${q(params.uuid)} to list "Inbox"`);
|
|
417
454
|
}
|
|
418
|
-
if (params.
|
|
455
|
+
if (params.loose === true) {
|
|
419
456
|
// Empty list-id = clear the container (P21/P22) — URL only; the other
|
|
420
457
|
// vectors reject or silently ignore container removal (P10/P11, P26).
|
|
421
458
|
if (vector !== "url-scheme")
|
|
422
459
|
unsupportedVector(this.op, vector);
|
|
423
460
|
return thingsUrl("update", { id: params.uuid, "list-id": "" }, ctx.token);
|
|
424
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
|
+
}
|
|
425
469
|
const project = pre.destProject?.resolved;
|
|
426
470
|
const area = pre.destArea?.resolved;
|
|
427
471
|
if (vector === "url-scheme") {
|
|
@@ -604,9 +648,10 @@ const projectAdd = {
|
|
|
604
648
|
const projectUpdate = {
|
|
605
649
|
op: "project.update",
|
|
606
650
|
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
|
|
607
|
-
preRead(db, params) {
|
|
651
|
+
preRead(db, params, now) {
|
|
608
652
|
assertNotesModesExclusive(params);
|
|
609
653
|
const pre = emptyPreState();
|
|
654
|
+
pre.todayIso = localToday(now);
|
|
610
655
|
pre.target = loadTarget(db, params.uuid);
|
|
611
656
|
return pre;
|
|
612
657
|
},
|
|
@@ -680,8 +725,8 @@ const projectMove = {
|
|
|
680
725
|
op: "project.move",
|
|
681
726
|
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE"],
|
|
682
727
|
preRead(db, params) {
|
|
683
|
-
if ((params.
|
|
684
|
-
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");
|
|
685
730
|
}
|
|
686
731
|
const pre = emptyPreState();
|
|
687
732
|
pre.target = loadTarget(db, params.uuid);
|
|
@@ -696,7 +741,7 @@ const projectMove = {
|
|
|
696
741
|
mode: "update",
|
|
697
742
|
uuid: params.uuid,
|
|
698
743
|
assert: [
|
|
699
|
-
params.
|
|
744
|
+
params.noArea === true
|
|
700
745
|
? { field: "area", equals: null }
|
|
701
746
|
: { field: "area.uuid", equals: pre.destArea?.resolved?.uuid ?? "" },
|
|
702
747
|
],
|
|
@@ -704,14 +749,14 @@ const projectMove = {
|
|
|
704
749
|
},
|
|
705
750
|
compile(params, vector, pre, ctx) {
|
|
706
751
|
if (vector === "url-scheme") {
|
|
707
|
-
// P23 (move) / P24 (empty area-id =
|
|
752
|
+
// P23 (move) / P24 (empty area-id = leave the area — URL is the ONLY
|
|
708
753
|
// surface: AppleScript rejects missing value/"" and json-null no-ops).
|
|
709
754
|
return thingsUrl("update-project", {
|
|
710
755
|
id: params.uuid,
|
|
711
|
-
"area-id": params.
|
|
756
|
+
"area-id": params.noArea === true ? "" : (pre.destArea?.resolved?.uuid ?? ""),
|
|
712
757
|
}, ctx.token);
|
|
713
758
|
}
|
|
714
|
-
if (params.
|
|
759
|
+
if (params.noArea === true)
|
|
715
760
|
unsupportedVector(this.op, vector);
|
|
716
761
|
return osa(`set area of project id ${q(params.uuid)} to area id ` +
|
|
717
762
|
q(pre.destArea?.resolved?.uuid ?? ""));
|
|
@@ -953,10 +998,13 @@ const areaAdd = {
|
|
|
953
998
|
};
|
|
954
999
|
const areaDelete = {
|
|
955
1000
|
op: "area.delete",
|
|
956
|
-
hazards: ["H-UNKNOWN-DESTINATION", "H-PERMANENT-DELETE"],
|
|
1001
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-AREA-NOT-EMPTY", "H-PERMANENT-DELETE"],
|
|
957
1002
|
preRead(db, params) {
|
|
958
1003
|
const pre = emptyPreState();
|
|
959
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
|
+
}
|
|
960
1008
|
return pre;
|
|
961
1009
|
},
|
|
962
1010
|
expectedDelta(pre) {
|
|
@@ -1184,7 +1232,7 @@ const reorder = {
|
|
|
1184
1232
|
preRead(db, params, now) {
|
|
1185
1233
|
const pre = emptyPreState();
|
|
1186
1234
|
let containerUuid = null;
|
|
1187
|
-
if (params.scope === "project"
|
|
1235
|
+
if (params.scope === "project") {
|
|
1188
1236
|
pre.destProject = resolveProject(db, params.container ?? {});
|
|
1189
1237
|
containerUuid = pre.destProject.resolved?.uuid ?? null;
|
|
1190
1238
|
}
|
|
@@ -1192,6 +1240,20 @@ const reorder = {
|
|
|
1192
1240
|
pre.destArea = resolveArea(db, params.container ?? {});
|
|
1193
1241
|
containerUuid = pre.destArea.resolved?.uuid ?? null;
|
|
1194
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
|
+
}
|
|
1195
1257
|
pre.reorder = computeReorderPre(db, params, containerUuid, now);
|
|
1196
1258
|
return pre;
|
|
1197
1259
|
},
|
|
@@ -1209,15 +1271,22 @@ const reorder = {
|
|
|
1209
1271
|
compile(params, vector, pre) {
|
|
1210
1272
|
if (vector !== "applescript")
|
|
1211
1273
|
unsupportedVector(this.op, vector);
|
|
1212
|
-
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"
|
|
1213
1278
|
? `project id ${q(pre.destProject?.resolved?.uuid ?? "")}`
|
|
1214
1279
|
: params.scope === "area"
|
|
1215
1280
|
? `area id ${q(pre.destArea?.resolved?.uuid ?? "")}`
|
|
1216
|
-
: params.scope === "
|
|
1217
|
-
?
|
|
1218
|
-
: params.scope === "
|
|
1219
|
-
? `list "
|
|
1220
|
-
:
|
|
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"`;
|
|
1221
1290
|
const wire = pre.reorder?.wireList ?? params.uuids;
|
|
1222
1291
|
if (params.scope === "someday") {
|
|
1223
1292
|
// The Someday handler STACKS each sent id above the list's current top
|
|
@@ -1365,7 +1434,7 @@ function headingChildren(db, headingUuid) {
|
|
|
1365
1434
|
return todos;
|
|
1366
1435
|
}
|
|
1367
1436
|
const headingRename = {
|
|
1368
|
-
op: "
|
|
1437
|
+
op: "project.rename-heading",
|
|
1369
1438
|
hazards: ["H-UNKNOWN-DESTINATION"],
|
|
1370
1439
|
preRead(db, params) {
|
|
1371
1440
|
const pre = emptyPreState();
|
|
@@ -1388,7 +1457,7 @@ const headingRename = {
|
|
|
1388
1457
|
},
|
|
1389
1458
|
};
|
|
1390
1459
|
const headingArchive = {
|
|
1391
|
-
op: "
|
|
1460
|
+
op: "project.archive-heading",
|
|
1392
1461
|
hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-CHILDREN"],
|
|
1393
1462
|
preRead(db, params) {
|
|
1394
1463
|
const pre = emptyPreState();
|
|
@@ -1441,7 +1510,7 @@ const headingArchive = {
|
|
|
1441
1510
|
},
|
|
1442
1511
|
};
|
|
1443
1512
|
const headingUnarchive = {
|
|
1444
|
-
op: "
|
|
1513
|
+
op: "project.unarchive-heading",
|
|
1445
1514
|
hazards: ["H-UNKNOWN-DESTINATION"],
|
|
1446
1515
|
preRead(db, params) {
|
|
1447
1516
|
const pre = emptyPreState();
|
|
@@ -1461,8 +1530,8 @@ const headingUnarchive = {
|
|
|
1461
1530
|
return osa(`set status of to do id ${q(params.uuid)} to open`);
|
|
1462
1531
|
},
|
|
1463
1532
|
};
|
|
1464
|
-
const
|
|
1465
|
-
op: "heading
|
|
1533
|
+
const headingAdd = {
|
|
1534
|
+
op: "project.add-heading",
|
|
1466
1535
|
hazards: ["H-UNKNOWN-DESTINATION"],
|
|
1467
1536
|
preRead(db, params) {
|
|
1468
1537
|
const pre = emptyPreState();
|
|
@@ -1497,6 +1566,32 @@ const headingCreate = {
|
|
|
1497
1566
|
});
|
|
1498
1567
|
},
|
|
1499
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
|
+
};
|
|
1500
1595
|
const todoClearDatedReminder = {
|
|
1501
1596
|
op: "todo.clear-dated-reminder",
|
|
1502
1597
|
hazards: ["H-UNKNOWN-DESTINATION", "H-NO-REMINDER"],
|
|
@@ -1573,14 +1668,17 @@ const todoMakeRepeating = {
|
|
|
1573
1668
|
pre.sameTitleUuids = sameTitleTaskUuids(db, nonHeadingTitle(pre), "to-do");
|
|
1574
1669
|
return pre;
|
|
1575
1670
|
},
|
|
1576
|
-
expectedDelta(pre,
|
|
1671
|
+
expectedDelta(pre, params, ctx) {
|
|
1577
1672
|
// Identity REPLACEMENT or preserve-as-instance (UI2-a / RSIM-R): a NEW
|
|
1578
1673
|
// template row (type=0 with a recurrence rule) is born; the original uuid is
|
|
1579
1674
|
// EITHER destroyed OR relinked as the current-occurrence instance. Discover
|
|
1580
1675
|
// the template with the create probe (excluding the pre-existing same-title
|
|
1581
1676
|
// rows), pick it by asserting it IS a template, then the `repeating` context
|
|
1582
1677
|
// hardens discovery (restored time-bound, source-fingerprint tiebreak) and
|
|
1583
|
-
// derives instance + source fate for the enriched result.
|
|
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.
|
|
1584
1682
|
return {
|
|
1585
1683
|
mode: "create",
|
|
1586
1684
|
probe: {
|
|
@@ -1592,6 +1690,11 @@ const todoMakeRepeating = {
|
|
|
1592
1690
|
repeating: {
|
|
1593
1691
|
sourceUuid: pre.target.uuid,
|
|
1594
1692
|
fingerprint: buildRepeatingFingerprint(pre.target),
|
|
1693
|
+
expectedRule: {
|
|
1694
|
+
type: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1695
|
+
unit: params.frequency,
|
|
1696
|
+
interval: params.interval,
|
|
1697
|
+
},
|
|
1595
1698
|
},
|
|
1596
1699
|
}),
|
|
1597
1700
|
},
|
|
@@ -1615,12 +1718,22 @@ const todoRescheduleRepeat = {
|
|
|
1615
1718
|
},
|
|
1616
1719
|
expectedDelta(_pre, params) {
|
|
1617
1720
|
// Identity PRESERVED (UI2-b): the same template uuid, rule mutated in
|
|
1618
|
-
// place. Assert the decoded rule's frequency + interval; ALSO capture
|
|
1619
|
-
// 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.
|
|
1620
1729
|
return {
|
|
1621
1730
|
mode: "update",
|
|
1622
1731
|
uuid: params.uuid,
|
|
1623
1732
|
assert: [
|
|
1733
|
+
{
|
|
1734
|
+
field: "repeating.rule.type",
|
|
1735
|
+
equals: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1736
|
+
},
|
|
1624
1737
|
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1625
1738
|
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1626
1739
|
],
|
|
@@ -1694,11 +1807,17 @@ const projectRescheduleRepeat = {
|
|
|
1694
1807
|
},
|
|
1695
1808
|
expectedDelta(_pre, params) {
|
|
1696
1809
|
// Identity PRESERVED (UIC2-a): same project uuid, rule mutated in place;
|
|
1697
|
-
// 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.
|
|
1698
1813
|
return {
|
|
1699
1814
|
mode: "update",
|
|
1700
1815
|
uuid: params.uuid,
|
|
1701
1816
|
assert: [
|
|
1817
|
+
{
|
|
1818
|
+
field: "repeating.rule.type",
|
|
1819
|
+
equals: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1820
|
+
},
|
|
1702
1821
|
{ field: "repeating.rule.unit", equals: params.frequency },
|
|
1703
1822
|
{ field: "repeating.rule.interval", equals: params.interval },
|
|
1704
1823
|
],
|
|
@@ -1774,13 +1893,14 @@ const projectMakeRepeating = {
|
|
|
1774
1893
|
pre.repeatSubtreeUuids = projectSubtreeUuids(db, pre.target.uuid);
|
|
1775
1894
|
return pre;
|
|
1776
1895
|
},
|
|
1777
|
-
expectedDelta(pre,
|
|
1896
|
+
expectedDelta(pre, params, ctx) {
|
|
1778
1897
|
// Identity REPLACEMENT or preserve-as-instance (UIC4-b / RSIM-R): a NEW
|
|
1779
1898
|
// template project (with a recurrence rule) is born; the source project is
|
|
1780
1899
|
// EITHER destroyed OR (when its subtree holds a nested repeater) relinked as
|
|
1781
1900
|
// the current-occurrence instance. Pick the TEMPLATE by asserting it IS one,
|
|
1782
1901
|
// excluding pre-existing same-title rows; the `repeating` context hardens
|
|
1783
1902
|
// discovery and derives instance + source fate + childrenReplaced.
|
|
1903
|
+
// `expectedRule` makes the landed rule verifiable (interval-race guard, §8l).
|
|
1784
1904
|
return {
|
|
1785
1905
|
mode: "create",
|
|
1786
1906
|
probe: {
|
|
@@ -1793,6 +1913,11 @@ const projectMakeRepeating = {
|
|
|
1793
1913
|
sourceUuid: pre.target.uuid,
|
|
1794
1914
|
fingerprint: buildRepeatingFingerprint(pre.target),
|
|
1795
1915
|
subtreeUuids: pre.repeatSubtreeUuids ?? [],
|
|
1916
|
+
expectedRule: {
|
|
1917
|
+
type: params.afterCompletion === true ? "after-completion" : "fixed",
|
|
1918
|
+
unit: params.frequency,
|
|
1919
|
+
interval: params.interval,
|
|
1920
|
+
},
|
|
1796
1921
|
},
|
|
1797
1922
|
}),
|
|
1798
1923
|
},
|
|
@@ -1810,23 +1935,23 @@ const projectMakeRepeating = {
|
|
|
1810
1935
|
return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, ruleExtras(params)));
|
|
1811
1936
|
},
|
|
1812
1937
|
};
|
|
1813
|
-
// project.
|
|
1938
|
+
// project.add-repeating is delivered by the runAddRepeatingProject
|
|
1814
1939
|
// orchestrator (project.add THEN project.make-repeating); it has no single
|
|
1815
1940
|
// atomic surface and is never dispatched directly through the pipeline.
|
|
1816
|
-
const
|
|
1941
|
+
const ADD_REPEATING_ORCHESTRATED = "project.add-repeating is delivered by the runAddRepeatingProject orchestrator (create " +
|
|
1817
1942
|
"the project, then promote it to a repeating series); it has no atomic surface and is never " +
|
|
1818
1943
|
"dispatched directly through the pipeline";
|
|
1819
|
-
const
|
|
1820
|
-
op: "project.
|
|
1944
|
+
const projectAddRepeating = {
|
|
1945
|
+
op: "project.add-repeating",
|
|
1821
1946
|
hazards: [],
|
|
1822
1947
|
preRead() {
|
|
1823
1948
|
return emptyPreState();
|
|
1824
1949
|
},
|
|
1825
1950
|
expectedDelta() {
|
|
1826
|
-
throw new Error(
|
|
1951
|
+
throw new Error(ADD_REPEATING_ORCHESTRATED);
|
|
1827
1952
|
},
|
|
1828
1953
|
compile() {
|
|
1829
|
-
throw new Error(
|
|
1954
|
+
throw new Error(ADD_REPEATING_ORCHESTRATED);
|
|
1830
1955
|
},
|
|
1831
1956
|
};
|
|
1832
1957
|
const todoConvertToProject = {
|
|
@@ -1863,7 +1988,7 @@ const todoConvertToProject = {
|
|
|
1863
1988
|
},
|
|
1864
1989
|
};
|
|
1865
1990
|
const headingConvertToProject = {
|
|
1866
|
-
op: "
|
|
1991
|
+
op: "project.promote-heading",
|
|
1867
1992
|
hazards: UI_HAZARDS,
|
|
1868
1993
|
preRead(db, params) {
|
|
1869
1994
|
const pre = emptyPreState();
|
|
@@ -1901,11 +2026,76 @@ const headingConvertToProject = {
|
|
|
1901
2026
|
// H-UNKNOWN-DESTINATION blocks a non-heading target before compile; a
|
|
1902
2027
|
// refuse here means a parentless heading (or one absent from its project's
|
|
1903
2028
|
// heading set) slipped past — fail loud rather than drive the wrong row.
|
|
1904
|
-
throw new Error(`
|
|
2029
|
+
throw new Error(`project.promote-heading: ${tax?.kind === "refuse" ? tax.detail : "no heading taxonomy resolved (guard bypassed?)"}`);
|
|
1905
2030
|
}
|
|
1906
2031
|
return uiDrive(headingConvertToProjectRecipe(tax.projectReveal, tax.ordinal));
|
|
1907
2032
|
},
|
|
1908
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));
|
|
2097
|
+
},
|
|
2098
|
+
};
|
|
1909
2099
|
// -------------------------------------------------- sidebar AREA reorder
|
|
1910
2100
|
/** The compile-time placement for area.reorder (resolved refs). */
|
|
1911
2101
|
function sidebarPlacementOf(params, pre) {
|
|
@@ -1931,6 +2121,16 @@ const areaReorderSidebar = {
|
|
|
1931
2121
|
params.position !== "last") {
|
|
1932
2122
|
throw new RangeError(`invalid position "${params.position}" — expected first | last`);
|
|
1933
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
|
+
}
|
|
1934
2134
|
const pre = emptyPreState();
|
|
1935
2135
|
pre.entityTarget = resolveArea(db, { uuid: params.target, title: params.target });
|
|
1936
2136
|
const ref = params.before ?? params.after;
|
|
@@ -1941,17 +2141,25 @@ const areaReorderSidebar = {
|
|
|
1941
2141
|
if (target != null && dest != null && target.uuid === dest.uuid) {
|
|
1942
2142
|
throw new RangeError("the destination area is the area being moved");
|
|
1943
2143
|
}
|
|
1944
|
-
// The sidebar drag addresses rows by their VISIBLE NAME
|
|
1945
|
-
//
|
|
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;
|
|
1946
2154
|
for (const area of [target, dest]) {
|
|
1947
2155
|
if (area == null)
|
|
1948
2156
|
continue;
|
|
1949
2157
|
const dup = db
|
|
1950
2158
|
.prepare("SELECT COUNT(*) AS n FROM TMArea WHERE title = ? COLLATE NOCASE")
|
|
1951
2159
|
.get(area.title);
|
|
1952
|
-
if (dup.n >
|
|
1953
|
-
throw new RangeError(`area title "${area.title}" is shared by ${dup.n} areas
|
|
1954
|
-
"
|
|
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");
|
|
1955
2163
|
}
|
|
1956
2164
|
}
|
|
1957
2165
|
pre.areaOrder = db.prepare(`SELECT uuid FROM TMArea ORDER BY "index", uuid`).all().map((r) => r.uuid);
|
|
@@ -2044,22 +2252,25 @@ export const COMMANDS = {
|
|
|
2044
2252
|
"project.set-tags": projectSetTags,
|
|
2045
2253
|
"todo.backdate": todoBackdate,
|
|
2046
2254
|
"todo.add-logged": todoAddLogged,
|
|
2047
|
-
"heading
|
|
2048
|
-
"heading
|
|
2049
|
-
"heading
|
|
2050
|
-
"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,
|
|
2051
2263
|
"todo.clear-dated-reminder": todoClearDatedReminder,
|
|
2052
2264
|
"todo.make-repeating": todoMakeRepeating,
|
|
2053
2265
|
"todo.reschedule-repeat": todoRescheduleRepeat,
|
|
2054
2266
|
"todo.pause-repeat": todoPauseRepeat,
|
|
2055
2267
|
"todo.resume-repeat": todoResumeRepeat,
|
|
2056
2268
|
"todo.convert-to-project": todoConvertToProject,
|
|
2057
|
-
"heading.convert-to-project": headingConvertToProject,
|
|
2058
2269
|
"project.reschedule-repeat": projectRescheduleRepeat,
|
|
2059
2270
|
"project.pause-repeat": projectPauseRepeat,
|
|
2060
2271
|
"project.resume-repeat": projectResumeRepeat,
|
|
2061
2272
|
"area.reorder": areaReorderSidebar,
|
|
2062
2273
|
"project.make-repeating": projectMakeRepeating,
|
|
2063
|
-
"project.
|
|
2274
|
+
"project.add-repeating": projectAddRepeating,
|
|
2064
2275
|
};
|
|
2065
2276
|
//# sourceMappingURL=commands.js.map
|