oracular 0.9.0 → 0.11.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 +24 -0
- package/README.md +6 -5
- package/dist/cli.js +63 -30
- package/dist/index.cjs +54 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -7
- package/dist/index.d.ts +16 -7
- package/dist/index.js +54 -32
- package/dist/index.js.map +1 -1
- package/dist/presets.cjs +13 -6
- package/dist/presets.cjs.map +1 -1
- package/dist/presets.d.cts +0 -17
- package/dist/presets.d.ts +0 -17
- package/dist/presets.js +13 -6
- package/dist/presets.js.map +1 -1
- package/package.json +1 -1
- package/schema/config.schema.json +4 -3
package/dist/presets.cjs
CHANGED
|
@@ -47,17 +47,24 @@ var churn = {
|
|
|
47
47
|
outcome: "account.cancelled",
|
|
48
48
|
reveal: { threshold: 0.5, z: 1.64 }
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
function freeze(value) {
|
|
51
|
+
if (typeof value === "object" && value !== null) {
|
|
52
|
+
for (const inner of Object.values(value)) freeze(inner);
|
|
53
|
+
Object.freeze(value);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
var presets = freeze({
|
|
51
58
|
willBuy,
|
|
52
59
|
upgrade,
|
|
53
60
|
stuck,
|
|
54
61
|
churn
|
|
55
|
-
};
|
|
56
|
-
var presetSources = {
|
|
62
|
+
});
|
|
63
|
+
var presetSources = freeze({
|
|
57
64
|
willBuy: {
|
|
58
65
|
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.
|
|
60
|
-
episodes:
|
|
66
|
+
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.0475 and 2.32 rounded",
|
|
67
|
+
episodes: 181055,
|
|
61
68
|
installs: 93537
|
|
62
69
|
},
|
|
63
70
|
upgrade: {
|
|
@@ -78,7 +85,7 @@ var presetSources = {
|
|
|
78
85
|
episodes: null,
|
|
79
86
|
installs: null
|
|
80
87
|
}
|
|
81
|
-
};
|
|
88
|
+
});
|
|
82
89
|
|
|
83
90
|
exports.presetSources = presetSources;
|
|
84
91
|
exports.presets = presets;
|
package/dist/presets.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
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.0475).\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 */\n/** Frozen through and through: a preset is shared, so a change by one caller would reach every other. */\nfunction freeze<T>(value: T): T {\n if (typeof value === 'object' && value !== null) {\n for (const inner of Object.values(value)) freeze(inner);\n Object.freeze(value);\n }\n return value;\n}\n\nexport const presets: Readonly<Record<PresetName, IntentConfig>> = freeze({\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>> = freeze({\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.0475 and 2.32 rounded',\n episodes: 181_055,\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"]}
|
package/dist/presets.d.cts
CHANGED
|
@@ -103,23 +103,6 @@ interface PresetSource {
|
|
|
103
103
|
installs: number | null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
* Starting points for four common intents, as plain config objects. Spread one into your config,
|
|
108
|
-
* rename its events to yours, override what you know better; see {@link presetSources} for where
|
|
109
|
-
* each preset's numbers come from.
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```ts
|
|
113
|
-
* import { presets } from 'oracular/presets';
|
|
114
|
-
*
|
|
115
|
-
* const config = {
|
|
116
|
-
* episodeTimeout: 30 * 60 * 1000,
|
|
117
|
-
* intents: {
|
|
118
|
-
* buy: { ...presets.willBuy, outcome: 'order.placed' },
|
|
119
|
-
* },
|
|
120
|
-
* };
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
106
|
declare const presets: Readonly<Record<PresetName, IntentConfig>>;
|
|
124
107
|
/**
|
|
125
108
|
* Where each preset's numbers come from. Read it before trusting a preset: a tuned one carries its
|
package/dist/presets.d.ts
CHANGED
|
@@ -103,23 +103,6 @@ interface PresetSource {
|
|
|
103
103
|
installs: number | null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
* Starting points for four common intents, as plain config objects. Spread one into your config,
|
|
108
|
-
* rename its events to yours, override what you know better; see {@link presetSources} for where
|
|
109
|
-
* each preset's numbers come from.
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```ts
|
|
113
|
-
* import { presets } from 'oracular/presets';
|
|
114
|
-
*
|
|
115
|
-
* const config = {
|
|
116
|
-
* episodeTimeout: 30 * 60 * 1000,
|
|
117
|
-
* intents: {
|
|
118
|
-
* buy: { ...presets.willBuy, outcome: 'order.placed' },
|
|
119
|
-
* },
|
|
120
|
-
* };
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
106
|
declare const presets: Readonly<Record<PresetName, IntentConfig>>;
|
|
124
107
|
/**
|
|
125
108
|
* Where each preset's numbers come from. Read it before trusting a preset: a tuned one carries its
|
package/dist/presets.js
CHANGED
|
@@ -45,17 +45,24 @@ var churn = {
|
|
|
45
45
|
outcome: "account.cancelled",
|
|
46
46
|
reveal: { threshold: 0.5, z: 1.64 }
|
|
47
47
|
};
|
|
48
|
-
|
|
48
|
+
function freeze(value) {
|
|
49
|
+
if (typeof value === "object" && value !== null) {
|
|
50
|
+
for (const inner of Object.values(value)) freeze(inner);
|
|
51
|
+
Object.freeze(value);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
var presets = freeze({
|
|
49
56
|
willBuy,
|
|
50
57
|
upgrade,
|
|
51
58
|
stuck,
|
|
52
59
|
churn
|
|
53
|
-
};
|
|
54
|
-
var presetSources = {
|
|
60
|
+
});
|
|
61
|
+
var presetSources = freeze({
|
|
55
62
|
willBuy: {
|
|
56
63
|
kind: "tuned",
|
|
57
|
-
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.
|
|
58
|
-
episodes:
|
|
64
|
+
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.0475 and 2.32 rounded",
|
|
65
|
+
episodes: 181055,
|
|
59
66
|
installs: 93537
|
|
60
67
|
},
|
|
61
68
|
upgrade: {
|
|
@@ -76,7 +83,7 @@ var presetSources = {
|
|
|
76
83
|
episodes: null,
|
|
77
84
|
installs: null
|
|
78
85
|
}
|
|
79
|
-
};
|
|
86
|
+
});
|
|
80
87
|
|
|
81
88
|
export { presetSources, presets };
|
|
82
89
|
//# sourceMappingURL=presets.js.map
|
package/dist/presets.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/units.ts","../src/presets.ts"],"names":[],"mappings":";AAoCO,IAAM,EAAA,GAAY;AAAA,EACvB,OAAA,EAAS,CAAC,CAAA,KAAM,CAAA,GAAI,GAAA;AAAA,EACpB,OAAA,EAAS,CAAC,CAAA,KAAM,CAAA,GAAI,GAAA;AAAA,EACpB,KAAA,EAAO,CAAC,CAAA,KAAM,CAAA,GAAI,IAAA;AAAA,EAClB,IAAA,EAAM,CAAC,CAAA,KAAM,CAAA,GAAI,KAAA;AAAA,EACjB,KAAA,EAAO,CAAC,CAAA,KAAM,CAAA,GAAI;AACpB,CAAA;;;AChBA,IAAM,OAAA,GAAwB;AAAA,EAC5B,KAAA,EAAO,KAAA;AAAA,EACP,aAAA,EAAe,CAAA;AAAA,EACf,QAAA,EAAU,EAAA,CAAG,KAAA,CAAM,CAAC,CAAA;AAAA,EACpB,OAAA,EAAS,EAAE,YAAA,EAAc,GAAA,EAAI;AAAA,EAC7B,OAAA,EAAS,oBAAA;AAAA,EACT,QAAQ,EAAE,QAAA,EAAU,EAAA,CAAG,IAAA,CAAK,CAAC,CAAA;AAC/B,CAAA;AASA,IAAM,OAAA,GAAwB;AAAA,EAC5B,KAAA,EAAO,IAAA;AAAA,EACP,aAAA,EAAe,EAAA;AAAA,EACf,QAAA,EAAU,EAAA,CAAG,IAAA,CAAK,CAAC,CAAA;AAAA,EACnB,SAAS,EAAE,gBAAA,EAAkB,GAAG,WAAA,EAAa,CAAA,EAAG,kBAAkB,CAAA,EAAE;AAAA,EACpE,OAAA,EAAS;AACX,CAAA;AASA,IAAM,KAAA,GAAsB;AAAA,EAC1B,KAAA,EAAO,GAAA;AAAA,EACP,aAAA,EAAe,CAAA;AAAA,EACf,QAAA,EAAU,EAAA,CAAG,OAAA,CAAQ,EAAE,CAAA;AAAA,EACvB,OAAA,EAAS,EAAE,aAAA,EAAe,CAAA,EAAG,gBAAgB,CAAA,EAAG,aAAA,EAAe,CAAA,EAAG,gBAAA,EAAkB,EAAA,EAAG;AAAA,EACvF,OAAA,EAAS,mBAAA;AAAA,EACT,QAAA,EAAU,KAAA;AAAA,EACV,MAAA,EAAQ,EAAE,SAAA,EAAW,GAAA,EAAK,UAAU,EAAA,CAAG,KAAA,CAAM,CAAC,CAAA;AAChD,CAAA;AASA,IAAM,KAAA,GAAsB;AAAA,EAC1B,KAAA,EAAO,IAAA;AAAA,EACP,aAAA,EAAe,EAAA;AAAA,EACf,QAAA,EAAU,EAAA,CAAG,IAAA,CAAK,EAAE,CAAA;AAAA,EACpB,OAAA,EAAS;AAAA,IACP,eAAA,EAAiB,CAAA;AAAA,IACjB,wBAAA,EAA0B,CAAA;AAAA,IAC1B,YAAA,EAAc,CAAA;AAAA,IACd,kBAAA,EAAoB;AAAA,GACtB;AAAA,EACA,OAAA,EAAS,mBAAA;AAAA,EACT,MAAA,EAAQ,EAAE,SAAA,EAAW,GAAA,EAAK,GAAG,IAAA;AAC/B,CAAA;AAmBO,IAAM,OAAA,GAAsD;AAAA,EACjE,OAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA;AACF;AAYO,IAAM,aAAA,GAA4D;AAAA,EACvE,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,OAAA;AAAA,IACN,IAAA,EAAM,uMAAA;AAAA,IACN,QAAA,EAAU,MAAA;AAAA,IACV,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,OAAA,EAAS;AAAA,IACP,IAAA,EAAM,cAAA;AAAA,IACN,IAAA,EAAM,gEAAA;AAAA,IACN,QAAA,EAAU,IAAA;AAAA,IACV,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,KAAA,EAAO;AAAA,IACL,IAAA,EAAM,cAAA;AAAA,IACN,IAAA,EAAM,qEAAA;AAAA,IACN,QAAA,EAAU,IAAA;AAAA,IACV,QAAA,EAAU;AAAA,GACZ;AAAA,EACA,KAAA,EAAO;AAAA,IACL,IAAA,EAAM,cAAA;AAAA,IACN,IAAA,EAAM,iEAAA;AAAA,IACN,QAAA,EAAU,IAAA;AAAA,IACV,QAAA,EAAU;AAAA;AAEd","file":"presets.js","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"]}
|
|
1
|
+
{"version":3,"sources":["../src/units.ts","../src/presets.ts"],"names":[],"mappings":"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","file":"presets.js","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.0475).\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 */\n/** Frozen through and through: a preset is shared, so a change by one caller would reach every other. */\nfunction freeze<T>(value: T): T {\n if (typeof value === 'object' && value !== null) {\n for (const inner of Object.values(value)) freeze(inner);\n Object.freeze(value);\n }\n return value;\n}\n\nexport const presets: Readonly<Record<PresetName, IntentConfig>> = freeze({\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>> = freeze({\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.0475 and 2.32 rounded',\n episodes: 181_055,\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"]}
|
package/package.json
CHANGED
|
@@ -107,13 +107,14 @@
|
|
|
107
107
|
"properties": {
|
|
108
108
|
"threshold": {
|
|
109
109
|
"type": "number",
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"description": "Probability the intent must reach. Default 0.5."
|
|
110
|
+
"exclusiveMinimum": 0,
|
|
111
|
+
"exclusiveMaximum": 1,
|
|
112
|
+
"description": "Probability the intent must reach, strictly between 0 and 1. Default 0.5."
|
|
113
113
|
},
|
|
114
114
|
"z": {
|
|
115
115
|
"type": "number",
|
|
116
116
|
"minimum": 0,
|
|
117
|
+
"maximum": 10,
|
|
117
118
|
"description": "How many standard deviations below p must still clear the threshold. Default 1.28."
|
|
118
119
|
},
|
|
119
120
|
"cooldown": {
|