@actuarial-ts/core 0.6.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 (103) hide show
  1. package/README.md +3 -3
  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/diagnosticAggregation.d.ts +3 -0
  6. package/dist/diagnosticAggregation.d.ts.map +1 -1
  7. package/dist/diagnosticAggregation.js +30 -7
  8. package/dist/diagnosticAggregation.js.map +1 -1
  9. package/dist/diagnosticDefinitions.d.ts +3 -0
  10. package/dist/diagnosticDefinitions.d.ts.map +1 -1
  11. package/dist/diagnosticDefinitions.js +844 -182
  12. package/dist/diagnosticDefinitions.js.map +1 -1
  13. package/dist/diagnosticDerivations.d.ts +23 -0
  14. package/dist/diagnosticDerivations.d.ts.map +1 -1
  15. package/dist/diagnosticDerivations.js +187 -43
  16. package/dist/diagnosticDerivations.js.map +1 -1
  17. package/dist/diagnosticExposure.d.ts +2 -2
  18. package/dist/diagnosticExposure.d.ts.map +1 -1
  19. package/dist/diagnosticExposure.js +228 -20
  20. package/dist/diagnosticExposure.js.map +1 -1
  21. package/dist/diagnosticExpressions.d.ts +1 -1
  22. package/dist/diagnosticExpressions.d.ts.map +1 -1
  23. package/dist/diagnosticExpressions.js +35 -15
  24. package/dist/diagnosticExpressions.js.map +1 -1
  25. package/dist/diagnosticFormulas.d.ts +3 -0
  26. package/dist/diagnosticFormulas.d.ts.map +1 -1
  27. package/dist/diagnosticFormulas.js +160 -26
  28. package/dist/diagnosticFormulas.js.map +1 -1
  29. package/dist/diagnosticIdentity.d.ts +15 -1
  30. package/dist/diagnosticIdentity.d.ts.map +1 -1
  31. package/dist/diagnosticIdentity.js +318 -51
  32. package/dist/diagnosticIdentity.js.map +1 -1
  33. package/dist/diagnosticOrdering.d.ts +8 -0
  34. package/dist/diagnosticOrdering.d.ts.map +1 -0
  35. package/dist/diagnosticOrdering.js +73 -0
  36. package/dist/diagnosticOrdering.js.map +1 -0
  37. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  38. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  39. package/dist/diagnosticPeriodAxis.js +69 -0
  40. package/dist/diagnosticPeriodAxis.js.map +1 -0
  41. package/dist/diagnosticPeriods.d.ts +2 -3
  42. package/dist/diagnosticPeriods.d.ts.map +1 -1
  43. package/dist/diagnosticPeriods.js +20 -68
  44. package/dist/diagnosticPeriods.js.map +1 -1
  45. package/dist/diagnosticPreparation.d.ts +47 -14
  46. package/dist/diagnosticPreparation.d.ts.map +1 -1
  47. package/dist/diagnosticPreparation.js +1482 -112
  48. package/dist/diagnosticPreparation.js.map +1 -1
  49. package/dist/diagnosticReview.d.ts +36 -14
  50. package/dist/diagnosticReview.d.ts.map +1 -1
  51. package/dist/diagnosticReview.js +448 -42
  52. package/dist/diagnosticReview.js.map +1 -1
  53. package/dist/diagnosticRunner.d.ts +2 -1
  54. package/dist/diagnosticRunner.d.ts.map +1 -1
  55. package/dist/diagnosticRunner.js +656 -70
  56. package/dist/diagnosticRunner.js.map +1 -1
  57. package/dist/diagnosticRuntime.d.ts +26 -0
  58. package/dist/diagnosticRuntime.d.ts.map +1 -0
  59. package/dist/diagnosticRuntime.js +243 -0
  60. package/dist/diagnosticRuntime.js.map +1 -0
  61. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  62. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  63. package/dist/diagnosticSourceOrdering.js +38 -0
  64. package/dist/diagnosticSourceOrdering.js.map +1 -0
  65. package/dist/index.d.ts +8 -4
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +5 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/mack.d.ts +4 -0
  70. package/dist/mack.d.ts.map +1 -1
  71. package/dist/mack.js +32 -4
  72. package/dist/mack.js.map +1 -1
  73. package/dist/triangle.d.ts.map +1 -1
  74. package/dist/triangle.js +59 -9
  75. package/dist/triangle.js.map +1 -1
  76. package/dist/types.d.ts +4 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js +22 -8
  79. package/dist/types.js.map +1 -1
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +3 -2
  83. package/src/capping.ts +109 -18
  84. package/src/diagnosticAggregation.ts +69 -20
  85. package/src/diagnosticDefinitions.ts +2675 -426
  86. package/src/diagnosticDerivations.ts +281 -49
  87. package/src/diagnosticExposure.ts +441 -50
  88. package/src/diagnosticExpressions.ts +99 -17
  89. package/src/diagnosticFormulas.ts +239 -39
  90. package/src/diagnosticIdentity.ts +691 -191
  91. package/src/diagnosticOrdering.ts +106 -0
  92. package/src/diagnosticPeriodAxis.ts +92 -0
  93. package/src/diagnosticPeriods.ts +67 -82
  94. package/src/diagnosticPreparation.ts +2369 -127
  95. package/src/diagnosticReview.ts +864 -58
  96. package/src/diagnosticRunner.ts +1076 -72
  97. package/src/diagnosticRuntime.ts +264 -0
  98. package/src/diagnosticSourceOrdering.ts +50 -0
  99. package/src/index.ts +31 -3
  100. package/src/mack.ts +72 -9
  101. package/src/triangle.ts +103 -13
  102. package/src/types.ts +55 -20
  103. package/src/version.ts +1 -1
