@actuarial-ts/core 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +3 -3
  2. package/dist/capping.d.ts.map +1 -1
  3. package/dist/capping.js +39 -6
  4. package/dist/capping.js.map +1 -1
  5. package/dist/diagnosticAggregation.d.ts +3 -0
  6. package/dist/diagnosticAggregation.d.ts.map +1 -1
  7. package/dist/diagnosticAggregation.js +30 -7
  8. package/dist/diagnosticAggregation.js.map +1 -1
  9. package/dist/diagnosticDefinitions.d.ts +3 -0
  10. package/dist/diagnosticDefinitions.d.ts.map +1 -1
  11. package/dist/diagnosticDefinitions.js +844 -182
  12. package/dist/diagnosticDefinitions.js.map +1 -1
  13. package/dist/diagnosticDerivations.d.ts +23 -0
  14. package/dist/diagnosticDerivations.d.ts.map +1 -1
  15. package/dist/diagnosticDerivations.js +187 -43
  16. package/dist/diagnosticDerivations.js.map +1 -1
  17. package/dist/diagnosticExposure.d.ts +2 -2
  18. package/dist/diagnosticExposure.d.ts.map +1 -1
  19. package/dist/diagnosticExposure.js +228 -20
  20. package/dist/diagnosticExposure.js.map +1 -1
  21. package/dist/diagnosticExpressions.d.ts +1 -1
  22. package/dist/diagnosticExpressions.d.ts.map +1 -1
  23. package/dist/diagnosticExpressions.js +35 -15
  24. package/dist/diagnosticExpressions.js.map +1 -1
  25. package/dist/diagnosticFormulas.d.ts +3 -0
  26. package/dist/diagnosticFormulas.d.ts.map +1 -1
  27. package/dist/diagnosticFormulas.js +160 -26
  28. package/dist/diagnosticFormulas.js.map +1 -1
  29. package/dist/diagnosticIdentity.d.ts +15 -1
  30. package/dist/diagnosticIdentity.d.ts.map +1 -1
  31. package/dist/diagnosticIdentity.js +318 -51
  32. package/dist/diagnosticIdentity.js.map +1 -1
  33. package/dist/diagnosticOrdering.d.ts +8 -0
  34. package/dist/diagnosticOrdering.d.ts.map +1 -0
  35. package/dist/diagnosticOrdering.js +73 -0
  36. package/dist/diagnosticOrdering.js.map +1 -0
  37. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  38. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  39. package/dist/diagnosticPeriodAxis.js +69 -0
  40. package/dist/diagnosticPeriodAxis.js.map +1 -0
  41. package/dist/diagnosticPeriods.d.ts +2 -3
  42. package/dist/diagnosticPeriods.d.ts.map +1 -1
  43. package/dist/diagnosticPeriods.js +20 -68
  44. package/dist/diagnosticPeriods.js.map +1 -1
  45. package/dist/diagnosticPreparation.d.ts +47 -14
  46. package/dist/diagnosticPreparation.d.ts.map +1 -1
  47. package/dist/diagnosticPreparation.js +1482 -112
  48. package/dist/diagnosticPreparation.js.map +1 -1
  49. package/dist/diagnosticReview.d.ts +36 -14
  50. package/dist/diagnosticReview.d.ts.map +1 -1
  51. package/dist/diagnosticReview.js +448 -42
  52. package/dist/diagnosticReview.js.map +1 -1
  53. package/dist/diagnosticRunner.d.ts +2 -1
  54. package/dist/diagnosticRunner.d.ts.map +1 -1
  55. package/dist/diagnosticRunner.js +656 -70
  56. package/dist/diagnosticRunner.js.map +1 -1
  57. package/dist/diagnosticRuntime.d.ts +26 -0
  58. package/dist/diagnosticRuntime.d.ts.map +1 -0
  59. package/dist/diagnosticRuntime.js +243 -0
  60. package/dist/diagnosticRuntime.js.map +1 -0
  61. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  62. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  63. package/dist/diagnosticSourceOrdering.js +38 -0
  64. package/dist/diagnosticSourceOrdering.js.map +1 -0
  65. package/dist/index.d.ts +8 -4
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +5 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/mack.d.ts +4 -0
  70. package/dist/mack.d.ts.map +1 -1
  71. package/dist/mack.js +32 -4
  72. package/dist/mack.js.map +1 -1
  73. package/dist/triangle.d.ts.map +1 -1
  74. package/dist/triangle.js +59 -9
  75. package/dist/triangle.js.map +1 -1
  76. package/dist/types.d.ts +4 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js +22 -8
  79. package/dist/types.js.map +1 -1
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +3 -2
  83. package/src/capping.ts +109 -18
  84. package/src/diagnosticAggregation.ts +69 -20
  85. package/src/diagnosticDefinitions.ts +2675 -426
  86. package/src/diagnosticDerivations.ts +281 -49
  87. package/src/diagnosticExposure.ts +441 -50
  88. package/src/diagnosticExpressions.ts +99 -17
  89. package/src/diagnosticFormulas.ts +239 -39
  90. package/src/diagnosticIdentity.ts +691 -191
  91. package/src/diagnosticOrdering.ts +106 -0
  92. package/src/diagnosticPeriodAxis.ts +92 -0
  93. package/src/diagnosticPeriods.ts +67 -82
  94. package/src/diagnosticPreparation.ts +2369 -127
  95. package/src/diagnosticReview.ts +864 -58
  96. package/src/diagnosticRunner.ts +1076 -72
  97. package/src/diagnosticRuntime.ts +264 -0
  98. package/src/diagnosticSourceOrdering.ts +50 -0
  99. package/src/index.ts +31 -3
  100. package/src/mack.ts +72 -9
  101. package/src/triangle.ts +103 -13
  102. package/src/types.ts +55 -20
  103. package/src/version.ts +1 -1
