@actuarial-ts/core 0.5.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 (120) hide show
  1. package/README.md +65 -226
  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/casualtyDiagnostics.d.ts +19 -49
  6. package/dist/casualtyDiagnostics.d.ts.map +1 -1
  7. package/dist/casualtyDiagnostics.js +84 -116
  8. package/dist/casualtyDiagnostics.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +32 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -0
  11. package/dist/diagnosticAggregation.js +73 -0
  12. package/dist/diagnosticAggregation.js.map +1 -0
  13. package/dist/diagnosticDefinitions.d.ts +289 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -0
  15. package/dist/diagnosticDefinitions.js +1688 -0
  16. package/dist/diagnosticDefinitions.js.map +1 -0
  17. package/dist/diagnosticDerivations.d.ts +35 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -0
  19. package/dist/diagnosticDerivations.js +224 -0
  20. package/dist/diagnosticDerivations.js.map +1 -0
  21. package/dist/diagnosticExposure.d.ts +51 -0
  22. package/dist/diagnosticExposure.d.ts.map +1 -0
  23. package/dist/diagnosticExposure.js +273 -0
  24. package/dist/diagnosticExposure.js.map +1 -0
  25. package/dist/diagnosticExpressions.d.ts +56 -0
  26. package/dist/diagnosticExpressions.d.ts.map +1 -0
  27. package/dist/diagnosticExpressions.js +124 -0
  28. package/dist/diagnosticExpressions.js.map +1 -0
  29. package/dist/diagnosticFormulas.d.ts +198 -0
  30. package/dist/diagnosticFormulas.d.ts.map +1 -0
  31. package/dist/diagnosticFormulas.js +219 -0
  32. package/dist/diagnosticFormulas.js.map +1 -0
  33. package/dist/diagnosticIdentity.d.ts +251 -0
  34. package/dist/diagnosticIdentity.d.ts.map +1 -0
  35. package/dist/diagnosticIdentity.js +721 -0
  36. package/dist/diagnosticIdentity.js.map +1 -0
  37. package/dist/diagnosticOrdering.d.ts +8 -0
  38. package/dist/diagnosticOrdering.d.ts.map +1 -0
  39. package/dist/diagnosticOrdering.js +73 -0
  40. package/dist/diagnosticOrdering.js.map +1 -0
  41. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  42. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  43. package/dist/diagnosticPeriodAxis.js +69 -0
  44. package/dist/diagnosticPeriodAxis.js.map +1 -0
  45. package/dist/diagnosticPeriods.d.ts +15 -0
  46. package/dist/diagnosticPeriods.d.ts.map +1 -0
  47. package/dist/diagnosticPeriods.js +50 -0
  48. package/dist/diagnosticPeriods.js.map +1 -0
  49. package/dist/diagnosticPreparation.d.ts +131 -0
  50. package/dist/diagnosticPreparation.d.ts.map +1 -0
  51. package/dist/diagnosticPreparation.js +1539 -0
  52. package/dist/diagnosticPreparation.js.map +1 -0
  53. package/dist/diagnosticReview.d.ts +66 -0
  54. package/dist/diagnosticReview.d.ts.map +1 -0
  55. package/dist/diagnosticReview.js +490 -0
  56. package/dist/diagnosticReview.js.map +1 -0
  57. package/dist/diagnosticRules.d.ts +32 -0
  58. package/dist/diagnosticRules.d.ts.map +1 -0
  59. package/dist/diagnosticRules.js +32 -0
  60. package/dist/diagnosticRules.js.map +1 -0
  61. package/dist/diagnosticRunner.d.ts +86 -0
  62. package/dist/diagnosticRunner.d.ts.map +1 -0
  63. package/dist/diagnosticRunner.js +703 -0
  64. package/dist/diagnosticRunner.js.map +1 -0
  65. package/dist/diagnosticRuntime.d.ts +26 -0
  66. package/dist/diagnosticRuntime.d.ts.map +1 -0
  67. package/dist/diagnosticRuntime.js +243 -0
  68. package/dist/diagnosticRuntime.js.map +1 -0
  69. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  70. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  71. package/dist/diagnosticSourceOrdering.js +38 -0
  72. package/dist/diagnosticSourceOrdering.js.map +1 -0
  73. package/dist/index.d.ts +28 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +16 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/mack.d.ts +4 -0
  78. package/dist/mack.d.ts.map +1 -1
  79. package/dist/mack.js +32 -4
  80. package/dist/mack.js.map +1 -1
  81. package/dist/triangle.d.ts.map +1 -1
  82. package/dist/triangle.js +59 -9
  83. package/dist/triangle.js.map +1 -1
  84. package/dist/types.d.ts +21 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js +128 -0
  87. package/dist/types.js.map +1 -1
  88. package/dist/version.d.ts +3 -0
  89. package/dist/version.d.ts.map +1 -0
  90. package/dist/version.js +3 -0
  91. package/dist/version.js.map +1 -0
  92. package/package.json +4 -3
  93. package/src/capping.ts +109 -18
  94. package/src/casualtyDiagnostics.ts +79 -189
  95. package/src/diagnosticAggregation.ts +128 -0
  96. package/src/diagnosticDefinitions.ts +3624 -0
  97. package/src/diagnosticDerivations.ts +321 -0
  98. package/src/diagnosticExposure.ts +482 -0
  99. package/src/diagnosticExpressions.ts +255 -0
  100. package/src/diagnosticFormulas.ts +350 -0
  101. package/src/diagnosticIdentity.ts +1267 -0
  102. package/src/diagnosticOrdering.ts +106 -0
  103. package/src/diagnosticPeriodAxis.ts +92 -0
  104. package/src/diagnosticPeriods.ts +102 -0
  105. package/src/diagnosticPreparation.ts +2475 -0
  106. package/src/diagnosticReview.ts +897 -0
  107. package/src/diagnosticRules.ts +69 -0
  108. package/src/diagnosticRunner.ts +1114 -0
  109. package/src/diagnosticRuntime.ts +264 -0
  110. package/src/diagnosticSourceOrdering.ts +50 -0
  111. package/src/index.ts +184 -1
  112. package/src/mack.ts +72 -9
  113. package/src/triangle.ts +103 -13
  114. package/src/types.ts +196 -0
  115. package/src/version.ts +2 -0
  116. package/dist/metricDiagnostics.d.ts +0 -212
  117. package/dist/metricDiagnostics.d.ts.map +0 -1
  118. package/dist/metricDiagnostics.js +0 -627
  119. package/dist/metricDiagnostics.js.map +0 -1
  120. package/src/metricDiagnostics.ts +0 -876
