@holoscript/plugin-insurance 2.0.1 → 2.0.2

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/src/actuarial.ts CHANGED
@@ -15,28 +15,25 @@
15
15
  * London, D. "Survival Models and Their Estimation" (3rd ed., 1997).
16
16
  */
17
17
 
18
- import {
19
- DOMAIN_SIMULATION_RECEIPT_SCHEMA,
20
- buildDomainSimulationReceipt,
21
- } from '@holoscript/core';
18
+ import { DOMAIN_SIMULATION_RECEIPT_SCHEMA, buildDomainSimulationReceipt } from '@holoscript/core';
22
19
 
23
20
  // ─── Types ────────────────────────────────────────────────────────────────────
24
21
 
25
22
  /** One row of a standard life table (age x). */
26
23
  export interface LifeTableRow {
27
- age: number; // x — integer age
28
- qx: number; // probability of death between age x and x+1
29
- lx: number; // number of survivors at age x (radix l₀ = 100 000)
30
- dx: number; // deaths between x and x+1
31
- Lx: number; // person-years lived between x and x+1
32
- Tx: number; // total person-years lived above age x
33
- ex: number; // curtate life expectancy at age x
24
+ age: number; // x — integer age
25
+ qx: number; // probability of death between age x and x+1
26
+ lx: number; // number of survivors at age x (radix l₀ = 100 000)
27
+ dx: number; // deaths between x and x+1
28
+ Lx: number; // person-years lived between x and x+1
29
+ Tx: number; // total person-years lived above age x
30
+ ex: number; // curtate life expectancy at age x
34
31
  }
35
32
 
36
33
  /** Abridged model life table — minimum required: age 0 through at least age 100. */
37
34
  export interface LifeTable {
38
- id: string;
39
- rows: LifeTableRow[];
35
+ id: string;
36
+ rows: LifeTableRow[];
40
37
  /** Annual effective interest rate (e.g. 0.05 = 5%) */
41
38
  interestRate: number;
42
39
  }
