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/CHANGELOG.md +22 -0
- package/README.md +27 -10
- package/dist/cli.js +11 -1
- package/dist/index.cjs +142 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -17
- package/dist/index.d.ts +114 -17
- package/dist/index.js +142 -52
- package/dist/index.js.map +1 -1
- package/dist/inspect.cjs +67 -0
- package/dist/inspect.cjs.map +1 -0
- package/dist/inspect.d.cts +143 -0
- package/dist/inspect.d.ts +143 -0
- package/dist/inspect.js +64 -0
- package/dist/inspect.js.map +1 -0
- package/dist/presets.cjs +86 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +129 -0
- package/dist/presets.d.ts +129 -0
- package/dist/presets.js +83 -0
- package/dist/presets.js.map +1 -0
- package/package.json +33 -1
- package/schema/config.schema.json +4 -0
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,
|
|
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:
|
|
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.
|
|
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
|
|
341
|
-
* itself (states from oracular 0.1.0 to 0.
|
|
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,
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* episode
|
|
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
|
|
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.
|
|
715
|
-
*
|
|
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
|
|
726
|
-
*
|
|
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,
|
|
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:
|
|
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.
|
|
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
|
|
341
|
-
* itself (states from oracular 0.1.0 to 0.
|
|
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,
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
* episode
|
|
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
|
|
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.
|
|
715
|
-
*
|
|
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
|
|
726
|
-
*
|
|
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 };
|