oracular 0.8.1 → 0.10.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 +26 -0
- package/README.md +90 -85
- package/dist/cli.js +102 -30
- package/dist/index.cjs +98 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -10
- package/dist/index.d.ts +30 -10
- package/dist/index.js +98 -30
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +11 -4
- package/dist/presets.cjs.map +1 -1
- package/dist/presets.d.cts +0 -17
- package/dist/presets.d.ts +0 -17
- package/dist/presets.js +11 -4
- package/dist/presets.js.map +1 -1
- package/package.json +10 -3
- package/schema/config.schema.json +18 -4
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/math.ts
|
|
4
4
|
function decayFactor(elapsed, halfLife) {
|
|
5
|
-
if (!(elapsed > 0)
|
|
5
|
+
if (!(elapsed > 0)) return 1;
|
|
6
6
|
return 2 ** (-elapsed / halfLife);
|
|
7
7
|
}
|
|
8
8
|
function betaMean(alpha, beta) {
|
|
@@ -82,10 +82,39 @@ function repeatFactor(n) {
|
|
|
82
82
|
function laplace(successes, total) {
|
|
83
83
|
return (successes + 1) / (total + 2);
|
|
84
84
|
}
|
|
85
|
-
function lift(q, b) {
|
|
85
|
+
function lift(q, b, type = "normalized-difference") {
|
|
86
|
+
if (type === "absolute-difference") return q - b;
|
|
87
|
+
if (type !== "normalized-difference") {
|
|
88
|
+
const rate = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, q));
|
|
89
|
+
const base = Math.max(Number.EPSILON, Math.min(1 - Number.EPSILON, b));
|
|
90
|
+
switch (type) {
|
|
91
|
+
case "relative-lift":
|
|
92
|
+
return (rate - base) / base;
|
|
93
|
+
case "log-lift":
|
|
94
|
+
return Math.log(rate / base);
|
|
95
|
+
case "odds-ratio":
|
|
96
|
+
return rate / (1 - rate) / (base / (1 - base));
|
|
97
|
+
case "risk-ratio":
|
|
98
|
+
case "association-rule":
|
|
99
|
+
return rate / base;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
86
102
|
if (q >= b) return b >= 1 ? 0 : (q - b) / (1 - b);
|
|
87
103
|
return b <= 0 ? 0 : (q - b) / b;
|
|
88
104
|
}
|
|
105
|
+
function learningScore(value, type = "normalized-difference") {
|
|
106
|
+
switch (type) {
|
|
107
|
+
case "risk-ratio":
|
|
108
|
+
case "odds-ratio":
|
|
109
|
+
case "association-rule":
|
|
110
|
+
return (value - 1) / (value + 1);
|
|
111
|
+
case "relative-lift":
|
|
112
|
+
case "log-lift":
|
|
113
|
+
return value / (1 + Math.abs(value));
|
|
114
|
+
default:
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
89
118
|
|
|
90
119
|
// src/util.ts
|
|
91
120
|
function isRecord(value) {
|
|
@@ -109,12 +138,11 @@ function mapRecord(record2, fn) {
|
|
|
109
138
|
var NONE = { hits: 0, misses: 0 };
|
|
110
139
|
function decayLearned(state, learning, at) {
|
|
111
140
|
if (state.learnedAt === null) return { ...state, learnedAt: at };
|
|
112
|
-
const learnedAt = Math.max(state.learnedAt, at);
|
|
113
141
|
const d = learning.halfLife === null ? 1 : decayFactor(at - state.learnedAt, learning.halfLife);
|
|
114
|
-
if (d === 1) return
|
|
142
|
+
if (d === 1) return at === state.learnedAt ? state : { ...state, learnedAt: at };
|
|
115
143
|
return {
|
|
116
144
|
...state,
|
|
117
|
-
learnedAt,
|
|
145
|
+
learnedAt: at,
|
|
118
146
|
learned: mapRecord(state.learned, (r) => ({ hits: r.hits * d, misses: r.misses * d })),
|
|
119
147
|
episodes: { hits: state.episodes.hits * d, misses: state.episodes.misses * d }
|
|
120
148
|
};
|
|
@@ -131,7 +159,9 @@ function signalWeights(state, intent, learning, signal) {
|
|
|
131
159
|
const nWithout = Math.max(0, totalEpisodes - nWith);
|
|
132
160
|
const outcomesWithout = Math.min(nWithout, Math.max(0, state.episodes.hits - record2.hits));
|
|
133
161
|
const qWithout = laplace(outcomesWithout, nWithout);
|
|
134
|
-
const
|
|
162
|
+
const baseline = learning.lift === "association-rule" ? (record2.hits + outcomesWithout + 2) / (nWith + nWithout + 4) : qWithout;
|
|
163
|
+
const score = learningScore(lift(qWith, baseline, learning.lift), learning.lift);
|
|
164
|
+
const learned = nWith === 0 ? 0 : learning.gain * Math.abs(initial) * score;
|
|
135
165
|
const nEff = nWith + nWithout === 0 ? 0 : nWith * nWithout / (nWith + nWithout);
|
|
136
166
|
const k = learning.priorEpisodes;
|
|
137
167
|
const effective = (k * initial + nEff * learned) / (k + nEff);
|
|
@@ -221,13 +251,12 @@ function withContributions(state, intent, contributions) {
|
|
|
221
251
|
function decayIntent(state, intent, at) {
|
|
222
252
|
if (state.updatedAt === null) return { ...state, updatedAt: at };
|
|
223
253
|
const d = decayFactor(at - state.updatedAt, intent.halfLife);
|
|
224
|
-
|
|
225
|
-
if (d === 1) return updatedAt === state.updatedAt ? state : { ...state, updatedAt };
|
|
254
|
+
if (d === 1) return at === state.updatedAt ? state : { ...state, updatedAt: at };
|
|
226
255
|
const contributions = mapRecord(state.contributions, (c) => ({
|
|
227
256
|
alpha: c.alpha * d,
|
|
228
257
|
beta: c.beta * d
|
|
229
258
|
}));
|
|
230
|
-
return withContributions({ ...state, updatedAt }, intent, contributions);
|
|
259
|
+
return withContributions({ ...state, updatedAt: at }, intent, contributions);
|
|
231
260
|
}
|
|
232
261
|
function applySignal(state, intent, signal, weight) {
|
|
233
262
|
const prev = getOwn(state.contributions, signal) ?? { alpha: 0, beta: 0 };
|
|
@@ -244,6 +273,7 @@ var DEFAULT_Z = 1.28;
|
|
|
244
273
|
var DEFAULT_GAIN = 2;
|
|
245
274
|
var DEFAULT_PRIOR_EPISODES = 10;
|
|
246
275
|
var MAX_MAGNITUDE = 1e6;
|
|
276
|
+
var MAX_Z = 10;
|
|
247
277
|
var CONFIG_KEYS = ["intents", "episodeTimeout", "learning", "reveal"];
|
|
248
278
|
var INTENT_KEYS = [
|
|
249
279
|
"prior",
|
|
@@ -257,7 +287,16 @@ var INTENT_KEYS = [
|
|
|
257
287
|
"reveal"
|
|
258
288
|
];
|
|
259
289
|
var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
|
|
260
|
-
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
|
|
290
|
+
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes", "lift"];
|
|
291
|
+
var LIFT_TYPES = [
|
|
292
|
+
"absolute-difference",
|
|
293
|
+
"relative-lift",
|
|
294
|
+
"risk-ratio",
|
|
295
|
+
"log-lift",
|
|
296
|
+
"odds-ratio",
|
|
297
|
+
"normalized-difference",
|
|
298
|
+
"association-rule"
|
|
299
|
+
];
|
|
261
300
|
var EXPLAIN_KEYS = ["nonZero", "limit"];
|
|
262
301
|
function typeError(path, message) {
|
|
263
302
|
throw new TypeError(`oracular: ${path} ${message}`);
|
|
@@ -319,14 +358,17 @@ function expectKeyName(name, path) {
|
|
|
319
358
|
if (name === "__proto__") typeError(path, 'must not use the reserved name "__proto__"');
|
|
320
359
|
return name;
|
|
321
360
|
}
|
|
322
|
-
function normalizeRevealOptions(value, path) {
|
|
361
|
+
function normalizeRevealOptions(value, path, strict = true) {
|
|
323
362
|
const record2 = expectRecord(value, path);
|
|
324
363
|
expectKeys(record2, REVEAL_KEYS, path);
|
|
325
364
|
const out = {};
|
|
326
365
|
if (record2.threshold !== void 0) {
|
|
327
|
-
out.threshold = expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
366
|
+
out.threshold = strict ? expectOpenUnit(record2.threshold, `${path}.threshold`) : expectClosedUnit(record2.threshold, `${path}.threshold`);
|
|
367
|
+
}
|
|
368
|
+
if (record2.z !== void 0) {
|
|
369
|
+
out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
370
|
+
if (strict && out.z > MAX_Z) rangeError(`${path}.z`, `must be at most ${MAX_Z}`);
|
|
328
371
|
}
|
|
329
|
-
if (record2.z !== void 0) out.z = expectNonNegative(record2.z, `${path}.z`);
|
|
330
372
|
if (record2.cooldown !== void 0) {
|
|
331
373
|
out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
|
|
332
374
|
}
|
|
@@ -342,6 +384,12 @@ function normalizeLearning(value, path) {
|
|
|
342
384
|
const record2 = expectRecord(value, path);
|
|
343
385
|
expectKeys(record2, LEARNING_KEYS, path);
|
|
344
386
|
const out = {};
|
|
387
|
+
if (record2.lift !== void 0) {
|
|
388
|
+
if (!LIFT_TYPES.includes(record2.lift)) {
|
|
389
|
+
typeError(`${path}.lift`, `must be one of: ${LIFT_TYPES.join(", ")}`);
|
|
390
|
+
}
|
|
391
|
+
out.lift = record2.lift;
|
|
392
|
+
}
|
|
345
393
|
if (record2.halfLife !== void 0)
|
|
346
394
|
out.halfLife = expectPositive(record2.halfLife, `${path}.halfLife`);
|
|
347
395
|
if (record2.gain !== void 0) out.gain = expectBoundedPositive(record2.gain, `${path}.gain`);
|
|
@@ -350,7 +398,7 @@ function normalizeLearning(value, path) {
|
|
|
350
398
|
}
|
|
351
399
|
return out;
|
|
352
400
|
}
|
|
353
|
-
function normalizeIntentConfig(value, path) {
|
|
401
|
+
function normalizeIntentConfig(value, path, strict = true) {
|
|
354
402
|
const record2 = expectRecord(value, path);
|
|
355
403
|
expectKeys(record2, INTENT_KEYS, path);
|
|
356
404
|
const signalsRecord = expectRecord(record2.signals, `${path}.signals`);
|
|
@@ -392,11 +440,18 @@ function normalizeIntentConfig(value, path) {
|
|
|
392
440
|
if (record2.attribution !== void 0) {
|
|
393
441
|
out.attribution = expectPositive(record2.attribution, `${path}.attribution`);
|
|
394
442
|
}
|
|
395
|
-
if (record2.reveal !== void 0)
|
|
396
|
-
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal
|
|
443
|
+
if (record2.reveal !== void 0) {
|
|
444
|
+
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal`, strict);
|
|
445
|
+
}
|
|
446
|
+
if (strict && out.outcome !== void 0 && hasOwn(signals, out.outcome) && out.resetOnOutcome !== false) {
|
|
447
|
+
typeError(
|
|
448
|
+
`${path}.outcome`,
|
|
449
|
+
"is also one of its signals: the reset on outcome would wipe what it adds; set resetOnOutcome: false to keep it"
|
|
450
|
+
);
|
|
451
|
+
}
|
|
397
452
|
return out;
|
|
398
453
|
}
|
|
399
|
-
function normalizeConfig(value) {
|
|
454
|
+
function normalizeConfig(value, strict = true) {
|
|
400
455
|
const record2 = expectRecord(value, "config");
|
|
401
456
|
expectKeys(record2, CONFIG_KEYS, "config");
|
|
402
457
|
const intentsRecord = expectRecord(record2.intents, "config.intents");
|
|
@@ -407,7 +462,8 @@ function normalizeConfig(value) {
|
|
|
407
462
|
expectKeyName(name, "config.intents");
|
|
408
463
|
intents[name] = normalizeIntentConfig(
|
|
409
464
|
intentsRecord[name],
|
|
410
|
-
`config.intents[${JSON.stringify(name)}]
|
|
465
|
+
`config.intents[${JSON.stringify(name)}]`,
|
|
466
|
+
strict
|
|
411
467
|
);
|
|
412
468
|
}
|
|
413
469
|
const out = { intents };
|
|
@@ -416,8 +472,9 @@ function normalizeConfig(value) {
|
|
|
416
472
|
}
|
|
417
473
|
if (record2.learning !== void 0)
|
|
418
474
|
out.learning = normalizeLearning(record2.learning, "config.learning");
|
|
419
|
-
if (record2.reveal !== void 0)
|
|
420
|
-
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal");
|
|
475
|
+
if (record2.reveal !== void 0) {
|
|
476
|
+
out.reveal = normalizeRevealOptions(record2.reveal, "config.reveal", strict);
|
|
477
|
+
}
|
|
421
478
|
return out;
|
|
422
479
|
}
|
|
423
480
|
function normalizeExplainOptions(value, path) {
|
|
@@ -447,12 +504,12 @@ function splitEvaluateOptions(value, path) {
|
|
|
447
504
|
else explain2[key] = v;
|
|
448
505
|
}
|
|
449
506
|
return {
|
|
450
|
-
reveal: normalizeRevealOptions(reveal, path),
|
|
507
|
+
reveal: normalizeRevealOptions(reveal, path, false),
|
|
451
508
|
explain: normalizeExplainOptions(explain2, path)
|
|
452
509
|
};
|
|
453
510
|
}
|
|
454
511
|
function resolveReveal(config, intent, opts, normalized = false) {
|
|
455
|
-
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options");
|
|
512
|
+
const o = opts === void 0 ? {} : normalized ? opts : normalizeRevealOptions(opts, "options", false);
|
|
456
513
|
return {
|
|
457
514
|
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
458
515
|
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
@@ -462,6 +519,7 @@ function resolveReveal(config, intent, opts, normalized = false) {
|
|
|
462
519
|
}
|
|
463
520
|
function resolveLearning(config) {
|
|
464
521
|
return {
|
|
522
|
+
lift: config.learning?.lift ?? "normalized-difference",
|
|
465
523
|
halfLife: config.learning?.halfLife ?? null,
|
|
466
524
|
gain: config.learning?.gain ?? DEFAULT_GAIN,
|
|
467
525
|
priorEpisodes: config.learning?.priorEpisodes ?? DEFAULT_PRIOR_EPISODES
|
|
@@ -489,11 +547,7 @@ function timedOutAt(episode, timeout, at) {
|
|
|
489
547
|
return at - episode.lastEventAt > timeout ? episode.lastEventAt + timeout : null;
|
|
490
548
|
}
|
|
491
549
|
function extendEpisode(episode, at) {
|
|
492
|
-
return {
|
|
493
|
-
startedAt: episode.startedAt ?? at,
|
|
494
|
-
lastEventAt: episode.lastEventAt === null ? at : Math.max(episode.lastEventAt, at),
|
|
495
|
-
intents: episode.intents
|
|
496
|
-
};
|
|
550
|
+
return { startedAt: episode.startedAt ?? at, lastEventAt: at, intents: episode.intents };
|
|
497
551
|
}
|
|
498
552
|
function episodeRecord(episode, intent) {
|
|
499
553
|
return getOwn(episode.intents, intent) ?? { seen: [], counts: {}, resolved: false };
|
|
@@ -551,7 +605,7 @@ function assessIntent(state, name, intent, learning, at) {
|
|
|
551
605
|
}
|
|
552
606
|
function cooldownEnd(since, cooldown, at) {
|
|
553
607
|
if (since === null) return null;
|
|
554
|
-
const end = since + cooldown;
|
|
608
|
+
const end = Math.min(since, at) + cooldown;
|
|
555
609
|
return at < end ? end : null;
|
|
556
610
|
}
|
|
557
611
|
function buildPrediction(is, name, reveal, at) {
|
|
@@ -679,6 +733,7 @@ function observe(state, event) {
|
|
|
679
733
|
let is = getIntentState(s, intentName, intent);
|
|
680
734
|
is = decayIntent(is, intent, at);
|
|
681
735
|
is = settlePending(is, intent, learning, at);
|
|
736
|
+
is = pinAnchors(is, at);
|
|
682
737
|
if (isSignal) {
|
|
683
738
|
const { effective } = signalWeights(is, intent, learning, name);
|
|
684
739
|
const n = occurrences(episode, intentName, name) + 1;
|
|
@@ -688,7 +743,8 @@ function observe(state, event) {
|
|
|
688
743
|
if (!isOutcome) episode = markSeen(episode, intentName, name);
|
|
689
744
|
}
|
|
690
745
|
if (isOutcome) {
|
|
691
|
-
|
|
746
|
+
const record2 = episodeRecord(episode, intentName);
|
|
747
|
+
if (!record2.resolved || record2.seen.length > 0) is = recordEpisode(is, record2.seen, 1);
|
|
692
748
|
is = creditPending(is, intent, at);
|
|
693
749
|
episode = markResolved(episode, intentName);
|
|
694
750
|
is = { ...is, lastOutcomeAt: at };
|
|
@@ -698,6 +754,16 @@ function observe(state, event) {
|
|
|
698
754
|
}
|
|
699
755
|
return { ...s, intents, episode };
|
|
700
756
|
}
|
|
757
|
+
function pinAnchors(is, at) {
|
|
758
|
+
const reveal = is.lastRevealedAt !== null && is.lastRevealedAt > at;
|
|
759
|
+
const outcome = is.lastOutcomeAt !== null && is.lastOutcomeAt > at;
|
|
760
|
+
if (!reveal && !outcome) return is;
|
|
761
|
+
return {
|
|
762
|
+
...is,
|
|
763
|
+
lastRevealedAt: reveal ? at : is.lastRevealedAt,
|
|
764
|
+
lastOutcomeAt: outcome ? at : is.lastOutcomeAt
|
|
765
|
+
};
|
|
766
|
+
}
|
|
701
767
|
function endEpisode(state, at) {
|
|
702
768
|
const s = requireState(state);
|
|
703
769
|
const t = expectTimestamp(at, "at");
|
|
@@ -1015,7 +1081,9 @@ function parseState(value) {
|
|
|
1015
1081
|
}
|
|
1016
1082
|
return {
|
|
1017
1083
|
version: STATE_VERSION,
|
|
1018
|
-
|
|
1084
|
+
// Without the strict checks: a config this release would refuse at createState still loads
|
|
1085
|
+
// when it is only stored, and the config passed in replaces it anyway.
|
|
1086
|
+
config: normalizeConfig(r.config, false),
|
|
1019
1087
|
intents: mapOwn(r.intents, "state.intents", intentState),
|
|
1020
1088
|
episode: episodeState(r.episode, "state.episode")
|
|
1021
1089
|
};
|