@actuarial-ts/core 0.7.0 → 0.8.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 (115) hide show
  1. package/README.md +46 -2
  2. package/dist/analysisRecipe.d.ts +55 -0
  3. package/dist/analysisRecipe.d.ts.map +1 -0
  4. package/dist/analysisRecipe.js +627 -0
  5. package/dist/analysisRecipe.js.map +1 -0
  6. package/dist/classifications.d.ts +30 -0
  7. package/dist/classifications.d.ts.map +1 -0
  8. package/dist/classifications.js +207 -0
  9. package/dist/classifications.js.map +1 -0
  10. package/dist/customizationContracts.d.ts +749 -0
  11. package/dist/customizationContracts.d.ts.map +1 -0
  12. package/dist/customizationContracts.js +19 -0
  13. package/dist/customizationContracts.js.map +1 -0
  14. package/dist/descriptiveStatistics.d.ts +8 -0
  15. package/dist/descriptiveStatistics.d.ts.map +1 -0
  16. package/dist/descriptiveStatistics.js +283 -0
  17. package/dist/descriptiveStatistics.js.map +1 -0
  18. package/dist/diagnosticAggregation.d.ts.map +1 -1
  19. package/dist/diagnosticAggregation.js +16 -0
  20. package/dist/diagnosticAggregation.js.map +1 -1
  21. package/dist/diagnosticFreeze.d.ts +4 -0
  22. package/dist/diagnosticFreeze.d.ts.map +1 -0
  23. package/dist/diagnosticFreeze.js +37 -0
  24. package/dist/diagnosticFreeze.js.map +1 -0
  25. package/dist/diagnosticIdentityStream.d.ts +4 -0
  26. package/dist/diagnosticIdentityStream.d.ts.map +1 -1
  27. package/dist/diagnosticIdentityStream.js +32 -2
  28. package/dist/diagnosticIdentityStream.js.map +1 -1
  29. package/dist/diagnosticPreparation.d.ts +21 -0
  30. package/dist/diagnosticPreparation.d.ts.map +1 -1
  31. package/dist/diagnosticPreparation.js +231 -19
  32. package/dist/diagnosticPreparation.js.map +1 -1
  33. package/dist/diagnosticReview.d.ts.map +1 -1
  34. package/dist/diagnosticReview.js +43 -25
  35. package/dist/diagnosticReview.js.map +1 -1
  36. package/dist/diagnosticReviewSources.d.ts.map +1 -1
  37. package/dist/diagnosticReviewSources.js +25 -4
  38. package/dist/diagnosticReviewSources.js.map +1 -1
  39. package/dist/diagnosticReviewStore.d.ts +2 -0
  40. package/dist/diagnosticReviewStore.d.ts.map +1 -1
  41. package/dist/diagnosticReviewStore.js +110 -36
  42. package/dist/diagnosticReviewStore.js.map +1 -1
  43. package/dist/diagnosticRunner.d.ts.map +1 -1
  44. package/dist/diagnosticRunner.js +1 -8
  45. package/dist/diagnosticRunner.js.map +1 -1
  46. package/dist/exposureEarning.d.ts +12 -0
  47. package/dist/exposureEarning.d.ts.map +1 -0
  48. package/dist/exposureEarning.js +276 -0
  49. package/dist/exposureEarning.js.map +1 -0
  50. package/dist/exposureMovements.d.ts +8 -0
  51. package/dist/exposureMovements.d.ts.map +1 -0
  52. package/dist/exposureMovements.js +70 -0
  53. package/dist/exposureMovements.js.map +1 -0
  54. package/dist/exposureRevisions.d.ts +13 -0
  55. package/dist/exposureRevisions.d.ts.map +1 -0
  56. package/dist/exposureRevisions.js +161 -0
  57. package/dist/exposureRevisions.js.map +1 -0
  58. package/dist/financialPipeline.d.ts +14 -0
  59. package/dist/financialPipeline.d.ts.map +1 -0
  60. package/dist/financialPipeline.js +75 -0
  61. package/dist/financialPipeline.js.map +1 -0
  62. package/dist/financialTerms.d.ts +20 -0
  63. package/dist/financialTerms.d.ts.map +1 -0
  64. package/dist/financialTerms.js +205 -0
  65. package/dist/financialTerms.js.map +1 -0
  66. package/dist/fnvAccumulator.d.ts.map +1 -1
  67. package/dist/fnvAccumulator.js +11 -5
  68. package/dist/fnvAccumulator.js.map +1 -1
  69. package/dist/historyPreparation.d.ts +8 -0
  70. package/dist/historyPreparation.d.ts.map +1 -0
  71. package/dist/historyPreparation.js +557 -0
  72. package/dist/historyPreparation.js.map +1 -0
  73. package/dist/index.d.ts +15 -2
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +15 -2
  76. package/dist/index.js.map +1 -1
  77. package/dist/policyPeriods.d.ts +29 -0
  78. package/dist/policyPeriods.d.ts.map +1 -0
  79. package/dist/policyPeriods.js +322 -0
  80. package/dist/policyPeriods.js.map +1 -0
  81. package/dist/populations.d.ts +11 -0
  82. package/dist/populations.d.ts.map +1 -0
  83. package/dist/populations.js +264 -0
  84. package/dist/populations.js.map +1 -0
  85. package/dist/reservingAdapter.d.ts +16 -0
  86. package/dist/reservingAdapter.d.ts.map +1 -0
  87. package/dist/reservingAdapter.js +74 -0
  88. package/dist/reservingAdapter.js.map +1 -0
  89. package/dist/version.d.ts +1 -1
  90. package/dist/version.js +1 -1
  91. package/package.json +2 -2
  92. package/src/analysisRecipe.ts +696 -0
  93. package/src/classifications.ts +231 -0
  94. package/src/customizationContracts.ts +763 -0
  95. package/src/descriptiveStatistics.ts +320 -0
  96. package/src/diagnosticAggregation.ts +16 -0
  97. package/src/diagnosticFreeze.ts +37 -0
  98. package/src/diagnosticIdentityStream.ts +37 -2
  99. package/src/diagnosticPreparation.ts +259 -26
  100. package/src/diagnosticReview.ts +49 -27
  101. package/src/diagnosticReviewSources.ts +22 -3
  102. package/src/diagnosticReviewStore.ts +139 -54
  103. package/src/diagnosticRunner.ts +1 -11
  104. package/src/exposureEarning.ts +314 -0
  105. package/src/exposureMovements.ts +78 -0
  106. package/src/exposureRevisions.ts +186 -0
  107. package/src/financialPipeline.ts +93 -0
  108. package/src/financialTerms.ts +263 -0
  109. package/src/fnvAccumulator.ts +11 -5
  110. package/src/historyPreparation.ts +645 -0
  111. package/src/index.ts +28 -1
  112. package/src/policyPeriods.ts +375 -0
  113. package/src/populations.ts +307 -0
  114. package/src/reservingAdapter.ts +98 -0
  115. package/src/version.ts +1 -1
