@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.
- package/README.md +12 -3
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +136 -26
- package/dist/canonical.js.map +1 -1
- 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/diagnosticEvidenceIntern.d.ts +16 -0
- package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
- package/dist/diagnosticEvidenceIntern.js +139 -0
- package/dist/diagnosticEvidenceIntern.js.map +1 -0
- package/dist/diagnosticExposure.d.ts +2 -2
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +319 -21
- 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 +321 -51
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticIdentityStream.d.ts +56 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -0
- package/dist/diagnosticIdentityStream.js +472 -0
- package/dist/diagnosticIdentityStream.js.map +1 -0
- 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 +75 -14
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +1606 -112
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +40 -15
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +600 -42
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +18 -0
- package/dist/diagnosticReviewSources.d.ts.map +1 -0
- package/dist/diagnosticReviewSources.js +76 -0
- package/dist/diagnosticReviewSources.js.map +1 -0
- package/dist/diagnosticReviewStore.d.ts +88 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -0
- package/dist/diagnosticReviewStore.js +455 -0
- package/dist/diagnosticReviewStore.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +27 -2
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +764 -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/fnvAccumulator.d.ts +6 -0
- package/dist/fnvAccumulator.d.ts.map +1 -0
- package/dist/fnvAccumulator.js +21 -0
- package/dist/fnvAccumulator.js.map +1 -0
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- 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/canonical.ts +144 -26
- 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/diagnosticEvidenceIntern.ts +151 -0
- package/src/diagnosticExposure.ts +550 -50
- package/src/diagnosticExpressions.ts +99 -17
- package/src/diagnosticFormulas.ts +239 -39
- package/src/diagnosticIdentity.ts +700 -191
- package/src/diagnosticIdentityStream.ts +553 -0
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +67 -82
- package/src/diagnosticPreparation.ts +2558 -132
- package/src/diagnosticReview.ts +1072 -58
- package/src/diagnosticReviewSources.ts +85 -0
- package/src/diagnosticReviewStore.ts +692 -0
- package/src/diagnosticRunner.ts +1273 -72
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/fnvAccumulator.ts +20 -0
- package/src/index.ts +69 -4
- 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,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 {
|
|
5
|
-
import {
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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({
|
|
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" &&
|
|
67
|
-
|
|
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
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
preparedCells.push(
|
|
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((
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
153
|
-
|
|
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([
|
|
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([
|
|
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
|