@actuarial-ts/core 0.6.0 → 0.7.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.
Files changed (133) hide show
  1. package/README.md +12 -3
  2. package/dist/canonical.d.ts +11 -0
  3. package/dist/canonical.d.ts.map +1 -1
  4. package/dist/canonical.js +136 -26
  5. package/dist/canonical.js.map +1 -1
  6. package/dist/capping.d.ts.map +1 -1
  7. package/dist/capping.js +39 -6
  8. package/dist/capping.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +3 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -1
  11. package/dist/diagnosticAggregation.js +30 -7
  12. package/dist/diagnosticAggregation.js.map +1 -1
  13. package/dist/diagnosticDefinitions.d.ts +3 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -1
  15. package/dist/diagnosticDefinitions.js +844 -182
  16. package/dist/diagnosticDefinitions.js.map +1 -1
  17. package/dist/diagnosticDerivations.d.ts +23 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -1
  19. package/dist/diagnosticDerivations.js +187 -43
  20. package/dist/diagnosticDerivations.js.map +1 -1
  21. package/dist/diagnosticEvidenceIntern.d.ts +16 -0
  22. package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
  23. package/dist/diagnosticEvidenceIntern.js +139 -0
  24. package/dist/diagnosticEvidenceIntern.js.map +1 -0
  25. package/dist/diagnosticExposure.d.ts +2 -2
  26. package/dist/diagnosticExposure.d.ts.map +1 -1
  27. package/dist/diagnosticExposure.js +319 -21
  28. package/dist/diagnosticExposure.js.map +1 -1
  29. package/dist/diagnosticExpressions.d.ts +1 -1
  30. package/dist/diagnosticExpressions.d.ts.map +1 -1
  31. package/dist/diagnosticExpressions.js +35 -15
  32. package/dist/diagnosticExpressions.js.map +1 -1
  33. package/dist/diagnosticFormulas.d.ts +3 -0
  34. package/dist/diagnosticFormulas.d.ts.map +1 -1
  35. package/dist/diagnosticFormulas.js +160 -26
  36. package/dist/diagnosticFormulas.js.map +1 -1
  37. package/dist/diagnosticIdentity.d.ts +15 -1
  38. package/dist/diagnosticIdentity.d.ts.map +1 -1
  39. package/dist/diagnosticIdentity.js +321 -51
  40. package/dist/diagnosticIdentity.js.map +1 -1
  41. package/dist/diagnosticIdentityStream.d.ts +56 -0
  42. package/dist/diagnosticIdentityStream.d.ts.map +1 -0
  43. package/dist/diagnosticIdentityStream.js +472 -0
  44. package/dist/diagnosticIdentityStream.js.map +1 -0
  45. package/dist/diagnosticOrdering.d.ts +8 -0
  46. package/dist/diagnosticOrdering.d.ts.map +1 -0
  47. package/dist/diagnosticOrdering.js +73 -0
  48. package/dist/diagnosticOrdering.js.map +1 -0
  49. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  50. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  51. package/dist/diagnosticPeriodAxis.js +69 -0
  52. package/dist/diagnosticPeriodAxis.js.map +1 -0
  53. package/dist/diagnosticPeriods.d.ts +2 -3
  54. package/dist/diagnosticPeriods.d.ts.map +1 -1
  55. package/dist/diagnosticPeriods.js +20 -68
  56. package/dist/diagnosticPeriods.js.map +1 -1
  57. package/dist/diagnosticPreparation.d.ts +75 -14
  58. package/dist/diagnosticPreparation.d.ts.map +1 -1
  59. package/dist/diagnosticPreparation.js +1606 -112
  60. package/dist/diagnosticPreparation.js.map +1 -1
  61. package/dist/diagnosticReview.d.ts +40 -15
  62. package/dist/diagnosticReview.d.ts.map +1 -1
  63. package/dist/diagnosticReview.js +600 -42
  64. package/dist/diagnosticReview.js.map +1 -1
  65. package/dist/diagnosticReviewSources.d.ts +18 -0
  66. package/dist/diagnosticReviewSources.d.ts.map +1 -0
  67. package/dist/diagnosticReviewSources.js +76 -0
  68. package/dist/diagnosticReviewSources.js.map +1 -0
  69. package/dist/diagnosticReviewStore.d.ts +88 -0
  70. package/dist/diagnosticReviewStore.d.ts.map +1 -0
  71. package/dist/diagnosticReviewStore.js +455 -0
  72. package/dist/diagnosticReviewStore.js.map +1 -0
  73. package/dist/diagnosticRunner.d.ts +27 -2
  74. package/dist/diagnosticRunner.d.ts.map +1 -1
  75. package/dist/diagnosticRunner.js +764 -70
  76. package/dist/diagnosticRunner.js.map +1 -1
  77. package/dist/diagnosticRuntime.d.ts +26 -0
  78. package/dist/diagnosticRuntime.d.ts.map +1 -0
  79. package/dist/diagnosticRuntime.js +243 -0
  80. package/dist/diagnosticRuntime.js.map +1 -0
  81. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  82. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  83. package/dist/diagnosticSourceOrdering.js +38 -0
  84. package/dist/diagnosticSourceOrdering.js.map +1 -0
  85. package/dist/fnvAccumulator.d.ts +6 -0
  86. package/dist/fnvAccumulator.d.ts.map +1 -0
  87. package/dist/fnvAccumulator.js +21 -0
  88. package/dist/fnvAccumulator.js.map +1 -0
  89. package/dist/index.d.ts +15 -8
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +10 -4
  92. package/dist/index.js.map +1 -1
  93. package/dist/mack.d.ts +4 -0
  94. package/dist/mack.d.ts.map +1 -1
  95. package/dist/mack.js +32 -4
  96. package/dist/mack.js.map +1 -1
  97. package/dist/triangle.d.ts.map +1 -1
  98. package/dist/triangle.js +59 -9
  99. package/dist/triangle.js.map +1 -1
  100. package/dist/types.d.ts +4 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js +22 -8
  103. package/dist/types.js.map +1 -1
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +3 -2
  107. package/src/canonical.ts +144 -26
  108. package/src/capping.ts +109 -18
  109. package/src/diagnosticAggregation.ts +69 -20
  110. package/src/diagnosticDefinitions.ts +2675 -426
  111. package/src/diagnosticDerivations.ts +281 -49
  112. package/src/diagnosticEvidenceIntern.ts +151 -0
  113. package/src/diagnosticExposure.ts +550 -50
  114. package/src/diagnosticExpressions.ts +99 -17
  115. package/src/diagnosticFormulas.ts +239 -39
  116. package/src/diagnosticIdentity.ts +700 -191
  117. package/src/diagnosticIdentityStream.ts +553 -0
  118. package/src/diagnosticOrdering.ts +106 -0
  119. package/src/diagnosticPeriodAxis.ts +92 -0
  120. package/src/diagnosticPeriods.ts +67 -82
  121. package/src/diagnosticPreparation.ts +2558 -132
  122. package/src/diagnosticReview.ts +1072 -58
  123. package/src/diagnosticReviewSources.ts +85 -0
  124. package/src/diagnosticReviewStore.ts +692 -0
  125. package/src/diagnosticRunner.ts +1273 -72
  126. package/src/diagnosticRuntime.ts +264 -0
  127. package/src/diagnosticSourceOrdering.ts +50 -0
  128. package/src/fnvAccumulator.ts +20 -0
  129. package/src/index.ts +69 -4
  130. package/src/mack.ts +72 -9
  131. package/src/triangle.ts +103 -13
  132. package/src/types.ts +55 -20
  133. package/src/version.ts +1 -1
