@actuarial-ts/core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/dist/analysisRecipe.d.ts +55 -0
- package/dist/analysisRecipe.d.ts.map +1 -0
- package/dist/analysisRecipe.js +627 -0
- package/dist/analysisRecipe.js.map +1 -0
- package/dist/classifications.d.ts +30 -0
- package/dist/classifications.d.ts.map +1 -0
- package/dist/classifications.js +207 -0
- package/dist/classifications.js.map +1 -0
- package/dist/customizationContracts.d.ts +749 -0
- package/dist/customizationContracts.d.ts.map +1 -0
- package/dist/customizationContracts.js +19 -0
- package/dist/customizationContracts.js.map +1 -0
- package/dist/descriptiveStatistics.d.ts +8 -0
- package/dist/descriptiveStatistics.d.ts.map +1 -0
- package/dist/descriptiveStatistics.js +283 -0
- package/dist/descriptiveStatistics.js.map +1 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +16 -0
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticFreeze.d.ts +4 -0
- package/dist/diagnosticFreeze.d.ts.map +1 -0
- package/dist/diagnosticFreeze.js +37 -0
- package/dist/diagnosticFreeze.js.map +1 -0
- package/dist/diagnosticIdentityStream.d.ts +4 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -1
- package/dist/diagnosticIdentityStream.js +32 -2
- package/dist/diagnosticIdentityStream.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +21 -0
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +231 -19
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +43 -25
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts.map +1 -1
- package/dist/diagnosticReviewSources.js +25 -4
- package/dist/diagnosticReviewSources.js.map +1 -1
- package/dist/diagnosticReviewStore.d.ts +2 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -1
- package/dist/diagnosticReviewStore.js +110 -36
- package/dist/diagnosticReviewStore.js.map +1 -1
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +1 -8
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/exposureEarning.d.ts +12 -0
- package/dist/exposureEarning.d.ts.map +1 -0
- package/dist/exposureEarning.js +276 -0
- package/dist/exposureEarning.js.map +1 -0
- package/dist/exposureMovements.d.ts +8 -0
- package/dist/exposureMovements.d.ts.map +1 -0
- package/dist/exposureMovements.js +70 -0
- package/dist/exposureMovements.js.map +1 -0
- package/dist/exposureRevisions.d.ts +13 -0
- package/dist/exposureRevisions.d.ts.map +1 -0
- package/dist/exposureRevisions.js +161 -0
- package/dist/exposureRevisions.js.map +1 -0
- package/dist/financialPipeline.d.ts +14 -0
- package/dist/financialPipeline.d.ts.map +1 -0
- package/dist/financialPipeline.js +75 -0
- package/dist/financialPipeline.js.map +1 -0
- package/dist/financialTerms.d.ts +20 -0
- package/dist/financialTerms.d.ts.map +1 -0
- package/dist/financialTerms.js +205 -0
- package/dist/financialTerms.js.map +1 -0
- package/dist/fnvAccumulator.d.ts.map +1 -1
- package/dist/fnvAccumulator.js +11 -5
- package/dist/fnvAccumulator.js.map +1 -1
- package/dist/historyPreparation.d.ts +8 -0
- package/dist/historyPreparation.d.ts.map +1 -0
- package/dist/historyPreparation.js +557 -0
- package/dist/historyPreparation.js.map +1 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/dist/index.js.map +1 -1
- package/dist/policyPeriods.d.ts +29 -0
- package/dist/policyPeriods.d.ts.map +1 -0
- package/dist/policyPeriods.js +322 -0
- package/dist/policyPeriods.js.map +1 -0
- package/dist/populations.d.ts +11 -0
- package/dist/populations.d.ts.map +1 -0
- package/dist/populations.js +264 -0
- package/dist/populations.js.map +1 -0
- package/dist/reservingAdapter.d.ts +16 -0
- package/dist/reservingAdapter.d.ts.map +1 -0
- package/dist/reservingAdapter.js +74 -0
- package/dist/reservingAdapter.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/analysisRecipe.ts +696 -0
- package/src/classifications.ts +231 -0
- package/src/customizationContracts.ts +763 -0
- package/src/descriptiveStatistics.ts +320 -0
- package/src/diagnosticAggregation.ts +16 -0
- package/src/diagnosticFreeze.ts +37 -0
- package/src/diagnosticIdentityStream.ts +37 -2
- package/src/diagnosticPreparation.ts +259 -26
- package/src/diagnosticReview.ts +49 -27
- package/src/diagnosticReviewSources.ts +22 -3
- package/src/diagnosticReviewStore.ts +139 -54
- package/src/diagnosticRunner.ts +1 -11
- package/src/exposureEarning.ts +314 -0
- package/src/exposureMovements.ts +78 -0
- package/src/exposureRevisions.ts +186 -0
- package/src/financialPipeline.ts +93 -0
- package/src/financialTerms.ts +263 -0
- package/src/fnvAccumulator.ts +11 -5
- package/src/historyPreparation.ts +645 -0
- package/src/index.ts +28 -1
- package/src/policyPeriods.ts +375 -0
- package/src/populations.ts +307 -0
- package/src/reservingAdapter.ts +98 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { canonicalJson } from "./canonical.js";
|
|
2
|
+
import type {
|
|
3
|
+
AnalysisClassificationDefinition,
|
|
4
|
+
AnalysisClassificationResult,
|
|
5
|
+
AnalysisClassifiedStatisticResult,
|
|
6
|
+
AnalysisDimensionDefinition,
|
|
7
|
+
AnalysisMeasureDefinitionV1,
|
|
8
|
+
AnalysisObservationSelection,
|
|
9
|
+
AnalysisPredicate,
|
|
10
|
+
ClaimStatusTransition,
|
|
11
|
+
CustomizationCapabilityFinding,
|
|
12
|
+
HistoricalScalar,
|
|
13
|
+
PreparedHistoricalObservation,
|
|
14
|
+
} from "./customizationContracts.js";
|
|
15
|
+
import { snapshotDiagnosticJson } from "./diagnosticRuntime.js";
|
|
16
|
+
import { evaluateAnalysisPredicate, validateAnalysisPredicate } from "./populations.js";
|
|
17
|
+
import { DiagnosticValidationError, type DiagnosticValidationIssue } from "./types.js";
|
|
18
|
+
import { calculateAnalysisStatistic } from "./descriptiveStatistics.js";
|
|
19
|
+
|
|
20
|
+
function observationDimensions(
|
|
21
|
+
observation: PreparedHistoricalObservation,
|
|
22
|
+
): Readonly<Record<string, HistoricalScalar | readonly HistoricalScalar[]>> {
|
|
23
|
+
return {
|
|
24
|
+
...observation.dimensions,
|
|
25
|
+
sourceNamespace: observation.sourceNamespace,
|
|
26
|
+
claimId: observation.claimId,
|
|
27
|
+
evaluationDate: observation.evaluationDate,
|
|
28
|
+
...(observation.status === null ? {} : { status: observation.status }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function finding(message: string, code: CustomizationCapabilityFinding["code"]): CustomizationCapabilityFinding {
|
|
33
|
+
return { code, capability: "typed-populations", path: "$.classification", message };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function numericMatches(
|
|
37
|
+
value: number,
|
|
38
|
+
group: Extract<AnalysisClassificationDefinition, { kind: "numeric-bands" }>["groups"][number],
|
|
39
|
+
): boolean {
|
|
40
|
+
return (group.minimum === undefined || (group.includeMinimum ? value >= group.minimum : value > group.minimum)) &&
|
|
41
|
+
(group.maximum === undefined || (group.includeMaximum ? value <= group.maximum : value < group.maximum));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateClassification(
|
|
45
|
+
definition: AnalysisClassificationDefinition,
|
|
46
|
+
dimensions: readonly AnalysisDimensionDefinition[],
|
|
47
|
+
): void {
|
|
48
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
49
|
+
const ids = definition.kind === "lookup" || definition.kind === "hierarchy"
|
|
50
|
+
? []
|
|
51
|
+
: definition.groups.map((group) => group.id);
|
|
52
|
+
if (new Set(ids).size !== ids.length)
|
|
53
|
+
issues.push({ domain: "definition", code: "duplicate-id", path: "$.classification.groups", message: "Classification group IDs must be unique" });
|
|
54
|
+
if (definition.kind !== "rules") {
|
|
55
|
+
const dimension = dimensions.find((candidate) => candidate.id === definition.dimensionId);
|
|
56
|
+
if (dimension === undefined)
|
|
57
|
+
issues.push({ domain: "definition", code: "unknown-reference", path: "$.classification.dimensionId", message: `Unknown classification dimension ${definition.dimensionId}` });
|
|
58
|
+
if (definition.kind === "numeric-bands" && dimension?.type !== "number")
|
|
59
|
+
issues.push({ domain: "definition", code: "incompatible-semantics", path: "$.classification.dimensionId", message: "Numeric bands require a numeric dimension" });
|
|
60
|
+
}
|
|
61
|
+
if (definition.kind === "numeric-bands")
|
|
62
|
+
definition.groups.forEach((group, index) => {
|
|
63
|
+
if (group.minimum !== undefined && group.maximum !== undefined && group.minimum > group.maximum)
|
|
64
|
+
issues.push({ domain: "definition", code: "invalid-configuration", path: `$.classification.groups[${index}]`, message: "Classification band minimum exceeds maximum" });
|
|
65
|
+
});
|
|
66
|
+
if (definition.kind === "rules")
|
|
67
|
+
definition.groups.forEach((group) => validateAnalysisPredicate(group.predicate, dimensions));
|
|
68
|
+
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Classifies one observation; allowed overlaps stay explicit and never imply an additive portfolio total. */
|
|
72
|
+
export function classifyAnalysisObservation(input: {
|
|
73
|
+
readonly definition: AnalysisClassificationDefinition;
|
|
74
|
+
readonly observation: PreparedHistoricalObservation;
|
|
75
|
+
readonly dimensions: readonly AnalysisDimensionDefinition[];
|
|
76
|
+
}): AnalysisClassificationResult {
|
|
77
|
+
validateClassification(input.definition, input.dimensions);
|
|
78
|
+
const values = observationDimensions(input.observation);
|
|
79
|
+
let groups: string[] = [];
|
|
80
|
+
let missing = false;
|
|
81
|
+
const definition = input.definition;
|
|
82
|
+
if (definition.kind === "numeric-bands") {
|
|
83
|
+
const value = values[definition.dimensionId];
|
|
84
|
+
if (typeof value !== "number") missing = true;
|
|
85
|
+
else groups = definition.groups.filter((group) => numericMatches(value, group)).map((group) => group.id);
|
|
86
|
+
} else if (definition.kind === "lookup") {
|
|
87
|
+
const observed = values[definition.dimensionId];
|
|
88
|
+
if (observed === undefined || observed === null) missing = true;
|
|
89
|
+
else {
|
|
90
|
+
const items = Array.isArray(observed) ? observed : [observed as HistoricalScalar];
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
const entry = definition.entries.find((candidate) => canonicalJson(candidate.value) === canonicalJson(item));
|
|
93
|
+
if (entry !== undefined) groups.push(entry.groupId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (definition.kind === "hierarchy") {
|
|
97
|
+
const observed = values[definition.dimensionId];
|
|
98
|
+
if (observed === undefined || observed === null) missing = true;
|
|
99
|
+
else {
|
|
100
|
+
const items = Array.isArray(observed) ? observed : [observed as HistoricalScalar];
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
const entry = definition.entries.find((candidate) => canonicalJson(candidate.value) === canonicalJson(item));
|
|
103
|
+
if (entry !== undefined) groups.push(...entry.groupIds);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
for (const group of definition.groups) {
|
|
108
|
+
const outcome = evaluateAnalysisPredicate(group.predicate, values, input.dimensions);
|
|
109
|
+
if (outcome === "match") groups.push(group.id);
|
|
110
|
+
if (outcome === "unknown") missing = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
groups = [...new Set(groups)].sort();
|
|
114
|
+
const overlapPolicy = definition.kind === "lookup" || definition.kind === "hierarchy" ? "allow" : definition.overlapPolicy;
|
|
115
|
+
const gapPolicy = definition.kind === "lookup" || definition.kind === "hierarchy" ? definition.unmappedPolicy : definition.gapPolicy;
|
|
116
|
+
if (groups.length > 1 && overlapPolicy === "reject")
|
|
117
|
+
return snapshotDiagnosticJson({ classificationId: definition.id, groupIds: groups, status: "unavailable", overlapping: true, findings: [finding("Classification matched overlapping groups under reject policy", "ambiguous-attribution")] });
|
|
118
|
+
if (groups.length === 0 && gapPolicy === "reject")
|
|
119
|
+
return snapshotDiagnosticJson({ classificationId: definition.id, groupIds: [], status: "unavailable", overlapping: false, findings: [finding(missing ? "Classification input is missing" : "Classification matched no group under reject policy", "missing-prerequisite")] });
|
|
120
|
+
return snapshotDiagnosticJson({ classificationId: definition.id, groupIds: groups, status: groups.length === 0 ? "unclassified" : "classified", overlapping: groups.length > 1, findings: [] });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type AnalysisPopulationPreset = "reported" | "open" | "reopened" | "closed-with-pay" | "closed-no-pay" | "expense-only";
|
|
124
|
+
|
|
125
|
+
/** Returns a reusable predicate. Callers may replace every field name with their mapped catalog IDs. */
|
|
126
|
+
export function createAnalysisPopulationPreset(
|
|
127
|
+
preset: AnalysisPopulationPreset,
|
|
128
|
+
fields: { readonly status?: string; readonly reportDate?: string; readonly paid?: string; readonly expense?: string } = {},
|
|
129
|
+
): AnalysisPredicate {
|
|
130
|
+
const status = fields.status ?? "status";
|
|
131
|
+
const reportDate = fields.reportDate ?? "reportDate";
|
|
132
|
+
const paid = fields.paid ?? "paid";
|
|
133
|
+
const expense = fields.expense ?? "expense";
|
|
134
|
+
if (preset === "reported") return { op: "missing", dimensionId: reportDate, isMissing: false };
|
|
135
|
+
if (preset === "open") return { op: "in", dimensionId: status, values: ["open", "reopened"] };
|
|
136
|
+
if (preset === "reopened") return { op: "eq", dimensionId: status, value: "reopened" };
|
|
137
|
+
if (preset === "closed-with-pay") return { op: "and", terms: [{ op: "eq", dimensionId: status, value: "closed" }, { op: "range", dimensionId: paid, minimum: 0, includeMinimum: false, includeMaximum: false }] };
|
|
138
|
+
if (preset === "closed-no-pay") return { op: "and", terms: [{ op: "eq", dimensionId: status, value: "closed" }, { op: "eq", dimensionId: paid, value: 0 }] };
|
|
139
|
+
return { op: "and", terms: [{ op: "range", dimensionId: expense, minimum: 0, includeMinimum: false, includeMaximum: false }, { op: "eq", dimensionId: paid, value: 0 }] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Detects only status changes visible between supplied snapshots; it does not invent event dates. */
|
|
143
|
+
export function detectClaimStatusTransitions(
|
|
144
|
+
observations: readonly PreparedHistoricalObservation[],
|
|
145
|
+
closedStatuses: readonly string[] = ["closed"],
|
|
146
|
+
): readonly ClaimStatusTransition[] {
|
|
147
|
+
const byClaim = new Map<string, PreparedHistoricalObservation[]>();
|
|
148
|
+
for (const observation of observations) {
|
|
149
|
+
const key = canonicalJson([observation.sourceNamespace, observation.claimId]);
|
|
150
|
+
const values = byClaim.get(key) ?? [];
|
|
151
|
+
values.push(observation);
|
|
152
|
+
byClaim.set(key, values);
|
|
153
|
+
}
|
|
154
|
+
const closed = new Set(closedStatuses);
|
|
155
|
+
const transitions: ClaimStatusTransition[] = [];
|
|
156
|
+
for (const history of byClaim.values()) {
|
|
157
|
+
history.sort((a, b) => a.evaluationDate.localeCompare(b.evaluationDate));
|
|
158
|
+
for (let index = 1; index < history.length; index += 1) {
|
|
159
|
+
const prior = history[index - 1]!;
|
|
160
|
+
const current = history[index]!;
|
|
161
|
+
if (prior.status === current.status) continue;
|
|
162
|
+
const kind = closed.has(prior.status ?? "") && !closed.has(current.status ?? "")
|
|
163
|
+
? "reopened"
|
|
164
|
+
: !closed.has(prior.status ?? "") && closed.has(current.status ?? "")
|
|
165
|
+
? "closed"
|
|
166
|
+
: prior.status === null
|
|
167
|
+
? "opened"
|
|
168
|
+
: "other";
|
|
169
|
+
transitions.push({
|
|
170
|
+
sourceNamespace: current.sourceNamespace,
|
|
171
|
+
claimId: current.claimId,
|
|
172
|
+
fromStatus: prior.status,
|
|
173
|
+
toStatus: current.status,
|
|
174
|
+
observedAt: current.evaluationDate,
|
|
175
|
+
kind,
|
|
176
|
+
observationPrecision: "between-snapshots",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return snapshotDiagnosticJson(transitions);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Calculates each possibly overlapping classification group and calculates
|
|
185
|
+
* the portfolio independently from the underlying observations. Group totals
|
|
186
|
+
* are never added together when one observation belongs to multiple groups.
|
|
187
|
+
*/
|
|
188
|
+
export function calculateClassifiedAnalysisStatistics(input: {
|
|
189
|
+
readonly definition: AnalysisClassificationDefinition;
|
|
190
|
+
readonly observations: readonly PreparedHistoricalObservation[];
|
|
191
|
+
readonly dimensions: readonly AnalysisDimensionDefinition[];
|
|
192
|
+
readonly measure: AnalysisMeasureDefinitionV1;
|
|
193
|
+
readonly selection: AnalysisObservationSelection;
|
|
194
|
+
}): AnalysisClassifiedStatisticResult {
|
|
195
|
+
const members = new Map<string, PreparedHistoricalObservation[]>();
|
|
196
|
+
const findings: CustomizationCapabilityFinding[] = [];
|
|
197
|
+
let overlapDetected = false;
|
|
198
|
+
for (const observation of input.observations) {
|
|
199
|
+
const classified = classifyAnalysisObservation({
|
|
200
|
+
definition: input.definition,
|
|
201
|
+
observation,
|
|
202
|
+
dimensions: input.dimensions,
|
|
203
|
+
});
|
|
204
|
+
findings.push(...classified.findings);
|
|
205
|
+
if (classified.status !== "classified") continue;
|
|
206
|
+
if (classified.groupIds.length > 1) overlapDetected = true;
|
|
207
|
+
for (const groupId of classified.groupIds) {
|
|
208
|
+
const group = members.get(groupId);
|
|
209
|
+
if (group === undefined) members.set(groupId, [observation]);
|
|
210
|
+
else group.push(observation);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const groups = [...members]
|
|
214
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
215
|
+
.map(([groupId, observations]) => ({
|
|
216
|
+
groupId,
|
|
217
|
+
result: calculateAnalysisStatistic({ measure: input.measure, observations, selection: input.selection }),
|
|
218
|
+
}));
|
|
219
|
+
return snapshotDiagnosticJson({
|
|
220
|
+
classificationId: input.definition.id,
|
|
221
|
+
groups,
|
|
222
|
+
portfolio: calculateAnalysisStatistic({
|
|
223
|
+
measure: input.measure,
|
|
224
|
+
observations: input.observations,
|
|
225
|
+
selection: input.selection,
|
|
226
|
+
}),
|
|
227
|
+
overlapDetected,
|
|
228
|
+
additiveGroupSumValid: !overlapDetected,
|
|
229
|
+
findings,
|
|
230
|
+
});
|
|
231
|
+
}
|