oracular 0.5.2 → 0.7.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
@@ -2,7 +2,9 @@
2
2
  * Version of the serialized state shape, stored in every state as `state.version`. Bumped only
3
3
  * when `EngineState` changes incompatibly, and every bump ships its migration: {@link deserialize}
4
4
  * upgrades states saved by earlier versions of this library on its own (version 1, written by
5
- * oracular 0.1.0 to 0.2.x, becomes version 2) and calls your `migrate` function only for a version
5
+ * oracular 0.1.0 to 0.2.x, and version 2, written by 0.3.0 to 0.5.x, become version 3), reads a
6
+ * state one version newer than its own as its own (a version bump only adds fields, so a rollback
7
+ * by one release keeps the user's history), and calls your `migrate` function only for a version
6
8
  * it does not know.
7
9
  *
8
10
  * @example
@@ -19,7 +21,7 @@
19
21
  * });
20
22
  * ```
21
23
  */
22
- declare const STATE_VERSION: 2;
24
+ declare const STATE_VERSION: 3;
23
25
  /**
24
26
  * Version of the recorder format: the `v` field of every record that {@link createRecord} and
25
27
  * {@link createEndRecord} produce. A new optional field does not change it; readers ignore fields
@@ -112,6 +114,17 @@ interface IntentConfig {
112
114
  * weight.
113
115
  */
114
116
  saturate?: boolean;
117
+ /**
118
+ * How long a closed episode's signals wait for the outcome before they take a miss, in the same
119
+ * unit as `at`. With it set, an episode that ends without the outcome is held instead of
120
+ * counted. When the outcome arrives inside the window, each held episode gets a share
121
+ * `2^(−age / halfLife)` of a hit and the rest of a miss, with `age` measured from the episode's
122
+ * close: the sessions that built up to the outcome are credited by how much of their evidence
123
+ * still stands. When the window passes, the episode is a miss, recorded at the moment the window
124
+ * ended. Unset: an episode that ends without the outcome is a miss at once. A natural first value
125
+ * is the intent's `halfLife`, where the smallest share inside the window is one half.
126
+ */
127
+ attribution?: number;
115
128
  /** Reveal defaults for this intent. */
116
129
  reveal?: RevealOptions;
117
130
  }
@@ -143,11 +156,18 @@ interface SignalContribution {
143
156
  }
144
157
  /** What this user's history says about one signal. Decays with `learning.halfLife`. */
145
158
  interface SignalRecord {
146
- /** Episodes in which the signal was followed by the outcome. */
159
+ /** Episodes in which the signal was followed by the outcome. An episode credited by a later outcome under `attribution` adds its share here and the rest to `misses`. */
147
160
  hits: number;
148
161
  /** Episodes in which it was not. */
149
162
  misses: number;
150
163
  }
164
+ /** A closed episode waiting for the intent's outcome inside its `attribution` window. */
165
+ interface PendingEpisode {
166
+ /** When the episode closed. Its wait ends `attribution` later. */
167
+ endedAt: number;
168
+ /** The signals seen in it, each once. */
169
+ seen: string[];
170
+ }
151
171
  /**
152
172
  * Everything the engine knows about one intent for this user. Plain data; read it for debugging
153
173
  * or dashboards, but change it only through the engine's functions.
@@ -163,6 +183,11 @@ interface IntentState {
163
183
  learned: Record<string, SignalRecord>;
164
184
  /** Episodes that ended with (`hits`) or without (`misses`) the outcome: the base rate. */
165
185
  episodes: SignalRecord;
186
+ /**
187
+ * Closed episodes whose verdict waits for the outcome (see {@link IntentConfig.attribution}),
188
+ * in order of close, at most 32. Empty unless `attribution` is set.
189
+ */
190
+ pending: PendingEpisode[];
166
191
  /** Time `learned` and `episodes` were last decayed to. */
167
192
  learnedAt: number | null;
168
193
  lastRevealedAt: number | null;
@@ -314,7 +339,8 @@ interface Evaluation {
314
339
  /**
315
340
  * Turns a stored state of a version this library cannot migrate itself into a current
316
341
  * `EngineState`. Receives the parsed JSON as-is and the version it declares. States saved by
317
- * oracular 0.1.0 to 0.2.x (version 1) never reach it: `deserialize` upgrades those on its own.
342
+ * oracular 0.1.0 to 0.5.x (versions 1 and 2) never reach it, nor does a state one version newer
343
+ * than `STATE_VERSION`: `deserialize` handles both on its own.
318
344
  *
319
345
  * @example
320
346
  * ```ts
