things-api 0.18.0 → 0.19.1

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 (224) hide show
  1. package/README.md +18 -9
  2. package/deputy/AppIcon.icns +0 -0
  3. package/deputy/VERSION +1 -1
  4. package/deputy/helpers-Info.plist +4 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +3 -1
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
  10. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
  11. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  12. package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
  13. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
  14. package/deputy/reader/Info.plist +2 -0
  15. package/deputy/reader/entitlements.plist +30 -0
  16. package/deputy/reader/main.swift +129 -40
  17. package/deputy/src/server.swift +25 -2
  18. package/deputy/src/tcc.swift +58 -0
  19. package/dist/audit/schema.d.ts +30 -1
  20. package/dist/audit/schema.js.map +1 -1
  21. package/dist/capability.d.ts +322 -0
  22. package/dist/capability.js +706 -0
  23. package/dist/capability.js.map +1 -0
  24. package/dist/cli/commands/doctor.d.ts +13 -1
  25. package/dist/cli/commands/doctor.js +141 -5
  26. package/dist/cli/commands/doctor.js.map +1 -1
  27. package/dist/cli/commands/helpers.js +189 -51
  28. package/dist/cli/commands/helpers.js.map +1 -1
  29. package/dist/cli/commands/mcp.js +8 -0
  30. package/dist/cli/commands/mcp.js.map +1 -1
  31. package/dist/cli/commands/op-result.js +6 -0
  32. package/dist/cli/commands/op-result.js.map +1 -1
  33. package/dist/cli/commands/reads.js +47 -1
  34. package/dist/cli/commands/reads.js.map +1 -1
  35. package/dist/cli/commands/repeat-flags.js +34 -2
  36. package/dist/cli/commands/repeat-flags.js.map +1 -1
  37. package/dist/cli/commands/setup.d.ts +9 -0
  38. package/dist/cli/commands/setup.js +76 -83
  39. package/dist/cli/commands/setup.js.map +1 -1
  40. package/dist/cli/commands/writes.d.ts +1 -1
  41. package/dist/cli/commands/writes.js +311 -142
  42. package/dist/cli/commands/writes.js.map +1 -1
  43. package/dist/cli/dry-run.d.ts +1 -1
  44. package/dist/cli/dry-run.js +1 -1
  45. package/dist/cli/glyphs.d.ts +23 -4
  46. package/dist/cli/glyphs.js +46 -19
  47. package/dist/cli/glyphs.js.map +1 -1
  48. package/dist/cli/help.d.ts +1 -1
  49. package/dist/cli/help.js +49 -27
  50. package/dist/cli/help.js.map +1 -1
  51. package/dist/cli/interrupt.d.ts +54 -6
  52. package/dist/cli/interrupt.js +78 -12
  53. package/dist/cli/interrupt.js.map +1 -1
  54. package/dist/cli/main.js +8 -5
  55. package/dist/cli/main.js.map +1 -1
  56. package/dist/cli/period.d.ts +3 -1
  57. package/dist/cli/period.js +1 -1
  58. package/dist/cli/period.js.map +1 -1
  59. package/dist/cli/read-driver.js +26 -1
  60. package/dist/cli/read-driver.js.map +1 -1
  61. package/dist/cli/render.d.ts +31 -4
  62. package/dist/cli/render.js +127 -15
  63. package/dist/cli/render.js.map +1 -1
  64. package/dist/client.d.ts +23 -3
  65. package/dist/client.js +36 -13
  66. package/dist/client.js.map +1 -1
  67. package/dist/contracts.d.ts +8 -4
  68. package/dist/contracts.js +1 -1
  69. package/dist/contracts.js.map +1 -1
  70. package/dist/deputy/install.d.ts +234 -6
  71. package/dist/deputy/install.js +843 -31
  72. package/dist/deputy/install.js.map +1 -1
  73. package/dist/deputy/notices.js +2 -2
  74. package/dist/deputy/notices.js.map +1 -1
  75. package/dist/deputy/protocol.d.ts +59 -5
  76. package/dist/deputy/protocol.js +48 -11
  77. package/dist/deputy/protocol.js.map +1 -1
  78. package/dist/deputy/routing.d.ts +37 -0
  79. package/dist/deputy/routing.js +181 -27
  80. package/dist/deputy/routing.js.map +1 -1
  81. package/dist/deputy/wake.d.ts +61 -0
  82. package/dist/deputy/wake.js +149 -0
  83. package/dist/deputy/wake.js.map +1 -0
  84. package/dist/diagnose.d.ts +19 -4
  85. package/dist/diagnose.js +64 -8
  86. package/dist/diagnose.js.map +1 -1
  87. package/dist/direct-setup.d.ts +104 -0
  88. package/dist/direct-setup.js +658 -0
  89. package/dist/direct-setup.js.map +1 -0
  90. package/dist/host-access.d.ts +75 -0
  91. package/dist/host-access.js +177 -0
  92. package/dist/host-access.js.map +1 -0
  93. package/dist/index.d.ts +20 -5
  94. package/dist/index.js +14 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/mcp/server.d.ts +27 -1
  97. package/dist/mcp/server.js +308 -109
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/op-result.d.ts +9 -0
  100. package/dist/op-result.js +13 -1
  101. package/dist/op-result.js.map +1 -1
  102. package/dist/read/filter-contract.d.ts +1 -1
  103. package/dist/read/filter-contract.js +4 -0
  104. package/dist/read/filter-contract.js.map +1 -1
  105. package/dist/read/shape.js +4 -0
  106. package/dist/read/shape.js.map +1 -1
  107. package/dist/read/views.d.ts +29 -0
  108. package/dist/read/views.js +60 -0
  109. package/dist/read/views.js.map +1 -1
  110. package/dist/session-grant.d.ts +48 -0
  111. package/dist/session-grant.js +185 -0
  112. package/dist/session-grant.js.map +1 -0
  113. package/dist/surface-copy.d.ts +19 -3
  114. package/dist/surface-copy.js +19 -3
  115. package/dist/surface-copy.js.map +1 -1
  116. package/dist/wizard.d.ts +58 -0
  117. package/dist/wizard.js +174 -0
  118. package/dist/wizard.js.map +1 -0
  119. package/dist/write/availability.d.ts +1 -16
  120. package/dist/write/availability.js +10 -65
  121. package/dist/write/availability.js.map +1 -1
  122. package/dist/write/batch.d.ts +20 -0
  123. package/dist/write/batch.js +111 -21
  124. package/dist/write/batch.js.map +1 -1
  125. package/dist/write/capabilities.d.ts +9 -0
  126. package/dist/write/capabilities.js +2 -0
  127. package/dist/write/capabilities.js.map +1 -1
  128. package/dist/write/clear-reminder.js +1 -1
  129. package/dist/write/clear-reminder.js.map +1 -1
  130. package/dist/write/commands.d.ts +22 -7
  131. package/dist/write/commands.js +231 -254
  132. package/dist/write/commands.js.map +1 -1
  133. package/dist/write/failure-hints.d.ts +32 -7
  134. package/dist/write/failure-hints.js +9 -14
  135. package/dist/write/failure-hints.js.map +1 -1
  136. package/dist/write/guards.js +22 -4
  137. package/dist/write/guards.js.map +1 -1
  138. package/dist/write/lock.d.ts +16 -13
  139. package/dist/write/lock.js +55 -0
  140. package/dist/write/lock.js.map +1 -1
  141. package/dist/write/make-repeating-project.js +9 -13
  142. package/dist/write/make-repeating-project.js.map +1 -1
  143. package/dist/write/move.js +9 -12
  144. package/dist/write/move.js.map +1 -1
  145. package/dist/write/operations.d.ts +51 -29
  146. package/dist/write/operations.js +16 -0
  147. package/dist/write/operations.js.map +1 -1
  148. package/dist/write/opid.d.ts +72 -14
  149. package/dist/write/opid.js +152 -8
  150. package/dist/write/opid.js.map +1 -1
  151. package/dist/write/param-schema.d.ts +80 -0
  152. package/dist/write/param-schema.js +754 -0
  153. package/dist/write/param-schema.js.map +1 -0
  154. package/dist/write/pipeline.d.ts +110 -5
  155. package/dist/write/pipeline.js +288 -14
  156. package/dist/write/pipeline.js.map +1 -1
  157. package/dist/write/pre-state.d.ts +38 -16
  158. package/dist/write/pre-state.js +117 -22
  159. package/dist/write/pre-state.js.map +1 -1
  160. package/dist/write/promote-clone.js +658 -444
  161. package/dist/write/promote-clone.js.map +1 -1
  162. package/dist/write/reorder.js +25 -8
  163. package/dist/write/reorder.js.map +1 -1
  164. package/dist/write/repeat-collateral.d.ts +31 -0
  165. package/dist/write/repeat-collateral.js +153 -0
  166. package/dist/write/repeat-collateral.js.map +1 -0
  167. package/dist/write/repeat-rule.d.ts +28 -1
  168. package/dist/write/repeat-rule.js +54 -4
  169. package/dist/write/repeat-rule.js.map +1 -1
  170. package/dist/write/resolution-timestamps.d.ts +11 -0
  171. package/dist/write/resolution-timestamps.js +108 -40
  172. package/dist/write/resolution-timestamps.js.map +1 -1
  173. package/dist/write/reversibility.js +4 -0
  174. package/dist/write/reversibility.js.map +1 -1
  175. package/dist/write/template-mutation.d.ts +137 -0
  176. package/dist/write/template-mutation.js +598 -0
  177. package/dist/write/template-mutation.js.map +1 -0
  178. package/dist/write/undo.js +10 -20
  179. package/dist/write/undo.js.map +1 -1
  180. package/dist/write/update-fields.d.ts +151 -0
  181. package/dist/write/update-fields.js +427 -0
  182. package/dist/write/update-fields.js.map +1 -0
  183. package/dist/write/vectors/applescript.js +6 -10
  184. package/dist/write/vectors/applescript.js.map +1 -1
  185. package/dist/write/vectors/registry.d.ts +1 -1
  186. package/dist/write/vectors/registry.js +10 -2
  187. package/dist/write/vectors/registry.js.map +1 -1
  188. package/dist/write/vectors/shortcuts.js +2 -2
  189. package/dist/write/vectors/shortcuts.js.map +1 -1
  190. package/dist/write/vectors/simulator.js +15 -4
  191. package/dist/write/vectors/simulator.js.map +1 -1
  192. package/dist/write/vectors/types.d.ts +268 -1
  193. package/dist/write/vectors/ui-certification.js +63 -13
  194. package/dist/write/vectors/ui-certification.js.map +1 -1
  195. package/dist/write/vectors/ui-chord.d.ts +144 -0
  196. package/dist/write/vectors/ui-chord.js +428 -0
  197. package/dist/write/vectors/ui-chord.js.map +1 -0
  198. package/dist/write/vectors/ui-drag.d.ts +8 -4
  199. package/dist/write/vectors/ui-drag.js +2 -0
  200. package/dist/write/vectors/ui-drag.js.map +1 -1
  201. package/dist/write/vectors/ui-recipes.d.ts +70 -3
  202. package/dist/write/vectors/ui-recipes.js +482 -44
  203. package/dist/write/vectors/ui-recipes.js.map +1 -1
  204. package/dist/write/vectors/ui.d.ts +280 -10
  205. package/dist/write/vectors/ui.js +1068 -77
  206. package/dist/write/vectors/ui.js.map +1 -1
  207. package/dist/write/vectors/url-scheme.js +3 -0
  208. package/dist/write/vectors/url-scheme.js.map +1 -1
  209. package/dist/write/verify/delta.d.ts +62 -0
  210. package/dist/write/verify/delta.js +36 -2
  211. package/dist/write/verify/delta.js.map +1 -1
  212. package/dist/write/verify/poller.d.ts +4 -1
  213. package/dist/write/verify/poller.js +14 -0
  214. package/dist/write/verify/poller.js.map +1 -1
  215. package/package.json +2 -1
  216. package/schema/envelope.schema.json +6 -4
  217. package/scripts/build-helpers.sh +5 -0
  218. package/skills/things-cli/SKILL.md +5 -3
  219. package/skills/things-cli/references/contracts.md +1 -0
  220. package/skills/things-cli/references/data-model.md +3 -0
  221. package/skills/things-cli/references/errors.md +1 -1
  222. package/skills/things-cli/references/gui.md +1 -0
  223. package/skills/things-cli/references/repeating.md +6 -0
  224. package/skills/things-cli/references/tag-cleanup.md +64 -0
