oracular 0.1.1 → 0.1.2

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#6](https://github.com/Yuribenjamin/oracular/pull/6) [`57d121f`](https://github.com/Yuribenjamin/oracular/commit/57d121f107f4ed92f2e59bf354cc6e19f705de1b) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - Docs corrections from the first production integration; new evaluate(), evidenceToReveal(), episodeClosed() and explain() filtering; default export condition.
8
+
3
9
  ## 0.1.1
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -223,7 +223,10 @@ endEpisode(state, at: number): EngineState
223
223
  predict(state, intent: string, at: number, opts?: RevealOptions): Prediction
224
224
  shouldReveal(state, intent: string, at: number, opts?: RevealOptions): boolean
225
225
  markRevealed(state, intent: string, at: number): EngineState
226
- explain(state, intent: string, at: number): SignalExplanation[]
226
+ explain(state, intent: string, at: number, opts?: { nonZero?: boolean; limit?: number }): SignalExplanation[]
227
+ evaluate(state, intent: string, at: number, opts?): { prediction, reveal, explanation } // all three, one decay pass
228
+ evidenceToReveal(intentConfig, opts?): number // pseudo-counts at which shouldReveal first becomes true
229
+ episodeClosed(prev, next): boolean // did an episode close between two states?
227
230
  serialize(state): string
228
231
  deserialize(json: string, opts?: { config?: EngineConfig; migrate?: Migrate }): EngineState
229
232
  STATE_VERSION // 1
package/dist/index.cjs CHANGED
@@ -1,5 +1,33 @@
1
1
  'use strict';
2
2
 
3
+ // src/math.ts
4
+ function decayFactor(elapsed, halfLife) {
5
+ if (!(elapsed > 0) || !Number.isFinite(elapsed)) return 1;
6
+ return 2 ** (-elapsed / halfLife);
7
+ }
8
+ function betaMean(alpha, beta) {
9
+ return alpha / (alpha + beta);
10
+ }
11
+ function betaVariance(alpha, beta) {
12
+ const n = alpha + beta;
13
+ return alpha * beta / (n * n * (n + 1));
14
+ }
15
+ function betaSd(alpha, beta) {
16
+ return Math.sqrt(betaVariance(alpha, beta));
17
+ }
18
+ function clamp01(x) {
19
+ if (x < 0) return 0;
20
+ if (x > 1) return 1;
21
+ return x;
22
+ }
23
+ function laplace(successes, total) {
24
+ return (successes + 1) / (total + 2);
25
+ }
26
+ function lift(q, b) {
27
+ if (q >= b) return b >= 1 ? 0 : (q - b) / (1 - b);
28
+ return b <= 0 ? 0 : (q - b) / b;
29
+ }
30
+
3
31
  // src/util.ts
