@actuarial-ts/core 0.6.0 → 0.7.0

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