things-api 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/dist/audit/schema.d.ts +7 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/canonical-ref.d.ts +21 -0
- package/dist/cli/canonical-ref.js +23 -0
- package/dist/cli/canonical-ref.js.map +1 -0
- package/dist/cli/commands/area.d.ts +18 -11
- package/dist/cli/commands/area.js +81 -77
- package/dist/cli/commands/area.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +1 -2
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install-skill.d.ts +19 -4
- package/dist/cli/commands/install-skill.js +130 -52
- package/dist/cli/commands/install-skill.js.map +1 -1
- package/dist/cli/commands/mcp.js +21 -1
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/project.d.ts +1 -0
- package/dist/cli/commands/project.js +92 -48
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/cli/commands/reads.d.ts +5 -1
- package/dist/cli/commands/reads.js +110 -31
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/setup.js +11 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/show.js +75 -36
- package/dist/cli/commands/show.js.map +1 -1
- package/dist/cli/commands/todo.js +2 -2
- package/dist/cli/commands/todo.js.map +1 -1
- package/dist/cli/commands/writes.js +795 -144
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/did-you-mean.d.ts +14 -3
- package/dist/cli/did-you-mean.js +44 -7
- package/dist/cli/did-you-mean.js.map +1 -1
- package/dist/cli/dry-run.d.ts +55 -0
- package/dist/cli/dry-run.js +92 -0
- package/dist/cli/dry-run.js.map +1 -0
- package/dist/cli/glyphs.d.ts +16 -2
- package/dist/cli/glyphs.js +60 -12
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.js +69 -13
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +8 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/move-hint.d.ts +3 -2
- package/dist/cli/move-hint.js +2 -2
- package/dist/cli/move-hint.js.map +1 -1
- package/dist/cli/read-driver.d.ts +53 -5
- package/dist/cli/read-driver.js +82 -15
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/ref-render.d.ts +14 -0
- package/dist/cli/ref-render.js +9 -0
- package/dist/cli/ref-render.js.map +1 -0
- package/dist/cli/render.d.ts +40 -14
- package/dist/cli/render.js +128 -55
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +7 -5
- package/dist/cli/resolve-invocation.js +16 -7
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/skill-check.d.ts +12 -6
- package/dist/cli/skill-check.js +31 -14
- package/dist/cli/skill-check.js.map +1 -1
- package/dist/cli/skill.d.ts +8 -0
- package/dist/cli/skill.js +10 -0
- package/dist/cli/skill.js.map +1 -1
- package/dist/cli/verb-hint.js +1 -1
- package/dist/cli/verb-hint.js.map +1 -1
- package/dist/client.d.ts +158 -26
- package/dist/client.js +254 -38
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +66 -2
- package/dist/config.js +120 -12
- package/dist/config.js.map +1 -1
- package/dist/contracts.d.ts +187 -28
- package/dist/contracts.js +22 -1
- package/dist/contracts.js.map +1 -1
- package/dist/db/baselines/db-v26.js +4 -1
- package/dist/db/baselines/db-v26.js.map +1 -1
- package/dist/diagnose.d.ts +34 -2
- package/dist/diagnose.js +36 -1
- package/dist/diagnose.js.map +1 -1
- package/dist/index.d.ts +21 -8
- package/dist/index.js +26 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +790 -322
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +57 -4
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.d.ts +2 -0
- package/dist/model/mappers.js +44 -2
- package/dist/model/mappers.js.map +1 -1
- package/dist/model/recurrence.d.ts +8 -1
- package/dist/model/recurrence.js.map +1 -1
- package/dist/model/serialize.js +15 -3
- package/dist/model/serialize.js.map +1 -1
- package/dist/read/area-filter.d.ts +76 -0
- package/dist/read/area-filter.js +58 -0
- package/dist/read/area-filter.js.map +1 -0
- package/dist/read/area-view.d.ts +22 -11
- package/dist/read/area-view.js +54 -28
- package/dist/read/area-view.js.map +1 -1
- package/dist/read/detail.js +15 -8
- package/dist/read/detail.js.map +1 -1
- package/dist/read/predicates.d.ts +18 -0
- package/dist/read/predicates.js +19 -0
- package/dist/read/predicates.js.map +1 -1
- package/dist/read/project-view.d.ts +91 -15
- package/dist/read/project-view.js +135 -37
- package/dist/read/project-view.js.map +1 -1
- package/dist/read/pseudo-area.d.ts +29 -0
- package/dist/read/pseudo-area.js +27 -0
- package/dist/read/pseudo-area.js.map +1 -0
- package/dist/read/queries.d.ts +175 -19
- package/dist/read/queries.js +376 -32
- package/dist/read/queries.js.map +1 -1
- package/dist/read/scope.d.ts +126 -0
- package/dist/read/scope.js +161 -0
- package/dist/read/scope.js.map +1 -0
- package/dist/read/search-rank.d.ts +2 -5
- package/dist/read/shape.d.ts +181 -0
- package/dist/read/shape.js +737 -0
- package/dist/read/shape.js.map +1 -0
- package/dist/read/show-target.d.ts +9 -1
- package/dist/read/show-target.js +77 -17
- package/dist/read/show-target.js.map +1 -1
- package/dist/read/stage.d.ts +199 -0
- package/dist/read/stage.js +125 -0
- package/dist/read/stage.js.map +1 -0
- package/dist/read/truncation.d.ts +7 -12
- package/dist/read/truncation.js +56 -21
- package/dist/read/truncation.js.map +1 -1
- package/dist/read/views.d.ts +68 -16
- package/dist/read/views.js +206 -52
- package/dist/read/views.js.map +1 -1
- package/dist/surface-copy.d.ts +16 -0
- package/dist/surface-copy.js +16 -0
- package/dist/surface-copy.js.map +1 -1
- package/dist/write/batch.d.ts +49 -10
- package/dist/write/batch.js +423 -71
- package/dist/write/batch.js.map +1 -1
- package/dist/write/commands.js +268 -55
- package/dist/write/commands.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +71 -10
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.d.ts +10 -1
- package/dist/write/heading.js +35 -5
- package/dist/write/heading.js.map +1 -1
- package/dist/write/make-repeating-project.d.ts +2 -2
- package/dist/write/make-repeating-project.js +9 -9
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/move.d.ts +134 -0
- package/dist/write/move.js +1874 -0
- package/dist/write/move.js.map +1 -0
- package/dist/write/operations.d.ts +105 -19
- package/dist/write/operations.js +56 -7
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +31 -0
- package/dist/write/opid.js +30 -0
- package/dist/write/opid.js.map +1 -0
- package/dist/write/pipeline.d.ts +70 -3
- package/dist/write/pipeline.js +198 -33
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +172 -8
- package/dist/write/pre-state.js +467 -25
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/reorder.d.ts +23 -3
- package/dist/write/reorder.js +2106 -86
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/reversibility.js +19 -7
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/scope-guard.d.ts +31 -0
- package/dist/write/scope-guard.js +162 -0
- package/dist/write/scope-guard.js.map +1 -0
- package/dist/write/undo.js +120 -13
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/applescript.js +16 -7
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +1 -1
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +83 -27
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +11 -0
- package/dist/write/vectors/ui-certification.d.ts +3 -3
- package/dist/write/vectors/ui-certification.js +41 -5
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-drag.d.ts +21 -1
- package/dist/write/vectors/ui-drag.js +138 -27
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +21 -1
- package/dist/write/vectors/ui-recipes.js +166 -7
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +25 -1
- package/dist/write/vectors/ui.js +142 -33
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -1
- package/dist/write/verify/delta.js +106 -29
- package/dist/write/verify/delta.js.map +1 -1
- package/package.json +7 -1
- package/schema/envelope.schema.json +397 -0
- package/skills/things-cli/SKILL.md +36 -17
- package/skills/things-cli/references/banner.md +35 -0
- package/skills/things-cli/references/contracts.md +32 -9
- package/skills/things-cli/references/errors.md +57 -0
- package/skills/things-cli/references/gui.md +1 -1
- package/skills/things-cli/references/model.md +18 -7
- package/skills/things-cli/references/ordering.md +88 -0
package/dist/write/pre-state.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { encodePackedDate, localToday } from "../model/dates.js";
|
|
2
2
|
import { TASK_STATUS_FROM_DB } from "../model/entities.js";
|
|
3
3
|
import { byUuid } from "../read/detail.js";
|
|
4
|
-
import { resolveNamedRef } from "../read/queries.js";
|
|
4
|
+
import { deadNameMatchHint, resolveHeadingRef, resolveNamedRef } from "../read/queries.js";
|
|
5
5
|
export function emptyPreState() {
|
|
6
6
|
return {
|
|
7
7
|
target: null,
|
|
8
|
+
todayIso: localToday(new Date()),
|
|
8
9
|
destProject: null,
|
|
9
10
|
destProjectStatus: null,
|
|
10
11
|
destArea: null,
|
|
@@ -14,6 +15,7 @@ export function emptyPreState() {
|
|
|
14
15
|
parentTag: null,
|
|
15
16
|
entityTarget: null,
|
|
16
17
|
childTags: [],
|
|
18
|
+
areaMembers: null,
|
|
17
19
|
openChildren: [],
|
|
18
20
|
canceledChildren: [],
|
|
19
21
|
completedChildren: [],
|
|
@@ -26,8 +28,62 @@ export function emptyPreState() {
|
|
|
26
28
|
projectRepeat: null,
|
|
27
29
|
repeatSubtreeUuids: null,
|
|
28
30
|
headingConvert: null,
|
|
31
|
+
headingMove: null,
|
|
32
|
+
headingMoveToProject: null,
|
|
33
|
+
headingDissolve: null,
|
|
29
34
|
};
|
|
30
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Read a project's current heading order and compute the FULL target order
|
|
38
|
+
* after moving `headings` (an ordered block) to the requested placement. The
|
|
39
|
+
* block lands contiguously in selection order; every other heading keeps its
|
|
40
|
+
* relative order. Children follow their heading on the wire (scf P1). Illegal
|
|
41
|
+
* requests (unknown/duplicated movee, an anchor that is a movee or absent)
|
|
42
|
+
* collect problems and leave the order unchanged.
|
|
43
|
+
*/
|
|
44
|
+
export function computeHeadingMovePre(db, project, headings, placement) {
|
|
45
|
+
const projectUuid = project.resolved?.uuid ?? "";
|
|
46
|
+
const current = db
|
|
47
|
+
.prepare(`SELECT uuid FROM TMTask WHERE type = 2 AND trashed = 0 AND project = ? ORDER BY "index"`)
|
|
48
|
+
.all(projectUuid).map((r) => r.uuid);
|
|
49
|
+
const problems = [];
|
|
50
|
+
const currentSet = new Set(current);
|
|
51
|
+
const movees = new Set();
|
|
52
|
+
for (const h of headings) {
|
|
53
|
+
if (movees.has(h))
|
|
54
|
+
problems.push(`heading ${h} listed more than once`);
|
|
55
|
+
movees.add(h);
|
|
56
|
+
}
|
|
57
|
+
if (headings.length === 0)
|
|
58
|
+
problems.push("no headings given");
|
|
59
|
+
for (const h of headings) {
|
|
60
|
+
if (!currentSet.has(h))
|
|
61
|
+
problems.push(`${h} is not a heading of this project`);
|
|
62
|
+
}
|
|
63
|
+
let anchor = null;
|
|
64
|
+
if ("before" in placement || "after" in placement) {
|
|
65
|
+
anchor = "before" in placement ? placement.before : placement.after;
|
|
66
|
+
if (!currentSet.has(anchor)) {
|
|
67
|
+
problems.push(`anchor heading ${anchor} is not a heading of this project`);
|
|
68
|
+
}
|
|
69
|
+
if (movees.has(anchor)) {
|
|
70
|
+
problems.push("the anchor heading cannot also be one of the moved headings");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (problems.length > 0)
|
|
74
|
+
return { project, current, targetOrder: current, problems };
|
|
75
|
+
const rest = current.filter((u) => !movees.has(u));
|
|
76
|
+
let targetOrder;
|
|
77
|
+
if ("position" in placement) {
|
|
78
|
+
targetOrder = placement.position === "first" ? [...headings, ...rest] : [...rest, ...headings];
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const idx = rest.indexOf(anchor);
|
|
82
|
+
const insertAt = "before" in placement ? idx : idx + 1;
|
|
83
|
+
targetOrder = [...rest.slice(0, insertAt), ...headings, ...rest.slice(insertAt)];
|
|
84
|
+
}
|
|
85
|
+
return { project, current, targetOrder, problems: [] };
|
|
86
|
+
}
|
|
31
87
|
/**
|
|
32
88
|
* The uuids of a project's source subtree rows (non-trashed to-dos AND
|
|
33
89
|
* headings, direct or heading-nested). `childrenReplaced` counts how many of
|
|
@@ -68,6 +124,144 @@ export function classifyHeadingConvert(db, target) {
|
|
|
68
124
|
}
|
|
69
125
|
return { kind: "ok", projectReveal: project.uuid, ordinal };
|
|
70
126
|
}
|
|
127
|
+
export function classifyHeadingMoveToProject(db, project, headingSel, toProject) {
|
|
128
|
+
const src = resolveProject(db, project);
|
|
129
|
+
if (src.resolved === null) {
|
|
130
|
+
return {
|
|
131
|
+
kind: "refuse",
|
|
132
|
+
refusal: "no-source",
|
|
133
|
+
detail: src.matches > 1
|
|
134
|
+
? "the source project title is ambiguous — pass its uuid"
|
|
135
|
+
: "the source project did not resolve",
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const h = resolveHeading(db, src.resolved.uuid, headingSel);
|
|
139
|
+
if (h.resolved === null) {
|
|
140
|
+
return {
|
|
141
|
+
kind: "refuse",
|
|
142
|
+
refusal: h.matches > 1 ? "heading-ambiguous" : "heading-not-found",
|
|
143
|
+
detail: h.matches > 1
|
|
144
|
+
? `the heading selector "${headingSel}" matches ${h.matches} headings in the project — disambiguate with a uuid`
|
|
145
|
+
: `no heading matching "${headingSel}" in the source project`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const headingRow = db
|
|
149
|
+
.prepare("SELECT title FROM TMTask WHERE uuid = ? AND type = 2 AND trashed = 0")
|
|
150
|
+
.get(h.resolved.uuid);
|
|
151
|
+
if (headingRow === undefined) {
|
|
152
|
+
return { kind: "refuse", refusal: "heading-not-found", detail: "the heading no longer exists" };
|
|
153
|
+
}
|
|
154
|
+
const title = headingRow.title ?? "";
|
|
155
|
+
if (title === "") {
|
|
156
|
+
return {
|
|
157
|
+
kind: "refuse",
|
|
158
|
+
refusal: "empty-heading-title",
|
|
159
|
+
detail: "the heading has no title — the Move… drive addresses the heading by its title-carrying " +
|
|
160
|
+
'"More. <title>" button, which cannot pick one titleless heading out of several',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Source title-collision: the "More. <title>" node cannot tell two same-titled
|
|
164
|
+
// headings in one project apart, even when the SELECTOR resolved by uuid.
|
|
165
|
+
const twins = db
|
|
166
|
+
.prepare("SELECT uuid FROM TMTask WHERE type = 2 AND trashed = 0 AND project = ? AND title = ? AND uuid != ?")
|
|
167
|
+
.all(src.resolved.uuid, title, h.resolved.uuid);
|
|
168
|
+
if (twins.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
kind: "refuse",
|
|
171
|
+
refusal: "heading-ambiguous",
|
|
172
|
+
detail: `the heading title "${title}" is shared by ${twins.length + 1} headings in this project — ` +
|
|
173
|
+
"the ellipsis Move… drive addresses headings by title and cannot disambiguate; " +
|
|
174
|
+
"rename one first",
|
|
175
|
+
candidates: [h.resolved.uuid, ...twins.map((t) => t.uuid)],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const dest = resolveProject(db, toProject);
|
|
179
|
+
if (dest.resolved === null) {
|
|
180
|
+
return {
|
|
181
|
+
kind: "refuse",
|
|
182
|
+
refusal: "no-dest",
|
|
183
|
+
detail: dest.matches > 1
|
|
184
|
+
? "the destination project title is ambiguous — pass its uuid"
|
|
185
|
+
: "the destination project did not resolve",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (dest.resolved.uuid === src.resolved.uuid) {
|
|
189
|
+
return {
|
|
190
|
+
kind: "refuse",
|
|
191
|
+
refusal: "same-project",
|
|
192
|
+
detail: "the destination is the heading's current project — use `project move-heading` to reorder " +
|
|
193
|
+
"a heading WITHIN its project",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Destination picker-collision: the picker searches BY TITLE, so a shared dest
|
|
197
|
+
// title would filter to >1 project and Return would pick the wrong one — fail
|
|
198
|
+
// closed even though the destination itself resolved (possibly by uuid).
|
|
199
|
+
const destTitle = dest.resolved.title;
|
|
200
|
+
const destTwins = db
|
|
201
|
+
.prepare("SELECT COUNT(*) AS n FROM TMTask WHERE type = 1 AND trashed = 0 AND title = ? AND uuid != ?")
|
|
202
|
+
.get(destTitle, dest.resolved.uuid);
|
|
203
|
+
if (destTwins.n > 0) {
|
|
204
|
+
return {
|
|
205
|
+
kind: "refuse",
|
|
206
|
+
refusal: "dest-ambiguous",
|
|
207
|
+
detail: `the destination title "${destTitle}" is shared by ${destTwins.n + 1} projects — the ` +
|
|
208
|
+
"Move… picker searches by title and would land the heading in the wrong one; rename or " +
|
|
209
|
+
"merge the duplicates first",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
kind: "ok",
|
|
214
|
+
pre: {
|
|
215
|
+
sourceProjectUuid: src.resolved.uuid,
|
|
216
|
+
headingUuid: h.resolved.uuid,
|
|
217
|
+
headingTitle: title,
|
|
218
|
+
destProjectUuid: dest.resolved.uuid,
|
|
219
|
+
destProjectTitle: destTitle,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export function classifyHeadingDissolve(db, target) {
|
|
224
|
+
if (target === null || target.type !== "heading") {
|
|
225
|
+
return { kind: "refuse", refusal: "not-a-heading", detail: "target is not a heading" };
|
|
226
|
+
}
|
|
227
|
+
const project = target.project;
|
|
228
|
+
if (project === null) {
|
|
229
|
+
return {
|
|
230
|
+
kind: "refuse",
|
|
231
|
+
refusal: "no-project",
|
|
232
|
+
detail: "the heading has no owning project — cannot reveal a project view to drive its row",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const title = target.title;
|
|
236
|
+
if (title === "") {
|
|
237
|
+
return {
|
|
238
|
+
kind: "refuse",
|
|
239
|
+
refusal: "empty-title",
|
|
240
|
+
detail: "the heading has no title — the Delete drive addresses the heading by its title-carrying " +
|
|
241
|
+
'"More. <title>" button, which cannot pick one titleless heading out of several',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const twins = db
|
|
245
|
+
.prepare("SELECT uuid FROM TMTask WHERE type = 2 AND trashed = 0 AND project = ? AND title = ? AND uuid != ?")
|
|
246
|
+
.all(project.uuid, title, target.uuid);
|
|
247
|
+
if (twins.length > 0) {
|
|
248
|
+
return {
|
|
249
|
+
kind: "refuse",
|
|
250
|
+
refusal: "title-ambiguous",
|
|
251
|
+
detail: `the heading title "${title}" is shared by ${twins.length + 1} headings in this project — ` +
|
|
252
|
+
"the ellipsis Delete drive addresses headings by title and cannot disambiguate; " +
|
|
253
|
+
"rename one first",
|
|
254
|
+
candidates: [target.uuid, ...twins.map((t) => t.uuid)],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const childUuids = db
|
|
258
|
+
.prepare('SELECT uuid FROM TMTask WHERE type = 0 AND trashed = 0 AND status = 0 AND heading = ? ORDER BY "index"')
|
|
259
|
+
.all(target.uuid).map((r) => r.uuid);
|
|
260
|
+
return {
|
|
261
|
+
kind: "ok",
|
|
262
|
+
pre: { projectReveal: project.uuid, headingUuid: target.uuid, headingTitle: title, childUuids },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
71
265
|
/**
|
|
72
266
|
* Count non-trashed projects with the given title that share the target's
|
|
73
267
|
* row-selection container, EXCLUDING the target itself. A non-zero count is a
|
|
@@ -155,18 +349,36 @@ export function classifyProjectRepeat(db, target) {
|
|
|
155
349
|
export function resolveArea(db, ref) {
|
|
156
350
|
return resolveNamedRef(db, "TMArea", "1=1", [], ref.uuid ?? ref.title ?? "");
|
|
157
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Resolve a project DESTINATION (`--to-project` / `--project` on add/move). NAME
|
|
354
|
+
* resolution is OPEN-only (`status = 0`) — a completed/canceled project is not a
|
|
355
|
+
* valid live destination by name: placing an open child there strands it (PLOG1,
|
|
356
|
+
* "a completed-in-place project strands children identically, one sweep later").
|
|
357
|
+
* A UUID / partial-uuid still reaches a completed project (explicit intent —
|
|
358
|
+
* `nameExtraWhere` narrows the NAME tiers only). When a name matches ONLY non-open
|
|
359
|
+
* projects, the resolution carries a `deadHint` so the destination not-found says
|
|
360
|
+
* the row exists and how to reach it (by uuid), instead of a bare "project not
|
|
361
|
+
* found".
|
|
362
|
+
*/
|
|
158
363
|
export function resolveProject(db, ref) {
|
|
159
|
-
|
|
364
|
+
const key = ref.uuid ?? ref.title ?? "";
|
|
365
|
+
const r = resolveNamedRef(db, "TMTask", "type = 1 AND trashed = 0", [], key, {
|
|
366
|
+
nameExtraWhere: "status = 0",
|
|
367
|
+
});
|
|
368
|
+
if (r.resolved === null && r.matches === 0) {
|
|
369
|
+
const completed = resolveNamedRef(db, "TMTask", "type = 1 AND trashed = 0 AND status != 0", [], key, {
|
|
370
|
+
prefixTier: false,
|
|
371
|
+
}).matches;
|
|
372
|
+
if (completed > 0)
|
|
373
|
+
return { ...r, deadHint: deadNameMatchHint({ completed }) };
|
|
374
|
+
}
|
|
375
|
+
return r;
|
|
160
376
|
}
|
|
161
|
-
export function resolveHeading(db, projectUuid,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
resolved: rows.length === 1 && first !== undefined ? first : null,
|
|
168
|
-
matches: rows.length,
|
|
169
|
-
};
|
|
377
|
+
export function resolveHeading(db, projectUuid, headingSel) {
|
|
378
|
+
// Shares the one heading-selector core (title | uuid | empty-string literal,
|
|
379
|
+
// no ordinal) with the project heading verbs — see resolveHeadingRef.
|
|
380
|
+
const r = resolveHeadingRef(db, projectUuid, headingSel);
|
|
381
|
+
return { resolved: r.resolved, matches: r.matches };
|
|
170
382
|
}
|
|
171
383
|
export function projectStatus(db, uuid) {
|
|
172
384
|
const row = db.prepare("SELECT status FROM TMTask WHERE uuid = ? AND type = 1").get(uuid);
|
|
@@ -222,6 +434,22 @@ export function childTagTitles(db, tagUuid) {
|
|
|
222
434
|
.all(tagUuid, tagUuid);
|
|
223
435
|
return rows.map((r) => r.title);
|
|
224
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Live member census for `area.delete` (A25/A25B). Counts the area's non-trashed
|
|
439
|
+
* DIRECT members: to-dos carrying the area FK (any status — a logged to-do is
|
|
440
|
+
* still trashed by the delete) and projects carrying the area FK. Deleting a
|
|
441
|
+
* non-empty area trashes the to-dos and takes the projects with it, so the guard
|
|
442
|
+
* uses these counts to refuse fail-closed unless the caller allows it.
|
|
443
|
+
*/
|
|
444
|
+
export function areaMemberCounts(db, areaUuid) {
|
|
445
|
+
const todos = db
|
|
446
|
+
.prepare("SELECT COUNT(*) AS n FROM TMTask WHERE type = 0 AND trashed = 0 AND area = ?")
|
|
447
|
+
.get(areaUuid).n;
|
|
448
|
+
const projects = db
|
|
449
|
+
.prepare("SELECT COUNT(*) AS n FROM TMTask WHERE type = 1 AND trashed = 0 AND area = ?")
|
|
450
|
+
.get(areaUuid).n;
|
|
451
|
+
return { projects, todos };
|
|
452
|
+
}
|
|
225
453
|
export function trashedCount(db) {
|
|
226
454
|
const row = db.prepare("SELECT COUNT(*) AS n FROM TMTask WHERE trashed = 1").get();
|
|
227
455
|
return row.n;
|
|
@@ -230,6 +458,7 @@ export function isRepeatingTemplate(task) {
|
|
|
230
458
|
return task !== null && task.type !== "heading" && task.repeating.isTemplate;
|
|
231
459
|
}
|
|
232
460
|
const NOT_TEMPLATE_ROW = "(rt1_recurrenceRule IS NULL AND repeater IS NULL)";
|
|
461
|
+
const TEMPLATE_ROW = "(rt1_recurrenceRule IS NOT NULL OR repeater IS NOT NULL)";
|
|
233
462
|
/**
|
|
234
463
|
* Scope membership + full wire list for `reorder`. Eligibility mirrors the
|
|
235
464
|
* lab evidence exactly:
|
|
@@ -246,8 +475,6 @@ const NOT_TEMPLATE_ROW = "(rt1_recurrenceRule IS NULL AND repeater IS NULL)";
|
|
|
246
475
|
* unprobed and the guard rejects them.
|
|
247
476
|
* - inbox: unscheduled to-dos with no container (start=0), by "index"
|
|
248
477
|
* (A6/P8a — the command ranks the sent list in order).
|
|
249
|
-
* - headings: the HEADING rows (type=2) of a project, by "index" (scf P1 —
|
|
250
|
-
* the private command accepts heading uuids; children follow).
|
|
251
478
|
* - someday: loose someday to-dos AND area-less someday projects, by
|
|
252
479
|
* "index"; same-type requests only. The Someday list handler is
|
|
253
480
|
* anchor-stacked with OPPOSITE stack directions by row type:
|
|
@@ -260,12 +487,37 @@ const NOT_TEMPLATE_ROW = "(rt1_recurrenceRule IS NULL AND repeater IS NULL)";
|
|
|
260
487
|
export function computeReorderPre(db, params, containerUuid, now) {
|
|
261
488
|
const todayIso = localToday(now);
|
|
262
489
|
const packedToday = encodePackedDate(todayIso);
|
|
263
|
-
const key = params.scope === "today" ||
|
|
264
|
-
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
.
|
|
490
|
+
const key = params.scope === "today" ||
|
|
491
|
+
params.scope === "evening" ||
|
|
492
|
+
params.scope === "container-day" ||
|
|
493
|
+
params.scope === "day" ||
|
|
494
|
+
params.scope === "tomorrow" ||
|
|
495
|
+
params.scope === "upcoming"
|
|
496
|
+
? "todayIndex"
|
|
497
|
+
: "index";
|
|
498
|
+
const rowsOf = (where, binds, rankCol, templateClause) => db
|
|
499
|
+
.prepare(`SELECT uuid, title, ${rankCol} AS rank, startBucket, type, ${TEMPLATE_ROW} AS isTemplate
|
|
500
|
+
FROM TMTask
|
|
501
|
+
WHERE trashed = 0 AND status = 0 AND ${templateClause} AND ${where}
|
|
502
|
+
ORDER BY ${rankCol} ASC`)
|
|
503
|
+
.all(...binds).map((r) => {
|
|
504
|
+
const m = {
|
|
505
|
+
uuid: r.uuid,
|
|
506
|
+
title: r.title,
|
|
507
|
+
rank: r.rank,
|
|
508
|
+
startBucket: r.startBucket,
|
|
509
|
+
type: r.type,
|
|
510
|
+
isTemplate: r.isTemplate === 1,
|
|
511
|
+
};
|
|
512
|
+
return m;
|
|
513
|
+
});
|
|
514
|
+
// Every scope EXCEPT the day-block scopes excludes templates (NOT_TEMPLATE_ROW).
|
|
515
|
+
const select = (where, binds, rankCol) => rowsOf(where, binds, rankCol, NOT_TEMPLATE_ROW);
|
|
516
|
+
// The `day`/`tomorrow` day-block scopes ADMIT templates as first-class members
|
|
517
|
+
// (their strictly-future projection sits on the block todayIndex axis — TMPLSORT/
|
|
518
|
+
// PTMPL); the leg family in reorder.ts splits per class (never a when=/deadline leg
|
|
519
|
+
// on a template — the §1 crash).
|
|
520
|
+
const selectWithTemplates = (where, binds) => rowsOf(where, binds, "todayIndex", "1=1");
|
|
269
521
|
let members = [];
|
|
270
522
|
const rejectedCandidates = new Map();
|
|
271
523
|
switch (params.scope) {
|
|
@@ -317,10 +569,6 @@ export function computeReorderPre(db, params, containerUuid, now) {
|
|
|
317
569
|
members = select("type = 0 AND start = 0", [], `"index"`);
|
|
318
570
|
break;
|
|
319
571
|
}
|
|
320
|
-
case "headings": {
|
|
321
|
-
members = select("type = 2 AND project = ?", [containerUuid ?? ""], `"index"`);
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
572
|
case "someday": {
|
|
325
573
|
// Loose someday to-dos AND area-less someday projects (P9e locked the
|
|
326
574
|
// project protocol) — same-type requests only, like the area scope.
|
|
@@ -351,6 +599,178 @@ export function computeReorderPre(db, params, containerUuid, now) {
|
|
|
351
599
|
}
|
|
352
600
|
break;
|
|
353
601
|
}
|
|
602
|
+
case "heading": {
|
|
603
|
+
// A heading's ANYTIME children, ranked on "index". The forward-order
|
|
604
|
+
// bounce back-inserts each (BOUNCE2-h); templates + scheduled/someday
|
|
605
|
+
// children are not members of this bucket.
|
|
606
|
+
members = select("type = 0 AND heading = ? AND start = 1 AND startDate IS NULL", [containerUuid ?? ""], `"index"`);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case "area-someday": {
|
|
610
|
+
// An area's SOMEDAY direct members (unheaded), ranked on "index". The
|
|
611
|
+
// reverse-order bounce front-inserts each (SOMEBNC-area) — the state-
|
|
612
|
+
// preserving surface the destructive area reorder command lacks (§9f).
|
|
613
|
+
members = select("type = 0 AND area = ? AND heading IS NULL AND start = 2 AND startDate IS NULL", [containerUuid ?? ""], `"index"`);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case "anytime": {
|
|
617
|
+
// Area-less loose ANYTIME to-dos, ranked on "index". The reverse-order
|
|
618
|
+
// bounce front-inserts each below the running global min (ANYBNC).
|
|
619
|
+
members = select("type = 0 AND project IS NULL AND area IS NULL AND heading IS NULL " +
|
|
620
|
+
"AND start = 1 AND startDate IS NULL", [], `"index"`);
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case "container-day": {
|
|
624
|
+
// A container's (project OR area) same-day scheduled children, ranked on
|
|
625
|
+
// todayIndex, date-preserving (DAYORD-b). The day is read off the first
|
|
626
|
+
// requested uuid — the planner guarantees every movee shares the bucket
|
|
627
|
+
// (rule 4). startBucket=0 (today-proper / a future day); the evening
|
|
628
|
+
// sub-bucket stays app-default (unprobed for the container specifier).
|
|
629
|
+
const firstUuid = params.uuids[0];
|
|
630
|
+
const first = firstUuid !== undefined
|
|
631
|
+
? db
|
|
632
|
+
.prepare("SELECT startDate, startBucket FROM TMTask WHERE uuid = ?")
|
|
633
|
+
.get(firstUuid)
|
|
634
|
+
: undefined;
|
|
635
|
+
if (first?.startDate != null && first.startBucket === 0) {
|
|
636
|
+
members = select("type = 0 AND heading IS NULL AND (project = ? OR area = ?) " +
|
|
637
|
+
"AND startBucket = 0 AND startDate = ?", [containerUuid ?? "", containerUuid ?? "", first.startDate], "todayIndex");
|
|
638
|
+
}
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case "day": {
|
|
642
|
+
// The DATED BOUNCE (SIT4 DAYBNC): a whole ARBITRARY future day-group across
|
|
643
|
+
// ALL containers, ranked on todayIndex. SCHEDULED members are every to-do on
|
|
644
|
+
// the day (loose, project-child, headed-child, area-direct — the when= round-
|
|
645
|
+
// trip preserves each container FK, incl. the heading FK, §2e/R21) PLUS every
|
|
646
|
+
// scheduled PROJECT row on the day (type=1 rows front-insert on the SAME
|
|
647
|
+
// global todayIndex axis, DAYBNC DP-1/DP-2 — the leg is update-project).
|
|
648
|
+
// DEADLINE-FORECAST members (DLBNC / #383) are ALSO first-class day-block
|
|
649
|
+
// members: someday/anytime-stage to-dos (start IN (1,2)) whose future
|
|
650
|
+
// `deadline` == D and whose `startDate` is NULL rest on the SAME Upcoming
|
|
651
|
+
// day-block todayIndex axis (§9o / DLBNC-1d), reordered by the deadline-cycle
|
|
652
|
+
// (URL deadline= clear + re-set) rather than the when= bounce. The two classes
|
|
653
|
+
// are merged and re-sorted by todayIndex into the true interleaved block order.
|
|
654
|
+
// The whole group is one bounce unit (a reverse-target pass re-bases it below
|
|
655
|
+
// the day's global min), so every same-day member is enumerated — an
|
|
656
|
+
// untouched row is just an unrequested member (co-bounced + disclosed as
|
|
657
|
+
// `touched`), never a strand. The day D is read off the first requested uuid
|
|
658
|
+
// (its startDate when scheduled, else its deadline when forecast). Templates
|
|
659
|
+
// are excluded by NOT_TEMPLATE_ROW.
|
|
660
|
+
const firstUuid = params.uuids[0];
|
|
661
|
+
const first = firstUuid !== undefined
|
|
662
|
+
? db
|
|
663
|
+
.prepare("SELECT startDate, startBucket, deadline, start, rt1_nextInstanceStartDate AS proj, " +
|
|
664
|
+
`${TEMPLATE_ROW} AS isTemplate FROM TMTask WHERE uuid = ?`)
|
|
665
|
+
.get(firstUuid)
|
|
666
|
+
: undefined;
|
|
667
|
+
// The day D read off the first requested uuid: a scheduled row's startDate, a
|
|
668
|
+
// forecast row's deadline, or a TEMPLATE's projection day (rt1_nextInstanceStart
|
|
669
|
+
// Date — TMPLSORT/PTMPL). Threaded into all three member cohorts below.
|
|
670
|
+
const dayPacked = first === undefined
|
|
671
|
+
? null
|
|
672
|
+
: first.isTemplate === 1
|
|
673
|
+
? first.proj
|
|
674
|
+
: first.startDate !== null && first.startBucket === 0
|
|
675
|
+
? first.startDate
|
|
676
|
+
: first.startDate === null && (first.start === 1 || first.start === 2)
|
|
677
|
+
? first.deadline
|
|
678
|
+
: null;
|
|
679
|
+
if (dayPacked !== null) {
|
|
680
|
+
const scheduled = select("type IN (0, 1) AND startBucket = 0 AND startDate = ?", [dayPacked], "todayIndex");
|
|
681
|
+
// §9o deadline-forecast cohort: start IN (1,2), startDate NULL, deadline == D.
|
|
682
|
+
// To-dos AND PROJECTS (type IN (0,1)) — forecast projects are first-class
|
|
683
|
+
// members of the SAME block todayIndex axis, interleaved with forecast to-dos
|
|
684
|
+
// (PROJDL-2a/2c, #385), reordered by the update-project deadline-cycle.
|
|
685
|
+
const forecast = select("type IN (0, 1) AND startDate IS NULL AND deadline = ? AND start IN (1, 2)", [dayPacked], "todayIndex");
|
|
686
|
+
// Repeating TEMPLATE projections on day D (TMPLSORT-3c / PTMPL-B5): to-do AND
|
|
687
|
+
// project templates whose rt1_nextInstanceStartDate == D render on the SAME
|
|
688
|
+
// block todayIndex axis. Admitted as members so the dispatch can split the
|
|
689
|
+
// per-class leg family; a PROJECT template is byte-untouched under the suffix
|
|
690
|
+
// rule, a TO-DO template front-inserts via a single-id `list "Upcoming"` leg.
|
|
691
|
+
const templates = selectWithTemplates(`type IN (0, 1) AND ${TEMPLATE_ROW} AND rt1_nextInstanceStartDate = ?`, [dayPacked]);
|
|
692
|
+
// One shared todayIndex axis (DLBNC-1d / TMPLSORT-2): merge + re-sort to block order.
|
|
693
|
+
members = [...scheduled, ...forecast, ...templates].toSorted((a, b) => a.rank - b.rank);
|
|
694
|
+
}
|
|
695
|
+
// An INBOX-stage row (start=0) carrying this deadline is OFF the block axis
|
|
696
|
+
// (todayIndex=0, §9o — the axis assignment is gated on start IN (1,2), not the
|
|
697
|
+
// bare deadline), so it is refused with an honest, unprobed-membership reason.
|
|
698
|
+
// Templates are NO LONGER rejected — they are first-class day-block members above
|
|
699
|
+
// (their leg family, never a dated when= leg, is compiled in reorder.ts). Area-
|
|
700
|
+
// direct project rows are scheduled members (SIT5 AREAPROJDAY).
|
|
701
|
+
for (const uuid of params.uuids) {
|
|
702
|
+
const t = db
|
|
703
|
+
.prepare("SELECT start, startDate, deadline, rt1_recurrenceRule AS rule, repeater " +
|
|
704
|
+
"FROM TMTask WHERE uuid = ?")
|
|
705
|
+
.get(uuid);
|
|
706
|
+
if (t === undefined)
|
|
707
|
+
continue;
|
|
708
|
+
if (t.rule !== null || t.repeater !== null)
|
|
709
|
+
continue;
|
|
710
|
+
if (dayPacked !== null &&
|
|
711
|
+
t.start === 0 &&
|
|
712
|
+
t.startDate === null &&
|
|
713
|
+
t.deadline !== null &&
|
|
714
|
+
t.deadline === dayPacked) {
|
|
715
|
+
rejectedCandidates.set(uuid, "is an INBOX-stage row (start=0) with this deadline — it rests OFF the Upcoming " +
|
|
716
|
+
"day-block axis (todayIndex=0, §9o: the block axis is gated on start IN (1,2), " +
|
|
717
|
+
"not the bare deadline), so its position there is unprobed; schedule it or move it " +
|
|
718
|
+
"out of the Inbox before reordering it on the day axis");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
case "tomorrow": {
|
|
724
|
+
// The whole TOMORROW day-group across ALL containers (ORDFIN2 TOMORROWLIST),
|
|
725
|
+
// ranked on todayIndex. `list "Tomorrow"` is a clean one-call native surface
|
|
726
|
+
// that ACCEPTS a scheduled PROJECT row inline (type=1, O12 analog) and re-
|
|
727
|
+
// ranks it in position — so members are type IN (0,1), and a project row is a
|
|
728
|
+
// valid MOVEE here (the ONLY reorder scope that accepts one on the day axis).
|
|
729
|
+
// The day is read off the first requested uuid; the planner guarantees it is
|
|
730
|
+
// tomorrow. Repeating TEMPLATES whose projection == tomorrow are ALSO first-
|
|
731
|
+
// class members of this one-call wire (TMPLSORT-3c-Tomorrow to-do templates /
|
|
732
|
+
// PTMPL-B5 project templates: the native `list "Tomorrow"` reorder places a
|
|
733
|
+
// template at its exact sent slot, umd-silent, no reparent, no crash) — the
|
|
734
|
+
// native surface is the ONE safe way to position a template on the day axis.
|
|
735
|
+
const firstUuid = params.uuids[0];
|
|
736
|
+
const first = firstUuid !== undefined
|
|
737
|
+
? db
|
|
738
|
+
.prepare("SELECT startDate, startBucket, rt1_nextInstanceStartDate AS proj, " +
|
|
739
|
+
`${TEMPLATE_ROW} AS isTemplate FROM TMTask WHERE uuid = ?`)
|
|
740
|
+
.get(firstUuid)
|
|
741
|
+
: undefined;
|
|
742
|
+
const dayPacked = first === undefined
|
|
743
|
+
? null
|
|
744
|
+
: first.isTemplate === 1
|
|
745
|
+
? first.proj
|
|
746
|
+
: first.startBucket === 0
|
|
747
|
+
? first.startDate
|
|
748
|
+
: null;
|
|
749
|
+
if (dayPacked !== null) {
|
|
750
|
+
const scheduled = select("type IN (0, 1) AND startBucket = 0 AND startDate = ?", [dayPacked], "todayIndex");
|
|
751
|
+
const templates = selectWithTemplates(`type IN (0, 1) AND ${TEMPLATE_ROW} AND rt1_nextInstanceStartDate = ?`, [dayPacked]);
|
|
752
|
+
members = [...scheduled, ...templates].toSorted((a, b) => a.rank - b.rank);
|
|
753
|
+
}
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
case "heading-someday": {
|
|
757
|
+
// A heading's SOMEDAY children, ranked on "index" (HEADSUB1 Arm B-someday /
|
|
758
|
+
// Arm C). Re-heading the block in forward target order BACK-INSERTS each
|
|
759
|
+
// deterministically (start=2 preserved) — the move-to-heading sort. Same
|
|
760
|
+
// bucket shape as the `heading` anytime scope, `start=2` instead of `start=1`.
|
|
761
|
+
members = select("type = 0 AND heading = ? AND start = 2 AND startDate IS NULL", [containerUuid ?? ""], `"index"`);
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
case "upcoming": {
|
|
765
|
+
// INTERNAL per-template front-insert leg (the `day` dispatch, TMPLSORT-1). The
|
|
766
|
+
// sent id(s) are TRUSTED day-block members already validated by the day
|
|
767
|
+
// dispatch — populate directly (so the H-REORDER-SCOPE guard passes) and let
|
|
768
|
+
// the wire be the sent id(s) only (no extension). Never a user scope.
|
|
769
|
+
if (params.uuids.length > 0) {
|
|
770
|
+
members = selectWithTemplates(`uuid IN (${params.uuids.map(() => "?").join(", ")})`, params.uuids);
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
354
774
|
}
|
|
355
775
|
const seen = new Set();
|
|
356
776
|
const duplicates = [];
|
|
@@ -399,6 +819,7 @@ export function computeReorderPre(db, params, containerUuid, now) {
|
|
|
399
819
|
rank: m.rank,
|
|
400
820
|
startBucket: m.startBucket,
|
|
401
821
|
type: m.type,
|
|
822
|
+
isTemplate: m.isTemplate,
|
|
402
823
|
})),
|
|
403
824
|
rejected,
|
|
404
825
|
duplicates,
|
|
@@ -411,7 +832,28 @@ function rowStartDate(db, uuid) {
|
|
|
411
832
|
const row = db.prepare("SELECT startDate FROM TMTask WHERE uuid = ?").get(uuid);
|
|
412
833
|
return row?.startDate ?? null;
|
|
413
834
|
}
|
|
414
|
-
|
|
415
|
-
|
|
835
|
+
/**
|
|
836
|
+
* A row's live Today/Evening flag — `"today"`, `"evening"`, or `null` — read
|
|
837
|
+
* from the SAME marker the `today`/`evening` reorder membership uses
|
|
838
|
+
* ({@link computeReorderPre}, O01/O03/§9n): an ARRIVED scheduled row
|
|
839
|
+
* (`start IN (1,2)` AND `startDate <= today`) carries the flag — `evening` at
|
|
840
|
+
* raw `startBucket=1` (a live evening flag expires daily, so a stale past-dated
|
|
841
|
+
* bucket-1 row reads `today` proper), `today` otherwise. Someday, anytime,
|
|
842
|
+
* inbox, and future-scheduled rows carry NO flag. Single-sourced here so any
|
|
843
|
+
* de-Today guard (e.g. the `projects` bounce's de-star refusal in reorder.ts)
|
|
844
|
+
* keys off the one marker and can never drift from the membership query.
|
|
845
|
+
*/
|
|
846
|
+
export function todayEveningFlagOf(db, uuid, now) {
|
|
847
|
+
const packedToday = encodePackedDate(localToday(now));
|
|
848
|
+
const row = db
|
|
849
|
+
.prepare("SELECT start, startDate, startBucket FROM TMTask WHERE uuid = ?")
|
|
850
|
+
.get(uuid);
|
|
851
|
+
if (row === undefined)
|
|
852
|
+
return null;
|
|
853
|
+
if (row.start !== 1 && row.start !== 2)
|
|
854
|
+
return null; // inbox (start=0)
|
|
855
|
+
if (row.startDate === null || row.startDate > packedToday)
|
|
856
|
+
return null; // someday / anytime / future
|
|
857
|
+
return row.startBucket === 1 && row.startDate === packedToday ? "evening" : "today";
|
|
416
858
|
}
|
|
417
859
|
//# sourceMappingURL=pre-state.js.map
|