@actuarial-ts/core 0.10.0 → 0.12.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 (91) hide show
  1. package/README.md +9 -7
  2. package/dist/analysisRecipe.d.ts.map +1 -1
  3. package/dist/analysisRecipe.js +20 -1
  4. package/dist/analysisRecipe.js.map +1 -1
  5. package/dist/canonical.d.ts.map +1 -1
  6. package/dist/canonical.js +64 -5
  7. package/dist/canonical.js.map +1 -1
  8. package/dist/customizationContracts.d.ts +16 -1
  9. package/dist/customizationContracts.d.ts.map +1 -1
  10. package/dist/descriptiveStatistics.js +1 -1
  11. package/dist/descriptiveStatistics.js.map +1 -1
  12. package/dist/diagnosticAggregation.d.ts +26 -0
  13. package/dist/diagnosticAggregation.d.ts.map +1 -1
  14. package/dist/diagnosticAggregation.js +125 -11
  15. package/dist/diagnosticAggregation.js.map +1 -1
  16. package/dist/diagnosticAuditText.d.ts +11 -0
  17. package/dist/diagnosticAuditText.d.ts.map +1 -0
  18. package/dist/diagnosticAuditText.js +179 -0
  19. package/dist/diagnosticAuditText.js.map +1 -0
  20. package/dist/diagnosticFormulas.d.ts.map +1 -1
  21. package/dist/diagnosticFormulas.js +71 -12
  22. package/dist/diagnosticFormulas.js.map +1 -1
  23. package/dist/diagnosticFreeze.d.ts +15 -1
  24. package/dist/diagnosticFreeze.d.ts.map +1 -1
  25. package/dist/diagnosticFreeze.js +82 -31
  26. package/dist/diagnosticFreeze.js.map +1 -1
  27. package/dist/diagnosticKeys.d.ts +9 -0
  28. package/dist/diagnosticKeys.d.ts.map +1 -0
  29. package/dist/diagnosticKeys.js +20 -0
  30. package/dist/diagnosticKeys.js.map +1 -0
  31. package/dist/diagnosticOrdering.d.ts.map +1 -1
  32. package/dist/diagnosticOrdering.js +9 -2
  33. package/dist/diagnosticOrdering.js.map +1 -1
  34. package/dist/diagnosticPeriods.d.ts.map +1 -1
  35. package/dist/diagnosticPeriods.js +19 -1
  36. package/dist/diagnosticPeriods.js.map +1 -1
  37. package/dist/diagnosticPreparation.d.ts +19 -0
  38. package/dist/diagnosticPreparation.d.ts.map +1 -1
  39. package/dist/diagnosticPreparation.js +544 -231
  40. package/dist/diagnosticPreparation.js.map +1 -1
  41. package/dist/diagnosticReview.d.ts.map +1 -1
  42. package/dist/diagnosticReview.js +419 -186
  43. package/dist/diagnosticReview.js.map +1 -1
  44. package/dist/diagnosticReviewSources.d.ts +6 -1
  45. package/dist/diagnosticReviewSources.d.ts.map +1 -1
  46. package/dist/diagnosticReviewSources.js +59 -14
  47. package/dist/diagnosticReviewSources.js.map +1 -1
  48. package/dist/diagnosticReviewStore.d.ts.map +1 -1
  49. package/dist/diagnosticReviewStore.js +48 -7
  50. package/dist/diagnosticReviewStore.js.map +1 -1
  51. package/dist/diagnosticRunner.d.ts.map +1 -1
  52. package/dist/diagnosticRunner.js +112 -37
  53. package/dist/diagnosticRunner.js.map +1 -1
  54. package/dist/diagnosticRuntime.d.ts.map +1 -1
  55. package/dist/diagnosticRuntime.js +11 -1
  56. package/dist/diagnosticRuntime.js.map +1 -1
  57. package/dist/diagnosticSourceOrdering.d.ts +27 -0
  58. package/dist/diagnosticSourceOrdering.d.ts.map +1 -1
  59. package/dist/diagnosticSourceOrdering.js +98 -12
  60. package/dist/diagnosticSourceOrdering.js.map +1 -1
  61. package/dist/mack.d.ts.map +1 -1
  62. package/dist/mack.js +35 -0
  63. package/dist/mack.js.map +1 -1
  64. package/dist/types.d.ts +1 -1
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js +1 -0
  67. package/dist/types.js.map +1 -1
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +1 -1
  71. package/src/analysisRecipe.ts +18 -1
  72. package/src/canonical.ts +63 -5
  73. package/src/customizationContracts.ts +12 -1
  74. package/src/descriptiveStatistics.ts +1 -1
  75. package/src/diagnosticAggregation.ts +173 -9
  76. package/src/diagnosticAuditText.ts +187 -0
  77. package/src/diagnosticFormulas.ts +77 -27
  78. package/src/diagnosticFreeze.ts +75 -30
  79. package/src/diagnosticKeys.ts +22 -0
  80. package/src/diagnosticOrdering.ts +9 -8
  81. package/src/diagnosticPeriods.ts +21 -1
  82. package/src/diagnosticPreparation.ts +624 -272
  83. package/src/diagnosticReview.ts +566 -268
  84. package/src/diagnosticReviewSources.ts +69 -18
  85. package/src/diagnosticReviewStore.ts +48 -6
  86. package/src/diagnosticRunner.ts +133 -54
  87. package/src/diagnosticRuntime.ts +11 -1
  88. package/src/diagnosticSourceOrdering.ts +115 -11
  89. package/src/mack.ts +41 -0
  90. package/src/types.ts +1 -0
  91. package/src/version.ts +1 -1
