@actuarial-ts/core 0.5.0 → 0.6.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.
Files changed (120) hide show
  1. package/README.md +65 -226
  2. package/dist/capping.d.ts.map +1 -1
  3. package/dist/capping.js +39 -6
  4. package/dist/capping.js.map +1 -1
  5. package/dist/casualtyDiagnostics.d.ts +19 -49
  6. package/dist/casualtyDiagnostics.d.ts.map +1 -1
  7. package/dist/casualtyDiagnostics.js +84 -116
  8. package/dist/casualtyDiagnostics.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +32 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -0
  11. package/dist/diagnosticAggregation.js +73 -0
  12. package/dist/diagnosticAggregation.js.map +1 -0
  13. package/dist/diagnosticDefinitions.d.ts +289 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -0
  15. package/dist/diagnosticDefinitions.js +1688 -0
  16. package/dist/diagnosticDefinitions.js.map +1 -0
  17. package/dist/diagnosticDerivations.d.ts +35 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -0
  19. package/dist/diagnosticDerivations.js +224 -0
  20. package/dist/diagnosticDerivations.js.map +1 -0
  21. package/dist/diagnosticExposure.d.ts +51 -0
  22. package/dist/diagnosticExposure.d.ts.map +1 -0
  23. package/dist/diagnosticExposure.js +273 -0
  24. package/dist/diagnosticExposure.js.map +1 -0
  25. package/dist/diagnosticExpressions.d.ts +56 -0
  26. package/dist/diagnosticExpressions.d.ts.map +1 -0
  27. package/dist/diagnosticExpressions.js +124 -0
  28. package/dist/diagnosticExpressions.js.map +1 -0
  29. package/dist/diagnosticFormulas.d.ts +198 -0
  30. package/dist/diagnosticFormulas.d.ts.map +1 -0
  31. package/dist/diagnosticFormulas.js +219 -0
  32. package/dist/diagnosticFormulas.js.map +1 -0
  33. package/dist/diagnosticIdentity.d.ts +251 -0
  34. package/dist/diagnosticIdentity.d.ts.map +1 -0
  35. package/dist/diagnosticIdentity.js +721 -0
  36. package/dist/diagnosticIdentity.js.map +1 -0
  37. package/dist/diagnosticOrdering.d.ts +8 -0
  38. package/dist/diagnosticOrdering.d.ts.map +1 -0
  39. package/dist/diagnosticOrdering.js +73 -0
  40. package/dist/diagnosticOrdering.js.map +1 -0
  41. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  42. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  43. package/dist/diagnosticPeriodAxis.js +69 -0
  44. package/dist/diagnosticPeriodAxis.js.map +1 -0
  45. package/dist/diagnosticPeriods.d.ts +15 -0
  46. package/dist/diagnosticPeriods.d.ts.map +1 -0
  47. package/dist/diagnosticPeriods.js +50 -0
  48. package/dist/diagnosticPeriods.js.map +1 -0
  49. package/dist/diagnosticPreparation.d.ts +131 -0
  50. package/dist/diagnosticPreparation.d.ts.map +1 -0
  51. package/dist/diagnosticPreparation.js +1539 -0
  52. package/dist/diagnosticPreparation.js.map +1 -0
  53. package/dist/diagnosticReview.d.ts +66 -0
  54. package/dist/diagnosticReview.d.ts.map +1 -0
  55. package/dist/diagnosticReview.js +490 -0
  56. package/dist/diagnosticReview.js.map +1 -0
  57. package/dist/diagnosticRules.d.ts +32 -0
  58. package/dist/diagnosticRules.d.ts.map +1 -0
  59. package/dist/diagnosticRules.js +32 -0
  60. package/dist/diagnosticRules.js.map +1 -0
  61. package/dist/diagnosticRunner.d.ts +86 -0
  62. package/dist/diagnosticRunner.d.ts.map +1 -0
  63. package/dist/diagnosticRunner.js +703 -0
  64. package/dist/diagnosticRunner.js.map +1 -0
  65. package/dist/diagnosticRuntime.d.ts +26 -0
  66. package/dist/diagnosticRuntime.d.ts.map +1 -0
  67. package/dist/diagnosticRuntime.js +243 -0
  68. package/dist/diagnosticRuntime.js.map +1 -0
  69. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  70. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  71. package/dist/diagnosticSourceOrdering.js +38 -0
  72. package/dist/diagnosticSourceOrdering.js.map +1 -0
  73. package/dist/index.d.ts +28 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +16 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/mack.d.ts +4 -0
  78. package/dist/mack.d.ts.map +1 -1
  79. package/dist/mack.js +32 -4
  80. package/dist/mack.js.map +1 -1
  81. package/dist/triangle.d.ts.map +1 -1
  82. package/dist/triangle.js +59 -9
  83. package/dist/triangle.js.map +1 -1
  84. package/dist/types.d.ts +21 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js +128 -0
  87. package/dist/types.js.map +1 -1
  88. package/dist/version.d.ts +3 -0
  89. package/dist/version.d.ts.map +1 -0
  90. package/dist/version.js +3 -0
  91. package/dist/version.js.map +1 -0
  92. package/package.json +4 -3
  93. package/src/capping.ts +109 -18
  94. package/src/casualtyDiagnostics.ts +79 -189
  95. package/src/diagnosticAggregation.ts +128 -0
  96. package/src/diagnosticDefinitions.ts +3624 -0
  97. package/src/diagnosticDerivations.ts +321 -0
  98. package/src/diagnosticExposure.ts +482 -0
  99. package/src/diagnosticExpressions.ts +255 -0
  100. package/src/diagnosticFormulas.ts +350 -0
  101. package/src/diagnosticIdentity.ts +1267 -0
  102. package/src/diagnosticOrdering.ts +106 -0
  103. package/src/diagnosticPeriodAxis.ts +92 -0
  104. package/src/diagnosticPeriods.ts +102 -0
  105. package/src/diagnosticPreparation.ts +2475 -0
  106. package/src/diagnosticReview.ts +897 -0
  107. package/src/diagnosticRules.ts +69 -0
  108. package/src/diagnosticRunner.ts +1114 -0
  109. package/src/diagnosticRuntime.ts +264 -0
  110. package/src/diagnosticSourceOrdering.ts +50 -0
  111. package/src/index.ts +184 -1
  112. package/src/mack.ts +72 -9
  113. package/src/triangle.ts +103 -13
  114. package/src/types.ts +196 -0
  115. package/src/version.ts +2 -0
  116. package/dist/metricDiagnostics.d.ts +0 -212
  117. package/dist/metricDiagnostics.d.ts.map +0 -1
  118. package/dist/metricDiagnostics.js +0 -627
  119. package/dist/metricDiagnostics.js.map +0 -1
  120. package/src/metricDiagnostics.ts +0 -876