@@ -1,12 +1,86 @@
1
1
  import { canonicalJson, fnv1a64 } from "./canonical.js";
2
+ import { normalizeDiagnosticsFilterIdentity, projectDiagnosticIdentity, } from "./diagnosticIdentity.js";
3
+ import { compareDiagnosticBlockers, compareDiagnosticContributions, compareDiagnosticFindings, } from "./diagnosticOrdering.js";
2
4
  import { assertCompiledDiagnosticDefinition, getCompiledDiagnosticDefinitionInternals, } from "./diagnosticDefinitions.js";
3
- import { auditedDiagnosticContribution, finalizeDiagnosticContributions } from "./diagnosticAggregation.js";
4
- import { deriveDiagnosticClaimMeasures } from "./diagnosticDerivations.js";
5
- import { diagnosticDevelopmentAge, normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
6
- import { reconcileDiagnosticExposures } from "./diagnosticExposure.js";
7
- import { DiagnosticValidationError } from "./types.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";
8
12
  const authentic = new WeakSet();
9
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
+ }
10
84
  function deepFreeze(value, seen = new WeakSet()) {
11
85
  if (value === null || typeof value !== "object" || seen.has(value))
12
86
  return value;
@@ -15,155 +89,1451 @@ function deepFreeze(value, seen = new WeakSet()) {
15
89
  deepFreeze(child, seen);
16
90
  return Object.freeze(value);
17
91
  }
18
- function codeUnit(left, right) { return left < right ? -1 : left > right ? 1 : 0; }
19
- function own(record, key) { return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined; }
20
- function normalizeFilter(filter) {
21
- if (filter === undefined)
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
+ });
22
185
  return null;