@@ -30,21 +30,125 @@ export function compareDiagnosticSourceLocations(
30
30
  );
31
31
  }
32
32
 
33
+ const SCHEMA_FIELDS = [
34
+ "artifactId",
35
+ "sourceFile",
36
+ "sourceSheet",
37
+ "sourceRow",
38
+ "sourceCell",
39
+ ] as const;
40
+ // Canonical JSON of an object always starts with "{"; this prefix cannot.
41
+ const SCHEMA_KEY_PREFIX = "";
42
+
43
+ /**
44
+ * Exact dedupe key: two source locations receive the same key exactly when
45
+ * their canonical JSON is identical. A location holding only the declared
46
+ * fields with string or finite-number values takes a cheap length-prefixed
47
+ * encoding; every other shape (extra own data, null or non-string values,
48
+ * foreign prototypes) defers to `canonicalJson`, including its error
49
+ * behaviour. Properties are read exactly as `canonicalJson` reads them. The
50
+ * key is an internal lookup token, never emitted or hashed into an identity.
51
+ */
52
+ export function diagnosticSourceLocationKey(
53
+ value: DiagnosticSourceLocation,
54
+ ): string {
55
+ const record = value as unknown as Record<string, unknown>;
56
+ const prototype: unknown = Object.getPrototypeOf(record);
57
+ if (prototype === Object.prototype || prototype === null) {
58
+ let key = SCHEMA_KEY_PREFIX;
59
+ let matched = 0;
60
+ for (let index = 0; index < SCHEMA_FIELDS.length; index++) {
61
+ const field = SCHEMA_FIELDS[index]!;
62
+ if (!Object.hasOwn(record, field)) {
63
+ key += "-";
64
+ continue;
65
+ }
66
+ matched++;
67
+ const item: unknown = record[field];
68
+ if (typeof item === "string") key += `s${item.length}:${item}`;
69
+ else if (
70
+ field === "sourceRow" &&
71
+ typeof item === "number" &&
72
+ Number.isFinite(item)
73
+ )
74
+ key += `n${String(normalizeDiagnosticNumber(item))}`;
75
+ else return canonicalJson(value);
76
+ }
77
+ if (Object.keys(record).length === matched) return key;
78
+ }
79
+ return canonicalJson(value);
80
+ }
81
+
82
+ interface NormalizedSource {
83
+ readonly key: string;
84
+ readonly value: DiagnosticSourceLocation;
85
+ }
86
+ /**
87
+ * Invocation-scoped memo: one normalized snapshot and key per input object.
88
+ * Holders must release it with the invocation; it must never outlive or be
89
+ * shared across public calls, and inputs must be immutable while it is live.
90
+ */
91
+ export type DiagnosticSourceLocationMemo = Map<
92
+ DiagnosticSourceLocation,
93
+ NormalizedSource
94
+ >;
95
+
96
+ function normalizedSource(value: DiagnosticSourceLocation): NormalizedSource {
97
+ const normalized = {
98
+ ...value,
99
+ ...(value.sourceRow === undefined
100
+ ? {}
101
+ : { sourceRow: normalizeDiagnosticNumber(value.sourceRow) }),
102
+ };
103
+ return { key: diagnosticSourceLocationKey(normalized), value: normalized };
104
+ }
105
+
33
106
  /** Exact-deduplicates, snapshots, and contract-sorts source provenance. */