4
32
  function isRecord(value) {
5
33
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -18,6 +46,96 @@ function mapRecord(record2, fn) {
18
46
  return out;
19
47
  }
20
48
 
49
+ // src/intent.ts
50
+ function priorCounts(intent) {
51
+ return {
52
+ alpha0: intent.prior * intent.priorStrength,
53
+ beta0: (1 - intent.prior) * intent.priorStrength
54
+ };
55
+ }
56
+ function initIntent(intent) {
57
+ const { alpha0, beta0 } = priorCounts(intent);
58
+ return {
59
+ alpha: alpha0,
60
+ beta: beta0,
61
+ updatedAt: null,
62
+ contributions: {},
63
+ learned: {},
64
+ episodes: { hits: 0, misses: 0 },
65
+ learnedAt: null,
66
+ lastRevealedAt: null,
67
+ lastOutcomeAt: null,
68
+ revealCount: 0
69
+ };
70
+ }
71
+ function withContributions(state, intent, contributions) {
72
+ const { alpha0, beta0 } = priorCounts(intent);
73
+ let alpha = alpha0;
74
+ let beta = beta0;
75
+ for (const c of Object.values(contributions)) {
76
+ alpha += c.alpha;
77
+ beta += c.beta;
78
+ }
79
+ return { ...state, alpha, beta, contributions };
80
+ }
81
+ function decayIntent(state, intent, at) {
82
+ if (state.updatedAt === null) return { ...state, updatedAt: at };
83
+ const d = decayFactor(at - state.updatedAt, intent.halfLife);
84
+ const updatedAt = Math.max(state.updatedAt, at);
85
+ if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
86
+ const contributions = mapRecord(state.contributions, (c) => ({
87
+ alpha: c.alpha * d,
88
+ beta: c.beta * d
89
+ }));
90
+ return withContributions({ ...state, updatedAt }, intent, contributions);
91
+ }
92
+ function applySignal(state, intent, signal, weight) {
93
+ const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
94
+ const next = weight >= 0 ? { alpha: prev.alpha + weight, beta: prev.beta } : { alpha: prev.alpha, beta: prev.beta - weight };
95
+ return withContributions(state, intent, { ...state.contributions, [signal]: next });
96
+ }
97
+ function resetToPrior(state, intent) {
98
+ return withContributions(state, intent, {});
99
+ }
100
+
101
+ // src/learning.ts
102
+ var NONE = { hits: 0, misses: 0 };
103
+ function decayLearned(state, learning, at) {
104
+ if (state.learnedAt === null) return { ...state, learnedAt: at };
105
+ const learnedAt = Math.max(state.learnedAt, at);
106
+ const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
107
+ if (d === 1) return learnedAt === state.learnedAt ? state : { ...state, learnedAt };
108
+ return {
109
+ ...state,
110
+ learnedAt,
111
+ learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
112
+ episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
113
+ };
114
+ }
115
+ function signalWeights(state, intent, learning, signal) {
116
+ const initial = getOwn(intent.signals, signal);
117
+ if (initial === void 0) {
118
+ throw new RangeError(`oracular: ${JSON.stringify(signal)} is not a signal of this intent`);
119
+ }
120
+ const record2 = getOwn(state.learned, signal) ?? NONE;
121
+ const n = record2.hits + record2.misses;
122
+ const q = laplace(record2.hits, n);
123
+ const b = laplace(state.episodes.hits, state.episodes.hits + state.episodes.misses);
124
+ const learned = n === 0 ? 0 : learning.gain * Math.abs(initial) * lift(q, b);
125
+ const k = learning.priorEpisodes;
126
+ const effective = (k * initial + n * learned) / (k + n);
127
+ return { initial, learned, effective, hits: record2.hits, misses: record2.misses };
128
+ }
129
+ function recordEpisode(state, signals, outcome) {
130
+ const learned = { ...state.learned };
131
+ for (const signal of signals) {
132
+ const r = getOwn(learned, signal) ?? NONE;
133
+ learned[signal] = outcome ? { hits: r.hits + 1, misses: r.misses } : { hits: r.hits, misses: r.misses + 1 };
134
+ }
135
+ const episodes = outcome ? { hits: state.episodes.hits + 1, misses: state.episodes.misses } : { hits: state.episodes.hits, misses: state.episodes.misses + 1 };
136
+ return { ...state, learned, episodes };
137
+ }
138
+
21
139
  // src/config.ts
22
140
  var DEFAULT_THRESHOLD = 0.5;
23
141
  var DEFAULT_Z = 1.28;
@@ -35,6 +153,7 @@ var INTENT_KEYS = [
35
153
  ];
36
154
  var REVEAL_KEYS = ["threshold", "z", "cooldown"];
37
155
  var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
156
+ var EXPLAIN_KEYS = ["nonZero", "limit"];
38
157
  function typeError(path, message) {
39
158
  throw new TypeError(`oracular: ${path} ${message}`);
40
159
  }
@@ -115,7 +234,7 @@ function normalizeLearning(value, path) {
115
234
  }
116
235
  return out;
117
236
  }
