@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/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@holoscript/plugin-insurance",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "main": "src/index.ts",
5
5
  "peerDependencies": {
6
- "@holoscript/core": "8.0.6",
7
- "@holoscript/engine": "6.1.3"
6
+ "@holoscript/core": ">=8.0.0",
7
+ "@holoscript/engine": ">=6.1.0"
8
8
  },
9
9
  "license": "MIT",
10
10
  "scripts": {
11
11
  "test": "vitest run --passWithNoTests",
12
12
  "test:coverage": "vitest run --coverage --passWithNoTests"
13
13
  }
14
- }
14
+ }
@@ -22,27 +22,107 @@ import {
22
22
  // ─── Illustrative life table (ILT) — truncated 1958 CSO qx values ────────────
23
23
  // Ages 0–100 (101 values). Terminal qx[100] = 1.0 (everyone dies by 100).
24
24
  const CSO_QX: number[] = [
25
- 0.004530, 0.001080, 0.000910, 0.000830, 0.000790, // 0-4
26
- 0.000760, 0.000730, 0.000720, 0.000720, 0.000730, // 5-9
27
- 0.000730, 0.000770, 0.000850, 0.000990, 0.001150, // 10-14
28
- 0.001330, 0.001510, 0.001670, 0.001780, 0.001870, // 15-19
29
- 0.001960, 0.002010, 0.002020, 0.002010, 0.001980, // 20-24
30
- 0.001960, 0.001960, 0.001970, 0.001980, 0.002000, // 25-29
31
- 0.002030, 0.002060, 0.002110, 0.002170, 0.002250, // 30-34
32
- 0.002360, 0.002500, 0.002680, 0.002890, 0.003140, // 35-39
33
- 0.003440, 0.003790, 0.004190, 0.004660, 0.005190, // 40-44
34
- 0.005810, 0.006500, 0.007280, 0.008170, 0.009180, // 45-49
35
- 0.010380, 0.011680, 0.013110, 0.014700, 0.016530, // 50-54
36
- 0.018640, 0.021010, 0.023580, 0.026440, 0.029560, // 55-59
37
- 0.032990, 0.036700, 0.040680, 0.044990, 0.049650, // 60-64
38
- 0.054770, 0.060330, 0.066440, 0.073040, 0.080240, // 65-69
39
- 0.088050, 0.096620, 0.105800, 0.115800, 0.126600, // 70-74
40
- 0.138200, 0.150600, 0.163900, 0.178200, 0.193600, // 75-79
41
- 0.210300, 0.228200, 0.247500, 0.268000, 0.290000, // 80-84
42
- 0.313000, 0.337400, 0.363000, 0.390000, 0.418200, // 85-89
43
- 0.447600, 0.477900, 0.509100, 0.541100, 0.573800, // 90-94
44
- 0.607000, 0.640400, 0.673800, 0.706900, 0.739400, // 95-99
45
- 1.000000, // 100
25
+ 0.00453,
26
+ 0.00108,
27
+ 0.00091,
28
+ 0.00083,
29
+ 0.00079, // 0-4
30
+ 0.00076,
31
+ 0.00073,
32
+ 0.00072,
33
+ 0.00072,
34
+ 0.00073, // 5-9
35
+ 0.00073,
36
+ 0.00077,
37
+ 0.00085,
38
+ 0.00099,
39
+ 0.00115, // 10-14
40
+ 0.00133,
41
+ 0.00151,
42
+ 0.00167,
43
+ 0.00178,
44
+ 0.00187, // 15-19
45
+ 0.00196,
46
+ 0.00201,
47
+ 0.00202,
48
+ 0.00201,
49
+ 0.00198, // 20-24
50
+ 0.00196,
51
+ 0.00196,
52
+ 0.00197,
53
+ 0.00198,
54
+ 0.002, // 25-29
55
+ 0.00203,
56
+ 0.00206,
57
+ 0.00211,
58
+ 0.00217,
59
+ 0.00225, // 30-34
60
+ 0.00236,
61
+ 0.0025,
62
+ 0.00268,
63
+ 0.00289,
64
+ 0.00314, // 35-39
65
+ 0.00344,
66
+ 0.00379,
67
+ 0.00419,
68
+ 0.00466,
69
+ 0.00519, // 40-44
70
+ 0.00581,
71
+ 0.0065,
72
+ 0.00728,
73
+ 0.00817,
74
+ 0.00918, // 45-49
75
+ 0.01038,
76
+ 0.01168,
77
+ 0.01311,
78
+ 0.0147,
79
+ 0.01653, // 50-54
80
+ 0.01864,
81
+ 0.02101,
82
+ 0.02358,
83
+ 0.02644,
84
+ 0.02956, // 55-59
85
+ 0.03299,
86
+ 0.0367,
87
+ 0.04068,
88
+ 0.04499,
89
+ 0.04965, // 60-64
90
+ 0.05477,
91
+ 0.06033,
92
+ 0.06644,
93
+ 0.07304,
94
+ 0.08024, // 65-69
95
+ 0.08805,
96
+ 0.09662,
97
+ 0.1058,
98
+ 0.1158,
99
+ 0.1266, // 70-74
100
+ 0.1382,
101
+ 0.1506,
102
+ 0.1639,
103
+ 0.1782,
104
+ 0.1936, // 75-79
105
+ 0.2103,
106
+ 0.2282,
107
+ 0.2475,
108
+ 0.268,
109
+ 0.29, // 80-84
110
+ 0.313,
111
+ 0.3374,
112
+ 0.363,
113
+ 0.39,
114
+ 0.4182, // 85-89
115
+ 0.4476,
116
+ 0.4779,
117
+ 0.5091,
118
+ 0.5411,
119
+ 0.5738, // 90-94
120
+ 0.607,
121
+ 0.6404,
122
+ 0.6738,
123
+ 0.7069,
124
+ 0.7394, // 95-99
125
+ 1.0, // 100
46
126
  ];
47
127
 
48
128
  const ILT = buildLifeTable('1958-CSO', CSO_QX, 0.05);
@@ -102,7 +182,7 @@ describe('computeActuarialValues — whole_life', () => {
102
182
  // so NSP is higher (≈ $23,000 for $100k benefit). Verify structural bounds only.
103
183
  const r = computeActuarialValues(
104
184
  { type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
105
- ILT,
185
+ ILT
106
186
  );
107
187
  expect(r.nsp).toBeGreaterThan(1_000);
108
188
  expect(r.nsp).toBeLessThan(30_000);
@@ -111,26 +191,44 @@ describe('computeActuarialValues — whole_life', () => {
111
191
  it('annual premium < NSP (premium is paid over lifetime)', () => {
112
192
  const r = computeActuarialValues(
113
193
  { type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
114
- ILT,
194
+ ILT
115
195
  );
116
196
  expect(r.annualPremium).toBeLessThan(r.nsp);
117
197
  });
118
198
 
119
199
  it('older issue age produces higher NSP (higher mortality)', () => {
120
- const r35 = computeActuarialValues({ type: 'whole_life', issueAge: 35, benefitAmount: 100_000 }, ILT);
121
- const r55 = computeActuarialValues({ type: 'whole_life', issueAge: 55, benefitAmount: 100_000 }, ILT);
200
+ const r35 = computeActuarialValues(
201
+ { type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
202
+ ILT
203
+ );
204
+ const r55 = computeActuarialValues(
205
+ { type: 'whole_life', issueAge: 55, benefitAmount: 100_000 },
206
+ ILT
207
+ );
122
208
  expect(r55.nsp).toBeGreaterThan(r35.nsp);
123
209
  });
124
210
 
125
211
  it('higher interest rate produces lower NSP (discounting effect)', () => {
126
- const r5pct = computeActuarialValues({ type: 'whole_life', issueAge: 40, benefitAmount: 100_000, interestRate: 0.05 }, ILT);
127
- const r10pct = computeActuarialValues({ type: 'whole_life', issueAge: 40, benefitAmount: 100_000, interestRate: 0.10 }, ILT);
212
+ const r5pct = computeActuarialValues(
213
+ { type: 'whole_life', issueAge: 40, benefitAmount: 100_000, interestRate: 0.05 },
214
+ ILT
215
+ );
216
+ const r10pct = computeActuarialValues(
217
+ { type: 'whole_life', issueAge: 40, benefitAmount: 100_000, interestRate: 0.1 },
218
+ ILT
219
+ );
128
220
  expect(r10pct.nsp).toBeLessThan(r5pct.nsp);
129
221
  });
130
222
 
131
223
  it('NSP scales linearly with benefit amount', () => {
132
- const r100k = computeActuarialValues({ type: 'whole_life', issueAge: 40, benefitAmount: 100_000 }, ILT);
133
- const r200k = computeActuarialValues({ type: 'whole_life', issueAge: 40, benefitAmount: 200_000 }, ILT);
224
+ const r100k = computeActuarialValues(
225
+ { type: 'whole_life', issueAge: 40, benefitAmount: 100_000 },
226
+ ILT
227
+ );
228
+ const r200k = computeActuarialValues(
229
+ { type: 'whole_life', issueAge: 40, benefitAmount: 200_000 },
230
+ ILT
231
+ );
134
232
  expect(r200k.nsp).toBeCloseTo(r100k.nsp * 2, 4);
135
233
  });
136
234
 
@@ -145,7 +243,7 @@ describe('computeActuarialValues — whole_life', () => {
145
243
 
146
244
  it('throws for issue age beyond table', () => {
147
245
  expect(() =>
148
- computeActuarialValues({ type: 'whole_life', issueAge: 200, benefitAmount: 1 }, ILT),
246
+ computeActuarialValues({ type: 'whole_life', issueAge: 200, benefitAmount: 1 }, ILT)
149
247
  ).toThrow();
150
248
  });
151
249
  });
@@ -154,19 +252,34 @@ describe('computeActuarialValues — whole_life', () => {
154
252
 
155
253
  describe('computeActuarialValues — term_life', () => {
156
254
  it('term NSP < whole_life NSP (subset of coverage period)', () => {
157
- const wl = computeActuarialValues({ type: 'whole_life', issueAge: 35, benefitAmount: 100_000 }, ILT);
158
- const term = computeActuarialValues({ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 20 }, ILT);
255
+ const wl = computeActuarialValues(
256
+ { type: 'whole_life', issueAge: 35, benefitAmount: 100_000 },
257
+ ILT
258
+ );
259
+ const term = computeActuarialValues(
260
+ { type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 20 },
261
+ ILT
262
+ );
159
263
  expect(term.nsp).toBeLessThan(wl.nsp);
160
264
  });
161
265
 
162
266
  it('longer term produces higher NSP', () => {
163
- const t10 = computeActuarialValues({ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 10 }, ILT);
164
- const t30 = computeActuarialValues({ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 30 }, ILT);
267
+ const t10 = computeActuarialValues(
268
+ { type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 10 },
269
+ ILT
270
+ );
271
+ const t30 = computeActuarialValues(
272
+ { type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 30 },
273
+ ILT
274
+ );
165
275
  expect(t30.nsp).toBeGreaterThan(t10.nsp);
166
276
  });
167
277
 
168
278
  it('policyType is term_life', () => {
169
- const r = computeActuarialValues({ type: 'term_life', issueAge: 35, benefitAmount: 1, termYears: 20 }, ILT);
279
+ const r = computeActuarialValues(
280
+ { type: 'term_life', issueAge: 35, benefitAmount: 1, termYears: 20 },
281
+ ILT
282
+ );
170
283
  expect(r.policyType).toBe('term_life');
171
284
  expect(r.termYears).toBe(20);
172
285
  });
@@ -176,13 +289,22 @@ describe('computeActuarialValues — term_life', () => {
176
289
 
177
290
  describe('computeActuarialValues — endowment', () => {
178
291
  it('endowment NSP ≥ equivalent term_life NSP (adds pure endowment)', () => {
179
- const term = computeActuarialValues({ type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 20 }, ILT);
180
- const endt = computeActuarialValues({ type: 'endowment', issueAge: 35, benefitAmount: 100_000, termYears: 20 }, ILT);
292
+ const term = computeActuarialValues(
293
+ { type: 'term_life', issueAge: 35, benefitAmount: 100_000, termYears: 20 },
294
+ ILT
295
+ );
296
+ const endt = computeActuarialValues(
297
+ { type: 'endowment', issueAge: 35, benefitAmount: 100_000, termYears: 20 },
298
+ ILT
299
+ );
181
300
  expect(endt.nsp).toBeGreaterThan(term.nsp);
182
301
  });
183
302
 
184
303
  it('endowment NSP ≤ benefit amount (PV cannot exceed undiscounted payout)', () => {
185
- const r = computeActuarialValues({ type: 'endowment', issueAge: 35, benefitAmount: 100_000, termYears: 30 }, ILT);
304
+ const r = computeActuarialValues(
305
+ { type: 'endowment', issueAge: 35, benefitAmount: 100_000, termYears: 30 },
306
+ ILT
307
+ );
186
308
  expect(r.nsp).toBeLessThan(100_000);
187
309
  expect(r.nsp).toBeGreaterThan(0);
188
310
  });
@@ -192,18 +314,27 @@ describe('computeActuarialValues — endowment', () => {
192
314
 
193
315
  describe('computeActuarialValues — annuity_due', () => {
194
316
  it('whole-life annuity NSP = benefit × ä_x (annuityDue)', () => {
195
- const r = computeActuarialValues({ type: 'annuity_due', issueAge: 65, benefitAmount: 10_000 }, ILT);
317
+ const r = computeActuarialValues(
318
+ { type: 'annuity_due', issueAge: 65, benefitAmount: 10_000 },
319
+ ILT
320
+ );
196
321
  expect(r.nsp).toBeCloseTo(r.annuityDue * 10_000, 4);
197
322
  });
198
323
 
199
324
  it('temporary annuity NSP < whole-life annuity NSP', () => {
200
- const wl = computeActuarialValues({ type: 'annuity_due', issueAge: 65, benefitAmount: 1 }, ILT);
201
- const tmp = computeActuarialValues({ type: 'annuity_due', issueAge: 65, benefitAmount: 1, termYears: 10 }, ILT);
325
+ const wl = computeActuarialValues({ type: 'annuity_due', issueAge: 65, benefitAmount: 1 }, ILT);
326
+ const tmp = computeActuarialValues(
327
+ { type: 'annuity_due', issueAge: 65, benefitAmount: 1, termYears: 10 },
328
+ ILT
329
+ );
202
330
  expect(tmp.nsp).toBeLessThan(wl.nsp);
203
331
  });
204
332
 
205
333
  it('annualPremium is 0 for annuity products', () => {
206
- const r = computeActuarialValues({ type: 'annuity_due', issueAge: 60, benefitAmount: 1_000 }, ILT);
334
+ const r = computeActuarialValues(
335
+ { type: 'annuity_due', issueAge: 60, benefitAmount: 1_000 },
336
+ ILT
337
+ );
207
338
  expect(r.annualPremium).toBe(0);
208
339
  });
209
340
  });
@@ -218,16 +349,16 @@ describe('computeNPV', () => {
218
349
  */
219
350
  const cashFlows = [
220
351
  { period: 0, amount: -1000 },
221
- { period: 1, amount: 400 },
222
- { period: 2, amount: 400 },
223
- { period: 3, amount: 400 },
352
+ { period: 1, amount: 400 },
353
+ { period: 2, amount: 400 },
354
+ { period: 3, amount: 400 },
224
355
  ];
225
356
 
226
357
  it('NPV at 10% is near zero for the ~9.7%-IRR project', () => {
227
358
  // IRR ≈ 9.7%; at 10% discount the NPV is small but not exactly zero.
228
359
  // Exact NPV = -1000 + 400/1.1 + 400/1.21 + 400/1.331 ≈ -$0.95... but
229
360
  // actually: 363.64 + 330.58 + 300.53 = 994.75 → NPV = -5.25.
230
- const r = computeNPV(cashFlows, 0.10);
361
+ const r = computeNPV(cashFlows, 0.1);
231
362
  expect(Math.abs(r.npv)).toBeLessThan(10); // within $10
232
363
  });
233
364
 
@@ -237,18 +368,24 @@ describe('computeNPV', () => {
237
368
  });
238
369
 
239
370
  it('IRR is approximately 9.7%', () => {
240
- const r = computeNPV(cashFlows, 0.10);
371
+ const r = computeNPV(cashFlows, 0.1);
241
372
  expect(r.irr).not.toBeNull();
242
373
  expect(r.irr!).toBeCloseTo(0.097, 2);
243
374
  });
244
375
 
245
376
  it('payback period is period 3 (cumulative turns positive at t=3)', () => {
246
- const r = computeNPV(cashFlows, 0.10);
377
+ const r = computeNPV(cashFlows, 0.1);
247
378
  expect(r.paybackPeriod).toBe(3);
248
379
  });
249
380
 
250
381
  it('project with all positive flows has no IRR in (0,2) range', () => {
251
- const r = computeNPV([{ period: 0, amount: 100 }, { period: 1, amount: 100 }], 0.05);
382
+ const r = computeNPV(
383
+ [
384
+ { period: 0, amount: 100 },
385
+ { period: 1, amount: 100 },
386
+ ],
387
+ 0.05
388
+ );
252
389
  // NPV is always positive for all-positive flows; IRR is null (no zero crossing)
253
390
  expect(r.irr).toBeNull();
254
391
  });
@@ -262,9 +399,7 @@ describe('computeNPV', () => {
262
399
 
263
400
  describe('computeVaR', () => {
264
401
  // Simulate 1000 loss observations from an exponential-like distribution
265
- const losses = Array.from({ length: 1000 }, (_, i) =>
266
- -500 + i * 1.5 + Math.sin(i * 0.13) * 50,
267
- );
402
+ const losses = Array.from({ length: 1000 }, (_, i) => -500 + i * 1.5 + Math.sin(i * 0.13) * 50);
268
403
 
269
404
  it('var99 ≥ var95 (higher confidence = larger loss threshold)', () => {
270
405
  const r = computeVaR(losses);
@@ -328,7 +463,7 @@ describe('gompertzMakehamQx', () => {
328
463
  });
329
464
 
330
465
  it('produces a valid life table when passed to buildLifeTable', () => {
331
- const qx = gompertzMakehamQx(params, 99);
466
+ const qx = gompertzMakehamQx(params, 99);
332
467
  const table = buildLifeTable('gompertz-test', qx, 0.05);
333
468
  expect(table.rows[0].lx).toBe(100_000);
334
469
  expect(table.rows[0].ex).toBeGreaterThan(50);
@@ -348,7 +483,7 @@ describe('gompertzMakehamQx', () => {
348
483
  describe('buildActuarialReceipt', () => {
349
484
  const result = computeActuarialValues(
350
485
  { type: 'whole_life', issueAge: 40, benefitAmount: 100_000 },
351
- ILT,
486
+ ILT
352
487
  );
353
488
 
354
489
  it('produces receipt with plugin=insurance and CAEL event', () => {
@@ -198,7 +198,7 @@ describe('insurance-underwriting — replay determinism', () => {
198
198
  // Independent re-run by a validator
199
199
  const rerunDigest = computeDecisionDigest(
200
200
  cohort.map((r) => model.decide(r.features)),
201
- 'sha256',
201
+ 'sha256'
202
202
  );
203
203
 
204
204
  const reExec = verifyReplayExecution(receipt, {
@@ -220,7 +220,7 @@ describe('insurance-underwriting — replay determinism', () => {
220
220
  const tampered = cohort.map((r, i) =>
221
221
  i === 0
222
222
  ? { group: r.group, features: { ...r.features, zip_risk: r.features.zip_risk + 1e-4 } }
223
- : r,
223
+ : r
224
224
  );
225
225
  const t = await runFairnessSweep(model, tampered, {
226
226
  seed: SEED,
@@ -260,14 +260,16 @@ describe('insurance-underwriting — regulatory crosswalk keys', () => {
260
260
  });
261
261
 
262
262
  it('NAIC crosswalk entry references race_proxy and homeowners-underwriting-scorer', () => {
263
- const naicEntry = NAIC_INSURANCE_CROSSWALK['NAIC AI Systems Evaluation Tool (sample case file + bias audit)'];
263
+ const naicEntry =
264
+ NAIC_INSURANCE_CROSSWALK['NAIC AI Systems Evaluation Tool (sample case file + bias audit)'];
264
265
  expect(naicEntry).toBeTruthy();
265
266
  expect(naicEntry).toContain('homeowners-underwriting-scorer');
266
267
  expect(naicEntry).toContain('race_proxy');
267
268
  });
268
269
 
269
270
  it('CO SB21-169 crosswalk entry references zip_risk proxy', () => {
270
- const coEntry = NAIC_INSURANCE_CROSSWALK['Colorado SB21-169 / Reg 10-1-1 (unfair-discrimination testing)'];
271
+ const coEntry =
272
+ NAIC_INSURANCE_CROSSWALK['Colorado SB21-169 / Reg 10-1-1 (unfair-discrimination testing)'];
271
273
  expect(coEntry).toBeTruthy();
272
274
  expect(coEntry).toContain('zip_risk');
273
275
  });