@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.
- package/README.md +3 -3
- package/dist/capping.d.ts.map +1 -1
- package/dist/capping.js +39 -6
- package/dist/capping.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +3 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +30 -7
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticDefinitions.d.ts +3 -0
- package/dist/diagnosticDefinitions.d.ts.map +1 -1
- package/dist/diagnosticDefinitions.js +844 -182
- package/dist/diagnosticDefinitions.js.map +1 -1
- package/dist/diagnosticDerivations.d.ts +23 -0
- package/dist/diagnosticDerivations.d.ts.map +1 -1
- package/dist/diagnosticDerivations.js +187 -43
- package/dist/diagnosticDerivations.js.map +1 -1
- package/dist/diagnosticExposure.d.ts +2 -2
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +228 -20
- package/dist/diagnosticExposure.js.map +1 -1
- package/dist/diagnosticExpressions.d.ts +1 -1
- package/dist/diagnosticExpressions.d.ts.map +1 -1
- package/dist/diagnosticExpressions.js +35 -15
- package/dist/diagnosticExpressions.js.map +1 -1
- package/dist/diagnosticFormulas.d.ts +3 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -1
- package/dist/diagnosticFormulas.js +160 -26
- package/dist/diagnosticFormulas.js.map +1 -1
- package/dist/diagnosticIdentity.d.ts +15 -1
- package/dist/diagnosticIdentity.d.ts.map +1 -1
- package/dist/diagnosticIdentity.js +318 -51
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticOrdering.d.ts +8 -0
- package/dist/diagnosticOrdering.d.ts.map +1 -0
- package/dist/diagnosticOrdering.js +73 -0
- package/dist/diagnosticOrdering.js.map +1 -0
- package/dist/diagnosticPeriodAxis.d.ts +9 -0
- package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
- package/dist/diagnosticPeriodAxis.js +69 -0
- package/dist/diagnosticPeriodAxis.js.map +1 -0
- package/dist/diagnosticPeriods.d.ts +2 -3
- package/dist/diagnosticPeriods.d.ts.map +1 -1
- package/dist/diagnosticPeriods.js +20 -68
- package/dist/diagnosticPeriods.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +47 -14
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +1482 -112
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +36 -14
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +448 -42
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticRunner.d.ts +2 -1
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +656 -70
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/diagnosticRuntime.d.ts +26 -0
- package/dist/diagnosticRuntime.d.ts.map +1 -0
- package/dist/diagnosticRuntime.js +243 -0
- package/dist/diagnosticRuntime.js.map +1 -0
- package/dist/diagnosticSourceOrdering.d.ts +6 -0
- package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
- package/dist/diagnosticSourceOrdering.js +38 -0
- package/dist/diagnosticSourceOrdering.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mack.d.ts +4 -0
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +32 -4
- package/dist/mack.js.map +1 -1
- package/dist/triangle.d.ts.map +1 -1
- package/dist/triangle.js +59 -9
- package/dist/triangle.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -8
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/capping.ts +109 -18
- package/src/diagnosticAggregation.ts +69 -20
- package/src/diagnosticDefinitions.ts +2675 -426
- package/src/diagnosticDerivations.ts +281 -49
- package/src/diagnosticExposure.ts +441 -50
- package/src/diagnosticExpressions.ts +99 -17
- package/src/diagnosticFormulas.ts +239 -39
- package/src/diagnosticIdentity.ts +691 -191
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +67 -82
- package/src/diagnosticPreparation.ts +2369 -127
- package/src/diagnosticReview.ts +864 -58
- package/src/diagnosticRunner.ts +1076 -72
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/index.ts +31 -3
- package/src/mack.ts +72 -9
- package/src/triangle.ts +103 -13
- package/src/types.ts +55 -20
- 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 {
|
|
5
|
-
import {
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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({
|
|
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" &&
|
|
67
|
-
|
|
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
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
118
|
-
const
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
blockers[
|
|
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(
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
const
|
|
148
|
-
for (const
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
153
|
-
|
|
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([
|
|
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([
|
|
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
|