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/CHANGELOG.md +22 -0
- package/README.md +27 -10
- package/dist/cli.js +11 -1
- package/dist/index.cjs +142 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -17
- package/dist/index.d.ts +114 -17
- package/dist/index.js +142 -52
- package/dist/index.js.map +1 -1
- package/dist/inspect.cjs +67 -0
- package/dist/inspect.cjs.map +1 -0
- package/dist/inspect.d.cts +143 -0
- package/dist/inspect.d.ts +143 -0
- package/dist/inspect.js +64 -0
- package/dist/inspect.js.map +1 -0
- package/dist/presets.cjs +86 -0
- package/dist/presets.cjs.map +1 -0
- package/dist/presets.d.cts +129 -0
- package/dist/presets.d.ts +129 -0
- package/dist/presets.js +83 -0
- package/dist/presets.js.map +1 -0
- package/package.json +33 -1
- package/schema/config.schema.json +4 -0
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 =
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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 =
|
|
602
|
-
is = recordEpisode(is, record2.seen,
|
|
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
|
-
{
|
|
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
|