@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,32 +1,52 @@
1
- import { canonicalJson } from "./canonical.js";
2
1
  import type { DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
3
2
  import {
4
3
  compareDiagnosticSourceLocations,
4
+ diagnosticSourceLocationKey,
5
5
  normalizeDiagnosticSourceLocations,
6
6
  } from "./diagnosticSourceOrdering.js";
7
7
 
8
8
  /**
9
9
  * Private, invocation-owned evidence interner. Only the authenticated review
10
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.
11
+ * Neither this pool nor its bookkeeping is retained on the result or a cell;
12
+ * the pool lives exactly as long as the evaluator invocation that created it.
12
13
  */
13
14
  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[]>();
15
+ type SourceEntry = {
16
+ key: string;
17
+ value: DiagnosticSourceLocation;
18
+ /** The owned one-element list citing exactly this prepared source. */
19
+ singleton?: readonly DiagnosticSourceLocation[];
20
+ /** Owned two-element unions of this entry's singleton with another's. */
21
+ pairs?: Map<SourceEntry, readonly DiagnosticSourceLocation[]>;
22
+ };
23
+ const singletonOf = (entry: SourceEntry): readonly DiagnosticSourceLocation[] => {
24
+ if (entry.singleton === undefined) {
25
+ entry.singleton = Object.freeze([entry.value]);
26
+ ownedLists.add(entry.singleton);
27
+ }
28
+ return entry.singleton;
29
+ };
30
+ // Strong, invocation-scoped tables: the prepared inputs outlive the pool, so
31
+ // weak tables would only add per-lookup cost and collector pressure here.
32
+ const preparedSources = new Map<object, SourceEntry>();
33
+ const ownedSources = new Map<object, SourceEntry>();
34
+ const ownedLists = new Set<readonly DiagnosticSourceLocation[]>();
35
+ const emptyList: readonly DiagnosticSourceLocation[] = Object.freeze([]);
36
+ ownedLists.add(emptyList);
18
37
  const sourceEntry = (
19
38
  value: DiagnosticSourceLocation,
20
39
  fromPrepared: boolean,
21
40
  ): SourceEntry => {
22
- const previous =
23
- ownedSources.get(value) ??
24
- (fromPrepared ? preparedSources.get(value) : undefined);
41
+ // A prepared source is almost always registered already; look there first.
42
+ const previous = fromPrepared
43
+ ? (preparedSources.get(value) ?? ownedSources.get(value))
44
+ : ownedSources.get(value);
25
45
  if (previous) return previous;
26
46
  // Preserve the authoritative full-value contract, including extra own
27
47
  // data. An artifact/file/sheet/row tuple is not an exact-value substitute.
28
48
  const normalized = normalizeDiagnosticSourceLocations([value])[0]!;
29
- const key = canonicalJson(normalized);
49
+ const key = diagnosticSourceLocationKey(normalized);
30
50
  // Fresh/unrecognized expression evidence takes the value path every time.
31
51
  // Its children may be caller-owned even if the outer source was frozen.
32
52
  if (!fromPrepared) return { key, value: normalized };
@@ -34,7 +54,7 @@ export function createDiagnosticReviewSourcePool() {
34
54
  // Across subsets, equal canonical keys can still have different original
35
55
  // representatives (for example -0/+0 in extra own data). Keep the snapshot
36
56
  // associated with each prepared source so the first occurrence still wins.
37
- const entry = { key, value: Object.freeze(normalized) };
57
+ const entry: SourceEntry = { key, value: Object.freeze(normalized) };
38
58
  ownedSources.set(entry.value, entry);
39
59
  preparedSources.set(value, entry);
40
60
  return entry;
@@ -58,13 +78,19 @@ export function createDiagnosticReviewSourcePool() {
58
78
  }
59
79
  },
60
80
  finish(): readonly DiagnosticSourceLocation[] {
61
- const values = Object.freeze(
62
- entries
63
- ? [...entries.values()].sort(compareDiagnosticSourceLocations)
64
- : first ? [first.value] : [],
65
- );
66
- if (owned) ownedLists.add(values);
67
- return values;
81
+ if (entries) {
82
+ const values = Object.freeze(
83
+ [...entries.values()].sort(compareDiagnosticSourceLocations),
84
+ );
85
+ if (owned) ownedLists.add(values);
86
+ return values;
87
+ }
88
+ if (!first) return owned ? emptyList : Object.freeze([]);
89
+ // Equal owned singletons are one immutable list: every measure subset
90
+ // of a cell that cites the same prepared row reuses it, so the compact
91
+ // store also sees one list identity instead of one per subset.
92
+ if (owned) return singletonOf(first);
93
+ return Object.freeze([first.value]);
68
94
  },
69
95
  };
70
96
  };