@@ -337,8 +363,9 @@ interface DeserializeOptions {
337
363
  /** The app's current config. Validated, swapped in, and used to prune signals and intents no longer declared. */
338
364
  config?: EngineConfig;
339
365
  /**
340
- * Called when the stored version is neither `STATE_VERSION` nor one this library migrates
341
- * itself (states from oracular 0.1.0 to 0.2.x are upgraded without it).
366
+ * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
367
+ * itself (states from oracular 0.1.0 to 0.5.x are upgraded without it), nor the next version,
368
+ * which it reads as its own.
342
369
  */
343
370
  migrate?: Migrate;
344
371
  }
@@ -433,6 +460,51 @@ interface EndRecord {
433
460
  }
434
461
  /** A line of a recorder log. */
435
462
  type RecorderRecord = EventRecord | EndRecord;
463
+ /** One intent in a {@link Snapshot}: its name and what {@link evaluate} says about it. */
464
+ interface IntentSnapshot extends Evaluation {
465
+ intent: string;
466
+ }
467
+ /**
468
+ * Every intent's prediction, gate and explanation at one moment, as {@link inspect} returns it:
469
+ * what a debug panel shows, what a test asserts on, what `console.table` prints.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * const { intents } = inspect(state, Date.now());
474
+ * console.table(intents.map((i) => ({ intent: i.intent, p: i.prediction.p, reveal: i.reveal })));
475
+ * ```
476
+ */
477
+ interface Snapshot {
478
+ at: number;
479
+ /** In config order. */
480
+ intents: IntentSnapshot[];
481
+ }
482
+ /**
483
+ * The intents `oracular/presets` ships. Each is a plain {@link IntentConfig} to spread into your
484
+ * config and override; `presetSources` there says where its numbers come from.
485
+ */
486
+ type PresetName = 'willBuy' | 'upgrade' | 'stuck' | 'churn';
487
+ /**
488
+ * Where a preset's numbers come from, as `presetSources` in `oracular/presets` reports it. A tuned
489
+ * preset was fitted by `oracular tune` on a real log and carries the sample behind it; a
490
+ * hand-written one is a careful guess nobody has measured. Either is a starting point: run in
491
+ * shadow mode and let `tune` replace it with your own numbers.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * import { presetSources } from 'oracular/presets';
496
+ * presetSources.willBuy.kind; // 'tuned'
497
+ * ```
498
+ */
499
+ interface PresetSource {
500
+ kind: 'tuned' | 'hand-written';
501
+ /** What the numbers were fitted on, or where the guess comes from. */
502
+ note: string;
503
+ /** Counted episodes behind a tuned preset; null for a hand-written one. */
504
+ episodes: number | null;
505
+ /** Installs those episodes came from; null for a hand-written one. */
506
+ installs: number | null;
507
+ }
436
508
 
437
509
  /**
438
510
  * {@link predict}, {@link shouldReveal} and {@link explain} in one call, from a single decay
@@ -457,6 +529,25 @@ type RecorderRecord = EventRecord | EndRecord;
457
529
  * ```
458
530
  */
459
531
  declare function evaluate(state: EngineState, intent: string, at: number, opts?: EvaluateOptions): Evaluation;
