things-api 0.19.3 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- 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/MacOS/things-deputy +0 -0
- package/dist/audit/schema.d.ts +42 -10
- package/dist/audit/schema.js +21 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/op-result.js +25 -7
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/repeat-flags.d.ts +19 -1
- package/dist/cli/commands/repeat-flags.js +71 -0
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/rescue.d.ts +12 -0
- package/dist/cli/commands/rescue.js +83 -0
- package/dist/cli/commands/rescue.js.map +1 -0
- package/dist/cli/commands/writes.js +86 -32
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/help.js +25 -25
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +12 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +9 -1
- package/dist/cli/resolve-invocation.js +71 -0
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/unknown-command.d.ts +26 -0
- package/dist/cli/unknown-command.js +135 -0
- package/dist/cli/unknown-command.js.map +1 -0
- package/dist/contracts.d.ts +4 -3
- package/dist/contracts.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +62 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +11 -0
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +15 -0
- package/dist/model/mappers.js.map +1 -1
- package/dist/op-result.d.ts +24 -2
- package/dist/op-result.js +77 -20
- package/dist/op-result.js.map +1 -1
- package/dist/process-instance.d.ts +41 -0
- package/dist/process-instance.js +82 -0
- package/dist/process-instance.js.map +1 -0
- package/dist/rescue.d.ts +247 -0
- package/dist/rescue.js +811 -0
- package/dist/rescue.js.map +1 -0
- package/dist/session-grant.js +4 -15
- package/dist/session-grant.js.map +1 -1
- package/dist/ui-state.d.ts +8 -4
- package/dist/ui-state.js +8 -4
- package/dist/ui-state.js.map +1 -1
- package/dist/write/batch.js +4 -1
- package/dist/write/batch.js.map +1 -1
- package/dist/write/clone.js +12 -6
- package/dist/write/clone.js.map +1 -1
- package/dist/write/commands.d.ts +9 -0
- package/dist/write/commands.js +135 -50
- package/dist/write/commands.js.map +1 -1
- package/dist/write/disclosures.d.ts +275 -0
- package/dist/write/disclosures.js +277 -0
- package/dist/write/disclosures.js.map +1 -0
- package/dist/write/experimental.d.ts +16 -0
- package/dist/write/experimental.js +19 -0
- package/dist/write/experimental.js.map +1 -1
- package/dist/write/failure-hints.js +3 -2
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +102 -3
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.js +4 -7
- package/dist/write/heading.js.map +1 -1
- package/dist/write/lock.d.ts +85 -1
- package/dist/write/lock.js +121 -6
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +2 -1
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/operations.d.ts +52 -6
- package/dist/write/operations.js +22 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +69 -8
- package/dist/write/opid.js +116 -20
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.js +14 -2
- package/dist/write/param-schema.js.map +1 -1
- package/dist/write/pipeline.d.ts +82 -3
- package/dist/write/pipeline.js +204 -60
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.d.ts +7 -0
- package/dist/write/planner.js +12 -0
- package/dist/write/planner.js.map +1 -1
- package/dist/write/preserve-modified.d.ts +17 -8
- package/dist/write/preserve-modified.js.map +1 -1
- package/dist/write/promote-clone.js +106 -53
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +45 -39
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +25 -1
- package/dist/write/repeat-rule.js +69 -0
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.js +104 -78
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +2 -2
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/spawn-expectation.d.ts +135 -0
- package/dist/write/spawn-expectation.js +237 -0
- package/dist/write/spawn-expectation.js.map +1 -0
- package/dist/write/template-mutation.js +34 -35
- package/dist/write/template-mutation.js.map +1 -1
- package/dist/write/undo.js +45 -5
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/simulator.js +48 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +12 -0
- package/dist/write/vectors/ui-recipes.js +11 -1
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui-state.d.ts +64 -6
- package/dist/write/vectors/ui-state.js +91 -4
- package/dist/write/vectors/ui-state.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +69 -3
- package/dist/write/vectors/ui.js +293 -146
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +26 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -2
- package/dist/write/verify/delta.js +46 -16
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +3 -2
- package/dist/write/verify/poller.js +3 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +1 -2
- package/skills/things-cli/SKILL.md +4 -2
- package/skills/things-cli/references/repeating.md +1 -1
- package/dist/cli/commands/ui-state.d.ts +0 -11
- package/dist/cli/commands/ui-state.js +0 -27
- package/dist/cli/commands/ui-state.js.map +0 -1
|
@@ -541,6 +541,51 @@ export interface RepeatRuleParams {
|
|
|
541
541
|
*/
|
|
542
542
|
next?: IsoDate;
|
|
543
543
|
}
|
|
544
|
+
/**
|
|
545
|
+
* `reschedule-repeat` params — the full {@link RepeatRuleParams} vocabulary with
|
|
546
|
+
* `frequency` + `interval` OPTIONAL, because the verb now has TWO spellings:
|
|
547
|
+
*
|
|
548
|
+
* - **Restate the rule** (both present): the Repeat dialog is driven, exactly as
|
|
549
|
+
* before — the ui vector, `dangerouslyDriveGui`, the whole rule rewritten.
|
|
550
|
+
* - **RE-ANCHOR** (`{ uuid, next }` alone, both absent): move the series' next
|
|
551
|
+
* occurrence to a date without restating its rule. One background `open` on
|
|
552
|
+
* the URL scheme, tier 0, no GUI-drive acknowledgement — the path REANCH1
|
|
553
|
+
* measured (docs/lab/reanch1-url-reanchor.md) and REANCH2 re-verified. It is
|
|
554
|
+
* guarded hard: Things ≥ 3.23, a strictly-future date, a fixed non-paused
|
|
555
|
+
* single-weekday rule, and a route matching the row type.
|
|
556
|
+
*
|
|
557
|
+
* The two are exclusive by construction — a re-anchor carries `next` and NOTHING
|
|
558
|
+
* else, so any other rule field means the dialog spelling and `frequency` +
|
|
559
|
+
* `interval` are required with it (enforced by `assertRescheduleRule`).
|
|
560
|
+
*/
|
|
561
|
+
export interface RescheduleRepeatParams extends Omit<RepeatRuleParams, "frequency" | "interval"> {
|
|
562
|
+
/** The recurrence unit — required UNLESS this is a bare re-anchor. */
|
|
563
|
+
frequency?: RepeatFrequency;
|
|
564
|
+
/** "every N units", 1–99 — required UNLESS this is a bare re-anchor. */
|
|
565
|
+
interval?: number;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* The bare RE-ANCHOR spelling of {@link RescheduleRepeatParams}: a target uuid and
|
|
569
|
+
* the date its series' next occurrence moves to, nothing else.
|
|
570
|
+
*/
|
|
571
|
+
export interface RepeatReanchorParams {
|
|
572
|
+
uuid: string;
|
|
573
|
+
next: IsoDate;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Is this reschedule the bare re-anchor (URL-drivable) rather than a rule
|
|
577
|
+
* restatement (GUI-bound)? The discriminator is `frequency`: validation
|
|
578
|
+
* (`assertRescheduleRule`) refuses every other rule field alongside its absence,
|
|
579
|
+
* so an params bag without a frequency IS `{ uuid, next }`.
|
|
580
|
+
*/
|
|
581
|
+
export declare function isRepeatReanchor(params: RescheduleRepeatParams): params is RepeatReanchorParams & RescheduleRepeatParams;
|
|
582
|
+
/**
|
|
583
|
+
* Does THIS call drive the GUI? Every {@link UI_DRIVE_OPS} member does, except a
|
|
584
|
+
* `reschedule-repeat` in its bare re-anchor spelling, which compiles to one URL
|
|
585
|
+
* dispatch and therefore carries no GUI-drive gate (H-UI-DRIVE). Keyed on the
|
|
586
|
+
* params, so the scare gate rides the SPELLING rather than the verb.
|
|
587
|
+
*/
|
|
588
|
+
export declare function drivesGuiForParams(op: OperationKind, params: Record<string, unknown>): boolean;
|
|
544
589
|
/**
|
|
545
590
|
* The calendar-anchor subset of the Repeat-dialog rule vocabulary the
|
|
546
591
|
* add-repeating composites carry on their PROMOTE leg (frequency/interval plus
|
|
@@ -617,7 +662,8 @@ export interface ProjectAddRepeatingParams extends AddRepeatingRuleFields {
|
|
|
617
662
|
* landing on the seed — this deadlines every occurrence AND keeps the seed
|
|
618
663
|
* deadline-free so the app does not SRCFATE-preserve it into a double-booked
|
|
619
664
|
* future instance (DBLSPAWN1). It therefore requires a concrete `when` on or
|
|
620
|
-
* before it
|
|
665
|
+
* before it. An AFTER-COMPLETION series is deadlined the same way: the offset is
|
|
666
|
+
* measured from each occurrence's own start, which such a series has (CNCAC2).
|
|
621
667
|
* `startDaysEarlier` is the SAME deadline geometry named from the other end (the
|
|
622
668
|
* per-occurrence lead directly, deadline derived as `when + N`); the two are
|
|
623
669
|
* over-determined together and must AGREE (`deadline − when == startDaysEarlier`).
|
|
@@ -631,7 +677,8 @@ export interface TodoAddRepeatingParams extends AddRepeatingRuleFields {
|
|
|
631
677
|
/**
|
|
632
678
|
* The FIRST occurrence's due date — mapped to the RULE (derive start-days-earlier =
|
|
633
679
|
* deadline − when), so every occurrence is deadlined and the seed carries none.
|
|
634
|
-
* Needs a concrete `when` on or before it
|
|
680
|
+
* Needs a concrete `when` on or before it. Applies to an after-completion series
|
|
681
|
+
* too — the offset is measured from each occurrence's start, not from a calendar.
|
|
635
682
|
*/
|
|
636
683
|
deadline?: IsoDate;
|
|
637
684
|
/**
|
|
@@ -639,8 +686,7 @@ export interface TodoAddRepeatingParams extends AddRepeatingRuleFields {
|
|
|
639
686
|
* before its own deadline (integer ≥ 0), the deadline derived as `when + N`.
|
|
640
687
|
* Where a concrete `deadline` names the first due date, this names the
|
|
641
688
|
* per-occurrence lead directly. Needs a concrete `when`; when BOTH are given they
|
|
642
|
-
* must AGREE (`deadline − when == startDaysEarlier`) or the call is refused.
|
|
643
|
-
* applicable to after-completion (no calendar start to count back from).
|
|
689
|
+
* must AGREE (`deadline − when == startDaysEarlier`) or the call is refused.
|
|
644
690
|
*/
|
|
645
691
|
startDaysEarlier?: number;
|
|
646
692
|
tags?: string[];
|
|
@@ -718,12 +764,12 @@ export interface OperationParamsMap {
|
|
|
718
764
|
"project.dissolve-heading": UuidParams;
|
|
719
765
|
"todo.clear-dated-reminder": UuidParams;
|
|
720
766
|
"todo.make-repeating": RepeatRuleParams;
|
|
721
|
-
"todo.reschedule-repeat":
|
|
767
|
+
"todo.reschedule-repeat": RescheduleRepeatParams;
|
|
722
768
|
"todo.pause-repeat": UuidParams;
|
|
723
769
|
"todo.resume-repeat": UuidParams;
|
|
724
770
|
"todo.create-next-copy": UuidParams;
|
|
725
771
|
"todo.convert-to-project": UuidParams;
|
|
726
|
-
"project.reschedule-repeat":
|
|
772
|
+
"project.reschedule-repeat": RescheduleRepeatParams;
|
|
727
773
|
"project.pause-repeat": UuidParams;
|
|
728
774
|
"project.resume-repeat": UuidParams;
|
|
729
775
|
"area.reorder": AreaReorderParams;
|
package/dist/write/operations.js
CHANGED
|
@@ -154,4 +154,26 @@ export const WEEKDAYS = [
|
|
|
154
154
|
"friday",
|
|
155
155
|
"saturday",
|
|
156
156
|
];
|
|
157
|
+
/**
|
|
158
|
+
* Is this reschedule the bare re-anchor (URL-drivable) rather than a rule
|
|
159
|
+
* restatement (GUI-bound)? The discriminator is `frequency`: validation
|
|
160
|
+
* (`assertRescheduleRule`) refuses every other rule field alongside its absence,
|
|
161
|
+
* so an params bag without a frequency IS `{ uuid, next }`.
|
|
162
|
+
*/
|
|
163
|
+
export function isRepeatReanchor(params) {
|
|
164
|
+
return params.frequency === undefined && params.next !== undefined;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Does THIS call drive the GUI? Every {@link UI_DRIVE_OPS} member does, except a
|
|
168
|
+
* `reschedule-repeat` in its bare re-anchor spelling, which compiles to one URL
|
|
169
|
+
* dispatch and therefore carries no GUI-drive gate (H-UI-DRIVE). Keyed on the
|
|
170
|
+
* params, so the scare gate rides the SPELLING rather than the verb.
|
|
171
|
+
*/
|
|
172
|
+
export function drivesGuiForParams(op, params) {
|
|
173
|
+
if (!isUiDriveOp(op))
|
|
174
|
+
return false;
|
|
175
|
+
if (op !== "todo.reschedule-repeat" && op !== "project.reschedule-repeat")
|
|
176
|
+
return true;
|
|
177
|
+
return !isRepeatReanchor(params);
|
|
178
|
+
}
|
|
157
179
|
//# sourceMappingURL=operations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/write/operations.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,UAAU;IACV,aAAa;IACb,eAAe;IACf,aAAa;IACb,aAAa;IACb,WAAW;IACX,eAAe;IACf,wBAAwB;IACxB,0BAA0B;IAC1B,aAAa;IACb,aAAa;IACb,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;IAChB,UAAU;IACV,aAAa;IACb,SAAS;IACT,YAAY;IACZ,aAAa;IACb,SAAS;IACT,gBAAgB;IAChB,aAAa;IACb,YAAY;IACZ,cAAc;IACd,cAAc;IACd,mBAAmB;IACnB,gBAAgB;IAChB,gBAAgB;IAChB,iBAAiB;IACjB,kBAAkB;IAClB,gBAAgB;IAChB,mBAAmB;IACnB,qBAAqB;IACrB,wBAAwB;IACxB,yBAAyB;IACzB,2BAA2B;IAC3B,yBAAyB;IACzB,sBAAsB;IACtB,iCAAiC;IACjC,0BAA0B;IAC1B,2BAA2B;IAC3B,qBAAqB;IACrB,wBAAwB;IACxB,mBAAmB;IACnB,oBAAoB;IACpB,uBAAuB;IACvB,yBAAyB;IACzB,2BAA2B;IAC3B,sBAAsB;IACtB,uBAAuB;IACvB,cAAc;IACd,wBAAwB;IACxB,uBAAuB;IACvB,oBAAoB;IACpB,YAAY;IACZ,eAAe;IACf,SAAS;CACD,CAAC;AAIX;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,qBAAqB;IACrB,wBAAwB;IACxB,mBAAmB;IACnB,oBAAoB;IACpB,sEAAsE;IACtE,6EAA6E;IAC7E,+EAA+E;IAC/E,0EAA0E;IAC1E,6DAA6D;IAC7D,uBAAuB;IACvB,yBAAyB;IACzB,yBAAyB;IACzB,2BAA2B;IAC3B,sBAAsB;IACtB,uBAAuB;IACvB,cAAc;IACd,uEAAuE;IACvE,4EAA4E;IAC5E,6EAA6E;IAC7E,iEAAiE;IACjE,wBAAwB;IACxB,2EAA2E;IAC3E,4EAA4E;IAC5E,+EAA+E;IAC/E,kEAAkE;IAClE,iCAAiC;IACjC,6EAA6E;IAC7E,yEAAyE;IACzE,2EAA2E;IAC3E,8EAA8E;IAC9E,gBAAgB;IAChB,0BAA0B;IAC1B,4EAA4E;IAC5E,+EAA+E;IAC/E,8EAA8E;IAC9E,gFAAgF;IAChF,6EAA6E;IAC7E,+EAA+E;IAC/E,uEAAuE;IACvE,4DAA4D;IAC5D,sBAAsB;CACd,CAAC;AAEX,MAAM,UAAU,WAAW,CAAC,EAAiB;IAC3C,OAAO,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAA6B;IACnD,qBAAqB;IACrB,wBAAwB;IACxB,yBAAyB;IACzB,2BAA2B;IAC3B,yBAAyB;IACzB,sBAAsB;IACtB,iCAAiC;IACjC,0BAA0B;CAClB,CAAC;AAEX,MAAM,UAAU,WAAW,CAAC,EAAiB;IAC3C,OAAO,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA6B;IAC1D,wBAAwB;IACxB,yBAAyB;IACzB,2BAA2B;IAC3B,yBAAyB;IACzB,0BAA0B;CAClB,CAAC;AAEX,MAAM,UAAU,iBAAiB,CAAC,EAAiB;IACjD,OAAO,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACzC,CAAC;AAyhBD,MAAM,CAAC,MAAM,QAAQ,GAAuB;IAC1C,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;CACF,CAAC"}
|
|
1
|
+
{"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/write/operations.ts"],"names":[],"mappings":"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"}
|
package/dist/write/opid.d.ts
CHANGED
|
@@ -25,10 +25,34 @@
|
|
|
25
25
|
* operation asserts a whole-database singleton, or nothing was recorded — the
|
|
26
26
|
* resubmission is REFUSED with a pointer at the item, never guessed.
|
|
27
27
|
*
|
|
28
|
-
* Every other result class (blocked, unsupported,
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Every other FINAL result class (blocked, unsupported, the other verify-failed
|
|
29
|
+
* reasons) is not a match: a blocked/unsupported attempt changed nothing, and a
|
|
30
|
+
* mismatch/silent-noop landed something the caller must look at first.
|
|
31
|
+
*
|
|
32
|
+
* The third class is an UNSUPERSEDED INTENT — a keyed write that recorded its
|
|
33
|
+
* start and has written no final record yet (#639). That is not a fourth match
|
|
34
|
+
* rule but a different question, and it is answered by asking whether the
|
|
35
|
+
* process that wrote it is still alive:
|
|
36
|
+
*
|
|
37
|
+
* - HOLDER ALIVE — the original is still running. The resubmission is REFUSED
|
|
38
|
+
* (`blocked:in-flight`) and pointed at `things op-result <key>`. This is the
|
|
39
|
+
* refusal that closes the race the pre-lock lookback used to leave open: a
|
|
40
|
+
* retry fired while the original is mid-drive used to see no record, queue
|
|
41
|
+
* behind the mutation lock, and execute the whole verb a second time once the
|
|
42
|
+
* original released it.
|
|
43
|
+
* - HOLDER GONE — the writer died between touching the app and recording an
|
|
44
|
+
* outcome, which is the SAME ambiguity a `verify-failed:timeout` leaves, so it
|
|
45
|
+
* takes the same answer: the intent's own recorded assertion is the presence
|
|
46
|
+
* oracle, re-evaluated against current state. An intent that recorded no
|
|
47
|
+
* usable oracle refuses honestly rather than guessing.
|
|
48
|
+
*
|
|
49
|
+
* DOUBLE-CHECKED LOCKING. Every caller consults this gate TWICE: once before
|
|
50
|
+
* taking the mutation lock (the cheap fast path — most resubmissions are settled
|
|
51
|
+
* by then and never touch the lock) and again immediately after acquiring it,
|
|
52
|
+
* before the first mutating leg. The second check is the load-bearing one: a
|
|
53
|
+
* retry that passed the first check while the original was still running, then
|
|
54
|
+
* waited out the lock, finds the original's record on the way back in and
|
|
55
|
+
* replays instead of re-executing.
|
|
32
56
|
*/
|
|
33
57
|
import type { AuditRecord } from "../audit/schema.ts";
|
|
34
58
|
import { type MutationResult, type WriteDeps, type WriteOptions } from "./pipeline.ts";
|
|
@@ -42,7 +66,12 @@ export declare const OP_ID_RE: RegExp;
|
|
|
42
66
|
*/
|
|
43
67
|
export declare const OPID_LOOKBACK_RECORDS = 1000;
|
|
44
68
|
export declare const OPID_LOOKBACK_MS: number;
|
|
45
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* The disclosure a reconciled replay carries — see {@link reconcileAmbiguous}.
|
|
71
|
+
* Deliberately worded for BOTH ambiguous origins (a confirmation that timed out,
|
|
72
|
+
* and a writer that died before recording an outcome): the caller's situation
|
|
73
|
+
* and next move are identical either way, and the record itself says which.
|
|
74
|
+
*/
|
|
46
75
|
export declare const RECONCILED_NOTE: string;
|
|
47
76
|
/**
|
|
48
77
|
* The recent-history lookback for an opId: the most recent MATCHABLE record
|
|
@@ -51,6 +80,26 @@ export declare const RECONCILED_NOTE: string;
|
|
|
51
80
|
* blocked records are naturally excluded.
|
|
52
81
|
*/
|
|
53
82
|
export declare function findOpIdRecord(records: AuditRecord[], opId: string, now: Date): AuditRecord | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* The UNSUPERSEDED intent for a key, if there is one: an `intent` record whose
|
|
85
|
+
* ATTEMPT never recorded an outcome.
|
|
86
|
+
*
|
|
87
|
+
* Supersession pairs an intent to its own final by `ts`, which is the schema's
|
|
88
|
+
* documented sibling invariant — both records of one attempt derive from the
|
|
89
|
+
* same `startedAt`, so they carry the same timestamp (`audit/schema.ts`). It is
|
|
90
|
+
* deliberately NOT "the last record wins": `readAuditRecords` RE-SORTS the trail
|
|
91
|
+
* by `ts`, so file order does not survive the read, and a composite's intent
|
|
92
|
+
* (written when it takes the lock) briefly carried a later `ts` than its own
|
|
93
|
+
* summary (stamped when the verb began) — which sorted the intent last and left
|
|
94
|
+
* every finished promote looking permanently in flight. Measured in TORPH1
|
|
95
|
+
* cell B; the fix is this pairing plus stamping a composite's intent with the
|
|
96
|
+
* verb's own `startedAt`.
|
|
97
|
+
*
|
|
98
|
+
* Pairing also handles the RE-DISPATCH shape by construction: a key retried
|
|
99
|
+
* after an ambiguous failure records intent+final for the first attempt and a
|
|
100
|
+
* fresh intent for the second, and only the second is unpaired.
|
|
101
|
+
*/
|
|
102
|
+
export declare function findPendingIntent(records: AuditRecord[], opId: string, now: Date): AuditRecord | undefined;
|
|
54
103
|
/**
|
|
55
104
|
* Can the recorded assertion decide, after the fact, whether the timed-out
|
|
56
105
|
* change landed? Usable means the assertion is keyed to a SUBJECT the attempt
|
|
@@ -69,9 +118,15 @@ export type PresenceOracle = {
|
|
|
69
118
|
export declare function presenceOracle(spec: DeltaSpec | undefined): PresenceOracle;
|
|
70
119
|
/**
|
|
71
120
|
* 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
|
|
73
|
-
* unsettleable attempt, or null when the write must run. Shared by
|
|
74
|
-
* line path and {@link replayIfApplied}.
|
|
121
|
+
* result when this key was already applied, a refusal when it is still running
|
|
122
|
+
* or matched an unsettleable attempt, or null when the write must run. Shared by
|
|
123
|
+
* the batch line path and {@link replayIfApplied}.
|
|
124
|
+
*
|
|
125
|
+
* An unsuperseded intent is consulted FIRST, because it describes the key's
|
|
126
|
+
* present, while any matchable final describes its past. (Both can exist: a key
|
|
127
|
+
* whose first attempt timed out and is now being re-driven has an old timeout
|
|
128
|
+
* final AND a live intent. Acting on the final there would replay a change the
|
|
129
|
+
* running attempt has not finished making.)
|
|
75
130
|
*/
|
|
76
131
|
export declare function resolveOpId(deps: WriteDeps, records: AuditRecord[], opId: string, now: Date): MutationResult | null;
|
|
77
132
|
/**
|
|
@@ -82,6 +137,12 @@ export declare function resolveOpId(deps: WriteDeps, records: AuditRecord[], opI
|
|
|
82
137
|
* flip-dance, and the promote-via-clone verbs; they never reach `run`, so
|
|
83
138
|
* without this the key would be recorded and never honored).
|
|
84
139
|
*
|
|
140
|
+
* Called TWICE per keyed write, and the second call is the one that matters —
|
|
141
|
+
* see the module header on double-checked locking. Both calls re-read the trail:
|
|
142
|
+
* the whole point of the post-acquire check is that the trail changed while this
|
|
143
|
+
* process was waiting for the lock, so a cached read would answer the stale
|
|
144
|
+
* question all over again.
|
|
145
|
+
*
|
|
85
146
|
* Three conditions fail-open (run the write) rather than fail-closed: no key, a
|
|
86
147
|
* dry run (it mints and records nothing, so there is nothing to deduplicate
|
|
87
148
|
* against), or no trail to read.
|
package/dist/write/opid.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { instanceAlive } from "../process-instance.js";
|
|
2
|
+
import { attach, disclose, disclosuresOf } from "./disclosures.js";
|
|
1
3
|
import { replayResultFromRecord, } from "./pipeline.js";
|
|
2
4
|
import { readAuditRecords } from "./undo.js";
|
|
3
5
|
import { createDbReader, evaluateDelta } from "./verify/delta.js";
|
|
@@ -10,10 +12,15 @@ export const OP_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
|
10
12
|
*/
|
|
11
13
|
export const OPID_LOOKBACK_RECORDS = 1000;
|
|
12
14
|
export const OPID_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The disclosure a reconciled replay carries — see {@link reconcileAmbiguous}.
|
|
17
|
+
* Deliberately worded for BOTH ambiguous origins (a confirmation that timed out,
|
|
18
|
+
* and a writer that died before recording an outcome): the caller's situation
|
|
19
|
+
* and next move are identical either way, and the record itself says which.
|
|
20
|
+
*/
|
|
21
|
+
export const RECONCILED_NOTE = "an earlier submission with this idempotency key never recorded a completed change; the change " +
|
|
22
|
+
"was found already in place, so nothing ran again. It cannot be reversed with `things undo` — " +
|
|
23
|
+
"there is no completed-change record to reverse";
|
|
17
24
|
/** Result classes an `opId` match acts on (see the module header). */
|
|
18
25
|
function matchable(result) {
|
|
19
26
|
return result === "ok" || result === "verify-failed:timeout";
|
|
@@ -37,6 +44,37 @@ export function findOpIdRecord(records, opId, now) {
|
|
|
37
44
|
}
|
|
38
45
|
return match;
|
|
39
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* The UNSUPERSEDED intent for a key, if there is one: an `intent` record whose
|
|
49
|
+
* ATTEMPT never recorded an outcome.
|
|
50
|
+
*
|
|
51
|
+
* Supersession pairs an intent to its own final by `ts`, which is the schema's
|
|
52
|
+
* documented sibling invariant — both records of one attempt derive from the
|
|
53
|
+
* same `startedAt`, so they carry the same timestamp (`audit/schema.ts`). It is
|
|
54
|
+
* deliberately NOT "the last record wins": `readAuditRecords` RE-SORTS the trail
|
|
55
|
+
* by `ts`, so file order does not survive the read, and a composite's intent
|
|
56
|
+
* (written when it takes the lock) briefly carried a later `ts` than its own
|
|
57
|
+
* summary (stamped when the verb began) — which sorted the intent last and left
|
|
58
|
+
* every finished promote looking permanently in flight. Measured in TORPH1
|
|
59
|
+
* cell B; the fix is this pairing plus stamping a composite's intent with the
|
|
60
|
+
* verb's own `startedAt`.
|
|
61
|
+
*
|
|
62
|
+
* Pairing also handles the RE-DISPATCH shape by construction: a key retried
|
|
63
|
+
* after an ambiguous failure records intent+final for the first attempt and a
|
|
64
|
+
* fresh intent for the second, and only the second is unpaired.
|
|
65
|
+
*/
|
|
66
|
+
export function findPendingIntent(records, opId, now) {
|
|
67
|
+
const cutoff = now.getTime() - OPID_LOOKBACK_MS;
|
|
68
|
+
const window = records.slice(-OPID_LOOKBACK_RECORDS);
|
|
69
|
+
const inWindow = (r) => r.opId === opId && new Date(r.ts).getTime() >= cutoff;
|
|
70
|
+
const settled = new Set();
|
|
71
|
+
for (const r of window) {
|
|
72
|
+
if (inWindow(r) && r.result !== "intent")
|
|
73
|
+
settled.add(r.ts); // that attempt finished
|
|
74
|
+
}
|
|
75
|
+
// The newest unpaired intent (there is at most one in practice).
|
|
76
|
+
return window.findLast((r) => inWindow(r) && r.result === "intent" && !settled.has(r.ts));
|
|
77
|
+
}
|
|
40
78
|
export function presenceOracle(spec) {
|
|
41
79
|
if (spec === undefined) {
|
|
42
80
|
return {
|
|
@@ -94,30 +132,62 @@ export function presenceOracle(spec) {
|
|
|
94
132
|
/** The refusal: the key matched an unconfirmed attempt nothing here can settle. */
|
|
95
133
|
function unreconcilable(record, why) {
|
|
96
134
|
const target = record.uuid === null ? "the item" : record.uuid;
|
|
135
|
+
// An intent's writer DIED; a timeout's writer finished and could not confirm.
|
|
136
|
+
// Both leave the same ambiguity, and the caller acts on it the same way, but
|
|
137
|
+
// saying which one happened is what makes the trace worth looking at.
|
|
138
|
+
const how = record.result === "intent"
|
|
139
|
+
? `started a change and its process ended without recording an outcome`
|
|
140
|
+
: `never confirmed its change`;
|
|
97
141
|
return {
|
|
98
142
|
kind: "blocked",
|
|
99
143
|
op: record.op,
|
|
100
144
|
reason: "reconcile",
|
|
101
|
-
detail: `an earlier submission with this idempotency key (${record.ts})
|
|
102
|
-
|
|
145
|
+
detail: `an earlier submission with this idempotency key (${record.ts}) ${how}, and ${why} — so ` +
|
|
146
|
+
"whether that change took effect cannot be settled from here",
|
|
103
147
|
remediation: `check ${target} in Things (\`things op-result ${record.opId ?? "<key>"}\` reports what the ` +
|
|
104
148
|
"earlier attempt recorded); once you know whether the change is there, leave it alone or " +
|
|
105
149
|
"re-run with a NEW --op-id",
|
|
106
150
|
};
|
|
107
151
|
}
|
|
108
152
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
153
|
+
* The refusal: this key is STILL RUNNING somewhere. Returned instead of letting
|
|
154
|
+
* the resubmission queue behind the mutation lock, because waiting there is how
|
|
155
|
+
* the double happened — the waiter wakes up after the original finishes and, on
|
|
156
|
+
* the pre-lock answer it is still carrying, executes the verb again.
|
|
157
|
+
*
|
|
158
|
+
* Refusing costs the caller one poll. It is also the only answer that is true
|
|
159
|
+
* for the whole class: nothing here can know whether the running original will
|
|
160
|
+
* succeed, so nothing here should act on a guess about it.
|
|
161
|
+
*/
|
|
162
|
+
function stillInFlight(record, holderPid) {
|
|
163
|
+
return {
|
|
164
|
+
kind: "blocked",
|
|
165
|
+
op: record.op,
|
|
166
|
+
reason: "in-flight",
|
|
167
|
+
detail: `an earlier submission with this idempotency key is STILL RUNNING (started ${record.ts}, ` +
|
|
168
|
+
`pid ${holderPid}) — it has not recorded an outcome yet, so nothing was run again here`,
|
|
169
|
+
remediation: `poll \`things op-result ${record.opId ?? "<key>"}\` until it reports a final outcome, then ` +
|
|
170
|
+
"act on that; do not resubmit while it is running (a GUI-driven change takes seconds, and " +
|
|
171
|
+
"longer on a large or syncing database)",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Reconcile a resubmission against an AMBIGUOUS original — one that touched the
|
|
176
|
+
* app and never recorded whether the change landed. Two records reach here: a
|
|
177
|
+
* `verify-failed:timeout` final (the attempt finished and could not confirm) and
|
|
178
|
+
* an orphaned `intent` (the attempt's process died mid-flight, #639). The
|
|
179
|
+
* ambiguity is identical, so the answer is: re-read current state through the
|
|
180
|
+
* attempt's own recorded assertion and decide.
|
|
111
181
|
*
|
|
112
182
|
* - satisfied → the change is there: replay it as already-applied, disclosing
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
183
|
+
* the reconciliation. The replay carries NO undo token — the trail holds no
|
|
184
|
+
* completed-change record for undo to reverse — and takes its uuid from the
|
|
185
|
+
* re-read, which is how a create whose uuid was never discovered (recorded
|
|
186
|
+
* `uuid: null`) still answers with the row it made.
|
|
117
187
|
* - not satisfied → the change is absent: return null so the write runs.
|
|
118
188
|
* - no usable oracle → refuse (never a guess).
|
|
119
189
|
*/
|
|
120
|
-
function
|
|
190
|
+
function reconcileAmbiguous(deps, record) {
|
|
121
191
|
const oracle = presenceOracle(record.expected);
|
|
122
192
|
if (!oracle.usable)
|
|
123
193
|
return unreconcilable(record, oracle.why);
|
|
@@ -132,25 +202,45 @@ function reconcileTimedOut(deps, record) {
|
|
|
132
202
|
if (replay.kind !== "ok")
|
|
133
203
|
return replay; // replayResultFromRecord always builds an ok
|
|
134
204
|
const { undoToken: _noToken, ...rest } = replay;
|
|
135
|
-
|
|
205
|
+
const bag = disclosuresOf(replay);
|
|
206
|
+
disclose(bag, "reconciled-replay", RECONCILED_NOTE);
|
|
207
|
+
return attach({
|
|
136
208
|
...rest,
|
|
137
209
|
uuid: evaluation.discoveredUuid ?? record.uuid,
|
|
138
210
|
observed: evaluation.observed,
|
|
139
211
|
...(evaluation.repeating !== undefined && { repeating: evaluation.repeating }),
|
|
140
|
-
|
|
141
|
-
};
|
|
212
|
+
}, bag);
|
|
142
213
|
}
|
|
143
214
|
/**
|
|
144
215
|
* 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
|
|
146
|
-
* unsettleable attempt, or null when the write must run. Shared by
|
|
147
|
-
* line path and {@link replayIfApplied}.
|
|
216
|
+
* result when this key was already applied, a refusal when it is still running
|
|
217
|
+
* or matched an unsettleable attempt, or null when the write must run. Shared by
|
|
218
|
+
* the batch line path and {@link replayIfApplied}.
|
|
219
|
+
*
|
|
220
|
+
* An unsuperseded intent is consulted FIRST, because it describes the key's
|
|
221
|
+
* present, while any matchable final describes its past. (Both can exist: a key
|
|
222
|
+
* whose first attempt timed out and is now being re-driven has an old timeout
|
|
223
|
+
* final AND a live intent. Acting on the final there would replay a change the
|
|
224
|
+
* running attempt has not finished making.)
|
|
148
225
|
*/
|
|
149
226
|
export function resolveOpId(deps, records, opId, now) {
|
|
227
|
+
const pending = findPendingIntent(records, opId, now);
|
|
228
|
+
if (pending !== undefined) {
|
|
229
|
+
const holder = pending.holder;
|
|
230
|
+
// No holder recorded (an M3 intent from before write-ahead intents, or a
|
|
231
|
+
// path that writes one unkeyed): liveness is unknowable, and the safe
|
|
232
|
+
// unknowable answer is the one that never doubles — treat it as running.
|
|
233
|
+
if (holder === undefined || instanceAlive(holder)) {
|
|
234
|
+
return stillInFlight(pending, holder?.pid ?? -1);
|
|
235
|
+
}
|
|
236
|
+
// The holder is gone and no outcome was ever recorded: same ambiguity as a
|
|
237
|
+
// timeout, same oracle-driven answer.
|
|
238
|
+
return reconcileAmbiguous(deps, pending);
|
|
239
|
+
}
|
|
150
240
|
const match = findOpIdRecord(records, opId, now);
|
|
151
241
|
if (match === undefined)
|
|
152
242
|
return null;
|
|
153
|
-
return match.result === "ok" ? replayResultFromRecord(match) :
|
|
243
|
+
return match.result === "ok" ? replayResultFromRecord(match) : reconcileAmbiguous(deps, match);
|
|
154
244
|
}
|
|
155
245
|
/**
|
|
156
246
|
* The dispatch decision for a SINGLE invocation, reading the trail itself. The
|
|
@@ -160,6 +250,12 @@ export function resolveOpId(deps, records, opId, now) {
|
|
|
160
250
|
* flip-dance, and the promote-via-clone verbs; they never reach `run`, so
|
|
161
251
|
* without this the key would be recorded and never honored).
|
|
162
252
|
*
|
|
253
|
+
* Called TWICE per keyed write, and the second call is the one that matters —
|
|
254
|
+
* see the module header on double-checked locking. Both calls re-read the trail:
|
|
255
|
+
* the whole point of the post-acquire check is that the trail changed while this
|
|
256
|
+
* process was waiting for the lock, so a cached read would answer the stale
|
|
257
|
+
* question all over again.
|
|
258
|
+
*
|
|
163
259
|
* Three conditions fail-open (run the write) rather than fail-closed: no key, a
|
|
164
260
|
* dry run (it mints and records nothing, so there is nothing to deduplicate
|
|
165
261
|
* against), or no trail to read.
|
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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"}
|
|
@@ -523,6 +523,18 @@ const REPEAT_RULE = {
|
|
|
523
523
|
startDaysEarlier: opt(int(0, 366, "a whole number of days ≥ 0")),
|
|
524
524
|
next: opt(isoDate()),
|
|
525
525
|
};
|
|
526
|
+
/**
|
|
527
|
+
* reschedule-repeat: the same vocabulary with `frequency` + `interval` OPTIONAL,
|
|
528
|
+
* because the verb has two spellings — restate the rule (both required), or move
|
|
529
|
+
* the next occurrence with `next` alone (the series RE-ANCHOR). Which one this is,
|
|
530
|
+
* and that a re-anchor carries nothing else, is settled by `assertRescheduleRule`
|
|
531
|
+
* — the schema's job is only to let the second shape through the type gate.
|
|
532
|
+
*/
|
|
533
|
+
const RESCHEDULE_RULE = {
|
|
534
|
+
...REPEAT_RULE,
|
|
535
|
+
frequency: opt(enumOf(["daily", "weekly", "monthly", "yearly"])),
|
|
536
|
+
interval: opt(int(1, 99, "a whole number 1–99")),
|
|
537
|
+
};
|
|
526
538
|
const SET_DATES = {
|
|
527
539
|
uuid: str("an item uuid"),
|
|
528
540
|
completedAt: opt(timestamp()),
|
|
@@ -685,12 +697,12 @@ export const PARAM_SCHEMAS = {
|
|
|
685
697
|
"project.dissolve-heading": { uuid: str("a heading uuid") },
|
|
686
698
|
"todo.clear-dated-reminder": UUID_ONLY,
|
|
687
699
|
"todo.make-repeating": REPEAT_RULE,
|
|
688
|
-
"todo.reschedule-repeat":
|
|
700
|
+
"todo.reschedule-repeat": RESCHEDULE_RULE,
|
|
689
701
|
"todo.pause-repeat": UUID_ONLY,
|
|
690
702
|
"todo.resume-repeat": UUID_ONLY,
|
|
691
703
|
"todo.create-next-copy": UUID_ONLY,
|
|
692
704
|
"todo.convert-to-project": UUID_ONLY,
|
|
693
|
-
"project.reschedule-repeat":
|
|
705
|
+
"project.reschedule-repeat": RESCHEDULE_RULE,
|
|
694
706
|
"project.pause-repeat": { uuid: str("a project uuid or unique title") },
|
|
695
707
|
"project.resume-repeat": { uuid: str("a project uuid or unique title") },
|
|
696
708
|
"area.reorder": {
|