@actuarial-ts/core 0.10.0 → 0.11.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.
- package/README.md +9 -7
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +64 -5
- package/dist/canonical.js.map +1 -1
- package/dist/customizationContracts.d.ts +10 -1
- package/dist/customizationContracts.d.ts.map +1 -1
- package/dist/descriptiveStatistics.js +1 -1
- package/dist/descriptiveStatistics.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +26 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +125 -11
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticAuditText.d.ts +11 -0
- package/dist/diagnosticAuditText.d.ts.map +1 -0
- package/dist/diagnosticAuditText.js +179 -0
- package/dist/diagnosticAuditText.js.map +1 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -1
- package/dist/diagnosticFormulas.js +71 -12
- package/dist/diagnosticFormulas.js.map +1 -1
- package/dist/diagnosticFreeze.d.ts +15 -1
- package/dist/diagnosticFreeze.d.ts.map +1 -1
- package/dist/diagnosticFreeze.js +82 -31
- package/dist/diagnosticFreeze.js.map +1 -1
- package/dist/diagnosticKeys.d.ts +9 -0
- package/dist/diagnosticKeys.d.ts.map +1 -0
- package/dist/diagnosticKeys.js +20 -0
- package/dist/diagnosticKeys.js.map +1 -0
- package/dist/diagnosticOrdering.d.ts.map +1 -1
- package/dist/diagnosticOrdering.js +9 -2
- package/dist/diagnosticOrdering.js.map +1 -1
- package/dist/diagnosticPeriods.d.ts.map +1 -1
- package/dist/diagnosticPeriods.js +19 -1
- package/dist/diagnosticPeriods.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +19 -0
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +544 -231
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +419 -186
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +6 -1
- package/dist/diagnosticReviewSources.d.ts.map +1 -1
- package/dist/diagnosticReviewSources.js +59 -14
- package/dist/diagnosticReviewSources.js.map +1 -1
- package/dist/diagnosticReviewStore.d.ts.map +1 -1
- package/dist/diagnosticReviewStore.js +48 -7
- package/dist/diagnosticReviewStore.js.map +1 -1
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +112 -37
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/diagnosticRuntime.d.ts.map +1 -1
- package/dist/diagnosticRuntime.js +11 -1
- package/dist/diagnosticRuntime.js.map +1 -1
- package/dist/diagnosticSourceOrdering.d.ts +27 -0
- package/dist/diagnosticSourceOrdering.d.ts.map +1 -1
- package/dist/diagnosticSourceOrdering.js +98 -12
- package/dist/diagnosticSourceOrdering.js.map +1 -1
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +35 -0
- package/dist/mack.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/canonical.ts +63 -5
- package/src/customizationContracts.ts +10 -1
- package/src/descriptiveStatistics.ts +1 -1
- package/src/diagnosticAggregation.ts +173 -9
- package/src/diagnosticAuditText.ts +187 -0
- package/src/diagnosticFormulas.ts +77 -27
- package/src/diagnosticFreeze.ts +75 -30
- package/src/diagnosticKeys.ts +22 -0
- package/src/diagnosticOrdering.ts +9 -8
- package/src/diagnosticPeriods.ts +21 -1
- package/src/diagnosticPreparation.ts +624 -272
- package/src/diagnosticReview.ts +566 -268
- package/src/diagnosticReviewSources.ts +69 -18
- package/src/diagnosticReviewStore.ts +48 -6
- package/src/diagnosticRunner.ts +133 -54
- package/src/diagnosticRuntime.ts +11 -1
- package/src/diagnosticSourceOrdering.ts +115 -11
- package/src/mack.ts +41 -0
- package/src/types.ts +1 -0
- package/src/version.ts +1 -1
package/dist/diagnosticReview.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { getCompiledDiagnosticDefinitionInternals, } from "./diagnosticDefinitions.js";
|
|
2
2
|
import { canonicalJson } from "./canonical.js";
|
|
3
|
+
import { canonicalScalarArrayJson } from "./diagnosticKeys.js";
|
|
3
4
|
import { createDiagnosticReviewSourcePool } from "./diagnosticReviewSources.js";
|
|
4
5
|
import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
|
|
5
6
|
import { normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
|
|
6
7
|
import { evaluateDiagnosticMeasureExpression, } from "./diagnosticFormulas.js";
|
|
8
|
+
import { hasDiagnosticOwn } from "./diagnosticRuntime.js";
|
|
7
9
|
import { assertPreparedDiagnosticData, assertCompactPreparedDiagnosticData, getCompactSourceSelectionParent, getCompactCoordinateSelectionParent, } from "./diagnosticPreparation.js";
|
|
8
10
|
import { createCompactReviewBuilder, selectCompactReviewSources, } from "./diagnosticReviewStore.js";
|
|
9
11
|
import { classifyDiagnosticComparison, diagnosticPredicateMatches, } from "./diagnosticRules.js";
|
|
10
12
|
import { finalizeDiagnosticContributions } from "./diagnosticAggregation.js";
|
|
11
13
|
import { compareDiagnosticSourceLocations, normalizeDiagnosticSourceLocations, } from "./diagnosticSourceOrdering.js";
|
|
14
|
+
// Shared immutable empties for the overwhelmingly common clean cell: readers
|
|
15
|
+
// only iterate or spread these, and every retained evaluation owns fresh arrays.
|
|
16
|
+
const NO_REASONS = Object.freeze([]);
|
|
17
|
+
const NO_REVIEW_OVERFLOWS = Object.freeze([]);
|
|
18
|
+
const NO_SOURCES = Object.freeze([]);
|
|
12
19
|
const REASON_ORDER = [
|
|
13
20
|
"missing",
|
|
14
21
|
"imputed",
|
|
@@ -46,41 +53,62 @@ function expressionLeafPaths(expression, path) {
|
|
|
46
53
|
...expressionLeafPaths(expression.right, `${path}/right`),
|
|
47
54
|
];
|
|
48
55
|
}
|
|
49
|
-
|
|
56
|
+
// A projected state depends only on its measure's definition and the cell's
|
|
57
|
+
// statistics record, and preparation shares equal statistics records across
|
|
58
|
+
// cells, so one projection per (measure, record) serves every cell holding it.
|
|
59
|
+
// The cache lives in one review invocation, so no projection crosses
|
|
60
|
+
// preparations or definitions. Bounded like the interner.
|
|
61
|
+
const MAX_CACHED_STATES = 100_000;
|
|
62
|
+
function states(prepared, cell, cache) {
|
|
50
63
|
const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
const result = {};
|
|
65
|
+
const ids = Object.keys(cell.components);
|
|
66
|
+
let cached = 0;
|
|
67
|
+
for (let index = 0; index < ids.length; index++) {
|
|
68
|
+
const id = ids[index];
|
|
69
|
+
const stats = cell.components[id];
|
|
70
|
+
let byStats = cache.get(id);
|
|
71
|
+
if (byStats === undefined) {
|
|
72
|
+
byStats = new Map();
|
|
73
|
+
cache.set(id, byStats);
|
|
74
|
+
}
|
|
75
|
+
let state = byStats.get(stats);
|
|
76
|
+
if (state === undefined) {
|
|
77
|
+
const measure = internals.measuresById.get(id);
|
|
78
|
+
let readiness;
|
|
79
|
+
if (stats.missing)
|
|
80
|
+
(readiness ??= []).push(stats.imputedZero ? "imputed" : "missing");
|
|
81
|
+
if (stats.nonFinite)
|
|
82
|
+
(readiness ??= []).push("non-finite");
|
|
83
|
+
if (stats.structural)
|
|
84
|
+
(readiness ??= []).push("structural-ambiguity");
|
|
85
|
+
if (stats.sum === null && !stats.nonFinite && !stats.structural)
|
|
86
|
+
(readiness ??= []).push("aggregation-overflow");
|
|
87
|
+
const quantity = {
|
|
88
|
+
kind: measure.kind,
|
|
89
|
+
unit: measure.unit,
|
|
90
|
+
};
|
|
91
|
+
if (measure.basisId)
|
|
92
|
+
quantity.basisId = measure.basisId;
|
|
93
|
+
if (measure.countPopulationId)
|
|
94
|
+
quantity.countPopulationId = measure.countPopulationId;
|
|
95
|
+
if (measure.exposureBasisId)
|
|
96
|
+
quantity.exposureBasisId = measure.exposureBasisId;
|
|
97
|
+
quantity.value = stats.value;
|
|
98
|
+
state = {
|
|
99
|
+
quantity: quantity,
|
|
77
100
|
stats,
|
|
78
|
-
readiness,
|
|
101
|
+
readiness: readiness ?? NO_REASONS,
|
|
79
102
|
// Cell expressions attach complete leaf-source evidence separately.
|
|
80
103
|
// Per-measure source lists here would be discarded and rebuilt.
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
};
|
|
105
|
+
cached += byStats.size;
|
|
106
|
+
if (cached < MAX_CACHED_STATES)
|
|
107
|
+
byStats.set(stats, state);
|
|
108
|
+
}
|
|
109
|
+
result[id] = state;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
84
112
|
}
|
|
85
113
|
function coordinate(cell) {
|
|
86
114
|
return {
|
|
@@ -118,16 +146,31 @@ function cellSourcesForSelection(cells, measureId) {
|
|
|
118
146
|
]));
|
|
119
147
|
}
|
|
120
148
|
/** Compact-only, invocation-owned caches. Never retain these on a prepared cell. */
|
|
121
|
-
function createCompactCellSourceReader() {
|
|
149
|
+
function createCompactCellSourceReader(cellCount) {
|
|
122
150
|
const pool = createDiagnosticReviewSourcePool();
|
|
123
151
|
const addCellSources = (target, cell, ids) => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
152
|
+
// Preparation cites one frozen list per source row across its measures;
|
|
153
|
+
// adding the same list again is a no-op, so skip it by identity.
|
|
154
|
+
let previous;
|
|
155
|
+
for (let position = 0; position < ids.length; position++) {
|
|
156
|
+
const id = ids[position];
|
|
157
|
+
const contributions = cell.contributions[id];
|
|
158
|
+
if (contributions !== undefined)
|
|
159
|
+
for (let index = 0; index < contributions.length; index++) {
|
|
160
|
+
const sources = contributions[index].sources;
|
|
161
|
+
if (sources === previous)
|
|
162
|
+
continue;
|
|
163
|
+
previous = sources;
|
|
164
|
+
for (let at = 0; at < sources.length; at++)
|
|
165
|
+
target.add(sources[at], true);
|
|
166
|
+
}
|
|
167
|
+
const blockers = cell.structuralBlockers[id];
|
|
168
|
+
if (blockers !== undefined)
|
|
169
|
+
for (let index = 0; index < blockers.length; index++) {
|
|
170
|
+
const sources = blockers[index].sources;
|
|
171
|
+
for (let at = 0; at < sources.length; at++)
|
|
172
|
+
target.add(sources[at], true);
|
|
173
|
+
}
|
|
131
174
|
}
|
|
132
175
|
};
|
|
133
176
|
const forSelection = (cells, measureId) => {
|
|
@@ -138,8 +181,30 @@ function createCompactCellSourceReader() {
|
|
|
138
181
|
return target.finish();
|
|
139
182
|
};
|
|
140
183
|
const dependencies = new WeakMap();
|
|
141
|
-
|
|
142
|
-
|
|
184
|
+
// Distinct dependency subsets are few (one per rule expression); each gets a
|
|
185
|
+
// small index, and equal subsets from different expressions share it.
|
|
186
|
+
const subsetIndexByKey = new Map();
|
|
187
|
+
const subsetIndexByIds = new Map();
|
|
188
|
+
// Invocation-scoped like the pool: cached lists live in flat arrays indexed
|
|
189
|
+
// by subset and cell position, released with this reader and never attached
|
|
190
|
+
// to a prepared cell.
|
|
191
|
+
const listsBySubset = [];
|
|
192
|
+
const subsetIndex = (ids) => {
|
|
193
|
+
let index = subsetIndexByIds.get(ids);
|
|
194
|
+
if (index === undefined) {
|
|
195
|
+
const key = canonicalJson(ids);
|
|
196
|
+
index = subsetIndexByKey.get(key);
|
|
197
|
+
if (index === undefined) {
|
|
198
|
+
index = listsBySubset.length;
|
|
199
|
+
listsBySubset.push(new Array(cellCount));
|
|
200
|
+
subsetIndexByKey.set(key, index);
|
|
201
|
+
}
|
|
202
|
+
subsetIndexByIds.set(ids, index);
|
|
203
|
+
}
|
|
204
|
+
return index;
|
|
205
|
+
};
|
|
206
|
+
// The one citation list most of a cell's subsets resolve to, and its owned list.
|
|
207
|
+
const cellSingletons = new Array(cellCount);
|
|
143
208
|
const measureIds = (expression) => {
|
|
144
209
|
let ids = dependencies.get(expression);
|
|
145
210
|
if (!ids) {
|
|
@@ -148,23 +213,50 @@ function createCompactCellSourceReader() {
|
|
|
148
213
|
}
|
|
149
214
|
return ids;
|
|
150
215
|
};
|
|
151
|
-
const forCell = (cell, ids) => {
|
|
152
|
-
|
|
153
|
-
|
|
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);
|
|
216
|
+
const forCell = (cell, cellIndex, ids) => {
|
|
217
|
+
const lists = listsBySubset[subsetIndex(ids)];
|
|
218
|
+
let sources = lists[cellIndex];
|
|
163
219
|
if (!sources) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
220
|
+
// Most subsets cite exactly one citation list of one prepared source
|
|
221
|
+
// (the cell's single row) and no blockers: resolve that list directly.
|
|
222
|
+
// Any other shape goes through the general collection.
|
|
223
|
+
let only;
|
|
224
|
+
for (let position = 0; position < ids.length && only !== null; position++) {
|
|
225
|
+
const id = ids[position];
|
|
226
|
+
const contributions = cell.contributions[id];
|
|
227
|
+
if (contributions !== undefined)
|
|
228
|
+
for (let index = 0; index < contributions.length; index++) {
|
|
229
|
+
const cited = contributions[index].sources;
|
|
230
|
+
if (only === undefined)
|
|
231
|
+
only = cited;
|
|
232
|
+
else if (only !== cited) {
|
|
233
|
+
only = null;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const blockers = cell.structuralBlockers[id];
|
|
238
|
+
if (blockers !== undefined && blockers.length > 0)
|
|
239
|
+
only = null;
|
|
240
|
+
}
|
|
241
|
+
if (only === undefined)
|
|
242
|
+
sources = pool.emptyList;
|
|
243
|
+
else if (only !== null && only.length === 1) {
|
|
244
|
+
const cached = cellSingletons[cellIndex];
|
|
245
|
+
if (cached !== undefined && cached.cited === only)
|
|
246
|
+
sources = cached.list;
|
|
247
|
+
else {
|
|
248
|
+
sources = pool.singletonForPrepared(only[0]);
|
|
249
|
+
cellSingletons[cellIndex] = { cited: only, list: sources };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (only !== null && only.length === 0)
|
|
253
|
+
sources = pool.emptyList;
|
|
254
|
+
else {
|
|
255
|
+
const target = pool.collection();
|
|
256
|
+
addCellSources(target, cell, ids);
|
|
257
|
+
sources = target.finish();
|
|
258
|
+
}
|
|
259
|
+
lists[cellIndex] = sources;
|
|
168
260
|
}
|
|
169
261
|
return sources;
|
|
170
262
|
};
|
|
@@ -179,34 +271,97 @@ function createCellExpressionEvaluator(prepared, compactSources) {
|
|
|
179
271
|
// Reuse projections across this review's rules, then release them before the
|
|
180
272
|
// receipt identity is constructed. Retaining a prepared cell must not retain
|
|
181
273
|
// this temporary bookkeeping or reuse another review's state.
|
|
182
|
-
const statesByCell = new
|
|
183
|
-
|
|
184
|
-
|
|
274
|
+
const statesByCell = new Array(prepared.cells.length);
|
|
275
|
+
const stateCache = new Map();
|
|
276
|
+
// Compact mode: the two operands of an evaluation are read and dropped, so
|
|
277
|
+
// one mutable operand per side is reused. The eager path keeps fresh objects.
|
|
278
|
+
const reusable = compactSources
|
|
279
|
+
? [
|
|
280
|
+
{ value: null, reasons: NO_REASONS, overflows: NO_REVIEW_OVERFLOWS, sources: NO_SOURCES },
|
|
281
|
+
{ value: null, reasons: NO_REASONS, overflows: NO_REVIEW_OVERFLOWS, sources: NO_SOURCES },
|
|
282
|
+
]
|
|
283
|
+
: undefined;
|
|
284
|
+
const operand = (slot, value, reasons, overflows, sources) => {
|
|
285
|
+
if (reusable === undefined || slot < 0)
|
|
286
|
+
return { value, reasons, overflows, sources };
|
|
287
|
+
const target = reusable[slot];
|
|
288
|
+
target.value = value;
|
|
289
|
+
target.reasons = reasons;
|
|
290
|
+
target.overflows = overflows;
|
|
291
|
+
target.sources = sources;
|
|
292
|
+
return target;
|
|
293
|
+
};
|
|
294
|
+
return (cell, cellIndex, expression, path, slot = -1) => {
|
|
295
|
+
let measures = statesByCell[cellIndex];
|
|
185
296
|
if (!measures) {
|
|
186
|
-
measures = states(prepared, cell);
|
|
187
|
-
statesByCell
|
|
297
|
+
measures = states(prepared, cell, stateCache);
|
|
298
|
+
statesByCell[cellIndex] = measures;
|
|
188
299
|
}
|
|
189
|
-
const result = evaluateDiagnosticMeasureExpression(expression, measures, path);
|
|
190
300
|
const sources = compactSources
|
|
191
|
-
? compactSources.forCell(cell, compactSources.measureIds(expression))
|
|
301
|
+
? compactSources.forCell(cell, cellIndex, compactSources.measureIds(expression))
|
|
192
302
|
: cellSources(cell, expressionMeasureIds(expression));
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
303
|
+
// A measure leaf is read straight from the projected state: the generic
|
|
304
|
+
// evaluator would build the same result and hand it back unchanged.
|
|
305
|
+
if (expression.op === "measure") {
|
|
306
|
+
const state = hasDiagnosticOwn(measures, expression.measureId)
|
|
307
|
+
? measures[expression.measureId]
|
|
308
|
+
: undefined;
|
|
309
|
+
if (state === undefined)
|
|
310
|
+
return operand(slot, null, ["missing"], NO_REVIEW_OVERFLOWS, sources);
|
|
311
|
+
const overflows = state.expressionOverflows;
|
|
312
|
+
return operand(slot, state.quantity.value, state.readiness, overflows === undefined || overflows.length === 0
|
|
313
|
+
? NO_REVIEW_OVERFLOWS
|
|
314
|
+
: overflows.map((overflow) => ({ ...overflow, coordinate: coordinate(cell), sources })), sources);
|
|
315
|
+
}
|
|
316
|
+
const result = evaluateDiagnosticMeasureExpression(expression, measures, path);
|
|
317
|
+
return operand(slot, result.value, result.reasons, result.overflows.length === 0
|
|
318
|
+
? NO_REVIEW_OVERFLOWS
|
|
319
|
+
: result.overflows.map((overflow) => ({
|
|
197
320
|
...overflow,
|
|
198
321
|
coordinate: coordinate(cell),
|
|
199
322
|
sources,
|
|
200
|
-
})),
|
|
201
|
-
sources,
|
|
202
|
-
};
|
|
323
|
+
})), sources);
|
|
203
324
|
};
|
|
204
325
|
}
|
|
205
326
|
function constant(value) {
|
|
206
327
|
return { value, reasons: [], overflows: [], sources: [] };
|
|
207
328
|
}
|
|
208
|
-
function
|
|
209
|
-
const
|
|
329
|
+
function createEvaluationScratch() {
|
|
330
|
+
const placeholder = {
|
|
331
|
+
sourceGroup: "",
|
|
332
|
+
origin: "",
|
|
333
|
+
valuation: "",
|
|
334
|
+
developmentAge: 0,
|
|
335
|
+
ageUnit: "",
|
|
336
|
+
};
|
|
337
|
+
return {
|
|
338
|
+
record: {
|
|
339
|
+
ruleId: "",
|
|
340
|
+
ruleKind: "compare",
|
|
341
|
+
severity: "warning",
|
|
342
|
+
scope: { kind: "cell", cell: placeholder, sources: NO_SOURCES },
|
|
343
|
+
status: "pass",
|
|
344
|
+
triggerReason: null,
|
|
345
|
+
left: null,
|
|
346
|
+
right: null,
|
|
347
|
+
relation: null,
|
|
348
|
+
notEvaluatedReasons: NO_REASONS,
|
|
349
|
+
expressionOverflows: NO_REVIEW_OVERFLOWS,
|
|
350
|
+
comparability: undefined,
|
|
351
|
+
},
|
|
352
|
+
cellScope: { kind: "cell", cell: placeholder, sources: NO_SOURCES },
|
|
353
|
+
pairScope: { kind: "valuation-pair", previous: placeholder, current: placeholder, sources: NO_SOURCES },
|
|
354
|
+
constants: new Map(),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
// One empty array serves both roles, exactly as the eager finalizer's interner
|
|
358
|
+
// would merge equal frozen empties into a single shared instance.
|
|
359
|
+
const freshEmptyLists = () => {
|
|
360
|
+
const empty = Object.freeze([]);
|
|
361
|
+
return { reasons: empty, overflows: empty };
|
|
362
|
+
};
|
|
363
|
+
function evaluation(prepared, rule, scope, left, right, passes, extraReasons = NO_REASONS, empty = freshEmptyLists(), scratch) {
|
|
364
|
+
const expressionOverflows = left.overflows.length === 0 && right.overflows.length === 0 ? [] : [
|
|
210
365
|
...new Map([...left.overflows, ...right.overflows].map((item) => [
|
|
211
366
|
canonicalJson(item),
|
|
212
367
|
item,
|
|
@@ -235,74 +390,86 @@ function evaluation(prepared, rule, scope, left, right, passes, extraReasons = [
|
|
|
235
390
|
}
|
|
236
391
|
return a.sources.length - b.sources.length;
|
|
237
392
|
});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
393
|
+
// Clean evaluated operands carry no reasons; skip the spread/filter round trip.
|
|
394
|
+
const reasons = left.reasons.length === 0 &&
|
|
395
|
+
right.reasons.length === 0 &&
|
|
396
|
+
extraReasons.length === 0 &&
|
|
397
|
+
expressionOverflows.length === 0 &&
|
|
398
|
+
left.value !== null &&
|
|
399
|
+
right.value !== null
|
|
400
|
+
? []
|
|
401
|
+
: uniqueReasons([
|
|
402
|
+
...left.reasons,
|
|
403
|
+
...right.reasons,
|
|
404
|
+
...extraReasons,
|
|
405
|
+
...(expressionOverflows.length > 0 ? ["expression-overflow"] : []),
|
|
406
|
+
...((left.value === null || right.value === null) &&
|
|
407
|
+
left.reasons.length === 0 &&
|
|
408
|
+
right.reasons.length === 0 &&
|
|
409
|
+
expressionOverflows.length === 0
|
|
410
|
+
? ["missing"]
|
|
411
|
+
: []),
|
|
412
|
+
]);
|
|
413
|
+
if (reasons.length > 0)
|
|
414
|
+
return evaluationRecord(rule, scope, left, right, rule.missingInput === "finding" ? "triggered" : "not-evaluated", rule.missingInput === "finding" ? missingTriggerReason(reasons) : null, null, reasons, expressionOverflows, scratch);
|
|
415
|
+
const compared = classifyDiagnosticComparison(left.value, right.value, rule.tolerance);
|
|
416
|
+
// Empty reason and overflow lists are this review's own immutable values;
|
|
417
|
+
// the eager finalizer leaves frozen children as they are and readers only
|
|
418
|
+
// iterate them, and no other invocation ever sees the same arrays.
|
|
419
|
+
if (compared.status === "not-evaluated")
|
|
420
|
+
return evaluationRecord(rule, scope, left, right, rule.missingInput === "finding" ? "triggered" : "not-evaluated", rule.missingInput === "finding"
|
|
421
|
+
? compared.reason === "tolerance-overflow"
|
|
422
|
+
? "tolerance-overflow"
|
|
423
|
+
: "missing-input"
|
|
424
|
+
: null, null, [compared.reason], empty.overflows, scratch);
|
|
425
|
+
const pass = passes(compared.relation);
|
|
426
|
+
return evaluationRecord(rule, scope, left, right, pass ? "pass" : "triggered", pass ? null : "predicate", compared.relation, empty.reasons, empty.overflows, scratch);
|
|
427
|
+
}
|
|
428
|
+
function missingTriggerReason(reasons) {
|
|
429
|
+
if (reasons.some((reason) => ["missing", "imputed", "non-finite", "structural-ambiguity"].includes(reason)))
|
|
259
430
|
return "missing-input";
|
|
260
|
-
|
|
261
|
-
|
|
431
|
+
if (reasons.includes("aggregation-overflow"))
|
|
432
|
+
return "aggregation-overflow";
|
|
433
|
+
if (reasons.includes("expression-overflow"))
|
|
434
|
+
return "expression-overflow";
|
|
435
|
+
if (reasons.includes("tolerance-overflow"))
|
|
436
|
+
return "tolerance-overflow";
|
|
437
|
+
return "missing-input";
|
|
438
|
+
}
|
|
439
|
+
/** One literal in the contract's key order; no intermediate spread objects. */
|
|
440
|
+
function evaluationRecord(rule, scope, left, right, status, triggerReason, relation, notEvaluatedReasons, overflows, scratch) {
|
|
441
|
+
if (scratch !== undefined) {
|
|
442
|
+
const target = scratch.record;
|
|
443
|
+
target.ruleId = rule.id;
|
|
444
|
+
target.ruleKind = rule.kind;
|
|
445
|
+
target.severity = rule.severity;
|
|
446
|
+
target.scope = scope;
|
|
447
|
+
target.status = status;
|
|
448
|
+
target.triggerReason = triggerReason;
|
|
449
|
+
target.left = left.value;
|
|
450
|
+
target.right = right.value;
|
|
451
|
+
target.relation = relation;
|
|
452
|
+
target.notEvaluatedReasons = notEvaluatedReasons;
|
|
453
|
+
target.expressionOverflows = overflows;
|
|
454
|
+
target.comparability = rule.kind === "layer-order" ? rule.comparability : undefined;
|
|
455
|
+
return target;
|
|
456
|
+
}
|
|
457
|
+
const value = {
|
|
262
458
|
ruleId: rule.id,
|
|
263
459
|
ruleKind: rule.kind,
|
|
264
460
|
severity: rule.severity,
|
|
265
461
|
scope,
|
|
266
|
-
|
|
267
|
-
|
|
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);
|
|
282
|
-
if (compared.status === "not-evaluated")
|
|
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",
|
|
462
|
+
status,
|
|
463
|
+
triggerReason,
|
|
300
464
|
left: left.value,
|
|
301
465
|
right: right.value,
|
|
302
|
-
relation
|
|
303
|
-
notEvaluatedReasons
|
|
304
|
-
expressionOverflows:
|
|
305
|
-
}
|
|
466
|
+
relation,
|
|
467
|
+
notEvaluatedReasons,
|
|
468
|
+
expressionOverflows: overflows,
|
|
469
|
+
};
|
|
470
|
+
return (rule.kind === "layer-order"
|
|
471
|
+
? { ...value, comparability: rule.comparability }
|
|
472
|
+
: value);
|
|
306
473
|
}
|
|
307
474
|
function filterCells(prepared, filter) {
|
|
308
475
|
if (!filter)
|
|
@@ -407,7 +574,7 @@ export function evaluateDiagnosticReviewRulesCompact(prepared) {
|
|
|
407
574
|
const invariant = parent
|
|
408
575
|
? compactEvaluationCache.get(parent) ?? evaluateDiagnosticReviewRulesCompact(parent)
|
|
409
576
|
: undefined;
|
|
410
|
-
const compactSources = createCompactCellSourceReader();
|
|
577
|
+
const compactSources = createCompactCellSourceReader(prepared.cells.length);
|
|
411
578
|
const builder = createCompactReviewBuilder(prepared.definition.definition.reviewRules, compactSources);
|
|
412
579
|
visitDiagnosticReviewRules(prepared, builder.append, compactSources, invariant !== undefined, coordinateParent !== undefined);
|
|
413
580
|
const evaluated = builder.finish();
|
|
@@ -418,17 +585,45 @@ export function evaluateDiagnosticReviewRulesCompact(prepared) {
|
|
|
418
585
|
/** One traversal and one formula implementation serve both storage contracts. */
|
|
419
586
|
function visitDiagnosticReviewRules(prepared, appendEvaluation, compactSources, controlsOnly = false, includeMonotonic = false) {
|
|
420
587
|
const evaluateExpression = createCellExpressionEvaluator(prepared, compactSources);
|
|
421
|
-
const
|
|
588
|
+
const ownedEmpty = freshEmptyLists();
|
|
589
|
+
// Only the compact builder, which copies what it keeps, receives reused records.
|
|
590
|
+
const scratch = compactSources === undefined ? undefined : createEvaluationScratch();
|
|
591
|
+
const constantOperand = (value) => {
|
|
592
|
+
if (scratch === undefined)
|
|
593
|
+
return constant(value);
|
|
594
|
+
let operand = scratch.constants.get(value);
|
|
595
|
+
if (operand === undefined) {
|
|
596
|
+
operand = { value, reasons: NO_REASONS, overflows: NO_REVIEW_OVERFLOWS, sources: NO_SOURCES };
|
|
597
|
+
scratch.constants.set(value, operand);
|
|
598
|
+
}
|
|
599
|
+
return operand;
|
|
600
|
+
};
|
|
601
|
+
const cells = prepared.cells;
|
|
602
|
+
// Cell positions, not cell objects, key every per-cell cache in this walk.
|
|
603
|
+
const cellIndexByCoordinate = new Map();
|
|
422
604
|
const expectedSourcesByCoordinate = new Map();
|
|
423
|
-
const coordinateKey = (sourceGroup, origin, valuation) =>
|
|
605
|
+
const coordinateKey = (sourceGroup, origin, valuation) => canonicalScalarArrayJson([sourceGroup, origin, valuation]);
|
|
606
|
+
// One review coordinate per cell; every rule's scope for that cell cites it.
|
|
607
|
+
const coordinates = new Array(cells.length);
|
|
608
|
+
const coordinateOf = (cell, cellIndex) => {
|
|
609
|
+
let value = coordinates[cellIndex];
|
|
610
|
+
if (!value) {
|
|
611
|
+
value = coordinate(cell);
|
|
612
|
+
coordinates[cellIndex] = value;
|
|
613
|
+
}
|
|
614
|
+
return value;
|
|
615
|
+
};
|
|
616
|
+
// Cells and expected-cell sources are resolved once per group; every
|
|
617
|
+
// monotonic rule then walks the same ordered valuations by position.
|
|
424
618
|
const monotonicGroups = [];
|
|
425
619
|
// Keep the original traversal and source order; only replace repeated searches.
|
|
426
620
|
// Other rule families do not need these indices.
|
|
427
621
|
if ((!controlsOnly || includeMonotonic) && prepared.definition.definition.reviewRules.some((rule) => rule.kind === "monotonic")) {
|
|
428
|
-
for (
|
|
622
|
+
for (let index = 0; index < cells.length; index++) {
|
|
623
|
+
const cell = cells[index];
|
|
429
624
|
const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
|
|
430
|
-
if (!
|
|
431
|
-
|
|
625
|
+
if (!cellIndexByCoordinate.has(key))
|
|
626
|
+
cellIndexByCoordinate.set(key, index);
|
|
432
627
|
}
|
|
433
628
|
for (const cell of prepared.expectedCells) {
|
|
434
629
|
if (!cell.source)
|
|
@@ -442,7 +637,7 @@ function visitDiagnosticReviewRules(prepared, appendEvaluation, compactSources,
|
|
|
442
637
|
// the rule expression. Build them once for all monotonic rules in this run.
|
|
443
638
|
const groups = new Map();
|
|
444
639
|
for (const cell of [...prepared.cells, ...prepared.expectedCells]) {
|
|
445
|
-
const key =
|
|
640
|
+
const key = canonicalScalarArrayJson([cell.sourceGroup, cell.origin]);
|
|
446
641
|
const values = groups.get(key) ?? new Set();
|
|
447
642
|
values.add(cell.valuation);
|
|
448
643
|
groups.set(key, values);
|
|
@@ -459,7 +654,13 @@ function visitDiagnosticReviewRules(prepared, appendEvaluation, compactSources,
|
|
|
459
654
|
for (const [key, values] of [...groups.entries()].sort(([left], [right]) => codeUnit(left, right))) {
|
|
460
655
|
const [sourceGroup, origin] = JSON.parse(key);
|
|
461
656
|
const valuations = [...values].sort((left, right) => valuationCoordinate(left) - valuationCoordinate(right) || codeUnit(left, right));
|
|
462
|
-
monotonicGroups.push({
|
|
657
|
+
monotonicGroups.push({
|
|
658
|
+
sourceGroup,
|
|
659
|
+
origin,
|
|
660
|
+
valuations,
|
|
661
|
+
cellIndices: valuations.map((valuation) => cellIndexByCoordinate.get(coordinateKey(sourceGroup, origin, valuation))),
|
|
662
|
+
expected: valuations.map((valuation) => expectedSourcesByCoordinate.get(coordinateKey(sourceGroup, origin, valuation)) ?? NO_SOURCES),
|
|
663
|
+
});
|
|
463
664
|
}
|
|
464
665
|
}
|
|
465
666
|
for (const [ruleIndex, rule] of prepared.definition.definition
|
|
@@ -563,52 +764,64 @@ function visitDiagnosticReviewRules(prepared, appendEvaluation, compactSources,
|
|
|
563
764
|
measureIds.reduce((count, measureId) => count + (cell.contributions[measureId]?.length ?? 0), 0), 0),
|
|
564
765
|
sources,
|
|
565
766
|
};
|
|
566
|
-
appendEvaluation(evaluation(prepared, rule, scope, left, constant(rule.expected), (relation) => relation === "equal"));
|
|
767
|
+
appendEvaluation(evaluation(prepared, rule, scope, left, constant(rule.expected), (relation) => relation === "equal", NO_REASONS, ownedEmpty));
|
|
567
768
|
continue;
|
|
568
769
|
}
|
|
569
770
|
if (rule.kind === "monotonic") {
|
|
570
|
-
|
|
771
|
+
const expressionPath = `${basePath}/expression`;
|
|
772
|
+
const monotonicPasses = (relation) => rule.direction === "nondecreasing" ? relation !== "greater" : relation !== "less";
|
|
773
|
+
for (const { sourceGroup, origin, valuations: sorted, cellIndices, expected } of monotonicGroups) {
|
|
571
774
|
for (let index = 1; index < sorted.length; index++) {
|
|
572
775
|
const previousValuation = sorted[index - 1];
|
|
573
776
|
const currentValuation = sorted[index];
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
const
|
|
777
|
+
const previousIndex = cellIndices[index - 1];
|
|
778
|
+
const currentIndex = cellIndices[index];
|
|
779
|
+
const previous = previousIndex === undefined ? undefined : cells[previousIndex];
|
|
780
|
+
const current = currentIndex === undefined ? undefined : cells[currentIndex];
|
|
577
781
|
const left = previous
|
|
578
|
-
? evaluateExpression(previous, rule.expression,
|
|
782
|
+
? evaluateExpression(previous, previousIndex, rule.expression, expressionPath, 0)
|
|
579
783
|
: {
|
|
580
784
|
value: null,
|
|
581
785
|
reasons: ["missing"],
|
|
582
786
|
overflows: [],
|
|
583
787
|
sources: compactSources
|
|
584
|
-
? compactSources.forPreparedSources(
|
|
585
|
-
: uniqueSources(
|
|
788
|
+
? compactSources.forPreparedSources(expected[index - 1])
|
|
789
|
+
: uniqueSources(expected[index - 1]),
|
|
586
790
|
};
|
|
587
791
|
const right = current
|
|
588
|
-
? evaluateExpression(current, rule.expression,
|
|
792
|
+
? evaluateExpression(current, currentIndex, rule.expression, expressionPath, 1)
|
|
589
793
|
: {
|
|
590
794
|
value: null,
|
|
591
795
|
reasons: ["missing"],
|
|
592
796
|
overflows: [],
|
|
593
797
|
sources: compactSources
|
|
594
|
-
? compactSources.forPreparedSources(
|
|
595
|
-
: uniqueSources(
|
|
798
|
+
? compactSources.forPreparedSources(expected[index])
|
|
799
|
+
: uniqueSources(expected[index]),
|
|
596
800
|
};
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
801
|
+
const previousCoordinate = previous
|
|
802
|
+
? coordinateOf(previous, previousIndex)
|
|
803
|
+
: coordinateFromLabels(prepared, sourceGroup, origin, previousValuation);
|
|
804
|
+
const currentCoordinate = current
|
|
805
|
+
? coordinateOf(current, currentIndex)
|
|
806
|
+
: coordinateFromLabels(prepared, sourceGroup, origin, currentValuation);
|
|
807
|
+
const pairSources = compactSources
|
|
808
|
+
? compactSources.union([left.sources, right.sources])
|
|
809
|
+
: uniqueSources([...left.sources, ...right.sources]);
|
|
810
|
+
let scope;
|
|
811
|
+
if (scratch !== undefined) {
|
|
812
|
+
scratch.pairScope.previous = previousCoordinate;
|
|
813
|
+
scratch.pairScope.current = currentCoordinate;
|
|
814
|
+
scratch.pairScope.sources = pairSources;
|
|
815
|
+
scope = scratch.pairScope;
|
|
816
|
+
}
|
|
817
|
+
else
|
|
818
|
+
scope = {
|
|
819
|
+
kind: "valuation-pair",
|
|
820
|
+
previous: previousCoordinate,
|
|
821
|
+
current: currentCoordinate,
|
|
822
|
+
sources: pairSources,
|
|
823
|
+
};
|
|
824
|
+
appendEvaluation(evaluation(prepared, rule, scope, left, right, monotonicPasses, NO_REASONS, ownedEmpty, scratch));
|
|
612
825
|
}
|
|
613
826
|
}
|
|
614
827
|
continue;
|
|
@@ -626,33 +839,53 @@ function visitDiagnosticReviewRules(prepared, appendEvaluation, compactSources,
|
|
|
626
839
|
: compactSources.measureIds(expression))),
|
|
627
840
|
].sort(codeUnit))
|
|
628
841
|
: undefined;
|
|
629
|
-
|
|
630
|
-
|
|
842
|
+
// Operand paths depend only on the rule; build them once, not per cell.
|
|
843
|
+
const leftPath = `${basePath}/when/left`;
|
|
844
|
+
const rightPath = `${basePath}/when/right`;
|
|
845
|
+
const actualPath = `${basePath}/actual`;
|
|
846
|
+
const expectedPath = `${basePath}/expected`;
|
|
847
|
+
const narrowerPath = `${basePath}/narrower`;
|
|
848
|
+
const broaderPath = `${basePath}/broader`;
|
|
849
|
+
// Per-rule helpers: created once, not once per cell.
|
|
850
|
+
const scopeFor = (cell, cellIndex, leftSources, rightSources) => {
|
|
851
|
+
if (scratch !== undefined) {
|
|
852
|
+
scratch.cellScope.cell = coordinateOf(cell, cellIndex);
|
|
853
|
+
scratch.cellScope.sources = compactSources.forCell(cell, cellIndex, scopeDependencies);
|
|
854
|
+
return scratch.cellScope;
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
631
857
|
kind: "cell",
|
|
632
|
-
cell:
|
|
858
|
+
cell: coordinateOf(cell, cellIndex),
|
|
633
859
|
sources: compactSources
|
|
634
|
-
? compactSources.forCell(cell, scopeDependencies)
|
|
860
|
+
? compactSources.forCell(cell, cellIndex, scopeDependencies)
|
|
635
861
|
: uniqueSources([...leftSources, ...rightSources]),
|
|
636
|
-
}
|
|
862
|
+
};
|
|
863
|
+
};
|
|
864
|
+
const comparePasses = (relation) => rule.kind === "compare" && !diagnosticPredicateMatches(rule.when.operator, relation);
|
|
865
|
+
const reconcilePasses = (relation) => relation === "equal";
|
|
866
|
+
const layerPasses = (relation) => relation !== "greater";
|
|
867
|
+
for (let index = 0; index < cells.length; index++) {
|
|
868
|
+
const cell = cells[index];
|
|
637
869
|
if (rule.kind === "compare") {
|
|
638
|
-
const
|
|
639
|
-
?
|
|
640
|
-
: evaluateExpression(cell,
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
870
|
+
const left = rule.when.left.op === "constant"
|
|
871
|
+
? constantOperand(rule.when.left.value)
|
|
872
|
+
: evaluateExpression(cell, index, rule.when.left, leftPath, 0);
|
|
873
|
+
const right = rule.when.right.op === "constant"
|
|
874
|
+
? constantOperand(rule.when.right.value)
|
|
875
|
+
: evaluateExpression(cell, index, rule.when.right, rightPath, 1);
|
|
876
|
+
appendEvaluation(evaluation(prepared, rule, scopeFor(cell, index, left.sources, right.sources), left, right, comparePasses, NO_REASONS, ownedEmpty, scratch));
|
|
644
877
|
}
|
|
645
878
|
else if (rule.kind === "reconcile") {
|
|
646
|
-
const left = evaluateExpression(cell, rule.actual,
|
|
879
|
+
const left = evaluateExpression(cell, index, rule.actual, actualPath, 0);
|
|
647
880
|
const right = rule.expected.op === "constant"
|
|
648
|
-
?
|
|
649
|
-
: evaluateExpression(cell, rule.expected,
|
|
650
|
-
appendEvaluation(evaluation(prepared, rule, scopeFor(left.sources, right.sources), left, right,
|
|
881
|
+
? constantOperand(rule.expected.value)
|
|
882
|
+
: evaluateExpression(cell, index, rule.expected, expectedPath, 1);
|
|
883
|
+
appendEvaluation(evaluation(prepared, rule, scopeFor(cell, index, left.sources, right.sources), left, right, reconcilePasses, NO_REASONS, ownedEmpty, scratch));
|
|
651
884
|
}
|
|
652
885
|
else {
|
|
653
|
-
const left = evaluateExpression(cell, rule.narrower,
|
|
654
|
-
const right = evaluateExpression(cell, rule.broader,
|
|
655
|
-
appendEvaluation(evaluation(prepared, rule, scopeFor(left.sources, right.sources), left, right,
|
|
886
|
+
const left = evaluateExpression(cell, index, rule.narrower, narrowerPath, 0);
|
|
887
|
+
const right = evaluateExpression(cell, index, rule.broader, broaderPath, 1);
|
|
888
|
+
appendEvaluation(evaluation(prepared, rule, scopeFor(cell, index, left.sources, right.sources), left, right, layerPasses, NO_REASONS, ownedEmpty, scratch));
|
|
656
889
|
}
|
|
657
890
|
}
|
|
658
891
|
}
|