oracular 0.12.1 → 0.14.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/dist/index.d.cts CHANGED
@@ -54,6 +54,13 @@ interface RevealOptions {
54
54
  z?: number;
55
55
  /** How long after `markRevealed` the intent stays revealed and `shouldReveal` is false. Default: the intent's `halfLife`. */
56
56
  cooldown?: number;
57
+ /**
58
+ * How long after the intent's outcome event `shouldReveal` stays false. Default: `cooldown`.
59
+ * 0 is allowed: the outcome then pauses nothing, while a reveal still starts its cooldown, for
60
+ * an intent whose outcome recurs within minutes, or a "stuck" intent whose help should be back
61
+ * at the next attempt.
62
+ */
63
+ outcomeCooldown?: number;
57
64
  /**
58
65
  * How `lower` is computed. `'normal'` (default): p − z·sd, clamped to [0, 1]. `'exact'`: the
59
66
  * Beta distribution's own quantile at the same one-sided level, Φ(−z), so `z` keeps its
@@ -307,8 +314,8 @@ interface Prediction {
307
314
  revealedAt: number | null;
308
315
  /**
309
316
  * When the current suppression ends: the later of the reveal cooldown (after `markRevealed`)
310
- * and the outcome cooldown (after the outcome event) still running at `at`, or null when
311
- * neither is. `shouldReveal` is false while this is non-null.
317
+ * and the outcome cooldown (`outcomeCooldown` after the outcome event) still running at `at`,
318
+ * or null when neither is. `shouldReveal` is false while this is non-null.
312
319
  */
313
320
  suppressedUntil: number | null;
314
321
  }
@@ -395,7 +402,11 @@ type Migrate = (stored: unknown, version: number) => EngineState;
395
402
  * ```
396
403
  */
397
404
  interface DeserializeOptions {
398
- /** The app's current config. Validated, swapped in, and used to prune signals and intents no longer declared. */
405
+ /**
406
+ * The app's current config. Validated, swapped in, and used to prune signals and intents no
407
+ * longer declared; the stored snapshot is then not read, so a key or value a newer release wrote
408
+ * into it does not stop the load.
409
+ */
399
410
  config?: EngineConfig;
400
411
  /**
401
412
  * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
@@ -726,9 +737,10 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
726
737
  * @param state - The current state. Not mutated.
727
738
  * @param intent - Name of an intent declared in the config.
728
739
  * @param at - The time to evaluate at, in the same unit as your durations.
729
- * @param opts - Overrides for `threshold`, `z`, `cooldown` and `bound`, resolved as call →
730
- * intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets `revealed` and
731
- * `suppressedUntil`; `threshold` only matters to {@link shouldReveal}.
740
+ * @param opts - Overrides for `threshold`, `z`, `cooldown`, `outcomeCooldown` and `bound`,
741
+ * resolved as call → intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets
742
+ * `revealed` and, with `outcomeCooldown` (default: `cooldown`), `suppressedUntil`;
743
+ * `threshold` only matters to {@link shouldReveal}.
732
744
  * @returns The prediction. `p`, `sd`, `lower` and `confidence` are in [0, 1]; `evidence` is
733
745
  * α + β.
734
746
  * @throws `RangeError` when `intent` is not declared in the config.
@@ -751,7 +763,8 @@ declare function predict(state: EngineState, intent: string, at: number, opts?:
751
763
  * @param intent - Name of an intent declared in the config.
752
764
  * @param at - The time to evaluate at.
753
765
  * @param opts - Overrides for `threshold` (default 0.5), `z` (default 1.28, a one-sided 90%
754
- * bound) and `cooldown` (default: the intent's `halfLife`).
766
+ * bound), `cooldown` (default: the intent's `halfLife`) and `outcomeCooldown` (default:
767
+ * `cooldown`; 0 lets the outcome pause nothing).
755
768
  * @returns True when the app should show, offer or reveal whatever this intent gates.
756
769
  * @throws `RangeError` when `intent` is not declared in the config.
757
770
  * @throws `TypeError` when `state` is not an engine state, `at` is not finite, or `opts` is
@@ -850,11 +863,14 @@ declare function serialize(state: EngineState): string;
850
863
  /**
851
864
  * Restores a state saved with {@link serialize}.
852
865
  *
853
- * Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
854
- * stored one is read without the checks that refuse a config that cannot work, so a state saved
855
- * before those checks existed still loads),
856
- * intents and signals no longer declared are dropped (α and β are recomputed from the kept
857
- * contributions), and newly declared intents start at their prior. States saved by oracular
866
+ * Always pass the app's current `config`: it is validated and replaces the stored snapshot,
867
+ * which is then not read at all, so a state written by a newer release, with a config key or
868
+ * value this one does not know, still loads after a rollback. Intents and signals no longer
869
+ * declared are dropped (α and β are recomputed from the kept contributions), and newly declared
870
+ * intents start at their prior. Without a config the stored snapshot runs the engine: it is read
871
+ * without the checks that refuse a config that cannot work, so a state saved before those checks
872
+ * existed still loads, but a key the library does not know is an error, since the engine would
873
+ * otherwise run without an option the state was built under. States saved by oracular
858
874
  * 0.1.0 to 0.11.x are upgraded to the current shape on their own, and a state one version newer
859
875
  * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
860
876
  * current one, ignoring the fields it adds; `migrate` is only for a version this library does
package/dist/index.d.ts CHANGED
@@ -54,6 +54,13 @@ interface RevealOptions {
54
54
  z?: number;
55
55
  /** How long after `markRevealed` the intent stays revealed and `shouldReveal` is false. Default: the intent's `halfLife`. */
56
56
  cooldown?: number;
57
+ /**
58
+ * How long after the intent's outcome event `shouldReveal` stays false. Default: `cooldown`.
59
+ * 0 is allowed: the outcome then pauses nothing, while a reveal still starts its cooldown, for
60
+ * an intent whose outcome recurs within minutes, or a "stuck" intent whose help should be back
61
+ * at the next attempt.
62
+ */
63
+ outcomeCooldown?: number;
57
64
  /**
58
65
  * How `lower` is computed. `'normal'` (default): p − z·sd, clamped to [0, 1]. `'exact'`: the
59
66
  * Beta distribution's own quantile at the same one-sided level, Φ(−z), so `z` keeps its
@@ -307,8 +314,8 @@ interface Prediction {
307
314
  revealedAt: number | null;
308
315
  /**
309
316
  * When the current suppression ends: the later of the reveal cooldown (after `markRevealed`)
310
- * and the outcome cooldown (after the outcome event) still running at `at`, or null when
311
- * neither is. `shouldReveal` is false while this is non-null.
317
+ * and the outcome cooldown (`outcomeCooldown` after the outcome event) still running at `at`,
318
+ * or null when neither is. `shouldReveal` is false while this is non-null.
312
319
  */
313
320
  suppressedUntil: number | null;
314
321
  }
@@ -395,7 +402,11 @@ type Migrate = (stored: unknown, version: number) => EngineState;
395
402
  * ```
