oracular 0.15.0 → 0.16.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
@@ -474,6 +474,15 @@ interface RecordedDecision {
474
474
  }[];
475
475
  /** Effective weight of every declared signal, in config order. */
476
476
  weights: Record<string, number>;
477
+ /**
478
+ * The prediction just before the event, on the record of this intent's own outcome event, when
479
+ * {@link createRecord} was given the state before it: the number `resetOnOutcome` wipes, which
480
+ * `p` above no longer shows. Absent on every other record.
481
+ */
482
+ before?: {
483
+ p: number;
484
+ lower: number;
485
+ };
477
486
  }
478
487
  /**
479
488
  * One line of a recorder log: an event the app observed and what the engine decided right after
@@ -821,25 +830,33 @@ declare function markRevealed(state: EngineState, intent: string, at: number): E
821
830
  * and the effective weight of every signal. Pure: the install id comes from the app, never from
822
831
  * here. See the Shadow mode guide for the whole workflow.
823
832
  *
833
+ * On the record of an intent's own outcome event, `p` is what the engine decides right after it,
834
+ * which with `resetOnOutcome` is the prior: the estimate the outcome confirmed is gone. Pass the
835
+ * state from before the event as `previous` and that intent's decision gains `before`, its `p`
836
+ * and `lower` at `event.at` on that state. Replay never reads it; people reading the log do.
837
+ *
824
838
  * @param state - The state after `observe(state, event)`. Not mutated.
825
839
  * @param event - The event just observed. Only `name` and `at` are recorded.
826
840
  * @param opts - `installId` (generated and stored by the app), `mode` (`'shadow'` while
827
841
  * reveals are computed but not shown, `'live'` once they are), and `top` (default 3).
842
+ * @param previous - The state before `observe(state, event)`, optional. Only read for the
843
+ * intents whose outcome the event is.
828
844
  * @returns A plain, JSON-serializable {@link EventRecord}.
829
- * @throws `TypeError` when `state` is not an engine state, the event is malformed, `opts` is
830
- * not an object, has an unknown key, an empty `installId` or a `mode` other than
831
- * `'shadow'` or `'live'`.
845
+ * @throws `TypeError` when `state` or `previous` is not an engine state, the event is
846
+ * malformed, `opts` is not an object, has an unknown key, an empty `installId` or a `mode`
847
+ * other than `'shadow'` or `'live'`.
832
848
  * @throws `RangeError` when `opts.top` is negative or fractional.
833
849
  * @example
834
850
  * ```ts
835
851
  * const recording = { installId: localStorage.getItem('oracular.install')!, mode: 'shadow' } as const;
836
852
  *
837
853
  * const event = { name: 'pricing.viewed', at: Date.now() };
854
+ * const previous = state;
838
855
  * state = observe(state, event);
839
- * appendLine(JSON.stringify(createRecord(state, event, recording)));
856
+ * appendLine(JSON.stringify(createRecord(state, event, recording, previous)));
840
857
  * ```
841
858
  */
842
- declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions): EventRecord;
859
+ declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions, previous?: EngineState): EventRecord;
843
860
  /**
844
861
  * A recorder record for the end of an episode. Write one whenever the app calls
845
862
  * {@link endEpisode}, with the same `at`, so that replay can end the episode at the same moment
package/dist/index.d.ts CHANGED
@@ -474,6 +474,15 @@ interface RecordedDecision {
474
474
  }[];
475
475
  /** Effective weight of every declared signal, in config order. */
476
476
  weights: Record<string, number>;
477
+ /**
478
+ * The prediction just before the event, on the record of this intent's own outcome event, when
479
+ * {@link createRecord} was given the state before it: the number `resetOnOutcome` wipes, which
480
+ * `p` above no longer shows. Absent on every other record.
481
+ */
482
+ before?: {
483
+ p: number;
484
+ lower: number;
485
+ };
477
486
  }
478
487
  /**
479
488
  * One line of a recorder log: an event the app observed and what the engine decided right after
@@ -821,25 +830,33 @@ declare function markRevealed(state: EngineState, intent: string, at: number): E
821
830
  * and the effective weight of every signal. Pure: the install id comes from the app, never from
822
831
  * here. See the Shadow mode guide for the whole workflow.
823
832
  *
833
+ * On the record of an intent's own outcome event, `p` is what the engine decides right after it,
834
+ * which with `resetOnOutcome` is the prior: the estimate the outcome confirmed is gone. Pass the
835
+ * state from before the event as `previous` and that intent's decision gains `before`, its `p`
836
+ * and `lower` at `event.at` on that state. Replay never reads it; people reading the log do.
837
+ *
824
838
  * @param state - The state after `observe(state, event)`. Not mutated.
825
839
  * @param event - The event just observed. Only `name` and `at` are recorded.
826
840
  * @param opts - `installId` (generated and stored by the app), `mode` (`'shadow'` while
827
841
  * reveals are computed but not shown, `'live'` once they are), and `top` (default 3).
842
+ * @param previous - The state before `observe(state, event)`, optional. Only read for the
843
+ * intents whose outcome the event is.
828
844
  * @returns A plain, JSON-serializable {@link EventRecord}.
829
- * @throws `TypeError` when `state` is not an engine state, the event is malformed, `opts` is
830
- * not an object, has an unknown key, an empty `installId` or a `mode` other than
831
- * `'shadow'` or `'live'`.
845
+ * @throws `TypeError` when `state` or `previous` is not an engine state, the event is
846
+ * malformed, `opts` is not an object, has an unknown key, an empty `installId` or a `mode`
847
+ * other than `'shadow'` or `'live'`.
832
848
  * @throws `RangeError` when `opts.top` is negative or fractional.
833
849
  * @example
834
850
  * ```ts
835
851
  * const recording = { installId: localStorage.getItem('oracular.install')!, mode: 'shadow' } as const;
836
852
  *
837
853
  * const event = { name: 'pricing.viewed', at: Date.now() };
854
+ * const previous = state;
838
855
  * state = observe(state, event);
839
- * appendLine(JSON.stringify(createRecord(state, event, recording)));
856
+ * appendLine(JSON.stringify(createRecord(state, event, recording, previous)));
840
857
  * ```
841
858
  */
842
- declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions): EventRecord;
859
+ declare function createRecord(state: EngineState, event: IntentEvent, opts: RecordOptions, previous?: EngineState): EventRecord;
843
860
  /**
844
861
  * A recorder record for the end of an episode. Write one whenever the app calls
845
862
  * {@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
- decisions[intentName] = {
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
  }