@@ -80,6 +106,26 @@ export function createDiagnosticReviewSourcePool() {
80
106
  values.length === first.length && values.every((value, index) => value === first[index]))) {
81
107
  return first;
82
108
  }
109
+ // Valuation-pair scopes union two owned singletons for every monotonic
110
+ // rule; the union of the same two entries is one immutable owned list.
111
+ if (lists.length === 2 && first!.length === 1 && lists[1]!.length === 1 &&
112
+ ownedLists.has(first!) && ownedLists.has(lists[1]!)) {
113
+ const left = ownedSources.get(first![0]!);
114
+ const right = ownedSources.get(lists[1]![0]!);
115
+ if (left && right) {
116
+ if (left === right || left.key === right.key) return singletonOf(left);
117
+ const known = left.pairs?.get(right);
118
+ if (known) return known;
119
+ const pair = Object.freeze(
120
+ compareDiagnosticSourceLocations(left.value, right.value) <= 0
121
+ ? [left.value, right.value]
122
+ : [right.value, left.value],
123
+ );
124
+ ownedLists.add(pair);
125
+ (left.pairs ??= new Map()).set(right, pair);
126
+ return pair;
127
+ }
128
+ }
83
129
  const target = collection();
84
130
  for (const values of lists)
85
131
  for (const source of values) target.add(source, false);
