@actuarial-ts/core 0.10.0 → 0.12.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 (91) hide show
  1. package/README.md +9 -7
  2. package/dist/analysisRecipe.d.ts.map +1 -1
  3. package/dist/analysisRecipe.js +20 -1
  4. package/dist/analysisRecipe.js.map +1 -1
  5. package/dist/canonical.d.ts.map +1 -1
  6. package/dist/canonical.js +64 -5
  7. package/dist/canonical.js.map +1 -1
  8. package/dist/customizationContracts.d.ts +16 -1
  9. package/dist/customizationContracts.d.ts.map +1 -1
  10. package/dist/descriptiveStatistics.js +1 -1
  11. package/dist/descriptiveStatistics.js.map +1 -1
  12. package/dist/diagnosticAggregation.d.ts +26 -0
  13. package/dist/diagnosticAggregation.d.ts.map +1 -1
  14. package/dist/diagnosticAggregation.js +125 -11
  15. package/dist/diagnosticAggregation.js.map +1 -1
  16. package/dist/diagnosticAuditText.d.ts +11 -0
  17. package/dist/diagnosticAuditText.d.ts.map +1 -0
  18. package/dist/diagnosticAuditText.js +179 -0
  19. package/dist/diagnosticAuditText.js.map +1 -0
  20. package/dist/diagnosticFormulas.d.ts.map +1 -1
  21. package/dist/diagnosticFormulas.js +71 -12
  22. package/dist/diagnosticFormulas.js.map +1 -1
  23. package/dist/diagnosticFreeze.d.ts +15 -1
  24. package/dist/diagnosticFreeze.d.ts.map +1 -1
  25. package/dist/diagnosticFreeze.js +82 -31
  26. package/dist/diagnosticFreeze.js.map +1 -1
  27. package/dist/diagnosticKeys.d.ts +9 -0
  28. package/dist/diagnosticKeys.d.ts.map +1 -0
  29. package/dist/diagnosticKeys.js +20 -0
  30. package/dist/diagnosticKeys.js.map +1 -0
  31. package/dist/diagnosticOrdering.d.ts.map +1 -1
  32. package/dist/diagnosticOrdering.js +9 -2
  33. package/dist/diagnosticOrdering.js.map +1 -1
  34. package/dist/diagnosticPeriods.d.ts.map +1 -1
  35. package/dist/diagnosticPeriods.js +19 -1
  36. package/dist/diagnosticPeriods.js.map +1 -1
  37. package/dist/diagnosticPreparation.d.ts +19 -0
  38. package/dist/diagnosticPreparation.d.ts.map +1 -1
  39. package/dist/diagnosticPreparation.js +544 -231
  40. package/dist/diagnosticPreparation.js.map +1 -1
  41. package/dist/diagnosticReview.d.ts.map +1 -1
  42. package/dist/diagnosticReview.js +419 -186
  43. package/dist/diagnosticReview.js.map +1 -1
  44. package/dist/diagnosticReviewSources.d.ts +6 -1
  45. package/dist/diagnosticReviewSources.d.ts.map +1 -1
  46. package/dist/diagnosticReviewSources.js +59 -14
  47. package/dist/diagnosticReviewSources.js.map +1 -1
  48. package/dist/diagnosticReviewStore.d.ts.map +1 -1
  49. package/dist/diagnosticReviewStore.js +48 -7
  50. package/dist/diagnosticReviewStore.js.map +1 -1
  51. package/dist/diagnosticRunner.d.ts.map +1 -1
  52. package/dist/diagnosticRunner.js +112 -37
  53. package/dist/diagnosticRunner.js.map +1 -1
  54. package/dist/diagnosticRuntime.d.ts.map +1 -1
  55. package/dist/diagnosticRuntime.js +11 -1
  56. package/dist/diagnosticRuntime.js.map +1 -1
  57. package/dist/diagnosticSourceOrdering.d.ts +27 -0
  58. package/dist/diagnosticSourceOrdering.d.ts.map +1 -1
  59. package/dist/diagnosticSourceOrdering.js +98 -12
  60. package/dist/diagnosticSourceOrdering.js.map +1 -1
  61. package/dist/mack.d.ts.map +1 -1
  62. package/dist/mack.js +35 -0
  63. package/dist/mack.js.map +1 -1
  64. package/dist/types.d.ts +1 -1
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/types.js +1 -0
  67. package/dist/types.js.map +1 -1
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +1 -1
  71. package/src/analysisRecipe.ts +18 -1
  72. package/src/canonical.ts +63 -5
  73. package/src/customizationContracts.ts +12 -1
  74. package/src/descriptiveStatistics.ts +1 -1
  75. package/src/diagnosticAggregation.ts +173 -9
  76. package/src/diagnosticAuditText.ts +187 -0
  77. package/src/diagnosticFormulas.ts +77 -27
  78. package/src/diagnosticFreeze.ts +75 -30
  79. package/src/diagnosticKeys.ts +22 -0
  80. package/src/diagnosticOrdering.ts +9 -8
  81. package/src/diagnosticPeriods.ts +21 -1
  82. package/src/diagnosticPreparation.ts +624 -272
  83. package/src/diagnosticReview.ts +566 -268
  84. package/src/diagnosticReviewSources.ts +69 -18
  85. package/src/diagnosticReviewStore.ts +48 -6
  86. package/src/diagnosticRunner.ts +133 -54
  87. package/src/diagnosticRuntime.ts +11 -1
  88. package/src/diagnosticSourceOrdering.ts +115 -11
  89. package/src/mack.ts +41 -0
  90. package/src/types.ts +1 -0
  91. package/src/version.ts +1 -1
@@ -1,6 +1,10 @@
1
1
  import { canonicalJson, fnv1a64 } from "./canonical.js";
