things-api 0.17.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 -0
- package/deputy/helpers-Info.plist +27 -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 +22 -0
- 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 +128 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +27 -0
- 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 +137 -0
- package/deputy/reader/Info.plist +22 -0
- package/deputy/reader/entitlements.plist +45 -0
- package/deputy/reader/main.swift +580 -0
- package/deputy/src/main.swift +133 -0
- package/deputy/src/osascript.swift +102 -0
- package/deputy/src/server.swift +373 -0
- package/deputy/src/sqlite.swift +121 -0
- 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 +181 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.d.ts +17 -0
- package/dist/cli/commands/helpers.js +275 -0
- package/dist/cli/commands/helpers.js.map +1 -0
- package/dist/cli/commands/op-result.d.ts +15 -0
- package/dist/cli/commands/op-result.js +45 -0
- package/dist/cli/commands/op-result.js.map +1 -0
- 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 +262 -109
- 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 +53 -27
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/helpers-check.d.ts +10 -0
- package/dist/cli/helpers-check.js +59 -0
- package/dist/cli/helpers-check.js.map +1 -0
- package/dist/cli/main.js +22 -1
- 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 +22 -2
- package/dist/client.js +52 -15
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +31 -0
- package/dist/config.js +9 -0
- package/dist/config.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/db/baselines/db-v26.js +8 -4
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/db/baselines/db-v27.d.ts +42 -0
- package/dist/db/baselines/db-v27.js +47 -0
- package/dist/db/baselines/db-v27.js.map +1 -0
- package/dist/db/baselines/index.js +2 -1
- package/dist/db/baselines/index.js.map +1 -1
- package/dist/db/locate.d.ts +4 -1
- package/dist/db/locate.js +3 -1
- package/dist/db/locate.js.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.js +9 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/deputy/bridge-worker.d.ts +1 -0
- package/dist/deputy/bridge-worker.js +79 -0
- package/dist/deputy/bridge-worker.js.map +1 -0
- package/dist/deputy/bridge.d.ts +14 -0
- package/dist/deputy/bridge.js +70 -0
- package/dist/deputy/bridge.js.map +1 -0
- package/dist/deputy/client.d.ts +19 -0
- package/dist/deputy/client.js +104 -0
- package/dist/deputy/client.js.map +1 -0
- package/dist/deputy/db-facade.d.ts +18 -0
- package/dist/deputy/db-facade.js +71 -0
- package/dist/deputy/db-facade.js.map +1 -0
- package/dist/deputy/files.d.ts +1 -0
- package/dist/deputy/files.js +16 -0
- package/dist/deputy/files.js.map +1 -0
- package/dist/deputy/install.d.ts +319 -0
- package/dist/deputy/install.js +1239 -0
- package/dist/deputy/install.js.map +1 -0
- package/dist/deputy/notice.d.ts +6 -0
- package/dist/deputy/notice.js +28 -0
- package/dist/deputy/notice.js.map +1 -0
- package/dist/deputy/notices.d.ts +31 -0
- package/dist/deputy/notices.js +94 -0
- package/dist/deputy/notices.js.map +1 -0
- package/dist/deputy/osa.d.ts +21 -0
- package/dist/deputy/osa.js +72 -0
- package/dist/deputy/osa.js.map +1 -0
- package/dist/deputy/protocol.d.ts +140 -0
- package/dist/deputy/protocol.js +143 -0
- package/dist/deputy/protocol.js.map +1 -0
- package/dist/deputy/routing.d.ts +84 -0
- package/dist/deputy/routing.js +422 -0
- package/dist/deputy/routing.js.map +1 -0
- package/dist/deputy/shortcuts-exec.d.ts +10 -0
- package/dist/deputy/shortcuts-exec.js +50 -0
- package/dist/deputy/shortcuts-exec.js.map +1 -0
- package/dist/diagnose.d.ts +57 -2
- package/dist/diagnose.js +105 -3
- 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 +31 -5
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +27 -1
- package/dist/mcp/server.js +300 -97
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/mappers.d.ts +12 -3
- package/dist/model/mappers.js +6 -1
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/recurrence.d.ts +5 -1
- package/dist/model/recurrence.js +19 -1
- package/dist/model/recurrence.js.map +1 -1
- package/dist/model/template-projection.d.ts +26 -0
- package/dist/model/template-projection.js +113 -0
- package/dist/model/template-projection.js.map +1 -0
- package/dist/op-result.d.ts +55 -0
- package/dist/op-result.js +161 -0
- package/dist/op-result.js.map +1 -0
- package/dist/read/detail.js +7 -1
- package/dist/read/detail.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/queries.js +8 -1
- package/dist/read/queries.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 +35 -4
- package/dist/read/views.js +120 -18
- 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/sync-health.js +6 -2
- package/dist/sync-health.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/accessibility-probe.js +4 -2
- package/dist/write/accessibility-probe.js.map +1 -1
- package/dist/write/automation-probe.js +5 -1
- package/dist/write/automation-probe.js.map +1 -1
- package/dist/write/availability.d.ts +1 -1
- package/dist/write/availability.js +10 -6
- 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 +170 -237
- package/dist/write/commands.js.map +1 -1
- package/dist/write/experimental.d.ts +19 -0
- package/dist/write/experimental.js +50 -0
- package/dist/write/experimental.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/move.js +14 -6
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +64 -25
- 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 +78 -2
- package/dist/write/pipeline.js +249 -4
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +8 -0
- package/dist/write/pre-state.js +62 -17
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +635 -367
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +95 -29
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-anchor.d.ts +30 -0
- package/dist/write/repeat-anchor.js +166 -1
- package/dist/write/repeat-anchor.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +28 -1
- package/dist/write/repeat-rule.js +64 -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 +10 -16
- 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/session-reachability.d.ts +55 -0
- package/dist/write/vectors/session-reachability.js +56 -8
- package/dist/write/vectors/session-reachability.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +7 -16
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.d.ts +16 -1
- package/dist/write/vectors/simulator.js +68 -12
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +124 -1
- package/dist/write/vectors/ui-recipes.d.ts +19 -0
- package/dist/write/vectors/ui-recipes.js +158 -38
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +89 -0
- package/dist/write/vectors/ui.js +504 -49
- 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/dist/write/verify/poller.d.ts +13 -0
- package/dist/write/verify/poller.js +38 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +9 -1
- package/schema/envelope.schema.json +5 -3
- package/scripts/build-helpers.sh +109 -0
- package/scripts/deputy-cert-setup.sh +60 -0
- package/skills/things-cli/SKILL.md +12 -5
- 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 +5 -1
- package/skills/things-cli/references/gui.md +1 -0
- package/skills/things-cli/references/repeating.md +75 -0
- package/skills/things-cli/references/tag-cleanup.md +64 -0
package/dist/write/commands.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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 {
|
|
5
|
+
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";
|
|
6
|
+
import { assertNotesModesExclusive, normalizeReminder, updateAssertions, updateWireParams, whenAssertions, whenWithReminder, } from "./update-fields.js";
|
|
7
7
|
import { resolveTagRefs } from "./tag-refs.js";
|
|
8
8
|
import { PRIVATE_REORDER_COMMAND } from "./experimental.js";
|
|
9
9
|
import { assertRepeatRule } from "./repeat-rule.js";
|
|
10
|
-
import { deadlineDriveNext } from "./repeat-anchor.js";
|
|
10
|
+
import { assessOffRuleFirst, deadlineDriveNext, deriveFixedAnchor } from "./repeat-anchor.js";
|
|
11
11
|
import { expectedRuleAssertions } from "./repeat-asserts.js";
|
|
12
12
|
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";
|
|
13
|
+
import { areaReorderSidebarRecipe, convertToProjectRecipe, dissolveHeadingRecipe, headingConvertToProjectRecipe, moveHeadingToProjectRecipe, makeRepeatingRecipe, createNextCopyRecipe, pauseRepeatRecipe, projectMakeRepeatingRecipe, projectPauseRepeatRecipe, projectRescheduleRepeatRecipe, projectResumeRepeatRecipe, rescheduleRepeatRecipe, resumeRepeatRecipe, } from "./vectors/ui-recipes.js";
|
|
14
14
|
import { buildRepeatingFingerprint } from "./verify/delta.js";
|
|
15
15
|
// ------------------------------------------------------------------ helpers
|
|
16
16
|
function thingsUrl(command, params, token) {
|
|
@@ -65,65 +65,6 @@ function q(value) {
|
|
|
65
65
|
function unsupportedVector(op, vector) {
|
|
66
66
|
throw new Error(`${op} cannot be compiled for vector ${vector} (planner bug)`);
|
|
67
67
|
}
|
|
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
68
|
function sortedTags(tags) {
|
|
128
69
|
return [...tags].toSorted();
|
|
129
70
|
}
|
|
@@ -139,21 +80,28 @@ function applyTagRefs(db, pre, tags) {
|
|
|
139
80
|
pre.missingTags = res.missing;
|
|
140
81
|
pre.resolvedTagTitles = res.titles;
|
|
141
82
|
}
|
|
142
|
-
/** Round-trip normalization: "6:5"-style inputs → canonical "06:05". */
|
|
143
|
-
function normalizeReminder(time) {
|
|
144
|
-
return decodeReminderTime(encodeReminderTime(time)) ?? time;
|
|
145
|
-
}
|
|
146
83
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
84
|
+
* Is a destination container SUPPLIED? A PRESENCE check, not a duck-test (#580):
|
|
85
|
+
* the parameter schema (param-schema.ts) has already proven that a present ref is
|
|
86
|
+
* a `{uuid}`/`{title}` object, so anything else reaching here is an engine bug,
|
|
87
|
+
* not caller input — and it THROWS rather than reading as "no destination given",
|
|
88
|
+
* which is precisely how a bare-string `project` used to compile an Inbox capture
|
|
89
|
+
* with no placement and no placement assertion, then verify clean.
|
|
149
90
|
*/
|
|
150
|
-
function
|
|
151
|
-
if (
|
|
152
|
-
return
|
|
153
|
-
|
|
91
|
+
export function containerGiven(ref) {
|
|
92
|
+
if (ref === undefined || ref === null)
|
|
93
|
+
return false;
|
|
94
|
+
assertContainerRef(ref);
|
|
95
|
+
return true;
|
|
154
96
|
}
|
|
155
|
-
|
|
156
|
-
|
|
97
|
+
/** Belt-and-braces: a present container reference must carry a uuid or a title. */
|
|
98
|
+
export function assertContainerRef(ref) {
|
|
99
|
+
if (typeof ref !== "object" || Array.isArray(ref)) {
|
|
100
|
+
throw new RangeError(`a container reference must be an object naming a uuid or a title — received ${JSON.stringify(ref)}`);
|
|
101
|
+
}
|
|
102
|
+
if (ref.uuid === undefined && ref.title === undefined) {
|
|
103
|
+
throw new RangeError("a container reference must name a uuid or a title — it names neither");
|
|
104
|
+
}
|
|
157
105
|
}
|
|
158
106
|
/**
|
|
159
107
|
* The create-probe title for the duplicate / identity-replacement ops, whose
|
|
@@ -351,108 +299,35 @@ const todoAdd = {
|
|
|
351
299
|
},
|
|
352
300
|
};
|
|
353
301
|
/**
|
|
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.
|
|
302
|
+
* The to-do / project update pair. Both legs — the URL parameters and the
|
|
303
|
+
* expected-delta assertions — are DERIVED from the single exhaustive registry in
|
|
304
|
+
* update-fields.ts (the #491 exhaustive-map doctrine): the only per-verb
|
|
305
|
+
* difference is the URL command name, so neither verb can accept a field the
|
|
306
|
+
* other drops, and a field added to `UpdateFields` breaks compilation there
|
|
307
|
+
* until every leg handles it.
|
|
366
308
|
*/
|
|
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;
|
|
309
|
+
function updateSpec(op, urlCommand) {
|
|
310
|
+
return {
|
|
311
|
+
op,
|
|
312
|
+
hazards: ["H-UNKNOWN-DESTINATION", "H-REPEAT-SCHEDULE", "H-REMINDER-SCOPE"],
|
|
313
|
+
preRead(db, params, now) {
|
|
314
|
+
assertNotesModesExclusive(params);
|
|
315
|
+
const pre = emptyPreState();
|
|
316
|
+
pre.todayIso = localToday(now);
|
|
317
|
+
pre.target = loadTarget(db, params.uuid);
|
|
318
|
+
return pre;
|
|
319
|
+
},
|
|
320
|
+
expectedDelta(pre, params, ctx) {
|
|
321
|
+
return { mode: "update", uuid: params.uuid, assert: updateAssertions(params, pre, ctx) };
|
|
322
|
+
},
|
|
323
|
+
compile(params, vector, pre, ctx) {
|
|
324
|
+
if (vector !== "url-scheme")
|
|
325
|
+
unsupportedVector(op, vector);
|
|
326
|
+
return thingsUrl(urlCommand, { id: params.uuid, ...updateWireParams(params, pre) }, ctx.token);
|
|
327
|
+
},
|
|
328
|
+
};
|
|
408
329
|
}
|
|
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
|
-
};
|
|
330
|
+
const todoUpdate = updateSpec("todo.update", "update");
|
|
456
331
|
function statusSpec(op, urlParam, asStatus, scriptStatus) {
|
|
457
332
|
return {
|
|
458
333
|
op,
|
|
@@ -826,6 +701,14 @@ const projectAdd = {
|
|
|
826
701
|
const area = pre.destArea?.resolved;
|
|
827
702
|
if (area !== undefined && area !== null)
|
|
828
703
|
assert.push({ field: "area.uuid", equals: area.uuid });
|
|
704
|
+
// KNOWN SHALLOW SPOT (audited 2026-08-23, exhaustive-map sweep): the seeded
|
|
705
|
+
// children — `todos` and the structured `items` (headings + rich children) —
|
|
706
|
+
// contribute NO assertion, so a project born with an empty body still
|
|
707
|
+
// verifies `ok`. Unlike a to-do's `checklistItems` (asserted through
|
|
708
|
+
// `checklistTitles`) there is no child-list field on the delta vocabulary to
|
|
709
|
+
// compare against, and adding one needs probe evidence that the imported
|
|
710
|
+
// children are readable inside the verify window before a missing child may
|
|
711
|
+
// be called a failure. Tracked in docs/up-next.md; do NOT "fix" it blind.
|
|
829
712
|
return {
|
|
830
713
|
mode: "create",
|
|
831
714
|
probe: {
|
|
@@ -873,55 +756,9 @@ const projectAdd = {
|
|
|
873
756
|
}, ctx.token);
|
|
874
757
|
},
|
|
875
758
|
};
|
|
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
|
-
};
|
|
759
|
+
// Projects carry the same reminderTime codec and notes/schedule semantics as
|
|
760
|
+
// to-dos (A3/E18) — same registry, same legs, only the URL command differs.
|
|
761
|
+
const projectUpdate = updateSpec("project.update", "update-project");
|
|
925
762
|
const projectSetTags = {
|
|
926
763
|
op: "project.set-tags",
|
|
927
764
|
hazards: ["H-UNKNOWN-DESTINATION", "H-UNKNOWN-TAG"],
|
|
@@ -1921,16 +1758,39 @@ function ruleExtras(params) {
|
|
|
1921
1758
|
};
|
|
1922
1759
|
}
|
|
1923
1760
|
/**
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
1927
|
-
*
|
|
1928
|
-
*
|
|
1929
|
-
* make/add
|
|
1930
|
-
*
|
|
1761
|
+
* The reschedule's EFFECTIVE rule params: the requested params PLUS the calendar
|
|
1762
|
+
* anchor DERIVED from `--when` when none was given explicitly (YANCH1 #493 — the
|
|
1763
|
+
* SAME derive-and-drive make/add-repeating apply upstream in promote-clone.ts).
|
|
1764
|
+
* The anchor is derived from the deadline-shifted drive date (`deadlineDriveNext`
|
|
1765
|
+
* = when + startDaysEarlier) so a deadlined rule's anchor names the DUE date,
|
|
1766
|
+
* exactly as make/add. A rule-only reschedule (no `--when`) or an explicit anchor
|
|
1767
|
+
* flag leaves params UNCHANGED (deriveFixedAnchor returns an empty patch).
|
|
1768
|
+
*
|
|
1769
|
+
* This is the single source of truth used by BOTH the compile (`reschedRuleExtras`
|
|
1770
|
+
* → the recipe DRIVES the anchor pop-ups) AND expectedDelta (`expectedRuleAssertions`
|
|
1771
|
+
* ASSERTS the same anchor), so the drive vocabulary and the verify vocabulary
|
|
1772
|
+
* derive IDENTICALLY. Without it a `--when`-only reschedule left the anchor at the
|
|
1773
|
+
* dialog's untouched default (yearly January 1, monthly 1st, weekly Sunday) — the
|
|
1774
|
+
* DRIVE never touched the anchor pop-ups while the reschedule silently kept the old
|
|
1775
|
+
* placement (the RSPA1 live failure: a yearly `--when`-only reschedule whose anchor
|
|
1776
|
+
* was never driven).
|
|
1777
|
+
*/
|
|
1778
|
+
function reschedEffParams(params) {
|
|
1779
|
+
return { ...params, ...deriveFixedAnchor(params, deadlineDriveNext(params)) };
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Recipe extras for a RESCHEDULE: the derived-anchor effective params (so the
|
|
1783
|
+
* recipe drives the weekly/monthly/yearly anchor pop-ups for a `--when`-only
|
|
1784
|
+
* reschedule), with the "Next:" drive date deadline-adjusted (YANCH1 #493) —
|
|
1785
|
+
* `--when` is the scheduled START, but a deadlined rule anchors on the DEADLINE,
|
|
1786
|
+
* so the dialog's "Next:" field is driven with `when + startDaysEarlier` and the
|
|
1787
|
+
* app back-shifts the instance start to `--when`. The expectedDelta keeps the RAW
|
|
1788
|
+
* `params.next` (the cursor asserts the START). make/add-repeating apply this shift
|
|
1789
|
+
* upstream (promote-clone.ts) and pass an already-adjusted rule, so only the
|
|
1790
|
+
* reschedule compile needs it here.
|
|
1931
1791
|
*/
|
|
1932
1792
|
function reschedRuleExtras(params) {
|
|
1933
|
-
const base = ruleExtras(params);
|
|
1793
|
+
const base = ruleExtras(reschedEffParams(params));
|
|
1934
1794
|
const drive = deadlineDriveNext(params);
|
|
1935
1795
|
return drive !== undefined ? { ...base, next: drive } : base;
|
|
1936
1796
|
}
|
|
@@ -2002,11 +1862,22 @@ const todoRescheduleRepeat = {
|
|
|
2002
1862
|
// the post-drive verify catches a wrong-anchor landing. A shallow unit+interval
|
|
2003
1863
|
// subset false-noop'd a reschedule that changed only the monthly anchor /
|
|
2004
1864
|
// deadline / ends (#491). ALSO capture the whole prior rule (+ deadline flag)
|
|
2005
|
-
// so the undo can re-drive it faithfully.
|
|
1865
|
+
// so the undo can re-drive it faithfully. The DERIVED-anchor effective params
|
|
1866
|
+
// (reschedEffParams) are asserted — the SAME bag the compile drives — so a
|
|
1867
|
+
// `--when`-only reschedule verifies the derived anchor the drive lands (RSPA1).
|
|
1868
|
+
const eff = reschedEffParams(params);
|
|
2006
1869
|
return {
|
|
2007
1870
|
mode: "update",
|
|
2008
1871
|
uuid: params.uuid,
|
|
2009
|
-
|
|
1872
|
+
// includeCursor asserts the first-occurrence cursor == --when. For an OFF-RULE
|
|
1873
|
+
// first (explicit anchor ≠ --when) the cursor lands on the next RULE-ALIGNED
|
|
1874
|
+
// occurrence, not --when (DACON1 cell DC4: --when Oct 16 with an Oct-16 due
|
|
1875
|
+
// anchor cursors to the next aligned start, 2029-10-02), so asserting it would
|
|
1876
|
+
// false-fail an honored off-rule reschedule — drop the cursor assertion there
|
|
1877
|
+
// and verify the rule anchor only.
|
|
1878
|
+
assert: expectedRuleAssertions(eff, {
|
|
1879
|
+
includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
|
|
1880
|
+
}),
|
|
2010
1881
|
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
2011
1882
|
};
|
|
2012
1883
|
},
|
|
@@ -2038,6 +1909,56 @@ const todoPauseRepeat = {
|
|
|
2038
1909
|
return uiDrive(pauseRepeatRecipe(params.uuid));
|
|
2039
1910
|
},
|
|
2040
1911
|
};
|
|
1912
|
+
/**
|
|
1913
|
+
* `Items ▸ Repeat ▸ Create Next Copy` (CNC1) — materialize the pending
|
|
1914
|
+
* occurrence and advance the series. The INTERNAL first leg of the
|
|
1915
|
+
* template-mutation composites (template-mutation.ts); it is not a user-facing
|
|
1916
|
+
* verb, because a bare "spawn one now" has no use we have been asked for and
|
|
1917
|
+
* every honest use of it mutates the row it mints.
|
|
1918
|
+
*
|
|
1919
|
+
* Verified as a CREATE: the minted row wears the template's own title, is a
|
|
1920
|
+
* to-do, carries a fresh (gesture wall-clock) creationDate, and is not one of
|
|
1921
|
+
* the rows the series already had — which is exactly the create-probe's
|
|
1922
|
+
* gauntlet, and it hands the composite back the minted uuid it needs. The one
|
|
1923
|
+
* field assertion is the FK: the discovered row must be an instance of THIS
|
|
1924
|
+
* template, so a same-titled row created by anything else in the window cannot
|
|
1925
|
+
* pass. The cursor advance is deliberately NOT asserted here — a create delta
|
|
1926
|
+
* asserts against the created row, and the composite re-reads the template
|
|
1927
|
+
* afterwards anyway to disclose when the next occurrence lands.
|
|
1928
|
+
*/
|
|
1929
|
+
const todoCreateNextCopy = {
|
|
1930
|
+
op: "todo.create-next-copy",
|
|
1931
|
+
hazards: UI_HAZARDS,
|
|
1932
|
+
preRead(db, params) {
|
|
1933
|
+
const pre = emptyPreState();
|
|
1934
|
+
pre.target = loadTarget(db, params.uuid);
|
|
1935
|
+
// The series' existing rows — the template plus every instance it has
|
|
1936
|
+
// already spawned, all of which share its title. They are the create
|
|
1937
|
+
// probe's exclusion set, so "the new row" is unambiguous even on a series
|
|
1938
|
+
// whose occurrences all look alike.
|
|
1939
|
+
pre.sameTitleUuids = seriesRowUuids(db, params.uuid);
|
|
1940
|
+
return pre;
|
|
1941
|
+
},
|
|
1942
|
+
expectedDelta(pre, params, ctx) {
|
|
1943
|
+
const target = pre.target;
|
|
1944
|
+
const title = target !== null && target.type !== "heading" ? target.title : "";
|
|
1945
|
+
return {
|
|
1946
|
+
mode: "create",
|
|
1947
|
+
probe: {
|
|
1948
|
+
title,
|
|
1949
|
+
type: "to-do",
|
|
1950
|
+
sinceEpoch: ctx.nowEpoch - 2,
|
|
1951
|
+
excludeUuids: pre.sameTitleUuids,
|
|
1952
|
+
},
|
|
1953
|
+
assert: [{ field: "repeating.templateUuid", equals: params.uuid }],
|
|
1954
|
+
};
|
|
1955
|
+
},
|
|
1956
|
+
compile(params, vector) {
|
|
1957
|
+
if (vector !== "ui")
|
|
1958
|
+
unsupportedVector(this.op, vector);
|
|
1959
|
+
return uiDrive(createNextCopyRecipe(params.uuid));
|
|
1960
|
+
},
|
|
1961
|
+
};
|
|
2041
1962
|
const todoResumeRepeat = {
|
|
2042
1963
|
op: "todo.resume-repeat",
|
|
2043
1964
|
hazards: UI_HAZARDS,
|
|
@@ -2080,11 +2001,22 @@ const projectRescheduleRepeat = {
|
|
|
2080
2001
|
// capture the prior rule (+ deadline flag) for the faithful undo. Assert the
|
|
2081
2002
|
// FULL requested rule (#491) via expectedRuleAssertions — same completeness as
|
|
2082
2003
|
// todo.reschedule-repeat (a shallow unit+interval subset false-noop'd an
|
|
2083
|
-
// anchor/deadline/ends-only reschedule).
|
|
2004
|
+
// anchor/deadline/ends-only reschedule). The DERIVED-anchor effective params
|
|
2005
|
+
// (reschedEffParams) are asserted — the SAME bag the compile drives — so a
|
|
2006
|
+
// `--when`-only reschedule verifies the derived anchor the drive lands (RSPA1).
|
|
2007
|
+
const eff = reschedEffParams(params);
|
|
2084
2008
|
return {
|
|
2085
2009
|
mode: "update",
|
|
2086
2010
|
uuid: params.uuid,
|
|
2087
|
-
|
|
2011
|
+
// includeCursor asserts the first-occurrence cursor == --when. For an OFF-RULE
|
|
2012
|
+
// first (explicit anchor ≠ --when) the cursor lands on the next RULE-ALIGNED
|
|
2013
|
+
// occurrence, not --when (DACON1 cell DC4: --when Oct 16 with an Oct-16 due
|
|
2014
|
+
// anchor cursors to the next aligned start, 2029-10-02), so asserting it would
|
|
2015
|
+
// false-fail an honored off-rule reschedule — drop the cursor assertion there
|
|
2016
|
+
// and verify the rule anchor only.
|
|
2017
|
+
assert: expectedRuleAssertions(eff, {
|
|
2018
|
+
includeCursor: assessOffRuleFirst(eff)?.kind !== "honored",
|
|
2019
|
+
}),
|
|
2088
2020
|
capture: [{ field: "repeating.rule" }, { field: "repeating.deadlined" }],
|
|
2089
2021
|
};
|
|
2090
2022
|
},
|
|
@@ -2567,6 +2499,7 @@ export const COMMANDS = {
|
|
|
2567
2499
|
"todo.make-repeating": todoMakeRepeating,
|
|
2568
2500
|
"todo.reschedule-repeat": todoRescheduleRepeat,
|
|
2569
2501
|
"todo.pause-repeat": todoPauseRepeat,
|
|
2502
|
+
"todo.create-next-copy": todoCreateNextCopy,
|
|
2570
2503
|
"todo.resume-repeat": todoResumeRepeat,
|
|
2571
2504
|
"todo.convert-to-project": todoConvertToProject,
|
|
2572
2505
|
"project.reschedule-repeat": projectRescheduleRepeat,
|