oracular 0.2.1 → 0.3.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 +22 -0
- package/README.md +44 -9
- package/dist/cli.js +656 -0
- package/dist/index.cjs +177 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +223 -30
- package/dist/index.d.ts +223 -30
- package/dist/index.js +175 -18
- package/dist/index.js.map +1 -1
- package/package.json +13 -4
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ function clamp01(x) {
|
|
|
18
18
|
if (x > 1) return 1;
|
|
19
19
|
return x;
|
|
20
20
|
}
|
|
21
|
+
function repeatFactor(n) {
|
|
22
|
+
return Math.log1p(1 / n) / Math.LN2;
|
|
23
|
+
}
|
|
21
24
|
function laplace(successes, total) {
|
|
22
25
|
return (successes + 1) / (total + 2);
|
|
23
26
|
}
|
|
@@ -143,6 +146,7 @@ var DEFAULT_THRESHOLD = 0.5;
|
|
|
143
146
|
var DEFAULT_Z = 1.28;
|
|
144
147
|
var DEFAULT_GAIN = 2;
|
|
145
148
|
var DEFAULT_PRIOR_EPISODES = 10;
|
|
149
|
+
var MAX_MAGNITUDE = 1e6;
|
|
146
150
|
var CONFIG_KEYS = ["intents", "episodeTimeout", "learning", "reveal"];
|
|
147
151
|
var INTENT_KEYS = [
|
|
148
152
|
"prior",
|
|
@@ -151,6 +155,7 @@ var INTENT_KEYS = [
|
|
|
151
155
|
"signals",
|
|
152
156
|
"outcome",
|
|
153
157
|
"resetOnOutcome",
|
|
158
|
+
"saturate",
|
|
154
159
|
"reveal"
|
|
155
160
|
];
|
|
156
161
|
var REVEAL_KEYS = ["threshold", "z", "cooldown"];
|
|
@@ -186,6 +191,11 @@ function expectPositive(value, path) {
|
|
|
186
191
|
if (n <= 0) rangeError(path, "must be greater than 0");
|
|
187
192
|
return n;
|
|
188
193
|
}
|
|
194
|
+
function expectBoundedPositive(value, path) {
|
|
195
|
+
const n = expectPositive(value, path);
|
|
196
|
+
if (n > MAX_MAGNITUDE) rangeError(path, `must be at most ${MAX_MAGNITUDE}`);
|
|
197
|
+
return n;
|
|
198
|
+
}
|
|
189
199
|
function expectNonNegative(value, path) {
|
|
190
200
|
const n = expectNumber(value, path);
|
|
191
201
|
if (n < 0) rangeError(path, "must be 0 or greater");
|
|
@@ -230,9 +240,9 @@ function normalizeLearning(value, path) {
|
|
|
230
240
|
const out = {};
|
|
231
241
|
if (record2.halfLife !== void 0)
|
|
232
242
|
out.halfLife = expectPositive(record2.halfLife, `${path}.halfLife`);
|
|
233
|
-
if (record2.gain !== void 0) out.gain =
|
|
243
|
+
if (record2.gain !== void 0) out.gain = expectBoundedPositive(record2.gain, `${path}.gain`);
|
|
234
244
|
if (record2.priorEpisodes !== void 0) {
|
|
235
|
-
out.priorEpisodes =
|
|
245
|
+
out.priorEpisodes = expectBoundedPositive(record2.priorEpisodes, `${path}.priorEpisodes`);
|
|
236
246
|
}
|
|
237
247
|
return out;
|
|
238
248
|
}
|
|
@@ -250,11 +260,17 @@ function normalizeIntentConfig(value, path) {
|
|
|
250
260
|
"must be non-zero (positive is evidence for the intent, negative is evidence against)"
|
|
251
261
|
);
|
|
252
262
|
}
|
|
263
|
+
if (Math.abs(w) > MAX_MAGNITUDE) {
|
|
264
|
+
rangeError(
|
|
265
|
+
`${path}.signals[${JSON.stringify(name)}]`,
|
|
266
|
+
`must be between -${MAX_MAGNITUDE} and ${MAX_MAGNITUDE}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
253
269
|
signals[name] = w;
|
|
254
270
|
}
|
|
255
271
|
const out = {
|
|
256
272
|
prior: expectOpenUnit(record2.prior, `${path}.prior`),
|
|
257
|
-
priorStrength:
|
|
273
|
+
priorStrength: expectBoundedPositive(record2.priorStrength, `${path}.priorStrength`),
|
|
258
274
|
halfLife: expectPositive(record2.halfLife, `${path}.halfLife`),
|
|
259
275
|
signals
|
|
260
276
|
};
|
|
@@ -265,6 +281,10 @@ function normalizeIntentConfig(value, path) {
|
|
|
265
281
|
}
|
|
266
282
|
out.resetOnOutcome = record2.resetOnOutcome;
|
|
267
283
|
}
|
|
284
|
+
if (record2.saturate !== void 0) {
|
|
285
|
+
if (typeof record2.saturate !== "boolean") typeError(`${path}.saturate`, "must be a boolean");
|
|
286
|
+
out.saturate = record2.saturate;
|
|
287
|
+
}
|
|
268
288
|
if (record2.reveal !== void 0)
|
|
269
289
|
out.reveal = normalizeRevealOptions(record2.reveal, `${path}.reveal`);
|
|
270
290
|
return out;
|
|
@@ -368,22 +388,27 @@ function extendEpisode(episode, at) {
|
|
|
368
388
|
};
|
|
369
389
|
}
|
|
370
390
|
function episodeRecord(episode, intent) {
|
|
371
|
-
return getOwn(episode.intents, intent) ?? { seen: [], resolved: false };
|
|
391
|
+
return getOwn(episode.intents, intent) ?? { seen: [], counts: {}, resolved: false };
|
|
392
|
+
}
|
|
393
|
+
function occurrences(episode, intent, signal) {
|
|
394
|
+
return getOwn(episodeRecord(episode, intent).counts, signal) ?? 0;
|
|
372
395
|
}
|
|
373
396
|
function markSeen(episode, intent, signal) {
|
|
374
397
|
const record2 = episodeRecord(episode, intent);
|
|
375
|
-
|
|
398
|
+
const seen = record2.seen.includes(signal) ? record2.seen : [...record2.seen, signal];
|
|
399
|
+
const counts = { ...record2.counts, [signal]: (getOwn(record2.counts, signal) ?? 0) + 1 };
|
|
400
|
+
return { ...episode, intents: { ...episode.intents, [intent]: { ...record2, seen, counts } } };
|
|
401
|
+
}
|
|
402
|
+
function markResolved(episode, intent) {
|
|
376
403
|
return {
|
|
377
404
|
...episode,
|
|
378
|
-
intents: { ...episode.intents, [intent]: {
|
|
405
|
+
intents: { ...episode.intents, [intent]: { seen: [], counts: {}, resolved: true } }
|
|
379
406
|
};
|
|
380
407
|
}
|
|
381
|
-
function markResolved(episode, intent) {
|
|
382
|
-
return { ...episode, intents: { ...episode.intents, [intent]: { seen: [], resolved: true } } };
|
|
383
|
-
}
|
|
384
408
|
|
|
385
409
|
// src/version.ts
|
|
386
|
-
var STATE_VERSION =
|
|
410
|
+
var STATE_VERSION = 2;
|
|
411
|
+
var RECORD_VERSION = 1;
|
|
387
412
|
|
|
388
413
|
// src/state.ts
|
|
389
414
|
function createState(config) {
|
|
@@ -543,7 +568,10 @@ function observe(state, event) {
|
|
|
543
568
|
is = decayLearned(is, learning, at);
|
|
544
569
|
if (isSignal) {
|
|
545
570
|
const { effective } = signalWeights(is, intent, learning, name);
|
|
546
|
-
|
|
571
|
+
const n = occurrences(episode, intentName, name) + 1;
|
|
572
|
+
const saturate = intent.saturate ?? s.config.episodeTimeout !== void 0;
|
|
573
|
+
const factor = saturate ? repeatFactor(n) : 1;
|
|
574
|
+
is = applySignal(is, intent, name, effective * factor);
|
|
547
575
|
if (!isOutcome) episode = markSeen(episode, intentName, name);
|
|
548
576
|
}
|
|
549
577
|
if (isOutcome) {
|
|
@@ -614,6 +642,103 @@ function markRevealed(state, intent, at) {
|
|
|
614
642
|
};
|
|
615
643
|
}
|
|
616
644
|
|
|
645
|
+
// src/recorder.ts
|
|
646
|
+
var RECORD_KEYS = ["installId", "mode", "top"];
|
|
647
|
+
var DEFAULT_TOP = 3;
|
|
648
|
+
function normalizeRecordOptions(value) {
|
|
649
|
+
if (!isRecord(value)) {
|
|
650
|
+
throw new TypeError("oracular: options must be an object like { installId, mode }");
|
|
651
|
+
}
|
|
652
|
+
for (const key of Object.keys(value)) {
|
|
653
|
+
if (!RECORD_KEYS.includes(key)) {
|
|
654
|
+
throw new TypeError(
|
|
655
|
+
`oracular: options.${key} is not a recognised option (expected one of: ${RECORD_KEYS.join(", ")})`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const installId = expectName(value.installId, "options.installId");
|
|
660
|
+
const mode = value.mode;
|
|
661
|
+
if (mode !== "shadow" && mode !== "live") {
|
|
662
|
+
throw new TypeError('oracular: options.mode must be "shadow" or "live"');
|
|
663
|
+
}
|
|
664
|
+
let top = DEFAULT_TOP;
|
|
665
|
+
if (value.top !== void 0) {
|
|
666
|
+
if (typeof value.top !== "number" || !Number.isFinite(value.top)) {
|
|
667
|
+
throw new TypeError("oracular: options.top must be a finite number");
|
|
668
|
+
}
|
|
669
|
+
if (!Number.isInteger(value.top) || value.top < 0) {
|
|
670
|
+
throw new RangeError("oracular: options.top must be a non-negative integer");
|
|
671
|
+
}
|
|
672
|
+
top = value.top;
|
|
673
|
+
}
|
|
674
|
+
return { installId, mode, top };
|
|
675
|
+
}
|
|
676
|
+
function createRecord(state, event, opts) {
|
|
677
|
+
const s = requireState(state);
|
|
678
|
+
const { name, at } = validateEvent(event);
|
|
679
|
+
const { installId, mode, top } = normalizeRecordOptions(opts);
|
|
680
|
+
const learning = resolveLearning(s.config);
|
|
681
|
+
const decisions = {};
|
|
682
|
+
for (const [intentName, intent] of Object.entries(s.config.intents)) {
|
|
683
|
+
const reveal = resolveReveal(s.config, intent, void 0);
|
|
684
|
+
const is = assessIntent(s, intentName, intent, learning, at);
|
|
685
|
+
const prediction = buildPrediction(is, intentName, reveal, at);
|
|
686
|
+
const rows = buildExplanation(is, intent, learning, {});
|
|
687
|
+
const weights = {};
|
|
688
|
+
for (const signal of Object.keys(intent.signals)) {
|
|
689
|
+
weights[signal] = rows.find((row) => row.signal === signal).effectiveWeight;
|
|
690
|
+
}
|
|
691
|
+
decisions[intentName] = {
|
|
692
|
+
p: prediction.p,
|
|
693
|
+
lower: prediction.lower,
|
|
694
|
+
shouldReveal: decideReveal(prediction, reveal),
|
|
695
|
+
suppressedUntil: prediction.suppressedUntil,
|
|
696
|
+
top: rows.filter((row) => row.contribution !== 0).slice(0, top).map((row) => ({ signal: row.signal, impact: row.impact })),
|
|
697
|
+
weights
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return { v: RECORD_VERSION, kind: "event", installId, mode, event: { name, at }, decisions };
|
|
701
|
+
}
|
|
702
|
+
function createEndRecord(at, opts) {
|
|
703
|
+
const t = expectTimestamp(at, "at");
|
|
704
|
+
const { installId, mode } = normalizeRecordOptions(opts);
|
|
705
|
+
return { v: RECORD_VERSION, kind: "end", installId, mode, at: t };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/migrate.ts
|
|
709
|
+
var MIGRATIONS = {
|
|
710
|
+
1: fromV1
|
|
711
|
+
};
|
|
712
|
+
function canMigrate(version) {
|
|
713
|
+
return hasOwn(MIGRATIONS, String(version));
|
|
714
|
+
}
|
|
715
|
+
function migrateStored(stored) {
|
|
716
|
+
let current = stored;
|
|
717
|
+
while (typeof current.version === "number" && canMigrate(current.version)) {
|
|
718
|
+
current = MIGRATIONS[current.version](current);
|
|
719
|
+
}
|
|
720
|
+
return current;
|
|
721
|
+
}
|
|
722
|
+
function fromV1(stored) {
|
|
723
|
+
const episode = stored.episode;
|
|
724
|
+
if (!isRecord(episode) || !isRecord(episode.intents)) return { ...stored, version: 2 };
|
|
725
|
+
const intents = {};
|
|
726
|
+
for (const key of Object.keys(episode.intents)) {
|
|
727
|
+
if (key === "__proto__") continue;
|
|
728
|
+
const record2 = episode.intents[key];
|
|
729
|
+
if (isRecord(record2) && Array.isArray(record2.seen)) {
|
|
730
|
+
const counts = {};
|
|
731
|
+
for (const signal of record2.seen) {
|
|
732
|
+
if (typeof signal === "string" && signal !== "__proto__") counts[signal] = 1;
|
|
733
|
+
}
|
|
734
|
+
intents[key] = { ...record2, counts };
|
|
735
|
+
} else {
|
|
736
|
+
intents[key] = record2;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return { ...stored, version: 2, episode: { ...episode, intents } };
|
|
740
|
+
}
|
|
741
|
+
|
|
617
742
|
// src/serialize.ts
|
|
618
743
|
function serialize(state) {
|
|
619
744
|
return JSON.stringify(requireState(state));
|
|
@@ -642,16 +767,19 @@ function deserialize(json, opts = {}) {
|
|
|
642
767
|
}
|
|
643
768
|
let stored = parsed;
|
|
644
769
|
if (parsed.version !== STATE_VERSION) {
|
|
645
|
-
if (
|
|
770
|
+
if (canMigrate(parsed.version)) {
|
|
771
|
+
stored = migrateStored(parsed);
|
|
772
|
+
} else if (opts.migrate === void 0) {
|
|
646
773
|
throw new RangeError(
|
|
647
|
-
`oracular: stored state is version ${parsed.version} but this library reads
|
|
774
|
+
`oracular: stored state is version ${parsed.version} but this library reads versions 1 to ${STATE_VERSION}; pass a migrate function`
|
|
648
775
|
);
|
|
776
|
+
} else {
|
|
777
|
+
stored = opts.migrate(parsed, parsed.version);
|
|
649
778
|
}
|
|
650
|
-
stored = opts.migrate(parsed, parsed.version);
|
|
651
779
|
}
|
|
652
780
|
const raw = parseState(stored);
|
|
653
781
|
const config = opts.config === void 0 ? raw.config : normalizeConfig(opts.config);
|
|
654
|
-
return reconcile(raw, config);
|
|
782
|
+
return requireComputable(reconcile(raw, config));
|
|
655
783
|
}
|
|
656
784
|
function typeError2(path, message) {
|
|
657
785
|
throw new TypeError(`oracular: ${path} ${message}`);
|
|
@@ -715,13 +843,21 @@ function intentState(value, path) {
|
|
|
715
843
|
revealCount
|
|
716
844
|
};
|
|
717
845
|
}
|
|
846
|
+
function occurrenceCount(value, path) {
|
|
847
|
+
const n = finite(value, path);
|
|
848
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
849
|
+
throw new RangeError(`oracular: ${path} must be a positive integer`);
|
|
850
|
+
}
|
|
851
|
+
return n;
|
|
852
|
+
}
|
|
718
853
|
function episodeIntent(value, path) {
|
|
719
854
|
const r = record(value, path);
|
|
720
855
|
if (!Array.isArray(r.seen) || !r.seen.every((s) => typeof s === "string")) {
|
|
721
856
|
typeError2(`${path}.seen`, "must be an array of strings");
|
|
722
857
|
}
|
|
858
|
+
const counts = mapOwn(r.counts, `${path}.counts`, occurrenceCount);
|
|
723
859
|
if (typeof r.resolved !== "boolean") typeError2(`${path}.resolved`, "must be a boolean");
|
|
724
|
-
return { seen: r.seen, resolved: r.resolved };
|
|
860
|
+
return { seen: r.seen, counts, resolved: r.resolved };
|
|
725
861
|
}
|
|
726
862
|
function episodeState(value, path) {
|
|
727
863
|
const r = record(value, path);
|
|
@@ -743,6 +879,25 @@ function parseState(value) {
|
|
|
743
879
|
episode: episodeState(r.episode, "state.episode")
|
|
744
880
|
};
|
|
745
881
|
}
|
|
882
|
+
function requireComputable(state) {
|
|
883
|
+
const learning = resolveLearning(state.config);
|
|
884
|
+
for (const [name, intent] of Object.entries(state.config.intents)) {
|
|
885
|
+
const is = state.intents[name];
|
|
886
|
+
const path = `state.intents[${JSON.stringify(name)}]`;
|
|
887
|
+
if (!Number.isFinite(is.alpha + is.beta) || !Number.isFinite(is.alpha * is.beta)) {
|
|
888
|
+
throw new RangeError(`oracular: ${path} holds more evidence than can be computed with`);
|
|
889
|
+
}
|
|
890
|
+
for (const signal of Object.keys(intent.signals)) {
|
|
891
|
+
const w = signalWeights(is, intent, learning, signal);
|
|
892
|
+
if (!Number.isFinite(w.learned) || !Number.isFinite(w.effective)) {
|
|
893
|
+
throw new RangeError(
|
|
894
|
+
`oracular: ${path} holds learned counts for ${JSON.stringify(signal)} too large to compute with`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return state;
|
|
900
|
+
}
|
|
746
901
|
function keep(source, declared) {
|
|
747
902
|
const out = {};
|
|
748
903
|
for (const [key, value] of Object.entries(source)) {
|
|
@@ -769,7 +924,9 @@ function reconcile(raw, config) {
|
|
|
769
924
|
const seen = episode.seen.filter(
|
|
770
925
|
(s, i) => hasOwn(intent.signals, s) && episode.seen.indexOf(s) === i
|
|
771
926
|
);
|
|
772
|
-
|
|
927
|
+
const counts = {};
|
|
928
|
+
for (const s of seen) counts[s] = getOwn(episode.counts, s) ?? 1;
|
|
929
|
+
episodeIntents[name] = { seen, counts, resolved: episode.resolved };
|
|
773
930
|
}
|
|
774
931
|
}
|
|
775
932
|
return {
|
|
@@ -784,6 +941,6 @@ function reconcile(raw, config) {
|
|
|
784
941
|
};
|
|
785
942
|
}
|
|
786
943
|
|
|
787
|
-
export { STATE_VERSION, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, markRevealed, observe, predict, serialize, shouldReveal };
|
|
944
|
+
export { RECORD_VERSION, STATE_VERSION, createEndRecord, createRecord, createState, deserialize, endEpisode, episodeClosed, evaluate, evidenceToReveal, explain, markRevealed, observe, predict, serialize, shouldReveal };
|
|
788
945
|
//# sourceMappingURL=index.js.map
|
|
789
946
|
//# sourceMappingURL=index.js.map
|