2
- import { freezeDiagnosticGraph as deepFreeze } from "./diagnosticFreeze.js";
3
- import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
2
+ import {
3
+ freezeDiagnosticGraph as deepFreeze,
4
+ freezeOwnedDiagnosticGraph as deepFreezeOwned,
5
+ } from "./diagnosticFreeze.js";
6
+ import { auditRecordCanonicalJson } from "./diagnosticAuditText.js";
7
+ import { canonicalScalarArrayJson } from "./diagnosticKeys.js";
4
8
  import {
5
9
  createDiagnosticIdentityArray,
6
10
  createDiagnosticIdentityObject,
@@ -32,7 +36,9 @@ import {
32
36
  } from "./diagnosticDefinitions.js";
33
37
  import {
34
38
  auditedDiagnosticContribution,
35
- finalizeDiagnosticContributions,
39
+ createDiagnosticStatsInterner,
40
+ finalizeDiagnosticContributionsInterned,
41
+ ownedDiagnosticContribution,
36
42
  type DiagnosticMeasureContribution,
37
43
  type DiagnosticStructuralBlocker,
38
44
  } from "./diagnosticAggregation.js";
@@ -233,6 +239,8 @@ export function reselectCompactDiagnosticMetrics(
233
239
  if (sourceScope(normalized) !== sourceScope(prepared.filter)) return undefined;
234
240
  const selected = Object.freeze({ ...prepared, filter: deepFreeze(normalized) }) as CompactPreparedDiagnosticData;
235
241
  authenticCompact.add(selected);
242
+ const recordedGroups = preparedSourceGroups.get(prepared);
243
+ if (recordedGroups !== undefined) preparedSourceGroups.set(selected, recordedGroups);
236
244
  const parent = sourceSelectionParents.get(prepared);
237
245
  const coordinateParent = coordinateSelectionParents.get(prepared);
238
246
  if (coordinateParent) coordinateSelectionParents.set(selected, coordinateParent);
@@ -276,7 +284,7 @@ function selectCompactSourceScope(
276
284
  allowCoordinates = false,
277
285
  ): CompactPreparedDiagnosticData | undefined {
278
286
  assertCompactPreparedDiagnosticData(prepared);
279
- const metadata = sourceSelectionMetadata.get(prepared);
287
+ const selection = sourceSelectionMetadata.get(prepared);
280
288
  const boundaryIssues = preparationBoundaryIssues({
281
289
  definition: prepared.definition, losses: [], exposures: [],
282
290
  ...(filter === undefined ? {} : { filter }),
@@ -284,10 +292,11 @@ function selectCompactSourceScope(
284
292
  if (boundaryIssues.length) throw new DiagnosticValidationError(boundaryIssues);
285
293
  const issues: DiagnosticValidationIssue[] = [];
286
294
  const normalizedFilter = normalizeFilter(prepared.definition, filter,
287
- metadata?.groups ?? new Set(prepared.inputAudit.map((item) => item.record.sourceGroup)), issues);
295
+ selection?.groups ?? new Set(prepared.inputAudit.map((item) => item.record.sourceGroup)), issues);
288
296
  const normalized = normalizedFilter.value;
289
297
  if (issues.length) throw new DiagnosticValidationError(issues);
290
- if (!metadata) return undefined;
298
+ if (!selection) return undefined;
299
+ const metadata = selection.load();
291
300
  if (allowOrigins && !metadata.allOriginsSelected) return undefined;
292
301
  if (allowCoordinates && !metadata.allCoordinatesSelected) return undefined;
293
302
  const remainingScope = (value: DiagnosticsFilter | null) => {
@@ -323,12 +332,12 @@ function selectCompactSourceScope(
323
332
  const cells = Object.freeze(prepared.cells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation)));
324
333
  const exposures = Object.freeze(prepared.exposures.filter((exposure) => exposure.status === "valid" && includes(exposure.sourceGroup) && includesOrigin(exposure.origin) && includesDate(exposure.origin, exposure.valuation)));
325
334
  const coordinateKeys = allowCoordinates ? new Set(cells.map((cell) => cellKey(cell.sourceGroup, cell.origin, cell.valuation))) : undefined;
326
- const sourceOrigins = allowCoordinates ? new Set(cells.map((cell) => canonicalJson([cell.sourceGroup, cell.origin]))) : undefined;
335
+ const sourceOrigins = allowCoordinates ? new Set(cells.map((cell) => sourceOriginKey(cell.sourceGroup, cell.origin))) : undefined;
327
336
  const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
328
337
  const orphanFindings = allowCoordinates ? exposures.flatMap((exposure) => {
329
338
  if (exposure.status !== "valid") return [];
330
339
  const matches = internals.measuresById.get(exposure.measureId)!.exposureTiming === "origin-static"
331
- ? sourceOrigins!.has(canonicalJson([exposure.sourceGroup, exposure.origin]))
340
+ ? sourceOrigins!.has(sourceOriginKey(exposure.sourceGroup, exposure.origin))
332
341
  : exposure.valuation !== undefined && coordinateKeys!.has(cellKey(exposure.sourceGroup, exposure.origin, exposure.valuation));
333
342
  return matches ? [] : [exposureWithoutLossFinding(exposure)];
334
343
  }) : [];
@@ -342,6 +351,8 @@ function selectCompactSourceScope(
342
351
  expectedCells: Object.freeze(prepared.expectedCells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation))),
343
352
  }) as CompactPreparedDiagnosticData;
344
353
  authenticCompact.add(selected);
354
+ const knownGroups = preparedSourceGroups.get(prepared);
355
+ if (knownGroups !== undefined) preparedSourceGroups.set(selected, knownGroups);
345
356
  if (allowCoordinates) coordinateSelectionParents.set(selected, prepared);
346
357
  else sourceSelectionParents.set(selected, prepared);
347
358
  return selected;
@@ -433,16 +444,46 @@ const authentic = new WeakSet<object>();
433
444
  const authenticCompact = new WeakSet<object>();
434
445
  const sourceSelectionParents = new WeakMap<CompactPreparedDiagnosticData, CompactPreparedDiagnosticData>();
435
446
  const coordinateSelectionParents = new WeakMap<CompactPreparedDiagnosticData, CompactPreparedDiagnosticData>();
436
- const sourceSelectionMetadata = new WeakMap<CompactPreparedDiagnosticData, {
437
- groups: ReadonlySet<string>;
438
- excluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
439
- originExcluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
440
- origins: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
441
- valuations: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
442
- allOriginsSelected: boolean;
443
- allCoordinatesSelected: boolean;
444
- auditRanks: ReadonlyMap<DiagnosticInputAuditRecord, number>;
445
- }>();
447
+ interface CompactSelectionMetadata {
448
+ readonly excluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
449
+ readonly originExcluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
450
+ readonly origins: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
451
+ readonly valuations: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
452
+ readonly allOriginsSelected: boolean;
453
+ readonly allCoordinatesSelected: boolean;
454
+ readonly auditRanks: ReadonlyMap<DiagnosticInputAuditRecord, number>;
455
+ }
456
+ /**
457
+ * Selection ranks for every included/excluded audit variant are derived only
458
+ * from the preparation's own owned snapshots, so they are computed on the
459
+ * first source/origin/date query rather than for every preparation. The
460
+ * captured preparation state is released once the ranks exist.
461
+ */
462
+ class LazySelectionMetadata {
463
+ #compute: (() => CompactSelectionMetadata) | null;
464
+ #value: CompactSelectionMetadata | undefined;
465
+ constructor(
466
+ readonly groups: ReadonlySet<string>,
467
+ compute: () => CompactSelectionMetadata,
468
+ ) {
469
+ this.#compute = compute;
470
+ }
471
+ load(): CompactSelectionMetadata {
472
+ if (this.#value === undefined) {
473
+ this.#value = this.#compute!();
474
+ this.#compute = null;
475
+ }
476
+ return this.#value;
477
+ }
478
+ }
479
+ const sourceSelectionMetadata = new WeakMap<CompactPreparedDiagnosticData, LazySelectionMetadata>();
480
+ // Every source group present in a preparation's input rows, hence in its audit.
481
+ // Selections keep the same audit records, so they share their parent's set.
482
+ const preparedSourceGroups = new WeakMap<object, ReadonlySet<string>>();
483
+ /** Source groups audited by an authentic preparation, when it recorded them. */
484
+ export function getPreparedSourceGroups(prepared: object): ReadonlySet<string> | undefined {
485
+ return preparedSourceGroups.get(prepared);
486
+ }
446
487
  const compactIdentityDocuments = new WeakMap<
447
488
  object,
448
489
  DiagnosticIdentityDocument
@@ -535,6 +576,80 @@ function uniqueSorted(values: readonly string[]): string[] {
535
576
  return [...new Set(values)].sort(codeUnit);
536
577
  }
537
578
 
579
+ /** Own keys in code-unit order; skips the sort when insertion order already is. */
580
+ function sortedKeys(record: object): string[] {
581
+ const keys = Object.keys(record);
582
+ for (let index = 1; index < keys.length; index++)
583
+ if (keys[index - 1]! > keys[index]!) return keys.sort(codeUnit);
584
+ return keys;
585
+ }
586
+
587
+ /**
588
+ * Own-key copy in code-unit key order with each value mapped, built without
589
+ * entry pairs. A literal "__proto__" key stays own data, as Object.fromEntries
590
+ * would keep it.
591
+ */
592
+ function sortedRecordCopy<T, U>(
593
+ record: Readonly<Record<string, T>>,
594
+ map: (value: T) => U,
595
+ ): Record<string, U> {
596
+ const keys = sortedKeys(record);
597
+ const out: Record<string, U> = {};
598
+ for (let index = 0; index < keys.length; index++) {
599
+ const key = keys[index]!;
600
+ const value = map(record[key]!);
601
+ if (key === "__proto__")
602
+ Object.defineProperty(out, key, {
603
+ value,
604
+ enumerable: true,
605
+ writable: true,
606
+ configurable: true,
607
+ });
608
+ else out[key] = value;
609
+ }
610
+ return out;
611
+ }
612
+
613
+ /** Assigns own data even for a literal "__proto__" measure id. */
614
+ function setMeasureEntry<T>(record: Record<string, T>, id: string, value: T): void {
615
+ if (id === "__proto__")
616
+ Object.defineProperty(record, id, {
617
+ value,
618
+ enumerable: true,
619
+ writable: true,
620
+ configurable: true,
621
+ });
622
+ else record[id] = value;
623
+ }
624
+
625
+ /** A fresh empty list per measure id, in id order; a literal "__proto__" id stays own data. */
626
+ function emptyListsByMeasure<T>(measureIds: readonly string[]): Record<string, T[]> {
627
+ const out: Record<string, T[]> = {};
628
+ for (let index = 0; index < measureIds.length; index++)
629
+ setMeasureEntry(out, measureIds[index]!, [] as T[]);
630
+ return out;
631
+ }
632
+
633
+ /**
634
+ * One shared immutable empty list per measure id. A caller that needs to append
635
+ * must first replace the entry, so the shared list is never mutated; most cells
636
+ * never append at all and keep every entry pointing at it.
637
+ */
638
+ function sharedEmptyListsByMeasure<T>(
639
+ measureIds: readonly string[],
640
+ empty: readonly T[],
641
+ ): Record<string, readonly T[]> {
642
+ const out: Record<string, readonly T[]> = {};
643
+ for (let index = 0; index < measureIds.length; index++)
644
+ setMeasureEntry(out, measureIds[index]!, empty);
645
+ return out;
646
+ }
647
+
648
+ /** Internal coordinate keys: validated tokens never contain U+0000. */
649
+ function sourceOriginKey(sourceGroup: string, origin: string): string {
650
+ return `${sourceGroup}${origin}`;
651
+ }
652
+
538
653
  function normalizeSources(
539
654
  values: readonly (DiagnosticSourceLocation | undefined | null)[],
540
655
  ): DiagnosticSourceLocation[] {
@@ -625,15 +740,9 @@ function mergeFindings(
625
740
 
626
741
  function snapshotLoss(
627
742
  row: DiagnosticLossInput,
743
+ audited: (value: number | null) => DiagnosticAuditedNumericValue = auditDiagnosticNumber,
628
744
  ): DiagnosticLossInputAuditSnapshot {
629
- const measures = Object.fromEntries(
630
- Object.keys(row.measures)
631
- .sort(codeUnit)
632
- .map((measureId) => [
633
- measureId,
634
- auditDiagnosticNumber(row.measures[measureId] ?? null),
635
- ]),
636
- );
745
+ const measures = sortedRecordCopy(row.measures, (value) => audited(value ?? null));
637
746
  return {
638
747
  recordId: row.recordId,
639
748
  rowType: row.rowType,
@@ -649,6 +758,7 @@ function snapshotLoss(
649
758
 
650
759
  function snapshotExposure(
651
760
  row: DiagnosticExposureObservation,
761
+ audited: (value: number | null) => DiagnosticAuditedNumericValue = auditDiagnosticNumber,
652
762
  ): DiagnosticExposureInputAuditSnapshot {
653
763
  return {
654
764
  key: row.key,
@@ -656,7 +766,7 @@ function snapshotExposure(
656
766
  origin: row.origin,
657
767
  valuation: row.valuation ?? null,
658
768
  measureId: row.measureId,
659
- value: auditDiagnosticNumber(row.value),
769
+ value: audited(row.value),
660
770
  complete: row.complete,
661
771
  source: row.source === undefined ? null : { ...row.source },
662
772
  };
@@ -1007,16 +1117,13 @@ function coordinateFor(
1007
1117
  };
1008
1118
  }
1009
1119
 
1120
+ /** Internal cell key: validated tokens and period labels never contain U+0000. */
1010
1121
  function cellKey(
1011
1122
  sourceGroup: string,
1012
1123
  origin: string,
1013
1124
  valuation: string,
1014
1125
  ): string {
1015
- return canonicalJson([sourceGroup, origin, valuation]);
1016
- }
1017
-
1018
- function parseCellKey(key: string): readonly [string, string, string] {
1019
- return JSON.parse(key) as [string, string, string];
1126
+ return `${sourceGroup}${origin}${valuation}`;
1020
1127
  }
1021
1128
 
1022
1129
  function coordinateContext(candidate: {
@@ -1141,7 +1248,7 @@ function commonCoordinate(
1141
1248
  }
1142
1249
 
1143
1250
  function exposureIdentity(candidate: ExposureCandidate): string {
1144
- return canonicalJson(
1251
+ return canonicalScalarArrayJson(
1145
1252
  candidate.timing === "valuation-specific"
1146
1253
  ? [
1147
1254
  candidate.row.measureId,
@@ -1187,38 +1294,212 @@ function exposureCoordinateContext(
1187
1294
  return { sourceGroup: coherentSource[0], origin: coherentOrigin[0] };
1188
1295
  }
1189
1296
 
1190
- function createAuditComparator(): (
1191
- left: DiagnosticInputAuditRecord,
1192
- right: DiagnosticInputAuditRecord,
1193
- ) => number {
1194
- const rank = { loss: 0, exposure: 1, "expected-cell": 2 } as const;
1195
- const disposition = {
1196
- invalid: 0,
1197
- "complete-period-cutoff": 1,
1198
- filter: 2,
1199
- retained: 3,
1200
- } as const;
1297
+ const AUDIT_KIND_RANK = { loss: 0, exposure: 1, "expected-cell": 2 } as const;
1298
+ const AUDIT_DISPOSITION_RANK = {
1299
+ invalid: 0,
1300
+ "complete-period-cutoff": 1,
1301
+ filter: 2,
1302
+ retained: 3,
1303
+ } as const;
1304
+
1305
+ export interface DiagnosticAuditOrder {
1306
+ readonly compare: (
1307
+ left: DiagnosticInputAuditRecord,
1308
+ right: DiagnosticInputAuditRecord,
1309
+ ) => number;
1310
+ /** Canonical text of a record, serialized once per record for this order. */
1311
+ readonly key: (record: DiagnosticInputAuditRecord["record"]) => string;
1312
+ }
1313
+
1314
+ function createAuditComparator(): DiagnosticAuditOrder {
1201
1315
  // Audit snapshots are already owned and complete before sorting. Preserve the
1202
1316
  // exact canonical-text ordering, but serialize each compared record once.
1203
1317
  // Keep this memo local to the sort; it must not retain keys with the result.
1204
- const keys = new WeakMap<DiagnosticInputAuditRecord["record"], string>();
1318
+ // A strong table is released with the comparator and looks up faster than a
1319
+ // weak one for the millions of comparisons a large audit sort performs.
1320
+ const keys = new Map<DiagnosticInputAuditRecord["record"], string>();
1205
1321
  const key = (record: DiagnosticInputAuditRecord["record"]): string => {
1206
1322
  const previous = keys.get(record);
1207
1323
  if (previous !== undefined) return previous;
1208
- const text = canonicalJson(record);
1324
+ const text = auditRecordCanonicalJson(record);
1209
1325
  keys.set(record, text);
1210
1326
  return text;
1211
1327
  };
1212
- return (left, right) =>
1213
- rank[left.kind] - rank[right.kind] ||
1214
- (left.record === right.record ? 0 : codeUnit(key(left.record), key(right.record))) ||
1215
- disposition[left.disposition] - disposition[right.disposition];
1328
+ return {
1329
+ key,
1330
+ compare: (left, right) =>
1331
+ AUDIT_KIND_RANK[left.kind] - AUDIT_KIND_RANK[right.kind] ||
1332
+ (left.record === right.record ? 0 : codeUnit(key(left.record), key(right.record))) ||
1333
+ AUDIT_DISPOSITION_RANK[left.disposition] - AUDIT_DISPOSITION_RANK[right.disposition],
1334
+ };
1335
+ }
1336
+
1337
+ /**
1338
+ * Sorts audit records in place by the comparator's order, comparing texts that
1339
+ * were resolved once per record instead of looking each up on every comparison.
1340
+ * The stable sort over the decorated rows yields exactly `audit.sort(compare)`.
1341
+ */
1342
+ function sortAuditRecords(
1343
+ audit: DiagnosticInputAuditRecord[],
1344
+ order: DiagnosticAuditOrder,
1345
+ ): void {
1346
+ interface Row {
1347
+ readonly item: DiagnosticInputAuditRecord;
1348
+ readonly kind: number;
1349
+ readonly disposition: number;
1350
+ /** Canonical text, resolved the first time a comparison needs it. */
1351
+ key: string | undefined;
1352
+ }
1353
+ const decorated: Row[] = audit.map((item) => ({
1354
+ item,
1355
+ kind: AUDIT_KIND_RANK[item.kind],
1356
+ disposition: AUDIT_DISPOSITION_RANK[item.disposition],
1357
+ key: undefined,
1358
+ }));
1359
+ const keyOf = (row: Row): string => row.key ?? (row.key = order.key(row.item.record));
1360
+ decorated.sort(
1361
+ (left, right) =>
1362
+ left.kind - right.kind ||
1363
+ (left.item.record === right.item.record ? 0 : codeUnit(keyOf(left), keyOf(right))) ||
1364
+ left.disposition - right.disposition,
1365
+ );
1366
+ for (let index = 0; index < decorated.length; index++) audit[index] = decorated[index]!.item;
1367
+ }
1368
+
1369
+ /**
1370
+ * Every included/excluded audit variant in the comparator's order, built from
1371
+ * the already sorted audit instead of re-sorting three copies of it. The audit
1372
+ * holds each record once in comparator order, so when no two distinct records
1373
+ * of one kind share a canonical text, the sorted variants are exactly: each
1374
+ * audit record's variants in disposition order (stable for equal dispositions:
1375
+ * the audit item, then its source-excluded, then its origin-excluded variant),
1376
+ * with variants whose record is a re-labelled raw snapshot merged in by the
1377
+ * same comparator. Any canonical-text tie falls back to the full stable sort.
1378
+ * Exported for the equivalence test only.
1379
+ */
1380
+ export function orderAuditVariants(
1381
+ audit: readonly DiagnosticInputAuditRecord[],
1382
+ excluded: ReadonlyMap<object, DiagnosticInputAuditRecord>,
1383
+ originExcluded: ReadonlyMap<object, DiagnosticInputAuditRecord>,
1384
+ order: DiagnosticAuditOrder,
1385
+ ): DiagnosticInputAuditRecord[] {
1386
+ const everything = () =>
1387
+ [...audit, ...excluded.values(), ...originExcluded.values()].sort(order.compare);
1388
+ for (let index = 1; index < audit.length; index++) {
1389
+ const previous = audit[index - 1]!;
1390
+ const current = audit[index]!;
1391
+ if (
1392
+ previous.kind === current.kind &&
1393
+ previous.record !== current.record &&
1394
+ order.key(previous.record) === order.key(current.record)
1395
+ )
1396
+ return everything();
1397
+ }
1398
+ const auditRecords = new Set<object>();
1399
+ for (const item of audit) auditRecords.add(item.record);
1400
+ const excludedByRecord = new Map<object, DiagnosticInputAuditRecord>();
1401
+ const detached: DiagnosticInputAuditRecord[] = [];
1402
+ for (const variant of excluded.values()) {
1403
+ if (auditRecords.has(variant.record)) excludedByRecord.set(variant.record, variant);
1404
+ else detached.push(variant);
1405
+ }
1406
+ if (detached.length > 0) {
1407
+ const auditKeys = new Set(
1408
+ audit.map((item) => `${AUDIT_KIND_RANK[item.kind]}:${order.key(item.record)}`),
1409
+ );
1410
+ for (const variant of detached)
1411
+ if (auditKeys.has(`${AUDIT_KIND_RANK[variant.kind]}:${order.key(variant.record)}`))
1412
+ return everything();
1413
+ detached.sort(order.compare);
1414
+ }
1415
+ const ordered: DiagnosticInputAuditRecord[] = [];
1416
+ let next = 0;
1417
+ for (const item of audit) {
1418
+ while (next < detached.length && order.compare(detached[next]!, item) < 0)
1419
+ ordered.push(detached[next++]!);
1420
+ const variants = [item];
1421
+ const excludedVariant = excludedByRecord.get(item.record);
1422
+ if (excludedVariant !== undefined) variants.push(excludedVariant);
1423
+ const originVariant = originExcluded.get(item.record);
1424
+ if (originVariant !== undefined) variants.push(originVariant);
1425
+ variants.sort(
1426
+ (left, right) =>
1427
+ AUDIT_DISPOSITION_RANK[left.disposition] - AUDIT_DISPOSITION_RANK[right.disposition],
1428
+ );
1429
+ for (const variant of variants) ordered.push(variant);
1430
+ }
1431
+ while (next < detached.length) ordered.push(detached[next++]!);
1432
+ return ordered;
1216
1433
  }
1217
1434
 
1218
1435
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
1219
1436
  return isDiagnosticPlainRecord(value);
1220
1437
  }
1221
1438
 
1439
+ // Allowed-key sets and the exact path suffixes each row check reports under.
1440
+ // Both are constants, so a clean row allocates no path text at all: every
1441
+ // string below is built inside the branch that reports an issue.
1442
+ const PREPARATION_INPUT_KEYS: ReadonlySet<string> = new Set([
1443
+ "definition",
1444
+ "losses",
1445
+ "exposures",
1446
+ "filter",
1447
+ "completePeriodCutoffs",
1448
+ "expectedCells",
1449
+ ]);
1450
+ const LOSS_ROW_KEYS: ReadonlySet<string> = new Set([
1451
+ "rowType",
1452
+ "recordId",
1453
+ "claimId",
1454
+ "sourceGroup",
1455
+ "origin",
1456
+ "valuation",
1457
+ "complete",
1458
+ "source",
1459
+ "measures",
1460
+ ]);
1461
+ const EXPOSURE_ROW_KEYS: ReadonlySet<string> = new Set([
1462
+ "key",
1463
+ "sourceGroup",
1464
+ "origin",
1465
+ "valuation",
1466
+ "measureId",
1467
+ "value",
1468
+ "complete",
1469
+ "source",
1470
+ ]);
1471
+ const SOURCE_LOCATION_KEYS: ReadonlySet<string> = new Set([
1472
+ "artifactId",
1473
+ "sourceFile",
1474
+ "sourceSheet",
1475
+ "sourceRow",
1476
+ "sourceCell",
1477
+ ]);
1478
+ const FILTER_KEYS: ReadonlySet<string> = new Set([
1479
+ "sourceGroups",
1480
+ "outputGroups",
1481
+ "origins",
1482
+ "originFrom",
1483
+ "originThrough",
1484
+ "valuations",
1485
+ "valuationFrom",
1486
+ "valuationThrough",
1487
+ "minDevelopmentAge",
1488
+ "maxDevelopmentAge",
1489
+ "instanceIds",
1490
+ ]);
1491
+ const CUTOFF_KEYS: ReadonlySet<string> = new Set([
1492
+ "sourceGroup",
1493
+ "originThrough",
1494
+ "valuationThrough",
1495
+ ]);
1496
+ const EXPECTED_CELL_KEYS: ReadonlySet<string> = new Set([
1497
+ "sourceGroup",
1498
+ "origin",
1499
+ "valuation",
1500
+ "source",
1501
+ ]);
1502
+
1222
1503
  function preparationBoundaryIssues(
1223
1504
  value: unknown,
1224
1505
  ): DiagnosticValidationIssue[] {
@@ -1238,6 +1519,9 @@ function preparationBoundaryIssues(
1238
1519
  path: string,
1239
1520
  message: string,
1240
1521
  ) => issues.push({ domain, code, path, message });
1522
+ /** Row path with a constant suffix, built only when an issue cites it. */
1523
+ const at = (prefix: string, index: number, suffix: string) =>
1524
+ `${prefix}[${index}]${suffix}`;
1241
1525
  const token = (
1242
1526
  item: unknown,
1243
1527
  path: string,
@@ -1253,36 +1537,100 @@ function preparationBoundaryIssues(
1253
1537
  "Expected a nonempty token with valid Unicode and no U+0000",
1254
1538
  );
1255
1539
  };
1540
+ /** `token` for a row field; the path text exists only for a reported issue. */
1541
+ const tokenAt = (
1542
+ item: unknown,
1543
+ prefix: string,
1544
+ index: number,
1545
+ suffix: string,
1546
+ domain: "input" | "configuration" = "input",
1547
+ ) => {
1548
+ if (typeof item === "string" && isDiagnosticToken(item)) return;
1549
+ token(item, at(prefix, index, suffix), domain);
1550
+ };
1256
1551
  const exactKeys = (
1257
1552
  item: Record<string, unknown>,
1258
- allowed: readonly string[],
1553
+ allowed: ReadonlySet<string>,
1259
1554
  path: string,
1260
1555
  domain: "input" | "configuration",
1261
1556
  ) => {
1262
1557
  for (const key of Object.keys(item)) {
1263
- const keyPath = `${path}.${key}`;
1264
- if (!allowed.includes(key))
1265
- issue(domain, "unknown-key", keyPath, `Unknown key ${key}`);
1558
+ if (!allowed.has(key))
1559
+ issue(domain, "unknown-key", `${path}.${key}`, `Unknown key ${key}`);
1266
1560
  else if (item[key] === undefined)
1267
1561
  issue(
1268
1562
  domain,
1269
1563
  "invalid-type",
1270
- keyPath,
1564
+ `${path}.${key}`,
1271
1565
  "Explicit undefined is not allowed",
1272
1566
  );
1273
1567
  }
1274
1568
  };
1569
+ /** `exactKeys` for a row, keeping the same own-key order and messages. */
1570
+ const exactKeysAt = (
1571
+ item: Record<string, unknown>,
1572
+ allowed: ReadonlySet<string>,
1573
+ prefix: string,
1574
+ index: number,
1575
+ suffix: string,
1576
+ domain: "input" | "configuration",
1577
+ ) => {
1578
+ for (const key in item) {
1579
+ if (!Object.hasOwn(item, key)) continue;
1580
+ if (!allowed.has(key))
1581
+ issue(
1582
+ domain,
1583
+ "unknown-key",
1584
+ `${at(prefix, index, suffix)}.${key}`,
1585
+ `Unknown key ${key}`,
1586
+ );
1587
+ else if (item[key] === undefined)
1588
+ issue(
1589
+ domain,
1590
+ "invalid-type",
1591
+ `${at(prefix, index, suffix)}.${key}`,
1592
+ "Explicit undefined is not allowed",
1593
+ );
1594
+ }
1595
+ };
1596
+ /** Source of a row; every reported path lies under the row's `.source`. */
1597
+ const sourceAt = (item: unknown, prefix: string, index: number) => {
1598
+ if (!isPlainRecord(item)) {
1599
+ issue(
1600
+ "input",
1601
+ "invalid-type",
1602
+ at(prefix, index, ".source"),
1603
+ "Source location must be an object",
1604
+ );
1605
+ return;
1606
+ }
1607
+ exactKeysAt(item, SOURCE_LOCATION_KEYS, prefix, index, ".source", "input");
1608
+ tokenAt(item.artifactId, prefix, index, ".source.artifactId");
1609
+ if (item.sourceFile !== undefined)
1610
+ tokenAt(item.sourceFile, prefix, index, ".source.sourceFile");
1611
+ if (item.sourceSheet !== undefined)
1612
+ tokenAt(item.sourceSheet, prefix, index, ".source.sourceSheet");
1613
+ if (item.sourceCell !== undefined)
1614
+ tokenAt(item.sourceCell, prefix, index, ".source.sourceCell");
1615
+ if (
1616
+ item.sourceRow !== undefined &&
1617
+ (typeof item.sourceRow !== "number" ||
1618
+ !Number.isSafeInteger(item.sourceRow) ||
1619
+ item.sourceRow < 0)
1620
+ )
1621
+ issue(
1622
+ "input",
1623
+ "invalid-number",
1624
+ at(prefix, index, ".source.sourceRow"),
1625
+ "Source row must be a nonnegative safe integer",
1626
+ );
1627
+ };
1275
1628
  const source = (item: unknown, path: string) => {
1276
1629
  if (!isPlainRecord(item)) {
1277
1630
  issue("input", "invalid-type", path, "Source location must be an object");
1278
1631
  return;
1279
1632
  }
1280
- exactKeys(
1281
- item,
1282
- ["artifactId", "sourceFile", "sourceSheet", "sourceRow", "sourceCell"],
1283
- path,
1284
- "input",
1285
- );
1633
+ exactKeys(item, SOURCE_LOCATION_KEYS, path, "input");
1286
1634
  token(item.artifactId, `${path}.artifactId`);
1287
1635
  for (const key of ["sourceFile", "sourceSheet", "sourceCell"] as const)
1288
1636
  if (item[key] !== undefined) token(item[key], `${path}.${key}`);
@@ -1310,141 +1658,104 @@ function preparationBoundaryIssues(
1310
1658
  }
1311
1659
  return item;
1312
1660
  };
1313
- exactKeys(
1314
- value,
1315
- [
1316
- "definition",
1317
- "losses",
1318
- "exposures",
1319
- "filter",
1320
- "completePeriodCutoffs",
1321
- "expectedCells",
1322
- ],
1323
- "$",
1324
- "configuration",
1325
- );
1326
- for (const [index, raw] of array(
1327
- value.losses,
1328
- "$.losses",
1329
- "input",
1330
- ).entries()) {
1331
- const path = `$.losses[${index}]`;
1661
+ exactKeys(value, PREPARATION_INPUT_KEYS, "$", "configuration");
1662
+ const losses = array(value.losses, "$.losses", "input");
1663
+ for (let index = 0; index < losses.length; index++) {
1664
+ const raw: unknown = losses[index];
1332
1665
  if (!isPlainRecord(raw)) {
1333
- issue("input", "invalid-type", path, "Loss record must be an object");
1666
+ issue(
1667
+ "input",
1668
+ "invalid-type",
1669
+ at("$.losses", index, ""),
1670
+ "Loss record must be an object",
1671
+ );
1334
1672
  continue;
1335
1673
  }
1336
- exactKeys(
1337
- raw,
1338
- [
1339
- "rowType",
1340
- "recordId",
1341
- "claimId",
1342
- "sourceGroup",
1343
- "origin",
1344
- "valuation",
1345
- "complete",
1346
- "source",
1347
- "measures",
1348
- ],
1349
- path,
1350
- "input",
1351
- );
1674
+ exactKeysAt(raw, LOSS_ROW_KEYS, "$.losses", index, "", "input");
1352
1675
  if (raw.rowType !== "claim" && raw.rowType !== "aggregate")
1353
1676
  issue(
1354
1677
  "input",
1355
1678
  "invalid-type",
1356
- `${path}.rowType`,
1679
+ at("$.losses", index, ".rowType"),
1357
1680
  "Loss row type must be claim or aggregate",
1358
1681
  );
1359
- token(raw.recordId, `${path}.recordId`);
1360
- token(raw.sourceGroup, `${path}.sourceGroup`);
1361
- token(raw.origin, `${path}.origin`);
1362
- token(raw.valuation, `${path}.valuation`);
1363
- if (raw.rowType === "claim") token(raw.claimId, `${path}.claimId`);
1682
+ tokenAt(raw.recordId, "$.losses", index, ".recordId");
1683
+ tokenAt(raw.sourceGroup, "$.losses", index, ".sourceGroup");
1684
+ tokenAt(raw.origin, "$.losses", index, ".origin");
1685
+ tokenAt(raw.valuation, "$.losses", index, ".valuation");
1686
+ if (raw.rowType === "claim")
1687
+ tokenAt(raw.claimId, "$.losses", index, ".claimId");
1364
1688
  else if (raw.claimId !== undefined)
1365
1689
  issue(
1366
1690
  "input",
1367
1691
  "unknown-key",
1368
- `${path}.claimId`,
1692
+ at("$.losses", index, ".claimId"),
1369
1693
  "Aggregate loss rows cannot contain claimId",
1370
1694
  );
1371
1695
  if (typeof raw.complete !== "boolean")
1372
1696
  issue(
1373
1697
  "input",
1374
1698
  "invalid-type",
1375
- `${path}.complete`,
1699
+ at("$.losses", index, ".complete"),
1376
1700
  "Loss completeness must be boolean",
1377
1701
  );
1378
- if (raw.source !== undefined) source(raw.source, `${path}.source`);
1379
- if (!isPlainRecord(raw.measures))
1702
+ if (raw.source !== undefined) sourceAt(raw.source, "$.losses", index);
1703
+ const measures: unknown = raw.measures;
1704
+ if (!isPlainRecord(measures))
1380
1705
  issue(
1381
1706
  "input",
1382
1707
  "invalid-type",
1383
- `${path}.measures`,
1708
+ at("$.losses", index, ".measures"),
1384
1709
  "Measures must be an object",
1385
1710
  );
1386
1711
  else
1387
- for (const [measureId, amount] of Object.entries(raw.measures)) {
1388
- token(measureId, `${path}.measures`);
1712
+ for (const measureId in measures) {
1713
+ if (!Object.hasOwn(measures, measureId)) continue;
1714
+ tokenAt(measureId, "$.losses", index, ".measures");
1715
+ const amount: unknown = measures[measureId];
1389
1716
  if (amount !== null && typeof amount !== "number")
1390
1717
  issue(
1391
1718
  "input",
1392
1719
  "invalid-type",
1393
- `${path}.measures.${measureId}`,
1720
+ `${at("$.losses", index, ".measures")}.${measureId}`,
1394
1721
  "Measure value must be a number or null",
1395
1722
  );
1396
1723
  }
1397
1724
  }
1398
- for (const [index, raw] of array(
1399
- value.exposures,
1400
- "$.exposures",
1401
- "input",
1402
- ).entries()) {
1403
- const path = `$.exposures[${index}]`;
1725
+ const exposures = array(value.exposures, "$.exposures", "input");
1726
+ for (let index = 0; index < exposures.length; index++) {
1727
+ const raw: unknown = exposures[index];
1404
1728
  if (!isPlainRecord(raw)) {
1405
1729
  issue(
1406
1730
  "input",
1407
1731
  "invalid-type",
1408
- path,
1732
+ at("$.exposures", index, ""),
1409
1733
  "Exposure observation must be an object",
1410
1734
  );
1411
1735
  continue;
1412
1736
  }
1413
- exactKeys(
1414
- raw,
1415
- [
1416
- "key",
1417
- "sourceGroup",
1418
- "origin",
1419
- "valuation",
1420
- "measureId",
1421
- "value",
1422
- "complete",
1423
- "source",
1424
- ],
1425
- path,
1426
- "input",
1427
- );
1428
- token(raw.key, `${path}.key`);
1429
- token(raw.sourceGroup, `${path}.sourceGroup`);
1430
- token(raw.origin, `${path}.origin`);
1431
- token(raw.measureId, `${path}.measureId`);
1432
- if (raw.valuation !== undefined) token(raw.valuation, `${path}.valuation`);
1737
+ exactKeysAt(raw, EXPOSURE_ROW_KEYS, "$.exposures", index, "", "input");
1738
+ tokenAt(raw.key, "$.exposures", index, ".key");
1739
+ tokenAt(raw.sourceGroup, "$.exposures", index, ".sourceGroup");
1740
+ tokenAt(raw.origin, "$.exposures", index, ".origin");
1741
+ tokenAt(raw.measureId, "$.exposures", index, ".measureId");
1742
+ if (raw.valuation !== undefined)
1743
+ tokenAt(raw.valuation, "$.exposures", index, ".valuation");
1433
1744
  if (raw.value !== null && typeof raw.value !== "number")
1434
1745
  issue(
1435
1746
  "input",
1436
1747
  "invalid-type",
1437
- `${path}.value`,
1748
+ at("$.exposures", index, ".value"),
1438
1749
  "Exposure value must be a number or null",
1439
1750
  );
1440
1751
  if (typeof raw.complete !== "boolean")
1441
1752
  issue(
1442
1753
  "input",
1443
1754
  "invalid-type",
1444
- `${path}.complete`,
1755
+ at("$.exposures", index, ".complete"),
1445
1756
  "Exposure completeness must be boolean",
1446
1757
  );
1447
- if (raw.source !== undefined) source(raw.source, `${path}.source`);
1758
+ if (raw.source !== undefined) sourceAt(raw.source, "$.exposures", index);
1448
1759
  }
1449
1760
  if (value.filter !== undefined && !isPlainRecord(value.filter))
1450
1761
  issue(
@@ -1455,20 +1766,7 @@ function preparationBoundaryIssues(
1455
1766
  );
1456
1767
  if (isPlainRecord(value.filter)) {
1457
1768
  const filter = value.filter;
1458
- const keys = [
1459
- "sourceGroups",
1460
- "outputGroups",
1461
- "origins",
1462
- "originFrom",
1463
- "originThrough",
1464
- "valuations",
1465
- "valuationFrom",
1466
- "valuationThrough",
1467
- "minDevelopmentAge",
1468
- "maxDevelopmentAge",
1469
- "instanceIds",
1470
- ] as const;
1471
- exactKeys(filter, keys, "$.filter", "configuration");
1769
+ exactKeys(filter, FILTER_KEYS, "$.filter", "configuration");
1472
1770
  for (const key of [
1473
1771
  "sourceGroups",
1474
1772
  "outputGroups",
@@ -1506,12 +1804,7 @@ function preparationBoundaryIssues(
1506
1804
  );
1507
1805
  continue;
1508
1806
  }
1509
- exactKeys(
1510
- raw,
1511
- ["sourceGroup", "originThrough", "valuationThrough"],
1512
- path,
1513
- "configuration",
1514
- );
1807
+ exactKeys(raw, CUTOFF_KEYS, path, "configuration");
1515
1808
  token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
1516
1809
  for (const key of ["originThrough", "valuationThrough"] as const)
1517
1810
  if (raw[key] !== null)
@@ -1533,12 +1826,7 @@ function preparationBoundaryIssues(
1533
1826
  );
1534
1827
  continue;
1535
1828
  }
1536
- exactKeys(
1537
- raw,
1538
- ["sourceGroup", "origin", "valuation", "source"],
1539
- path,
1540
- "configuration",
1541
- );
1829
+ exactKeys(raw, EXPECTED_CELL_KEYS, path, "configuration");
1542
1830
  token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
1543
1831
  token(raw.origin, `${path}.origin`, "configuration");
1544
1832
  token(raw.valuation, `${path}.valuation`, "configuration");
@@ -1689,19 +1977,28 @@ function prepareDiagnosticDataInMode(
1689
1977
  const filter = normalizedFilter.value;
1690
1978
  const findings: DiagnosticMetricFinding[] = [];
1691
1979
  const pendingBlockers: PendingBlocker[] = [];
1980
+ // Audited numbers are immutable scalar records repeated across rows; one
1981
+ // owned instance per distinct value serves every snapshot of this preparation.
1982
+ const auditedNumbers = new Map<number | null, DiagnosticAuditedNumericValue>();
1983
+ const auditedNumber = (value: number | null): DiagnosticAuditedNumericValue => {
1984
+ let audited = auditedNumbers.get(value);
1985
+ if (audited === undefined) {
1986
+ audited = auditDiagnosticNumber(value);
1987
+ auditedNumbers.set(value, audited);
1988
+ }
1989
+ return audited;
1990
+ };
1991
+ // Source clones are complete once copied; freezing them here lets the owned
1992
+ // freeze walk treat every citation list attached below as finished.
1692
1993
  const lossCandidates: LossCandidate[] = input.losses.map((inputRow) => ({
1693
1994
  row: {
1694
1995
  ...inputRow,
1695
1996
  ...(inputRow.source === undefined
1696
1997
  ? {}
1697
- : { source: { ...inputRow.source } }),
1698
- measures: Object.fromEntries(
1699
- Object.keys(inputRow.measures)
1700
- .sort(codeUnit)
1701
- .map((key) => [key, inputRow.measures[key]!]),
1702
- ),
1998
+ : { source: Object.freeze({ ...inputRow.source }) }),
1999
+ measures: sortedRecordCopy(inputRow.measures, (value) => value),
1703
2000
  } as DiagnosticLossInput,
1704
- snapshot: snapshotLoss(inputRow),
2001
+ snapshot: snapshotLoss(inputRow, auditedNumber),
1705
2002
  disposition: selectedBySource(filter, inputRow.sourceGroup)
1706
2003
  ? "retained"
1707
2004
  : "filter",
@@ -1731,8 +2028,6 @@ function prepareDiagnosticDataInMode(
1731
2028
  }),
1732
2029
  );
1733
2030
  }
1734
- if (origin !== null)
1735
- candidate.snapshot = { ...candidate.snapshot, origin: origin.label };
1736
2031
  try {
1737
2032
  valuation = normalizeDiagnosticPeriod(
1738
2033
  definition,
@@ -1750,10 +2045,24 @@ function prepareDiagnosticDataInMode(
1750
2045
  }),
1751
2046
  );
1752
2047
  }
1753
- if (valuation !== null)
2048
+ // One snapshot copy carries every normalized label; overriding existing
2049
+ // keys keeps the original key order exactly as successive copies did, and
2050
+ // a snapshot whose labels were canonical already stays the same record.
2051
+ const originChanged = origin !== null && candidate.snapshot.origin !== origin.label;
2052
+ const valuationChanged =
2053
+ valuation !== null && candidate.snapshot.valuation !== valuation.label;
2054
+ if (originChanged && valuationChanged)
1754
2055
  candidate.snapshot = {
1755
2056
  ...candidate.snapshot,
1756
- valuation: valuation.label,
2057
+ origin: origin!.label,
2058
+ valuation: valuation!.label,
2059
+ };
2060
+ else if (originChanged)
2061
+ candidate.snapshot = { ...candidate.snapshot, origin: origin!.label };
2062
+ else if (valuationChanged)
2063
+ candidate.snapshot = {
2064
+ ...candidate.snapshot,
2065
+ valuation: valuation!.label,
1757
2066
  };
1758
2067
  if (origin === null || valuation === null) {
1759
2068
  candidate.disposition = "invalid";
@@ -1787,16 +2096,13 @@ function prepareDiagnosticDataInMode(
1787
2096
  developmentAge,
1788
2097
  ageUnit: definition.definition.periodAxis.ageUnit,
1789
2098
  };
1790
- candidate.row = {
1791
- ...candidate.row,
1792
- origin: origin.label,
1793
- valuation: valuation.label,
1794
- } as DiagnosticLossInput;
1795
- candidate.snapshot = {
1796
- ...candidate.snapshot,
1797
- origin: candidate.row.origin,
1798
- valuation: candidate.row.valuation,
1799
- };
2099
+ // Canonical labels are the common case; copy only when a label changes.
2100
+ if (candidate.row.origin !== origin.label || candidate.row.valuation !== valuation.label)
2101
+ candidate.row = {
2102
+ ...candidate.row,
2103
+ origin: origin.label,
2104
+ valuation: valuation.label,
2105
+ } as DiagnosticLossInput;
1800
2106
  if (
1801
2107
  beyondCutoff(
1802
2108
  candidate as LossCandidate & { coordinate: NormalizedCoordinate },
@@ -1824,7 +2130,7 @@ function prepareDiagnosticDataInMode(
1824
2130
  ? {}
1825
2131
  : { source: { ...inputRow.source } }),
1826
2132
  },
1827
- snapshot: snapshotExposure(inputRow),
2133
+ snapshot: snapshotExposure(inputRow, auditedNumber),
1828
2134
  disposition: selectedBySource(filter, inputRow.sourceGroup)
1829
2135
  ? "retained"
1830
2136
  : "filter",
@@ -2091,7 +2397,7 @@ function prepareDiagnosticDataInMode(
2091
2397
  const claimId = (candidate.row as DiagnosticClaimObservation).claimId;
2092
2398
  addToMap(
2093
2399
  bySnapshot,
2094
- canonicalJson([
2400
+ canonicalScalarArrayJson([
2095
2401
  claimId,
2096
2402
  candidate.row.sourceGroup,
2097
2403
  candidate.row.origin,
@@ -2110,7 +2416,7 @@ function prepareDiagnosticDataInMode(
2110
2416
  if (
2111
2417
  uniqueSorted(
2112
2418
  cohort.map((candidate) =>
2113
- canonicalJson([candidate.row.sourceGroup, candidate.row.origin]),
2419
+ canonicalScalarArrayJson([candidate.row.sourceGroup, candidate.row.origin]),
2114
2420
  ),
2115
2421
  ).length > 1
2116
2422
  )
@@ -2171,7 +2477,7 @@ function prepareDiagnosticDataInMode(
2171
2477
  const timing = internals.measuresById.get(
2172
2478
  exposure.measureId,
2173
2479
  )?.exposureTiming;
2174
- const identity = canonicalJson(
2480
+ const identity = canonicalScalarArrayJson(
2175
2481
  timing === "valuation-specific"
2176
2482
  ? [
2177
2483
  exposure.measureId,
@@ -2236,23 +2542,29 @@ function prepareDiagnosticDataInMode(
2236
2542
  ]),
2237
2543
  );
2238
2544
  const cellRows = new Map<string, DiagnosticLossInput[]>();
2239
- for (const candidate of retainedCandidates)
2240
- addToMap(
2241
- cellRows,
2242
- cellKey(
2545
+ // The coordinate behind each cell key, so it is never parsed back from text.
2546
+ const cellCoordinates = new Map<string, readonly [string, string, string]>();
2547
+ for (const candidate of retainedCandidates) {
2548
+ const key = cellKey(
2549
+ candidate.row.sourceGroup,
2550
+ candidate.row.origin,
2551
+ candidate.row.valuation,
2552
+ );
2553
+ if (!cellCoordinates.has(key))
2554
+ cellCoordinates.set(key, [
2243
2555
  candidate.row.sourceGroup,
2244
2556
  candidate.row.origin,
2245
2557
  candidate.row.valuation,
2246
- ),
2247
- derivedByRecord.get(candidate.row.recordId)!,
2248
- );
2558
+ ]);
2559
+ addToMap(cellRows, key, derivedByRecord.get(candidate.row.recordId)!);
2560
+ }
2249
2561
 
2250
2562
  // Exposure attachment is a coordinate join. Index the established cell order
2251
2563
  // once; scanning every cell for each origin-static exposure is quadratic.
2252
2564
  const cellKeysBySourceOrigin = new Map<string, string[]>();
2253
2565
  for (const key of cellRows.keys()) {
2254
- const [sourceGroup, origin] = parseCellKey(key);
2255
- addToMap(cellKeysBySourceOrigin, canonicalJson([sourceGroup, origin]), key);
2566
+ const [sourceGroup, origin] = cellCoordinates.get(key)!;
2567
+ addToMap(cellKeysBySourceOrigin, sourceOriginKey(sourceGroup, origin), key);
2256
2568
  }
2257
2569
 
2258
2570
  const allMeasureIds = definition.definition.measures
@@ -2280,7 +2592,7 @@ function prepareDiagnosticDataInMode(
2280
2592
  );
2281
2593
  else
2282
2594
  for (const key of cellKeysBySourceOrigin.get(
2283
- canonicalJson([candidate.row.sourceGroup, candidate.origin.label]),
2595
+ sourceOriginKey(candidate.row.sourceGroup, candidate.origin.label),
2284
2596
  ) ?? [])
2285
2597
  targets.add(key);
2286
2598
  }
@@ -2314,7 +2626,7 @@ function prepareDiagnosticDataInMode(
2314
2626
  const keys =
2315
2627
  timing === "origin-static"
2316
2628
  ? (cellKeysBySourceOrigin.get(
2317
- canonicalJson([exposure.sourceGroup, exposure.origin]),
2629
+ sourceOriginKey(exposure.sourceGroup, exposure.origin),
2318
2630
  ) ?? [])
2319
2631
  : coordinateKey !== null && cellRows.has(coordinateKey)
2320
2632
  ? [coordinateKey]
@@ -2338,23 +2650,32 @@ function prepareDiagnosticDataInMode(
2338
2650
  // and cells. Share only freshly computed owned values, never contributions,
2339
2651
  // source identities, prepared-cell envelopes, or authenticated SDK handles.
2340
2652
  // The invocation-scoped pool is bounded and released after preparation.
2341
- const componentSharing = createDiagnosticEvidenceInterner();
2653
+ const componentSharing = createDiagnosticStatsInterner();
2654
+ // Clean cells share immutable empties; readers only iterate these lists.
2655
+ const noBlockers: readonly DiagnosticStructuralBlocker[] = Object.freeze([]);
2656
+ const noFindings = Object.freeze([]) as unknown as DiagnosticMetricFinding[];
2342
2657
 
2343
2658
  for (const [key, rows] of cellRows) {
2344
- const [sourceGroup, origin, valuation] = parseCellKey(key);
2659
+ const coordinates = cellCoordinates.get(key)!;
2660
+ const sourceGroup = coordinates[0];
2661
+ const origin = coordinates[1];
2662
+ const valuation = coordinates[2];
2345
2663
  const coordinate = coordinateFor(definition, origin, valuation).coordinate!;
2346
- const contributions = Object.fromEntries(
2347
- allMeasureIds.map((measureId) => [
2348
- measureId,
2349
- [] as DiagnosticMeasureContribution[],
2350
- ]),
2351
- ) as Record<string, DiagnosticMeasureContribution[]>;
2352
- const blockers = Object.fromEntries(
2353
- allMeasureIds.map((measureId) => [
2354
- measureId,
2355
- [] as DiagnosticStructuralBlocker[],
2356
- ]),
2357
- ) as Record<string, DiagnosticStructuralBlocker[]>;
2664
+ const contributions = emptyListsByMeasure<DiagnosticMeasureContribution>(allMeasureIds);
2665
+ // Structural blockers are rare; every measure starts on the shared empty
2666
+ // list and only a measure that actually gets one allocates its own.
2667
+ const blockers = sharedEmptyListsByMeasure<DiagnosticStructuralBlocker>(
2668
+ allMeasureIds,
2669
+ noBlockers,
2670
+ );
2671
+ const addBlocker = (
2672
+ measureId: string,
2673
+ blocker: DiagnosticStructuralBlocker,
2674
+ ): void => {
2675
+ const existing = blockers[measureId]!;
2676
+ if (existing === noBlockers) setMeasureEntry(blockers, measureId, [blocker]);
2677
+ else (existing as DiagnosticStructuralBlocker[]).push(blocker);
2678
+ };
2358
2679
  const cellFindings: DiagnosticMetricFinding[] = [];
2359
2680
  const derivedContribution = (
2360
2681
  row: DiagnosticLossInput,
@@ -2379,8 +2700,14 @@ function prepareDiagnosticDataInMode(
2379
2700
  ? { ...base, status: "imputed-zero", value: 0 }
2380
2701
  : { ...base, status: "missing", value: null };
2381
2702
  };
2382
- for (const row of rows)
2383
- for (const measure of nonExposureMeasures) {
2703
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
2704
+ const row = rows[rowIndex]!;
2705
+ // One frozen citation list per row serves every measure it contributes to.
2706
+ const rowSources: readonly DiagnosticSourceLocation[] = Object.freeze(
2707
+ row.source ? [row.source] : [],
2708
+ );
2709
+ for (let at = 0; at < nonExposureMeasures.length; at++) {
2710
+ const measure = nonExposureMeasures[at]!;
2384
2711
  if (measure.source === "derived") {
2385
2712
  const state = derivedAuditByRecord.get(row.recordId)!.derived[
2386
2713
  measure.id
@@ -2408,14 +2735,15 @@ function prepareDiagnosticDataInMode(
2408
2735
  }
2409
2736
  } else
2410
2737
  contributions[measure.id]!.push(
2411
- auditedDiagnosticContribution(
2738
+ ownedDiagnosticContribution(
2412
2739
  row.recordId,
2413
2740
  own(row.measures, measure.id),
2414
2741
  measure.missing,
2415
- row.source ? [row.source] : [],
2742
+ rowSources,
2416
2743
  ),
2417
2744
  );
2418
2745
  }
2746
+ }
2419
2747
  const attachedExposures = validExposuresByCell.get(key) ?? [];
2420
2748
  for (const exposure of attachedExposures)
2421
2749
  contributions[exposure.measureId]!.push(
@@ -2436,8 +2764,7 @@ function prepareDiagnosticDataInMode(
2436
2764
  sources: normalizeSources(pending.finding.sources),
2437
2765
  finding: pending.finding,
2438
2766
  };
2439
- for (const measureId of pending.measureIds)
2440
- blockers[measureId]!.push(blocker);
2767
+ for (const measureId of pending.measureIds) addBlocker(measureId, blocker);
2441
2768
  }
2442
2769
  for (const measure of exposureMeasures) {
2443
2770
  const attempted =
@@ -2457,7 +2784,7 @@ function prepareDiagnosticDataInMode(
2457
2784
  });
2458
2785
  findings.push(finding);
2459
2786
  cellFindings.push(finding);
2460
- blockers[measure.id]!.push({
2787
+ addBlocker(measure.id, {
2461
2788
  code: finding.code,
2462
2789
  message: finding.message,
2463
2790
  sourceIds: uniqueSorted(rows.map((row) => row.recordId)),
@@ -2467,32 +2794,39 @@ function prepareDiagnosticDataInMode(
2467
2794
  }
2468
2795
  }
2469
2796
  for (const measureId of allMeasureIds) {
2470
- blockers[measureId] = [
2471
- ...new Map(
2472
- blockers[measureId]!.map((blocker) => [
2473
- canonicalJson(blocker),
2474
- blocker,
2475
- ]),
2476
- ).values(),
2477
- ].sort(compareDiagnosticBlockers);
2478
- contributions[measureId]!.sort(compareDiagnosticContributions);
2479
- }
2480
- const components = Object.fromEntries(
2481
- allMeasureIds.map((measureId) => {
2482
- const measure = internals.measuresById.get(measureId)!;
2483
- return [
2797
+ const measureBlockers = blockers[measureId]!;
2798
+ if (measureBlockers.length > 0)
2799
+ setMeasureEntry(
2800
+ blockers,
2484
2801
  measureId,
2485
- componentSharing.internOwned(
2486
- finalizeDiagnosticContributions(
2487
- contributions[measureId]!,
2488
- measure.missing,
2489
- blockers[measureId],
2490
- ),
2491
- "plain",
2492
- ),
2493
- ];
2494
- }),
2495
- ) as Record<string, DiagnosticMeasureStats>;
2802
+ [
2803
+ ...new Map(
2804
+ measureBlockers.map((blocker) => [canonicalJson(blocker), blocker]),
2805
+ ).values(),
2806
+ ].sort(compareDiagnosticBlockers),
2807
+ );
2808
+ if (contributions[measureId]!.length > 1)
2809
+ contributions[measureId]!.sort(compareDiagnosticContributions);
2810
+ }
2811
+ const components: Record<string, DiagnosticMeasureStats> = {};
2812
+ for (let at = 0; at < allMeasureIds.length; at++) {
2813
+ const measureId = allMeasureIds[at]!;
2814
+ const measure = internals.measuresById.get(measureId)!;
2815
+ const stats = finalizeDiagnosticContributionsInterned(
2816
+ contributions[measureId]!,
2817
+ measure.missing,
2818
+ blockers[measureId]!,
2819
+ componentSharing,
2820
+ );
2821
+ if (measureId === "__proto__")
2822
+ Object.defineProperty(components, measureId, {
2823
+ value: stats,
2824
+ enumerable: true,
2825
+ writable: true,
2826
+ configurable: true,
2827
+ });
2828
+ else components[measureId] = stats;
2829
+ }
2496
2830
  for (const measureId of allMeasureIds) {
2497
2831
  const stats = components[measureId]!;
2498
2832
  // The finalized counts already prove whether any per-status finding is
@@ -2558,11 +2892,14 @@ function prepareDiagnosticDataInMode(
2558
2892
  valuation,
2559
2893
  developmentAge: coordinate.developmentAge,
2560
2894
  ageUnit: coordinate.ageUnit,
2561
- lossRecordIds: uniqueSorted(rows.map((row) => row.recordId)),
2895
+ lossRecordIds:
2896
+ rows.length === 1
2897
+ ? [rows[0]!.recordId]
2898
+ : uniqueSorted(rows.map((row) => row.recordId)),
2562
2899
  contributions,
2563
2900
  components,
2564
2901
  structuralBlockers: blockers,
2565
- findings: mergeFindings(cellFindings),
2902
+ findings: cellFindings.length === 0 ? noFindings : mergeFindings(cellFindings),
2566
2903
  });
2567
2904
  }
2568
2905
 
@@ -2625,7 +2962,7 @@ function prepareDiagnosticDataInMode(
2625
2962
  ),
2626
2963
  )
2627
2964
  .map((candidate) => candidate.row);
2628
- const compareAudits = createAuditComparator();
2965
+ const auditOrder = createAuditComparator();
2629
2966
  const audit: DiagnosticInputAuditRecord[] = [
2630
2967
  ...lossCandidates.map((candidate): DiagnosticInputAuditRecord => ({
2631
2968
  kind: "loss",
@@ -2642,20 +2979,26 @@ function prepareDiagnosticDataInMode(
2642
2979
  disposition: candidate.disposition,
2643
2980
  record: candidate.snapshot,
2644
2981
  })),
2645
- ].sort(compareAudits);
2982
+ ];
2983
+ sortAuditRecords(audit, auditOrder);
2646
2984
  const normalizedFindings = mergeFindings(findings);
2647
- const frozenCells = preparedCells.map((cell) => deepFreeze(cell));
2985
+ // Freeze each owned graph exactly once. Cells dominate large preparations;
2986
+ // walking them a second time from the envelope would double the cost, and
2987
+ // their shared children (contributions, statistics, citation lists, audited
2988
+ // numbers, empty records) were attached frozen and complete, so the owned
2989
+ // walk skips them instead of re-visiting each per reference.
2990
+ const frozenCells = Object.freeze(preparedCells.map((cell) => deepFreezeOwned(cell)));
2648
2991
  const cutoffValues = normalizedCutoffs.map((cutoff) => cutoff.value);
2649
- const ownedData = deepFreeze({
2992
+ const ownedData = Object.freeze({
2650
2993
  definitionIntegrity: definition.definitionIntegrity,
2651
- filter,
2652
- completePeriodCutoffs: cutoffValues,
2653
- inputAudit: audit,
2994
+ filter: deepFreeze(filter),
2995
+ completePeriodCutoffs: deepFreeze(cutoffValues),
2996
+ inputAudit: deepFreezeOwned(audit),
2654
2997
  cells: frozenCells,
2655
- exposures: reconciled,
2998
+ exposures: deepFreeze(reconciled),
2656
2999
  expectedCellsProvided: input.expectedCells !== undefined,
2657
- expectedCells,
2658
- findings: normalizedFindings,
3000
+ expectedCells: deepFreeze(expectedCells),
3001
+ findings: deepFreeze(normalizedFindings),
2659
3002
  });
2660
3003
  if (mode === "compact") {
2661
3004
  // Every child was frozen above (the compiled definition owns its snapshot).
@@ -2664,31 +3007,35 @@ function prepareDiagnosticDataInMode(
2664
3007
  ...ownedData,
2665
3008
  }) as unknown as CompactPreparedDiagnosticData;
2666
3009
  authenticCompact.add(prepared);
3010
+ preparedSourceGroups.set(prepared, knownSourceGroups);
2667
3011
  if (definition.definition.lossRowGrain === "aggregate" && [...knownSourceGroups].every((group) => selectedBySource(filter, group)) &&
2668
3012
  normalizedFindings.length === 0 && reconciled.every((exposure) => exposure.status === "valid")) {
3013
+ const computeSelectionMetadata = (): CompactSelectionMetadata => {
2669
3014
  const excluded = new Map<object, DiagnosticInputAuditRecord>();
2670
3015
  lossCandidates.forEach((candidate, index) => {
2671
3016
  const raw = rawLossSnapshots[index]!;
2672
3017
  // Normalization changes only the coordinate labels in these snapshots.
2673
3018
  // When those were canonical already, reuse the same complete record.
3019
+ // Raw snapshots were never attached to the audit, so freeze them here.
2674
3020
  const record = raw.origin === candidate.snapshot.origin && raw.valuation === candidate.snapshot.valuation
2675
- ? candidate.snapshot : raw;
2676
- excluded.set(candidate.snapshot, deepFreeze({ kind: "loss", disposition: "filter", record }));
3021
+ ? candidate.snapshot : deepFreeze(raw);
3022
+ excluded.set(candidate.snapshot, Object.freeze({ kind: "loss", disposition: "filter", record }));
2677
3023
  });
2678
3024
  exposureCandidates.forEach((candidate, index) => {
2679
3025
  const raw = rawExposureSnapshots[index]!;
2680
3026
  const record = raw.origin === candidate.snapshot.origin && raw.valuation === candidate.snapshot.valuation
2681
- ? candidate.snapshot : raw;
2682
- excluded.set(candidate.snapshot, deepFreeze({ kind: "exposure", disposition: "filter", record }));
3027
+ ? candidate.snapshot : deepFreeze(raw);
3028
+ excluded.set(candidate.snapshot, Object.freeze({ kind: "exposure", disposition: "filter", record }));
2683
3029
  });
2684
- expectedCandidates.forEach((candidate) => excluded.set(candidate.snapshot, deepFreeze({
3030
+ expectedCandidates.forEach((candidate) => excluded.set(candidate.snapshot, Object.freeze({
2685
3031
  kind: "expected-cell", disposition: "filter", record: candidate.snapshot,
2686
3032
  })));
2687
3033
  // Rank every possible included/excluded audit variant once. Query sorts
2688
3034
  // then use only numbers; canonical record strings live only in this
2689
- // preparation's comparator and are not retained by the context.
3035
+ // preparation's comparator and are not retained by the context. Every
3036
+ // audit record was deeply frozen with the envelope above.
2690
3037
  const originExcluded = new Map<object, DiagnosticInputAuditRecord>(audit.map((item) => [item.record,
2691
- deepFreeze({ ...item, disposition: "filter" as const })]));
3038
+ Object.freeze({ ...item, disposition: "filter" as const })]));
2692
3039
  const origins = new Map<string, DiagnosticNormalizedPeriod>();
2693
3040
  for (const candidate of lossCandidates) if (candidate.coordinate)
2694
3041
  origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
@@ -2696,7 +3043,7 @@ function prepareDiagnosticDataInMode(
2696
3043
  origins.set(candidate.origin.label, candidate.origin);
2697
3044
  for (const candidate of expectedCandidates)
2698
3045
  origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
2699
- const orderedVariants = [...audit, ...excluded.values(), ...originExcluded.values()].sort(compareAudits);
3046
+ const orderedVariants = orderAuditVariants(audit, excluded, originExcluded, auditOrder);
2700
3047
  const auditRanks = new Map(orderedVariants.map((item, index) => [item, index]));
2701
3048
  const allOriginsSelected = [...origins.values()].every((origin) =>
2702
3049
  selectedByOrigin(filter, origin, normalizedFilter.originCoordinates));
@@ -2713,12 +3060,17 @@ function prepareDiagnosticDataInMode(
2713
3060
  rememberCoordinate(candidate.coordinate.valuation, candidate.coordinate.developmentAge);
2714
3061
  for (const candidate of exposureCandidates) if (candidate.valuation !== null)
2715
3062
  rememberCoordinate(candidate.valuation, candidate.developmentAge!);
2716
- sourceSelectionMetadata.set(prepared, { groups: knownSourceGroups, excluded, originExcluded,
2717
- origins, valuations, allOriginsSelected, allCoordinatesSelected, auditRanks });
3063
+ return { excluded, originExcluded, origins, valuations, allOriginsSelected,
3064
+ allCoordinatesSelected, auditRanks };
3065
+ };
3066
+ sourceSelectionMetadata.set(prepared,
3067
+ new LazySelectionMetadata(knownSourceGroups, computeSelectionMetadata));
2718
3068
  }
2719
3069
  return prepared;
2720
3070
  }
2721
- return createEagerPreparedDiagnosticData(definition, ownedData);
3071
+ const eager = createEagerPreparedDiagnosticData(definition, ownedData);
3072
+ preparedSourceGroups.set(eager, knownSourceGroups);
3073
+ return eager;
2722
3074
  }
2723
3075
 
2724
3076
  function createEagerPreparedDiagnosticData(