532
+ /**
533
+ * {@link evaluate} for every declared intent at once, in config order: the whole engine's view at
534
+ * one moment, as plain data. This is what to log, assert on, or hand to `renderInspector` from
535
+ * `oracular/inspect` for a live panel. Read-only.
536
+ *
537
+ * @param state - The current state. Not mutated.
538
+ * @param at - The time to evaluate at.
539
+ * @param opts - Reveal overrides and explain filters, applied to every intent.
540
+ * @returns `at` and one {@link IntentSnapshot} per intent.
541
+ * @throws `TypeError` when `state` is not an engine state, `at` is not finite, or `opts` is
542
+ * malformed.
543
+ * @example
544
+ * ```ts
545
+ * for (const { intent, prediction, reveal } of inspect(state, Date.now()).intents) {
546
+ * console.log(intent, prediction.p.toFixed(3), reveal ? 'reveal' : '');
547
+ * }
548
+ * ```
549
+ */
550
+ declare function inspect(state: EngineState, at: number, opts?: EvaluateOptions): Snapshot;
460
551
 
461
552
  /**
462
553
  * How much positive evidence an intent needs before {@link shouldReveal} first becomes true:
@@ -517,10 +608,11 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
517
608
  * `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
518
609
  * That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
519
610
  * it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
520
- * outcome of: give the remembered signals a hit, count the episode toward the base rate, record
521
- * `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens or extends
522
- * the current episode; an idle gap longer than `config.episodeTimeout` closes the previous
523
- * episode first.
611
+ * outcome of: give the remembered signals a hit, count the episode toward the base rate, credit
612
+ * the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
613
+ * each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
614
+ * or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
615
+ * previous episode first.
524
616
  *
525
617
  * @param state - The current state. Not mutated.
526
618
  * @param event - `name` is matched against every intent's signals and outcome; `at` is the
@@ -540,7 +632,8 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
540
632
  declare function observe(state: EngineState, event: IntentEvent): EngineState;
541
633
  /**
542
634
  * Ends the current episode at `at`. Every intent whose outcome did not arrive gives a miss to
543
- * each signal seen this episode and counts the episode toward its base rate. Call it when a
635
+ * each signal seen this episode and counts the episode toward its base rate, or, with
636
+ * `IntentConfig.attribution` set, holds the episode for the outcome instead. Call it when a
544
637
  * session ends: tab closed, app backgrounded, user signed out.
545
638
  *
546
639
  * @param state - The current state. Not mutated.
@@ -562,7 +655,8 @@ declare function endEpisode(state: EngineState, at: number): EngineState;
562
655
  * Whether an episode closed between two states: `prev` had an open episode and `next` either
563
656
  * has none or has a different one. Useful after `observe` or `endEpisode` to react to a
564
657
  * session ending, for example to persist, or to read the misses it produced as
565
- * `next.intents[x].episodes.misses - prev.intents[x].episodes.misses`.
658
+ * `next.intents[x].episodes.misses - prev.intents[x].episodes.misses` (0 for an intent with
659
+ * `attribution` set, whose verdict waits).
566
660
  *
567
661
  * @param prev - The state before the operation.
568
662
  * @param next - The state after it.
@@ -711,8 +805,10 @@ declare function serialize(state: EngineState): string;
711
805
  * Always pass the app's current `config`: it is validated and replaces the stored snapshot,
712
806
  * intents and signals no longer declared are dropped (α and β are recomputed from the kept
713
807
  * contributions), and newly declared intents start at their prior. States saved by oracular
714
- * 0.1.0 to 0.2.x are upgraded to the current shape on their own; `migrate` is only for a version
715
- * this library does not know.
808
+ * 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
809
+ * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
810
+ * current one, ignoring the fields it adds; `migrate` is only for a version this library does
811
+ * not know.
716
812
  *
717
813
  * @param json - A string produced by {@link serialize}.
718
814
  * @param opts - `config`: the app's current config. `migrate`: called as
@@ -722,8 +818,9 @@ declare function serialize(state: EngineState): string;
722
818
  * @throws `SyntaxError` when `json` is not valid JSON.
723
819
  * @throws `TypeError` when the stored data, the result of `migrate`, `opts` or `opts.config`
724
820
  * has the wrong shape.
725
- * @throws `RangeError` when the stored version is unknown and no `migrate` is given, when
726
- * `migrate` does not return the current version, when a stored number is out of range, or
821
+ * @throws `RangeError` when the stored version is neither one this library reads (1 to one past
822
+ * {@link STATE_VERSION}) nor handled by a given `migrate`, when `migrate` does not return the
823
+ * current version, when a stored number is out of range, or
727
824
  * when stored numbers are finite but too large to compute with (a tampered or corrupted state
728
825
  * that would otherwise answer NaN).
729
826
  * @example
@@ -823,4 +920,4 @@ declare const ms: Units;
823
920
  */