@@ -1,31 +1,89 @@
1
1
  /**
2
2
  * Shared client-idempotency (`opId`) machinery — the ONE place the lookback
3
- * window and match rule live, so the batch path and the single-op path agree by
4
- * construction rather than by two copies staying in sync.
3
+ * window, the match rule and the ambiguous-outcome reconciliation live, so the
4
+ * batch path and the single-op path agree by construction rather than by two
5
+ * copies staying in sync.
5
6
  *
6
7
  * An `opId` is a caller-supplied idempotency key recorded on the audit record of
7
8
  * the mutation it accompanied. Before a mutation that carries one is dispatched,
8
- * the recent change history is scanned for an `ok` record bearing the SAME id;
9
- * on a hit the mutation is SKIPPED (reported already-applied) rather than
10
- * re-run. The id namespace is the whole trail — a batch leg and a single op that
11
- * share an id match each other — so resubmitting an ambiguously-failed write
12
- * (as a batch line OR a single `--op-id` invocation) is safe against a double.
9
+ * the recent change history is scanned for a record bearing the SAME id; on a
10
+ * hit the mutation is SKIPPED (reported already-applied) rather than re-run. The
11
+ * id namespace is the whole trail — a batch leg and a single op that share an id
12
+ * match each other — so resubmitting an ambiguously-failed write (as a batch
13
+ * line OR a single `--op-id` invocation) is safe against a double.
14
+ *
15
+ * Two match classes:
16
+ *
17
+ * - a VERIFIED-OK record — the earlier change is known to have landed, so the
18
+ * resubmission replays its identity and runs nothing;
19
+ * - a `verify-failed:timeout` record — the earlier attempt dispatched and never
20
+ * confirmed, so what landed is unknown. That record carries the assertion the
21
+ * attempt was verifying ({@link AuditRecord.expected}), and THAT assertion is
22
+ * the presence oracle: re-evaluated against current state it says the change
23
+ * is there (replay it, disclosing the reconciliation) or is absent (execute
24
+ * normally). When the recorded assertion cannot distinguish the two — the
25
+ * operation asserts a whole-database singleton, or nothing was recorded — the
26
+ * resubmission is REFUSED with a pointer at the item, never guessed.
27
+ *
28
+ * Every other result class (blocked, unsupported, intent, the other
29
+ * verify-failed reasons) is not a match: a blocked/unsupported attempt changed
30
+ * nothing, a mismatch/silent-noop landed something the caller must look at
31
+ * first, and an intent marker means the attempt may still be in flight.
13
32
  */