23
- const set = (values) => values === undefined ? undefined : [...new Set(values)].sort(codeUnit);
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
+ };
24
316
  return {
25
- ...(filter.sourceGroups === undefined ? {} : { sourceGroups: set(filter.sourceGroups) }),
26
- ...(filter.outputGroups === undefined ? {} : { outputGroups: set(filter.outputGroups) }),
27
- ...(filter.origins === undefined ? {} : { origins: set(filter.origins) }),
28
- ...(filter.originFrom === undefined ? {} : { originFrom: filter.originFrom }),
29
- ...(filter.originThrough === undefined ? {} : { originThrough: filter.originThrough }),
30
- ...(filter.valuations === undefined ? {} : { valuations: set(filter.valuations) }),
31
- ...(filter.valuationFrom === undefined ? {} : { valuationFrom: filter.valuationFrom }),
32
- ...(filter.valuationThrough === undefined ? {} : { valuationThrough: filter.valuationThrough }),
33
- ...(filter.minDevelopmentAge === undefined ? {} : { minDevelopmentAge: filter.minDevelopmentAge }),
34
- ...(filter.maxDevelopmentAge === undefined ? {} : { maxDevelopmentAge: filter.maxDevelopmentAge }),
35
- ...(filter.instanceIds === undefined ? {} : { instanceIds: set(filter.instanceIds) }),
36
- };
37
- }
38
- function selected(filter, group, origin, valuation, age) {
39
- return (filter?.sourceGroups === undefined || filter.sourceGroups.includes(group)) &&
40
- (filter?.origins === undefined || filter.origins.includes(origin)) &&
41
- (filter?.valuations === undefined || filter.valuations.includes(valuation)) &&
42
- (filter?.originFrom === undefined || origin >= filter.originFrom) &&
43
- (filter?.originThrough === undefined || origin <= filter.originThrough) &&
44
- (filter?.valuationFrom === undefined || valuation >= filter.valuationFrom) &&
45
- (filter?.valuationThrough === undefined || valuation <= filter.valuationThrough) &&
46
- (filter?.minDevelopmentAge === undefined || age >= filter.minDevelopmentAge) &&
47
- (filter?.maxDevelopmentAge === undefined || age <= filter.maxDevelopmentAge);
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;
48
713
  }
49
714
  export function prepareDiagnosticData(input) {
50
- assertCompiledDiagnosticDefinition(input.definition);
715
+ assertCompiledDiagnosticDefinition(input?.definition);
716
+ const boundaryIssues = preparationBoundaryIssues(input);
717
+ if (boundaryIssues.length > 0)
718
+ throw new DiagnosticValidationError(boundaryIssues);
51
719
  const definition = input.definition;
52
720
  const internals = getCompiledDiagnosticDefinitionInternals(definition);
53
721
  const issues = [];
54
- const filter = normalizeFilter(input.filter);
55
- for (const bound of [filter?.minDevelopmentAge, filter?.maxDevelopmentAge])
56
- if (bound !== undefined && (!Number.isSafeInteger(bound) || bound < 0))
57
- issues.push({ domain: "configuration", code: "invalid-configuration", path: "$.filter", message: "Development-age bounds must be nonnegative safe integers" });
58
- if (filter?.minDevelopmentAge !== undefined && filter.maxDevelopmentAge !== undefined && filter.minDevelopmentAge > filter.maxDevelopmentAge)
59
- issues.push({ domain: "configuration", code: "invalid-configuration", path: "$.filter", message: "Minimum development age exceeds maximum" });
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]));
60
730
  input.losses.forEach((row, index) => {
61
731
  if (row.rowType !== definition.definition.lossRowGrain)
62
- issues.push({ domain: "input", code: "invalid-input-relationship", path: `$.losses[${index}].rowType`, message: "Loss row type does not match definition grain" });
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
+ });
63
738
  });
64
739
  input.exposures.forEach((row, index) => {
65
740
  const measure = internals.measuresById.get(row.measureId);
66
- if (measure?.exposureTiming === "valuation-specific" && row.valuation === undefined)
67
- issues.push({ domain: "input", code: "missing-required", path: `$.exposures[${index}].valuation`, message: "Valuation-specific exposure requires valuation" });
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
+ });
68
799
  });
