things-api 0.18.0 → 0.19.1
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 +18 -9
- package/deputy/AppIcon.icns +0 -0
- package/deputy/VERSION +1 -1
- package/deputy/helpers-Info.plist +4 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +3 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
- package/deputy/reader/Info.plist +2 -0
- package/deputy/reader/entitlements.plist +30 -0
- package/deputy/reader/main.swift +129 -40
- package/deputy/src/server.swift +25 -2
- package/deputy/src/tcc.swift +58 -0
- package/dist/audit/schema.d.ts +30 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +322 -0
- package/dist/capability.js +706 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +13 -1
- package/dist/cli/commands/doctor.js +141 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +189 -51
- package/dist/cli/commands/helpers.js.map +1 -1
- package/dist/cli/commands/mcp.js +8 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/op-result.js +6 -0
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/reads.js +47 -1
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/repeat-flags.js +34 -2
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +9 -0
- package/dist/cli/commands/setup.js +76 -83
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/writes.d.ts +1 -1
- package/dist/cli/commands/writes.js +311 -142
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/dry-run.d.ts +1 -1
- package/dist/cli/dry-run.js +1 -1
- package/dist/cli/glyphs.d.ts +23 -4
- package/dist/cli/glyphs.js +46 -19
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +49 -27
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/interrupt.d.ts +54 -6
- package/dist/cli/interrupt.js +78 -12
- package/dist/cli/interrupt.js.map +1 -1
- package/dist/cli/main.js +8 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +3 -1
- package/dist/cli/period.js +1 -1
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.js +26 -1
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +31 -4
- package/dist/cli/render.js +127 -15
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +23 -3
- package/dist/client.js +36 -13
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +8 -4
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +234 -6
- package/dist/deputy/install.js +843 -31
- package/dist/deputy/install.js.map +1 -1
- package/dist/deputy/notices.js +2 -2
- package/dist/deputy/notices.js.map +1 -1
- package/dist/deputy/protocol.d.ts +59 -5
- package/dist/deputy/protocol.js +48 -11
- package/dist/deputy/protocol.js.map +1 -1
- package/dist/deputy/routing.d.ts +37 -0
- package/dist/deputy/routing.js +181 -27
- package/dist/deputy/routing.js.map +1 -1
- package/dist/deputy/wake.d.ts +61 -0
- package/dist/deputy/wake.js +149 -0
- package/dist/deputy/wake.js.map +1 -0
- package/dist/diagnose.d.ts +19 -4
- package/dist/diagnose.js +64 -8
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +104 -0
- package/dist/direct-setup.js +658 -0
- package/dist/direct-setup.js.map +1 -0
- package/dist/host-access.d.ts +75 -0
- package/dist/host-access.js +177 -0
- package/dist/host-access.js.map +1 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +27 -1
- package/dist/mcp/server.js +308 -109
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.d.ts +9 -0
- package/dist/op-result.js +13 -1
- package/dist/op-result.js.map +1 -1
- package/dist/read/filter-contract.d.ts +1 -1
- package/dist/read/filter-contract.js +4 -0
- package/dist/read/filter-contract.js.map +1 -1
- package/dist/read/shape.js +4 -0
- package/dist/read/shape.js.map +1 -1
- package/dist/read/views.d.ts +29 -0
- package/dist/read/views.js +60 -0
- package/dist/read/views.js.map +1 -1
- package/dist/session-grant.d.ts +48 -0
- package/dist/session-grant.js +185 -0
- package/dist/session-grant.js.map +1 -0
- package/dist/surface-copy.d.ts +19 -3
- package/dist/surface-copy.js +19 -3
- package/dist/surface-copy.js.map +1 -1
- package/dist/wizard.d.ts +58 -0
- package/dist/wizard.js +174 -0
- package/dist/wizard.js.map +1 -0
- package/dist/write/availability.d.ts +1 -16
- package/dist/write/availability.js +10 -65
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.d.ts +20 -0
- package/dist/write/batch.js +111 -21
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +9 -0
- package/dist/write/capabilities.js +2 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/clear-reminder.js +1 -1
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.d.ts +22 -7
- package/dist/write/commands.js +231 -254
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +32 -7
- package/dist/write/failure-hints.js +9 -14
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.js +22 -4
- package/dist/write/guards.js.map +1 -1
- package/dist/write/lock.d.ts +16 -13
- package/dist/write/lock.js +55 -0
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +9 -13
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/move.js +9 -12
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +51 -29
- package/dist/write/operations.js +16 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +72 -14
- package/dist/write/opid.js +152 -8
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +80 -0
- package/dist/write/param-schema.js +754 -0
- package/dist/write/param-schema.js.map +1 -0
- package/dist/write/pipeline.d.ts +110 -5
- package/dist/write/pipeline.js +288 -14
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +38 -16
- package/dist/write/pre-state.js +117 -22
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +658 -444
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +25 -8
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-collateral.d.ts +31 -0
- package/dist/write/repeat-collateral.js +153 -0
- package/dist/write/repeat-collateral.js.map +1 -0
- package/dist/write/repeat-rule.d.ts +28 -1
- package/dist/write/repeat-rule.js +54 -4
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +11 -0
- package/dist/write/resolution-timestamps.js +108 -40
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +4 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/template-mutation.d.ts +137 -0
- package/dist/write/template-mutation.js +598 -0
- package/dist/write/template-mutation.js.map +1 -0
- package/dist/write/undo.js +10 -20
- package/dist/write/undo.js.map +1 -1
- package/dist/write/update-fields.d.ts +151 -0
- package/dist/write/update-fields.js +427 -0
- package/dist/write/update-fields.js.map +1 -0
- package/dist/write/vectors/applescript.js +6 -10
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +1 -1
- package/dist/write/vectors/registry.js +10 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +2 -2
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +15 -4
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +268 -1
- package/dist/write/vectors/ui-certification.js +63 -13
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-chord.d.ts +144 -0
- package/dist/write/vectors/ui-chord.js +428 -0
- package/dist/write/vectors/ui-chord.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +8 -4
- package/dist/write/vectors/ui-drag.js +2 -0
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +70 -3
- package/dist/write/vectors/ui-recipes.js +482 -44
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +280 -10
- package/dist/write/vectors/ui.js +1068 -77
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +3 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +62 -0
- package/dist/write/verify/delta.js +36 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +4 -1
- package/dist/write/verify/poller.js +14 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +2 -1
- package/schema/envelope.schema.json +6 -4
- package/scripts/build-helpers.sh +5 -0
- package/skills/things-cli/SKILL.md +5 -3
- package/skills/things-cli/references/contracts.md +1 -0
- package/skills/things-cli/references/data-model.md +3 -0
- package/skills/things-cli/references/errors.md +1 -1
- package/skills/things-cli/references/gui.md +1 -0
- package/skills/things-cli/references/repeating.md +6 -0
- package/skills/things-cli/references/tag-cleanup.md +64 -0
package/dist/write/commands.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { localToday, zonedWallInstant } from "../model/dates.js";
|
|
2
2
|
import { byUuid } from "../read/detail.js";
|
|
3
3
|
import { manualLogDateEpoch, pendingLogCount } from "../read/log-boundary.js";
|
|
4
4
|
import { isLooseRef } from "../read/pseudo-area.js";
|
|
5
|
-
import {
|
|
6
|
-
import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, trashedCount, } from "./pre-state.js";
|
|
5
|
+
import { RESOLUTION_TIMESTAMP_EXPECTED } from "../surface-copy.js";
|
|
6
|
+
import { areaMemberCounts, childTagTitles, classifyHeadingConvert, classifyHeadingDissolve, classifyHeadingMoveToProject, classifyProjectRepeat, computeHeadingMovePre, computeReorderPre, emptyPreState, loadTarget, projectChildren, projectStatus, projectSubtreeUuids, resolveArea, resolveHeading, resolveProject, resolveTag, sameTitleTaskUuids, seriesRowUuids, trashedCount, } from "./pre-state.js";
|
|
7
|
+
import { assertNotesModesExclusive, normalizeReminder, updateAssertions, updateWireParams, whenAssertions, whenWithReminder, } from "./update-fields.js";
|
|
7
8
|
import { resolveTagRefs } from "./tag-refs.js";
|
|
8
9
|
import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
|
|
9
10
|
import { assertRepeatRule } from "./repeat-rule.js";
|
|
10
11
|
import { assessOffRuleFirst, deadlineDriveNext, deriveFixedAnchor } from "./repeat-anchor.js";
|
|
11
12
|
import { expectedRuleAssertions } from "./repeat-asserts.js";
|
|
12
13
|
import { escapeAppleScript } from "./vectors/applescript.js";
|
|
13
|
-
import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
|
|
14
|
+
import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingChordRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, createNextCopyRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
|
|
14
15
|
import { buildRepeatingFingerprint } from "./verify/delta.js";
|
|
15
16
|
// ------------------------------------------------------------------ helpers
|
|
16
17
|
function thingsUrl(command, params, token) {
|
|
@@ -65,65 +66,6 @@ function q(value) {
|
|
|
65
66
|
function unsupportedVector(op, vector) {
|
|
66
67
|
throw new Error(`${op} cannot be compiled for vector ${vector} (planner bug)`);
|
|
67
68
|
}
|
|
68
|
-
function whenAssertions(when, todayIso, opts = { mode: "add" }) {
|
|
69
|
-
// Strict shape check: an unvalidated string used to flow straight into the
|
|
70
|
-
// URL (e.g. "2026-07-20@09:30", the raw URL grammar) — the app would SET
|
|
71
|
-
// date+reminder while verification asserted the literal string as the date,
|
|
72
|
-
// reporting a false mismatch on a write that succeeded.
|
|
73
|
-
if (when !== "today" &&
|
|
74
|
-
when !== "evening" &&
|
|
75
|
-
when !== "anytime" &&
|
|
76
|
-
when !== "someday" &&
|
|
77
|
-
!/^\d{4}-\d{2}-\d{2}$/.test(when)) {
|
|
78
|
-
throw new RangeError(when.includes("@")
|
|
79
|
-
? `invalid when "${when}" — a reminder time is a separate parameter (reminder: "HH:mm"; CLI --reminder), not an @ suffix`
|
|
80
|
-
: `invalid when "${when}" — expected today | evening | anytime | someday | YYYY-MM-DD`);
|
|
81
|
-
}
|
|
82
|
-
switch (when) {
|
|
83
|
-
case "today":
|
|
84
|
-
// Today's non-evening section: in Today (the `today` marker) AND NOT in the
|
|
85
|
-
// evening sub-bucket (the presence-keyed `evening` marker absent — asserted
|
|
86
|
-
// as null, which valuesEqual treats as absent). Gated to Today members under
|
|
87
|
-
// the verify clock exactly as the retired `todaySection` was.
|
|
88
|
-
//
|
|
89
|
-
// The `startDate` assertion differs by op (field bug §0½.8):
|
|
90
|
-
// - ADD mints a fresh Today item with no schedule history, so the app dates
|
|
91
|
-
// it EXACTLY today — exact equality is right.
|
|
92
|
-
// - UPDATE of an item ALREADY in Today whose `startDate` has already arrived
|
|
93
|
-
// (past or today): the app PRESERVES that historical date rather than
|
|
94
|
-
// rewriting the storage byte to today (arrived-date law). Assert the
|
|
95
|
-
// arrived-date PREDICATE (non-null and <= today) so a preserved historical
|
|
96
|
-
// date verifies, while an undated deadline-only pull (null startDate) is
|
|
97
|
-
// still rejected — the item's Today membership then rests only on a
|
|
98
|
-
// deadline, not on the requested schedule.
|
|
99
|
-
return [
|
|
100
|
-
{ field: "start", equals: "active" },
|
|
101
|
-
opts.mode === "update"
|
|
102
|
-
? { field: "startDate", satisfies: { predicate: "arrived-on-or-before", date: todayIso } }
|
|
103
|
-
: { field: "startDate", equals: todayIso },
|
|
104
|
-
{ field: "today", equals: true },
|
|
105
|
-
{ field: "evening", equals: null },
|
|
106
|
-
];
|
|
107
|
-
case "evening":
|
|
108
|
-
// The This-Evening sub-bucket: the `evening` marker (which implies `today`).
|
|
109
|
-
return [
|
|
110
|
-
{ field: "start", equals: "active" },
|
|
111
|
-
{ field: "startDate", equals: todayIso },
|
|
112
|
-
{ field: "evening", equals: true },
|
|
113
|
-
];
|
|
114
|
-
case "anytime":
|
|
115
|
-
return [
|
|
116
|
-
{ field: "start", equals: "active" },
|
|
117
|
-
{ field: "startDate", equals: null },
|
|
118
|
-
];
|
|
119
|
-
case "someday":
|
|
120
|
-
return [{ field: "start", equals: "someday" }];
|
|
121
|
-
default:
|
|
122
|
-
// Concrete date: assert only the date — start-state semantics differ
|
|
123
|
-
// for past/today/future dates (only the date itself is invariant).
|
|
124
|
-
return [{ field: "startDate", equals: when }];
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
69
|
function sortedTags(tags) {
|
|
128
70
|
return [...tags].toSorted();
|
|
129
71
|
}
|
|
@@ -139,21 +81,28 @@ function applyTagRefs(db, pre, tags) {
|
|
|
139
81
|
pre.missingTags = res.missing;
|
|
140
82
|
pre.resolvedTagTitles = res.titles;
|
|
141
83
|
}
|
|
142
|
-
/** Round-trip normalization: "6:5"-style inputs → canonical "06:05". */
|
|
143
|
-
function normalizeReminder(time) {
|
|
144
|
-
return decodeReminderTime(encodeReminderTime(time)) ?? time;
|
|
145
|
-
}
|
|
146
84
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
85
|
+
* Is a destination container SUPPLIED? A PRESENCE check, not a duck-test (#580):
|
|
86
|
+
* the parameter schema (param-schema.ts) has already proven that a present ref is
|
|
87
|
+
* a `{uuid}`/`{title}` object, so anything else reaching here is an engine bug,
|
|
88
|
+
* not caller input — and it THROWS rather than reading as "no destination given",
|
|
89
|
+
* which is precisely how a bare-string `project` used to compile an Inbox capture
|
|
90
|
+
* with no placement and no placement assertion, then verify clean.
|
|
149
91
|
*/
|
|
150
|
-
function
|
|
151
|
-
if (
|
|
152
|
-
return
|
|
153
|
-
|
|
92
|
+
export function containerGiven(ref) {
|
|
93
|
+
if (ref === undefined || ref === null)
|
|
94
|
+
return false;
|
|
95
|
+
assertContainerRef(ref);
|
|
96
|
+
return true;
|
|
154
97
|
}
|
|
155
|
-
|
|
156
|
-
|
|
98
|
+
/** Belt-and-braces: a present container reference must carry a uuid or a title. */
|
|
99
|
+
export function assertContainerRef(ref) {
|
|
100
|
+
if (typeof ref !== "object" || Array.isArray(ref)) {
|
|
101
|
+
throw new RangeError(`a container reference must be an object naming a uuid or a title — received ${JSON.stringify(ref)}`);
|
|
102
|
+
}
|
|
103
|
+
if (ref.uuid === undefined && ref.title === undefined) {
|
|
104
|
+
throw new RangeError("a container reference must name a uuid or a title — it names neither");
|
|
105
|
+
}
|
|
157
106
|
}
|
|
158
107
|
/**
|
|
159
108
|
* The create-probe title for the duplicate / identity-replacement ops, whose
|
|
@@ -351,108 +300,35 @@ const todoAdd = {
|
|
|
351
300
|
},
|
|
352
301
|
};
|
|
353
302
|
/**
|
|
354
|
-
* The
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* presentation-dead (the GUI hides its bell; the byte lingers in the DB). We do
|
|
361
|
-
* NOT auto-preserve such a stale byte — carrying it into the re-schedule would
|
|
362
|
-
* RESURRECT a reminder the user believes gone. A LIVE reminder (startDate
|
|
363
|
-
* today/future) is preserved as before. The liveness test is the SAME
|
|
364
|
-
* {@link reminderIsLive} predicate the read side gates on, consulted against the
|
|
365
|
-
* target's CURRENT `startDate` under the response clock.
|
|
303
|
+
* The to-do / project update pair. Both legs — the URL parameters and the
|
|
304
|
+
* expected-delta assertions — are DERIVED from the single exhaustive registry in
|
|
305
|
+
* update-fields.ts (the #491 exhaustive-map doctrine): the only per-verb
|
|
306
|
+
* difference is the URL command name, so neither verb can accept a field the
|
|
307
|
+
* other drops, and a field added to `UpdateFields` breaks compilation there
|
|
308
|
+
* until every leg handles it.
|
|
366
309
|
*/
|
|
367
|
-
function
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
388
|
-
function assertNotesModesExclusive(params) {
|
|
389
|
-
if (params.notes !== undefined &&
|
|
390
|
-
(params.appendNotes !== undefined || params.prependNotes !== undefined)) {
|
|
391
|
-
throw new RangeError("notes (replace) is exclusive with appendNotes/prependNotes");
|
|
392
|
-
}
|
|
393
|
-
if (params.appendNotes !== undefined && params.prependNotes !== undefined) {
|
|
394
|
-
throw new RangeError("appendNotes and prependNotes cannot be combined in one update");
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
function expectedNotes(pre, params) {
|
|
398
|
-
const current = pre.target !== null && pre.target.type !== "heading" ? pre.target.notes : "";
|
|
399
|
-
// Separator semantics probed: newline-joined, no stray newline against an
|
|
400
|
-
// empty note (E04/E05/E11/E12).
|
|
401
|
-
if (params.appendNotes !== undefined) {
|
|
402
|
-
return current === "" ? params.appendNotes : `${current}\n${params.appendNotes}`;
|
|
403
|
-
}
|
|
404
|
-
if (params.prependNotes !== undefined) {
|
|
405
|
-
return current === "" ? params.prependNotes : `${params.prependNotes}\n${current}`;
|
|
406
|
-
}
|
|
407
|
-
return undefined;
|
|
310
|
+
function updateSpec(op, urlCommand) {
|
|
311
|
+
return {
|
|
312
|
+
op,
|
|
313
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
|
|
314
|
+
preRead(db, params, now) {
|
|
315
|
+
assertNotesModesExclusive(params);
|
|
316
|
+
const pre = emptyPreState();
|
|
317
|
+
pre.todayIso = localToday(now);
|
|
318
|
+
pre.target = loadTarget(db, params.uuid);
|
|
319
|
+
return pre;
|
|
320
|
+
},
|
|
321
|
+
expectedDelta(pre, params, ctx) {
|
|
322
|
+
return { mode: "update", uuid: params.uuid, assert: updateAssertions(params, pre, ctx) };
|
|
323
|
+
},
|
|
324
|
+
compile(params, vector, pre, ctx) {
|
|
325
|
+
if (vector !== "url-scheme")
|
|
326
|
+
unsupportedVector(op, vector);
|
|
327
|
+
return thingsUrl(urlCommand, { id: params.uuid, ...updateWireParams(params, pre) }, ctx.token);
|
|
328
|
+
},
|
|
329
|
+
};
|
|
408
330
|
}
|
|
409
|
-
const todoUpdate =
|
|
410
|
-
op: "todo.update",
|
|
411
|
-
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
|
|
412
|
-
preRead(db, params, now) {
|
|
413
|
-
assertNotesModesExclusive(params);
|
|
414
|
-
const pre = emptyPreState();
|
|
415
|
-
pre.todayIso = localToday(now);
|
|
416
|
-
pre.target = loadTarget(db, params.uuid);
|
|
417
|
-
return pre;
|
|
418
|
-
},
|
|
419
|
-
expectedDelta(pre, params, ctx) {
|
|
420
|
-
const assert = [];
|
|
421
|
-
if (params.title !== undefined)
|
|
422
|
-
assert.push({ field: "title", equals: params.title });
|
|
423
|
-
if (params.notes !== undefined)
|
|
424
|
-
assert.push({ field: "notes", equals: params.notes });
|
|
425
|
-
const joined = expectedNotes(pre, params);
|
|
426
|
-
if (joined !== undefined)
|
|
427
|
-
assert.push({ field: "notes", equals: joined });
|
|
428
|
-
if (params.when !== undefined) {
|
|
429
|
-
assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
|
|
430
|
-
const reminder = effectiveReminder(pre, params);
|
|
431
|
-
assert.push({
|
|
432
|
-
field: "reminder",
|
|
433
|
-
equals: reminder === null ? null : normalizeReminder(reminder),
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
if (params.deadline !== undefined)
|
|
437
|
-
assert.push({ field: "deadline", equals: params.deadline });
|
|
438
|
-
return { mode: "update", uuid: params.uuid, assert };
|
|
439
|
-
},
|
|
440
|
-
compile(params, vector, pre, ctx) {
|
|
441
|
-
if (vector !== "url-scheme")
|
|
442
|
-
unsupportedVector(this.op, vector);
|
|
443
|
-
return thingsUrl("update", {
|
|
444
|
-
id: params.uuid,
|
|
445
|
-
title: params.title,
|
|
446
|
-
notes: params.notes,
|
|
447
|
-
"append-notes": params.appendNotes,
|
|
448
|
-
"prepend-notes": params.prependNotes,
|
|
449
|
-
when: params.when === undefined
|
|
450
|
-
? undefined
|
|
451
|
-
: whenWithReminder(params.when, effectiveReminder(pre, params)),
|
|
452
|
-
deadline: params.deadline === null ? "" : params.deadline,
|
|
453
|
-
}, ctx.token);
|
|
454
|
-
},
|
|
455
|
-
};
|
|
331
|
+
const todoUpdate = updateSpec("todo.update", "update");
|
|
456
332
|
function statusSpec(op, urlParam, asStatus, scriptStatus) {
|
|
457
333
|
return {
|
|
458
334
|
op,
|
|
@@ -826,6 +702,14 @@ const projectAdd = {
|
|
|
826
702
|
const area = pre.destArea?.resolved;
|
|
827
703
|
if (area !== undefined && area !== null)
|
|
828
704
|
assert.push({ field: "area.uuid", equals: area.uuid });
|
|
705
|
+
// KNOWN SHALLOW SPOT (audited 2026-08-23, exhaustive-map sweep): the seeded
|
|
706
|
+
// children — `todos` and the structured `items` (headings + rich children) —
|
|
707
|
+
// contribute NO assertion, so a project born with an empty body still
|
|
708
|
+
// verifies `ok`. Unlike a to-do's `checklistItems` (asserted through
|
|
709
|
+
// `checklistTitles`) there is no child-list field on the delta vocabulary to
|
|
710
|
+
// compare against, and adding one needs probe evidence that the imported
|
|
711
|
+
// children are readable inside the verify window before a missing child may
|
|
712
|
+
// be called a failure. Tracked in docs/up-next.md; do NOT "fix" it blind.
|
|
829
713
|
return {
|
|
830
714
|
mode: "create",
|
|
831
715
|
probe: {
|
|
@@ -873,55 +757,9 @@ const projectAdd = {
|
|
|
873
757
|
}, ctx.token);
|
|
874
758
|
},
|
|
875
759
|
};
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
preRead(db, params, now) {
|
|
880
|
-
assertNotesModesExclusive(params);
|
|
881
|
-
const pre = emptyPreState();
|
|
882
|
-
pre.todayIso = localToday(now);
|
|
883
|
-
pre.target = loadTarget(db, params.uuid);
|
|
884
|
-
return pre;
|
|
885
|
-
},
|
|
886
|
-
expectedDelta(pre, params, ctx) {
|
|
887
|
-
const assert = [];
|
|
888
|
-
if (params.title !== undefined)
|
|
889
|
-
assert.push({ field: "title", equals: params.title });
|
|
890
|
-
if (params.notes !== undefined)
|
|
891
|
-
assert.push({ field: "notes", equals: params.notes });
|
|
892
|
-
const joined = expectedNotes(pre, params);
|
|
893
|
-
if (joined !== undefined)
|
|
894
|
-
assert.push({ field: "notes", equals: joined });
|
|
895
|
-
if (params.when !== undefined) {
|
|
896
|
-
assert.push(...whenAssertions(params.when, ctx.todayIso, { mode: "update" }));
|
|
897
|
-
// Projects carry the same reminderTime codec as to-dos (A3); a bare
|
|
898
|
-
// when= clears an existing reminder unless auto-preserved.
|
|
899
|
-
const reminder = effectiveReminder(pre, params);
|
|
900
|
-
assert.push({
|
|
901
|
-
field: "reminder",
|
|
902
|
-
equals: reminder === null ? null : normalizeReminder(reminder),
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
if (params.deadline !== undefined)
|
|
906
|
-
assert.push({ field: "deadline", equals: params.deadline });
|
|
907
|
-
return { mode: "update", uuid: params.uuid, assert };
|
|
908
|
-
},
|
|
909
|
-
compile(params, vector, pre, ctx) {
|
|
910
|
-
if (vector !== "url-scheme")
|
|
911
|
-
unsupportedVector(this.op, vector);
|
|
912
|
-
return thingsUrl("update-project", {
|
|
913
|
-
id: params.uuid,
|
|
914
|
-
title: params.title,
|
|
915
|
-
notes: params.notes,
|
|
916
|
-
"append-notes": params.appendNotes,
|
|
917
|
-
"prepend-notes": params.prependNotes,
|
|
918
|
-
when: params.when === undefined
|
|
919
|
-
? undefined
|
|
920
|
-
: whenWithReminder(params.when, effectiveReminder(pre, params)),
|
|
921
|
-
deadline: params.deadline === null ? "" : params.deadline,
|
|
922
|
-
}, ctx.token);
|
|
923
|
-
},
|
|
924
|
-
};
|
|
760
|
+
// Projects carry the same reminderTime codec and notes/schedule semantics as
|
|
761
|
+
// to-dos (A3/E18) — same registry, same legs, only the URL command differs.
|
|
762
|
+
const projectUpdate = updateSpec("project.update", "update-project");
|
|
925
763
|
const projectSetTags = {
|
|
926
764
|
op: "project.set-tags",
|
|
927
765
|
hazards: ["H-UNKNOWN-DESTINATION", "H-UNKNOWN-TAG"],
|
|
@@ -1579,22 +1417,26 @@ const reorder = {
|
|
|
1579
1417
|
};
|
|
1580
1418
|
// ---- resolution-timestamp normalization (§5 of the plan) ------------------
|
|
1581
1419
|
/**
|
|
1582
|
-
* Parse
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1420
|
+
* Parse a date (`2025-01-15`) OR datetime (`2025-01-15T09:30[:ss]`, or the same
|
|
1421
|
+
* value spelled with a space — `2025-01-15 09:30`) and resolve it to a single
|
|
1422
|
+
* UTC instant in the effective `zone`. Both separators are first-class
|
|
1423
|
+
* spellings of the SAME instant: the space form is what a human (or a shell
|
|
1424
|
+
* heredoc) types, the `T` form is what a serializer emits, and neither is
|
|
1425
|
+
* rewritten into the other. A date-only value lands at NOON in that zone
|
|
1426
|
+
* (B-DATEONLY: noon decodes to the intended calendar date in every zone;
|
|
1427
|
+
* midnight can slip a day). A datetime is read as wall-clock time in the
|
|
1428
|
+
* effective zone. `zone` undefined = the process-local (app host) zone, which
|
|
1429
|
+
* for a local CLI run IS the app's own zone.
|
|
1588
1430
|
*/
|
|
1589
1431
|
export function resolveResolutionInstant(input, zone) {
|
|
1590
1432
|
const dateOnly = /^(\d{4}-\d{2}-\d{2})$/.exec(input);
|
|
1591
1433
|
if (dateOnly !== null)
|
|
1592
1434
|
return zonedWallInstant(dateOnly[1], 12, 0, 0, zone);
|
|
1593
|
-
const dt = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(input);
|
|
1435
|
+
const dt = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(input);
|
|
1594
1436
|
if (dt !== null) {
|
|
1595
1437
|
return zonedWallInstant(dt[1], Number(dt[2]), Number(dt[3]), dt[4] === undefined ? 0 : Number(dt[4]), zone);
|
|
1596
1438
|
}
|
|
1597
|
-
throw new RangeError(`invalid timestamp "${input}" — expected
|
|
1439
|
+
throw new RangeError(`invalid timestamp "${input}" — expected ${RESOLUTION_TIMESTAMP_EXPECTED}`);
|
|
1598
1440
|
}
|
|
1599
1441
|
const pad2 = (n) => String(n).padStart(2, "0");
|
|
1600
1442
|
/** The host-local calendar date (`YYYY-MM-DD`) an instant renders as — what the verify layer observes for stopped/created. */
|
|
@@ -1825,9 +1667,18 @@ const headingAdd = {
|
|
|
1825
1667
|
});
|
|
1826
1668
|
},
|
|
1827
1669
|
};
|
|
1670
|
+
/**
|
|
1671
|
+
* project.move-heading — within-project heading order, on the ARROW-CHORD ui
|
|
1672
|
+
* vector (CHORDMH1, on the HEADORD1 law). Things 3.23 left the private reorder
|
|
1673
|
+
* command declared and inert, which took heading order offline entirely; the
|
|
1674
|
+
* replacement is the app's own keyboard affordance (⌘↑/⌘↓/⌘⌥↑/⌘⌥↓ on a selected
|
|
1675
|
+
* heading row), driven as a verified closed loop by src/write/vectors/ui-chord.ts.
|
|
1676
|
+
* This is the ONE reorder that leaves the `privateReorderIsNoOp` range — every
|
|
1677
|
+
* other scope still routes through its own gate.
|
|
1678
|
+
*/
|
|
1828
1679
|
const projectMoveHeading = {
|
|
1829
1680
|
op: "project.move-heading",
|
|
1830
|
-
hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-ORDER"],
|
|
1681
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-HEADING-ORDER", "H-UI-DRIVE"],
|
|
1831
1682
|
preRead(db, params) {
|
|
1832
1683
|
const pre = emptyPreState();
|
|
1833
1684
|
pre.destProject = resolveProject(db, params.project);
|
|
@@ -1835,24 +1686,41 @@ const projectMoveHeading = {
|
|
|
1835
1686
|
return pre;
|
|
1836
1687
|
},
|
|
1837
1688
|
expectedDelta(pre) {
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1689
|
+
const move = pre.headingMove;
|
|
1690
|
+
return {
|
|
1691
|
+
mode: "ordering",
|
|
1692
|
+
key: "index",
|
|
1693
|
+
// The full end state: every heading of the project, strictly ascending.
|
|
1694
|
+
sequence: move?.targetOrder ?? [],
|
|
1695
|
+
// RRF1: the headings the caller did NOT name must hold their EXACT prior
|
|
1696
|
+
// rank — a chord is a single-row write, and this is what proves the one
|
|
1697
|
+
// that landed was the row the plan addressed (a heading row exposes no
|
|
1698
|
+
// title to read the selection back through).
|
|
1699
|
+
unchanged: move?.untouched ?? [],
|
|
1700
|
+
// Children follow their heading through an intact FK, un-renumbered and
|
|
1701
|
+
// un-touched (no userModificationDate bump — that is what `frozen` adds).
|
|
1702
|
+
frozen: (move?.children ?? []).map((c) => ({
|
|
1703
|
+
uuid: c.uuid,
|
|
1704
|
+
assert: [{ field: "heading.uuid", equals: c.heading }],
|
|
1705
|
+
})),
|
|
1706
|
+
...(move?.targetOrder[0] !== undefined && { subject: move.targetOrder[0] }),
|
|
1707
|
+
};
|
|
1842
1708
|
},
|
|
1843
|
-
compile(
|
|
1844
|
-
|
|
1845
|
-
// used (experimental — gated by allow-experimental + the sdef canary),
|
|
1846
|
-
// fed the computed full order.
|
|
1847
|
-
if (vector !== "applescript")
|
|
1709
|
+
compile(params, vector, pre) {
|
|
1710
|
+
if (vector !== "ui")
|
|
1848
1711
|
unsupportedVector(this.op, vector);
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1712
|
+
const move = pre.headingMove;
|
|
1713
|
+
if (move === null || move.problems.length > 0) {
|
|
1714
|
+
// The membership/anchor refusals are surfaced by H-HEADING-ORDER
|
|
1715
|
+
// before compile; reaching here with one means the guard was bypassed.
|
|
1716
|
+
throw new Error(`project.move-heading: ${move === null ? "no heading move computed (guard bypassed?)" : move.problems.join("; ")}`);
|
|
1717
|
+
}
|
|
1718
|
+
// The movee set is EXACTLY what the caller named — never derived from the
|
|
1719
|
+
// untouched set, which fences a different (smaller) thing: a bystander whose
|
|
1720
|
+
// POSITION shifts is still a bystander no chord may move.
|
|
1721
|
+
return uiDrive(moveHeadingChordRecipe(pre.destProject?.resolved?.uuid ?? "", move.targetOrder, [
|
|
1722
|
+
...params.headings,
|
|
1723
|
+
]));
|
|
1856
1724
|
},
|
|
1857
1725
|
};
|
|
1858
1726
|
const todoClearDatedReminder = {
|
|
@@ -1901,11 +1769,41 @@ function uiDrive(recipe) {
|
|
|
1901
1769
|
// the validator from commands.ts; the implementation + the full combination
|
|
1902
1770
|
// matrix live in repeat-rule.ts.
|
|
1903
1771
|
export { assertRepeatRule };
|
|
1772
|
+
/**
|
|
1773
|
+
* The recipe extras for a MAKE-REPEATING drive: {@link ruleExtras} with the
|
|
1774
|
+
* "Next:" drive date deadline-adjusted, exactly as {@link reschedRuleExtras}
|
|
1775
|
+
* does for a reschedule.
|
|
1776
|
+
*
|
|
1777
|
+
* ONE MEANING FOR `next`, ONE PLACE THAT SHIFTS IT (NEXTPOP1). `next` is the
|
|
1778
|
+
* requested first-occurrence START — what `--when` means to a caller — in every
|
|
1779
|
+
* params bag, at every layer. A DEADLINED rule anchors the dialog on the
|
|
1780
|
+
* DEADLINE (YANCH1 #493), so the date the "Next:" control must carry is
|
|
1781
|
+
* `next + startDaysEarlier`; that conversion belongs to the compile, where the
|
|
1782
|
+
* dialog is, and nowhere else.
|
|
1783
|
+
*
|
|
1784
|
+
* It used to happen upstream too: the promote orchestrators shifted `next`
|
|
1785
|
+
* before handing the bag to this op, so the SAME field meant a START coming from
|
|
1786
|
+
* a caller and a DUE date coming from a composite. Everything downstream that
|
|
1787
|
+
* shifts — `assessOffRuleFirst`, via `assertRepeatRule` in preRead — then shifted
|
|
1788
|
+
* a second time, and a deadlined MONTHLY promote was refused before it ran with
|
|
1789
|
+
* a message about a date the caller never asked for (`--deadline 2026-08-20`
|
|
1790
|
+
* + `--when 2026-08-06` → "a first occurrence on 2026-08-20 would not hold",
|
|
1791
|
+
* measured in-lab). The orchestrators now pass `--when` through unshifted.
|
|
1792
|
+
*/
|
|
1793
|
+
function makeRuleExtras(params) {
|
|
1794
|
+
const base = ruleExtras(params);
|
|
1795
|
+
const drive = deadlineDriveNext(params);
|
|
1796
|
+
return drive !== undefined ? { ...base, next: drive } : base;
|
|
1797
|
+
}
|
|
1904
1798
|
/**
|
|
1905
1799
|
* The extended-vocabulary fields of a rule as a recipe `extras` bag, including
|
|
1906
1800
|
* ONLY the keys that are present (exactOptionalPropertyTypes: never set an
|
|
1907
1801
|
* optional field to undefined). A bare `{ uuid, frequency, interval }` yields
|
|
1908
1802
|
* `{}`, so the recipe drives exactly the certified two-control path.
|
|
1803
|
+
*
|
|
1804
|
+
* `next` is copied through VERBATIM — a START. Every compile that drives the
|
|
1805
|
+
* dialog wraps this with its own deadline shift ({@link makeRuleExtras},
|
|
1806
|
+
* {@link reschedRuleExtras}); this function never shifts (NEXTPOP1).
|
|
1909
1807
|
*/
|
|
1910
1808
|
function ruleExtras(params) {
|
|
1911
1809
|
return {
|
|
@@ -1948,15 +1846,31 @@ function reschedEffParams(params) {
|
|
|
1948
1846
|
* `--when` is the scheduled START, but a deadlined rule anchors on the DEADLINE,
|
|
1949
1847
|
* so the dialog's "Next:" field is driven with `when + startDaysEarlier` and the
|
|
1950
1848
|
* app back-shifts the instance start to `--when`. The expectedDelta keeps the RAW
|
|
1951
|
-
* `params.next` (the cursor asserts the START).
|
|
1952
|
-
*
|
|
1953
|
-
*
|
|
1849
|
+
* `params.next` (the cursor asserts the START). {@link makeRuleExtras} is the
|
|
1850
|
+
* make/add-repeating twin of this — the shift lands in the compile on every verb
|
|
1851
|
+
* that drives the dialog, and nowhere upstream of it (NEXTPOP1).
|
|
1954
1852
|
*/
|
|
1955
1853
|
function reschedRuleExtras(params) {
|
|
1956
1854
|
const base = ruleExtras(reschedEffParams(params));
|
|
1957
1855
|
const drive = deadlineDriveNext(params);
|
|
1958
1856
|
return drive !== undefined ? { ...base, next: drive } : base;
|
|
1959
1857
|
}
|
|
1858
|
+
/**
|
|
1859
|
+
* The rule-vocabulary keys this reschedule REQUESTED — the input to the
|
|
1860
|
+
* unexplained-delta check (CGRD1 guard 3): every decoded-rule field that moves
|
|
1861
|
+
* must be attributable to one of these, or to a mapped co-mover of one.
|
|
1862
|
+
*
|
|
1863
|
+
* Built from the EFFECTIVE params, the same bag the recipe drives, so a
|
|
1864
|
+
* `--when`-only reschedule that DERIVES a calendar anchor counts that anchor as
|
|
1865
|
+
* requested rather than as an unexplained move (RSPA1 / YANCH1 #493) — the drive
|
|
1866
|
+
* really does address those pop-ups. `uuid` is the target, not a rule field.
|
|
1867
|
+
*/
|
|
1868
|
+
function requestedRuleKeys(params) {
|
|
1869
|
+
const eff = reschedEffParams(params);
|
|
1870
|
+
return Object.entries(eff)
|
|
1871
|
+
.filter(([key, value]) => key !== "uuid" && value !== undefined)
|
|
1872
|
+
.map(([key]) => key);
|
|
1873
|
+
}
|
|
1960
1874
|
/** ui ops all guard existence/type + the H-UI-DRIVE acknowledgement. */
|
|
1961
1875
|
const UI_HAZARDS = ["H-UNKNOWN-DESTINATION", "H-UI-DRIVE"];
|
|
1962
1876
|
const todoMakeRepeating = {
|
|
@@ -2004,7 +1918,7 @@ const todoMakeRepeating = {
|
|
|
2004
1918
|
compile(params, vector) {
|
|
2005
1919
|
if (vector !== "ui")
|
|
2006
1920
|
unsupportedVector(this.op, vector);
|
|
2007
|
-
return uiDrive(makeRepeatingRecipe(params.uuid, params.frequency, params.interval,
|
|
1921
|
+
return uiDrive(makeRepeatingRecipe(params.uuid, params.frequency, params.interval, makeRuleExtras(params)));
|
|
2008
1922
|
},
|
|
2009
1923
|
};
|
|
2010
1924
|
const todoRescheduleRepeat = {
|
|
@@ -2042,6 +1956,12 @@ const todoRescheduleRepeat = {
|
|
|
2042
1956
|
includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
|
|
2043
1957
|
}),
|
|
2044
1958
|
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
1959
|
+
// UNEXPLAINED-DELTA DETECTION (CGRD1 guard 3). The assertions above prove
|
|
1960
|
+
// the requested rule landed; this proves nothing ELSE did. A reschedule
|
|
1961
|
+
// rewrites an EXISTING rule, so there is a full pre-state to diff against —
|
|
1962
|
+
// which is why make-repeating / add-repeating do not carry it: they mint the
|
|
1963
|
+
// rule, so every field is new by construction and there is nothing to compare.
|
|
1964
|
+
collateral: { requested: requestedRuleKeys(params) },
|
|
2045
1965
|
};
|
|
2046
1966
|
},
|
|
2047
1967
|
compile(params, vector) {
|
|
@@ -2072,6 +1992,56 @@ const todoPauseRepeat = {
|
|
|
2072
1992
|
return uiDrive(pauseRepeatRecipe(params.uuid));
|
|
2073
1993
|
},
|
|
2074
1994
|
};
|
|
1995
|
+
/**
|
|
1996
|
+
* `Items ▸ Repeat ▸ Create Next Copy` (CNC1) — materialize the pending
|
|
1997
|
+
* occurrence and advance the series. The INTERNAL first leg of the
|
|
1998
|
+
* template-mutation composites (template-mutation.ts); it is not a user-facing
|
|
1999
|
+
* verb, because a bare "spawn one now" has no use we have been asked for and
|
|
2000
|
+
* every honest use of it mutates the row it mints.
|
|
2001
|
+
*
|
|
2002
|
+
* Verified as a CREATE: the minted row wears the template's own title, is a
|
|
2003
|
+
* to-do, carries a fresh (gesture wall-clock) creationDate, and is not one of
|
|
2004
|
+
* the rows the series already had — which is exactly the create-probe's
|
|
2005
|
+
* gauntlet, and it hands the composite back the minted uuid it needs. The one
|
|
2006
|
+
* field assertion is the FK: the discovered row must be an instance of THIS
|
|
2007
|
+
* template, so a same-titled row created by anything else in the window cannot
|
|
2008
|
+
* pass. The cursor advance is deliberately NOT asserted here — a create delta
|
|
2009
|
+
* asserts against the created row, and the composite re-reads the template
|
|
2010
|
+
* afterwards anyway to disclose when the next occurrence lands.
|
|
2011
|
+
*/
|
|
2012
|
+
const todoCreateNextCopy = {
|
|
2013
|
+
op: "todo.create-next-copy",
|
|
2014
|
+
hazards: UI_HAZARDS,
|
|
2015
|
+
preRead(db, params) {
|
|
2016
|
+
const pre = emptyPreState();
|
|
2017
|
+
pre.target = loadTarget(db, params.uuid);
|
|
2018
|
+
// The series' existing rows — the template plus every instance it has
|
|
2019
|
+
// already spawned, all of which share its title. They are the create
|
|
2020
|
+
// probe's exclusion set, so "the new row" is unambiguous even on a series
|
|
2021
|
+
// whose occurrences all look alike.
|
|
2022
|
+
pre.sameTitleUuids = seriesRowUuids(db, params.uuid);
|
|
2023
|
+
return pre;
|
|
2024
|
+
},
|
|
2025
|
+
expectedDelta(pre, params, ctx) {
|
|
2026
|
+
const target = pre.target;
|
|
2027
|
+
const title = target !== null && target.type !== "heading" ? target.title : "";
|
|
2028
|
+
return {
|
|
2029
|
+
mode: "create",
|
|
2030
|
+
probe: {
|
|
2031
|
+
title,
|
|
2032
|
+
type: "to-do",
|
|
2033
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
2034
|
+
excludeUuids: pre.sameTitleUuids,
|
|
2035
|
+
},
|
|
2036
|
+
assert: [{ field: "repeating.templateUuid", equals: params.uuid }],
|
|
2037
|
+
};
|
|
2038
|
+
},
|
|
2039
|
+
compile(params, vector) {
|
|
2040
|
+
if (vector !== "ui")
|
|
2041
|
+
unsupportedVector(this.op, vector);
|
|
2042
|
+
return uiDrive(createNextCopyRecipe(params.uuid));
|
|
2043
|
+
},
|
|
2044
|
+
};
|
|
2075
2045
|
const todoResumeRepeat = {
|
|
2076
2046
|
op: "todo.resume-repeat",
|
|
2077
2047
|
hazards: UI_HAZARDS,
|
|
@@ -2131,6 +2101,12 @@ const projectRescheduleRepeat = {
|
|
|
2131
2101
|
includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
|
|
2132
2102
|
}),
|
|
2133
2103
|
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
2104
|
+
// UNEXPLAINED-DELTA DETECTION (CGRD1 guard 3). The assertions above prove
|
|
2105
|
+
// the requested rule landed; this proves nothing ELSE did. A reschedule
|
|
2106
|
+
// rewrites an EXISTING rule, so there is a full pre-state to diff against —
|
|
2107
|
+
// which is why make-repeating / add-repeating do not carry it: they mint the
|
|
2108
|
+
// rule, so every field is new by construction and there is nothing to compare.
|
|
2109
|
+
collateral: { requested: requestedRuleKeys(params) },
|
|
2134
2110
|
};
|
|
2135
2111
|
},
|
|
2136
2112
|
compile(params, vector) {
|
|
@@ -2240,7 +2216,7 @@ const projectMakeRepeating = {
|
|
|
2240
2216
|
// Unreachable in practice: the guards block refuse/anytime before compile.
|
|
2241
2217
|
throw new Error("project.make-repeating: no selectable-row taxonomy resolved (guard bypassed?)");
|
|
2242
2218
|
}
|
|
2243
|
-
return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval,
|
|
2219
|
+
return uiDrive(projectMakeRepeatingRecipe(tax.containerReveal, params.uuid, tax.title, params.frequency, params.interval, makeRuleExtras(params)));
|
|
2244
2220
|
},
|
|
2245
2221
|
};
|
|
2246
2222
|
// project.add-repeating is delivered by the runAddRepeatingProject
|
|
@@ -2612,6 +2588,7 @@ export const COMMANDS = {
|
|
|
2612
2588
|
"todo.make-repeating": todoMakeRepeating,
|
|
2613
2589
|
"todo.reschedule-repeat": todoRescheduleRepeat,
|
|
2614
2590
|
"todo.pause-repeat": todoPauseRepeat,
|
|
2591
|
+
"todo.create-next-copy": todoCreateNextCopy,
|
|
2615
2592
|
"todo.resume-repeat": todoResumeRepeat,
|
|
2616
2593
|
"todo.convert-to-project": todoConvertToProject,
|
|
2617
2594
|
"project.reschedule-repeat": projectRescheduleRepeat,
|