oracular 0.6.0 → 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,129 @@
1
+ /**
2
+ * Options that decide when `shouldReveal` fires and how long cooldowns last. Resolved per call →
3
+ * intent → engine → defaults.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * shouldReveal(state, 'upgrade', Date.now(), { threshold: 0.7, z: 1.64 });
8
+ * ```
9
+ */
10
+ interface RevealOptions {
11
+ /** Probability the intent must reach, in [0, 1]. Default 0.5. */
12
+ threshold?: number;
13
+ /** How many standard deviations below p must still clear the threshold. Default 1.28 (one-sided 90%). */
14
+ z?: number;
15
+ /** How long after `markRevealed` the intent stays revealed and `shouldReveal` is false. Default: the intent's `halfLife`. */
16
+ cooldown?: number;
17
+ }
18
+ /**
19
+ * One thing the user might be about to do, tracked as a Beta(α, β) distribution with
20
+ * α₀ = prior × priorStrength and β₀ = (1 − prior) × priorStrength.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const churn: IntentConfig = {
25
+ * prior: 0.08,
26
+ * priorStrength: 20,
27
+ * halfLife: 14 * 24 * 60 * 60 * 1000,
28
+ * signals: { 'session.short': 1, 'notifications.disabled': 3, 'feature.new.used': -2 },
29
+ * outcome: 'account.cancelled',
30
+ * reveal: { threshold: 0.5, z: 1.64 },
31
+ * };
32
+ * ```
33
+ */
34
+ interface IntentConfig {
35
+ /** Prior probability, strictly between 0 and 1. */
36
+ prior: number;
37
+ /** Strength of the prior as a pseudo-count, greater than 0 and at most 1e6. */
38
+ priorStrength: number;
39
+ /** Half-life of evidence, in the same unit as `at`. Evidence decays back toward the prior. */
40
+ halfLife: number;
41
+ /** Event name → initial weight. Positive is evidence for the intent, negative against. Non-zero, between −1e6 and 1e6. */
42
+ signals: Record<string, number>;
43
+ /** Event name that confirms the intent happened, e.g. `'checkout.completed'`. */
44
+ outcome?: string;
45
+ /** Reset α, β and contributions to the prior when the outcome arrives. Default true. */
46
+ resetOnOutcome?: boolean;
47
+ /**
48
+ * Repeats of a signal within one episode add less: the n-th occurrence adds
49
+ * `w × log2(1 + 1/n)`, so ten occurrences add 3.46 weights, not ten. The count restarts when
50
+ * the episode ends or this intent's outcome arrives. Default: true when `episodeTimeout` is
51
+ * set, false otherwise, because without an idle timeout episodes end only when the app calls
52
+ * `endEpisode`, and if it never does the count would never restart. Set true to saturate
53
+ * anyway (your app ends every episode itself), or false for intents where the repetition
54
+ * itself is the evidence (a user stuck on the same step), so every occurrence adds its full
55
+ * weight.
56
+ */
57
+ saturate?: boolean;
58
+ /**
59
+ * How long a closed episode's signals wait for the outcome before they take a miss, in the same
60
+ * unit as `at`. With it set, an episode that ends without the outcome is held instead of
61
+ * counted. When the outcome arrives inside the window, each held episode gets a share
62
+ * `2^(−age / halfLife)` of a hit and the rest of a miss, with `age` measured from the episode's
63
+ * close: the sessions that built up to the outcome are credited by how much of their evidence
64
+ * still stands. When the window passes, the episode is a miss, recorded at the moment the window
65
+ * ended. Unset: an episode that ends without the outcome is a miss at once. A natural first value
66
+ * is the intent's `halfLife`, where the smallest share inside the window is one half.
67
+ */
68
+ attribution?: number;
69
+ /** Reveal defaults for this intent. */
70
+ reveal?: RevealOptions;
71
+ }
72
+ /**
73
+ * The intents `oracular/presets` ships. Each is a plain {@link IntentConfig} to spread into your
74
+ * config and override; `presetSources` there says where its numbers come from.
75
+ */
76
+ type PresetName = 'willBuy' | 'upgrade' | 'stuck' | 'churn';
77
+ /**
78
+ * Where a preset's numbers come from, as `presetSources` in `oracular/presets` reports it. A tuned
79
+ * preset was fitted by `oracular tune` on a real log and carries the sample behind it; a
80
+ * hand-written one is a careful guess nobody has measured. Either is a starting point: run in
81
+ * shadow mode and let `tune` replace it with your own numbers.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * import { presetSources } from 'oracular/presets';
86
+ * presetSources.willBuy.kind; // 'tuned'
87
+ * ```
88
+ */
89
+ interface PresetSource {
90
+ kind: 'tuned' | 'hand-written';
91
+ /** What the numbers were fitted on, or where the guess comes from. */
92
+ note: string;
93
+ /** Counted episodes behind a tuned preset; null for a hand-written one. */
94
+ episodes: number | null;
95
+ /** Installs those episodes came from; null for a hand-written one. */
96
+ installs: number | null;
97
+ }
98
+
99
+ /**
100
+ * Starting points for four common intents, as plain config objects. Spread one into your config,
101
+ * rename its events to yours, override what you know better; see {@link presetSources} for where
102
+ * each preset's numbers come from.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { presets } from 'oracular/presets';
107
+ *
108
+ * const config = {
109
+ * episodeTimeout: 30 * 60 * 1000,
110
+ * intents: {
111
+ * buy: { ...presets.willBuy, outcome: 'order.placed' },
112
+ * },
113
+ * };
114
+ * ```
115
+ */
116
+ declare const presets: Readonly<Record<PresetName, IntentConfig>>;
117
+ /**
118
+ * Where each preset's numbers come from. Read it before trusting a preset: a tuned one carries its
119
+ * sample size, a hand-written one carries nothing but reasoning.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import { presetSources } from 'oracular/presets';
124
+ * presetSources.willBuy.kind; // 'tuned'
125
+ * ```
126
+ */
127
+ declare const presetSources: Readonly<Record<PresetName, PresetSource>>;
128
+
129
+ export { type PresetName, type PresetSource, presetSources, presets };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Options that decide when `shouldReveal` fires and how long cooldowns last. Resolved per call →
3
+ * intent → engine → defaults.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * shouldReveal(state, 'upgrade', Date.now(), { threshold: 0.7, z: 1.64 });
8
+ * ```
9
+ */
10
+ interface RevealOptions {
11
+ /** Probability the intent must reach, in [0, 1]. Default 0.5. */
12
+ threshold?: number;
13
+ /** How many standard deviations below p must still clear the threshold. Default 1.28 (one-sided 90%). */
14
+ z?: number;
15
+ /** How long after `markRevealed` the intent stays revealed and `shouldReveal` is false. Default: the intent's `halfLife`. */
16
+ cooldown?: number;
17
+ }
18
+ /**
19
+ * One thing the user might be about to do, tracked as a Beta(α, β) distribution with
20
+ * α₀ = prior × priorStrength and β₀ = (1 − prior) × priorStrength.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const churn: IntentConfig = {
25
+ * prior: 0.08,
26
+ * priorStrength: 20,
27
+ * halfLife: 14 * 24 * 60 * 60 * 1000,
28
+ * signals: { 'session.short': 1, 'notifications.disabled': 3, 'feature.new.used': -2 },
29
+ * outcome: 'account.cancelled',
30
+ * reveal: { threshold: 0.5, z: 1.64 },
31
+ * };
32
+ * ```
33
+ */
34
+ interface IntentConfig {
35
+ /** Prior probability, strictly between 0 and 1. */
36
+ prior: number;
37
+ /** Strength of the prior as a pseudo-count, greater than 0 and at most 1e6. */
38
+ priorStrength: number;
39
+ /** Half-life of evidence, in the same unit as `at`. Evidence decays back toward the prior. */
40
+ halfLife: number;
41
+ /** Event name → initial weight. Positive is evidence for the intent, negative against. Non-zero, between −1e6 and 1e6. */
42
+ signals: Record<string, number>;
43
+ /** Event name that confirms the intent happened, e.g. `'checkout.completed'`. */
44
+ outcome?: string;
45
+ /** Reset α, β and contributions to the prior when the outcome arrives. Default true. */
46
+ resetOnOutcome?: boolean;
47
+ /**
48
+ * Repeats of a signal within one episode add less: the n-th occurrence adds
49
+ * `w × log2(1 + 1/n)`, so ten occurrences add 3.46 weights, not ten. The count restarts when
50
+ * the episode ends or this intent's outcome arrives. Default: true when `episodeTimeout` is
51
+ * set, false otherwise, because without an idle timeout episodes end only when the app calls
52
+ * `endEpisode`, and if it never does the count would never restart. Set true to saturate
53
+ * anyway (your app ends every episode itself), or false for intents where the repetition
54
+ * itself is the evidence (a user stuck on the same step), so every occurrence adds its full
55
+ * weight.
56
+ */
57
+ saturate?: boolean;
58
+ /**
59
+ * How long a closed episode's signals wait for the outcome before they take a miss, in the same
60
+ * unit as `at`. With it set, an episode that ends without the outcome is held instead of
61
+ * counted. When the outcome arrives inside the window, each held episode gets a share
62
+ * `2^(−age / halfLife)` of a hit and the rest of a miss, with `age` measured from the episode's
63
+ * close: the sessions that built up to the outcome are credited by how much of their evidence
64
+ * still stands. When the window passes, the episode is a miss, recorded at the moment the window
65
+ * ended. Unset: an episode that ends without the outcome is a miss at once. A natural first value
66
+ * is the intent's `halfLife`, where the smallest share inside the window is one half.
67
+ */
68
+ attribution?: number;
69
+ /** Reveal defaults for this intent. */
70
+ reveal?: RevealOptions;
71
+ }
72
+ /**
73
+ * The intents `oracular/presets` ships. Each is a plain {@link IntentConfig} to spread into your
74
+ * config and override; `presetSources` there says where its numbers come from.
75
+ */
76
+ type PresetName = 'willBuy' | 'upgrade' | 'stuck' | 'churn';
77
+ /**
78
+ * Where a preset's numbers come from, as `presetSources` in `oracular/presets` reports it. A tuned
79
+ * preset was fitted by `oracular tune` on a real log and carries the sample behind it; a
80
+ * hand-written one is a careful guess nobody has measured. Either is a starting point: run in
81
+ * shadow mode and let `tune` replace it with your own numbers.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * import { presetSources } from 'oracular/presets';
86
+ * presetSources.willBuy.kind; // 'tuned'
87
+ * ```
88
+ */
89
+ interface PresetSource {
90
+ kind: 'tuned' | 'hand-written';
91
+ /** What the numbers were fitted on, or where the guess comes from. */
92
+ note: string;
93
+ /** Counted episodes behind a tuned preset; null for a hand-written one. */
94
+ episodes: number | null;
95
+ /** Installs those episodes came from; null for a hand-written one. */
96
+ installs: number | null;
97
+ }
98
+
99
+ /**
100
+ * Starting points for four common intents, as plain config objects. Spread one into your config,
101
+ * rename its events to yours, override what you know better; see {@link presetSources} for where
102
+ * each preset's numbers come from.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { presets } from 'oracular/presets';
107
+ *
108
+ * const config = {
109
+ * episodeTimeout: 30 * 60 * 1000,
110
+ * intents: {
111
+ * buy: { ...presets.willBuy, outcome: 'order.placed' },
112
+ * },
113
+ * };
114
+ * ```
115
+ */
116
+ declare const presets: Readonly<Record<PresetName, IntentConfig>>;
117
+ /**
118
+ * Where each preset's numbers come from. Read it before trusting a preset: a tuned one carries its
119
+ * sample size, a hand-written one carries nothing but reasoning.
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * import { presetSources } from 'oracular/presets';
124
+ * presetSources.willBuy.kind; // 'tuned'
125
+ * ```
126
+ */
127
+ declare const presetSources: Readonly<Record<PresetName, PresetSource>>;
128
+
129
+ export { type PresetName, type PresetSource, presetSources, presets };
@@ -0,0 +1,83 @@
1
+ // src/units.ts
2
+ var ms = {
3
+ seconds: (n) => n * 1e3,
4
+ minutes: (n) => n * 6e4,
5
+ hours: (n) => n * 36e5,
6
+ days: (n) => n * 864e5,
7
+ weeks: (n) => n * 6048e5
8
+ };
9
+
10
+ // src/presets.ts
11
+ var willBuy = {
12
+ prior: 0.048,
13
+ priorStrength: 4,
14
+ halfLife: ms.hours(1),
15
+ signals: { "cart.added": 2.3 },
16
+ outcome: "checkout.completed",
17
+ reveal: { cooldown: ms.days(1) }
18
+ };
19
+ var upgrade = {
20
+ prior: 0.05,
21
+ priorStrength: 10,
22
+ halfLife: ms.days(7),
23
+ signals: { "pricing.viewed": 3, "limit.hit": 2, "export.clicked": 1 },
24
+ outcome: "upgrade.completed"
25
+ };
26
+ var stuck = {
27
+ prior: 0.1,
28
+ priorStrength: 5,
29
+ halfLife: ms.minutes(10),
30
+ signals: { "editor.undo": 1, "search.empty": 2, "help.opened": 3, "task.completed": -4 },
31
+ outcome: "help.article.read",
32
+ saturate: false,
33
+ reveal: { threshold: 0.6, cooldown: ms.hours(1) }
34
+ };
35
+ var churn = {
36
+ prior: 0.08,
37
+ priorStrength: 20,
38
+ halfLife: ms.days(14),
39
+ signals: {
40
+ "session.short": 1,
41
+ "notifications.disabled": 3,
42
+ "export.all": 4,
43
+ "feature.new.used": -2
44
+ },
45
+ outcome: "account.cancelled",
46
+ reveal: { threshold: 0.5, z: 1.64 }
47
+ };
48
+ var presets = {
49
+ willBuy,
50
+ upgrade,
51
+ stuck,
52
+ churn
53
+ };
54
+ var presetSources = {
55
+ willBuy: {
56
+ 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.0477 and 2.32 rounded",
58
+ episodes: 181080,
59
+ installs: 93537
60
+ },
61
+ upgrade: {
62
+ kind: "hand-written",
63
+ note: "the quickstart config from the documentation; no log behind it",
64
+ episodes: null,
65
+ installs: null
66
+ },
67
+ stuck: {
68
+ kind: "hand-written",
69
+ note: "the onboarding-tip example from the documentation; no log behind it",
70
+ episodes: null,
71
+ installs: null
72
+ },
73
+ churn: {
74
+ kind: "hand-written",
75
+ note: "the churn-save example from the documentation; no log behind it",
76
+ episodes: null,
77
+ installs: null
78
+ }
79
+ };
80
+
81
+ export { presetSources, presets };
82
+ //# sourceMappingURL=presets.js.map
83
+ //# sourceMappingURL=presets.js.map
@@ -0,0 +1 @@
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.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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oracular",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "On-device intent engine. Your app learns when the user is ready. Nothing leaves the device.",
5
5
  "keywords": [
6
6
  "oracular",
@@ -42,9 +42,41 @@
42
42
  },