396
403
  */
397
404
  interface DeserializeOptions {
398
- /** The app's current config. Validated, swapped in, and used to prune signals and intents no longer declared. */
405
+ /**
406
+ * The app's current config. Validated, swapped in, and used to prune signals and intents no
407
+ * longer declared; the stored snapshot is then not read, so a key or value a newer release wrote
408
+ * into it does not stop the load.
409
+ */
399
410
  config?: EngineConfig;
400
411
  /**
401
412
  * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
@@ -726,9 +737,10 @@ declare function episodeClosed(prev: EngineState, next: EngineState): boolean;
726
737
  * @param state - The current state. Not mutated.
727
738
  * @param intent - Name of an intent declared in the config.
728
739
  * @param at - The time to evaluate at, in the same unit as your durations.
729
- * @param opts - Overrides for `threshold`, `z`, `cooldown` and `bound`, resolved as call →
730
- * intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets `revealed` and
731
- * `suppressedUntil`; `threshold` only matters to {@link shouldReveal}.
740
+ * @param opts - Overrides for `threshold`, `z`, `cooldown`, `outcomeCooldown` and `bound`,
741
+ * resolved as call → intent → engine → defaults. `z` and `bound` set `lower`; `cooldown` sets
742
+ * `revealed` and, with `outcomeCooldown` (default: `cooldown`), `suppressedUntil`;
743
+ * `threshold` only matters to {@link shouldReveal}.
732
744
  * @returns The prediction. `p`, `sd`, `lower` and `confidence` are in [0, 1]; `evidence` is
733
745
  * α + β.
734
746
  * @throws `RangeError` when `intent` is not declared in the config.
@@ -751,7 +763,8 @@ declare function predict(state: EngineState, intent: string, at: number, opts?:
751
763
  * @param intent - Name of an intent declared in the config.
752
764
  * @param at - The time to evaluate at.
753
765
  * @param opts - Overrides for `threshold` (default 0.5), `z` (default 1.28, a one-sided 90%
754
- * bound) and `cooldown` (default: the intent's `halfLife`).
766
+ * bound), `cooldown` (default: the intent's `halfLife`) and `outcomeCooldown` (default:
767
+ * `cooldown`; 0 lets the outcome pause nothing).
755
768
  * @returns True when the app should show, offer or reveal whatever this intent gates.
756
769
  * @throws `RangeError` when `intent` is not declared in the config.
757
770
  * @throws `TypeError` when `state` is not an engine state, `at` is not finite, or `opts` is
@@ -850,11 +863,14 @@ declare function serialize(state: EngineState): string;
850
863
  /**
851
864
  * Restores a state saved with {@link serialize}.
852
865
  *
853
- * Always pass the app's current `config`: it is validated and replaces the stored snapshot (the
854
- * stored one is read without the checks that refuse a config that cannot work, so a state saved
855
- * before those checks existed still loads),
856
- * intents and signals no longer declared are dropped (α and β are recomputed from the kept
857
- * contributions), and newly declared intents start at their prior. States saved by oracular
866
+ * Always pass the app's current `config`: it is validated and replaces the stored snapshot,
867
+ * which is then not read at all, so a state written by a newer release, with a config key or
868
+ * value this one does not know, still loads after a rollback. Intents and signals no longer
869
+ * declared are dropped (α and β are recomputed from the kept contributions), and newly declared
870
+ * intents start at their prior. Without a config the stored snapshot runs the engine: it is read
871
+ * without the checks that refuse a config that cannot work, so a state saved before those checks
872
+ * existed still loads, but a key the library does not know is an error, since the engine would
873
+ * otherwise run without an option the state was built under. States saved by oracular
858
874
  * 0.1.0 to 0.11.x are upgraded to the current shape on their own, and a state one version newer
859
875
  * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
860
876
  * current one, ignoring the fields it adds; `migrate` is only for a version this library does
package/dist/index.js CHANGED
@@ -287,7 +287,7 @@ var INTENT_KEYS = [
287
287
  "attribution",
288
288
  "reveal"
289
289
  ];
290
- var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
290
+ var REVEAL_KEYS = ["threshold", "z", "cooldown", "outcomeCooldown", "bound"];
291
291
  var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes", "lift"];
292
292
  var LIFT_TYPES = [
293
293
  "absolute-difference",
@@ -373,6 +373,9 @@ function normalizeRevealOptions(value, path, strict = true) {
373
373
  if (record2.cooldown !== void 0) {
374
374
  out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
375
375
  }
376
+ if (record2.outcomeCooldown !== void 0) {
377
+ out.outcomeCooldown = expectNonNegative(record2.outcomeCooldown, `${path}.outcomeCooldown`);
378
+ }
376
379
  if (record2.bound !== void 0) {
377
380
  if (record2.bound !== "normal" && record2.bound !== "exact") {
378
381
  typeError(`${path}.bound`, 'must be "normal" or "exact"');
@@ -511,10 +514,12 @@ function splitEvaluateOptions(value, path) {
511
514
  }
512
515
  function resolveReveal(config, intent, opts, normalized = false) {
513
516
  const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options", false);
517
+ const cooldown = o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife;
514
518
  return {
515
519
  threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
516
520
  z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
517
- cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife,
521
+ cooldown,
522
+ outcomeCooldown: o.outcomeCooldown ?? intent.reveal?.outcomeCooldown ?? config?.reveal?.outcomeCooldown ?? cooldown,
518
523
  bound: o.bound ?? intent.reveal?.bound ?? config?.reveal?.bound ?? "normal"
519
524
  };
520
525
  }
@@ -619,7 +624,7 @@ function buildPrediction(is, name, reveal, at) {
619
624
  const p = betaMean(is.alpha, is.beta);
620
625
  const sd = betaSd(is.alpha, is.beta);
621
626
  const revealEnd = cooldownEnd(is.lastRevealedAt, reveal.cooldown, at);
622
- const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.cooldown, at);
627
+ const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.outcomeCooldown, at);
623
628
  const suppressedUntil = revealEnd === null ? outcomeEnd : outcomeEnd === null ? revealEnd : Math.max(revealEnd, outcomeEnd);
624
629
  return {
625
630
  intent: name,
@@ -1003,9 +1008,9 @@ function deserialize(json, opts = {}) {
1003
1008
  stored = opts.migrate(parsed, parsed.version);
1004
1009
  }
1005
1010
  }
1006
- const raw = parseState(stored);
1007
- const config = opts.config === void 0 ? raw.config : normalizeConfig(opts.config);
1008
- return requireComputable(reconcile(raw, config));
1011
+ const config = opts.config === void 0 ? void 0 : normalizeConfig(opts.config);
1012
+ const raw = parseState(stored, config);
1013
+ return requireComputable(reconcile(raw, raw.config));
1009
1014
  }
1010
1015
  function typeError2(path, message) {
1011
1016
  throw new TypeError(`oracular: ${path} ${message}`);
@@ -1114,16 +1119,15 @@ function clockState(value, path) {
1114
1119
  behindAt: nullOrFinite(r.behindAt, `${path}.behindAt`)
1115
1120
  };
1116
1121
  }
1117
- function parseState(value) {
1122
+ function parseState(value, config) {
1118
1123
  const r = record(value, "state");
1119
1124
  if (r.version !== STATE_VERSION) {
1120
1125
  throw new RangeError(`oracular: state.version must be ${STATE_VERSION} after migration`);
1121
1126
  }
1127
+ const snapshot = record(r.config, "state.config");
1122
1128
  return {
1123
1129
  version: STATE_VERSION,
1124
- // Without the strict checks: a config this release would refuse at createState still loads
1125
- // when it is only stored, and the config passed in replaces it anyway.
1126
- config: normalizeConfig(r.config, false),
1130
+ config: config ?? normalizeConfig(snapshot, false),
1127
1131
  intents: mapOwn(r.intents, "state.intents", intentState),
1128
1132
  episode: episodeState(r.episode, "state.episode"),
1129
1133
  clock: clockState(r.clock, "state.clock")