@actuarial-ts/data 0.2.0 → 0.3.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.
package/dist/csv.d.ts CHANGED
@@ -10,6 +10,18 @@
10
10
  * validation. Ragged rows are preserved as-is — validation is the caller's
11
11
  * job (see lossRun.ts).
12
12
  */
13
- /** Parses CSV text into a grid of string fields (rows x columns). */
14
- export declare function parseCsv(text: string): string[][];
13
+ export interface CsvParseResult {
14
+ /** The parsed grid (rows x columns). Ragged rows preserved as-is. */
15
+ rows: string[][];
16
+ /**
17
+ * Structural problems the parser recovered from. Content is still parsed
18
+ * leniently — a warning here means the OUTPUT may not mean what the file's
19
+ * author intended, which the caller must surface rather than swallow: an
20
+ * unterminated quote consumes the remainder of the input into one field,
21
+ * and five good rows silently becoming one is how claims vanish.
22
+ */
23
+ warnings: string[];
24
+ }
25
+ /** Parses CSV text into a grid of string fields plus structural warnings. */
26
+ export declare function parseCsv(text: string): CsvParseResult;
15
27
  //# sourceMappingURL=csv.d.ts.map
package/dist/csv.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qEAAqE;AACrE,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAwEjD"}
1
+ {"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC;IACjB;;;;;;OAMG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAwFrD"}
package/dist/csv.js CHANGED
@@ -10,16 +10,19 @@
10
10
  * validation. Ragged rows are preserved as-is — validation is the caller's
11
11
  * job (see lossRun.ts).
12
12
  */
