oracular 0.8.1 → 0.9.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
@@ -62,6 +62,8 @@ interface RevealOptions {
62
62
  */
63
63
  bound?: 'normal' | 'exact';
64
64
  }
65
+ /** The association measure converted to a signed, bounded score when learning signal weights. */
66
+ type LiftType = 'absolute-difference' | 'relative-lift' | 'risk-ratio' | 'log-lift' | 'odds-ratio' | 'normalized-difference' | 'association-rule';
65
67
  /**
66
68
  * How the engine learns from the user's own outcomes.
67
69
  *
@@ -74,9 +76,17 @@ interface RevealOptions {
74
76
  * ```
75
77
  */
76
78
  interface LearningConfig {
79
+ /**
80
+ * Optional association measure. Every choice is converted to a score in [−1, 1], neutral at
81
+ * zero, before learning: ratios use (R − 1)/(R + 1), relative/log lift use x/(1 + |x|), and
82
+ * absolute/normalized differences keep their scale. Omit to preserve the existing normalized
83
+ * difference exactly. 'association-rule' uses the overall outcome rate as its baseline; the
84
+ * others use episodes without the signal. Raw ratios are reporting metrics, never weights.
85
+ */
86
+ lift?: LiftType;
77
87
  /** Half-life of learned hit/miss counts, in the same unit as `at`. Omit for no forgetting. */
78
88
  halfLife?: number;
79
- /** A perfectly predictive signal converges to `gain × |initial weight|`. Default 2; at most 1e6. */
89
+ /** Caps learned magnitude at `gain × |initial weight|` for every lift option. Default 2; at most 1e6. */
80
90
  gain?: number;
81
91
  /** Pseudo-count behind the developer's initial weight: how many observed episodes it takes to weigh as much as the prior. Default 10; at most 1e6. */
82
92
  priorEpisodes?: number;
@@ -847,7 +857,8 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
847
857
  * Builds a fresh engine state from a config. Every intent starts at its prior and no episode is
848
858
  * open. The config is validated and a normalized copy is stored inside the state, so a persisted
849
859
  * state remembers the weights it was built with; pass your current config to {@link deserialize}
850
- * when loading.
860
+ * when loading. `config.learning.lift` selects the lift formula; omitting it preserves the
861
+ * normalized difference used by existing configs and states.
851
862
  *
852
863
  * @param config - Intents with their priors and signals, plus engine-wide options. Validated
853
864
  * strictly: unknown keys, non-finite numbers and out-of-range values throw.
@@ -932,4 +943,4 @@ declare const ms: Units;
932
943
  */
933
944
  declare function validateConfig(config: unknown): EngineConfig;
934
945
 
935
- 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 };
946
+ 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 LiftType, 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
@@ -62,6 +62,8 @@ interface RevealOptions {
62
62
  */
63
63
  bound?: 'normal' | 'exact';
64
64
  }
65
+ /** The association measure converted to a signed, bounded score when learning signal weights. */
66
+ type LiftType = 'absolute-difference' | 'relative-lift' | 'risk-ratio' | 'log-lift' | 'odds-ratio' | 'normalized-difference' | 'association-rule';
65
67
  /**
66
68
  * How the engine learns from the user's own outcomes.
67
69
  *
@@ -74,9 +76,17 @@ interface RevealOptions {
74
76
  * ```
75
77
  */
