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