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.
- package/README.md +18 -9
- package/deputy/AppIcon.icns +0 -0
- package/deputy/VERSION +1 -1
- package/deputy/helpers-Info.plist +4 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +3 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
- package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
- package/deputy/reader/Info.plist +2 -0
- package/deputy/reader/entitlements.plist +30 -0
- package/deputy/reader/main.swift +129 -40
- package/deputy/src/server.swift +25 -2
- package/deputy/src/tcc.swift +58 -0
- package/dist/audit/schema.d.ts +30 -1
- package/dist/audit/schema.js.map +1 -1
- package/dist/capability.d.ts +322 -0
- package/dist/capability.js +706 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +13 -1
- package/dist/cli/commands/doctor.js +141 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/helpers.js +189 -51
- package/dist/cli/commands/helpers.js.map +1 -1
- package/dist/cli/commands/mcp.js +8 -0
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/op-result.js +6 -0
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/reads.js +47 -1
- package/dist/cli/commands/reads.js.map +1 -1
- package/dist/cli/commands/repeat-flags.js +34 -2
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +9 -0
- package/dist/cli/commands/setup.js +76 -83
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/writes.d.ts +1 -1
- package/dist/cli/commands/writes.js +311 -142
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/dry-run.d.ts +1 -1
- package/dist/cli/dry-run.js +1 -1
- package/dist/cli/glyphs.d.ts +23 -4
- package/dist/cli/glyphs.js +46 -19
- package/dist/cli/glyphs.js.map +1 -1
- package/dist/cli/help.d.ts +1 -1
- package/dist/cli/help.js +49 -27
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/interrupt.d.ts +54 -6
- package/dist/cli/interrupt.js +78 -12
- package/dist/cli/interrupt.js.map +1 -1
- package/dist/cli/main.js +8 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/period.d.ts +3 -1
- package/dist/cli/period.js +1 -1
- package/dist/cli/period.js.map +1 -1
- package/dist/cli/read-driver.js +26 -1
- package/dist/cli/read-driver.js.map +1 -1
- package/dist/cli/render.d.ts +31 -4
- package/dist/cli/render.js +127 -15
- package/dist/cli/render.js.map +1 -1
- package/dist/client.d.ts +23 -3
- package/dist/client.js +36 -13
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +8 -4
- package/dist/contracts.js +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/deputy/install.d.ts +234 -6
- package/dist/deputy/install.js +843 -31
- package/dist/deputy/install.js.map +1 -1
- package/dist/deputy/notices.js +2 -2
- package/dist/deputy/notices.js.map +1 -1
- package/dist/deputy/protocol.d.ts +59 -5
- package/dist/deputy/protocol.js +48 -11
- package/dist/deputy/protocol.js.map +1 -1
- package/dist/deputy/routing.d.ts +37 -0
- package/dist/deputy/routing.js +181 -27
- package/dist/deputy/routing.js.map +1 -1
- package/dist/deputy/wake.d.ts +61 -0
- package/dist/deputy/wake.js +149 -0
- package/dist/deputy/wake.js.map +1 -0
- package/dist/diagnose.d.ts +19 -4
- package/dist/diagnose.js +64 -8
- package/dist/diagnose.js.map +1 -1
- package/dist/direct-setup.d.ts +104 -0
- package/dist/direct-setup.js +658 -0
- package/dist/direct-setup.js.map +1 -0
- package/dist/host-access.d.ts +75 -0
- package/dist/host-access.js +177 -0
- package/dist/host-access.js.map +1 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +27 -1
- package/dist/mcp/server.js +308 -109
- package/dist/mcp/server.js.map +1 -1
- package/dist/op-result.d.ts +9 -0
- package/dist/op-result.js +13 -1
- package/dist/op-result.js.map +1 -1
- package/dist/read/filter-contract.d.ts +1 -1
- package/dist/read/filter-contract.js +4 -0
- package/dist/read/filter-contract.js.map +1 -1
- package/dist/read/shape.js +4 -0
- package/dist/read/shape.js.map +1 -1
- package/dist/read/views.d.ts +29 -0
- package/dist/read/views.js +60 -0
- package/dist/read/views.js.map +1 -1
- package/dist/session-grant.d.ts +48 -0
- package/dist/session-grant.js +185 -0
- package/dist/session-grant.js.map +1 -0
- package/dist/surface-copy.d.ts +19 -3
- package/dist/surface-copy.js +19 -3
- package/dist/surface-copy.js.map +1 -1
- package/dist/wizard.d.ts +58 -0
- package/dist/wizard.js +174 -0
- package/dist/wizard.js.map +1 -0
- package/dist/write/availability.d.ts +1 -16
- package/dist/write/availability.js +10 -65
- package/dist/write/availability.js.map +1 -1
- package/dist/write/batch.d.ts +20 -0
- package/dist/write/batch.js +111 -21
- package/dist/write/batch.js.map +1 -1
- package/dist/write/capabilities.d.ts +9 -0
- package/dist/write/capabilities.js +2 -0
- package/dist/write/capabilities.js.map +1 -1
- package/dist/write/clear-reminder.js +1 -1
- package/dist/write/clear-reminder.js.map +1 -1
- package/dist/write/commands.d.ts +22 -7
- package/dist/write/commands.js +231 -254
- package/dist/write/commands.js.map +1 -1
- package/dist/write/failure-hints.d.ts +32 -7
- package/dist/write/failure-hints.js +9 -14
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.js +22 -4
- package/dist/write/guards.js.map +1 -1
- package/dist/write/lock.d.ts +16 -13
- package/dist/write/lock.js +55 -0
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +9 -13
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/move.js +9 -12
- package/dist/write/move.js.map +1 -1
- package/dist/write/operations.d.ts +51 -29
- package/dist/write/operations.js +16 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +72 -14
- package/dist/write/opid.js +152 -8
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.d.ts +80 -0
- package/dist/write/param-schema.js +754 -0
- package/dist/write/param-schema.js.map +1 -0
- package/dist/write/pipeline.d.ts +110 -5
- package/dist/write/pipeline.js +288 -14
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/pre-state.d.ts +38 -16
- package/dist/write/pre-state.js +117 -22
- package/dist/write/pre-state.js.map +1 -1
- package/dist/write/promote-clone.js +658 -444
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +25 -8
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-collateral.d.ts +31 -0
- package/dist/write/repeat-collateral.js +153 -0
- package/dist/write/repeat-collateral.js.map +1 -0
- package/dist/write/repeat-rule.d.ts +28 -1
- package/dist/write/repeat-rule.js +54 -4
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.d.ts +11 -0
- package/dist/write/resolution-timestamps.js +108 -40
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +4 -0
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/template-mutation.d.ts +137 -0
- package/dist/write/template-mutation.js +598 -0
- package/dist/write/template-mutation.js.map +1 -0
- package/dist/write/undo.js +10 -20
- package/dist/write/undo.js.map +1 -1
- package/dist/write/update-fields.d.ts +151 -0
- package/dist/write/update-fields.js +427 -0
- package/dist/write/update-fields.js.map +1 -0
- package/dist/write/vectors/applescript.js +6 -10
- package/dist/write/vectors/applescript.js.map +1 -1
- package/dist/write/vectors/registry.d.ts +1 -1
- package/dist/write/vectors/registry.js +10 -2
- package/dist/write/vectors/registry.js.map +1 -1
- package/dist/write/vectors/shortcuts.js +2 -2
- package/dist/write/vectors/shortcuts.js.map +1 -1
- package/dist/write/vectors/simulator.js +15 -4
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +268 -1
- package/dist/write/vectors/ui-certification.js +63 -13
- package/dist/write/vectors/ui-certification.js.map +1 -1
- package/dist/write/vectors/ui-chord.d.ts +144 -0
- package/dist/write/vectors/ui-chord.js +428 -0
- package/dist/write/vectors/ui-chord.js.map +1 -0
- package/dist/write/vectors/ui-drag.d.ts +8 -4
- package/dist/write/vectors/ui-drag.js +2 -0
- package/dist/write/vectors/ui-drag.js.map +1 -1
- package/dist/write/vectors/ui-recipes.d.ts +70 -3
- package/dist/write/vectors/ui-recipes.js +482 -44
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +280 -10
- package/dist/write/vectors/ui.js +1068 -77
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +3 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +62 -0
- package/dist/write/verify/delta.js +36 -2
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +4 -1
- package/dist/write/verify/poller.js +14 -0
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +2 -1
- package/schema/envelope.schema.json +6 -4
- package/scripts/build-helpers.sh +5 -0
- package/skills/things-cli/SKILL.md +5 -3
- package/skills/things-cli/references/contracts.md +1 -0
- package/skills/things-cli/references/data-model.md +3 -0
- package/skills/things-cli/references/errors.md +1 -1
- package/skills/things-cli/references/gui.md +1 -0
- package/skills/things-cli/references/repeating.md +6 -0
- package/skills/things-cli/references/tag-cleanup.md +64 -0
package/dist/write/opid.d.ts
CHANGED
|
@@ -1,31 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared client-idempotency (`opId`) machinery — the ONE place the lookback
|
|
3
|
-
* window
|
|
4
|
-
* construction rather than by two
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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).
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
|
89
|
+
export declare function replayIfApplied(deps: WriteDeps, options: WriteOptions): MutationResult | null;
|
package/dist/write/opid.js
CHANGED
|
@@ -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).
|
|
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
|
|
12
|
-
* that id, within the last {@link OPID_LOOKBACK_RECORDS} records AND
|
|
13
|
-
* {@link OPID_LOOKBACK_MS} (whichever is more restrictive). Undo/intent/
|
|
14
|
-
* records are naturally excluded
|
|
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
|
|
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
|
|
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
|
package/dist/write/opid.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opid.js","sourceRoot":"","sources":["../../src/write/opid.ts"],"names":[],"mappings":"
|
|
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 {};
|