34
107
  export function normalizeDiagnosticSourceLocations(
35
108
  values: readonly (DiagnosticSourceLocation | null | undefined)[],
36
109
  ): DiagnosticSourceLocation[] {
110
+ return normalizeDiagnosticSourceLocationsMemoized(values);
111
+ }
112
+
113
+ /**
114
+ * `normalizeDiagnosticSourceLocations` that reuses one owned snapshot per
115
+ * distinct input object across calls sharing a memo. Results are identical;
116
+ * only the identity of equal snapshots is shared within the invocation.
117
+ */
118
+ export function normalizeDiagnosticSourceLocationsMemoized(
119
+ values: readonly (DiagnosticSourceLocation | null | undefined)[],
120
+ memo?: DiagnosticSourceLocationMemo,
121
+ ): DiagnosticSourceLocation[] {
122
+ if (values.length === 0) return [];
123
+ // One cited source is the overwhelmingly common case: there is nothing to
124
+ // deduplicate or order, so the union map and its iteration are skipped.
125
+ // `normalizedSource` still runs, so an unrepresentable value throws here
126
+ // exactly as it would on the general path.
127
+ if (values.length === 1) {
128
+ const only = values[0];
129
+ if (only === null || only === undefined) return [];
130
+ let entry = memo?.get(only);
131
+ if (entry === undefined) {
132
+ entry = normalizedSource(only);
133
+ memo?.set(only, entry);
134
+ }
135
+ return [entry.value];
136
+ }
37
137
  const unique = new Map<string, DiagnosticSourceLocation>();
38
- for (const value of values)
39
- if (value !== null && value !== undefined) {
40
- const normalized = {
41
- ...value,
42
- ...(value.sourceRow === undefined
43
- ? {}
44
- : { sourceRow: normalizeDiagnosticNumber(value.sourceRow) }),
45
- };
46
- const key = canonicalJson(normalized);
47
- if (!unique.has(key)) unique.set(key, normalized);
138
+ for (let index = 0; index < values.length; index++) {
139
+ const value = values[index];
140
+ if (value === null || value === undefined) continue;
141
+ let entry = memo?.get(value);
142
+ if (entry === undefined) {
143
+ entry = normalizedSource(value);
144
+ memo?.set(value, entry);
48
145
  }
49
- return [...unique.values()].sort(compareDiagnosticSourceLocations);
146
+ if (!unique.has(entry.key)) unique.set(entry.key, entry.value);
147
+ }
148
+ if (unique.size === 1) return [unique.values().next().value!];
149
+ const ordered = [...unique.values()];
150
+ for (let index = 1; index < ordered.length; index++)
151
+ if (compareDiagnosticSourceLocations(ordered[index - 1]!, ordered[index]!) > 0)
152
+ return ordered.sort(compareDiagnosticSourceLocations);
153
+ return ordered;
50
154
  }
package/src/mack.ts CHANGED
@@ -241,6 +241,27 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
241
241
  }
242
242
 
243
243
  // Project the full rectangle with the projection factors.