@@ -4,86 +4,318 @@ import {
4
4
  type CompiledDiagnosticDefinition,
5
5
  } from "./diagnosticDefinitions.js";
6
6
  import type { DiagnosticClaimExpression } from "./diagnosticExpressions.js";
7
- import { DiagnosticValidationError, type DiagnosticValidationIssue } from "./types.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";
8
18
 
9
19
  export type DiagnosticMeasureValues = Readonly<Record<string, number | null>>;
10
20
 
11
- export type DiagnosticRowWithDerivedMeasures<TRow extends { measures: DiagnosticMeasureValues }> =
12
- Omit<TRow, "measures"> & { readonly measures: DiagnosticMeasureValues };
21
+ export type DiagnosticRowWithDerivedMeasures<
22
+ TRow extends { measures: DiagnosticMeasureValues },
23
+ > = Omit<TRow, "measures"> & { readonly measures: DiagnosticMeasureValues };
13
24
 
14
- function evaluateClaimExpression(
15
- expression: DiagnosticClaimExpression,
16
- measures: Readonly<Record<string, number | null>>,
17
- ): number | null {
18
- if (expression.op === "measure") {
19
- const value = measures[expression.measureId];
20
- return typeof value === "number" && Number.isFinite(value) ? value : null;
21
- }
22
- if (expression.op === "claim-layer") {
23
- const value = measures[expression.measureId];
24
- if (typeof value !== "number" || !Number.isFinite(value)) return null;
25
- const excess = Math.max(value - expression.attachment, 0);
26
- const result = expression.limit === null ? excess : Math.min(excess, expression.limit);
27
- return Number.isFinite(result) ? (Object.is(result, -0) ? 0 : result) : null;
28
- }
29
- if (expression.op === "subtract") {
30
- const left = evaluateClaimExpression(expression.left, measures);
31
- const right = evaluateClaimExpression(expression.right, measures);
32
- if (left === null || right === null) return null;
33
- const result = left - right;
34
- return Number.isFinite(result) ? (Object.is(result, -0) ? 0 : result) : null;
35
- }
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
+ } {
36
70
  let sum = 0;
37
71
  let correction = 0;
38
- for (const term of expression.terms) {
39
- const value = evaluateClaimExpression(term, measures);
40
- if (value === null) return null;
72
+ for (const value of values) {
41
73
  const next = sum + value;
42
- correction += Math.abs(sum) >= Math.abs(value) ? (sum - next) + value : (value - next) + sum;
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;
43
84
  sum = next;
44
- if (!Number.isFinite(sum) || !Number.isFinite(correction)) return null;
45
85
  }
46
86
  const result = sum + correction;
47
- return Number.isFinite(result) ? (Object.is(result, -0) ? 0 : result) : null;
87
+ return Number.isFinite(result)
88
+ ? { value: Object.is(result, -0) ? 0 : result }
89
+ : { value: null, nonFiniteKind: nonFiniteKind(result) };
48
90
  }
49
91
 
50
- /** Materializes compiler-approved claim-level measures without mutating caller rows. */
51
- export function deriveDiagnosticClaimMeasures<TRow extends { measures: DiagnosticMeasureValues }>(
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 }>(
52
180
  rows: readonly TRow[],
53
181
  definition: CompiledDiagnosticDefinition,
54
- ): readonly DiagnosticRowWithDerivedMeasures<TRow>[] {
55
- assertCompiledDiagnosticDefinition(definition);
182
+ ): void {
56
183
  const internals = getCompiledDiagnosticDefinitionInternals(definition);
57
184
  const issues: DiagnosticValidationIssue[] = [];
58
185
  if (definition.definition.lossRowGrain !== "claim") {
59
- issues.push({ domain: "input", code: "invalid-input-relationship", path: "$.definition.lossRowGrain", message: "Claim derivation requires a claim-grain definition" });
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
+ });
60
192
  }