69
800
  if (issues.length > 0)
70
801
  throw new DiagnosticValidationError(issues);
71
- const normalizedLosses = [];
72
- const audit = [];
73
- for (const row of input.losses) {
74
- let disposition = "retained";
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;
75
824
  try {
76
- const period = diagnosticDevelopmentAge(definition, row.origin, row.valuation);
77
- const normalized = { ...row, origin: period.origin.label, valuation: period.valuation.label };
78
- if (!selected(filter, row.sourceGroup, normalized.origin, normalized.valuation, period.developmentAge))
79
- disposition = "filter";
80
- if (disposition === "retained")
81
- normalizedLosses.push(normalized);
82
- audit.push({ kind: "loss", disposition, record: normalized });
825
+ origin = normalizeDiagnosticPeriod(definition, "origin", candidate.row.origin);
83
826
  }
84
827
  catch {
85
- audit.push({ kind: "loss", disposition: "invalid", record: row });
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
+ }));
86
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";
87
900
  }
88
- const derivedLosses = definition.definition.lossRowGrain === "claim"
89
- ? deriveDiagnosticClaimMeasures(normalizedLosses, definition)
90
- : normalizedLosses;
91
- const cells = new Map();
92
- for (const row of derivedLosses) {
93
- const key = `${row.sourceGroup}\u0000${row.origin}\u0000${row.valuation}`;
94
- const list = cells.get(key) ?? [];
95
- list.push(row);
96
- cells.set(key, list);
97
- }
98
- const normalizedExposureInputs = [];
99
- for (const row of input.exposures) {
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;
100
917
  try {
101
- const origin = normalizeDiagnosticPeriod(definition, "origin", row.origin).label;
102
- const valuation = row.valuation === undefined ? undefined : normalizeDiagnosticPeriod(definition, "valuation", row.valuation).label;
103
- const normalized = { ...row, origin, ...(valuation === undefined ? {} : { valuation }) };
104
- const timing = internals.measuresById.get(row.measureId)?.exposureTiming;
105
- const age = valuation === undefined ? 0 : diagnosticDevelopmentAge(definition, origin, valuation).developmentAge;
106
- const keep = timing === "origin-static"
107
- ? (filter?.sourceGroups === undefined || filter.sourceGroups.includes(row.sourceGroup)) && (filter?.origins === undefined || filter.origins.includes(origin))
108
- : selected(filter, row.sourceGroup, origin, valuation ?? "", age);
109
- audit.push({ kind: "exposure", disposition: keep ? "retained" : "filter", record: normalized });
110
- if (keep)
111
- normalizedExposureInputs.push(normalized);
918
+ candidate.origin = normalizeDiagnosticPeriod(definition, "origin", candidate.row.origin);
112
919
  }
113
920
  catch {
114
- audit.push({ kind: "exposure", disposition: "invalid", record: row });
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 });
115
1186
  }
116
1187
  }
117
- const timingByMeasure = Object.fromEntries(definition.definition.measures.filter((measure) => measure.kind === "exposure").map((measure) => [measure.id, measure.exposureTiming]));
118
- const exposures = reconcileDiagnosticExposures(normalizedExposureInputs, timingByMeasure);
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));
119
1213
  const preparedCells = [];
120
- for (const [key, rows] of cells) {
121
- const [sourceGroup, origin, valuation] = key.split("\u0000");
122
- const period = diagnosticDevelopmentAge(definition, origin, valuation);
123
- const contributions = Object.create(null);
124
- const blockers = Object.create(null);
125
- for (const measure of definition.definition.measures) {
126
- contributions[measure.id] = [];
127
- blockers[measure.id] = [];
128
- }
129
- for (const row of rows) {
130
- for (const measure of definition.definition.measures.filter((item) => item.source !== "exposure")) {
131
- contributions[measure.id].push(auditedDiagnosticContribution(row.recordId, own(row.measures, measure.id), measure.missing, row.source ? [row.source] : []));
132
- }
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
+ }
133
1229
  }
