@actuarial-ts/data 0.6.1 → 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,6 +1,19 @@
1
1
  import {
2
2
  DiagnosticValidationError,
3
+ diagnosticJsonPreflight,
4
+ isDiagnosticToken,
3
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,
4
17
  canonicalJson,
5
18
  evaluateDiagnosticReviewRules,
6
19
  fnv1a64,
@@ -14,7 +27,12 @@ import {
14
27
  type DiagnosticReviewRuleEvaluation,
15
28
  type DiagnosticSourceLocation,
16
29
  type PreparedDiagnosticData,
30
+ type PreparedDiagnosticDataContent,
31
+ type CompactPreparedDiagnosticData,
32
+ type CompactDiagnosticReviewEvaluations,
33
+ type DiagnosticReviewPage,
17
34
  } from "@actuarial-ts/core";
35
+ import { CompactDiagnosticJson } from "./compactDiagnosticJson.js";
18
36
  import { z } from "zod";
19
37
  import {
20
38
  createNotEvaluatedDataCheck,
@@ -73,6 +91,33 @@ export interface ReviewPreparedDiagnosticDataInput {
73
91
  readonly evidence: DiagnosticReviewEvidence | null;
74
92
  }
75
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
+
76
121
  const fixed = [
77
122
  ["diagnostic/structural/loss-identity", "Loss identities are unique", "fail"],
78
123
  [
@@ -229,25 +274,34 @@ function compareSourceArrays(
229
274
  return left.length - right.length;
230
275
  }
231
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
+
232
299
  function normalizeDataFindings(values: readonly DataFinding[]): DataFinding[] {
233
300
  const merged = new Map<string, DataFinding>();
234
301
  for (const finding of values) {
235
302
  const context = finding.context;
236
303
  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
- });
304
+ const key = findingMergeKey(finding);
251
305
  const previous = merged.get(key);
252
306
  const previousContext = previous?.context;
253
307
  const normalizedContext =
@@ -283,6 +337,10 @@ function normalizeDataFindings(values: readonly DataFinding[]): DataFinding[] {
283
337
  : { context: normalizedContext }),
284
338
  });
285
339
  }
340
+ return [...merged.values()].sort(compareDataFindings);
341
+ }
342
+
343
+ function compareDataFindings(left: DataFinding, right: DataFinding): number {
286
344
  const text = (left: string, right: string) =>
287
345
  left < right ? -1 : left > right ? 1 : 0;
288
346
  const field = (finding: DataFinding, key: keyof DataFindingContext) =>
@@ -297,50 +355,49 @@ function normalizeDataFindings(values: readonly DataFinding[]): DataFinding[] {
297
355
  field(right, key) as string | undefined,
298
356
  text,
299
357
  );
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 ?? [],
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: [] },
343
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
+ )
344
401
  );
345
402
  }
346
403
 
@@ -456,19 +513,14 @@ function findingContext(finding: DiagnosticMetricFinding): DataFindingContext {
456
513
  };
457
514
  }
458
515
 
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);
516
+ function structuralChecks(
517
+ prepared: PreparedDiagnosticDataContent,
518
+ evidence: DiagnosticDeepReadonly<DiagnosticReviewEvidence> | null,
519
+ ): DataCheck[] {
468
520
  const findingsByCheck = new Map<string, DataFinding[]>(
469
521
  fixed.map(([id]) => [id, []]),
470
522
  );
471
- for (const finding of input.prepared.findings) {
523
+ for (const finding of prepared.findings) {
472
524
  const id = codeToCheck[finding.code];
473
525
  if (id)
474
526
  findingsByCheck.get(id)!.push({
@@ -530,10 +582,9 @@ export function reviewPreparedDiagnosticData(
530
582
  });
531
583
  }
532
584
 
533
- const hasExposureMeasures =
534
- input.prepared.definition.definition.measures.some(
535
- (measure) => measure.source === "exposure",
536
- );
585
+ const hasExposureMeasures = prepared.definition.definition.measures.some(
586
+ (measure) => measure.source === "exposure",
587
+ );
537
588
  const checks: DataCheck[] = fixed.map(
538
589
  ([id, description, severity], index) => {
539
590
  if ([1, 5, 6, 7].includes(index) && !hasExposureMeasures)
@@ -542,7 +593,7 @@ export function reviewPreparedDiagnosticData(
542
593
  description,
543
594
  "the definition declares no exposure measures",
544
595
  );
545
- if (index === 8 && !input.prepared.expectedCellsProvided)
596
+ if (index === 8 && !prepared.expectedCellsProvided)
546
597
  return createNotEvaluatedDataCheck(
547
598
  id,
548
599
  description,
@@ -562,6 +613,68 @@ export function reviewPreparedDiagnosticData(
562
613
  );
563
614
  },
564
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);
565
678
 
566
679
  for (const rule of input.prepared.definition.definition.reviewRules) {
567
680
  const matching = evaluations.filter((item) => item.ruleId === rule.id);
@@ -579,45 +692,7 @@ export function reviewPreparedDiagnosticData(
579
692
  : matching.some((item) => item.status === "not-evaluated")
580
693
  ? "not-evaluated"
581
694
  : "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
- ]);
695
+ const findings = matching.flatMap((item) => evaluationFindings(item, rule));
621
696
  const normalizedFindings = normalizeDataFindings(findings);
622
697
  checks.push({
623
698
  id: rule.id,
@@ -651,3 +726,466 @@ export function reviewPreparedDiagnosticData(
651
726
  reportFingerprint: `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-review-report", review: identityBody }))}`,
652
727
  });
653
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;
1191
+ }