14
33
  import type { AuditRecord } from "../audit/schema.ts";
34
+ import { type MutationResult, type WriteDeps, type WriteOptions } from "./pipeline.ts";
35
+ import { type DeltaSpec } from "./verify/delta.ts";
15
36
  /** The idempotency-key charset/length, identical for a batch line and a single-op flag. */
16
37
  export declare const OP_ID_RE: RegExp;
17
38
  /**
18
39
  * opId idempotency lookback: at most the last {@link OPID_LOOKBACK_RECORDS}
19
40
  * records, AND only the last {@link OPID_LOOKBACK_MS} (whichever is more
20
- * restrictive). Phase 1 matches VERIFIED-OK records only.
41
+ * restrictive).
21
42
  */
22
43
  export declare const OPID_LOOKBACK_RECORDS = 1000;
23
44
  export declare const OPID_LOOKBACK_MS: number;
45
+ /** The disclosure a reconciled replay carries — see {@link reconcileTimedOut}. */
46
+ export declare const RECONCILED_NOTE: string;
47
+ /**
48
+ * The recent-history lookback for an opId: the most recent MATCHABLE record
49
+ * carrying that id, within the last {@link OPID_LOOKBACK_RECORDS} records AND
50
+ * the last {@link OPID_LOOKBACK_MS} (whichever is more restrictive). Undo/intent/
51
+ * blocked records are naturally excluded.
52
+ */
53
+ export declare function findOpIdRecord(records: AuditRecord[], opId: string, now: Date): AuditRecord | undefined;
24
54
  /**
25
- * The recent-history lookback for an opId: the most recent `ok` record carrying
26
- * that id, within the last {@link OPID_LOOKBACK_RECORDS} records AND the last
27
- * {@link OPID_LOOKBACK_MS} (whichever is more restrictive). Undo/intent/blocked
28
- * records are naturally excluded only `result === "ok"` matches (phase 1; the
29
- * verify-failed:timeout reconciliation variant is a queued phase-2 round).
55
+ * Can the recorded assertion decide, after the fact, whether the timed-out
56
+ * change landed? Usable means the assertion is keyed to a SUBJECT the attempt
57
+ * itself identified a target row, or a creation probe naming the row it was
58
+ * making. Unusable means the assertion is either vacuous (nothing to check) or a
59
+ * whole-database singleton that names none of the items the call was about, so a
60
+ * satisfied assertion now says nothing about THIS call. `why` completes the
61
+ * sentence "… and <why>" in the refusal.
62
+ */
63
+ export type PresenceOracle = {
64
+ usable: true;
65
+ } | {
66
+ usable: false;
67
+ why: string;
68
+ };
69
+ export declare function presenceOracle(spec: DeltaSpec | undefined): PresenceOracle;
70
+ /**
71
+ * The lookback as a DISPATCH DECISION over an already-read trail: the replay
72
+ * result when this key was already applied, a refusal when it matched an
73
+ * unsettleable attempt, or null when the write must run. Shared by the batch
74
+ * line path and {@link replayIfApplied}.
75
+ */
76
+ export declare function resolveOpId(deps: WriteDeps, records: AuditRecord[], opId: string, now: Date): MutationResult | null;
77
+ /**
78
+ * The dispatch decision for a SINGLE invocation, reading the trail itself. The
79
+ * ONE gate every single-invocation entry point calls before doing anything — the
80
+ * client's single-op `run`, and the composites that own their own dispatch (the
81
+ * complete/cancel/exception template-target verbs, the `--completed-at`
82
+ * flip-dance, and the promote-via-clone verbs; they never reach `run`, so
83
+ * without this the key would be recorded and never honored).
84
+ *
85
+ * Three conditions fail-open (run the write) rather than fail-closed: no key, a
86
+ * dry run (it mints and records nothing, so there is nothing to deduplicate
87
+ * against), or no trail to read.
30
88
  */
