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