oracular 0.7.1 → 0.8.1
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 +12 -0
- package/README.md +3 -2
- package/dist/index.cjs +68 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -10
- package/dist/index.d.ts +22 -10
- package/dist/index.js +68 -9
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.cts +4 -1
- package/dist/inspect.d.ts +4 -1
- package/dist/presets.d.cts +7 -0
- package/dist/presets.d.ts +7 -0
- package/package.json +10 -4
- package/schema/config.schema.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#46](https://github.com/Yuribenjamin/oracular/pull/46) [`d796e99`](https://github.com/Yuribenjamin/oracular/commit/d796e990fed2b15d2fbf9afa1744782f9da41433) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **Release tags.** From this release, each release is tagged `oracular@<version>` instead of `v<version>`, and the GitHub release that goes with it is created per package. The reason is the repository's shape: it is now an npm workspace, with the root still the `oracular` package, so that a second package can be published from it; Changesets names tags that way in a workspace, and this release is the one that proves the pipeline end to end before anything depends on it. Nothing in the library changes. What also does not change: the existing `v0.1.0` to `v0.8.0` tags stay where they are, and there is no `v` alias for new releases. If you pin by tag, `oracular@0.8.1` is the first of the new form, and from here on the tag is the package name followed by the version.
|
|
8
|
+
|
|
9
|
+
## 0.8.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#44](https://github.com/Yuribenjamin/oracular/pull/44) [`912f60d`](https://github.com/Yuribenjamin/oracular/commit/912f60da297fe01a64decc1f91916cdcd3a98c60) Thanks [@Yuribenjamin](https://github.com/Yuribenjamin)! - **Exact lower bound.** `RevealOptions.bound: 'exact'` makes `lower` the Beta distribution's own quantile at the one-sided level `z` names (Φ(−z), so 1.28 is still a 90% bound) instead of the normal approximation p − z·sd, which is optimistic exactly where the gate matters: thin evidence near the threshold. Beta(4, 3) puts p at 0.571, the approximation at 0.347 and the exact tenth percentile at 0.333; by Beta(30, 20) the two agree to a thousandth. `predict`, `shouldReveal`, `evaluate`, the recorder and `evidenceToReveal` all follow the option, resolved per call → intent → engine → default like the others, and the config schema lists it. Nothing moves unless it is set: every state and every number without `bound` is exactly what it was. Under `'exact'`, `z: 0` is the median rather than the mean, and `lower` can sit above `p` for a prior near 1 with little strength. A stored config that sets `bound` is read by 0.8 and later; 0.7 throws a `TypeError` on the unknown key, so drop the option before rolling a deploy back. The state version is unchanged. The quantile costs 0.54 kB: the core goes from 6.65 kB to 7.19 kB minified and gzipped, past the 7 kB ceiling the stability page set, so the ceiling moves once, to 7.9 kB (the shipped size plus about 10%), and the quickstart budget from 3.9 kB to 4.5 kB; the stability page records the decision.
|
|
14
|
+
|
|
3
15
|
## 0.7.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ Learned counts never decay unless you set `learning.halfLife`.
|
|
|
140
140
|
|
|
141
141
|
### Reveal only when confident
|
|
142
142
|
|
|
143
|
-
`shouldReveal` is true when p ≥ threshold **and** p − z·sd ≥ threshold, where sd is the Beta standard deviation and z defaults to 1.28 (a one-sided 90% bound). Thin evidence with a high mean does not fire; it takes a few consistent signals. After you `markRevealed`, the intent stays revealed for a `cooldown` (default: its half-life) so you don't fire on every event. The same cooldown runs after the intent's outcome event. `predict` reports both: `revealed` is true inside a reveal cooldown, and `suppressedUntil` is when the later of the two cooldowns ends, or null when `shouldReveal` is free to fire.
|
|
143
|
+
`shouldReveal` is true when p ≥ threshold **and** p − z·sd ≥ threshold, where sd is the Beta standard deviation and z defaults to 1.28 (a one-sided 90% bound). Thin evidence with a high mean does not fire; it takes a few consistent signals. That lower bound is a normal approximation; `bound: 'exact'` swaps in the Beta distribution's own quantile at the same level, which asks for a little more on thin evidence. After you `markRevealed`, the intent stays revealed for a `cooldown` (default: its half-life) so you don't fire on every event. The same cooldown runs after the intent's outcome event. `predict` reports both: `revealed` is true inside a reveal cooldown, and `suppressedUntil` is when the later of the two cooldowns ends, or null when `shouldReveal` is free to fire.
|
|
144
144
|
|
|
145
145
|
### Outcomes reset the slate and pause reveals
|
|
146
146
|
|
|
@@ -299,6 +299,7 @@ interface RevealOptions {
|
|
|
299
299
|
threshold?: number; // default 0.5
|
|
300
300
|
z?: number; // default 1.28
|
|
301
301
|
cooldown?: number; // default: the intent's halfLife
|
|
302
|
+
bound?: 'normal' | 'exact'; // default 'normal': p − z·sd; 'exact': the Beta quantile at Φ(−z)
|
|
302
303
|
}
|
|
303
304
|
```
|
|
304
305
|
|
|
@@ -311,7 +312,7 @@ interface Prediction {
|
|
|
311
312
|
intent: string;
|
|
312
313
|
p: number; // α / (α + β)
|
|
313
314
|
sd: number; // Beta standard deviation
|
|
314
|
-
lower: number; // p − z·sd
|
|
315
|
+
lower: number; // p − z·sd clamped to [0, 1], or the exact Beta quantile with bound: 'exact'; what shouldReveal compares to threshold
|
|
315
316
|
confidence: number; // 1 − 2·sd, clamped to [0, 1]. A heuristic: 1 is a sharp estimate, 0 is as vague as a Beta gets
|
|
316
317
|
evidence: number; // α + β, prior strength included
|
|
317
318
|
revealed: boolean; // inside the cooldown after markRevealed
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,62 @@ function clamp01(x) {
|
|
|
20
20
|
if (x > 1) return 1;
|
|
21
21
|
return x;
|
|
22
22
|
}
|
|
23
|
+
function normalTail(z) {
|
|
24
|
+
const x = z / Math.SQRT2;
|
|
25
|
+
const t = 1 / (1 + 0.3275911 * x);
|
|
26
|
+
const poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
|
|
27
|
+
return 0.5 * poly * Math.exp(-x * x);
|
|
28
|
+
}
|
|
29
|
+
function lgamma(x) {
|
|
30
|
+
let acc = 0;
|
|
31
|
+
let v = x;
|
|
32
|
+
while (v < 8) {
|
|
33
|
+
acc -= Math.log(v);
|
|
34
|
+
v += 1;
|
|
35
|
+
}
|
|
36
|
+
const r = 1 / v;
|
|
37
|
+
const r2 = r * r;
|
|
38
|
+
return acc + (v - 0.5) * Math.log(v) - v + 0.9189385332046728 + r * (1 / 12 - r2 * (1 / 360 - r2 * (1 / 1260 - r2 / 1680)));
|
|
39
|
+
}
|
|
40
|
+
function betacf(x, a, b) {
|
|
41
|
+
const tiny = 1e-300;
|
|
42
|
+
const guard = (v) => Math.abs(v) < tiny ? tiny : v;
|
|
43
|
+
let c = 1 / tiny;
|
|
44
|
+
let d = 1;
|
|
45
|
+
let h = 1;
|
|
46
|
+
for (let i = 1; i < 2e4; i++) {
|
|
47
|
+
const m = i >> 1;
|
|
48
|
+
const aa = (i & 1 ? -(a + m) * (a + b + m) : m * (b - m)) * x / ((a + i - 1) * (a + i));
|
|
49
|
+
d = 1 / guard(1 + aa * d);
|
|
50
|
+
c = guard(1 + aa / c);
|
|
51
|
+
const del = c * d;
|
|
52
|
+
h *= del;
|
|
53
|
+
if (Math.abs(del - 1) < 1e-12) break;
|
|
54
|
+
}
|
|
55
|
+
return h;
|
|
56
|
+
}
|
|
57
|
+
function incompleteBeta(x, a, b) {
|
|
58
|
+
if (x <= 0) return 0;
|
|
59
|
+
if (x >= 1) return 1;
|
|
60
|
+
const front = Math.exp(
|
|
61
|
+
lgamma(a + b) - lgamma(a) - lgamma(b) + a * Math.log(x) + b * Math.log(1 - x)
|
|
62
|
+
);
|
|
63
|
+
return x < (a + 1) / (a + b + 2) ? front * betacf(x, a, b) / a : 1 - front * betacf(1 - x, b, a) / b;
|
|
64
|
+
}
|
|
65
|
+
function betaQuantile(q, a, b) {
|
|
66
|
+
let lo = 0;
|
|
67
|
+
let hi = 1;
|
|
68
|
+
for (let i = 0; i < 40 && hi - lo > 1e-9; i++) {
|
|
69
|
+
const mid = (lo + hi) / 2;
|
|
70
|
+
if (incompleteBeta(mid, a, b) < q) lo = mid;
|
|
71
|
+
else hi = mid;
|
|
72
|
+
}
|
|
73
|
+
return (lo + hi) / 2;
|
|
74
|
+
}
|
|
75
|
+
function lowerBound(alpha, beta, z, bound) {
|
|
76
|
+
if (bound === "exact") return betaQuantile(normalTail(z), alpha, beta);
|
|
77
|
+
return clamp01(betaMean(alpha, beta) - z * betaSd(alpha, beta));
|
|
78
|
+
}
|
|
23
79
|
function repeatFactor(n) {
|
|
24
80
|
return Math.log1p(1 / n) / Math.LN2;
|
|
25
81
|
}
|
|
@@ -200,7 +256,7 @@ var INTENT_KEYS = [
|
|
|
200
256
|
"attribution",
|
|
201
257
|
"reveal"
|
|
202
258
|
];
|
|
203
|
-
var REVEAL_KEYS = ["threshold", "z", "cooldown"];
|
|
259
|
+
var REVEAL_KEYS = ["threshold", "z", "cooldown", "bound"];
|
|
204
260
|
var LEARNING_KEYS = ["halfLife", "gain", "priorEpisodes"];
|
|
205
261
|
var EXPLAIN_KEYS = ["nonZero", "limit"];
|
|
206
262
|
function typeError(path, message) {
|
|
@@ -274,6 +330,12 @@ function normalizeRevealOptions(value, path) {
|
|
|
274
330
|
if (record2.cooldown !== void 0) {
|
|
275
331
|
out.cooldown = expectNonNegative(record2.cooldown, `${path}.cooldown`);
|
|
276
332
|
}
|
|
333
|
+
if (record2.bound !== void 0) {
|
|
334
|
+
if (record2.bound !== "normal" && record2.bound !== "exact") {
|
|
335
|
+
typeError(`${path}.bound`, 'must be "normal" or "exact"');
|
|
336
|
+
}
|
|
337
|
+
out.bound = record2.bound;
|
|
338
|
+
}
|
|
277
339
|
return out;
|
|
278
340
|
}
|
|
279
341
|
function normalizeLearning(value, path) {
|
|
@@ -394,7 +456,8 @@ function resolveReveal(config, intent, opts, normalized = false) {
|
|
|
394
456
|
return {
|
|
395
457
|
threshold: o.threshold ?? intent.reveal?.threshold ?? config?.reveal?.threshold ?? DEFAULT_THRESHOLD,
|
|
396
458
|
z: o.z ?? intent.reveal?.z ?? config?.reveal?.z ?? DEFAULT_Z,
|
|
397
|
-
cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife
|
|
459
|
+
cooldown: o.cooldown ?? intent.reveal?.cooldown ?? config?.reveal?.cooldown ?? intent.halfLife,
|
|
460
|
+
bound: o.bound ?? intent.reveal?.bound ?? config?.reveal?.bound ?? "normal"
|
|
398
461
|
};
|
|
399
462
|
}
|
|
400
463
|
function resolveLearning(config) {
|
|
@@ -501,7 +564,7 @@ function buildPrediction(is, name, reveal, at) {
|
|
|
501
564
|
intent: name,
|
|
502
565
|
p,
|
|
503
566
|
sd,
|
|
504
|
-
lower:
|
|
567
|
+
lower: lowerBound(is.alpha, is.beta, reveal.z, reveal.bound),
|
|
505
568
|
confidence: clamp01(1 - 2 * sd),
|
|
506
569
|
evidence: is.alpha + is.beta,
|
|
507
570
|
revealed: revealEnd !== null,
|
|
@@ -569,13 +632,9 @@ function inspect(state, at, opts) {
|
|
|
569
632
|
// src/evidence.ts
|
|
570
633
|
function evidenceToReveal(intent, opts) {
|
|
571
634
|
const config = normalizeIntentConfig(intent, "intent");
|
|
572
|
-
const { threshold, z } = resolveReveal(void 0, config, opts);
|
|
635
|
+
const { threshold, z, bound } = resolveReveal(void 0, config, opts);
|
|
573
636
|
const { alpha0, beta0 } = priorCounts(config);
|
|
574
|
-
const open = (w) =>
|
|
575
|
-
const alpha = alpha0 + w;
|
|
576
|
-
const p = betaMean(alpha, beta0);
|
|
577
|
-
return p >= threshold && clamp01(p - z * betaSd(alpha, beta0)) >= threshold;
|
|
578
|
-
};
|
|
637
|
+
const open = (w) => betaMean(alpha0 + w, beta0) >= threshold && lowerBound(alpha0 + w, beta0, z, bound) >= threshold;
|
|
579
638
|
if (threshold >= 1) return Number.POSITIVE_INFINITY;
|
|
580
639
|
if (open(0)) return 0;
|
|
581
640
|
let lo = 0;
|