@@ -0,0 +1,1539 @@
1
+ import { canonicalJson, fnv1a64 } from "./canonical.js";
2
+ import { normalizeDiagnosticsFilterIdentity, projectDiagnosticIdentity, } from "./diagnosticIdentity.js";
3
+ import { compareDiagnosticBlockers, compareDiagnosticContributions, compareDiagnosticFindings, } from "./diagnosticOrdering.js";
4
+ import { assertCompiledDiagnosticDefinition, getCompiledDiagnosticDefinitionInternals, } from "./diagnosticDefinitions.js";
5
+ import { auditedDiagnosticContribution, finalizeDiagnosticContributions, } from "./diagnosticAggregation.js";
6
+ import { deriveDiagnosticClaimMeasuresWithAudit, } from "./diagnosticDerivations.js";
7
+ import { compareDiagnosticPeriods, normalizeDiagnosticPeriod, } from "./diagnosticPeriods.js";
8
+ import { auditDiagnosticNumber, reconcileDiagnosticExposures, } from "./diagnosticExposure.js";
9
+ import { DiagnosticValidationError, } from "./types.js";
10
+ import { normalizeDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
11
+ import { isDiagnosticPlainRecord, isDiagnosticToken, } from "./diagnosticRuntime.js";
12
+ const authentic = new WeakSet();
13
+ const identities = new WeakMap();
14
+ const STRUCTURAL = {
15
+ "duplicate-loss-record-id": ["Loss record identity is duplicated", "fail"],
16
+ "duplicate-claim-snapshot": ["Claim snapshot identity is duplicated", "fail"],
17
+ "claim-identity-conflict": [
18
+ "Claim identity has conflicting source group or origin",
19
+ "fail",
20
+ ],
21
+ "duplicate-aggregate-snapshot": [
22
+ "Aggregate source-cell identity is duplicated",
23
+ "fail",
24
+ ],
25
+ "duplicate-exposure-identity": ["Exposure identity is duplicated", "fail"],
26
+ "conflicting-exposure-identity": [
27
+ "Exposure identity has conflicting observations",
28
+ "fail",
29
+ ],
30
+ "unknown-origin-period": [
31
+ "Origin period is not declared by the period axis",
32
+ "fail",
33
+ ],
34
+ "unknown-valuation-period": [
35
+ "Valuation period is not declared by the period axis",
36
+ "fail",
37
+ ],
38
+ "valuation-before-origin": ["Valuation precedes origin", "fail"],
39
+ "unsafe-development-age": [
40
+ "Derived development age is not a nonnegative safe integer",
41
+ "fail",
42
+ ],
43
+ "undeclared-loss-measure": [
44
+ "Loss input contains an undeclared measure",
45
+ "fail",
46
+ ],
47
+ "wrong-source-loss-measure": [
48
+ "Loss input contains a measure that is not raw loss input",
49
+ "fail",
50
+ ],
51
+ "undeclared-exposure-measure": [
52
+ "Exposure input contains an undeclared measure",
53
+ "fail",
54
+ ],
55
+ "wrong-source-exposure-measure": [
56
+ "Exposure input names a measure that is not exposure input",
57
+ "fail",
58
+ ],
59
+ "incomplete-loss-record": ["Loss record is marked incomplete", "fail"],
60
+ "missing-exposure-value": [
61
+ "Exposure observation has a missing value",
62
+ "fail",
63
+ ],
64
+ "incomplete-exposure": ["Exposure observation is marked incomplete", "fail"],
65
+ "non-finite-exposure": ["Exposure observation is non-finite", "fail"],
66
+ "loss-without-exposure": [
67
+ "Loss source cell has no matching required exposure",
68
+ "warning",
69
+ ],
70
+ "exposure-without-loss": [
71
+ "Exposure has no matching retained loss source cell",
72
+ "warning",
73
+ ],
74
+ "missing-expected-cell": ["Expected source cell is absent", "fail"],
75
+ };
76
+ function codeUnit(left, right) {
77
+ return left < right ? -1 : left > right ? 1 : 0;
78
+ }
79
+ function own(record, key) {
80
+ return Object.prototype.hasOwnProperty.call(record, key)
81
+ ? record[key]
82
+ : undefined;
83
+ }
84
+ function deepFreeze(value, seen = new WeakSet()) {
85
+ if (value === null || typeof value !== "object" || seen.has(value))
86
+ return value;
87
+ seen.add(value);
88
+ for (const child of Object.values(value))
89
+ deepFreeze(child, seen);
90
+ return Object.freeze(value);
91
+ }
92
+ function uniqueSorted(values) {
93
+ return [...new Set(values)].sort(codeUnit);
94
+ }
95
+ function normalizeSources(values) {
96
+ return normalizeDiagnosticSourceLocations(values);
97
+ }
98
+ function structuralFinding(code, context = {}) {
99
+ const [message, severity] = STRUCTURAL[code];
100
+ return {
101
+ code,
102
+ message,
103
+ severity,
104
+ category: "structural",
105
+ ...context,
106
+ sources: normalizeSources(context.sources ?? []),
107
+ };
108
+ }
109
+ function aggregationFinding(code, context) {
110
+ const catalog = {
111
+ "diagnostic-measure-missing": ["Measure input is missing", "warning"],
112
+ "diagnostic-measure-imputed-zero": [
113
+ "Missing measure input was imputed as zero",
114
+ "info",
115
+ ],
116
+ "diagnostic-measure-non-finite": ["Measure input is non-finite", "fail"],
117
+ "diagnostic-measure-overflow": ["Measure aggregation overflowed", "fail"],
118
+ };
119
+ const [message, severity] = catalog[code];
120
+ return {
121
+ code,
122
+ message,
123
+ severity,
124
+ category: "aggregation",
125
+ ...context,
126
+ sources: normalizeSources(context.sources ?? []),
127
+ };
128
+ }
129
+ function mergeFindings(values) {
130
+ const merged = new Map();
131
+ for (const value of values) {
132
+ const key = canonicalJson({ ...value, sources: [] });
133
+ const previous = merged.get(key);
134
+ merged.set(key, previous === undefined
135
+ ? { ...value, sources: normalizeSources(value.sources) }
136
+ : {
137
+ ...previous,
138
+ sources: normalizeSources([...previous.sources, ...value.sources]),
139
+ });
140
+ }
141
+ return [...merged.values()].sort(compareDiagnosticFindings);
142
+ }
143
+ function snapshotLoss(row) {
144
+ const measures = Object.fromEntries(Object.keys(row.measures)
145
+ .sort(codeUnit)
146
+ .map((measureId) => [
147
+ measureId,
148
+ auditDiagnosticNumber(row.measures[measureId] ?? null),
149
+ ]));
150
+ return {
151
+ recordId: row.recordId,
152
+ rowType: row.rowType,
153
+ claimId: row.rowType === "claim" ? row.claimId : null,
154
+ sourceGroup: row.sourceGroup,
155
+ origin: row.origin,
156
+ valuation: row.valuation,
157
+ complete: row.complete,
158
+ measures,
159
+ source: row.source === undefined ? null : { ...row.source },
160
+ };
161
+ }
162
+ function snapshotExposure(row) {
163
+ return {
164
+ key: row.key,
165
+ sourceGroup: row.sourceGroup,
166
+ origin: row.origin,
167
+ valuation: row.valuation ?? null,
168
+ measureId: row.measureId,
169
+ value: auditDiagnosticNumber(row.value),
170
+ complete: row.complete,
171
+ source: row.source === undefined ? null : { ...row.source },
172
+ };
173
+ }
174
+ function normalizeConfiguredPeriod(definition, side, label, path, issues) {
175
+ try {
176
+ return normalizeDiagnosticPeriod(definition, side, label);
177
+ }
178
+ catch {
179
+ issues.push({
180
+ domain: "configuration",
181
+ code: "invalid-period",
182
+ path,
183
+ message: `Unknown ${side} period ${JSON.stringify(label)}`,
184
+ });
185
+ return null;
186
+ }
187
+ }
188
+ function normalizeFilter(definition, input, knownSourceGroups, issues) {
189
+ if (input === undefined)
190
+ return {
191
+ value: null,
192
+ originCoordinates: new Map(),
193
+ valuationCoordinates: new Map(),
194
+ };
195
+ const checkTokens = (values, path) => {
196
+ if (values === undefined)
197
+ return undefined;
198
+ values.forEach((value, index) => {
199
+ if (typeof value !== "string" || value.length === 0)
200
+ issues.push({
201
+ domain: "configuration",
202
+ code: "invalid-string",
203
+ path: `${path}[${index}]`,
204
+ message: "Filter values must be nonempty strings",
205
+ });
206
+ });
207
+ return uniqueSorted(values.filter((value) => typeof value === "string" && value.length > 0));
208
+ };
209
+ const normalizeList = (values, side, path) => {
210
+ const checked = checkTokens(values, path);
211
+ if (checked === undefined)
212
+ return { labels: undefined, coordinates: new Map() };
213
+ const normalized = checked
214
+ .map((label, index) => normalizeConfiguredPeriod(definition, side, label, `${path}[${index}]`, issues))
215
+ .filter((value) => value !== null);
216
+ return {
217
+ labels: uniqueSorted(normalized.map((value) => value.label)),
218
+ coordinates: new Map(normalized.map((value) => [value.label, value.coordinate])),
219
+ };
220
+ };
221
+ const origins = normalizeList(input.origins, "origin", "$.filter.origins");
222
+ const valuations = normalizeList(input.valuations, "valuation", "$.filter.valuations");
223
+ const originFrom = input.originFrom === undefined
224
+ ? null
225
+ : normalizeConfiguredPeriod(definition, "origin", input.originFrom, "$.filter.originFrom", issues);
226
+ const originThrough = input.originThrough === undefined
227
+ ? null
228
+ : normalizeConfiguredPeriod(definition, "origin", input.originThrough, "$.filter.originThrough", issues);
229
+ const valuationFrom = input.valuationFrom === undefined
230
+ ? null
231
+ : normalizeConfiguredPeriod(definition, "valuation", input.valuationFrom, "$.filter.valuationFrom", issues);
232
+ const valuationThrough = input.valuationThrough === undefined
233
+ ? null
234
+ : normalizeConfiguredPeriod(definition, "valuation", input.valuationThrough, "$.filter.valuationThrough", issues);
235
+ if (originFrom &&
236
+ originThrough &&
237
+ compareDiagnosticPeriods(originFrom, originThrough) > 0)
238
+ issues.push({
239
+ domain: "configuration",
240
+ code: "invalid-configuration",
241
+ path: "$.filter",
242
+ message: "Origin range start exceeds end",
243
+ });
244
+ if (valuationFrom &&
245
+ valuationThrough &&
246
+ compareDiagnosticPeriods(valuationFrom, valuationThrough) > 0)
247
+ issues.push({
248
+ domain: "configuration",
249
+ code: "invalid-configuration",
250
+ path: "$.filter",
251
+ message: "Valuation range start exceeds end",
252
+ });
253
+ for (const [name, bound] of [
254
+ ["minDevelopmentAge", input.minDevelopmentAge],
255
+ ["maxDevelopmentAge", input.maxDevelopmentAge],
256
+ ]) {
257
+ if (bound !== undefined && (!Number.isSafeInteger(bound) || bound < 0))
258
+ issues.push({
259
+ domain: "configuration",
260
+ code: "invalid-configuration",
261
+ path: `$.filter.${name}`,
262
+ message: "Development-age bounds must be nonnegative safe integers",
263
+ });
264
+ }
265
+ if (input.minDevelopmentAge !== undefined &&
266
+ input.maxDevelopmentAge !== undefined &&
267
+ input.minDevelopmentAge > input.maxDevelopmentAge)
268
+ issues.push({
269
+ domain: "configuration",
270
+ code: "invalid-configuration",
271
+ path: "$.filter",
272
+ message: "Minimum development age exceeds maximum",
273
+ });
274
+ const sourceGroups = checkTokens(input.sourceGroups, "$.filter.sourceGroups");
275
+ const outputGroups = checkTokens(input.outputGroups, "$.filter.outputGroups");
276
+ const instanceIds = checkTokens(input.instanceIds, "$.filter.instanceIds");
277
+ const validInstances = new Set(definition.definition.instances.map((instance) => instance.id));
278
+ for (const [index, sourceGroup] of (sourceGroups ?? []).entries()) {
279
+ if (!knownSourceGroups.has(sourceGroup))
280
+ issues.push({
281
+ domain: "configuration",
282
+ code: "unknown-reference",
283
+ path: `$.filter.sourceGroups[${index}]`,
284
+ message: `Unknown source group ${sourceGroup}`,
285
+ });
286
+ }
287
+ for (const [index, instanceId] of (instanceIds ?? []).entries())
288
+ if (!validInstances.has(instanceId))
289
+ issues.push({
290
+ domain: "configuration",
291
+ code: "unknown-reference",
292
+ path: `$.filter.instanceIds[${index}]`,
293
+ message: `Unknown metric instance ${instanceId}`,
294
+ });
295
+ const value = {
296
+ ...(sourceGroups === undefined ? {} : { sourceGroups }),
297
+ ...(outputGroups === undefined ? {} : { outputGroups }),
298
+ ...(origins.labels === undefined ? {} : { origins: origins.labels }),
299
+ ...(originFrom === null ? {} : { originFrom: originFrom.label }),
300
+ ...(originThrough === null ? {} : { originThrough: originThrough.label }),
301
+ ...(valuations.labels === undefined
302
+ ? {}
303
+ : { valuations: valuations.labels }),
304
+ ...(valuationFrom === null ? {} : { valuationFrom: valuationFrom.label }),
305
+ ...(valuationThrough === null
306
+ ? {}
307
+ : { valuationThrough: valuationThrough.label }),
308
+ ...(input.minDevelopmentAge === undefined
309
+ ? {}
310
+ : { minDevelopmentAge: input.minDevelopmentAge }),
311
+ ...(input.maxDevelopmentAge === undefined
312
+ ? {}
313
+ : { maxDevelopmentAge: input.maxDevelopmentAge }),
314
+ ...(instanceIds === undefined ? {} : { instanceIds }),
315
+ };
316
+ return {
317
+ value,
318
+ originCoordinates: new Map([
319
+ ...origins.coordinates,
320
+ ...(originFrom
321
+ ? [[originFrom.label, originFrom.coordinate]]
322
+ : []),
323
+ ...(originThrough
324
+ ? [[originThrough.label, originThrough.coordinate]]
325
+ : []),
326
+ ]),
327
+ valuationCoordinates: new Map([
328
+ ...valuations.coordinates,
329
+ ...(valuationFrom
330
+ ? [[valuationFrom.label, valuationFrom.coordinate]]
331
+ : []),
332
+ ...(valuationThrough
333
+ ? [[valuationThrough.label, valuationThrough.coordinate]]
334
+ : []),
335
+ ]),
336
+ };
337
+ }
338
+ function normalizeCutoffs(definition, input, knownSourceGroups, issues) {
339
+ const groups = new Set();
340
+ const normalized = [];
341
+ input.forEach((cutoff, index) => {
342
+ if (!knownSourceGroups.has(cutoff.sourceGroup))
343
+ issues.push({
344
+ domain: "configuration",
345
+ code: "unknown-reference",
346
+ path: `$.completePeriodCutoffs[${index}].sourceGroup`,
347
+ message: `Unknown source group ${cutoff.sourceGroup}`,
348
+ });
349
+ if (groups.has(cutoff.sourceGroup))
350
+ issues.push({
351
+ domain: "configuration",
352
+ code: "duplicate-id",
353
+ path: `$.completePeriodCutoffs[${index}].sourceGroup`,
354
+ message: "Complete-period cutoff source group is duplicated",
355
+ });
356
+ groups.add(cutoff.sourceGroup);
357
+ const origin = cutoff.originThrough === null
358
+ ? null
359
+ : normalizeConfiguredPeriod(definition, "origin", cutoff.originThrough, `$.completePeriodCutoffs[${index}].originThrough`, issues);
360
+ const valuation = cutoff.valuationThrough === null
361
+ ? null
362
+ : normalizeConfiguredPeriod(definition, "valuation", cutoff.valuationThrough, `$.completePeriodCutoffs[${index}].valuationThrough`, issues);
363
+ normalized.push({
364
+ value: {
365
+ sourceGroup: cutoff.sourceGroup,
366
+ originThrough: origin?.label ?? null,
367
+ valuationThrough: valuation?.label ?? null,
368
+ },
369
+ originThrough: origin,
370
+ valuationThrough: valuation,
371
+ });
372
+ });
373
+ return normalized.sort((left, right) => codeUnit(left.value.sourceGroup, right.value.sourceGroup));
374
+ }
375
+ function coordinateFor(definition, originLabel, valuationLabel) {
376
+ let origin;
377
+ let valuation;
378
+ try {
379
+ origin = normalizeDiagnosticPeriod(definition, "origin", originLabel);
380
+ }
381
+ catch {
382
+ return { coordinate: null, issue: "unknown-origin-period" };
383
+ }
384
+ try {
385
+ valuation = normalizeDiagnosticPeriod(definition, "valuation", valuationLabel);
386
+ }
387
+ catch {
388
+ return { coordinate: null, issue: "unknown-valuation-period" };
389
+ }
390
+ const developmentAge = valuation.coordinate -
391
+ origin.coordinate +
392
+ definition.definition.periodAxis.ageOffset;
393
+ if (!Number.isSafeInteger(developmentAge))
394
+ return { coordinate: null, issue: "unsafe-development-age" };
395
+ if (developmentAge < 0)
396
+ return { coordinate: null, issue: "valuation-before-origin" };
397
+ return {
398
+ coordinate: {
399
+ origin,
400
+ valuation,
401
+ developmentAge,
402
+ ageUnit: definition.definition.periodAxis.ageUnit,
403
+ },
404
+ issue: null,
405
+ };
406
+ }
407
+ function cellKey(sourceGroup, origin, valuation) {
408
+ return canonicalJson([sourceGroup, origin, valuation]);
409
+ }
410
+ function parseCellKey(key) {
411
+ return JSON.parse(key);
412
+ }
413
+ function coordinateContext(candidate) {
414
+ if (candidate.coordinate === null)
415
+ return {};
416
+ return {
417
+ sourceGroup: candidate.row.sourceGroup,
418
+ origin: candidate.coordinate.origin.label,
419
+ valuation: candidate.coordinate.valuation.label,
420
+ developmentAge: candidate.coordinate.developmentAge,
421
+ ageUnit: candidate.coordinate.ageUnit,
422
+ };
423
+ }
424
+ function selectedBySource(filter, sourceGroup) {
425
+ return (filter?.sourceGroups === undefined ||
426
+ filter.sourceGroups.includes(sourceGroup));
427
+ }
428
+ function selectedByOrigin(filter, origin, coordinates) {
429
+ const from = filter?.originFrom === undefined
430
+ ? null
431
+ : coordinates.get(filter.originFrom);
432
+ const through = filter?.originThrough === undefined
433
+ ? null
434
+ : coordinates.get(filter.originThrough);
435
+ return ((filter?.origins === undefined || filter.origins.includes(origin.label)) &&
436
+ (from === null || origin.coordinate >= from) &&
437
+ (through === null || origin.coordinate <= through));
438
+ }
439
+ function selectedByValuation(filter, valuation, coordinates) {
440
+ const from = filter?.valuationFrom === undefined
441
+ ? null
442
+ : coordinates.get(filter.valuationFrom);
443
+ const through = filter?.valuationThrough === undefined
444
+ ? null
445
+ : coordinates.get(filter.valuationThrough);
446
+ return ((filter?.valuations === undefined ||
447
+ filter.valuations.includes(valuation.label)) &&
448
+ (from === null || valuation.coordinate >= from) &&
449
+ (through === null || valuation.coordinate <= through));
450
+ }
451
+ function selectedByAge(filter, age) {
452
+ return ((filter?.minDevelopmentAge === undefined ||
453
+ age >= filter.minDevelopmentAge) &&
454
+ (filter?.maxDevelopmentAge === undefined || age <= filter.maxDevelopmentAge));
455
+ }
456
+ function beyondCutoff(candidate, cutoffByGroup) {
457
+ const cutoff = cutoffByGroup.get(candidate.row.sourceGroup);
458
+ return (cutoff !== undefined &&
459
+ ((cutoff.originThrough !== null &&
460
+ compareDiagnosticPeriods(candidate.coordinate.origin, cutoff.originThrough) > 0) ||
461
+ (cutoff.valuationThrough !== null &&
462
+ compareDiagnosticPeriods(candidate.coordinate.valuation, cutoff.valuationThrough) > 0)));
463
+ }
464
+ function addToMap(map, key, value) {
465
+ const values = map.get(key) ?? [];
466
+ values.push(value);
467
+ map.set(key, values);
468
+ }
469
+ function commonCoordinate(candidates) {
470
+ const keys = uniqueSorted(candidates.flatMap((candidate) => candidate.coordinate === null
471
+ ? []
472
+ : [
473
+ cellKey(candidate.row.sourceGroup, candidate.coordinate.origin.label, candidate.coordinate.valuation.label),
474
+ ]));
475
+ return keys.length === 1 ? coordinateContext(candidates[0]) : {};
476
+ }
477
+ function exposureIdentity(candidate) {
478
+ return canonicalJson(candidate.timing === "valuation-specific"
479
+ ? [
480
+ candidate.row.measureId,
481
+ candidate.row.key,
482
+ candidate.row.valuation ?? null,
483
+ ]
484
+ : [candidate.row.measureId, candidate.row.key]);
485
+ }
486
+ function exposureCoordinateContext(candidates) {
487
+ const coherentSource = uniqueSorted(candidates.map((candidate) => candidate.row.sourceGroup));
488
+ const coherentOrigin = uniqueSorted(candidates.flatMap((candidate) => candidate.origin ? [candidate.origin.label] : []));
489
+ const coherentValuation = uniqueSorted(candidates.flatMap((candidate) => candidate.valuation ? [candidate.valuation.label] : []));
490
+ if (coherentSource.length !== 1 || coherentOrigin.length !== 1)
491
+ return {};
492
+ const first = candidates[0];
493
+ if (first.timing === "valuation-specific" &&
494
+ coherentValuation.length === 1 &&
495
+ first.developmentAge !== null &&
496
+ first.ageUnit !== null) {
497
+ return {
498
+ sourceGroup: coherentSource[0],
499
+ origin: coherentOrigin[0],
500
+ valuation: coherentValuation[0],
501
+ developmentAge: first.developmentAge,
502
+ ageUnit: first.ageUnit,
503
+ };
504
+ }
505
+ return { sourceGroup: coherentSource[0], origin: coherentOrigin[0] };
506
+ }
507
+ function auditSort(left, right) {
508
+ const rank = { loss: 0, exposure: 1, "expected-cell": 2 };
509
+ const disposition = {
510
+ invalid: 0,
511
+ "complete-period-cutoff": 1,
512
+ filter: 2,
513
+ retained: 3,
514
+ };
515
+ return (rank[left.kind] - rank[right.kind] ||
516
+ codeUnit(canonicalJson(left.record), canonicalJson(right.record)) ||
517
+ disposition[left.disposition] - disposition[right.disposition]);
518
+ }
519
+ function isPlainRecord(value) {
520
+ return isDiagnosticPlainRecord(value);
521
+ }
522
+ function preparationBoundaryIssues(value) {
523
+ const issues = [];
524
+ if (!isPlainRecord(value))
525
+ return [
526
+ {
527
+ domain: "input",
528
+ code: "invalid-type",
529
+ path: "$",
530
+ message: "Preparation input must be an object",
531
+ },
532
+ ];
533
+ const issue = (domain, code, path, message) => issues.push({ domain, code, path, message });
534
+ const token = (item, path, domain = "input") => {
535
+ if (typeof item !== "string")
536
+ issue(domain, "invalid-type", path, "Expected a string");
537
+ else if (!isDiagnosticToken(item))
538
+ issue(domain, "invalid-string", path, "Expected a nonempty token with valid Unicode and no U+0000");
539
+ };
540
+ const exactKeys = (item, allowed, path, domain) => {
541
+ for (const key of Object.keys(item)) {
542
+ const keyPath = `${path}.${key}`;
543
+ if (!allowed.includes(key))
544
+ issue(domain, "unknown-key", keyPath, `Unknown key ${key}`);
545
+ else if (item[key] === undefined)
546
+ issue(domain, "invalid-type", keyPath, "Explicit undefined is not allowed");
547
+ }
548
+ };
549
+ const source = (item, path) => {
550
+ if (!isPlainRecord(item)) {
551
+ issue("input", "invalid-type", path, "Source location must be an object");
552
+ return;
553
+ }
554
+ exactKeys(item, ["artifactId", "sourceFile", "sourceSheet", "sourceRow", "sourceCell"], path, "input");
555
+ token(item.artifactId, `${path}.artifactId`);
556
+ for (const key of ["sourceFile", "sourceSheet", "sourceCell"])
557
+ if (item[key] !== undefined)
558
+ token(item[key], `${path}.${key}`);
559
+ if (item.sourceRow !== undefined &&
560
+ (typeof item.sourceRow !== "number" ||
561
+ !Number.isSafeInteger(item.sourceRow) ||
562
+ item.sourceRow < 0))
563
+ issue("input", "invalid-number", `${path}.sourceRow`, "Source row must be a nonnegative safe integer");
564
+ };
565
+ const array = (item, path, domain) => {
566
+ if (!Array.isArray(item)) {
567
+ issue(domain, "invalid-type", path, "Expected an array");
568
+ return [];
569
+ }
570
+ return item;
571
+ };
572
+ exactKeys(value, [
573
+ "definition",
574
+ "losses",
575
+ "exposures",
576
+ "filter",
577
+ "completePeriodCutoffs",
578
+ "expectedCells",
579
+ ], "$", "configuration");
580
+ for (const [index, raw] of array(value.losses, "$.losses", "input").entries()) {
581
+ const path = `$.losses[${index}]`;
582
+ if (!isPlainRecord(raw)) {
583
+ issue("input", "invalid-type", path, "Loss record must be an object");
584
+ continue;
585
+ }
586
+ exactKeys(raw, [
587
+ "rowType",
588
+ "recordId",
589
+ "claimId",
590
+ "sourceGroup",
591
+ "origin",
592
+ "valuation",
593
+ "complete",
594
+ "source",
595
+ "measures",
596
+ ], path, "input");
597
+ if (raw.rowType !== "claim" && raw.rowType !== "aggregate")
598
+ issue("input", "invalid-type", `${path}.rowType`, "Loss row type must be claim or aggregate");
599
+ token(raw.recordId, `${path}.recordId`);
600
+ token(raw.sourceGroup, `${path}.sourceGroup`);
601
+ token(raw.origin, `${path}.origin`);
602
+ token(raw.valuation, `${path}.valuation`);
603
+ if (raw.rowType === "claim")
604
+ token(raw.claimId, `${path}.claimId`);
605
+ else if (raw.claimId !== undefined)
606
+ issue("input", "unknown-key", `${path}.claimId`, "Aggregate loss rows cannot contain claimId");
607
+ if (typeof raw.complete !== "boolean")
608
+ issue("input", "invalid-type", `${path}.complete`, "Loss completeness must be boolean");
609
+ if (raw.source !== undefined)
610
+ source(raw.source, `${path}.source`);
611
+ if (!isPlainRecord(raw.measures))
612
+ issue("input", "invalid-type", `${path}.measures`, "Measures must be an object");
613
+ else
614
+ for (const [measureId, amount] of Object.entries(raw.measures)) {
615
+ token(measureId, `${path}.measures`);
616
+ if (amount !== null && typeof amount !== "number")
617
+ issue("input", "invalid-type", `${path}.measures.${measureId}`, "Measure value must be a number or null");
618
+ }
619
+ }
620
+ for (const [index, raw] of array(value.exposures, "$.exposures", "input").entries()) {
621
+ const path = `$.exposures[${index}]`;
622
+ if (!isPlainRecord(raw)) {
623
+ issue("input", "invalid-type", path, "Exposure observation must be an object");
624
+ continue;
625
+ }
626
+ exactKeys(raw, [
627
+ "key",
628
+ "sourceGroup",
629
+ "origin",
630
+ "valuation",
631
+ "measureId",
632
+ "value",
633
+ "complete",
634
+ "source",
635
+ ], path, "input");
636
+ token(raw.key, `${path}.key`);
637
+ token(raw.sourceGroup, `${path}.sourceGroup`);
638
+ token(raw.origin, `${path}.origin`);
639
+ token(raw.measureId, `${path}.measureId`);
640
+ if (raw.valuation !== undefined)
641
+ token(raw.valuation, `${path}.valuation`);
642
+ if (raw.value !== null && typeof raw.value !== "number")
643
+ issue("input", "invalid-type", `${path}.value`, "Exposure value must be a number or null");
644
+ if (typeof raw.complete !== "boolean")
645
+ issue("input", "invalid-type", `${path}.complete`, "Exposure completeness must be boolean");
646
+ if (raw.source !== undefined)
647
+ source(raw.source, `${path}.source`);
648
+ }
649
+ if (value.filter !== undefined && !isPlainRecord(value.filter))
650
+ issue("configuration", "invalid-type", "$.filter", "Filter must be an object");
651
+ if (isPlainRecord(value.filter)) {
652
+ const filter = value.filter;
653
+ const keys = [
654
+ "sourceGroups",
655
+ "outputGroups",
656
+ "origins",
657
+ "originFrom",
658
+ "originThrough",
659
+ "valuations",
660
+ "valuationFrom",
661
+ "valuationThrough",
662
+ "minDevelopmentAge",
663
+ "maxDevelopmentAge",
664
+ "instanceIds",
665
+ ];
666
+ exactKeys(filter, keys, "$.filter", "configuration");
667
+ for (const key of [
668
+ "sourceGroups",
669
+ "outputGroups",
670
+ "origins",
671
+ "valuations",
672
+ "instanceIds",
673
+ ])
674
+ if (filter[key] !== undefined)
675
+ array(filter[key], `$.filter.${key}`, "configuration").forEach((item, index) => token(item, `$.filter.${key}[${index}]`, "configuration"));
676
+ for (const key of [
677
+ "originFrom",
678
+ "originThrough",
679
+ "valuationFrom",
680
+ "valuationThrough",
681
+ ])
682
+ if (filter[key] !== undefined)
683
+ token(filter[key], `$.filter.${key}`, "configuration");
684
+ }
685
+ if (value.completePeriodCutoffs !== undefined)
686
+ for (const [index, raw] of array(value.completePeriodCutoffs, "$.completePeriodCutoffs", "configuration").entries()) {
687
+ const path = `$.completePeriodCutoffs[${index}]`;
688
+ if (!isPlainRecord(raw)) {
689
+ issue("configuration", "invalid-type", path, "Cutoff must be an object");
690
+ continue;
691
+ }
692
+ exactKeys(raw, ["sourceGroup", "originThrough", "valuationThrough"], path, "configuration");
693
+ token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
694
+ for (const key of ["originThrough", "valuationThrough"])
695
+ if (raw[key] !== null)
696
+ token(raw[key], `${path}.${key}`, "configuration");
697
+ }
698
+ if (value.expectedCells !== undefined)
699
+ for (const [index, raw] of array(value.expectedCells, "$.expectedCells", "configuration").entries()) {
700
+ const path = `$.expectedCells[${index}]`;
701
+ if (!isPlainRecord(raw)) {
702
+ issue("configuration", "invalid-type", path, "Expected cell must be an object");
703
+ continue;
704
+ }
705
+ exactKeys(raw, ["sourceGroup", "origin", "valuation", "source"], path, "configuration");
706
+ token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
707
+ token(raw.origin, `${path}.origin`, "configuration");
708
+ token(raw.valuation, `${path}.valuation`, "configuration");
709
+ if (raw.source !== undefined)
710
+ source(raw.source, `${path}.source`);
711
+ }
712
+ return issues;
713
+ }
714
+ export function prepareDiagnosticData(input) {
715
+ assertCompiledDiagnosticDefinition(input?.definition);
716
+ const boundaryIssues = preparationBoundaryIssues(input);
717
+ if (boundaryIssues.length > 0)
718
+ throw new DiagnosticValidationError(boundaryIssues);
719
+ const definition = input.definition;
720
+ const internals = getCompiledDiagnosticDefinitionInternals(definition);
721
+ const issues = [];
722
+ const knownSourceGroups = new Set([
723
+ ...input.losses.map((row) => row.sourceGroup),
724
+ ...input.exposures.map((row) => row.sourceGroup),
725
+ ...(input.expectedCells ?? []).map((row) => row.sourceGroup),
726
+ ]);
727
+ const normalizedFilter = normalizeFilter(definition, input.filter, knownSourceGroups, issues);
728
+ const normalizedCutoffs = normalizeCutoffs(definition, input.completePeriodCutoffs ?? [], knownSourceGroups, issues);
729
+ const cutoffByGroup = new Map(normalizedCutoffs.map((cutoff) => [cutoff.value.sourceGroup, cutoff]));
730
+ input.losses.forEach((row, index) => {
731
+ if (row.rowType !== definition.definition.lossRowGrain)
732
+ issues.push({
733
+ domain: "input",
734
+ code: "invalid-input-relationship",
735
+ path: `$.losses[${index}].rowType`,
736
+ message: "Loss row type does not match definition grain",
737
+ });
738
+ });
739
+ input.exposures.forEach((row, index) => {
740
+ const measure = internals.measuresById.get(row.measureId);
741
+ if (measure?.exposureTiming === "valuation-specific" &&
742
+ row.valuation === undefined)
743
+ issues.push({
744
+ domain: "input",
745
+ code: "missing-required",
746
+ path: `$.exposures[${index}].valuation`,
747
+ message: "Valuation-specific exposure requires valuation",
748
+ });
749
+ });
750
+ const expectedCandidates = [];
751
+ const expectedKeys = new Set();
752
+ (input.expectedCells ?? []).forEach((row, index) => {
753
+ const origin = normalizeConfiguredPeriod(definition, "origin", row.origin, `$.expectedCells[${index}].origin`, issues);
754
+ const valuation = normalizeConfiguredPeriod(definition, "valuation", row.valuation, `$.expectedCells[${index}].valuation`, issues);
755
+ if (!origin || !valuation)
756
+ return;
757
+ const developmentAge = valuation.coordinate -
758
+ origin.coordinate +
759
+ definition.definition.periodAxis.ageOffset;
760
+ if (!Number.isSafeInteger(developmentAge) || developmentAge < 0) {
761
+ issues.push({
762
+ domain: "configuration",
763
+ code: "invalid-period",
764
+ path: `$.expectedCells[${index}].valuation`,
765
+ message: "Expected-cell valuation precedes origin or produces an unsafe development age",
766
+ });
767
+ return;
768
+ }
769
+ const normalized = {
770
+ ...row,
771
+ origin: origin.label,
772
+ valuation: valuation.label,
773
+ };
774
+ const key = cellKey(row.sourceGroup, origin.label, valuation.label);
775
+ if (expectedKeys.has(key))
776
+ issues.push({
777
+ domain: "configuration",
778
+ code: "duplicate-id",
779
+ path: `$.expectedCells[${index}]`,
780
+ message: "Expected source cell is duplicated after period normalization",
781
+ });
782
+ expectedKeys.add(key);
783
+ expectedCandidates.push({
784
+ row: normalized,
785
+ snapshot: {
786
+ sourceGroup: row.sourceGroup,
787
+ origin: origin.label,
788
+ valuation: valuation.label,
789
+ source: row.source === undefined ? null : { ...row.source },
790
+ },
791
+ disposition: "retained",
792
+ coordinate: {
793
+ origin,
794
+ valuation,
795
+ developmentAge,
796
+ ageUnit: definition.definition.periodAxis.ageUnit,
797
+ },
798
+ });
799
+ });
800
+ if (issues.length > 0)
801
+ throw new DiagnosticValidationError(issues);
802
+ const filter = normalizedFilter.value;
803
+ const findings = [];
804
+ const pendingBlockers = [];
805
+ const lossCandidates = input.losses.map((inputRow) => ({
806
+ row: {
807
+ ...inputRow,
808
+ measures: Object.fromEntries(Object.keys(inputRow.measures)
809
+ .sort(codeUnit)
810
+ .map((key) => [key, inputRow.measures[key]])),
811
+ },
812
+ snapshot: snapshotLoss(inputRow),
813
+ disposition: selectedBySource(filter, inputRow.sourceGroup)
814
+ ? "retained"
815
+ : "filter",
816
+ coordinate: null,
817
+ invalid: false,
818
+ }));
819
+ for (const candidate of lossCandidates) {
820
+ if (candidate.disposition !== "retained")
821
+ continue;
822
+ let origin = null;
823
+ let valuation = null;
824
+ try {
825
+ origin = normalizeDiagnosticPeriod(definition, "origin", candidate.row.origin);
826
+ }
827
+ catch {
828
+ findings.push(structuralFinding("unknown-origin-period", {
829
+ recordId: candidate.row.recordId,
830
+ sourceGroup: candidate.row.sourceGroup,
831
+ origin: candidate.row.origin,
832
+ valuation: candidate.row.valuation,
833
+ sources: candidate.row.source ? [candidate.row.source] : [],
834
+ }));
835
+ }
836
+ if (origin !== null)
837
+ candidate.snapshot = { ...candidate.snapshot, origin: origin.label };
838
+ try {
839
+ valuation = normalizeDiagnosticPeriod(definition, "valuation", candidate.row.valuation);
840
+ }
841
+ catch {
842
+ findings.push(structuralFinding("unknown-valuation-period", {
843
+ recordId: candidate.row.recordId,
844
+ sourceGroup: candidate.row.sourceGroup,
845
+ origin: origin?.label ?? candidate.row.origin,
846
+ valuation: candidate.row.valuation,
847
+ sources: candidate.row.source ? [candidate.row.source] : [],
848
+ }));
849
+ }
850
+ if (valuation !== null)
851
+ candidate.snapshot = {
852
+ ...candidate.snapshot,
853
+ valuation: valuation.label,
854
+ };
855
+ if (origin === null || valuation === null) {
856
+ candidate.disposition = "invalid";
857
+ candidate.invalid = true;
858
+ continue;
859
+ }
860
+ const developmentAge = valuation.coordinate -
861
+ origin.coordinate +
862
+ definition.definition.periodAxis.ageOffset;
863
+ if (!Number.isSafeInteger(developmentAge) || developmentAge < 0) {
864
+ const code = Number.isSafeInteger(developmentAge)
865
+ ? "valuation-before-origin"
866
+ : "unsafe-development-age";
867
+ findings.push(structuralFinding(code, {
868
+ recordId: candidate.row.recordId,
869
+ sourceGroup: candidate.row.sourceGroup,
870
+ origin: origin.label,
871
+ valuation: valuation.label,
872
+ sources: candidate.row.source ? [candidate.row.source] : [],
873
+ }));
874
+ candidate.disposition = "invalid";
875
+ candidate.invalid = true;
876
+ continue;
877
+ }
878
+ candidate.coordinate = {
879
+ origin,
880
+ valuation,
881
+ developmentAge,
882
+ ageUnit: definition.definition.periodAxis.ageUnit,
883
+ };
884
+ candidate.row = {
885
+ ...candidate.row,
886
+ origin: origin.label,
887
+ valuation: valuation.label,
888
+ };
889
+ candidate.snapshot = {
890
+ ...candidate.snapshot,
891
+ origin: candidate.row.origin,
892
+ valuation: candidate.row.valuation,
893
+ };
894
+ if (beyondCutoff(candidate, cutoffByGroup))
895
+ candidate.disposition = "complete-period-cutoff";
896
+ else if (!selectedByOrigin(filter, origin, normalizedFilter.originCoordinates) ||
897
+ !selectedByValuation(filter, valuation, normalizedFilter.valuationCoordinates) ||
898
+ !selectedByAge(filter, developmentAge))
899
+ candidate.disposition = "filter";
900
+ }
901
+ const exposureCandidates = input.exposures.map((inputRow) => ({
902
+ row: { ...inputRow },
903
+ snapshot: snapshotExposure(inputRow),
904
+ disposition: selectedBySource(filter, inputRow.sourceGroup)
905
+ ? "retained"
906
+ : "filter",
907
+ origin: null,
908
+ valuation: null,
909
+ developmentAge: null,
910
+ ageUnit: null,
911
+ timing: internals.measuresById.get(inputRow.measureId)?.exposureTiming ?? null,
912
+ invalid: false,
913
+ }));
914
+ for (const candidate of exposureCandidates) {
915
+ if (candidate.disposition !== "retained")
916
+ continue;
917
+ try {
918
+ candidate.origin = normalizeDiagnosticPeriod(definition, "origin", candidate.row.origin);
919
+ }
920
+ catch {
921
+ findings.push(structuralFinding("unknown-origin-period", {
922
+ measureId: candidate.row.measureId,
923
+ exposureKey: candidate.row.key,
924
+ sourceGroup: candidate.row.sourceGroup,
925
+ origin: candidate.row.origin,
926
+ sources: candidate.row.source ? [candidate.row.source] : [],
927
+ }));
928
+ }
929
+ if (candidate.origin !== null)
930
+ candidate.snapshot = {
931
+ ...candidate.snapshot,
932
+ origin: candidate.origin.label,
933
+ };
934
+ if (candidate.row.valuation !== undefined) {
935
+ try {
936
+ candidate.valuation = normalizeDiagnosticPeriod(definition, "valuation", candidate.row.valuation);
937
+ }
938
+ catch {
939
+ findings.push(structuralFinding("unknown-valuation-period", {
940
+ measureId: candidate.row.measureId,
941
+ exposureKey: candidate.row.key,
942
+ sourceGroup: candidate.row.sourceGroup,
943
+ origin: candidate.origin?.label ?? candidate.row.origin,
944
+ valuation: candidate.row.valuation,
945
+ sources: candidate.row.source ? [candidate.row.source] : [],
946
+ }));
947
+ }
948
+ if (candidate.valuation !== null)
949
+ candidate.snapshot = {
950
+ ...candidate.snapshot,
951
+ valuation: candidate.valuation.label,
952
+ };
953
+ }
954
+ if (candidate.origin === null ||
955
+ (candidate.row.valuation !== undefined && candidate.valuation === null)) {
956
+ candidate.disposition = "invalid";
957
+ candidate.invalid = true;
958
+ continue;
959
+ }
960
+ if (candidate.valuation !== null) {
961
+ const age = candidate.valuation.coordinate -
962
+ candidate.origin.coordinate +
963
+ definition.definition.periodAxis.ageOffset;
964
+ if (!Number.isSafeInteger(age) || age < 0) {
965
+ const code = Number.isSafeInteger(age)
966
+ ? "valuation-before-origin"
967
+ : "unsafe-development-age";
968
+ findings.push(structuralFinding(code, {
969
+ measureId: candidate.row.measureId,
970
+ exposureKey: candidate.row.key,
971
+ sourceGroup: candidate.row.sourceGroup,
972
+ origin: candidate.origin.label,
973
+ valuation: candidate.valuation.label,
974
+ sources: candidate.row.source ? [candidate.row.source] : [],
975
+ }));
976
+ candidate.disposition = "invalid";
977
+ candidate.invalid = true;
978
+ continue;
979
+ }
980
+ candidate.developmentAge = age;
981
+ candidate.ageUnit = definition.definition.periodAxis.ageUnit;
982
+ }
983
+ candidate.row = {
984
+ ...candidate.row,
985
+ origin: candidate.origin.label,
986
+ ...(candidate.valuation ? { valuation: candidate.valuation.label } : {}),
987
+ };
988
+ candidate.snapshot = {
989
+ ...candidate.snapshot,
990
+ origin: candidate.origin.label,
991
+ valuation: candidate.valuation?.label ?? null,
992
+ };
993
+ const cutoff = cutoffByGroup.get(candidate.row.sourceGroup);
994
+ const beyondOrigin = cutoff?.originThrough !== null &&
995
+ cutoff?.originThrough !== undefined &&
996
+ compareDiagnosticPeriods(candidate.origin, cutoff.originThrough) > 0;
997
+ const beyondValuation = candidate.timing === "valuation-specific" &&
998
+ cutoff?.valuationThrough !== null &&
999
+ cutoff?.valuationThrough !== undefined &&
1000
+ candidate.valuation !== null &&
1001
+ compareDiagnosticPeriods(candidate.valuation, cutoff.valuationThrough) >
1002
+ 0;
1003
+ if (beyondOrigin || beyondValuation)
1004
+ candidate.disposition = "complete-period-cutoff";
1005
+ else {
1006
+ const originSelected = selectedByOrigin(filter, candidate.origin, normalizedFilter.originCoordinates);
1007
+ const valuationSelected = candidate.timing === "valuation-specific" &&
1008
+ candidate.valuation !== null
1009
+ ? selectedByValuation(filter, candidate.valuation, normalizedFilter.valuationCoordinates) && selectedByAge(filter, candidate.developmentAge)
1010
+ : true;
1011
+ if (!originSelected || !valuationSelected)
1012
+ candidate.disposition = "filter";
1013
+ }
1014
+ }
1015
+ for (const candidate of expectedCandidates) {
1016
+ if (!selectedBySource(filter, candidate.row.sourceGroup))
1017
+ candidate.disposition = "filter";
1018
+ else if (beyondCutoff(candidate, cutoffByGroup))
1019
+ candidate.disposition = "complete-period-cutoff";
1020
+ else if (!selectedByOrigin(filter, candidate.coordinate.origin, normalizedFilter.originCoordinates) ||
1021
+ !selectedByValuation(filter, candidate.coordinate.valuation, normalizedFilter.valuationCoordinates) ||
1022
+ !selectedByAge(filter, candidate.coordinate.developmentAge))
1023
+ candidate.disposition = "filter";
1024
+ }
1025
+ const provisionalLosses = lossCandidates.filter((candidate) => candidate.disposition === "retained");
1026
+ const lossFindings = (code, candidates, extra = {}) => {
1027
+ const finding = structuralFinding(code, {
1028
+ ...commonCoordinate(candidates),
1029
+ ...extra,
1030
+ sources: normalizeSources(candidates.map((candidate) => candidate.row.source)),
1031
+ });
1032
+ findings.push(finding);
1033
+ for (const candidate of candidates)
1034
+ candidate.invalid = true;
1035
+ pendingBlockers.push({
1036
+ finding,
1037
+ measureIds: definition.definition.measures
1038
+ .filter((measure) => measure.source !== "exposure")
1039
+ .map((measure) => measure.id),
1040
+ cellKeys: uniqueSorted(candidates.flatMap((candidate) => candidate.coordinate
1041
+ ? [
1042
+ cellKey(candidate.row.sourceGroup, candidate.coordinate.origin.label, candidate.coordinate.valuation.label),
1043
+ ]
1044
+ : [])),
1045
+ sourceIds: uniqueSorted(candidates.map((candidate) => candidate.row.recordId)),
1046
+ });
1047
+ };
1048
+ for (const candidate of provisionalLosses) {
1049
+ const localCodes = [];
1050
+ for (const measureId of Object.keys(candidate.row.measures).sort(codeUnit)) {
1051
+ const measure = internals.measuresById.get(measureId);
1052
+ if (!measure)
1053
+ localCodes.push({
1054
+ code: "undeclared-loss-measure",
1055
+ measureId,
1056
+ offendingKey: measureId,
1057
+ });
1058
+ else if (measure.source !== "loss")
1059
+ localCodes.push({
1060
+ code: "wrong-source-loss-measure",
1061
+ measureId,
1062
+ offendingKey: measureId,
1063
+ });
1064
+ }
1065
+ if (!candidate.row.complete)
1066
+ localCodes.push({ code: "incomplete-loss-record" });
1067
+ for (const item of localCodes)
1068
+ lossFindings(item.code, [candidate], {
1069
+ recordId: candidate.row.recordId,
1070
+ ...(candidate.row.rowType === "claim"
1071
+ ? { claimId: candidate.row.claimId }
1072
+ : {}),
1073
+ ...(item.measureId ? { measureId: item.measureId } : {}),
1074
+ ...(item.offendingKey ? { offendingKey: item.offendingKey } : {}),
1075
+ });
1076
+ }
1077
+ const byRecordId = new Map();
1078
+ for (const candidate of provisionalLosses)
1079
+ addToMap(byRecordId, candidate.row.recordId, candidate);
1080
+ for (const [recordId, cohort] of byRecordId)
1081
+ if (cohort.length > 1)
1082
+ lossFindings("duplicate-loss-record-id", cohort, { recordId });
1083
+ if (definition.definition.lossRowGrain === "aggregate") {
1084
+ const byCell = new Map();
1085
+ for (const candidate of provisionalLosses)
1086
+ addToMap(byCell, cellKey(candidate.row.sourceGroup, candidate.row.origin, candidate.row.valuation), candidate);
1087
+ for (const cohort of byCell.values())
1088
+ if (cohort.length > 1)
1089
+ lossFindings("duplicate-aggregate-snapshot", cohort);
1090
+ }
1091
+ else {
1092
+ const bySnapshot = new Map();
1093
+ const byClaim = new Map();
1094
+ for (const candidate of provisionalLosses) {
1095
+ const claimId = candidate.row.claimId;
1096
+ addToMap(bySnapshot, canonicalJson([
1097
+ claimId,
1098
+ candidate.row.sourceGroup,
1099
+ candidate.row.origin,
1100
+ candidate.row.valuation,
1101
+ ]), candidate);
1102
+ addToMap(byClaim, claimId, candidate);
1103
+ }
1104
+ for (const cohort of bySnapshot.values())
1105
+ if (cohort.length > 1)
1106
+ lossFindings("duplicate-claim-snapshot", cohort, {
1107
+ claimId: cohort[0].row.claimId,
1108
+ });
1109
+ for (const [claimId, cohort] of byClaim)
1110
+ if (uniqueSorted(cohort.map((candidate) => canonicalJson([candidate.row.sourceGroup, candidate.row.origin]))).length > 1)
1111
+ lossFindings("claim-identity-conflict", cohort, { claimId });
1112
+ }
1113
+ for (const candidate of provisionalLosses)
1114
+ if (candidate.invalid)
1115
+ candidate.disposition = "invalid";
1116
+ const declaredExposureCandidates = [];
1117
+ for (const candidate of exposureCandidates.filter((item) => item.disposition === "retained")) {
1118
+ const measure = internals.measuresById.get(candidate.row.measureId);
1119
+ if (!measure) {
1120
+ findings.push(structuralFinding("undeclared-exposure-measure", {
1121
+ measureId: candidate.row.measureId,
1122
+ offendingKey: candidate.row.measureId,
1123
+ exposureKey: candidate.row.key,
1124
+ ...(candidate.origin ? { origin: candidate.origin.label } : {}),
1125
+ sources: candidate.row.source ? [candidate.row.source] : [],
1126
+ }));
1127
+ candidate.invalid = true;
1128
+ candidate.disposition = "invalid";
1129
+ }
1130
+ else if (measure.source !== "exposure") {
1131
+ findings.push(structuralFinding("wrong-source-exposure-measure", {
1132
+ measureId: candidate.row.measureId,
1133
+ offendingKey: candidate.row.measureId,
1134
+ exposureKey: candidate.row.key,
1135
+ ...(candidate.origin ? { origin: candidate.origin.label } : {}),
1136
+ sources: candidate.row.source ? [candidate.row.source] : [],
1137
+ }));
1138
+ candidate.invalid = true;
1139
+ candidate.disposition = "invalid";
1140
+ }
1141
+ else
1142
+ declaredExposureCandidates.push(candidate);
1143
+ }
1144
+ const reconciled = reconcileDiagnosticExposures(declaredExposureCandidates.map((candidate) => candidate.row), Object.fromEntries(definition.definition.measures
1145
+ .filter((measure) => measure.source === "exposure")
1146
+ .map((measure) => [measure.id, measure.exposureTiming])), definition.definition.periodAxis);
1147
+ const exposureCohorts = new Map();
1148
+ for (const candidate of declaredExposureCandidates)
1149
+ addToMap(exposureCohorts, exposureIdentity(candidate), candidate);
1150
+ const invalidExposureBlockers = [];
1151
+ for (const exposure of reconciled) {
1152
+ const timing = internals.measuresById.get(exposure.measureId)?.exposureTiming;
1153
+ const identity = canonicalJson(timing === "valuation-specific"
1154
+ ? [
1155
+ exposure.measureId,
1156
+ exposure.key,
1157
+ exposure.status === "valid"
1158
+ ? (exposure.valuation ?? null)
1159
+ : (exposure.observations[0]?.valuation ?? null),
1160
+ ]
1161
+ : [exposure.measureId, exposure.key]);
1162
+ const cohort = exposureCohorts.get(identity) ?? [];
1163
+ if (exposure.status === "valid")
1164
+ continue;
1165
+ for (const candidate of cohort) {
1166
+ candidate.invalid = true;
1167
+ candidate.disposition = "invalid";
1168
+ }
1169
+ const context = exposureCoordinateContext(cohort);
1170
+ const issueCodes = {
1171
+ missing: "missing-exposure-value",
1172
+ incomplete: "incomplete-exposure",
1173
+ "non-finite": "non-finite-exposure",
1174
+ duplicate: "duplicate-exposure-identity",
1175
+ conflict: "conflicting-exposure-identity",
1176
+ };
1177
+ for (const issue of exposure.issues) {
1178
+ const finding = structuralFinding(issueCodes[issue], {
1179
+ ...context,
1180
+ measureId: exposure.measureId,
1181
+ exposureKey: exposure.key,
1182
+ sources: normalizeSources(cohort.map((candidate) => candidate.row.source)),
1183
+ });
1184
+ findings.push(finding);
1185
+ invalidExposureBlockers.push({ finding, candidates: cohort });
1186
+ }
1187
+ }
1188
+ const retainedCandidates = lossCandidates.filter((candidate) => candidate.disposition === "retained");
1189
+ const retainedRows = retainedCandidates.map((candidate) => candidate.row);
1190
+ const derivedAudits = definition.definition.lossRowGrain === "claim"
1191
+ ? deriveDiagnosticClaimMeasuresWithAudit(retainedRows, definition)
1192
+ : [];
1193
+ const derivedRows = definition.definition.lossRowGrain === "claim"
1194
+ ? derivedAudits.map((item) => item.row)
1195
+ : retainedRows;
1196
+ const derivedByRecord = new Map(derivedRows.map((row) => [row.recordId, row]));
1197
+ const derivedAuditByRecord = new Map(derivedAudits.map((item) => [
1198
+ item.row.recordId,
1199
+ item,
1200
+ ]));
1201
+ const cellRows = new Map();
1202
+ for (const candidate of retainedCandidates)
1203
+ addToMap(cellRows, cellKey(candidate.row.sourceGroup, candidate.row.origin, candidate.row.valuation), derivedByRecord.get(candidate.row.recordId));
1204
+ const allMeasureIds = definition.definition.measures
1205
+ .map((measure) => measure.id)
1206
+ .sort(codeUnit);
1207
+ const nonExposureMeasures = definition.definition.measures
1208
+ .filter((measure) => measure.source !== "exposure")
1209
+ .sort((left, right) => codeUnit(left.id, right.id));
1210
+ const exposureMeasures = definition.definition.measures
1211
+ .filter((measure) => measure.source === "exposure")
1212
+ .sort((left, right) => codeUnit(left.id, right.id));
1213
+ const preparedCells = [];
1214
+ const invalidExposureTargets = new Map();
1215
+ for (const item of invalidExposureBlockers) {
1216
+ const targets = new Set();
1217
+ for (const candidate of item.candidates) {
1218
+ if (!candidate.origin)
1219
+ continue;
1220
+ if (candidate.timing === "valuation-specific" && candidate.valuation)
1221
+ targets.add(cellKey(candidate.row.sourceGroup, candidate.origin.label, candidate.valuation.label));
1222
+ else
1223
+ for (const key of cellRows.keys()) {
1224
+ const [sourceGroup, origin] = parseCellKey(key);
1225
+ if (sourceGroup === candidate.row.sourceGroup &&
1226
+ origin === candidate.origin.label)
1227
+ targets.add(key);
1228
+ }
1229
+ }
1230
+ invalidExposureTargets.set(canonicalJson(item.finding), targets);
1231
+ pendingBlockers.push({
1232
+ finding: item.finding,
1233
+ measureIds: [item.finding.measureId],
1234
+ cellKeys: [...targets],
1235
+ sourceIds: item.finding.exposureKey ? [item.finding.exposureKey] : [],
1236
+ });
1237
+ }
1238
+ const validAttachmentKeys = new Map();
1239
+ for (const exposure of reconciled) {
1240
+ if (exposure.status !== "valid")
1241
+ continue;
1242
+ const timing = internals.measuresById.get(exposure.measureId).exposureTiming;
1243
+ const keys = [...cellRows.keys()].filter((key) => {
1244
+ const [sourceGroup, origin, valuation] = parseCellKey(key);
1245
+ return (sourceGroup === exposure.sourceGroup &&
1246
+ origin === exposure.origin &&
1247
+ (timing === "origin-static" || valuation === exposure.valuation));
1248
+ });
1249
+ validAttachmentKeys.set(exposure, keys);
1250
+ if (keys.length === 0)
1251
+ findings.push(structuralFinding("exposure-without-loss", {
1252
+ measureId: exposure.measureId,
1253
+ exposureKey: exposure.key,
1254
+ sourceGroup: exposure.sourceGroup,
1255
+ origin: exposure.origin,
1256
+ ...(exposure.valuation ? { valuation: exposure.valuation } : {}),
1257
+ sources: exposure.sources,
1258
+ }));
1259
+ }
1260
+ for (const [key, rows] of cellRows) {
1261
+ const [sourceGroup, origin, valuation] = parseCellKey(key);
1262
+ const coordinate = coordinateFor(definition, origin, valuation).coordinate;
1263
+ const contributions = Object.fromEntries(allMeasureIds.map((measureId) => [
1264
+ measureId,
1265
+ [],
1266
+ ]));
1267
+ const blockers = Object.fromEntries(allMeasureIds.map((measureId) => [
1268
+ measureId,
1269
+ [],
1270
+ ]));
1271
+ const cellFindings = [];
1272
+ const derivedContribution = (row, missingPolicy, state) => {
1273
+ const base = {
1274
+ sourceId: row.recordId,
1275
+ sources: row.source ? [row.source] : [],
1276
+ deduplicated: 0,
1277
+ };
1278
+ if (state.status === "observed")
1279
+ return { ...base, status: "observed", value: state.value };
1280
+ if (state.status === "non-finite")
1281
+ return {
1282
+ ...base,
1283
+ status: "non-finite",
1284
+ value: null,
1285
+ nonFiniteKind: state.nonFiniteKind,
1286
+ };
1287
+ return missingPolicy === "zero"
1288
+ ? { ...base, status: "imputed-zero", value: 0 }
1289
+ : { ...base, status: "missing", value: null };
1290
+ };
1291
+ for (const row of rows)
1292
+ for (const measure of nonExposureMeasures) {
1293
+ if (measure.source === "derived") {
1294
+ const state = derivedAuditByRecord.get(row.recordId).derived[measure.id];
1295
+ contributions[measure.id].push(derivedContribution(row, measure.missing, state));
1296
+ for (const overflow of derivedAuditByRecord.get(row.recordId)
1297
+ .expressionOverflows[measure.id] ?? []) {
1298
+ cellFindings.push({
1299
+ code: "diagnostic-expression-overflow",
1300
+ message: "Measure expression overflowed",
1301
+ severity: "fail",
1302
+ category: "aggregation",
1303
+ measureId: measure.id,
1304
+ expressionPath: overflow.expressionPath,
1305
+ recordId: row.recordId,
1306
+ sourceGroup,
1307
+ origin,
1308
+ valuation,
1309
+ developmentAge: coordinate.developmentAge,
1310
+ ageUnit: coordinate.ageUnit,
1311
+ sources: row.source ? [row.source] : [],
1312
+ });
1313
+ }
1314
+ }
1315
+ else
1316
+ contributions[measure.id].push(auditedDiagnosticContribution(row.recordId, own(row.measures, measure.id), measure.missing, row.source ? [row.source] : []));
1317
+ }
1318
+ for (const exposure of reconciled)
1319
+ if (exposure.status === "valid" &&
1320
+ validAttachmentKeys.get(exposure)?.includes(key))
1321
+ contributions[exposure.measureId].push(auditedDiagnosticContribution(exposure.key, exposure.value, "unknown", exposure.sources, exposure.deduplicated));
1322
+ for (const pending of pendingBlockers) {
1323
+ if (!pending.cellKeys.includes(key))
1324
+ continue;
1325
+ cellFindings.push(pending.finding);
1326
+ const blocker = {
1327
+ code: pending.finding.code,
1328
+ message: pending.finding.message,
1329
+ sourceIds: uniqueSorted(pending.sourceIds),
1330
+ sources: normalizeSources(pending.finding.sources),
1331
+ finding: pending.finding,
1332
+ };
1333
+ for (const measureId of pending.measureIds)
1334
+ blockers[measureId].push(blocker);
1335
+ }
1336
+ for (const measure of exposureMeasures) {
1337
+ const attempted = [...validAttachmentKeys.entries()].some(([exposure, keys]) => exposure.status === "valid" &&
1338
+ exposure.measureId === measure.id &&
1339
+ keys.includes(key)) ||
1340
+ invalidExposureBlockers.some((item) => item.finding.measureId === measure.id &&
1341
+ invalidExposureTargets.get(canonicalJson(item.finding))?.has(key));
1342
+ if (contributions[measure.id].length === 0 && !attempted) {
1343
+ const sources = normalizeSources(rows.map((row) => row.source));
1344
+ const finding = structuralFinding("loss-without-exposure", {
1345
+ measureId: measure.id,
1346
+ sourceGroup,
1347
+ origin,
1348
+ valuation,
1349
+ developmentAge: coordinate.developmentAge,
1350
+ ageUnit: coordinate.ageUnit,
1351
+ sources,
1352
+ });
1353
+ findings.push(finding);
1354
+ cellFindings.push(finding);
1355
+ blockers[measure.id].push({
1356
+ code: finding.code,
1357
+ message: finding.message,
1358
+ sourceIds: uniqueSorted(rows.map((row) => row.recordId)),
1359
+ sources,
1360
+ finding,
1361
+ });
1362
+ }
1363
+ }
1364
+ for (const measureId of allMeasureIds) {
1365
+ blockers[measureId] = [
1366
+ ...new Map(blockers[measureId].map((blocker) => [
1367
+ canonicalJson(blocker),
1368
+ blocker,
1369
+ ])).values(),
1370
+ ].sort(compareDiagnosticBlockers);
1371
+ contributions[measureId].sort(compareDiagnosticContributions);
1372
+ }
1373
+ const components = Object.fromEntries(allMeasureIds.map((measureId) => {
1374
+ const measure = internals.measuresById.get(measureId);
1375
+ return [
1376
+ measureId,
1377
+ finalizeDiagnosticContributions(contributions[measureId], measure.missing, blockers[measureId]),
1378
+ ];
1379
+ }));
1380
+ for (const measureId of allMeasureIds) {
1381
+ const measureContributions = contributions[measureId];
1382
+ const context = {
1383
+ measureId,
1384
+ sourceGroup,
1385
+ origin,
1386
+ valuation,
1387
+ developmentAge: coordinate.developmentAge,
1388
+ ageUnit: coordinate.ageUnit,
1389
+ };
1390
+ const missing = measureContributions.filter((item) => item.status === "missing");
1391
+ const imputed = measureContributions.filter((item) => item.status === "imputed-zero");
1392
+ const nonFinite = measureContributions.filter((item) => item.status === "non-finite");
1393
+ if (missing.length > 0)
1394
+ cellFindings.push(aggregationFinding("diagnostic-measure-missing", {
1395
+ ...context,
1396
+ sources: missing.flatMap((item) => item.sources),
1397
+ }));
1398
+ if (imputed.length > 0)
1399
+ cellFindings.push(aggregationFinding("diagnostic-measure-imputed-zero", {
1400
+ ...context,
1401
+ sources: imputed.flatMap((item) => item.sources),
1402
+ }));
1403
+ if (nonFinite.length > 0)
1404
+ cellFindings.push(aggregationFinding("diagnostic-measure-non-finite", {
1405
+ ...context,
1406
+ sources: nonFinite.flatMap((item) => item.sources),
1407
+ }));
1408
+ if (components[measureId].sum === null &&
1409
+ components[measureId].nonFinite === 0 &&
1410
+ components[measureId].structural === 0)
1411
+ cellFindings.push(aggregationFinding("diagnostic-measure-overflow", {
1412
+ ...context,
1413
+ sources: measureContributions.flatMap((item) => item.sources),
1414
+ }));
1415
+ }
1416
+ findings.push(...cellFindings);
1417
+ preparedCells.push({
1418
+ sourceGroup,
1419
+ origin,
1420
+ valuation,
1421
+ developmentAge: coordinate.developmentAge,
1422
+ ageUnit: coordinate.ageUnit,
1423
+ lossRecordIds: uniqueSorted(rows.map((row) => row.recordId)),
1424
+ contributions,
1425
+ components,
1426
+ structuralBlockers: blockers,
1427
+ findings: mergeFindings(cellFindings),
1428
+ });
1429
+ }
1430
+ for (const candidate of expectedCandidates.filter((item) => item.disposition === "retained")) {
1431
+ const key = cellKey(candidate.row.sourceGroup, candidate.row.origin, candidate.row.valuation);
1432
+ if (!cellRows.has(key))
1433
+ findings.push(structuralFinding("missing-expected-cell", {
1434
+ sourceGroup: candidate.row.sourceGroup,
1435
+ origin: candidate.row.origin,
1436
+ valuation: candidate.row.valuation,
1437
+ developmentAge: candidate.coordinate.developmentAge,
1438
+ ageUnit: candidate.coordinate.ageUnit,
1439
+ sources: candidate.row.source ? [candidate.row.source] : [],
1440
+ }));
1441
+ }
1442
+ const originCoordinates = new Map();
1443
+ const valuationCoordinates = new Map();
1444
+ for (const cell of preparedCells) {
1445
+ originCoordinates.set(cell.origin, normalizeDiagnosticPeriod(definition, "origin", cell.origin).coordinate);
1446
+ valuationCoordinates.set(cell.valuation, normalizeDiagnosticPeriod(definition, "valuation", cell.valuation)
1447
+ .coordinate);
1448
+ }
1449
+ preparedCells.sort((left, right) => codeUnit(left.sourceGroup, right.sourceGroup) ||
1450
+ originCoordinates.get(left.origin) -
1451
+ originCoordinates.get(right.origin) ||
1452
+ valuationCoordinates.get(left.valuation) -
1453
+ valuationCoordinates.get(right.valuation) ||
1454
+ codeUnit(left.origin, right.origin) ||
1455
+ codeUnit(left.valuation, right.valuation));
1456
+ const expectedCells = expectedCandidates
1457
+ .filter((candidate) => candidate.disposition === "retained")
1458
+ .sort((left, right) => codeUnit(left.row.sourceGroup, right.row.sourceGroup) ||
1459
+ compareDiagnosticPeriods(left.coordinate.origin, right.coordinate.origin) ||
1460
+ compareDiagnosticPeriods(left.coordinate.valuation, right.coordinate.valuation))
1461
+ .map((candidate) => candidate.row);
1462
+ const audit = [
1463
+ ...lossCandidates.map((candidate) => ({
1464
+ kind: "loss",
1465
+ disposition: candidate.disposition,
1466
+ record: candidate.snapshot,
1467
+ })),
1468
+ ...exposureCandidates.map((candidate) => ({
1469
+ kind: "exposure",
1470
+ disposition: candidate.disposition,
1471
+ record: candidate.snapshot,
1472
+ })),
1473
+ ...expectedCandidates.map((candidate) => ({
1474
+ kind: "expected-cell",
1475
+ disposition: candidate.disposition,
1476
+ record: candidate.snapshot,
1477
+ })),
1478
+ ].sort(auditSort);
1479
+ const normalizedFindings = mergeFindings(findings);
1480
+ const frozenCells = preparedCells.map((cell) => deepFreeze(cell));
1481
+ const cutoffValues = normalizedCutoffs.map((cutoff) => cutoff.value);
1482
+ const ownedData = deepFreeze({
1483
+ definitionIntegrity: definition.definitionIntegrity,
1484
+ filter,
1485
+ completePeriodCutoffs: cutoffValues,
1486
+ inputAudit: audit,
1487
+ cells: frozenCells,
1488
+ exposures: reconciled,
1489
+ expectedCellsProvided: input.expectedCells !== undefined,
1490
+ expectedCells,
1491
+ findings: normalizedFindings,
1492
+ });
1493
+ const identity = projectDiagnosticIdentity({
1494
+ ...ownedData,
1495
+ filter: normalizeDiagnosticsFilterIdentity(filter),
1496
+ expectedCells: expectedCells.map((cell) => ({
1497
+ ...cell,
1498
+ source: cell.source ?? null,
1499
+ })),
1500
+ });
1501
+ const preparationFingerprint = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
1502
+ const prepared = deepFreeze({
1503
+ definition,
1504
+ preparationFingerprint,
1505
+ ...ownedData,
1506
+ });
1507
+ authentic.add(prepared);
1508
+ identities.set(prepared, identity);
1509
+ return prepared;
1510
+ }
1511
+ export function assertPreparedDiagnosticData(value) {
1512
+ if (value === null || typeof value !== "object" || !authentic.has(value))
1513
+ throw new DiagnosticValidationError([
1514
+ {
1515
+ domain: "input",
1516
+ code: "invalid-input-relationship",
1517
+ path: "$.prepared",
1518
+ message: "Prepared diagnostic data is not authentic",
1519
+ },
1520
+ ]);
1521
+ }
1522
+ export function getPreparedDiagnosticDataIdentity(value) {
1523
+ assertPreparedDiagnosticData(value);
1524
+ return identities.get(value);
1525
+ }
1526
+ export function verifyPreparedDiagnosticDataIntegrity(value) {
1527
+ const identity = getPreparedDiagnosticDataIdentity(value);
1528
+ const tag = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
1529
+ if (tag !== value.preparationFingerprint)
1530
+ throw new DiagnosticValidationError([
1531
+ {
1532
+ domain: "input",
1533
+ code: "invalid-input-relationship",
1534
+ path: "$.prepared.preparationFingerprint",
1535
+ message: "Prepared diagnostic data integrity does not match its content",
1536
+ },
1537
+ ]);
1538
+ }
1539
+ //# sourceMappingURL=diagnosticPreparation.js.map