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.
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ // src/inspect.ts
4
+ var escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
5
+ var BOX = "font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#1f2328;background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:10px 12px;max-width:100%;overflow:auto";
6
+ var TABLE = "border-collapse:collapse;margin:6px 0 10px";
7
+ var CELL = "padding:2px 10px 2px 0;text-align:right;white-space:nowrap";
8
+ var HEAD = `${CELL};color:#656d76;font-weight:normal;border-bottom:1px solid #d8dee4`;
9
+ var NAME = "padding:2px 10px 2px 0;text-align:left;white-space:nowrap";
10
+ function renderInspector(snapshot, opts = {}) {
11
+ const digits = opts.digits ?? 3;
12
+ const n = (x) => x.toFixed(digits);
13
+ const time = (t) => t === null ? "\u2013" : new Date(t).toISOString();
14
+ const title = escapeHtml(opts.title ?? "oracular");
15
+ const parts = [
16
+ `<div style="${BOX}">`,
17
+ `<div><strong>${title}</strong> <span style="color:#656d76">at ${time(snapshot.at)}</span></div>`
18
+ ];
19
+ for (const { intent, prediction, reveal, explanation } of snapshot.intents) {
20
+ const gate = reveal ? '<span style="color:#1a7f37;font-weight:bold">reveal</span>' : prediction.suppressedUntil !== null ? `<span style="color:#9a6700">suppressed until ${time(prediction.suppressedUntil)}</span>` : '<span style="color:#656d76">no reveal</span>';
21
+ parts.push(
22
+ `<div style="margin-top:8px"><strong>${escapeHtml(intent)}</strong> p ${n(prediction.p)} \xB7 lower ${n(prediction.lower)} \xB7 sd ${n(prediction.sd)} \xB7 evidence ${n(prediction.evidence)} \xB7 ${gate}</div>`
23
+ );
24
+ if (explanation.length === 0) {
25
+ parts.push('<div style="color:#656d76">no signals declared</div>');
26
+ continue;
27
+ }
28
+ parts.push(`<table style="${TABLE}"><thead><tr>`);
29
+ parts.push(`<th style="${HEAD};text-align:left">signal</th>`);
30
+ for (const h of ["holds", "impact on p", "initial", "learned", "effective", "hits / misses"]) {
31
+ parts.push(`<th style="${HEAD}">${h}</th>`);
32
+ }
33
+ parts.push("</tr></thead><tbody>");
34
+ for (const row of explanation) {
35
+ const dim = row.contribution === 0 ? ";color:#8c959f" : "";
36
+ parts.push(
37
+ `<tr><td style="${NAME}${dim}">${escapeHtml(row.signal)}</td>`,
38
+ `<td style="${CELL}${dim}">${n(row.contribution)}</td>`,
39
+ `<td style="${CELL}${dim}">${row.impact >= 0 ? "+" : ""}${n(row.impact)}</td>`,
40
+ `<td style="${CELL}${dim}">${n(row.initialWeight)}</td>`,
41
+ `<td style="${CELL}${dim}">${n(row.learnedWeight)}</td>`,
42
+ `<td style="${CELL}${dim}">${n(row.effectiveWeight)}</td>`,
43
+ `<td style="${CELL}${dim}">${n(row.hits)} / ${n(row.misses)}</td></tr>`
44
+ );
45
+ }
46
+ parts.push("</tbody></table>");
47
+ }
48
+ parts.push("</div>");
49
+ return parts.join("");
50
+ }
51
+ function mountInspector(container, read, opts = {}) {
52
+ const update = () => {
53
+ container.innerHTML = renderInspector(read(), opts);
54
+ };
55
+ update();
56
+ return {
57
+ update,
58
+ unmount: () => {
59
+ container.innerHTML = "";
60
+ }
61
+ };
62
+ }
63
+
64
+ exports.mountInspector = mountInspector;
65
+ exports.renderInspector = renderInspector;
66
+ //# sourceMappingURL=inspect.cjs.map
67
+ //# sourceMappingURL=inspect.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/inspect.ts"],"names":[],"mappings":"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","file":"inspect.cjs","sourcesContent":["import type { Snapshot } from './types';\n\nexport type { IntentSnapshot, Snapshot } from './types';\n\n/** Options for {@link renderInspector}. */\nexport interface RenderOptions {\n /** Heading above the tables. Default \"oracular\". */\n title?: string;\n /** Decimal places for probabilities and weights. Default 3. */\n digits?: number;\n}\n\nconst escapeHtml = (text: string): string =>\n text.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n\nconst BOX =\n 'font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#1f2328;background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:10px 12px;max-width:100%;overflow:auto';\nconst TABLE = 'border-collapse:collapse;margin:6px 0 10px';\nconst CELL = 'padding:2px 10px 2px 0;text-align:right;white-space:nowrap';\nconst HEAD = `${CELL};color:#656d76;font-weight:normal;border-bottom:1px solid #d8dee4`;\nconst NAME = 'padding:2px 10px 2px 0;text-align:left;white-space:nowrap';\n\n/**\n * A snapshot as an HTML fragment: for each intent, its prediction and its gate in one line, then\n * one row per signal with the contribution it holds, its impact on p, the initial, learned and\n * effective weights, and the hit/miss record. Inline styles, no script, every string escaped, so\n * the fragment can go into any element's `innerHTML` or into a file. Pure.\n *\n * @param snapshot - From {@link inspect}.\n * @param opts - `title` and `digits`.\n * @returns An HTML string.\n * @example\n * ```ts\n * panel.innerHTML = renderInspector(inspect(state, Date.now()));\n * ```\n */\nexport function renderInspector(snapshot: Snapshot, opts: RenderOptions = {}): string {\n const digits = opts.digits ?? 3;\n const n = (x: number) => x.toFixed(digits);\n const time = (t: number | null) => (t === null ? '–' : new Date(t).toISOString());\n const title = escapeHtml(opts.title ?? 'oracular');\n const parts: string[] = [\n `<div style=\"${BOX}\">`,\n `<div><strong>${title}</strong> <span style=\"color:#656d76\">at ${time(snapshot.at)}</span></div>`,\n ];\n for (const { intent, prediction, reveal, explanation } of snapshot.intents) {\n const gate = reveal\n ? '<span style=\"color:#1a7f37;font-weight:bold\">reveal</span>'\n : prediction.suppressedUntil !== null\n ? `<span style=\"color:#9a6700\">suppressed until ${time(prediction.suppressedUntil)}</span>`\n : '<span style=\"color:#656d76\">no reveal</span>';\n parts.push(\n `<div style=\"margin-top:8px\"><strong>${escapeHtml(intent)}</strong> p ${n(prediction.p)} · lower ${n(prediction.lower)} · sd ${n(prediction.sd)} · evidence ${n(prediction.evidence)} · ${gate}</div>`,\n );\n if (explanation.length === 0) {\n parts.push('<div style=\"color:#656d76\">no signals declared</div>');\n continue;\n }\n parts.push(`<table style=\"${TABLE}\"><thead><tr>`);\n parts.push(`<th style=\"${HEAD};text-align:left\">signal</th>`);\n for (const h of ['holds', 'impact on p', 'initial', 'learned', 'effective', 'hits / misses']) {\n parts.push(`<th style=\"${HEAD}\">${h}</th>`);\n }\n parts.push('</tr></thead><tbody>');\n for (const row of explanation) {\n const dim = row.contribution === 0 ? ';color:#8c959f' : '';\n parts.push(\n `<tr><td style=\"${NAME}${dim}\">${escapeHtml(row.signal)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.contribution)}</td>`,\n `<td style=\"${CELL}${dim}\">${row.impact >= 0 ? '+' : ''}${n(row.impact)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.initialWeight)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.learnedWeight)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.effectiveWeight)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.hits)} / ${n(row.misses)}</td></tr>`,\n );\n }\n parts.push('</tbody></table>');\n }\n parts.push('</div>');\n return parts.join('');\n}\n\n/** What {@link mountInspector} returns. */\nexport interface MountedInspector {\n /** Re-reads the snapshot and re-renders. Call it after every `observe`, or from a timer you own. */\n update: () => void;\n /** Empties the container. */\n unmount: () => void;\n}\n\n/**\n * Renders a live inspector into a container and keeps it current on demand. The container is\n * anything with an `innerHTML` property, so a DOM element; the library itself touches no DOM API\n * and sets no timer. Your app decides when to refresh: after each `observe`, or on an interval.\n *\n * @param container - The element to render into. Only its `innerHTML` is written.\n * @param read - Returns the snapshot to show, typically `() => inspect(state, Date.now())`.\n * @param opts - Passed to {@link renderInspector}.\n * @returns `update` to re-render and `unmount` to clear the container. The first render happens\n * at once.\n * @example\n * ```ts\n * const panel = mountInspector(document.querySelector('#oracular')!, () => inspect(state, Date.now()));\n * state = observe(state, event);\n * panel.update();\n * ```\n */\nexport function mountInspector(\n container: { innerHTML: string },\n read: () => Snapshot,\n opts: RenderOptions = {},\n): MountedInspector {\n const update = () => {\n container.innerHTML = renderInspector(read(), opts);\n };\n update();\n return {\n update,\n unmount: () => {\n container.innerHTML = '';\n },\n };\n}\n"]}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * What `predict` returns for one intent at one moment.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * const { p, lower, confidence, revealed, suppressedUntil } = predict(state, 'upgrade', now);
7
+ * ```
8
+ */
9
+ interface Prediction {
10
+ intent: string;
11
+ /** Mean of the Beta distribution: α / (α + β). */
12
+ p: number;
13
+ /** Standard deviation of the Beta distribution. */
14
+ sd: number;
15
+ /** p − z·sd, clamped to [0, 1]. This is what `shouldReveal` compares to the threshold. */
16
+ lower: number;
17
+ /** 1 − 2·sd, clamped to [0, 1]. A heuristic: 1 when the estimate is sharp, 0 when it is as vague as possible. */
18
+ confidence: number;
19
+ /** α + β: total pseudo-observations, prior strength included. */
20
+ evidence: number;
21
+ /** True while inside the cooldown that follows `markRevealed`. */
22
+ revealed: boolean;
23
+ revealedAt: number | null;
24
+ /**
25
+ * When the current suppression ends: the later of the reveal cooldown (after `markRevealed`)
26
+ * and the outcome cooldown (after the outcome event) still running at `at`, or null when
27
+ * neither is. `shouldReveal` is false while this is non-null.
28
+ */
29
+ suppressedUntil: number | null;
30
+ }
31
+ /**
32
+ * One row of `explain`: what a single signal is doing to an intent right now, and what has been
33
+ * learned about it.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const [strongest] = explain(state, 'upgrade', now);
38
+ * strongest?.signal; // the signal with the largest |impact|
39
+ * ```
40
+ */
41
+ interface SignalExplanation {
42
+ signal: string;
43
+ /** Signed pseudo-counts this signal currently holds: positive in α, negative in β, after decay. */
44
+ contribution: number;
45
+ /** p minus what p would be without this signal's contribution. */
46
+ impact: number;
47
+ /** The developer's weight. */
48
+ initialWeight: number;
49
+ /** What this user's history alone says the weight should be. */
50
+ learnedWeight: number;
51
+ /**
52
+ * The blend of initial and learned weight: what the signal adds the first time it occurs in an
53
+ * episode. Repeats within the episode add less (see {@link IntentConfig.saturate}).
54
+ */
55
+ effectiveWeight: number;
56
+ hits: number;
57
+ misses: number;
58
+ }
59
+ /**
60
+ * What `evaluate` returns: the same values `predict`, `shouldReveal` and `explain` would give
61
+ * for the same inputs, computed from a single decay pass.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const { prediction, reveal, explanation } = evaluate(state, 'upgrade', Date.now());
66
+ * ```
67
+ */
68
+ interface Evaluation {
69
+ prediction: Prediction;
70
+ reveal: boolean;
71
+ explanation: SignalExplanation[];
72
+ }
73
+ /** One intent in a {@link Snapshot}: its name and what {@link evaluate} says about it. */
74
+ interface IntentSnapshot extends Evaluation {
75
+ intent: string;
76
+ }
77
+ /**
78
+ * Every intent's prediction, gate and explanation at one moment, as {@link inspect} returns it:
79
+ * what a debug panel shows, what a test asserts on, what `console.table` prints.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const { intents } = inspect(state, Date.now());
84
+ * console.table(intents.map((i) => ({ intent: i.intent, p: i.prediction.p, reveal: i.reveal })));
85
+ * ```
86
+ */
87
+ interface Snapshot {
88
+ at: number;
89
+ /** In config order. */
90
+ intents: IntentSnapshot[];
91
+ }
92
+
93
+ /** Options for {@link renderInspector}. */
94
+ interface RenderOptions {
95
+ /** Heading above the tables. Default "oracular". */
96
+ title?: string;
97
+ /** Decimal places for probabilities and weights. Default 3. */
98
+ digits?: number;
99
+ }
100
+ /**
101
+ * A snapshot as an HTML fragment: for each intent, its prediction and its gate in one line, then
102
+ * one row per signal with the contribution it holds, its impact on p, the initial, learned and
103
+ * effective weights, and the hit/miss record. Inline styles, no script, every string escaped, so
104
+ * the fragment can go into any element's `innerHTML` or into a file. Pure.
105
+ *
106
+ * @param snapshot - From {@link inspect}.
107
+ * @param opts - `title` and `digits`.
108
+ * @returns An HTML string.
109
+ * @example
110
+ * ```ts
111
+ * panel.innerHTML = renderInspector(inspect(state, Date.now()));
112
+ * ```
113
+ */
114
+ declare function renderInspector(snapshot: Snapshot, opts?: RenderOptions): string;
115
+ /** What {@link mountInspector} returns. */
116
+ interface MountedInspector {
117
+ /** Re-reads the snapshot and re-renders. Call it after every `observe`, or from a timer you own. */
118
+ update: () => void;
119
+ /** Empties the container. */
120
+ unmount: () => void;
121
+ }
122
+ /**
123
+ * Renders a live inspector into a container and keeps it current on demand. The container is
124
+ * anything with an `innerHTML` property, so a DOM element; the library itself touches no DOM API
125
+ * and sets no timer. Your app decides when to refresh: after each `observe`, or on an interval.
126
+ *
127
+ * @param container - The element to render into. Only its `innerHTML` is written.
128
+ * @param read - Returns the snapshot to show, typically `() => inspect(state, Date.now())`.
129
+ * @param opts - Passed to {@link renderInspector}.
130
+ * @returns `update` to re-render and `unmount` to clear the container. The first render happens
131
+ * at once.
132
+ * @example
133
+ * ```ts
134
+ * const panel = mountInspector(document.querySelector('#oracular')!, () => inspect(state, Date.now()));
135
+ * state = observe(state, event);
136
+ * panel.update();
137
+ * ```
138
+ */
139
+ declare function mountInspector(container: {
140
+ innerHTML: string;
141
+ }, read: () => Snapshot, opts?: RenderOptions): MountedInspector;
142
+
143
+ export { type IntentSnapshot, type MountedInspector, type RenderOptions, type Snapshot, mountInspector, renderInspector };
@@ -0,0 +1,143 @@
1
+ /**
2
+ * What `predict` returns for one intent at one moment.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * const { p, lower, confidence, revealed, suppressedUntil } = predict(state, 'upgrade', now);
7
+ * ```
8
+ */
9
+ interface Prediction {
10
+ intent: string;
11
+ /** Mean of the Beta distribution: α / (α + β). */
12
+ p: number;
13
+ /** Standard deviation of the Beta distribution. */
14
+ sd: number;
15
+ /** p − z·sd, clamped to [0, 1]. This is what `shouldReveal` compares to the threshold. */
16
+ lower: number;
17
+ /** 1 − 2·sd, clamped to [0, 1]. A heuristic: 1 when the estimate is sharp, 0 when it is as vague as possible. */
18
+ confidence: number;
19
+ /** α + β: total pseudo-observations, prior strength included. */
20
+ evidence: number;
21
+ /** True while inside the cooldown that follows `markRevealed`. */
22
+ revealed: boolean;
23
+ revealedAt: number | null;
24
+ /**
25
+ * When the current suppression ends: the later of the reveal cooldown (after `markRevealed`)
26
+ * and the outcome cooldown (after the outcome event) still running at `at`, or null when
27
+ * neither is. `shouldReveal` is false while this is non-null.
28
+ */
29
+ suppressedUntil: number | null;
30
+ }
31
+ /**
32
+ * One row of `explain`: what a single signal is doing to an intent right now, and what has been
33
+ * learned about it.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const [strongest] = explain(state, 'upgrade', now);
38
+ * strongest?.signal; // the signal with the largest |impact|
39
+ * ```
40
+ */
41
+ interface SignalExplanation {
42
+ signal: string;
43
+ /** Signed pseudo-counts this signal currently holds: positive in α, negative in β, after decay. */
44
+ contribution: number;
45
+ /** p minus what p would be without this signal's contribution. */
46
+ impact: number;
47
+ /** The developer's weight. */
48
+ initialWeight: number;
49
+ /** What this user's history alone says the weight should be. */
50
+ learnedWeight: number;
51
+ /**
52
+ * The blend of initial and learned weight: what the signal adds the first time it occurs in an
53
+ * episode. Repeats within the episode add less (see {@link IntentConfig.saturate}).
54
+ */
55
+ effectiveWeight: number;
56
+ hits: number;
57
+ misses: number;
58
+ }
59
+ /**
60
+ * What `evaluate` returns: the same values `predict`, `shouldReveal` and `explain` would give
61
+ * for the same inputs, computed from a single decay pass.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * const { prediction, reveal, explanation } = evaluate(state, 'upgrade', Date.now());
66
+ * ```
67
+ */
68
+ interface Evaluation {
69
+ prediction: Prediction;
70
+ reveal: boolean;
71
+ explanation: SignalExplanation[];
72
+ }
73
+ /** One intent in a {@link Snapshot}: its name and what {@link evaluate} says about it. */
74
+ interface IntentSnapshot extends Evaluation {
75
+ intent: string;
76
+ }
77
+ /**
78
+ * Every intent's prediction, gate and explanation at one moment, as {@link inspect} returns it:
79
+ * what a debug panel shows, what a test asserts on, what `console.table` prints.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const { intents } = inspect(state, Date.now());
84
+ * console.table(intents.map((i) => ({ intent: i.intent, p: i.prediction.p, reveal: i.reveal })));
85
+ * ```
86
+ */
87
+ interface Snapshot {
88
+ at: number;
89
+ /** In config order. */
90
+ intents: IntentSnapshot[];
91
+ }
92
+
93
+ /** Options for {@link renderInspector}. */
94
+ interface RenderOptions {
95
+ /** Heading above the tables. Default "oracular". */
96
+ title?: string;
97
+ /** Decimal places for probabilities and weights. Default 3. */
98
+ digits?: number;
99
+ }
100
+ /**
101
+ * A snapshot as an HTML fragment: for each intent, its prediction and its gate in one line, then
102
+ * one row per signal with the contribution it holds, its impact on p, the initial, learned and
103
+ * effective weights, and the hit/miss record. Inline styles, no script, every string escaped, so
104
+ * the fragment can go into any element's `innerHTML` or into a file. Pure.
105
+ *
106
+ * @param snapshot - From {@link inspect}.
107
+ * @param opts - `title` and `digits`.
108
+ * @returns An HTML string.
109
+ * @example
110
+ * ```ts
111
+ * panel.innerHTML = renderInspector(inspect(state, Date.now()));
112
+ * ```
113
+ */
114
+ declare function renderInspector(snapshot: Snapshot, opts?: RenderOptions): string;
115
+ /** What {@link mountInspector} returns. */
116
+ interface MountedInspector {
117
+ /** Re-reads the snapshot and re-renders. Call it after every `observe`, or from a timer you own. */
118
+ update: () => void;
119
+ /** Empties the container. */
120
+ unmount: () => void;
121
+ }
122
+ /**
123
+ * Renders a live inspector into a container and keeps it current on demand. The container is
124
+ * anything with an `innerHTML` property, so a DOM element; the library itself touches no DOM API
125
+ * and sets no timer. Your app decides when to refresh: after each `observe`, or on an interval.
126
+ *
127
+ * @param container - The element to render into. Only its `innerHTML` is written.
128
+ * @param read - Returns the snapshot to show, typically `() => inspect(state, Date.now())`.
129
+ * @param opts - Passed to {@link renderInspector}.
130
+ * @returns `update` to re-render and `unmount` to clear the container. The first render happens
131
+ * at once.
132
+ * @example
133
+ * ```ts
134
+ * const panel = mountInspector(document.querySelector('#oracular')!, () => inspect(state, Date.now()));
135
+ * state = observe(state, event);
136
+ * panel.update();
137
+ * ```
138
+ */
139
+ declare function mountInspector(container: {
140
+ innerHTML: string;
141
+ }, read: () => Snapshot, opts?: RenderOptions): MountedInspector;
142
+
143
+ export { type IntentSnapshot, type MountedInspector, type RenderOptions, type Snapshot, mountInspector, renderInspector };
@@ -0,0 +1,64 @@
1
+ // src/inspect.ts
2
+ var escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
3
+ var BOX = "font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#1f2328;background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:10px 12px;max-width:100%;overflow:auto";
4
+ var TABLE = "border-collapse:collapse;margin:6px 0 10px";
5
+ var CELL = "padding:2px 10px 2px 0;text-align:right;white-space:nowrap";
6
+ var HEAD = `${CELL};color:#656d76;font-weight:normal;border-bottom:1px solid #d8dee4`;
7
+ var NAME = "padding:2px 10px 2px 0;text-align:left;white-space:nowrap";
8
+ function renderInspector(snapshot, opts = {}) {
9
+ const digits = opts.digits ?? 3;
10
+ const n = (x) => x.toFixed(digits);
11
+ const time = (t) => t === null ? "\u2013" : new Date(t).toISOString();
12
+ const title = escapeHtml(opts.title ?? "oracular");
13
+ const parts = [
14
+ `<div style="${BOX}">`,
15
+ `<div><strong>${title}</strong> <span style="color:#656d76">at ${time(snapshot.at)}</span></div>`
16
+ ];
17
+ for (const { intent, prediction, reveal, explanation } of snapshot.intents) {
18
+ const gate = reveal ? '<span style="color:#1a7f37;font-weight:bold">reveal</span>' : prediction.suppressedUntil !== null ? `<span style="color:#9a6700">suppressed until ${time(prediction.suppressedUntil)}</span>` : '<span style="color:#656d76">no reveal</span>';
19
+ parts.push(
20
+ `<div style="margin-top:8px"><strong>${escapeHtml(intent)}</strong> p ${n(prediction.p)} \xB7 lower ${n(prediction.lower)} \xB7 sd ${n(prediction.sd)} \xB7 evidence ${n(prediction.evidence)} \xB7 ${gate}</div>`
21
+ );
22
+ if (explanation.length === 0) {
23
+ parts.push('<div style="color:#656d76">no signals declared</div>');
24
+ continue;
25
+ }
26
+ parts.push(`<table style="${TABLE}"><thead><tr>`);
27
+ parts.push(`<th style="${HEAD};text-align:left">signal</th>`);
28
+ for (const h of ["holds", "impact on p", "initial", "learned", "effective", "hits / misses"]) {
29
+ parts.push(`<th style="${HEAD}">${h}</th>`);
30
+ }
31
+ parts.push("</tr></thead><tbody>");
32
+ for (const row of explanation) {
33
+ const dim = row.contribution === 0 ? ";color:#8c959f" : "";
34
+ parts.push(
35
+ `<tr><td style="${NAME}${dim}">${escapeHtml(row.signal)}</td>`,
36
+ `<td style="${CELL}${dim}">${n(row.contribution)}</td>`,
37
+ `<td style="${CELL}${dim}">${row.impact >= 0 ? "+" : ""}${n(row.impact)}</td>`,
38
+ `<td style="${CELL}${dim}">${n(row.initialWeight)}</td>`,
39
+ `<td style="${CELL}${dim}">${n(row.learnedWeight)}</td>`,
40
+ `<td style="${CELL}${dim}">${n(row.effectiveWeight)}</td>`,
41
+ `<td style="${CELL}${dim}">${n(row.hits)} / ${n(row.misses)}</td></tr>`
42
+ );
43
+ }
44
+ parts.push("</tbody></table>");
45
+ }
46
+ parts.push("</div>");
47
+ return parts.join("");
48
+ }
49
+ function mountInspector(container, read, opts = {}) {
50
+ const update = () => {
51
+ container.innerHTML = renderInspector(read(), opts);
52
+ };
53
+ update();
54
+ return {
55
+ update,
56
+ unmount: () => {
57
+ container.innerHTML = "";
58
+ }
59
+ };
60
+ }
61
+
62
+ export { mountInspector, renderInspector };
63
+ //# sourceMappingURL=inspect.js.map
64
+ //# sourceMappingURL=inspect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/inspect.ts"],"names":[],"mappings":";AAYA,IAAM,UAAA,GAAa,CAAC,IAAA,KAClB,IAAA,CAAK,OAAA,CAAQ,UAAA,EAAY,CAAC,CAAA,KAAM,CAAA,EAAA,EAAK,CAAA,CAAE,UAAA,CAAW,CAAC,CAAC,CAAA,CAAA,CAAG,CAAA;AAEzD,IAAM,GAAA,GACJ,oLAAA;AACF,IAAM,KAAA,GAAQ,4CAAA;AACd,IAAM,IAAA,GAAO,4DAAA;AACb,IAAM,IAAA,GAAO,GAAG,IAAI,CAAA,iEAAA,CAAA;AACpB,IAAM,IAAA,GAAO,2DAAA;AAgBN,SAAS,eAAA,CAAgB,QAAA,EAAoB,IAAA,GAAsB,EAAC,EAAW;AACpF,EAAA,MAAM,MAAA,GAAS,KAAK,MAAA,IAAU,CAAA;AAC9B,EAAA,MAAM,CAAA,GAAI,CAAC,CAAA,KAAc,CAAA,CAAE,QAAQ,MAAM,CAAA;AACzC,EAAA,MAAM,IAAA,GAAO,CAAC,CAAA,KAAsB,CAAA,KAAM,IAAA,GAAO,WAAM,IAAI,IAAA,CAAK,CAAC,CAAA,CAAE,WAAA,EAAY;AAC/E,EAAA,MAAM,KAAA,GAAQ,UAAA,CAAW,IAAA,CAAK,KAAA,IAAS,UAAU,CAAA;AACjD,EAAA,MAAM,KAAA,GAAkB;AAAA,IACtB,eAAe,GAAG,CAAA,EAAA,CAAA;AAAA,IAClB,gBAAgB,KAAK,CAAA,yCAAA,EAA4C,IAAA,CAAK,QAAA,CAAS,EAAE,CAAC,CAAA,aAAA;AAAA,GACpF;AACA,EAAA,KAAA,MAAW,EAAE,MAAA,EAAQ,UAAA,EAAY,QAAQ,WAAA,EAAY,IAAK,SAAS,OAAA,EAAS;AAC1E,IAAA,MAAM,IAAA,GAAO,MAAA,GACT,4DAAA,GACA,UAAA,CAAW,eAAA,KAAoB,IAAA,GAC7B,CAAA,6CAAA,EAAgD,IAAA,CAAK,UAAA,CAAW,eAAe,CAAC,CAAA,OAAA,CAAA,GAChF,8CAAA;AACN,IAAA,KAAA,CAAM,IAAA;AAAA,MACJ,CAAA,oCAAA,EAAuC,UAAA,CAAW,MAAM,CAAC,CAAA,YAAA,EAAe,EAAE,UAAA,CAAW,CAAC,CAAC,CAAA,YAAA,EAAY,CAAA,CAAE,UAAA,CAAW,KAAK,CAAC,CAAA,SAAA,EAAS,CAAA,CAAE,UAAA,CAAW,EAAE,CAAC,CAAA,eAAA,EAAe,CAAA,CAAE,UAAA,CAAW,QAAQ,CAAC,CAAA,MAAA,EAAM,IAAI,CAAA,MAAA;AAAA,KAChM;AACA,IAAA,IAAI,WAAA,CAAY,WAAW,CAAA,EAAG;AAC5B,MAAA,KAAA,CAAM,KAAK,sDAAsD,CAAA;AACjE,MAAA;AAAA,IACF;AACA,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,cAAA,EAAiB,KAAK,CAAA,aAAA,CAAe,CAAA;AAChD,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,WAAA,EAAc,IAAI,CAAA,6BAAA,CAA+B,CAAA;AAC5D,IAAA,KAAA,MAAW,CAAA,IAAK,CAAC,OAAA,EAAS,aAAA,EAAe,WAAW,SAAA,EAAW,WAAA,EAAa,eAAe,CAAA,EAAG;AAC5F,MAAA,KAAA,CAAM,IAAA,CAAK,CAAA,WAAA,EAAc,IAAI,CAAA,EAAA,EAAK,CAAC,CAAA,KAAA,CAAO,CAAA;AAAA,IAC5C;AACA,IAAA,KAAA,CAAM,KAAK,sBAAsB,CAAA;AACjC,IAAA,KAAA,MAAW,OAAO,WAAA,EAAa;AAC7B,MAAA,MAAM,GAAA,GAAM,GAAA,CAAI,YAAA,KAAiB,CAAA,GAAI,gBAAA,GAAmB,EAAA;AACxD,MAAA,KAAA,CAAM,IAAA;AAAA,QACJ,CAAA,eAAA,EAAkB,IAAI,CAAA,EAAG,GAAG,KAAK,UAAA,CAAW,GAAA,CAAI,MAAM,CAAC,CAAA,KAAA,CAAA;AAAA,QACvD,CAAA,WAAA,EAAc,IAAI,CAAA,EAAG,GAAG,KAAK,CAAA,CAAE,GAAA,CAAI,YAAY,CAAC,CAAA,KAAA,CAAA;AAAA,QAChD,CAAA,WAAA,EAAc,IAAI,CAAA,EAAG,GAAG,KAAK,GAAA,CAAI,MAAA,IAAU,CAAA,GAAI,GAAA,GAAM,EAAE,CAAA,EAAG,CAAA,CAAE,GAAA,CAAI,MAAM,CAAC,CAAA,KAAA,CAAA;AAAA,QACvE,CAAA,WAAA,EAAc,IAAI,CAAA,EAAG,GAAG,KAAK,CAAA,CAAE,GAAA,CAAI,aAAa,CAAC,CAAA,KAAA,CAAA;AAAA,QACjD,CAAA,WAAA,EAAc,IAAI,CAAA,EAAG,GAAG,KAAK,CAAA,CAAE,GAAA,CAAI,aAAa,CAAC,CAAA,KAAA,CAAA;AAAA,QACjD,CAAA,WAAA,EAAc,IAAI,CAAA,EAAG,GAAG,KAAK,CAAA,CAAE,GAAA,CAAI,eAAe,CAAC,CAAA,KAAA,CAAA;AAAA,QACnD,CAAA,WAAA,EAAc,IAAI,CAAA,EAAG,GAAG,CAAA,EAAA,EAAK,CAAA,CAAE,GAAA,CAAI,IAAI,CAAC,CAAA,GAAA,EAAM,CAAA,CAAE,GAAA,CAAI,MAAM,CAAC,CAAA,UAAA;AAAA,OAC7D;AAAA,IACF;AACA,IAAA,KAAA,CAAM,KAAK,kBAAkB,CAAA;AAAA,EAC/B;AACA,EAAA,KAAA,CAAM,KAAK,QAAQ,CAAA;AACnB,EAAA,OAAO,KAAA,CAAM,KAAK,EAAE,CAAA;AACtB;AA2BO,SAAS,cAAA,CACd,SAAA,EACA,IAAA,EACA,IAAA,GAAsB,EAAC,EACL;AAClB,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,SAAA,CAAU,SAAA,GAAY,eAAA,CAAgB,IAAA,EAAK,EAAG,IAAI,CAAA;AAAA,EACpD,CAAA;AACA,EAAA,MAAA,EAAO;AACP,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,SAAS,MAAM;AACb,MAAA,SAAA,CAAU,SAAA,GAAY,EAAA;AAAA,IACxB;AAAA,GACF;AACF","file":"inspect.js","sourcesContent":["import type { Snapshot } from './types';\n\nexport type { IntentSnapshot, Snapshot } from './types';\n\n/** Options for {@link renderInspector}. */\nexport interface RenderOptions {\n /** Heading above the tables. Default \"oracular\". */\n title?: string;\n /** Decimal places for probabilities and weights. Default 3. */\n digits?: number;\n}\n\nconst escapeHtml = (text: string): string =>\n text.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n\nconst BOX =\n 'font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#1f2328;background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:10px 12px;max-width:100%;overflow:auto';\nconst TABLE = 'border-collapse:collapse;margin:6px 0 10px';\nconst CELL = 'padding:2px 10px 2px 0;text-align:right;white-space:nowrap';\nconst HEAD = `${CELL};color:#656d76;font-weight:normal;border-bottom:1px solid #d8dee4`;\nconst NAME = 'padding:2px 10px 2px 0;text-align:left;white-space:nowrap';\n\n/**\n * A snapshot as an HTML fragment: for each intent, its prediction and its gate in one line, then\n * one row per signal with the contribution it holds, its impact on p, the initial, learned and\n * effective weights, and the hit/miss record. Inline styles, no script, every string escaped, so\n * the fragment can go into any element's `innerHTML` or into a file. Pure.\n *\n * @param snapshot - From {@link inspect}.\n * @param opts - `title` and `digits`.\n * @returns An HTML string.\n * @example\n * ```ts\n * panel.innerHTML = renderInspector(inspect(state, Date.now()));\n * ```\n */\nexport function renderInspector(snapshot: Snapshot, opts: RenderOptions = {}): string {\n const digits = opts.digits ?? 3;\n const n = (x: number) => x.toFixed(digits);\n const time = (t: number | null) => (t === null ? '–' : new Date(t).toISOString());\n const title = escapeHtml(opts.title ?? 'oracular');\n const parts: string[] = [\n `<div style=\"${BOX}\">`,\n `<div><strong>${title}</strong> <span style=\"color:#656d76\">at ${time(snapshot.at)}</span></div>`,\n ];\n for (const { intent, prediction, reveal, explanation } of snapshot.intents) {\n const gate = reveal\n ? '<span style=\"color:#1a7f37;font-weight:bold\">reveal</span>'\n : prediction.suppressedUntil !== null\n ? `<span style=\"color:#9a6700\">suppressed until ${time(prediction.suppressedUntil)}</span>`\n : '<span style=\"color:#656d76\">no reveal</span>';\n parts.push(\n `<div style=\"margin-top:8px\"><strong>${escapeHtml(intent)}</strong> p ${n(prediction.p)} · lower ${n(prediction.lower)} · sd ${n(prediction.sd)} · evidence ${n(prediction.evidence)} · ${gate}</div>`,\n );\n if (explanation.length === 0) {\n parts.push('<div style=\"color:#656d76\">no signals declared</div>');\n continue;\n }\n parts.push(`<table style=\"${TABLE}\"><thead><tr>`);\n parts.push(`<th style=\"${HEAD};text-align:left\">signal</th>`);\n for (const h of ['holds', 'impact on p', 'initial', 'learned', 'effective', 'hits / misses']) {\n parts.push(`<th style=\"${HEAD}\">${h}</th>`);\n }\n parts.push('</tr></thead><tbody>');\n for (const row of explanation) {\n const dim = row.contribution === 0 ? ';color:#8c959f' : '';\n parts.push(\n `<tr><td style=\"${NAME}${dim}\">${escapeHtml(row.signal)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.contribution)}</td>`,\n `<td style=\"${CELL}${dim}\">${row.impact >= 0 ? '+' : ''}${n(row.impact)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.initialWeight)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.learnedWeight)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.effectiveWeight)}</td>`,\n `<td style=\"${CELL}${dim}\">${n(row.hits)} / ${n(row.misses)}</td></tr>`,\n );\n }\n parts.push('</tbody></table>');\n }\n parts.push('</div>');\n return parts.join('');\n}\n\n/** What {@link mountInspector} returns. */\nexport interface MountedInspector {\n /** Re-reads the snapshot and re-renders. Call it after every `observe`, or from a timer you own. */\n update: () => void;\n /** Empties the container. */\n unmount: () => void;\n}\n\n/**\n * Renders a live inspector into a container and keeps it current on demand. The container is\n * anything with an `innerHTML` property, so a DOM element; the library itself touches no DOM API\n * and sets no timer. Your app decides when to refresh: after each `observe`, or on an interval.\n *\n * @param container - The element to render into. Only its `innerHTML` is written.\n * @param read - Returns the snapshot to show, typically `() => inspect(state, Date.now())`.\n * @param opts - Passed to {@link renderInspector}.\n * @returns `update` to re-render and `unmount` to clear the container. The first render happens\n * at once.\n * @example\n * ```ts\n * const panel = mountInspector(document.querySelector('#oracular')!, () => inspect(state, Date.now()));\n * state = observe(state, event);\n * panel.update();\n * ```\n */\nexport function mountInspector(\n container: { innerHTML: string },\n read: () => Snapshot,\n opts: RenderOptions = {},\n): MountedInspector {\n const update = () => {\n container.innerHTML = renderInspector(read(), opts);\n };\n update();\n return {\n update,\n unmount: () => {\n container.innerHTML = '';\n },\n };\n}\n"]}
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ // src/units.ts
4
+ var ms = {
5
+ seconds: (n) => n * 1e3,
6
+ minutes: (n) => n * 6e4,
7
+ hours: (n) => n * 36e5,
8
+ days: (n) => n * 864e5,
9
+ weeks: (n) => n * 6048e5
10
+ };
11
+
12
+ // src/presets.ts
13
+ var willBuy = {
14
+ prior: 0.048,
15
+ priorStrength: 4,
16
+ halfLife: ms.hours(1),
17
+ signals: { "cart.added": 2.3 },
18
+ outcome: "checkout.completed",
19
+ reveal: { cooldown: ms.days(1) }
20
+ };
21
+ var upgrade = {
22
+ prior: 0.05,
23
+ priorStrength: 10,
24
+ halfLife: ms.days(7),
25
+ signals: { "pricing.viewed": 3, "limit.hit": 2, "export.clicked": 1 },
26
+ outcome: "upgrade.completed"
27
+ };
28
+ var stuck = {
29
+ prior: 0.1,
30
+ priorStrength: 5,
31
+ halfLife: ms.minutes(10),
32
+ signals: { "editor.undo": 1, "search.empty": 2, "help.opened": 3, "task.completed": -4 },
33
+ outcome: "help.article.read",
34
+ saturate: false,
35
+ reveal: { threshold: 0.6, cooldown: ms.hours(1) }
36
+ };
37
+ var churn = {
38
+ prior: 0.08,
39
+ priorStrength: 20,
40
+ halfLife: ms.days(14),
41
+ signals: {
42
+ "session.short": 1,
43
+ "notifications.disabled": 3,
44
+ "export.all": 4,
45
+ "feature.new.used": -2
46
+ },
47
+ outcome: "account.cancelled",
48
+ reveal: { threshold: 0.5, z: 1.64 }
49
+ };
50
+ var presets = {
51
+ willBuy,
52
+ upgrade,
53
+ stuck,
54
+ churn
55
+ };
56
+ var presetSources = {
57
+ willBuy: {
58
+ kind: "tuned",
59
+ note: "oracular tune on a public e-commerce clickstream: one shop, May to September 2015, 2.7 million events, 4.8% of sessions ending with an order; fitted October 2026, the fitted 0.0477 and 2.32 rounded",
60
+ episodes: 181080,
61
+ installs: 93537
62
+ },
63
+ upgrade: {
64
+ kind: "hand-written",
65
+ note: "the quickstart config from the documentation; no log behind it",
66
+ episodes: null,
67
+ installs: null
68
+ },
69
+ stuck: {
70
+ kind: "hand-written",
71
+ note: "the onboarding-tip example from the documentation; no log behind it",
72
+ episodes: null,
73
+ installs: null
74
+ },
75
+ churn: {
76
+ kind: "hand-written",
77
+ note: "the churn-save example from the documentation; no log behind it",
78
+ episodes: null,
79
+ installs: null
80
+ }
81
+ };
82
+
83
+ exports.presetSources = presetSources;
84
+ exports.presets = presets;
85
+ //# sourceMappingURL=presets.cjs.map
86
+ //# sourceMappingURL=presets.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/units.ts","../src/presets.ts"],"names":[],"mappings":"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","file":"presets.cjs","sourcesContent":["/** The units of {@link ms}: each takes a count and returns milliseconds. */\nexport interface Units {\n /** `n` seconds in milliseconds: `n × 1000`. */\n readonly seconds: (n: number) => number;\n /** `n` minutes in milliseconds: `n × 60 000`. */\n readonly minutes: (n: number) => number;\n /** `n` hours in milliseconds: `n × 3 600 000`. */\n readonly hours: (n: number) => number;\n /** `n` days in milliseconds: `n × 86 400 000`. */\n readonly days: (n: number) => number;\n /** `n` weeks in milliseconds: `n × 604 800 000`. */\n readonly weeks: (n: number) => number;\n}\n\n/**\n * Durations in milliseconds, the unit of `Date.now()`: `ms.days(7)` is `604800000`. Plain\n * multiplications and nothing else, for `halfLife`, `cooldown`, `episodeTimeout` and\n * `learning.halfLife` when your timestamps are in milliseconds. If your `at` is in another unit,\n * write durations in that unit instead: the engine only needs the two to agree.\n *\n * @example\n * ```ts\n * const config = {\n * episodeTimeout: ms.minutes(30),\n * intents: {\n * upgrade: {\n * prior: 0.05,\n * priorStrength: 10,\n * halfLife: ms.days(7),\n * signals: { 'pricing.viewed': 3 },\n * reveal: { cooldown: ms.weeks(2) },\n * },\n * },\n * };\n * ```\n */\nexport const ms: Units = {\n seconds: (n) => n * 1_000,\n minutes: (n) => n * 60_000,\n hours: (n) => n * 3_600_000,\n days: (n) => n * 86_400_000,\n weeks: (n) => n * 604_800_000,\n};\n","import type { IntentConfig, PresetName, PresetSource } from './types';\nimport { ms } from './units';\n\nexport type { PresetName, PresetSource } from './types';\n\n/**\n * A visitor about to buy. Tuned on a public e-commerce clickstream of 2.7 million events (see the\n * case study in the guide): one shop, one summer (May to September 2015), 181,080 sessions from\n * 93,537 visitors, 8,628 of them ending with an order, a session conversion of 4.8%; scored on as\n * many visitors the fit never saw. A measured starting point, not a universal number: a shop that\n * converts 1% or 20% of its sessions has a different prior and most likely a different weight.\n * Start here, run in shadow mode, and let `tune` replace these with your own numbers.\n *\n * - `prior` 0.048: the share of sessions that end with an order (fitted 0.0477).\n * - `priorStrength` 4: visitors differ a lot in how often they buy, so one visitor's own history\n * should outweigh the population after a handful of sessions.\n * - `halfLife` one hour: the best point of the grid; buying intent within a visit is a matter of\n * minutes, and tomorrow's visit starts over.\n * - `cart.added` 2.3: its measured lift (fitted 2.32). 58% of sessions with a cart add end with an\n * order, against 0.8% of those without. There is no weight for viewing a product: on that data a\n * view is in 99% of sessions and says nothing about buying. Add one only if your data disagrees.\n * - `cooldown` one day: after an order, or after acting on the intent, stay quiet for a day; the\n * half-life would otherwise make the default cooldown an hour.\n *\n * Rename `cart.added` and `checkout.completed` to your events by spreading.\n */\nconst willBuy: IntentConfig = {\n prior: 0.048,\n priorStrength: 4,\n halfLife: ms.hours(1),\n signals: { 'cart.added': 2.3 },\n outcome: 'checkout.completed',\n reveal: { cooldown: ms.days(1) },\n};\n\n/**\n * A user about to upgrade: the quickstart config, hand-written. Interest in a paid plan builds over\n * days, so the half-life is a week; the prior is a guess at the share of users who upgrade in a\n * week; `priorStrength` 10 means about six full-weight signals across sessions open the default\n * gate; the weights rank the signals by how directly they point at paying. Nobody has measured\n * these against a log.\n */\nconst upgrade: IntentConfig = {\n prior: 0.05,\n priorStrength: 10,\n halfLife: ms.days(7),\n signals: { 'pricing.viewed': 3, 'limit.hit': 2, 'export.clicked': 1 },\n outcome: 'upgrade.completed',\n};\n\n/**\n * A user stuck on a task, hand-written. Being stuck is a state of the current session, so the\n * half-life is ten minutes and the cooldown an hour; `saturate: false` because here repetition is\n * the evidence (the fifth undo in a row says more than the first); `task.completed` is strong\n * evidence against; the gate is raised to 0.6 because an unwanted tip is an interruption. Nobody\n * has measured these against a log.\n */\nconst stuck: IntentConfig = {\n prior: 0.1,\n priorStrength: 5,\n halfLife: ms.minutes(10),\n signals: { 'editor.undo': 1, 'search.empty': 2, 'help.opened': 3, 'task.completed': -4 },\n outcome: 'help.article.read',\n saturate: false,\n reveal: { threshold: 0.6, cooldown: ms.hours(1) },\n};\n\n/**\n * A user about to leave, hand-written. Churn builds over weeks, so the half-life is two weeks and\n * the prior strength 20: it should take a lot to call someone a churn risk. Exporting all their\n * data is the strongest signal; using a new feature counts against. `z` 1.64 asks for a one-sided\n * 95% bound before a save offer, because a discount has a real cost. Nobody has measured these\n * against a log.\n */\nconst churn: IntentConfig = {\n prior: 0.08,\n priorStrength: 20,\n halfLife: ms.days(14),\n signals: {\n 'session.short': 1,\n 'notifications.disabled': 3,\n 'export.all': 4,\n 'feature.new.used': -2,\n },\n outcome: 'account.cancelled',\n reveal: { threshold: 0.5, z: 1.64 },\n};\n\n/**\n * Starting points for four common intents, as plain config objects. Spread one into your config,\n * rename its events to yours, override what you know better; see {@link presetSources} for where\n * each preset's numbers come from.\n *\n * @example\n * ```ts\n * import { presets } from 'oracular/presets';\n *\n * const config = {\n * episodeTimeout: 30 * 60 * 1000,\n * intents: {\n * buy: { ...presets.willBuy, outcome: 'order.placed' },\n * },\n * };\n * ```\n */\nexport const presets: Readonly<Record<PresetName, IntentConfig>> = {\n willBuy,\n upgrade,\n stuck,\n churn,\n};\n\n/**\n * Where each preset's numbers come from. Read it before trusting a preset: a tuned one carries its\n * sample size, a hand-written one carries nothing but reasoning.\n *\n * @example\n * ```ts\n * import { presetSources } from 'oracular/presets';\n * presetSources.willBuy.kind; // 'tuned'\n * ```\n */\nexport const presetSources: Readonly<Record<PresetName, PresetSource>> = {\n willBuy: {\n kind: 'tuned',\n note: 'oracular tune on a public e-commerce clickstream: one shop, May to September 2015, 2.7 million events, 4.8% of sessions ending with an order; fitted October 2026, the fitted 0.0477 and 2.32 rounded',\n episodes: 181_080,\n installs: 93_537,\n },\n upgrade: {\n kind: 'hand-written',\n note: 'the quickstart config from the documentation; no log behind it',\n episodes: null,\n installs: null,\n },\n stuck: {\n kind: 'hand-written',\n note: 'the onboarding-tip example from the documentation; no log behind it',\n episodes: null,\n installs: null,\n },\n churn: {\n kind: 'hand-written',\n note: 'the churn-save example from the documentation; no log behind it',\n episodes: null,\n installs: null,\n },\n};\n"]}