13
- /** Parses CSV text into a grid of string fields (rows x columns). */
13
+ /** Parses CSV text into a grid of string fields plus structural warnings. */
14
14
  export function parseCsv(text) {
15
15
  let s = text;
16
16
  if (s.length > 0 && s.charCodeAt(0) === 0xfeff)
17
17
  s = s.slice(1);
18
18
  const rows = [];
19
+ const warnings = [];
19
20
  let row = [];
20
21
  let field = "";
21
22
  let inQuotes = false;
22
23
  let fieldWasQuoted = false;
24
+ let line = 1;
25
+ let quoteOpenedAtLine = 0;
23
26
  const endField = () => {
24
27
  row.push(field);
25
28
  field = "";
@@ -48,12 +51,15 @@ export function parseCsv(text) {
48
51
  i++;
49
52
  continue;
50
53
  }
54
+ if (ch === "\n")
55
+ line++;
51
56
  field += ch;
52
57
  i++;
53
58
  continue;
54
59
  }
55
60
  if (ch === '"' && field === "" && !fieldWasQuoted) {
56
61
  inQuotes = true;
62
+ quoteOpenedAtLine = line;
57
63
  fieldWasQuoted = true;
58
64
  i++;
59
65
  continue;
@@ -67,11 +73,13 @@ export function parseCsv(text) {
67
73
  if (s[i + 1] === "\n")
68
74
  i++;
69
75
  endRow();
76
+ line++;
70
77
  i++;
71
78
  continue;
72
79
  }
73
80
  if (ch === "\n") {
74
81
  endRow();
82
+ line++;
75
83
  i++;
76
84
  continue;
77
85
  }
@@ -79,8 +87,15 @@ export function parseCsv(text) {
79
87
  i++;
80
88
  }
81
89
  // Flush a final row that has no trailing newline. An unterminated quoted
82
- // field flushes with whatever content it accumulated (lenient by design).
90
+ // field still flushes with whatever it accumulated — content stays lenient
91
+ // but the structural problem is REPORTED: everything after the stray quote
92
+ // was consumed into one field, and the caller must not present that output
93
+ // as a faithful read of the file.
94
+ if (inQuotes) {
95
+ warnings.push(`unterminated quoted field starting at line ${quoteOpenedAtLine}: the remainder of the ` +
96
+ "input was consumed into a single field; check the file for a stray or unescaped quote");
97
+ }
83
98
  endRow();
84
- return rows;
99
+ return { rows, warnings };
85
100
  }
86
101
  //# sourceMappingURL=csv.js.map
package/dist/csv.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"csv.js","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,qEAAqE;AACrE,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,IAAI,CAAC,GAAG,IAAI,CAAC;IACb,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE/D,MAAM,IAAI,GAAe,EAAE,CAAC;IAC5B,IAAI,GAAG,GAAa,EAAE,CAAC;IACvB,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,MAAM,QAAQ,GAAG,GAAS,EAAE;QAC1B,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,KAAK,GAAG,EAAE,CAAC;QACX,cAAc,GAAG,KAAK,CAAC;IACzB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,GAAS,EAAE;QACxB,qEAAqE;QACrE,oCAAoC;QACpC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,IAAI,CAAC,cAAc;YAAE,OAAO;QAChE,QAAQ,EAAE,CAAC;QACX,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,GAAG,GAAG,EAAE,CAAC;IACX,CAAC,CAAC;IAEF,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QACjB,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACf,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBACrB,KAAK,IAAI,GAAG,CAAC;oBACb,CAAC,IAAI,CAAC,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,QAAQ,GAAG,KAAK,CAAC;gBACjB,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;YACD,KAAK,IAAI,EAAE,CAAC;YACZ,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,IAAI,KAAK,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC;YAClD,QAAQ,GAAG,IAAI,CAAC;YAChB,cAAc,GAAG,IAAI,CAAC;YACtB,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,EAAE,CAAC;YACX,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI;gBAAE,CAAC,EAAE,CAAC;YAC3B,MAAM,EAAE,CAAC;YACT,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,MAAM,EAAE,CAAC;YACT,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,KAAK,IAAI,EAAE,CAAC;QACZ,CAAC,EAAE,CAAC;IACN,CAAC;IACD,yEAAyE;IACzE,0EAA0E;IAC1E,MAAM,EAAE,CAAC;IACT,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"csv.js","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH,6EAA6E;AAC7E,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,IAAI,CAAC,GAAG,IAAI,CAAC;IACb,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAE/D,MAAM,IAAI,GAAe,EAAE,CAAC;IAC5B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,GAAG,GAAa,EAAE,CAAC;IACvB,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,cAAc,GAAG,KAAK,CAAC;IAC3B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAE1B,MAAM,QAAQ,GAAG,GAAS,EAAE;QAC1B,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChB,KAAK,GAAG,EAAE,CAAC;QACX,cAAc,GAAG,KAAK,CAAC;IACzB,CAAC,CAAC;IAEF,MAAM,MAAM,GAAG,GAAS,EAAE;QACxB,qEAAqE;QACrE,oCAAoC;QACpC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,IAAI,CAAC,cAAc;YAAE,OAAO;QAChE,QAAQ,EAAE,CAAC;QACX,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,GAAG,GAAG,EAAE,CAAC;IACX,CAAC,CAAC;IAEF,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;QACjB,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBACf,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBACrB,KAAK,IAAI,GAAG,CAAC;oBACb,CAAC,IAAI,CAAC,CAAC;oBACP,SAAS;gBACX,CAAC;gBACD,QAAQ,GAAG,KAAK,CAAC;gBACjB,CAAC,EAAE,CAAC;gBACJ,SAAS;YACX,CAAC;YACD,IAAI,EAAE,KAAK,IAAI;gBAAE,IAAI,EAAE,CAAC;YACxB,KAAK,IAAI,EAAE,CAAC;YACZ,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,IAAI,KAAK,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC;YAClD,QAAQ,GAAG,IAAI,CAAC;YAChB,iBAAiB,GAAG,IAAI,CAAC;YACzB,cAAc,GAAG,IAAI,CAAC;YACtB,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;YACf,QAAQ,EAAE,CAAC;YACX,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI;gBAAE,CAAC,EAAE,CAAC;YAC3B,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,CAAC;YACP,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,MAAM,EAAE,CAAC;YACT,IAAI,EAAE,CAAC;YACP,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,KAAK,IAAI,EAAE,CAAC;QACZ,CAAC,EAAE,CAAC;IACN,CAAC;IACD,yEAAyE;IACzE,6EAA6E;IAC7E,2EAA2E;IAC3E,2EAA2E;IAC3E,kCAAkC;IAClC,IAAI,QAAQ,EAAE,CAAC;QACb,QAAQ,CAAC,IAAI,CACX,8CAA8C,iBAAiB,yBAAyB;YACtF,uFAAuF,CAC1F,CAAC;IACJ,CAAC;IACD,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAC5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"lossRun.d.ts","sourceRoot":"","sources":["../src/lossRun.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAmCxD,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAkBD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAkFhE"}
1
+ {"version":3,"file":"lossRun.d.ts","sourceRoot":"","sources":["../src/lossRun.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAmCxD,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAwBD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAqGhE"}
package/dist/lossRun.js CHANGED
@@ -42,12 +42,18 @@ function isValidIsoDate(value) {
42
42
  const d = Number(match[3]);
43
43
  if (m < 1 || m > 12 || d < 1)
44
44
  return false;
45
- const daysInMonth = new Date(Date.UTC(y, m, 0)).getUTCDate();
45
+ // Arithmetic rule, mirroring compliance/src/metadata.ts (this package cannot
46
+ // import compliance — the dependency points the other way). The previous
47
+ // Date.UTC form mapped years 0-99 into the 1900s; the mapping happens to
48
+ // preserve leapness for every year except 0000, so this is a correctness-of-
49
+ // form consolidation rather than a live-bug fix.
50
+ const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
51
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1];
46
52
  return d <= daysInMonth;
47
53
  }
48
54
  /** Parses a loss-run CSV into ClaimSnapshots plus per-row validation errors. */
49
55
  export function parseLossRunCsv(text) {
50
- const grid = parseCsv(text);
56
+ const { rows: grid, warnings: csvWarnings } = parseCsv(text);
51
57
  const headers = (grid[0] ?? []).map(normalizeHeader);
52
58
  const missing = REQUIRED_COLUMNS.filter((c) => !headers.includes(c));
53
59
  if (missing.length > 0) {
@@ -60,6 +66,12 @@ export function parseLossRunCsv(text) {
60
66
  });
61
67
  const claims = [];
62
68
  const errors = [];
69
+ for (const warning of csvWarnings) {
70
+ // Structural CSV problems surface through the same channel as row errors:
71
+ // partial loading is intended behavior here, silent partial loading is not.
72
+ const lineMatch = warning.match(/line (\d+)/);
73
+ errors.push({ row: lineMatch ? Number(lineMatch[1]) : 1, message: `CSV structure: ${warning}` });
74
+ }
63
75
  for (let r = 1; r < grid.length; r++) {
64
76
  const rowNumber = r + 1; // 1-based including the header row
65
77
  const cells = grid[r];
@@ -81,9 +93,20 @@ export function parseLossRunCsv(text) {
81
93
  const evaluationDate = date("evaluation_date");
82
94
  const amount = (name) => {
83
95
  const value = cell(name);
84
- const n = value === "" ? NaN : Number(value);
96
+ // Currency is decimal digits with an optional sign and fraction —
97
+ // Number() also accepts hex (0x2710 -> 10000), binary, octal and
98
+ // scientific notation, which silently changes magnitudes instead of
99
+ // erroring. Formatted amounts are rejected with a pointed message
100
+ // rather than guessed at: "1,234" could be one thousand or 1.234
101
+ // depending on locale, and a loss run is no place to guess.
102
+ if (/[,()]|\s/.test(value.trim()) && value.trim() !== "") {
103
+ rowErrors.push(`${name} must be an unformatted decimal (got "${value}"); remove thousands separators, ` +
104
+ "parentheses and spaces");
105
+ return null;
106
+ }
107
+ const n = /^-?\d+(\.\d+)?$/.test(value.trim()) ? Number(value.trim()) : NaN;
85
108
  if (!Number.isFinite(n)) {
86
- rowErrors.push(`${name} must be a finite number (got "${value}")`);
109
+ rowErrors.push(`${name} must be a finite decimal number (got "${value}")`);
87
110
  return null;
88
111
  }
89
112
  return n;
@@ -1 +1 @@
1
- {"version":3,"file":"lossRun.js","sourceRoot":"","sources":["../src/lossRun.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"lossRun.js","sourceRoot":"","sources":["../src/lossRun.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,eAAe,CAAC;AAE5E,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAkDD,8EAA8E;AAC9E,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EAAE,EACvB,IAAI,GAAE,sBAA2B,GAChC,gBAAgB,CAuFlB;AAqDD,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,gBAAgB,CAuFpF"}
1
+ {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,eAAe,CAAC;AAE5E,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CAChF;AAED,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAmDD,8EAA8E;AAC9E,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EAAE,EACvB,IAAI,GAAE,sBAA2B,GAChC,gBAAgB,CAsGlB;AA+ED,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,gBAAgB,CA+FpF"}
package/dist/review.js CHANGED
@@ -29,6 +29,7 @@ function summarize(checks) {
29
29
  return { checks, summary };
30
30
  }
31
31
  const CLAIM_DESCRIPTIONS = {
32
+ "non-finite-value": "Every money field is a finite number (no NaN or Infinity)",
32
33
  "negative-paid": "Cumulative paid amounts are non-negative",
33
34
  "negative-case": "Case reserves are non-negative (negative case is legitimate but rare)",
34
35
  "paid-decreasing": "Cumulative paid never decreases across a claim's snapshots ordered by evaluation date",
@@ -44,8 +45,14 @@ export function reviewClaimData(claims, opts = {}) {
44
45
  const dateOrder = [];
45
46
  const futureDated = [];
46
47
  const closedWithCase = [];
47
- claims.forEach((c, idx) => {
48
- const where = `claim ${c.claimId} row ${idx + 1} (eval ${c.evaluationDate})`;
48
+ claims.forEach((c) => {
49
+ // Identified by claimId + evaluation date, which the DATA carries. The old
50
+ // label fabricated "row N" from this array's index — but this function
51
+ // receives parsed claims, not the file, and parseLossRunCsv numbers real
52
+ // CSV rows (header-inclusive) in its own errors. Two different "row"
53
+ // meanings pointed auditors at the wrong line; an identifier we cannot
54
+ // compute is one we must not print.
55
+ const where = `claim ${c.claimId} (eval ${c.evaluationDate})`;
49
56
  if (c.paidToDate < 0) {
50
57
  negativePaid.push(`${where}: paid_to_date ${c.paidToDate}`);
51
58
  }
@@ -105,10 +112,20 @@ export function reviewClaimData(claims, opts = {}) {
105
112
  }
106
113
  }
107
114
  }
115
+ const nonFinite = [];
116
+ for (const c of claims) {
117
+ const where = `claim ${c.claimId}`;
118
+ if (!Number.isFinite(c.paidToDate))
119
+ nonFinite.push(`${where}: paid_to_date ${String(c.paidToDate)}`);
120
+ if (!Number.isFinite(c.caseReserve))
121
+ nonFinite.push(`${where}: case_reserve ${String(c.caseReserve)}`);
122
+ }
108
123
  const futureCheck = opts.asOfDate === undefined
109
124
  ? notEvaluated("future-dated", CLAIM_DESCRIPTIONS["future-dated"], "no asOfDate provided")
110
125
  : makeCheck("future-dated", CLAIM_DESCRIPTIONS["future-dated"], "fail", futureDated);
111
126
  return summarize([
127
+ // First: if the numbers are not numbers, the other verdicts are noise.
128
+ makeCheck("non-finite-value", CLAIM_DESCRIPTIONS["non-finite-value"], "fail", nonFinite),
112
129
  makeCheck("negative-paid", CLAIM_DESCRIPTIONS["negative-paid"], "fail", negativePaid),
113
130
  makeCheck("negative-case", CLAIM_DESCRIPTIONS["negative-case"], "warning", negativeCase),
114
131
  makeCheck("paid-decreasing", CLAIM_DESCRIPTIONS["paid-decreasing"], "fail", paidDecreasing),
@@ -119,12 +136,35 @@ export function reviewClaimData(claims, opts = {}) {
119
136
  ]);
120
137
  }
121
138
  const TRIANGLE_DESCRIPTIONS = {
139
+ "non-finite-value": "Every observed cell is a finite number (no NaN or Infinity)",
122
140
  "shape-mismatch": "Paid and incurred triangles share the same origins and ages",
123
141
  "paid-exceeds-incurred": "Paid never exceeds incurred in any cell (1e-9 relative tolerance)",
124
142
  "negative-incremental-paid": "Cumulative paid is non-decreasing along each origin row (salvage/subrogation can legitimately violate this)",
125
143
  "negative-incremental-incurred": "Cumulative incurred is non-decreasing along each origin row (case takedowns can legitimately violate this)",
126
144
  "interior-missing": "No row has a missing cell between observed cells",
127
145
  };
146
+ /**
147
+ * Findings for NaN/Infinity in observed cells. This check exists because every
148
+ * OTHER check is a relational comparison, and every relational comparison is
149
+ * false for NaN — so without it, a triangle of NaN cells passes the entire
150
+ * review and renders into disclosure Section 3 as clean data. It runs FIRST:
151
+ * if the numbers are not numbers, the other verdicts are noise.
152
+ */
153
+ function nonFiniteTriangleFindings(tri) {
154
+ const out = [];
155
+ for (let i = 0; i < tri.values.length; i++) {
156
+ const row = tri.values[i];
157
+ for (let j = 0; j < row.length; j++) {
158
+ const v = row[j];
159
+ if (v === null || v === undefined)
160
+ continue;
161
+ if (!Number.isFinite(v)) {
162
+ out.push(`${tri.kind} ${tri.origins[i]} age ${tri.ages[j]}: ${String(v)}`);
163
+ }
164
+ }
165
+ }
166
+ return out;
167
+ }
128
168
  /** Findings where a row's cumulative values decrease between observed cells. */
129
169
  function negativeIncrementalFindings(tri) {
130
170
  const out = [];
@@ -151,7 +191,10 @@ function interiorMissingFindings(tri) {
151
191
  const out = [];
152
192
  for (let i = 0; i < tri.values.length; i++) {
153
193
  const row = tri.values[i];
154
- const observed = row.map((v) => v !== null);
194
+ // undefined and null are both absences (a JSON round-trip can produce
195
+ // either); the negative-incremental walk above already treats them
196
+ // identically, and a gap must not change meaning between checks.
197
+ const observed = row.map((v) => v !== null && v !== undefined);
155
198
  const first = observed.indexOf(true);
156
199
  const last = observed.lastIndexOf(true);
157
200
  if (first === -1)
@@ -177,12 +220,14 @@ export function reviewTriangles(paid, incurred) {
177
220
  if (!sameAges) {
178
221
  shapeFindings.push(`ages differ: paid [${paid.ages.join(", ")}] vs incurred [${incurred.ages.join(", ")}]`);
179
222
  }
223
+ const nonFiniteCheck = makeCheck("non-finite-value", TRIANGLE_DESCRIPTIONS["non-finite-value"], "fail", [...nonFiniteTriangleFindings(paid), ...nonFiniteTriangleFindings(incurred)]);
180
224
  const shapeCheck = makeCheck("shape-mismatch", TRIANGLE_DESCRIPTIONS["shape-mismatch"], "fail", shapeFindings);
181
225
  if (shapeFindings.length > 0) {
182
226
  // Cell-level comparisons are meaningless across mismatched grids; the
183
227
  // remaining checks stay listed (disclosure) but are not evaluated.
184
228
  const reason = "triangle shapes differ";
185
229
  return summarize([
230
+ nonFiniteCheck,
186
231
  shapeCheck,
187
232
  notEvaluated("paid-exceeds-incurred", TRIANGLE_DESCRIPTIONS["paid-exceeds-incurred"], reason),
188
233
  notEvaluated("negative-incremental-paid", TRIANGLE_DESCRIPTIONS["negative-incremental-paid"], reason),
@@ -206,6 +251,7 @@ export function reviewTriangles(paid, incurred) {
206
251
  }
207
252
  }
208
253
  return summarize([
254
+ nonFiniteCheck,
209
255
  shapeCheck,
210
256
  makeCheck("paid-exceeds-incurred", TRIANGLE_DESCRIPTIONS["paid-exceeds-incurred"], "fail", paidExceeds),
211
257
  makeCheck("negative-incremental-paid", TRIANGLE_DESCRIPTIONS["negative-incremental-paid"], "warning", negativeIncrementalFindings(paid)),
@@ -1 +1 @@
1
- {"version":3,"file":"review.js","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"review.js","sourceRoot":"","sources":["../src/review.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuarial-ts/data",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Data ingestion and ASOP No. 23 data-quality review for the actuarial-ts SDK: RFC 4180 CSV parsing, loss-run import, long-format triangle assembly, and claim/triangle review checks.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
+ "src",
18
19
  "README.md",
19
20
  "LICENSE",
20
21
  "NOTICE"
@@ -47,7 +48,7 @@
47
48
  "prepack": "tsc -p tsconfig.build.json"
48
49
  },
49
50
  "dependencies": {
50
- "@actuarial-ts/core": "^0.2.0"
51
+ "@actuarial-ts/core": "^0.3.0"
51
52
  },
52
53
  "devDependencies": {
53
54
  "typescript": "^5.7.3",
package/src/csv.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Minimal RFC 4180-subset CSV parser.
3
+ *
4
+ * Supported: comma delimiter, quoted fields, doubled quotes ("") as escaped
5
+ * quotes inside quoted fields, commas and newlines inside quoted fields,
6
+ * CRLF and LF row endings, a leading UTF-8 BOM, and a tolerated trailing
7
+ * newline. Lines that are completely empty outside quotes are skipped.
8
+ *
9
+ * Deliberately NOT here: header handling, type coercion, and shape
10
+ * validation. Ragged rows are preserved as-is — validation is the caller's
11
+ * job (see lossRun.ts).
12
+ */
13
+
14
+ export interface CsvParseResult {
15
+ /** The parsed grid (rows x columns). Ragged rows preserved as-is. */
16
+ rows: string[][];
17
+ /**
18
+ * Structural problems the parser recovered from. Content is still parsed
19
+ * leniently — a warning here means the OUTPUT may not mean what the file's
20
+ * author intended, which the caller must surface rather than swallow: an
21
+ * unterminated quote consumes the remainder of the input into one field,
22
+ * and five good rows silently becoming one is how claims vanish.
23
+ */
24
+ warnings: string[];
25
+ }
26
+
27
+ /** Parses CSV text into a grid of string fields plus structural warnings. */
28
+ export function parseCsv(text: string): CsvParseResult {
29
+ let s = text;
30
+ if (s.length > 0 && s.charCodeAt(0) === 0xfeff) s = s.slice(1);
31
+
32
+ const rows: string[][] = [];
33
+ const warnings: string[] = [];
34
+ let row: string[] = [];
35
+ let field = "";
36
+ let inQuotes = false;
37
+ let fieldWasQuoted = false;
38
+ let line = 1;
39
+ let quoteOpenedAtLine = 0;
40
+
41
+ const endField = (): void => {
42
+ row.push(field);
43
+ field = "";
44
+ fieldWasQuoted = false;
45
+ };
46
+
47
+ const endRow = (): void => {
48
+ // A line with zero characters outside quotes is skipped; a line like
49
+ // `""` or `,` still produces a row.
50
+ if (row.length === 0 && field === "" && !fieldWasQuoted) return;
51
+ endField();
52
+ rows.push(row);
53
+ row = [];
54
+ };
55
+
56
+ let i = 0;
57
+ while (i < s.length) {
58
+ const ch = s[i]!;
59
+ if (inQuotes) {
60
+ if (ch === '"') {
61
+ if (s[i + 1] === '"') {
62
+ field += '"';
63
+ i += 2;
64
+ continue;
65
+ }
66
+ inQuotes = false;
67
+ i++;
68
+ continue;
69
+ }
70
+ if (ch === "\n") line++;
71
+ field += ch;
72
+ i++;
73
+ continue;
74
+ }
75
+ if (ch === '"' && field === "" && !fieldWasQuoted) {
76
+ inQuotes = true;
77
+ quoteOpenedAtLine = line;
78
+ fieldWasQuoted = true;
79
+ i++;
80
+ continue;
81
+ }
82
+ if (ch === ",") {
83
+ endField();
84
+ i++;
85
+ continue;
86
+ }
87
+ if (ch === "\r") {
88
+ if (s[i + 1] === "\n") i++;
89
+ endRow();
90
+ line++;
91
+ i++;
92
+ continue;
93
+ }
94
+ if (ch === "\n") {
95
+ endRow();
96
+ line++;
97
+ i++;
98
+ continue;
99
+ }
100
+ field += ch;
101
+ i++;
102
+ }
103
+ // Flush a final row that has no trailing newline. An unterminated quoted
104
+ // field still flushes with whatever it accumulated — content stays lenient —
105
+ // but the structural problem is REPORTED: everything after the stray quote
106
+ // was consumed into one field, and the caller must not present that output
107
+ // as a faithful read of the file.
108
+ if (inQuotes) {
109
+ warnings.push(
110
+ `unterminated quoted field starting at line ${quoteOpenedAtLine}: the remainder of the ` +
111
+ "input was consumed into a single field; check the file for a stray or unescaped quote",
112
+ );
113
+ }
114
+ endRow();
115
+ return { rows, warnings };
116
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./csv.js";
2
+ export * from "./lossRun.js";
3
+ export * from "./longFormat.js";
4
+ export * from "./review.js";
Binary file
package/src/lossRun.ts ADDED
@@ -0,0 +1,171 @@
1
+ import type { ClaimSnapshot } from "@actuarial-ts/core";
2
+ import { ReservingError } from "@actuarial-ts/core";
3
+ import { parseCsv } from "./csv.js";
4
+
5
+ /**
6
+ * Loss-run CSV import. Mirrors the workbench import contract:
7
+ *
8
+ * Required columns (one row per claim per evaluation snapshot):
9
+ * claim_id, accident_date, report_date, evaluation_date,
10
+ * paid_to_date, case_reserve, status
11
+ *
12
+ * Headers are normalized (trimmed, lower-cased, whitespace -> underscores);
13
+ * extra columns are ignored. Missing required columns make the file
14
+ * unparseable and throw ReservingError("SHAPE", ...). Row-level failures are
15
+ * collected — not thrown — so the caller decides whether to abort or load
16
+ * the clean rows; a row with any error contributes no claim.
17
+ *
18
+ * Row numbers in errors are 1-based INCLUDING the header row, so the first
19
+ * data row is row 2.
20
+ *
21
+ * Unlike the workbench importer, negative paid/case amounts are accepted
22
+ * here: the ASOP 23 review layer (reviewClaimData) flags them, keeping the
23
+ * signal visible instead of silently rejecting salvage/subrogation rows.
24
+ */
25
+
26
+ const REQUIRED_COLUMNS = [
27
+ "claim_id",
28
+ "accident_date",
29
+ "report_date",
30
+ "evaluation_date",
31
+ "paid_to_date",
32
+ "case_reserve",
33
+ "status",
34
+ ] as const;
35
+
36
+ export interface LossRunRowError {
37
+ /** 1-based row number including the header (first data row = 2). */
38
+ row: number;
39
+ message: string;
40
+ }
41
+
42
+ export interface LossRunParseResult {
43
+ claims: ClaimSnapshot[];
44
+ errors: LossRunRowError[];
45
+ }
46
+
47
+ function normalizeHeader(h: string): string {
48
+ return h.trim().toLowerCase().replace(/\s+/g, "_");
49
+ }
50
+
51
+ /** True when the string is a real calendar date in yyyy-mm-dd form. */
52
+ function isValidIsoDate(value: string): boolean {
53
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
54
+ if (!match) return false;
55
+ const y = Number(match[1]);
56
+ const m = Number(match[2]);
57
+ const d = Number(match[3]);
58
+ if (m < 1 || m > 12 || d < 1) return false;
59
+ // Arithmetic rule, mirroring compliance/src/metadata.ts (this package cannot
60
+ // import compliance — the dependency points the other way). The previous
61
+ // Date.UTC form mapped years 0-99 into the 1900s; the mapping happens to
62
+ // preserve leapness for every year except 0000, so this is a correctness-of-
63
+ // form consolidation rather than a live-bug fix.
64
+ const leap = (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
65
+ const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1]!;
66
+ return d <= daysInMonth;
67
+ }
68
+
69
+ /** Parses a loss-run CSV into ClaimSnapshots plus per-row validation errors. */
70
+ export function parseLossRunCsv(text: string): LossRunParseResult {
71
+ const { rows: grid, warnings: csvWarnings } = parseCsv(text);
72
+ const headers = (grid[0] ?? []).map(normalizeHeader);
73
+ const missing = REQUIRED_COLUMNS.filter((c) => !headers.includes(c));
74
+ if (missing.length > 0) {
75
+ throw new ReservingError(
76
+ "SHAPE",
77
+ `Missing required column(s): ${missing.join(", ")}. Found: ${
78
+ headers.filter(Boolean).join(", ") || "(none)"
79
+ }`,
80
+ );
81
+ }
82
+ const columnIndex = new Map<string, number>();
83
+ headers.forEach((h, idx) => {
84
+ if (!columnIndex.has(h)) columnIndex.set(h, idx);
85
+ });
86
+
87
+ const claims: ClaimSnapshot[] = [];
88
+ const errors: LossRunRowError[] = [];
89
+ for (const warning of csvWarnings) {
90
+ // Structural CSV problems surface through the same channel as row errors:
91
+ // partial loading is intended behavior here, silent partial loading is not.
92
+ const lineMatch = warning.match(/line (\d+)/);
93
+ errors.push({ row: lineMatch ? Number(lineMatch[1]) : 1, message: `CSV structure: ${warning}` });
94
+ }
95
+
96
+ for (let r = 1; r < grid.length; r++) {
97
+ const rowNumber = r + 1; // 1-based including the header row
98
+ const cells = grid[r]!;
99
+ const cell = (name: string): string => (cells[columnIndex.get(name)!] ?? "").trim();
100
+ const rowErrors: string[] = [];
101
+
102
+ const claimId = cell("claim_id");
103
+ if (claimId === "") rowErrors.push("claim_id is required");
104
+
105
+ const date = (name: string): string | null => {
106
+ const value = cell(name);
107
+ if (!isValidIsoDate(value)) {
108
+ rowErrors.push(`${name} must be a real calendar date in yyyy-mm-dd form (got "${value}")`);
109
+ return null;
110
+ }
111
+ return value;
112
+ };
113
+ const accidentDate = date("accident_date");
114
+ const reportDate = date("report_date");
115
+ const evaluationDate = date("evaluation_date");
116
+
117
+ const amount = (name: string): number | null => {
118
+ const value = cell(name);
119
+ // Currency is decimal digits with an optional sign and fraction —
120
+ // Number() also accepts hex (0x2710 -> 10000), binary, octal and
121
+ // scientific notation, which silently changes magnitudes instead of
122
+ // erroring. Formatted amounts are rejected with a pointed message
123
+ // rather than guessed at: "1,234" could be one thousand or 1.234
124
+ // depending on locale, and a loss run is no place to guess.
125
+ if (/[,()]|\s/.test(value.trim()) && value.trim() !== "") {
126
+ rowErrors.push(
127
+ `${name} must be an unformatted decimal (got "${value}"); remove thousands separators, ` +
128
+ "parentheses and spaces",
129
+ );
130
+ return null;
131
+ }
132
+ const n = /^-?\d+(\.\d+)?$/.test(value.trim()) ? Number(value.trim()) : NaN;
133
+ if (!Number.isFinite(n)) {
134
+ rowErrors.push(`${name} must be a finite decimal number (got "${value}")`);
135
+ return null;
136
+ }
137
+ return n;
138
+ };
139
+ const paidToDate = amount("paid_to_date");
140
+ const caseReserve = amount("case_reserve");
141
+
142
+ const statusRaw = cell("status").toLowerCase();
143
+ const status: "open" | "closed" | null =
144
+ statusRaw === "open" || statusRaw === "closed" ? statusRaw : null;
145
+ if (status === null) {
146
+ rowErrors.push(`status must be "open" or "closed" (got "${cell("status")}")`);
147
+ }
148
+
149
+ // Cross-field checks need all three dates to be individually valid.
150
+ if (accidentDate !== null && reportDate !== null && evaluationDate !== null) {
151
+ if (reportDate < accidentDate) rowErrors.push("report_date precedes accident_date");
152
+ if (evaluationDate < reportDate) rowErrors.push("evaluation_date precedes report_date");
153
+ }
154
+
155
+ if (rowErrors.length > 0) {
156
+ for (const message of rowErrors) errors.push({ row: rowNumber, message });
157
+ continue;
158
+ }
159
+ claims.push({
160
+ claimId,
161
+ accidentDate: accidentDate!,
162
+ reportDate: reportDate!,
163
+ evaluationDate: evaluationDate!,
164
+ paidToDate: paidToDate!,
165
+ caseReserve: caseReserve!,
166
+ status: status!,
167
+ });
168
+ }
169
+
170
+ return { claims, errors };
171
+ }
package/src/review.ts ADDED
@@ -0,0 +1,374 @@
1
+ import type { ClaimSnapshot, Triangle } from "@actuarial-ts/core";
2
+
3
+ /**
4
+ * ASOP No. 23 (Data Quality)-oriented data review.
5
+ *
6
+ * The report lists every check PERFORMED, not just the ones that found
7
+ * something — the actuary's disclosure needs "what was reviewed" as much as
8
+ * "what was found". A check that could not be evaluated (no as-of date; a
9
+ * triangle shape mismatch blocking cell comparisons) stays in the report
10
+ * with an explicit "not evaluated" detail so the disclosure never overstates
11
+ * the review.
12
+ *
13
+ * Severity philosophy:
14
+ * - "fail" = the data is wrong or internally inconsistent (negative paid,
15
+ * dates out of order, duplicate snapshots, paid > incurred).
16
+ * - "warning" = legitimate but rare/reportable (negative case reserves,
17
+ * salvage-driven negative incremental paid, closed claims
18
+ * still carrying case).
19
+ *
20
+ * These utilities are designed to support the actuary's compliance with
21
+ * ASOP No. 23; responsibility for compliance remains with the credentialed
22
+ * actuary.
23
+ */
24
+
25
+ export type DataCheckStatus = "pass" | "warning" | "fail" | "not-evaluated";
26
+
27
+ export interface DataCheck {
28
+ id: string;
29
+ description: string;
30
+ status: DataCheckStatus;
31
+ details: string[];
32
+ }
33
+
34
+ export interface DataReviewReport {
35
+ checks: DataCheck[];
36
+ summary: { pass: number; warning: number; fail: number; notEvaluated: number };
37
+ }
38
+
39
+ export interface ReviewClaimDataOptions {
40
+ /** ISO date; when given, any claim date after it fails "future-dated". */
41
+ asOfDate?: string;
42
+ }
43
+
44
+ /** At most this many offending items are listed per check, then "+N more". */
45
+ const MAX_DETAILS = 20;
46
+
47
+ function capDetails(items: string[]): string[] {
48
+ if (items.length <= MAX_DETAILS) return items;
49
+ return [...items.slice(0, MAX_DETAILS), `+${items.length - MAX_DETAILS} more`];
50
+ }
51
+
52
+ function makeCheck(
53
+ id: string,
54
+ description: string,
55
+ statusWhenFound: "warning" | "fail",
56
+ findings: string[],
57
+ ): DataCheck {
58
+ return {
59
+ id,
60
+ description,
61
+ status: findings.length > 0 ? statusWhenFound : "pass",
62
+ details: capDetails(findings),
63
+ };
64
+ }
65
+
66
+ function notEvaluated(id: string, description: string, reason: string): DataCheck {
67
+ // A check that could not run is REPORTED as such - counting it as "pass"
68
+ // would overstate the review in the very disclosure this feeds.
69
+ return { id, description, status: "not-evaluated", details: [`not evaluated: ${reason}`] };
70
+ }
71
+
72
+ function summarize(checks: DataCheck[]): DataReviewReport {
73
+ const summary = { pass: 0, warning: 0, fail: 0, notEvaluated: 0 };
74
+ for (const c of checks) {
75
+ if (c.status === "not-evaluated") summary.notEvaluated++;
76
+ else summary[c.status]++;
77
+ }
78
+ return { checks, summary };
79
+ }
80
+
81
+ const CLAIM_DESCRIPTIONS = {
82
+ "non-finite-value": "Every money field is a finite number (no NaN or Infinity)",
83
+ "negative-paid": "Cumulative paid amounts are non-negative",
84
+ "negative-case": "Case reserves are non-negative (negative case is legitimate but rare)",
85
+ "paid-decreasing":
86
+ "Cumulative paid never decreases across a claim's snapshots ordered by evaluation date",
87
+ "date-order": "accident_date <= report_date <= evaluation_date on every snapshot",
88
+ "duplicate-snapshot": "No claim has two snapshots at the same evaluation date",
89
+ "future-dated": "No claim date exceeds the as-of date",
90
+ "closed-with-case": "Closed claims carry no outstanding case reserve",
91
+ } as const;
92
+
93
+ /** Reviews claim-level snapshots against the ASOP 23-oriented check suite. */
94
+ export function reviewClaimData(
95
+ claims: ClaimSnapshot[],
96
+ opts: ReviewClaimDataOptions = {},
97
+ ): DataReviewReport {
98
+ const negativePaid: string[] = [];
99
+ const negativeCase: string[] = [];
100
+ const dateOrder: string[] = [];
101
+ const futureDated: string[] = [];
102
+ const closedWithCase: string[] = [];
103
+
104
+ claims.forEach((c) => {
105
+ // Identified by claimId + evaluation date, which the DATA carries. The old
106
+ // label fabricated "row N" from this array's index — but this function
107
+ // receives parsed claims, not the file, and parseLossRunCsv numbers real
108
+ // CSV rows (header-inclusive) in its own errors. Two different "row"
109
+ // meanings pointed auditors at the wrong line; an identifier we cannot
110
+ // compute is one we must not print.
111
+ const where = `claim ${c.claimId} (eval ${c.evaluationDate})`;
112
+ if (c.paidToDate < 0) {
113
+ negativePaid.push(`${where}: paid_to_date ${c.paidToDate}`);
114
+ }
115
+ if (c.caseReserve < 0) {
116
+ negativeCase.push(`${where}: case_reserve ${c.caseReserve}`);
117
+ }
118
+ if (c.reportDate < c.accidentDate) {
119
+ dateOrder.push(
120
+ `${where}: report_date ${c.reportDate} precedes accident_date ${c.accidentDate}`,
121
+ );
122
+ }
123
+ if (c.evaluationDate < c.reportDate) {
124
+ dateOrder.push(
125
+ `${where}: evaluation_date ${c.evaluationDate} precedes report_date ${c.reportDate}`,
126
+ );
127
+ }
128
+ if (opts.asOfDate !== undefined) {
129
+ const asOf = opts.asOfDate;
130
+ if (c.accidentDate > asOf) {
131
+ futureDated.push(`${where}: accident_date ${c.accidentDate} exceeds as-of ${asOf}`);
132
+ }
133
+ if (c.reportDate > asOf) {
134
+ futureDated.push(`${where}: report_date ${c.reportDate} exceeds as-of ${asOf}`);
135
+ }
136
+ if (c.evaluationDate > asOf) {
137
+ futureDated.push(`${where}: evaluation_date ${c.evaluationDate} exceeds as-of ${asOf}`);
138
+ }
139
+ }
140
+ if (c.status === "closed" && c.caseReserve > 0) {
141
+ closedWithCase.push(`${where}: case_reserve ${c.caseReserve} on a closed claim`);
142
+ }
143
+ });
144
+
145
+ // Per-claim timeline checks: duplicates and decreasing cumulative paid.
146
+ const byClaim = new Map<string, ClaimSnapshot[]>();
147
+ for (const c of claims) {
148
+ const list = byClaim.get(c.claimId);
149
+ if (list) list.push(c);
150
+ else byClaim.set(c.claimId, [c]);
151
+ }
152
+ const paidDecreasing: string[] = [];
153
+ const duplicates: string[] = [];
154
+ for (const [claimId, snaps] of byClaim) {
155
+ const sorted = [...snaps].sort((a, b) => a.evaluationDate.localeCompare(b.evaluationDate));
156
+ const seenEvals = new Set<string>();
157
+ for (const s of sorted) {
158
+ if (seenEvals.has(s.evaluationDate)) {
159
+ duplicates.push(`claim ${claimId}: duplicate snapshot at ${s.evaluationDate}`);
160
+ }
161
+ seenEvals.add(s.evaluationDate);
162
+ }
163
+ for (let k = 1; k < sorted.length; k++) {
164
+ const prev = sorted[k - 1]!;
165
+ const cur = sorted[k]!;
166
+ // Same-date pairs are the duplicate check's finding, not this one's.
167
+ if (cur.evaluationDate === prev.evaluationDate) continue;
168
+ if (cur.paidToDate < prev.paidToDate) {
169
+ paidDecreasing.push(
170
+ `claim ${claimId}: paid_to_date ${prev.paidToDate} -> ${cur.paidToDate} between ${prev.evaluationDate} and ${cur.evaluationDate}`,
171
+ );
172
+ }
173
+ }
174
+ }
175
+
176
+ const nonFinite: string[] = [];
177
+ for (const c of claims) {
178
+ const where = `claim ${c.claimId}`;
179
+ if (!Number.isFinite(c.paidToDate)) nonFinite.push(`${where}: paid_to_date ${String(c.paidToDate)}`);
180
+ if (!Number.isFinite(c.caseReserve)) nonFinite.push(`${where}: case_reserve ${String(c.caseReserve)}`);
181
+ }
182
+
183
+ const futureCheck =
184
+ opts.asOfDate === undefined
185
+ ? notEvaluated("future-dated", CLAIM_DESCRIPTIONS["future-dated"], "no asOfDate provided")
186
+ : makeCheck("future-dated", CLAIM_DESCRIPTIONS["future-dated"], "fail", futureDated);
187
+
188
+ return summarize([
189
+ // First: if the numbers are not numbers, the other verdicts are noise.
190
+ makeCheck("non-finite-value", CLAIM_DESCRIPTIONS["non-finite-value"], "fail", nonFinite),
191
+ makeCheck("negative-paid", CLAIM_DESCRIPTIONS["negative-paid"], "fail", negativePaid),
192
+ makeCheck("negative-case", CLAIM_DESCRIPTIONS["negative-case"], "warning", negativeCase),
193
+ makeCheck("paid-decreasing", CLAIM_DESCRIPTIONS["paid-decreasing"], "fail", paidDecreasing),
194
+ makeCheck("date-order", CLAIM_DESCRIPTIONS["date-order"], "fail", dateOrder),
195
+ makeCheck("duplicate-snapshot", CLAIM_DESCRIPTIONS["duplicate-snapshot"], "fail", duplicates),
196
+ futureCheck,
197
+ makeCheck("closed-with-case", CLAIM_DESCRIPTIONS["closed-with-case"], "warning", closedWithCase),
198
+ ]);
199
+ }
200
+
201
+ const TRIANGLE_DESCRIPTIONS = {
202
+ "non-finite-value": "Every observed cell is a finite number (no NaN or Infinity)",
203
+ "shape-mismatch": "Paid and incurred triangles share the same origins and ages",
204
+ "paid-exceeds-incurred": "Paid never exceeds incurred in any cell (1e-9 relative tolerance)",
205
+ "negative-incremental-paid":
206
+ "Cumulative paid is non-decreasing along each origin row (salvage/subrogation can legitimately violate this)",
207
+ "negative-incremental-incurred":
208
+ "Cumulative incurred is non-decreasing along each origin row (case takedowns can legitimately violate this)",
209
+ "interior-missing": "No row has a missing cell between observed cells",
210
+ } as const;
211
+
212
+ /**
213
+ * Findings for NaN/Infinity in observed cells. This check exists because every
214
+ * OTHER check is a relational comparison, and every relational comparison is
215
+ * false for NaN — so without it, a triangle of NaN cells passes the entire
216
+ * review and renders into disclosure Section 3 as clean data. It runs FIRST:
217
+ * if the numbers are not numbers, the other verdicts are noise.
218
+ */
219
+ function nonFiniteTriangleFindings(tri: Triangle): string[] {
220
+ const out: string[] = [];
221
+ for (let i = 0; i < tri.values.length; i++) {
222
+ const row = tri.values[i]!;
223
+ for (let j = 0; j < row.length; j++) {
224
+ const v = row[j];
225
+ if (v === null || v === undefined) continue;
226
+ if (!Number.isFinite(v)) {
227
+ out.push(`${tri.kind} ${tri.origins[i]} age ${tri.ages[j]}: ${String(v)}`);
228
+ }
229
+ }
230
+ }
231
+ return out;
232
+ }
233
+
234
+ /** Findings where a row's cumulative values decrease between observed cells. */
235
+ function negativeIncrementalFindings(tri: Triangle): string[] {
236
+ const out: string[] = [];
237
+ for (let i = 0; i < tri.values.length; i++) {
238
+ const row = tri.values[i]!;
239
+ let prev: number | null = null;
240
+ let prevAge: number | null = null;
241
+ for (let j = 0; j < row.length; j++) {
242
+ const v = row[j];
243
+ if (v === null || v === undefined) continue;
244
+ const age = tri.ages[j]!;
245
+ if (prev !== null && v < prev) {
246
+ out.push(
247
+ `${tri.kind} ${tri.origins[i]} age ${prevAge} -> ${age}: ${prev} -> ${v}`,
248
+ );
249
+ }
250
+ prev = v;
251
+ prevAge = age;
252
+ }
253
+ }
254
+ return out;
255
+ }
256
+
257
+ /** Findings for null cells with observed cells both before and after in the row. */
258
+ function interiorMissingFindings(tri: Triangle): string[] {
259
+ const out: string[] = [];
260
+ for (let i = 0; i < tri.values.length; i++) {
261
+ const row = tri.values[i]!;
262
+ // undefined and null are both absences (a JSON round-trip can produce
263
+ // either); the negative-incremental walk above already treats them
264
+ // identically, and a gap must not change meaning between checks.
265
+ const observed = row.map((v) => v !== null && v !== undefined);
266
+ const first = observed.indexOf(true);
267
+ const last = observed.lastIndexOf(true);
268
+ if (first === -1) continue;
269
+ for (let j = first + 1; j < last; j++) {
270
+ if (!observed[j]) {
271
+ out.push(`${tri.kind} ${tri.origins[i]} age ${tri.ages[j]}: interior cell missing`);
272
+ }
273
+ }
274
+ }
275
+ return out;
276
+ }
277
+
278
+ /** Reviews a paid/incurred triangle pair for cross-triangle consistency. */
279
+ export function reviewTriangles(paid: Triangle, incurred: Triangle): DataReviewReport {
280
+ const shapeFindings: string[] = [];
281
+ const sameOrigins =
282
+ paid.origins.length === incurred.origins.length &&
283
+ paid.origins.every((o, i) => o === incurred.origins[i]);
284
+ const sameAges =
285
+ paid.ages.length === incurred.ages.length &&
286
+ paid.ages.every((a, j) => a === incurred.ages[j]);
287
+ if (!sameOrigins) {
288
+ shapeFindings.push(
289
+ `origins differ: paid [${paid.origins.join(", ")}] vs incurred [${incurred.origins.join(", ")}]`,
290
+ );
291
+ }
292
+ if (!sameAges) {
293
+ shapeFindings.push(
294
+ `ages differ: paid [${paid.ages.join(", ")}] vs incurred [${incurred.ages.join(", ")}]`,
295
+ );
296
+ }
297
+ const nonFiniteCheck = makeCheck(
298
+ "non-finite-value",
299
+ TRIANGLE_DESCRIPTIONS["non-finite-value"],
300
+ "fail",
301
+ [...nonFiniteTriangleFindings(paid), ...nonFiniteTriangleFindings(incurred)],
302
+ );
303
+ const shapeCheck = makeCheck(
304
+ "shape-mismatch",
305
+ TRIANGLE_DESCRIPTIONS["shape-mismatch"],
306
+ "fail",
307
+ shapeFindings,
308
+ );
309
+ if (shapeFindings.length > 0) {
310
+ // Cell-level comparisons are meaningless across mismatched grids; the
311
+ // remaining checks stay listed (disclosure) but are not evaluated.
312
+ const reason = "triangle shapes differ";
313
+ return summarize([
314
+ nonFiniteCheck,
315
+ shapeCheck,
316
+ notEvaluated("paid-exceeds-incurred", TRIANGLE_DESCRIPTIONS["paid-exceeds-incurred"], reason),
317
+ notEvaluated(
318
+ "negative-incremental-paid",
319
+ TRIANGLE_DESCRIPTIONS["negative-incremental-paid"],
320
+ reason,
321
+ ),
322
+ notEvaluated(
323
+ "negative-incremental-incurred",
324
+ TRIANGLE_DESCRIPTIONS["negative-incremental-incurred"],
325
+ reason,
326
+ ),
327
+ notEvaluated("interior-missing", TRIANGLE_DESCRIPTIONS["interior-missing"], reason),
328
+ ]);
329
+ }
330
+
331
+ const paidExceeds: string[] = [];
332
+ for (let i = 0; i < paid.values.length; i++) {
333
+ const paidRow = paid.values[i]!;
334
+ const incRow = incurred.values[i]!;
335
+ for (let j = 0; j < paidRow.length; j++) {
336
+ const p = paidRow[j];
337
+ const inc = incRow[j];
338
+ if (p === null || p === undefined || inc === null || inc === undefined) continue;
339
+ const tolerance = 1e-9 * Math.max(1, Math.abs(p), Math.abs(inc));
340
+ if (p - inc > tolerance) {
341
+ paidExceeds.push(`${paid.origins[i]} age ${paid.ages[j]}: paid ${p} > incurred ${inc}`);
342
+ }
343
+ }
344
+ }
345
+
346
+ return summarize([
347
+ nonFiniteCheck,
348
+ shapeCheck,
349
+ makeCheck(
350
+ "paid-exceeds-incurred",
351
+ TRIANGLE_DESCRIPTIONS["paid-exceeds-incurred"],
352
+ "fail",
353
+ paidExceeds,
354
+ ),
355
+ makeCheck(
356
+ "negative-incremental-paid",
357
+ TRIANGLE_DESCRIPTIONS["negative-incremental-paid"],
358
+ "warning",
359
+ negativeIncrementalFindings(paid),
360
+ ),
361
+ makeCheck(
362
+ "negative-incremental-incurred",
363
+ TRIANGLE_DESCRIPTIONS["negative-incremental-incurred"],
364
+ "warning",
365
+ negativeIncrementalFindings(incurred),
366
+ ),
367
+ makeCheck(
368
+ "interior-missing",
369
+ TRIANGLE_DESCRIPTIONS["interior-missing"],
370
+ "warning",
371
+ [...interiorMissingFindings(paid), ...interiorMissingFindings(incurred)],
372
+ ),
373
+ ]);
374
+ }