@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
@@ -0,0 +1,264 @@
1
+ import {
2
+ DiagnosticValidationError,
3
+ type DiagnosticValidationDomain,
4
+ type DiagnosticValidationIssue,
5
+ } from "./types.js";
6
+
7
+ export const MAX_DIAGNOSTIC_JSON_DEPTH = 256;
8
+ export const MAX_DIAGNOSTIC_JSON_NODES = 1_000_000;
9
+
10
+ /** True only for ordinary data records, including deliberately prototype-free records. */
11
+ export function isDiagnosticPlainRecord(
12
+ value: unknown,
13
+ ): value is Record<string, unknown> {
14
+ if (value === null || typeof value !== "object" || Array.isArray(value))
15
+ return false;
16
+ const prototype: unknown = Object.getPrototypeOf(value);
17
+ return prototype === Object.prototype || prototype === null;
18
+ }
19
+
20
+ /** Own-property lookup which remains correct for every legal string key. */
21
+ export function hasDiagnosticOwn(value: object, key: PropertyKey): boolean {
22
+ return Object.prototype.hasOwnProperty.call(value, key);
23
+ }
24
+
25
+ /** Creates a record in which `__proto__`, `constructor`, and `toString` are ordinary keys. */
26
+ export function diagnosticRecord<T>(): Record<string, T> {
27
+ return Object.create(null) as Record<string, T>;
28
+ }
29
+
30
+ export function normalizeDiagnosticNumber(value: number): number {
31
+ return Object.is(value, -0) ? 0 : value;
32
+ }
33
+
34
+ /** Rejects U+0000 and unpaired UTF-16 surrogate code units. */
35
+ export function isWellFormedDiagnosticString(value: string): boolean {
36
+ for (let index = 0; index < value.length; index++) {
37
+ const code = value.charCodeAt(index);
38
+ if (code === 0) return false;
39
+ if (code >= 0xd800 && code <= 0xdbff) {
40
+ const next = value.charCodeAt(index + 1);
41
+ if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
42
+ index++;
43
+ } else if (code >= 0xdc00 && code <= 0xdfff) return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ export function isDiagnosticToken(value: unknown): value is string {
49
+ return (
50
+ typeof value === "string" &&
51
+ isWellFormedDiagnosticString(value) &&
52
+ value.length > 0 &&
53
+ !/^[\u0009-\u000d\u0020]|[\u0009-\u000d\u0020]$/.test(value)
54
+ );
55
+ }
56
+
57
+ /** Strict Gregorian date validation for public SDK date boundaries. */
58
+ export function isRealIsoDate(value: unknown): value is string {
59
+ if (typeof value !== "string" || !isWellFormedDiagnosticString(value))
60
+ return false;
61
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
62
+ if (!match) return false;
63
+ const year = Number(match[1]);
64
+ const month = Number(match[2]);
65
+ const day = Number(match[3]);
66
+ if (year < 1 || year > 9999 || month < 1 || month > 12 || day < 1)
67
+ return false;
68
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
69
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
70
+ return day <= days[month - 1]!;
71
+ }
72
+
73
+ interface JsonFrame {
74
+ readonly value: unknown;
75
+ readonly path: string;
76
+ readonly depth: number;
77
+ readonly exiting: boolean;
78
+ }
79
+
80
+ /**
81
+ * Iterative JSON boundary preflight. It never invokes getters and reports
82
+ * malformed values before recursive normalization or hashing can run.
83
+ */
84
+ export function diagnosticJsonPreflight(
85
+ value: unknown,
86
+ domain: DiagnosticValidationDomain,
87
+ options: { readonly maxDepth?: number; readonly maxNodes?: number } = {},
88
+ ): readonly DiagnosticValidationIssue[] {
89
+ const maxDepth = options.maxDepth ?? MAX_DIAGNOSTIC_JSON_DEPTH;
90
+ const maxNodes = options.maxNodes ?? MAX_DIAGNOSTIC_JSON_NODES;
91
+ const issues: DiagnosticValidationIssue[] = [];
92
+ const active = new WeakSet<object>();
93
+ const stack: JsonFrame[] = [{ value, path: "$", depth: 1, exiting: false }];
94
+ let nodes = 0;
95
+ while (stack.length > 0) {
96
+ const frame = stack.pop()!;
97
+ if (frame.exiting) {
98
+ active.delete(frame.value as object);
99
+ continue;
100
+ }
101
+ nodes++;
102
+ if (nodes > maxNodes) {
103
+ issues.push({
104
+ domain,
105
+ code: "expression-limit",
106
+ path: "$",
107
+ message: `JSON node count exceeds ${maxNodes}`,
108
+ });
109
+ break;
110
+ }
111
+ if (frame.depth > maxDepth) {
112
+ issues.push({
113
+ domain,
114
+ code: "expression-limit",
115
+ path: frame.path,
116
+ message: `JSON depth exceeds ${maxDepth}`,
117
+ });
118
+ continue;
119
+ }
120
+ const item = frame.value;
121
+ if (item === null || typeof item === "boolean") continue;
122
+ if (typeof item === "string") {
123
+ if (!isWellFormedDiagnosticString(item))
124
+ issues.push({
125
+ domain,
126
+ code: "invalid-json-value",
127
+ path: frame.path,
128
+ message: "String is not well-formed UTF-16 or contains U+0000",
129
+ });
130
+ continue;
131
+ }
132
+ if (typeof item === "number") {
133
+ if (!Number.isFinite(item))
134
+ issues.push({
135
+ domain,
136
+ code: "invalid-json-value",
137
+ path: frame.path,
138
+ message: "JSON numeric value must be finite",
139
+ });
140
+ continue;
141
+ }
142
+ if (typeof item !== "object") {
143
+ issues.push({
144
+ domain,
145
+ code: "invalid-json-value",
146
+ path: frame.path,
147
+ message: "Value is not plain JSON data",
148
+ });
149
+ continue;
150
+ }
151
+ const isArray = Array.isArray(item);
152
+ if (
153
+ isArray
154
+ ? Object.getPrototypeOf(item) !== Array.prototype
155
+ : !isDiagnosticPlainRecord(item)
156
+ ) {
157
+ issues.push({
158
+ domain,
159
+ code: "invalid-json-value",
160
+ path: frame.path,
161
+ message: "Value must use a plain object or array prototype",
162
+ });
163
+ continue;
164
+ }
165
+ if (active.has(item)) {
166
+ issues.push({
167
+ domain,
168
+ code: "cycle",
169
+ path: frame.path,
170
+ message: "JSON value contains a cycle",
171
+ });
172
+ continue;
173
+ }
174
+ active.add(item);
175
+ stack.push({ ...frame, exiting: true });
176
+ if (isArray) {
177
+ for (const key of Reflect.ownKeys(item)) {
178
+ if (key === "length") continue;
179
+ const index = typeof key === "string" ? Number(key) : Number.NaN;
180
+ if (
181
+ Number.isInteger(index) && index >= 0 && index < item.length &&
182
+ String(index) === key
183
+ ) continue;
184
+ const path = typeof key === "symbol"
185
+ ? frame.path
186
+ : /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
187
+ ? `${frame.path}.${key}`
188
+ : `${frame.path}[${JSON.stringify(key)}]`;
189
+ issues.push({
190
+ domain,
191
+ code: "invalid-json-value",
192
+ path,
193
+ message: "JSON arrays may contain only indexed data properties",
194
+ });
195
+ }
196
+ for (let index = item.length - 1; index >= 0; index--) {
197
+ const path = `${frame.path}[${index}]`;
198
+ const descriptor = Object.getOwnPropertyDescriptor(item, String(index));
199
+ if (!descriptor || !("value" in descriptor))
200
+ issues.push({
201
+ domain,
202
+ code: "invalid-json-value",
203
+ path,
204
+ message: "JSON arrays may contain only indexed data properties",
205
+ });
206
+ else
207
+ stack.push({
208
+ value: descriptor.value,
209
+ path,
210
+ depth: frame.depth + 1,
211
+ exiting: false,
212
+ });
213
+ }
214
+ continue;
215
+ }
216
+ for (const key of Object.keys(item).sort().reverse()) {
217
+ const path = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
218
+ ? `${frame.path}.${key}`
219
+ : `${frame.path}[${JSON.stringify(key)}]`;
220
+ if (!isWellFormedDiagnosticString(key))
221
+ issues.push({
222
+ domain,
223
+ code: "invalid-json-value",
224
+ path,
225
+ message: "Object key is not well-formed UTF-16 or contains U+0000",
226
+ });
227
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
228
+ if (!descriptor || !("value" in descriptor)) {
229
+ issues.push({
230
+ domain,
231
+ code: "invalid-json-value",
232
+ path,
233
+ message: "JSON objects may contain only data properties",
234
+ });
235
+ } else {
236
+ stack.push({
237
+ value: descriptor.value,
238
+ path,
239
+ depth: frame.depth + 1,
240
+ exiting: false,
241
+ });
242
+ }
243
+ }
244
+ }
245
+ return issues;
246
+ }
247
+
248
+ /** Snapshot JSON-shaped data into owned, deeply frozen arrays/null-prototype records. */
249
+ export function snapshotDiagnosticJson<T>(value: T): T {
250
+ const issues = diagnosticJsonPreflight(value, "input");
251
+ if (issues.length > 0) throw new DiagnosticValidationError(issues);
252
+ const clone = (item: unknown): unknown => {
253
+ if (item === null || typeof item !== "object") {
254
+ return typeof item === "number" ? normalizeDiagnosticNumber(item) : item;
255
+ }
256
+ if (Array.isArray(item)) return Object.freeze(item.map(clone));
257
+ const result = diagnosticRecord<unknown>();
258
+ for (const key of Object.keys(item).sort()) {
259
+ result[key] = clone((item as Record<string, unknown>)[key]);
260
+ }
261
+ return Object.freeze(result);
262
+ };
263
+ return clone(value) as T;
264
+ }
@@ -0,0 +1,50 @@
1
+ import { canonicalJson } from "./canonical.js";
2
+ import type { DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
3
+ import { normalizeDiagnosticNumber } from "./diagnosticRuntime.js";
4
+
5
+ function codeUnit(left: string, right: string): number {
6
+ return left < right ? -1 : left > right ? 1 : 0;
7
+ }
8
+
9
+ function optional<T>(
10
+ left: T | undefined,
11
+ right: T | undefined,
12
+ compare: (a: T, b: T) => number,
13
+ ): number {
14
+ if (left === undefined) return right === undefined ? 0 : -1;
15
+ if (right === undefined) return 1;
16
+ return compare(left, right);
17
+ }
18
+
19
+ /** Contract comparator for source provenance; source rows compare numerically. */
20
+ export function compareDiagnosticSourceLocations(
21
+ left: DiagnosticSourceLocation,
22
+ right: DiagnosticSourceLocation,
23
+ ): number {
24
+ return (
25
+ codeUnit(left.artifactId, right.artifactId) ||
26
+ optional(left.sourceFile, right.sourceFile, codeUnit) ||
27
+ optional(left.sourceSheet, right.sourceSheet, codeUnit) ||
28
+ optional(left.sourceRow, right.sourceRow, (a, b) => a - b) ||
29
+ optional(left.sourceCell, right.sourceCell, codeUnit)
30
+ );
31
+ }
32
+
33
+ /** Exact-deduplicates, snapshots, and contract-sorts source provenance. */
34
+ export function normalizeDiagnosticSourceLocations(
35
+ values: readonly (DiagnosticSourceLocation | null | undefined)[],
36
+ ): DiagnosticSourceLocation[] {
37
+ 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);
48
+ }
49
+ return [...unique.values()].sort(compareDiagnosticSourceLocations);
50
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,19 @@ export * from "./types.js";
2
2
  export * from "./util.js";
3
3
  export * from "./canonical.js";
4
4
  export * from "./version.js";
5
+ export {
6
+ MAX_DIAGNOSTIC_JSON_DEPTH,
7
+ MAX_DIAGNOSTIC_JSON_NODES,
8
+ diagnosticJsonPreflight,
9
+ diagnosticRecord,
10
+ hasDiagnosticOwn,
11
+ isDiagnosticPlainRecord,
12
+ isDiagnosticToken,
13
+ isRealIsoDate,
14
+ isWellFormedDiagnosticString,
15
+ normalizeDiagnosticNumber,
16
+ snapshotDiagnosticJson,
17
+ } from "./diagnosticRuntime.js";
5
18
  export {
6
19
  MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES,
7
20
  MAX_DIAGNOSTIC_EXPRESSION_DEPTH,
@@ -63,7 +76,10 @@ export type {
63
76
  NormalizedDiagnosticReviewRuleIdentity,
64
77
  NormalizedDiagnosticSourceLocationIdentity,
65
78
  NormalizedDiagnosticToleranceIdentity,
79
+ NormalizedDiagnosticsFilterIdentity,
80
+ DiagnosticIdentityProjection,
66
81
  } from "./diagnosticIdentity.js";
82
+ export { projectDiagnosticIdentity, normalizeDiagnosticsFilterIdentity } from "./diagnosticIdentity.js";
67
83
  export {
68
84
  CASUALTY_FORMULA_TEMPLATES,
69
85
  applyDiagnosticPresentation,
@@ -75,9 +91,7 @@ export type {
75
91
  DiagnosticQuantitySemantics,
76
92
  FinalizedDiagnosticMeasure,
77
93
  } from "./diagnosticFormulas.js";
78
- export {
79
- classifyDiagnosticComparison,
80
- } from "./diagnosticRules.js";
94
+ export { classifyDiagnosticComparison } from "./diagnosticRules.js";
81
95
  export { deriveDiagnosticClaimMeasures } from "./diagnosticDerivations.js";
82
96
  export type {
83
97
  DiagnosticMeasureValues,
@@ -89,6 +103,11 @@ export {
89
103
  normalizeDiagnosticPeriod,
90
104
  } from "./diagnosticPeriods.js";
91
105
  export type { DiagnosticNormalizedPeriod } from "./diagnosticPeriods.js";
106
+ export {
107
+ compareDiagnosticSourceLocations,
108
+ normalizeDiagnosticSourceLocations,
109
+ } from "./diagnosticSourceOrdering.js";
110
+ export { compareDiagnosticIdentityValues } from "./diagnosticOrdering.js";
92
111
  export {
93
112
  auditedDiagnosticContribution,
94
113
  finalizeDiagnosticContributions,
@@ -112,12 +131,16 @@ export type {
112
131
  DiagnosticClaimObservation,
113
132
  DiagnosticCompletePeriodCutoff,
114
133
  DiagnosticExpectedCell,
134
+ DiagnosticExpectedCellAuditSnapshot,
135
+ DiagnosticExposureInputAuditSnapshot,
115
136
  DiagnosticInputAuditRecord,
116
137
  DiagnosticInputDisposition,
117
138
  DiagnosticLossInput,
139
+ DiagnosticLossInputAuditSnapshot,
118
140
  DiagnosticLossRecordBase,
119
141
  DiagnosticLossSnapshot,
120
142
  NormalizedDiagnosticPreparationIdentity,
143
+ NormalizedDiagnosticExpectedCellIdentity,
121
144
  PrepareDiagnosticDataInput,
122
145
  PreparedDiagnosticData,
123
146
  PreparedDiagnosticSourceCell,
@@ -131,9 +154,14 @@ export {
131
154
  } from "./diagnosticRunner.js";
132
155
  export { evaluateDiagnosticReviewRules } from "./diagnosticReview.js";
133
156
  export type {
157
+ DiagnosticCellReviewScope,
158
+ DiagnosticControlTotalReviewScope,
134
159
  DiagnosticReviewCoordinate,
135
160
  DiagnosticReviewEvaluationScope,
161
+ DiagnosticReviewExpressionOverflow,
136
162
  DiagnosticReviewRuleEvaluation,
163
+ DiagnosticReviewRuleEvaluationBase,
164
+ DiagnosticValuationPairReviewScope,
137
165
  } from "./diagnosticReview.js";
138
166
  export type {
139
167
  CommonMaturityResult,
package/src/mack.ts CHANGED
@@ -11,6 +11,10 @@ export interface MackOptions {
11
11
  selected?: (number | null)[];
12
12
  /** Multiplicative tail factor beyond the last observed age (1 = none). */
13
13
  tailFactor?: number;
14
+ /** Standard error of the selected tail-factor estimate (Mack 1999). */
15
+ tailStandardError?: number;
16
+ /** Tail random-error scale sigma, applied as sigma / sqrt(C) (Mack 1999). */
17
+ tailSigma?: number;
14
18
  }
15
19
 
16
20
  /**
@@ -46,7 +50,10 @@ export function mackEstimators(tri: Triangle): {
46
50
  const n = tri.origins.length;
47
51
  const K = tri.ages.length;
48
52
  if (K < 2) {
49
- throw new ReservingError("TOO_SMALL", "Mack requires at least two development ages");
53
+ throw new ReservingError(
54
+ "TOO_SMALL",
55
+ "Mack requires at least two development ages",
56
+ );
50
57
  }
51
58
  const f: number[] = [];
52
59
  const denomSums: number[] = [];
@@ -169,7 +176,45 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
169
176
  }
170
177
  const tail = options.tailFactor ?? 1;
171
178
  if (!isNum(tail) || tail <= 0) {
172
- throw new ReservingError("BAD_TAIL", "Tail factor must be a positive number");
179
+ throw new ReservingError(
180
+ "BAD_TAIL",
181
+ "Tail factor must be a positive number",
182
+ );
183
+ }
184
+ const hasPublishedTailInputs =
185
+ options.tailStandardError !== undefined || options.tailSigma !== undefined;
186
+ if (
187
+ hasPublishedTailInputs &&
188
+ (options.tailStandardError === undefined || options.tailSigma === undefined)
189
+ ) {
190
+ throw new ReservingError(
191
+ "BAD_TAIL",
192
+ "tailStandardError and tailSigma must be supplied together",
193
+ );
194
+ }
195
+ if (
196
+ options.tailStandardError !== undefined &&
197
+ (!isNum(options.tailStandardError) || options.tailStandardError < 0)
198
+ ) {
199
+ throw new ReservingError(
200
+ "BAD_TAIL",
201
+ "Tail standard error must be a finite nonnegative number",
202
+ );
203
+ }
204
+ if (
205
+ options.tailSigma !== undefined &&
206
+ (!isNum(options.tailSigma) || options.tailSigma < 0)
207
+ ) {
208
+ throw new ReservingError(
209
+ "BAD_TAIL",
210
+ "Tail sigma must be a finite nonnegative number",
211
+ );
212
+ }
213
+ if (tail === 1 && hasPublishedTailInputs) {
214
+ throw new ReservingError(
215
+ "BAD_TAIL",
216
+ "Tail uncertainty inputs require a tailFactor other than 1",
217
+ );
173
218
  }
174
219
 
175
220
  // sigma^2_k estimates: data-estimated where possible; Mack's extrapolation
@@ -189,9 +234,10 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
189
234
  sigma2Tail = s2b;
190
235
  }
191
236
  denomTail = denomSums[K - 2]!;
192
- warnings.push(
193
- "The tail step's standard-error contribution extrapolates sigma^2 beyond the observed columns and reuses the final column's volume; treat it as approximate (Mack 1999)",
194
- );
237
+ if (!hasPublishedTailInputs)
238
+ warnings.push(
239
+ "The tail step's standard-error contribution extrapolates sigma^2 beyond the observed columns and reuses the final column's volume; supply tailStandardError and tailSigma for the explicit Mack (1999) treatment",
240
+ );
195
241
  }
196
242
 
197
243
  // Project the full rectangle with the projection factors.
@@ -233,7 +279,12 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
233
279
  );
234
280
  }
235
281
  if (tail !== 1 && projected[i]![K - 1]! > 0) {
236
- mse += (sigma2Tail / tail ** 2) * (1 / projected[i]![K - 1]! + 1 / denomTail);
282
+ const finalCumulative = projected[i]![K - 1]!;
283
+ mse += hasPublishedTailInputs
284
+ ? (options.tailSigma! ** 2 / finalCumulative +
285
+ options.tailStandardError! ** 2) /
286
+ tail ** 2
287
+ : (sigma2Tail / tail ** 2) * (1 / finalCumulative + 1 / denomTail);
237
288
  }
238
289
  mse *= ultimate ** 2;
239
290
  mseByRow[i] = mse;
@@ -283,7 +334,10 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
283
334
  const uj = ultimateOf(j);
284
335
  if (!(uj > 0)) continue;
285
336
  const floor = Math.max(maturity[i]!, maturity[j]!);
286
- partnerUltimatesByFloor.set(floor, (partnerUltimatesByFloor.get(floor) ?? 0) + uj);
337
+ partnerUltimatesByFloor.set(
338
+ floor,
339
+ (partnerUltimatesByFloor.get(floor) ?? 0) + uj,
340
+ );
287
341
  }
288
342
 
289
343
  for (const [floor, partnerUltimates] of partnerUltimatesByFloor) {
@@ -295,7 +349,9 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
295
349
  shared += (2 * sigma2[k]!) / fEff[k]! ** 2 / denomSums[k]!;
296
350
  }
297
351
  if (tail !== 1) {
298
- shared += (2 * sigma2Tail) / tail ** 2 / denomTail;
352
+ shared += hasPublishedTailInputs
353
+ ? (2 * options.tailStandardError! ** 2) / tail ** 2
354
+ : (2 * sigma2Tail) / tail ** 2 / denomTail;
299
355
  }
300
356
 
301
357
  totalMse += ui * partnerUltimates * shared;
@@ -317,7 +373,14 @@ export function runMack(tri: Triangle, options: MackOptions = {}): MackResult {
317
373
  developmentFactors: fEff,
318
374
  sigmaSquared: sigma2,
319
375
  tailFactor: tail,
320
- sigmaSquaredTail: tail !== 1 ? sigma2Tail : undefined,
376
+ sigmaSquaredTail:
377
+ tail !== 1 && !hasPublishedTailInputs ? sigma2Tail : undefined,
378
+ tailStandardError:
379
+ tail !== 1 && hasPublishedTailInputs
380
+ ? options.tailStandardError
381
+ : undefined,
382
+ tailSigma:
383
+ tail !== 1 && hasPublishedTailInputs ? options.tailSigma : undefined,
321
384
  rows,
322
385
  totals: {
323
386
  ...totals,