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 +1 @@
1
- {"version":3,"file":"surface-copy.js","sourceRoot":"","sources":["../src/surface-copy.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,yCAAyC;AACzC,MAAM,CAAC,MAAM,WAAW,GAAG,kDAAkD,CAAC;AAE9E,gEAAgE;AAChE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AAExC,6CAA6C;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEjD,uDAAuD;AACvD,MAAM,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,oEAAoE,CAAC;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,+DAA+D,CAAC;AAE5F,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,kCAAkC,kBAAkB,GAAG,CAAC;AAElF,6DAA6D;AAC7D,MAAM,CAAC,MAAM,QAAQ,GAAG,oCAAoC,CAAC;AAE7D,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,+DAA+D;AAC/D,MAAM,CAAC,MAAM,eAAe,GAAG,mGAAmG,kBAAkB,gCAAgC,CAAC;AAErL,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,4DAA4D,qBAAqB,sCAAsC,CAAC;AAE1J,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,oDAAoD,CAAC;AAErF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAC1B,qJAAqJ,CAAC;AAExJ;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAsD;IACnF,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,IAAI;YACP,OAAO,EAAE,CAAC;QACZ,KAAK,OAAO;YACV,OAAO;gBACL,wEAAwE;oBACtE,8CAA8C;aACjD,CAAC;QACJ,KAAK,iBAAiB;YACpB,OAAO;gBACL,6EAA6E;oBAC3E,8CAA8C;aACjD,CAAC;IACN,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"surface-copy.js","sourceRoot":"","sources":["../src/surface-copy.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,yCAAyC;AACzC,MAAM,CAAC,MAAM,WAAW,GAAG,kDAAkD,CAAC;AAE9E,gEAAgE;AAChE,MAAM,CAAC,MAAM,WAAW,GAAG,YAAY,CAAC;AAExC,6CAA6C;AAC7C,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEjD,uDAAuD;AACvD,MAAM,CAAC,MAAM,UAAU,GAAG,qBAAqB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,oEAAoE,CAAC;AACjG,MAAM,CAAC,MAAM,YAAY,GAAG,+DAA+D,CAAC;AAE5F,sEAAsE;AACtE,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,kCAAkC,kBAAkB,GAAG,CAAC;AAElF,6DAA6D;AAC7D,MAAM,CAAC,MAAM,QAAQ,GAAG,oCAAoC,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,sDAAsD,CAAC;AAEhF,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAErC,+DAA+D;AAC/D,MAAM,CAAC,MAAM,eAAe,GAAG,mGAAmG,kBAAkB,gCAAgC,CAAC;AAErL,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,4DAA4D,qBAAqB,sCAAsC,CAAC;AAE1J,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,oDAAoD,CAAC;AAErF,oFAAoF;AACpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,sEAAsE,UAAU,yCAAyC,CAAC;AAE1J;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAC1B,qJAAqJ,CAAC;AAExJ;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GACxB,obAAob,CAAC;AAEvb;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAsD;IACnF,QAAQ,MAAM,CAAC,MAAM,EAAE,CAAC;QACtB,KAAK,IAAI;YACP,OAAO,EAAE,CAAC;QACZ,KAAK,OAAO;YACV,OAAO;gBACL,wEAAwE;oBACtE,8CAA8C;aACjD,CAAC;QACJ,KAAK,iBAAiB;YACpB,OAAO;gBACL,6EAA6E;oBAC3E,8CAA8C;aACjD,CAAC;IACN,CAAC;AACH,CAAC"}
@@ -1,12 +1,3 @@
1
- /**
2
- * Batch mutations: N ops through the SAME pipeline as single mutations —
3
- * every op individually pre-read, guarded, verified, and audited. The wins
4
- * are amortization (one process, one DB handle, one config load) and a
5
- * per-op result stream; there is deliberately NO transactional semantics
6
- * (the app's surfaces have none to offer). Ops run SEQUENTIALLY: the
7
- * mutation lock serializes them anyway, and create-probe verification must
8
- * never race.
9
- */
10
1
  import { type OperationKind } from "./operations.ts";
11
2
  import { type WriteDeps, type WriteOptions } from "./pipeline.ts";
12
3
  import { type ReorderResult } from "./reorder.ts";
@@ -14,12 +5,36 @@ import { type ReorderResult } from "./reorder.ts";
14
5
  export interface BatchOp {
15
6
  op: OperationKind;
16
7
  params: Record<string, unknown>;
8
+ /**
9
+ * Client handle for the uuid this op CREATES — a later line references it as
10
+ * `"$tempId"` (or `"$tempId.instance"` / `"$tempId.replaced"`) in any
11
+ * ref-accepting param. Valid only on uuid-minting ops (never `tag.add` — tags
12
+ * have no uuid; reference a tag by its title). `[A-Za-z0-9_-]{1,32}`; unique
13
+ * within one batch.
14
+ */
15
+ tempId?: string;
16
+ /**
17
+ * Client idempotency id: a resubmitted batch whose line carries the same id
18
+ * as a prior successful change is skipped (reported already-applied) instead
19
+ * of re-created. `[A-Za-z0-9_-]{1,64}`.
20
+ */
21
+ opId?: string;
17
22
  /** Per-op acknowledgements/overrides (a safe subset of WriteOptions). */
18
23
  options?: {
19
24
  acknowledgeChecklistReset?: boolean;
20
25
  acknowledgeProjectReopen?: boolean;
21
26
  dangerouslyPermanent?: boolean;
22
27
  acknowledgeTagSubtree?: boolean;
28
+ /** Delete a non-empty area together with its contents (area.delete). */
29
+ allowNonEmptyArea?: boolean;
30
+ /**
31
+ * Acknowledge a GUI-driven op (make-repeating, convert-to-project, …) — the
32
+ * second of its two keys. Required for any ui-drive op (several of which are
33
+ * uuid-minting, so tempId-eligible), or the per-leg H-UI-DRIVE gate blocks it.
34
+ */
35
+ dangerouslyDriveGui?: boolean;
36
+ /** Create any missing tag (mkdir-p for parent/child) instead of failing on an unknown tag. */
37
+ createTags?: boolean;
23
38
  vector?: WriteOptions["vector"];
24
39
  verifyTimeoutMs?: number;
25
40
  maxDisruption?: WriteOptions["maxDisruption"];
@@ -33,11 +48,35 @@ export type BatchItemOutcome = ReorderResult | {
33
48
  kind: "skipped";
34
49
  op: string;
35
50
  detail: string;
51
+ } | {
52
+ /** An idempotency-id match: the op was already applied by an earlier submission. */
53
+ kind: "already-applied";
54
+ op: string;
55
+ uuid: string;
56
+ detail: string;
36
57
  };
37
58
  export interface BatchItemResult {
38
59
  index: number;
39
60
  op: string;
40
61
  outcome: BatchItemOutcome;
62
+ /** Echoed when the op declared a temp id (ADDITIVE). */
63
+ tempId?: string;
64
+ /** The uuid bound to this op's temp id, once the leg minted one (ADDITIVE). */
65
+ boundUuid?: string;
66
+ /** Echoed when the op carried a client idempotency id (ADDITIVE). */
67
+ opId?: string;
68
+ }
69
+ /** The whole-batch result: the per-op stream plus the batch-level additions. */
70
+ export interface BatchResult {
71
+ results: BatchItemResult[];
72
+ /** Every temp id that bound a uuid → that uuid (ADDITIVE; empty when none declared). */
73
+ tempIdMapping: Record<string, string>;
74
+ /**
75
+ * The batch-level undo token — pass it to `things undo --txn <token>` to
76
+ * reverse the WHOLE submission as one unit. Absent for a dry-run, an
77
+ * all-rejected batch, or one where no leg reached the pipeline.
78
+ */
79
+ undoToken?: string;
41
80
  }
42
81
  export interface BatchOptions {
43
82
  /** Stop at the first non-ok outcome; remaining ops report kind "skipped". */
@@ -48,4 +87,4 @@ export interface BatchOptions {
48
87
  }
49
88
  /** True when an outcome should be treated as a failure for --fail-fast/exit. */
50
89
  export declare function outcomeFailed(outcome: BatchItemOutcome): boolean;
51
- export declare function runBatch(deps: WriteDeps, ops: BatchOp[], options?: BatchOptions, onResult?: (result: BatchItemResult) => void): Promise<BatchItemResult[]>;
90
+ export declare function runBatch(deps: WriteDeps, ops: BatchOp[], options?: BatchOptions, onResult?: (result: BatchItemResult) => void): Promise<BatchResult>;
@@ -1,100 +1,452 @@
1
- /**
2
- * Batch mutations: N ops through the SAME pipeline as single mutations —
3
- * every op individually pre-read, guarded, verified, and audited. The wins
4
- * are amortization (one process, one DB handle, one config load) and a
5
- * per-op result stream; there is deliberately NO transactional semantics
6
- * (the app's surfaces have none to offer). Ops run SEQUENTIALLY: the
7
- * mutation lock serializes them anyway, and create-probe verification must
8
- * never race.
9
- */
10
1
  import { OPERATION_KINDS } from "./operations.js";
11
- import { runMutation } from "./pipeline.js";
2
+ import { findAppliedOpId, OP_ID_RE } from "./opid.js";
3
+ import { fingerprintLabel, runMutation } from "./pipeline.js";
12
4
  import { runReorder } from "./reorder.js";
5
+ import { readAuditRecords } from "./undo.js";
13
6
  const KNOWN_OPS = new Set(OPERATION_KINDS);
7
+ /**
8
+ * Ops that MINT a uuid, so may declare a `tempId` — the ratified rule is
9
+ * "anything that creates a new uuid". NB: `tag.add` is deliberately absent (tags
10
+ * have no uuid — identity is the title).
11
+ */
12
+ const UUID_MINTING_OPS = new Set([
13
+ "todo.add",
14
+ "todo.add-logged",
15
+ "project.add",
16
+ "project.add-repeating",
17
+ "area.add",
18
+ "project.add-heading",
19
+ "todo.duplicate",
20
+ "project.duplicate",
21
+ "todo.make-repeating",
22
+ "project.make-repeating",
23
+ "todo.convert-to-project",
24
+ "project.promote-heading",
25
+ ]);
26
+ const TEMP_ID_RE = /^[A-Za-z0-9_-]{1,32}$/;
27
+ /**
28
+ * Param keys whose values are REFERENCES (uuid / name / container) — the only
29
+ * places a `"$temp"` handle is resolved. A value starting with `$` anywhere
30
+ * else (a title, notes) is a literal, never a ref. Container refs nest the id
31
+ * under `.uuid`; `uuids` is a list.
32
+ */
33
+ const REF_KEYS = new Set([
34
+ "uuid",
35
+ "uuids",
36
+ "target",
37
+ "before",
38
+ "after",
39
+ "project",
40
+ "area",
41
+ "container",
42
+ "heading",
43
+ "headings",
44
+ ]);
14
45
  /** True when an outcome should be treated as a failure for --fail-fast/exit. */
15
46
  export function outcomeFailed(outcome) {
16
- return outcome.kind !== "ok" && outcome.kind !== "dry-run";
47
+ return outcome.kind !== "ok" && outcome.kind !== "dry-run" && outcome.kind !== "already-applied";
48
+ }
49
+ /**
50
+ * Pre-flight scan of every declared `tempId`, BEFORE any leg runs. Returns the
51
+ * name→line index of valid declarations and, per offending line, the usage
52
+ * detail. Any declaration error rejects the WHOLE batch (nothing executes) —
53
+ * a temp-id script with a bad/duplicate/misplaced handle is a structural error,
54
+ * like a torn JSONL line.
55
+ */
56
+ function validateDeclarations(ops) {
57
+ const declIndex = new Map();
58
+ const errors = new Map();
59
+ for (let i = 0; i < ops.length; i++) {
60
+ const entry = ops[i];
61
+ if (typeof entry !== "object" || entry === null)
62
+ continue;
63
+ const tempId = entry.tempId;
64
+ if (tempId === undefined)
65
+ continue;
66
+ if (entry.op === "tag.add") {
67
+ errors.set(i, 'tempId is not valid on "tag.add": a tag has no uuid to bind — reference a tag by its title instead');
68
+ continue;
69
+ }
70
+ if (!UUID_MINTING_OPS.has(entry.op)) {
71
+ errors.set(i, `tempId is only valid on an op that creates something (e.g. todo.add, project.add, project.add-heading) — not "${entry.op}"`);
72
+ continue;
73
+ }
74
+ if (typeof tempId !== "string" || !TEMP_ID_RE.test(tempId)) {
75
+ errors.set(i, "tempId must match [A-Za-z0-9_-] and be 1–32 characters");
76
+ continue;
77
+ }
78
+ const prior = declIndex.get(tempId);
79
+ if (prior !== undefined) {
80
+ errors.set(i, `duplicate tempId "${tempId}" (already declared on line ${prior + 1})`);
81
+ continue;
82
+ }
83
+ declIndex.set(tempId, i);
84
+ }
85
+ return { declIndex, errors };
86
+ }
87
+ /** Parse a `"$name"` / `"$name.instance"` token into its handle + accessor. */
88
+ function parseRef(value) {
89
+ const body = value.slice(1); // drop the leading "$"
90
+ const dot = body.indexOf(".");
91
+ if (dot < 0)
92
+ return { name: body };
93
+ return { name: body.slice(0, dot), accessor: body.slice(dot + 1) };
94
+ }
95
+ /** Resolve one `$`-token to a uuid, or a fail-closed usage detail. */
96
+ function resolveRef(value, declIndex, bindings, currentIndex) {
97
+ const { name, accessor } = parseRef(value);
98
+ const declaredAt = declIndex.get(name);
99
+ if (declaredAt === undefined) {
100
+ return {
101
+ ok: false,
102
+ detail: `unresolved-temp-ref: "${value}" names no tempId declared in this batch`,
103
+ };
104
+ }
105
+ const bound = bindings.get(name);
106
+ if (bound === undefined) {
107
+ if (declaredAt > currentIndex) {
108
+ return {
109
+ ok: false,
110
+ detail: `unresolved-temp-ref: "${value}" is a forward reference — tempId "${name}" is declared later (line ${declaredAt + 1})`,
111
+ };
112
+ }
113
+ return {
114
+ ok: false,
115
+ detail: `unresolved-temp-ref: tempId "${name}" bound nothing (its op failed or was skipped) — cannot resolve "${value}"`,
116
+ };
117
+ }
118
+ if (accessor === undefined)
119
+ return { ok: true, uuid: bound.primary };
120
+ if (accessor === "instance") {
121
+ if (bound.instance === null) {
122
+ return { ok: false, detail: `"${value}" has no spawned-instance uuid to resolve` };
123
+ }
124
+ return { ok: true, uuid: bound.instance };
125
+ }
126
+ if (accessor === "replaced") {
127
+ if (bound.replaced === null) {
128
+ return { ok: false, detail: `"${value}" has no replaced uuid to resolve` };
129
+ }
130
+ return { ok: true, uuid: bound.replaced };
131
+ }
132
+ return {
133
+ ok: false,
134
+ detail: `"${value}" uses an unknown accessor ".${accessor}" (only .instance and .replaced exist)`,
135
+ };
136
+ }
137
+ /** True for a string that must be treated as a temp ref (leading `$`). */
138
+ function isRef(value) {
139
+ return typeof value === "string" && value.startsWith("$");
140
+ }
141
+ /**
142
+ * Resolve every `$`-reference in a line's ref-accepting params against the
143
+ * current bindings. Returns rewritten params, or the FIRST fail-closed detail
144
+ * (that line becomes invalid; independent later lines still run).
145
+ */
146
+ function resolveRefs(params, declIndex, bindings, currentIndex) {
147
+ const out = { ...params };
148
+ const resolveOne = (value) => resolveRef(value, declIndex, bindings, currentIndex);
149
+ for (const key of Object.keys(out)) {
150
+ if (!REF_KEYS.has(key))
151
+ continue;
152
+ const value = out[key];
153
+ if (isRef(value)) {
154
+ const r = resolveOne(value);
155
+ if (!r.ok)
156
+ return r;
157
+ out[key] = r.uuid;
158
+ }
159
+ else if (Array.isArray(value)) {
160
+ const next = [...value];
161
+ for (let i = 0; i < next.length; i++) {
162
+ if (isRef(next[i])) {
163
+ const r = resolveOne(next[i]);
164
+ if (!r.ok)
165
+ return r;
166
+ next[i] = r.uuid;
167
+ }
168
+ }
169
+ out[key] = next;
170
+ }
171
+ else if (typeof value === "object" && value !== null) {
172
+ // Container ref: resolve a `$` uuid sub-field, leave title/others alone.
173
+ const ref = value;
174
+ if (isRef(ref["uuid"])) {
175
+ const r = resolveOne(ref["uuid"]);
176
+ if (!r.ok)
177
+ return r;
178
+ out[key] = { ...ref, uuid: r.uuid };
179
+ }
180
+ }
181
+ }
182
+ return { ok: true, params: out };
183
+ }
184
+ /** Does any line use a `$`-reference in a ref-accepting param? (dry-run gate) */
185
+ function usesRef(params) {
186
+ for (const key of Object.keys(params)) {
187
+ if (!REF_KEYS.has(key))
188
+ continue;
189
+ const value = params[key];
190
+ if (isRef(value))
191
+ return true;
192
+ if (Array.isArray(value) && value.some(isRef))
193
+ return true;
194
+ if (typeof value === "object" &&
195
+ value !== null &&
196
+ isRef(value["uuid"]))
197
+ return true;
198
+ }
199
+ return false;
200
+ }
201
+ /** Bind a leg's discovered uuids to its temp id, once it verifies ok. */
202
+ function bindingFromResult(result) {
203
+ if (typeof result.uuid !== "string")
204
+ return null;
205
+ return {
206
+ primary: result.uuid,
207
+ instance: result.repeating?.instanceUuid ?? null,
208
+ replaced: result.repeating?.replacedUuid ?? null,
209
+ };
210
+ }
211
+ /** Write the batch-level summary audit record (role "summary"; its undoToken is the batch token). */
212
+ function auditBatchSummary(deps, startedAt, txnId, legCount, tempIdMapping, actor) {
213
+ const fp = deps.fingerprint();
214
+ const hasMapping = Object.keys(tempIdMapping).length > 0;
215
+ const record = {
216
+ v: 1,
217
+ ts: startedAt.toISOString(),
218
+ actor,
219
+ host: deps.config.host,
220
+ op: "batch",
221
+ uuid: null,
222
+ vector: null,
223
+ disruption: null,
224
+ invocation: `batch ×${legCount}`,
225
+ txn: { id: txnId, role: "summary" },
226
+ requested: { legCount, ...(hasMapping && { tempIdMapping }) },
227
+ pre: null,
228
+ observed: hasMapping ? { tempIdMapping } : null,
229
+ result: "ok",
230
+ verify: null,
231
+ durationMs: (deps.now?.() ?? new Date()).getTime() - startedAt.getTime(),
232
+ env: {
233
+ pkg: deps.pkgVersion ?? "0.0.1",
234
+ dbVersion: fp.observation.databaseVersion,
235
+ fingerprint: fingerprintLabel(fp, deps.config),
236
+ },
237
+ };
238
+ deps.audit.append(record);
17
239
  }
18
240
  export async function runBatch(deps, ops, options = {}, onResult) {
241
+ const startedAt = deps.now?.() ?? new Date();
242
+ const actor = options.actor ?? deps.config.actor;
243
+ const bindings = new Map();
244
+ const tempIdMapping = {};
245
+ // Pre-flight: validate every temp-id declaration BEFORE any leg runs.
246
+ const { declIndex, errors: declErrors } = validateDeclarations(ops);
247
+ if (declErrors.size > 0) {
248
+ const results = ops.map((entry, index) => {
249
+ const op = String(entry?.op);
250
+ const detail = declErrors.get(index);
251
+ const outcome = detail !== undefined
252
+ ? { kind: "invalid", op, detail }
253
+ : {
254
+ kind: "skipped",
255
+ op,
256
+ detail: "not run — the batch has a temp-id declaration error (see the invalid line)",
257
+ };
258
+ const result = { index, op, outcome };
259
+ onResult?.(result);
260
+ return result;
261
+ });
262
+ return { results, tempIdMapping };
263
+ }
264
+ // opId idempotency reads the recent trail ONCE, before executing.
265
+ const usesOpId = ops.some((o) => typeof o === "object" && o !== null && o.opId !== undefined);
266
+ const priorRecords = usesOpId && deps.auditDirPath !== undefined ? readAuditRecords(deps.auditDirPath) : [];
267
+ const txnId = `txn-batch-${startedAt.getTime().toString(36)}-${process.pid.toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
19
268
  const results = [];
20
269
  let halted = false;
21
- for (let index = 0; index < ops.length; index++) {
22
- const entry = ops[index];
23
- let outcome;
24
- if (halted) {
25
- outcome = {
26
- kind: "skipped",
27
- op: String(entry?.op),
28
- detail: "skipped after earlier failure (--fail-fast)",
270
+ let legsDispatched = 0;
271
+ /** Bind (or rebind) a temp id to a discovered uuid so later `$refs` resolve. */
272
+ const bind = (tempId, binding) => {
273
+ bindings.set(tempId, binding);
274
+ tempIdMapping[tempId] = binding.primary;
275
+ };
276
+ /** Compute one line's outcome + bound uuid; mutates bindings/dispatch counter. */
277
+ const runLine = async (entry, index, tempId, opId) => {
278
+ if (typeof entry !== "object" || entry === null || typeof entry.op !== "string") {
279
+ return {
280
+ outcome: {
281
+ kind: "invalid",
282
+ op: String(entry?.op),
283
+ detail: "each op needs {op, params}",
284
+ },
29
285
  };
30
286
  }
31
- else if (typeof entry !== "object" || entry === null || typeof entry.op !== "string") {
32
- outcome = {
33
- kind: "invalid",
34
- op: String(entry?.op),
35
- detail: "each op needs {op, params}",
287
+ if (!KNOWN_OPS.has(entry.op)) {
288
+ return {
289
+ outcome: {
290
+ kind: "invalid",
291
+ op: entry.op,
292
+ detail: `unknown op "${entry.op}" — see \`things capabilities\``,
293
+ },
36
294
  };
37
295
  }
38
- else if (!KNOWN_OPS.has(entry.op)) {
39
- outcome = {
40
- kind: "invalid",
41
- op: entry.op,
42
- detail: `unknown op "${entry.op}" — see \`things capabilities\``,
296
+ if (typeof entry.params !== "object" || entry.params === null) {
297
+ return { outcome: { kind: "invalid", op: entry.op, detail: "params must be an object" } };
298
+ }
299
+ if (opId !== undefined && !OP_ID_RE.test(opId)) {
300
+ return {
301
+ outcome: {
302
+ kind: "invalid",
303
+ op: entry.op,
304
+ detail: "opId must match [A-Za-z0-9_-] and be 1–64 characters",
305
+ },
43
306
  };
44
307
  }
45
- else if (typeof entry.params !== "object" || entry.params === null) {
46
- outcome = { kind: "invalid", op: entry.op, detail: "params must be an object" };
308
+ // opId idempotency: an earlier submission already applied this line — skip,
309
+ // report already-applied with the recorded uuid, and rebind it so later
310
+ // $refs still resolve.
311
+ if (opId !== undefined) {
312
+ const applied = findAppliedOpId(priorRecords, opId, startedAt);
313
+ if (applied !== undefined) {
314
+ const uuid = applied.uuid ?? "";
315
+ if (tempId !== undefined && uuid !== "")
316
+ bind(tempId, { primary: uuid, instance: null, replaced: null });
317
+ return {
318
+ outcome: {
319
+ kind: "already-applied",
320
+ op: entry.op,
321
+ uuid,
322
+ detail: "already applied by an earlier submission (matching opId in the change history) — not re-run",
323
+ },
324
+ ...(tempId !== undefined && uuid !== "" && { boundUuid: uuid }),
325
+ };
326
+ }
47
327
  }
48
- else {
49
- const writeOptions = {
50
- ...(entry.options?.acknowledgeChecklistReset !== undefined && {
51
- acknowledgeChecklistReset: entry.options.acknowledgeChecklistReset,
52
- }),
53
- ...(entry.options?.acknowledgeProjectReopen !== undefined && {
54
- acknowledgeProjectReopen: entry.options.acknowledgeProjectReopen,
55
- }),
56
- ...(entry.options?.dangerouslyPermanent !== undefined && {
57
- dangerouslyPermanent: entry.options.dangerouslyPermanent,
58
- }),
59
- ...(entry.options?.acknowledgeTagSubtree !== undefined && {
60
- acknowledgeTagSubtree: entry.options.acknowledgeTagSubtree,
61
- }),
62
- ...(entry.options?.vector !== undefined && { vector: entry.options.vector }),
63
- ...(entry.options?.verifyTimeoutMs !== undefined && {
64
- verifyTimeoutMs: entry.options.verifyTimeoutMs,
65
- }),
66
- ...(entry.options?.maxDisruption !== undefined && {
67
- maxDisruption: entry.options.maxDisruption,
68
- }),
69
- ...(options.dryRun === true && { dryRun: true }),
70
- ...(options.actor !== undefined && { actor: options.actor }),
328
+ // Temp refs resolve only during real execution — nothing is created in a
329
+ // dry-run to bind them, so preview a ref-using line as skipped (not a false
330
+ // failure).
331
+ if (options.dryRun === true && usesRef(entry.params)) {
332
+ return {
333
+ outcome: {
334
+ kind: "skipped",
335
+ op: entry.op,
336
+ detail: "not previewed temp-references resolve only during real execution (not --dry-run)",
337
+ },
71
338
  };
72
- try {
73
- // Params arrive as parsed JSON; the pipeline's pre-read + guards are
74
- // the runtime validators (loud on bad shapes), same as single ops.
75
- outcome =
76
- entry.op === "reorder"
77
- ? // batch ops run sequentially by design: the mutation lock serializes them and create-probe verification must never race
78
- await runReorder(deps, entry.params, writeOptions)
79
- : // same sequencing requirement as the reorder branch above; batch is
80
- // a consumer entry point, so a consumer `when` normalizes to the zone
81
- await runMutation(deps, entry.op, entry.params, { ...writeOptions, normalizeWhen: true });
339
+ }
340
+ const resolved = resolveRefs(entry.params, declIndex, bindings, index);
341
+ if (!resolved.ok) {
342
+ return { outcome: { kind: "invalid", op: entry.op, detail: resolved.detail } };
343
+ }
344
+ const writeOptions = {
345
+ ...(entry.options?.acknowledgeChecklistReset !== undefined && {
346
+ acknowledgeChecklistReset: entry.options.acknowledgeChecklistReset,
347
+ }),
348
+ ...(entry.options?.acknowledgeProjectReopen !== undefined && {
349
+ acknowledgeProjectReopen: entry.options.acknowledgeProjectReopen,
350
+ }),
351
+ ...(entry.options?.dangerouslyPermanent !== undefined && {
352
+ dangerouslyPermanent: entry.options.dangerouslyPermanent,
353
+ }),
354
+ ...(entry.options?.acknowledgeTagSubtree !== undefined && {
355
+ acknowledgeTagSubtree: entry.options.acknowledgeTagSubtree,
356
+ }),
357
+ ...(entry.options?.allowNonEmptyArea !== undefined && {
358
+ allowNonEmptyArea: entry.options.allowNonEmptyArea,
359
+ }),
360
+ ...(entry.options?.dangerouslyDriveGui !== undefined && {
361
+ dangerouslyDriveGui: entry.options.dangerouslyDriveGui,
362
+ }),
363
+ ...(entry.options?.createTags !== undefined && { createTags: entry.options.createTags }),
364
+ ...(entry.options?.vector !== undefined && { vector: entry.options.vector }),
365
+ ...(entry.options?.verifyTimeoutMs !== undefined && {
366
+ verifyTimeoutMs: entry.options.verifyTimeoutMs,
367
+ }),
368
+ ...(entry.options?.maxDisruption !== undefined && {
369
+ maxDisruption: entry.options.maxDisruption,
370
+ }),
371
+ ...(options.dryRun === true && { dryRun: true }),
372
+ ...(opId !== undefined && { opId }),
373
+ // Group every leg under one txn so the batch summary's undo token replays
374
+ // the whole submission (dry-run legs are not recorded, so no txn).
375
+ ...(options.dryRun !== true && { txn: { id: txnId, role: "leg" } }),
376
+ actor,
377
+ };
378
+ try {
379
+ // Params arrive as parsed JSON; the pipeline's pre-read + guards are the
380
+ // runtime validators (loud on bad shapes), same as single ops.
381
+ const result = entry.op === "reorder"
382
+ ? // batch ops run sequentially by design: the mutation lock serializes them and create-probe verification must never race
383
+ await runReorder(deps, resolved.params, writeOptions)
384
+ : // same sequencing requirement as the reorder branch above; batch is a
385
+ // consumer entry point, so a consumer `when` normalizes to the zone
386
+ await runMutation(deps, entry.op, resolved.params, { ...writeOptions, normalizeWhen: true });
387
+ if (options.dryRun !== true)
388
+ legsDispatched += 1;
389
+ // A leg binds its temp id ONLY when it verified ok — a failed leg
390
+ // (including a rule-mismatch that surfaces discovery uuids in its error)
391
+ // binds nothing, so later refs to it fail fast.
392
+ let boundUuid;
393
+ if (result.kind === "ok" && tempId !== undefined) {
394
+ const binding = bindingFromResult(result);
395
+ if (binding !== null) {
396
+ bind(tempId, binding);
397
+ boundUuid = binding.primary;
398
+ }
82
399
  }
83
- catch (err) {
84
- // Param-shape errors (exclusive combos etc.) surface per-op, not fatally.
85
- outcome = {
400
+ return { outcome: result, ...(boundUuid !== undefined && { boundUuid }) };
401
+ }
402
+ catch (err) {
403
+ // Param-shape errors (exclusive combos etc.) surface per-op, not fatally.
404
+ return {
405
+ outcome: {
86
406
  kind: "invalid",
87
407
  op: entry.op,
88
408
  detail: err instanceof Error ? err.message : String(err),
89
- };
90
- }
409
+ },
410
+ };
411
+ }
412
+ };
413
+ for (let index = 0; index < ops.length; index++) {
414
+ const entry = ops[index];
415
+ const tempId = typeof entry === "object" && entry !== null ? entry.tempId : undefined;
416
+ const opId = typeof entry === "object" && entry !== null ? entry.opId : undefined;
417
+ let outcome;
418
+ let boundUuid;
419
+ if (halted) {
420
+ outcome = {
421
+ kind: "skipped",
422
+ op: String(entry?.op),
423
+ detail: "skipped after earlier failure (--fail-fast)",
424
+ };
91
425
  }
92
- const result = { index, op: String(entry?.op), outcome };
426
+ else {
427
+ ({ outcome, boundUuid } = await runLine(entry, index, tempId, opId));
428
+ }
429
+ const result = {
430
+ index,
431
+ op: String(entry?.op),
432
+ outcome,
433
+ ...(tempId !== undefined && { tempId }),
434
+ ...(boundUuid !== undefined && { boundUuid }),
435
+ ...(opId !== undefined && { opId }),
436
+ };
93
437
  results.push(result);
94
438
  onResult?.(result);
95
439
  if (options.failFast === true && !halted && outcomeFailed(outcome))
96
440
  halted = true;
97
441
  }
98
- return results;
442
+ // Write the batch summary (and mint the batch undo token) when at least one
443
+ // leg actually reached the pipeline. Skipped for dry-run and all-rejected
444
+ // batches — there is nothing to undo.
445
+ let undoToken;
446
+ if (options.dryRun !== true && legsDispatched > 0) {
447
+ auditBatchSummary(deps, startedAt, txnId, legsDispatched, tempIdMapping, actor);
448
+ undoToken = txnId;
449
+ }
450
+ return { results, tempIdMapping, ...(undoToken !== undefined && { undoToken }) };
99
451
  }
100
452
  //# sourceMappingURL=batch.js.map