@actuarial-ts/core 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +44 -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
@@ -8,6 +8,8 @@ import {
8
8
  } from "./diagnosticDefinitions.js";
9
9
  import type { NormalizedDiagnosticReviewFilterIdentity } from "./diagnosticIdentity.js";
10
10
  import { canonicalJson } from "./canonical.js";
11
+ import { createDiagnosticReviewSourcePool } from "./diagnosticReviewSources.js";
12
+ import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
11
13
  import { normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
12
14
  import {
13
15
  evaluateDiagnosticMeasureExpression,
@@ -15,8 +17,15 @@ import {
15
17
  } from "./diagnosticFormulas.js";
16
18
  import {
17
19
  assertPreparedDiagnosticData,
20
+ assertCompactPreparedDiagnosticData,
21
+ type CompactPreparedDiagnosticData,
22
+ type PreparedDiagnosticDataContent,
18
23
  type PreparedDiagnosticData,
19
24
  } from "./diagnosticPreparation.js";
25
+ import {
26
+ createCompactReviewBuilder,
27
+ type CompactDiagnosticReviewEvaluations,
28
+ } from "./diagnosticReviewStore.js";
20
29
  import {
21
30
  classifyDiagnosticComparison,
22
31
  diagnosticPredicateMatches,
@@ -173,7 +182,7 @@ function expressionLeafPaths(
173
182
  }
174
183
 
175
184
  function states(
176
- prepared: PreparedDiagnosticData,
185
+ prepared: PreparedDiagnosticDataContent,
177
186
  cell: PreparedDiagnosticData["cells"][number],
178
187
  ): Record<string, FinalizedDiagnosticMeasure> {
179
188
  const internals = getCompiledDiagnosticDefinitionInternals(
@@ -206,7 +215,8 @@ function states(
206
215
  },
207
216
  stats,
208
217
  readiness,
209
- sources: cellSources(cell, [id]),
218
+ // Cell expressions attach complete leaf-source evidence separately.
219
+ // Per-measure source lists here would be discarded and rebuilt.
210
220
  },
211
221
  ];
212
222
  }),
@@ -226,7 +236,7 @@ function coordinate(
226
236
  }
227
237
 
228
238
  function coordinateFromLabels(
229
- prepared: PreparedDiagnosticData,
239
+ prepared: PreparedDiagnosticDataContent,
230
240
  sourceGroup: string,
231
241
  originLabel: string,
232
242
  valuationLabel: string,
@@ -286,27 +296,116 @@ function cellSourcesForSelection(
286
296
  );
287
297
  }
288
298
 
289
- function evaluateExpression(
290
- prepared: PreparedDiagnosticData,
291
- cell: PreparedDiagnosticData["cells"][number],
292
- expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
293
- path: string,
294
- ): EvaluatedOperand {
295
- const result = evaluateDiagnosticMeasureExpression(
296
- expression,
297
- states(prepared, cell),
298
- path,
299
- );
300
- const sources = cellSources(cell, expressionMeasureIds(expression));
299
+ /** Compact-only, invocation-owned caches. Never retain these on a prepared cell. */
300
+ function createCompactCellSourceReader() {
301
+ const pool = createDiagnosticReviewSourcePool();
302
+ const addCellSources = (
303
+ target: ReturnType<typeof pool.collection>,
304
+ cell: PreparedDiagnosticData["cells"][number],
305
+ ids: readonly string[],
306
+ ): void => {
307
+ for (const id of ids) {
308
+ for (const contribution of cell.contributions[id] ?? [])
309
+ for (const source of contribution.sources) target.add(source, true);
310
+ for (const blocker of cell.structuralBlockers[id] ?? [])
311
+ for (const source of blocker.sources) target.add(source, true);
312
+ }
313
+ };
314
+ const forSelection = (
315
+ cells: readonly PreparedDiagnosticData["cells"][number][],
316
+ measureId: string,
317
+ ): readonly DiagnosticSourceLocation[] => {
318
+ const target = pool.collection();
319
+ const ids = [measureId];
320
+ for (const cell of cells) addCellSources(target, cell, ids);
321
+ return target.finish();
322
+ };
323
+ const dependencies = new WeakMap<object, readonly string[]>();
324
+ const dependencyKeys = new WeakMap<object, string>();
325
+ const byCell = new WeakMap<
326
+ PreparedDiagnosticData["cells"][number],
327
+ Map<string, readonly DiagnosticSourceLocation[]>
328
+ >();
329
+ const measureIds = (
330
+ expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
331
+ ): readonly string[] => {
332
+ let ids = dependencies.get(expression);
333
+ if (!ids) {
334
+ ids = Object.freeze(expressionMeasureIds(expression));
335
+ dependencies.set(expression, ids);
336
+ }
337
+ return ids;
338
+ };
339
+ const forCell = (
340
+ cell: PreparedDiagnosticData["cells"][number],
341
+ ids: readonly string[],
342
+ ): readonly DiagnosticSourceLocation[] => {
343
+ let key = dependencyKeys.get(ids);
344
+ if (key === undefined) {
345
+ key = canonicalJson(ids);
346
+ dependencyKeys.set(ids, key);
347
+ }
348
+ let subsets = byCell.get(cell);
349
+ if (!subsets) {
350
+ subsets = new Map();
351
+ byCell.set(cell, subsets);
352
+ }
353
+ let sources = subsets.get(key);
354
+ if (!sources) {
355
+ const target = pool.collection();
356
+ addCellSources(target, cell, ids);
357
+ sources = target.finish();
358
+ subsets.set(key, sources);
359
+ }
360
+ return sources;
361
+ };
301
362
  return {
302
- value: result.value,
303
- reasons: result.reasons,
304
- overflows: result.overflows.map((overflow) => ({
305
- ...overflow,
306
- coordinate: coordinate(cell),
363
+ ...pool,
364
+ measureIds,
365
+ forCell,
366
+ forSelection,
367
+ };
368
+ }
369
+
370
+ function createCellExpressionEvaluator(
371
+ prepared: PreparedDiagnosticDataContent,
372
+ compactSources?: ReturnType<typeof createCompactCellSourceReader>,
373
+ ) {
374
+ // Reuse projections across this review's rules, then release them before the
375
+ // receipt identity is constructed. Retaining a prepared cell must not retain
376
+ // this temporary bookkeeping or reuse another review's state.
377
+ const statesByCell = new WeakMap<
378
+ PreparedDiagnosticData["cells"][number],
379
+ Record<string, FinalizedDiagnosticMeasure>
380
+ >();
381
+ return (
382
+ cell: PreparedDiagnosticData["cells"][number],
383
+ expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
384
+ path: string,
385
+ ): EvaluatedOperand => {
386
+ let measures = statesByCell.get(cell);
387
+ if (!measures) {
388
+ measures = states(prepared, cell);
389
+ statesByCell.set(cell, measures);
390
+ }
391
+ const result = evaluateDiagnosticMeasureExpression(
392
+ expression,
393
+ measures,
394
+ path,
395
+ );
396
+ const sources = compactSources
397
+ ? compactSources.forCell(cell, compactSources.measureIds(expression))
398
+ : cellSources(cell, expressionMeasureIds(expression));
399
+ return {
400
+ value: result.value,
401
+ reasons: result.reasons,
402
+ overflows: result.overflows.map((overflow) => ({
403
+ ...overflow,
404
+ coordinate: coordinate(cell),
405
+ sources,
406
+ })),
307
407
  sources,
308
- })),
309
- sources,
408
+ };
310
409
  };
311
410
  }
312
411
 
@@ -315,7 +414,7 @@ function constant(value: number): EvaluatedOperand {
315
414
  }
316
415
 
317
416
  function evaluation(
318
- prepared: PreparedDiagnosticData,
417
+ prepared: PreparedDiagnosticDataContent,
319
418
  rule: DiagnosticReviewRule,
320
419
  scope: DiagnosticReviewEvaluationScope,
321
420
  left: EvaluatedOperand,
@@ -458,7 +557,7 @@ function evaluation(
458
557
  }
459
558
 
460
559
  function filterCells(
461
- prepared: PreparedDiagnosticData,
560
+ prepared: PreparedDiagnosticDataContent,
462
561
  filter: DiagnosticReviewFilter | null | undefined,
463
562
  ): readonly PreparedDiagnosticData["cells"][number][] {
464
563
  if (!filter) return prepared.cells;
@@ -524,7 +623,7 @@ function filterCells(
524
623
  }
525
624
 
526
625
  function projectedCells(
527
- prepared: PreparedDiagnosticData,
626
+ prepared: PreparedDiagnosticDataContent,
528
627
  rule: Extract<DiagnosticReviewRule, { kind: "control-total" }>,
529
628
  ): readonly PreparedDiagnosticData["cells"][number][] {
530
629
  const selected = filterCells(prepared, rule.filter);
@@ -551,12 +650,116 @@ function projectedCells(
551
650
  );
552
651
  }
553
652
 
653
+ /**
654
+ * Finish each evaluation before retaining it. All mutable descendants here
655
+ * were constructed by this review; frozen descendants belong to the compiled
656
+ * definition or an earlier finalized child and must retain their identity.
657
+ * The pool is local to one review and never sees prepared cells or SDK brands.
658
+ */
659
+ function createEvaluationFinalizer(): (
660
+ value: DiagnosticReviewRuleEvaluation,
661
+ ) => DiagnosticReviewRuleEvaluation {
662
+ const sharing = createDiagnosticEvidenceInterner();
663
+ const finalizeChild = (value: unknown, sourceSlot = false): unknown => {
664
+ if (value === null || typeof value !== "object" || Object.isFrozen(value))
665
+ return value;
666
+ const array = Array.isArray(value);
667
+ for (const key of Object.keys(value)) {
668
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
669
+ const child = finalizeChild(
670
+ descriptor.value,
671
+ array ? sourceSlot : key === "source" || key === "sources",
672
+ );
673
+ if (child !== descriptor.value)
674
+ Object.defineProperty(value, key, { ...descriptor, value: child });
675
+ }
676
+ return sharing.internOwned(
677
+ Object.freeze(value),
678
+ sourceSlot ? "source" : "plain",
679
+ );
680
+ };
681
+ return (value) => {
682
+ // Keep every evaluation as its original, distinct record. Only its owned
683
+ // children may share storage; no evaluation or array entry is removed.
684
+ for (const key of ["scope", "notEvaluatedReasons", "expressionOverflows"]) {
685
+ const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
686
+ const child = finalizeChild(descriptor.value);
687
+ if (child !== descriptor.value)
688
+ Object.defineProperty(value, key, { ...descriptor, value: child });
689
+ }
690
+ return Object.freeze(value);
691
+ };
692
+ }
693
+
554
694
  /** Evaluates definition-owned semantic review rules over authenticated prepared cells. */
555
695
  export function evaluateDiagnosticReviewRules(
556
696
  prepared: PreparedDiagnosticData,
557
697
  ): readonly DiagnosticReviewRuleEvaluation[] {
558
698
  assertPreparedDiagnosticData(prepared);
559
699
  const results: DiagnosticReviewRuleEvaluation[] = [];
700
+ const finalizeEvaluation = createEvaluationFinalizer();
701
+ visitDiagnosticReviewRules(prepared, (value) => {
702
+ results.push(finalizeEvaluation(value));
703
+ });
704
+ return Object.freeze(results);
705
+ }
706
+
707
+ /** Keeps every evaluation in compact owned storage; no identity is computed. */
708
+ export function evaluateDiagnosticReviewRulesCompact(
709
+ prepared: CompactPreparedDiagnosticData,
710
+ ): CompactDiagnosticReviewEvaluations {
711
+ assertCompactPreparedDiagnosticData(prepared);
712
+ const compactSources = createCompactCellSourceReader();
713
+ const builder = createCompactReviewBuilder(
714
+ prepared.definition.definition.reviewRules,
715
+ compactSources,
716
+ );
717
+ visitDiagnosticReviewRules(prepared, builder.append, compactSources);
718
+ return builder.finish();
719
+ }
720
+
721
+ /** One traversal and one formula implementation serve both storage contracts. */
722
+ function visitDiagnosticReviewRules(
723
+ prepared: PreparedDiagnosticDataContent,
724
+ appendEvaluation: (value: DiagnosticReviewRuleEvaluation) => void,
725
+ compactSources?: ReturnType<typeof createCompactCellSourceReader>,
726
+ ): void {
727
+ const evaluateExpression = createCellExpressionEvaluator(
728
+ prepared,
729
+ compactSources,
730
+ );
731
+ const cellByCoordinate = new Map<
732
+ string,
733
+ PreparedDiagnosticData["cells"][number]
734
+ >();
735
+ const expectedSourcesByCoordinate = new Map<
736
+ string,
737
+ DiagnosticSourceLocation[]
738
+ >();
739
+ const coordinateKey = (
740
+ sourceGroup: string,
741
+ origin: string,
742
+ valuation: string,
743
+ ) => canonicalJson([sourceGroup, origin, valuation]);
744
+ // Keep the original traversal and source order; only replace repeated searches.
745
+ // Other rule families do not need these indices.
746
+ if (
747
+ prepared.definition.definition.reviewRules.some(
748
+ (rule) => rule.kind === "monotonic",
749
+ )
750
+ ) {
751
+ for (const cell of prepared.cells) {
752
+ const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
753
+ if (!cellByCoordinate.has(key)) cellByCoordinate.set(key, cell);
754
+ }
755
+ for (const cell of prepared.expectedCells) {
756
+ if (!cell.source) continue;
757
+ const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
758
+ const sources = expectedSourcesByCoordinate.get(key) ?? [];
759
+ sources.push(cell.source);
760
+ expectedSourcesByCoordinate.set(key, sources);
761
+ }
762
+ }
560
763
  for (const [ruleIndex, rule] of (
561
764
  prepared.definition.definition
562
765
  .reviewRules as unknown as readonly DiagnosticReviewRule[]
@@ -571,7 +774,9 @@ export function evaluateDiagnosticReviewRules(
571
774
  const sourceByMeasure = Object.fromEntries(
572
775
  measureIds.map((measureId) => [
573
776
  measureId,
574
- cellSourcesForSelection(cells, measureId),
777
+ compactSources
778
+ ? compactSources.forSelection(cells, measureId)
779
+ : cellSourcesForSelection(cells, measureId),
575
780
  ]),
576
781
  );
577
782
  const globalStates: Record<string, FinalizedDiagnosticMeasure> =
@@ -654,9 +859,13 @@ export function evaluateDiagnosticReviewRules(
654
859
  globalStates,
655
860
  `/reviewRules/${ruleIndex}/expression`,
656
861
  );
657
- const sources = uniqueSources(
658
- measureIds.flatMap((measureId) => sourceByMeasure[measureId] ?? []),
659
- );
862
+ const sources = compactSources
863
+ ? compactSources.union(
864
+ measureIds.map((measureId) => sourceByMeasure[measureId] ?? []),
865
+ )
866
+ : uniqueSources(
867
+ measureIds.flatMap((measureId) => sourceByMeasure[measureId] ?? []),
868
+ );
660
869
  const left: EvaluatedOperand = {
661
870
  value: evaluated.value,
662
871
  reasons: evaluated.reasons,
@@ -685,7 +894,7 @@ export function evaluateDiagnosticReviewRules(
685
894
  ),
686
895
  sources,
687
896
  };
688
- results.push(
897
+ appendEvaluation(
689
898
  evaluation(
690
899
  prepared,
691
900
  rule,
@@ -725,30 +934,18 @@ export function evaluateDiagnosticReviewRules(
725
934
  for (let index = 1; index < sorted.length; index++) {
726
935
  const previousValuation = sorted[index - 1]!;
727
936
  const currentValuation = sorted[index]!;
728
- const previous = prepared.cells.find(
729
- (cell) =>
730
- cell.sourceGroup === sourceGroup &&
731
- cell.origin === origin &&
732
- cell.valuation === previousValuation,
937
+ const previous = cellByCoordinate.get(
938
+ coordinateKey(sourceGroup, origin, previousValuation),
733
939
  );
734
- const current = prepared.cells.find(
735
- (cell) =>
736
- cell.sourceGroup === sourceGroup &&
737
- cell.origin === origin &&
738
- cell.valuation === currentValuation,
940
+ const current = cellByCoordinate.get(
941
+ coordinateKey(sourceGroup, origin, currentValuation),
739
942
  );
740
943
  const expectedSources = (valuation: string) =>
741
- prepared.expectedCells
742
- .filter(
743
- (cell) =>
744
- cell.sourceGroup === sourceGroup &&
745
- cell.origin === origin &&
746
- cell.valuation === valuation,
747
- )
748
- .flatMap((cell) => (cell.source ? [cell.source] : []));
944
+ expectedSourcesByCoordinate.get(
945
+ coordinateKey(sourceGroup, origin, valuation),
946
+ ) ?? [];
749
947
  const left = previous
750
948
  ? evaluateExpression(
751
- prepared,
752
949
  previous,
753
950
  rule.expression,
754
951
  `${basePath}/expression`,
@@ -757,11 +954,14 @@ export function evaluateDiagnosticReviewRules(
757
954
  value: null,
758
955
  reasons: ["missing" as const],
759
956
  overflows: [],
760
- sources: uniqueSources(expectedSources(previousValuation)),
957
+ sources: compactSources
958
+ ? compactSources.forPreparedSources(
959
+ expectedSources(previousValuation),
960
+ )
961
+ : uniqueSources(expectedSources(previousValuation)),
761
962
  };
762
963
  const right = current
763
964
  ? evaluateExpression(
764
- prepared,
765
965
  current,
766
966
  rule.expression,
767
967
  `${basePath}/expression`,
@@ -770,7 +970,11 @@ export function evaluateDiagnosticReviewRules(
770
970
  value: null,
771
971
  reasons: ["missing" as const],
772
972
  overflows: [],
773
- sources: uniqueSources(expectedSources(currentValuation)),
973
+ sources: compactSources
974
+ ? compactSources.forPreparedSources(
975
+ expectedSources(currentValuation),
976
+ )
977
+ : uniqueSources(expectedSources(currentValuation)),
774
978
  };
775
979
  const scope: DiagnosticReviewEvaluationScope = {
776
980
  kind: "valuation-pair",
@@ -790,9 +994,11 @@ export function evaluateDiagnosticReviewRules(
790
994
  origin,
791
995
  currentValuation,
792
996
  ),
793
- sources: uniqueSources([...left.sources, ...right.sources]),
997
+ sources: compactSources
998
+ ? compactSources.union([left.sources, right.sources])
999
+ : uniqueSources([...left.sources, ...right.sources]),
794
1000
  };
795
- results.push(
1001
+ appendEvaluation(
796
1002
  evaluation(prepared, rule, scope, left, right, (relation) =>
797
1003
  rule.direction === "nondecreasing"
798
1004
  ? relation !== "greater"
@@ -803,26 +1009,49 @@ export function evaluateDiagnosticReviewRules(
803
1009
  }
804
1010
  continue;
805
1011
  }
1012
+ // Scope evidence is the union of this rule's expression leaves, not every
1013
+ // measure in the cell. Constants have no source dependency.
1014
+ const scopeDependencies = compactSources
1015
+ ? Object.freeze(
1016
+ [
1017
+ ...new Set(
1018
+ (rule.kind === "compare"
1019
+ ? [rule.when.left, rule.when.right]
1020
+ : rule.kind === "reconcile"
1021
+ ? [rule.actual, rule.expected]
1022
+ : [rule.narrower, rule.broader]
1023
+ ).flatMap((expression) =>
1024
+ expression.op === "constant"
1025
+ ? []
1026
+ : compactSources.measureIds(expression),
1027
+ ),
1028
+ ),
1029
+ ].sort(codeUnit),
1030
+ )
1031
+ : undefined;
806
1032
  for (const cell of prepared.cells) {
807
1033
  const scopeFor = (
808
- sources: readonly DiagnosticSourceLocation[],
1034
+ leftSources: readonly DiagnosticSourceLocation[],
1035
+ rightSources: readonly DiagnosticSourceLocation[],
809
1036
  ): DiagnosticCellReviewScope => ({
810
1037
  kind: "cell",
811
1038
  cell: coordinate(cell),
812
- sources: uniqueSources(sources),
1039
+ sources: compactSources
1040
+ ? compactSources.forCell(cell, scopeDependencies!)
1041
+ : uniqueSources([...leftSources, ...rightSources]),
813
1042
  });
814
1043
  if (rule.kind === "compare") {
815
1044
  const operand = (item: typeof rule.when.left, path: string) =>
816
1045
  item.op === "constant"
817
1046
  ? constant(item.value)
818
- : evaluateExpression(prepared, cell, item, path);
1047
+ : evaluateExpression(cell, item, path);
819
1048
  const left = operand(rule.when.left, `${basePath}/when/left`);
820
1049
  const right = operand(rule.when.right, `${basePath}/when/right`);
821
- results.push(
1050
+ appendEvaluation(
822
1051
  evaluation(
823
1052
  prepared,
824
1053
  rule,
825
- scopeFor([...left.sources, ...right.sources]),
1054
+ scopeFor(left.sources, right.sources),
826
1055
  left,
827
1056
  right,
828
1057
  (relation) =>
@@ -831,7 +1060,6 @@ export function evaluateDiagnosticReviewRules(
831
1060
  );
832
1061
  } else if (rule.kind === "reconcile") {
833
1062
  const left = evaluateExpression(
834
- prepared,
835
1063
  cell,
836
1064
  rule.actual,
837
1065
  `${basePath}/actual`,
@@ -839,17 +1067,12 @@ export function evaluateDiagnosticReviewRules(
839
1067
  const right =
840
1068
  rule.expected.op === "constant"
841
1069
  ? constant(rule.expected.value)
842
- : evaluateExpression(
843
- prepared,
844
- cell,
845
- rule.expected,
846
- `${basePath}/expected`,
847
- );
848
- results.push(
1070
+ : evaluateExpression(cell, rule.expected, `${basePath}/expected`);
1071
+ appendEvaluation(
849
1072
  evaluation(
850
1073
  prepared,
851
1074
  rule,
852
- scopeFor([...left.sources, ...right.sources]),
1075
+ scopeFor(left.sources, right.sources),
853
1076
  left,
854
1077
  right,
855
1078
  (relation) => relation === "equal",
@@ -857,22 +1080,20 @@ export function evaluateDiagnosticReviewRules(
857
1080
  );
858
1081
  } else {
859
1082
  const left = evaluateExpression(
860
- prepared,
861
1083
  cell,
862
1084
  rule.narrower,
863
1085
  `${basePath}/narrower`,
864
1086
  );
865
1087
  const right = evaluateExpression(
866
- prepared,
867
1088
  cell,
868
1089
  rule.broader,
869
1090
  `${basePath}/broader`,
870
1091
  );
871
- results.push(
1092
+ appendEvaluation(
872
1093
  evaluation(
873
1094
  prepared,
874
1095
  rule,
875
- scopeFor([...left.sources, ...right.sources]),
1096
+ scopeFor(left.sources, right.sources),
876
1097
  left,
877
1098
  right,
878
1099
  (relation) => relation !== "greater",
@@ -881,17 +1102,4 @@ export function evaluateDiagnosticReviewRules(
881
1102
  }
882
1103
  }
883
1104
  }
884
- return deepFreeze(results);
885
- }
886
-
887
- function deepFreeze<T>(
888
- value: T,
889
- seen = new WeakSet<object>(),
890
- ): DiagnosticDeepReadonly<T> {
891
- if (value === null || typeof value !== "object" || seen.has(value))
892
- return value as DiagnosticDeepReadonly<T>;
893
- seen.add(value);
894
- for (const child of Object.values(value as Record<string, unknown>))
895
- deepFreeze(child, seen);
896
- return Object.freeze(value) as DiagnosticDeepReadonly<T>;
897
1105
  }
@@ -0,0 +1,85 @@
1
+ import { canonicalJson } from "./canonical.js";
2
+ import type { DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
3
+ import {
4
+ compareDiagnosticSourceLocations,
5
+ normalizeDiagnosticSourceLocations,
6
+ } from "./diagnosticSourceOrdering.js";
7
+
8
+ /**
9
+ * Private, invocation-owned evidence interner. Only the authenticated review
10
+ * evaluator registers preparation sources: its preparation is deeply frozen.
11
+ * Neither this pool nor its bookkeeping is retained on the result or a cell.
12
+ */
13
+ export function createDiagnosticReviewSourcePool() {
14
+ type SourceEntry = { key: string; value: DiagnosticSourceLocation };
15
+ const preparedSources = new WeakMap<object, SourceEntry>();
16
+ const ownedSources = new WeakMap<object, SourceEntry>();
17
+ const ownedLists = new WeakSet<readonly DiagnosticSourceLocation[]>();
18
+ const sourceEntry = (
19
+ value: DiagnosticSourceLocation,
20
+ fromPrepared: boolean,
21
+ ): SourceEntry => {
22
+ const previous =
23
+ ownedSources.get(value) ??
24
+ (fromPrepared ? preparedSources.get(value) : undefined);
25
+ if (previous) return previous;
26
+ // Preserve the authoritative full-value contract, including extra own
27
+ // data. An artifact/file/sheet/row tuple is not an exact-value substitute.
28
+ const normalized = normalizeDiagnosticSourceLocations([value])[0]!;
29
+ const key = canonicalJson(normalized);
30
+ // Fresh/unrecognized expression evidence takes the value path every time.
31
+ // Its children may be caller-owned even if the outer source was frozen.
32
+ if (!fromPrepared) return { key, value: normalized };
33
+ // Deduplicate by canonical value only inside each ordered union below.
34
+ // Across subsets, equal canonical keys can still have different original
35
+ // representatives (for example -0/+0 in extra own data). Keep the snapshot
36
+ // associated with each prepared source so the first occurrence still wins.
37
+ const entry = { key, value: Object.freeze(normalized) };
38
+ ownedSources.set(entry.value, entry);
39
+ preparedSources.set(value, entry);
40
+ return entry;
41
+ };
42
+ const collection = () => {
43
+ const entries = new Map<string, DiagnosticSourceLocation>();
44
+ let owned = true;
45
+ return {
46
+ /** `fromPrepared` is private evaluator authority, not an input option. */
47
+ add(value: DiagnosticSourceLocation, fromPrepared: boolean): void {
48
+ const entry = sourceEntry(value, fromPrepared);
49
+ owned &&= ownedSources.has(entry.value);
50
+ if (!entries.has(entry.key)) entries.set(entry.key, entry.value);
51
+ },
52
+ finish(): readonly DiagnosticSourceLocation[] {
53
+ const values = Object.freeze(
54
+ [...entries.values()].sort(compareDiagnosticSourceLocations),
55
+ );
56
+ if (owned) ownedLists.add(values);
57
+ return values;
58
+ },
59
+ };
60
+ };
61
+ const union = (
62
+ lists: readonly (readonly DiagnosticSourceLocation[])[],
63
+ ): readonly DiagnosticSourceLocation[] => {
64
+ const target = collection();
65
+ for (const values of lists)
66
+ for (const source of values) target.add(source, false);
67
+ return target.finish();
68
+ };
69
+ const forPreparedSources = (
70
+ values: readonly DiagnosticSourceLocation[],
71
+ ): readonly DiagnosticSourceLocation[] => {
72
+ const target = collection();
73
+ for (const source of values) target.add(source, true);
74
+ return target.finish();
75
+ };
76
+ return {
77
+ collection,
78
+ union,
79
+ forPreparedSources,
80
+ // Ownership means this invocation's snapshots, not Object.isFrozen on an
81
+ // arbitrary caller array/object. The builder uses these only while appending.
82
+ sourceKey: (value: DiagnosticSourceLocation) => ownedSources.get(value)?.key,
83
+ ownsList: (value: readonly DiagnosticSourceLocation[]) => ownedLists.has(value),
84
+ };
85
+ }