@@ -0,0 +1,321 @@
1
+ import {
2
+ assertCompiledDiagnosticDefinition,
3
+ getCompiledDiagnosticDefinitionInternals,
4
+ type CompiledDiagnosticDefinition,
5
+ } from "./diagnosticDefinitions.js";
6
+ import type { DiagnosticClaimExpression } from "./diagnosticExpressions.js";
7
+ import type { DiagnosticExpressionOverflow } from "./diagnosticRules.js";
8
+ import {
9
+ DiagnosticValidationError,
10
+ type DiagnosticValidationIssue,
11
+ } from "./types.js";
12
+ import {
13
+ diagnosticRecord,
14
+ hasDiagnosticOwn,
15
+ isDiagnosticPlainRecord,
16
+ normalizeDiagnosticNumber,
17
+ } from "./diagnosticRuntime.js";
18
+
19
+ export type DiagnosticMeasureValues = Readonly<Record<string, number | null>>;
20
+
21
+ export type DiagnosticRowWithDerivedMeasures<
22
+ TRow extends { measures: DiagnosticMeasureValues },
23
+ > = Omit<TRow, "measures"> & { readonly measures: DiagnosticMeasureValues };
24
+
25
+ export type DiagnosticDerivedValueAudit =
26
+ | { readonly status: "observed"; readonly value: number }
27
+ | { readonly status: "missing"; readonly value: null }
28
+ | {
29
+ readonly status: "non-finite";
30
+ readonly value: null;
31
+ readonly nonFiniteKind: "nan" | "positive-infinity" | "negative-infinity";
32
+ };
33
+
34
+ export interface DiagnosticDerivedRowAudit<
35
+ TRow extends { measures: DiagnosticMeasureValues },
36
+ > {
37
+ readonly row: DiagnosticRowWithDerivedMeasures<TRow>;
38
+ readonly derived: Readonly<Record<string, DiagnosticDerivedValueAudit>>;
39
+ readonly expressionOverflows: Readonly<
40
+ Record<string, readonly DiagnosticExpressionOverflow[]>
41
+ >;
42
+ }
43
+
44
+ interface ClaimExpressionAudit {
45
+ readonly state: DiagnosticDerivedValueAudit;
46
+ readonly overflows: readonly DiagnosticExpressionOverflow[];
47
+ }
48
+
49
+ function nonFiniteKind(
50
+ value: number,
51
+ ): "nan" | "positive-infinity" | "negative-infinity" {
52
+ return Number.isNaN(value)
53
+ ? "nan"
54
+ : value > 0
55
+ ? "positive-infinity"
56
+ : "negative-infinity";
57
+ }
58
+
59
+ function mergeNonFiniteKinds(
60
+ values: readonly ("nan" | "positive-infinity" | "negative-infinity")[],
61
+ ): "nan" | "positive-infinity" | "negative-infinity" {
62
+ const unique = new Set(values);
63
+ return unique.size === 1 ? values[0]! : "nan";
64
+ }
65
+
66
+ function neumaier(values: readonly number[]): {
67
+ readonly value: number | null;
68
+ readonly nonFiniteKind?: "nan" | "positive-infinity" | "negative-infinity";
69
+ } {
70
+ let sum = 0;
71
+ let correction = 0;
72
+ for (const value of values) {
73
+ const next = sum + value;
74
+ if (!Number.isFinite(next))
75
+ return { value: null, nonFiniteKind: nonFiniteKind(next) };
76
+ const nextCorrection =
77
+ correction +
78
+ (Math.abs(sum) >= Math.abs(value)
79
+ ? sum - next + value
80
+ : value - next + sum);
81
+ if (!Number.isFinite(nextCorrection))
82
+ return { value: null, nonFiniteKind: nonFiniteKind(nextCorrection) };
83
+ correction = nextCorrection;
84
+ sum = next;
85
+ }
86
+ const result = sum + correction;
87
+ return Number.isFinite(result)
88
+ ? { value: Object.is(result, -0) ? 0 : result }
89
+ : { value: null, nonFiniteKind: nonFiniteKind(result) };
90
+ }
91
+
92
+ function evaluateClaimExpressionAudit(
93
+ expression: DiagnosticClaimExpression,
94
+ states: Readonly<Record<string, DiagnosticDerivedValueAudit>>,
95
+ path: string,
96
+ ): ClaimExpressionAudit {
97
+ if (expression.op === "measure")
98
+ return {
99
+ state: hasDiagnosticOwn(states, expression.measureId)
100
+ ? states[expression.measureId]!
101
+ : { status: "missing", value: null },
102
+ overflows: [],
103
+ };
104
+ if (expression.op === "claim-layer") {
105
+ const source = hasDiagnosticOwn(states, expression.measureId)
106
+ ? states[expression.measureId]!
107
+ : { status: "missing" as const, value: null };
108
+ if (source.status !== "observed") return { state: source, overflows: [] };
109
+ const value =
110
+ source.value <= expression.attachment
111
+ ? 0
112
+ : expression.limit === null
113
+ ? source.value - expression.attachment
114
+ : Math.min(source.value - expression.attachment, expression.limit);
115
+ if (Number.isFinite(value))
116
+ return {
117
+ state: { status: "observed", value: normalizeDiagnosticNumber(value) },
118
+ overflows: [],
119
+ };
120
+ return {
121
+ state: {
122
+ status: "non-finite",
123
+ value: null,
124
+ nonFiniteKind: nonFiniteKind(value),
125
+ },
126
+ overflows: [{ expressionPath: path, sources: [] }],
127
+ };
128
+ }
129
+ const children =
130
+ expression.op === "add"
131
+ ? expression.terms.map((term, index) =>
132
+ evaluateClaimExpressionAudit(term, states, `${path}/terms/${index}`),
133
+ )
134
+ : [
135
+ evaluateClaimExpressionAudit(expression.left, states, `${path}/left`),
136
+ evaluateClaimExpressionAudit(
137
+ expression.right,
138
+ states,
139
+ `${path}/right`,
140
+ ),
141
+ ];
142
+ const overflows = children.flatMap((child) => child.overflows);
143
+ const nonFinite = children.flatMap((child) =>
144
+ child.state.status === "non-finite" ? [child.state.nonFiniteKind] : [],
145
+ );
146
+ if (nonFinite.length > 0)
147
+ return {
148
+ state: {
149
+ status: "non-finite",
150
+ value: null,
151
+ nonFiniteKind: mergeNonFiniteKinds(nonFinite),
152
+ },
153
+ overflows,
154
+ };
155
+ if (children.some((child) => child.state.status === "missing"))
156
+ return { state: { status: "missing", value: null }, overflows };
157
+ const values = children.map((child) => child.state.value!);
158
+ const calculation =
159
+ expression.op === "add"
160
+ ? neumaier(values)
161
+ : { value: values[0]! - values[1]! };
162
+ if (calculation.value !== null && Number.isFinite(calculation.value)) {
163
+ return {
164
+ state: {
165
+ status: "observed",
166
+ value: normalizeDiagnosticNumber(calculation.value),
167
+ },
168
+ overflows,
169
+ };
170
+ }
171
+ const failedKind =
172
+ calculation.nonFiniteKind ?? nonFiniteKind(calculation.value ?? Number.NaN);
173
+ return {
174
+ state: { status: "non-finite", value: null, nonFiniteKind: failedKind },
175
+ overflows: [...overflows, { expressionPath: path, sources: [] }],
176
+ };
177
+ }
178
+
179
+ function validateRows<TRow extends { measures: DiagnosticMeasureValues }>(
180
+ rows: readonly TRow[],
181
+ definition: CompiledDiagnosticDefinition,
182
+ ): void {
183
+ const internals = getCompiledDiagnosticDefinitionInternals(definition);
184
+ const issues: DiagnosticValidationIssue[] = [];
185
+ if (definition.definition.lossRowGrain !== "claim") {
186
+ issues.push({
187
+ domain: "input",
188
+ code: "invalid-input-relationship",
189
+ path: "$.definition.lossRowGrain",
190
+ message: "Claim derivation requires a claim-grain definition",
191
+ });
192
+ }
193
+ if (!Array.isArray(rows)) {
194
+ issues.push({
195
+ domain: "input",
196
+ code: "invalid-type",
197
+ path: "$.rows",
198
+ message: "Claim rows must be an array",
199
+ });
200
+ } else {
201
+ rows.forEach((row, rowIndex) => {
202
+ const measures = row?.measures;
203
+ if (!isDiagnosticPlainRecord(measures)) {
204
+ issues.push({
205
+ domain: "input",
206
+ code: "invalid-type",
207
+ path: `$.rows[${rowIndex}].measures`,
208
+ message: "Row measures must be a plain record",
209
+ });
210
+ return;
211
+ }
212
+ for (const [measureId, value] of Object.entries(measures)) {
213
+ const measure = internals.measuresById.get(measureId);
214
+ const path = `$.rows[${rowIndex}].measures[${JSON.stringify(measureId)}]`;
215
+ if (!measure)
216
+ issues.push({
217
+ domain: "input",
218
+ code: "unknown-reference",
219
+ path,
220
+ message: `Unknown measure ${measureId}`,
221
+ });
222
+ else if (measure.source !== "loss")
223
+ issues.push({
224
+ domain: "input",
225
+ code: "invalid-input-relationship",
226
+ path,
227
+ message: `Caller rows cannot supply ${measure.source} measure ${measureId}`,
228
+ });
229
+ if (value !== null && typeof value !== "number")
230
+ issues.push({
231
+ domain: "input",
232
+ code: "invalid-type",
233
+ path,
234
+ message: "Measure value must be a number or null",
235
+ });
236
+ }
237
+ });
238
+ }
239
+ if (issues.length > 0) throw new DiagnosticValidationError(issues);
240
+ }
241
+
242
+ /** @internal Shared with diagnostic preparation to preserve derivation quality. */
243
+ export function deriveDiagnosticClaimMeasuresWithAudit<
244
+ TRow extends { measures: DiagnosticMeasureValues },
245
+ >(
246
+ rows: readonly TRow[],
247
+ definition: CompiledDiagnosticDefinition,
248
+ ): readonly DiagnosticDerivedRowAudit<TRow>[] {
249
+ assertCompiledDiagnosticDefinition(definition);
250
+ validateRows(rows, definition);
251
+ const internals = getCompiledDiagnosticDefinitionInternals(definition);
252
+ const plan = [...internals.derivationsByOutputMeasureId.values()];
253
+ const normalizedIndex = new Map(
254
+ definition.definition.derivedMeasures.map((derivation, index) => [
255
+ derivation.outputMeasureId,
256
+ index,
257
+ ]),
258
+ );
259
+ return Object.freeze(
260
+ rows.map((row) => {
261
+ const measures = diagnosticRecord<number | null>();
262
+ const states = diagnosticRecord<DiagnosticDerivedValueAudit>();
263
+ for (const [id, value] of Object.entries(row.measures)) {
264
+ measures[id] = value;
265
+ states[id] =
266
+ value === null || value === undefined
267
+ ? { status: "missing" as const, value: null }
268
+ : Number.isFinite(value)
269
+ ? {
270
+ status: "observed" as const,
271
+ value: normalizeDiagnosticNumber(value),
272
+ }
273
+ : {
274
+ status: "non-finite" as const,
275
+ value: null,
276
+ nonFiniteKind: nonFiniteKind(value),
277
+ };
278
+ }
279
+ const derived = diagnosticRecord<DiagnosticDerivedValueAudit>();
280
+ const expressionOverflows =
281
+ diagnosticRecord<readonly DiagnosticExpressionOverflow[]>();
282
+ for (const derivation of plan) {
283
+ const index = normalizedIndex.get(derivation.outputMeasureId)!;
284
+ const evaluated = evaluateClaimExpressionAudit(
285
+ derivation.expression,
286
+ states,
287
+ `/derivedMeasures/${index}/expression`,
288
+ );
289
+ states[derivation.outputMeasureId] = evaluated.state;
290
+ derived[derivation.outputMeasureId] = evaluated.state;
291
+ expressionOverflows[derivation.outputMeasureId] = Object.freeze([
292
+ ...evaluated.overflows,
293
+ ]);
294
+ measures[derivation.outputMeasureId] = evaluated.state.value;
295
+ }
296
+ return Object.freeze({
297
+ row: Object.freeze({
298
+ ...row,
299
+ measures: Object.freeze(measures),
300
+ }) as DiagnosticRowWithDerivedMeasures<TRow>,
301
+ derived: Object.freeze(derived),
302
+ expressionOverflows: Object.freeze(expressionOverflows),
303
+ });
304
+ }),
305
+ );
306
+ }
307
+
308
+ /** Materializes compiler-approved claim-level measures without mutating caller rows. */
309
+ export function deriveDiagnosticClaimMeasures<
310
+ TRow extends { measures: DiagnosticMeasureValues },
311
+ >(
312
+ rows: readonly TRow[],
313
+ definition: CompiledDiagnosticDefinition,
314
+ ): readonly DiagnosticRowWithDerivedMeasures<TRow>[] {
315
+ assertCompiledDiagnosticDefinition(definition);
316
+ return Object.freeze(
317
+ deriveDiagnosticClaimMeasuresWithAudit(rows, definition).map(
318
+ (item) => item.row,
319
+ ),
320
+ );
321
+ }