134
- for (const exposure of exposures) {
135
- if (exposure.status !== "valid" || exposure.sourceGroup !== sourceGroup || exposure.origin !== origin || (exposure.valuation !== undefined && exposure.valuation !== valuation))
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))
136
1324
  continue;
137
- contributions[exposure.measureId].push(auditedDiagnosticContribution(exposure.key, exposure.value, "unknown", exposure.sources, exposure.deduplicated));
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
+ }
138
1363
  }
139
- for (const measure of definition.definition.measures.filter((item) => item.source === "exposure")) {
140
- if (contributions[measure.id].length === 0)
141
- blockers[measure.id].push({ code: "loss-without-exposure", message: "Loss cell has no applicable exposure observation", sourceIds: rows.map((row) => row.recordId).sort(codeUnit), sources: [] });
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);
142
1372
  }
143
- const components = Object.fromEntries(definition.definition.measures.map((measure) => [measure.id, finalizeDiagnosticContributions(contributions[measure.id], measure.missing, blockers[measure.id])]));
144
- preparedCells.push(deepFreeze({ sourceGroup, origin, valuation, developmentAge: period.developmentAge, ageUnit: period.ageUnit, lossRecordIds: rows.map((row) => row.recordId).sort(codeUnit), contributions, components, structuralBlockers: blockers, findings: [] }));
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
+ }));
145
1441
  }
146
- preparedCells.sort((a, b) => codeUnit(a.sourceGroup, b.sourceGroup) || codeUnit(a.origin, b.origin) || codeUnit(a.valuation, b.valuation));
147
- const expectedCells = (input.expectedCells ?? []).map((item) => ({ ...item }));
148
- for (const item of expectedCells)
149
- audit.push({ kind: "expected-cell", disposition: "retained", record: item });
150
- const identity = deepFreeze({ definitionIntegrity: definition.definitionIntegrity, filter, completePeriodCutoffs: [...(input.completePeriodCutoffs ?? [])], inputAudit: audit, cells: preparedCells, exposures, expectedCellsProvided: input.expectedCells !== undefined, expectedCells, findings: [] });
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
+ });
151
1501
  const preparationFingerprint = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
152
- const preparedBase = { definition, preparationFingerprint, filter, completePeriodCutoffs: input.completePeriodCutoffs ?? [], inputAudit: audit, cells: preparedCells, exposures, expectedCellsProvided: input.expectedCells !== undefined, expectedCells, findings: [] };
153
- const prepared = deepFreeze(preparedBase);
1502
+ const prepared = deepFreeze({
1503
+ definition,
1504
+ preparationFingerprint,
1505
+ ...ownedData,
1506
+ });
154
1507
  authentic.add(prepared);
155
1508
  identities.set(prepared, identity);
156
1509
  return prepared;
157
1510
  }
158
1511
  export function assertPreparedDiagnosticData(value) {
159
1512
  if (value === null || typeof value !== "object" || !authentic.has(value))
160
- throw new DiagnosticValidationError([{ domain: "input", code: "invalid-input-relationship", path: "$.prepared", message: "Value is not authentic prepared diagnostic data" }]);
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);
161
1525
  }
162
- export function getPreparedDiagnosticDataIdentity(value) { assertPreparedDiagnosticData(value); return identities.get(value); }
163
1526
  export function verifyPreparedDiagnosticDataIntegrity(value) {
164
1527
  const identity = getPreparedDiagnosticDataIdentity(value);
165
1528
  const tag = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
166
1529
  if (tag !== value.preparationFingerprint)
167
- throw new DiagnosticValidationError([{ domain: "input", code: "invalid-input-relationship", path: "$.prepared.preparationFingerprint", message: "Prepared diagnostic data integrity does not match its content" }]);
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
+ ]);
168
1538
  }
169
1539
  //# sourceMappingURL=diagnosticPreparation.js.map