824
921
  declare function validateConfig(config: unknown): EngineConfig;
825
922
 
826
- export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentState, type LearningConfig, type Migrate, type Prediction, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
923
+ export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
package/dist/index.d.ts CHANGED
@@ -2,7 +2,9 @@
2
2
  * Version of the serialized state shape, stored in every state as `state.version`. Bumped only
3
3
  * when `EngineState` changes incompatibly, and every bump ships its migration: {@link deserialize}
4
4
  * upgrades states saved by earlier versions of this library on its own (version 1, written by
5
- * oracular 0.1.0 to 0.2.x, becomes version 2) and calls your `migrate` function only for a version
5
+ * oracular 0.1.0 to 0.2.x, and version 2, written by 0.3.0 to 0.5.x, become version 3), reads a
6
+ * state one version newer than its own as its own (a version bump only adds fields, so a rollback
7
+ * by one release keeps the user's history), and calls your `migrate` function only for a version
6
8
  * it does not know.
7
9
  *
8
10
  * @example
@@ -19,7 +21,7 @@
19
21
  * });
20
22
  * ```
21
23
  */
22
- declare const STATE_VERSION: 2;
24
+ declare const STATE_VERSION: 3;
23
25
  /**
24
26
  * Version of the recorder format: the `v` field of every record that {@link createRecord} and
25
27
  * {@link createEndRecord} produce. A new optional field does not change it; readers ignore fields
@@ -112,6 +114,17 @@ interface IntentConfig {
112
114
  * weight.
113
115
  */
114
116
  saturate?: boolean;
117
+ /**
118
+ * How long a closed episode's signals wait for the outcome before they take a miss, in the same
119
+ * unit as `at`. With it set, an episode that ends without the outcome is held instead of
120
+ * counted. When the outcome arrives inside the window, each held episode gets a share
121
+ * `2^(−age / halfLife)` of a hit and the rest of a miss, with `age` measured from the episode's
122
+ * close: the sessions that built up to the outcome are credited by how much of their evidence
123
+ * still stands. When the window passes, the episode is a miss, recorded at the moment the window
124
+ * ended. Unset: an episode that ends without the outcome is a miss at once. A natural first value
125
+ * is the intent's `halfLife`, where the smallest share inside the window is one half.
126
+ */
127
+ attribution?: number;
115
128
  /** Reveal defaults for this intent. */
116
129
  reveal?: RevealOptions;
117
130
  }
@@ -143,11 +156,18 @@ interface SignalContribution {
143
156
  }
144
157
  /** What this user's history says about one signal. Decays with `learning.halfLife`. */
145
158
  interface SignalRecord {
146
- /** Episodes in which the signal was followed by the outcome. */
159
+ /** Episodes in which the signal was followed by the outcome. An episode credited by a later outcome under `attribution` adds its share here and the rest to `misses`. */
147
160
  hits: number;
148
161
  /** Episodes in which it was not. */
149
162
  misses: number;
150
163
  }
164
+ /** A closed episode waiting for the intent's outcome inside its `attribution` window. */
165
+ interface PendingEpisode {
166
+ /** When the episode closed. Its wait ends `attribution` later. */
167
+ endedAt: number;
168
+ /** The signals seen in it, each once. */
169
+ seen: string[];
170
+ }
151
171
  /**
152
172
  * Everything the engine knows about one intent for this user. Plain data; read it for debugging
153
173
  * or dashboards, but change it only through the engine's functions.
@@ -163,6 +183,11 @@ interface IntentState {
163
183
  learned: Record<string, SignalRecord>;
164
184
  /** Episodes that ended with (`hits`) or without (`misses`) the outcome: the base rate. */
165
185
  episodes: SignalRecord;
186
+ /**
187
+ * Closed episodes whose verdict waits for the outcome (see {@link IntentConfig.attribution}),
188
+ * in order of close, at most 32. Empty unless `attribution` is set.
189
+ */
190
+ pending: PendingEpisode[];
166
191
  /** Time `learned` and `episodes` were last decayed to. */
167
192
  learnedAt: number | null;
168
193
  lastRevealedAt: number | null;
@@ -314,7 +339,8 @@ interface Evaluation {
314
339
  /**
315
340
  * Turns a stored state of a version this library cannot migrate itself into a current
316
341
  * `EngineState`. Receives the parsed JSON as-is and the version it declares. States saved by
317
- * oracular 0.1.0 to 0.2.x (version 1) never reach it: `deserialize` upgrades those on its own.
342
+ * oracular 0.1.0 to 0.5.x (versions 1 and 2) never reach it, nor does a state one version newer
343
+ * than `STATE_VERSION`: `deserialize` handles both on its own.
318
344
  *
319
345
  * @example
320
346
  * ```ts
