oracular 0.15.0 → 0.17.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 +12 -0
- package/README.md +8 -10
- package/dist/index.cjs +14 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -7
- package/dist/index.d.ts +42 -7
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema/config.schema.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -69,8 +69,25 @@ interface RevealOptions {
|
|
|
69
69
|
*/
|
|
70
70
|
bound?: 'normal' | 'exact';
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The association measure converted to a signed, bounded score when learning signal weights.
|
|
74
|
+
* Every value but the default, `'normalized-difference'`, is deprecated: a sweep of all seven on
|
|
75
|
+
* the clickstream case study found none that beats the default on either split, so the six are
|
|
76
|
+
* removed in 1.0. They keep working, unchanged, until then; `learning.lift` itself stays.
|
|
77
|
+
*/
|
|
78
|
+
type LiftType =
|
|
79
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
80
|
+
'absolute-difference'
|
|
81
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
82
|
+
| 'relative-lift'
|
|
83
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
84
|
+
| 'risk-ratio'
|
|
85
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
86
|
+
| 'log-lift'
|
|
87
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
88
|
+
| 'odds-ratio' | 'normalized-difference'
|
|
89
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
90
|
+
| 'association-rule';
|
|
74
91
|
/**
|
|
75
92
|
* How the engine learns from the user's own outcomes.
|
|
76
93
|
*
|
|
@@ -89,6 +106,7 @@ interface LearningConfig {
|
|
|
89
106
|
* absolute/normalized differences keep their scale. Omit to preserve the existing normalized
|
|
90
107
|
* difference exactly. 'association-rule' uses the overall outcome rate as its baseline; the
|
|
91
108
|
* others use episodes without the signal. Raw ratios are reporting metrics, never weights.
|
|
109
|
+
* Every value but the default is deprecated and removed in 1.0 (see {@link LiftType}).
|
|
92
110
|
*/
|
|
93
111
|
lift?: LiftType;
|
|
94
112
|
/** Half-life of learned hit/miss counts, in the same unit as `at`. Omit for no forgetting. */
|
|
@@ -474,6 +492,15 @@ interface RecordedDecision {
|
|
|
474
492
|
}[];
|
|
475
493
|
/** Effective weight of every declared signal, in config order. */
|
|
476
494
|
weights: Record<string, number>;
|
|
495
|
+
/**
|
|
496
|
+
* The prediction just before the event, on the record of this intent's own outcome event, when
|
|
497
|
+
* {@link createRecord} was given the state before it: the number `resetOnOutcome` wipes, which
|
|
498
|
+
* `p` above no longer shows. Absent on every other record.
|
|
499
|
+
*/
|
|
500
|
+
before?: {
|
|
501
|
+
p: number;
|
|
502
|
+
lower: number;
|
|
503
|
+
};
|
|
477
504
|
}
|
|
478
505
|
/**
|
|
479
506
|
* One line of a recorder log: an event the app observed and what the engine decided right after
|
|
@@ -821,25 +848,33 @@ declare function markRevealed(state: EngineState, intent: string, at: number): E
|
|
|
821
848
|
* and the effective weight of every signal. Pure: the install id comes from the app, never from
|
|
822
849
|
* here. See the Shadow mode guide for the whole workflow.
|
|
823
850
|
*
|
|
851
|
+
* On the record of an intent's own outcome event, `p` is what the engine decides right after it,
|
|
852
|
+
* which with `resetOnOutcome` is the prior: the estimate the outcome confirmed is gone. Pass the
|
|
853
|
+
* state from before the event as `previous` and that intent's decision gains `before`, its `p`
|
|
854
|
+
* and `lower` at `event.at` on that state. Replay never reads it; people reading the log do.
|
|
855
|
+
*
|
|
824
856
|
* @param state - The state after `observe(state, event)`. Not mutated.
|
|
825
857
|
* @param event - The event just observed. Only `name` and `at` are recorded.
|
|
826
858
|
* @param opts - `installId` (generated and stored by the app), `mode` (`'shadow'` while
|
|
827
859
|
* reveals are computed but not shown, `'live'` once they are), and `top` (default 3).
|
|
860
|
+
* @param previous - The state before `observe(state, event)`, optional. Only read for the
|
|
861
|
+
* intents whose outcome the event is.
|
|
828
862
|
* @returns A plain, JSON-serializable {@link EventRecord}.
|
|
829
|
-
* @throws `TypeError` when `state` is not an engine state, the event is
|
|
830
|
-
* not an object, has an unknown key, an empty `installId` or a `mode`
|
|
831
|
-
* `'shadow'` or `'live'`.
|
|
863
|
+
* @throws `TypeError` when `state` or `previous` is not an engine state, the event is
|
|
864
|
+
* malformed, `opts` is not an object, has an unknown key, an empty `installId` or a `mode`
|
|
865
|
+
* other than `'shadow'` or `'live'`.
|
|
832
866
|
* @throws `RangeError` when `opts.top` is negative or fractional.
|
|
833
867
|
* @example
|
|
834
868
|
* ```ts
|
|
835
869
|
* const recording = { installId: localStorage.getItem('oracular.install')!, mode: 'shadow' } as const;
|
|
836
870
|
*
|
|
837
871
|
* const event = { name: 'pricing.viewed', at: Date.now() };
|
|
872
|
+
* const previous = state;
|
|
838
873
|
* state = observe(state, event);
|
|
839
|
-
* appendLine(JSON.stringify(createRecord(state, event, recording)));
|
|
874
|
+
* appendLine(JSON.stringify(createRecord(state, event, recording, previous)));
|
|
840
875
|
* ```
|
|
841
876
|
*/
|
|
842
|
-
declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions): EventRecord;
|
|
877
|
+
declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions, previous?: EngineState): EventRecord;
|
|
843
878
|
/**
|
|
844
879
|
* A recorder record for the end of an episode. Write one whenever the app calls
|
|
845
880
|
* {@link endEpisode}, with the same `at`, so that replay can end the episode at the same moment
|
package/dist/index.d.ts
CHANGED
|
@@ -69,8 +69,25 @@ interface RevealOptions {
|
|
|
69
69
|
*/
|
|
70
70
|
bound?: 'normal' | 'exact';
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
72
|
+
/**
|
|
73
|
+
* The association measure converted to a signed, bounded score when learning signal weights.
|
|
74
|
+
* Every value but the default, `'normalized-difference'`, is deprecated: a sweep of all seven on
|
|
75
|
+
* the clickstream case study found none that beats the default on either split, so the six are
|
|
76
|
+
* removed in 1.0. They keep working, unchanged, until then; `learning.lift` itself stays.
|
|
77
|
+
*/
|
|
78
|
+
type LiftType =
|
|
79
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
80
|
+
'absolute-difference'
|
|
81
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
82
|
+
| 'relative-lift'
|
|
83
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
84
|
+
| 'risk-ratio'
|
|
85
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
86
|
+
| 'log-lift'
|
|
87
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
88
|
+
| 'odds-ratio' | 'normalized-difference'
|
|
89
|
+
/** @deprecated Removed in 1.0: use the default, `'normalized-difference'`. */
|
|
90
|
+
| 'association-rule';
|
|
74
91
|
/**
|
|
75
92
|
* How the engine learns from the user's own outcomes.
|
|
76
93
|
*
|
|
@@ -89,6 +106,7 @@ interface LearningConfig {
|
|
|
89
106
|
* absolute/normalized differences keep their scale. Omit to preserve the existing normalized
|
|
90
107
|
* difference exactly. 'association-rule' uses the overall outcome rate as its baseline; the
|
|
91
108
|
* others use episodes without the signal. Raw ratios are reporting metrics, never weights.
|
|
109
|
+
* Every value but the default is deprecated and removed in 1.0 (see {@link LiftType}).
|
|
92
110
|
*/
|
|
93
111
|
lift?: LiftType;
|
|
94
112
|
/** Half-life of learned hit/miss counts, in the same unit as `at`. Omit for no forgetting. */
|
|
@@ -474,6 +492,15 @@ interface RecordedDecision {
|
|
|
474
492
|
}[];
|
|
475
493
|
/** Effective weight of every declared signal, in config order. */
|
|
476
494
|
weights: Record<string, number>;
|
|
495
|
+
/**
|
|
496
|
+
* The prediction just before the event, on the record of this intent's own outcome event, when
|
|
497
|
+
* {@link createRecord} was given the state before it: the number `resetOnOutcome` wipes, which
|
|
498
|
+
* `p` above no longer shows. Absent on every other record.
|
|
499
|
+
*/
|
|
500
|
+
before?: {
|
|
501
|
+
p: number;
|
|
502
|
+
lower: number;
|
|
503
|
+
};
|
|
477
504
|
}
|
|
478
505
|
/**
|
|
479
506
|
* One line of a recorder log: an event the app observed and what the engine decided right after
|
|
@@ -821,25 +848,33 @@ declare function markRevealed(state: EngineState, intent: string, at: number): E
|
|
|
821
848
|
* and the effective weight of every signal. Pure: the install id comes from the app, never from
|
|
822
849
|
* here. See the Shadow mode guide for the whole workflow.
|
|
823
850
|
*
|
|
851
|
+
* On the record of an intent's own outcome event, `p` is what the engine decides right after it,
|
|
852
|
+
* which with `resetOnOutcome` is the prior: the estimate the outcome confirmed is gone. Pass the
|
|
853
|
+
* state from before the event as `previous` and that intent's decision gains `before`, its `p`
|
|
854
|
+
* and `lower` at `event.at` on that state. Replay never reads it; people reading the log do.
|
|
855
|
+
*
|
|
824
856
|
* @param state - The state after `observe(state, event)`. Not mutated.
|
|
825
857
|
* @param event - The event just observed. Only `name` and `at` are recorded.
|
|
826
858
|
* @param opts - `installId` (generated and stored by the app), `mode` (`'shadow'` while
|
|
827
859
|
* reveals are computed but not shown, `'live'` once they are), and `top` (default 3).
|
|
860
|
+
* @param previous - The state before `observe(state, event)`, optional. Only read for the
|
|
861
|
+
* intents whose outcome the event is.
|
|
828
862
|
* @returns A plain, JSON-serializable {@link EventRecord}.
|
|
829
|
-
* @throws `TypeError` when `state` is not an engine state, the event is
|
|
830
|
-
* not an object, has an unknown key, an empty `installId` or a `mode`
|
|
831
|
-
* `'shadow'` or `'live'`.
|
|
863
|
+
* @throws `TypeError` when `state` or `previous` is not an engine state, the event is
|
|
864
|
+
* malformed, `opts` is not an object, has an unknown key, an empty `installId` or a `mode`
|
|
865
|
+
* other than `'shadow'` or `'live'`.
|
|
832
866
|
* @throws `RangeError` when `opts.top` is negative or fractional.
|
|
833
867
|
* @example
|
|
834
868
|
* ```ts
|
|
835
869
|
* const recording = { installId: localStorage.getItem('oracular.install')!, mode: 'shadow' } as const;
|
|
836
870
|
*
|
|
837
871
|
* const event = { name: 'pricing.viewed', at: Date.now() };
|
|
872
|
+
* const previous = state;
|
|
838
873
|
* state = observe(state, event);
|
|
839
|
-
* appendLine(JSON.stringify(createRecord(state, event, recording)));
|
|
874
|
+
* appendLine(JSON.stringify(createRecord(state, event, recording, previous)));
|
|
840
875
|
* ```
|
|
841
876
|
*/
|
|
842
|
-
declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions): EventRecord;
|
|
877
|
+
declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions, previous?: EngineState): EventRecord;
|
|
843
878
|
/**
|
|
844
879
|
* A recorder record for the end of an episode. Write one whenever the app calls
|
|
845
880
|
* {@link endEpisode}, with the same `at`, so that replay can end the episode at the same moment
|
package/dist/index.js
CHANGED
|
@@ -897,10 +897,11 @@ function normalizeRecordOptions(value) {
|
|
|
897
897
|
}
|
|
898
898
|
return { installId, mode, top };
|
|
899
899
|
}
|
|
900
|
-
function createRecord(state, event, opts) {
|
|
900
|
+
function createRecord(state, event, opts, previous) {
|
|
901
901
|
const s = requireState(state);
|
|
902
902
|
const { name, at } = validateEvent(event);
|
|
903
903
|
const { installId, mode, top } = normalizeRecordOptions(opts);
|
|
904
|
+
const prev = previous === void 0 ? void 0 : requireState(previous);
|
|
904
905
|
const learning = resolveLearning(s.config);
|
|
905
906
|
const decisions = {};
|
|
906
907
|
for (const [intentName, intent] of Object.entries(s.config.intents)) {
|
|
@@ -912,7 +913,7 @@ function createRecord(state, event, opts) {
|
|
|
912
913
|
for (const signal of Object.keys(intent.signals)) {
|
|
913
914
|
weights[signal] = rows.find((row) => row.signal === signal).effectiveWeight;
|
|
914
915
|
}
|
|
915
|
-
|
|
916
|
+
const decision = {
|
|
916
917
|
p: prediction.p,
|
|
917
918
|
lower: prediction.lower,
|
|
918
919
|
shouldReveal: decideReveal(prediction, reveal),
|
|
@@ -920,6 +921,17 @@ function createRecord(state, event, opts) {
|
|
|
920
921
|
top: rows.filter((row) => row.contribution !== 0).slice(0, top).map((row) => ({ signal: row.signal, impact: row.impact })),
|
|
921
922
|
weights
|
|
922
923
|
};
|
|
924
|
+
const prevIntent = prev === void 0 ? void 0 : getOwn(prev.config.intents, intentName);
|
|
925
|
+
if (prev !== void 0 && prevIntent !== void 0 && intent.outcome === name) {
|
|
926
|
+
const was = buildPrediction(
|
|
927
|
+
assessIntent(prev, intentName, prevIntent, resolveLearning(prev.config), at),
|
|
928
|
+
intentName,
|
|
929
|
+
resolveReveal(prev.config, prevIntent, void 0),
|
|
930
|
+
at
|
|
931
|
+
);
|
|
932
|
+
decision.before = { p: was.p, lower: was.lower };
|
|
933
|
+
}
|
|
934
|
+
decisions[intentName] = decision;
|
|
923
935
|
}
|
|
924
936
|
return { v: RECORD_VERSION, kind: "event", installId, mode, event: { name, at }, decisions };
|
|
925
937
|
}
|