118
- function normalizeIntent(value, path) {
237
+ function normalizeIntentConfig(value, path) {
119
238
  const record2 = expectRecord(value, path);
120
239
  expectKeys(record2, INTENT_KEYS, path);
121
240
  const signalsRecord = expectRecord(record2.signals, `${path}.signals`);
@@ -157,7 +276,10 @@ function normalizeConfig(value) {
157
276
  const intents = {};
158
277
  for (const name of names) {
159
278
  expectKeyName(name, "config.intents");
160
- intents[name] = normalizeIntent(intentsRecord[name], `config.intents[${JSON.stringify(name)}]`);
279
+ intents[name] = normalizeIntentConfig(
280
+ intentsRecord[name],
281
+ `config.intents[${JSON.stringify(name)}]`
282
+ );
161
283
  }
162
284
  const out = { intents };
163
285
  if (record2.episodeTimeout !== void 0) {
@@ -169,12 +291,43 @@ function normalizeConfig(value) {
169
291
  out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal");
170
292
  return out;
171
293
  }
172
- function resolveReveal(config, intent, opts) {
173
- const o = opts === void 0 ? {} : normalizeRevealOptions(opts, "options");
294
+ function normalizeExplainOptions(value, path) {
295
+ const record2 = expectRecord(value, path);
296
+ expectKeys(record2, EXPLAIN_KEYS, path);
297
+ const out = {};
298
+ if (record2.nonZero !== void 0) {
299
+ if (typeof record2.nonZero !== "boolean") typeError(`${path}.nonZero`, "must be a boolean");
300
+ out.nonZero = record2.nonZero;
301
+ }
302
+ if (record2.limit !== void 0) {
303
+ const n = expectNumber(record2.limit, `${path}.limit`);
304
+ if (!Number.isInteger(n) || n < 0)
305
+ rangeError(`${path}.limit`, "must be a non-negative integer");
306
+ out.limit = n;
307
+ }
308
+ return out;
309
+ }
310
+ function splitEvaluateOptions(value, path) {
311
+ if (value === void 0) return { reveal: {}, explain: {} };
312
+ const record2 = expectRecord(value, path);
313
+ expectKeys(record2, [...REVEAL_KEYS, ...EXPLAIN_KEYS], path);
314
+ const reveal = {};
315
+ const explain2 = {};
316
+ for (const [key, v] of Object.entries(record2)) {
317
+ if (REVEAL_KEYS.includes(key)) reveal[key] = v;
318
+ else explain2[key] = v;
319
+ }
320
+ return {
321
+ reveal: normalizeRevealOptions(reveal, path),
322
+ explain: normalizeExplainOptions(explain2, path)
323
+ };
324
+ }
325
+ function resolveReveal(config, intent, opts, normalized = false) {
326
+ const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options");
174
327
  return {
175
- threshold: o.threshold ?? intent.reveal?.threshold ?? config.reveal?.threshold ?? DEFAULT_THRESHOLD,
176
- z: o.z ?? intent.reveal?.z ?? config.reveal?.z ?? DEFAULT_Z,
177
- cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config.reveal?.cooldown ?? intent.halfLife
328
+ threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
329
+ z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
330
+ cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife
178
331
  };
179
332
  }
180
333
  function resolveLearning(config) {
@@ -197,124 +350,6 @@ function validateEvent(event) {
197
350
  return { name, at };
198
351
  }
199
352
 
200
- // src/math.ts
201
- function decayFactor(elapsed, halfLife) {
202
- if (!(elapsed > 0) || !Number.isFinite(elapsed)) return 1;
203
- return 2 ** (-elapsed / halfLife);
204
- }
205
- function betaMean(alpha, beta) {
206
- return alpha / (alpha + beta);
207
- }
208
- function betaVariance(alpha, beta) {
209
- const n = alpha + beta;
210
- return alpha * beta / (n * n * (n + 1));
211
- }
212
- function betaSd(alpha, beta) {
213
- return Math.sqrt(betaVariance(alpha, beta));
214
- }
215
- function clamp01(x) {
216
- if (x < 0) return 0;
217
- if (x > 1) return 1;
218
- return x;
219
- }
220
- function laplace(successes, total) {
221
- return (successes + 1) / (total + 2);
222
- }
223
- function lift(q, b) {
224
- if (q >= b) return b >= 1 ? 0 : (q - b) / (1 - b);
225
- return b <= 0 ? 0 : (q - b) / b;
226
- }
227
-
228
- // src/intent.ts
229
- function priorCounts(intent) {
230
- return {
231
- alpha0: intent.prior * intent.priorStrength,
232
- beta0: (1 - intent.prior) * intent.priorStrength
233
- };
234
- }
235
- function initIntent(intent) {
236
- const { alpha0, beta0 } = priorCounts(intent);
237
- return {
238
- alpha: alpha0,
239
- beta: beta0,
240
- updatedAt: null,
241
- contributions: {},
242
- learned: {},
243
- episodes: { hits: 0, misses: 0 },
244
- learnedAt: null,
245
- lastRevealedAt: null,
246
- lastOutcomeAt: null,
247
- revealCount: 0
248
- };
249
- }
250
- function withContributions(state, intent, contributions) {
251
- const { alpha0, beta0 } = priorCounts(intent);
252
- let alpha = alpha0;
253
- let beta = beta0;
254
- for (const c of Object.values(contributions)) {
255
- alpha += c.alpha;
256
- beta += c.beta;
257
- }
258
- return { ...state, alpha, beta, contributions };
259
- }
260
- function decayIntent(state, intent, at) {
261
- if (state.updatedAt === null) return { ...state, updatedAt: at };
262
- const d = decayFactor(at - state.updatedAt, intent.halfLife);
263
- const updatedAt = Math.max(state.updatedAt, at);
264
- if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
265
- const contributions = mapRecord(state.contributions, (c) => ({
266
- alpha: c.alpha * d,
267
- beta: c.beta * d
268
- }));
269
- return withContributions({ ...state, updatedAt }, intent, contributions);
270
- }
271
- function applySignal(state, intent, signal, weight) {
272
- const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
273
- const next = weight >= 0 ? { alpha: prev.alpha + weight, beta: prev.beta } : { alpha: prev.alpha, beta: prev.beta - weight };
274
- return withContributions(state, intent, { ...state.contributions, [signal]: next });
275
- }
276
- function resetToPrior(state, intent) {
277
- return withContributions(state, intent, {});
278
- }
279
-
280
- // src/learning.ts
281
- var NONE = { hits: 0, misses: 0 };
282
- function decayLearned(state, learning, at) {
283
- if (state.learnedAt === null) return { ...state, learnedAt: at };
284
- const learnedAt = Math.max(state.learnedAt, at);
285
- const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
286
- if (d === 1) return learnedAt === state.learnedAt ? state : { ...state, learnedAt };
287
- return {
288
- ...state,
289
- learnedAt,
290
- learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
291
- episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
292
- };
293
- }
294
- function signalWeights(state, intent, learning, signal) {
295
- const initial = getOwn(intent.signals, signal);
296
- if (initial === void 0) {
297
- throw new RangeError(`oracular: ${JSON.stringify(signal)} is not a signal of this intent`);
298
- }
299
- const record2 = getOwn(state.learned, signal) ?? NONE;
300
- const n = record2.hits + record2.misses;
301
- const q = laplace(record2.hits, n);
302
- const b = laplace(state.episodes.hits, state.episodes.hits + state.episodes.misses);
303
- const learned = n === 0 ? 0 : learning.gain * Math.abs(initial) * lift(q, b);
304
- const k = learning.priorEpisodes;
305
- const effective = (k * initial + n * learned) / (k + n);
306
- return { initial, learned, effective, hits: record2.hits, misses: record2.misses };
307
- }
308
- function recordEpisode(state, signals, outcome) {
309
- const learned = { ...state.learned };
310
- for (const signal of signals) {
311
- const r = getOwn(learned, signal) ?? NONE;
312
- learned[signal] = outcome ? { hits: r.hits + 1, misses: r.misses } : { hits: r.hits, misses: r.misses + 1 };
313
- }
314
- const episodes = outcome ? { hits: state.episodes.hits + 1, misses: state.episodes.misses } : { hits: state.episodes.hits, misses: state.episodes.misses + 1 };
315
- return { ...state, learned, episodes };
316
- }
317
-
318
353
  // src/episode.ts
319
354
  function emptyEpisode() {
320
355
  return { startedAt: null, lastEventAt: null, intents: {} };
@@ -374,20 +409,42 @@ function getIntentState(state, name, intent) {
374
409
  return getOwn(state.intents, name) ?? initIntent(intent);
375
410
  }
376
411
 
377
- // src/explain.ts
378
- function explain(state, intent, at) {
379
- const s = requireState(state);
380
- const name = expectName(intent, "intent");
381
- const t = expectTimestamp(at, "at");
382
- const config = requireIntentConfig(s, name);
383
- const learning = resolveLearning(s.config);
384
- let is = decayIntent(getIntentState(s, name, config), config, t);
385
- is = decayLearned(is, learning, t);
412
+ // src/assess.ts
413
+ function assessIntent(state, name, intent, learning, at) {
414
+ return decayLearned(decayIntent(getIntentState(state, name, intent), intent, at), learning, at);
415
+ }
416
+ function cooldownEnd(since, cooldown, at) {
417
+ if (since === null) return null;
418
+ const end = since + cooldown;
419
+ return at < end ? end : null;
420
+ }
421
+ function buildPrediction(is, name, reveal, at) {
422
+ const p = betaMean(is.alpha, is.beta);
423
+ const sd = betaSd(is.alpha, is.beta);
424
+ const revealEnd = cooldownEnd(is.lastRevealedAt, reveal.cooldown, at);
425
+ const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.cooldown, at);
426
+ const suppressedUntil = revealEnd === null ? outcomeEnd : outcomeEnd === null ? revealEnd : Math.max(revealEnd, outcomeEnd);
427
+ return {
428
+ intent: name,
429
+ p,
430
+ sd,
431
+ lower: clamp01(p - reveal.z * sd),
432
+ confidence: clamp01(1 - 2 * sd),
433
+ evidence: is.alpha + is.beta,
434
+ revealed: revealEnd !== null,
435
+ revealedAt: is.lastRevealedAt,
436
+ suppressedUntil
437
+ };
438
+ }
439
+ function decideReveal(prediction, reveal) {
440
+ return prediction.suppressedUntil === null && prediction.p >= reveal.threshold && prediction.lower >= reveal.threshold;
441
+ }
442
+ function buildExplanation(is, intent, learning, opts) {
386
443
  const p = betaMean(is.alpha, is.beta);
387
444
  const out = [];
388
- for (const signal of Object.keys(config.signals)) {
445
+ for (const signal of Object.keys(intent.signals)) {
389
446
  const c = getOwn(is.contributions, signal) ?? { alpha: 0, beta: 0 };
390
- const w = signalWeights(is, config, learning, signal);
447
+ const w = signalWeights(is, intent, learning, signal);
391
448
  const without = betaMean(is.alpha - c.alpha, is.beta - c.beta);
392
449
  out.push({
393
450
  signal,
@@ -401,7 +458,8 @@ function explain(state, intent, at) {
401
458
  });
402
459
  }
403
460
  out.sort((a, b) => Math.abs(b.impact) - Math.abs(a.impact) || compare(a.signal, b.signal));
404
- return out;
461
+ const filtered = opts.nonZero ? out.filter((row) => row.contribution !== 0) : out;
462
+ return opts.limit === void 0 ? filtered : filtered.slice(0, opts.limit);
405
463
  }
406
464
  function compare(a, b) {
407
465
  if (a < b) return -1;
@@ -409,6 +467,62 @@ function compare(a, b) {
409
467
  return 0;
410
468
  }
411
469
 
470
+ // src/evaluate.ts
471
+ function evaluate(state, intent, at, opts) {
472
+ const s = requireState(state);
473
+ const name = expectName(intent, "intent");
474
+ const t = expectTimestamp(at, "at");
475
+ const config = requireIntentConfig(s, name);
476
+ const split = splitEvaluateOptions(opts, "options");
477
+ const reveal = resolveReveal(s.config, config, split.reveal, true);
478
+ const learning = resolveLearning(s.config);
479
+ const is = assessIntent(s, name, config, learning, t);
480
+ const prediction = buildPrediction(is, name, reveal, t);
481
+ return {
482
+ prediction,
483
+ reveal: decideReveal(prediction, reveal),
484
+ explanation: buildExplanation(is, config, learning, split.explain)
485
+ };
486
+ }
487
+
488
+ // src/evidence.ts
489
+ function evidenceToReveal(intent, opts) {
490
+ const config = normalizeIntentConfig(intent, "intent");
491
+ const { threshold, z } = resolveReveal(void 0, config, opts);
492
+ const { alpha0, beta0 } = priorCounts(config);
493
+ const open = (w) => {
494
+ const alpha = alpha0 + w;
495
+ const p = betaMean(alpha, beta0);
496
+ return p >= threshold && clamp01(p - z * betaSd(alpha, beta0)) >= threshold;
497
+ };
498
+ if (threshold >= 1) return Number.POSITIVE_INFINITY;
499
+ if (open(0)) return 0;
500
+ let lo = 0;
501
+ let hi = 1;
502
+ while (!open(hi)) {
503
+ lo = hi;
504
+ hi *= 2;
505
+ if (hi > 1e15) return Number.POSITIVE_INFINITY;
506
+ }
507
+ for (let i = 0; i < 200 && hi - lo > 1e-9; i++) {
508
+ const mid = (lo + hi) / 2;
509
+ if (open(mid)) hi = mid;
510
+ else lo = mid;
511
+ }
512
+ return hi;
513
+ }
514
+
515
+ // src/explain.ts
516
+ function explain(state, intent, at, opts) {
517
+ const s = requireState(state);
518
+ const name = expectName(intent, "intent");
519
+ const t = expectTimestamp(at, "at");
520
+ const config = requireIntentConfig(s, name);
521
+ const filters = opts === void 0 ? {} : normalizeExplainOptions(opts, "options");
522
+ const learning = resolveLearning(s.config);
523
+ return buildExplanation(assessIntent(s, name, config, learning, t), config, learning, filters);
524
+ }
525
+
412
526
  // src/observe.ts
413
527
  function observe(state, event) {
414
528
  let s = requireState(state);
@@ -460,6 +574,11 @@ function closeEpisode(state, at) {
460
574
  }
461
575
  return { ...state, intents, episode: emptyEpisode() };
462
576
  }
577
+ function episodeClosed(prev, next) {
578
+ const a = requireState(prev);
579
+ const b = requireState(next);
580
+ return a.episode.startedAt !== null && b.episode.startedAt !== a.episode.startedAt;
581
+ }
463
582
 
464
583
  // src/predict.ts
465
584
  function predict(state, intent, at, opts) {
@@ -469,35 +588,14 @@ function predict(state, intent, at, opts) {
469
588
  const config = requireIntentConfig(s, name);
470
589
  const reveal = resolveReveal(s.config, config, opts);
471
590
  const is = decayIntent(getIntentState(s, name, config), config, t);
472
- const p = betaMean(is.alpha, is.beta);
473
- const sd = betaSd(is.alpha, is.beta);
474
- const revealEnd = cooldownEnd(is.lastRevealedAt, reveal.cooldown, t);
475
- const outcomeEnd = cooldownEnd(is.lastOutcomeAt, reveal.cooldown, t);
476
- const suppressedUntil = revealEnd === null ? outcomeEnd : outcomeEnd === null ? revealEnd : Math.max(revealEnd, outcomeEnd);
477
- return {
478
- intent: name,
479
- p,
480
- sd,
481
- lower: clamp01(p - reveal.z * sd),
482
- confidence: clamp01(1 - 2 * sd),
483
- evidence: is.alpha + is.beta,
484
- revealed: revealEnd !== null,
485
- revealedAt: is.lastRevealedAt,
486
- suppressedUntil
487
- };
488
- }
489
- function cooldownEnd(since, cooldown, at) {
490
- if (since === null) return null;
491
- const end = since + cooldown;
492
- return at < end ? end : null;
591
+ return buildPrediction(is, name, reveal, t);
493
592
  }
494
593
  function shouldReveal(state, intent, at, opts) {
495
594
  const s = requireState(state);
496
595
  const name = expectName(intent, "intent");
497
596
  const config = requireIntentConfig(s, name);
498
- const { threshold } = resolveReveal(s.config, config, opts);
499
- const prediction = predict(s, name, at, opts);
500
- return prediction.suppressedUntil === null && prediction.p >= threshold && prediction.lower >= threshold;
597
+ const reveal = resolveReveal(s.config, config, opts);
598
+ return decideReveal(predict(s, name, at, opts), reveal);
501
599
  }
502
600
  function markRevealed(state, intent, at) {
503
601
  const s = requireState(state);
@@ -688,6 +786,9 @@ exports.STATE_VERSION = STATE_VERSION;
688
786
  exports.createState = createState;
689
787
  exports.deserialize = deserialize;
690
788
  exports.endEpisode = endEpisode;
789
+ exports.episodeClosed = episodeClosed;
790
+ exports.evaluate = evaluate;
791
+ exports.evidenceToReveal = evidenceToReveal;
691
792
  exports.explain = explain;
692
793
  exports.markRevealed = markRevealed;
693
794
  exports.observe = observe;