244
+ // A non-positive projection factor makes Mack's variance terms undefined:
245
+ // every one of them divides by f_k^2. The SELECTED path above coerces such a
246
+ // factor to 1.000 with a warning; the volume-weighted path must not silently
247
+ // do the same, because f_k there IS the data's own estimate and changing it
248
+ // would change the projection. Instead the affected columns are excluded from
249
+ // the variance and the result says so. Without this the column contributes
250
+ // sigma^2/0 = Infinity, which the ultimate (collapsed to 0 by the same zero
251
+ // factor) then turns into NaN - a standard error that serializes as null.
252
+ const degenerateFactorColumns = new Set<number>();
253
+ for (let k = 0; k < K - 1; k++) if (!(fEff[k]! > 0)) degenerateFactorColumns.add(k);
254
+ if (degenerateFactorColumns.size > 0) {
255
+ const intervals = [...degenerateFactorColumns]
256
+ .map((k) => `${tri.ages[k]}-${tri.ages[k + 1]}`)
257
+ .join(", ");
258
+ warnings.push(
259
+ `Development factor for ${intervals} months is not positive; Mack's variance ` +
260
+ `is undefined there, so those columns are excluded from every standard error, ` +
261
+ `which is therefore understated`,
262
+ );
263
+ }
264
+
244
265
  const projected: number[][] = tri.values.map((row) => {
245
266
  const out: number[] = new Array(K).fill(NaN);
246
267
  const last = lastObservedIndex(row);
@@ -260,6 +281,8 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
260
281
  let mse = 0;
261
282
  let droppedColumns = 0;
262
283
  for (let k = last; k < K - 1; k++) {
284
+ // Excluded globally above, with a warning naming the interval.
285
+ if (degenerateFactorColumns.has(k)) continue;
263
286
  const cik = projected[i]![k]!;
264
287
  if (!(cik > 0)) {
265
288
  // Mack's 1/C_ik is undefined for a non-positive cumulative. Skipping
@@ -346,6 +369,7 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
346
369
 
347
370
  let shared = 0;
348
371
  for (let k = floor; k < K - 1; k++) {
372
+ if (degenerateFactorColumns.has(k)) continue;
349
373
  shared += (2 * sigma2[k]!) / fEff[k]! ** 2 / denomSums[k]!;
350
374
  }
351
375
  if (tail !== 1) {
@@ -368,6 +392,23 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
368
392
  );
369
393
  const totalSe = Math.sqrt(totalMse);
370
394
 
395
+ // Safety net, not the primary path: every degeneracy above is handled by
396
+ // excluding a column with a warning, so nothing here should be non-finite.
397
+ // A standard error that escapes as NaN serializes to JSON as `null`, which a
398
+ // reader cannot distinguish from "not computed" - so refuse loudly instead.
399
+ for (const r of rows) {
400
+ if (!Number.isFinite(r.standardError))
401
+ throw new ReservingError(
402
+ "DEGENERATE_TRIANGLE",
403
+ `Standard error for origin ${r.origin} is not finite; the triangle is degenerate for Mack's model`,
404
+ );
405
+ }
406
+ if (!Number.isFinite(totalSe))
407
+ throw new ReservingError(
408
+ "DEGENERATE_TRIANGLE",
409
+ "Total standard error is not finite; the triangle is degenerate for Mack's model",
410
+ );
411
+
371
412
  return {
372
413
  method: "mack",
373
414
  developmentFactors: fEff,
package/src/types.ts CHANGED
@@ -510,6 +510,7 @@ export const RESERVING_ERROR_CODES = [
510
510
  "BAD_LIMIT",
511
511
  "BAD_LOSSES",
512
512
  "BAD_MARGIN",
513
+ "DEGENERATE_TRIANGLE",
513
514
  "BAD_ORIGIN",
514
515
  "BAD_PERCENTILE",
515
516
  "BAD_PREMIUM",
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.10.0";
2
+ export const CORE_PACKAGE_VERSION = "0.12.0";