@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.
@@ -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"] }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['src/**/*.test.ts'],
8
+ passWithNoTests: true,
9
+ },
10
+ });