@@ -1,4 +1,6 @@
1
1
  import { canonicalJson, fnv1a64 } from "./canonical.js";
2
+ import { freezeDiagnosticGraph as deepFreeze } from "./diagnosticFreeze.js";
3
+ import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
2
4
  import {
3
5
  createDiagnosticIdentityArray,
4
6
  createDiagnosticIdentityObject,
@@ -204,6 +206,163 @@ export interface CompactPreparedDiagnosticData extends PreparedDiagnosticDataCon
204
206
  readonly [compactPreparedDiagnosticDataBrand]: true;
205
207
  }
206
208
 
209
+ /**
210
+ * Reuse immutable preparation when only metric selection changes. A different
211
+ * source scope returns undefined so callers can perform full preparation.
212
+ * Invalid filters and forged preparations still fail at the public boundary.
213
+ * Review and metric execution gates must still run for the returned handle.
214
+ */
215
+ export function reselectCompactDiagnosticMetrics(
216
+ prepared: CompactPreparedDiagnosticData,
217
+ filter?: DiagnosticsFilter,
218
+ ): CompactPreparedDiagnosticData | undefined {
219
+ assertCompactPreparedDiagnosticData(prepared);
220
+ const boundaryIssues = preparationBoundaryIssues({
221
+ definition: prepared.definition, losses: [], exposures: [],
222
+ ...(filter === undefined ? {} : { filter }),
223
+ });
224
+ if (boundaryIssues.length) throw new DiagnosticValidationError(boundaryIssues);
225
+ const issues: DiagnosticValidationIssue[] = [];
226
+ const knownGroups = new Set(prepared.inputAudit.map((item) => item.record.sourceGroup));
227
+ const normalized = normalizeFilter(prepared.definition, filter, knownGroups, issues).value;
228
+ if (issues.length) throw new DiagnosticValidationError(issues);
229
+ const sourceScope = (value: DiagnosticsFilter | null) => {
230
+ const { instanceIds: _instances, ...scope } = value ?? {};
231
+ return canonicalJson(scope);
232
+ };
233
+ if (sourceScope(normalized) !== sourceScope(prepared.filter)) return undefined;
234
+ const selected = Object.freeze({ ...prepared, filter: deepFreeze(normalized) }) as CompactPreparedDiagnosticData;
235
+ authenticCompact.add(selected);
236
+ const parent = sourceSelectionParents.get(prepared);
237
+ const coordinateParent = coordinateSelectionParents.get(prepared);
238
+ if (coordinateParent) coordinateSelectionParents.set(selected, coordinateParent);
239
+ if (parent || sourceSelectionMetadata.has(prepared))
240
+ sourceSelectionParents.set(selected, parent ?? prepared);
241
+ return selected;
242
+ }
243
+
244
+ /**
245
+ * Project source-group subsets of a clean aggregate preparation. Other scope
246
+ * changes or preparations requiring structural reconciliation return undefined
247
+ * for full preparation. Excluded input audits retain their original raw periods.
248
+ */
249
+ export function selectCompactDiagnosticSourceGroups(
250
+ prepared: CompactPreparedDiagnosticData,
251
+ filter?: DiagnosticsFilter,
252
+ ): CompactPreparedDiagnosticData | undefined {
253
+ return selectCompactSourceScope(prepared, filter, false);
254
+ }
255
+
256
+ /** Select whole origins and source groups from a clean preparation with all origins. */
257
+ export function selectCompactDiagnosticOrigins(
258
+ prepared: CompactPreparedDiagnosticData,
259
+ filter?: DiagnosticsFilter,
260
+ ): CompactPreparedDiagnosticData | undefined {
261
+ return selectCompactSourceScope(prepared, filter, true);
262
+ }
263
+
264
+ /** Select date/age coordinates from a clean aggregate preparation covering the full input scope. */
265
+ export function selectCompactDiagnosticCoordinates(
266
+ prepared: CompactPreparedDiagnosticData,
267
+ filter?: DiagnosticsFilter,
268
+ ): CompactPreparedDiagnosticData | undefined {
269
+ return selectCompactSourceScope(prepared, filter, true, true);
270
+ }
271
+
272
+ function selectCompactSourceScope(
273
+ prepared: CompactPreparedDiagnosticData,
274
+ filter: DiagnosticsFilter | undefined,
275
+ allowOrigins: boolean,
276
+ allowCoordinates = false,
277
+ ): CompactPreparedDiagnosticData | undefined {
278
+ assertCompactPreparedDiagnosticData(prepared);
279
+ const metadata = sourceSelectionMetadata.get(prepared);
280
+ const boundaryIssues = preparationBoundaryIssues({
281
+ definition: prepared.definition, losses: [], exposures: [],
282
+ ...(filter === undefined ? {} : { filter }),
283
+ });
284
+ if (boundaryIssues.length) throw new DiagnosticValidationError(boundaryIssues);
285
+ const issues: DiagnosticValidationIssue[] = [];
286
+ const normalizedFilter = normalizeFilter(prepared.definition, filter,
287
+ metadata?.groups ?? new Set(prepared.inputAudit.map((item) => item.record.sourceGroup)), issues);
288
+ const normalized = normalizedFilter.value;
289
+ if (issues.length) throw new DiagnosticValidationError(issues);
290
+ if (!metadata) return undefined;
291
+ if (allowOrigins && !metadata.allOriginsSelected) return undefined;
292
+ if (allowCoordinates && !metadata.allCoordinatesSelected) return undefined;
293
+ const remainingScope = (value: DiagnosticsFilter | null) => {
294
+ const { instanceIds: _instances, sourceGroups: _groups, ...scope } = value ?? {};
295
+ if (!allowOrigins) return canonicalJson(scope);
296
+ const { origins: _origins, originFrom: _from, originThrough: _through, ...rest } = scope;
297
+ if (!allowCoordinates) return canonicalJson(rest);
298
+ const { valuations: _valuations, valuationFrom: _vFrom, valuationThrough: _vThrough,
299
+ minDevelopmentAge: _min, maxDevelopmentAge: _max, ...other } = rest;
300
+ return canonicalJson(other);
301
+ };
302
+ // Source exclusion precedes every date/age/cutoff decision. With no selected
303
+ // sources, all audits have the same source-excluded snapshots regardless of
304
+ // those selectors; boundary validation above must still run.
305
+ if (normalized?.sourceGroups?.length !== 0 &&
306
+ remainingScope(normalized) !== remainingScope(prepared.filter)) return undefined;
307
+ const groups = normalized?.sourceGroups === undefined ? undefined : new Set(normalized.sourceGroups);
308
+ const includes = (group: string) => !groups || groups.has(group);
309
+ const includedOrigins = allowOrigins ? new Set([...metadata.origins].filter(([, origin]) =>
310
+ selectedByOrigin(normalized, origin, normalizedFilter.originCoordinates)).map(([label]) => label)) : undefined;
311
+ const includesOrigin = (origin: string) => !includedOrigins || includedOrigins.has(origin);
312
+ const includesDate = (origin: string, valuation: string | null | undefined) => {
313
+ if (!allowCoordinates || valuation === undefined || valuation === null) return true;
314
+ const at = metadata.valuations.get(valuation)!;
315
+ const age = at.coordinate - metadata.origins.get(origin)!.coordinate + prepared.definition.definition.periodAxis.ageOffset;
316
+ return selectedByValuation(normalized, at, normalizedFilter.valuationCoordinates) && selectedByAge(normalized, age);
317
+ };
318
+ const audit = prepared.inputAudit.map((item) => !includes(item.record.sourceGroup)
319
+ ? metadata.excluded.get(item.record)!
320
+ : item.disposition === "retained" && (!includesOrigin(item.record.origin) || !includesDate(item.record.origin, item.record.valuation))
321
+ ? metadata.originExcluded.get(item.record)! : item);
322
+ audit.sort((left, right) => metadata.auditRanks.get(left)! - metadata.auditRanks.get(right)!);
323
+ const cells = Object.freeze(prepared.cells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation)));
324
+ const exposures = Object.freeze(prepared.exposures.filter((exposure) => exposure.status === "valid" && includes(exposure.sourceGroup) && includesOrigin(exposure.origin) && includesDate(exposure.origin, exposure.valuation)));
325
+ 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;
327
+ const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
328
+ const orphanFindings = allowCoordinates ? exposures.flatMap((exposure) => {
329
+ if (exposure.status !== "valid") return [];
330
+ const matches = internals.measuresById.get(exposure.measureId)!.exposureTiming === "origin-static"
331
+ ? sourceOrigins!.has(canonicalJson([exposure.sourceGroup, exposure.origin]))
332
+ : exposure.valuation !== undefined && coordinateKeys!.has(cellKey(exposure.sourceGroup, exposure.origin, exposure.valuation));
333
+ return matches ? [] : [exposureWithoutLossFinding(exposure)];
334
+ }) : [];
335
+ const selected = Object.freeze({
336
+ ...prepared,
337
+ filter: deepFreeze(normalized),
338
+ inputAudit: Object.freeze(audit),
339
+ cells,
340
+ exposures,
341
+ findings: orphanFindings.length ? deepFreeze(mergeFindings([...prepared.findings, ...orphanFindings])) : prepared.findings,
342
+ expectedCells: Object.freeze(prepared.expectedCells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation))),
343
+ }) as CompactPreparedDiagnosticData;
344
+ authenticCompact.add(selected);
345
+ if (allowCoordinates) coordinateSelectionParents.set(selected, prepared);
346
+ else sourceSelectionParents.set(selected, prepared);
347
+ return selected;
348
+ }
349
+
350
+ /** Private ownership proof: only whole-source projection can register a parent. */
351
+ export function getCompactSourceSelectionParent(
352
+ prepared: CompactPreparedDiagnosticData,
353
+ ): CompactPreparedDiagnosticData | undefined {
354
+ assertCompactPreparedDiagnosticData(prepared);
355
+ return sourceSelectionParents.get(prepared);
356
+ }
357
+
358
+ /** Private proof of cell reuse; does not imply unchanged development-history checks. */
359
+ export function getCompactCoordinateSelectionParent(
360
+ prepared: CompactPreparedDiagnosticData,
361
+ ): CompactPreparedDiagnosticData | undefined {
362
+ assertCompactPreparedDiagnosticData(prepared);
363
+ return coordinateSelectionParents.get(prepared);
364
+ }
365
+
207
366
  export interface NormalizedDiagnosticPreparationIdentity {
208
367
  readonly definitionIntegrity: string;
209
368
  readonly filter: NormalizedDiagnosticsFilterIdentity | null;
@@ -272,6 +431,18 @@ interface PendingBlocker {
272
431
 
273
432
  const authentic = new WeakSet<object>();
274
433
  const authenticCompact = new WeakSet<object>();
434
+ const sourceSelectionParents = new WeakMap<CompactPreparedDiagnosticData, CompactPreparedDiagnosticData>();
435
+ 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
+ }>();
275
446
  const compactIdentityDocuments = new WeakMap<
276
447
  object,
277
448
  DiagnosticIdentityDocument
@@ -360,18 +531,6 @@ function own<T>(
360
531
  : undefined;
361
532
  }
362
533
 
363
- function deepFreeze<T>(
364
- value: T,
365
- seen = new WeakSet<object>(),
366
- ): DiagnosticDeepReadonly<T> {
367
- if (value === null || typeof value !== "object" || seen.has(value))
368
- return value as DiagnosticDeepReadonly<T>;
369
- seen.add(value);
370
- for (const child of Object.values(value as Record<string, unknown>))
371
- deepFreeze(child, seen);
372
- return Object.freeze(value) as DiagnosticDeepReadonly<T>;
373
- }
374
-
375
534
  function uniqueSorted(values: readonly string[]): string[] {
376
535
  return [...new Set(values)].sort(codeUnit);
377
536
  }
@@ -382,6 +541,17 @@ function normalizeSources(
382
541
  return normalizeDiagnosticSourceLocations(values);
383
542
  }
384
543
 
544
+ function exposureWithoutLossFinding(exposure: Extract<ReconciledDiagnosticExposure, { status: "valid" }>): DiagnosticMetricFinding {
545
+ return structuralFinding("exposure-without-loss", {
546
+ measureId: exposure.measureId,
547
+ exposureKey: exposure.key,
548
+ sourceGroup: exposure.sourceGroup,
549
+ origin: exposure.origin,
550
+ ...(exposure.valuation ? { valuation: exposure.valuation } : {}),
551
+ sources: exposure.sources,
552
+ });
553
+ }
554
+
385
555
  function structuralFinding(
386
556
  code: StructuralCode,
387
557
  context: Omit<
@@ -1041,7 +1211,7 @@ function createAuditComparator(): (
1041
1211
  };
1042
1212
  return (left, right) =>
1043
1213
  rank[left.kind] - rank[right.kind] ||
1044
- codeUnit(key(left.record), key(right.record)) ||
1214
+ (left.record === right.record ? 0 : codeUnit(key(left.record), key(right.record))) ||
1045
1215
  disposition[left.disposition] - disposition[right.disposition];
1046
1216
  }
1047
1217
 
@@ -1538,6 +1708,7 @@ function prepareDiagnosticDataInMode(
1538
1708
  coordinate: null,
1539
1709
  invalid: false,
1540
1710
  }));
