@actuarial-ts/core 0.6.1 → 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.
- package/README.md +11 -2
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +136 -26
- package/dist/canonical.js.map +1 -1
- package/dist/diagnosticEvidenceIntern.d.ts +16 -0
- package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
- package/dist/diagnosticEvidenceIntern.js +139 -0
- package/dist/diagnosticEvidenceIntern.js.map +1 -0
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +97 -7
- package/dist/diagnosticExposure.js.map +1 -1
- package/dist/diagnosticIdentity.d.ts.map +1 -1
- package/dist/diagnosticIdentity.js +6 -3
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticIdentityStream.d.ts +56 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -0
- package/dist/diagnosticIdentityStream.js +472 -0
- package/dist/diagnosticIdentityStream.js.map +1 -0
- package/dist/diagnosticPreparation.d.ts +28 -0
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +160 -36
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +4 -1
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +203 -51
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +18 -0
- package/dist/diagnosticReviewSources.d.ts.map +1 -0
- package/dist/diagnosticReviewSources.js +76 -0
- package/dist/diagnosticReviewSources.js.map +1 -0
- package/dist/diagnosticReviewStore.d.ts +88 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -0
- package/dist/diagnosticReviewStore.js +455 -0
- package/dist/diagnosticReviewStore.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +25 -1
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +115 -7
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/fnvAccumulator.d.ts +6 -0
- package/dist/fnvAccumulator.d.ts.map +1 -0
- package/dist/fnvAccumulator.js +21 -0
- package/dist/fnvAccumulator.js.map +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.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 +144 -26
- package/src/diagnosticEvidenceIntern.ts +151 -0
- package/src/diagnosticExposure.ts +126 -17
- package/src/diagnosticIdentity.ts +15 -6
- package/src/diagnosticIdentityStream.ts +553 -0
- package/src/diagnosticPreparation.ts +268 -84
- package/src/diagnosticReview.ts +293 -85
- package/src/diagnosticReviewSources.ts +85 -0
- package/src/diagnosticReviewStore.ts +692 -0
- package/src/diagnosticRunner.ts +206 -9
- package/src/fnvAccumulator.ts +20 -0
- package/src/index.ts +38 -1
- package/src/version.ts +1 -1
package/src/diagnosticReview.ts
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from "./diagnosticDefinitions.js";
|
|
9
9
|
import type { NormalizedDiagnosticReviewFilterIdentity } from "./diagnosticIdentity.js";
|
|
10
10
|
import { canonicalJson } from "./canonical.js";
|
|
11
|
+
import { createDiagnosticReviewSourcePool } from "./diagnosticReviewSources.js";
|
|
12
|
+
import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
|
|
11
13
|
import { normalizeDiagnosticPeriod } from "./diagnosticPeriods.js";
|
|
12
14
|
import {
|
|
13
15
|
evaluateDiagnosticMeasureExpression,
|
|
@@ -15,8 +17,15 @@ import {
|
|
|
15
17
|
} from "./diagnosticFormulas.js";
|
|
16
18
|
import {
|
|
17
19
|
assertPreparedDiagnosticData,
|
|
20
|
+
assertCompactPreparedDiagnosticData,
|
|
21
|
+
type CompactPreparedDiagnosticData,
|
|
22
|
+
type PreparedDiagnosticDataContent,
|
|
18
23
|
type PreparedDiagnosticData,
|
|
19
24
|
} from "./diagnosticPreparation.js";
|
|
25
|
+
import {
|
|
26
|
+
createCompactReviewBuilder,
|
|
27
|
+
type CompactDiagnosticReviewEvaluations,
|
|
28
|
+
} from "./diagnosticReviewStore.js";
|
|
20
29
|
import {
|
|
21
30
|
classifyDiagnosticComparison,
|
|
22
31
|
diagnosticPredicateMatches,
|
|
@@ -173,7 +182,7 @@ function expressionLeafPaths(
|
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
function states(
|
|
176
|
-
prepared:
|
|
185
|
+
prepared: PreparedDiagnosticDataContent,
|
|
177
186
|
cell: PreparedDiagnosticData["cells"][number],
|
|
178
187
|
): Record<string, FinalizedDiagnosticMeasure> {
|
|
179
188
|
const internals = getCompiledDiagnosticDefinitionInternals(
|
|
@@ -206,7 +215,8 @@ function states(
|
|
|
206
215
|
},
|
|
207
216
|
stats,
|
|
208
217
|
readiness,
|
|
209
|
-
|
|
218
|
+
// Cell expressions attach complete leaf-source evidence separately.
|
|
219
|
+
// Per-measure source lists here would be discarded and rebuilt.
|
|
210
220
|
},
|
|
211
221
|
];
|
|
212
222
|
}),
|
|
@@ -226,7 +236,7 @@ function coordinate(
|
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
function coordinateFromLabels(
|
|
229
|
-
prepared:
|
|
239
|
+
prepared: PreparedDiagnosticDataContent,
|
|
230
240
|
sourceGroup: string,
|
|
231
241
|
originLabel: string,
|
|
232
242
|
valuationLabel: string,
|
|
@@ -286,27 +296,116 @@ function cellSourcesForSelection(
|
|
|
286
296
|
);
|
|
287
297
|
}
|
|
288
298
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
299
|
+
/** Compact-only, invocation-owned caches. Never retain these on a prepared cell. */
|
|
300
|
+
function createCompactCellSourceReader() {
|
|
301
|
+
const pool = createDiagnosticReviewSourcePool();
|
|
302
|
+
const addCellSources = (
|
|
303
|
+
target: ReturnType<typeof pool.collection>,
|
|
304
|
+
cell: PreparedDiagnosticData["cells"][number],
|
|
305
|
+
ids: readonly string[],
|
|
306
|
+
): void => {
|
|
307
|
+
for (const id of ids) {
|
|
308
|
+
for (const contribution of cell.contributions[id] ?? [])
|
|
309
|
+
for (const source of contribution.sources) target.add(source, true);
|
|
310
|
+
for (const blocker of cell.structuralBlockers[id] ?? [])
|
|
311
|
+
for (const source of blocker.sources) target.add(source, true);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const forSelection = (
|
|
315
|
+
cells: readonly PreparedDiagnosticData["cells"][number][],
|
|
316
|
+
measureId: string,
|
|
317
|
+
): readonly DiagnosticSourceLocation[] => {
|
|
318
|
+
const target = pool.collection();
|
|
319
|
+
const ids = [measureId];
|
|
320
|
+
for (const cell of cells) addCellSources(target, cell, ids);
|
|
321
|
+
return target.finish();
|
|
322
|
+
};
|
|
323
|
+
const dependencies = new WeakMap<object, readonly string[]>();
|
|
324
|
+
const dependencyKeys = new WeakMap<object, string>();
|
|
325
|
+
const byCell = new WeakMap<
|
|
326
|
+
PreparedDiagnosticData["cells"][number],
|
|
327
|
+
Map<string, readonly DiagnosticSourceLocation[]>
|
|
328
|
+
>();
|
|
329
|
+
const measureIds = (
|
|
330
|
+
expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
|
|
331
|
+
): readonly string[] => {
|
|
332
|
+
let ids = dependencies.get(expression);
|
|
333
|
+
if (!ids) {
|
|
334
|
+
ids = Object.freeze(expressionMeasureIds(expression));
|
|
335
|
+
dependencies.set(expression, ids);
|
|
336
|
+
}
|
|
337
|
+
return ids;
|
|
338
|
+
};
|
|
339
|
+
const forCell = (
|
|
340
|
+
cell: PreparedDiagnosticData["cells"][number],
|
|
341
|
+
ids: readonly string[],
|
|
342
|
+
): readonly DiagnosticSourceLocation[] => {
|
|
343
|
+
let key = dependencyKeys.get(ids);
|
|
344
|
+
if (key === undefined) {
|
|
345
|
+
key = canonicalJson(ids);
|
|
346
|
+
dependencyKeys.set(ids, key);
|
|
347
|
+
}
|
|
348
|
+
let subsets = byCell.get(cell);
|
|
349
|
+
if (!subsets) {
|
|
350
|
+
subsets = new Map();
|
|
351
|
+
byCell.set(cell, subsets);
|
|
352
|
+
}
|
|
353
|
+
let sources = subsets.get(key);
|
|
354
|
+
if (!sources) {
|
|
355
|
+
const target = pool.collection();
|
|
356
|
+
addCellSources(target, cell, ids);
|
|
357
|
+
sources = target.finish();
|
|
358
|
+
subsets.set(key, sources);
|
|
359
|
+
}
|
|
360
|
+
return sources;
|
|
361
|
+
};
|
|
301
362
|
return {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
363
|
+
...pool,
|
|
364
|
+
measureIds,
|
|
365
|
+
forCell,
|
|
366
|
+
forSelection,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function createCellExpressionEvaluator(
|
|
371
|
+
prepared: PreparedDiagnosticDataContent,
|
|
372
|
+
compactSources?: ReturnType<typeof createCompactCellSourceReader>,
|
|
373
|
+
) {
|
|
374
|
+
// Reuse projections across this review's rules, then release them before the
|
|
375
|
+
// receipt identity is constructed. Retaining a prepared cell must not retain
|
|
376
|
+
// this temporary bookkeeping or reuse another review's state.
|
|
377
|
+
const statesByCell = new WeakMap<
|
|
378
|
+
PreparedDiagnosticData["cells"][number],
|
|
379
|
+
Record<string, FinalizedDiagnosticMeasure>
|
|
380
|
+
>();
|
|
381
|
+
return (
|
|
382
|
+
cell: PreparedDiagnosticData["cells"][number],
|
|
383
|
+
expression: import("./diagnosticExpressions.js").DiagnosticMeasureExpression,
|
|
384
|
+
path: string,
|
|
385
|
+
): EvaluatedOperand => {
|
|
386
|
+
let measures = statesByCell.get(cell);
|
|
387
|
+
if (!measures) {
|
|
388
|
+
measures = states(prepared, cell);
|
|
389
|
+
statesByCell.set(cell, measures);
|
|
390
|
+
}
|
|
391
|
+
const result = evaluateDiagnosticMeasureExpression(
|
|
392
|
+
expression,
|
|
393
|
+
measures,
|
|
394
|
+
path,
|
|
395
|
+
);
|
|
396
|
+
const sources = compactSources
|
|
397
|
+
? compactSources.forCell(cell, compactSources.measureIds(expression))
|
|
398
|
+
: cellSources(cell, expressionMeasureIds(expression));
|
|
399
|
+
return {
|
|
400
|
+
value: result.value,
|
|
401
|
+
reasons: result.reasons,
|
|
402
|
+
overflows: result.overflows.map((overflow) => ({
|
|
403
|
+
...overflow,
|
|
404
|
+
coordinate: coordinate(cell),
|
|
405
|
+
sources,
|
|
406
|
+
})),
|
|
307
407
|
sources,
|
|
308
|
-
}
|
|
309
|
-
sources,
|
|
408
|
+
};
|
|
310
409
|
};
|
|
311
410
|
}
|
|
312
411
|
|
|
@@ -315,7 +414,7 @@ function constant(value: number): EvaluatedOperand {
|
|
|
315
414
|
}
|
|
316
415
|
|
|
317
416
|
function evaluation(
|
|
318
|
-
prepared:
|
|
417
|
+
prepared: PreparedDiagnosticDataContent,
|
|
319
418
|
rule: DiagnosticReviewRule,
|
|
320
419
|
scope: DiagnosticReviewEvaluationScope,
|
|
321
420
|
left: EvaluatedOperand,
|
|
@@ -458,7 +557,7 @@ function evaluation(
|
|
|
458
557
|
}
|
|
459
558
|
|
|
460
559
|
function filterCells(
|
|
461
|
-
prepared:
|
|
560
|
+
prepared: PreparedDiagnosticDataContent,
|
|
462
561
|
filter: DiagnosticReviewFilter | null | undefined,
|
|
463
562
|
): readonly PreparedDiagnosticData["cells"][number][] {
|
|
464
563
|
if (!filter) return prepared.cells;
|
|
@@ -524,7 +623,7 @@ function filterCells(
|
|
|
524
623
|
}
|
|
525
624
|
|
|
526
625
|
function projectedCells(
|
|
527
|
-
prepared:
|
|
626
|
+
prepared: PreparedDiagnosticDataContent,
|
|
528
627
|
rule: Extract<DiagnosticReviewRule, { kind: "control-total" }>,
|
|
529
628
|
): readonly PreparedDiagnosticData["cells"][number][] {
|
|
530
629
|
const selected = filterCells(prepared, rule.filter);
|
|
@@ -551,12 +650,116 @@ function projectedCells(
|
|
|
551
650
|
);
|
|
552
651
|
}
|
|
553
652
|
|
|
653
|
+
/**
|
|
654
|
+
* Finish each evaluation before retaining it. All mutable descendants here
|
|
655
|
+
* were constructed by this review; frozen descendants belong to the compiled
|
|
656
|
+
* definition or an earlier finalized child and must retain their identity.
|
|
657
|
+
* The pool is local to one review and never sees prepared cells or SDK brands.
|
|
658
|
+
*/
|
|
659
|
+
function createEvaluationFinalizer(): (
|
|
660
|
+
value: DiagnosticReviewRuleEvaluation,
|
|
661
|
+
) => DiagnosticReviewRuleEvaluation {
|
|
662
|
+
const sharing = createDiagnosticEvidenceInterner();
|
|
663
|
+
const finalizeChild = (value: unknown, sourceSlot = false): unknown => {
|
|
664
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value))
|
|
665
|
+
return value;
|
|
666
|
+
const array = Array.isArray(value);
|
|
667
|
+
for (const key of Object.keys(value)) {
|
|
668
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
|
669
|
+
const child = finalizeChild(
|
|
670
|
+
descriptor.value,
|
|
671
|
+
array ? sourceSlot : key === "source" || key === "sources",
|
|
672
|
+
);
|
|
673
|
+
if (child !== descriptor.value)
|
|
674
|
+
Object.defineProperty(value, key, { ...descriptor, value: child });
|
|
675
|
+
}
|
|
676
|
+
return sharing.internOwned(
|
|
677
|
+
Object.freeze(value),
|
|
678
|
+
sourceSlot ? "source" : "plain",
|
|
679
|
+
);
|
|
680
|
+
};
|
|
681
|
+
return (value) => {
|
|
682
|
+
// Keep every evaluation as its original, distinct record. Only its owned
|
|
683
|
+
// children may share storage; no evaluation or array entry is removed.
|
|
684
|
+
for (const key of ["scope", "notEvaluatedReasons", "expressionOverflows"]) {
|
|
685
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
|
686
|
+
const child = finalizeChild(descriptor.value);
|
|
687
|
+
if (child !== descriptor.value)
|
|
688
|
+
Object.defineProperty(value, key, { ...descriptor, value: child });
|
|
689
|
+
}
|
|
690
|
+
return Object.freeze(value);
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
554
694
|
/** Evaluates definition-owned semantic review rules over authenticated prepared cells. */
|
|
555
695
|
export function evaluateDiagnosticReviewRules(
|
|
556
696
|
prepared: PreparedDiagnosticData,
|
|
557
697
|
): readonly DiagnosticReviewRuleEvaluation[] {
|
|
558
698
|
assertPreparedDiagnosticData(prepared);
|
|
559
699
|
const results: DiagnosticReviewRuleEvaluation[] = [];
|
|
700
|
+
const finalizeEvaluation = createEvaluationFinalizer();
|
|
701
|
+
visitDiagnosticReviewRules(prepared, (value) => {
|
|
702
|
+
results.push(finalizeEvaluation(value));
|
|
703
|
+
});
|
|
704
|
+
return Object.freeze(results);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Keeps every evaluation in compact owned storage; no identity is computed. */
|
|
708
|
+
export function evaluateDiagnosticReviewRulesCompact(
|
|
709
|
+
prepared: CompactPreparedDiagnosticData,
|
|
710
|
+
): CompactDiagnosticReviewEvaluations {
|
|
711
|
+
assertCompactPreparedDiagnosticData(prepared);
|
|
712
|
+
const compactSources = createCompactCellSourceReader();
|
|
713
|
+
const builder = createCompactReviewBuilder(
|
|
714
|
+
prepared.definition.definition.reviewRules,
|
|
715
|
+
compactSources,
|
|
716
|
+
);
|
|
717
|
+
visitDiagnosticReviewRules(prepared, builder.append, compactSources);
|
|
718
|
+
return builder.finish();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** One traversal and one formula implementation serve both storage contracts. */
|
|
722
|
+
function visitDiagnosticReviewRules(
|
|
723
|
+
prepared: PreparedDiagnosticDataContent,
|
|
724
|
+
appendEvaluation: (value: DiagnosticReviewRuleEvaluation) => void,
|
|
725
|
+
compactSources?: ReturnType<typeof createCompactCellSourceReader>,
|
|
726
|
+
): void {
|
|
727
|
+
const evaluateExpression = createCellExpressionEvaluator(
|
|
728
|
+
prepared,
|
|
729
|
+
compactSources,
|
|
730
|
+
);
|
|
731
|
+
const cellByCoordinate = new Map<
|
|
732
|
+
string,
|
|
733
|
+
PreparedDiagnosticData["cells"][number]
|
|
734
|
+
>();
|
|
735
|
+
const expectedSourcesByCoordinate = new Map<
|
|
736
|
+
string,
|
|
737
|
+
DiagnosticSourceLocation[]
|
|
738
|
+
>();
|
|
739
|
+
const coordinateKey = (
|
|
740
|
+
sourceGroup: string,
|
|
741
|
+
origin: string,
|
|
742
|
+
valuation: string,
|
|
743
|
+
) => canonicalJson([sourceGroup, origin, valuation]);
|
|
744
|
+
// Keep the original traversal and source order; only replace repeated searches.
|
|
745
|
+
// Other rule families do not need these indices.
|
|
746
|
+
if (
|
|
747
|
+
prepared.definition.definition.reviewRules.some(
|
|
748
|
+
(rule) => rule.kind === "monotonic",
|
|
749
|
+
)
|
|
750
|
+
) {
|
|
751
|
+
for (const cell of prepared.cells) {
|
|
752
|
+
const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
|
|
753
|
+
if (!cellByCoordinate.has(key)) cellByCoordinate.set(key, cell);
|
|
754
|
+
}
|
|
755
|
+
for (const cell of prepared.expectedCells) {
|
|
756
|
+
if (!cell.source) continue;
|
|
757
|
+
const key = coordinateKey(cell.sourceGroup, cell.origin, cell.valuation);
|
|
758
|
+
const sources = expectedSourcesByCoordinate.get(key) ?? [];
|
|
759
|
+
sources.push(cell.source);
|
|
760
|
+
expectedSourcesByCoordinate.set(key, sources);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
560
763
|
for (const [ruleIndex, rule] of (
|
|
561
764
|
prepared.definition.definition
|
|
562
765
|
.reviewRules as unknown as readonly DiagnosticReviewRule[]
|
|
@@ -571,7 +774,9 @@ export function evaluateDiagnosticReviewRules(
|
|
|
571
774
|
const sourceByMeasure = Object.fromEntries(
|
|
572
775
|
measureIds.map((measureId) => [
|
|
573
776
|
measureId,
|
|
574
|
-
|
|
777
|
+
compactSources
|
|
778
|
+
? compactSources.forSelection(cells, measureId)
|
|
779
|
+
: cellSourcesForSelection(cells, measureId),
|
|
575
780
|
]),
|
|
576
781
|
);
|
|
577
782
|
const globalStates: Record<string, FinalizedDiagnosticMeasure> =
|
|
@@ -654,9 +859,13 @@ export function evaluateDiagnosticReviewRules(
|
|
|
654
859
|
globalStates,
|
|
655
860
|
`/reviewRules/${ruleIndex}/expression`,
|
|
656
861
|
);
|
|
657
|
-
const sources =
|
|
658
|
-
|
|
659
|
-
|
|
862
|
+
const sources = compactSources
|
|
863
|
+
? compactSources.union(
|
|
864
|
+
measureIds.map((measureId) => sourceByMeasure[measureId] ?? []),
|
|
865
|
+
)
|
|
866
|
+
: uniqueSources(
|
|
867
|
+
measureIds.flatMap((measureId) => sourceByMeasure[measureId] ?? []),
|
|
868
|
+
);
|
|
660
869
|
const left: EvaluatedOperand = {
|
|
661
870
|
value: evaluated.value,
|
|
662
871
|
reasons: evaluated.reasons,
|
|
@@ -685,7 +894,7 @@ export function evaluateDiagnosticReviewRules(
|
|
|
685
894
|
),
|
|
686
895
|
sources,
|
|
687
896
|
};
|
|
688
|
-
|
|
897
|
+
appendEvaluation(
|
|
689
898
|
evaluation(
|
|
690
899
|
prepared,
|
|
691
900
|
rule,
|
|
@@ -725,30 +934,18 @@ export function evaluateDiagnosticReviewRules(
|
|
|
725
934
|
for (let index = 1; index < sorted.length; index++) {
|
|
726
935
|
const previousValuation = sorted[index - 1]!;
|
|
727
936
|
const currentValuation = sorted[index]!;
|
|
728
|
-
const previous =
|
|
729
|
-
(
|
|
730
|
-
cell.sourceGroup === sourceGroup &&
|
|
731
|
-
cell.origin === origin &&
|
|
732
|
-
cell.valuation === previousValuation,
|
|
937
|
+
const previous = cellByCoordinate.get(
|
|
938
|
+
coordinateKey(sourceGroup, origin, previousValuation),
|
|
733
939
|
);
|
|
734
|
-
const current =
|
|
735
|
-
(
|
|
736
|
-
cell.sourceGroup === sourceGroup &&
|
|
737
|
-
cell.origin === origin &&
|
|
738
|
-
cell.valuation === currentValuation,
|
|
940
|
+
const current = cellByCoordinate.get(
|
|
941
|
+
coordinateKey(sourceGroup, origin, currentValuation),
|
|
739
942
|
);
|
|
740
943
|
const expectedSources = (valuation: string) =>
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
cell.sourceGroup === sourceGroup &&
|
|
745
|
-
cell.origin === origin &&
|
|
746
|
-
cell.valuation === valuation,
|
|
747
|
-
)
|
|
748
|
-
.flatMap((cell) => (cell.source ? [cell.source] : []));
|
|
944
|
+
expectedSourcesByCoordinate.get(
|
|
945
|
+
coordinateKey(sourceGroup, origin, valuation),
|
|
946
|
+
) ?? [];
|
|
749
947
|
const left = previous
|
|
750
948
|
? evaluateExpression(
|
|
751
|
-
prepared,
|
|
752
949
|
previous,
|
|
753
950
|
rule.expression,
|
|
754
951
|
`${basePath}/expression`,
|
|
@@ -757,11 +954,14 @@ export function evaluateDiagnosticReviewRules(
|
|
|
757
954
|
value: null,
|
|
758
955
|
reasons: ["missing" as const],
|
|
759
956
|
overflows: [],
|
|
760
|
-
sources:
|
|
957
|
+
sources: compactSources
|
|
958
|
+
? compactSources.forPreparedSources(
|
|
959
|
+
expectedSources(previousValuation),
|
|
960
|
+
)
|
|
961
|
+
: uniqueSources(expectedSources(previousValuation)),
|
|
761
962
|
};
|
|
762
963
|
const right = current
|
|
763
964
|
? evaluateExpression(
|
|
764
|
-
prepared,
|
|
765
965
|
current,
|
|
766
966
|
rule.expression,
|
|
767
967
|
`${basePath}/expression`,
|
|
@@ -770,7 +970,11 @@ export function evaluateDiagnosticReviewRules(
|
|
|
770
970
|
value: null,
|
|
771
971
|
reasons: ["missing" as const],
|
|
772
972
|
overflows: [],
|
|
773
|
-
sources:
|
|
973
|
+
sources: compactSources
|
|
974
|
+
? compactSources.forPreparedSources(
|
|
975
|
+
expectedSources(currentValuation),
|
|
976
|
+
)
|
|
977
|
+
: uniqueSources(expectedSources(currentValuation)),
|
|
774
978
|
};
|
|
775
979
|
const scope: DiagnosticReviewEvaluationScope = {
|
|
776
980
|
kind: "valuation-pair",
|
|
@@ -790,9 +994,11 @@ export function evaluateDiagnosticReviewRules(
|
|
|
790
994
|
origin,
|
|
791
995
|
currentValuation,
|
|
792
996
|
),
|
|
793
|
-
sources:
|
|
997
|
+
sources: compactSources
|
|
998
|
+
? compactSources.union([left.sources, right.sources])
|
|
999
|
+
: uniqueSources([...left.sources, ...right.sources]),
|
|
794
1000
|
};
|
|
795
|
-
|
|
1001
|
+
appendEvaluation(
|
|
796
1002
|
evaluation(prepared, rule, scope, left, right, (relation) =>
|
|
797
1003
|
rule.direction === "nondecreasing"
|
|
798
1004
|
? relation !== "greater"
|
|
@@ -803,26 +1009,49 @@ export function evaluateDiagnosticReviewRules(
|
|
|
803
1009
|
}
|
|
804
1010
|
continue;
|
|
805
1011
|
}
|
|
1012
|
+
// Scope evidence is the union of this rule's expression leaves, not every
|
|
1013
|
+
// measure in the cell. Constants have no source dependency.
|
|
1014
|
+
const scopeDependencies = compactSources
|
|
1015
|
+
? Object.freeze(
|
|
1016
|
+
[
|
|
1017
|
+
...new Set(
|
|
1018
|
+
(rule.kind === "compare"
|
|
1019
|
+
? [rule.when.left, rule.when.right]
|
|
1020
|
+
: rule.kind === "reconcile"
|
|
1021
|
+
? [rule.actual, rule.expected]
|
|
1022
|
+
: [rule.narrower, rule.broader]
|
|
1023
|
+
).flatMap((expression) =>
|
|
1024
|
+
expression.op === "constant"
|
|
1025
|
+
? []
|
|
1026
|
+
: compactSources.measureIds(expression),
|
|
1027
|
+
),
|
|
1028
|
+
),
|
|
1029
|
+
].sort(codeUnit),
|
|
1030
|
+
)
|
|
1031
|
+
: undefined;
|
|
806
1032
|
for (const cell of prepared.cells) {
|
|
807
1033
|
const scopeFor = (
|
|
808
|
-
|
|
1034
|
+
leftSources: readonly DiagnosticSourceLocation[],
|
|
1035
|
+
rightSources: readonly DiagnosticSourceLocation[],
|
|
809
1036
|
): DiagnosticCellReviewScope => ({
|
|
810
1037
|
kind: "cell",
|
|
811
1038
|
cell: coordinate(cell),
|
|
812
|
-
sources:
|
|
1039
|
+
sources: compactSources
|
|
1040
|
+
? compactSources.forCell(cell, scopeDependencies!)
|
|
1041
|
+
: uniqueSources([...leftSources, ...rightSources]),
|
|
813
1042
|
});
|
|
814
1043
|
if (rule.kind === "compare") {
|
|
815
1044
|
const operand = (item: typeof rule.when.left, path: string) =>
|
|
816
1045
|
item.op === "constant"
|
|
817
1046
|
? constant(item.value)
|
|
818
|
-
: evaluateExpression(
|
|
1047
|
+
: evaluateExpression(cell, item, path);
|
|
819
1048
|
const left = operand(rule.when.left, `${basePath}/when/left`);
|
|
820
1049
|
const right = operand(rule.when.right, `${basePath}/when/right`);
|
|
821
|
-
|
|
1050
|
+
appendEvaluation(
|
|
822
1051
|
evaluation(
|
|
823
1052
|
prepared,
|
|
824
1053
|
rule,
|
|
825
|
-
scopeFor(
|
|
1054
|
+
scopeFor(left.sources, right.sources),
|
|
826
1055
|
left,
|
|
827
1056
|
right,
|
|
828
1057
|
(relation) =>
|
|
@@ -831,7 +1060,6 @@ export function evaluateDiagnosticReviewRules(
|
|
|
831
1060
|
);
|
|
832
1061
|
} else if (rule.kind === "reconcile") {
|
|
833
1062
|
const left = evaluateExpression(
|
|
834
|
-
prepared,
|
|
835
1063
|
cell,
|
|
836
1064
|
rule.actual,
|
|
837
1065
|
`${basePath}/actual`,
|
|
@@ -839,17 +1067,12 @@ export function evaluateDiagnosticReviewRules(
|
|
|
839
1067
|
const right =
|
|
840
1068
|
rule.expected.op === "constant"
|
|
841
1069
|
? constant(rule.expected.value)
|
|
842
|
-
: evaluateExpression(
|
|
843
|
-
|
|
844
|
-
cell,
|
|
845
|
-
rule.expected,
|
|
846
|
-
`${basePath}/expected`,
|
|
847
|
-
);
|
|
848
|
-
results.push(
|
|
1070
|
+
: evaluateExpression(cell, rule.expected, `${basePath}/expected`);
|
|
1071
|
+
appendEvaluation(
|
|
849
1072
|
evaluation(
|
|
850
1073
|
prepared,
|
|
851
1074
|
rule,
|
|
852
|
-
scopeFor(
|
|
1075
|
+
scopeFor(left.sources, right.sources),
|
|
853
1076
|
left,
|
|
854
1077
|
right,
|
|
855
1078
|
(relation) => relation === "equal",
|
|
@@ -857,22 +1080,20 @@ export function evaluateDiagnosticReviewRules(
|
|
|
857
1080
|
);
|
|
858
1081
|
} else {
|
|
859
1082
|
const left = evaluateExpression(
|
|
860
|
-
prepared,
|
|
861
1083
|
cell,
|
|
862
1084
|
rule.narrower,
|
|
863
1085
|
`${basePath}/narrower`,
|
|
864
1086
|
);
|
|
865
1087
|
const right = evaluateExpression(
|
|
866
|
-
prepared,
|
|
867
1088
|
cell,
|
|
868
1089
|
rule.broader,
|
|
869
1090
|
`${basePath}/broader`,
|
|
870
1091
|
);
|
|
871
|
-
|
|
1092
|
+
appendEvaluation(
|
|
872
1093
|
evaluation(
|
|
873
1094
|
prepared,
|
|
874
1095
|
rule,
|
|
875
|
-
scopeFor(
|
|
1096
|
+
scopeFor(left.sources, right.sources),
|
|
876
1097
|
left,
|
|
877
1098
|
right,
|
|
878
1099
|
(relation) => relation !== "greater",
|
|
@@ -881,17 +1102,4 @@ export function evaluateDiagnosticReviewRules(
|
|
|
881
1102
|
}
|
|
882
1103
|
}
|
|
883
1104
|
}
|
|
884
|
-
return deepFreeze(results);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
function deepFreeze<T>(
|
|
888
|
-
value: T,
|
|
889
|
-
seen = new WeakSet<object>(),
|
|
890
|
-
): DiagnosticDeepReadonly<T> {
|
|
891
|
-
if (value === null || typeof value !== "object" || seen.has(value))
|
|
892
|
-
return value as DiagnosticDeepReadonly<T>;
|
|
893
|
-
seen.add(value);
|
|
894
|
-
for (const child of Object.values(value as Record<string, unknown>))
|
|
895
|
-
deepFreeze(child, seen);
|
|
896
|
-
return Object.freeze(value) as DiagnosticDeepReadonly<T>;
|
|
897
1105
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { canonicalJson } from "./canonical.js";
|
|
2
|
+
import type { DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
|
|
3
|
+
import {
|
|
4
|
+
compareDiagnosticSourceLocations,
|
|
5
|
+
normalizeDiagnosticSourceLocations,
|
|
6
|
+
} from "./diagnosticSourceOrdering.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Private, invocation-owned evidence interner. Only the authenticated review
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export function createDiagnosticReviewSourcePool() {
|
|
14
|
+
type SourceEntry = { key: string; value: DiagnosticSourceLocation };
|
|
15
|
+
const preparedSources = new WeakMap<object, SourceEntry>();
|
|
16
|
+
const ownedSources = new WeakMap<object, SourceEntry>();
|
|
17
|
+
const ownedLists = new WeakSet<readonly DiagnosticSourceLocation[]>();
|
|
18
|
+
const sourceEntry = (
|
|
19
|
+
value: DiagnosticSourceLocation,
|
|
20
|
+
fromPrepared: boolean,
|
|
21
|
+
): SourceEntry => {
|
|
22
|
+
const previous =
|
|
23
|
+
ownedSources.get(value) ??
|
|
24
|
+
(fromPrepared ? preparedSources.get(value) : undefined);
|
|
25
|
+
if (previous) return previous;
|
|
26
|
+
// Preserve the authoritative full-value contract, including extra own
|
|
27
|
+
// data. An artifact/file/sheet/row tuple is not an exact-value substitute.
|
|
28
|
+
const normalized = normalizeDiagnosticSourceLocations([value])[0]!;
|
|
29
|
+
const key = canonicalJson(normalized);
|
|
30
|
+
// Fresh/unrecognized expression evidence takes the value path every time.
|
|
31
|
+
// Its children may be caller-owned even if the outer source was frozen.
|
|
32
|
+
if (!fromPrepared) return { key, value: normalized };
|
|
33
|
+
// Deduplicate by canonical value only inside each ordered union below.
|
|
34
|
+
// Across subsets, equal canonical keys can still have different original
|
|
35
|
+
// representatives (for example -0/+0 in extra own data). Keep the snapshot
|
|
36
|
+
// associated with each prepared source so the first occurrence still wins.
|
|
37
|
+
const entry = { key, value: Object.freeze(normalized) };
|
|
38
|
+
ownedSources.set(entry.value, entry);
|
|
39
|
+
preparedSources.set(value, entry);
|
|
40
|
+
return entry;
|
|
41
|
+
};
|
|
42
|
+
const collection = () => {
|
|
43
|
+
const entries = new Map<string, DiagnosticSourceLocation>();
|
|
44
|
+
let owned = true;
|
|
45
|
+
return {
|
|
46
|
+
/** `fromPrepared` is private evaluator authority, not an input option. */
|
|
47
|
+
add(value: DiagnosticSourceLocation, fromPrepared: boolean): void {
|
|
48
|
+
const entry = sourceEntry(value, fromPrepared);
|
|
49
|
+
owned &&= ownedSources.has(entry.value);
|
|
50
|
+
if (!entries.has(entry.key)) entries.set(entry.key, entry.value);
|
|
51
|
+
},
|
|
52
|
+
finish(): readonly DiagnosticSourceLocation[] {
|
|
53
|
+
const values = Object.freeze(
|
|
54
|
+
[...entries.values()].sort(compareDiagnosticSourceLocations),
|
|
55
|
+
);
|
|
56
|
+
if (owned) ownedLists.add(values);
|
|
57
|
+
return values;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const union = (
|
|
62
|
+
lists: readonly (readonly DiagnosticSourceLocation[])[],
|
|
63
|
+
): readonly DiagnosticSourceLocation[] => {
|
|
64
|
+
const target = collection();
|
|
65
|
+
for (const values of lists)
|
|
66
|
+
for (const source of values) target.add(source, false);
|
|
67
|
+
return target.finish();
|
|
68
|
+
};
|
|
69
|
+
const forPreparedSources = (
|
|
70
|
+
values: readonly DiagnosticSourceLocation[],
|
|
71
|
+
): readonly DiagnosticSourceLocation[] => {
|
|
72
|
+
const target = collection();
|
|
73
|
+
for (const source of values) target.add(source, true);
|
|
74
|
+
return target.finish();
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
collection,
|
|
78
|
+
union,
|
|
79
|
+
forPreparedSources,
|
|
80
|
+
// Ownership means this invocation's snapshots, not Object.isFrozen on an
|
|
81
|
+
// arbitrary caller array/object. The builder uses these only while appending.
|
|
82
|
+
sourceKey: (value: DiagnosticSourceLocation) => ownedSources.get(value)?.key,
|
|
83
|
+
ownsList: (value: readonly DiagnosticSourceLocation[]) => ownedLists.has(value),
|
|
84
|
+
};
|
|
85
|
+
}
|