@actuarial-ts/data 0.4.0 → 0.6.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 (42) hide show
  1. package/README.md +45 -127
  2. package/dist/casualtyDiagnosticReview.d.ts +46 -0
  3. package/dist/casualtyDiagnosticReview.d.ts.map +1 -0
  4. package/dist/casualtyDiagnosticReview.js +23 -0
  5. package/dist/casualtyDiagnosticReview.js.map +1 -0
  6. package/dist/diagnosticDefinition.d.ts +4 -0
  7. package/dist/diagnosticDefinition.d.ts.map +1 -0
  8. package/dist/diagnosticDefinition.js +6 -0
  9. package/dist/diagnosticDefinition.js.map +1 -0
  10. package/dist/diagnosticInput.d.ts +97 -13
  11. package/dist/diagnosticInput.d.ts.map +1 -1
  12. package/dist/diagnosticInput.js +58 -46
  13. package/dist/diagnosticInput.js.map +1 -1
  14. package/dist/diagnosticPreparedReview.d.ts +43 -0
  15. package/dist/diagnosticPreparedReview.d.ts.map +1 -0
  16. package/dist/diagnosticPreparedReview.js +58 -0
  17. package/dist/diagnosticPreparedReview.js.map +1 -0
  18. package/dist/index.d.ts +4 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +4 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/review.d.ts +15 -1
  23. package/dist/review.d.ts.map +1 -1
  24. package/dist/review.js +3 -2
  25. package/dist/review.js.map +1 -1
  26. package/dist/version.d.ts +2 -0
  27. package/dist/version.d.ts.map +1 -0
  28. package/dist/version.js +2 -0
  29. package/dist/version.js.map +1 -0
  30. package/package.json +3 -3
  31. package/src/casualtyDiagnosticReview.ts +17 -0
  32. package/src/diagnosticDefinition.ts +6 -0
  33. package/src/diagnosticInput.ts +61 -66
  34. package/src/diagnosticPreparedReview.ts +39 -0
  35. package/src/index.ts +4 -1
  36. package/src/review.ts +18 -3
  37. package/src/version.ts +1 -0
  38. package/dist/diagnosticReview.d.ts +0 -68
  39. package/dist/diagnosticReview.d.ts.map +0 -1
  40. package/dist/diagnosticReview.js +0 -370
  41. package/dist/diagnosticReview.js.map +0 -1
  42. package/src/diagnosticReview.ts +0 -547