package/src/triangle.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  TriangleKind,
6
6
  } from "./types.js";
7
7
  import { ReservingError } from "./types.js";
8
+ import { isDiagnosticToken, isRealIsoDate } from "./diagnosticRuntime.js";
8
9
 
9
10
  /**
10
11
  * Builds development triangles from claim-level evaluation snapshots.
@@ -27,16 +28,12 @@ interface ParsedDate {
27
28
  }
28
29
 
29
30
  function parseISO(date: string): ParsedDate {
30
- const match = /^(\d{4})-(\d{2})-(\d{2})/.exec(date);
31
- if (!match) {
31
+ if (!isRealIsoDate(date))
32
32
  throw new ReservingError("BAD_DATE", `Invalid ISO date: "${date}"`);
33
- }
33
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date)!;
34
34
  const y = Number(match[1]);
35
35
  const m = Number(match[2]);
36
36
  const d = Number(match[3]);
37
- if (m < 1 || m > 12 || d < 1 || d > 31) {
38
- throw new ReservingError("BAD_DATE", `Invalid ISO date: "${date}"`);
39
- }
40
37
  return { y, m, d };
41
38
  }
42
39
 
@@ -90,6 +87,7 @@ export interface TriangleSet {
90
87
 
91
88
  interface ClaimTimeline {
92
89
  originIdx: number;
90
+ accidentISO: string;
93
91
  reportISO: string;
94
92
  /** Snapshots sorted ascending by evaluation date. */
95
93
  snapshots: ClaimSnapshot[];
