@actuarial-ts/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +12 -3
  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/capping.d.ts.map +1 -1
  7. package/dist/capping.js +39 -6
  8. package/dist/capping.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +3 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -1
  11. package/dist/diagnosticAggregation.js +30 -7
  12. package/dist/diagnosticAggregation.js.map +1 -1
  13. package/dist/diagnosticDefinitions.d.ts +3 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -1
  15. package/dist/diagnosticDefinitions.js +844 -182
  16. package/dist/diagnosticDefinitions.js.map +1 -1
  17. package/dist/diagnosticDerivations.d.ts +23 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -1
  19. package/dist/diagnosticDerivations.js +187 -43
  20. package/dist/diagnosticDerivations.js.map +1 -1
  21. package/dist/diagnosticEvidenceIntern.d.ts +16 -0
  22. package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
  23. package/dist/diagnosticEvidenceIntern.js +139 -0
  24. package/dist/diagnosticEvidenceIntern.js.map +1 -0
  25. package/dist/diagnosticExposure.d.ts +2 -2
  26. package/dist/diagnosticExposure.d.ts.map +1 -1
  27. package/dist/diagnosticExposure.js +319 -21
  28. package/dist/diagnosticExposure.js.map +1 -1
  29. package/dist/diagnosticExpressions.d.ts +1 -1
  30. package/dist/diagnosticExpressions.d.ts.map +1 -1
  31. package/dist/diagnosticExpressions.js +35 -15
  32. package/dist/diagnosticExpressions.js.map +1 -1
  33. package/dist/diagnosticFormulas.d.ts +3 -0
  34. package/dist/diagnosticFormulas.d.ts.map +1 -1
  35. package/dist/diagnosticFormulas.js +160 -26
  36. package/dist/diagnosticFormulas.js.map +1 -1
  37. package/dist/diagnosticIdentity.d.ts +15 -1
  38. package/dist/diagnosticIdentity.d.ts.map +1 -1
  39. package/dist/diagnosticIdentity.js +321 -51
  40. package/dist/diagnosticIdentity.js.map +1 -1
  41. package/dist/diagnosticIdentityStream.d.ts +56 -0
  42. package/dist/diagnosticIdentityStream.d.ts.map +1 -0
  43. package/dist/diagnosticIdentityStream.js +472 -0
  44. package/dist/diagnosticIdentityStream.js.map +1 -0
  45. package/dist/diagnosticOrdering.d.ts +8 -0
  46. package/dist/diagnosticOrdering.d.ts.map +1 -0
  47. package/dist/diagnosticOrdering.js +73 -0
  48. package/dist/diagnosticOrdering.js.map +1 -0
  49. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  50. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  51. package/dist/diagnosticPeriodAxis.js +69 -0
  52. package/dist/diagnosticPeriodAxis.js.map +1 -0
  53. package/dist/diagnosticPeriods.d.ts +2 -3
  54. package/dist/diagnosticPeriods.d.ts.map +1 -1
  55. package/dist/diagnosticPeriods.js +20 -68
  56. package/dist/diagnosticPeriods.js.map +1 -1
  57. package/dist/diagnosticPreparation.d.ts +75 -14
  58. package/dist/diagnosticPreparation.d.ts.map +1 -1
  59. package/dist/diagnosticPreparation.js +1606 -112
  60. package/dist/diagnosticPreparation.js.map +1 -1
  61. package/dist/diagnosticReview.d.ts +40 -15
  62. package/dist/diagnosticReview.d.ts.map +1 -1
  63. package/dist/diagnosticReview.js +600 -42
  64. package/dist/diagnosticReview.js.map +1 -1
  65. package/dist/diagnosticReviewSources.d.ts +18 -0
  66. package/dist/diagnosticReviewSources.d.ts.map +1 -0
  67. package/dist/diagnosticReviewSources.js +76 -0
  68. package/dist/diagnosticReviewSources.js.map +1 -0
  69. package/dist/diagnosticReviewStore.d.ts +88 -0
  70. package/dist/diagnosticReviewStore.d.ts.map +1 -0
  71. package/dist/diagnosticReviewStore.js +455 -0
  72. package/dist/diagnosticReviewStore.js.map +1 -0
  73. package/dist/diagnosticRunner.d.ts +27 -2
  74. package/dist/diagnosticRunner.d.ts.map +1 -1
  75. package/dist/diagnosticRunner.js +764 -70
  76. package/dist/diagnosticRunner.js.map +1 -1
  77. package/dist/diagnosticRuntime.d.ts +26 -0
  78. package/dist/diagnosticRuntime.d.ts.map +1 -0
  79. package/dist/diagnosticRuntime.js +243 -0
  80. package/dist/diagnosticRuntime.js.map +1 -0
  81. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  82. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  83. package/dist/diagnosticSourceOrdering.js +38 -0
  84. package/dist/diagnosticSourceOrdering.js.map +1 -0
  85. package/dist/fnvAccumulator.d.ts +6 -0
  86. package/dist/fnvAccumulator.d.ts.map +1 -0
  87. package/dist/fnvAccumulator.js +21 -0
  88. package/dist/fnvAccumulator.js.map +1 -0
  89. package/dist/index.d.ts +15 -8
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +10 -4
  92. package/dist/index.js.map +1 -1
  93. package/dist/mack.d.ts +4 -0
  94. package/dist/mack.d.ts.map +1 -1
  95. package/dist/mack.js +32 -4
  96. package/dist/mack.js.map +1 -1
  97. package/dist/triangle.d.ts.map +1 -1
  98. package/dist/triangle.js +59 -9
  99. package/dist/triangle.js.map +1 -1
  100. package/dist/types.d.ts +4 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js +22 -8
  103. package/dist/types.js.map +1 -1
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +3 -2
  107. package/src/canonical.ts +144 -26
  108. package/src/capping.ts +109 -18
  109. package/src/diagnosticAggregation.ts +69 -20
  110. package/src/diagnosticDefinitions.ts +2675 -426
  111. package/src/diagnosticDerivations.ts +281 -49
  112. package/src/diagnosticEvidenceIntern.ts +151 -0
  113. package/src/diagnosticExposure.ts +550 -50
  114. package/src/diagnosticExpressions.ts +99 -17
  115. package/src/diagnosticFormulas.ts +239 -39
  116. package/src/diagnosticIdentity.ts +700 -191
  117. package/src/diagnosticIdentityStream.ts +553 -0
  118. package/src/diagnosticOrdering.ts +106 -0
  119. package/src/diagnosticPeriodAxis.ts +92 -0
  120. package/src/diagnosticPeriods.ts +67 -82
  121. package/src/diagnosticPreparation.ts +2558 -132
  122. package/src/diagnosticReview.ts +1072 -58
  123. package/src/diagnosticReviewSources.ts +85 -0
  124. package/src/diagnosticReviewStore.ts +692 -0
  125. package/src/diagnosticRunner.ts +1273 -72
  126. package/src/diagnosticRuntime.ts +264 -0
  127. package/src/diagnosticSourceOrdering.ts +50 -0
  128. package/src/fnvAccumulator.ts +20 -0
  129. package/src/index.ts +69 -4
  130. package/src/mack.ts +72 -9
  131. package/src/triangle.ts +103 -13
  132. package/src/types.ts +55 -20
  133. package/src/version.ts +1 -1