43
43
  "default": "./dist/index.cjs"
44
44
  },
45
+ "./presets": {
46
+ "import": {
47
+ "types": "./dist/presets.d.ts",
48
+ "default": "./dist/presets.js"
49
+ },
50
+ "require": {
51
+ "types": "./dist/presets.d.cts",
52
+ "default": "./dist/presets.cjs"
53
+ },
54
+ "default": "./dist/presets.cjs"
55
+ },
56
+ "./inspect": {
57
+ "import": {
58
+ "types": "./dist/inspect.d.ts",
59
+ "default": "./dist/inspect.js"
60
+ },
61
+ "require": {
62
+ "types": "./dist/inspect.d.cts",
63
+ "default": "./dist/inspect.cjs"
64
+ },
65
+ "default": "./dist/inspect.cjs"
66
+ },
45
67
  "./config.schema.json": "./schema/config.schema.json",
46
68
  "./package.json": "./package.json"
47
69
  },
70
+ "typesVersions": {
71
+ "*": {
72
+ "presets": [
73
+ "./dist/presets.d.ts"
74
+ ],
75
+ "inspect": [
76
+ "./dist/inspect.d.ts"
77
+ ]
78
+ }
79
+ },
48
80
  "files": [
49
81
  "dist",
50
82
  "schema",