@holoscript/plugin-insurance 2.0.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 +16 -0
- package/LICENSE +21 -0
- package/package.json +14 -0
- package/src/__tests__/actuarial.test.ts +378 -0
- package/src/__tests__/fairness-underwriting.test.ts +342 -0
- package/src/actuarial.ts +451 -0
- package/src/fairness-underwriting.ts +302 -0
- package/src/index.ts +16 -0
- package/src/traits/ClaimTrait.ts +23 -0
- package/src/traits/PolicyTrait.ts +22 -0
- package/src/traits/RiskAssessmentTrait.ts +27 -0
- package/src/traits/UnderwritingTrait.ts +28 -0
- package/src/traits/types.ts +4 -0
- package/tsconfig.json +1 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insurance underwriting fairness adapter — D.057 tracer-bullet
|
|
3
|
+
*
|
|
4
|
+
* End-to-end D.057 slice for the NAIC AI Systems Evaluation Tool pilot
|
|
5
|
+
* (insurance underwriting bias audit, 12-state live pilot Jan-Sep 2026).
|
|
6
|
+
*
|
|
7
|
+
* Layers assembled here:
|
|
8
|
+
* STATE — homeowners underwriting FairnessModel + realistic synthetic cohort
|
|
9
|
+
* (≥200 rows, protectedAttribute=race_proxy via ZIP-income decile per
|
|
10
|
+
* NAIC focus area) + CohortPerturber encoding insurance drift
|
|
11
|
+
* (seasonal portfolio shift + credit_tier noise).
|
|
12
|
+
* RECEIPT — FairnessReceipt (fairness.receipt.v1) with NAIC 2026 pilot +
|
|
13
|
+
* CO SB21-169 crosswalk entries.
|
|
14
|
+
*
|
|
15
|
+
* Core stays domain-free (D.007): no insurance vocabulary bleeds into
|
|
16
|
+
* @holoscript/engine. The model, cohort, and perturber are INJECTED here and
|
|
17
|
+
* consumed by the standard runFairnessSweep / runFairnessRobustness harness.
|
|
18
|
+
*
|
|
19
|
+
* F-flags carried:
|
|
20
|
+
* F-1 (synthetic data only; no real applicant records are used)
|
|
21
|
+
* F-2 partially closed: realistic synthetic vs. toy (ZIP-income decile proxy
|
|
22
|
+
* matches NAIC focus area; credit_tier and seasonal drift are domain-real)
|
|
23
|
+
*
|
|
24
|
+
* @see @holoscript/engine FairnessSweep / FairnessReceipt
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { Simulation } from '@holoscript/engine';
|
|
28
|
+
|
|
29
|
+
// Re-export the types and helper from the Simulation namespace so the rest of
|
|
30
|
+
// this module can use them by their short names.
|
|
31
|
+
type FairnessModel = Simulation.FairnessModel;
|
|
32
|
+
type FairnessRecord = Simulation.FairnessRecord;
|
|
33
|
+
type CohortPerturber = Simulation.CohortPerturber;
|
|
34
|
+
type FairnessPerturbation = Simulation.FairnessPerturbation;
|
|
35
|
+
const mulberry32 = Simulation.mulberry32;
|
|
36
|
+
|
|
37
|
+
// ── Insurance-domain feature constants ──────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Feature names for the homeowners underwriting model.
|
|
41
|
+
* All normalized to [0, 1] so the model weights are directly comparable.
|
|
42
|
+
*
|
|
43
|
+
* prior_claims — normalised prior-claim count (0=none, 1=5+)
|
|
44
|
+
* credit_tier — normalised credit tier (0=poor, 1=excellent)
|
|
45
|
+
* zip_risk — normalised ZIP-code risk decile (0=lowest-risk, 1=highest-risk)
|
|
46
|
+
* this is the bias trap: highly correlated with race-proxy group
|
|
47
|
+
* age_band — normalised applicant age band (0=youngest, 1=oldest)
|
|
48
|
+
*/
|
|
49
|
+
export const UNDERWRITING_FEATURES = [
|
|
50
|
+
'prior_claims',
|
|
51
|
+
'credit_tier',
|
|
52
|
+
'zip_risk',
|
|
53
|
+
'age_band',
|
|
54
|
+
] as const;
|
|
55
|
+
export type UnderwritingFeature = (typeof UNDERWRITING_FEATURES)[number];
|
|
56
|
+
|
|
57
|
+
// ── Protected attribute: race_proxy ─────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The NAIC AI Systems Evaluation Tool pilot focuses on race-proxy bias
|
|
61
|
+
* transmitted through ZIP-code risk deciles and credit scores. We represent
|
|
62
|
+
* the protected attribute as 'race_proxy', with values 'low-decile' (group
|
|
63
|
+
* benefitting from lower average ZIP-risk attribution) and 'high-decile'
|
|
64
|
+
* (group facing higher average attribution due to geographic concentration).
|
|
65
|
+
*
|
|
66
|
+
* In synthetic data, group membership determines the ZIP-income decile
|
|
67
|
+
* distribution: high-decile applicants draw from a higher zip_risk range,
|
|
68
|
+
* replicating the demographic concentration pattern the NAIC pilot examines.
|
|
69
|
+
*
|
|
70
|
+
* Domain rationale: per NAIC model AI governance bulletin and CO Reg 10-1-1,
|
|
71
|
+
* ZIP-code variables are facially neutral but operationally correlated with
|
|
72
|
+
* race/ethnicity in many US markets. This is the bias the NAIC pilot looks for.
|
|
73
|
+
*/
|
|
74
|
+
export type RaceProxyGroup = 'low-decile' | 'high-decile';
|
|
75
|
+
export const PROTECTED_ATTRIBUTE = 'race_proxy' as const;
|
|
76
|
+
|
|
77
|
+
// ── Homeowners underwriting model ───────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export interface HomeownersModelWeights {
|
|
80
|
+
prior_claims: number;
|
|
81
|
+
credit_tier: number;
|
|
82
|
+
zip_risk: number;
|
|
83
|
+
age_band: number;
|
|
84
|
+
bias: number;
|
|
85
|
+
threshold: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Transparent linear scorer — approved iff score >= threshold.
|
|
90
|
+
* score = bias
|
|
91
|
+
* + credit_tier_w * credit_tier
|
|
92
|
+
* - prior_claims_w * prior_claims
|
|
93
|
+
* - zip_risk_w * zip_risk
|
|
94
|
+
* - age_band_w * age_band
|
|
95
|
+
*
|
|
96
|
+
* The BIASED default weights over-penalize zip_risk (the proxy variable),
|
|
97
|
+
* creating disparate impact against high-decile applicants. The REMEDIATED
|
|
98
|
+
* weights zero out zip_risk — the model then relies only on actuarially
|
|
99
|
+
* legitimate inputs and passes the 4/5ths threshold.
|
|
100
|
+
*/
|
|
101
|
+
export function createHomeownersModel(
|
|
102
|
+
weights: HomeownersModelWeights,
|
|
103
|
+
modelId = 'homeowners-underwriting-scorer',
|
|
104
|
+
): FairnessModel {
|
|
105
|
+
return {
|
|
106
|
+
id: modelId,
|
|
107
|
+
decide: (features: Record<string, number>): boolean => {
|
|
108
|
+
const score =
|
|
109
|
+
weights.bias +
|
|
110
|
+
weights.credit_tier * (features.credit_tier ?? 0) -
|
|
111
|
+
weights.prior_claims * (features.prior_claims ?? 0) -
|
|
112
|
+
weights.zip_risk * (features.zip_risk ?? 0) -
|
|
113
|
+
weights.age_band * (features.age_band ?? 0);
|
|
114
|
+
return score >= weights.threshold;
|
|
115
|
+
},
|
|
116
|
+
fingerprint: () => ({
|
|
117
|
+
kind: 'homeowners-linear-scorer.v1',
|
|
118
|
+
weights,
|
|
119
|
+
}),
|
|
120
|
+
determinism: { grade: 'exact' },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Biased model weights: zip_risk carries strong negative weight, acting as
|
|
126
|
+
* a race-proxy (NAIC AI Evaluation Tool focus area). Produces
|
|
127
|
+
* FLAG-DISPARATE-IMPACT for the high-decile group.
|
|
128
|
+
*/
|
|
129
|
+
export const BIASED_WEIGHTS: HomeownersModelWeights = {
|
|
130
|
+
prior_claims: 0.45,
|
|
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,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Remediated model weights: zip_risk eliminated (NAIC remediation guidance).
|
|
140
|
+
* Relies only on actuarially legitimate inputs (prior_claims, credit_tier,
|
|
141
|
+
* age_band). Produces PASS for the same cohort — and ROBUSTLY-FAIR under
|
|
142
|
+
* the seasonal drift + credit-noise robustness band.
|
|
143
|
+
*
|
|
144
|
+
* Bias is set higher to keep approval above the 4/5ths threshold even under
|
|
145
|
+
* the portfolio-drift band applied to high-decile zip_risk (which has no
|
|
146
|
+
* weight here, so drift is irrelevant — credit_tier noise is the binding
|
|
147
|
+
* constraint; the higher bias provides the margin).
|
|
148
|
+
*/
|
|
149
|
+
export const REMEDIATED_WEIGHTS: HomeownersModelWeights = {
|
|
150
|
+
prior_claims: 0.40,
|
|
151
|
+
credit_tier: 0.55,
|
|
152
|
+
zip_risk: 0.00, // zeroed out — D.057 remediation
|
|
153
|
+
age_band: 0.08,
|
|
154
|
+
bias: 0.55,
|
|
155
|
+
threshold: 0.50,
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// ── Synthetic cohort generator ───────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate a realistic synthetic homeowners underwriting cohort.
|
|
162
|
+
*
|
|
163
|
+
* Cohort design rationale (NAIC AI Systems Evaluation Tool):
|
|
164
|
+
* - n >= 200 rows (NAIC pilot requires meaningful sample sizes)
|
|
165
|
+
* - 50/50 split between low-decile and high-decile groups
|
|
166
|
+
* - ZIP-income decile proxy: high-decile applicants draw zip_risk from
|
|
167
|
+
* [0.55, 0.95], low-decile from [0.05, 0.50] — matches real US
|
|
168
|
+
* geographic concentration patterns without using real addresses
|
|
169
|
+
* - credit_tier: both groups draw from similar distributions (slightly
|
|
170
|
+
* lower mean for high-decile, reflecting systemic credit disparity)
|
|
171
|
+
* - prior_claims, age_band: comparable across groups (no intentional bias)
|
|
172
|
+
*
|
|
173
|
+
* @param seed Deterministic seed for reproducibility (part of replay key)
|
|
174
|
+
* @param n Cohort size (default 400: 200/group — well above NAIC minimum)
|
|
175
|
+
*/
|
|
176
|
+
export function makeInsuranceCohort(seed: number, n = 400): FairnessRecord[] {
|
|
177
|
+
const rng = mulberry32(seed);
|
|
178
|
+
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
|
|
179
|
+
const rows: FairnessRecord[] = [];
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < n; i++) {
|
|
182
|
+
// Alternate groups to guarantee 50/50 split
|
|
183
|
+
const group: RaceProxyGroup = i % 2 === 0 ? 'low-decile' : 'high-decile';
|
|
184
|
+
|
|
185
|
+
// ZIP-risk proxy: the bias trap — high-decile draws from higher range
|
|
186
|
+
const zip_risk =
|
|
187
|
+
group === 'high-decile'
|
|
188
|
+
? clamp01(0.55 + 0.40 * rng()) // [0.55, 0.95]
|
|
189
|
+
: clamp01(0.05 + 0.45 * rng()); // [0.05, 0.50]
|
|
190
|
+
|
|
191
|
+
// Credit tier: slightly lower for high-decile (systemic disparity)
|
|
192
|
+
const credit_tier =
|
|
193
|
+
group === 'high-decile'
|
|
194
|
+
? clamp01(0.30 + 0.55 * rng()) // mean ~0.575
|
|
195
|
+
: clamp01(0.40 + 0.55 * rng()); // mean ~0.675
|
|
196
|
+
|
|
197
|
+
// Prior claims and age band: comparable across groups
|
|
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]
|
|
200
|
+
|
|
201
|
+
rows.push({
|
|
202
|
+
group,
|
|
203
|
+
features: { prior_claims, credit_tier, zip_risk, age_band },
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return rows;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Domain-specific cohort perturber ─────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Insurance underwriting cohort perturber for robustness sweeps.
|
|
213
|
+
*
|
|
214
|
+
* Encodes two real insurance drift patterns for the NAIC pilot:
|
|
215
|
+
*
|
|
216
|
+
* 1. Seasonal portfolio shift (driftShift): in high-loss seasons (hurricane,
|
|
217
|
+
* wildfire), high-decile applicants see portfolio-level ZIP-risk elevation.
|
|
218
|
+
* Applied as a positive shift to zip_risk for high-decile records.
|
|
219
|
+
*
|
|
220
|
+
* 2. Credit-tier measurement noise (noiseScale): credit bureau refresh lag
|
|
221
|
+
* introduces noise in credit_tier for both groups.
|
|
222
|
+
*
|
|
223
|
+
* Bootstrap resampling is applied first (standard demographic ensemble), then
|
|
224
|
+
* the domain-specific drift and noise are layered on.
|
|
225
|
+
*
|
|
226
|
+
* @see runFairnessRobustness — consumed as the `perturber` option
|
|
227
|
+
*/
|
|
228
|
+
export const insuranceUnderwritingPerturber: CohortPerturber = (
|
|
229
|
+
cohort: readonly FairnessRecord[],
|
|
230
|
+
{ driftShift, noiseScale, rng }: FairnessPerturbation,
|
|
231
|
+
): FairnessRecord[] => {
|
|
232
|
+
const n = cohort.length;
|
|
233
|
+
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
|
|
234
|
+
return Array.from({ length: n }, () => {
|
|
235
|
+
const src = cohort[Math.floor(rng() * n)];
|
|
236
|
+
const isHighDecile = src.group === 'high-decile';
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
group: src.group,
|
|
240
|
+
features: {
|
|
241
|
+
// Seasonal ZIP-risk elevation for high-decile (portfolio drift)
|
|
242
|
+
zip_risk: clamp01(
|
|
243
|
+
src.features.zip_risk +
|
|
244
|
+
(isHighDecile ? driftShift : 0) + // directed drift for high-decile
|
|
245
|
+
(rng() - 0.5) * 0.5 * noiseScale, // symmetric noise for both
|
|
246
|
+
),
|
|
247
|
+
// Credit bureau measurement noise (both groups)
|
|
248
|
+
credit_tier: clamp01(
|
|
249
|
+
src.features.credit_tier + (rng() - 0.5) * 2 * noiseScale,
|
|
250
|
+
),
|
|
251
|
+
// Prior claims: low noise (claim history is more stable)
|
|
252
|
+
prior_claims: clamp01(
|
|
253
|
+
src.features.prior_claims + (rng() - 0.5) * noiseScale,
|
|
254
|
+
),
|
|
255
|
+
// Age band: stable (no drift)
|
|
256
|
+
age_band: src.features.age_band,
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// ── NAIC + Colorado regulatory crosswalk override ────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Insurance-vertical regulatory mapping, layered onto the core
|
|
266
|
+
* DEFAULT_FAIRNESS_CROSSWALK. The overrides add NAIC-pilot-specific and
|
|
267
|
+
* CO SB21-169/Reg-10-1-1 entries that name the insurance-specific evidence.
|
|
268
|
+
*
|
|
269
|
+
* Pass this as `regulatoryMapping` to runFairnessSweep / runFairnessRobustness
|
|
270
|
+
* to produce receipts satisfying both the default crosswalk and the
|
|
271
|
+
* insurance-vertical additions.
|
|
272
|
+
*
|
|
273
|
+
* Key entries added:
|
|
274
|
+
* NAIC 2026 AI pilot — this receipt IS the case file the NAIC evaluation
|
|
275
|
+
* tool expects: metrics = bias audit, protectedAttribute = race_proxy.
|
|
276
|
+
* CO SB21-169 / Reg 10-1-1 — adverse-impact ratio (4/5ths) + zip_risk proxy
|
|
277
|
+
* disclosure + credit_tier noise documentation.
|
|
278
|
+
*/
|
|
279
|
+
export const NAIC_INSURANCE_CROSSWALK: Record<string, string> = {
|
|
280
|
+
// Core EU/US entries (matching DEFAULT_FAIRNESS_CROSSWALK keys)
|
|
281
|
+
'EU-AI-Act Art.10 (data governance / bias mitigation)':
|
|
282
|
+
'metrics.demographicParityDiff + per-cohort approvalRate; protectedAttribute=race_proxy (ZIP-income decile proxy)',
|
|
283
|
+
'EU-AI-Act Art.12 (record-keeping)':
|
|
284
|
+
'replayFingerprint + inputHash (append-only, content-hashed); cohort seed = replay key',
|
|
285
|
+
'EU-AI-Act Art.15 (accuracy/robustness)':
|
|
286
|
+
'replayFingerprint determinism=exact; robustness band encodes seasonal portfolio drift + credit-bureau noise',
|
|
287
|
+
'Colorado SB21-169 / Reg 10-1-1 (unfair-discrimination testing)':
|
|
288
|
+
'metrics.adverseImpactRatio (4/5ths) + fourFifthsPass; zip_risk proxy identified + remediation path shown',
|
|
289
|
+
'NAIC AI Systems Evaluation Tool (sample case file + bias audit)':
|
|
290
|
+
'this receipt IS the NAIC case file: modelId=homeowners-underwriting-scorer, protectedAttribute=race_proxy, metrics=bias audit, replayFingerprint=case-file hash',
|
|
291
|
+
'NAIC 2026 AI pilot (12-state; Jan-Sep 2026)':
|
|
292
|
+
'homeowners underwriting vertical; race_proxy=ZIP-income decile; cohort n>=200 per NAIC guidance; seasonal drift band included',
|
|
293
|
+
'FDA SaMD bias review / HIPAA audit trail':
|
|
294
|
+
'time-stamped, non-overwritable content hash = audit record',
|
|
295
|
+
'SR 11-7 / revised-MRM (independent validation)':
|
|
296
|
+
'validator re-runs from (modelHash,seed,inputHash,weights) -> same fingerprint; zip_risk weight=0.0 in remediated model',
|
|
297
|
+
// Robustness-specific
|
|
298
|
+
'CCAR/DFAST (reproducible stress scenarios)':
|
|
299
|
+
'LHS ensemble = seasonal-drift + credit-noise sweep; ensembleHash + replayFingerprint reproduce it',
|
|
300
|
+
'Colorado SB21-169 / NAIC (testing under data drift)':
|
|
301
|
+
'verdict holds across seasonal portfolio-shift + credit-bureau-noise space, not a single sample',
|
|
302
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { createPolicyHandler, type PolicyConfig, type PolicyType, type PolicyStatus } from './traits/PolicyTrait';
|
|
2
|
+
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';
|
|
5
|
+
export * from './traits/types';
|
|
6
|
+
|
|
7
|
+
import { createPolicyHandler } from './traits/PolicyTrait';
|
|
8
|
+
import { createClaimHandler } from './traits/ClaimTrait';
|
|
9
|
+
import { createRiskAssessmentHandler } from './traits/RiskAssessmentTrait';
|
|
10
|
+
import { createUnderwritingHandler } from './traits/UnderwritingTrait';
|
|
11
|
+
|
|
12
|
+
export * from './actuarial';
|
|
13
|
+
export * from './fairness-underwriting';
|
|
14
|
+
|
|
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()];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @claim Trait — Insurance claim processing. @trait claim */
|
|
2
|
+
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
|
+
|
|
4
|
+
export type ClaimStatus = 'filed' | 'under_review' | 'approved' | 'denied' | 'paid' | 'appealed' | 'closed';
|
|
5
|
+
export interface ClaimConfig { claimNumber: string; policyNumber: string; dateOfLoss: string; description: string; claimAmount: number; claimantName: string; }
|
|
6
|
+
export interface ClaimState { status: ClaimStatus; approvedAmount: number; adjusterAssigned: string | null; filedAt: number; }
|
|
7
|
+
|
|
8
|
+
const defaultConfig: ClaimConfig = { claimNumber: '', policyNumber: '', dateOfLoss: '', description: '', claimAmount: 0, claimantName: '' };
|
|
9
|
+
|
|
10
|
+
export function createClaimHandler(): TraitHandler<ClaimConfig> {
|
|
11
|
+
return { name: 'claim', defaultConfig,
|
|
12
|
+
onAttach(n: HSPlusNode, _c: ClaimConfig, ctx: TraitContext) { n.__claimState = { status: 'filed' as ClaimStatus, approvedAmount: 0, adjusterAssigned: null, filedAt: Date.now() }; ctx.emit?.('claim:filed'); },
|
|
13
|
+
onDetach(n: HSPlusNode, _c: ClaimConfig, ctx: TraitContext) { delete n.__claimState; ctx.emit?.('claim:removed'); },
|
|
14
|
+
onUpdate() {},
|
|
15
|
+
onEvent(n: HSPlusNode, c: ClaimConfig, ctx: TraitContext, e: TraitEvent) {
|
|
16
|
+
const s = n.__claimState as ClaimState | undefined; if (!s) return;
|
|
17
|
+
if (e.type === 'claim:assign_adjuster') { s.adjusterAssigned = (e.payload?.adjuster as string) ?? null; s.status = 'under_review'; ctx.emit?.('claim:reviewing', { adjuster: s.adjusterAssigned }); }
|
|
18
|
+
if (e.type === 'claim:approve') { s.status = 'approved'; s.approvedAmount = (e.payload?.amount as number) ?? c.claimAmount; ctx.emit?.('claim:approved', { amount: s.approvedAmount }); }
|
|
19
|
+
if (e.type === 'claim:deny') { s.status = 'denied'; ctx.emit?.('claim:denied', { reason: e.payload?.reason }); }
|
|
20
|
+
if (e.type === 'claim:pay') { s.status = 'paid'; ctx.emit?.('claim:paid', { amount: s.approvedAmount }); }
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** @policy Trait — Insurance policy management. @trait policy */
|
|
2
|
+
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
|
+
|
|
4
|
+
export type PolicyType = 'life' | 'health' | 'auto' | 'home' | 'commercial' | 'liability' | 'cyber';
|
|
5
|
+
export type PolicyStatus = 'quoted' | 'bound' | 'active' | 'lapsed' | 'cancelled' | 'expired';
|
|
6
|
+
export interface PolicyConfig { policyNumber: string; type: PolicyType; insuredName: string; premium: number; coverageAmount: number; deductible: number; effectiveDate: string; expirationDate: string; }
|
|
7
|
+
export interface PolicyState { status: PolicyStatus; claimCount: number; totalPaidClaims: number; remainingCoverage: number; }
|
|
8
|
+
|
|
9
|
+
const defaultConfig: PolicyConfig = { policyNumber: '', type: 'auto', insuredName: '', premium: 0, coverageAmount: 0, deductible: 0, effectiveDate: '', expirationDate: '' };
|
|
10
|
+
|
|
11
|
+
export function createPolicyHandler(): TraitHandler<PolicyConfig> {
|
|
12
|
+
return { name: 'policy', defaultConfig,
|
|
13
|
+
onAttach(n: HSPlusNode, c: PolicyConfig, ctx: TraitContext) { n.__policyState = { status: 'active' as PolicyStatus, claimCount: 0, totalPaidClaims: 0, remainingCoverage: c.coverageAmount }; ctx.emit?.('policy:bound', { type: c.type, coverage: c.coverageAmount }); },
|
|
14
|
+
onDetach(n: HSPlusNode, _c: PolicyConfig, ctx: TraitContext) { delete n.__policyState; ctx.emit?.('policy:removed'); },
|
|
15
|
+
onUpdate() {},
|
|
16
|
+
onEvent(n: HSPlusNode, _c: PolicyConfig, ctx: TraitContext, e: TraitEvent) {
|
|
17
|
+
const s = n.__policyState as PolicyState | undefined; if (!s) return;
|
|
18
|
+
if (e.type === 'policy:cancel') { s.status = 'cancelled'; ctx.emit?.('policy:cancelled'); }
|
|
19
|
+
if (e.type === 'policy:renew') { s.status = 'active'; ctx.emit?.('policy:renewed'); }
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** @risk_assessment Trait — Actuarial risk scoring. @trait risk_assessment */
|
|
2
|
+
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
|
+
|
|
4
|
+
export interface RiskFactor { name: string; weight: number; value: number; maxValue: number; }
|
|
5
|
+
export interface RiskAssessmentConfig { factors: RiskFactor[]; baseRate: number; riskClass: 'preferred' | 'standard' | 'substandard' | 'declined'; maxRiskScore: number; }
|
|
6
|
+
|
|
7
|
+
const defaultConfig: RiskAssessmentConfig = { factors: [], baseRate: 100, riskClass: 'standard', maxRiskScore: 100 };
|
|
8
|
+
|
|
9
|
+
export function createRiskAssessmentHandler(): TraitHandler<RiskAssessmentConfig> {
|
|
10
|
+
return { name: 'risk_assessment', defaultConfig,
|
|
11
|
+
onAttach(n: HSPlusNode, c: RiskAssessmentConfig, ctx: TraitContext) {
|
|
12
|
+
const score = c.factors.reduce((s, f) => s + (f.value / f.maxValue) * f.weight, 0);
|
|
13
|
+
n.__riskAssessState = { riskScore: Math.min(c.maxRiskScore, score), adjustedPremium: c.baseRate * (1 + score / 100), isAcceptable: c.riskClass !== 'declined' };
|
|
14
|
+
ctx.emit?.('risk_assessment:scored', { score, riskClass: c.riskClass });
|
|
15
|
+
},
|
|
16
|
+
onDetach(n: HSPlusNode, _c: RiskAssessmentConfig, ctx: TraitContext) { delete n.__riskAssessState; ctx.emit?.('risk_assessment:removed'); },
|
|
17
|
+
onUpdate() {},
|
|
18
|
+
onEvent(n: HSPlusNode, c: RiskAssessmentConfig, ctx: TraitContext, e: TraitEvent) {
|
|
19
|
+
if (e.type === 'risk_assessment:recalculate') {
|
|
20
|
+
const s = n.__riskAssessState as Record<string, unknown> | undefined; if (!s) return;
|
|
21
|
+
const score = c.factors.reduce((sum, f) => sum + (f.value / f.maxValue) * f.weight, 0);
|
|
22
|
+
s.riskScore = Math.min(c.maxRiskScore, score); s.adjustedPremium = c.baseRate * (1 + score / 100);
|
|
23
|
+
ctx.emit?.('risk_assessment:updated', { score, premium: s.adjustedPremium });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** @underwriting Trait — Policy underwriting decision. @trait underwriting */
|
|
2
|
+
import type { TraitHandler, HSPlusNode, TraitContext, TraitEvent } from './types';
|
|
3
|
+
|
|
4
|
+
export type UnderwritingDecision = 'approve' | 'approve_with_conditions' | 'refer' | 'decline';
|
|
5
|
+
export interface UnderwritingConfig { applicationId: string; riskScore: number; requestedCoverage: number; maxAutoCoverage: number; referralThreshold: number; declineThreshold: number; conditions: string[]; }
|
|
6
|
+
export interface UnderwritingState { decision: UnderwritingDecision | null; reviewedAt: number | null; underwriterId: string | null; }
|
|
7
|
+
|
|
8
|
+
const defaultConfig: UnderwritingConfig = { applicationId: '', riskScore: 0, requestedCoverage: 0, maxAutoCoverage: 500000, referralThreshold: 70, declineThreshold: 90, conditions: [] };
|
|
9
|
+
|
|
10
|
+
export function createUnderwritingHandler(): TraitHandler<UnderwritingConfig> {
|
|
11
|
+
return { name: 'underwriting', defaultConfig,
|
|
12
|
+
onAttach(n: HSPlusNode, _c: UnderwritingConfig, ctx: TraitContext) { n.__uwState = { decision: null, reviewedAt: null, underwriterId: null }; ctx.emit?.('underwriting:submitted'); },
|
|
13
|
+
onDetach(n: HSPlusNode, _c: UnderwritingConfig, ctx: TraitContext) { delete n.__uwState; ctx.emit?.('underwriting:removed'); },
|
|
14
|
+
onUpdate() {},
|
|
15
|
+
onEvent(n: HSPlusNode, c: UnderwritingConfig, ctx: TraitContext, e: TraitEvent) {
|
|
16
|
+
const s = n.__uwState as UnderwritingState | undefined; if (!s) return;
|
|
17
|
+
if (e.type === 'underwriting:auto_decide') {
|
|
18
|
+
if (c.riskScore >= c.declineThreshold) s.decision = 'decline';
|
|
19
|
+
else if (c.riskScore >= c.referralThreshold || c.requestedCoverage > c.maxAutoCoverage) s.decision = 'refer';
|
|
20
|
+
else if (c.conditions.length > 0) s.decision = 'approve_with_conditions';
|
|
21
|
+
else s.decision = 'approve';
|
|
22
|
+
s.reviewedAt = Date.now();
|
|
23
|
+
ctx.emit?.('underwriting:decided', { decision: s.decision, riskScore: c.riskScore });
|
|
24
|
+
}
|
|
25
|
+
if (e.type === 'underwriting:manual_decide') { s.decision = (e.payload?.decision as UnderwritingDecision) ?? 'refer'; s.underwriterId = (e.payload?.underwriterId as string) ?? null; s.reviewedAt = Date.now(); ctx.emit?.('underwriting:manual_decision', { decision: s.decision }); }
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export interface HSPlusNode { id?: string; properties?: Record<string, unknown>; [key: string]: unknown; }
|
|
2
|
+
export interface TraitContext { emit?: (event: string, payload?: unknown) => void; [key: string]: unknown; }
|
|
3
|
+
export interface TraitEvent { type: string; payload?: Record<string, unknown>; [key: string]: unknown; }
|
|
4
|
+
export interface TraitHandler<T = unknown> { name: string; defaultConfig: T; onAttach(n: HSPlusNode, c: T, ctx: TraitContext): void; onDetach(n: HSPlusNode, c: T, ctx: TraitContext): void; onUpdate(n: HSPlusNode, c: T, ctx: TraitContext, d: number): void; onEvent(n: HSPlusNode, c: T, ctx: TraitContext, e: TraitEvent): void; }
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "../../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src", "declaration": true }, "include": ["src"] }
|