@@ -101,9 +99,17 @@ export function buildTriangles(
101
99
  ): TriangleSet {
102
100
  const { cadence, asOfDate } = options;
103
101
  if (claims.length === 0) {
104
- throw new ReservingError("NO_CLAIMS", "Cannot build triangles from an empty loss run");
102
+ throw new ReservingError(
103
+ "NO_CLAIMS",
104
+ "Cannot build triangles from an empty loss run",
105
+ );
105
106
  }
106
107
  const asOf = parseISO(asOfDate);
108
+ if (cadence !== "annual" && cadence !== "quarterly")
109
+ throw new ReservingError(
110
+ "BAD_ORIGIN",
111
+ "Cadence must be annual or quarterly",
112
+ );
107
113
  const asOfMonth = monthIndex(asOf);
108
114
  const asOfIsMonthEnd = asOf.d === daysInMonth(asOf.y, asOf.m);
109
115
  // The latest complete evaluation month.
@@ -115,15 +121,53 @@ export function buildTriangles(
115
121
  let minPeriod = Infinity;
116
122
  let maxPeriod = -Infinity;
117
123
  for (const snap of claims) {
124
+ if (!isDiagnosticToken(snap.claimId))
125
+ throw new ReservingError(
126
+ "BAD_ORIGIN",
127
+ "Claim id must be a nonempty token",
128
+ );
118
129
  const accident = parseISO(snap.accidentDate);
130
+ parseISO(snap.reportDate);
131
+ parseISO(snap.evaluationDate);
132
+ if (
133
+ snap.reportDate < snap.accidentDate ||
134
+ snap.evaluationDate < snap.reportDate
135
+ )
136
+ throw new ReservingError(
137
+ "BAD_DATE",
138
+ `Claim ${snap.claimId} dates must satisfy accident <= report <= evaluation`,
139
+ );
140
+ if (!Number.isFinite(snap.paidToDate) || !Number.isFinite(snap.caseReserve))
141
+ throw new ReservingError(
142
+ "BAD_LOSSES",
143
+ `Claim ${snap.claimId} contains a non-finite amount`,
144
+ );
145
+ if (snap.status !== "open" && snap.status !== "closed")
146
+ throw new ReservingError(
147
+ "BAD_LOSSES",
148
+ `Claim ${snap.claimId} has an invalid status`,
149
+ );
119
150
  if (snap.evaluationDate > asOfDate) continue; // beyond the analysis date
120
151
  const period = periodIndexOf(accident, cadence);
121
152
  minPeriod = Math.min(minPeriod, period);
122
153
  maxPeriod = Math.max(maxPeriod, period);
123
154
  let timeline = byClaim.get(snap.claimId);
124
155
  if (!timeline) {
125
- timeline = { originIdx: period, reportISO: snap.reportDate, snapshots: [] };
156
+ timeline = {
157
+ originIdx: period,
158
+ accidentISO: snap.accidentDate,
159
+ reportISO: snap.reportDate,
160
+ snapshots: [],
161
+ };
126
162
  byClaim.set(snap.claimId, timeline);
163
+ } else if (
164
+ timeline.accidentISO !== snap.accidentDate ||
165
+ timeline.reportISO !== snap.reportDate
166
+ ) {
167
+ throw new ReservingError(
168
+ "BAD_ORIGIN",
169
+ `Claim ${snap.claimId} has conflicting accident or report dates across snapshots`,
170
+ );
127
171
  }
128
172
  timeline.snapshots.push(snap);
129
173
  }
@@ -135,13 +179,18 @@ export function buildTriangles(
135
179
  }
136
180
  for (const timeline of byClaim.values()) {
137
181
  timeline.snapshots.sort((a, b) =>
138
- a.evaluationDate < b.evaluationDate ? -1 : a.evaluationDate > b.evaluationDate ? 1 : 0,
182
+ a.evaluationDate < b.evaluationDate
183
+ ? -1
184
+ : a.evaluationDate > b.evaluationDate
185
+ ? 1
186
+ : 0,
139
187
  );
140
188
  }
141
189
 
142
190
  const nOrigins = maxPeriod - minPeriod + 1;
143
191
  const origins: string[] = [];
144
- for (let p = minPeriod; p <= maxPeriod; p++) origins.push(periodLabel(p, cadence));
192
+ for (let p = minPeriod; p <= maxPeriod; p++)
193
+ origins.push(periodLabel(p, cadence));
145
194
 
146
195
  // Ages available to the oldest origin period determine the column count.
147
196
  const oldestStart = periodStartMonth(minPeriod, cadence);
@@ -198,7 +247,8 @@ export function buildTriangles(
198
247
  else break;
199
248
  }
200
249
  const paid = state?.paidToDate ?? 0;
201
- const caseReserve = state?.status === "open" ? (state?.caseReserve ?? 0) : 0;
250
+ const caseReserve =
251
+ state?.status === "open" ? (state?.caseReserve ?? 0) : 0;
202
252
  const isClosed = state?.status === "closed";
203
253
  add(set.reportedCount, i, j, 1);
204
254
  add(set.paid, i, j, paid);
@@ -223,13 +273,53 @@ export function triangleFromGrid(
223
273
  ages: number[],
224
274
  values: (number | null)[][],
225
275
  ): Triangle {
276
+ const kinds: readonly TriangleKind[] = [
277
+ "paid",
278
+ "incurred",
279
+ "caseReserve",
280
+ "reportedCount",
281
+ "openCount",
282
+ "closedCount",
283
+ "closedWithPayCount",
284
+ ];
285
+ if (!kinds.includes(kind))
286
+ throw new ReservingError("SHAPE", "Unknown triangle kind");
287
+ if (
288
+ origins.some((origin) => !isDiagnosticToken(origin)) ||
289
+ new Set(origins).size !== origins.length
290
+ ) {
291
+ throw new ReservingError("SHAPE", "Origins must be unique nonempty tokens");
292
+ }
293
+ if (
294
+ ages.some((age) => !Number.isSafeInteger(age) || age <= 0) ||
295
+ ages.some((age, index) => index > 0 && age <= ages[index - 1]!)
296
+ ) {
297
+ throw new ReservingError(
298
+ "SHAPE",
299
+ "Development ages must be finite positive safe integers in strictly ascending order",
300
+ );
301
+ }
226
302
  if (values.length !== origins.length) {
227
303
  throw new ReservingError("SHAPE", "Row count does not match origin count");
228
304
  }
229
305
  for (const row of values) {
230
306
  if (row.length !== ages.length) {
231
- throw new ReservingError("SHAPE", "Column count does not match age count");
307
+ throw new ReservingError(
308
+ "SHAPE",
309
+ "Column count does not match age count",
310
+ );
311
+ }
312
+ if (row.some((value) => value !== null && !Number.isFinite(value))) {
313
+ throw new ReservingError(
314
+ "SHAPE",
315
+ "Observed triangle cells must be finite numbers",
316
+ );
232
317
  }
233
318
  }
234
- return { kind, origins: [...origins], ages: [...ages], values: values.map((r) => [...r]) };
319
+ return {
320
+ kind,
321
+ origins: [...origins],
322
+ ages: [...ages],
323
+ values: values.map((r) => [...r]),
324
+ };
235
325
  }
package/src/types.ts CHANGED
@@ -190,6 +190,10 @@ export interface MackResult {
190
190
  tailFactor?: number;
191
191
  /** Extrapolated sigma^2 for the tail step; present only when a tail was applied. */
192
192
  sigmaSquaredTail?: number;
193
+ /** Explicit tail-factor estimation error when supplied under Mack (1999). */
194
+ tailStandardError?: number;
195
+ /** Explicit tail random-error scale when supplied under Mack (1999). */
196
+ tailSigma?: number;
193
197
  rows: MackRow[];
194
198
  totals: {
195
199
  latest: number;
@@ -363,13 +367,20 @@ type DiagnosticPathSegment =
363
367
  | { readonly kind: "property"; readonly value: string }
364
368
  | { readonly kind: "index"; readonly value: number };
365
369
 
366
- function diagnosticPathSegments(path: string): readonly DiagnosticPathSegment[] {
370
+ function diagnosticPathSegments(
371
+ path: string,
372
+ ): readonly DiagnosticPathSegment[] {
367
373
  const segments: DiagnosticPathSegment[] = [];
368
374
  let cursor = 1;
369
375
  while (cursor < path.length) {
370
376
  if (path[cursor] === ".") {
371
377
  const start = ++cursor;
372
- while (cursor < path.length && path[cursor] !== "." && path[cursor] !== "[") cursor++;
378
+ while (
379
+ cursor < path.length &&
380
+ path[cursor] !== "." &&
381
+ path[cursor] !== "["
382
+ )
383
+ cursor++;
373
384
  segments.push({ kind: "property", value: path.slice(start, cursor) });
374
385
  continue;
375
386
  }
@@ -377,10 +388,14 @@ function diagnosticPathSegments(path: string): readonly DiagnosticPathSegment[]
377
388
  const close = path.indexOf("]", cursor);
378
389
  if (close < 0) return [{ kind: "property", value: path }];
379
390
  const token = path.slice(cursor + 1, close);
380
- if (/^\d+$/.test(token)) segments.push({ kind: "index", value: Number(token) });
391
+ if (/^\d+$/.test(token))
392
+ segments.push({ kind: "index", value: Number(token) });
381
393
  else {
382
394
  try {
383
- segments.push({ kind: "property", value: JSON.parse(token) as string });
395
+ segments.push({
396
+ kind: "property",
397
+ value: JSON.parse(token) as string,
398
+ });
384
399
  } catch {
385
400
  return [{ kind: "property", value: path }];
386
401
  }
@@ -402,12 +417,14 @@ function compareDiagnosticPaths(left: string, right: string): number {
402
417
  const x = a[index]!;
403
418
  const y = b[index]!;
404
419
  if (x.kind !== y.kind) return x.kind === "property" ? -1 : 1;
405
- const compared = x.kind === "index"
406
- ? x.value - (y as Extract<DiagnosticPathSegment, { kind: "index" }>).value
407
- : compareCodeUnits(
408
- x.value,
409
- (y as Extract<DiagnosticPathSegment, { kind: "property" }>).value,
410
- );
420
+ const compared =
421
+ x.kind === "index"
422
+ ? x.value -
423
+ (y as Extract<DiagnosticPathSegment, { kind: "index" }>).value
424
+ : compareCodeUnits(
425
+ x.value,
426
+ (y as Extract<DiagnosticPathSegment, { kind: "property" }>).value,
427
+ );
411
428
  if (compared !== 0) return compared;
412
429
  }
413
430
  return a.length - b.length;
@@ -419,19 +436,37 @@ function normalizeDiagnosticIssues(
419
436
  if (issues.length === 0) {
420
437
  throw new Error("DiagnosticValidationError requires at least one issue");
421
438
  }
422
- const unique = new Map<string, DiagnosticValidationIssue>();
439
+ const unique = new Map<
440
+ string,
441
+ Map<string, Map<string, Map<string, DiagnosticValidationIssue>>>
442
+ >();
423
443
  for (const issue of issues) {
424
444
  const normalized = Object.freeze({ ...issue });
425
- const key = `${issue.domain}\u0000${issue.code}\u0000${issue.path}\u0000${issue.message}`;
426
- if (!unique.has(key)) unique.set(key, normalized);
445
+ const byCode = unique.get(issue.domain) ?? new Map();
446
+ unique.set(issue.domain, byCode);
447
+ const byPath = byCode.get(issue.code) ?? new Map();
448
+ byCode.set(issue.code, byPath);
449
+ const byMessage = byPath.get(issue.path) ?? new Map();
450
+ byPath.set(issue.path, byMessage);
451
+ if (!byMessage.has(issue.message)) byMessage.set(issue.message, normalized);
427
452
  }
428
- return Object.freeze([...unique.values()].sort((left, right) =>
429
- DIAGNOSTIC_DOMAIN_ORDER.indexOf(left.domain) - DIAGNOSTIC_DOMAIN_ORDER.indexOf(right.domain) ||
430
- DIAGNOSTIC_ISSUE_ORDER.indexOf(left.code) - DIAGNOSTIC_ISSUE_ORDER.indexOf(right.code) ||
431
- compareDiagnosticPaths(left.path, right.path) ||
432
- compareCodeUnits(left.code, right.code) ||
433
- compareCodeUnits(left.message, right.message),
434
- ));
453
+ const flattened = [...unique.values()].flatMap((byCode) =>
454
+ [...byCode.values()].flatMap((byPath) =>
455
+ [...byPath.values()].flatMap((byMessage) => [...byMessage.values()]),
456
+ ),
457
+ );
458
+ return Object.freeze(
459
+ flattened.sort(
460
+ (left, right) =>
461
+ DIAGNOSTIC_DOMAIN_ORDER.indexOf(left.domain) -
462
+ DIAGNOSTIC_DOMAIN_ORDER.indexOf(right.domain) ||
463
+ DIAGNOSTIC_ISSUE_ORDER.indexOf(left.code) -
464
+ DIAGNOSTIC_ISSUE_ORDER.indexOf(right.code) ||
465
+ compareDiagnosticPaths(left.path, right.path) ||
466
+ compareCodeUnits(left.code, right.code) ||
467
+ compareCodeUnits(left.message, right.message),
468
+ ),
469
+ );
435
470
  }
436
471
 
437
472
  /** Public, deterministic validation failure for every generalized diagnostic boundary. */
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** This package's runtime version. A test pins it to package.json. */
2
- export const CORE_PACKAGE_VERSION = "0.6.0";
2
+ export const CORE_PACKAGE_VERSION = "0.7.0";