@@ -337,8 +363,9 @@ interface DeserializeOptions {
337
363
  /** The app's current config. Validated, swapped in, and used to prune signals and intents no longer declared. */
338
364
  config?: EngineConfig;
339
365
  /**
340
- * Called when the stored version is neither `STATE_VERSION` nor one this library migrates
341
- * itself (states from oracular 0.1.0 to 0.2.x are upgraded without it).
366
+ * Called when the stored version is neither `STATE_VERSION`, nor one this library migrates
367
+ * itself (states from oracular 0.1.0 to 0.5.x are upgraded without it), nor the next version,
368
+ * which it reads as its own.
342
369
  */
343
370
  migrate?: Migrate;
344
371
  }
@@ -433,6 +460,51 @@ interface EndRecord {
433
460
  }
434
461
  /** A line of a recorder log. */
435
462
  type RecorderRecord = EventRecord | EndRecord;
463
+ /** One intent in a {@link Snapshot}: its name and what {@link evaluate} says about it. */
464
+ interface IntentSnapshot extends Evaluation {
465
+ intent: string;
466
+ }
467
+ /**
468
+ * Every intent's prediction, gate and explanation at one moment, as {@link inspect} returns it:
469
+ * what a debug panel shows, what a test asserts on, what `console.table` prints.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * const { intents } = inspect(state, Date.now());
474
+ * console.table(intents.map((i) => ({ intent: i.intent, p: i.prediction.p, reveal: i.reveal })));
475
+ * ```
476
+ */
477
+ interface Snapshot {
478
+ at: number;
479
+ /** In config order. */
480
+ intents: IntentSnapshot[];
481
+ }
482
+ /**
483
+ * The intents `oracular/presets` ships. Each is a plain {@link IntentConfig} to spread into your
484
+ * config and override; `presetSources` there says where its numbers come from.
485
+ */
486
+ type PresetName = 'willBuy' | 'upgrade' | 'stuck' | 'churn';
487
+ /**
488
+ * Where a preset's numbers come from, as `presetSources` in `oracular/presets` reports it. A tuned
489
+ * preset was fitted by `oracular tune` on a real log and carries the sample behind it; a
490
+ * hand-written one is a careful guess nobody has measured. Either is a starting point: run in
491
+ * shadow mode and let `tune` replace it with your own numbers.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * import { presetSources } from 'oracular/presets';
496
+ * presetSources.willBuy.kind; // 'tuned'
497
+ * ```
498
+ */
499
+ interface PresetSource {
500
+ kind: 'tuned' | 'hand-written';
501
+ /** What the numbers were fitted on, or where the guess comes from. */
502
+ note: string;
503
+ /** Counted episodes behind a tuned preset; null for a hand-written one. */
504
+ episodes: number | null;
505
+ /** Installs those episodes came from; null for a hand-written one. */
506
+ installs: number | null;
507
+ }
436
508
 