@@ -96,6 +142,11 @@ export function createDiagnosticReviewSourcePool() {
96
142
  collection,
97
143
  union,
98
144
  forPreparedSources,
145
+ /** The owned empty list every empty owned collection resolves to. */
146
+ emptyList,
147
+ /** The owned singleton list for one prepared source, without a collection. */
148
+ singletonForPrepared: (value: DiagnosticSourceLocation) =>
149
+ singletonOf(sourceEntry(value, true)),
99
150
  // Ownership means this invocation's snapshots, not Object.isFrozen on an
100
151
  // arbitrary caller array/object. The builder uses these only while appending.
101
152
  sourceKey: (value: DiagnosticSourceLocation) => ownedSources.get(value)?.key,
@@ -305,9 +305,11 @@ export function createCompactReviewBuilder(
305
305
  const sourceIds = new Map<string, number>();
306
306
  // Only this evaluator invocation's snapshots may skip canonicalization.
307
307
  // Merely freezing a caller array/object does not prove deep snapshot ownership.
308
- const ownedSourceIds = new WeakMap<object, number>();
309
- const ownedListIds = new WeakMap<readonly DiagnosticSourceLocation[], number>();
308
+ // The builder is released with its invocation, so strong tables are safe.
309
+ const ownedSourceIds = new Map<object, number>();
310
+ const ownedListIds = new Map<readonly DiagnosticSourceLocation[], number>();
310
311
  const listIds = new Map<string, number>();
312
+ const singletonListIds = new Map<number, number>();
311
313
  const overflows = new Map<number, readonly EncodedOverflow[]>();
312
314
  const controls = new Map<number, Omit<Control, "sources">>();
313
315
  const ruleIds = new Map(rules.map((rule, index) => [rule.id, index]));
@@ -315,7 +317,28 @@ export function createCompactReviewBuilder(
315
317
  const counts = zeroCounts();
316
318
  let count = 0;
317
319
  let finished = false;
320
+ // Identity shortcut for coordinates the evaluator reuses across rules. A hit
321
+ // is honoured only when the object's current fields still equal the stored
322
+ // snapshot, so a mutated caller object is re-indexed exactly as before.
323
+ const coordinateIdByObject = new Map<DiagnosticReviewCoordinate, number>();
318
324
  const coordinateId = (value: DiagnosticReviewCoordinate): number => {
325
+ const cached = coordinateIdByObject.get(value);
326
+ if (cached !== undefined) {
327
+ const stored = coordinates[cached]!;
328
+ if (
329
+ stored.sourceGroup === value.sourceGroup &&
330
+ stored.origin === value.origin &&
331
+ stored.valuation === value.valuation &&
332
+ Object.is(stored.developmentAge, value.developmentAge) &&
333
+ stored.ageUnit === value.ageUnit
334
+ )
335
+ return cached;
336
+ }
337
+ const id = coordinateIdByFields(value);
338
+ coordinateIdByObject.set(value, id);
339
+ return id;
340
+ };
341
+ const coordinateIdByFields = (value: DiagnosticReviewCoordinate): number => {
319
342
  let index = coordinateIds;
320
343
  for (const key of [
321
344
  value.sourceGroup,
@@ -342,11 +365,13 @@ export function createCompactReviewBuilder(
342
365
  const sourceListId = (
343
366
  values: readonly DiagnosticSourceLocation[],
344
367
  ): number => {
368
+ // Only owned lists ever enter this table, so a hit already proves ownership
369
+ // and its capacity was checked when it was first indexed.
370
+ const knownList = ownedListIds.get(values);
371
+ if (knownList !== undefined) return knownList;
345
372
  assertCompactReviewCapacity(values.length, 0);
346
373
  const ownedList = sourceOwnership?.ownsList(values) ?? false;
347
- const knownList = ownedList ? ownedListIds.get(values) : undefined;
348
- if (knownList !== undefined) return knownList;
349
- const ids = values.map((value) => {
374
+ const sourceIdOf = (value: DiagnosticSourceLocation): number => {
350
375
  const ownedKey = sourceOwnership?.sourceKey(value);
351
376
  const owned = ownedKey !== undefined;
352
377
  const known = owned ? ownedSourceIds.get(value) : undefined;
@@ -361,7 +386,24 @@ export function createCompactReviewBuilder(
361
386
  }
362
387
  if (owned) ownedSourceIds.set(value, id);
363
388
  return id;
364
- });
389
+ };
390
+ // One-source lists are the common cell scope: index them by source id
391
+ // without building an id array or a joined text key. The text-keyed table
392
+ // stays consistent so longer lists can never collide with them.
393
+ if (values.length === 1) {
394
+ const sourceId = sourceIdOf(values[0]!);
395
+ let id = singletonListIds.get(sourceId);
396
+ if (id === undefined) {
397
+ assertCompactReviewCapacity(sourceLists.length);
398
+ id = sourceLists.length;
399
+ singletonListIds.set(sourceId, id);
400
+ listIds.set(String(sourceId), id);
401
+ sourceLists.push(Uint32Array.of(sourceId));
402
+ }
403
+ if (ownedList) ownedListIds.set(values, id);
404
+ return id;
405
+ }
406
+ const ids = values.map(sourceIdOf);
365
407
  // Do not construct a giant signature for high-fanout control-total evidence.
366
408
  const key = ids.length <= 64 ? ids.join(",") : undefined;
367
409
  let id = key === undefined ? undefined : listIds.get(key);
@@ -2,7 +2,6 @@ import type {
2
2
  DiagnosticDeepReadonly,
3
3
  DiagnosticMeasureDefinition,
4
4
  DiagnosticMetricPresentation,
5
- DiagnosticsFilter,
6
5
  JsonValue,
7
6
  } from "./diagnosticDefinitions.js";
8
7
  import { getCompiledDiagnosticDefinitionInternals } from "./diagnosticDefinitions.js";
@@ -48,6 +47,12 @@ import { compareDiagnosticFindings } from "./diagnosticOrdering.js";
48
47
  import { normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
49
48
  import { normalizeDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
50
49
  import { freezeDiagnosticGraph as deepFreeze } from "./diagnosticFreeze.js";
50
+ import {
51
+ normalizeDiagnosticSourceLocationsMemoized,
52
+ type DiagnosticSourceLocationMemo,
53
+ } from "./diagnosticSourceOrdering.js";
54
+ import type { DiagnosticMeasureContribution } from "./diagnosticAggregation.js";
55
+ import { getPreparedSourceGroups } from "./diagnosticPreparation.js";
51
56
  import {
52
57
  diagnosticJsonPreflight,
53
58
  hasDiagnosticOwn,
@@ -209,10 +214,32 @@ function expressionMeasureIds(
209
214
  ),
210
215
  ].sort(codeUnit);
211
216
  }
217
+ const NO_RUNNER_SOURCES: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] =
218
+ Object.freeze([]);
219
+
220
+ /** One run cites each prepared source once per measure; a memo normalizes it once. */
212
221
  function unionSources(
213
222
  values: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[],
214
- ): import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] {
215
- return normalizeDiagnosticSourceLocations(values);
223
+ memo?: DiagnosticSourceLocationMemo,
224
+ ): readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] {
225
+ return normalizeDiagnosticSourceLocationsMemoized(values, memo);
226
+ }
227
+
228
+ type SourceList = readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[];
229
+
230
+ /**
231
+ * Union of two lists that are already normalized owned unions of this run:
232
+ * an identical or empty operand contributes nothing new, so the other list is
233
+ * already the union and needs no re-normalization.
234
+ */
235
+ function unionOfNormalized(
236
+ left: SourceList,
237
+ right: SourceList,
238
+ memo?: DiagnosticSourceLocationMemo,
239
+ ): SourceList {
240
+ if (left === right || right.length === 0) return left;
241
+ if (left.length === 0) return right;
242
+ return unionSources([...left, ...right], memo);
216
243
  }
217
244
 
218
245
  function mergeFindings(
@@ -271,16 +298,28 @@ function validateGroupingContent(
271
298
  }
272
299
  if (boundaryIssues.length > 0)
273
300
  throw new DiagnosticValidationError(boundaryIssues);
274
- const auditedSourceGroups = input.prepared.inputAudit.flatMap((item) => {
275
- return input.prepared.filter?.sourceGroups === undefined ||
276
- input.prepared.filter.sourceGroups.includes(item.record.sourceGroup)
277
- ? [item.record.sourceGroup]
278
- : [];
279
- });
280
- const sourceGroups = new Set([
281
- ...auditedSourceGroups,
282
- ...input.prepared.cells.map((cell) => cell.sourceGroup),
283
- ]);
301
+ const sourceGroups = new Set<string>();
302
+ const selectedGroups =
303
+ input.prepared.filter?.sourceGroups === undefined
304
+ ? undefined
305
+ : new Set(input.prepared.filter.sourceGroups);
306
+ // The audit holds every input row, so its source groups are exactly the
307
+ // groups the preparation recorded; rescanning the audit is the fallback.
308
+ const known = getPreparedSourceGroups(input.prepared);
309
+ if (known !== undefined) {
310
+ for (const group of known)
311
+ if (selectedGroups === undefined || selectedGroups.has(group))
312
+ sourceGroups.add(group);
313
+ } else {
314
+ const audit = input.prepared.inputAudit;
315
+ for (let index = 0; index < audit.length; index++) {
316
+ const group = audit[index]!.record.sourceGroup;
317
+ if (selectedGroups === undefined || selectedGroups.has(group))
318
+ sourceGroups.add(group);
319
+ }
320
+ }
321
+ const cells = input.prepared.cells;
322
+ for (let index = 0; index < cells.length; index++) sourceGroups.add(cells[index]!.sourceGroup);
284
323
  for (const [source, target] of Object.entries(input.groupMap ?? {})) {
285
324
  if (!sourceGroups.has(source))
286
325
  throw new DiagnosticValidationError([
@@ -343,18 +382,25 @@ function mergeStats(
343
382
  cells: readonly PreparedDiagnosticDataContent["cells"][number][],
344
383
  measure: { readonly id: string; readonly missing: "unknown" | "zero" },
345
384
  ): DiagnosticMeasureStats {
346
- const contributions = cells.flatMap(
347
- (cell) => cell.contributions[measure.id] ?? [],
348
- );
349
- const blockers = [
350
- ...new Map(
351
- cells
352
- .flatMap((cell) => cell.structuralBlockers[measure.id] ?? [])
353
- .map((blocker) => [canonicalJson(blocker), blocker]),
354
- ).entries(),
355
- ]
356
- .sort(([left], [right]) => codeUnit(left, right))
357
- .map(([, blocker]) => blocker);
385
+ const contributions: DiagnosticMeasureContribution[] = [];
386
+ for (let at = 0; at < cells.length; at++) {
387
+ const items = cells[at]!.contributions[measure.id];
388
+ if (items !== undefined)
389
+ for (let index = 0; index < items.length; index++) contributions.push(items[index]!);
390
+ }
391
+ const blockers = cells.some(
392
+ (cell) => (cell.structuralBlockers[measure.id]?.length ?? 0) > 0,
393
+ )
394
+ ? [
395
+ ...new Map(
396
+ cells
397
+ .flatMap((cell) => cell.structuralBlockers[measure.id] ?? [])
398
+ .map((blocker) => [canonicalJson(blocker), blocker]),
399
+ ).entries(),
400
+ ]
401
+ .sort(([left], [right]) => codeUnit(left, right))
402
+ .map(([, blocker]) => blocker)
403
+ : [];
358
404
  return finalizeDiagnosticContributions(
359
405
  contributions,
360
406
  measure.missing,
@@ -418,6 +464,7 @@ function evaluatePoint(
418
464
  readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[]
419
465
  >
420
466
  >,
467
+ sourceMemo?: DiagnosticSourceLocationMemo,
421
468
  ): DiagnosticMetricEvaluation {
422
469
  const definition = prepared.definition;
423
470
  const internals = getCompiledDiagnosticDefinitionInternals(definition);
@@ -456,12 +503,16 @@ function evaluatePoint(
456
503
  }
457
504
  const expressionSources = (
458
505
  expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
459
- ) =>
460
- unionSources(
461
- expressionMeasureIds(expression).flatMap(
462
- (measureId) => sourcesByMeasure[measureId] ?? [],
463
- ),
506
+ ) => {
507
+ const measureIds = expressionMeasureIds(expression);
508
+ // A single measure's citation list is already a normalized owned union.
509
+ if (measureIds.length === 1)
510
+ return sourcesByMeasure[measureIds[0]!] ?? NO_RUNNER_SOURCES;
511
+ return unionSources(
512
+ measureIds.flatMap((measureId) => sourcesByMeasure[measureId] ?? []),
513
+ sourceMemo,
464
514
  );
515
+ };
465
516
  const withOverflowSources = <
466
517
  T extends {
467
518
  readonly value: number | null;
@@ -498,10 +549,11 @@ function evaluatePoint(
498
549
  (
499
550
  internals.calculationDependenciesByInstanceId.get(instance.id) ?? []
500
551
  ).flatMap((measureId) => sourcesByMeasure[measureId] ?? []),
552
+ sourceMemo,
501
553
  );
502
554
  const roleExpressionSources = (
503
555
  expression: import("./diagnosticExpressions.js").DiagnosticRoleExpression,
504
- ): import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] => {
556
+ ): readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] => {
505
557
  if (expression.op === "role")
506
558
  return expressionSources(instance.bindings[expression.role]!);
507
559
  return unionSources(
@@ -509,6 +561,7 @@ function evaluatePoint(
509
561
  ? expression.terms
510
562
  : [expression.left, expression.right]
511
563
  ).flatMap(roleExpressionSources),
564
+ sourceMemo,
512
565
  );
513
566
  };
514
567
  const numeratorSources = roleExpressionSources(formula.numerator);
@@ -652,7 +705,7 @@ function evaluatePoint(
652
705
  );
653
706
  const left = operand(rule.when.left, pointer(rulePath, "left"));
654
707
  const right = operand(rule.when.right, pointer(rulePath, "right"));
655
- const ruleSources = unionSources([...left.sources, ...right.sources]);
708
+ const ruleSources = unionOfNormalized(left.sources, right.sources, sourceMemo);
656
709
  const expressionOverflows = [
657
710
  ...new Map(
658
711
  [...left.overflows, ...right.overflows].map((overflow) => [
@@ -892,7 +945,10 @@ function calculateMetricDiagnostics(
892
945
  snapshotOwnedDimensions(value, dimensionSnapshots),
893
946
  ]),
894
947
  );
895
- const buckets = new Map<string, typeof prepared.cells>();
948
+ const buckets = new Map<
949
+ string,
950
+ { group: string; origin: string; valuation: string; cells: (typeof prepared.cells)[number][] }
951
+ >();
896
952
  for (const cell of prepared.cells) {
897
953
  const group =
898
954
  input.groupMap !== undefined &&
@@ -904,41 +960,63 @@ function calculateMetricDiagnostics(
904
960
  !prepared.filter.outputGroups.includes(group)
905
961
  )
906
962
  continue;
907
- const key = canonicalJson([group, cell.origin, cell.valuation]);
908
- buckets.set(key, [...(buckets.get(key) ?? []), cell]);
963
+ // Grouping key only; emergence order is sorted afterwards. Output groups
964
+ // and period labels are validated tokens, so U+0000 cannot occur in them.
965
+ const key = `${group}${cell.origin}${cell.valuation}`;
966
+ const bucket = buckets.get(key);
967
+ if (bucket) bucket.cells.push(cell);
968
+ else buckets.set(key, { group, origin: cell.origin, valuation: cell.valuation, cells: [cell] });
909
969
  }
970
+ // Prepared source objects are immutable for this run; snapshot each once.
971
+ const sourceMemo: DiagnosticSourceLocationMemo = new Map();
910
972
  const selectedInstances = prepared.definition.definition.instances.filter(
911
973
  (instance) =>
912
974
  prepared.filter?.instanceIds === undefined ||
913
975
  prepared.filter.instanceIds.includes(instance.id),
914
976
  );
915
977
  const emergence: DiagnosticEmergencePoint[] = [];
916
- for (const [key, cells] of buckets) {
917
- const [group, origin, valuation] = JSON.parse(key) as [
918
- string,
919
- string,
920
- string,
921
- ];
978
+ for (const { group, origin, valuation, cells } of buckets.values()) {
922
979
  const components = Object.fromEntries(
923
980
  prepared.definition.definition.measures.map((measure) => [
924
981
  measure.id,
925
982
  mergeStats(cells, measure),
926
983
  ]),
927
984
  );
985
+ // Loss measures of one bucket cite exactly the same rows: when a measure's
986
+ // citation sequence is the previous measure's, by identity, its union is too.
987
+ let previousCited: import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] | undefined;
988
+ let previousUnion: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] | undefined;
928
989
  const sourcesByMeasure = Object.fromEntries(
929
- prepared.definition.definition.measures.map((measure) => [
930
- measure.id,
931
- unionSources(
932
- cells.flatMap((cell) => [
933
- ...(cell.contributions[measure.id] ?? []).flatMap(
934
- (item) => item.sources,
935
- ),
936
- ...(cell.structuralBlockers[measure.id] ?? []).flatMap(
937
- (item) => item.sources,
938
- ),
939
- ]),
940
- ),
941
- ]),
990
+ prepared.definition.definition.measures.map((measure) => {
991
+ const cited: import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] =
992
+ [];
993
+ for (let at = 0; at < cells.length; at++) {
994
+ const cell = cells[at]!;
995
+ const contributions = cell.contributions[measure.id];
996
+ if (contributions !== undefined)
997
+ for (let index = 0; index < contributions.length; index++) {
998
+ const sources = contributions[index]!.sources;
999
+ for (let position = 0; position < sources.length; position++)
1000
+ cited.push(sources[position]!);
1001
+ }
1002
+ const blockers = cell.structuralBlockers[measure.id];
1003
+ if (blockers !== undefined)
1004
+ for (let index = 0; index < blockers.length; index++) {
1005
+ const sources = blockers[index]!.sources;
1006
+ for (let position = 0; position < sources.length; position++)
1007
+ cited.push(sources[position]!);
1008
+ }
1009
+ }
1010
+ const union: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[] =
1011
+ previousUnion !== undefined &&
1012
+ previousCited!.length === cited.length &&
1013
+ cited.every((source, index) => source === previousCited![index])
1014
+ ? previousUnion
1015
+ : unionSources(cited, sourceMemo);
1016
+ previousCited = cited;
1017
+ previousUnion = union;
1018
+ return [measure.id, union];
1019
+ }),
942
1020
  );
943
1021
  const context = {
944
1022
  group,
@@ -1017,6 +1095,7 @@ function calculateMetricDiagnostics(
1017
1095
  mappedFindings,
1018
1096
  blockerFindingsByMeasure,
1019
1097
  sourcesByMeasure,
1098
+ sourceMemo,
1020
1099
  ),
1021
1100
  ]),
1022
1101
  );
@@ -213,7 +213,17 @@ export function diagnosticJsonPreflight(
213
213
  }
214
214
  continue;
215
215
  }
216
- for (const key of Object.keys(item).sort().reverse()) {
216
+ // Children are pushed in descending key order so they pop ascending; sort
217
+ // only when insertion order is not already sorted, and walk backwards
218
+ // rather than reversing a copy.
219
+ const keys = Object.keys(item);
220
+ for (let index = 1; index < keys.length; index++)
221
+ if (keys[index - 1]! > keys[index]!) {
222
+ keys.sort();
223
+ break;
224
+ }
225
+ for (let position = keys.length - 1; position >= 0; position--) {
226
+ const key = keys[position]!;
217
227
  const path = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
218
228
  ? `${frame.path}.${key}`
219
229
  : `${frame.path}[${JSON.stringify(key)}]`;