@bondedhq/shared 0.0.0-stage → 0.1.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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -2
  3. package/dist/abis.d.ts +8639 -0
  4. package/dist/abis.js +11234 -0
  5. package/dist/actuarial.d.ts +158 -0
  6. package/dist/actuarial.js +210 -0
  7. package/dist/agent-url.d.ts +172 -0
  8. package/dist/agent-url.js +248 -0
  9. package/dist/agent.d.ts +184 -0
  10. package/dist/agent.js +133 -0
  11. package/dist/allowances.d.ts +54 -0
  12. package/dist/allowances.js +68 -0
  13. package/dist/bounty-example.d.ts +6 -0
  14. package/dist/bounty-example.js +23 -0
  15. package/dist/bounty-spec.d.ts +36 -0
  16. package/dist/bounty-spec.js +111 -0
  17. package/dist/canonical.d.ts +8 -0
  18. package/dist/canonical.js +44 -0
  19. package/dist/chains.d.ts +58 -0
  20. package/dist/chains.js +123 -0
  21. package/dist/deployments.d.ts +32 -0
  22. package/dist/deployments.js +41 -0
  23. package/dist/index.d.ts +33 -0
  24. package/dist/index.js +33 -0
  25. package/dist/leaderboard.d.ts +105 -0
  26. package/dist/leaderboard.js +85 -0
  27. package/dist/llm.d.ts +121 -0
  28. package/dist/llm.js +105 -0
  29. package/dist/mandate-rules.d.ts +54 -0
  30. package/dist/mandate-rules.js +69 -0
  31. package/dist/module-install.d.ts +145 -0
  32. package/dist/module-install.js +133 -0
  33. package/dist/notifications.d.ts +48 -0
  34. package/dist/notifications.js +45 -0
  35. package/dist/observed-rates.d.ts +125 -0
  36. package/dist/observed-rates.js +158 -0
  37. package/dist/problems.d.ts +44 -0
  38. package/dist/problems.js +148 -0
  39. package/dist/quote.d.ts +123 -0
  40. package/dist/quote.js +167 -0
  41. package/dist/report-fixes.d.ts +89 -0
  42. package/dist/report-fixes.js +159 -0
  43. package/dist/runner.d.ts +376 -0
  44. package/dist/runner.js +353 -0
  45. package/dist/schemas/attack.d.ts +121 -0
  46. package/dist/schemas/attack.js +142 -0
  47. package/dist/schemas/attestation.d.ts +284 -0
  48. package/dist/schemas/attestation.js +175 -0
  49. package/dist/schemas/common.d.ts +22 -0
  50. package/dist/schemas/common.js +53 -0
  51. package/dist/schemas/mandate-commitment.d.ts +13 -0
  52. package/dist/schemas/mandate-commitment.js +37 -0
  53. package/dist/schemas/mandate.d.ts +170 -0
  54. package/dist/schemas/mandate.js +113 -0
  55. package/dist/self-serve.d.ts +133 -0
  56. package/dist/self-serve.js +110 -0
  57. package/dist/sentinel-cascade.d.ts +64 -0
  58. package/dist/sentinel-cascade.js +64 -0
  59. package/dist/sentinel.d.ts +133 -0
  60. package/dist/sentinel.js +101 -0
  61. package/dist/suggested-mandate.d.ts +81 -0
  62. package/dist/suggested-mandate.js +112 -0
  63. package/dist/tee.d.ts +61 -0
  64. package/dist/tee.js +93 -0
  65. package/dist/tiers.d.ts +19 -0
  66. package/dist/tiers.js +23 -0
  67. package/dist/troubleshooting-doc.d.ts +11 -0
  68. package/dist/troubleshooting-doc.js +46 -0
  69. package/package.json +59 -3
