@sentientui/policy 0.3.0 → 0.3.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/LICENSE +21 -0
- package/README.md +74 -0
- package/dist/index.d.cts +15 -1
- package/dist/index.d.ts +15 -1
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Carlos Sánchez Campos / SentientUI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @sentientui/policy
|
|
2
|
+
|
|
3
|
+
Pure decision-policy functions shared by the SentientUI API and the keyless local
|
|
4
|
+
engine. This package is the single source of truth for *how a decision is made* —
|
|
5
|
+
Thompson sampling, empirical-Bayes pooling/shrinkage, arm encoding, slot
|
|
6
|
+
validation, and layout selection — so the server and the on-device engine always
|
|
7
|
+
agree.
|
|
8
|
+
|
|
9
|
+
Everything here is a **pure function**: no I/O, no global state, no side effects.
|
|
10
|
+
Randomized functions take an injectable `rand: () => number` (uniform `[0,1)`) so
|
|
11
|
+
results are fully reproducible when you pass a seeded PRNG. The default is
|
|
12
|
+
`Math.random`, which is **non-deterministic** — pass a seed for tests or
|
|
13
|
+
replayable decisions.
|
|
14
|
+
|
|
15
|
+
## What's inside
|
|
16
|
+
|
|
17
|
+
**Bandit / Thompson sampling** (`bandit.ts`)
|
|
18
|
+
- `sampleBeta(alpha, beta, rand?)` — one draw from `Beta(alpha, beta)` (Marsaglia–Tsang gamma method).
|
|
19
|
+
- `sampleArm(arms, rand?)` — Thompson-samples each arm's Beta posterior and returns the argmax arm id (or `null` for no arms).
|
|
20
|
+
|
|
21
|
+
**Empirical-Bayes pooling & shrinkage** (`shrinkage.ts`, `pooling.ts`)
|
|
22
|
+
- `shrunkPosterior(persona, pooled, m?)` — one-axis read-time shrinkage; cells are born warm (`w = m / (m + exposures)`) and detach as their own data accumulates. `SHRINKAGE_M` is the default strength.
|
|
23
|
+
- `posteriorOfCounts({ exposures, conversions })` — `Beta` posterior from raw counts (`alpha = conversions + 1`, `beta = max(0, exposures − conversions) + 1`).
|
|
24
|
+
- `pooledPosterior(cells, personaKnown, m?)` — hierarchical partial pooling over `(segment, persona)`; reproduces legacy segment-only and persona-only behavior when only those cells are present. `POOL_ALL` is the `__all__` sentinel for marginal/global rows.
|
|
25
|
+
- `weightCellsFor(...)` — the write-side counterpart: which weight rows a single trial/credit must bump.
|
|
26
|
+
|
|
27
|
+
**Arm encoding & slot validation** (`arm-encoding.ts`)
|
|
28
|
+
- `canonicalArm(values)` / `parseArm(arm)` — stable string encoding of a multi-dimensional arm and its inverse.
|
|
29
|
+
- `marginalArmKey(dim, value)`, `slotBaselineArm(decl)`, `slotResultFor(decl, arm)` — arm helpers for a slot declaration.
|
|
30
|
+
- `validateSlotDecl(decl)` — structural validation of a `SlotDecl`, returning `{ ok: true }` or `{ ok: false, reason }`.
|
|
31
|
+
|
|
32
|
+
**Layout selection** (`layout-heuristics.ts`, `choose-layout.ts`, `hash.ts`)
|
|
33
|
+
- `candidateLayouts(sections, sectionTypes, persona)` — the candidate section orderings for a persona.
|
|
34
|
+
- `applyClusterHeuristic(sections, sectionTypes, persona)` — the persona's heuristic ordering (`CLUSTER_PRIORITY`), used as the fallback.
|
|
35
|
+
- `chooseLayout(sections, sectionTypes, persona, learned, rand?)` — Thompson-samples the learned layout posteriors over the candidates, falling back to the heuristic.
|
|
36
|
+
- `hashLayout(order)` — stable hash of a section order (the `layoutHash` key).
|
|
37
|
+
|
|
38
|
+
**Personas** (`personas.ts`)
|
|
39
|
+
- `PERSONAS`, `PersonaKey`, `UNKNOWN_PERSONA`, `PERSONA_DISPLAY` — the canonical persona set and display names.
|
|
40
|
+
- `canonicalPersona(label)` — normalize an arbitrary/legacy label to a `PersonaKey`.
|
|
41
|
+
|
|
42
|
+
**Deterministic helpers** (`deterministic.ts`)
|
|
43
|
+
- `fnv1a(input)` — FNV-1a hash.
|
|
44
|
+
- `pickDeterministicArm(sessionId, slotId, arms)` — hash-based, seed-free arm pick (stable per session/slot).
|
|
45
|
+
- `confidenceBand(c)` — map a `[0,1]` confidence to `'low' | 'medium' | 'high'`.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { sampleArm, posteriorOfCounts } from '@sentientui/policy';
|
|
51
|
+
|
|
52
|
+
// Thompson-sample the arm to serve from each arm's Beta posterior.
|
|
53
|
+
const arms = [
|
|
54
|
+
{ arm: 'control', ...posteriorOfCounts({ exposures: 200, conversions: 20 }) },
|
|
55
|
+
{ arm: 'variant_b', ...posteriorOfCounts({ exposures: 180, conversions: 27 }) },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const chosen = sampleArm(arms); // e.g. 'variant_b' — uses Math.random
|
|
59
|
+
|
|
60
|
+
// Pass a seeded PRNG for reproducible selection (tests, replayable decisions):
|
|
61
|
+
const chosenSeeded = sampleArm(arms, mySeededRng);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { chooseLayout, hashLayout, type LearnedLayout } from '@sentientui/policy';
|
|
66
|
+
|
|
67
|
+
const learned = new Map<string, LearnedLayout>(); // from your layout_weights store
|
|
68
|
+
const order = chooseLayout(sections, sectionTypes, 'buyer', learned);
|
|
69
|
+
const key = hashLayout(order);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/dist/index.d.cts
CHANGED
|
@@ -57,6 +57,10 @@ type LearnedLayout = {
|
|
|
57
57
|
* orderings, using learned posteriors from layout_weights. Candidates with no
|
|
58
58
|
* learned row use the uniform 1/1 prior — identical to variant cold start.
|
|
59
59
|
* Falls back to the persona's heuristic only if sampling yields no candidate.
|
|
60
|
+
*
|
|
61
|
+
* @param rand Uniform [0,1) source. Defaults to `Math.random`, which is
|
|
62
|
+
* NON-DETERMINISTIC. Pass a seeded PRNG when you need a reproducible layout
|
|
63
|
+
* (tests, replayable decisions) — otherwise the sampled order varies per call.
|
|
60
64
|
*/
|
|
61
65
|
declare function chooseLayout(sections: string[], sectionTypes: Map<string, string>, persona: PersonaKey, learned: Map<string, LearnedLayout>, rand?: () => number): string[];
|
|
62
66
|
|
|
@@ -66,13 +70,23 @@ type ArmPosterior = {
|
|
|
66
70
|
alpha: number;
|
|
67
71
|
beta: number;
|
|
68
72
|
};
|
|
69
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* One draw from Beta(alpha, beta). Moved verbatim from apps/api/src/domain/bandit.ts.
|
|
75
|
+
*
|
|
76
|
+
* @param rand Uniform [0,1) source. Defaults to `Math.random`, which is
|
|
77
|
+
* NON-DETERMINISTIC. Pass a seeded PRNG when you need reproducible output
|
|
78
|
+
* (tests, replayable decisions, snapshotting) — otherwise results vary per call.
|
|
79
|
+
*/
|
|
70
80
|
declare function sampleBeta(alpha: number, beta: number, rand?: () => number): number;
|
|
71
81
|
/**
|
|
72
82
|
* Thompson Sampling selection: samples Beta(alpha, beta) per arm and returns
|
|
73
83
|
* the argmax arm id, or null when no arms are given. Uncertain arms get
|
|
74
84
|
* explored; confident winners get exploited — same semantics as the legacy
|
|
75
85
|
* chooseVariant, generalized to arbitrary arm strings.
|
|
86
|
+
*
|
|
87
|
+
* @param rand Uniform [0,1) source. Defaults to `Math.random`, which is
|
|
88
|
+
* NON-DETERMINISTIC. Pass a seeded PRNG for reproducible selection (tests,
|
|
89
|
+
* replayable assignments) — otherwise the chosen arm varies per call.
|
|
76
90
|
*/
|
|
77
91
|
declare function sampleArm(arms: ArmPosterior[], rand?: () => number): string | null;
|
|
78
92
|
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,10 @@ type LearnedLayout = {
|
|
|
57
57
|
* orderings, using learned posteriors from layout_weights. Candidates with no
|
|
58
58
|
* learned row use the uniform 1/1 prior — identical to variant cold start.
|
|
59
59
|
* Falls back to the persona's heuristic only if sampling yields no candidate.
|
|
60
|
+
*
|
|
61
|
+
* @param rand Uniform [0,1) source. Defaults to `Math.random`, which is
|
|
62
|
+
* NON-DETERMINISTIC. Pass a seeded PRNG when you need a reproducible layout
|
|
63
|
+
* (tests, replayable decisions) — otherwise the sampled order varies per call.
|
|
60
64
|
*/
|
|
61
65
|
declare function chooseLayout(sections: string[], sectionTypes: Map<string, string>, persona: PersonaKey, learned: Map<string, LearnedLayout>, rand?: () => number): string[];
|
|
62
66
|
|
|
@@ -66,13 +70,23 @@ type ArmPosterior = {
|
|
|
66
70
|
alpha: number;
|
|
67
71
|
beta: number;
|
|
68
72
|
};
|
|
69
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* One draw from Beta(alpha, beta). Moved verbatim from apps/api/src/domain/bandit.ts.
|
|
75
|
+
*
|
|
76
|
+
* @param rand Uniform [0,1) source. Defaults to `Math.random`, which is
|
|
77
|
+
* NON-DETERMINISTIC. Pass a seeded PRNG when you need reproducible output
|
|
78
|
+
* (tests, replayable decisions, snapshotting) — otherwise results vary per call.
|
|
79
|
+
*/
|
|
70
80
|
declare function sampleBeta(alpha: number, beta: number, rand?: () => number): number;
|
|
71
81
|
/**
|
|
72
82
|
* Thompson Sampling selection: samples Beta(alpha, beta) per arm and returns
|
|
73
83
|
* the argmax arm id, or null when no arms are given. Uncertain arms get
|
|
74
84
|
* explored; confident winners get exploited — same semantics as the legacy
|
|
75
85
|
* chooseVariant, generalized to arbitrary arm strings.
|
|
86
|
+
*
|
|
87
|
+
* @param rand Uniform [0,1) source. Defaults to `Math.random`, which is
|
|
88
|
+
* NON-DETERMINISTIC. Pass a seeded PRNG for reproducible selection (tests,
|
|
89
|
+
* replayable assignments) — otherwise the chosen arm varies per call.
|
|
76
90
|
*/
|
|
77
91
|
declare function sampleArm(arms: ArmPosterior[], rand?: () => number): string | null;
|
|
78
92
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentientui/policy",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Pure decision-policy functions shared by the SentientUI API and the keyless local engine",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@types/node": "^22.10.2",
|