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.js CHANGED
@@ -47,6 +47,86 @@ function mapRecord(record2, fn) {
47
47
  return out;
48
48
  }
49
49
 
50
+ // src/learning.ts
51
+ var NONE = { hits: 0, misses: 0 };
52
+ function decayLearned(state, learning, at) {
53
+ if (state.learnedAt === null) return { ...state, learnedAt: at };
54
+ const learnedAt = Math.max(state.learnedAt, at);
55
+ const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
56
+ if (d === 1) return learnedAt === state.learnedAt ? state : { ...state, learnedAt };
57
+ return {
58
+ ...state,
59
+ learnedAt,
60
+ learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
61
+ episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
62
+ };
63
+ }
64
+ function signalWeights(state, intent, learning, signal) {
65
+ const initial = getOwn(intent.signals, signal);
66
+ if (initial === void 0) {
67
+ throw new RangeError(`oracular: ${JSON.stringify(signal)} is not a signal of this intent`);
68
+ }
69
+ const record2 = getOwn(state.learned, signal) ?? NONE;
70
+ const nWith = record2.hits + record2.misses;
71
+ const qWith = laplace(record2.hits, nWith);
72
+ const totalEpisodes = state.episodes.hits + state.episodes.misses;
73
+ const nWithout = Math.max(0, totalEpisodes - nWith);
74
+ const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
75
+ const qWithout = laplace(outcomesWithout, nWithout);
76
+ const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * lift(qWith, qWithout);
77
+ const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
78
+ const k = learning.priorEpisodes;
79
+ const effective = (k * initial + nEff * learned) / (k + nEff);
80
+ return { initial, learned, effective, hits: record2.hits, misses: record2.misses };
81
+ }
82
+ function recordEpisode(state, signals, credit) {
83
+ const miss = 1 - credit;
84
+ const learned = { ...state.learned };
85
+ for (const signal of signals) {
86
+ const r = getOwn(learned, signal) ?? NONE;
87
+ learned[signal] = { hits: r.hits + credit, misses: r.misses + miss };
88
+ }
89
+ const episodes = { hits: state.episodes.hits + credit, misses: state.episodes.misses + miss };
90
+ return { ...state, learned, episodes };
91
+ }
92
+
93
+ // src/attribution.ts
94
+ var MAX_PENDING = 32;
95
+ function expiry(episode, intent) {
96
+ return episode.endedAt + (intent.attribution ?? 0);
97
+ }
98
+ function settlePending(state, intent, learning, at) {
99
+ let is = state;
100
+ let settled = 0;
101
+ for (const episode of state.pending) {
102
+ const end = expiry(episode, intent);
103
+ if (!(end < at)) break;
104
+ is = recordEpisode(decayLearned(is, learning, end), episode.seen, 0);
105
+ settled++;
106
+ }
107
+ if (settled > 0) is = { ...is, pending: state.pending.slice(settled) };
108
+ return decayLearned(is, learning, at);
109
+ }
110
+ function deferEpisode(state, seen, endedAt) {
111
+ const pending = insert(state.pending, { endedAt, seen: [...seen] });
112
+ if (pending.length <= MAX_PENDING) return { ...state, pending };
113
+ const [oldest, ...rest] = pending;
114
+ return { ...recordEpisode(state, oldest.seen, 0), pending: rest };
115
+ }
116
+ function insert(pending, episode) {
117
+ let i = pending.length;
118
+ while (i > 0 && pending[i - 1].endedAt > episode.endedAt) i--;
119
+ return [...pending.slice(0, i), episode, ...pending.slice(i)];
120
+ }
121
+ function creditPending(state, intent, at) {
122
+ if (state.pending.length === 0) return state;
123
+ let is = state;
124
+ for (const episode of state.pending) {
125
+ is = recordEpisode(is, episode.seen, decayFactor(at - episode.endedAt, intent.halfLife));
126
+ }
127
+ return { ...is, pending: [] };
128
+ }
129
+
50
130
  // src/intent.ts
