things-api 0.18.0 → 0.19.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 -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 +13 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +160 -0
- package/dist/capability.js +416 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli/commands/doctor.js +127 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +178 -51
- package/dist/cli/commands/helpers.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/setup.d.ts +9 -0
- package/dist/cli/commands/setup.js +63 -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 +241 -107
- 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/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 +22 -2
- package/dist/client.js +36 -13
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +6 -2
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +219 -5
- package/dist/deputy/install.js +803 -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 +21 -0
- package/dist/deputy/routing.js +111 -27
- package/dist/deputy/routing.js.map +1 -1
- package/dist/diagnose.d.ts +14 -1
- package/dist/diagnose.js +46 -6
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +78 -0
- package/dist/direct-setup.js +484 -0
- package/dist/direct-setup.js.map +1 -0
- package/dist/host-access.d.ts +63 -0
- package/dist/host-access.js +165 -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 +285 -100
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.d.ts +9 -0
- package/dist/op-result.js +5 -0
- 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 +170 -0
- package/dist/session-grant.js.map +1 -0
- package/dist/surface-copy.d.ts +8 -0
- package/dist/surface-copy.js +8 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/wizard.d.ts +58 -0
- package/dist/wizard.js +167 -0
- package/dist/wizard.js.map +1 -0
- package/dist/write/availability.d.ts +1 -1
- package/dist/write/availability.js +3 -3
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.d.ts +20 -0
- package/dist/write/batch.js +94 -15
- 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 +12 -1
- package/dist/write/commands.js +112 -224
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +3 -1
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.js +19 -2
- 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/operations.d.ts +36 -21
- package/dist/write/operations.js +7 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +14 -0
- package/dist/write/opid.js +22 -0
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +124 -0
- package/dist/write/param-schema.js +747 -0
- package/dist/write/param-schema.js.map +1 -0
- package/dist/write/pipeline.d.ts +76 -2
- package/dist/write/pipeline.js +190 -2
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +8 -0
- package/dist/write/pre-state.js +29 -2
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +539 -439
- 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-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 +83 -38
- 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 +451 -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 +3 -0
- 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 +7 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +57 -0
- package/dist/write/vectors/ui-recipes.d.ts +19 -0
- package/dist/write/vectors/ui-recipes.js +30 -0
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +13 -0
- package/dist/write/vectors/ui.js +62 -13
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +21 -0
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +2 -1
- package/schema/envelope.schema.json +5 -3
- 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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { undoToken } from "../audit/schema.js";
|
|
2
|
+
import { uiAllowed, uiCapability as uiCapabilityDefault } from "../capability.js";
|
|
2
3
|
import { addDaysIso, decodePackedDate, localToday } from "../model/dates.js";
|
|
3
4
|
import { byUuid } from "../read/detail.js";
|
|
4
5
|
import { resolveProjectWriteTarget, resolveTaskUuidPrefix } from "../read/queries.js";
|
|
5
6
|
import { runCloneProject, runCloneTodo } from "./clone.js";
|
|
6
7
|
import { promoteProjectViaGui } from "./make-repeating-project.js";
|
|
7
|
-
import { fingerprintLabel, runMutation, } from "./pipeline.js";
|
|
8
|
+
import { fingerprintLabel, runComposite, runMutation, } from "./pipeline.js";
|
|
8
9
|
import { assessOffRuleFirst, daysBetweenIso, deriveFixedAnchor, isIsoDate, } from "./repeat-anchor.js";
|
|
9
|
-
import { assertRepeatRule, ruleToInverseParams } from "./repeat-rule.js";
|
|
10
|
+
import { assertRepeatRule, ruleToInverseParams, splitAddRepeatingRule } from "./repeat-rule.js";
|
|
10
11
|
import { createDbReader } from "./verify/delta.js";
|
|
11
12
|
import { H_UI_SESSION_UNREACHABLE } from "./vectors/session-reachability.js";
|
|
12
13
|
const PLACEMENT_NOTE = "the series' current instance lands at its container's default position — its prior slot was " +
|
|
@@ -50,22 +51,51 @@ function blockedUiDrive(op) {
|
|
|
50
51
|
detail: "this operation promotes an item to a repeating series by driving the local Things app " +
|
|
51
52
|
"through the Accessibility API (the Repeat… dialog) — it may briefly interact with the UI",
|
|
52
53
|
remediation: "pass dangerouslyDriveGui (--dangerously-drive-gui) to proceed; the vector also requires " +
|
|
53
|
-
"`things config set ui-enabled true` and
|
|
54
|
+
"`things config set ui-enabled true` and `things helpers setup --gui`, which grants " +
|
|
55
|
+
"GUI-driving to the helper pair — the only identity it is granted to " +
|
|
56
|
+
"(docs/design/permissions-doctrine.md, Article IV)",
|
|
54
57
|
};
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
57
|
-
* SESSGATE
|
|
58
|
-
* it SEEDS a row (clone / add) before the GUI promote leg drives the
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
60
|
+
* The pre-seed UI PREFLIGHT (SESSGATE #480 + issue #512). A promote composite is
|
|
61
|
+
* NOT atomic: it SEEDS a row (clone / add) before the GUI promote leg drives the
|
|
62
|
+
* dialog. Anything that will certainly stop that drive must be established BEFORE
|
|
63
|
+
* the seed exists, or the composite mutates, fails, and cleans up after itself for
|
|
64
|
+
* a reason it could have known up front. Two checks, cheapest and most decisive
|
|
65
|
+
* first, both prompt-free:
|
|
66
|
+
*
|
|
67
|
+
* 1. STANDING (permissions doctrine, Article IV). Driving the window needs
|
|
68
|
+
* Accessibility + Automation → System Events, held by the helper pair and
|
|
69
|
+
* nothing else. The pipeline already gates this — but only on the PROMOTE leg,
|
|
70
|
+
* which runs after the seed, so a machine without the grants used to create
|
|
71
|
+
* the row, refuse, and trash it. Same verdict, same copy, one leg earlier;
|
|
72
|
+
* nothing is created. Keyed on a vector's `drivesGui` DECLARATION exactly as
|
|
73
|
+
* the pipeline's gate is, never on its id, so a fake/simulator substituted
|
|
74
|
+
* under "ui" is not gated on the developer's own host state.
|
|
75
|
+
* 2. REACHABILITY (SESSGATE). If the Mac's session is AX-blind (screen locked /
|
|
76
|
+
* full-screen Space), the dialog would open on an unreachable window. Only
|
|
77
|
+
* "session" scope refuses here: a window merely on another Space is left for
|
|
78
|
+
* the in-drive relocation (the reveal has not run yet, so refusing before the
|
|
79
|
+
* seed would be a false positive). A fail-open probe proceeds — the promote
|
|
80
|
+
* leg's own gate + the seed cleanup remain the backstop.
|
|
81
|
+
*
|
|
82
|
+
* Running the standing check FIRST also keeps the probe itself doctrine-clean: no
|
|
83
|
+
* System Events call is attempted on a host that has not granted one.
|
|
67
84
|
*/
|
|
68
|
-
async function
|
|
85
|
+
async function gateUiPreflight(deps, op) {
|
|
86
|
+
const gui = deps.vectors.find((v) => v.drivesGui === true && v.simulates !== true);
|
|
87
|
+
if (gui !== undefined) {
|
|
88
|
+
const capability = (deps.uiCapability ?? (() => uiCapabilityDefault()))();
|
|
89
|
+
if (!uiAllowed(capability)) {
|
|
90
|
+
return {
|
|
91
|
+
kind: "blocked",
|
|
92
|
+
op,
|
|
93
|
+
reason: "environment",
|
|
94
|
+
detail: `this operation drives the Things window, and ${capability.detail} — nothing was created`,
|
|
95
|
+
remediation: capability.remediation.join("; "),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
69
99
|
if (!deps.config.ui.enabled)
|
|
70
100
|
return null;
|
|
71
101
|
const ui = deps.vectors.find((v) => v.probeReachability !== undefined);
|
|
@@ -117,35 +147,23 @@ async function cleanupSeed(deps, kind, createdUuid, promote, options, txnId) {
|
|
|
117
147
|
*
|
|
118
148
|
* The `rule` bag is the SUPERSET the make/add legs actually carry: make-repeating
|
|
119
149
|
* passes a full {@link RepeatRuleParams} (rule-level `deadline`/`startDaysEarlier`
|
|
120
|
-
* included), add-repeating an
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
150
|
+
* and the requested `next` included), add-repeating an
|
|
151
|
+
* {@link AddRepeatingRuleFields} plus whichever of the deadline pair its geometry
|
|
152
|
+
* folded in. Every field flows through by SPREAD, never by a hand-copied field
|
|
153
|
+
* list — a list here is how the make-repeating promote once stripped
|
|
154
|
+
* `deadline`/`startDaysEarlier` (a deadlined make-repeating produced a
|
|
155
|
+
* NON-deadlined template, YANCH1 #493) and how the project promote later dropped
|
|
156
|
+
* `next` (#549). The ONE field that is deliberately re-derived rather than copied
|
|
157
|
+
* is `next`: the drive date is deadline-shifted by the caller, and an
|
|
158
|
+
* after-completion dialog has no first-occurrence field to drive at all.
|
|
126
159
|
*/
|
|
127
160
|
function ruleParamsFor(uuid, rule, nextIso) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
...(rule.monthly !== undefined && { monthly: rule.monthly }),
|
|
135
|
-
...(rule.yearly !== undefined && { yearly: rule.yearly }),
|
|
136
|
-
...(rule.ends !== undefined && { ends: rule.ends }),
|
|
137
|
-
// The repeat reminder picker is drivable (ANCH2). make-repeating carries a
|
|
138
|
-
// rule-level reminder through; add-repeating threads the base to-do's
|
|
139
|
-
// --reminder here too (ADR1 #480), since the dialog conversion otherwise drops
|
|
140
|
-
// the seed's one-off reminder from the series.
|
|
141
|
-
...(rule.reminder !== undefined && { reminder: rule.reminder }),
|
|
142
|
-
// The deadline offset is a rule-level field on make-repeating (the "Add
|
|
143
|
-
// deadlines" checkbox + "start N days earlier"); it must ride the promote or a
|
|
144
|
-
// `make-repeating --deadline` lands a non-deadlined series (YANCH1 #493).
|
|
145
|
-
...(rule.deadline !== undefined && { deadline: rule.deadline }),
|
|
146
|
-
...(rule.startDaysEarlier !== undefined && { startDaysEarlier: rule.startDaysEarlier }),
|
|
147
|
-
...(nextIso !== undefined && rule.afterCompletion !== true && { next: nextIso }),
|
|
148
|
-
};
|
|
161
|
+
const out = { ...rule, uuid };
|
|
162
|
+
// Never the caller's raw `next` — see above; the drive date arrives as `nextIso`.
|
|
163
|
+
delete out.next;
|
|
164
|
+
if (nextIso !== undefined && rule.afterCompletion !== true)
|
|
165
|
+
out.next = nextIso;
|
|
166
|
+
return out;
|
|
149
167
|
}
|
|
150
168
|
const UNIT_SINGULAR = {
|
|
151
169
|
daily: "day",
|
|
@@ -214,6 +232,32 @@ function instanceStartDate(db, instanceUuid) {
|
|
|
214
232
|
return null;
|
|
215
233
|
return decodePackedDate(row.s);
|
|
216
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* The oracle for the post-drive first-occurrence check, picked by rule KIND
|
|
237
|
+
* (issue #508).
|
|
238
|
+
*
|
|
239
|
+
* A FIXED-schedule series is anchored on the calendar: the dialog's "Next:" field
|
|
240
|
+
* IS driven, and the template's own cursor (`rt1_instanceCreationStartDate`) holds
|
|
241
|
+
* the resulting first occurrence — the right thing to compare against.
|
|
242
|
+
*
|
|
243
|
+
* An AFTER-COMPLETION series has NO calendar anchor, so {@link ruleParamsFor}
|
|
244
|
+
* deliberately leaves "Next:" alone, and the app mints the template with an EMPTY
|
|
245
|
+
* cursor (RSIM2 / RSIM-P P4: no next / reference dates exist until a completion
|
|
246
|
+
* happens). Comparing that empty cursor against `--when` reported a FALSE
|
|
247
|
+
* `verify-failed:mismatch` on a perfectly correct creation (#508, reproduced 6/6
|
|
248
|
+
* on the live host). The requested date lives on the materialized INSTANCE's own
|
|
249
|
+
* `startDate` — the row the promote preserved and relinked — so that is the oracle
|
|
250
|
+
* for an after-completion series.
|
|
251
|
+
*
|
|
252
|
+
* Returns `null` when no oracle is reachable. For an after-completion rule that
|
|
253
|
+
* means "unverifiable" and the caller SKIPS the check (the create delta already
|
|
254
|
+
* proved the series landed); for a fixed rule an absent cursor is a genuine miss.
|
|
255
|
+
*/
|
|
256
|
+
function landedFirstStart(deps, templateUuid, instanceUuid, afterCompletion) {
|
|
257
|
+
if (!afterCompletion)
|
|
258
|
+
return firstOccurrenceOf(deps.db, templateUuid);
|
|
259
|
+
return instanceUuid === null ? null : instanceStartDate(deps.db, instanceUuid);
|
|
260
|
+
}
|
|
217
261
|
/**
|
|
218
262
|
* DBLSPAWN1 (docs/lab/dblspawn1-preserved-instance.md, golden-v3 / Things 3.22.14):
|
|
219
263
|
* a promote whose source is PRESERVED (SRCFATE deadline / terminal-element trigger)
|
|
@@ -234,7 +278,16 @@ function instanceStartDate(db, instanceUuid) {
|
|
|
234
278
|
* promote did not preserve a future instance. Best-effort: a failed trash is reported
|
|
235
279
|
* in the warning rather than failing the whole compound (the series is already sound).
|
|
236
280
|
*/
|
|
237
|
-
async function trashRedundantFuturePreservedInstance(deps, kind, promote, options, txnId, now) {
|
|
281
|
+
async function trashRedundantFuturePreservedInstance(deps, kind, promote, options, txnId, now, afterCompletion) {
|
|
282
|
+
// AFTER-COMPLETION series are exempt: the double-book is a CURSOR phenomenon
|
|
283
|
+
// (`rt1_nextInstanceStartDate` pointing at an already-materialized occurrence),
|
|
284
|
+
// and an after-completion template is minted with NO cursor at all — the next
|
|
285
|
+
// occurrence is unknown until a completion happens (RSIM2 / RSIM-P P4). Its
|
|
286
|
+
// preserved instance is therefore the series' ONLY occurrence, whatever its
|
|
287
|
+
// date; trashing it would destroy the series' current occurrence rather than a
|
|
288
|
+
// duplicate. (Unreachable before #508, whose false verify-failed returned first.)
|
|
289
|
+
if (afterCompletion)
|
|
290
|
+
return null;
|
|
238
291
|
const rep = promote.repeating;
|
|
239
292
|
// Preserved iff the native promote relinked the source (replacedUuid === null) AND
|
|
240
293
|
// there is a materialized instance. A DELETE-fate promote reports replacedUuid !==
|
|
@@ -424,141 +477,151 @@ async function makeRepeatingViaClone(deps, kind, params, options) {
|
|
|
424
477
|
},
|
|
425
478
|
};
|
|
426
479
|
}
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
480
|
+
// PRE-SEED UI PREFLIGHT (#480/#512): refuse a host that cannot drive the window,
|
|
481
|
+
// or a locked / full-screen session, BEFORE minting a clone — otherwise the
|
|
482
|
+
// promote's dialog never opens and the whole compound fails, stranding a
|
|
483
|
+
// disposable clone. Zero mutation on refusal.
|
|
484
|
+
const gate = await gateUiPreflight(deps, op);
|
|
431
485
|
if (gate !== null)
|
|
432
486
|
return gate;
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
? {
|
|
445
|
-
|
|
487
|
+
// COMPOSITE LOCK: everything below is ONE verb executed as several mutations
|
|
488
|
+
// (clone → trash → promote → the DBLSPAWN1 clean-up), and they must not
|
|
489
|
+
// interleave with another writer's legs — the promote's row selection is by
|
|
490
|
+
// TITLE, so a concurrent clone of the same item makes it ambiguous. One lock,
|
|
491
|
+
// held to the end; each leg's own acquisition is a reentrant no-op.
|
|
492
|
+
return runComposite(deps, op, async () => {
|
|
493
|
+
const startedAt = now;
|
|
494
|
+
const txnId = newTxnId(startedAt);
|
|
495
|
+
// 1. Clone the source as a disposable, embedded leg (--preserve-created). The
|
|
496
|
+
// clone has captured X's full content by the time it returns.
|
|
497
|
+
const clone = kind === "project"
|
|
498
|
+
? await runCloneProject(deps, { uuid: srcUuid, preserveCreated: true }, legOptions(options, txnId))
|
|
499
|
+
: await runCloneTodo(deps, { uuid: srcUuid, preserveCreated: true }, legOptions(options, txnId));
|
|
500
|
+
if (clone.kind !== "ok" || clone.uuid === null) {
|
|
501
|
+
// A clone refusal (nested repeating template, H-CLONE-SOURCE) surfaces
|
|
502
|
+
// coherently here — re-label it to the make-repeating op for the caller.
|
|
503
|
+
return clone.kind === "ok"
|
|
504
|
+
? {
|
|
505
|
+
kind: "verify-failed",
|
|
506
|
+
op,
|
|
507
|
+
reason: "mismatch",
|
|
508
|
+
expected: {
|
|
509
|
+
mode: "create",
|
|
510
|
+
probe: { title: src.title, type: expectedType, sinceEpoch: 0 },
|
|
511
|
+
assert: [],
|
|
512
|
+
},
|
|
513
|
+
observed: null,
|
|
514
|
+
detail: "the disposable clone was created but its uuid was not discovered — nothing was promoted or trashed",
|
|
515
|
+
}
|
|
516
|
+
: { ...clone, op };
|
|
517
|
+
}
|
|
518
|
+
const cloneUuid = clone.uuid;
|
|
519
|
+
// --preserve-modified: X is the ONLY pre-existing row the compound touches (the
|
|
520
|
+
// clone/promote legs mint fresh rows). Capture its pre-write umd BEFORE the
|
|
521
|
+
// trash bumps it — the trash leg restores it forward, and the value rides the
|
|
522
|
+
// summary record's preModDates so the symmetric undo restore fires on the
|
|
523
|
+
// revived X (undo.ts, 2026-08-13 ruling). The clone leg above reads X but never
|
|
524
|
+
// writes it, so its umd is still pristine here.
|
|
525
|
+
const preserveModified = options.preserveModified === true;
|
|
526
|
+
const preUmd = preserveModified
|
|
527
|
+
? createDbReader(deps.db, now, deps.zone).modDateOf(srcUuid)
|
|
528
|
+
: null;
|
|
529
|
+
// 2. Trash the original BEFORE promoting — the clone already holds X's content,
|
|
530
|
+
// and a live same-titled X would make the promote's project row-selection
|
|
531
|
+
// ambiguous (H-PROJECT-REPEAT). X survives in the Trash (the recoverable half).
|
|
532
|
+
const trash = await runMutation(deps, `${kind}.delete`, { uuid: srcUuid }, legOptions(options, txnId, undefined, preserveModified ? { preserveModified: true } : undefined));
|
|
533
|
+
if (trash.kind !== "ok") {
|
|
534
|
+
return {
|
|
535
|
+
...trash,
|
|
446
536
|
op,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
537
|
+
...("detail" in trash
|
|
538
|
+
? {
|
|
539
|
+
detail: `${trash.detail} — the disposable clone (uuid ${cloneUuid}) was created but the ` +
|
|
540
|
+
`original ${srcUuid} could not be moved to the Trash, so it was NOT promoted; trash ` +
|
|
541
|
+
"the clone and retry",
|
|
542
|
+
}
|
|
543
|
+
: {}),
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
// 3. Native-promote the clone (with the ANCH2 Next drive + derived weekday).
|
|
547
|
+
const rule = ruleParamsFor(cloneUuid, effParams, nextIso);
|
|
548
|
+
const promote = kind === "project"
|
|
549
|
+
? await promoteProjectViaGui(deps, rule, legOptions(options, txnId, "ui"))
|
|
550
|
+
: await runMutation(deps, "todo.make-repeating", rule, legOptions(options, txnId, "ui"));
|
|
551
|
+
if (promote.kind !== "ok") {
|
|
552
|
+
// The clone persists but was not promoted; best-effort ROLL BACK the trash so
|
|
553
|
+
// the original is not stranded in the Trash.
|
|
554
|
+
const restoreOp = kind === "project" ? "project.restore" : "todo.restore";
|
|
555
|
+
const rolledBack = await runMutation(deps, restoreOp, { uuid: srcUuid }, legOptions(options, txnId));
|
|
556
|
+
const rollNote = rolledBack.kind === "ok"
|
|
557
|
+
? `the original ${srcUuid} was restored from the Trash`
|
|
558
|
+
: `the original ${srcUuid} could NOT be restored from the Trash — restore it in the app`;
|
|
559
|
+
return {
|
|
560
|
+
...promote,
|
|
561
|
+
op,
|
|
562
|
+
...("detail" in promote
|
|
563
|
+
? {
|
|
564
|
+
detail: `${promote.detail} — the disposable clone (uuid ${cloneUuid}) was created but the ` +
|
|
565
|
+
`promote did not land; ${rollNote}. Trash the clone and retry`,
|
|
566
|
+
}
|
|
567
|
+
: {}),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const { templateUuid } = discoveryOf(promote);
|
|
571
|
+
let { instanceUuid } = discoveryOf(promote);
|
|
572
|
+
// Post-drive verify (ANCH2 + YANCH1): the instance START must have landed on the
|
|
573
|
+
// requested `--when` — for a deadlined rule the driven Next is the deadline
|
|
574
|
+
// (when + startDaysEarlier) and the app back-shifts the start to `--when`, so the
|
|
575
|
+
// check is against `expectedStartIso`, not the raw drive date. Fail closed on
|
|
576
|
+
// mismatch rather than report a wrong-phase ok. The ORACLE is rule-kind dependent
|
|
577
|
+
// (#508) — see landedFirstStart; an unverifiable after-completion series skips.
|
|
578
|
+
const afterCompletion = effParams.afterCompletion === true;
|
|
579
|
+
if (expectedStartIso !== undefined) {
|
|
580
|
+
const landed = landedFirstStart(deps, templateUuid, instanceUuid, afterCompletion);
|
|
581
|
+
if (!(afterCompletion && landed === null) && landed !== expectedStartIso) {
|
|
582
|
+
return nextMismatch(op, templateUuid, expectedStartIso, landed);
|
|
455
583
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
584
|
+
}
|
|
585
|
+
const warnings = [
|
|
586
|
+
landedRuleEcho(effParams, expectedStartIso ?? firstOccurrenceOf(deps.db, templateUuid)),
|
|
587
|
+
`the original ${expectedType} (uuid ${srcUuid}) was moved to the Trash; \`things undo\` ` +
|
|
588
|
+
"removes the new series (trash-both) and restores it",
|
|
589
|
+
PLACEMENT_NOTE,
|
|
590
|
+
];
|
|
591
|
+
// DBLSPAWN1: if the promote PRESERVED the source (deadline / terminal-element
|
|
592
|
+
// trigger) as a FUTURE-dated instance, the app would spawn a duplicate on that date
|
|
593
|
+
// — trash the redundant occurrence and disclose (cursor mints the single real one).
|
|
594
|
+
const dbl = await trashRedundantFuturePreservedInstance(deps, kind, promote, options, txnId, now, afterCompletion);
|
|
595
|
+
if (dbl !== null) {
|
|
596
|
+
warnings.push(dbl.warning);
|
|
597
|
+
if (instanceUuid === dbl.trashedUuid)
|
|
598
|
+
instanceUuid = null;
|
|
599
|
+
}
|
|
600
|
+
const offRule = offRuleFirstNote(effParams);
|
|
601
|
+
if (offRule !== null)
|
|
602
|
+
warnings.push(offRule);
|
|
603
|
+
if (promote.warnings !== undefined)
|
|
604
|
+
warnings.push(...promote.warnings);
|
|
605
|
+
appendPromoteSummary(deps, {
|
|
606
|
+
startedAt,
|
|
476
607
|
op,
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
// 3. Native-promote the clone (with the ANCH2 Next drive + derived weekday).
|
|
487
|
-
const rule = ruleParamsFor(cloneUuid, effParams, nextIso);
|
|
488
|
-
const promote = kind === "project"
|
|
489
|
-
? await promoteProjectViaGui(deps, rule, legOptions(options, txnId, "ui"))
|
|
490
|
-
: await runMutation(deps, "todo.make-repeating", rule, legOptions(options, txnId, "ui"));
|
|
491
|
-
if (promote.kind !== "ok") {
|
|
492
|
-
// The clone persists but was not promoted; best-effort ROLL BACK the trash so
|
|
493
|
-
// the original is not stranded in the Trash.
|
|
494
|
-
const restoreOp = kind === "project" ? "project.restore" : "todo.restore";
|
|
495
|
-
const rolledBack = await runMutation(deps, restoreOp, { uuid: srcUuid }, legOptions(options, txnId));
|
|
496
|
-
const rollNote = rolledBack.kind === "ok"
|
|
497
|
-
? `the original ${srcUuid} was restored from the Trash`
|
|
498
|
-
: `the original ${srcUuid} could NOT be restored from the Trash — restore it in the app`;
|
|
499
|
-
return {
|
|
500
|
-
...promote,
|
|
608
|
+
txnId,
|
|
609
|
+
templateUuid,
|
|
610
|
+
instanceUuid,
|
|
611
|
+
originalUuid: srcUuid,
|
|
612
|
+
invocation: `${op}: clone ${srcUuid} → trash ${srcUuid} → promote ${cloneUuid} → template ${templateUuid}`,
|
|
613
|
+
requested: effParams,
|
|
614
|
+
...(preserveModified && preUmd !== null && { preModDates: { [srcUuid]: preUmd } }),
|
|
615
|
+
});
|
|
616
|
+
return promoteOk({
|
|
501
617
|
op,
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
const { templateUuid } = discoveryOf(promote);
|
|
511
|
-
let { instanceUuid } = discoveryOf(promote);
|
|
512
|
-
// Post-drive verify (ANCH2 + YANCH1): the instance START must have landed on the
|
|
513
|
-
// requested `--when` — for a deadlined rule the driven Next is the deadline
|
|
514
|
-
// (when + startDaysEarlier) and the app back-shifts the start to `--when`, so the
|
|
515
|
-
// check is against `expectedStartIso`, not the raw drive date. Fail closed on
|
|
516
|
-
// mismatch rather than report a wrong-phase ok.
|
|
517
|
-
if (expectedStartIso !== undefined) {
|
|
518
|
-
const landed = firstOccurrenceOf(deps.db, templateUuid);
|
|
519
|
-
if (landed !== expectedStartIso) {
|
|
520
|
-
return nextMismatch(op, templateUuid, expectedStartIso, landed);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
const warnings = [
|
|
524
|
-
landedRuleEcho(effParams, expectedStartIso ?? firstOccurrenceOf(deps.db, templateUuid)),
|
|
525
|
-
`the original ${expectedType} (uuid ${srcUuid}) was moved to the Trash; \`things undo\` ` +
|
|
526
|
-
"removes the new series (trash-both) and restores it",
|
|
527
|
-
PLACEMENT_NOTE,
|
|
528
|
-
];
|
|
529
|
-
// DBLSPAWN1: if the promote PRESERVED the source (deadline / terminal-element
|
|
530
|
-
// trigger) as a FUTURE-dated instance, the app would spawn a duplicate on that date
|
|
531
|
-
// — trash the redundant occurrence and disclose (cursor mints the single real one).
|
|
532
|
-
const dbl = await trashRedundantFuturePreservedInstance(deps, kind, promote, options, txnId, now);
|
|
533
|
-
if (dbl !== null) {
|
|
534
|
-
warnings.push(dbl.warning);
|
|
535
|
-
if (instanceUuid === dbl.trashedUuid)
|
|
536
|
-
instanceUuid = null;
|
|
537
|
-
}
|
|
538
|
-
const offRule = offRuleFirstNote(effParams);
|
|
539
|
-
if (offRule !== null)
|
|
540
|
-
warnings.push(offRule);
|
|
541
|
-
if (promote.warnings !== undefined)
|
|
542
|
-
warnings.push(...promote.warnings);
|
|
543
|
-
appendPromoteSummary(deps, {
|
|
544
|
-
startedAt,
|
|
545
|
-
op,
|
|
546
|
-
txnId,
|
|
547
|
-
templateUuid,
|
|
548
|
-
instanceUuid,
|
|
549
|
-
originalUuid: srcUuid,
|
|
550
|
-
invocation: `${op}: clone ${srcUuid} → trash ${srcUuid} → promote ${cloneUuid} → template ${templateUuid}`,
|
|
551
|
-
requested: effParams,
|
|
552
|
-
...(preserveModified && preUmd !== null && { preModDates: { [srcUuid]: preUmd } }),
|
|
553
|
-
});
|
|
554
|
-
return promoteOk({
|
|
555
|
-
op,
|
|
556
|
-
templateUuid,
|
|
557
|
-
instanceUuid,
|
|
558
|
-
replacedUuid: cloneUuid,
|
|
559
|
-
title: src.title,
|
|
560
|
-
txnId,
|
|
561
|
-
warnings,
|
|
618
|
+
templateUuid,
|
|
619
|
+
instanceUuid,
|
|
620
|
+
replacedUuid: cloneUuid,
|
|
621
|
+
title: src.title,
|
|
622
|
+
txnId,
|
|
623
|
+
warnings,
|
|
624
|
+
});
|
|
562
625
|
});
|
|
563
626
|
}
|
|
564
627
|
export function runMakeRepeatingTodo(deps, params, options = {}) {
|
|
@@ -636,165 +699,185 @@ async function addRepeatingViaCreate(deps, kind, addParams, rule, title, options
|
|
|
636
699
|
},
|
|
637
700
|
};
|
|
638
701
|
}
|
|
639
|
-
//
|
|
640
|
-
//
|
|
641
|
-
//
|
|
642
|
-
|
|
702
|
+
// PRE-SEED UI PREFLIGHT (#480/#512): refuse a host that cannot drive the window,
|
|
703
|
+
// or a locked / full-screen session, BEFORE seeding the row (the two legs are not
|
|
704
|
+
// atomic — a doomed promote would strand the seed). Zero mutation on refusal; a
|
|
705
|
+
// window merely on another Space is relocated in-drive.
|
|
706
|
+
const gate = await gateUiPreflight(deps, op);
|
|
643
707
|
if (gate !== null)
|
|
644
708
|
return gate;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
709
|
+
// COMPOSITE LOCK: add → promote (→ the seed auto-trash / DBLSPAWN1 clean-up)
|
|
710
|
+
// is one verb, several mutations; hold one lock across all of them so a
|
|
711
|
+
// concurrent composite cannot land its own legs between ours.
|
|
712
|
+
return runComposite(deps, op, async () => {
|
|
713
|
+
const startedAt = deps.now?.() ?? new Date();
|
|
714
|
+
const txnId = newTxnId(startedAt);
|
|
715
|
+
// 1. Create the item (full add vocabulary) as an embedded leg.
|
|
716
|
+
const addOp = kind === "project" ? "project.add" : "todo.add";
|
|
717
|
+
const add = await runMutation(deps, addOp, addParams, legOptions(options, txnId, "url-scheme"));
|
|
718
|
+
if (add.kind !== "ok" || add.uuid === null) {
|
|
719
|
+
return add.kind === "ok"
|
|
720
|
+
? {
|
|
721
|
+
kind: "verify-failed",
|
|
722
|
+
op,
|
|
723
|
+
reason: "mismatch",
|
|
724
|
+
expected: {
|
|
725
|
+
mode: "create",
|
|
726
|
+
probe: { title, type: expectedType, sinceEpoch: 0 },
|
|
727
|
+
assert: [],
|
|
728
|
+
},
|
|
729
|
+
observed: null,
|
|
730
|
+
detail: `the ${expectedType} was created but its uuid was not discovered — it cannot be promoted to repeating`,
|
|
731
|
+
}
|
|
732
|
+
: { ...add, op };
|
|
733
|
+
}
|
|
734
|
+
const createdUuid = add.uuid;
|
|
735
|
+
// 2. Native-promote the fresh row (ANCH2 Next drive + derived weekday + the
|
|
736
|
+
// base reminder driven onto the series, ADR1).
|
|
737
|
+
const ruleParams = ruleParamsFor(createdUuid, effRuleWithReminder, nextIso);
|
|
738
|
+
const promote = kind === "project"
|
|
739
|
+
? await promoteProjectViaGui(deps, ruleParams, legOptions(options, txnId, "ui"))
|
|
740
|
+
: await runMutation(deps, "todo.make-repeating", ruleParams, legOptions(options, txnId, "ui"));
|
|
741
|
+
if (promote.kind !== "ok") {
|
|
742
|
+
// The seed persists (the two legs are not atomic) but the promote did not
|
|
743
|
+
// land. RATIFIED RULING (2026-08-15, issue #480): auto-trash our OWN seed
|
|
744
|
+
// inside the txn — it is our artifact, recreatable verbatim from the command
|
|
745
|
+
// args, and the Trash is recoverable — then disclose it. If the auto-trash
|
|
746
|
+
// itself fails, the result carries the seed's REAL, resolvable uuid with a
|
|
747
|
+
// working `delete` remediation, so cleanup is never ambiguous (the #480
|
|
748
|
+
// second bug: a failed add-repeating left a residue whose reported uuid was
|
|
749
|
+
// not actionable).
|
|
750
|
+
const patch = await cleanupSeed(deps, kind, createdUuid, promote, options, txnId);
|
|
751
|
+
return { ...promote, op, ...patch };
|
|
752
|
+
}
|
|
753
|
+
const { templateUuid } = discoveryOf(promote);
|
|
754
|
+
let { instanceUuid } = discoveryOf(promote);
|
|
755
|
+
// Post-drive verify (ANCH2 + DBLSPAWN1): the instance START must have landed on the
|
|
756
|
+
// requested `--when` — for a deadlined rule the driven Next is the deadline (when +
|
|
757
|
+
// startDaysEarlier) and the app back-shifts the start to `--when`, so the check is
|
|
758
|
+
// against `expectedStartIso`, not the raw drive date. Fail closed on mismatch rather
|
|
759
|
+
// than report a wrong-phase ok. The series EXISTS here (promote landed) but on the
|
|
760
|
+
// wrong phase, so this is a genuine partial success, NOT a seed to trash. The
|
|
761
|
+
// ORACLE is rule-kind dependent (#508) — see landedFirstStart; an after-completion
|
|
762
|
+
// series verifies against its materialized instance, and skips when it has none.
|
|
763
|
+
const afterCompletion = effRuleWithReminder.afterCompletion === true;
|
|
764
|
+
if (expectedStartIso !== undefined) {
|
|
765
|
+
const landed = landedFirstStart(deps, templateUuid, instanceUuid, afterCompletion);
|
|
766
|
+
if (!(afterCompletion && landed === null) && landed !== expectedStartIso)
|
|
767
|
+
return nextMismatch(op, templateUuid, expectedStartIso, landed);
|
|
768
|
+
}
|
|
769
|
+
const warnings = [
|
|
770
|
+
landedRuleEcho(ruleParams, expectedStartIso ?? firstOccurrenceOf(deps.db, templateUuid)),
|
|
771
|
+
PLACEMENT_NOTE,
|
|
772
|
+
];
|
|
773
|
+
// DBLSPAWN1 backstop: the deadline-mapping above keeps the seed deadline-free (no
|
|
774
|
+
// SRCFATE preserve), but any OTHER preserve trigger reaching the seed (defensive)
|
|
775
|
+
// would double-book a future first occurrence — trash the redundant instance.
|
|
776
|
+
const dbl = await trashRedundantFuturePreservedInstance(deps, kind, promote, options, txnId, startedAt, afterCompletion);
|
|
777
|
+
if (dbl !== null) {
|
|
778
|
+
warnings.push(dbl.warning);
|
|
779
|
+
if (instanceUuid === dbl.trashedUuid)
|
|
780
|
+
instanceUuid = null;
|
|
781
|
+
}
|
|
782
|
+
const offRule = offRuleFirstNote(ruleParams);
|
|
783
|
+
if (offRule !== null)
|
|
784
|
+
warnings.push(offRule);
|
|
785
|
+
if (promote.warnings !== undefined)
|
|
786
|
+
warnings.push(...promote.warnings);
|
|
787
|
+
appendPromoteSummary(deps, {
|
|
788
|
+
startedAt,
|
|
789
|
+
op,
|
|
790
|
+
txnId,
|
|
791
|
+
templateUuid,
|
|
792
|
+
instanceUuid,
|
|
793
|
+
invocation: `${op}: add "${title}" ${createdUuid} → template ${templateUuid}`,
|
|
794
|
+
requested: { title, ...effRule },
|
|
795
|
+
});
|
|
796
|
+
return promoteOk({
|
|
797
|
+
op,
|
|
798
|
+
templateUuid,
|
|
799
|
+
instanceUuid,
|
|
800
|
+
replacedUuid: createdUuid,
|
|
801
|
+
title,
|
|
802
|
+
txnId,
|
|
803
|
+
warnings,
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Fold a concrete item-level `deadline` (and/or an explicit `startDaysEarlier`)
|
|
809
|
+
* into the RULE, returning the rule to promote with and the deadline the SEED
|
|
810
|
+
* should keep (none — the rule owns it). Shared by the to-do and project
|
|
811
|
+
* add-repeating verbs so one geometry, and one set of refusals, serves both.
|
|
812
|
+
*
|
|
813
|
+
* DBLSPAWN1 (docs/lab/dblspawn1-preserved-instance.md): each occurrence is due
|
|
814
|
+
* `deadline − when` days after its start (the "Add deadlines" + "start N days
|
|
815
|
+
* earlier" dialog fields). This is the deadline the series actually wants, and
|
|
816
|
+
* mapping it up front is also what keeps the SEED deadline-free — a to-do seed
|
|
817
|
+
* carrying a deadline is SRCFATE-preserved as a materialized instance, and a
|
|
818
|
+
* future-dated first occurrence then DOUBLE-BOOKS against the template cursor
|
|
819
|
+
* and spawns a duplicate on the date (cell C). A project seed is DELETE-fate, so
|
|
820
|
+
* there the un-mapped deadline was simply LOST rather than duplicated — a
|
|
821
|
+
* quieter bug with the same cause and the same fix. A deadline needs a concrete
|
|
822
|
+
* `when` (the per-occurrence offset is measured from the start) on/after it.
|
|
823
|
+
*
|
|
824
|
+
* DEADLINE/OFFSET AGREEMENT (ruling 2026-08-18): the same rule-global offset can
|
|
825
|
+
* be named two ways — a concrete `deadline` (offset = deadline − when) OR an
|
|
826
|
+
* explicit `startDaysEarlier` N (deadline derived as when + N). The geometry
|
|
827
|
+
* when/deadline/N is OVER-DETERMINED: the dialog's start-offset is rule-global
|
|
828
|
+
* and the first occurrence's start is derived as due − N, so there is no
|
|
829
|
+
* per-first-instance gap to absorb a disagreement (unlike DACON1's off-rule-first
|
|
830
|
+
* calendar freedom). So when BOTH are given they must AGREE — an exact match is
|
|
831
|
+
* harmless redundancy, a mismatch is inexpressible and refused fast (zero
|
|
832
|
+
* mutation). Either input ALONE maps to the rule.
|
|
833
|
+
*/
|
|
834
|
+
function mapDeadlineOntoRule(baseRule, when, deadline, startDaysEarlier) {
|
|
835
|
+
if (!isIsoDate(when)) {
|
|
836
|
+
throw new RangeError("a repeating --deadline or --start-days-earlier needs a concrete --when date (the deadline " +
|
|
837
|
+
"offset is measured from each occurrence's start) — schedule the series on a YYYY-MM-DD " +
|
|
838
|
+
"--when, or drop --deadline / --start-days-earlier");
|
|
684
839
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
840
|
+
let startEarlier;
|
|
841
|
+
if (deadline !== undefined) {
|
|
842
|
+
const derived = daysBetweenIso(when, deadline);
|
|
843
|
+
if (derived < 0) {
|
|
844
|
+
throw new RangeError(`--deadline (${deadline}) must be on or after --when (${when}) — a deadline cannot ` +
|
|
845
|
+
"precede the occurrence's own start");
|
|
846
|
+
}
|
|
847
|
+
if (startDaysEarlier !== undefined && startDaysEarlier !== derived) {
|
|
848
|
+
throw new RangeError(`--deadline (${deadline}) puts each occurrence's due date ${derived} day` +
|
|
849
|
+
`${derived === 1 ? "" : "s"} after its start (--when ${when}), but ` +
|
|
850
|
+
`--start-days-earlier says ${startDaysEarlier} — these disagree. Drop one, or make them ` +
|
|
851
|
+
`agree (--start-days-earlier ${derived}, or --deadline ${addDaysIso(when, startDaysEarlier)}).`);
|
|
852
|
+
}
|
|
853
|
+
startEarlier = derived;
|
|
697
854
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
// DBLSPAWN1 backstop: the deadline-mapping above keeps the seed deadline-free (no
|
|
703
|
-
// SRCFATE preserve), but any OTHER preserve trigger reaching the seed (defensive)
|
|
704
|
-
// would double-book a future first occurrence — trash the redundant instance.
|
|
705
|
-
const dbl = await trashRedundantFuturePreservedInstance(deps, kind, promote, options, txnId, startedAt);
|
|
706
|
-
if (dbl !== null) {
|
|
707
|
-
warnings.push(dbl.warning);
|
|
708
|
-
if (instanceUuid === dbl.trashedUuid)
|
|
709
|
-
instanceUuid = null;
|
|
855
|
+
else {
|
|
856
|
+
// `--start-days-earlier N` alone: the deadline is derived as when + N. A bad N
|
|
857
|
+
// (non-integer / negative) is refused by assertRepeatRule once mapped.
|
|
858
|
+
startEarlier = startDaysEarlier;
|
|
710
859
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
appendPromoteSummary(deps, {
|
|
717
|
-
startedAt,
|
|
718
|
-
op,
|
|
719
|
-
txnId,
|
|
720
|
-
templateUuid,
|
|
721
|
-
instanceUuid,
|
|
722
|
-
invocation: `${op}: add "${title}" ${createdUuid} → template ${templateUuid}`,
|
|
723
|
-
requested: { title, ...effRule },
|
|
724
|
-
});
|
|
725
|
-
return promoteOk({
|
|
726
|
-
op,
|
|
727
|
-
templateUuid,
|
|
728
|
-
instanceUuid,
|
|
729
|
-
replacedUuid: createdUuid,
|
|
730
|
-
title,
|
|
731
|
-
txnId,
|
|
732
|
-
warnings,
|
|
733
|
-
});
|
|
860
|
+
// The RULE owns the deadline; the seed carries none.
|
|
861
|
+
return {
|
|
862
|
+
rule: { ...baseRule, deadline: true, startDaysEarlier: startEarlier },
|
|
863
|
+
seedDeadline: undefined,
|
|
864
|
+
};
|
|
734
865
|
}
|
|
735
866
|
export async function runAddRepeatingTodo(deps, params, options = {}) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
// DBLSPAWN1 (docs/lab/dblspawn1-preserved-instance.md): a concrete item-level
|
|
747
|
-
// `--deadline <date>` on add-repeating maps to the RULE's deadline — each occurrence
|
|
748
|
-
// is due `deadline − when` days after its start (the "Add deadlines" + "start N days
|
|
749
|
-
// earlier" dialog fields). This is the deadline the series actually wants, and it
|
|
750
|
-
// keeps the SEED deadline-free: a seed carrying a deadline is SRCFATE-preserved as a
|
|
751
|
-
// materialized instance, and when that first occurrence is future-dated the app
|
|
752
|
-
// DOUBLE-BOOKS it against the template cursor and spawns a duplicate on the date
|
|
753
|
-
// (cell C). The seed owning the deadline was the two-step dance the live agent hit
|
|
754
|
-
// (add-repeating dropped the rule deadline, forcing a follow-up reschedule-repeat);
|
|
755
|
-
// mapping it up front makes that unnecessary. A deadline needs a concrete `--when`
|
|
756
|
-
// (the per-occurrence offset is deadline − start) and must be on/after it.
|
|
757
|
-
//
|
|
758
|
-
// DEADLINE/OFFSET AGREEMENT (ruling 2026-08-18): the same rule-global offset can be
|
|
759
|
-
// named two ways — a concrete `--deadline <date>` (offset = deadline − when) OR an
|
|
760
|
-
// explicit `--start-days-earlier N` (offset = N, deadline derived as when + N). The
|
|
761
|
-
// geometry when/deadline/N is OVER-DETERMINED: the dialog's start-offset is
|
|
762
|
-
// rule-global and the first occurrence's start is derived as due − N, so there is no
|
|
763
|
-
// per-first-instance gap to absorb a disagreement (unlike DACON1's off-rule-first
|
|
764
|
-
// calendar freedom). So when BOTH are given they must AGREE (`deadline − when == N`)
|
|
765
|
-
// — an exact match is harmless redundancy, a mismatch is inexpressible and refused
|
|
766
|
-
// fast (zero mutation). Either input ALONE maps to the rule and keeps the seed
|
|
767
|
-
// deadline-free.
|
|
867
|
+
// The rule/add split is driven by the exhaustive key map (splitAddRepeatingRule),
|
|
868
|
+
// not by a hand-written destructure — a field added to either vocabulary lands on
|
|
869
|
+
// the right leg instead of falling between them (#549 / YANCH1 #493).
|
|
870
|
+
const { rule: baseRule, add } = splitAddRepeatingRule(params);
|
|
871
|
+
const { afterCompletion } = params;
|
|
872
|
+
const startDaysEarlier = add.startDaysEarlier;
|
|
873
|
+
// A concrete `--deadline` (or `--start-days-earlier`) belongs to the RULE, not
|
|
874
|
+
// the seed — see mapDeadlineOntoRule for the geometry and the refusals. The
|
|
875
|
+
// to-do stakes: an un-mapped seed deadline makes the app preserve the seed as a
|
|
876
|
+
// future-dated instance that double-books the cursor (DBLSPAWN1 cell C).
|
|
768
877
|
let rule = baseRule;
|
|
769
878
|
let seedDeadline = add.deadline;
|
|
770
879
|
if ((add.deadline !== undefined || startDaysEarlier !== undefined) && afterCompletion !== true) {
|
|
771
|
-
|
|
772
|
-
throw new RangeError("a repeating --deadline or --start-days-earlier needs a concrete --when date (the deadline " +
|
|
773
|
-
"offset is measured from each occurrence's start) — schedule the series on a YYYY-MM-DD " +
|
|
774
|
-
"--when, or drop --deadline / --start-days-earlier");
|
|
775
|
-
}
|
|
776
|
-
let startEarlier;
|
|
777
|
-
if (add.deadline !== undefined) {
|
|
778
|
-
const derived = daysBetweenIso(add.when, add.deadline);
|
|
779
|
-
if (derived < 0) {
|
|
780
|
-
throw new RangeError(`--deadline (${add.deadline}) must be on or after --when (${add.when}) — a deadline cannot ` +
|
|
781
|
-
"precede the occurrence's own start");
|
|
782
|
-
}
|
|
783
|
-
if (startDaysEarlier !== undefined && startDaysEarlier !== derived) {
|
|
784
|
-
throw new RangeError(`--deadline (${add.deadline}) puts each occurrence's due date ${derived} day` +
|
|
785
|
-
`${derived === 1 ? "" : "s"} after its start (--when ${add.when}), but ` +
|
|
786
|
-
`--start-days-earlier says ${startDaysEarlier} — these disagree. Drop one, or make them ` +
|
|
787
|
-
`agree (--start-days-earlier ${derived}, or --deadline ${addDaysIso(add.when, startDaysEarlier)}).`);
|
|
788
|
-
}
|
|
789
|
-
startEarlier = derived;
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
// `--start-days-earlier N` alone: the deadline is derived as when + N. A bad N
|
|
793
|
-
// (non-integer / negative) is refused by assertRepeatRule once mapped.
|
|
794
|
-
startEarlier = startDaysEarlier;
|
|
795
|
-
}
|
|
796
|
-
rule = { ...baseRule, deadline: true, startDaysEarlier: startEarlier };
|
|
797
|
-
seedDeadline = undefined; // the RULE owns the deadline; the seed carries none
|
|
880
|
+
({ rule, seedDeadline } = mapDeadlineOntoRule(baseRule, add.when, add.deadline, startDaysEarlier));
|
|
798
881
|
}
|
|
799
882
|
else if (startDaysEarlier !== undefined) {
|
|
800
883
|
// afterCompletion === true here: an after-completion repeat has no calendar
|
|
@@ -802,45 +885,54 @@ export async function runAddRepeatingTodo(deps, params, options = {}) {
|
|
|
802
885
|
throw new RangeError("--start-days-earlier applies only to a fixed-schedule deadline — an after-completion repeat " +
|
|
803
886
|
"has no calendar start to count back from; drop --after-completion or --start-days-earlier");
|
|
804
887
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
...(add.area !== undefined && { area: add.area }),
|
|
815
|
-
...(add.heading !== undefined && { heading: add.heading }),
|
|
816
|
-
...(add.createdAt !== undefined && { createdAt: add.createdAt }),
|
|
817
|
-
};
|
|
888
|
+
// The seed carries the WHOLE add vocabulary by spread (a field added to
|
|
889
|
+
// TodoAddRepeatingParams reaches the create leg without a line here), minus the
|
|
890
|
+
// two the rule took: `startDaysEarlier` is rule-only, and the deadline is
|
|
891
|
+
// whatever the geometry left for the seed (none, once mapped).
|
|
892
|
+
const addParams = { ...add };
|
|
893
|
+
delete addParams["startDaysEarlier"];
|
|
894
|
+
delete addParams["deadline"];
|
|
895
|
+
if (seedDeadline !== undefined)
|
|
896
|
+
addParams["deadline"] = seedDeadline;
|
|
818
897
|
return addRepeatingViaCreate(deps, "todo", addParams, rule, add.title, options);
|
|
819
898
|
}
|
|
820
|
-
export function runAddRepeatingProject(deps, params, options = {}) {
|
|
821
|
-
|
|
822
|
-
const rule =
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
899
|
+
export async function runAddRepeatingProject(deps, params, options = {}) {
|
|
900
|
+
// Same exhaustive rule/add split as the to-do verb (one key map, both verbs).
|
|
901
|
+
const { rule: baseRule, add } = splitAddRepeatingRule(params);
|
|
902
|
+
const { afterCompletion } = params;
|
|
903
|
+
// A concrete `--deadline` maps to the RULE here exactly as it does on the to-do
|
|
904
|
+
// verb (DBLSPAWN1 residual). The project stakes are quieter than the to-do's but
|
|
905
|
+
// no more acceptable: a project seed is DELETE-fate, so the app does not preserve
|
|
906
|
+
// it into a double-booked instance — it simply DROPS the deadline, and the series
|
|
907
|
+
// came out undeadlined with nothing said. Mapped, every occurrence is due
|
|
908
|
+
// `deadline − when` days after its start.
|
|
909
|
+
let rule = baseRule;
|
|
910
|
+
let seedDeadline = add.deadline;
|
|
911
|
+
if (add.deadline !== undefined) {
|
|
912
|
+
if (afterCompletion === true) {
|
|
913
|
+
// The to-do verb can leave a deadline on an after-completion seed because
|
|
914
|
+
// that seed SURVIVES as the series' instance. An after-completion project's
|
|
915
|
+
// seed is deleted and its instance is minted deadline-free (RSIM-P P4), so
|
|
916
|
+
// the deadline would vanish either way — refuse rather than drop it quietly.
|
|
917
|
+
throw new RangeError("an after-completion repeating project cannot carry a --deadline: the rule has no calendar " +
|
|
918
|
+
"start to measure a per-occurrence deadline from, and the created project is replaced by " +
|
|
919
|
+
"the series' own instance, which is created without one — drop --after-completion to " +
|
|
920
|
+
"deadline every occurrence, or drop --deadline");
|
|
921
|
+
}
|
|
922
|
+
({ rule, seedDeadline } = mapDeadlineOntoRule(baseRule, add.when, add.deadline, undefined));
|
|
923
|
+
}
|
|
831
924
|
// Seed a pure-AX taxonomy: an area lands a selectable AREA-view row; otherwise
|
|
832
925
|
// create in Someday (UIC4-f) so the promote skips the anytime-header problem.
|
|
833
926
|
const seedWhen = add.when ?? (add.area === undefined ? "someday" : undefined);
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
};
|
|
927
|
+
// The whole add vocabulary by spread (see the to-do verb), with the two the
|
|
928
|
+
// promote owns re-derived: the seed taxonomy `when` and the mapped deadline.
|
|
929
|
+
const addParams = { ...add };
|
|
930
|
+
delete addParams["when"];
|
|
931
|
+
delete addParams["deadline"];
|
|
932
|
+
if (seedWhen !== undefined)
|
|
933
|
+
addParams["when"] = seedWhen;
|
|
934
|
+
if (seedDeadline !== undefined)
|
|
935
|
+
addParams["deadline"] = seedDeadline;
|
|
844
936
|
return addRepeatingViaCreate(deps, "project", addParams, rule, add.title, options);
|
|
845
937
|
}
|
|
846
938
|
// ====================================================== template-direct clone
|
|
@@ -941,95 +1033,103 @@ export async function cloneTemplateViaRepromote(deps, kind, src, srcUuid, params
|
|
|
941
1033
|
},
|
|
942
1034
|
};
|
|
943
1035
|
}
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
|
|
1036
|
+
// PRE-SEED UI PREFLIGHT (#480/#512): refuse a host that cannot drive the window,
|
|
1037
|
+
// or a locked / full-screen session, BEFORE minting the plain clone (a doomed
|
|
1038
|
+
// promote would strand it). Zero mutation on refusal.
|
|
1039
|
+
const gate = await gateUiPreflight(deps, op);
|
|
947
1040
|
if (gate !== null)
|
|
948
1041
|
return gate;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
?
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
probe: { title, type: expectedType, sinceEpoch: 0 },
|
|
974
|
-
assert: [],
|
|
975
|
-
},
|
|
976
|
-
observed: null,
|
|
977
|
-
detail: "the plain clone was created but its uuid was not discovered — nothing was promoted",
|
|
978
|
-
}
|
|
979
|
-
: { ...clone, op };
|
|
980
|
-
}
|
|
981
|
-
const cloneUuid = clone.uuid;
|
|
982
|
-
// 5. Native-promote the clone with the FULL decoded rule (ruleToInverseParams
|
|
983
|
-
// carries deadline/start-earlier + the calendar anchors + ends).
|
|
984
|
-
const ruleParams = { uuid: cloneUuid, ...inverse };
|
|
985
|
-
const promote = kind === "project"
|
|
986
|
-
? await promoteProjectViaGui(deps, ruleParams, legOptions(options, txnId, "ui"))
|
|
987
|
-
: await runMutation(deps, "todo.make-repeating", ruleParams, legOptions(options, txnId, "ui"));
|
|
988
|
-
if (promote.kind !== "ok") {
|
|
989
|
-
// The plain clone persists but was not promoted — honest report (no original
|
|
990
|
-
// to roll back; the clone is a fresh row the caller can trash and retry).
|
|
991
|
-
return {
|
|
992
|
-
...promote,
|
|
993
|
-
op,
|
|
994
|
-
...("detail" in promote
|
|
1042
|
+
// COMPOSITE LOCK: clone-as-plain → promote-with-the-source's-rule is one verb;
|
|
1043
|
+
// hold one lock across both legs.
|
|
1044
|
+
return runComposite(deps, op, async () => {
|
|
1045
|
+
const startedAt = deps.now?.() ?? new Date();
|
|
1046
|
+
const txnId = newTxnId(startedAt);
|
|
1047
|
+
// 4. Mint the plain clone as an embedded leg — cloneTemplateAsPlain reaches the
|
|
1048
|
+
// clone orchestrator's plain-content path (recurrence + schedule stripped);
|
|
1049
|
+
// --title/--preserve-created ride through the CloneParams.
|
|
1050
|
+
const cloneParams = {
|
|
1051
|
+
uuid: srcUuid,
|
|
1052
|
+
...(params.title !== undefined && { title: params.title }),
|
|
1053
|
+
...(params.preserveCreated === true && { preserveCreated: true }),
|
|
1054
|
+
};
|
|
1055
|
+
const cloneOptions = {
|
|
1056
|
+
...legOptions(options, txnId),
|
|
1057
|
+
cloneTemplateAsPlain: true,
|
|
1058
|
+
};
|
|
1059
|
+
const clone = kind === "project"
|
|
1060
|
+
? await runCloneProject(deps, cloneParams, cloneOptions)
|
|
1061
|
+
: await runCloneTodo(deps, cloneParams, cloneOptions);
|
|
1062
|
+
if (clone.kind !== "ok" || clone.uuid === null) {
|
|
1063
|
+
// A nested-repeater refusal (a template CONTAINING a nested repeater) or any
|
|
1064
|
+
// clone failure surfaces coherently here — re-label it to the compound op.
|
|
1065
|
+
return clone.kind === "ok"
|
|
995
1066
|
? {
|
|
996
|
-
|
|
997
|
-
|
|
1067
|
+
kind: "verify-failed",
|
|
1068
|
+
op,
|
|
1069
|
+
reason: "mismatch",
|
|
1070
|
+
expected: {
|
|
1071
|
+
mode: "create",
|
|
1072
|
+
probe: { title, type: expectedType, sinceEpoch: 0 },
|
|
1073
|
+
assert: [],
|
|
1074
|
+
},
|
|
1075
|
+
observed: null,
|
|
1076
|
+
detail: "the plain clone was created but its uuid was not discovered — nothing was promoted",
|
|
998
1077
|
}
|
|
999
|
-
: {}
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
warnings
|
|
1078
|
+
: { ...clone, op };
|
|
1079
|
+
}
|
|
1080
|
+
const cloneUuid = clone.uuid;
|
|
1081
|
+
// 5. Native-promote the clone with the FULL decoded rule (ruleToInverseParams
|
|
1082
|
+
// carries deadline/start-earlier + the calendar anchors + ends).
|
|
1083
|
+
const ruleParams = { uuid: cloneUuid, ...inverse };
|
|
1084
|
+
const promote = kind === "project"
|
|
1085
|
+
? await promoteProjectViaGui(deps, ruleParams, legOptions(options, txnId, "ui"))
|
|
1086
|
+
: await runMutation(deps, "todo.make-repeating", ruleParams, legOptions(options, txnId, "ui"));
|
|
1087
|
+
if (promote.kind !== "ok") {
|
|
1088
|
+
// The plain clone persists but was not promoted — honest report (no original
|
|
1089
|
+
// to roll back; the clone is a fresh row the caller can trash and retry).
|
|
1090
|
+
return {
|
|
1091
|
+
...promote,
|
|
1092
|
+
op,
|
|
1093
|
+
...("detail" in promote
|
|
1094
|
+
? {
|
|
1095
|
+
detail: `${promote.detail} — the plain clone (uuid ${cloneUuid}) was created but the promote ` +
|
|
1096
|
+
`did not land; trash the clone with \`things ${kind} delete ${cloneUuid}\` and retry`,
|
|
1097
|
+
}
|
|
1098
|
+
: {}),
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
const { templateUuid, instanceUuid } = discoveryOf(promote);
|
|
1102
|
+
const warnings = [NEW_SERIES_NOTE, PLACEMENT_NOTE];
|
|
1103
|
+
if (params.preserveCreated === true) {
|
|
1104
|
+
warnings.push("--preserve-created is best-effort on a template clone: the promote may replace the clone " +
|
|
1105
|
+
"row with the new template, whose creation date is the conversion time");
|
|
1106
|
+
}
|
|
1107
|
+
if (src.repeating.paused === true) {
|
|
1108
|
+
warnings.push(`the source template was PAUSED; the new series is created UNPAUSED and begins spawning — ` +
|
|
1109
|
+
`pause it with \`things ${kind} pause-repeat\` if you want it suspended`);
|
|
1110
|
+
}
|
|
1111
|
+
if (promote.warnings !== undefined)
|
|
1112
|
+
warnings.push(...promote.warnings);
|
|
1113
|
+
// Summary WITHOUT originalUuid → undo is the add-repeating trash-both (remove
|
|
1114
|
+
// the minted series; there is no original to restore).
|
|
1115
|
+
appendPromoteSummary(deps, {
|
|
1116
|
+
startedAt,
|
|
1117
|
+
op,
|
|
1118
|
+
txnId,
|
|
1119
|
+
templateUuid,
|
|
1120
|
+
instanceUuid,
|
|
1121
|
+
invocation: `${kind}.clone (template) ${srcUuid}: clone → promote ${cloneUuid} → template ${templateUuid}`,
|
|
1122
|
+
requested: { source: srcUuid, title, ...inverse },
|
|
1123
|
+
});
|
|
1124
|
+
return promoteOk({
|
|
1125
|
+
op,
|
|
1126
|
+
templateUuid,
|
|
1127
|
+
instanceUuid,
|
|
1128
|
+
replacedUuid: cloneUuid,
|
|
1129
|
+
title,
|
|
1130
|
+
txnId,
|
|
1131
|
+
warnings,
|
|
1132
|
+
});
|
|
1033
1133
|
});
|
|
1034
1134
|
}
|
|
1035
1135
|
//# sourceMappingURL=promote-clone.js.map
|