437
509
  /**
438
510
  * {@link predict}, {@link shouldReveal} and {@link explain} in one call, from a single decay
@@ -457,6 +529,25 @@ type RecorderRecord = EventRecord | EndRecord;
457
529
  * ```
458
530
  */
459
531
  declare function evaluate(state: EngineState, intent: string, at: number, opts?: EvaluateOptions): Evaluation;
532
+ /**
533
+ * {@link evaluate} for every declared intent at once, in config order: the whole engine's view at
534
+ * one moment, as plain data. This is what to log, assert on, or hand to `renderInspector` from
535
+ * `oracular/inspect` for a live panel. Read-only.
536
+ *
537
+ * @param state - The current state. Not mutated.
538
+ * @param at - The time to evaluate at.
539
+ * @param opts - Reveal overrides and explain filters, applied to every intent.
540
+ * @returns `at` and one {@link IntentSnapshot} per intent.
541
+ * @throws `TypeError` when `state` is not an engine state, `at` is not finite, or `opts` is
542
+ * malformed.
543
+ * @example
544
+ * ```ts
545
+ * for (const { intent, prediction, reveal } of inspect(state, Date.now()).intents) {
546
+ * console.log(intent, prediction.p.toFixed(3), reveal ? 'reveal' : '');
547
+ * }
548
+ * ```
549
+ */
550
+ declare function inspect(state: EngineState, at: number, opts?: EvaluateOptions): Snapshot;
460
551
 