61
193
  if (!Array.isArray(rows)) {
62
- issues.push({ domain: "input", code: "invalid-type", path: "$.rows", message: "Claim rows must be an array" });
194
+ issues.push({
195
+ domain: "input",
196
+ code: "invalid-type",
197
+ path: "$.rows",
198
+ message: "Claim rows must be an array",
199
+ });
63
200
  } else {
64
201
  rows.forEach((row, rowIndex) => {
65
202
  const measures = row?.measures;
66
- if (measures === null || typeof measures !== "object" || Array.isArray(measures)) {
67
- issues.push({ domain: "input", code: "invalid-type", path: `$.rows[${rowIndex}].measures`, message: "Row measures must be a plain record" });
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
+ });
68
210
  return;
69
211
  }
70
212
  for (const [measureId, value] of Object.entries(measures)) {
71
213
  const measure = internals.measuresById.get(measureId);
72
214
  const path = `$.rows[${rowIndex}].measures[${JSON.stringify(measureId)}]`;
73
- if (!measure) issues.push({ domain: "input", code: "unknown-reference", path, message: `Unknown measure ${measureId}` });
74
- else if (measure.source !== "loss") issues.push({ domain: "input", code: "invalid-input-relationship", path, message: `Caller rows cannot supply ${measure.source} measure ${measureId}` });
75
- if (value !== null && typeof value !== "number") issues.push({ domain: "input", code: "invalid-type", path, message: "Measure value must be a number or null" });
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
+ });
76
236
  }
77
237
  });
78
238
  }
79
239
  if (issues.length > 0) throw new DiagnosticValidationError(issues);
240
+ }
80
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);
81
252
  const plan = [...internals.derivationsByOutputMeasureId.values()];
82
- return Object.freeze(rows.map((row) => {
83
- const measures: Record<string, number | null> = { ...row.measures };
84
- for (const derivation of plan) {
85
- measures[derivation.outputMeasureId] = evaluateClaimExpression(derivation.expression, measures);
86
- }
87
- return Object.freeze({ ...row, measures: Object.freeze(measures) }) as DiagnosticRowWithDerivedMeasures<TRow>;
88
- }));
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
+ );
89
321
  }