1711
+ const rawLossSnapshots = lossCandidates.map((candidate) => candidate.snapshot);
1541
1712
 
1542
1713
  for (const candidate of lossCandidates) {
1543
1714
  if (candidate.disposition !== "retained") continue;
@@ -1666,6 +1837,7 @@ function prepareDiagnosticDataInMode(
1666
1837
  invalid: false,
1667
1838
  }),
1668
1839
  );
1840
+ const rawExposureSnapshots = exposureCandidates.map((candidate) => candidate.snapshot);
1669
1841
  for (const candidate of exposureCandidates) {
1670
1842
  if (candidate.disposition !== "retained") continue;
1671
1843
  try {
@@ -2150,14 +2322,7 @@ function prepareDiagnosticDataInMode(
2150
2322
  for (const key of keys) addToMap(validExposuresByCell, key, exposure);
2151
2323
  if (keys.length === 0)
2152
2324
  findings.push(
2153
- structuralFinding("exposure-without-loss", {
2154
- measureId: exposure.measureId,
2155
- exposureKey: exposure.key,
2156
- sourceGroup: exposure.sourceGroup,
2157
- origin: exposure.origin,
2158
- ...(exposure.valuation ? { valuation: exposure.valuation } : {}),
2159
- sources: exposure.sources,
2160
- }),
2325
+ exposureWithoutLossFinding(exposure),
2161
2326
  );
2162
2327
  }
2163
2328
 
@@ -2169,6 +2334,12 @@ function prepareDiagnosticDataInMode(
2169
2334
  for (const key of new Set(pending.cellKeys))
2170
2335
  addToMap(pendingBlockersByCell, key, pending);
2171
2336
 
2337
+ // Statistics are immutable scalar records, often identical across measures
2338
+ // and cells. Share only freshly computed owned values, never contributions,
2339
+ // source identities, prepared-cell envelopes, or authenticated SDK handles.
2340
+ // The invocation-scoped pool is bounded and released after preparation.
2341
+ const componentSharing = createDiagnosticEvidenceInterner();
2342
+
2172
2343
  for (const [key, rows] of cellRows) {
2173
2344
  const [sourceGroup, origin, valuation] = parseCellKey(key);
2174
2345
  const coordinate = coordinateFor(definition, origin, valuation).coordinate!;
@@ -2311,15 +2482,24 @@ function prepareDiagnosticDataInMode(
2311
2482
  const measure = internals.measuresById.get(measureId)!;
2312
2483
  return [
2313
2484
  measureId,
2314
- finalizeDiagnosticContributions(
2315
- contributions[measureId]!,
2316
- measure.missing,
2317
- blockers[measureId],
2485
+ componentSharing.internOwned(
2486
+ finalizeDiagnosticContributions(
2487
+ contributions[measureId]!,
2488
+ measure.missing,
2489
+ blockers[measureId],
2490
+ ),
2491
+ "plain",
2318
2492
  ),
2319
2493
  ];
2320
2494
  }),
2321
2495
  ) as Record<string, DiagnosticMeasureStats>;
2322
2496
  for (const measureId of allMeasureIds) {
2497
+ const stats = components[measureId]!;
2498
+ // The finalized counts already prove whether any per-status finding is
2499
+ // possible. Avoid allocating three empty partitions and a finding context
2500
+ // for every clean observed measure in large aggregate datasets.
2501
+ if (stats.missing === 0 && stats.nonFinite === 0 &&
2502
+ !(stats.sum === null && stats.structural === 0)) continue;
2323
2503
  const measureContributions = contributions[measureId]!;
2324
2504
  const context = {
2325
2505
  measureId,
@@ -2445,6 +2625,7 @@ function prepareDiagnosticDataInMode(
2445
2625
  ),
2446
2626
  )
2447
2627
  .map((candidate) => candidate.row);
2628
+ const compareAudits = createAuditComparator();
2448
2629
  const audit: DiagnosticInputAuditRecord[] = [
2449
2630
  ...lossCandidates.map((candidate): DiagnosticInputAuditRecord => ({
2450
2631
  kind: "loss",
@@ -2461,7 +2642,7 @@ function prepareDiagnosticDataInMode(
2461
2642
  disposition: candidate.disposition,
2462
2643
  record: candidate.snapshot,
2463
2644
  })),
2464
- ].sort(createAuditComparator());
2645
+ ].sort(compareAudits);
2465
2646
  const normalizedFindings = mergeFindings(findings);
2466
2647
  const frozenCells = preparedCells.map((cell) => deepFreeze(cell));
2467
2648
  const cutoffValues = normalizedCutoffs.map((cutoff) => cutoff.value);
@@ -2483,6 +2664,58 @@ function prepareDiagnosticDataInMode(
2483
2664
  ...ownedData,
2484
2665
  }) as unknown as CompactPreparedDiagnosticData;
2485
2666
  authenticCompact.add(prepared);
2667
+ if (definition.definition.lossRowGrain === "aggregate" && [...knownSourceGroups].every((group) => selectedBySource(filter, group)) &&
2668
+ normalizedFindings.length === 0 && reconciled.every((exposure) => exposure.status === "valid")) {
2669
+ const excluded = new Map<object, DiagnosticInputAuditRecord>();
2670
+ lossCandidates.forEach((candidate, index) => {
2671
+ const raw = rawLossSnapshots[index]!;
2672
+ // Normalization changes only the coordinate labels in these snapshots.
2673
+ // When those were canonical already, reuse the same complete record.
2674
+ 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 }));
2677
+ });
2678
+ exposureCandidates.forEach((candidate, index) => {
2679
+ const raw = rawExposureSnapshots[index]!;
2680
+ 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 }));
2683
+ });
2684
+ expectedCandidates.forEach((candidate) => excluded.set(candidate.snapshot, deepFreeze({
2685
+ kind: "expected-cell", disposition: "filter", record: candidate.snapshot,
2686
+ })));
2687
+ // Rank every possible included/excluded audit variant once. Query sorts
2688
+ // then use only numbers; canonical record strings live only in this
2689
+ // preparation's comparator and are not retained by the context.
2690
+ const originExcluded = new Map<object, DiagnosticInputAuditRecord>(audit.map((item) => [item.record,
2691
+ deepFreeze({ ...item, disposition: "filter" as const })]));
2692
+ const origins = new Map<string, DiagnosticNormalizedPeriod>();
2693
+ for (const candidate of lossCandidates) if (candidate.coordinate)
2694
+ origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
2695
+ for (const candidate of exposureCandidates) if (candidate.origin)
2696
+ origins.set(candidate.origin.label, candidate.origin);
2697
+ for (const candidate of expectedCandidates)
2698
+ origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
2699
+ const orderedVariants = [...audit, ...excluded.values(), ...originExcluded.values()].sort(compareAudits);
2700
+ const auditRanks = new Map(orderedVariants.map((item, index) => [item, index]));
2701
+ const allOriginsSelected = [...origins.values()].every((origin) =>
2702
+ selectedByOrigin(filter, origin, normalizedFilter.originCoordinates));
2703
+ const valuations = new Map<string, DiagnosticNormalizedPeriod>();
2704
+ let allCoordinatesSelected = true;
2705
+ const rememberCoordinate = (valuation: DiagnosticNormalizedPeriod, age: number) => {
2706
+ valuations.set(valuation.label, valuation);
2707
+ if (!selectedByValuation(filter, valuation, normalizedFilter.valuationCoordinates) || !selectedByAge(filter, age))
2708
+ allCoordinatesSelected = false;
2709
+ };
2710
+ for (const candidate of lossCandidates) if (candidate.coordinate)
2711
+ rememberCoordinate(candidate.coordinate.valuation, candidate.coordinate.developmentAge);
2712
+ for (const candidate of expectedCandidates)
2713
+ rememberCoordinate(candidate.coordinate.valuation, candidate.coordinate.developmentAge);
2714
+ for (const candidate of exposureCandidates) if (candidate.valuation !== null)
2715
+ rememberCoordinate(candidate.valuation, candidate.developmentAge!);
2716
+ sourceSelectionMetadata.set(prepared, { groups: knownSourceGroups, excluded, originExcluded,
2717
+ origins, valuations, allOriginsSelected, allCoordinatesSelected, auditRanks });
2718
+ }
2486
2719
  return prepared;