@@ -0,0 +1,133 @@
1
+ import { encodeAbiParameters, encodeFunctionData } from "viem";
2
+ import { mandateModuleFactoryAbi } from "./abis.js";
3
+ import { withMandateCommitment } from "./schemas/mandate-commitment.js";
4
+ /**
5
+ * The ERC-7579 account functions used to install a MandateModule and to batch calls.
6
+ * `mode` is the ERC-7579 ModeCode (bytes32).
7
+ */
8
+ export const erc7579AccountAbi = [
9
+ {
10
+ type: "function",
11
+ name: "execute",
12
+ stateMutability: "payable",
13
+ inputs: [
14
+ { name: "mode", type: "bytes32" },
15
+ { name: "executionCalldata", type: "bytes" },
16
+ ],
17
+ outputs: [],
18
+ },
19
+ {
20
+ type: "function",
21
+ name: "installModule",
22
+ stateMutability: "payable",
23
+ inputs: [
24
+ { name: "moduleTypeId", type: "uint256" },
25
+ { name: "module", type: "address" },
26
+ { name: "initData", type: "bytes" },
27
+ ],
28
+ outputs: [],
29
+ },
30
+ {
31
+ type: "function",
32
+ name: "uninstallModule",
33
+ stateMutability: "payable",
34
+ inputs: [
35
+ { name: "moduleTypeId", type: "uint256" },
36
+ { name: "module", type: "address" },
37
+ { name: "deInitData", type: "bytes" },
38
+ ],
39
+ outputs: [],
40
+ },
41
+ {
42
+ type: "function",
43
+ name: "isModuleInstalled",
44
+ stateMutability: "view",
45
+ inputs: [
46
+ { name: "moduleTypeId", type: "uint256" },
47
+ { name: "module", type: "address" },
48
+ { name: "additionalContext", type: "bytes" },
49
+ ],
50
+ outputs: [{ name: "", type: "bool" }],
51
+ },
52
+ ];
53
+ /** ERC-7579 module types MandateModule is installed as. */
54
+ export const MODULE_TYPE_EXECUTOR = 2n;
55
+ export const MODULE_TYPE_HOOK = 4n;
56
+ /** ERC-7579 mode for a batch of calls that reverts if any fails (call type 0x01). */
57
+ export const ERC7579_BATCH_MODE = `0x01${"00".repeat(31)}`;
58
+ /** Init data for installing MandateModule as a hook on `accountType`. */
59
+ export function hookInstallData(accountType) {
60
+ if (accountType === "erc7579")
61
+ return "0x";
62
+ // Safe7579: abi.encode(HookType.GLOBAL (0), bytes4(0), bytes(""))
63
+ return encodeAbiParameters([{ type: "uint8" }, { type: "bytes4" }, { type: "bytes" }], [0, "0x00000000", "0x"]);
64
+ }
65
+ /** The three calls, for a module address already known (see `prepareModuleInstall`). */
66
+ export function moduleInstallCalls(input) {
67
+ const params = withMandateCommitment(input.params);
68
+ const install = (moduleType, initData) => ({
69
+ to: input.account,
70
+ value: 0n,
71
+ data: encodeFunctionData({
72
+ abi: erc7579AccountAbi,
73
+ functionName: "installModule",
74
+ args: [moduleType, input.module, initData],
75
+ }),
76
+ });
77
+ return [
78
+ {
79
+ to: input.moduleFactory,
80
+ value: 0n,
81
+ data: encodeFunctionData({
82
+ abi: mandateModuleFactoryAbi,
83
+ functionName: "createModule",
84
+ args: [params, input.agent, input.guardian, input.agentId],
85
+ }),
86
+ },
87
+ install(MODULE_TYPE_EXECUTOR, "0x"),
88
+ install(MODULE_TYPE_HOOK, hookInstallData(input.accountType)),
89
+ ];
90
+ }
91
+ /** Calls as an ERC-7579 batch for `account.execute`. */
92
+ export function erc7579Batch(calls) {
93
+ const executionCalldata = encodeAbiParameters([
94
+ {
95
+ type: "tuple[]",
96
+ components: [
97
+ { name: "target", type: "address" },
98
+ { name: "value", type: "uint256" },
99
+ { name: "callData", type: "bytes" },
100
+ ],
101
+ },
102
+ ], [calls.map((c) => ({ target: c.to, value: c.value, callData: c.data }))]);
103
+ return {
104
+ mode: ERC7579_BATCH_MODE,
105
+ executionCalldata,
106
+ data: encodeFunctionData({
107
+ abi: erc7579AccountAbi,
108
+ functionName: "execute",
109
+ args: [ERC7579_BATCH_MODE, executionCalldata],
110
+ }),
111
+ };
112
+ }
113
+ /**
114
+ * Everything an account needs to create and install its MandateModule in one transaction:
115
+ * reads the module's future address from the factory, then builds createModule followed by
116
+ * installModule as executor and as hook. Send `calls` as a Safe MultiSend from the Safe, or
117
+ * `execute.data` to an ERC-7579 account (for example as a user operation's call data).
118
+ *
119
+ * The predicted address assumes nothing changes in between: the account creates no other
120
+ * module first and the vault factory's oracle stays the same.
121
+ */
122
+ export async function prepareModuleInstall(client, input) {
123
+ const params = withMandateCommitment(input.params);
124
+ const module = (await client.readContract({
125
+ address: input.moduleFactory,
126
+ abi: mandateModuleFactoryAbi,
127
+ functionName: "predictModule",
128
+ args: [input.account, params, input.agent, input.guardian, input.agentId],
129
+ }));
130
+ const calls = moduleInstallCalls({ ...input, params, module });
131
+ return { module, params, calls, execute: erc7579Batch(calls) };
132
+ }
133
+ //# sourceMappingURL=module-install.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What Bonded tells an agent's owner about (webhook, optional email and the in-app list,
3
+ * `GET /self/notifications/inbox`). Each type can be turned off in the owner's settings
4
+ * (`muted`). Each event is sent once: its `dedupeKey` names it.
5
+ */
6
+ export declare const NOTIFICATION_TYPES: {
7
+ readonly rating_finished: "A rating or preview finished.";
8
+ readonly rating_failed: "A rating or preview stopped.";
9
+ readonly rating_refused: "A rating or preview was refused (not attested).";
10
+ readonly score_expiring: "The agent's score expires in 7 days.";
11
+ readonly runner_offline: "The runner has been offline for over 30 minutes while a rating or renewal is due.";
12
+ readonly breach_recorded: "The vault recorded a breach.";
13
+ readonly claim_filed: "A claim was filed for a breach.";
14
+ readonly claim_paid: "A claim was paid.";
15
+ readonly cover_lapsing: "The cover's premium is past due (in its grace period) or the cover lapsed.";
16
+ readonly low_gas: "The agent key is low on gas.";
17
+ };
18
+ export type NotificationType = keyof typeof NOTIFICATION_TYPES;
19
+ export declare const NOTIFICATION_TYPE_LIST: NotificationType[];
20
+ /** How long before a score's expiry the owner is told. */
21
+ export declare const SCORE_EXPIRY_WARNING_SECONDS: number;
22
+ /** How long a runner may be away, while a rating or renewal is due, before the owner is told. */
23
+ export declare const RUNNER_OFFLINE_ALERT_MS: number;
24
+ /**
25
+ * The agent key pays gas for every `execute` through its vault. With no trade of its own to
26
+ * measure from, a trade is assumed to cost this much gas (an assumption, reported as such).
27
+ */
28
+ export declare const ASSUMED_EXECUTE_GAS = 350000n;
29
+ /** Gas is low when it covers fewer trades than this. */
30
+ export declare const LOW_GAS_TRADES = 20;
31
+ export interface AgentGas {
32
+ /** The agent key's ETH balance, in wei (decimal string) and in ETH. */
33
+ balanceWei: string;
34
+ balanceEth: number;
35
+ low: boolean;
36
+ /** Trades the balance pays for at the gas price now; null when the price is unknown. */
37
+ estimatedTradesLeft: number | null;
38
+ /** Where the per-trade cost comes from: the agent's own recent trades, or the assumption. */
39
+ basis: "measured" | "assumed";
40
+ gasPerTrade: string;
41
+ }
42
+ export declare function agentGasStatus(input: {
43
+ balanceWei: bigint;
44
+ gasPriceWei: bigint | null;
45
+ /** Median gas used by the agent's own recent trades, when there are some. */
46
+ measuredGasPerTrade?: bigint | null;
47
+ }): AgentGas;
48
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1,45 @@
1
+ /**
2
+ * What Bonded tells an agent's owner about (webhook, optional email and the in-app list,
3
+ * `GET /self/notifications/inbox`). Each type can be turned off in the owner's settings
4
+ * (`muted`). Each event is sent once: its `dedupeKey` names it.
5
+ */
6
+ export const NOTIFICATION_TYPES = {
7
+ rating_finished: "A rating or preview finished.",
8
+ rating_failed: "A rating or preview stopped.",
9
+ rating_refused: "A rating or preview was refused (not attested).",
10
+ score_expiring: "The agent's score expires in 7 days.",
11
+ runner_offline: "The runner has been offline for over 30 minutes while a rating or renewal is due.",
12
+ breach_recorded: "The vault recorded a breach.",
13
+ claim_filed: "A claim was filed for a breach.",
14
+ claim_paid: "A claim was paid.",
15
+ cover_lapsing: "The cover's premium is past due (in its grace period) or the cover lapsed.",
16
+ low_gas: "The agent key is low on gas.",
17
+ };
18
+ export const NOTIFICATION_TYPE_LIST = Object.keys(NOTIFICATION_TYPES);
19
+ /** How long before a score's expiry the owner is told. */
20
+ export const SCORE_EXPIRY_WARNING_SECONDS = 7 * 86_400;
21
+ /** How long a runner may be away, while a rating or renewal is due, before the owner is told. */
22
+ export const RUNNER_OFFLINE_ALERT_MS = 30 * 60_000;
23
+ /**
24
+ * The agent key pays gas for every `execute` through its vault. With no trade of its own to
25
+ * measure from, a trade is assumed to cost this much gas (an assumption, reported as such).
26
+ */
27
+ export const ASSUMED_EXECUTE_GAS = 350000n;
28
+ /** Gas is low when it covers fewer trades than this. */
29
+ export const LOW_GAS_TRADES = 20;
30
+ export function agentGasStatus(input) {
31
+ const gas = input.measuredGasPerTrade && input.measuredGasPerTrade > 0n
32
+ ? input.measuredGasPerTrade
33
+ : ASSUMED_EXECUTE_GAS;
34
+ const perTrade = input.gasPriceWei !== null ? gas * input.gasPriceWei : null;
35
+ const left = perTrade && perTrade > 0n ? Number(input.balanceWei / perTrade) : null;
36
+ return {
37
+ balanceWei: input.balanceWei.toString(),
38
+ balanceEth: Number(input.balanceWei) / 1e18,
39
+ low: input.balanceWei === 0n || (left !== null && left < LOW_GAS_TRADES),
40
+ estimatedTradesLeft: left,
41
+ basis: input.measuredGasPerTrade && input.measuredGasPerTrade > 0n ? "measured" : "assumed",
42
+ gasPerTrade: gas.toString(),
43
+ };
44
+ }
45
+ //# sourceMappingURL=notifications.js.map
@@ -0,0 +1,125 @@
1
+ import { type ClassGroup } from "./actuarial.js";
2
+ /**
3
+ * Observed breach rates in production (docs/SPECIFICATION.md Section 9.1, "λ ... is updated from
4
+ * telemetry across the insured book"). The Arena measures b, the chance an agent breaches when it
5
+ * meets an attack; λ, how often agents meet attacks in a year, is a published prior. Production
6
+ * breaches recorded by vaults are the evidence that can replace the prior, and this module turns
7
+ * them into rates with their counts and exposure, so every number travels with its sample size.
8
+ *
9
+ * The method is actual against expected:
10
+ *
11
+ * - exposure: the agent-years during which rated agents held a valid score;
12
+ * - expected breaches in group g: Σ over that exposure of λ_g · b̄_g · years, where b̄_g is the
13
+ * posterior mean breach rate from the agent's own rating, (k + 1) ÷ (n + 2);
14
+ * - observed breaches: the vault's breach log over the same exposure;
15
+ * - implied λ_g = prior λ_g · observed ÷ expected.
16
+ *
17
+ * A vault's breach record says which rule was broken, not which attack caused it, so most
18
+ * production breaches can't be put in a group. Those are counted as unattributed: they inform the
19
+ * book-wide ratio, never a group's λ. Pricing keeps the published priors until a group passes
20
+ * `OBSERVED_RATES_THRESHOLD` (see `pricingLambdas`).
21
+ */
22
+ /**
23
+ * When an observed λ may replace a group's prior: at least this much exposure, and at least this
24
+ * many breaches attributed to the group. Ten events gives a 90% interval of roughly ±50% on a
25
+ * Poisson rate, which is about as wide as the gap between the priors themselves; below that, the
26
+ * prior is the better estimate. Both numbers are judgement, written here so they can be reviewed.
27
+ */
28
+ export declare const OBSERVED_RATES_THRESHOLD: {
29
+ readonly minAgentYears: 25;
30
+ readonly minBreaches: 10;
31
+ };
32
+ /** One rated period of an agent: from a score's issue to its expiry or its replacement. */
33
+ export interface ExposurePeriod {
34
+ agentId: string;
35
+ /** Unix seconds. */
36
+ from: number;
37
+ /** Unix seconds; the score's expiry. */
38
+ to: number;
39
+ /** The rating's per-group episodes and breaches (`Scoring.groups`), by group id. */
40
+ groups: Record<string, {
41
+ episodes: number;
42
+ breaches: number;
43
+ }>;
44
+ }
45
+ /** One breach a production vault recorded. */
46
+ export interface ProductionBreach {
47
+ agentId: string;
48
+ /** Unix seconds. */
49
+ at: number;
50
+ /** The vault blocked it (Enforce mode). It still counts: the agent tried. */
51
+ blocked: boolean;
52
+ /** The attack group, when something attributes the breach to one (rare: see the module note). */
53
+ group?: string;
54
+ }
55
+ export interface GroupObservedRate {
56
+ id: string;
57
+ name: string;
58
+ /** The published prior (`CLASS_GROUPS`). */
59
+ priorLambda: number;
60
+ /** Breaches attributed to this group within exposure. */
61
+ breaches: number;
62
+ /** What the priors and the agents' own ratings predict over the same exposure. */
63
+ expectedBreaches: number;
64
+ /** Attributed breaches per agent-year (0 with no exposure). */
65
+ ratePerAgentYear: number;
66
+ /** 90% upper bound on that rate (Poisson); null with no exposure. */
67
+ rateUpper90: number | null;
68
+ /** prior × observed ÷ expected; null when nothing is expected. */
69
+ impliedLambda: number | null;
70
+ /** What pricing uses: the prior, until this group passes the threshold. */
71
+ pricingLambda: number;
72
+ usingObserved: boolean;
73
+ }
74
+ export interface ObservedRates {
75
+ exposure: {
76
+ agentYears: number;
77
+ agents: number;
78
+ periods: number;
79
+ };
80
+ breaches: {
81
+ /** Every breach given, in exposure or not. */
82
+ total: number;
83
+ /** Breaches while the agent held a valid score: the ones these rates use. */
84
+ inExposure: number;
85
+ /** Breaches by agents without a valid score at the time: not used. */
86
+ outsideExposure: number;
87
+ blocked: number;
88
+ attributed: number;
89
+ unattributed: number;
90
+ };
91
+ /** The whole book: every in-exposure breach against every group's expected breaches. */
92
+ overall: {
93
+ breaches: number;
94
+ expectedBreaches: number;
95
+ /** observed ÷ expected; null when nothing is expected. */
96
+ actualToExpected: number | null;
97
+ ratePerAgentYear: number;
98
+ rateUpper90: number | null;
99
+ };
100
+ groups: GroupObservedRate[];
101
+ threshold: typeof OBSERVED_RATES_THRESHOLD;
102
+ /** True when any group's pricing λ comes from observation. */
103
+ usingObserved: boolean;
104
+ }
105
+ /**
106
+ * Estimates observed breach rates from rated exposure and production breaches. Overlapping
107
+ * periods of one agent are cut where the newer score starts, so time is never counted twice.
108
+ * Periods and breaches after `now` are ignored.
109
+ */
110
+ export declare function estimateObservedRates(input: {
111
+ periods: readonly ExposurePeriod[];
112
+ breaches: readonly ProductionBreach[];
113
+ /** Unix seconds. */
114
+ now: number;
115
+ groups?: readonly ClassGroup[];
116
+ threshold?: typeof OBSERVED_RATES_THRESHOLD;
117
+ }): ObservedRates;
118
+ /** The λ each group's pricing uses: the published prior unless observation passed the threshold. */
119
+ export declare function pricingLambdas(rates: ObservedRates): Record<string, number>;
120
+ /**
121
+ * Upper bound on a Poisson mean after observing k events: the μ at which P(X ≤ k) = 1 − level.
122
+ * Solved by bisection on the exact CDF.
123
+ */
124
+ export declare function poissonUpperBound(k: number, level?: number): number;
125
+ //# sourceMappingURL=observed-rates.d.ts.map
@@ -0,0 +1,158 @@
1
+ import { CLASS_GROUPS } from "./actuarial.js";
2
+ /**
3
+ * Observed breach rates in production (docs/SPECIFICATION.md Section 9.1, "λ ... is updated from
4
+ * telemetry across the insured book"). The Arena measures b, the chance an agent breaches when it
5
+ * meets an attack; λ, how often agents meet attacks in a year, is a published prior. Production
6
+ * breaches recorded by vaults are the evidence that can replace the prior, and this module turns
7
+ * them into rates with their counts and exposure, so every number travels with its sample size.
8
+ *
9
+ * The method is actual against expected:
10
+ *
11
+ * - exposure: the agent-years during which rated agents held a valid score;
12
+ * - expected breaches in group g: Σ over that exposure of λ_g · b̄_g · years, where b̄_g is the
13
+ * posterior mean breach rate from the agent's own rating, (k + 1) ÷ (n + 2);
14
+ * - observed breaches: the vault's breach log over the same exposure;
15
+ * - implied λ_g = prior λ_g · observed ÷ expected.
16
+ *
17
+ * A vault's breach record says which rule was broken, not which attack caused it, so most
18
+ * production breaches can't be put in a group. Those are counted as unattributed: they inform the
19
+ * book-wide ratio, never a group's λ. Pricing keeps the published priors until a group passes
20
+ * `OBSERVED_RATES_THRESHOLD` (see `pricingLambdas`).
21
+ */
22
+ /**
23
+ * When an observed λ may replace a group's prior: at least this much exposure, and at least this
24
+ * many breaches attributed to the group. Ten events gives a 90% interval of roughly ±50% on a
25
+ * Poisson rate, which is about as wide as the gap between the priors themselves; below that, the
26
+ * prior is the better estimate. Both numbers are judgement, written here so they can be reviewed.
27
+ */
28
+ export const OBSERVED_RATES_THRESHOLD = { minAgentYears: 25, minBreaches: 10 };
29
+ const YEAR_SECONDS = 365.25 * 86_400;
30
+ /**
31
+ * Estimates observed breach rates from rated exposure and production breaches. Overlapping
32
+ * periods of one agent are cut where the newer score starts, so time is never counted twice.
33
+ * Periods and breaches after `now` are ignored.
34
+ */
35
+ export function estimateObservedRates(input) {
36
+ const groups = input.groups ?? CLASS_GROUPS;
37
+ const threshold = input.threshold ?? OBSERVED_RATES_THRESHOLD;
38
+ const byAgent = new Map();
39
+ for (const p of input.periods) {
40
+ const list = byAgent.get(p.agentId) ?? [];
41
+ list.push(p);
42
+ byAgent.set(p.agentId, list);
43
+ }
44
+ // Each agent's periods, in order, each ending where the next starts (a newer score replaces
45
+ // the older one) and none past now.
46
+ const periods = [];
47
+ for (const list of byAgent.values()) {
48
+ list.sort((a, b) => a.from - b.from);
49
+ list.forEach((p, i) => {
50
+ const next = list[i + 1];
51
+ const to = Math.min(p.to, next ? next.from : Infinity, input.now);
52
+ if (to > p.from)
53
+ periods.push({ ...p, to });
54
+ });
55
+ }
56
+ const years = (p) => (p.to - p.from) / YEAR_SECONDS;
57
+ const agentYears = sum(periods.map(years));
58
+ const expected = new Map(groups.map((g) => [
59
+ g.id,
60
+ sum(periods.map((p) => {
61
+ const r = p.groups[g.id] ?? { episodes: 0, breaches: 0 };
62
+ return g.lambda * ((r.breaches + 1) / (r.episodes + 2)) * years(p);
63
+ })),
64
+ ]));
65
+ const covered = (b) => b.at <= input.now &&
66
+ periods.some((p) => p.agentId === b.agentId && b.at >= p.from && b.at < p.to);
67
+ const counted = input.breaches.filter(covered);
68
+ const known = new Set(groups.map((g) => g.id));
69
+ const attributed = counted.filter((b) => b.group !== undefined && known.has(b.group));
70
+ const rows = groups.map((g) => {
71
+ const breaches = attributed.filter((b) => b.group === g.id).length;
72
+ const exp = expected.get(g.id) ?? 0;
73
+ const impliedLambda = exp > 0 ? (g.lambda * breaches) / exp : null;
74
+ const usingObserved = impliedLambda !== null &&
75
+ agentYears >= threshold.minAgentYears &&
76
+ breaches >= threshold.minBreaches;
77
+ return {
78
+ id: g.id,
79
+ name: g.name,
80
+ priorLambda: g.lambda,
81
+ breaches,
82
+ expectedBreaches: exp,
83
+ ratePerAgentYear: agentYears > 0 ? breaches / agentYears : 0,
84
+ rateUpper90: agentYears > 0 ? poissonUpperBound(breaches) / agentYears : null,
85
+ impliedLambda,
86
+ pricingLambda: usingObserved ? impliedLambda : g.lambda,
87
+ usingObserved,
88
+ };
89
+ });
90
+ const expectedTotal = sum([...expected.values()]);
91
+ return {
92
+ exposure: {
93
+ agentYears,
94
+ agents: new Set(periods.map((p) => p.agentId)).size,
95
+ periods: periods.length,
96
+ },
97
+ breaches: {
98
+ total: input.breaches.length,
99
+ inExposure: counted.length,
100
+ outsideExposure: input.breaches.length - counted.length,
101
+ blocked: counted.filter((b) => b.blocked).length,
102
+ attributed: attributed.length,
103
+ unattributed: counted.length - attributed.length,
104
+ },
105
+ overall: {
106
+ breaches: counted.length,
107
+ expectedBreaches: expectedTotal,
108
+ actualToExpected: expectedTotal > 0 ? counted.length / expectedTotal : null,
109
+ ratePerAgentYear: agentYears > 0 ? counted.length / agentYears : 0,
110
+ rateUpper90: agentYears > 0 ? poissonUpperBound(counted.length) / agentYears : null,
111
+ },
112
+ groups: rows,
113
+ threshold,
114
+ usingObserved: rows.some((r) => r.usingObserved),
115
+ };
116
+ }
117
+ /** The λ each group's pricing uses: the published prior unless observation passed the threshold. */
118
+ export function pricingLambdas(rates) {
119
+ return Object.fromEntries(rates.groups.map((g) => [g.id, g.pricingLambda]));
120
+ }
121
+ /**
122
+ * Upper bound on a Poisson mean after observing k events: the μ at which P(X ≤ k) = 1 − level.
123
+ * Solved by bisection on the exact CDF.
124
+ */
125
+ export function poissonUpperBound(k, level = 0.9) {
126
+ if (!Number.isInteger(k) || k < 0)
127
+ throw new RangeError(`need an integer k >= 0, got ${k}`);
128
+ if (!(level > 0 && level < 1))
129
+ throw new RangeError(`level must be in (0, 1), got ${level}`);
130
+ let lo = 0;
131
+ let hi = Math.max(10, 4 * (k + 1));
132
+ while (poissonCdf(k, hi) > 1 - level)
133
+ hi *= 2;
134
+ for (let i = 0; i < 80; i++) {
135
+ const mid = (lo + hi) / 2;
136
+ if (poissonCdf(k, mid) > 1 - level)
137
+ lo = mid;
138
+ else
139
+ hi = mid;
140
+ }
141
+ return (lo + hi) / 2;
142
+ }
143
+ /** P(X ≤ k) for X ~ Poisson(mu), summed in log space so a large mu doesn't underflow. */
144
+ function poissonCdf(k, mu) {
145
+ if (mu <= 0)
146
+ return 1;
147
+ let logTerm = -mu;
148
+ let total = Math.exp(logTerm);
149
+ for (let i = 1; i <= k; i++) {
150
+ logTerm += Math.log(mu) - Math.log(i);
151
+ total += Math.exp(logTerm);
152
+ }
153
+ return Math.min(1, total);
154
+ }
155
+ function sum(values) {
156
+ return values.reduce((total, v) => total + v, 0);
157
+ }
158
+ //# sourceMappingURL=observed-rates.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Every problem, warning and refusal a builder can see, with a stable `code`, the sentence (or the
3
+ * gist of it, when the real one carries numbers or times) and what to do about it. The API puts
4
+ * the code next to each sentence (`code` beside `error`, `issues[].code` beside check problems,
5
+ * `problems[].code` in the health panel), the web app links each message to its entry on the
6
+ * troubleshooting page by code (`#<code>`), and the docs page is built from `PROBLEMS`.
7
+ *
8
+ * Codes never change once published: a new message gets a new entry. `match` is how a sentence
9
+ * the services already produce is recognised, so codes don't depend on every call site.
10
+ */
11
+ export type ProblemArea = "sign-in" | "requests" | "registration" | "connection" | "check" | "rating" | "cover" | "vault" | "notifications" | "service";
12
+ export interface ProblemEntry {
13
+ code: string;
14
+ area: ProblemArea;
15
+ /** The message, or its gist when the real one carries numbers, ids or times. */
16
+ sentence: string;
17
+ /** What to do about it, in plain words. */
18
+ fix: string;
19
+ /** How the sentences the services produce are recognised. The first entry that matches wins. */
20
+ match: readonly RegExp[];
21
+ }
22
+ /** The catch-all for a sentence no entry recognises (it still has its own words). */
23
+ export declare const UNKNOWN_PROBLEM = "other";
24
+ /** Ordered: specific entries before general ones. */
25
+ export declare const PROBLEMS: readonly ProblemEntry[];
26
+ /** The stable code for a sentence (`UNKNOWN_PROBLEM` when no entry recognises it). */
27
+ export declare function problemCode(sentence: string): string;
28
+ /** A sentence with its code and fix, as the API returns problems. */
29
+ export interface CodedProblem {
30
+ code: string;
31
+ text: string;
32
+ fix?: string;
33
+ }
34
+ export declare function codedProblem(text: string, code?: string): CodedProblem;
35
+ /** The entry for a code, if there is one. */
36
+ export declare function problemEntry(code: string): ProblemEntry | undefined;
37
+ /** The troubleshooting list for the docs page: code, sentence and fix, without the matchers. */
38
+ export declare function troubleshootingList(): {
39
+ code: string;
40
+ area: ProblemArea;
41
+ sentence: string;
42
+ fix: string;
43
+ }[];
44
+ //# sourceMappingURL=problems.d.ts.map