@actuarial-ts/data 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/diagnosticInput.d.ts +3 -3
- package/dist/diagnosticInput.d.ts.map +1 -1
- package/dist/diagnosticInput.js +386 -38
- package/dist/diagnosticInput.js.map +1 -1
- package/dist/diagnosticPreparedReview.d.ts +9 -6
- package/dist/diagnosticPreparedReview.d.ts.map +1 -1
- package/dist/diagnosticPreparedReview.js +425 -34
- package/dist/diagnosticPreparedReview.js.map +1 -1
- package/dist/exposure.d.ts.map +1 -1
- package/dist/exposure.js +25 -9
- package/dist/exposure.js.map +1 -1
- package/dist/lossRun.d.ts.map +1 -1
- package/dist/lossRun.js +31 -8
- package/dist/lossRun.js.map +1 -1
- package/dist/review.d.ts +1 -2
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +33 -4
- package/dist/review.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -3
- package/src/diagnosticInput.ts +594 -46
- package/src/diagnosticPreparedReview.ts +651 -37
- package/src/exposure.ts +61 -16
- package/src/lossRun.ts +55 -13
- package/src/review.ts +177 -40
- package/src/version.ts +1 -1
|
@@ -1,39 +1,653 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
import {
|
|
2
|
+
DiagnosticValidationError,
|
|
3
|
+
assertPreparedDiagnosticData,
|
|
4
|
+
canonicalJson,
|
|
5
|
+
evaluateDiagnosticReviewRules,
|
|
6
|
+
fnv1a64,
|
|
7
|
+
compareDiagnosticSourceLocations,
|
|
8
|
+
compareDiagnosticIdentityValues,
|
|
9
|
+
normalizeDiagnosticSourceLocations,
|
|
10
|
+
projectDiagnosticIdentity,
|
|
11
|
+
type DiagnosticIdentityProjection,
|
|
12
|
+
type DiagnosticDeepReadonly,
|
|
13
|
+
type DiagnosticMetricFinding,
|
|
14
|
+
type DiagnosticReviewRuleEvaluation,
|
|
15
|
+
type DiagnosticSourceLocation,
|
|
16
|
+
type PreparedDiagnosticData,
|
|
17
|
+
} from "@actuarial-ts/core";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import {
|
|
20
|
+
createNotEvaluatedDataCheck,
|
|
21
|
+
createStructuredDataCheck,
|
|
22
|
+
summarizeDataChecks,
|
|
23
|
+
type DataCheck,
|
|
24
|
+
type DataFinding,
|
|
25
|
+
type DataFindingContext,
|
|
26
|
+
type DataReviewReport,
|
|
27
|
+
} from "./review.js";
|
|
28
|
+
|
|
29
|
+
export interface DiagnosticGroupingAssignment {
|
|
30
|
+
readonly key: string;
|
|
31
|
+
readonly group: string;
|
|
32
|
+
readonly source?: DiagnosticSourceLocation;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface DiagnosticCachedFormulaEvidence {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly source?: DiagnosticSourceLocation;
|
|
38
|
+
readonly formula?: string;
|
|
39
|
+
readonly cachedValue?: number | null;
|
|
40
|
+
readonly declaredFormulaSource: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DiagnosticReviewEvidence {
|
|
44
|
+
readonly groupingAssignments: readonly DiagnosticGroupingAssignment[];
|
|
45
|
+
readonly cachedFormulas: readonly DiagnosticCachedFormulaEvidence[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DiagnosticReviewIdentityBody {
|
|
49
|
+
readonly definitionIntegrity: string;
|
|
50
|
+
readonly preparationFingerprint: string;
|
|
51
|
+
readonly evidence: DiagnosticIdentityProjection<DiagnosticReviewEvidence> | null;
|
|
52
|
+
readonly checks: readonly {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly status: DataCheck["status"];
|
|
55
|
+
readonly findings: readonly DiagnosticIdentityProjection<DataFinding>[];
|
|
56
|
+
}[];
|
|
57
|
+
readonly summary: DataReviewReport["summary"];
|
|
58
|
+
readonly evaluations: readonly DiagnosticIdentityProjection<DiagnosticReviewRuleEvaluation>[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface DiagnosticReviewReceipt {
|
|
62
|
+
readonly definitionIntegrity: string;
|
|
63
|
+
readonly preparationFingerprint: string;
|
|
64
|
+
readonly report: DiagnosticDeepReadonly<DataReviewReport>;
|
|
65
|
+
readonly evaluations: readonly DiagnosticReviewRuleEvaluation[];
|
|
66
|
+
readonly evidence: DiagnosticDeepReadonly<DiagnosticReviewEvidence> | null;
|
|
67
|
+
readonly identityBody: DiagnosticDeepReadonly<DiagnosticReviewIdentityBody>;
|
|
68
|
+
readonly reportFingerprint: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ReviewPreparedDiagnosticDataInput {
|
|
72
|
+
readonly prepared: PreparedDiagnosticData;
|
|
73
|
+
readonly evidence: DiagnosticReviewEvidence | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fixed = [
|
|
77
|
+
["diagnostic/structural/loss-identity", "Loss identities are unique", "fail"],
|
|
78
|
+
[
|
|
79
|
+
"diagnostic/structural/exposure-identity",
|
|
80
|
+
"Exposure identities are coherent",
|
|
81
|
+
"fail",
|
|
82
|
+
],
|
|
83
|
+
["diagnostic/structural/period-validity", "Periods are valid", "fail"],
|
|
84
|
+
[
|
|
85
|
+
"diagnostic/structural/measure-contract",
|
|
86
|
+
"Measure keys match their declared sources",
|
|
87
|
+
"fail",
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
"diagnostic/structural/loss-completeness",
|
|
91
|
+
"Loss records are complete",
|
|
92
|
+
"fail",
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"diagnostic/structural/exposure-completeness",
|
|
96
|
+
"Exposure records are complete and finite",
|
|
97
|
+
"fail",
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
"diagnostic/structural/loss-without-exposure",
|
|
101
|
+
"Loss cells have required exposure",
|
|
102
|
+
"warning",
|
|
103
|
+
],
|
|
104
|
+
[
|
|
105
|
+
"diagnostic/structural/exposure-without-loss",
|
|
106
|
+
"Exposures attach to retained loss cells",
|
|
107
|
+
"warning",
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
"diagnostic/structural/expected-cell-coverage",
|
|
111
|
+
"Expected cells are present",
|
|
112
|
+
"fail",
|
|
113
|
+
],
|
|
114
|
+
[
|
|
115
|
+
"diagnostic/structural/grouping-consistency",
|
|
116
|
+
"Grouping assignments are consistent",
|
|
117
|
+
"fail",
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"diagnostic/structural/cached-formula-provenance",
|
|
121
|
+
"Cached formulas retain provenance",
|
|
122
|
+
"warning",
|
|
123
|
+
],
|
|
23
124
|
] as const;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
125
|
+
|
|
126
|
+
const codeToCheck: Readonly<Record<string, string>> = {
|
|
127
|
+
"duplicate-loss-record-id": fixed[0][0],
|
|
128
|
+
"duplicate-claim-snapshot": fixed[0][0],
|
|
129
|
+
"claim-identity-conflict": fixed[0][0],
|
|
130
|
+
"duplicate-aggregate-snapshot": fixed[0][0],
|
|
131
|
+
"duplicate-exposure-identity": fixed[1][0],
|
|
132
|
+
"conflicting-exposure-identity": fixed[1][0],
|
|
133
|
+
"unknown-origin-period": fixed[2][0],
|
|
134
|
+
"unknown-valuation-period": fixed[2][0],
|
|
135
|
+
"valuation-before-origin": fixed[2][0],
|
|
136
|
+
"unsafe-development-age": fixed[2][0],
|
|
137
|
+
"undeclared-loss-measure": fixed[3][0],
|
|
138
|
+
"wrong-source-loss-measure": fixed[3][0],
|
|
139
|
+
"undeclared-exposure-measure": fixed[3][0],
|
|
140
|
+
"wrong-source-exposure-measure": fixed[3][0],
|
|
141
|
+
"incomplete-loss-record": fixed[4][0],
|
|
142
|
+
"missing-exposure-value": fixed[5][0],
|
|
143
|
+
"incomplete-exposure": fixed[5][0],
|
|
144
|
+
"non-finite-exposure": fixed[5][0],
|
|
145
|
+
"loss-without-exposure": fixed[6][0],
|
|
146
|
+
"exposure-without-loss": fixed[7][0],
|
|
147
|
+
"missing-expected-cell": fixed[8][0],
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const sourceSchema = z
|
|
151
|
+
.object({
|
|
152
|
+
artifactId: z.string().min(1),
|
|
153
|
+
sourceFile: z.string().min(1).optional(),
|
|
154
|
+
sourceSheet: z.string().min(1).optional(),
|
|
155
|
+
sourceRow: z.number().int().nonnegative().optional(),
|
|
156
|
+
sourceCell: z.string().min(1).optional(),
|
|
157
|
+
})
|
|
158
|
+
.strict();
|
|
159
|
+
|
|
160
|
+
const evidenceSchema = z
|
|
161
|
+
.object({
|
|
162
|
+
groupingAssignments: z.array(
|
|
163
|
+
z
|
|
164
|
+
.object({
|
|
165
|
+
key: z.string().min(1),
|
|
166
|
+
group: z.string().min(1),
|
|
167
|
+
source: sourceSchema.optional(),
|
|
168
|
+
})
|
|
169
|
+
.strict(),
|
|
170
|
+
),
|
|
171
|
+
cachedFormulas: z.array(
|
|
172
|
+
z
|
|
173
|
+
.object({
|
|
174
|
+
id: z.string().min(1),
|
|
175
|
+
source: sourceSchema.optional(),
|
|
176
|
+
formula: z
|
|
177
|
+
.string()
|
|
178
|
+
.min(1)
|
|
179
|
+
.refine(
|
|
180
|
+
(value) => value.trim().length > 0,
|
|
181
|
+
"Formula must contain non-whitespace text",
|
|
182
|
+
)
|
|
183
|
+
.optional(),
|
|
184
|
+
cachedValue: z.number().finite().nullable().optional(),
|
|
185
|
+
declaredFormulaSource: z.boolean(),
|
|
186
|
+
})
|
|
187
|
+
.strict(),
|
|
188
|
+
),
|
|
189
|
+
})
|
|
190
|
+
.strict();
|
|
191
|
+
|
|
192
|
+
function freeze<T>(
|
|
193
|
+
value: T,
|
|
194
|
+
seen = new WeakSet<object>(),
|
|
195
|
+
): DiagnosticDeepReadonly<T> {
|
|
196
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
197
|
+
return value as DiagnosticDeepReadonly<T>;
|
|
198
|
+
seen.add(value);
|
|
199
|
+
for (const child of Object.values(value as Record<string, unknown>))
|
|
200
|
+
freeze(child, seen);
|
|
201
|
+
return Object.freeze(value) as DiagnosticDeepReadonly<T>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function issuePath(root: string, path: readonly PropertyKey[]): string {
|
|
205
|
+
return `${root}${path.map((part) => (typeof part === "number" ? `[${part}]` : /^[A-Za-z_$][\w$]*$/.test(String(part)) ? `.${String(part)}` : `[${JSON.stringify(String(part))}]`)).join("")}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function compareOptional<T>(
|
|
209
|
+
left: T | undefined,
|
|
210
|
+
right: T | undefined,
|
|
211
|
+
compare: (a: T, b: T) => number,
|
|
212
|
+
): number {
|
|
213
|
+
if (left === undefined) return right === undefined ? 0 : -1;
|
|
214
|
+
if (right === undefined) return 1;
|
|
215
|
+
return compare(left, right);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function compareSourceArrays(
|
|
219
|
+
left: readonly DiagnosticSourceLocation[],
|
|
220
|
+
right: readonly DiagnosticSourceLocation[],
|
|
221
|
+
): number {
|
|
222
|
+
for (let index = 0; index < Math.min(left.length, right.length); index++) {
|
|
223
|
+
const compared = compareDiagnosticSourceLocations(
|
|
224
|
+
left[index]!,
|
|
225
|
+
right[index]!,
|
|
226
|
+
);
|
|
227
|
+
if (compared !== 0) return compared;
|
|
228
|
+
}
|
|
229
|
+
return left.length - right.length;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeDataFindings(values: readonly DataFinding[]): DataFinding[] {
|
|
233
|
+
const merged = new Map<string, DataFinding>();
|
|
234
|
+
for (const finding of values) {
|
|
235
|
+
const context = finding.context;
|
|
236
|
+
const reviewScope = context?.reviewScope;
|
|
237
|
+
const key = canonicalJson({
|
|
238
|
+
...finding,
|
|
239
|
+
...(context === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: {
|
|
242
|
+
context: {
|
|
243
|
+
...context,
|
|
244
|
+
sources: [],
|
|
245
|
+
...(reviewScope === undefined
|
|
246
|
+
? {}
|
|
247
|
+
: { reviewScope: { ...reviewScope, sources: [] } }),
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
const previous = merged.get(key);
|
|
252
|
+
const previousContext = previous?.context;
|
|
253
|
+
const normalizedContext =
|
|
254
|
+
context === undefined
|
|
255
|
+
? undefined
|
|
256
|
+
: {
|
|
257
|
+
...context,
|
|
258
|
+
...(context.sources === undefined &&
|
|
259
|
+
previousContext?.sources === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
sources: normalizeDiagnosticSourceLocations([
|
|
263
|
+
...(previousContext?.sources ?? []),
|
|
264
|
+
...(context.sources ?? []),
|
|
265
|
+
]),
|
|
266
|
+
}),
|
|
267
|
+
...(reviewScope === undefined
|
|
268
|
+
? {}
|
|
269
|
+
: {
|
|
270
|
+
reviewScope: {
|
|
271
|
+
...reviewScope,
|
|
272
|
+
sources: normalizeDiagnosticSourceLocations([
|
|
273
|
+
...(previousContext?.reviewScope?.sources ?? []),
|
|
274
|
+
...reviewScope.sources,
|
|
275
|
+
]),
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
};
|
|
279
|
+
merged.set(key, {
|
|
280
|
+
...finding,
|
|
281
|
+
...(normalizedContext === undefined
|
|
282
|
+
? {}
|
|
283
|
+
: { context: normalizedContext }),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const text = (left: string, right: string) =>
|
|
287
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
288
|
+
const field = (finding: DataFinding, key: keyof DataFindingContext) =>
|
|
289
|
+
finding.context?.[key];
|
|
290
|
+
const textField = (
|
|
291
|
+
left: DataFinding,
|
|
292
|
+
right: DataFinding,
|
|
293
|
+
key: keyof DataFindingContext,
|
|
294
|
+
) =>
|
|
295
|
+
compareOptional(
|
|
296
|
+
field(left, key) as string | undefined,
|
|
297
|
+
field(right, key) as string | undefined,
|
|
298
|
+
text,
|
|
299
|
+
);
|
|
300
|
+
return [...merged.values()].sort(
|
|
301
|
+
(left, right) =>
|
|
302
|
+
text(left.code, right.code) ||
|
|
303
|
+
textField(left, right, "ruleId") ||
|
|
304
|
+
textField(left, right, "measureId") ||
|
|
305
|
+
textField(left, right, "offendingKey") ||
|
|
306
|
+
textField(left, right, "groupingKey") ||
|
|
307
|
+
textField(left, right, "cachedEvidenceId") ||
|
|
308
|
+
textField(left, right, "sourceGroup") ||
|
|
309
|
+
textField(left, right, "group") ||
|
|
310
|
+
textField(left, right, "origin") ||
|
|
311
|
+
textField(left, right, "valuation") ||
|
|
312
|
+
compareOptional(
|
|
313
|
+
field(left, "developmentAge") as number | undefined,
|
|
314
|
+
field(right, "developmentAge") as number | undefined,
|
|
315
|
+
(a, b) => a - b,
|
|
316
|
+
) ||
|
|
317
|
+
textField(left, right, "ageUnit") ||
|
|
318
|
+
textField(left, right, "recordId") ||
|
|
319
|
+
textField(left, right, "claimId") ||
|
|
320
|
+
textField(left, right, "exposureKey") ||
|
|
321
|
+
text(
|
|
322
|
+
canonicalJson(
|
|
323
|
+
left.context?.reviewScope === undefined
|
|
324
|
+
? null
|
|
325
|
+
: { ...left.context.reviewScope, sources: [] },
|
|
326
|
+
),
|
|
327
|
+
canonicalJson(
|
|
328
|
+
right.context?.reviewScope === undefined
|
|
329
|
+
? null
|
|
330
|
+
: { ...right.context.reviewScope, sources: [] },
|
|
331
|
+
),
|
|
332
|
+
) ||
|
|
333
|
+
textField(left, right, "sourceFile") ||
|
|
334
|
+
compareOptional(
|
|
335
|
+
field(left, "sourceRow") as number | undefined,
|
|
336
|
+
field(right, "sourceRow") as number | undefined,
|
|
337
|
+
(a, b) => a - b,
|
|
338
|
+
) ||
|
|
339
|
+
text(left.message, right.message) ||
|
|
340
|
+
compareSourceArrays(
|
|
341
|
+
left.context?.sources ?? [],
|
|
342
|
+
right.context?.sources ?? [],
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function validateDiagnosticReviewEvidence(
|
|
348
|
+
value: unknown,
|
|
349
|
+
root = "$.evidence",
|
|
350
|
+
): DiagnosticDeepReadonly<DiagnosticReviewEvidence> {
|
|
351
|
+
const undefinedPaths: string[] = [];
|
|
352
|
+
const stack: { readonly value: unknown; readonly path: string }[] = [
|
|
353
|
+
{ value, path: root },
|
|
354
|
+
];
|
|
355
|
+
const seen = new WeakSet<object>();
|
|
356
|
+
while (stack.length > 0) {
|
|
357
|
+
const current = stack.pop()!;
|
|
358
|
+
if (
|
|
359
|
+
current.value === null ||
|
|
360
|
+
typeof current.value !== "object" ||
|
|
361
|
+
seen.has(current.value)
|
|
362
|
+
)
|
|
363
|
+
continue;
|
|
364
|
+
seen.add(current.value);
|
|
365
|
+
for (const [key, child] of Object.entries(current.value)) {
|
|
366
|
+
const path = Array.isArray(current.value)
|
|
367
|
+
? `${current.path}[${key}]`
|
|
368
|
+
: issuePath(current.path, [key]);
|
|
369
|
+
if (child === undefined) undefinedPaths.push(path);
|
|
370
|
+
else stack.push({ value: child, path });
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (undefinedPaths.length > 0)
|
|
374
|
+
throw new DiagnosticValidationError(
|
|
375
|
+
undefinedPaths.sort().map((path) => ({
|
|
376
|
+
domain: "input",
|
|
377
|
+
code: "invalid-type",
|
|
378
|
+
path,
|
|
379
|
+
message: "Explicit undefined is not allowed",
|
|
380
|
+
})),
|
|
381
|
+
);
|
|
382
|
+
const parsed = evidenceSchema.safeParse(value);
|
|
383
|
+
if (!parsed.success) {
|
|
384
|
+
throw new DiagnosticValidationError(
|
|
385
|
+
parsed.error.issues.map((issue) => ({
|
|
386
|
+
domain: "input" as const,
|
|
387
|
+
code:
|
|
388
|
+
issue.code === "unrecognized_keys"
|
|
389
|
+
? ("unknown-key" as const)
|
|
390
|
+
: issue.code === "too_small"
|
|
391
|
+
? ("invalid-string" as const)
|
|
392
|
+
: ("invalid-type" as const),
|
|
393
|
+
path: issuePath(root, issue.path),
|
|
394
|
+
message: issue.message,
|
|
395
|
+
})),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
const codeUnit = (left: string, right: string) =>
|
|
399
|
+
left < right ? -1 : left > right ? 1 : 0;
|
|
400
|
+
parsed.data.groupingAssignments.sort(
|
|
401
|
+
(left, right) =>
|
|
402
|
+
codeUnit(left.key, right.key) ||
|
|
403
|
+
codeUnit(left.group, right.group) ||
|
|
404
|
+
compareOptional(
|
|
405
|
+
left.source,
|
|
406
|
+
right.source,
|
|
407
|
+
compareDiagnosticSourceLocations,
|
|
408
|
+
),
|
|
409
|
+
);
|
|
410
|
+
parsed.data.cachedFormulas.sort(
|
|
411
|
+
(left, right) =>
|
|
412
|
+
codeUnit(left.id, right.id) ||
|
|
413
|
+
compareOptional(
|
|
414
|
+
left.source,
|
|
415
|
+
right.source,
|
|
416
|
+
compareDiagnosticSourceLocations,
|
|
417
|
+
) ||
|
|
418
|
+
compareDiagnosticIdentityValues(
|
|
419
|
+
[left.formula, left.cachedValue, left.declaredFormulaSource],
|
|
420
|
+
[right.formula, right.cachedValue, right.declaredFormulaSource],
|
|
421
|
+
),
|
|
422
|
+
);
|
|
423
|
+
return freeze(parsed.data);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function findingContext(finding: DiagnosticMetricFinding): DataFindingContext {
|
|
427
|
+
return {
|
|
428
|
+
...(finding.ruleId === undefined ? {} : { ruleId: finding.ruleId }),
|
|
429
|
+
...(finding.measureId === undefined
|
|
430
|
+
? {}
|
|
431
|
+
: { measureId: finding.measureId }),
|
|
432
|
+
...(finding.expressionPath === undefined
|
|
433
|
+
? {}
|
|
434
|
+
: { expressionPath: finding.expressionPath }),
|
|
435
|
+
...(finding.offendingKey === undefined
|
|
436
|
+
? {}
|
|
437
|
+
: { offendingKey: finding.offendingKey }),
|
|
438
|
+
...(finding.sourceGroup === undefined
|
|
439
|
+
? {}
|
|
440
|
+
: { sourceGroup: finding.sourceGroup }),
|
|
441
|
+
...(finding.group === undefined ? {} : { group: finding.group }),
|
|
442
|
+
...(finding.origin === undefined ? {} : { origin: finding.origin }),
|
|
443
|
+
...(finding.valuation === undefined
|
|
444
|
+
? {}
|
|
445
|
+
: { valuation: finding.valuation }),
|
|
446
|
+
...(finding.developmentAge === undefined
|
|
447
|
+
? {}
|
|
448
|
+
: { developmentAge: finding.developmentAge }),
|
|
449
|
+
...(finding.ageUnit === undefined ? {} : { ageUnit: finding.ageUnit }),
|
|
450
|
+
...(finding.recordId === undefined ? {} : { recordId: finding.recordId }),
|
|
451
|
+
...(finding.claimId === undefined ? {} : { claimId: finding.claimId }),
|
|
452
|
+
...(finding.exposureKey === undefined
|
|
453
|
+
? {}
|
|
454
|
+
: { exposureKey: finding.exposureKey }),
|
|
455
|
+
sources: finding.sources,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function reviewPreparedDiagnosticData(
|
|
460
|
+
input: ReviewPreparedDiagnosticDataInput,
|
|
461
|
+
): DiagnosticReviewReceipt {
|
|
462
|
+
assertPreparedDiagnosticData(input.prepared);
|
|
463
|
+
const evidence =
|
|
464
|
+
input.evidence === null
|
|
465
|
+
? null
|
|
466
|
+
: validateDiagnosticReviewEvidence(input.evidence);
|
|
467
|
+
const evaluations = evaluateDiagnosticReviewRules(input.prepared);
|
|
468
|
+
const findingsByCheck = new Map<string, DataFinding[]>(
|
|
469
|
+
fixed.map(([id]) => [id, []]),
|
|
470
|
+
);
|
|
471
|
+
for (const finding of input.prepared.findings) {
|
|
472
|
+
const id = codeToCheck[finding.code];
|
|
473
|
+
if (id)
|
|
474
|
+
findingsByCheck.get(id)!.push({
|
|
475
|
+
code: finding.code,
|
|
476
|
+
message: finding.message,
|
|
477
|
+
context: findingContext(finding),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (evidence) {
|
|
482
|
+
const assignments = new Map<string, DiagnosticGroupingAssignment[]>();
|
|
483
|
+
for (const item of evidence.groupingAssignments) {
|
|
484
|
+
const values = assignments.get(item.key) ?? [];
|
|
485
|
+
values.push(item);
|
|
486
|
+
assignments.set(item.key, values);
|
|
487
|
+
}
|
|
488
|
+
for (const [key, values] of assignments) {
|
|
489
|
+
if (new Set(values.map((item) => item.group)).size > 1)
|
|
490
|
+
findingsByCheck.get(fixed[9][0])!.push({
|
|
491
|
+
code: "inconsistent-group-mapping",
|
|
492
|
+
message: "Grouping evidence assigns one key to multiple groups",
|
|
493
|
+
context: {
|
|
494
|
+
groupingKey: key,
|
|
495
|
+
sources: normalizeDiagnosticSourceLocations(
|
|
496
|
+
values.map((item) => item.source),
|
|
497
|
+
),
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const failingCached = new Map<string, DiagnosticCachedFormulaEvidence[]>();
|
|
502
|
+
for (const item of evidence.cachedFormulas) {
|
|
503
|
+
const hasOwnCachedValue = Object.prototype.hasOwnProperty.call(
|
|
504
|
+
item,
|
|
505
|
+
"cachedValue",
|
|
506
|
+
);
|
|
507
|
+
if (
|
|
508
|
+
item.declaredFormulaSource &&
|
|
509
|
+
(item.formula === undefined ||
|
|
510
|
+
!hasOwnCachedValue ||
|
|
511
|
+
item.cachedValue === null ||
|
|
512
|
+
item.source === undefined)
|
|
513
|
+
) {
|
|
514
|
+
const values = failingCached.get(item.id) ?? [];
|
|
515
|
+
values.push(item);
|
|
516
|
+
failingCached.set(item.id, values);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const [id, values] of failingCached)
|
|
520
|
+
findingsByCheck.get(fixed[10][0])!.push({
|
|
521
|
+
code: "cached-formula-provenance",
|
|
522
|
+
message:
|
|
523
|
+
"Declared formula-derived value lacks complete formula provenance",
|
|
524
|
+
context: {
|
|
525
|
+
cachedEvidenceId: id,
|
|
526
|
+
sources: normalizeDiagnosticSourceLocations(
|
|
527
|
+
values.map((item) => item.source),
|
|
528
|
+
),
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const hasExposureMeasures =
|
|
534
|
+
input.prepared.definition.definition.measures.some(
|
|
535
|
+
(measure) => measure.source === "exposure",
|
|
536
|
+
);
|
|
537
|
+
const checks: DataCheck[] = fixed.map(
|
|
538
|
+
([id, description, severity], index) => {
|
|
539
|
+
if ([1, 5, 6, 7].includes(index) && !hasExposureMeasures)
|
|
540
|
+
return createNotEvaluatedDataCheck(
|
|
541
|
+
id,
|
|
542
|
+
description,
|
|
543
|
+
"the definition declares no exposure measures",
|
|
544
|
+
);
|
|
545
|
+
if (index === 8 && !input.prepared.expectedCellsProvided)
|
|
546
|
+
return createNotEvaluatedDataCheck(
|
|
547
|
+
id,
|
|
548
|
+
description,
|
|
549
|
+
"the expected-cell grid was omitted",
|
|
550
|
+
);
|
|
551
|
+
if ((index === 9 || index === 10) && evidence === null)
|
|
552
|
+
return createNotEvaluatedDataCheck(
|
|
553
|
+
id,
|
|
554
|
+
description,
|
|
555
|
+
"review evidence was omitted",
|
|
556
|
+
);
|
|
557
|
+
return createStructuredDataCheck(
|
|
558
|
+
id,
|
|
559
|
+
description,
|
|
560
|
+
severity,
|
|
561
|
+
normalizeDataFindings(findingsByCheck.get(id)!),
|
|
562
|
+
);
|
|
563
|
+
},
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
for (const rule of input.prepared.definition.definition.reviewRules) {
|
|
567
|
+
const matching = evaluations.filter((item) => item.ruleId === rule.id);
|
|
568
|
+
const overflow = matching.some(
|
|
569
|
+
(item) => item.expressionOverflows.length > 0,
|
|
570
|
+
);
|
|
571
|
+
const status =
|
|
572
|
+
overflow ||
|
|
573
|
+
matching.some(
|
|
574
|
+
(item) => item.status === "triggered" && item.severity === "fail",
|
|
575
|
+
)
|
|
576
|
+
? "fail"
|
|
577
|
+
: matching.some((item) => item.status === "triggered")
|
|
578
|
+
? "warning"
|
|
579
|
+
: matching.some((item) => item.status === "not-evaluated")
|
|
580
|
+
? "not-evaluated"
|
|
581
|
+
: "pass";
|
|
582
|
+
const findings: DataFinding[] = matching.flatMap((item) => [
|
|
583
|
+
...item.expressionOverflows.map((overflowItem) => ({
|
|
584
|
+
code: "diagnostic-expression-overflow",
|
|
585
|
+
message: "Measure expression overflowed",
|
|
586
|
+
context: {
|
|
587
|
+
ruleId: rule.id,
|
|
588
|
+
expressionPath: overflowItem.expressionPath,
|
|
589
|
+
reviewScope: item.scope,
|
|
590
|
+
...(overflowItem.coordinate === null ? {} : overflowItem.coordinate),
|
|
591
|
+
sources: overflowItem.sources,
|
|
592
|
+
},
|
|
593
|
+
})),
|
|
594
|
+
...(item.status === "triggered"
|
|
595
|
+
? [
|
|
596
|
+
{
|
|
597
|
+
code: rule.code,
|
|
598
|
+
message: rule.description,
|
|
599
|
+
context: {
|
|
600
|
+
ruleId: rule.id,
|
|
601
|
+
reviewScope: item.scope,
|
|
602
|
+
sources: item.scope.sources,
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
]
|
|
606
|
+
: []),
|
|
607
|
+
...(item.status === "not-evaluated"
|
|
608
|
+
? [
|
|
609
|
+
{
|
|
610
|
+
code: "diagnostic-review-rule-not-evaluated",
|
|
611
|
+
message: "Diagnostic review rule was not evaluated",
|
|
612
|
+
context: {
|
|
613
|
+
ruleId: rule.id,
|
|
614
|
+
reviewScope: item.scope,
|
|
615
|
+
sources: item.scope.sources,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
]
|
|
619
|
+
: []),
|
|
620
|
+
]);
|
|
621
|
+
const normalizedFindings = normalizeDataFindings(findings);
|
|
622
|
+
checks.push({
|
|
623
|
+
id: rule.id,
|
|
624
|
+
description: rule.description,
|
|
625
|
+
status,
|
|
626
|
+
details: normalizedFindings.slice(0, 20).map((item) => item.message),
|
|
627
|
+
findings: normalizedFindings,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const report = freeze(summarizeDataChecks(checks));
|
|
632
|
+
const identityBody = projectDiagnosticIdentity({
|
|
633
|
+
definitionIntegrity: input.prepared.definition.definitionIntegrity,
|
|
634
|
+
preparationFingerprint: input.prepared.preparationFingerprint,
|
|
635
|
+
evidence,
|
|
636
|
+
checks: report.checks.map((check) => ({
|
|
637
|
+
id: check.id,
|
|
638
|
+
status: check.status,
|
|
639
|
+
findings: check.findings,
|
|
640
|
+
})),
|
|
641
|
+
summary: report.summary,
|
|
642
|
+
evaluations,
|
|
643
|
+
});
|
|
644
|
+
return freeze({
|
|
645
|
+
definitionIntegrity: input.prepared.definition.definitionIntegrity,
|
|
646
|
+
preparationFingerprint: input.prepared.preparationFingerprint,
|
|
647
|
+
report,
|
|
648
|
+
evaluations,
|
|
649
|
+
evidence,
|
|
650
|
+
identityBody,
|
|
651
|
+
reportFingerprint: `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-review-report", review: identityBody }))}`,
|
|
652
|
+
});
|
|
39
653
|
}
|