76
78
  interface LearningConfig {
79
+ /**
80
+ * Optional association measure. Every choice is converted to a score in [−1, 1], neutral at
81
+ * zero, before learning: ratios use (R − 1)/(R + 1), relative/log lift use x/(1 + |x|), and
82
+ * absolute/normalized differences keep their scale. Omit to preserve the existing normalized
83
+ * difference exactly. 'association-rule' uses the overall outcome rate as its baseline; the
84
+ * others use episodes without the signal. Raw ratios are reporting metrics, never weights.
85
+ */
86
+ lift?: LiftType;
77
87
  /** Half-life of learned hit/miss counts, in the same unit as `at`. Omit for no forgetting. */
78
88
  halfLife?: number;
79
- /** A perfectly predictive signal converges to `gain × |initial weight|`. Default 2; at most 1e6. */
89
+ /** Caps learned magnitude at `gain × |initial weight|` for every lift option. Default 2; at most 1e6. */
80
90
  gain?: number;
81
91
  /** Pseudo-count behind the developer's initial weight: how many observed episodes it takes to weigh as much as the prior. Default 10; at most 1e6. */
82
92
  priorEpisodes?: number;
@@ -847,7 +857,8 @@ declare function deserialize(json: string, opts?: DeserializeOptions): EngineSta
847
857
  * Builds a fresh engine state from a config. Every intent starts at its prior and no episode is
848
858
  * open. The config is validated and a normalized copy is stored inside the state, so a persisted
849
859
  * state remembers the weights it was built with; pass your current config to {@link deserialize}
850
- * when loading.
860
+ * when loading. `config.learning.lift` selects the lift formula; omitting it preserves the
861
+ * normalized difference used by existing configs and states.
851
862
  *
852
863
  * @param config - Intents with their priors and signals, plus engine-wide options. Validated
853
864
  * strictly: unknown keys, non-finite numbers and out-of-range values throw.
@@ -932,4 +943,4 @@ declare const ms: Units;
932
943
  */
933
944
  declare function validateConfig(config: unknown): EngineConfig;
934
945
 
935
- 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 };
946
+ 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 LiftType, 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.js CHANGED
@@ -80,10 +80,39 @@ function repeatFactor(n) {
80
80
  function laplace(successes, total) {
81
81
  return (successes + 1) / (total + 2);
82
82
  }
83
- function lift(q, b) {
83
+ function lift(q, b, type = "normalized-difference") {
84
+ if (type === "absolute-difference") return q - b;
85
+ if (type !== "normalized-difference") {
86
+ const rate = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, q));
87
+ const base = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, b));
88
+ switch (type) {
89
+ case "relative-lift":
90
+ return (rate - base) / base;
91
+ case "log-lift":
92
+ return Math.log(rate / base);
93
+ case "odds-ratio":
94
+ return rate / (1 - rate) / (base / (1 - base));
95
+ case "risk-ratio":
96
+ case "association-rule":
97
+ return rate / base;
98
+ }
99
+ }
84
100
  if (q >= b) return b >= 1 ? 0 : (q - b) / (1 - b);
85
101
  return b <= 0 ? 0 : (q - b) / b;
86
102
  }
103
+ function learningScore(value, type = "normalized-difference") {
104
+ switch (type) {
105
+ case "risk-ratio":
106
+ case "odds-ratio":
107
+ case "association-rule":
108
+ return (value - 1) / (value + 1);
109
+ case "relative-lift":
110
+ case "log-lift":
111
+ return value / (1 + Math.abs(value));
112
+ default:
113
+ return value;
114
+ }
115
+ }
87
116
 
88
117
  // src/util.ts
89
118
  function isRecord(value) {
@@ -129,7 +158,9 @@ function signalWeights(state, intent, learning, signal) {
129
158
  const nWithout = Math.max(0, totalEpisodes - nWith);
130
159
  const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
131
160
  const qWithout = laplace(outcomesWithout, nWithout);
132
- const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * lift(qWith, qWithout);
161
+ const baseline = learning.lift === "association-rule" ? (record2.hits + outcomesWithout + 2) / (nWith + nWithout + 4) : qWithout;
162
+ const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
163
+ const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
133
164
  const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
134
165
  const k = learning.priorEpisodes;
135
166
  const effective = (k * initial + nEff * learned) / (k + nEff);
@@ -255,7 +286,16 @@ var INTENT_KEYS = [
255
286
  "reveal"
256
287
  ];
257
288
  var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
258
- var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
289
+ var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes", "lift"];
290
+ var LIFT_TYPES = [
291
+ "absolute-difference",
292
+ "relative-lift",
293
+ "risk-ratio",
294
+ "log-lift",
295
+ "odds-ratio",
296
+ "normalized-difference",
297
+ "association-rule"
298
+ ];
259
299
  var EXPLAIN_KEYS = ["nonZero", "limit"];
260
300
  function typeError(path, message) {
261
301
  throw new TypeError(`oracular: ${path} ${message}`);
@@ -340,6 +380,12 @@ function normalizeLearning(value, path) {
340
380
  const record2 = expectRecord(value, path);
341
381
  expectKeys(record2, LEARNING_KEYS, path);
342
382
  const out = {};
383
+ if (record2.lift !== void 0) {
384
+ if (!LIFT_TYPES.includes(record2.lift)) {
385
+ typeError(`${path}.lift`, `must be one of: ${LIFT_TYPES.join(", ")}`);
386
+ }
387
+ out.lift = record2.lift;
388
+ }
343
389
  if (record2.halfLife !== void 0)
344
390
  out.halfLife = expectPositive(record2.halfLife, `${path}.halfLife`);
345
391
  if (record2.gain !== void 0) out.gain = expectBoundedPositive(record2.gain, `${path}.gain`);
@@ -460,6 +506,7 @@ function resolveReveal(config, intent, opts, normalized = false) {
460
506
  }
461
507
  function resolveLearning(config) {
462
508
  return {
509
+ lift: config.learning?.lift ?? "normalized-difference",
463
510
  halfLife: config.learning?.halfLife ?? null,
464
511
  gain: config.learning?.gain ?? DEFAULT_GAIN,
465
512
  priorEpisodes: config.learning?.priorEpisodes ?? DEFAULT_PRIOR_EPISODES