@@ -1,10 +1,54 @@
1
- import { getCompiledDiagnosticDefinitionInternals } from "./diagnosticDefinitions.js";
2
- import { evaluateDiagnosticMeasureExpression } from "./diagnosticFormulas.js";
3
- import { assertPreparedDiagnosticData } from "./diagnosticPreparation.js";
4
- import { classifyDiagnosticComparison, diagnosticPredicateMatches } from "./diagnosticRules.js";
5
- function states(prepared, components) {
1
+ import { getCompiledDiagnosticDefinitionInternals, } from "./diagnosticDefinitions.js";
2
+ import { canonicalJson } from "./canonical.js";
3
+ import { createDiagnosticReviewSourcePool } from "./diagnosticReviewSources.js";
4
+ import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
5
+ import { normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
6
+ import { evaluateDiagnosticMeasureExpression, } from "./diagnosticFormulas.js";
7
+ import { assertPreparedDiagnosticData, assertCompactPreparedDiagnosticData, } from "./diagnosticPreparation.js";
8
+ import { createCompactReviewBuilder, } from "./diagnosticReviewStore.js";
9
+ import { classifyDiagnosticComparison, diagnosticPredicateMatches, } from "./diagnosticRules.js";
10
+ import { finalizeDiagnosticContributions } from "./diagnosticAggregation.js";
11
+ import { compareDiagnosticSourceLocations, normalizeDiagnosticSourceLocations, } from "./diagnosticSourceOrdering.js";
12
+ const REASON_ORDER = [
13
+ "missing",
14
+ "imputed",
15
+ "non-finite",
16
+ "structural-ambiguity",
17
+ "aggregation-overflow",
18
+ "expression-overflow",
19
+ "tolerance-overflow",
20
+ ];
21
+ function codeUnit(left, right) {
22
+ return left < right ? -1 : left > right ? 1 : 0;
23
+ }
24
+ function uniqueReasons(values) {
25
+ return REASON_ORDER.filter((reason) => values.includes(reason));
26
+ }
27
+ function uniqueSources(values) {
28
+ return normalizeDiagnosticSourceLocations(values);
29
+ }
30
+ function expressionMeasureIds(expression) {
31
+ if (expression.op === "measure")
32
+ return [expression.measureId];
33
+ return [
34
+ ...new Set((expression.op === "add"
35
+ ? expression.terms
36
+ : [expression.left, expression.right]).flatMap(expressionMeasureIds)),
37
+ ].sort(codeUnit);
38
+ }
39
+ function expressionLeafPaths(expression, path) {
40
+ if (expression.op === "measure")
41
+ return [{ measureId: expression.measureId, expressionPath: path }];
42
+ if (expression.op === "add")
43
+ return expression.terms.flatMap((term, index) => expressionLeafPaths(term, `${path}/terms/${index}`));
44
+ return [
45
+ ...expressionLeafPaths(expression.left, `${path}/left`),
46
+ ...expressionLeafPaths(expression.right, `${path}/right`),
47
+ ];
48
+ }
49
+ function states(prepared, cell) {
6
50
  const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
7
- return Object.fromEntries(Object.entries(components).map(([id, stats]) => {
51
+ return Object.fromEntries(Object.entries(cell.components).map(([id, stats]) => {
8
52
  const measure = internals.measuresById.get(id);
9
53
  const readiness = [];
10
54
  if (stats.missing)
@@ -15,70 +59,584 @@ function states(prepared, components) {
15
59
  readiness.push("structural-ambiguity");
16
60
  if (stats.sum === null && !stats.nonFinite && !stats.structural)
17
61
  readiness.push("aggregation-overflow");
18
- return [id, { quantity: { kind: measure.kind, unit: measure.unit, value: stats.value }, stats, readiness }];
62
+ return [
63
+ id,
64
+ {
65
+ quantity: {
66
+ kind: measure.kind,
67
+ unit: measure.unit,
68
+ ...(measure.basisId ? { basisId: measure.basisId } : {}),
69
+ ...(measure.countPopulationId
70
+ ? { countPopulationId: measure.countPopulationId }
71
+ : {}),
72
+ ...(measure.exposureBasisId
73
+ ? { exposureBasisId: measure.exposureBasisId }
74
+ : {}),
75
+ value: stats.value,
76
+ },
77
+ stats,
78
+ readiness,
79
+ // Cell expressions attach complete leaf-source evidence separately.
80
+ // Per-measure source lists here would be discarded and rebuilt.
81
+ },
82
+ ];
19
83
  }));
20
84
  }
21
- function coordinate(cell) { return { sourceGroup: cell.sourceGroup, origin: cell.origin, valuation: cell.valuation, developmentAge: cell.developmentAge, ageUnit: cell.ageUnit }; }
22
- function evaluation(rule, scope, left, right, asserted, reasons = []) {
23
- if (left === null || right === null || reasons.length)
24
- return { ruleId: rule.id, kind: rule.kind, status: rule.missingInput === "finding" ? "triggered" : "not-evaluated", severity: rule.severity, triggerReason: rule.missingInput === "finding" ? "missing-input" : null, left, right, relation: null, notEvaluatedReasons: reasons.length ? reasons : ["missing"], expressionOverflows: [], reviewScope: scope };
25
- const compared = classifyDiagnosticComparison(left, right, rule.tolerance);
85
+ function coordinate(cell) {
86
+ return {
87
+ sourceGroup: cell.sourceGroup,
88
+ origin: cell.origin,
89
+ valuation: cell.valuation,
90
+ developmentAge: cell.developmentAge,
91
+ ageUnit: cell.ageUnit,
92
+ };
93
+ }
94
+ function coordinateFromLabels(prepared, sourceGroup, originLabel, valuationLabel) {
95
+ const origin = normalizeDiagnosticPeriod(prepared.definition, "origin", originLabel);
96
+ const valuation = normalizeDiagnosticPeriod(prepared.definition, "valuation", valuationLabel);
97
+ return {
98
+ sourceGroup,
99
+ origin: origin.label,
100
+ valuation: valuation.label,
101
+ developmentAge: valuation.coordinate -
102
+ origin.coordinate +
103
+ prepared.definition.definition.periodAxis.ageOffset,
104
+ ageUnit: prepared.definition.definition.periodAxis.ageUnit,
105
+ };
106
+ }
107
+ function cellSources(cell, measureIds) {
108
+ const ids = measureIds ?? Object.keys(cell.contributions);
109
+ return uniqueSources(ids.flatMap((measureId) => [
110
+ ...(cell.contributions[measureId] ?? []).flatMap((contribution) => contribution.sources),
111
+ ...(cell.structuralBlockers[measureId] ?? []).flatMap((blocker) => blocker.sources),
112
+ ]));
113
+ }
114
+ function cellSourcesForSelection(cells, measureId) {
115
+ return uniqueSources(cells.flatMap((cell) => [
116
+ ...(cell.contributions[measureId] ?? []).flatMap((contribution) => contribution.sources),
117
+ ...(cell.structuralBlockers[measureId] ?? []).flatMap((blocker) => blocker.sources),
118
+ ]));
119
+ }
120
+ /** Compact-only, invocation-owned caches. Never retain these on a prepared cell. */
121
+ function createCompactCellSourceReader() {
122
+ const pool = createDiagnosticReviewSourcePool();
123
+ const addCellSources = (target, cell, ids) => {
124
+ for (const id of ids) {
125
+ for (const contribution of cell.contributions[id] ?? [])
126
+ for (const source of contribution.sources)
127
+ target.add(source, true);
128
+ for (const blocker of cell.structuralBlockers[id] ?? [])
129
+ for (const source of blocker.sources)
130
+ target.add(source, true);
131
+ }
132
+ };
133
+ const forSelection = (cells, measureId) => {
134
+ const target = pool.collection();
135
+ const ids = [measureId];
136
+ for (const cell of cells)
137
+ addCellSources(target, cell, ids);
138
+ return target.finish();
139
+ };
140
+ const dependencies = new WeakMap();
141
+ const dependencyKeys = new WeakMap();
142
+ const byCell = new WeakMap();
143
+ const measureIds = (expression) => {
144
+ let ids = dependencies.get(expression);
145
+ if (!ids) {
146
+ ids = Object.freeze(expressionMeasureIds(expression));
147
+ dependencies.set(expression, ids);
148
+ }
149
+ return ids;
150
+ };
151
+ const forCell = (cell, ids) => {
152
+ let key = dependencyKeys.get(ids);
153
+ if (key === undefined) {
154
+ key = canonicalJson(ids);
155
+ dependencyKeys.set(ids, key);
156
+ }
157
+ let subsets = byCell.get(cell);
158
+ if (!subsets) {
159
+ subsets = new Map();
160
+ byCell.set(cell, subsets);
161
+ }
162
+ let sources = subsets.get(key);
163
+ if (!sources) {
164
+ const target = pool.collection();
165
+ addCellSources(target, cell, ids);
166
+ sources = target.finish();
167
+ subsets.set(key, sources);
168
+ }
169
+ return sources;
170
+ };
171
+ return {
172
+ ...pool,
173
+ measureIds,
174
+ forCell,
175
+ forSelection,
176
+ };
177
+ }
178
+ function createCellExpressionEvaluator(prepared, compactSources) {
179
+ // Reuse projections across this review's rules, then release them before the
180
+ // receipt identity is constructed. Retaining a prepared cell must not retain
181
+ // this temporary bookkeeping or reuse another review's state.
182
+ const statesByCell = new WeakMap();
183
+ return (cell, expression, path) => {
184
+ let measures = statesByCell.get(cell);
185
+ if (!measures) {
186
+ measures = states(prepared, cell);
187
+ statesByCell.set(cell, measures);
188
+ }
189
+ const result = evaluateDiagnosticMeasureExpression(expression, measures, path);
190
+ const sources = compactSources
191
+ ? compactSources.forCell(cell, compactSources.measureIds(expression))
192
+ : cellSources(cell, expressionMeasureIds(expression));
193
+ return {
194
+ value: result.value,
195
+ reasons: result.reasons,
196
+ overflows: result.overflows.map((overflow) => ({
197
+ ...overflow,
198
+ coordinate: coordinate(cell),
199
+ sources,
200
+ })),
201
+ sources,
202
+ };
203
+ };
204
+ }
205
+ function constant(value) {
206
+ return { value, reasons: [], overflows: [], sources: [] };
207
+ }
208
+ function evaluation(prepared, rule, scope, left, right, passes, extraReasons = []) {
209
+ const expressionOverflows = [
210
+ ...new Map([...left.overflows, ...right.overflows].map((item) => [
211
+ canonicalJson(item),
212
+ item,
213
+ ])).values(),
214
+ ].sort((a, b) => {
215
+ const path = codeUnit(a.expressionPath, b.expressionPath);
216
+ if (path !== 0)
217
+ return path;
218
+ if (a.coordinate === null)
219
+ return b.coordinate === null ? 0 : 1;
220
+ if (b.coordinate === null)
221
+ return -1;
222
+ const coordinate = codeUnit(a.coordinate.sourceGroup, b.coordinate.sourceGroup) ||
223
+ normalizeDiagnosticPeriod(prepared.definition, "origin", a.coordinate.origin).coordinate -
224
+ normalizeDiagnosticPeriod(prepared.definition, "origin", b.coordinate.origin).coordinate ||
225
+ normalizeDiagnosticPeriod(prepared.definition, "valuation", a.coordinate.valuation).coordinate -
226
+ normalizeDiagnosticPeriod(prepared.definition, "valuation", b.coordinate.valuation).coordinate ||
227
+ codeUnit(a.coordinate.origin, b.coordinate.origin) ||
228
+ codeUnit(a.coordinate.valuation, b.coordinate.valuation);
229
+ if (coordinate !== 0)
230
+ return coordinate;
231
+ for (let index = 0; index < Math.min(a.sources.length, b.sources.length); index++) {
232
+ const source = compareDiagnosticSourceLocations(a.sources[index], b.sources[index]);
233
+ if (source !== 0)
234
+ return source;
235
+ }
236
+ return a.sources.length - b.sources.length;
237
+ });
238
+ const reasons = uniqueReasons([
239
+ ...left.reasons,
240
+ ...right.reasons,
241
+ ...extraReasons,
242
+ ...(expressionOverflows.length > 0 ? ["expression-overflow"] : []),
243
+ ...((left.value === null || right.value === null) &&
244
+ left.reasons.length === 0 &&
245
+ right.reasons.length === 0 &&
246
+ expressionOverflows.length === 0
247
+ ? ["missing"]
248
+ : []),
249
+ ]);
250
+ const missingReason = () => {
251
+ if (reasons.some((reason) => ["missing", "imputed", "non-finite", "structural-ambiguity"].includes(reason)))
252
+ return "missing-input";
253
+ if (reasons.includes("aggregation-overflow"))
254
+ return "aggregation-overflow";
255
+ if (reasons.includes("expression-overflow"))
256
+ return "expression-overflow";
257
+ if (reasons.includes("tolerance-overflow"))
258
+ return "tolerance-overflow";
259
+ return "missing-input";
260
+ };
261
+ const result = (value) => ({
262
+ ruleId: rule.id,
263
+ ruleKind: rule.kind,
264
+ severity: rule.severity,
265
+ scope,
266
+ ...value,
267
+ ...(rule.kind === "layer-order"
268
+ ? { comparability: rule.comparability }
269
+ : {}),
270
+ });
271
+ if (reasons.length > 0)
272
+ return result({
273
+ status: rule.missingInput === "finding" ? "triggered" : "not-evaluated",
274
+ triggerReason: rule.missingInput === "finding" ? missingReason() : null,
275
+ left: left.value,
276
+ right: right.value,
277
+ relation: null,
278
+ notEvaluatedReasons: reasons,
279
+ expressionOverflows,
280
+ });
281
+ const compared = classifyDiagnosticComparison(left.value, right.value, rule.tolerance);
26
282
  if (compared.status === "not-evaluated")
27
- return { ruleId: rule.id, kind: rule.kind, status: "not-evaluated", severity: rule.severity, triggerReason: null, left, right, relation: null, notEvaluatedReasons: [compared.reason], expressionOverflows: [], reviewScope: scope };
28
- return { ruleId: rule.id, kind: rule.kind, status: asserted ? "pass" : "triggered", severity: rule.severity, triggerReason: asserted ? null : "predicate", left, right, relation: compared.relation, notEvaluatedReasons: [], expressionOverflows: [], reviewScope: scope };
283
+ return result({
284
+ status: rule.missingInput === "finding" ? "triggered" : "not-evaluated",
285
+ triggerReason: rule.missingInput === "finding"
286
+ ? compared.reason === "tolerance-overflow"
287
+ ? "tolerance-overflow"
288
+ : "missing-input"
289
+ : null,
290
+ left: left.value,
291
+ right: right.value,
292
+ relation: null,
293
+ notEvaluatedReasons: [compared.reason],
294
+ expressionOverflows: [],
295
+ });
296
+ const pass = passes(compared.relation);
297
+ return result({
298
+ status: pass ? "pass" : "triggered",
299
+ triggerReason: pass ? null : "predicate",
300
+ left: left.value,
301
+ right: right.value,
302
+ relation: compared.relation,
303
+ notEvaluatedReasons: [],
304
+ expressionOverflows: [],
305
+ });
306
+ }
307
+ function filterCells(prepared, filter) {
308
+ if (!filter)
309
+ return prepared.cells;
310
+ const originFrom = filter.originFrom == null
311
+ ? null
312
+ : normalizeDiagnosticPeriod(prepared.definition, "origin", filter.originFrom).coordinate;
313
+ const originThrough = filter.originThrough == null
314
+ ? null
315
+ : normalizeDiagnosticPeriod(prepared.definition, "origin", filter.originThrough).coordinate;
316
+ const valuationFrom = filter.valuationFrom == null
317
+ ? null
318
+ : normalizeDiagnosticPeriod(prepared.definition, "valuation", filter.valuationFrom).coordinate;
319
+ const valuationThrough = filter.valuationThrough == null
320
+ ? null
321
+ : normalizeDiagnosticPeriod(prepared.definition, "valuation", filter.valuationThrough).coordinate;
322
+ return prepared.cells.filter((cell) => {
323
+ const origin = normalizeDiagnosticPeriod(prepared.definition, "origin", cell.origin).coordinate;
324
+ const valuation = normalizeDiagnosticPeriod(prepared.definition, "valuation", cell.valuation).coordinate;
325
+ return ((filter.sourceGroups == null ||
326
+ filter.sourceGroups.includes(cell.sourceGroup)) &&
327
+ (filter.origins == null || filter.origins.includes(cell.origin)) &&
328
+ (originFrom === null || origin >= originFrom) &&
329
+ (originThrough === null || origin <= originThrough) &&
330
+ (filter.valuations == null ||
331
+ filter.valuations.includes(cell.valuation)) &&
332
+ (valuationFrom === null || valuation >= valuationFrom) &&
333
+ (valuationThrough === null || valuation <= valuationThrough) &&
334
+ (filter.minDevelopmentAge == null ||
335
+ cell.developmentAge >= filter.minDevelopmentAge) &&
336
+ (filter.maxDevelopmentAge == null ||
337
+ cell.developmentAge <= filter.maxDevelopmentAge));
338
+ });
339
+ }
340
+ function projectedCells(prepared, rule) {
341
+ const selected = filterCells(prepared, rule.filter);
342
+ if (rule.projection.kind === "all-cells")
343
+ return selected;
344
+ if (rule.projection.kind === "valuation") {
345
+ const valuation = normalizeDiagnosticPeriod(prepared.definition, "valuation", rule.projection.valuation).label;
346
+ return selected.filter((cell) => cell.valuation === valuation);
347
+ }
348
+ const latest = new Map();
349
+ for (const cell of selected) {
350
+ const key = canonicalJson([cell.sourceGroup, cell.origin]);
351
+ const previous = latest.get(key);
352
+ if (!previous || cell.developmentAge > previous.developmentAge)
353
+ latest.set(key, cell);
354
+ }
355
+ return [...latest.values()].sort((left, right) => codeUnit(left.sourceGroup, right.sourceGroup) ||
356
+ codeUnit(left.origin, right.origin));
357
+ }
358
+ /**
359
+ * Finish each evaluation before retaining it. All mutable descendants here
360
+ * were constructed by this review; frozen descendants belong to the compiled
361
+ * definition or an earlier finalized child and must retain their identity.
362
+ * The pool is local to one review and never sees prepared cells or SDK brands.
363
+ */
364
+ function createEvaluationFinalizer() {
365
+ const sharing = createDiagnosticEvidenceInterner();
366
+ const finalizeChild = (value, sourceSlot = false) => {
367
+ if (value === null || typeof value !== "object" || Object.isFrozen(value))
368
+ return value;
369
+ const array = Array.isArray(value);
370
+ for (const key of Object.keys(value)) {
371
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
372
+ const child = finalizeChild(descriptor.value, array ? sourceSlot : key === "source" || key === "sources");
373
+ if (child !== descriptor.value)
374
+ Object.defineProperty(value, key, { ...descriptor, value: child });
375
+ }
376
+ return sharing.internOwned(Object.freeze(value), sourceSlot ? "source" : "plain");
377
+ };
378
+ return (value) => {
379
+ // Keep every evaluation as its original, distinct record. Only its owned
380
+ // children may share storage; no evaluation or array entry is removed.
381
+ for (const key of ["scope", "notEvaluatedReasons", "expressionOverflows"]) {
382
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
383
+ const child = finalizeChild(descriptor.value);
384
+ if (child !== descriptor.value)
385
+ Object.defineProperty(value, key, { ...descriptor, value: child });
386
+ }
387
+ return Object.freeze(value);
388
+ };
29
389
  }
30
390
  /** Evaluates definition-owned semantic review rules over authenticated prepared cells. */
31
391
  export function evaluateDiagnosticReviewRules(prepared) {
32
392
  assertPreparedDiagnosticData(prepared);
33
393
  const results = [];
34
- for (const rule of prepared.definition.definition.reviewRules) {
394
+ const finalizeEvaluation = createEvaluationFinalizer();
395
+ visitDiagnosticReviewRules(prepared, (value) => {
396
+ results.push(finalizeEvaluation(value));
397
+ });
398
+ return Object.freeze(results);
399
+ }
400
+ /** Keeps every evaluation in compact owned storage; no identity is computed. */
401
+ export function evaluateDiagnosticReviewRulesCompact(prepared) {
402
+ assertCompactPreparedDiagnosticData(prepared);
403
+ const compactSources = createCompactCellSourceReader();
404
+ const builder = createCompactReviewBuilder(prepared.definition.definition.reviewRules, compactSources);
405
+ visitDiagnosticReviewRules(prepared, builder.append, compactSources);
406
+ return builder.finish();
407
+ }
408
+ /** One traversal and one formula implementation serve both storage contracts. */
409
+ function visitDiagnosticReviewRules(prepared, appendEvaluation, compactSources) {
410
+ const evaluateExpression = createCellExpressionEvaluator(prepared, compactSources);
411
+ const cellByCoordinate = new Map();
412
+ const expectedSourcesByCoordinate = new Map();
413
+ const coordinateKey = (sourceGroup, origin, valuation) => canonicalJson([sourceGroup, origin, valuation]);
414
+ // Keep the original traversal and source order; only replace repeated searches.
415
+ // Other rule families do not need these indices.
416
+ if (prepared.definition.definition.reviewRules.some((rule) => rule.kind === "monotonic")) {
417
+ for (const cell of prepared.cells) {
418
+ const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
419
+ if (!cellByCoordinate.has(key))
420
+ cellByCoordinate.set(key, cell);
421
+ }
422
+ for (const cell of prepared.expectedCells) {
423
+ if (!cell.source)
424
+ continue;
425
+ const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
426
+ const sources = expectedSourcesByCoordinate.get(key) ?? [];
427
+ sources.push(cell.source);
428
+ expectedSourcesByCoordinate.set(key, sources);
429
+ }
430
+ }
431
+ for (const [ruleIndex, rule] of prepared.definition.definition
432
+ .reviewRules.entries()) {
433
+ const basePath = `/reviewRules/${ruleIndex}`;
35
434
  if (rule.kind === "control-total") {
36
- const values = prepared.cells.map((cell) => evaluateDiagnosticMeasureExpression(rule.expression, states(prepared, cell.components), "$.reviewRules.control-total").value);
37
- const left = values.some((value) => value === null) ? null : values.reduce((sum, value) => sum + value, 0);
38
- const scope = { kind: "control-total", selectedCellCount: prepared.cells.length, selectedContributionCount: prepared.cells.reduce((sum, cell) => sum + Object.values(cell.contributions).reduce((n, items) => n + items.length, 0), 0), sources: [] };
39
- const classified = classifyDiagnosticComparison(left, rule.expected, rule.tolerance);
40
- results.push(evaluation(rule, scope, left, rule.expected, classified.status === "evaluated" && classified.relation === "equal"));
435
+ const cells = projectedCells(prepared, rule);
436
+ const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
437
+ const measureIds = expressionMeasureIds(rule.expression);
438
+ const sourceByMeasure = Object.fromEntries(measureIds.map((measureId) => [
439
+ measureId,
440
+ compactSources
441
+ ? compactSources.forSelection(cells, measureId)
442
+ : cellSourcesForSelection(cells, measureId),
443
+ ]));
444
+ const globalStates = Object.create(null);
445
+ const leaves = expressionLeafPaths(rule.expression, `/reviewRules/${ruleIndex}/expression`);
446
+ for (const measureId of measureIds) {
447
+ const measure = internals.measuresById.get(measureId);
448
+ const contributions = cells.flatMap((cell) => cell.contributions[measureId] ?? []);
449
+ const blockers = [
450
+ ...new Map(cells
451
+ .flatMap((cell) => cell.structuralBlockers[measureId] ?? [])
452
+ .map((blocker) => [canonicalJson(blocker), blocker])).entries(),
453
+ ]
454
+ .sort(([left], [right]) => codeUnit(left, right))
455
+ .map(([, blocker]) => blocker);
456
+ const stats = finalizeDiagnosticContributions(contributions, measure.missing, blockers);
457
+ const sourceCellOverflow = cells.some((cell) => {
458
+ const component = cell.components[measureId];
459
+ return (component !== undefined &&
460
+ component.sum === null &&
461
+ component.nonFinite === 0 &&
462
+ component.structural === 0);
463
+ });
464
+ const selectionOverflow = stats.sum === null && stats.nonFinite === 0 && stats.structural === 0;
465
+ const emptySelection = cells.length === 0;
466
+ const expressionOverflows = selectionOverflow && !sourceCellOverflow
467
+ ? leaves
468
+ .filter((leaf) => leaf.measureId === measureId)
469
+ .map((leaf) => ({
470
+ expressionPath: leaf.expressionPath,
471
+ sources: sourceByMeasure[measureId],
472
+ }))
473
+ : [];
474
+ const reasons = [];
475
+ if (emptySelection || stats.missing)
476
+ reasons.push(stats.imputedZero ? "imputed" : "missing");
477
+ if (stats.nonFinite)
478
+ reasons.push("non-finite");
479
+ if (stats.structural)
480
+ reasons.push("structural-ambiguity");
481
+ if (sourceCellOverflow)
482
+ reasons.push("aggregation-overflow");
483
+ else if (selectionOverflow)
484
+ reasons.push("expression-overflow");
485
+ globalStates[measureId] = {
486
+ quantity: {
487
+ kind: measure.kind,
488
+ unit: measure.unit,
489
+ ...(measure.basisId ? { basisId: measure.basisId } : {}),
490
+ ...(measure.countPopulationId
491
+ ? { countPopulationId: measure.countPopulationId }
492
+ : {}),
493
+ ...(measure.exposureBasisId
494
+ ? { exposureBasisId: measure.exposureBasisId }
495
+ : {}),
496
+ value: emptySelection || sourceCellOverflow || selectionOverflow
497
+ ? null
498
+ : stats.value,
499
+ },
500
+ stats,
501
+ readiness: reasons,
502
+ expressionOverflows,
503
+ sources: sourceByMeasure[measureId],
504
+ };
505
+ }
506
+ const evaluated = evaluateDiagnosticMeasureExpression(rule.expression, globalStates, `/reviewRules/${ruleIndex}/expression`);
507
+ const sources = compactSources
508
+ ? compactSources.union(measureIds.map((measureId) => sourceByMeasure[measureId] ?? []))
509
+ : uniqueSources(measureIds.flatMap((measureId) => sourceByMeasure[measureId] ?? []));
510
+ const left = {
511
+ value: evaluated.value,
512
+ reasons: evaluated.reasons,
513
+ overflows: evaluated.overflows.map((overflow) => ({
514
+ ...overflow,
515
+ coordinate: null,
516
+ sources: overflow.sources.length > 0 ? overflow.sources : sources,
517
+ })),
518
+ sources,
519
+ };
520
+ const scope = {
521
+ kind: "control-total",
522
+ projection: rule.projection,
523
+ filter: (rule.filter ??
524
+ null),
525
+ selectedCellCount: cells.length,
526
+ selectedContributionCount: cells.reduce((sum, cell) => sum +
527
+ measureIds.reduce((count, measureId) => count + (cell.contributions[measureId]?.length ?? 0), 0), 0),
528
+ sources,
529
+ };
530
+ appendEvaluation(evaluation(prepared, rule, scope, left, constant(rule.expected), (relation) => relation === "equal"));
41
531
  continue;
42
532
  }
43
533
  if (rule.kind === "monotonic") {
44
534
  const groups = new Map();
45
535
  for (const cell of prepared.cells) {
46
- const key = `${cell.sourceGroup}\0${cell.origin}`;
47
- groups.set(key, [...(groups.get(key) ?? []), cell]);
536
+ const key = canonicalJson([cell.sourceGroup, cell.origin]);
537
+ const values = groups.get(key) ?? new Set();
538
+ values.add(cell.valuation);
539
+ groups.set(key, values);
540
+ }
541
+ for (const expected of prepared.expectedCells) {
542
+ const key = canonicalJson([expected.sourceGroup, expected.origin]);
543
+ const values = groups.get(key) ?? new Set();
544
+ values.add(expected.valuation);
545
+ groups.set(key, values);
48
546
  }
49
- for (const cells of groups.values()) {
50
- const sorted = [...cells].sort((a, b) => a.developmentAge - b.developmentAge);
547
+ for (const [key, valuations] of [...groups.entries()].sort(([left], [right]) => codeUnit(left, right))) {
548
+ const [sourceGroup, origin] = JSON.parse(key);
549
+ const sorted = [...valuations].sort((left, right) => normalizeDiagnosticPeriod(prepared.definition, "valuation", left)
550
+ .coordinate -
551
+ normalizeDiagnosticPeriod(prepared.definition, "valuation", right)
552
+ .coordinate || codeUnit(left, right));
51
553
  for (let index = 1; index < sorted.length; index++) {
52
- const previous = sorted[index - 1], current = sorted[index];
53
- const left = evaluateDiagnosticMeasureExpression(rule.expression, states(prepared, previous.components), "$.reviewRules.monotonic").value;
54
- const right = evaluateDiagnosticMeasureExpression(rule.expression, states(prepared, current.components), "$.reviewRules.monotonic").value;
55
- results.push(evaluation(rule, { kind: "valuation-pair", previous: coordinate(previous), current: coordinate(current), sources: [] }, left, right, rule.direction === "nondecreasing" ? left !== null && right !== null && left <= right : left !== null && right !== null && left >= right));
554
+ const previousValuation = sorted[index - 1];
555
+ const currentValuation = sorted[index];
556
+ const previous = cellByCoordinate.get(coordinateKey(sourceGroup, origin, previousValuation));
557
+ const current = cellByCoordinate.get(coordinateKey(sourceGroup, origin, currentValuation));
558
+ const expectedSources = (valuation) => expectedSourcesByCoordinate.get(coordinateKey(sourceGroup, origin, valuation)) ?? [];
559
+ const left = previous
560
+ ? evaluateExpression(previous, rule.expression, `${basePath}/expression`)
561
+ : {
562
+ value: null,
563
+ reasons: ["missing"],
564
+ overflows: [],
565
+ sources: compactSources
566
+ ? compactSources.forPreparedSources(expectedSources(previousValuation))
567
+ : uniqueSources(expectedSources(previousValuation)),
568
+ };
569
+ const right = current
570
+ ? evaluateExpression(current, rule.expression, `${basePath}/expression`)
571
+ : {
572
+ value: null,
573
+ reasons: ["missing"],
574
+ overflows: [],
575
+ sources: compactSources
576
+ ? compactSources.forPreparedSources(expectedSources(currentValuation))
577
+ : uniqueSources(expectedSources(currentValuation)),
578
+ };
579
+ const scope = {
580
+ kind: "valuation-pair",
581
+ previous: previous
582
+ ? coordinate(previous)
583
+ : coordinateFromLabels(prepared, sourceGroup, origin, previousValuation),
584
+ current: current
585
+ ? coordinate(current)
586
+ : coordinateFromLabels(prepared, sourceGroup, origin, currentValuation),
587
+ sources: compactSources
588
+ ? compactSources.union([left.sources, right.sources])
589
+ : uniqueSources([...left.sources, ...right.sources]),
590
+ };
591
+ appendEvaluation(evaluation(prepared, rule, scope, left, right, (relation) => rule.direction === "nondecreasing"
592
+ ? relation !== "greater"
593
+ : relation !== "less"));
56
594
  }
57
595
  }
58
596
  continue;
59
597
  }
598
+ // Scope evidence is the union of this rule's expression leaves, not every
599
+ // measure in the cell. Constants have no source dependency.
600
+ const scopeDependencies = compactSources
601
+ ? Object.freeze([
602
+ ...new Set((rule.kind === "compare"
603
+ ? [rule.when.left, rule.when.right]
604
+ : rule.kind === "reconcile"
605
+ ? [rule.actual, rule.expected]
606
+ : [rule.narrower, rule.broader]).flatMap((expression) => expression.op === "constant"
607
+ ? []
608
+ : compactSources.measureIds(expression))),
609
+ ].sort(codeUnit))
610
+ : undefined;
60
611
  for (const cell of prepared.cells) {
61
- const state = states(prepared, cell.components);
62
- const scope = { kind: "cell", coordinate: coordinate(cell), sources: [] };
612
+ const scopeFor = (leftSources, rightSources) => ({
613
+ kind: "cell",
614
+ cell: coordinate(cell),
615
+ sources: compactSources
616
+ ? compactSources.forCell(cell, scopeDependencies)
617
+ : uniqueSources([...leftSources, ...rightSources]),
618
+ });
63
619
  if (rule.kind === "compare") {
64
- const operand = (item) => item.op === "constant" ? item.value : evaluateDiagnosticMeasureExpression(item, state, "$.reviewRules.compare").value;
65
- const left = operand(rule.when.left), right = operand(rule.when.right);
66
- const classified = classifyDiagnosticComparison(left, right, rule.tolerance);
67
- results.push(evaluation(rule, scope, left, right, classified.status === "evaluated" && !diagnosticPredicateMatches(rule.when.operator, classified.relation)));
620
+ const operand = (item, path) => item.op === "constant"
621
+ ? constant(item.value)
622
+ : evaluateExpression(cell, item, path);
623
+ const left = operand(rule.when.left, `${basePath}/when/left`);
624
+ const right = operand(rule.when.right, `${basePath}/when/right`);
625
+ appendEvaluation(evaluation(prepared, rule, scopeFor(left.sources, right.sources), left, right, (relation) => !diagnosticPredicateMatches(rule.when.operator, relation)));
68
626
  }
69
627
  else if (rule.kind === "reconcile") {
70
- const left = evaluateDiagnosticMeasureExpression(rule.actual, state, "$.reviewRules.reconcile.actual").value;
71
- const right = rule.expected.op === "constant" ? rule.expected.value : evaluateDiagnosticMeasureExpression(rule.expected, state, "$.reviewRules.reconcile.expected").value;
72
- const classified = classifyDiagnosticComparison(left, right, rule.tolerance);
73
- results.push(evaluation(rule, scope, left, right, classified.status === "evaluated" && classified.relation === "equal"));
628
+ const left = evaluateExpression(cell, rule.actual, `${basePath}/actual`);
629
+ const right = rule.expected.op === "constant"
630
+ ? constant(rule.expected.value)
631
+ : evaluateExpression(cell, rule.expected, `${basePath}/expected`);
632
+ appendEvaluation(evaluation(prepared, rule, scopeFor(left.sources, right.sources), left, right, (relation) => relation === "equal"));
74
633
  }
75
634
  else {
76
- const left = evaluateDiagnosticMeasureExpression(rule.narrower, state, "$.reviewRules.layer.narrower").value;
77
- const right = evaluateDiagnosticMeasureExpression(rule.broader, state, "$.reviewRules.layer.broader").value;
78
- results.push(evaluation(rule, scope, left, right, left !== null && right !== null && left <= right));
635
+ const left = evaluateExpression(cell, rule.narrower, `${basePath}/narrower`);
636
+ const right = evaluateExpression(cell, rule.broader, `${basePath}/broader`);
637
+ appendEvaluation(evaluation(prepared, rule, scopeFor(left.sources, right.sources), left, right, (relation) => relation !== "greater"));
79
638
  }
80
639
  }
81
640
  }
82
- return Object.freeze(results.map((item) => Object.freeze(item)));
83
641
  }
84
642
  //# sourceMappingURL=diagnosticReview.js.map