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.
Files changed (208) hide show
  1. package/README.md +16 -5
  2. package/dist/audit/schema.d.ts +7 -0
  3. package/dist/audit/schema.js.map +1 -1
  4. package/dist/cli/canonical-ref.d.ts +21 -0
  5. package/dist/cli/canonical-ref.js +23 -0
  6. package/dist/cli/canonical-ref.js.map +1 -0
  7. package/dist/cli/commands/area.d.ts +18 -11
  8. package/dist/cli/commands/area.js +81 -77
  9. package/dist/cli/commands/area.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +1 -2
  11. package/dist/cli/commands/doctor.js +11 -1
  12. package/dist/cli/commands/doctor.js.map +1 -1
  13. package/dist/cli/commands/install-skill.d.ts +19 -4
  14. package/dist/cli/commands/install-skill.js +130 -52
  15. package/dist/cli/commands/install-skill.js.map +1 -1
  16. package/dist/cli/commands/mcp.js +21 -1
  17. package/dist/cli/commands/mcp.js.map +1 -1
  18. package/dist/cli/commands/project.d.ts +1 -0
  19. package/dist/cli/commands/project.js +92 -48
  20. package/dist/cli/commands/project.js.map +1 -1
  21. package/dist/cli/commands/reads.d.ts +5 -1
  22. package/dist/cli/commands/reads.js +110 -31
  23. package/dist/cli/commands/reads.js.map +1 -1
  24. package/dist/cli/commands/setup.js +11 -4
  25. package/dist/cli/commands/setup.js.map +1 -1
  26. package/dist/cli/commands/show.js +75 -36
  27. package/dist/cli/commands/show.js.map +1 -1
  28. package/dist/cli/commands/todo.js +2 -2
  29. package/dist/cli/commands/todo.js.map +1 -1
  30. package/dist/cli/commands/writes.js +795 -144
  31. package/dist/cli/commands/writes.js.map +1 -1
  32. package/dist/cli/did-you-mean.d.ts +14 -3
  33. package/dist/cli/did-you-mean.js +44 -7
  34. package/dist/cli/did-you-mean.js.map +1 -1
  35. package/dist/cli/dry-run.d.ts +55 -0
  36. package/dist/cli/dry-run.js +92 -0
  37. package/dist/cli/dry-run.js.map +1 -0
  38. package/dist/cli/glyphs.d.ts +16 -2
  39. package/dist/cli/glyphs.js +60 -12
  40. package/dist/cli/glyphs.js.map +1 -1
  41. package/dist/cli/help.js +69 -13
  42. package/dist/cli/help.js.map +1 -1
  43. package/dist/cli/main.js +8 -1
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/move-hint.d.ts +3 -2
  46. package/dist/cli/move-hint.js +2 -2
  47. package/dist/cli/move-hint.js.map +1 -1
  48. package/dist/cli/read-driver.d.ts +53 -5
  49. package/dist/cli/read-driver.js +82 -15
  50. package/dist/cli/read-driver.js.map +1 -1
  51. package/dist/cli/ref-render.d.ts +14 -0
  52. package/dist/cli/ref-render.js +9 -0
  53. package/dist/cli/ref-render.js.map +1 -0
  54. package/dist/cli/render.d.ts +40 -14
  55. package/dist/cli/render.js +128 -55
  56. package/dist/cli/render.js.map +1 -1
  57. package/dist/cli/resolve-invocation.d.ts +7 -5
  58. package/dist/cli/resolve-invocation.js +16 -7
  59. package/dist/cli/resolve-invocation.js.map +1 -1
  60. package/dist/cli/skill-check.d.ts +12 -6
  61. package/dist/cli/skill-check.js +31 -14
  62. package/dist/cli/skill-check.js.map +1 -1
  63. package/dist/cli/skill.d.ts +8 -0
  64. package/dist/cli/skill.js +10 -0
  65. package/dist/cli/skill.js.map +1 -1
  66. package/dist/cli/verb-hint.js +1 -1
  67. package/dist/cli/verb-hint.js.map +1 -1
  68. package/dist/client.d.ts +158 -26
  69. package/dist/client.js +254 -38
  70. package/dist/client.js.map +1 -1
  71. package/dist/config.d.ts +66 -2
  72. package/dist/config.js +120 -12
  73. package/dist/config.js.map +1 -1
  74. package/dist/contracts.d.ts +187 -28
  75. package/dist/contracts.js +22 -1
  76. package/dist/contracts.js.map +1 -1
  77. package/dist/db/baselines/db-v26.js +4 -1
  78. package/dist/db/baselines/db-v26.js.map +1 -1
  79. package/dist/diagnose.d.ts +34 -2
  80. package/dist/diagnose.js +36 -1
  81. package/dist/diagnose.js.map +1 -1
  82. package/dist/index.d.ts +21 -8
  83. package/dist/index.js +26 -3
  84. package/dist/index.js.map +1 -1
  85. package/dist/mcp/server.d.ts +9 -0
  86. package/dist/mcp/server.js +790 -322
  87. package/dist/mcp/server.js.map +1 -1
  88. package/dist/model/entities.d.ts +57 -4
  89. package/dist/model/entities.js.map +1 -1
  90. package/dist/model/mappers.d.ts +2 -0
  91. package/dist/model/mappers.js +44 -2
  92. package/dist/model/mappers.js.map +1 -1
  93. package/dist/model/recurrence.d.ts +8 -1
  94. package/dist/model/recurrence.js.map +1 -1
  95. package/dist/model/serialize.js +15 -3
  96. package/dist/model/serialize.js.map +1 -1
  97. package/dist/read/area-filter.d.ts +76 -0
  98. package/dist/read/area-filter.js +58 -0
  99. package/dist/read/area-filter.js.map +1 -0
  100. package/dist/read/area-view.d.ts +22 -11
  101. package/dist/read/area-view.js +54 -28
  102. package/dist/read/area-view.js.map +1 -1
  103. package/dist/read/detail.js +15 -8
  104. package/dist/read/detail.js.map +1 -1
  105. package/dist/read/predicates.d.ts +18 -0
  106. package/dist/read/predicates.js +19 -0
  107. package/dist/read/predicates.js.map +1 -1
  108. package/dist/read/project-view.d.ts +91 -15
  109. package/dist/read/project-view.js +135 -37
  110. package/dist/read/project-view.js.map +1 -1
  111. package/dist/read/pseudo-area.d.ts +29 -0
  112. package/dist/read/pseudo-area.js +27 -0
  113. package/dist/read/pseudo-area.js.map +1 -0
  114. package/dist/read/queries.d.ts +175 -19
  115. package/dist/read/queries.js +376 -32
  116. package/dist/read/queries.js.map +1 -1
  117. package/dist/read/scope.d.ts +126 -0
  118. package/dist/read/scope.js +161 -0
  119. package/dist/read/scope.js.map +1 -0
  120. package/dist/read/search-rank.d.ts +2 -5
  121. package/dist/read/shape.d.ts +181 -0
  122. package/dist/read/shape.js +737 -0
  123. package/dist/read/shape.js.map +1 -0
  124. package/dist/read/show-target.d.ts +9 -1
  125. package/dist/read/show-target.js +77 -17
  126. package/dist/read/show-target.js.map +1 -1
  127. package/dist/read/stage.d.ts +199 -0
  128. package/dist/read/stage.js +125 -0
  129. package/dist/read/stage.js.map +1 -0
  130. package/dist/read/truncation.d.ts +7 -12
  131. package/dist/read/truncation.js +56 -21
  132. package/dist/read/truncation.js.map +1 -1
  133. package/dist/read/views.d.ts +68 -16
  134. package/dist/read/views.js +206 -52
  135. package/dist/read/views.js.map +1 -1
  136. package/dist/surface-copy.d.ts +16 -0
  137. package/dist/surface-copy.js +16 -0
  138. package/dist/surface-copy.js.map +1 -1
  139. package/dist/write/batch.d.ts +49 -10
  140. package/dist/write/batch.js +423 -71
  141. package/dist/write/batch.js.map +1 -1
  142. package/dist/write/commands.js +268 -55
  143. package/dist/write/commands.js.map +1 -1
  144. package/dist/write/guards.d.ts +1 -1
  145. package/dist/write/guards.js +71 -10
  146. package/dist/write/guards.js.map +1 -1
  147. package/dist/write/heading.d.ts +10 -1
  148. package/dist/write/heading.js +35 -5
  149. package/dist/write/heading.js.map +1 -1
  150. package/dist/write/make-repeating-project.d.ts +2 -2
  151. package/dist/write/make-repeating-project.js +9 -9
  152. package/dist/write/make-repeating-project.js.map +1 -1
  153. package/dist/write/move.d.ts +134 -0
  154. package/dist/write/move.js +1874 -0
  155. package/dist/write/move.js.map +1 -0
  156. package/dist/write/operations.d.ts +105 -19
  157. package/dist/write/operations.js +56 -7
  158. package/dist/write/operations.js.map +1 -1
  159. package/dist/write/opid.d.ts +31 -0
  160. package/dist/write/opid.js +30 -0
  161. package/dist/write/opid.js.map +1 -0
  162. package/dist/write/pipeline.d.ts +70 -3
  163. package/dist/write/pipeline.js +198 -33
  164. package/dist/write/pipeline.js.map +1 -1
  165. package/dist/write/pre-state.d.ts +172 -8
  166. package/dist/write/pre-state.js +467 -25
  167. package/dist/write/pre-state.js.map +1 -1
  168. package/dist/write/reorder.d.ts +23 -3
  169. package/dist/write/reorder.js +2106 -86
  170. package/dist/write/reorder.js.map +1 -1
  171. package/dist/write/reversibility.js +19 -7
  172. package/dist/write/reversibility.js.map +1 -1
  173. package/dist/write/scope-guard.d.ts +31 -0
  174. package/dist/write/scope-guard.js +162 -0
  175. package/dist/write/scope-guard.js.map +1 -0
  176. package/dist/write/undo.js +120 -13
  177. package/dist/write/undo.js.map +1 -1
  178. package/dist/write/vectors/applescript.js +16 -7
  179. package/dist/write/vectors/applescript.js.map +1 -1
  180. package/dist/write/vectors/shortcuts.js +1 -1
  181. package/dist/write/vectors/shortcuts.js.map +1 -1
  182. package/dist/write/vectors/simulator.js +83 -27
  183. package/dist/write/vectors/simulator.js.map +1 -1
  184. package/dist/write/vectors/types.d.ts +11 -0
  185. package/dist/write/vectors/ui-certification.d.ts +3 -3
  186. package/dist/write/vectors/ui-certification.js +41 -5
  187. package/dist/write/vectors/ui-certification.js.map +1 -1
  188. package/dist/write/vectors/ui-drag.d.ts +21 -1
  189. package/dist/write/vectors/ui-drag.js +138 -27
  190. package/dist/write/vectors/ui-drag.js.map +1 -1
  191. package/dist/write/vectors/ui-recipes.d.ts +21 -1
  192. package/dist/write/vectors/ui-recipes.js +166 -7
  193. package/dist/write/vectors/ui-recipes.js.map +1 -1
  194. package/dist/write/vectors/ui.d.ts +25 -1
  195. package/dist/write/vectors/ui.js +142 -33
  196. package/dist/write/vectors/ui.js.map +1 -1
  197. package/dist/write/verify/delta.d.ts +17 -1
  198. package/dist/write/verify/delta.js +106 -29
  199. package/dist/write/verify/delta.js.map +1 -1
  200. package/package.json +7 -1
  201. package/schema/envelope.schema.json +397 -0
  202. package/skills/things-cli/SKILL.md +36 -17
  203. package/skills/things-cli/references/banner.md +35 -0
  204. package/skills/things-cli/references/contracts.md +32 -9
  205. package/skills/things-cli/references/errors.md +57 -0
  206. package/skills/things-cli/references/gui.md +1 -1
  207. package/skills/things-cli/references/model.md +18 -7
  208. package/skills/things-cli/references/ordering.md +88 -0
@@ -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
- return resolveNamedRef(db, "TMTask", "type = 1 AND trashed = 0", [], ref.uuid ?? ref.title ?? "");
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, headingTitle) {
162
- const rows = db
163
- .prepare("SELECT uuid, title FROM TMTask WHERE type = 2 AND trashed = 0 AND project = ? AND title = ? COLLATE NOCASE")
164
- .all(projectUuid, headingTitle);
165
- const first = rows[0];
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" || params.scope === "evening" ? "todayIndex" : "index";
264
- const select = (where, binds, rankCol) => db
265
- .prepare(`SELECT uuid, title, ${rankCol} AS rank, startBucket, type FROM TMTask
266
- WHERE trashed = 0 AND status = 0 AND ${NOT_TEMPLATE_ROW} AND ${where}
267
- ORDER BY ${rankCol} ASC`)
268
- .all(...binds);
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
- export function projectOf(task) {
415
- return task !== null && task.type === "project" ? task : null;
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