2487
2720
  }
2488
2721
  return createEagerPreparedDiagnosticData(definition, ownedData);
@@ -18,12 +18,15 @@ import {
18
18
  import {
19
19
  assertPreparedDiagnosticData,
20
20
  assertCompactPreparedDiagnosticData,
21
+ getCompactSourceSelectionParent,
22
+ getCompactCoordinateSelectionParent,
21
23
  type CompactPreparedDiagnosticData,
22
24
  type PreparedDiagnosticDataContent,
23
25
  type PreparedDiagnosticData,
24
26
  } from "./diagnosticPreparation.js";
25
27
  import {
26
28
  createCompactReviewBuilder,
29
+ selectCompactReviewSources,
27
30
  type CompactDiagnosticReviewEvaluations,
28
31
  } from "./diagnosticReviewStore.js";
29
32
  import {
@@ -704,18 +707,32 @@ export function evaluateDiagnosticReviewRules(
704
707
  return Object.freeze(results);
705
708
  }
706
709
 
710
+ const compactEvaluationCache = new WeakMap<CompactPreparedDiagnosticData, CompactDiagnosticReviewEvaluations>();
711
+
707
712
  /** Keeps every evaluation in compact owned storage; no identity is computed. */
708
713
  export function evaluateDiagnosticReviewRulesCompact(
709
714
  prepared: CompactPreparedDiagnosticData,
710
715
  ): CompactDiagnosticReviewEvaluations {
711
716
  assertCompactPreparedDiagnosticData(prepared);
717
+ const coordinateParent = getCompactCoordinateSelectionParent(prepared);
718
+ const parent = prepared.cells.length === 0 && prepared.expectedCells.length === 0
719
+ ? undefined : coordinateParent ?? getCompactSourceSelectionParent(prepared);
720
+ const invariant = parent
721
+ ? compactEvaluationCache.get(parent) ?? evaluateDiagnosticReviewRulesCompact(parent)
722
+ : undefined;
712
723
  const compactSources = createCompactCellSourceReader();
713
724
  const builder = createCompactReviewBuilder(
714
725
  prepared.definition.definition.reviewRules,
715
726
  compactSources,
716
727
  );
717
- visitDiagnosticReviewRules(prepared, builder.append, compactSources);
718
- return builder.finish();
728
+ visitDiagnosticReviewRules(prepared, builder.append, compactSources, invariant !== undefined, coordinateParent !== undefined);
729
+ const evaluated = builder.finish();
730
+ const result = invariant ? selectCompactReviewSources(invariant, evaluated,
731
+ prepared.filter?.sourceGroups === undefined ? undefined : new Set(prepared.filter.sourceGroups),
732
+ new Set([...prepared.cells.map((cell) => cell.origin), ...prepared.expectedCells.map((cell) => cell.origin)]),
733
+ coordinateParent ? new Set(prepared.cells.map((cell) => JSON.stringify([cell.sourceGroup, cell.origin, cell.valuation]))) : undefined) : evaluated;
734
+ compactEvaluationCache.set(prepared, result);
735
+ return result;
719
736
  }
720
737
 
721
738
  /** One traversal and one formula implementation serve both storage contracts. */
@@ -723,6 +740,8 @@ function visitDiagnosticReviewRules(
723
740
  prepared: PreparedDiagnosticDataContent,
724
741
  appendEvaluation: (value: DiagnosticReviewRuleEvaluation) => void,
725
742
  compactSources?: ReturnType<typeof createCompactCellSourceReader>,
743
+ controlsOnly = false,
744
+ includeMonotonic = false,
726
745
  ): void {
727
746
  const evaluateExpression = createCellExpressionEvaluator(
728
747
  prepared,
@@ -741,10 +760,11 @@ function visitDiagnosticReviewRules(
741
760
  origin: string,
742
761
  valuation: string,
743
762
  ) => canonicalJson([sourceGroup, origin, valuation]);
763
+ const monotonicGroups: Array<{ sourceGroup: string; origin: string; valuations: string[] }> = [];
744
764
  // Keep the original traversal and source order; only replace repeated searches.
745
765
  // Other rule families do not need these indices.
746
766
  if (
747
- prepared.definition.definition.reviewRules.some(
767
+ (!controlsOnly || includeMonotonic) && prepared.definition.definition.reviewRules.some(
748
768
  (rule) => rule.kind === "monotonic",
749
769
  )
750
770
  ) {
@@ -759,11 +779,36 @@ function visitDiagnosticReviewRules(
759
779
  sources.push(cell.source);
760
780
  expectedSourcesByCoordinate.set(key, sources);
761
781
  }
782
+ // Grouping and valuation ordering depend on the selected preparation, not
783
+ // the rule expression. Build them once for all monotonic rules in this run.
784
+ const groups = new Map<string, Set<string>>();
785
+ for (const cell of [...prepared.cells, ...prepared.expectedCells]) {
786
+ const key = canonicalJson([cell.sourceGroup, cell.origin]);
787
+ const values = groups.get(key) ?? new Set<string>();
788
+ values.add(cell.valuation);
789
+ groups.set(key, values);
790
+ }
791
+ const valuationCoordinates = new Map<string, number>();
792
+ const valuationCoordinate = (label: string) => {
793
+ let value = valuationCoordinates.get(label);
794
+ if (value === undefined) {
795
+ value = normalizeDiagnosticPeriod(prepared.definition, "valuation", label).coordinate;
796
+ valuationCoordinates.set(label, value);
797
+ }
798
+ return value;
799
+ };
800
+ for (const [key, values] of [...groups.entries()].sort(([left], [right]) => codeUnit(left, right))) {
801
+ const [sourceGroup, origin] = JSON.parse(key) as [string, string];
802
+ const valuations = [...values].sort((left, right) =>
803
+ valuationCoordinate(left) - valuationCoordinate(right) || codeUnit(left, right));
804
+ monotonicGroups.push({ sourceGroup, origin, valuations });
805
+ }
762
806
  }
763
807
  for (const [ruleIndex, rule] of (
764
808
  prepared.definition.definition
765
809
  .reviewRules as unknown as readonly DiagnosticReviewRule[]
766
810
  ).entries()) {
811
+ if (controlsOnly && rule.kind !== "control-total" && !(includeMonotonic && rule.kind === "monotonic")) continue;
767
812
  const basePath = `/reviewRules/${ruleIndex}`;
768
813
  if (rule.kind === "control-total") {
769
814
  const cells = projectedCells(prepared, rule);
@@ -907,30 +952,7 @@ function visitDiagnosticReviewRules(
907
952
  continue;
908
953
  }
909
954
  if (rule.kind === "monotonic") {
910
- const groups = new Map<string, Set<string>>();
911
- for (const cell of prepared.cells) {
912
- const key = canonicalJson([cell.sourceGroup, cell.origin]);
913
- const values = groups.get(key) ?? new Set<string>();
914
- values.add(cell.valuation);
915
- groups.set(key, values);
916
- }
917
- for (const expected of prepared.expectedCells) {
918
- const key = canonicalJson([expected.sourceGroup, expected.origin]);
919
- const values = groups.get(key) ?? new Set<string>();
920
- values.add(expected.valuation);
921
- groups.set(key, values);
922
- }
923
- for (const [key, valuations] of [...groups.entries()].sort(
924
- ([left], [right]) => codeUnit(left, right),
925
- )) {
926
- const [sourceGroup, origin] = JSON.parse(key) as [string, string];
927
- const sorted = [...valuations].sort(
928
- (left, right) =>
929
- normalizeDiagnosticPeriod(prepared.definition, "valuation", left)
930
- .coordinate -
931
- normalizeDiagnosticPeriod(prepared.definition, "valuation", right)
932
- .coordinate || codeUnit(left, right),
933
- );
955
+ for (const { sourceGroup, origin, valuations: sorted } of monotonicGroups) {
934
956
  for (let index = 1; index < sorted.length; index++) {
935
957
  const previousValuation = sorted[index - 1]!;
936
958
  const currentValuation = sorted[index]!;
@@ -40,18 +40,28 @@ export function createDiagnosticReviewSourcePool() {
40
40
  return entry;
41
41
  };
42
42
  const collection = () => {
43
- const entries = new Map<string, DiagnosticSourceLocation>();
43
+ // Most cell expressions cite one source row repeatedly across measures.
44
+ // Defer the union map and sorting until a second distinct source exists.
45
+ let first: SourceEntry | undefined;
46
+ let entries: Map<string, DiagnosticSourceLocation> | undefined;
44
47
  let owned = true;
45
48
  return {
46
49
  /** `fromPrepared` is private evaluator authority, not an input option. */
47
50
  add(value: DiagnosticSourceLocation, fromPrepared: boolean): void {
48
51
  const entry = sourceEntry(value, fromPrepared);
49
52
  owned &&= ownedSources.has(entry.value);
50
- if (!entries.has(entry.key)) entries.set(entry.key, entry.value);
53
+ if (!first) first = entry;
54
+ else if (entries) {
55
+ if (!entries.has(entry.key)) entries.set(entry.key, entry.value);
56
+ } else if (entry.key !== first.key) {
57
+ entries = new Map([[first.key, first.value], [entry.key, entry.value]]);
58
+ }
51
59
  },
52
60
  finish(): readonly DiagnosticSourceLocation[] {
53
61
  const values = Object.freeze(
54
- [...entries.values()].sort(compareDiagnosticSourceLocations),
62
+ entries
63
+ ? [...entries.values()].sort(compareDiagnosticSourceLocations)
64
+ : first ? [first.value] : [],
55
65
  );
56
66
  if (owned) ownedLists.add(values);
57
67
  return values;
@@ -61,6 +71,15 @@ export function createDiagnosticReviewSourcePool() {
61
71
  const union = (
62
72
  lists: readonly (readonly DiagnosticSourceLocation[])[],
63
73
  ): readonly DiagnosticSourceLocation[] => {
74
+ const first = lists[0];
75
+ // Repeated cell expressions often resolve to exactly the same owned source
76
+ // sequence. Reuse it without rebuilding a union or its builder list ID.
77
+ // Reference equality preserves full values and first representatives; no
78
+ // caller-frozen array or merely equal canonical fingerprint is trusted.
79
+ if (first && lists.every((values) => ownedLists.has(values) &&
80
+ values.length === first.length && values.every((value, index) => value === first[index]))) {
81
+ return first;
82
+ }
64
83
  const target = collection();
65
84
  for (const values of lists)
66
85
  for (const source of values) target.add(source, false);