31
- export declare function findAppliedOpId(records: AuditRecord[], opId: string, now: Date): AuditRecord | undefined;
89
+ export declare function replayIfApplied(deps: WriteDeps, options: WriteOptions): MutationResult | null;
@@ -1,25 +1,35 @@
1
+ import { replayResultFromRecord, } from "./pipeline.js";
2
+ import { readAuditRecords } from "./undo.js";
3
+ import { createDbReader, evaluateDelta } from "./verify/delta.js";
1
4
  /** The idempotency-key charset/length, identical for a batch line and a single-op flag. */
2
5
  export const OP_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
3
6
  /**
4
7
  * opId idempotency lookback: at most the last {@link OPID_LOOKBACK_RECORDS}
5
8
  * records, AND only the last {@link OPID_LOOKBACK_MS} (whichever is more
6
- * restrictive). Phase 1 matches VERIFIED-OK records only.
9
+ * restrictive).
7
10
  */
8
11
  export const OPID_LOOKBACK_RECORDS = 1000;
9
12
  export const OPID_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
13
+ /** The disclosure a reconciled replay carries — see {@link reconcileTimedOut}. */
14
+ export const RECONCILED_NOTE = "an earlier submission with this idempotency key did not finish confirming; the change was " +
15
+ "found already in place, so nothing ran again. It cannot be reversed with `things undo` — the " +
16
+ "earlier attempt never recorded a completed change to reverse";
17
+ /** Result classes an `opId` match acts on (see the module header). */
18
+ function matchable(result) {
19
+ return result === "ok" || result === "verify-failed:timeout";
20
+ }
10
21
  /**
11
- * The recent-history lookback for an opId: the most recent `ok` record carrying
12
- * that id, within the last {@link OPID_LOOKBACK_RECORDS} records AND the last
13
- * {@link OPID_LOOKBACK_MS} (whichever is more restrictive). Undo/intent/blocked
14
- * records are naturally excluded — only `result === "ok"` matches (phase 1; the
15
- * verify-failed:timeout reconciliation variant is a queued phase-2 round).
22
+ * The recent-history lookback for an opId: the most recent MATCHABLE record
23
+ * carrying that id, within the last {@link OPID_LOOKBACK_RECORDS} records AND
24
+ * the last {@link OPID_LOOKBACK_MS} (whichever is more restrictive). Undo/intent/
25
+ * blocked records are naturally excluded.
16
26
  */