461
552
  /**
462
553
  * How much positive evidence an intent needs before {@link shouldReveal} first becomes true:
@@ -517,10 +608,11 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
517
608
  * `w × log2(1 + 1/n)`, counted since the episode started or the intent's outcome last arrived.
518
609
  * That is the default when `config.episodeTimeout` is set, and `IntentConfig.saturate` overrides
519
610
  * it either way; otherwise every occurrence adds all of `w`. For every intent the event is the
520
- * outcome of: give the remembered signals a hit, count the episode toward the base rate, record
521
- * `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens or extends
522
- * the current episode; an idle gap longer than `config.episodeTimeout` closes the previous
523
- * episode first.
611
+ * outcome of: give the remembered signals a hit, count the episode toward the base rate, credit
612
+ * the closed episodes still waiting under `IntentConfig.attribution` by `2^(−age / halfLife)`
613
+ * each, record `lastOutcomeAt`, and (by default) reset the evidence to the prior. Any event opens
614
+ * or extends the current episode; an idle gap longer than `config.episodeTimeout` closes the
615
+ * previous episode first.
524
616
  *
525
617
  * @param state - The current state. Not mutated.
526
618
  * @param event - `name` is matched against every intent's signals and outcome; `at` is the
@@ -540,7 +632,8 @@ declare function explain(state: EngineState, intent: string, at: number, opts?:
540
632
  declare function observe(state: EngineState, event: IntentEvent): EngineState;
541
633
  /**
542
634
  * Ends the current episode at `at`. Every intent whose outcome did not arrive gives a miss to
543
- * each signal seen this episode and counts the episode toward its base rate. Call it when a
635
+ * each signal seen this episode and counts the episode toward its base rate, or, with
636
+ * `IntentConfig.attribution` set, holds the episode for the outcome instead. Call it when a
544
637
  * session ends: tab closed, app backgrounded, user signed out.
545
638
  *
546
639
  * @param state - The current state. Not mutated.
@@ -562,7 +655,8 @@ declare function endEpisode(state: EngineState, at: number): EngineState;
562
655
  * Whether an episode closed between two states: `prev` had an open episode and `next` either
563
656
  * has none or has a different one. Useful after `observe` or `endEpisode` to react to a
564
657
  * session ending, for example to persist, or to read the misses it produced as
565
- * `next.intents[x].episodes.misses - prev.intents[x].episodes.misses`.
658
+ * `next.intents[x].episodes.misses - prev.intents[x].episodes.misses` (0 for an intent with
659
+ * `attribution` set, whose verdict waits).
566
660
  *
567
661
  * @param prev - The state before the operation.
568
662
  * @param next - The state after it.
@@ -711,8 +805,10 @@ declare function serialize(state: EngineState): string;
711
805
  * Always pass the app's current `config`: it is validated and replaces the stored snapshot,
712
806
  * intents and signals no longer declared are dropped (α and β are recomputed from the kept
713
807
  * contributions), and newly declared intents start at their prior. States saved by oracular
714
- * 0.1.0 to 0.2.x are upgraded to the current shape on their own; `migrate` is only for a version
715
- * this library does not know.
808
+ * 0.1.0 to 0.5.x are upgraded to the current shape on their own, and a state one version newer
809
+ * than {@link STATE_VERSION} (written by the next release, read after a rollback) is read as the
810
+ * current one, ignoring the fields it adds; `migrate` is only for a version this library does
811
+ * not know.
716
812
  *
717
813
  * @param json - A string produced by {@link serialize}.
718
814
  * @param opts - `config`: the app's current config. `migrate`: called as
@@ -722,8 +818,9 @@ declare function serialize(state: EngineState): string;
722
818
  * @throws `SyntaxError` when `json` is not valid JSON.
723
819
  * @throws `TypeError` when the stored data, the result of `migrate`, `opts` or `opts.config`
724
820
  * has the wrong shape.
725
- * @throws `RangeError` when the stored version is unknown and no `migrate` is given, when
726
- * `migrate` does not return the current version, when a stored number is out of range, or
821
+ * @throws `RangeError` when the stored version is neither one this library reads (1 to one past
822
+ * {@link STATE_VERSION}) nor handled by a given `migrate`, when `migrate` does not return the
823
+ * current version, when a stored number is out of range, or
727
824
  * when stored numbers are finite but too large to compute with (a tampered or corrupted state
728
825
  * that would otherwise answer NaN).
729
826
  * @example
@@ -823,4 +920,4 @@ declare const ms: Units;
823
920
  */
824
921
  declare function validateConfig(config: unknown): EngineConfig;
825
922
 
826
- export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentState, type LearningConfig, type Migrate, type Prediction, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
923
+ export { type DeserializeOptions, type EndRecord, type EngineConfig, type EngineState, type EpisodeIntentRecord, type EpisodeState, type EvaluateOptions, type Evaluation, type EventRecord, type ExplainOptions, type IntentConfig, type IntentEvent, type IntentSnapshot, type IntentState, type LearningConfig, type Migrate, type PendingEpisode, type Prediction, type PresetName, type PresetSource, RECORD_VERSION, type RecordMode, type RecordOptions, type RecordedDecision, type RecorderRecord, type RevealOptions, STATE_VERSION, type SignalContribution, type SignalExplanation, type SignalRecord, type Snapshot, type Units, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };