@actuarial-ts/core 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.
Files changed (63) hide show
  1. package/README.md +11 -2
  2. package/dist/canonical.d.ts +11 -0
  3. package/dist/canonical.d.ts.map +1 -1
  4. package/dist/canonical.js +136 -26
  5. package/dist/canonical.js.map +1 -1
  6. package/dist/diagnosticEvidenceIntern.d.ts +16 -0
  7. package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
  8. package/dist/diagnosticEvidenceIntern.js +139 -0
  9. package/dist/diagnosticEvidenceIntern.js.map +1 -0
  10. package/dist/diagnosticExposure.d.ts.map +1 -1
  11. package/dist/diagnosticExposure.js +97 -7
  12. package/dist/diagnosticExposure.js.map +1 -1
  13. package/dist/diagnosticIdentity.d.ts.map +1 -1
  14. package/dist/diagnosticIdentity.js +6 -3
  15. package/dist/diagnosticIdentity.js.map +1 -1
  16. package/dist/diagnosticIdentityStream.d.ts +56 -0
  17. package/dist/diagnosticIdentityStream.d.ts.map +1 -0
  18. package/dist/diagnosticIdentityStream.js +472 -0
  19. package/dist/diagnosticIdentityStream.js.map +1 -0
  20. package/dist/diagnosticPreparation.d.ts +28 -0
  21. package/dist/diagnosticPreparation.d.ts.map +1 -1
  22. package/dist/diagnosticPreparation.js +160 -36
  23. package/dist/diagnosticPreparation.js.map +1 -1
  24. package/dist/diagnosticReview.d.ts +4 -1
  25. package/dist/diagnosticReview.d.ts.map +1 -1
  26. package/dist/diagnosticReview.js +203 -51
  27. package/dist/diagnosticReview.js.map +1 -1
  28. package/dist/diagnosticReviewSources.d.ts +18 -0
  29. package/dist/diagnosticReviewSources.d.ts.map +1 -0
  30. package/dist/diagnosticReviewSources.js +76 -0
  31. package/dist/diagnosticReviewSources.js.map +1 -0
  32. package/dist/diagnosticReviewStore.d.ts +88 -0
  33. package/dist/diagnosticReviewStore.d.ts.map +1 -0
  34. package/dist/diagnosticReviewStore.js +455 -0
  35. package/dist/diagnosticReviewStore.js.map +1 -0
  36. package/dist/diagnosticRunner.d.ts +25 -1
  37. package/dist/diagnosticRunner.d.ts.map +1 -1
  38. package/dist/diagnosticRunner.js +115 -7
  39. package/dist/diagnosticRunner.js.map +1 -1
  40. package/dist/fnvAccumulator.d.ts +6 -0
  41. package/dist/fnvAccumulator.d.ts.map +1 -0
  42. package/dist/fnvAccumulator.js +21 -0
  43. package/dist/fnvAccumulator.js.map +1 -0
  44. package/dist/index.d.ts +8 -5
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +5 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/version.d.ts +1 -1
  49. package/dist/version.js +1 -1
  50. package/package.json +1 -1
  51. package/src/canonical.ts +144 -26
  52. package/src/diagnosticEvidenceIntern.ts +151 -0
  53. package/src/diagnosticExposure.ts +126 -17
  54. package/src/diagnosticIdentity.ts +15 -6
  55. package/src/diagnosticIdentityStream.ts +553 -0
  56. package/src/diagnosticPreparation.ts +268 -84
  57. package/src/diagnosticReview.ts +293 -85
  58. package/src/diagnosticReviewSources.ts +85 -0
  59. package/src/diagnosticReviewStore.ts +692 -0
  60. package/src/diagnosticRunner.ts +206 -9
  61. package/src/fnvAccumulator.ts +20 -0
  62. package/src/index.ts +38 -1
  63. package/src/version.ts +1 -1
@@ -1,4 +1,11 @@
1
1
  import { canonicalJson, fnv1a64 } from "./canonical.js";
2
+ import {
3
+ createDiagnosticIdentityArray,
4
+ createDiagnosticIdentityObject,
5
+ createDiagnosticIdentityValue,
6
+ fingerprintDiagnosticIdentity,
7
+ type DiagnosticIdentityDocument,
8
+ } from "./diagnosticIdentityStream.js";
2
9
  import {
3
10
  normalizeDiagnosticsFilterIdentity,
4
11
  projectDiagnosticIdentity,
@@ -75,8 +82,7 @@ export interface DiagnosticLossSnapshot extends DiagnosticLossRecordBase {
75
82
  }
76
83
 
77
84
  export type DiagnosticLossInput =
78
- | DiagnosticClaimObservation
79
- | DiagnosticLossSnapshot;
85
+ DiagnosticClaimObservation | DiagnosticLossSnapshot;
80
86
 
81
87
  export interface DiagnosticCompletePeriodCutoff {
82
88
  readonly sourceGroup: string;
@@ -101,10 +107,7 @@ export interface PrepareDiagnosticDataInput {
101
107
  }
102
108
 
103
109
  export type DiagnosticInputDisposition =
104
- | "invalid"
105
- | "complete-period-cutoff"
106
- | "filter"
107
- | "retained";
110
+ "invalid" | "complete-period-cutoff" | "filter" | "retained";
108
111
 
109
112
  export interface DiagnosticLossInputAuditSnapshot {
110
113
  readonly recordId: string;
@@ -185,6 +188,22 @@ export interface PreparedDiagnosticData {
185
188
  readonly findings: readonly DiagnosticMetricFinding[];
186
189
  }
187
190
 
191
+ /** Complete prepared content shared by the eager and compact execution paths. */
192
+ export type PreparedDiagnosticDataContent = Omit<
193
+ PreparedDiagnosticData,
194
+ typeof preparedDiagnosticDataBrand | "preparationFingerprint"
195
+ >;
196
+
197
+ declare const compactPreparedDiagnosticDataBrand: unique symbol;
198
+ /**
199
+ * Owned, frozen preparation without an eagerly materialized identity or hash.
200
+ * This is a distinct authenticated value, not an eager preparation with a
201
+ * missing or placeholder fingerprint.
202
+ */
203
+ export interface CompactPreparedDiagnosticData extends PreparedDiagnosticDataContent {
204
+ readonly [compactPreparedDiagnosticDataBrand]: true;
205
+ }
206
+
188
207
  export interface NormalizedDiagnosticPreparationIdentity {
189
208
  readonly definitionIntegrity: string;
190
209
  readonly filter: NormalizedDiagnosticsFilterIdentity | null;
@@ -252,6 +271,12 @@ interface PendingBlocker {
252
271
  }
253
272
 
254
273
  const authentic = new WeakSet<object>();
274
+ const authenticCompact = new WeakSet<object>();
275
+ const compactIdentityDocuments = new WeakMap<
276
+ object,
277
+ DiagnosticIdentityDocument
278
+ >();
279
+ const compactFingerprints = new WeakMap<object, string>();
255
280
  const identities = new WeakMap<
256
281
  object,
257
282
  DiagnosticDeepReadonly<NormalizedDiagnosticPreparationIdentity>
@@ -992,10 +1017,10 @@ function exposureCoordinateContext(
992
1017
  return { sourceGroup: coherentSource[0], origin: coherentOrigin[0] };
993
1018
  }
994
1019
 
995
- function auditSort(
1020
+ function createAuditComparator(): (
996
1021
  left: DiagnosticInputAuditRecord,
997
1022
  right: DiagnosticInputAuditRecord,
998
- ): number {
1023
+ ) => number {
999
1024
  const rank = { loss: 0, exposure: 1, "expected-cell": 2 } as const;
1000
1025
  const disposition = {
1001
1026
  invalid: 0,
@@ -1003,11 +1028,21 @@ function auditSort(
1003
1028
  filter: 2,
1004
1029
  retained: 3,
1005
1030
  } as const;
1006
- return (
1031
+ // Audit snapshots are already owned and complete before sorting. Preserve the
1032
+ // exact canonical-text ordering, but serialize each compared record once.
1033
+ // Keep this memo local to the sort; it must not retain keys with the result.
1034
+ const keys = new WeakMap<DiagnosticInputAuditRecord["record"], string>();
1035
+ const key = (record: DiagnosticInputAuditRecord["record"]): string => {
1036
+ const previous = keys.get(record);
1037
+ if (previous !== undefined) return previous;
1038
+ const text = canonicalJson(record);
1039
+ keys.set(record, text);
1040
+ return text;
1041
+ };
1042
+ return (left, right) =>
1007
1043
  rank[left.kind] - rank[right.kind] ||
1008
- codeUnit(canonicalJson(left.record), canonicalJson(right.record)) ||
1009
- disposition[left.disposition] - disposition[right.disposition]
1010
- );
1044
+ codeUnit(key(left.record), key(right.record)) ||
1045
+ disposition[left.disposition] - disposition[right.disposition];
1011
1046
  }
1012
1047
 
1013
1048
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
@@ -1345,6 +1380,22 @@ function preparationBoundaryIssues(
1345
1380
  export function prepareDiagnosticData(
1346
1381
  input: PrepareDiagnosticDataInput,
1347
1382
  ): PreparedDiagnosticData {
1383
+ return prepareDiagnosticDataInMode(input, "eager") as PreparedDiagnosticData;
1384
+ }
1385
+
1386
+ export function prepareDiagnosticDataCompact(
1387
+ input: PrepareDiagnosticDataInput,
1388
+ ): CompactPreparedDiagnosticData {
1389
+ return prepareDiagnosticDataInMode(
1390
+ input,
1391
+ "compact",
1392
+ ) as CompactPreparedDiagnosticData;
1393
+ }
1394
+
1395
+ function prepareDiagnosticDataInMode(
1396
+ input: PrepareDiagnosticDataInput,
1397
+ mode: "eager" | "compact",
1398
+ ): PreparedDiagnosticData | CompactPreparedDiagnosticData {
1348
1399
  assertCompiledDiagnosticDefinition(
1349
1400
  (input as unknown as { readonly definition?: unknown } | null)?.definition,
1350
1401
  );
@@ -1432,6 +1483,7 @@ export function prepareDiagnosticData(
1432
1483
  }
1433
1484
  const normalized = {
1434
1485
  ...row,
1486
+ ...(row.source === undefined ? {} : { source: { ...row.source } }),
1435
1487
  origin: origin.label,
1436
1488
  valuation: valuation.label,
1437
1489
  };
@@ -1470,6 +1522,9 @@ export function prepareDiagnosticData(
1470
1522
  const lossCandidates: LossCandidate[] = input.losses.map((inputRow) => ({
1471
1523
  row: {
1472
1524
  ...inputRow,
1525
+ ...(inputRow.source === undefined
1526
+ ? {}
1527
+ : { source: { ...inputRow.source } }),
1473
1528
  measures: Object.fromEntries(
1474
1529
  Object.keys(inputRow.measures)
1475
1530
  .sort(codeUnit)
@@ -1592,7 +1647,12 @@ export function prepareDiagnosticData(
1592
1647
 
1593
1648
  const exposureCandidates: ExposureCandidate[] = input.exposures.map(
1594
1649
  (inputRow) => ({
1595
- row: { ...inputRow },
1650
+ row: {
1651
+ ...inputRow,
1652
+ ...(inputRow.source === undefined
1653
+ ? {}
1654
+ : { source: { ...inputRow.source } }),
1655
+ },
1596
1656
  snapshot: snapshotExposure(inputRow),
1597
1657
  disposition: selectedBySource(filter, inputRow.sourceGroup)
1598
1658
  ? "retained"
@@ -2015,6 +2075,14 @@ export function prepareDiagnosticData(
2015
2075
  derivedByRecord.get(candidate.row.recordId)!,
2016
2076
  );
2017
2077
 
2078
+ // Exposure attachment is a coordinate join. Index the established cell order
2079
+ // once; scanning every cell for each origin-static exposure is quadratic.
2080
+ const cellKeysBySourceOrigin = new Map<string, string[]>();
2081
+ for (const key of cellRows.keys()) {
2082
+ const [sourceGroup, origin] = parseCellKey(key);
2083
+ addToMap(cellKeysBySourceOrigin, canonicalJson([sourceGroup, origin]), key);
2084
+ }
2085
+
2018
2086
  const allMeasureIds = definition.definition.measures
2019
2087
  .map((measure) => measure.id)
2020
2088
  .sort(codeUnit);
@@ -2025,7 +2093,7 @@ export function prepareDiagnosticData(
2025
2093
  .filter((measure) => measure.source === "exposure")
2026
2094
  .sort((left, right) => codeUnit(left.id, right.id));
2027
2095
  const preparedCells: PreparedDiagnosticSourceCell[] = [];
2028
- const invalidExposureTargets = new Map<string, Set<string>>();
2096
+ const invalidExposureMeasuresByCell = new Map<string, Set<string>>();
2029
2097
  for (const item of invalidExposureBlockers) {
2030
2098
  const targets = new Set<string>();
2031
2099
  for (const candidate of item.candidates) {
@@ -2039,16 +2107,17 @@ export function prepareDiagnosticData(
2039
2107
  ),
2040
2108
  );
2041
2109
  else
2042
- for (const key of cellRows.keys()) {
2043
- const [sourceGroup, origin] = parseCellKey(key);
2044
- if (
2045
- sourceGroup === candidate.row.sourceGroup &&
2046
- origin === candidate.origin.label
2047
- )
2048
- targets.add(key);
2049
- }
2110
+ for (const key of cellKeysBySourceOrigin.get(
2111
+ canonicalJson([candidate.row.sourceGroup, candidate.origin.label]),
2112
+ ) ?? [])
2113
+ targets.add(key);
2114
+ }
2115
+ for (const key of targets) {
2116
+ const measures =
2117
+ invalidExposureMeasuresByCell.get(key) ?? new Set<string>();
2118
+ measures.add(item.finding.measureId!);
2119
+ invalidExposureMeasuresByCell.set(key, measures);
2050
2120
  }
2051
- invalidExposureTargets.set(canonicalJson(item.finding), targets);
2052
2121
  pendingBlockers.push({
2053
2122
  finding: item.finding,
2054
2123
  measureIds: [item.finding.measureId!],
@@ -2057,21 +2126,28 @@ export function prepareDiagnosticData(
2057
2126
  });
2058
2127
  }
2059
2128
 
2060
- const validAttachmentKeys = new Map<ReconciledDiagnosticExposure, string[]>();
2129
+ const validExposuresByCell = new Map<
2130
+ string,
2131
+ Extract<ReconciledDiagnosticExposure, { status: "valid" }>[]
2132
+ >();
2061
2133
  for (const exposure of reconciled) {
2062
2134
  if (exposure.status !== "valid") continue;
2063
2135
  const timing = internals.measuresById.get(
2064
2136
  exposure.measureId,
2065
2137
  )!.exposureTiming;
2066
- const keys = [...cellRows.keys()].filter((key) => {
2067
- const [sourceGroup, origin, valuation] = parseCellKey(key);
2068
- return (
2069
- sourceGroup === exposure.sourceGroup &&
2070
- origin === exposure.origin &&
2071
- (timing === "origin-static" || valuation === exposure.valuation)
2072
- );
2073
- });
2074
- validAttachmentKeys.set(exposure, keys);
2138
+ const coordinateKey =
2139
+ exposure.valuation === undefined
2140
+ ? null
2141
+ : cellKey(exposure.sourceGroup, exposure.origin, exposure.valuation);
2142
+ const keys =
2143
+ timing === "origin-static"
2144
+ ? (cellKeysBySourceOrigin.get(
2145
+ canonicalJson([exposure.sourceGroup, exposure.origin]),
2146
+ ) ?? [])
2147
+ : coordinateKey !== null && cellRows.has(coordinateKey)
2148
+ ? [coordinateKey]
2149
+ : [];
2150
+ for (const key of keys) addToMap(validExposuresByCell, key, exposure);
2075
2151
  if (keys.length === 0)
2076
2152
  findings.push(
2077
2153
  structuralFinding("exposure-without-loss", {
@@ -2085,6 +2161,14 @@ export function prepareDiagnosticData(
2085
2161
  );
2086
2162
  }
2087
2163
 
2164
+ const pendingBlockersByCell = new Map<
2165
+ string,
2166
+ (typeof pendingBlockers)[number][]
2167
+ >();
2168
+ for (const pending of pendingBlockers)
2169
+ for (const key of new Set(pending.cellKeys))
2170
+ addToMap(pendingBlockersByCell, key, pending);
2171
+
2088
2172
  for (const [key, rows] of cellRows) {
2089
2173
  const [sourceGroup, origin, valuation] = parseCellKey(key);
2090
2174
  const coordinate = coordinateFor(definition, origin, valuation).coordinate!;
@@ -2161,22 +2245,18 @@ export function prepareDiagnosticData(
2161
2245
  ),
2162
2246
  );
2163
2247
  }
2164
- for (const exposure of reconciled)
2165
- if (
2166
- exposure.status === "valid" &&
2167
- validAttachmentKeys.get(exposure)?.includes(key)
2168
- )
2169
- contributions[exposure.measureId]!.push(
2170
- auditedDiagnosticContribution(
2171
- exposure.key,
2172
- exposure.value,
2173
- "unknown",
2174
- exposure.sources,
2175
- exposure.deduplicated,
2176
- ),
2177
- );
2178
- for (const pending of pendingBlockers) {
2179
- if (!pending.cellKeys.includes(key)) continue;
2248
+ const attachedExposures = validExposuresByCell.get(key) ?? [];
2249
+ for (const exposure of attachedExposures)
2250
+ contributions[exposure.measureId]!.push(
2251
+ auditedDiagnosticContribution(
2252
+ exposure.key,
2253
+ exposure.value,
2254
+ "unknown",
2255
+ exposure.sources,
2256
+ exposure.deduplicated,
2257
+ ),
2258
+ );
2259
+ for (const pending of pendingBlockersByCell.get(key) ?? []) {
2180
2260
  cellFindings.push(pending.finding);
2181
2261
  const blocker = {
2182
2262
  code: pending.finding.code,
@@ -2190,17 +2270,9 @@ export function prepareDiagnosticData(
2190
2270
  }
2191
2271
  for (const measure of exposureMeasures) {
2192
2272
  const attempted =
2193
- [...validAttachmentKeys.entries()].some(
2194
- ([exposure, keys]) =>
2195
- exposure.status === "valid" &&
2196
- exposure.measureId === measure.id &&
2197
- keys.includes(key),
2198
- ) ||
2199
- invalidExposureBlockers.some(
2200
- (item) =>
2201
- item.finding.measureId === measure.id &&
2202
- invalidExposureTargets.get(canonicalJson(item.finding))?.has(key),
2203
- );
2273
+ attachedExposures.some(
2274
+ (exposure) => exposure.measureId === measure.id,
2275
+ ) || invalidExposureMeasuresByCell.get(key)?.has(measure.id);
2204
2276
  if (contributions[measure.id]!.length === 0 && !attempted) {
2205
2277
  const sources = normalizeSources(rows.map((row) => row.source));
2206
2278
  const finding = structuralFinding("loss-without-exposure", {
@@ -2374,28 +2446,22 @@ export function prepareDiagnosticData(
2374
2446
  )
2375
2447
  .map((candidate) => candidate.row);
2376
2448
  const audit: DiagnosticInputAuditRecord[] = [
2377
- ...lossCandidates.map(
2378
- (candidate): DiagnosticInputAuditRecord => ({
2379
- kind: "loss",
2380
- disposition: candidate.disposition,
2381
- record: candidate.snapshot,
2382
- }),
2383
- ),
2384
- ...exposureCandidates.map(
2385
- (candidate): DiagnosticInputAuditRecord => ({
2386
- kind: "exposure",
2387
- disposition: candidate.disposition,
2388
- record: candidate.snapshot,
2389
- }),
2390
- ),
2391
- ...expectedCandidates.map(
2392
- (candidate): DiagnosticInputAuditRecord => ({
2393
- kind: "expected-cell",
2394
- disposition: candidate.disposition,
2395
- record: candidate.snapshot,
2396
- }),
2397
- ),
2398
- ].sort(auditSort);
2449
+ ...lossCandidates.map((candidate): DiagnosticInputAuditRecord => ({
2450
+ kind: "loss",
2451
+ disposition: candidate.disposition,
2452
+ record: candidate.snapshot,
2453
+ })),
2454
+ ...exposureCandidates.map((candidate): DiagnosticInputAuditRecord => ({
2455
+ kind: "exposure",
2456
+ disposition: candidate.disposition,
2457
+ record: candidate.snapshot,
2458
+ })),
2459
+ ...expectedCandidates.map((candidate): DiagnosticInputAuditRecord => ({
2460
+ kind: "expected-cell",
2461
+ disposition: candidate.disposition,
2462
+ record: candidate.snapshot,
2463
+ })),
2464
+ ].sort(createAuditComparator());
2399
2465
  const normalizedFindings = mergeFindings(findings);
2400
2466
  const frozenCells = preparedCells.map((cell) => deepFreeze(cell));
2401
2467
  const cutoffValues = normalizedCutoffs.map((cutoff) => cutoff.value);
@@ -2410,11 +2476,29 @@ export function prepareDiagnosticData(
2410
2476
  expectedCells,
2411
2477
  findings: normalizedFindings,
2412
2478
  });
2479
+ if (mode === "compact") {
2480
+ // Every child was frozen above (the compiled definition owns its snapshot).
2481
+ const prepared = Object.freeze({
2482
+ definition,
2483
+ ...ownedData,
2484
+ }) as unknown as CompactPreparedDiagnosticData;
2485
+ authenticCompact.add(prepared);
2486
+ return prepared;
2487
+ }
2488
+ return createEagerPreparedDiagnosticData(definition, ownedData);
2489
+ }
2490
+
2491
+ function createEagerPreparedDiagnosticData(
2492
+ definition: CompiledDiagnosticDefinition,
2493
+ ownedData: Omit<PreparedDiagnosticDataContent, "definition"> & {
2494
+ readonly definitionIntegrity: string;
2495
+ },
2496
+ ): PreparedDiagnosticData {
2413
2497
  const identity: NormalizedDiagnosticPreparationIdentity =
2414
2498
  projectDiagnosticIdentity({
2415
2499
  ...ownedData,
2416
- filter: normalizeDiagnosticsFilterIdentity(filter),
2417
- expectedCells: expectedCells.map((cell) => ({
2500
+ filter: normalizeDiagnosticsFilterIdentity(ownedData.filter),
2501
+ expectedCells: ownedData.expectedCells.map((cell) => ({
2418
2502
  ...cell,
2419
2503
  source: cell.source ?? null,
2420
2504
  })),
@@ -2430,6 +2514,106 @@ export function prepareDiagnosticData(
2430
2514
  return prepared;
2431
2515
  }
2432
2516
 
2517
+ export function assertCompactPreparedDiagnosticData(
2518
+ value: unknown,
2519
+ ): asserts value is CompactPreparedDiagnosticData {
2520
+ if (
2521
+ value === null ||
2522
+ typeof value !== "object" ||
2523
+ !authenticCompact.has(value)
2524
+ )
2525
+ throw new DiagnosticValidationError([
2526
+ {
2527
+ domain: "input",
2528
+ code: "invalid-input-relationship",
2529
+ path: "$.prepared",
2530
+ message: "Compact prepared diagnostic data is not authentic",
2531
+ },
2532
+ ]);
2533
+ }
2534
+
2535
+ /**
2536
+ * Explicit eager bridge for callers that need the legacy complete JSON shape.
2537
+ * This materializes and hashes the full identity; compact execution never calls
2538
+ * it implicitly, and the compact value does not retain the resulting graph.
2539
+ */
2540
+ export function materializePreparedDiagnosticData(
2541
+ value: CompactPreparedDiagnosticData,
2542
+ ): PreparedDiagnosticData {
2543
+ assertCompactPreparedDiagnosticData(value);
2544
+ return createEagerPreparedDiagnosticData(value.definition, {
2545
+ definitionIntegrity: value.definition.definitionIntegrity,
2546
+ filter: value.filter,
2547
+ completePeriodCutoffs: value.completePeriodCutoffs,
2548
+ inputAudit: value.inputAudit,
2549
+ cells: value.cells,
2550
+ exposures: value.exposures,
2551
+ expectedCellsProvided: value.expectedCellsProvided,
2552
+ expectedCells: value.expectedCells,
2553
+ findings: value.findings,
2554
+ });
2555
+ }
2556
+
2557
+ /**
2558
+ * Core-owned, complete identity projection, emitted without a second retained
2559
+ * preparation graph. A structural document is not itself actuarial authority:
2560
+ * this factory authenticates the frozen preparation before creating one.
2561
+ */
2562
+ export function getCompactPreparedDiagnosticDataIdentityDocument(
2563
+ value: CompactPreparedDiagnosticData,
2564
+ ): DiagnosticIdentityDocument {
2565
+ assertCompactPreparedDiagnosticData(value);
2566
+ const previous = compactIdentityDocuments.get(value);
2567
+ if (previous) return previous;
2568
+ const document = createDiagnosticIdentityObject({
2569
+ definitionIntegrity: createDiagnosticIdentityValue(
2570
+ value.definition.definitionIntegrity,
2571
+ ),
2572
+ filter: createDiagnosticIdentityValue(
2573
+ normalizeDiagnosticsFilterIdentity(value.filter),
2574
+ ),
2575
+ completePeriodCutoffs: createDiagnosticIdentityValue(
2576
+ value.completePeriodCutoffs,
2577
+ ),
2578
+ inputAudit: createDiagnosticIdentityValue(value.inputAudit),
2579
+ cells: createDiagnosticIdentityValue(value.cells),
2580
+ exposures: createDiagnosticIdentityValue(value.exposures),
2581
+ expectedCellsProvided: createDiagnosticIdentityValue(
2582
+ value.expectedCellsProvided,
2583
+ ),
2584
+ expectedCells: createDiagnosticIdentityArray(
2585
+ value.expectedCells.length,
2586
+ (index) => {
2587
+ const cell = value.expectedCells[index]!;
2588
+ return createDiagnosticIdentityObject({
2589
+ sourceGroup: createDiagnosticIdentityValue(cell.sourceGroup),
2590
+ origin: createDiagnosticIdentityValue(cell.origin),
2591
+ valuation: createDiagnosticIdentityValue(cell.valuation),
2592
+ source: createDiagnosticIdentityValue(cell.source ?? null),
2593
+ });
2594
+ },
2595
+ ),
2596
+ findings: createDiagnosticIdentityValue(value.findings),
2597
+ });
2598
+ compactIdentityDocuments.set(value, document);
2599
+ return document;
2600
+ }
2601
+
2602
+ /** Compute the exact legacy preparation tag on explicit evidence access only. */
2603
+ export function getCompactPreparedDiagnosticDataFingerprint(
2604
+ value: CompactPreparedDiagnosticData,
2605
+ ): string {
2606
+ assertCompactPreparedDiagnosticData(value);
2607
+ const previous = compactFingerprints.get(value);
2608
+ if (previous) return previous;
2609
+ const tag = fingerprintDiagnosticIdentity(
2610
+ getCompactPreparedDiagnosticDataIdentityDocument(value),
2611
+ { kind: "diagnostic-preparation", property: "preparation" },
2612
+ );
2613
+ compactFingerprints.set(value, tag);
2614
+ return tag;
2615
+ }
2616
+
2433
2617
  export function assertPreparedDiagnosticData(
2434
2618
  value: unknown,
2435
2619
  ): asserts value is PreparedDiagnosticData {