17
- export function findAppliedOpId(records, opId, now) {
27
+ export function findOpIdRecord(records, opId, now) {
18
28
  const cutoff = now.getTime() - OPID_LOOKBACK_MS;
19
29
  const window = records.slice(-OPID_LOOKBACK_RECORDS);
20
30
  let match;
21
31
  for (const r of window) {
22
- if (r.opId !== opId || r.result !== "ok")
32
+ if (r.opId !== opId || !matchable(r.result))
23
33
  continue;
24
34
  if (new Date(r.ts).getTime() < cutoff)
25
35
  continue;
@@ -27,4 +37,138 @@ export function findAppliedOpId(records, opId, now) {
27
37
  }
28
38
  return match;
29
39
  }
40
+ export function presenceOracle(spec) {
41
+ if (spec === undefined) {
42
+ return {
43
+ usable: false,
44
+ why: "that attempt did not record what it was waiting to see",
45
+ };
46
+ }
47
+ switch (spec.mode) {
48
+ case "update":
49
+ case "state": {
50
+ const cascade = spec.mode === "state" ? (spec.cascade?.length ?? 0) : 0;
51
+ return spec.assert.length + cascade > 0
52
+ ? { usable: true }
53
+ : { usable: false, why: "it named no field whose value would show the change" };
54
+ }
55
+ case "entity-updated":
56
+ return spec.assert.length > 0
57
+ ? { usable: true }
58
+ : { usable: false, why: "it named no field whose value would show the change" };
59
+ case "create":
60
+ // An unbounded probe (sinceEpoch 0) matches any same-titled item ever
61
+ // created, so an older namesake would be read as this call's own work.
62
+ return spec.probe.sinceEpoch > 0
63
+ ? { usable: true }
64
+ : {
65
+ usable: false,
66
+ why: "its record of the new item is not bounded in time, so an older item of the same " +
67
+ "name would be mistaken for it",
68
+ };
69
+ case "entity-created":
70
+ case "gone":
71
+ return { usable: true };
72
+ case "ordering":
73
+ return spec.sequence.length > 1
74
+ ? { usable: true }
75
+ : { usable: false, why: "a single item's order reads the same before and after" };
76
+ case "trash-emptied":
77
+ return {
78
+ usable: false,
79
+ why: "emptying the Trash records none of the items it destroyed, so an empty Trash now " +
80
+ "says nothing about whether this call is what emptied it",
81
+ };
82
+ case "logged-now":
83
+ return {
84
+ usable: false,
85
+ why: "moving finished items to the Logbook records none of the items it moved, and Things " +
86
+ "moves them on its own as well",
87
+ };
88
+ default: {
89
+ const exhaustive = spec;
90
+ throw new Error(`unknown delta mode: ${JSON.stringify(exhaustive)}`);
91
+ }
92
+ }
93
+ }
94
+ /** The refusal: the key matched an unconfirmed attempt nothing here can settle. */
95
+ function unreconcilable(record, why) {
96
+ const target = record.uuid === null ? "the item" : record.uuid;
97
+ return {
98
+ kind: "blocked",
99
+ op: record.op,
100
+ reason: "reconcile",
101
+ detail: `an earlier submission with this idempotency key (${record.ts}) never confirmed its ` +
102
+ `change, and ${why} — so whether that change took effect cannot be settled from here`,
103
+ remediation: `check ${target} in Things (\`things op-result ${record.opId ?? "<key>"}\` reports what the ` +
104
+ "earlier attempt recorded); once you know whether the change is there, leave it alone or " +
105
+ "re-run with a NEW --op-id",
106
+ };
107
+ }
108
+ /**
109
+ * Reconcile a resubmission against a TIMED-OUT original: re-read current state
110
+ * through the attempt's own recorded assertion and decide.
111
+ *
112
+ * - satisfied → the change is there: replay it as already-applied, disclosing
113
+ * that a timeout was reconciled. The replay carries NO undo token — the trail
114
+ * holds no completed-change record for undo to reverse — and takes its uuid
115
+ * from the re-read, which is how a create whose uuid was never discovered
116
+ * (recorded `uuid: null`) still answers with the row it made.
117
+ * - not satisfied → the change is absent: return null so the write runs.
118
+ * - no usable oracle → refuse (never a guess).
119
+ */
120
+ function reconcileTimedOut(deps, record) {
121
+ const oracle = presenceOracle(record.expected);
122
+ if (!oracle.usable)
123
+ return unreconcilable(record, oracle.why);
124
+ const spec = record.expected;
125
+ const evaluation = evaluateDelta(spec, createDbReader(deps.db, deps.now?.() ?? new Date(), deps.zone),
126
+ // The movement classification needs the attempt's pre-read, which is long
127
+ // gone; only `satisfied`/`observed` are consulted here, and neither uses it.
128
+ { modDates: {}, fields: {} });
129
+ if (!evaluation.satisfied)
130
+ return null;
131
+ const replay = replayResultFromRecord(record);
132
+ if (replay.kind !== "ok")
133
+ return replay; // replayResultFromRecord always builds an ok
134
+ const { undoToken: _noToken, ...rest } = replay;
135
+ return {
136
+ ...rest,
137
+ uuid: evaluation.discoveredUuid ?? record.uuid,
138
+ observed: evaluation.observed,
139
+ ...(evaluation.repeating !== undefined && { repeating: evaluation.repeating }),
140
+ warnings: [...(replay.warnings ?? []), RECONCILED_NOTE],
141
+ };
142
+ }
143
+ /**
144
+ * The lookback as a DISPATCH DECISION over an already-read trail: the replay
145
+ * result when this key was already applied, a refusal when it matched an
146
+ * unsettleable attempt, or null when the write must run. Shared by the batch
147
+ * line path and {@link replayIfApplied}.
148
+ */
149
+ export function resolveOpId(deps, records, opId, now) {
150
+ const match = findOpIdRecord(records, opId, now);
151
+ if (match === undefined)
152
+ return null;
153
+ return match.result === "ok" ? replayResultFromRecord(match) : reconcileTimedOut(deps, match);
154
+ }
155
+ /**
156
+ * The dispatch decision for a SINGLE invocation, reading the trail itself. The
157
+ * ONE gate every single-invocation entry point calls before doing anything — the
158
+ * client's single-op `run`, and the composites that own their own dispatch (the
159
+ * complete/cancel/exception template-target verbs, the `--completed-at`
160
+ * flip-dance, and the promote-via-clone verbs; they never reach `run`, so
161
+ * without this the key would be recorded and never honored).
162
+ *
163
+ * Three conditions fail-open (run the write) rather than fail-closed: no key, a
164
+ * dry run (it mints and records nothing, so there is nothing to deduplicate
165
+ * against), or no trail to read.
166
+ */
167
+ export function replayIfApplied(deps, options) {
168
+ if (options.opId === undefined || options.dryRun === true)
169
+ return null;
170
+ if (deps.auditDirPath === undefined)
171
+ return null;
172
+ return resolveOpId(deps, readAuditRecords(deps.auditDirPath), options.opId, deps.now?.() ?? new Date());
173
+ }
30
174
  //# sourceMappingURL=opid.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"opid.js","sourceRoot":"","sources":["../../src/write/opid.ts"],"names":[],"mappings":"AAeA,2FAA2F;AAC3F,MAAM,CAAC,MAAM,QAAQ,GAAG,uBAAuB,CAAC;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAsB,EACtB,IAAY,EACZ,GAAS;IAET,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,gBAAgB,CAAC;IAChD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACrD,IAAI,KAA8B,CAAC;IACnC,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI;YAAE,SAAS;QACnD,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,GAAG,MAAM;YAAE,SAAS;QAChD,KAAK,GAAG,CAAC,CAAC,CAAC,4DAA4D;IACzE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"opid.js","sourceRoot":"","sources":["../../src/write/opid.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import { type OperationKind, type OperationParamsMap } from "./operations.ts";
2
+ /**
3
+ * The shape vocabulary. `custom` carries a hand-written validator for the shapes
4
+ * the primitives cannot express (a discriminated bag, a structured item list, a
5
+ * calendar anchor — those WRAP the engine's existing validators rather than
6
+ * restating them). `unvalidated` is the escape hatch and requires a WRITTEN
7
+ * reason; there are currently none.
8
+ */
9
+ export type FieldKind = "string" | "boolean" | "number" | "stringArray" | "enum" | "container" | "custom" | "unvalidated";
10
+ export interface FieldSpec {
11
+ kind: FieldKind;
12
+ /** Absent (or explicitly `null`) is accepted. */
13
+ optional: boolean;
14
+ /** Behavioral description of the accepted shape — the "expected" half of every refusal. */
15
+ describe: string;
16
+ /** enum: the accepted values. */
17
+ values?: readonly string[];
18
+ /** string: reject the empty string. */
19
+ nonEmpty?: boolean;
20
+ /** number: whole numbers only, and the inclusive bounds. */
21
+ integer?: boolean;
22
+ min?: number;
23
+ max?: number;
24
+ /** custom: returns a refusal detail, or null when the value is well-shaped. */
25
+ validate?: (value: unknown, path: string) => string | null;
26
+ /** unvalidated: why this field carries no shape check. */
27
+ reason?: string;
28
+ }
29
+ /** The received-type half of a refusal, in the words a caller reading JSON uses. */
30
+ export declare function describeType(value: unknown): string;
31
+ type OpSchema<K extends OperationKind> = {
32
+ [F in keyof OperationParamsMap[K]]-?: FieldSpec;
33
+ };
34
+ /**
35
+ * The registry. EXHAUSTIVE on both axes by construction: a new
36
+ * {@link OperationKind} breaks compilation here, and a new parameter on any
37
+ * operation breaks compilation in that operation's entry.
38
+ */
39
+ export declare const PARAM_SCHEMAS: {
40
+ [K in OperationKind]: OpSchema<K>;
41
+ };
42
+ /**
43
+ * The STRUCTURAL parameter check for one operation. Returns the FIRST refusal
44
+ * detail (JSON path · expected shape · received type), or null when the bag is
45
+ * well-shaped. Never throws; never mutates or normalizes the params.
46
+ */
47
+ export declare function validateOperationParams(op: OperationKind, params: unknown): string | null;
48
+ /** A structural parameter refusal — an input-contract error, never an app failure. */
49
+ export declare class ParamSchemaError extends RangeError {
50
+ /** The operation whose parameter bag was refused. */
51
+ readonly op: OperationKind;
52
+ constructor(op: OperationKind, detail: string);
53
+ }
54
+ /** {@link validateOperationParams}, as a throwing assertion for the pipeline. */
55
+ export declare function assertOperationParams(op: OperationKind, params: unknown): void;
56
+ /** One parameter's machine-readable summary (the `capabilities` params column). */
57
+ export interface ParamSummary {
58
+ name: string;
59
+ kind: FieldKind;
60
+ optional: boolean;
61
+ /** Behavioral description of the accepted shape. */
62
+ expects: string;
63
+ /** enum only: the accepted values. */
64
+ values?: readonly string[];
65
+ }
66
+ /**
67
+ * The per-op parameter summary `things capabilities` publishes, so the promise
68
+ * that the catalog carries "operation kinds AND their parameter shapes" is met by
69
+ * data rather than by prose.
70
+ */
71
+ export declare function paramSummary(op: OperationKind): ParamSummary[];
72
+ /** Runtime completeness law: every cataloged operation declares a schema. */
73
+ export declare function operationsMissingSchema(): OperationKind[];
74
+ /** Every `unvalidated` escape in the registry, with its written reason (currently none). */
75
+ export declare function unvalidatedFields(): {
76
+ op: OperationKind;
77
+ field: string;
78
+ reason: string;
79
+ }[];
80
+ export {};