@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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { canonicalJson, fnv1a64 } from "./canonical.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
freezeDiagnosticGraph as deepFreeze,
|
|
4
|
+
freezeOwnedDiagnosticGraph as deepFreezeOwned,
|
|
5
|
+
} from "./diagnosticFreeze.js";
|
|
6
|
+
import { auditRecordCanonicalJson } from "./diagnosticAuditText.js";
|
|
7
|
+
import { canonicalScalarArrayJson } from "./diagnosticKeys.js";
|
|
4
8
|
import {
|
|
5
9
|
createDiagnosticIdentityArray,
|
|
6
10
|
createDiagnosticIdentityObject,
|
|
@@ -32,7 +36,9 @@ import {
|
|
|
32
36
|
} from "./diagnosticDefinitions.js";
|
|
33
37
|
import {
|
|
34
38
|
auditedDiagnosticContribution,
|
|
35
|
-
|
|
39
|
+
createDiagnosticStatsInterner,
|
|
40
|
+
finalizeDiagnosticContributionsInterned,
|
|
41
|
+
ownedDiagnosticContribution,
|
|
36
42
|
type DiagnosticMeasureContribution,
|
|
37
43
|
type DiagnosticStructuralBlocker,
|
|
38
44
|
} from "./diagnosticAggregation.js";
|
|
@@ -233,6 +239,8 @@ export function reselectCompactDiagnosticMetrics(
|
|
|
233
239
|
if (sourceScope(normalized) !== sourceScope(prepared.filter)) return undefined;
|
|
234
240
|
const selected = Object.freeze({ ...prepared, filter: deepFreeze(normalized) }) as CompactPreparedDiagnosticData;
|
|
235
241
|
authenticCompact.add(selected);
|
|
242
|
+
const recordedGroups = preparedSourceGroups.get(prepared);
|
|
243
|
+
if (recordedGroups !== undefined) preparedSourceGroups.set(selected, recordedGroups);
|
|
236
244
|
const parent = sourceSelectionParents.get(prepared);
|
|
237
245
|
const coordinateParent = coordinateSelectionParents.get(prepared);
|
|
238
246
|
if (coordinateParent) coordinateSelectionParents.set(selected, coordinateParent);
|
|
@@ -276,7 +284,7 @@ function selectCompactSourceScope(
|
|
|
276
284
|
allowCoordinates = false,
|
|
277
285
|
): CompactPreparedDiagnosticData | undefined {
|
|
278
286
|
assertCompactPreparedDiagnosticData(prepared);
|
|
279
|
-
const
|
|
287
|
+
const selection = sourceSelectionMetadata.get(prepared);
|
|
280
288
|
const boundaryIssues = preparationBoundaryIssues({
|
|
281
289
|
definition: prepared.definition, losses: [], exposures: [],
|
|
282
290
|
...(filter === undefined ? {} : { filter }),
|
|
@@ -284,10 +292,11 @@ function selectCompactSourceScope(
|
|
|
284
292
|
if (boundaryIssues.length) throw new DiagnosticValidationError(boundaryIssues);
|
|
285
293
|
const issues: DiagnosticValidationIssue[] = [];
|
|
286
294
|
const normalizedFilter = normalizeFilter(prepared.definition, filter,
|
|
287
|
-
|
|
295
|
+
selection?.groups ?? new Set(prepared.inputAudit.map((item) => item.record.sourceGroup)), issues);
|
|
288
296
|
const normalized = normalizedFilter.value;
|
|
289
297
|
if (issues.length) throw new DiagnosticValidationError(issues);
|
|
290
|
-
if (!
|
|
298
|
+
if (!selection) return undefined;
|
|
299
|
+
const metadata = selection.load();
|
|
291
300
|
if (allowOrigins && !metadata.allOriginsSelected) return undefined;
|
|
292
301
|
if (allowCoordinates && !metadata.allCoordinatesSelected) return undefined;
|
|
293
302
|
const remainingScope = (value: DiagnosticsFilter | null) => {
|
|
@@ -323,12 +332,12 @@ function selectCompactSourceScope(
|
|
|
323
332
|
const cells = Object.freeze(prepared.cells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation)));
|
|
324
333
|
const exposures = Object.freeze(prepared.exposures.filter((exposure) => exposure.status === "valid" && includes(exposure.sourceGroup) && includesOrigin(exposure.origin) && includesDate(exposure.origin, exposure.valuation)));
|
|
325
334
|
const coordinateKeys = allowCoordinates ? new Set(cells.map((cell) => cellKey(cell.sourceGroup, cell.origin, cell.valuation))) : undefined;
|
|
326
|
-
const sourceOrigins = allowCoordinates ? new Set(cells.map((cell) =>
|
|
335
|
+
const sourceOrigins = allowCoordinates ? new Set(cells.map((cell) => sourceOriginKey(cell.sourceGroup, cell.origin))) : undefined;
|
|
327
336
|
const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
|
|
328
337
|
const orphanFindings = allowCoordinates ? exposures.flatMap((exposure) => {
|
|
329
338
|
if (exposure.status !== "valid") return [];
|
|
330
339
|
const matches = internals.measuresById.get(exposure.measureId)!.exposureTiming === "origin-static"
|
|
331
|
-
? sourceOrigins!.has(
|
|
340
|
+
? sourceOrigins!.has(sourceOriginKey(exposure.sourceGroup, exposure.origin))
|
|
332
341
|
: exposure.valuation !== undefined && coordinateKeys!.has(cellKey(exposure.sourceGroup, exposure.origin, exposure.valuation));
|
|
333
342
|
return matches ? [] : [exposureWithoutLossFinding(exposure)];
|
|
334
343
|
}) : [];
|
|
@@ -342,6 +351,8 @@ function selectCompactSourceScope(
|
|
|
342
351
|
expectedCells: Object.freeze(prepared.expectedCells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation))),
|
|
343
352
|
}) as CompactPreparedDiagnosticData;
|
|
344
353
|
authenticCompact.add(selected);
|
|
354
|
+
const knownGroups = preparedSourceGroups.get(prepared);
|
|
355
|
+
if (knownGroups !== undefined) preparedSourceGroups.set(selected, knownGroups);
|
|
345
356
|
if (allowCoordinates) coordinateSelectionParents.set(selected, prepared);
|
|
346
357
|
else sourceSelectionParents.set(selected, prepared);
|
|
347
358
|
return selected;
|
|
@@ -433,16 +444,46 @@ const authentic = new WeakSet<object>();
|
|
|
433
444
|
const authenticCompact = new WeakSet<object>();
|
|
434
445
|
const sourceSelectionParents = new WeakMap<CompactPreparedDiagnosticData, CompactPreparedDiagnosticData>();
|
|
435
446
|
const coordinateSelectionParents = new WeakMap<CompactPreparedDiagnosticData, CompactPreparedDiagnosticData>();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
447
|
+
interface CompactSelectionMetadata {
|
|
448
|
+
readonly excluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
|
|
449
|
+
readonly originExcluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
|
|
450
|
+
readonly origins: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
|
|
451
|
+
readonly valuations: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
|
|
452
|
+
readonly allOriginsSelected: boolean;
|
|
453
|
+
readonly allCoordinatesSelected: boolean;
|
|
454
|
+
readonly auditRanks: ReadonlyMap<DiagnosticInputAuditRecord, number>;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Selection ranks for every included/excluded audit variant are derived only
|
|
458
|
+
* from the preparation's own owned snapshots, so they are computed on the
|
|
459
|
+
* first source/origin/date query rather than for every preparation. The
|
|
460
|
+
* captured preparation state is released once the ranks exist.
|
|
461
|
+
*/
|
|
462
|
+
class LazySelectionMetadata {
|
|
463
|
+
#compute: (() => CompactSelectionMetadata) | null;
|
|
464
|
+
#value: CompactSelectionMetadata | undefined;
|
|
465
|
+
constructor(
|
|
466
|
+
readonly groups: ReadonlySet<string>,
|
|
467
|
+
compute: () => CompactSelectionMetadata,
|
|
468
|
+
) {
|
|
469
|
+
this.#compute = compute;
|
|
470
|
+
}
|
|
471
|
+
load(): CompactSelectionMetadata {
|
|
472
|
+
if (this.#value === undefined) {
|
|
473
|
+
this.#value = this.#compute!();
|
|
474
|
+
this.#compute = null;
|
|
475
|
+
}
|
|
476
|
+
return this.#value;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const sourceSelectionMetadata = new WeakMap<CompactPreparedDiagnosticData, LazySelectionMetadata>();
|
|
480
|
+
// Every source group present in a preparation's input rows, hence in its audit.
|
|
481
|
+
// Selections keep the same audit records, so they share their parent's set.
|
|
482
|
+
const preparedSourceGroups = new WeakMap<object, ReadonlySet<string>>();
|
|
483
|
+
/** Source groups audited by an authentic preparation, when it recorded them. */
|
|
484
|
+
export function getPreparedSourceGroups(prepared: object): ReadonlySet<string> | undefined {
|
|
485
|
+
return preparedSourceGroups.get(prepared);
|
|
486
|
+
}
|
|
446
487
|
const compactIdentityDocuments = new WeakMap<
|
|
447
488
|
object,
|
|
448
489
|
DiagnosticIdentityDocument
|
|
@@ -535,6 +576,80 @@ function uniqueSorted(values: readonly string[]): string[] {
|
|
|
535
576
|
return [...new Set(values)].sort(codeUnit);
|
|
536
577
|
}
|
|
537
578
|
|
|
579
|
+
/** Own keys in code-unit order; skips the sort when insertion order already is. */
|
|
580
|
+
function sortedKeys(record: object): string[] {
|
|
581
|
+
const keys = Object.keys(record);
|
|
582
|
+
for (let index = 1; index < keys.length; index++)
|
|
583
|
+
if (keys[index - 1]! > keys[index]!) return keys.sort(codeUnit);
|
|
584
|
+
return keys;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Own-key copy in code-unit key order with each value mapped, built without
|
|
589
|
+
* entry pairs. A literal "__proto__" key stays own data, as Object.fromEntries
|
|
590
|
+
* would keep it.
|
|
591
|
+
*/
|
|
592
|
+
function sortedRecordCopy<T, U>(
|
|
593
|
+
record: Readonly<Record<string, T>>,
|
|
594
|
+
map: (value: T) => U,
|
|
595
|
+
): Record<string, U> {
|
|
596
|
+
const keys = sortedKeys(record);
|
|
597
|
+
const out: Record<string, U> = {};
|
|
598
|
+
for (let index = 0; index < keys.length; index++) {
|
|
599
|
+
const key = keys[index]!;
|
|
600
|
+
const value = map(record[key]!);
|
|
601
|
+
if (key === "__proto__")
|
|
602
|
+
Object.defineProperty(out, key, {
|
|
603
|
+
value,
|
|
604
|
+
enumerable: true,
|
|
605
|
+
writable: true,
|
|
606
|
+
configurable: true,
|
|
607
|
+
});
|
|
608
|
+
else out[key] = value;
|
|
609
|
+
}
|
|
610
|
+
return out;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Assigns own data even for a literal "__proto__" measure id. */
|
|
614
|
+
function setMeasureEntry<T>(record: Record<string, T>, id: string, value: T): void {
|
|
615
|
+
if (id === "__proto__")
|
|
616
|
+
Object.defineProperty(record, id, {
|
|
617
|
+
value,
|
|
618
|
+
enumerable: true,
|
|
619
|
+
writable: true,
|
|
620
|
+
configurable: true,
|
|
621
|
+
});
|
|
622
|
+
else record[id] = value;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** A fresh empty list per measure id, in id order; a literal "__proto__" id stays own data. */
|
|
626
|
+
function emptyListsByMeasure<T>(measureIds: readonly string[]): Record<string, T[]> {
|
|
627
|
+
const out: Record<string, T[]> = {};
|
|
628
|
+
for (let index = 0; index < measureIds.length; index++)
|
|
629
|
+
setMeasureEntry(out, measureIds[index]!, [] as T[]);
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* One shared immutable empty list per measure id. A caller that needs to append
|
|
635
|
+
* must first replace the entry, so the shared list is never mutated; most cells
|
|
636
|
+
* never append at all and keep every entry pointing at it.
|
|
637
|
+
*/
|
|
638
|
+
function sharedEmptyListsByMeasure<T>(
|
|
639
|
+
measureIds: readonly string[],
|
|
640
|
+
empty: readonly T[],
|
|
641
|
+
): Record<string, readonly T[]> {
|
|
642
|
+
const out: Record<string, readonly T[]> = {};
|
|
643
|
+
for (let index = 0; index < measureIds.length; index++)
|
|
644
|
+
setMeasureEntry(out, measureIds[index]!, empty);
|
|
645
|
+
return out;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Internal coordinate keys: validated tokens never contain U+0000. */
|
|
649
|
+
function sourceOriginKey(sourceGroup: string, origin: string): string {
|
|
650
|
+
return `${sourceGroup}${origin}`;
|
|
651
|
+
}
|
|
652
|
+
|
|
538
653
|
function normalizeSources(
|
|
539
654
|
values: readonly (DiagnosticSourceLocation | undefined | null)[],
|
|
540
655
|
): DiagnosticSourceLocation[] {
|
|
@@ -625,15 +740,9 @@ function mergeFindings(
|
|
|
625
740
|
|
|
626
741
|
function snapshotLoss(
|
|
627
742
|
row: DiagnosticLossInput,
|
|
743
|
+
audited: (value: number | null) => DiagnosticAuditedNumericValue = auditDiagnosticNumber,
|
|
628
744
|
): DiagnosticLossInputAuditSnapshot {
|
|
629
|
-
const measures =
|
|
630
|
-
Object.keys(row.measures)
|
|
631
|
-
.sort(codeUnit)
|
|
632
|
-
.map((measureId) => [
|
|
633
|
-
measureId,
|
|
634
|
-
auditDiagnosticNumber(row.measures[measureId] ?? null),
|
|
635
|
-
]),
|
|
636
|
-
);
|
|
745
|
+
const measures = sortedRecordCopy(row.measures, (value) => audited(value ?? null));
|
|
637
746
|
return {
|
|
638
747
|
recordId: row.recordId,
|
|
639
748
|
rowType: row.rowType,
|
|
@@ -649,6 +758,7 @@ function snapshotLoss(
|
|
|
649
758
|
|
|
650
759
|
function snapshotExposure(
|
|
651
760
|
row: DiagnosticExposureObservation,
|
|
761
|
+
audited: (value: number | null) => DiagnosticAuditedNumericValue = auditDiagnosticNumber,
|
|
652
762
|
): DiagnosticExposureInputAuditSnapshot {
|
|
653
763
|
return {
|
|
654
764
|
key: row.key,
|
|
@@ -656,7 +766,7 @@ function snapshotExposure(
|
|
|
656
766
|
origin: row.origin,
|
|
657
767
|
valuation: row.valuation ?? null,
|
|
658
768
|
measureId: row.measureId,
|
|
659
|
-
value:
|
|
769
|
+
value: audited(row.value),
|
|
660
770
|
complete: row.complete,
|
|
661
771
|
source: row.source === undefined ? null : { ...row.source },
|
|
662
772
|
};
|
|
@@ -1007,16 +1117,13 @@ function coordinateFor(
|
|
|
1007
1117
|
};
|
|
1008
1118
|
}
|
|
1009
1119
|
|
|
1120
|
+
/** Internal cell key: validated tokens and period labels never contain U+0000. */
|
|
1010
1121
|
function cellKey(
|
|
1011
1122
|
sourceGroup: string,
|
|
1012
1123
|
origin: string,
|
|
1013
1124
|
valuation: string,
|
|
1014
1125
|
): string {
|
|
1015
|
-
return
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function parseCellKey(key: string): readonly [string, string, string] {
|
|
1019
|
-
return JSON.parse(key) as [string, string, string];
|
|
1126
|
+
return `${sourceGroup}${origin}${valuation}`;
|
|
1020
1127
|
}
|
|
1021
1128
|
|
|
1022
1129
|
function coordinateContext(candidate: {
|
|
@@ -1141,7 +1248,7 @@ function commonCoordinate(
|
|
|
1141
1248
|
}
|
|
1142
1249
|
|
|
1143
1250
|
function exposureIdentity(candidate: ExposureCandidate): string {
|
|
1144
|
-
return
|
|
1251
|
+
return canonicalScalarArrayJson(
|
|
1145
1252
|
candidate.timing === "valuation-specific"
|
|
1146
1253
|
? [
|
|
1147
1254
|
candidate.row.measureId,
|
|
@@ -1187,38 +1294,212 @@ function exposureCoordinateContext(
|
|
|
1187
1294
|
return { sourceGroup: coherentSource[0], origin: coherentOrigin[0] };
|
|
1188
1295
|
}
|
|
1189
1296
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1297
|
+
const AUDIT_KIND_RANK = { loss: 0, exposure: 1, "expected-cell": 2 } as const;
|
|
1298
|
+
const AUDIT_DISPOSITION_RANK = {
|
|
1299
|
+
invalid: 0,
|
|
1300
|
+
"complete-period-cutoff": 1,
|
|
1301
|
+
filter: 2,
|
|
1302
|
+
retained: 3,
|
|
1303
|
+
} as const;
|
|
1304
|
+
|
|
1305
|
+
export interface DiagnosticAuditOrder {
|
|
1306
|
+
readonly compare: (
|
|
1307
|
+
left: DiagnosticInputAuditRecord,
|
|
1308
|
+
right: DiagnosticInputAuditRecord,
|
|
1309
|
+
) => number;
|
|
1310
|
+
/** Canonical text of a record, serialized once per record for this order. */
|
|
1311
|
+
readonly key: (record: DiagnosticInputAuditRecord["record"]) => string;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function createAuditComparator(): DiagnosticAuditOrder {
|
|
1201
1315
|
// Audit snapshots are already owned and complete before sorting. Preserve the
|
|
1202
1316
|
// exact canonical-text ordering, but serialize each compared record once.
|
|
1203
1317
|
// Keep this memo local to the sort; it must not retain keys with the result.
|
|
1204
|
-
|
|
1318
|
+
// A strong table is released with the comparator and looks up faster than a
|
|
1319
|
+
// weak one for the millions of comparisons a large audit sort performs.
|
|
1320
|
+
const keys = new Map<DiagnosticInputAuditRecord["record"], string>();
|
|
1205
1321
|
const key = (record: DiagnosticInputAuditRecord["record"]): string => {
|
|
1206
1322
|
const previous = keys.get(record);
|
|
1207
1323
|
if (previous !== undefined) return previous;
|
|
1208
|
-
const text =
|
|
1324
|
+
const text = auditRecordCanonicalJson(record);
|
|
1209
1325
|
keys.set(record, text);
|
|
1210
1326
|
return text;
|
|
1211
1327
|
};
|
|
1212
|
-
return
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1328
|
+
return {
|
|
1329
|
+
key,
|
|
1330
|
+
compare: (left, right) =>
|
|
1331
|
+
AUDIT_KIND_RANK[left.kind] - AUDIT_KIND_RANK[right.kind] ||
|
|
1332
|
+
(left.record === right.record ? 0 : codeUnit(key(left.record), key(right.record))) ||
|
|
1333
|
+
AUDIT_DISPOSITION_RANK[left.disposition] - AUDIT_DISPOSITION_RANK[right.disposition],
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Sorts audit records in place by the comparator's order, comparing texts that
|
|
1339
|
+
* were resolved once per record instead of looking each up on every comparison.
|
|
1340
|
+
* The stable sort over the decorated rows yields exactly `audit.sort(compare)`.
|
|
1341
|
+
*/
|
|
1342
|
+
function sortAuditRecords(
|
|
1343
|
+
audit: DiagnosticInputAuditRecord[],
|
|
1344
|
+
order: DiagnosticAuditOrder,
|
|
1345
|
+
): void {
|
|
1346
|
+
interface Row {
|
|
1347
|
+
readonly item: DiagnosticInputAuditRecord;
|
|
1348
|
+
readonly kind: number;
|
|
1349
|
+
readonly disposition: number;
|
|
1350
|
+
/** Canonical text, resolved the first time a comparison needs it. */
|
|
1351
|
+
key: string | undefined;
|
|
1352
|
+
}
|
|
1353
|
+
const decorated: Row[] = audit.map((item) => ({
|
|
1354
|
+
item,
|
|
1355
|
+
kind: AUDIT_KIND_RANK[item.kind],
|
|
1356
|
+
disposition: AUDIT_DISPOSITION_RANK[item.disposition],
|
|
1357
|
+
key: undefined,
|
|
1358
|
+
}));
|
|
1359
|
+
const keyOf = (row: Row): string => row.key ?? (row.key = order.key(row.item.record));
|
|
1360
|
+
decorated.sort(
|
|
1361
|
+
(left, right) =>
|
|
1362
|
+
left.kind - right.kind ||
|
|
1363
|
+
(left.item.record === right.item.record ? 0 : codeUnit(keyOf(left), keyOf(right))) ||
|
|
1364
|
+
left.disposition - right.disposition,
|
|
1365
|
+
);
|
|
1366
|
+
for (let index = 0; index < decorated.length; index++) audit[index] = decorated[index]!.item;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* Every included/excluded audit variant in the comparator's order, built from
|
|
1371
|
+
* the already sorted audit instead of re-sorting three copies of it. The audit
|
|
1372
|
+
* holds each record once in comparator order, so when no two distinct records
|
|
1373
|
+
* of one kind share a canonical text, the sorted variants are exactly: each
|
|
1374
|
+
* audit record's variants in disposition order (stable for equal dispositions:
|
|
1375
|
+
* the audit item, then its source-excluded, then its origin-excluded variant),
|
|
1376
|
+
* with variants whose record is a re-labelled raw snapshot merged in by the
|
|
1377
|
+
* same comparator. Any canonical-text tie falls back to the full stable sort.
|
|
1378
|
+
* Exported for the equivalence test only.
|
|
1379
|
+
*/
|
|
1380
|
+
export function orderAuditVariants(
|
|
1381
|
+
audit: readonly DiagnosticInputAuditRecord[],
|
|
1382
|
+
excluded: ReadonlyMap<object, DiagnosticInputAuditRecord>,
|
|
1383
|
+
originExcluded: ReadonlyMap<object, DiagnosticInputAuditRecord>,
|
|
1384
|
+
order: DiagnosticAuditOrder,
|
|
1385
|
+
): DiagnosticInputAuditRecord[] {
|
|
1386
|
+
const everything = () =>
|
|
1387
|
+
[...audit, ...excluded.values(), ...originExcluded.values()].sort(order.compare);
|
|
1388
|
+
for (let index = 1; index < audit.length; index++) {
|
|
1389
|
+
const previous = audit[index - 1]!;
|
|
1390
|
+
const current = audit[index]!;
|
|
1391
|
+
if (
|
|
1392
|
+
previous.kind === current.kind &&
|
|
1393
|
+
previous.record !== current.record &&
|
|
1394
|
+
order.key(previous.record) === order.key(current.record)
|
|
1395
|
+
)
|
|
1396
|
+
return everything();
|
|
1397
|
+
}
|
|
1398
|
+
const auditRecords = new Set<object>();
|
|
1399
|
+
for (const item of audit) auditRecords.add(item.record);
|
|
1400
|
+
const excludedByRecord = new Map<object, DiagnosticInputAuditRecord>();
|
|
1401
|
+
const detached: DiagnosticInputAuditRecord[] = [];
|
|
1402
|
+
for (const variant of excluded.values()) {
|
|
1403
|
+
if (auditRecords.has(variant.record)) excludedByRecord.set(variant.record, variant);
|
|
1404
|
+
else detached.push(variant);
|
|
1405
|
+
}
|
|
1406
|
+
if (detached.length > 0) {
|
|
1407
|
+
const auditKeys = new Set(
|
|
1408
|
+
audit.map((item) => `${AUDIT_KIND_RANK[item.kind]}:${order.key(item.record)}`),
|
|
1409
|
+
);
|
|
1410
|
+
for (const variant of detached)
|
|
1411
|
+
if (auditKeys.has(`${AUDIT_KIND_RANK[variant.kind]}:${order.key(variant.record)}`))
|
|
1412
|
+
return everything();
|
|
1413
|
+
detached.sort(order.compare);
|
|
1414
|
+
}
|
|
1415
|
+
const ordered: DiagnosticInputAuditRecord[] = [];
|
|
1416
|
+
let next = 0;
|
|
1417
|
+
for (const item of audit) {
|
|
1418
|
+
while (next < detached.length && order.compare(detached[next]!, item) < 0)
|
|
1419
|
+
ordered.push(detached[next++]!);
|
|
1420
|
+
const variants = [item];
|
|
1421
|
+
const excludedVariant = excludedByRecord.get(item.record);
|
|
1422
|
+
if (excludedVariant !== undefined) variants.push(excludedVariant);
|
|
1423
|
+
const originVariant = originExcluded.get(item.record);
|
|
1424
|
+
if (originVariant !== undefined) variants.push(originVariant);
|
|
1425
|
+
variants.sort(
|
|
1426
|
+
(left, right) =>
|
|
1427
|
+
AUDIT_DISPOSITION_RANK[left.disposition] - AUDIT_DISPOSITION_RANK[right.disposition],
|
|
1428
|
+
);
|
|
1429
|
+
for (const variant of variants) ordered.push(variant);
|
|
1430
|
+
}
|
|
1431
|
+
while (next < detached.length) ordered.push(detached[next++]!);
|
|
1432
|
+
return ordered;
|
|
1216
1433
|
}
|
|
1217
1434
|
|
|
1218
1435
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
1219
1436
|
return isDiagnosticPlainRecord(value);
|
|
1220
1437
|
}
|
|
1221
1438
|
|
|
1439
|
+
// Allowed-key sets and the exact path suffixes each row check reports under.
|
|
1440
|
+
// Both are constants, so a clean row allocates no path text at all: every
|
|
1441
|
+
// string below is built inside the branch that reports an issue.
|
|
1442
|
+
const PREPARATION_INPUT_KEYS: ReadonlySet<string> = new Set([
|
|
1443
|
+
"definition",
|
|
1444
|
+
"losses",
|
|
1445
|
+
"exposures",
|
|
1446
|
+
"filter",
|
|
1447
|
+
"completePeriodCutoffs",
|
|
1448
|
+
"expectedCells",
|
|
1449
|
+
]);
|
|
1450
|
+
const LOSS_ROW_KEYS: ReadonlySet<string> = new Set([
|
|
1451
|
+
"rowType",
|
|
1452
|
+
"recordId",
|
|
1453
|
+
"claimId",
|
|
1454
|
+
"sourceGroup",
|
|
1455
|
+
"origin",
|
|
1456
|
+
"valuation",
|
|
1457
|
+
"complete",
|
|
1458
|
+
"source",
|
|
1459
|
+
"measures",
|
|
1460
|
+
]);
|
|
1461
|
+
const EXPOSURE_ROW_KEYS: ReadonlySet<string> = new Set([
|
|
1462
|
+
"key",
|
|
1463
|
+
"sourceGroup",
|
|
1464
|
+
"origin",
|
|
1465
|
+
"valuation",
|
|
1466
|
+
"measureId",
|
|
1467
|
+
"value",
|
|
1468
|
+
"complete",
|
|
1469
|
+
"source",
|
|
1470
|
+
]);
|
|
1471
|
+
const SOURCE_LOCATION_KEYS: ReadonlySet<string> = new Set([
|
|
1472
|
+
"artifactId",
|
|
1473
|
+
"sourceFile",
|
|
1474
|
+
"sourceSheet",
|
|
1475
|
+
"sourceRow",
|
|
1476
|
+
"sourceCell",
|
|
1477
|
+
]);
|
|
1478
|
+
const FILTER_KEYS: ReadonlySet<string> = new Set([
|
|
1479
|
+
"sourceGroups",
|
|
1480
|
+
"outputGroups",
|
|
1481
|
+
"origins",
|
|
1482
|
+
"originFrom",
|
|
1483
|
+
"originThrough",
|
|
1484
|
+
"valuations",
|
|
1485
|
+
"valuationFrom",
|
|
1486
|
+
"valuationThrough",
|
|
1487
|
+
"minDevelopmentAge",
|
|
1488
|
+
"maxDevelopmentAge",
|
|
1489
|
+
"instanceIds",
|
|
1490
|
+
]);
|
|
1491
|
+
const CUTOFF_KEYS: ReadonlySet<string> = new Set([
|
|
1492
|
+
"sourceGroup",
|
|
1493
|
+
"originThrough",
|
|
1494
|
+
"valuationThrough",
|
|
1495
|
+
]);
|
|
1496
|
+
const EXPECTED_CELL_KEYS: ReadonlySet<string> = new Set([
|
|
1497
|
+
"sourceGroup",
|
|
1498
|
+
"origin",
|
|
1499
|
+
"valuation",
|
|
1500
|
+
"source",
|
|
1501
|
+
]);
|
|
1502
|
+
|
|
1222
1503
|
function preparationBoundaryIssues(
|
|
1223
1504
|
value: unknown,
|
|
1224
1505
|
): DiagnosticValidationIssue[] {
|
|
@@ -1238,6 +1519,9 @@ function preparationBoundaryIssues(
|
|
|
1238
1519
|
path: string,
|
|
1239
1520
|
message: string,
|
|
1240
1521
|
) => issues.push({ domain, code, path, message });
|
|
1522
|
+
/** Row path with a constant suffix, built only when an issue cites it. */
|
|
1523
|
+
const at = (prefix: string, index: number, suffix: string) =>
|
|
1524
|
+
`${prefix}[${index}]${suffix}`;
|
|
1241
1525
|
const token = (
|
|
1242
1526
|
item: unknown,
|
|
1243
1527
|
path: string,
|
|
@@ -1253,36 +1537,100 @@ function preparationBoundaryIssues(
|
|
|
1253
1537
|
"Expected a nonempty token with valid Unicode and no U+0000",
|
|
1254
1538
|
);
|
|
1255
1539
|
};
|
|
1540
|
+
/** `token` for a row field; the path text exists only for a reported issue. */
|
|
1541
|
+
const tokenAt = (
|
|
1542
|
+
item: unknown,
|
|
1543
|
+
prefix: string,
|
|
1544
|
+
index: number,
|
|
1545
|
+
suffix: string,
|
|
1546
|
+
domain: "input" | "configuration" = "input",
|
|
1547
|
+
) => {
|
|
1548
|
+
if (typeof item === "string" && isDiagnosticToken(item)) return;
|
|
1549
|
+
token(item, at(prefix, index, suffix), domain);
|
|
1550
|
+
};
|
|
1256
1551
|
const exactKeys = (
|
|
1257
1552
|
item: Record<string, unknown>,
|
|
1258
|
-
allowed:
|
|
1553
|
+
allowed: ReadonlySet<string>,
|
|
1259
1554
|
path: string,
|
|
1260
1555
|
domain: "input" | "configuration",
|
|
1261
1556
|
) => {
|
|
1262
1557
|
for (const key of Object.keys(item)) {
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
issue(domain, "unknown-key", keyPath, `Unknown key ${key}`);
|
|
1558
|
+
if (!allowed.has(key))
|
|
1559
|
+
issue(domain, "unknown-key", `${path}.${key}`, `Unknown key ${key}`);
|
|
1266
1560
|
else if (item[key] === undefined)
|
|
1267
1561
|
issue(
|
|
1268
1562
|
domain,
|
|
1269
1563
|
"invalid-type",
|
|
1270
|
-
|
|
1564
|
+
`${path}.${key}`,
|
|
1271
1565
|
"Explicit undefined is not allowed",
|
|
1272
1566
|
);
|
|
1273
1567
|
}
|
|
1274
1568
|
};
|
|
1569
|
+
/** `exactKeys` for a row, keeping the same own-key order and messages. */
|
|
1570
|
+
const exactKeysAt = (
|
|
1571
|
+
item: Record<string, unknown>,
|
|
1572
|
+
allowed: ReadonlySet<string>,
|
|
1573
|
+
prefix: string,
|
|
1574
|
+
index: number,
|
|
1575
|
+
suffix: string,
|
|
1576
|
+
domain: "input" | "configuration",
|
|
1577
|
+
) => {
|
|
1578
|
+
for (const key in item) {
|
|
1579
|
+
if (!Object.hasOwn(item, key)) continue;
|
|
1580
|
+
if (!allowed.has(key))
|
|
1581
|
+
issue(
|
|
1582
|
+
domain,
|
|
1583
|
+
"unknown-key",
|
|
1584
|
+
`${at(prefix, index, suffix)}.${key}`,
|
|
1585
|
+
`Unknown key ${key}`,
|
|
1586
|
+
);
|
|
1587
|
+
else if (item[key] === undefined)
|
|
1588
|
+
issue(
|
|
1589
|
+
domain,
|
|
1590
|
+
"invalid-type",
|
|
1591
|
+
`${at(prefix, index, suffix)}.${key}`,
|
|
1592
|
+
"Explicit undefined is not allowed",
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
/** Source of a row; every reported path lies under the row's `.source`. */
|
|
1597
|
+
const sourceAt = (item: unknown, prefix: string, index: number) => {
|
|
1598
|
+
if (!isPlainRecord(item)) {
|
|
1599
|
+
issue(
|
|
1600
|
+
"input",
|
|
1601
|
+
"invalid-type",
|
|
1602
|
+
at(prefix, index, ".source"),
|
|
1603
|
+
"Source location must be an object",
|
|
1604
|
+
);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
exactKeysAt(item, SOURCE_LOCATION_KEYS, prefix, index, ".source", "input");
|
|
1608
|
+
tokenAt(item.artifactId, prefix, index, ".source.artifactId");
|
|
1609
|
+
if (item.sourceFile !== undefined)
|
|
1610
|
+
tokenAt(item.sourceFile, prefix, index, ".source.sourceFile");
|
|
1611
|
+
if (item.sourceSheet !== undefined)
|
|
1612
|
+
tokenAt(item.sourceSheet, prefix, index, ".source.sourceSheet");
|
|
1613
|
+
if (item.sourceCell !== undefined)
|
|
1614
|
+
tokenAt(item.sourceCell, prefix, index, ".source.sourceCell");
|
|
1615
|
+
if (
|
|
1616
|
+
item.sourceRow !== undefined &&
|
|
1617
|
+
(typeof item.sourceRow !== "number" ||
|
|
1618
|
+
!Number.isSafeInteger(item.sourceRow) ||
|
|
1619
|
+
item.sourceRow < 0)
|
|
1620
|
+
)
|
|
1621
|
+
issue(
|
|
1622
|
+
"input",
|
|
1623
|
+
"invalid-number",
|
|
1624
|
+
at(prefix, index, ".source.sourceRow"),
|
|
1625
|
+
"Source row must be a nonnegative safe integer",
|
|
1626
|
+
);
|
|
1627
|
+
};
|
|
1275
1628
|
const source = (item: unknown, path: string) => {
|
|
1276
1629
|
if (!isPlainRecord(item)) {
|
|
1277
1630
|
issue("input", "invalid-type", path, "Source location must be an object");
|
|
1278
1631
|
return;
|
|
1279
1632
|
}
|
|
1280
|
-
exactKeys(
|
|
1281
|
-
item,
|
|
1282
|
-
["artifactId", "sourceFile", "sourceSheet", "sourceRow", "sourceCell"],
|
|
1283
|
-
path,
|
|
1284
|
-
"input",
|
|
1285
|
-
);
|
|
1633
|
+
exactKeys(item, SOURCE_LOCATION_KEYS, path, "input");
|
|
1286
1634
|
token(item.artifactId, `${path}.artifactId`);
|
|
1287
1635
|
for (const key of ["sourceFile", "sourceSheet", "sourceCell"] as const)
|
|
1288
1636
|
if (item[key] !== undefined) token(item[key], `${path}.${key}`);
|
|
@@ -1310,141 +1658,104 @@ function preparationBoundaryIssues(
|
|
|
1310
1658
|
}
|
|
1311
1659
|
return item;
|
|
1312
1660
|
};
|
|
1313
|
-
exactKeys(
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
"losses",
|
|
1318
|
-
"exposures",
|
|
1319
|
-
"filter",
|
|
1320
|
-
"completePeriodCutoffs",
|
|
1321
|
-
"expectedCells",
|
|
1322
|
-
],
|
|
1323
|
-
"$",
|
|
1324
|
-
"configuration",
|
|
1325
|
-
);
|
|
1326
|
-
for (const [index, raw] of array(
|
|
1327
|
-
value.losses,
|
|
1328
|
-
"$.losses",
|
|
1329
|
-
"input",
|
|
1330
|
-
).entries()) {
|
|
1331
|
-
const path = `$.losses[${index}]`;
|
|
1661
|
+
exactKeys(value, PREPARATION_INPUT_KEYS, "$", "configuration");
|
|
1662
|
+
const losses = array(value.losses, "$.losses", "input");
|
|
1663
|
+
for (let index = 0; index < losses.length; index++) {
|
|
1664
|
+
const raw: unknown = losses[index];
|
|
1332
1665
|
if (!isPlainRecord(raw)) {
|
|
1333
|
-
issue(
|
|
1666
|
+
issue(
|
|
1667
|
+
"input",
|
|
1668
|
+
"invalid-type",
|
|
1669
|
+
at("$.losses", index, ""),
|
|
1670
|
+
"Loss record must be an object",
|
|
1671
|
+
);
|
|
1334
1672
|
continue;
|
|
1335
1673
|
}
|
|
1336
|
-
|
|
1337
|
-
raw,
|
|
1338
|
-
[
|
|
1339
|
-
"rowType",
|
|
1340
|
-
"recordId",
|
|
1341
|
-
"claimId",
|
|
1342
|
-
"sourceGroup",
|
|
1343
|
-
"origin",
|
|
1344
|
-
"valuation",
|
|
1345
|
-
"complete",
|
|
1346
|
-
"source",
|
|
1347
|
-
"measures",
|
|
1348
|
-
],
|
|
1349
|
-
path,
|
|
1350
|
-
"input",
|
|
1351
|
-
);
|
|
1674
|
+
exactKeysAt(raw, LOSS_ROW_KEYS, "$.losses", index, "", "input");
|
|
1352
1675
|
if (raw.rowType !== "claim" && raw.rowType !== "aggregate")
|
|
1353
1676
|
issue(
|
|
1354
1677
|
"input",
|
|
1355
1678
|
"invalid-type",
|
|
1356
|
-
|
|
1679
|
+
at("$.losses", index, ".rowType"),
|
|
1357
1680
|
"Loss row type must be claim or aggregate",
|
|
1358
1681
|
);
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
if (raw.rowType === "claim")
|
|
1682
|
+
tokenAt(raw.recordId, "$.losses", index, ".recordId");
|
|
1683
|
+
tokenAt(raw.sourceGroup, "$.losses", index, ".sourceGroup");
|
|
1684
|
+
tokenAt(raw.origin, "$.losses", index, ".origin");
|
|
1685
|
+
tokenAt(raw.valuation, "$.losses", index, ".valuation");
|
|
1686
|
+
if (raw.rowType === "claim")
|
|
1687
|
+
tokenAt(raw.claimId, "$.losses", index, ".claimId");
|
|
1364
1688
|
else if (raw.claimId !== undefined)
|
|
1365
1689
|
issue(
|
|
1366
1690
|
"input",
|
|
1367
1691
|
"unknown-key",
|
|
1368
|
-
|
|
1692
|
+
at("$.losses", index, ".claimId"),
|
|
1369
1693
|
"Aggregate loss rows cannot contain claimId",
|
|
1370
1694
|
);
|
|
1371
1695
|
if (typeof raw.complete !== "boolean")
|
|
1372
1696
|
issue(
|
|
1373
1697
|
"input",
|
|
1374
1698
|
"invalid-type",
|
|
1375
|
-
|
|
1699
|
+
at("$.losses", index, ".complete"),
|
|
1376
1700
|
"Loss completeness must be boolean",
|
|
1377
1701
|
);
|
|
1378
|
-
if (raw.source !== undefined)
|
|
1379
|
-
|
|
1702
|
+
if (raw.source !== undefined) sourceAt(raw.source, "$.losses", index);
|
|
1703
|
+
const measures: unknown = raw.measures;
|
|
1704
|
+
if (!isPlainRecord(measures))
|
|
1380
1705
|
issue(
|
|
1381
1706
|
"input",
|
|
1382
1707
|
"invalid-type",
|
|
1383
|
-
|
|
1708
|
+
at("$.losses", index, ".measures"),
|
|
1384
1709
|
"Measures must be an object",
|
|
1385
1710
|
);
|
|
1386
1711
|
else
|
|
1387
|
-
for (const
|
|
1388
|
-
|
|
1712
|
+
for (const measureId in measures) {
|
|
1713
|
+
if (!Object.hasOwn(measures, measureId)) continue;
|
|
1714
|
+
tokenAt(measureId, "$.losses", index, ".measures");
|
|
1715
|
+
const amount: unknown = measures[measureId];
|
|
1389
1716
|
if (amount !== null && typeof amount !== "number")
|
|
1390
1717
|
issue(
|
|
1391
1718
|
"input",
|
|
1392
1719
|
"invalid-type",
|
|
1393
|
-
`${
|
|
1720
|
+
`${at("$.losses", index, ".measures")}.${measureId}`,
|
|
1394
1721
|
"Measure value must be a number or null",
|
|
1395
1722
|
);
|
|
1396
1723
|
}
|
|
1397
1724
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
"input",
|
|
1402
|
-
).entries()) {
|
|
1403
|
-
const path = `$.exposures[${index}]`;
|
|
1725
|
+
const exposures = array(value.exposures, "$.exposures", "input");
|
|
1726
|
+
for (let index = 0; index < exposures.length; index++) {
|
|
1727
|
+
const raw: unknown = exposures[index];
|
|
1404
1728
|
if (!isPlainRecord(raw)) {
|
|
1405
1729
|
issue(
|
|
1406
1730
|
"input",
|
|
1407
1731
|
"invalid-type",
|
|
1408
|
-
|
|
1732
|
+
at("$.exposures", index, ""),
|
|
1409
1733
|
"Exposure observation must be an object",
|
|
1410
1734
|
);
|
|
1411
1735
|
continue;
|
|
1412
1736
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
"measureId",
|
|
1421
|
-
"value",
|
|
1422
|
-
"complete",
|
|
1423
|
-
"source",
|
|
1424
|
-
],
|
|
1425
|
-
path,
|
|
1426
|
-
"input",
|
|
1427
|
-
);
|
|
1428
|
-
token(raw.key, `${path}.key`);
|
|
1429
|
-
token(raw.sourceGroup, `${path}.sourceGroup`);
|
|
1430
|
-
token(raw.origin, `${path}.origin`);
|
|
1431
|
-
token(raw.measureId, `${path}.measureId`);
|
|
1432
|
-
if (raw.valuation !== undefined) token(raw.valuation, `${path}.valuation`);
|
|
1737
|
+
exactKeysAt(raw, EXPOSURE_ROW_KEYS, "$.exposures", index, "", "input");
|
|
1738
|
+
tokenAt(raw.key, "$.exposures", index, ".key");
|
|
1739
|
+
tokenAt(raw.sourceGroup, "$.exposures", index, ".sourceGroup");
|
|
1740
|
+
tokenAt(raw.origin, "$.exposures", index, ".origin");
|
|
1741
|
+
tokenAt(raw.measureId, "$.exposures", index, ".measureId");
|
|
1742
|
+
if (raw.valuation !== undefined)
|
|
1743
|
+
tokenAt(raw.valuation, "$.exposures", index, ".valuation");
|
|
1433
1744
|
if (raw.value !== null && typeof raw.value !== "number")
|
|
1434
1745
|
issue(
|
|
1435
1746
|
"input",
|
|
1436
1747
|
"invalid-type",
|
|
1437
|
-
|
|
1748
|
+
at("$.exposures", index, ".value"),
|
|
1438
1749
|
"Exposure value must be a number or null",
|
|
1439
1750
|
);
|
|
1440
1751
|
if (typeof raw.complete !== "boolean")
|
|
1441
1752
|
issue(
|
|
1442
1753
|
"input",
|
|
1443
1754
|
"invalid-type",
|
|
1444
|
-
|
|
1755
|
+
at("$.exposures", index, ".complete"),
|
|
1445
1756
|
"Exposure completeness must be boolean",
|
|
1446
1757
|
);
|
|
1447
|
-
if (raw.source !== undefined)
|
|
1758
|
+
if (raw.source !== undefined) sourceAt(raw.source, "$.exposures", index);
|
|
1448
1759
|
}
|
|
1449
1760
|
if (value.filter !== undefined && !isPlainRecord(value.filter))
|
|
1450
1761
|
issue(
|
|
@@ -1455,20 +1766,7 @@ function preparationBoundaryIssues(
|
|
|
1455
1766
|
);
|
|
1456
1767
|
if (isPlainRecord(value.filter)) {
|
|
1457
1768
|
const filter = value.filter;
|
|
1458
|
-
|
|
1459
|
-
"sourceGroups",
|
|
1460
|
-
"outputGroups",
|
|
1461
|
-
"origins",
|
|
1462
|
-
"originFrom",
|
|
1463
|
-
"originThrough",
|
|
1464
|
-
"valuations",
|
|
1465
|
-
"valuationFrom",
|
|
1466
|
-
"valuationThrough",
|
|
1467
|
-
"minDevelopmentAge",
|
|
1468
|
-
"maxDevelopmentAge",
|
|
1469
|
-
"instanceIds",
|
|
1470
|
-
] as const;
|
|
1471
|
-
exactKeys(filter, keys, "$.filter", "configuration");
|
|
1769
|
+
exactKeys(filter, FILTER_KEYS, "$.filter", "configuration");
|
|
1472
1770
|
for (const key of [
|
|
1473
1771
|
"sourceGroups",
|
|
1474
1772
|
"outputGroups",
|
|
@@ -1506,12 +1804,7 @@ function preparationBoundaryIssues(
|
|
|
1506
1804
|
);
|
|
1507
1805
|
continue;
|
|
1508
1806
|
}
|
|
1509
|
-
exactKeys(
|
|
1510
|
-
raw,
|
|
1511
|
-
["sourceGroup", "originThrough", "valuationThrough"],
|
|
1512
|
-
path,
|
|
1513
|
-
"configuration",
|
|
1514
|
-
);
|
|
1807
|
+
exactKeys(raw, CUTOFF_KEYS, path, "configuration");
|
|
1515
1808
|
token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
|
|
1516
1809
|
for (const key of ["originThrough", "valuationThrough"] as const)
|
|
1517
1810
|
if (raw[key] !== null)
|
|
@@ -1533,12 +1826,7 @@ function preparationBoundaryIssues(
|
|
|
1533
1826
|
);
|
|
1534
1827
|
continue;
|
|
1535
1828
|
}
|
|
1536
|
-
exactKeys(
|
|
1537
|
-
raw,
|
|
1538
|
-
["sourceGroup", "origin", "valuation", "source"],
|
|
1539
|
-
path,
|
|
1540
|
-
"configuration",
|
|
1541
|
-
);
|
|
1829
|
+
exactKeys(raw, EXPECTED_CELL_KEYS, path, "configuration");
|
|
1542
1830
|
token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
|
|
1543
1831
|
token(raw.origin, `${path}.origin`, "configuration");
|
|
1544
1832
|
token(raw.valuation, `${path}.valuation`, "configuration");
|
|
@@ -1689,19 +1977,28 @@ function prepareDiagnosticDataInMode(
|
|
|
1689
1977
|
const filter = normalizedFilter.value;
|
|
1690
1978
|
const findings: DiagnosticMetricFinding[] = [];
|
|
1691
1979
|
const pendingBlockers: PendingBlocker[] = [];
|
|
1980
|
+
// Audited numbers are immutable scalar records repeated across rows; one
|
|
1981
|
+
// owned instance per distinct value serves every snapshot of this preparation.
|
|
1982
|
+
const auditedNumbers = new Map<number | null, DiagnosticAuditedNumericValue>();
|
|
1983
|
+
const auditedNumber = (value: number | null): DiagnosticAuditedNumericValue => {
|
|
1984
|
+
let audited = auditedNumbers.get(value);
|
|
1985
|
+
if (audited === undefined) {
|
|
1986
|
+
audited = auditDiagnosticNumber(value);
|
|
1987
|
+
auditedNumbers.set(value, audited);
|
|
1988
|
+
}
|
|
1989
|
+
return audited;
|
|
1990
|
+
};
|
|
1991
|
+
// Source clones are complete once copied; freezing them here lets the owned
|
|
1992
|
+
// freeze walk treat every citation list attached below as finished.
|
|
1692
1993
|
const lossCandidates: LossCandidate[] = input.losses.map((inputRow) => ({
|
|
1693
1994
|
row: {
|
|
1694
1995
|
...inputRow,
|
|
1695
1996
|
...(inputRow.source === undefined
|
|
1696
1997
|
? {}
|
|
1697
|
-
: { source: { ...inputRow.source } }),
|
|
1698
|
-
measures:
|
|
1699
|
-
Object.keys(inputRow.measures)
|
|
1700
|
-
.sort(codeUnit)
|
|
1701
|
-
.map((key) => [key, inputRow.measures[key]!]),
|
|
1702
|
-
),
|
|
1998
|
+
: { source: Object.freeze({ ...inputRow.source }) }),
|
|
1999
|
+
measures: sortedRecordCopy(inputRow.measures, (value) => value),
|
|
1703
2000
|
} as DiagnosticLossInput,
|
|
1704
|
-
snapshot: snapshotLoss(inputRow),
|
|
2001
|
+
snapshot: snapshotLoss(inputRow, auditedNumber),
|
|
1705
2002
|
disposition: selectedBySource(filter, inputRow.sourceGroup)
|
|
1706
2003
|
? "retained"
|
|
1707
2004
|
: "filter",
|
|
@@ -1731,8 +2028,6 @@ function prepareDiagnosticDataInMode(
|
|
|
1731
2028
|
}),
|
|
1732
2029
|
);
|
|
1733
2030
|
}
|
|
1734
|
-
if (origin !== null)
|
|
1735
|
-
candidate.snapshot = { ...candidate.snapshot, origin: origin.label };
|
|
1736
2031
|
try {
|
|
1737
2032
|
valuation = normalizeDiagnosticPeriod(
|
|
1738
2033
|
definition,
|
|
@@ -1750,10 +2045,24 @@ function prepareDiagnosticDataInMode(
|
|
|
1750
2045
|
}),
|
|
1751
2046
|
);
|
|
1752
2047
|
}
|
|
1753
|
-
|
|
2048
|
+
// One snapshot copy carries every normalized label; overriding existing
|
|
2049
|
+
// keys keeps the original key order exactly as successive copies did, and
|
|
2050
|
+
// a snapshot whose labels were canonical already stays the same record.
|
|
2051
|
+
const originChanged = origin !== null && candidate.snapshot.origin !== origin.label;
|
|
2052
|
+
const valuationChanged =
|
|
2053
|
+
valuation !== null && candidate.snapshot.valuation !== valuation.label;
|
|
2054
|
+
if (originChanged && valuationChanged)
|
|
1754
2055
|
candidate.snapshot = {
|
|
1755
2056
|
...candidate.snapshot,
|
|
1756
|
-
|
|
2057
|
+
origin: origin!.label,
|
|
2058
|
+
valuation: valuation!.label,
|
|
2059
|
+
};
|
|
2060
|
+
else if (originChanged)
|
|
2061
|
+
candidate.snapshot = { ...candidate.snapshot, origin: origin!.label };
|
|
2062
|
+
else if (valuationChanged)
|
|
2063
|
+
candidate.snapshot = {
|
|
2064
|
+
...candidate.snapshot,
|
|
2065
|
+
valuation: valuation!.label,
|
|
1757
2066
|
};
|
|
1758
2067
|
if (origin === null || valuation === null) {
|
|
1759
2068
|
candidate.disposition = "invalid";
|
|
@@ -1787,16 +2096,13 @@ function prepareDiagnosticDataInMode(
|
|
|
1787
2096
|
developmentAge,
|
|
1788
2097
|
ageUnit: definition.definition.periodAxis.ageUnit,
|
|
1789
2098
|
};
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
origin: candidate.row.origin,
|
|
1798
|
-
valuation: candidate.row.valuation,
|
|
1799
|
-
};
|
|
2099
|
+
// Canonical labels are the common case; copy only when a label changes.
|
|
2100
|
+
if (candidate.row.origin !== origin.label || candidate.row.valuation !== valuation.label)
|
|
2101
|
+
candidate.row = {
|
|
2102
|
+
...candidate.row,
|
|
2103
|
+
origin: origin.label,
|
|
2104
|
+
valuation: valuation.label,
|
|
2105
|
+
} as DiagnosticLossInput;
|
|
1800
2106
|
if (
|
|
1801
2107
|
beyondCutoff(
|
|
1802
2108
|
candidate as LossCandidate & { coordinate: NormalizedCoordinate },
|
|
@@ -1824,7 +2130,7 @@ function prepareDiagnosticDataInMode(
|
|
|
1824
2130
|
? {}
|
|
1825
2131
|
: { source: { ...inputRow.source } }),
|
|
1826
2132
|
},
|
|
1827
|
-
snapshot: snapshotExposure(inputRow),
|
|
2133
|
+
snapshot: snapshotExposure(inputRow, auditedNumber),
|
|
1828
2134
|
disposition: selectedBySource(filter, inputRow.sourceGroup)
|
|
1829
2135
|
? "retained"
|
|
1830
2136
|
: "filter",
|
|
@@ -2091,7 +2397,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2091
2397
|
const claimId = (candidate.row as DiagnosticClaimObservation).claimId;
|
|
2092
2398
|
addToMap(
|
|
2093
2399
|
bySnapshot,
|
|
2094
|
-
|
|
2400
|
+
canonicalScalarArrayJson([
|
|
2095
2401
|
claimId,
|
|
2096
2402
|
candidate.row.sourceGroup,
|
|
2097
2403
|
candidate.row.origin,
|
|
@@ -2110,7 +2416,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2110
2416
|
if (
|
|
2111
2417
|
uniqueSorted(
|
|
2112
2418
|
cohort.map((candidate) =>
|
|
2113
|
-
|
|
2419
|
+
canonicalScalarArrayJson([candidate.row.sourceGroup, candidate.row.origin]),
|
|
2114
2420
|
),
|
|
2115
2421
|
).length > 1
|
|
2116
2422
|
)
|
|
@@ -2171,7 +2477,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2171
2477
|
const timing = internals.measuresById.get(
|
|
2172
2478
|
exposure.measureId,
|
|
2173
2479
|
)?.exposureTiming;
|
|
2174
|
-
const identity =
|
|
2480
|
+
const identity = canonicalScalarArrayJson(
|
|
2175
2481
|
timing === "valuation-specific"
|
|
2176
2482
|
? [
|
|
2177
2483
|
exposure.measureId,
|
|
@@ -2236,23 +2542,29 @@ function prepareDiagnosticDataInMode(
|
|
|
2236
2542
|
]),
|
|
2237
2543
|
);
|
|
2238
2544
|
const cellRows = new Map<string, DiagnosticLossInput[]>();
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2545
|
+
// The coordinate behind each cell key, so it is never parsed back from text.
|
|
2546
|
+
const cellCoordinates = new Map<string, readonly [string, string, string]>();
|
|
2547
|
+
for (const candidate of retainedCandidates) {
|
|
2548
|
+
const key = cellKey(
|
|
2549
|
+
candidate.row.sourceGroup,
|
|
2550
|
+
candidate.row.origin,
|
|
2551
|
+
candidate.row.valuation,
|
|
2552
|
+
);
|
|
2553
|
+
if (!cellCoordinates.has(key))
|
|
2554
|
+
cellCoordinates.set(key, [
|
|
2243
2555
|
candidate.row.sourceGroup,
|
|
2244
2556
|
candidate.row.origin,
|
|
2245
2557
|
candidate.row.valuation,
|
|
2246
|
-
)
|
|
2247
|
-
|
|
2248
|
-
|
|
2558
|
+
]);
|
|
2559
|
+
addToMap(cellRows, key, derivedByRecord.get(candidate.row.recordId)!);
|
|
2560
|
+
}
|
|
2249
2561
|
|
|
2250
2562
|
// Exposure attachment is a coordinate join. Index the established cell order
|
|
2251
2563
|
// once; scanning every cell for each origin-static exposure is quadratic.
|
|
2252
2564
|
const cellKeysBySourceOrigin = new Map<string, string[]>();
|
|
2253
2565
|
for (const key of cellRows.keys()) {
|
|
2254
|
-
const [sourceGroup, origin] =
|
|
2255
|
-
addToMap(cellKeysBySourceOrigin,
|
|
2566
|
+
const [sourceGroup, origin] = cellCoordinates.get(key)!;
|
|
2567
|
+
addToMap(cellKeysBySourceOrigin, sourceOriginKey(sourceGroup, origin), key);
|
|
2256
2568
|
}
|
|
2257
2569
|
|
|
2258
2570
|
const allMeasureIds = definition.definition.measures
|
|
@@ -2280,7 +2592,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2280
2592
|
);
|
|
2281
2593
|
else
|
|
2282
2594
|
for (const key of cellKeysBySourceOrigin.get(
|
|
2283
|
-
|
|
2595
|
+
sourceOriginKey(candidate.row.sourceGroup, candidate.origin.label),
|
|
2284
2596
|
) ?? [])
|
|
2285
2597
|
targets.add(key);
|
|
2286
2598
|
}
|
|
@@ -2314,7 +2626,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2314
2626
|
const keys =
|
|
2315
2627
|
timing === "origin-static"
|
|
2316
2628
|
? (cellKeysBySourceOrigin.get(
|
|
2317
|
-
|
|
2629
|
+
sourceOriginKey(exposure.sourceGroup, exposure.origin),
|
|
2318
2630
|
) ?? [])
|
|
2319
2631
|
: coordinateKey !== null && cellRows.has(coordinateKey)
|
|
2320
2632
|
? [coordinateKey]
|
|
@@ -2338,23 +2650,32 @@ function prepareDiagnosticDataInMode(
|
|
|
2338
2650
|
// and cells. Share only freshly computed owned values, never contributions,
|
|
2339
2651
|
// source identities, prepared-cell envelopes, or authenticated SDK handles.
|
|
2340
2652
|
// The invocation-scoped pool is bounded and released after preparation.
|
|
2341
|
-
const componentSharing =
|
|
2653
|
+
const componentSharing = createDiagnosticStatsInterner();
|
|
2654
|
+
// Clean cells share immutable empties; readers only iterate these lists.
|
|
2655
|
+
const noBlockers: readonly DiagnosticStructuralBlocker[] = Object.freeze([]);
|
|
2656
|
+
const noFindings = Object.freeze([]) as unknown as DiagnosticMetricFinding[];
|
|
2342
2657
|
|
|
2343
2658
|
for (const [key, rows] of cellRows) {
|
|
2344
|
-
const
|
|
2659
|
+
const coordinates = cellCoordinates.get(key)!;
|
|
2660
|
+
const sourceGroup = coordinates[0];
|
|
2661
|
+
const origin = coordinates[1];
|
|
2662
|
+
const valuation = coordinates[2];
|
|
2345
2663
|
const coordinate = coordinateFor(definition, origin, valuation).coordinate!;
|
|
2346
|
-
const contributions =
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2664
|
+
const contributions = emptyListsByMeasure<DiagnosticMeasureContribution>(allMeasureIds);
|
|
2665
|
+
// Structural blockers are rare; every measure starts on the shared empty
|
|
2666
|
+
// list and only a measure that actually gets one allocates its own.
|
|
2667
|
+
const blockers = sharedEmptyListsByMeasure<DiagnosticStructuralBlocker>(
|
|
2668
|
+
allMeasureIds,
|
|
2669
|
+
noBlockers,
|
|
2670
|
+
);
|
|
2671
|
+
const addBlocker = (
|
|
2672
|
+
measureId: string,
|
|
2673
|
+
blocker: DiagnosticStructuralBlocker,
|
|
2674
|
+
): void => {
|
|
2675
|
+
const existing = blockers[measureId]!;
|
|
2676
|
+
if (existing === noBlockers) setMeasureEntry(blockers, measureId, [blocker]);
|
|
2677
|
+
else (existing as DiagnosticStructuralBlocker[]).push(blocker);
|
|
2678
|
+
};
|
|
2358
2679
|
const cellFindings: DiagnosticMetricFinding[] = [];
|
|
2359
2680
|
const derivedContribution = (
|
|
2360
2681
|
row: DiagnosticLossInput,
|
|
@@ -2379,8 +2700,14 @@ function prepareDiagnosticDataInMode(
|
|
|
2379
2700
|
? { ...base, status: "imputed-zero", value: 0 }
|
|
2380
2701
|
: { ...base, status: "missing", value: null };
|
|
2381
2702
|
};
|
|
2382
|
-
for (
|
|
2383
|
-
|
|
2703
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
2704
|
+
const row = rows[rowIndex]!;
|
|
2705
|
+
// One frozen citation list per row serves every measure it contributes to.
|
|
2706
|
+
const rowSources: readonly DiagnosticSourceLocation[] = Object.freeze(
|
|
2707
|
+
row.source ? [row.source] : [],
|
|
2708
|
+
);
|
|
2709
|
+
for (let at = 0; at < nonExposureMeasures.length; at++) {
|
|
2710
|
+
const measure = nonExposureMeasures[at]!;
|
|
2384
2711
|
if (measure.source === "derived") {
|
|
2385
2712
|
const state = derivedAuditByRecord.get(row.recordId)!.derived[
|
|
2386
2713
|
measure.id
|
|
@@ -2408,14 +2735,15 @@ function prepareDiagnosticDataInMode(
|
|
|
2408
2735
|
}
|
|
2409
2736
|
} else
|
|
2410
2737
|
contributions[measure.id]!.push(
|
|
2411
|
-
|
|
2738
|
+
ownedDiagnosticContribution(
|
|
2412
2739
|
row.recordId,
|
|
2413
2740
|
own(row.measures, measure.id),
|
|
2414
2741
|
measure.missing,
|
|
2415
|
-
|
|
2742
|
+
rowSources,
|
|
2416
2743
|
),
|
|
2417
2744
|
);
|
|
2418
2745
|
}
|
|
2746
|
+
}
|
|
2419
2747
|
const attachedExposures = validExposuresByCell.get(key) ?? [];
|
|
2420
2748
|
for (const exposure of attachedExposures)
|
|
2421
2749
|
contributions[exposure.measureId]!.push(
|
|
@@ -2436,8 +2764,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2436
2764
|
sources: normalizeSources(pending.finding.sources),
|
|
2437
2765
|
finding: pending.finding,
|
|
2438
2766
|
};
|
|
2439
|
-
for (const measureId of pending.measureIds)
|
|
2440
|
-
blockers[measureId]!.push(blocker);
|
|
2767
|
+
for (const measureId of pending.measureIds) addBlocker(measureId, blocker);
|
|
2441
2768
|
}
|
|
2442
2769
|
for (const measure of exposureMeasures) {
|
|
2443
2770
|
const attempted =
|
|
@@ -2457,7 +2784,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2457
2784
|
});
|
|
2458
2785
|
findings.push(finding);
|
|
2459
2786
|
cellFindings.push(finding);
|
|
2460
|
-
|
|
2787
|
+
addBlocker(measure.id, {
|
|
2461
2788
|
code: finding.code,
|
|
2462
2789
|
message: finding.message,
|
|
2463
2790
|
sourceIds: uniqueSorted(rows.map((row) => row.recordId)),
|
|
@@ -2467,32 +2794,39 @@ function prepareDiagnosticDataInMode(
|
|
|
2467
2794
|
}
|
|
2468
2795
|
}
|
|
2469
2796
|
for (const measureId of allMeasureIds) {
|
|
2470
|
-
blockers[measureId]
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
blocker,
|
|
2475
|
-
]),
|
|
2476
|
-
).values(),
|
|
2477
|
-
].sort(compareDiagnosticBlockers);
|
|
2478
|
-
contributions[measureId]!.sort(compareDiagnosticContributions);
|
|
2479
|
-
}
|
|
2480
|
-
const components = Object.fromEntries(
|
|
2481
|
-
allMeasureIds.map((measureId) => {
|
|
2482
|
-
const measure = internals.measuresById.get(measureId)!;
|
|
2483
|
-
return [
|
|
2797
|
+
const measureBlockers = blockers[measureId]!;
|
|
2798
|
+
if (measureBlockers.length > 0)
|
|
2799
|
+
setMeasureEntry(
|
|
2800
|
+
blockers,
|
|
2484
2801
|
measureId,
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2802
|
+
[
|
|
2803
|
+
...new Map(
|
|
2804
|
+
measureBlockers.map((blocker) => [canonicalJson(blocker), blocker]),
|
|
2805
|
+
).values(),
|
|
2806
|
+
].sort(compareDiagnosticBlockers),
|
|
2807
|
+
);
|
|
2808
|
+
if (contributions[measureId]!.length > 1)
|
|
2809
|
+
contributions[measureId]!.sort(compareDiagnosticContributions);
|
|
2810
|
+
}
|
|
2811
|
+
const components: Record<string, DiagnosticMeasureStats> = {};
|
|
2812
|
+
for (let at = 0; at < allMeasureIds.length; at++) {
|
|
2813
|
+
const measureId = allMeasureIds[at]!;
|
|
2814
|
+
const measure = internals.measuresById.get(measureId)!;
|
|
2815
|
+
const stats = finalizeDiagnosticContributionsInterned(
|
|
2816
|
+
contributions[measureId]!,
|
|
2817
|
+
measure.missing,
|
|
2818
|
+
blockers[measureId]!,
|
|
2819
|
+
componentSharing,
|
|
2820
|
+
);
|
|
2821
|
+
if (measureId === "__proto__")
|
|
2822
|
+
Object.defineProperty(components, measureId, {
|
|
2823
|
+
value: stats,
|
|
2824
|
+
enumerable: true,
|
|
2825
|
+
writable: true,
|
|
2826
|
+
configurable: true,
|
|
2827
|
+
});
|
|
2828
|
+
else components[measureId] = stats;
|
|
2829
|
+
}
|
|
2496
2830
|
for (const measureId of allMeasureIds) {
|
|
2497
2831
|
const stats = components[measureId]!;
|
|
2498
2832
|
// The finalized counts already prove whether any per-status finding is
|
|
@@ -2558,11 +2892,14 @@ function prepareDiagnosticDataInMode(
|
|
|
2558
2892
|
valuation,
|
|
2559
2893
|
developmentAge: coordinate.developmentAge,
|
|
2560
2894
|
ageUnit: coordinate.ageUnit,
|
|
2561
|
-
lossRecordIds:
|
|
2895
|
+
lossRecordIds:
|
|
2896
|
+
rows.length === 1
|
|
2897
|
+
? [rows[0]!.recordId]
|
|
2898
|
+
: uniqueSorted(rows.map((row) => row.recordId)),
|
|
2562
2899
|
contributions,
|
|
2563
2900
|
components,
|
|
2564
2901
|
structuralBlockers: blockers,
|
|
2565
|
-
findings: mergeFindings(cellFindings),
|
|
2902
|
+
findings: cellFindings.length === 0 ? noFindings : mergeFindings(cellFindings),
|
|
2566
2903
|
});
|
|
2567
2904
|
}
|
|
2568
2905
|
|
|
@@ -2625,7 +2962,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2625
2962
|
),
|
|
2626
2963
|
)
|
|
2627
2964
|
.map((candidate) => candidate.row);
|
|
2628
|
-
const
|
|
2965
|
+
const auditOrder = createAuditComparator();
|
|
2629
2966
|
const audit: DiagnosticInputAuditRecord[] = [
|
|
2630
2967
|
...lossCandidates.map((candidate): DiagnosticInputAuditRecord => ({
|
|
2631
2968
|
kind: "loss",
|
|
@@ -2642,20 +2979,26 @@ function prepareDiagnosticDataInMode(
|
|
|
2642
2979
|
disposition: candidate.disposition,
|
|
2643
2980
|
record: candidate.snapshot,
|
|
2644
2981
|
})),
|
|
2645
|
-
]
|
|
2982
|
+
];
|
|
2983
|
+
sortAuditRecords(audit, auditOrder);
|
|
2646
2984
|
const normalizedFindings = mergeFindings(findings);
|
|
2647
|
-
|
|
2985
|
+
// Freeze each owned graph exactly once. Cells dominate large preparations;
|
|
2986
|
+
// walking them a second time from the envelope would double the cost, and
|
|
2987
|
+
// their shared children (contributions, statistics, citation lists, audited
|
|
2988
|
+
// numbers, empty records) were attached frozen and complete, so the owned
|
|
2989
|
+
// walk skips them instead of re-visiting each per reference.
|
|
2990
|
+
const frozenCells = Object.freeze(preparedCells.map((cell) => deepFreezeOwned(cell)));
|
|
2648
2991
|
const cutoffValues = normalizedCutoffs.map((cutoff) => cutoff.value);
|
|
2649
|
-
const ownedData =
|
|
2992
|
+
const ownedData = Object.freeze({
|
|
2650
2993
|
definitionIntegrity: definition.definitionIntegrity,
|
|
2651
|
-
filter,
|
|
2652
|
-
completePeriodCutoffs: cutoffValues,
|
|
2653
|
-
inputAudit: audit,
|
|
2994
|
+
filter: deepFreeze(filter),
|
|
2995
|
+
completePeriodCutoffs: deepFreeze(cutoffValues),
|
|
2996
|
+
inputAudit: deepFreezeOwned(audit),
|
|
2654
2997
|
cells: frozenCells,
|
|
2655
|
-
exposures: reconciled,
|
|
2998
|
+
exposures: deepFreeze(reconciled),
|
|
2656
2999
|
expectedCellsProvided: input.expectedCells !== undefined,
|
|
2657
|
-
expectedCells,
|
|
2658
|
-
findings: normalizedFindings,
|
|
3000
|
+
expectedCells: deepFreeze(expectedCells),
|
|
3001
|
+
findings: deepFreeze(normalizedFindings),
|
|
2659
3002
|
});
|
|
2660
3003
|
if (mode === "compact") {
|
|
2661
3004
|
// Every child was frozen above (the compiled definition owns its snapshot).
|
|
@@ -2664,31 +3007,35 @@ function prepareDiagnosticDataInMode(
|
|
|
2664
3007
|
...ownedData,
|
|
2665
3008
|
}) as unknown as CompactPreparedDiagnosticData;
|
|
2666
3009
|
authenticCompact.add(prepared);
|
|
3010
|
+
preparedSourceGroups.set(prepared, knownSourceGroups);
|
|
2667
3011
|
if (definition.definition.lossRowGrain === "aggregate" && [...knownSourceGroups].every((group) => selectedBySource(filter, group)) &&
|
|
2668
3012
|
normalizedFindings.length === 0 && reconciled.every((exposure) => exposure.status === "valid")) {
|
|
3013
|
+
const computeSelectionMetadata = (): CompactSelectionMetadata => {
|
|
2669
3014
|
const excluded = new Map<object, DiagnosticInputAuditRecord>();
|
|
2670
3015
|
lossCandidates.forEach((candidate, index) => {
|
|
2671
3016
|
const raw = rawLossSnapshots[index]!;
|
|
2672
3017
|
// Normalization changes only the coordinate labels in these snapshots.
|
|
2673
3018
|
// When those were canonical already, reuse the same complete record.
|
|
3019
|
+
// Raw snapshots were never attached to the audit, so freeze them here.
|
|
2674
3020
|
const record = raw.origin === candidate.snapshot.origin && raw.valuation === candidate.snapshot.valuation
|
|
2675
|
-
? candidate.snapshot : raw;
|
|
2676
|
-
excluded.set(candidate.snapshot,
|
|
3021
|
+
? candidate.snapshot : deepFreeze(raw);
|
|
3022
|
+
excluded.set(candidate.snapshot, Object.freeze({ kind: "loss", disposition: "filter", record }));
|
|
2677
3023
|
});
|
|
2678
3024
|
exposureCandidates.forEach((candidate, index) => {
|
|
2679
3025
|
const raw = rawExposureSnapshots[index]!;
|
|
2680
3026
|
const record = raw.origin === candidate.snapshot.origin && raw.valuation === candidate.snapshot.valuation
|
|
2681
|
-
? candidate.snapshot : raw;
|
|
2682
|
-
excluded.set(candidate.snapshot,
|
|
3027
|
+
? candidate.snapshot : deepFreeze(raw);
|
|
3028
|
+
excluded.set(candidate.snapshot, Object.freeze({ kind: "exposure", disposition: "filter", record }));
|
|
2683
3029
|
});
|
|
2684
|
-
expectedCandidates.forEach((candidate) => excluded.set(candidate.snapshot,
|
|
3030
|
+
expectedCandidates.forEach((candidate) => excluded.set(candidate.snapshot, Object.freeze({
|
|
2685
3031
|
kind: "expected-cell", disposition: "filter", record: candidate.snapshot,
|
|
2686
3032
|
})));
|
|
2687
3033
|
// Rank every possible included/excluded audit variant once. Query sorts
|
|
2688
3034
|
// then use only numbers; canonical record strings live only in this
|
|
2689
|
-
// preparation's comparator and are not retained by the context.
|
|
3035
|
+
// preparation's comparator and are not retained by the context. Every
|
|
3036
|
+
// audit record was deeply frozen with the envelope above.
|
|
2690
3037
|
const originExcluded = new Map<object, DiagnosticInputAuditRecord>(audit.map((item) => [item.record,
|
|
2691
|
-
|
|
3038
|
+
Object.freeze({ ...item, disposition: "filter" as const })]));
|
|
2692
3039
|
const origins = new Map<string, DiagnosticNormalizedPeriod>();
|
|
2693
3040
|
for (const candidate of lossCandidates) if (candidate.coordinate)
|
|
2694
3041
|
origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
|
|
@@ -2696,7 +3043,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2696
3043
|
origins.set(candidate.origin.label, candidate.origin);
|
|
2697
3044
|
for (const candidate of expectedCandidates)
|
|
2698
3045
|
origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
|
|
2699
|
-
const orderedVariants =
|
|
3046
|
+
const orderedVariants = orderAuditVariants(audit, excluded, originExcluded, auditOrder);
|
|
2700
3047
|
const auditRanks = new Map(orderedVariants.map((item, index) => [item, index]));
|
|
2701
3048
|
const allOriginsSelected = [...origins.values()].every((origin) =>
|
|
2702
3049
|
selectedByOrigin(filter, origin, normalizedFilter.originCoordinates));
|
|
@@ -2713,12 +3060,17 @@ function prepareDiagnosticDataInMode(
|
|
|
2713
3060
|
rememberCoordinate(candidate.coordinate.valuation, candidate.coordinate.developmentAge);
|
|
2714
3061
|
for (const candidate of exposureCandidates) if (candidate.valuation !== null)
|
|
2715
3062
|
rememberCoordinate(candidate.valuation, candidate.developmentAge!);
|
|
2716
|
-
|
|
2717
|
-
|
|
3063
|
+
return { excluded, originExcluded, origins, valuations, allOriginsSelected,
|
|
3064
|
+
allCoordinatesSelected, auditRanks };
|
|
3065
|
+
};
|
|
3066
|
+
sourceSelectionMetadata.set(prepared,
|
|
3067
|
+
new LazySelectionMetadata(knownSourceGroups, computeSelectionMetadata));
|
|
2718
3068
|
}
|
|
2719
3069
|
return prepared;
|
|
2720
3070
|
}
|
|
2721
|
-
|
|
3071
|
+
const eager = createEagerPreparedDiagnosticData(definition, ownedData);
|
|
3072
|
+
preparedSourceGroups.set(eager, knownSourceGroups);
|
|
3073
|
+
return eager;
|
|
2722
3074
|
}
|
|
2723
3075
|
|
|
2724
3076
|
function createEagerPreparedDiagnosticData(
|