51
131
  function priorCounts(intent) {
52
132
  return {
@@ -63,6 +143,7 @@ function initIntent(intent) {
63
143
  contributions: {},
64
144
  learned: {},
65
145
  episodes: { hits: 0, misses: 0 },
146
+ pending: [],
66
147
  learnedAt: null,
67
148
  lastRevealedAt: null,
68
149
  lastOutcomeAt: null,
@@ -99,48 +180,6 @@ function resetToPrior(state, intent) {
99
180
  return withContributions(state, intent, {});
100
181
  }
101
182
 
102
- // src/learning.ts
103
- var NONE = { hits: 0, misses: 0 };
104
- function decayLearned(state, learning, at) {
105
- if (state.learnedAt === null) return { ...state, learnedAt: at };
106
- const learnedAt = Math.max(state.learnedAt, at);
107
- const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
108
- if (d === 1) return learnedAt === state.learnedAt ? state : { ...state, learnedAt };
109
- return {
110
- ...state,
111
- learnedAt,
112
- learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
113
- episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
114
- };
115
- }
116
- function signalWeights(state, intent, learning, signal) {
117
- const initial = getOwn(intent.signals, signal);
118
- if (initial === void 0) {
119
- throw new RangeError(`oracular: ${JSON.stringify(signal)} is not a signal of this intent`);
120
- }
121
- const record2 = getOwn(state.learned, signal) ?? NONE;
122
- const nWith = record2.hits + record2.misses;
123
- const qWith = laplace(record2.hits, nWith);
124
- const totalEpisodes = state.episodes.hits + state.episodes.misses;
125
- const nWithout = Math.max(0, totalEpisodes - nWith);
126
- const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
127
- const qWithout = laplace(outcomesWithout, nWithout);
128
- const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * lift(qWith, qWithout);
129
- const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
130
- const k = learning.priorEpisodes;
131
- const effective = (k * initial + nEff * learned) / (k + nEff);
132
- return { initial, learned, effective, hits: record2.hits, misses: record2.misses };
133
- }
134
- function recordEpisode(state, signals, outcome) {
135
- const learned = { ...state.learned };
136
- for (const signal of signals) {
137
- const r = getOwn(learned, signal) ?? NONE;
138
- learned[signal] = outcome ? { hits: r.hits + 1, misses: r.misses } : { hits: r.hits, misses: r.misses + 1 };
139
- }
140
- const episodes = outcome ? { hits: state.episodes.hits + 1, misses: state.episodes.misses } : { hits: state.episodes.hits, misses: state.episodes.misses + 1 };
141
- return { ...state, learned, episodes };
142
- }
143
-
144
183
  // src/config.ts
145
184
  var DEFAULT_THRESHOLD = 0.5;
146
185
  var DEFAULT_Z = 1.28;
@@ -156,6 +195,7 @@ var INTENT_KEYS = [
156
195
  "outcome",
157
196
  "resetOnOutcome",
158
197
  "saturate",
198
+ "attribution",
159
199
  "reveal"
160
200
  ];
161
201
  var REVEAL_KEYS = ["threshold", "z", "cooldown"];
@@ -285,6 +325,9 @@ function normalizeIntentConfig(value, path) {
285
325
  if (typeof record2.saturate !== "boolean") typeError(`${path}.saturate`, "must be a boolean");
286
326
  out.saturate = record2.saturate;
287
327
  }
328
+ if (record2.attribution !== void 0) {
329
+ out.attribution = expectPositive(record2.attribution, `${path}.attribution`);
330
+ }
288
331
  if (record2.reveal !== void 0)
289
332
  out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal`);
290
333
  return out;
@@ -407,7 +450,7 @@ function markResolved(episode, intent) {
407
450
  }
408
451
 
409
452
  // src/version.ts
410
- var STATE_VERSION = 2;
453
+ var STATE_VERSION = 3;
411
454
  var RECORD_VERSION = 1;
412
455
 
413
456
  // src/state.ts
@@ -438,7 +481,8 @@ function getIntentState(state, name, intent) {
438
481
 
439
482
  // src/assess.ts
440
483
  function assessIntent(state, name, intent, learning, at) {
441
- return decayLearned(decayIntent(getIntentState(state, name, intent), intent, at), learning, at);
484
+ const is = decayIntent(getIntentState(state, name, intent), intent, at);
485
+ return settlePending(is, intent, learning, at);
442
486
  }
443
487
  function cooldownEnd(since, cooldown, at) {
444
488
  if (since === null) return null;
@@ -511,6 +555,14 @@ function evaluate(state, intent, at, opts) {
511
555
  explanation: buildExplanation(is, config, learning, split.explain)
512
556
  };
513
557
  }
558
+ function inspect(state, at, opts) {
559
+ const s = requireState(state);
560
+ const intents = Object.keys(s.config.intents).map((intent) => ({
561
+ intent,
562
+ ...evaluate(s, intent, at, opts)
563
+ }));
564
+ return { at, intents };
565
+ }
514
566
 
515
567
  // src/evidence.ts
516
568
  function evidenceToReveal(intent, opts) {
@@ -565,7 +617,7 @@ function observe(state, event) {
565
617
  if (!isSignal && !isOutcome) continue;
566
618
  let is = getIntentState(s, intentName, intent);
567
619
  is = decayIntent(is, intent, at);
568
- is = decayLearned(is, learning, at);
620
+ is = settlePending(is, intent, learning, at);
569
621
  if (isSignal) {
570
622
  const { effective } = signalWeights(is, intent, learning, name);
571
623
  const n = occurrences(episode, intentName, name) + 1;
@@ -575,7 +627,8 @@ function observe(state, event) {
575
627
  if (!isOutcome) episode = markSeen(episode, intentName, name);
576
628
  }
577
629
  if (isOutcome) {
578
- is = recordEpisode(is, episodeRecord(episode, intentName).seen, true);
630
+ is = recordEpisode(is, episodeRecord(episode, intentName).seen, 1);
631
+ is = creditPending(is, intent, at);
579
632
  episode = markResolved(episode, intentName);
580
633
  is = { ...is, lastOutcomeAt: at };
581
634
  if (intent.resetOnOutcome ?? true) is = resetToPrior(is, intent);
@@ -598,8 +651,8 @@ function closeEpisode(state, at) {
598
651
  const record2 = episodeRecord(state.episode, intentName);
599
652
  if (record2.resolved) continue;
600
653
  let is = getIntentState(state, intentName, intent);
601
- is = decayLearned(is, learning, at);
602
- is = recordEpisode(is, record2.seen, false);
654
+ is = settlePending(is, intent, learning, at);
655
+ is = intent.attribution === void 0 ? recordEpisode(is, record2.seen, 0) : deferEpisode(is, record2.seen, at);
603
656
  intents[intentName] = is;
604
657
  }
605
658
  return { ...state, intents, episode: emptyEpisode() };
@@ -707,7 +760,8 @@ function createEndRecord(at, opts) {
707
760
 
708
761
  // src/migrate.ts
709
762
  var MIGRATIONS = {
710
- 1: fromV1
763
+ 1: fromV1,
764
+ 2: fromV2
711
765
  };
712
766
  function canMigrate(version) {
713
767
  return hasOwn(MIGRATIONS, String(version));
@@ -738,6 +792,16 @@ function fromV1(stored) {
738
792
  }
739
793
  return { ...stored, version: 2, episode: { ...episode, intents } };
740
794
  }
795
+ function fromV2(stored) {
796
+ if (!isRecord(stored.intents)) return { ...stored, version: 3 };
797
+ const intents = {};
798
+ for (const key of Object.keys(stored.intents)) {
799
+ if (key === "__proto__") continue;
800
+ const intent = stored.intents[key];
801
+ intents[key] = isRecord(intent) ? { ...intent, pending: [] } : intent;
802
+ }
803
+ return { ...stored, version: 3, intents };
804
+ }
741
805
 
742
806
  // src/serialize.ts
743
807
  function serialize(state) {
@@ -769,9 +833,11 @@ function deserialize(json, opts = {}) {
769
833
  if (parsed.version !== STATE_VERSION) {
770
834
  if (canMigrate(parsed.version)) {
771
835
  stored = migrateStored(parsed);
836
+ } else if (parsed.version === STATE_VERSION + 1) {
837
+ stored = { ...parsed, version: STATE_VERSION };
772
838
  } else if (opts.migrate === void 0) {
773
839
  throw new RangeError(
774
- `oracular: stored state is version ${parsed.version} but this library reads versions 1 to ${STATE_VERSION}; pass a migrate function`
840
+ `oracular: stored state is version ${parsed.version} but this library reads versions 1 to ${STATE_VERSION + 1}; pass a migrate function`
775
841
  );
776
842
  } else {
777
843
  stored = opts.migrate(parsed, parsed.version);
@@ -824,6 +890,19 @@ function mapOwn(value, path, fn) {
824
890
  }
825
891
  return out;
826
892
  }
893
+ function pendingEpisodes(value, path) {
894
+ if (!Array.isArray(value)) typeError2(path, "must be an array");
895
+ if (value.length > MAX_PENDING) {
896
+ throw new RangeError(`oracular: ${path} must hold at most ${MAX_PENDING} episodes`);
897
+ }
898
+ return value.map((item, i) => {
899
+ const r = record(item, `${path}[${i}]`);
900
+ if (!Array.isArray(r.seen) || !r.seen.every((s) => typeof s === "string")) {
901
+ typeError2(`${path}[${i}].seen`, "must be an array of strings");
902
+ }
903
+ return { endedAt: finite(r.endedAt, `${path}[${i}].endedAt`), seen: r.seen };
904
+ });
905
+ }
827
906
  function intentState(value, path) {
828
907
  const r = record(value, path);
829
908
  const revealCount = finite(r.revealCount, `${path}.revealCount`);
@@ -837,6 +916,7 @@ function intentState(value, path) {
837
916
  contributions: mapOwn(r.contributions, `${path}.contributions`, contribution),
838
917
  learned: mapOwn(r.learned, `${path}.learned`, signalRecord),
839
918
  episodes: signalRecord(r.episodes, `${path}.episodes`),
919
+ pending: pendingEpisodes(r.pending, `${path}.pending`),
840
920
  learnedAt: nullOrFinite(r.learnedAt, `${path}.learnedAt`),
841
921
  lastRevealedAt: nullOrFinite(r.lastRevealedAt, `${path}.lastRevealedAt`),
842
922
  lastOutcomeAt: nullOrFinite(r.lastOutcomeAt, `${path}.lastOutcomeAt`),
@@ -905,6 +985,12 @@ function keep(source, declared) {
905
985
  }
906
986
  return out;
907
987
  }
988
+ function keepPending(pending, signals) {
989
+ return pending.map((episode) => ({
990
+ endedAt: episode.endedAt,
991
+ seen: episode.seen.filter((s, i) => hasOwn(signals, s) && episode.seen.indexOf(s) === i)
992
+ })).sort((a, b) => a.endedAt - b.endedAt);
993
+ }
908
994
  function reconcile(raw, config) {
909
995
  const intents = {};
910
996
  const episodeIntents = {};
@@ -914,7 +1000,11 @@ function reconcile(raw, config) {
914
1000
  intents[name] = initIntent(intent);
915
1001
  } else {
916
1002
  intents[name] = withContributions(
917
- { ...stored, learned: keep(stored.learned, intent.signals) },
1003
+ {
1004
+ ...stored,
1005
+ learned: keep(stored.learned, intent.signals),
1006
+ pending: keepPending(stored.pending, intent.signals)
1007
+ },
918
1008
  intent,
919
1009
  keep(stored.contributions, intent.signals)
920
1010
  );
@@ -955,6 +1045,6 @@ function validateConfig(config) {
955
1045
  return normalizeConfig(config);
956
1046
  }
957
1047
 
958
- export { RECORD_VERSION, STATE_VERSION, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
1048
+ export { RECORD_VERSION, STATE_VERSION, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, inspect, markRevealed, ms, observe, predict, serialize, shouldReveal, validateConfig };
959
1049
  //# sourceMappingURL=index.js.map
960
1050
  //# sourceMappingURL=index.js.map