@actuarial-ts/data 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.
@@ -1,39 +1,1191 @@
1
- import { canonicalJson, evaluateDiagnosticReviewRules, fnv1a64, type DiagnosticDeepReadonly, type DiagnosticReviewRuleEvaluation, type PreparedDiagnosticData } from "@actuarial-ts/core";
2
- import { createNotEvaluatedDataCheck, createStructuredDataCheck, summarizeDataChecks, type DataCheck, type DataFinding, type DataReviewReport } from "./review.js";
3
-
4
- export interface DiagnosticGroupingAssignment { readonly key: string; readonly group: string; readonly source?: import("@actuarial-ts/core").DiagnosticSourceLocation }
5
- export interface DiagnosticCachedFormulaEvidence { readonly id: string; readonly source?: import("@actuarial-ts/core").DiagnosticSourceLocation; readonly formula?: string; readonly cachedValue?: number|null; readonly declaredFormulaSource: boolean }
6
- export interface DiagnosticReviewEvidence { readonly groupingAssignments: readonly DiagnosticGroupingAssignment[]; readonly cachedFormulas: readonly DiagnosticCachedFormulaEvidence[] }
7
- export interface DiagnosticReviewIdentityBody { readonly definitionIntegrity:string; readonly preparationFingerprint:string; readonly evidence:DiagnosticDeepReadonly<DiagnosticReviewEvidence>|null; readonly checks:readonly {readonly id:string;readonly status:DataCheck["status"];readonly findings:readonly DataFinding[]}[]; readonly summary:DataReviewReport["summary"]; readonly evaluations:readonly DiagnosticReviewRuleEvaluation[] }
8
- export interface DiagnosticReviewReceipt { readonly report:DiagnosticDeepReadonly<DataReviewReport>; readonly evaluations:readonly DiagnosticReviewRuleEvaluation[]; readonly evidence:DiagnosticDeepReadonly<DiagnosticReviewEvidence>|null; readonly identityBody:DiagnosticDeepReadonly<DiagnosticReviewIdentityBody>; readonly reportFingerprint:string }
9
- export interface ReviewPreparedDiagnosticDataInput { readonly prepared:PreparedDiagnosticData; readonly evidence:DiagnosticReviewEvidence|null }
10
-
11
- const fixed=[
12
- ["diagnostic/structural/loss-identity","Loss identities are unique","fail"],
13
- ["diagnostic/structural/exposure-identity","Exposure identities are coherent","fail"],
14
- ["diagnostic/structural/period-validity","Periods are valid","fail"],
15
- ["diagnostic/structural/measure-contract","Measure keys match their declared sources","fail"],
16
- ["diagnostic/structural/loss-completeness","Loss records are complete","fail"],
17
- ["diagnostic/structural/exposure-completeness","Exposure records are complete and finite","fail"],
18
- ["diagnostic/structural/loss-without-exposure","Loss cells have required exposure","warning"],
19
- ["diagnostic/structural/exposure-without-loss","Exposures attach to retained loss cells","warning"],
20
- ["diagnostic/structural/expected-cell-coverage","Expected cells are present","fail"],
21
- ["diagnostic/structural/grouping-consistency","Grouping assignments are consistent","fail"],
22
- ["diagnostic/structural/cached-formula-provenance","Cached formulas retain provenance","warning"],
1
+ import {
2
+ DiagnosticValidationError,
3
+ diagnosticJsonPreflight,
4
+ isDiagnosticToken,
5
+ assertPreparedDiagnosticData,
6
+ assertCompactPreparedDiagnosticData,
7
+ evaluateDiagnosticReviewRulesCompact,
8
+ getDiagnosticReviewEvaluation,
9
+ getDiagnosticReviewEvaluationSummary,
10
+ getCompactDiagnosticReviewEvaluationsIdentityDocument,
11
+ getCompactPreparedDiagnosticDataFingerprint,
12
+ createDiagnosticIdentityArray,
13
+ createDiagnosticIdentityObject,
14
+ createDiagnosticIdentityValue,
15
+ fingerprintDiagnosticIdentity,
16
+ type DiagnosticIdentityDocument,
17
+ canonicalJson,
18
+ evaluateDiagnosticReviewRules,
19
+ fnv1a64,
20
+ compareDiagnosticSourceLocations,
21
+ compareDiagnosticIdentityValues,
22
+ normalizeDiagnosticSourceLocations,
23
+ projectDiagnosticIdentity,
24
+ type DiagnosticIdentityProjection,
25
+ type DiagnosticDeepReadonly,
26
+ type DiagnosticMetricFinding,
27
+ type DiagnosticReviewRuleEvaluation,
28
+ type DiagnosticSourceLocation,
29
+ type PreparedDiagnosticData,
30
+ type PreparedDiagnosticDataContent,
31
+ type CompactPreparedDiagnosticData,
32
+ type CompactDiagnosticReviewEvaluations,
33
+ type DiagnosticReviewPage,
34
+ } from "@actuarial-ts/core";
35
+ import { CompactDiagnosticJson } from "./compactDiagnosticJson.js";
36
+ import { z } from "zod";
37
+ import {
38
+ createNotEvaluatedDataCheck,
39
+ createStructuredDataCheck,
40
+ summarizeDataChecks,
41
+ type DataCheck,
42
+ type DataFinding,
43
+ type DataFindingContext,
44
+ type DataReviewReport,
45
+ } from "./review.js";
46
+
47
+ export interface DiagnosticGroupingAssignment {
48
+ readonly key: string;
49
+ readonly group: string;
50
+ readonly source?: DiagnosticSourceLocation;
51
+ }
52
+
53
+ export interface DiagnosticCachedFormulaEvidence {
54
+ readonly id: string;
55
+ readonly source?: DiagnosticSourceLocation;
56
+ readonly formula?: string;
57
+ readonly cachedValue?: number | null;
58
+ readonly declaredFormulaSource: boolean;
59
+ }
60
+
61
+ export interface DiagnosticReviewEvidence {
62
+ readonly groupingAssignments: readonly DiagnosticGroupingAssignment[];
63
+ readonly cachedFormulas: readonly DiagnosticCachedFormulaEvidence[];
64
+ }
65
+
66
+ export interface DiagnosticReviewIdentityBody {
67
+ readonly definitionIntegrity: string;
68
+ readonly preparationFingerprint: string;
69
+ readonly evidence: DiagnosticIdentityProjection<DiagnosticReviewEvidence> | null;
70
+ readonly checks: readonly {
71
+ readonly id: string;
72
+ readonly status: DataCheck["status"];
73
+ readonly findings: readonly DiagnosticIdentityProjection<DataFinding>[];
74
+ }[];
75
+ readonly summary: DataReviewReport["summary"];
76
+ readonly evaluations: readonly DiagnosticIdentityProjection<DiagnosticReviewRuleEvaluation>[];
77
+ }
78
+
79
+ export interface DiagnosticReviewReceipt {
80
+ readonly definitionIntegrity: string;
81
+ readonly preparationFingerprint: string;
82
+ readonly report: DiagnosticDeepReadonly<DataReviewReport>;
83
+ readonly evaluations: readonly DiagnosticReviewRuleEvaluation[];
84
+ readonly evidence: DiagnosticDeepReadonly<DiagnosticReviewEvidence> | null;
85
+ readonly identityBody: DiagnosticDeepReadonly<DiagnosticReviewIdentityBody>;
86
+ readonly reportFingerprint: string;
87
+ }
88
+
89
+ export interface ReviewPreparedDiagnosticDataInput {
90
+ readonly prepared: PreparedDiagnosticData;
91
+ readonly evidence: DiagnosticReviewEvidence | null;
92
+ }
93
+
94
+ export interface CompactDiagnosticReviewCheck {
95
+ readonly id: string;
96
+ readonly description: string;
97
+ readonly status: DataCheck["status"];
98
+ readonly details: readonly string[];
99
+ readonly findingCount: number;
100
+ }
101
+ declare const compactFindingsBrand: unique symbol;
102
+ export interface CompactDiagnosticReviewFindings {
103
+ readonly [compactFindingsBrand]: true;
104
+ readonly count: number;
105
+ }
106
+ export interface CompactDiagnosticReviewReceipt {
107
+ readonly definitionIntegrity: string;
108
+ readonly report: {
109
+ readonly checks: readonly CompactDiagnosticReviewCheck[];
110
+ readonly summary: DiagnosticDeepReadonly<DataReviewReport["summary"]>;
111
+ };
112
+ readonly evaluations: CompactDiagnosticReviewEvaluations;
113
+ readonly findings: CompactDiagnosticReviewFindings;
114
+ readonly evidence: DiagnosticDeepReadonly<DiagnosticReviewEvidence> | null;
115
+ }
116
+ export interface ReviewPreparedDiagnosticDataCompactInput {
117
+ readonly prepared: CompactPreparedDiagnosticData;
118
+ readonly evidence: DiagnosticReviewEvidence | null;
119
+ }
120
+
121
+ const fixed = [
122
+ ["diagnostic/structural/loss-identity", "Loss identities are unique", "fail"],
123
+ [
124
+ "diagnostic/structural/exposure-identity",
125
+ "Exposure identities are coherent",
126
+ "fail",
127
+ ],
128
+ ["diagnostic/structural/period-validity", "Periods are valid", "fail"],
129
+ [
130
+ "diagnostic/structural/measure-contract",
131
+ "Measure keys match their declared sources",
132
+ "fail",
133
+ ],
134
+ [
135
+ "diagnostic/structural/loss-completeness",
136
+ "Loss records are complete",
137
+ "fail",
138
+ ],
139
+ [
140
+ "diagnostic/structural/exposure-completeness",
141
+ "Exposure records are complete and finite",
142
+ "fail",
143
+ ],
144
+ [
145
+ "diagnostic/structural/loss-without-exposure",
146
+ "Loss cells have required exposure",
147
+ "warning",
148
+ ],
149
+ [
150
+ "diagnostic/structural/exposure-without-loss",
151
+ "Exposures attach to retained loss cells",
152
+ "warning",
153
+ ],
154
+ [
155
+ "diagnostic/structural/expected-cell-coverage",
156
+ "Expected cells are present",
157
+ "fail",
158
+ ],
159
+ [
160
+ "diagnostic/structural/grouping-consistency",
161
+ "Grouping assignments are consistent",
162
+ "fail",
163
+ ],
164
+ [
165
+ "diagnostic/structural/cached-formula-provenance",
166
+ "Cached formulas retain provenance",
167
+ "warning",
168
+ ],
23
169
  ] as const;
24
- const codeToCheck:Record<string,string>={"duplicate-loss-record-id":fixed[0][0],"duplicate-claim-snapshot":fixed[0][0],"claim-identity-conflict":fixed[0][0],"duplicate-aggregate-snapshot":fixed[0][0],"duplicate-exposure-identity":fixed[1][0],"conflicting-exposure-identity":fixed[1][0],"unknown-origin-period":fixed[2][0],"unknown-valuation-period":fixed[2][0],"valuation-before-origin":fixed[2][0],"unsafe-development-age":fixed[2][0],"undeclared-loss-measure":fixed[3][0],"wrong-source-loss-measure":fixed[3][0],"undeclared-exposure-measure":fixed[3][0],"wrong-source-exposure-measure":fixed[3][0],"incomplete-loss-record":fixed[4][0],"missing-exposure-value":fixed[5][0],"incomplete-exposure":fixed[5][0],"non-finite-exposure":fixed[5][0],"loss-without-exposure":fixed[6][0],"exposure-without-loss":fixed[7][0],"missing-expected-cell":fixed[8][0]};
25
- function freeze<T>(value:T):DiagnosticDeepReadonly<T>{if(value&&typeof value==="object"){for(const child of Object.values(value as Record<string,unknown>))freeze(child);Object.freeze(value)}return value as DiagnosticDeepReadonly<T>}
26
-
27
- export function reviewPreparedDiagnosticData(input:ReviewPreparedDiagnosticDataInput):DiagnosticReviewReceipt{
28
- // Authenticity is enforced by the core evaluator before evidence can affect a receipt.
29
- const evaluations=evaluateDiagnosticReviewRules(input.prepared);
30
- const evidence=input.evidence===null?null:freeze(structuredClone(input.evidence));
31
- const findingsByCheck=new Map<string,DataFinding[]>(fixed.map(([id])=>[id,[] as DataFinding[]]));
32
- for(const finding of input.prepared.findings){const id=codeToCheck[finding.code];if(id)findingsByCheck.get(id)!.push({code:finding.code,message:finding.message,context:{measureId:finding.measureId,sourceGroup:finding.sourceGroup,origin:finding.origin,valuation:finding.valuation,developmentAge:finding.developmentAge,ageUnit:finding.ageUnit,sources:finding.sources}})}
33
- if(evidence){const assignments=new Map<string,Set<string>>();for(const item of evidence.groupingAssignments){const values=assignments.get(item.key)??new Set<string>();values.add(item.group);assignments.set(item.key,values)}for(const [key,groups] of assignments)if(groups.size>1)findingsByCheck.get(fixed[9][0])!.push({code:"inconsistent-group-mapping",message:"Grouping evidence assigns one key to multiple groups",context:{groupingKey:key,sources:[]}});for(const item of evidence.cachedFormulas)if(item.declaredFormulaSource&&(item.formula===undefined||item.formula.length===0||item.cachedValue===undefined||item.source===undefined))findingsByCheck.get(fixed[10][0])!.push({code:"cached-formula-provenance",message:"Declared formula-derived value lacks complete formula provenance",context:{cachedEvidenceId:item.id,sources:item.source?[item.source]:[]}})}
34
- const checks:DataCheck[]=fixed.map(([id,description,severity],index)=>{if((index===9||index===10)&&evidence===null)return createNotEvaluatedDataCheck(id,description,"review evidence was omitted");return createStructuredDataCheck(id,description,severity,findingsByCheck.get(id)!)});
35
- for(const rule of input.prepared.definition.definition.reviewRules){const matching=evaluations.filter((item)=>item.ruleId===rule.id);const status=matching.some((item)=>item.status==="triggered"&&item.severity==="fail")?"fail":matching.some((item)=>item.status==="triggered")?"warning":matching.some((item)=>item.status==="not-evaluated")?"not-evaluated":"pass";const findings=matching.filter((item)=>item.status==="triggered").map((item)=>({code:rule.code,message:rule.description,context:{ruleId:rule.id,reviewScope:item.reviewScope,sources:item.reviewScope.sources}}));checks.push({id:rule.id,description:rule.description,status,details:findings.slice(0,20).map((item)=>item.message),findings})}
36
- const report=freeze(summarizeDataChecks(checks));
37
- const identityBody=freeze({definitionIntegrity:input.prepared.definition.definitionIntegrity,preparationFingerprint:input.prepared.preparationFingerprint,evidence,checks:report.checks.map((check)=>({id:check.id,status:check.status,findings:check.findings})),summary:report.summary,evaluations});
38
- return freeze({report,evaluations,evidence,identityBody,reportFingerprint:`fnv1a64-jcs-v1:${fnv1a64(canonicalJson({identityVersion:1,kind:"diagnostic-review",review:identityBody}))}`});
170
+
171
+ const codeToCheck: Readonly<Record<string, string>> = {
172
+ "duplicate-loss-record-id": fixed[0][0],
173
+ "duplicate-claim-snapshot": fixed[0][0],
174
+ "claim-identity-conflict": fixed[0][0],
175
+ "duplicate-aggregate-snapshot": fixed[0][0],
176
+ "duplicate-exposure-identity": fixed[1][0],
177
+ "conflicting-exposure-identity": fixed[1][0],
178
+ "unknown-origin-period": fixed[2][0],
179
+ "unknown-valuation-period": fixed[2][0],
180
+ "valuation-before-origin": fixed[2][0],
181
+ "unsafe-development-age": fixed[2][0],
182
+ "undeclared-loss-measure": fixed[3][0],
183
+ "wrong-source-loss-measure": fixed[3][0],
184
+ "undeclared-exposure-measure": fixed[3][0],
185
+ "wrong-source-exposure-measure": fixed[3][0],
186
+ "incomplete-loss-record": fixed[4][0],
187
+ "missing-exposure-value": fixed[5][0],
188
+ "incomplete-exposure": fixed[5][0],
189
+ "non-finite-exposure": fixed[5][0],
190
+ "loss-without-exposure": fixed[6][0],
191
+ "exposure-without-loss": fixed[7][0],
192
+ "missing-expected-cell": fixed[8][0],
193
+ };
194
+
195
+ const sourceSchema = z
196
+ .object({
197
+ artifactId: z.string().min(1),
198
+ sourceFile: z.string().min(1).optional(),
199
+ sourceSheet: z.string().min(1).optional(),
200
+ sourceRow: z.number().int().nonnegative().optional(),
201
+ sourceCell: z.string().min(1).optional(),
202
+ })
203
+ .strict();
204
+
205
+ const evidenceSchema = z
206
+ .object({
207
+ groupingAssignments: z.array(
208
+ z
209
+ .object({
210
+ key: z.string().min(1),
211
+ group: z.string().min(1),
212
+ source: sourceSchema.optional(),
213
+ })
214
+ .strict(),
215
+ ),
216
+ cachedFormulas: z.array(
217
+ z
218
+ .object({
219
+ id: z.string().min(1),
220
+ source: sourceSchema.optional(),
221
+ formula: z
222
+ .string()
223
+ .min(1)
224
+ .refine(
225
+ (value) => value.trim().length > 0,
226
+ "Formula must contain non-whitespace text",
227
+ )
228
+ .optional(),
229
+ cachedValue: z.number().finite().nullable().optional(),
230
+ declaredFormulaSource: z.boolean(),
231
+ })
232
+ .strict(),
233
+ ),
234
+ })
235
+ .strict();
236
+
237
+ function freeze<T>(
238
+ value: T,
239
+ seen = new WeakSet<object>(),
240
+ ): DiagnosticDeepReadonly<T> {
241
+ if (value === null || typeof value !== "object" || seen.has(value))
242
+ return value as DiagnosticDeepReadonly<T>;
243
+ seen.add(value);
244
+ for (const child of Object.values(value as Record<string, unknown>))
245
+ freeze(child, seen);
246
+ return Object.freeze(value) as DiagnosticDeepReadonly<T>;
247
+ }
248
+
249
+ function issuePath(root: string, path: readonly PropertyKey[]): string {
250
+ return `${root}${path.map((part) => (typeof part === "number" ? `[${part}]` : /^[A-Za-z_$][\w$]*$/.test(String(part)) ? `.${String(part)}` : `[${JSON.stringify(String(part))}]`)).join("")}`;
251
+ }
252
+
253
+ function compareOptional<T>(
254
+ left: T | undefined,
255
+ right: T | undefined,
256
+ compare: (a: T, b: T) => number,
257
+ ): number {
258
+ if (left === undefined) return right === undefined ? 0 : -1;
259
+ if (right === undefined) return 1;
260
+ return compare(left, right);
261
+ }
262
+
263
+ function compareSourceArrays(
264
+ left: readonly DiagnosticSourceLocation[],
265
+ right: readonly DiagnosticSourceLocation[],
266
+ ): number {
267
+ for (let index = 0; index < Math.min(left.length, right.length); index++) {
268
+ const compared = compareDiagnosticSourceLocations(
269
+ left[index]!,
270
+ right[index]!,
271
+ );
272
+ if (compared !== 0) return compared;
273
+ }
274
+ return left.length - right.length;
275
+ }
276
+
277
+ function findingMergeSkeleton(finding: DataFinding): DataFinding {
278
+ const context = finding.context;
279
+ const reviewScope = context?.reviewScope;
280
+ return {
281
+ ...finding,
282
+ ...(context === undefined
283
+ ? {}
284
+ : {
285
+ context: {
286
+ ...context,
287
+ sources: [],
288
+ ...(reviewScope === undefined
289
+ ? {}
290
+ : { reviewScope: { ...reviewScope, sources: [] } }),
291
+ },
292
+ }),
293
+ };
294
+ }
295
+ function findingMergeKey(finding: DataFinding): string {
296
+ return canonicalJson(findingMergeSkeleton(finding));
297
+ }
298
+
299
+ function normalizeDataFindings(values: readonly DataFinding[]): DataFinding[] {
300
+ const merged = new Map<string, DataFinding>();
301
+ for (const finding of values) {
302
+ const context = finding.context;
303
+ const reviewScope = context?.reviewScope;
304
+ const key = findingMergeKey(finding);
305
+ const previous = merged.get(key);
306
+ const previousContext = previous?.context;
307
+ const normalizedContext =
308
+ context === undefined
309
+ ? undefined
310
+ : {
311
+ ...context,
312
+ ...(context.sources === undefined &&
313
+ previousContext?.sources === undefined
314
+ ? {}
315
+ : {
316
+ sources: normalizeDiagnosticSourceLocations([
317
+ ...(previousContext?.sources ?? []),
318
+ ...(context.sources ?? []),
319
+ ]),
320
+ }),
321
+ ...(reviewScope === undefined
322
+ ? {}
323
+ : {
324
+ reviewScope: {
325
+ ...reviewScope,
326
+ sources: normalizeDiagnosticSourceLocations([
327
+ ...(previousContext?.reviewScope?.sources ?? []),
328
+ ...reviewScope.sources,
329
+ ]),
330
+ },
331
+ }),
332
+ };
333
+ merged.set(key, {
334
+ ...finding,
335
+ ...(normalizedContext === undefined
336
+ ? {}
337
+ : { context: normalizedContext }),
338
+ });
339
+ }
340
+ return [...merged.values()].sort(compareDataFindings);
341
+ }
342
+
343
+ function compareDataFindings(left: DataFinding, right: DataFinding): number {
344
+ const text = (left: string, right: string) =>
345
+ left < right ? -1 : left > right ? 1 : 0;
346
+ const field = (finding: DataFinding, key: keyof DataFindingContext) =>
347
+ finding.context?.[key];
348
+ const textField = (
349
+ left: DataFinding,
350
+ right: DataFinding,
351
+ key: keyof DataFindingContext,
352
+ ) =>
353
+ compareOptional(
354
+ field(left, key) as string | undefined,
355
+ field(right, key) as string | undefined,
356
+ text,
357
+ );
358
+ return (
359
+ text(left.code, right.code) ||
360
+ textField(left, right, "ruleId") ||
361
+ textField(left, right, "measureId") ||
362
+ textField(left, right, "offendingKey") ||
363
+ textField(left, right, "groupingKey") ||
364
+ textField(left, right, "cachedEvidenceId") ||
365
+ textField(left, right, "sourceGroup") ||
366
+ textField(left, right, "group") ||
367
+ textField(left, right, "origin") ||
368
+ textField(left, right, "valuation") ||
369
+ compareOptional(
370
+ field(left, "developmentAge") as number | undefined,
371
+ field(right, "developmentAge") as number | undefined,
372
+ (a, b) => a - b,
373
+ ) ||
374
+ textField(left, right, "ageUnit") ||
375
+ textField(left, right, "recordId") ||
376
+ textField(left, right, "claimId") ||
377
+ textField(left, right, "exposureKey") ||
378
+ text(
379
+ canonicalJson(
380
+ left.context?.reviewScope === undefined
381
+ ? null
382
+ : { ...left.context.reviewScope, sources: [] },
383
+ ),
384
+ canonicalJson(
385
+ right.context?.reviewScope === undefined
386
+ ? null
387
+ : { ...right.context.reviewScope, sources: [] },
388
+ ),
389
+ ) ||
390
+ textField(left, right, "sourceFile") ||
391
+ compareOptional(
392
+ field(left, "sourceRow") as number | undefined,
393
+ field(right, "sourceRow") as number | undefined,
394
+ (a, b) => a - b,
395
+ ) ||
396
+ text(left.message, right.message) ||
397
+ compareSourceArrays(
398
+ left.context?.sources ?? [],
399
+ right.context?.sources ?? [],
400
+ )
401
+ );
402
+ }
403
+
404
+ export function validateDiagnosticReviewEvidence(
405
+ value: unknown,
406
+ root = "$.evidence",
407
+ ): DiagnosticDeepReadonly<DiagnosticReviewEvidence> {
408
+ const undefinedPaths: string[] = [];
409
+ const stack: { readonly value: unknown; readonly path: string }[] = [
410
+ { value, path: root },
411
+ ];
412
+ const seen = new WeakSet<object>();
413
+ while (stack.length > 0) {
414
+ const current = stack.pop()!;
415
+ if (
416
+ current.value === null ||
417
+ typeof current.value !== "object" ||
418
+ seen.has(current.value)
419
+ )
420
+ continue;
421
+ seen.add(current.value);
422
+ for (const [key, child] of Object.entries(current.value)) {
423
+ const path = Array.isArray(current.value)
424
+ ? `${current.path}[${key}]`
425
+ : issuePath(current.path, [key]);
426
+ if (child === undefined) undefinedPaths.push(path);
427
+ else stack.push({ value: child, path });
428
+ }
429
+ }
430
+ if (undefinedPaths.length > 0)
431
+ throw new DiagnosticValidationError(
432
+ undefinedPaths.sort().map((path) => ({
433
+ domain: "input",
434
+ code: "invalid-type",
435
+ path,
436
+ message: "Explicit undefined is not allowed",
437
+ })),
438
+ );
439
+ const parsed = evidenceSchema.safeParse(value);
440
+ if (!parsed.success) {
441
+ throw new DiagnosticValidationError(
442
+ parsed.error.issues.map((issue) => ({
443
+ domain: "input" as const,
444
+ code:
445
+ issue.code === "unrecognized_keys"
446
+ ? ("unknown-key" as const)
447
+ : issue.code === "too_small"
448
+ ? ("invalid-string" as const)
449
+ : ("invalid-type" as const),
450
+ path: issuePath(root, issue.path),
451
+ message: issue.message,
452
+ })),
453
+ );
454
+ }
455
+ const codeUnit = (left: string, right: string) =>
456
+ left < right ? -1 : left > right ? 1 : 0;
457
+ parsed.data.groupingAssignments.sort(
458
+ (left, right) =>
459
+ codeUnit(left.key, right.key) ||
460
+ codeUnit(left.group, right.group) ||
461
+ compareOptional(
462
+ left.source,
463
+ right.source,
464
+ compareDiagnosticSourceLocations,
465
+ ),
466
+ );
467
+ parsed.data.cachedFormulas.sort(
468
+ (left, right) =>
469
+ codeUnit(left.id, right.id) ||
470
+ compareOptional(
471
+ left.source,
472
+ right.source,
473
+ compareDiagnosticSourceLocations,
474
+ ) ||
475
+ compareDiagnosticIdentityValues(
476
+ [left.formula, left.cachedValue, left.declaredFormulaSource],
477
+ [right.formula, right.cachedValue, right.declaredFormulaSource],
478
+ ),
479
+ );
480
+ return freeze(parsed.data);
481
+ }
482
+
483
+ function findingContext(finding: DiagnosticMetricFinding): DataFindingContext {
484
+ return {
485
+ ...(finding.ruleId === undefined ? {} : { ruleId: finding.ruleId }),
486
+ ...(finding.measureId === undefined
487
+ ? {}
488
+ : { measureId: finding.measureId }),
489
+ ...(finding.expressionPath === undefined
490
+ ? {}
491
+ : { expressionPath: finding.expressionPath }),
492
+ ...(finding.offendingKey === undefined
493
+ ? {}
494
+ : { offendingKey: finding.offendingKey }),
495
+ ...(finding.sourceGroup === undefined
496
+ ? {}
497
+ : { sourceGroup: finding.sourceGroup }),
498
+ ...(finding.group === undefined ? {} : { group: finding.group }),
499
+ ...(finding.origin === undefined ? {} : { origin: finding.origin }),
500
+ ...(finding.valuation === undefined
501
+ ? {}
502
+ : { valuation: finding.valuation }),
503
+ ...(finding.developmentAge === undefined
504
+ ? {}
505
+ : { developmentAge: finding.developmentAge }),
506
+ ...(finding.ageUnit === undefined ? {} : { ageUnit: finding.ageUnit }),
507
+ ...(finding.recordId === undefined ? {} : { recordId: finding.recordId }),
508
+ ...(finding.claimId === undefined ? {} : { claimId: finding.claimId }),
509
+ ...(finding.exposureKey === undefined
510
+ ? {}
511
+ : { exposureKey: finding.exposureKey }),
512
+ sources: finding.sources,
513
+ };
514
+ }
515
+
516
+ function structuralChecks(
517
+ prepared: PreparedDiagnosticDataContent,
518
+ evidence: DiagnosticDeepReadonly<DiagnosticReviewEvidence> | null,
519
+ ): DataCheck[] {
520
+ const findingsByCheck = new Map<string, DataFinding[]>(
521
+ fixed.map(([id]) => [id, []]),
522
+ );
523
+ for (const finding of prepared.findings) {
524
+ const id = codeToCheck[finding.code];
525
+ if (id)
526
+ findingsByCheck.get(id)!.push({
527
+ code: finding.code,
528
+ message: finding.message,
529
+ context: findingContext(finding),
530
+ });
531
+ }
532
+
533
+ if (evidence) {
534
+ const assignments = new Map<string, DiagnosticGroupingAssignment[]>();
535
+ for (const item of evidence.groupingAssignments) {
536
+ const values = assignments.get(item.key) ?? [];
537
+ values.push(item);
538
+ assignments.set(item.key, values);
539
+ }
540
+ for (const [key, values] of assignments) {
541
+ if (new Set(values.map((item) => item.group)).size > 1)
542
+ findingsByCheck.get(fixed[9][0])!.push({
543
+ code: "inconsistent-group-mapping",
544
+ message: "Grouping evidence assigns one key to multiple groups",
545
+ context: {
546
+ groupingKey: key,
547
+ sources: normalizeDiagnosticSourceLocations(
548
+ values.map((item) => item.source),
549
+ ),
550
+ },
551
+ });
552
+ }
553
+ const failingCached = new Map<string, DiagnosticCachedFormulaEvidence[]>();
554
+ for (const item of evidence.cachedFormulas) {
555
+ const hasOwnCachedValue = Object.prototype.hasOwnProperty.call(
556
+ item,
557
+ "cachedValue",
558
+ );
559
+ if (
560
+ item.declaredFormulaSource &&
561
+ (item.formula === undefined ||
562
+ !hasOwnCachedValue ||
563
+ item.cachedValue === null ||
564
+ item.source === undefined)
565
+ ) {
566
+ const values = failingCached.get(item.id) ?? [];
567
+ values.push(item);
568
+ failingCached.set(item.id, values);
569
+ }
570
+ }
571
+ for (const [id, values] of failingCached)
572
+ findingsByCheck.get(fixed[10][0])!.push({
573
+ code: "cached-formula-provenance",
574
+ message:
575
+ "Declared formula-derived value lacks complete formula provenance",
576
+ context: {
577
+ cachedEvidenceId: id,
578
+ sources: normalizeDiagnosticSourceLocations(
579
+ values.map((item) => item.source),
580
+ ),
581
+ },
582
+ });
583
+ }
584
+
585
+ const hasExposureMeasures = prepared.definition.definition.measures.some(
586
+ (measure) => measure.source === "exposure",
587
+ );
588
+ const checks: DataCheck[] = fixed.map(
589
+ ([id, description, severity], index) => {
590
+ if ([1, 5, 6, 7].includes(index) && !hasExposureMeasures)
591
+ return createNotEvaluatedDataCheck(
592
+ id,
593
+ description,
594
+ "the definition declares no exposure measures",
595
+ );
596
+ if (index === 8 && !prepared.expectedCellsProvided)
597
+ return createNotEvaluatedDataCheck(
598
+ id,
599
+ description,
600
+ "the expected-cell grid was omitted",
601
+ );
602
+ if ((index === 9 || index === 10) && evidence === null)
603
+ return createNotEvaluatedDataCheck(
604
+ id,
605
+ description,
606
+ "review evidence was omitted",
607
+ );
608
+ return createStructuredDataCheck(
609
+ id,
610
+ description,
611
+ severity,
612
+ normalizeDataFindings(findingsByCheck.get(id)!),
613
+ );
614
+ },
615
+ );
616
+ return checks;
617
+ }
618
+
619
+ function evaluationFindings(
620
+ item: DiagnosticReviewRuleEvaluation,
621
+ rule: {
622
+ readonly id: string;
623
+ readonly code: string;
624
+ readonly description: string;
625
+ },
626
+ ): DataFinding[] {
627
+ return [
628
+ ...item.expressionOverflows.map((overflowItem) => ({
629
+ code: "diagnostic-expression-overflow",
630
+ message: "Measure expression overflowed",
631
+ context: {
632
+ ruleId: rule.id,
633
+ expressionPath: overflowItem.expressionPath,
634
+ reviewScope: item.scope,
635
+ ...(overflowItem.coordinate === null ? {} : overflowItem.coordinate),
636
+ sources: overflowItem.sources,
637
+ },
638
+ })),
639
+ ...(item.status === "triggered"
640
+ ? [
641
+ {
642
+ code: rule.code,
643
+ message: rule.description,
644
+ context: {
645
+ ruleId: rule.id,
646
+ reviewScope: item.scope,
647
+ sources: item.scope.sources,
648
+ },
649
+ },
650
+ ]
651
+ : []),
652
+ ...(item.status === "not-evaluated"
653
+ ? [
654
+ {
655
+ code: "diagnostic-review-rule-not-evaluated",
656
+ message: "Diagnostic review rule was not evaluated",
657
+ context: {
658
+ ruleId: rule.id,
659
+ reviewScope: item.scope,
660
+ sources: item.scope.sources,
661
+ },
662
+ },
663
+ ]
664
+ : []),
665
+ ];
666
+ }
667
+
668
+ export function reviewPreparedDiagnosticData(
669
+ input: ReviewPreparedDiagnosticDataInput,
670
+ ): DiagnosticReviewReceipt {
671
+ assertPreparedDiagnosticData(input.prepared);
672
+ const evidence =
673
+ input.evidence === null
674
+ ? null
675
+ : validateDiagnosticReviewEvidence(input.evidence);
676
+ const evaluations = evaluateDiagnosticReviewRules(input.prepared);
677
+ const checks = structuralChecks(input.prepared, evidence);
678
+
679
+ for (const rule of input.prepared.definition.definition.reviewRules) {
680
+ const matching = evaluations.filter((item) => item.ruleId === rule.id);
681
+ const overflow = matching.some(
682
+ (item) => item.expressionOverflows.length > 0,
683
+ );
684
+ const status =
685
+ overflow ||
686
+ matching.some(
687
+ (item) => item.status === "triggered" && item.severity === "fail",
688
+ )
689
+ ? "fail"
690
+ : matching.some((item) => item.status === "triggered")
691
+ ? "warning"
692
+ : matching.some((item) => item.status === "not-evaluated")
693
+ ? "not-evaluated"
694
+ : "pass";
695
+ const findings = matching.flatMap((item) => evaluationFindings(item, rule));
696
+ const normalizedFindings = normalizeDataFindings(findings);
697
+ checks.push({
698
+ id: rule.id,
699
+ description: rule.description,
700
+ status,
701
+ details: normalizedFindings.slice(0, 20).map((item) => item.message),
702
+ findings: normalizedFindings,
703
+ });
704
+ }
705
+
706
+ const report = freeze(summarizeDataChecks(checks));
707
+ const identityBody = projectDiagnosticIdentity({
708
+ definitionIntegrity: input.prepared.definition.definitionIntegrity,
709
+ preparationFingerprint: input.prepared.preparationFingerprint,
710
+ evidence,
711
+ checks: report.checks.map((check) => ({
712
+ id: check.id,
713
+ status: check.status,
714
+ findings: check.findings,
715
+ })),
716
+ summary: report.summary,
717
+ evaluations,
718
+ });
719
+ return freeze({
720
+ definitionIntegrity: input.prepared.definition.definitionIntegrity,
721
+ preparationFingerprint: input.prepared.preparationFingerprint,
722
+ report,
723
+ evaluations,
724
+ evidence,
725
+ identityBody,
726
+ reportFingerprint: `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-review-report", review: identityBody }))}`,
727
+ });
728
+ }
729
+
730
+ interface FindingBlock {
731
+ readonly checkId: string;
732
+ readonly start: number;
733
+ readonly ids: Uint32Array;
734
+ }
735
+ interface FindingState {
736
+ readonly table: CompactDiagnosticJson;
737
+ readonly blocks: readonly FindingBlock[];
738
+ readonly count: number;
739
+ }
740
+ const findingStates = new WeakMap<object, FindingState>();
741
+ const compactReceipts = new WeakSet<object>();
742
+ const compactReceiptPreparations = new WeakMap<
743
+ CompactDiagnosticReviewReceipt,
744
+ CompactPreparedDiagnosticData
745
+ >();
746
+ const compactReceiptFingerprints = new WeakMap<
747
+ CompactDiagnosticReviewReceipt,
748
+ string
749
+ >();
750
+ function compactError(message: string, path = "$"): never {
751
+ throw new DiagnosticValidationError([
752
+ { domain: "input", code: "invalid-input-relationship", path, message },
753
+ ]);
754
+ }
755
+ export function assertCompactDiagnosticReviewReceipt(
756
+ value: unknown,
757
+ ): asserts value is CompactDiagnosticReviewReceipt {
758
+ if (
759
+ value === null ||
760
+ typeof value !== "object" ||
761
+ !compactReceipts.has(value)
762
+ )
763
+ compactError("Value is not an authentic compact diagnostic review receipt");
764
+ }
765
+ function findingState(store: CompactDiagnosticReviewFindings): FindingState {
766
+ if (store === null || typeof store !== "object" || !findingStates.has(store))
767
+ compactError("Value is not an authentic compact diagnostic finding store");
768
+ return findingStates.get(store)!;
769
+ }
770
+ function findingLocation(
771
+ state: FindingState,
772
+ index: number,
773
+ ): { block: FindingBlock; id: number } {
774
+ if (!Number.isSafeInteger(index) || index < 0 || index >= state.count)
775
+ compactError("Finding index is outside this review", "$.index");
776
+ const block = state.blocks.find(
777
+ (block) => index >= block.start && index < block.start + block.ids.length,
778
+ )!;
779
+ return { block, id: block.ids[index - block.start]! };
780
+ }
781
+ export interface DiagnosticReviewFindingEntry {
782
+ readonly index: number;
783
+ readonly checkId: string;
784
+ readonly finding: DiagnosticDeepReadonly<DataFinding>;
785
+ }
786
+ type WithoutSources<T> = T extends readonly (infer V)[]
787
+ ? readonly WithoutSources<V>[]
788
+ : T extends object
789
+ ? {
790
+ readonly [K in keyof T as K extends "sources"
791
+ ? "sourceCount"
792
+ : K]: K extends "sources" ? number : WithoutSources<T[K]>;
793
+ }
794
+ : T;
795
+ export interface DiagnosticReviewFindingSummary {
796
+ readonly index: number;
797
+ readonly checkId: string;
798
+ readonly finding: WithoutSources<DataFinding>;
799
+ }
800
+ export interface DiagnosticReviewFindingQuery {
801
+ readonly checkId?: string;
802
+ readonly offset?: number;
803
+ readonly limit?: number;
804
+ }
805
+ export interface DiagnosticReviewFindingSourceQuery {
806
+ readonly location?: "context" | "scope";
807
+ readonly offset?: number;
808
+ readonly limit?: number;
809
+ }
810
+ const findingQuerySchema = z
811
+ .object({
812
+ checkId: z.string().refine(isDiagnosticToken).optional(),
813
+ offset: z.number().int().nonnegative().safe().optional(),
814
+ limit: z.number().int().min(1).max(1000).optional(),
815
+ })
816
+ .strict();
817
+ const findingSourceQuerySchema = z
818
+ .object({
819
+ location: z.enum(["context", "scope"]).optional(),
820
+ offset: z.number().int().nonnegative().safe().optional(),
821
+ limit: z.number().int().min(1).max(1000).optional(),
822
+ })
823
+ .strict();
824
+ function pageResult<T>(
825
+ items: T[],
826
+ total: number,
827
+ offset: number,
828
+ ): DiagnosticReviewPage<T> {
829
+ return Object.freeze({
830
+ total,
831
+ offset,
832
+ items: Object.freeze(items),
833
+ nextOffset: offset + items.length < total ? offset + items.length : null,
834
+ });
835
+ }
836
+ export function getDiagnosticReviewFinding(
837
+ store: CompactDiagnosticReviewFindings,
838
+ index: number,
839
+ ): DiagnosticReviewFindingEntry {
840
+ const state = findingState(store);
841
+ const { block, id } = findingLocation(state, index);
842
+ return Object.freeze({
843
+ index,
844
+ checkId: block.checkId,
845
+ finding: state.table.read(id) as DiagnosticDeepReadonly<DataFinding>,
846
+ });
847
+ }
848
+ /** Full ordered evidence, including every finding's source lists. */
849
+ export function iterateDiagnosticReviewFindings(
850
+ store: CompactDiagnosticReviewFindings,
851
+ ): IterableIterator<DiagnosticReviewFindingEntry> {
852
+ const state = findingState(store);
853
+ return (function* () {
854
+ for (let index = 0; index < state.count; index++)
855
+ yield getDiagnosticReviewFinding(store, index);
856
+ })();
857
+ }
858
+ /** Summary pages never expand source lists, including high-fanout control totals. */
859
+ export function pageDiagnosticReviewFindings(
860
+ store: CompactDiagnosticReviewFindings,
861
+ query: DiagnosticReviewFindingQuery = {},
862
+ ): DiagnosticReviewPage<DiagnosticReviewFindingSummary> {
863
+ const state = findingState(store);
864
+ const issues = diagnosticJsonPreflight(query, "input");
865
+ if (issues.length) throw new DiagnosticValidationError(issues);
866
+ const parsed = findingQuerySchema.safeParse(query);
867
+ if (!parsed.success) compactError("Invalid finding page query");
868
+ const { checkId, offset = 0, limit = 100 } = parsed.data;
869
+ const blocks = state.blocks.filter(
870
+ (block) => checkId === undefined || block.checkId === checkId,
871
+ );
872
+ const total = blocks.reduce((sum, block) => sum + block.ids.length, 0);
873
+ const items: DiagnosticReviewFindingSummary[] = [];
874
+ let position = 0;
875
+ for (const block of blocks) {
876
+ for (
877
+ let local = Math.max(0, offset - position);
878
+ local < block.ids.length && items.length < limit;
879
+ local++
880
+ )
881
+ items.push(
882
+ Object.freeze({
883
+ index: block.start + local,
884
+ checkId: block.checkId,
885
+ finding: state.table.read(
886
+ block.ids[local]!,
887
+ true,
888
+ ) as WithoutSources<DataFinding>,
889
+ }),
890
+ );
891
+ position += block.ids.length;
892
+ if (items.length >= limit) break;
893
+ }
894
+ return pageResult(items, total, offset);
895
+ }
896
+ export function pageDiagnosticReviewFindingSources(
897
+ store: CompactDiagnosticReviewFindings,
898
+ index: number,
899
+ query: DiagnosticReviewFindingSourceQuery = {},
900
+ ): DiagnosticReviewPage<DiagnosticSourceLocation> {
901
+ const state = findingState(store);
902
+ const { id } = findingLocation(state, index);
903
+ const issues = diagnosticJsonPreflight(query, "input");
904
+ if (issues.length) throw new DiagnosticValidationError(issues);
905
+ const parsed = findingSourceQuerySchema.safeParse(query);
906
+ if (!parsed.success) compactError("Invalid finding source-page query");
907
+ const { location = "context", offset = 0, limit = 100 } = parsed.data;
908
+ const context = state.table.property(id, "context");
909
+ const parent =
910
+ location === "scope"
911
+ ? state.table.property(context, "reviewScope")
912
+ : context;
913
+ const sources = state.table.property(parent, "sources");
914
+ if (sources === undefined) return pageResult([], 0, offset);
915
+ const count = state.table.length(sources);
916
+ return pageResult(
917
+ Array.from(
918
+ { length: Math.max(0, Math.min(limit, count - offset)) },
919
+ (_, local) =>
920
+ state.table.read(
921
+ state.table.arrayItem(sources, offset + local),
922
+ ) as DiagnosticSourceLocation,
923
+ ),
924
+ count,
925
+ offset,
926
+ );
927
+ }
928
+
929
+ /** Compact owner receipt; identity projection/hash are deliberately deferred. */
930
+ export function reviewPreparedDiagnosticDataCompact(
931
+ input: ReviewPreparedDiagnosticDataCompactInput,
932
+ ): CompactDiagnosticReviewReceipt {
933
+ assertCompactPreparedDiagnosticData(input.prepared);
934
+ const evidence =
935
+ input.evidence === null
936
+ ? null
937
+ : validateDiagnosticReviewEvidence(input.evidence);
938
+ const evaluations = evaluateDiagnosticReviewRulesCompact(input.prepared);
939
+ const table = new CompactDiagnosticJson();
940
+ const blocks: FindingBlock[] = [];
941
+ const checks: CompactDiagnosticReviewCheck[] = [];
942
+ let findingCount = 0;
943
+ const appendFindings = (
944
+ checkId: string,
945
+ values: Iterable<DataFinding>,
946
+ ): FindingBlock => {
947
+ type SourceUnion = readonly number[] | Set<number>;
948
+ interface Pending {
949
+ keyId: number;
950
+ contextSources?: SourceUnion;
951
+ scopeSources?: SourceUnion;
952
+ }
953
+ const pending: Pending[] = [];
954
+ const sources: DiagnosticSourceLocation[] = [];
955
+ const sourceIds = new Map<string, number>();
956
+ const candidates = new Map<string, number | number[]>();
957
+ const mergeSources = (
958
+ previous: SourceUnion | undefined,
959
+ incoming: readonly DiagnosticSourceLocation[] | undefined,
960
+ ): SourceUnion | undefined => {
961
+ if (incoming === undefined) return previous;
962
+ const ids = incoming.map((source) => {
963
+ const key = canonicalJson(source);
964
+ let id = sourceIds.get(key);
965
+ if (id === undefined) {
966
+ id = sources.length;
967
+ sources.push(source);
968
+ sourceIds.set(key, id);
969
+ }
970
+ return id;
971
+ });
972
+ if (previous === undefined) return ids;
973
+ if (
974
+ ids.every((id) =>
975
+ previous instanceof Set ? previous.has(id) : previous.includes(id),
976
+ )
977
+ )
978
+ return previous;
979
+ const union = previous instanceof Set ? previous : new Set(previous);
980
+ for (const id of ids) union.add(id);
981
+ return union;
982
+ };
983
+ const sourceValues = (union: SourceUnion | undefined) =>
984
+ [...(union ?? [])]
985
+ .map((id) => sources[id]!)
986
+ .sort(compareDiagnosticSourceLocations);
987
+ for (const value of values) {
988
+ const finding = normalizeDataFindings([value])[0]!;
989
+ const key = findingMergeKey(finding);
990
+ // Hashes index candidates only. Exact source-free keys decide equality.
991
+ const hash = fnv1a64(key);
992
+ const bucket = candidates.get(hash);
993
+ const possible =
994
+ bucket === undefined
995
+ ? []
996
+ : typeof bucket === "number"
997
+ ? [bucket]
998
+ : bucket;
999
+ const matching = possible.find(
1000
+ (index) => canonicalJson(table.read(pending[index]!.keyId)) === key,
1001
+ );
1002
+ let entry: Pending;
1003
+ if (matching === undefined) {
1004
+ const index = pending.length;
1005
+ entry = { keyId: table.add(findingMergeSkeleton(finding)) };
1006
+ pending.push(entry);
1007
+ candidates.set(
1008
+ hash,
1009
+ bucket === undefined ? index : [...possible, index],
1010
+ );
1011
+ } else {
1012
+ entry = pending[matching]!;
1013
+ entry.keyId = table.add(findingMergeSkeleton(finding));
1014
+ }
1015
+ entry.contextSources = mergeSources(
1016
+ entry.contextSources,
1017
+ finding.context?.sources,
1018
+ );
1019
+ entry.scopeSources = mergeSources(
1020
+ entry.scopeSources,
1021
+ finding.context?.reviewScope?.sources,
1022
+ );
1023
+ }
1024
+ // Source-free fields decide nearly every comparison. Expand source IDs only
1025
+ // for the contract's final tie-break, never for each ordinary comparison.
1026
+ pending.sort(
1027
+ (a, b) =>
1028
+ compareDataFindings(
1029
+ table.read(a.keyId) as DataFinding,
1030
+ table.read(b.keyId) as DataFinding,
1031
+ ) ||
1032
+ compareSourceArrays(
1033
+ sourceValues(a.contextSources),
1034
+ sourceValues(b.contextSources),
1035
+ ),
1036
+ );
1037
+ const ids = pending.map((entry) => {
1038
+ const skeleton = table.read(entry.keyId) as DataFinding;
1039
+ if (skeleton.context === undefined) return table.add(skeleton);
1040
+ const context: DataFindingContext = { ...skeleton.context };
1041
+ if (entry.contextSources === undefined) delete context.sources;
1042
+ else context.sources = sourceValues(entry.contextSources);
1043
+ if (context.reviewScope !== undefined)
1044
+ context.reviewScope = {
1045
+ ...context.reviewScope,
1046
+ sources: sourceValues(entry.scopeSources),
1047
+ };
1048
+ // Each final source list is encoded once, after every duplicate was merged.
1049
+ return table.add({ ...skeleton, context });
1050
+ });
1051
+ const block = { checkId, start: findingCount, ids: Uint32Array.from(ids) };
1052
+ findingCount += ids.length;
1053
+ blocks.push(block);
1054
+ return block;
1055
+ };
1056
+ for (const check of structuralChecks(input.prepared, evidence)) {
1057
+ const block = appendFindings(check.id, check.findings);
1058
+ checks.push(
1059
+ Object.freeze({
1060
+ id: check.id,
1061
+ description: check.description,
1062
+ status: check.status,
1063
+ details: Object.freeze([...check.details]),
1064
+ findingCount: block.ids.length,
1065
+ }),
1066
+ );
1067
+ }
1068
+ for (const [
1069
+ ruleIndex,
1070
+ rule,
1071
+ ] of input.prepared.definition.definition.reviewRules.entries()) {
1072
+ const range = evaluations.rules[ruleIndex]!;
1073
+ const counts = range.summary;
1074
+ const status =
1075
+ counts.fail > 0
1076
+ ? "fail"
1077
+ : counts.warning > 0
1078
+ ? "warning"
1079
+ : counts.notEvaluated > 0
1080
+ ? "not-evaluated"
1081
+ : "pass";
1082
+ const values = function* (): IterableIterator<DataFinding> {
1083
+ if (counts.pass === range.count) return;
1084
+ for (
1085
+ let index = range.start;
1086
+ index < range.start + range.count;
1087
+ index++
1088
+ ) {
1089
+ if (
1090
+ getDiagnosticReviewEvaluationSummary(evaluations, index)
1091
+ .effectiveStatus === "pass"
1092
+ )
1093
+ continue;
1094
+ yield* evaluationFindings(
1095
+ getDiagnosticReviewEvaluation(evaluations, index),
1096
+ rule,
1097
+ );
1098
+ }
1099
+ };
1100
+ const block = appendFindings(rule.id, values());
1101
+ const details = Array.from(
1102
+ block.ids.subarray(0, 20),
1103
+ (id) => table.read(table.property(id, "message")!) as string,
1104
+ );
1105
+ checks.push(
1106
+ Object.freeze({
1107
+ id: rule.id,
1108
+ description: rule.description,
1109
+ status,
1110
+ details: Object.freeze(details),
1111
+ findingCount: block.ids.length,
1112
+ }),
1113
+ );
1114
+ }
1115
+ table.seal();
1116
+ const findings = Object.freeze({
1117
+ count: findingCount,
1118
+ }) as CompactDiagnosticReviewFindings;
1119
+ findingStates.set(findings, { table, blocks, count: findingCount });
1120
+ const summary = summarizeDataChecks(
1121
+ checks.map((check) => ({
1122
+ ...check,
1123
+ details: [...check.details],
1124
+ findings: [],
1125
+ })),
1126
+ ).summary;
1127
+ const receipt = Object.freeze({
1128
+ definitionIntegrity: input.prepared.definition.definitionIntegrity,
1129
+ report: Object.freeze({
1130
+ checks: Object.freeze(checks),
1131
+ summary: Object.freeze(summary),
1132
+ }),
1133
+ evaluations,
1134
+ findings,
1135
+ evidence,
1136
+ });
1137
+ compactReceipts.add(receipt);
1138
+ compactReceiptPreparations.set(receipt, input.prepared);
1139
+ return receipt;
1140
+ }
1141
+
1142
+ /** Exact legacy review identity, available only from an authentic immutable owner. */
1143
+ export function getCompactDiagnosticReviewReceiptIdentityDocument(
1144
+ receipt: CompactDiagnosticReviewReceipt,
1145
+ ): DiagnosticIdentityDocument {
1146
+ assertCompactDiagnosticReviewReceipt(receipt);
1147
+ const state = findingState(receipt.findings);
1148
+ const prepared = compactReceiptPreparations.get(receipt)!;
1149
+ return createDiagnosticIdentityObject({
1150
+ definitionIntegrity: createDiagnosticIdentityValue(
1151
+ receipt.definitionIntegrity,
1152
+ ),
1153
+ preparationFingerprint: createDiagnosticIdentityValue(
1154
+ getCompactPreparedDiagnosticDataFingerprint(prepared),
1155
+ ),
1156
+ evidence: createDiagnosticIdentityValue(receipt.evidence),
1157
+ checks: createDiagnosticIdentityArray(
1158
+ receipt.report.checks.length,
1159
+ (index) => {
1160
+ const check = receipt.report.checks[index]!;
1161
+ const block = state.blocks[index]!;
1162
+ return createDiagnosticIdentityObject({
1163
+ id: createDiagnosticIdentityValue(check.id),
1164
+ status: createDiagnosticIdentityValue(check.status),
1165
+ findings: createDiagnosticIdentityArray(block.ids.length, (local) =>
1166
+ state.table.identityDocument(block.ids[local]!),
1167
+ ),
1168
+ });
1169
+ },
1170
+ ),
1171
+ summary: createDiagnosticIdentityValue(receipt.report.summary),
1172
+ evaluations: getCompactDiagnosticReviewEvaluationsIdentityDocument(
1173
+ receipt.evaluations,
1174
+ ),
1175
+ });
1176
+ }
1177
+ /** Explicit evidence operation; caches only the small immutable-owner fingerprint. */
1178
+ export function getCompactDiagnosticReviewReceiptFingerprint(
1179
+ receipt: CompactDiagnosticReviewReceipt,
1180
+ ): string {
1181
+ assertCompactDiagnosticReviewReceipt(receipt);
1182
+ let fingerprint = compactReceiptFingerprints.get(receipt);
1183
+ if (fingerprint === undefined) {
1184
+ fingerprint = fingerprintDiagnosticIdentity(
1185
+ getCompactDiagnosticReviewReceiptIdentityDocument(receipt),
1186
+ { kind: "diagnostic-review-report", property: "review" },
1187
+ );
1188
+ compactReceiptFingerprints.set(receipt, fingerprint);
1189
+ }
1190
+ return fingerprint;
39
1191
  }