@@ -1,547 +0,0 @@
1
- import {
2
- developmentAgeMonths,
3
- parseQuarterPeriod,
4
- type DevelopmentAgeConvention,
5
- type DiagnosticExposureRow,
6
- type DiagnosticLossRow,
7
- type DiagnosticMeasureMap,
8
- } from "@actuarial-ts/core";
9
- import {
10
- createNotEvaluatedDataCheck,
11
- createStructuredDataCheck,
12
- summarizeDataChecks,
13
- type DataCheck,
14
- type DataFinding,
15
- type DataFindingContext,
16
- type DataReviewReport,
17
- } from "./review.js";
18
-
19
- export interface DiagnosticReviewSnapshot extends DiagnosticLossRow {
20
- source?: Pick<DataFindingContext, "sourceFile" | "sourceRow">;
21
- }
22
-
23
- export interface DiagnosticReviewExposure extends DiagnosticExposureRow {
24
- source?: Pick<DataFindingContext, "sourceFile" | "sourceRow">;
25
- }
26
-
27
- export interface DiagnosticAmountPair {
28
- id: string;
29
- paidMeasure: string;
30
- incurredMeasure: string;
31
- }
32
-
33
- export interface DiagnosticLayerReviewDefinition extends DiagnosticAmountPair {
34
- /** This layer must be no greater than the named broader layer. */
35
- broaderLayerId?: string;
36
- }
37
-
38
- export interface DiagnosticLayerControlTotal {
39
- id: string;
40
- measure: string;
41
- expected: number;
42
- group?: string;
43
- origin?: string;
44
- valuation?: string;
45
- }
46
-
47
- export interface DiagnosticGroupingAssignment {
48
- key: string;
49
- group: string;
50
- source?: Pick<DataFindingContext, "sourceFile" | "sourceRow">;
51
- }
52
-
53
- export interface CachedFormulaProvenance {
54
- id: string;
55
- sourceFile?: string;
56
- sourceRow?: number;
57
- /** Formula text when the source cell is formula-backed. */
58
- formula?: string;
59
- /** A cached numeric value imported from the workbook. */
60
- cachedValue?: number | null;
61
- declaredFormulaSource: boolean;
62
- }
63
-
64
- export const DIAGNOSTIC_REVIEW_CHECK_CODES = [
65
- "duplicate-aggregate-snapshot",
66
- "duplicate-exposure-key",
67
- "invalid-development-age",
68
- "development-age-mismatch",
69
- "valuation-before-origin",
70
- "count-reconciliation",
71
- "closed-no-pay-exceeds-reported",
72
- "paid-exceeds-incurred",
73
- "cumulative-paid-decreasing",
74
- "cumulative-reported-decreasing",
75
- "closed-reopen-signal",
76
- "layer-order",
77
- "layer-control-reconciliation",
78
- "loss-without-exposure",
79
- "exposure-without-loss",
80
- "zero-exposure",
81
- "incomplete-exposure",
82
- "inconsistent-group-mapping",
83
- "cached-formula-provenance",
84
- ] as const;
85
-
86
- export type DiagnosticReviewCheckCode = (typeof DIAGNOSTIC_REVIEW_CHECK_CODES)[number];
87
-
88
- export interface ReviewDiagnosticDataOptions {
89
- reportedMeasure?: string;
90
- openMeasure?: string;
91
- closedNoPayMeasure?: string;
92
- closedWithPayMeasure?: string;
93
- exposureMeasure?: string;
94
- amountPairs?: readonly DiagnosticAmountPair[];
95
- layers?: readonly DiagnosticLayerReviewDefinition[];
96
- controlTotals?: readonly DiagnosticLayerControlTotal[];
97
- groupingAssignments?: readonly DiagnosticGroupingAssignment[];
98
- cachedFormulaProvenance?: readonly CachedFormulaProvenance[];
99
- ageConvention?: DevelopmentAgeConvention;
100
- tolerance?: { absolute?: number; relative?: number; ageMonths?: number };
101
- severities?: Partial<Record<DiagnosticReviewCheckCode, "warning" | "fail">>;
102
- }
103
-
104
- const DESCRIPTIONS: Record<DiagnosticReviewCheckCode, string> = {
105
- "duplicate-aggregate-snapshot": "No group/origin/valuation aggregate snapshot key appears twice",
106
- "duplicate-exposure-key": "Every exposure key is unique (repeated valuation copies remain reviewable)",
107
- "invalid-development-age": "Development ages are finite, non-negative quarterly month counts",
108
- "development-age-mismatch": "Recorded age agrees with origin, valuation, and the selected age convention",
109
- "valuation-before-origin": "No valuation quarter precedes its origin quarter",
110
- "count-reconciliation": "Reported count equals open plus closed-no-pay plus closed-with-pay count",
111
- "closed-no-pay-exceeds-reported": "Closed-no-pay count does not exceed reported count",
112
- "paid-exceeds-incurred": "Paid does not exceed incurred on any configured amount basis",
113
- "cumulative-paid-decreasing": "Cumulative paid does not decrease across valuation snapshots",
114
- "cumulative-reported-decreasing": "Cumulative reported count does not decrease across valuation snapshots",
115
- "closed-reopen-signal": "Cumulative closed count does not decrease without a reopen explanation",
116
- "layer-order": "Narrower layer amounts do not exceed their configured broader layers",
117
- "layer-control-reconciliation": "Layer measures reconcile to caller-supplied control totals",
118
- "loss-without-exposure": "Every group/origin loss key has exposure",
119
- "exposure-without-loss": "Every group/origin exposure key has loss data",
120
- "zero-exposure": "Exposure is not explicitly zero",
121
- "incomplete-exposure": "Exposure records are complete and carry the configured measure",
122
- "inconsistent-group-mapping": "Each caller grouping key maps to exactly one group",
123
- "cached-formula-provenance": "Cached workbook values retain formula provenance when declared formula-derived",
124
- };
125
-
126
- const DEFAULT_SEVERITY: Record<DiagnosticReviewCheckCode, "warning" | "fail"> = {
127
- "duplicate-aggregate-snapshot": "fail",
128
- "duplicate-exposure-key": "fail",
129
- "invalid-development-age": "fail",
130
- "development-age-mismatch": "fail",
131
- "valuation-before-origin": "fail",
132
- "count-reconciliation": "fail",
133
- "closed-no-pay-exceeds-reported": "fail",
134
- "paid-exceeds-incurred": "fail",
135
- "cumulative-paid-decreasing": "warning",
136
- "cumulative-reported-decreasing": "fail",
137
- "closed-reopen-signal": "warning",
138
- "layer-order": "fail",
139
- "layer-control-reconciliation": "fail",
140
- "loss-without-exposure": "warning",
141
- "exposure-without-loss": "warning",
142
- "zero-exposure": "fail",
143
- "incomplete-exposure": "fail",
144
- "inconsistent-group-mapping": "fail",
145
- "cached-formula-provenance": "warning",
146
- };
147
-
148
- function context(row: DiagnosticReviewSnapshot | DiagnosticReviewExposure): DataFindingContext {
149
- return {
150
- origin: row.origin,
151
- group: row.group,
152
- ...( "valuation" in row && row.valuation !== undefined ? { valuation: row.valuation } : {}),
153
- ...( "ageMonths" in row ? { ageMonths: row.ageMonths } : {}),
154
- ...row.source,
155
- };
156
- }
157
-
158
- function finding(code: DiagnosticReviewCheckCode, message: string, ctx?: DataFindingContext): DataFinding {
159
- return { code, message, ...(ctx && Object.values(ctx).some((value) => value !== undefined) ? { context: ctx } : {}) };
160
- }
161
-
162
- function finite(measures: DiagnosticMeasureMap, name: string): number | null {
163
- const value = measures[name];
164
- return value !== null && value !== undefined && Number.isFinite(value) ? value : null;
165
- }
166
-
167
- function exceeds(a: number, b: number, absolute: number, relative: number): boolean {
168
- return a - b > absolute + relative * Math.max(1, Math.abs(a), Math.abs(b));
169
- }
170
-
171
- function differs(a: number, b: number, absolute: number, relative: number): boolean {
172
- return Math.abs(a - b) > absolute + relative * Math.max(1, Math.abs(a), Math.abs(b));
173
- }
174
-
175
- function make(
176
- code: DiagnosticReviewCheckCode,
177
- findings: readonly DataFinding[],
178
- options: ReviewDiagnosticDataOptions,
179
- ): DataCheck {
180
- return createStructuredDataCheck(
181
- code,
182
- DESCRIPTIONS[code],
183
- options.severities?.[code] ?? DEFAULT_SEVERITY[code],
184
- findings,
185
- );
186
- }
187
-
188
- function notEvaluated(code: DiagnosticReviewCheckCode, reason: string): DataCheck {
189
- return createNotEvaluatedDataCheck(code, DESCRIPTIONS[code], reason);
190
- }
191
-
192
- interface MeasureCoverage {
193
- applicable: boolean;
194
- findings: DataFinding[];
195
- }
196
-
197
- function reviewMeasureCoverage(
198
- code: DiagnosticReviewCheckCode,
199
- snapshots: readonly DiagnosticReviewSnapshot[],
200
- measureNames: readonly string[],
201
- ): MeasureCoverage {
202
- const required = [...new Set(measureNames)];
203
- const applicable = snapshots.some((row) => required.some(
204
- (name) => Object.prototype.hasOwnProperty.call(row.measures, name),
205
- ));
206
- if (!applicable) return { applicable: false, findings: [] };
207
-
208
- const findings: DataFinding[] = [];
209
- for (const row of snapshots) {
210
- const unavailable = required.filter((name) => finite(row.measures, name) === null);
211
- if (unavailable.length > 0) findings.push(finding(
212
- code,
213
- `Required measure${unavailable.length === 1 ? "" : "s"} missing or non-finite: ${unavailable.join(", ")}`,
214
- context(row),
215
- ));
216
- }
217
- return { applicable: true, findings };
218
- }
219
-
220
- /** Known violations and incomplete inputs remain reportable; absent inputs do not pass. */
221
- function makeWithCoverage(
222
- code: DiagnosticReviewCheckCode,
223
- findings: readonly DataFinding[],
224
- coverage: MeasureCoverage,
225
- noApplicableReason: string,
226
- options: ReviewDiagnosticDataOptions,
227
- ): DataCheck {
228
- const combined = [...findings, ...coverage.findings];
229
- if (combined.length > 0 || coverage.applicable) return make(code, combined, options);
230
- return notEvaluated(code, noApplicableReason);
231
- }
232
-
233
- /**
234
- * Quarterly aggregate diagnostics review using the existing DataReviewReport
235
- * status model. Every check remains listed, including checks that could not be
236
- * evaluated because the caller supplied no applicable configuration.
237
- */
238
- export function reviewDiagnosticData(
239
- snapshots: readonly DiagnosticReviewSnapshot[],
240
- exposures: readonly DiagnosticReviewExposure[],
241
- options: ReviewDiagnosticDataOptions = {},
242
- ): DataReviewReport {
243
- const reported = options.reportedMeasure ?? "reportedCount";
244
- const open = options.openMeasure ?? "openCount";
245
- const cnp = options.closedNoPayMeasure ?? "closedNoPayCount";
246
- const cwp = options.closedWithPayMeasure ?? "closedWithPayCount";
247
- const exposureMeasure = options.exposureMeasure ?? "exposure";
248
- const absolute = options.tolerance?.absolute ?? 1e-9;
249
- const relative = options.tolerance?.relative ?? 1e-9;
250
- const ageTolerance = options.tolerance?.ageMonths ?? 0;
251
- const amountPairs = options.amountPairs ?? [];
252
-
253
- const duplicateSnapshots: DataFinding[] = [];
254
- const seenSnapshots = new Set<string>();
255
- for (const row of snapshots) {
256
- const key = `${row.group}\u0000${row.origin}\u0000${row.valuation}`;
257
- if (seenSnapshots.has(key)) duplicateSnapshots.push(finding(
258
- "duplicate-aggregate-snapshot",
259
- `Duplicate aggregate snapshot ${row.group}/${row.origin}/${row.valuation}`,
260
- context(row),
261
- ));
262
- seenSnapshots.add(key);
263
- }
264
-
265
- const duplicateExposures: DataFinding[] = [];
266
- const seenExposure = new Set<string>();
267
- for (const row of exposures) {
268
- if (seenExposure.has(row.key)) duplicateExposures.push(finding(
269
- "duplicate-exposure-key",
270
- `Duplicate exposure key ${row.key}`,
271
- { ...context(row), ...(row.source ?? {}) },
272
- ));
273
- seenExposure.add(row.key);
274
- }
275
-
276
- const invalidAge: DataFinding[] = [];
277
- const ageMismatch: DataFinding[] = [];
278
- const valuationBefore: DataFinding[] = [];
279
- for (const row of snapshots) {
280
- if (!Number.isFinite(row.ageMonths) || row.ageMonths < 0 || !Number.isInteger(row.ageMonths) || row.ageMonths % 3 !== 0) {
281
- invalidAge.push(finding("invalid-development-age", `Invalid quarterly development age ${row.ageMonths}`, context(row)));
282
- }
283
- try {
284
- parseQuarterPeriod(row.origin);
285
- parseQuarterPeriod(row.valuation);
286
- let expected: number;
287
- try {
288
- expected = developmentAgeMonths(row.origin, row.valuation, options.ageConvention);
289
- } catch (error) {
290
- valuationBefore.push(finding("valuation-before-origin", error instanceof Error ? error.message : String(error), context(row)));
291
- continue;
292
- }
293
- if (Number.isFinite(row.ageMonths) && Math.abs(row.ageMonths - expected) > ageTolerance) {
294
- ageMismatch.push(finding(
295
- "development-age-mismatch",
296
- `Recorded age ${row.ageMonths} differs from expected age ${expected}`,
297
- context(row),
298
- ));
299
- }
300
- } catch (error) {
301
- ageMismatch.push(finding(
302
- "development-age-mismatch",
303
- `Origin/valuation period is not parseable: ${error instanceof Error ? error.message : String(error)}`,
304
- context(row),
305
- ));
306
- }
307
- }
308
-
309
- const countReconciliation: DataFinding[] = [];
310
- const cnpExceeds: DataFinding[] = [];
311
- for (const row of snapshots) {
312
- const r = finite(row.measures, reported);
313
- const o = finite(row.measures, open);
314
- const n = finite(row.measures, cnp);
315
- const w = finite(row.measures, cwp);
316
- if (r !== null && o !== null && n !== null && w !== null && differs(r, o + n + w, absolute, relative)) {
317
- countReconciliation.push(finding(
318
- "count-reconciliation",
319
- `${reported} ${r} != ${open} ${o} + ${cnp} ${n} + ${cwp} ${w}`,
320
- context(row),
321
- ));
322
- }
323
- if (r !== null && n !== null && exceeds(n, r, absolute, relative)) {
324
- cnpExceeds.push(finding("closed-no-pay-exceeds-reported", `${cnp} ${n} > ${reported} ${r}`, context(row)));
325
- }
326
- }
327
-
328
- const paidExceeds: DataFinding[] = [];
329
- for (const row of snapshots) {
330
- for (const pair of amountPairs) {
331
- const paid = finite(row.measures, pair.paidMeasure);
332
- const incurred = finite(row.measures, pair.incurredMeasure);
333
- if (paid !== null && incurred !== null && exceeds(paid, incurred, absolute, relative)) {
334
- paidExceeds.push(finding(
335
- "paid-exceeds-incurred",
336
- `${pair.id}: ${pair.paidMeasure} ${paid} > ${pair.incurredMeasure} ${incurred}`,
337
- context(row),
338
- ));
339
- }
340
- }
341
- }
342
-
343
- const byTimeline = new Map<string, DiagnosticReviewSnapshot[]>();
344
- for (const row of snapshots) {
345
- const key = `${row.group}\u0000${row.origin}`;
346
- const list = byTimeline.get(key) ?? [];
347
- list.push(row);
348
- byTimeline.set(key, list);
349
- }
350
- const paidDecreasing: DataFinding[] = [];
351
- const reportedDecreasing: DataFinding[] = [];
352
- const closedDecreasing: DataFinding[] = [];
353
- for (const timeline of byTimeline.values()) {
354
- const sorted = [...timeline].sort((a, b) => a.ageMonths - b.ageMonths);
355
- for (let i = 1; i < sorted.length; i++) {
356
- const previous = sorted[i - 1]!;
357
- const current = sorted[i]!;
358
- for (const pair of amountPairs) {
359
- const before = finite(previous.measures, pair.paidMeasure);
360
- const after = finite(current.measures, pair.paidMeasure);
361
- if (before !== null && after !== null && exceeds(before, after, absolute, relative)) {
362
- paidDecreasing.push(finding(
363
- "cumulative-paid-decreasing",
364
- `${pair.id}: ${pair.paidMeasure} decreased ${before} -> ${after}`,
365
- context(current),
366
- ));
367
- }
368
- }
369
- const beforeReported = finite(previous.measures, reported);
370
- const afterReported = finite(current.measures, reported);
371
- if (beforeReported !== null && afterReported !== null && exceeds(beforeReported, afterReported, absolute, relative)) {
372
- reportedDecreasing.push(finding(
373
- "cumulative-reported-decreasing",
374
- `${reported} decreased ${beforeReported} -> ${afterReported}`,
375
- context(current),
376
- ));
377
- }
378
- const previousCnp = finite(previous.measures, cnp);
379
- const previousCwp = finite(previous.measures, cwp);
380
- const currentCnp = finite(current.measures, cnp);
381
- const currentCwp = finite(current.measures, cwp);
382
- if (previousCnp !== null && previousCwp !== null && currentCnp !== null && currentCwp !== null) {
383
- const beforeClosed = previousCnp + previousCwp;
384
- const afterClosed = currentCnp + currentCwp;
385
- if (exceeds(beforeClosed, afterClosed, absolute, relative)) {
386
- closedDecreasing.push(finding(
387
- "closed-reopen-signal",
388
- `Total closed count decreased ${beforeClosed} -> ${afterClosed}; investigate reopen activity`,
389
- context(current),
390
- ));
391
- }
392
- }
393
- }
394
- }
395
-
396
- const layerOrder: DataFinding[] = [];
397
- const layerById = new Map((options.layers ?? []).map((layer) => [layer.id, layer]));
398
- for (const layer of options.layers ?? []) {
399
- if (!layer.broaderLayerId) continue;
400
- const broader = layerById.get(layer.broaderLayerId);
401
- if (!broader) {
402
- layerOrder.push(finding("layer-order", `Layer ${layer.id} names unknown broader layer ${layer.broaderLayerId}`));
403
- continue;
404
- }
405
- for (const row of snapshots) {
406
- for (const [narrowName, broadName, label] of [
407
- [layer.paidMeasure, broader.paidMeasure, "paid"],
408
- [layer.incurredMeasure, broader.incurredMeasure, "incurred"],
409
- ] as const) {
410
- const narrow = finite(row.measures, narrowName);
411
- const broad = finite(row.measures, broadName);
412
- if (narrow !== null && broad !== null && exceeds(narrow, broad, absolute, relative)) {
413
- layerOrder.push(finding(
414
- "layer-order",
415
- `${layer.id} ${label} ${narrow} > broader ${broader.id} ${label} ${broad}`,
416
- context(row),
417
- ));
418
- }
419
- }
420
- }
421
- }
422
-
423
- const controlReconciliation: DataFinding[] = [];
424
- for (const control of options.controlTotals ?? []) {
425
- let actual = 0;
426
- let observed = 0;
427
- for (const row of snapshots) {
428
- if (control.group !== undefined && row.group !== control.group) continue;
429
- if (control.origin !== undefined && row.origin !== control.origin) continue;
430
- if (control.valuation !== undefined && row.valuation !== control.valuation) continue;
431
- const value = finite(row.measures, control.measure);
432
- if (value !== null) { actual += value; observed++; }
433
- }
434
- if (observed === 0 || differs(actual, control.expected, absolute, relative)) {
435
- controlReconciliation.push(finding(
436
- "layer-control-reconciliation",
437
- `${control.id}: ${control.measure} actual ${observed === 0 ? "missing" : actual} != control ${control.expected}`,
438
- { group: control.group, origin: control.origin, valuation: control.valuation },
439
- ));
440
- }
441
- }
442
-
443
- const lossKeys = new Set(snapshots.map((row) => `${row.group}\u0000${row.origin}`));
444
- const exposureKeys = new Set(exposures.map((row) => `${row.group}\u0000${row.origin}`));
445
- const lossWithoutExposure: DataFinding[] = [];
446
- for (const key of lossKeys) if (!exposureKeys.has(key)) {
447
- const [group, origin] = key.split("\u0000") as [string, string];
448
- lossWithoutExposure.push(finding("loss-without-exposure", `Loss key ${group}/${origin} has no exposure`, { group, origin }));
449
- }
450
- const exposureWithoutLoss: DataFinding[] = [];
451
- for (const key of exposureKeys) if (!lossKeys.has(key)) {
452
- const [group, origin] = key.split("\u0000") as [string, string];
453
- exposureWithoutLoss.push(finding("exposure-without-loss", `Exposure key ${group}/${origin} has no loss data`, { group, origin }));
454
- }
455
-
456
- const zeroExposure: DataFinding[] = [];
457
- const incompleteExposure: DataFinding[] = [];
458
- for (const row of exposures) {
459
- const value = row.measures[exposureMeasure];
460
- if (value === 0) zeroExposure.push(finding("zero-exposure", `Exposure key ${row.key} has explicit zero ${exposureMeasure}`, context(row)));
461
- if (row.complete === false || value === null || value === undefined || !Number.isFinite(value)) {
462
- incompleteExposure.push(finding("incomplete-exposure", `Exposure key ${row.key} has incomplete ${exposureMeasure}`, context(row)));
463
- }
464
- }
465
-
466
- const inconsistentGrouping: DataFinding[] = [];
467
- const groupByKey = new Map<string, DiagnosticGroupingAssignment>();
468
- for (const assignment of options.groupingAssignments ?? []) {
469
- const previous = groupByKey.get(assignment.key);
470
- if (previous && previous.group !== assignment.group) {
471
- inconsistentGrouping.push(finding(
472
- "inconsistent-group-mapping",
473
- `Grouping key ${assignment.key} maps to both ${previous.group} and ${assignment.group}`,
474
- { group: assignment.group, ...assignment.source },
475
- ));
476
- } else groupByKey.set(assignment.key, assignment);
477
- }
478
-
479
- const formulaProvenance: DataFinding[] = [];
480
- for (const item of options.cachedFormulaProvenance ?? []) {
481
- if (item.declaredFormulaSource && item.cachedValue !== undefined && (!item.formula || item.formula.trim() === "")) {
482
- formulaProvenance.push(finding(
483
- "cached-formula-provenance",
484
- `Cached formula value ${item.id} has no retained formula text`,
485
- { sourceFile: item.sourceFile, sourceRow: item.sourceRow },
486
- ));
487
- }
488
- }
489
-
490
- const countMeasures = [reported, open, cnp, cwp];
491
- const amountMeasures = amountPairs.flatMap((pair) => [pair.paidMeasure, pair.incurredMeasure]);
492
- const paidMeasures = amountPairs.map((pair) => pair.paidMeasure);
493
- const comparableSnapshots = [...byTimeline.values()]
494
- .filter((timeline) => timeline.length > 1)
495
- .flat();
496
- const layerComparisons = (options.layers ?? []).flatMap((layer) => {
497
- if (!layer.broaderLayerId) return [];
498
- const broader = layerById.get(layer.broaderLayerId);
499
- return broader === undefined ? [] : [
500
- layer.paidMeasure,
501
- broader.paidMeasure,
502
- layer.incurredMeasure,
503
- broader.incurredMeasure,
504
- ];
505
- });
506
-
507
- const checks: DataCheck[] = [
508
- make("duplicate-aggregate-snapshot", duplicateSnapshots, options),
509
- make("duplicate-exposure-key", duplicateExposures, options),
510
- make("invalid-development-age", invalidAge, options),
511
- make("development-age-mismatch", ageMismatch, options),
512
- make("valuation-before-origin", valuationBefore, options),
513
- makeWithCoverage("count-reconciliation", countReconciliation, reviewMeasureCoverage("count-reconciliation", snapshots, countMeasures), "no snapshots contain the configured count measures", options),
514
- makeWithCoverage("closed-no-pay-exceeds-reported", cnpExceeds, reviewMeasureCoverage("closed-no-pay-exceeds-reported", snapshots, [reported, cnp]), `no snapshots contain ${reported} or ${cnp}`, options),
515
- amountPairs.length > 0
516
- ? makeWithCoverage("paid-exceeds-incurred", paidExceeds, reviewMeasureCoverage("paid-exceeds-incurred", snapshots, amountMeasures), "no snapshots contain the configured paid/incurred measures", options)
517
- : notEvaluated("paid-exceeds-incurred", "no amountPairs configured"),
518
- amountPairs.length > 0
519
- ? comparableSnapshots.length > 0
520
- ? makeWithCoverage("cumulative-paid-decreasing", paidDecreasing, reviewMeasureCoverage("cumulative-paid-decreasing", comparableSnapshots, paidMeasures), "no comparable snapshots contain the configured paid measures", options)
521
- : notEvaluated("cumulative-paid-decreasing", "no group/origin timeline has multiple snapshots")
522
- : notEvaluated("cumulative-paid-decreasing", "no amountPairs configured"),
523
- comparableSnapshots.length > 0
524
- ? makeWithCoverage("cumulative-reported-decreasing", reportedDecreasing, reviewMeasureCoverage("cumulative-reported-decreasing", comparableSnapshots, [reported]), `no comparable snapshots contain ${reported}`, options)
525
- : notEvaluated("cumulative-reported-decreasing", "no group/origin timeline has multiple snapshots"),
526
- comparableSnapshots.length > 0
527
- ? makeWithCoverage("closed-reopen-signal", closedDecreasing, reviewMeasureCoverage("closed-reopen-signal", comparableSnapshots, [cnp, cwp]), `no comparable snapshots contain ${cnp} or ${cwp}`, options)
528
- : notEvaluated("closed-reopen-signal", "no group/origin timeline has multiple snapshots"),
529
- (options.layers?.length ?? 0) > 0
530
- ? makeWithCoverage(
531
- "layer-order",
532
- layerOrder,
533
- reviewMeasureCoverage("layer-order", snapshots, layerComparisons),
534
- "no snapshots contain measures for a valid broader-layer relationship",
535
- options,
536
- )
537
- : notEvaluated("layer-order", "no layer definitions configured"),
538
- (options.controlTotals?.length ?? 0) > 0 ? make("layer-control-reconciliation", controlReconciliation, options) : notEvaluated("layer-control-reconciliation", "no control totals configured"),
539
- make("loss-without-exposure", lossWithoutExposure, options),
540
- make("exposure-without-loss", exposureWithoutLoss, options),
541
- make("zero-exposure", zeroExposure, options),
542
- make("incomplete-exposure", incompleteExposure, options),
543
- (options.groupingAssignments?.length ?? 0) > 0 ? make("inconsistent-group-mapping", inconsistentGrouping, options) : notEvaluated("inconsistent-group-mapping", "no grouping assignments configured"),
544
- (options.cachedFormulaProvenance?.length ?? 0) > 0 ? make("cached-formula-provenance", formulaProvenance, options) : notEvaluated("cached-formula-provenance", "no cached formula provenance supplied"),
545
- ];
546
- return summarizeDataChecks(checks);
547
- }