@@ -44,63 +41,63 @@ export interface LifeTable {
44
41
  export type PolicyType = 'whole_life' | 'term_life' | 'endowment' | 'annuity_due';
45
42
 
46
43
  export interface PolicySpec {
47
- type: PolicyType;
48
- issueAge: number; // age at policy issue
49
- termYears?: number; // required for term_life and endowment
50
- benefitAmount: number; // face amount (currency units)
44
+ type: PolicyType;
45
+ issueAge: number; // age at policy issue
46
+ termYears?: number; // required for term_life and endowment
47
+ benefitAmount: number; // face amount (currency units)
51
48
  /** Annual effective interest rate override; falls back to table default */
52
49
  interestRate?: number;
53
50
  }
54
51
 
55
52
  export interface ActuarialResult {
56
53
  /** Net Single Premium — expected present value of benefits */
57
- nsp: number;
54
+ nsp: number;
58
55
  /** Level annual premium (equivalence principle) */
59
56
  annualPremium: number;
60
57
  /** Curtate life expectancy at issue age */
61
58
  lifeExpectancy: number;
62
59
  /** Present value of an annuity-due of 1 per year at issue age */
63
- annuityDue: number;
60
+ annuityDue: number;
64
61
  /** Policy type */
65
- policyType: PolicyType;
66
- issueAge: number;
67
- termYears: number | null;
62
+ policyType: PolicyType;
63
+ issueAge: number;
64
+ termYears: number | null;
68
65
  }
69
66
 
70
67
  export interface CashFlow {
71
68
  /** Period index (0-based; typically year number) */
72
- period: number;
69
+ period: number;
73
70
  /** Cash inflow (+) or outflow (−) */
74
- amount: number;
71
+ amount: number;
75
72
  }
76
73
 
77
74
  export interface NPVResult {
78
- npv: number;
79
- irr: number | null; // null if no real IRR found in 0–200% range
75
+ npv: number;
76
+ irr: number | null; // null if no real IRR found in 0–200% range
80
77
  paybackPeriod: number | null; // period where cumulative CF first ≥ 0
81
78
  }
82
79
 
83
80
  export interface VaRResult {
84
- confidenceLevel: number; // e.g. 0.95
85
- var95: number; // VaR at 95%
86
- var99: number; // VaR at 99%
87
- cvar95: number; // CVaR (expected shortfall) at 95%
88
- cvar99: number; // CVaR at 99%
89
- expectedLoss: number;
90
- maxLoss: number;
91
- sampleSize: number;
81
+ confidenceLevel: number; // e.g. 0.95
82
+ var95: number; // VaR at 95%
83
+ var99: number; // VaR at 99%
84
+ cvar95: number; // CVaR (expected shortfall) at 95%
85
+ cvar99: number; // CVaR at 99%
86
+ expectedLoss: number;
87
+ maxLoss: number;
88
+ sampleSize: number;
92
89
  }
93
90
 
94
91
  export interface ActuarialReceipt {
95
- plugin: string;
96
- runId: string;
97
- payloadHash: string;
92
+ plugin: string;
93
+ runId: string;
94
+ payloadHash: string;
98
95
  hashAlgorithm: string;
99
- cael: { event: string; schemaVersion: string; ts: string };
100
- acceptance: { accepted: boolean; violations: string[] };
96
+ cael: { event: string; schemaVersion: string; ts: string };
97
+ acceptance: { accepted: boolean; violations: string[] };
101
98
  resultSummary: {
102
- nsp: number;
103
- annualPremium: number;
99
+ nsp: number;
100
+ annualPremium: number;
104
101
  lifeExpectancy: number;
105
102
  };
106
103
  }
@@ -115,7 +112,8 @@ const RADIX = 100_000;
115
112
  */
116
113
  export function buildLifeTable(id: string, qxByAge: number[], interestRate: number): LifeTable {
117
114
  if (qxByAge.length < 2) throw new Error('[actuarial] qxByAge must have at least 2 entries');
118
- if (qxByAge.some((q) => q < 0 || q > 1)) throw new Error('[actuarial] all qx values must be in [0,1]');
115
+ if (qxByAge.some((q) => q < 0 || q > 1))
116
+ throw new Error('[actuarial] all qx values must be in [0,1]');
119
117
  if (interestRate < 0) throw new Error('[actuarial] interestRate must be ≥ 0');
120
118
 
121
119
  const rows: LifeTableRow[] = [];
@@ -154,12 +152,12 @@ export function buildLifeTable(id: string, qxByAge: number[], interestRate: numb
154
152
  for (let x = 0; x < n; x++) {
155
153
  rows.push({
156
154
  age: x,
157
- qx: qxByAge[x],
158
- lx: lx_arr[x],
159
- dx: dx_arr[x],
160
- Lx: Lx_arr[x],
161
- Tx: Tx_arr[x],
162
- ex: ex_arr[x],
155
+ qx: qxByAge[x],
156
+ lx: lx_arr[x],
157
+ dx: dx_arr[x],
158
+ Lx: Lx_arr[x],
159
+ Tx: Tx_arr[x],
160
+ ex: ex_arr[x],
163
161
  });
164
162
  }
165
163
 
@@ -177,13 +175,19 @@ export function buildLifeTable(id: string, qxByAge: number[], interestRate: numb
177
175
  * Cx = v^(x+1) · dx (discounted deaths)
178
176
  * Mx = Σ(x to ω) Cx (accumulated Cx)
179
177
  */
180
- function commutationColumns(table: LifeTable, iRate: number): {
181
- Dx: number[]; Nx: number[]; Cx: number[]; Mx: number[];
178
+ function commutationColumns(
179
+ table: LifeTable,
180
+ iRate: number
181
+ ): {
182
+ Dx: number[];
183
+ Nx: number[];
184
+ Cx: number[];
185
+ Mx: number[];
182
186
  } {
183
- const v = 1 / (1 + iRate);
184
- const n = table.rows.length;
185
- const Dx = table.rows.map((r) => Math.pow(v, r.age) * r.lx);
186
- const Cx = table.rows.map((r) => Math.pow(v, r.age + 1) * r.dx);
187
+ const v = 1 / (1 + iRate);
188
+ const n = table.rows.length;
189
+ const Dx = table.rows.map((r) => Math.pow(v, r.age) * r.lx);
190
+ const Cx = table.rows.map((r) => Math.pow(v, r.age + 1) * r.dx);
187
191
 
188
192
  const Nx: number[] = new Array(n).fill(0);
189
193
  const Mx: number[] = new Array(n).fill(0);
@@ -209,90 +213,91 @@ function commutationColumns(table: LifeTable, iRate: number): {
209
213
  * annuity_due — benefit B paid at START of each year while alive (no death benefit)
210
214
  */
211
215
  export function computeActuarialValues(spec: PolicySpec, table: LifeTable): ActuarialResult {
212
- const n = table.rows.length;
213
- const iRate = spec.interestRate ?? table.interestRate;
214
- const x = spec.issueAge;
215
- const B = spec.benefitAmount;
216
+ const n = table.rows.length;
217
+ const iRate = spec.interestRate ?? table.interestRate;
218
+ const x = spec.issueAge;
219
+ const B = spec.benefitAmount;
216
220
 
217
- if (x < 0 || x >= n) throw new Error(`[actuarial] issueAge ${x} out of table range [0, ${n - 1}]`);
218
- if (iRate < 0) throw new Error('[actuarial] interestRate must be 0');
221
+ if (x < 0 || x >= n)
222
+ throw new Error(`[actuarial] issueAge ${x} out of table range [0, ${n - 1}]`);
223
+ if (iRate < 0) throw new Error('[actuarial] interestRate must be ≥ 0');
219
224
 
220
225
  const { Dx, Nx, Mx } = commutationColumns(table, iRate);
221
226
 
222
- const row = table.rows[x];
223
- const lifeExp = row.ex;
227
+ const row = table.rows[x];
228
+ const lifeExp = row.ex;
224
229
 
225
230
  // ── Whole life ──────────────────────────────────────────────────────────────
226
231
  if (spec.type === 'whole_life') {
227
- const Ax = Mx[x] / Dx[x]; // net single premium per unit benefit
228
- const annuity = Nx[x] / Dx[x]; // ä_x (annuity-due of 1)
229
- const nsp = B * Ax;
230
- const Px = Ax / annuity; // level premium per unit benefit per year
232
+ const Ax = Mx[x] / Dx[x]; // net single premium per unit benefit
233
+ const annuity = Nx[x] / Dx[x]; // ä_x (annuity-due of 1)
234
+ const nsp = B * Ax;
235
+ const Px = Ax / annuity; // level premium per unit benefit per year
231
236
  return {
232
237
  nsp,
233
238
  annualPremium: B * Px,
234
239
  lifeExpectancy: lifeExp,
235
- annuityDue: annuity,
236
- policyType: 'whole_life',
237
- issueAge: x,
238
- termYears: null,
240
+ annuityDue: annuity,
241
+ policyType: 'whole_life',
242
+ issueAge: x,
243
+ termYears: null,
239
244
  };
240
245
  }
241
246
 
242
247
  // ── Term life ───────────────────────────────────────────────────────────────
243
248
  if (spec.type === 'term_life') {
244
249
  const term = spec.termYears ?? 20;
245
- const xn = Math.min(x + term, n - 1);
246
- const A1xn = (Mx[x] - Mx[xn]) / Dx[x]; // A^1_{x:n} term insurance per unit
247
- const axn = (Nx[x] - Nx[xn]) / Dx[x]; // ä_{x:n} term annuity-due per unit
248
- const nsp = B * A1xn;
250
+ const xn = Math.min(x + term, n - 1);
251
+ const A1xn = (Mx[x] - Mx[xn]) / Dx[x]; // A^1_{x:n} term insurance per unit
252
+ const axn = (Nx[x] - Nx[xn]) / Dx[x]; // ä_{x:n} term annuity-due per unit
253
+ const nsp = B * A1xn;
249
254
  return {
250
255
  nsp,
251
256
  annualPremium: axn > 0 ? nsp / axn : 0,
252
257
  lifeExpectancy: lifeExp,
253
- annuityDue: axn,
254
- policyType: 'term_life',
255
- issueAge: x,
256
- termYears: term,
258
+ annuityDue: axn,
259
+ policyType: 'term_life',
260
+ issueAge: x,
261
+ termYears: term,
257
262
  };
258
263
  }
259
264
 
260
265
  // ── Endowment ───────────────────────────────────────────────────────────────
261
266
  if (spec.type === 'endowment') {
262
267
  const term = spec.termYears ?? 20;
263
- const xn = Math.min(x + term, n - 1);
268
+ const xn = Math.min(x + term, n - 1);
264
269
  // Endowment = term life + pure endowment (survival to n)
265
- const A1xn = (Mx[x] - Mx[xn]) / Dx[x]; // insurance component
266
- const Exn = Dx[xn] / Dx[x]; // pure endowment (n Ex)
267
- const Axn = A1xn + Exn; // endowment NSP per unit
268
- const axn = (Nx[x] - Nx[xn]) / Dx[x];
269
- const nsp = B * Axn;
270
+ const A1xn = (Mx[x] - Mx[xn]) / Dx[x]; // insurance component
271
+ const Exn = Dx[xn] / Dx[x]; // pure endowment (n Ex)
272
+ const Axn = A1xn + Exn; // endowment NSP per unit
273
+ const axn = (Nx[x] - Nx[xn]) / Dx[x];
274
+ const nsp = B * Axn;
270
275
  return {
271
276
  nsp,
272
277
  annualPremium: axn > 0 ? nsp / axn : 0,
273
278
  lifeExpectancy: lifeExp,
274
- annuityDue: axn,
275
- policyType: 'endowment',
276
- issueAge: x,
277
- termYears: term,
279
+ annuityDue: axn,
280
+ policyType: 'endowment',
281
+ issueAge: x,
282
+ termYears: term,
278
283
  };
279
284
  }
280
285
 
281
286
  // ── Life annuity-due ────────────────────────────────────────────────────────
282
287
  if (spec.type === 'annuity_due') {
283
- const term = spec.termYears;
288
+ const term = spec.termYears;
284
289
  let annuityDue: number;
285
290
  let termYears: number | null;
286
291
 
287
292
  if (term != null) {
288
293
  // Temporary life annuity-due ä_{x:n}
289
- const xn = Math.min(x + term, n - 1);
294
+ const xn = Math.min(x + term, n - 1);
290
295
  annuityDue = (Nx[x] - Nx[xn]) / Dx[x];
291
- termYears = term;
296
+ termYears = term;
292
297
  } else {
293
298
  // Whole life annuity-due ä_x
294
299
  annuityDue = Nx[x] / Dx[x];
295
- termYears = null;
300
+ termYears = null;
296
301
  }
297
302
 
298
303
  const nsp = B * annuityDue;
@@ -301,8 +306,8 @@ export function computeActuarialValues(spec: PolicySpec, table: LifeTable): Actu
301
306
  annualPremium: 0, // annuities don't have a separately stated premium
302
307
  lifeExpectancy: lifeExp,
303
308
  annuityDue,
304
- policyType: 'annuity_due',
305
- issueAge: x,
309
+ policyType: 'annuity_due',
310
+ issueAge: x,
306
311
  termYears,
307
312
  };
308
313
  }
@@ -318,7 +323,7 @@ export function computeActuarialValues(spec: PolicySpec, table: LifeTable): Actu
318
323
  */
319
324
  export function computeNPV(cashFlows: CashFlow[], discountRate: number): NPVResult {
320
325
  if (cashFlows.length === 0) throw new Error('[actuarial] cashFlows must not be empty');
321
- if (discountRate < 0) throw new Error('[actuarial] discountRate must be ≥ 0');
326
+ if (discountRate < 0) throw new Error('[actuarial] discountRate must be ≥ 0');
322
327
 
323
328
  const sorted = [...cashFlows].sort((a, b) => a.period - b.period);
324
329
 
@@ -329,18 +334,25 @@ export function computeNPV(cashFlows: CashFlow[], discountRate: number): NPVResu
329
334
  let paybackPeriod: number | null = null;
330
335
  for (const cf of sorted) {
331
336
  cum += cf.amount;
332
- if (cum >= 0 && paybackPeriod === null) { paybackPeriod = cf.period; }
337
+ if (cum >= 0 && paybackPeriod === null) {
338
+ paybackPeriod = cf.period;
339
+ }
333
340
  }
334
341
 
335
342
  // IRR via bisection (finds rate where NPV = 0)
336
343
  let irr: number | null = null;
337
344
  const npvAt = (r: number) => sorted.reduce((s, cf) => s + cf.amount / (1 + r) ** cf.period, 0);
338
- const lo = 0, hi = 2.0; // 0% to 200%
345
+ const lo = 0,
346
+ hi = 2.0; // 0% to 200%
339
347
  if (Math.sign(npvAt(lo)) !== Math.sign(npvAt(hi))) {
340
- let a = lo, b = hi;
348
+ let a = lo,
349
+ b = hi;
341
350
  for (let i = 0; i < 100; i++) {
342
351
  const mid = (a + b) / 2;
343
- if (Math.abs(b - a) < 1e-10) { irr = mid; break; }
352
+ if (Math.abs(b - a) < 1e-10) {
353
+ irr = mid;
354
+ break;
355
+ }
344
356
  Math.sign(npvAt(mid)) === Math.sign(npvAt(a)) ? (a = mid) : (b = mid);
345
357
  irr = (a + b) / 2;
346
358
  }
@@ -359,7 +371,7 @@ export function computeVaR(losses: number[]): VaRResult {
359
371
  if (losses.length < 2) throw new Error('[actuarial] VaR requires at least 2 loss observations');
360
372
 
361
373
  const sorted = [...losses].sort((a, b) => a - b);
362
- const n = sorted.length;
374
+ const n = sorted.length;
363
375
 
364
376
  const idx95 = Math.ceil(0.95 * n) - 1;
365
377
  const idx99 = Math.ceil(0.99 * n) - 1;
@@ -374,7 +386,7 @@ export function computeVaR(losses: number[]): VaRResult {
374
386
  const cvar99 = tail99.reduce((s, v) => s + v, 0) / tail99.length;
375
387
 
376
388
  const expectedLoss = sorted.reduce((s, v) => s + v, 0) / n;
377
- const maxLoss = sorted[n - 1];
389
+ const maxLoss = sorted[n - 1];
378
390
 
379
391
  return {
380
392
  confidenceLevel: 0.95,
@@ -400,13 +412,16 @@ export function computeVaR(losses: number[]): VaRResult {
400
412
  *
401
413
  * Returns qx values for ages 0 .. maxAge.
402
414
  */
403
- export function gompertzMakehamQx(params: { A: number; B: number; c: number }, maxAge: number): number[] {
415
+ export function gompertzMakehamQx(
416
+ params: { A: number; B: number; c: number },
417
+ maxAge: number
418
+ ): number[] {
404
419
  const { A, B, c } = params;
405
420
  if (B <= 0 || c <= 1) throw new Error('[actuarial] Gompertz params require B>0 and c>1');
406
421
  const lnC = Math.log(c);
407
422
  return Array.from({ length: maxAge + 1 }, (_, x) => {
408
423
  // qx ≈ 1 − exp(−∫ₓˣ⁺¹ μ(t) dt) = 1 − exp(−A − B(cˣ)(c−1)/ln(c))
409
- const integral = A + B * Math.pow(c, x) * (c - 1) / lnC;
424
+ const integral = A + (B * Math.pow(c, x) * (c - 1)) / lnC;
410
425
  return Math.min(1, 1 - Math.exp(-integral));
411
426
  });
412
427
  }
@@ -414,34 +429,37 @@ export function gompertzMakehamQx(params: { A: number; B: number; c: number }, m
414
429
  // ─── Receipt ─────────────────────────────────────────────────────────────────
415
430
 
416
431
  export function buildActuarialReceipt(
417
- result: ActuarialResult,
418
- options?: { runId?: string },
432
+ result: ActuarialResult,
433
+ options?: { runId?: string }
419
434
  ): ActuarialReceipt {
420
435
  const violations: Array<{ criterion: string; message: string }> = [];
421
436
  if (result.nsp < 0)
422
437
  violations.push({ criterion: 'nsp', message: `negative NSP: ${result.nsp.toFixed(4)}` });
423
438
  if (result.annualPremium < 0)
424
- violations.push({ criterion: 'premium', message: `negative annual premium: ${result.annualPremium.toFixed(4)}` });
439
+ violations.push({
440
+ criterion: 'premium',
441
+ message: `negative annual premium: ${result.annualPremium.toFixed(4)}`,
442
+ });
425
443
 
426
444
  const raw = buildDomainSimulationReceipt({
427
- plugin: 'insurance',
445
+ plugin: 'insurance',
428
446
  pluginVersion: '1.0.0',
429
- runId: options?.runId ?? `act-${Date.now().toString(36)}`,
430
- modelId: `${result.policyType}-age${result.issueAge}`,
447
+ runId: options?.runId ?? `act-${Date.now().toString(36)}`,
448
+ modelId: `${result.policyType}-age${result.issueAge}`,
431
449
  solverConfig: {
432
- solverType: 'commutation-columns',
433
- scale: 'individual-policy',
434
- policyType: result.policyType,
435
- issueAge: result.issueAge,
450
+ solverType: 'commutation-columns',
451
+ scale: 'individual-policy',
452
+ policyType: result.policyType,
453
+ issueAge: result.issueAge,
436
454
  },
437
455
  resultSummary: {
438
- nsp: +result.nsp.toFixed(4),
439
- annualPremium: +result.annualPremium.toFixed(4),
456
+ nsp: +result.nsp.toFixed(4),
457
+ annualPremium: +result.annualPremium.toFixed(4),
440
458
  lifeExpectancy: +result.lifeExpectancy.toFixed(2),
441
459
  },
442
460
  cael: {
443
- version: 'cael.v1',
444
- event: 'insurance.actuarial',
461
+ version: 'cael.v1',
462
+ event: 'insurance.actuarial',
445
463
  solverType: 'insurance.commutation-columns',
446
464
  },
447
465
  acceptance: { accepted: violations.length === 0, violations },
@@ -100,7 +100,7 @@ export interface HomeownersModelWeights {
100
100
  */
101
101
  export function createHomeownersModel(
102
102
  weights: HomeownersModelWeights,
103
- modelId = 'homeowners-underwriting-scorer',
103
+ modelId = 'homeowners-underwriting-scorer'
104
104
  ): FairnessModel {
105
105
  return {
106
106
  id: modelId,
@@ -129,10 +129,10 @@ export function createHomeownersModel(
129
129
  export const BIASED_WEIGHTS: HomeownersModelWeights = {
130
130
  prior_claims: 0.45,
131
131
  credit_tier: 0.55,
132
- zip_risk: 0.60, // over-penalises high-decile (the bias lever)
133
- age_band: 0.10,
134
- bias: 0.70,
135
- threshold: 0.50,
132
+ zip_risk: 0.6, // over-penalises high-decile (the bias lever)
133
+ age_band: 0.1,
134
+ bias: 0.7,
135
+ threshold: 0.5,
136
136
  };
137
137
 
138
138
  /**
@@ -147,12 +147,12 @@ export const BIASED_WEIGHTS: HomeownersModelWeights = {
147
147
  * constraint; the higher bias provides the margin).
148
148
  */
149
149
  export const REMEDIATED_WEIGHTS: HomeownersModelWeights = {
150
- prior_claims: 0.40,
150
+ prior_claims: 0.4,
151
151
  credit_tier: 0.55,
152
- zip_risk: 0.00, // zeroed out — D.057 remediation
152
+ zip_risk: 0.0, // zeroed out — D.057 remediation
153
153
  age_band: 0.08,
154
154
  bias: 0.55,
155
- threshold: 0.50,
155
+ threshold: 0.5,
156
156
  };
157
157
 
158
158
  // ── Synthetic cohort generator ───────────────────────────────────────────────
@@ -185,18 +185,18 @@ export function makeInsuranceCohort(seed: number, n = 400): FairnessRecord[] {
185
185
  // ZIP-risk proxy: the bias trap — high-decile draws from higher range
186
186
  const zip_risk =
187
187
  group === 'high-decile'
188
- ? clamp01(0.55 + 0.40 * rng()) // [0.55, 0.95]
188
+ ? clamp01(0.55 + 0.4 * rng()) // [0.55, 0.95]
189
189
  : clamp01(0.05 + 0.45 * rng()); // [0.05, 0.50]
190
190
 
191
191
  // Credit tier: slightly lower for high-decile (systemic disparity)
192
192
  const credit_tier =
193
193
  group === 'high-decile'
194
- ? clamp01(0.30 + 0.55 * rng()) // mean ~0.575
195
- : clamp01(0.40 + 0.55 * rng()); // mean ~0.675
194
+ ? clamp01(0.3 + 0.55 * rng()) // mean ~0.575
195
+ : clamp01(0.4 + 0.55 * rng()); // mean ~0.675
196
196
 
197
197
  // Prior claims and age band: comparable across groups
198
198
  const prior_claims = clamp01(0.05 + 0.45 * rng()); // [0.05, 0.50]
199
- const age_band = clamp01(0.20 + 0.60 * rng()); // [0.20, 0.80]
199
+ const age_band = clamp01(0.2 + 0.6 * rng()); // [0.20, 0.80]
200
200
 
201
201
  rows.push({
202
202
  group,
@@ -227,7 +227,7 @@ export function makeInsuranceCohort(seed: number, n = 400): FairnessRecord[] {
227
227
  */
228
228
  export const insuranceUnderwritingPerturber: CohortPerturber = (
229
229
  cohort: readonly FairnessRecord[],
230
- { driftShift, noiseScale, rng }: FairnessPerturbation,
230
+ { driftShift, noiseScale, rng }: FairnessPerturbation
231
231
  ): FairnessRecord[] => {
232
232
  const n = cohort.length;
233
233
  const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
@@ -242,16 +242,12 @@ export const insuranceUnderwritingPerturber: CohortPerturber = (
242
242
  zip_risk: clamp01(
243
243
  src.features.zip_risk +
244
244
  (isHighDecile ? driftShift : 0) + // directed drift for high-decile
245
- (rng() - 0.5) * 0.5 * noiseScale, // symmetric noise for both
245
+ (rng() - 0.5) * 0.5 * noiseScale // symmetric noise for both
246
246
  ),
247
247
  // Credit bureau measurement noise (both groups)
248
- credit_tier: clamp01(
249
- src.features.credit_tier + (rng() - 0.5) * 2 * noiseScale,
250
- ),
248
+ credit_tier: clamp01(src.features.credit_tier + (rng() - 0.5) * 2 * noiseScale),
251
249
  // Prior claims: low noise (claim history is more stable)
252
- prior_claims: clamp01(
253
- src.features.prior_claims + (rng() - 0.5) * noiseScale,
254
- ),
250
+ prior_claims: clamp01(src.features.prior_claims + (rng() - 0.5) * noiseScale),
255
251
  // Age band: stable (no drift)
256
252
  age_band: src.features.age_band,
257
253
  },
package/src/index.ts CHANGED
@@ -1,7 +1,20 @@
1
- export { createPolicyHandler, type PolicyConfig, type PolicyType, type PolicyStatus } from './traits/PolicyTrait';
1
+ export {
2
+ createPolicyHandler,
3
+ type PolicyConfig,
4
+ type PolicyType,
5
+ type PolicyStatus,
6
+ } from './traits/PolicyTrait';
2
7
  export { createClaimHandler, type ClaimConfig, type ClaimStatus } from './traits/ClaimTrait';
3
- export { createRiskAssessmentHandler, type RiskAssessmentConfig, type RiskFactor } from './traits/RiskAssessmentTrait';
4
- export { createUnderwritingHandler, type UnderwritingConfig, type UnderwritingDecision } from './traits/UnderwritingTrait';
8
+ export {
9
+ createRiskAssessmentHandler,
10
+ type RiskAssessmentConfig,
11
+ type RiskFactor,
12
+ } from './traits/RiskAssessmentTrait';
13
+ export {
14
+ createUnderwritingHandler,
15
+ type UnderwritingConfig,
16
+ type UnderwritingDecision,
17
+ } from './traits/UnderwritingTrait';
5
18
  export * from './traits/types';
6
19
 
7
20
  import { createPolicyHandler } from './traits/PolicyTrait';
@@ -12,5 +25,14 @@ import { createUnderwritingHandler } from './traits/UnderwritingTrait';
12
25
  export * from './actuarial';
13
26
  export * from './fairness-underwriting';
14
27
 
15
- export const pluginMeta = { name: '@holoscript/plugin-insurance', version: '1.0.0', traits: ['policy', 'claim', 'risk_assessment', 'underwriting', 'actuarial_math'] };
16
- export const traitHandlers = [createPolicyHandler(), createClaimHandler(), createRiskAssessmentHandler(), createUnderwritingHandler()];
28
+ export const pluginMeta = {
29
+ name: '@holoscript/plugin-insurance',
30
+ version: '1.0.0',
31
+ traits: ['policy', 'claim', 'risk_assessment', 'underwriting', 'actuarial_math'],
32
+ };
33
+ export const traitHandlers = [
34
+ createPolicyHandler(),
35
+ createClaimHandler(),
36
+ createRiskAssessmentHandler(),
37
+ createUnderwritingHandler(),
38
+ ];