@actuarial-ts/core 0.7.0 → 0.8.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 +46 -2
- package/dist/analysisRecipe.d.ts +55 -0
- package/dist/analysisRecipe.d.ts.map +1 -0
- package/dist/analysisRecipe.js +627 -0
- package/dist/analysisRecipe.js.map +1 -0
- package/dist/classifications.d.ts +30 -0
- package/dist/classifications.d.ts.map +1 -0
- package/dist/classifications.js +207 -0
- package/dist/classifications.js.map +1 -0
- package/dist/customizationContracts.d.ts +749 -0
- package/dist/customizationContracts.d.ts.map +1 -0
- package/dist/customizationContracts.js +19 -0
- package/dist/customizationContracts.js.map +1 -0
- package/dist/descriptiveStatistics.d.ts +8 -0
- package/dist/descriptiveStatistics.d.ts.map +1 -0
- package/dist/descriptiveStatistics.js +283 -0
- package/dist/descriptiveStatistics.js.map +1 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +16 -0
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticFreeze.d.ts +4 -0
- package/dist/diagnosticFreeze.d.ts.map +1 -0
- package/dist/diagnosticFreeze.js +37 -0
- package/dist/diagnosticFreeze.js.map +1 -0
- package/dist/diagnosticIdentityStream.d.ts +4 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -1
- package/dist/diagnosticIdentityStream.js +32 -2
- package/dist/diagnosticIdentityStream.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +21 -0
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +231 -19
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +43 -25
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts.map +1 -1
- package/dist/diagnosticReviewSources.js +25 -4
- package/dist/diagnosticReviewSources.js.map +1 -1
- package/dist/diagnosticReviewStore.d.ts +2 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -1
- package/dist/diagnosticReviewStore.js +110 -36
- package/dist/diagnosticReviewStore.js.map +1 -1
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +1 -8
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/exposureEarning.d.ts +12 -0
- package/dist/exposureEarning.d.ts.map +1 -0
- package/dist/exposureEarning.js +276 -0
- package/dist/exposureEarning.js.map +1 -0
- package/dist/exposureMovements.d.ts +8 -0
- package/dist/exposureMovements.d.ts.map +1 -0
- package/dist/exposureMovements.js +70 -0
- package/dist/exposureMovements.js.map +1 -0
- package/dist/exposureRevisions.d.ts +13 -0
- package/dist/exposureRevisions.d.ts.map +1 -0
- package/dist/exposureRevisions.js +161 -0
- package/dist/exposureRevisions.js.map +1 -0
- package/dist/financialPipeline.d.ts +14 -0
- package/dist/financialPipeline.d.ts.map +1 -0
- package/dist/financialPipeline.js +75 -0
- package/dist/financialPipeline.js.map +1 -0
- package/dist/financialTerms.d.ts +20 -0
- package/dist/financialTerms.d.ts.map +1 -0
- package/dist/financialTerms.js +205 -0
- package/dist/financialTerms.js.map +1 -0
- package/dist/fnvAccumulator.d.ts.map +1 -1
- package/dist/fnvAccumulator.js +11 -5
- package/dist/fnvAccumulator.js.map +1 -1
- package/dist/historyPreparation.d.ts +8 -0
- package/dist/historyPreparation.d.ts.map +1 -0
- package/dist/historyPreparation.js +557 -0
- package/dist/historyPreparation.js.map +1 -0
- package/dist/index.d.ts +15 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/dist/index.js.map +1 -1
- package/dist/policyPeriods.d.ts +29 -0
- package/dist/policyPeriods.d.ts.map +1 -0
- package/dist/policyPeriods.js +322 -0
- package/dist/policyPeriods.js.map +1 -0
- package/dist/populations.d.ts +11 -0
- package/dist/populations.d.ts.map +1 -0
- package/dist/populations.js +264 -0
- package/dist/populations.js.map +1 -0
- package/dist/reservingAdapter.d.ts +16 -0
- package/dist/reservingAdapter.d.ts.map +1 -0
- package/dist/reservingAdapter.js +74 -0
- package/dist/reservingAdapter.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/analysisRecipe.ts +696 -0
- package/src/classifications.ts +231 -0
- package/src/customizationContracts.ts +763 -0
- package/src/descriptiveStatistics.ts +320 -0
- package/src/diagnosticAggregation.ts +16 -0
- package/src/diagnosticFreeze.ts +37 -0
- package/src/diagnosticIdentityStream.ts +37 -2
- package/src/diagnosticPreparation.ts +259 -26
- package/src/diagnosticReview.ts +49 -27
- package/src/diagnosticReviewSources.ts +22 -3
- package/src/diagnosticReviewStore.ts +139 -54
- package/src/diagnosticRunner.ts +1 -11
- package/src/exposureEarning.ts +314 -0
- package/src/exposureMovements.ts +78 -0
- package/src/exposureRevisions.ts +186 -0
- package/src/financialPipeline.ts +93 -0
- package/src/financialTerms.ts +263 -0
- package/src/fnvAccumulator.ts +11 -5
- package/src/historyPreparation.ts +645 -0
- package/src/index.ts +28 -1
- package/src/policyPeriods.ts +375 -0
- package/src/populations.ts +307 -0
- package/src/reservingAdapter.ts +98 -0
- package/src/version.ts +1 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { canonicalJson, fnv1a64 } from "./canonical.js";
|
|
2
|
+
import { freezeDiagnosticGraph as deepFreeze } from "./diagnosticFreeze.js";
|
|
3
|
+
import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
|
|
2
4
|
import {
|
|
3
5
|
createDiagnosticIdentityArray,
|
|
4
6
|
createDiagnosticIdentityObject,
|
|
@@ -204,6 +206,163 @@ export interface CompactPreparedDiagnosticData extends PreparedDiagnosticDataCon
|
|
|
204
206
|
readonly [compactPreparedDiagnosticDataBrand]: true;
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Reuse immutable preparation when only metric selection changes. A different
|
|
211
|
+
* source scope returns undefined so callers can perform full preparation.
|
|
212
|
+
* Invalid filters and forged preparations still fail at the public boundary.
|
|
213
|
+
* Review and metric execution gates must still run for the returned handle.
|
|
214
|
+
*/
|
|
215
|
+
export function reselectCompactDiagnosticMetrics(
|
|
216
|
+
prepared: CompactPreparedDiagnosticData,
|
|
217
|
+
filter?: DiagnosticsFilter,
|
|
218
|
+
): CompactPreparedDiagnosticData | undefined {
|
|
219
|
+
assertCompactPreparedDiagnosticData(prepared);
|
|
220
|
+
const boundaryIssues = preparationBoundaryIssues({
|
|
221
|
+
definition: prepared.definition, losses: [], exposures: [],
|
|
222
|
+
...(filter === undefined ? {} : { filter }),
|
|
223
|
+
});
|
|
224
|
+
if (boundaryIssues.length) throw new DiagnosticValidationError(boundaryIssues);
|
|
225
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
226
|
+
const knownGroups = new Set(prepared.inputAudit.map((item) => item.record.sourceGroup));
|
|
227
|
+
const normalized = normalizeFilter(prepared.definition, filter, knownGroups, issues).value;
|
|
228
|
+
if (issues.length) throw new DiagnosticValidationError(issues);
|
|
229
|
+
const sourceScope = (value: DiagnosticsFilter | null) => {
|
|
230
|
+
const { instanceIds: _instances, ...scope } = value ?? {};
|
|
231
|
+
return canonicalJson(scope);
|
|
232
|
+
};
|
|
233
|
+
if (sourceScope(normalized) !== sourceScope(prepared.filter)) return undefined;
|
|
234
|
+
const selected = Object.freeze({ ...prepared, filter: deepFreeze(normalized) }) as CompactPreparedDiagnosticData;
|
|
235
|
+
authenticCompact.add(selected);
|
|
236
|
+
const parent = sourceSelectionParents.get(prepared);
|
|
237
|
+
const coordinateParent = coordinateSelectionParents.get(prepared);
|
|
238
|
+
if (coordinateParent) coordinateSelectionParents.set(selected, coordinateParent);
|
|
239
|
+
if (parent || sourceSelectionMetadata.has(prepared))
|
|
240
|
+
sourceSelectionParents.set(selected, parent ?? prepared);
|
|
241
|
+
return selected;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Project source-group subsets of a clean aggregate preparation. Other scope
|
|
246
|
+
* changes or preparations requiring structural reconciliation return undefined
|
|
247
|
+
* for full preparation. Excluded input audits retain their original raw periods.
|
|
248
|
+
*/
|
|
249
|
+
export function selectCompactDiagnosticSourceGroups(
|
|
250
|
+
prepared: CompactPreparedDiagnosticData,
|
|
251
|
+
filter?: DiagnosticsFilter,
|
|
252
|
+
): CompactPreparedDiagnosticData | undefined {
|
|
253
|
+
return selectCompactSourceScope(prepared, filter, false);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Select whole origins and source groups from a clean preparation with all origins. */
|
|
257
|
+
export function selectCompactDiagnosticOrigins(
|
|
258
|
+
prepared: CompactPreparedDiagnosticData,
|
|
259
|
+
filter?: DiagnosticsFilter,
|
|
260
|
+
): CompactPreparedDiagnosticData | undefined {
|
|
261
|
+
return selectCompactSourceScope(prepared, filter, true);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Select date/age coordinates from a clean aggregate preparation covering the full input scope. */
|
|
265
|
+
export function selectCompactDiagnosticCoordinates(
|
|
266
|
+
prepared: CompactPreparedDiagnosticData,
|
|
267
|
+
filter?: DiagnosticsFilter,
|
|
268
|
+
): CompactPreparedDiagnosticData | undefined {
|
|
269
|
+
return selectCompactSourceScope(prepared, filter, true, true);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function selectCompactSourceScope(
|
|
273
|
+
prepared: CompactPreparedDiagnosticData,
|
|
274
|
+
filter: DiagnosticsFilter | undefined,
|
|
275
|
+
allowOrigins: boolean,
|
|
276
|
+
allowCoordinates = false,
|
|
277
|
+
): CompactPreparedDiagnosticData | undefined {
|
|
278
|
+
assertCompactPreparedDiagnosticData(prepared);
|
|
279
|
+
const metadata = sourceSelectionMetadata.get(prepared);
|
|
280
|
+
const boundaryIssues = preparationBoundaryIssues({
|
|
281
|
+
definition: prepared.definition, losses: [], exposures: [],
|
|
282
|
+
...(filter === undefined ? {} : { filter }),
|
|
283
|
+
});
|
|
284
|
+
if (boundaryIssues.length) throw new DiagnosticValidationError(boundaryIssues);
|
|
285
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
286
|
+
const normalizedFilter = normalizeFilter(prepared.definition, filter,
|
|
287
|
+
metadata?.groups ?? new Set(prepared.inputAudit.map((item) => item.record.sourceGroup)), issues);
|
|
288
|
+
const normalized = normalizedFilter.value;
|
|
289
|
+
if (issues.length) throw new DiagnosticValidationError(issues);
|
|
290
|
+
if (!metadata) return undefined;
|
|
291
|
+
if (allowOrigins && !metadata.allOriginsSelected) return undefined;
|
|
292
|
+
if (allowCoordinates && !metadata.allCoordinatesSelected) return undefined;
|
|
293
|
+
const remainingScope = (value: DiagnosticsFilter | null) => {
|
|
294
|
+
const { instanceIds: _instances, sourceGroups: _groups, ...scope } = value ?? {};
|
|
295
|
+
if (!allowOrigins) return canonicalJson(scope);
|
|
296
|
+
const { origins: _origins, originFrom: _from, originThrough: _through, ...rest } = scope;
|
|
297
|
+
if (!allowCoordinates) return canonicalJson(rest);
|
|
298
|
+
const { valuations: _valuations, valuationFrom: _vFrom, valuationThrough: _vThrough,
|
|
299
|
+
minDevelopmentAge: _min, maxDevelopmentAge: _max, ...other } = rest;
|
|
300
|
+
return canonicalJson(other);
|
|
301
|
+
};
|
|
302
|
+
// Source exclusion precedes every date/age/cutoff decision. With no selected
|
|
303
|
+
// sources, all audits have the same source-excluded snapshots regardless of
|
|
304
|
+
// those selectors; boundary validation above must still run.
|
|
305
|
+
if (normalized?.sourceGroups?.length !== 0 &&
|
|
306
|
+
remainingScope(normalized) !== remainingScope(prepared.filter)) return undefined;
|
|
307
|
+
const groups = normalized?.sourceGroups === undefined ? undefined : new Set(normalized.sourceGroups);
|
|
308
|
+
const includes = (group: string) => !groups || groups.has(group);
|
|
309
|
+
const includedOrigins = allowOrigins ? new Set([...metadata.origins].filter(([, origin]) =>
|
|
310
|
+
selectedByOrigin(normalized, origin, normalizedFilter.originCoordinates)).map(([label]) => label)) : undefined;
|
|
311
|
+
const includesOrigin = (origin: string) => !includedOrigins || includedOrigins.has(origin);
|
|
312
|
+
const includesDate = (origin: string, valuation: string | null | undefined) => {
|
|
313
|
+
if (!allowCoordinates || valuation === undefined || valuation === null) return true;
|
|
314
|
+
const at = metadata.valuations.get(valuation)!;
|
|
315
|
+
const age = at.coordinate - metadata.origins.get(origin)!.coordinate + prepared.definition.definition.periodAxis.ageOffset;
|
|
316
|
+
return selectedByValuation(normalized, at, normalizedFilter.valuationCoordinates) && selectedByAge(normalized, age);
|
|
317
|
+
};
|
|
318
|
+
const audit = prepared.inputAudit.map((item) => !includes(item.record.sourceGroup)
|
|
319
|
+
? metadata.excluded.get(item.record)!
|
|
320
|
+
: item.disposition === "retained" && (!includesOrigin(item.record.origin) || !includesDate(item.record.origin, item.record.valuation))
|
|
321
|
+
? metadata.originExcluded.get(item.record)! : item);
|
|
322
|
+
audit.sort((left, right) => metadata.auditRanks.get(left)! - metadata.auditRanks.get(right)!);
|
|
323
|
+
const cells = Object.freeze(prepared.cells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation)));
|
|
324
|
+
const exposures = Object.freeze(prepared.exposures.filter((exposure) => exposure.status === "valid" && includes(exposure.sourceGroup) && includesOrigin(exposure.origin) && includesDate(exposure.origin, exposure.valuation)));
|
|
325
|
+
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) => canonicalJson([cell.sourceGroup, cell.origin]))) : undefined;
|
|
327
|
+
const internals = getCompiledDiagnosticDefinitionInternals(prepared.definition);
|
|
328
|
+
const orphanFindings = allowCoordinates ? exposures.flatMap((exposure) => {
|
|
329
|
+
if (exposure.status !== "valid") return [];
|
|
330
|
+
const matches = internals.measuresById.get(exposure.measureId)!.exposureTiming === "origin-static"
|
|
331
|
+
? sourceOrigins!.has(canonicalJson([exposure.sourceGroup, exposure.origin]))
|
|
332
|
+
: exposure.valuation !== undefined && coordinateKeys!.has(cellKey(exposure.sourceGroup, exposure.origin, exposure.valuation));
|
|
333
|
+
return matches ? [] : [exposureWithoutLossFinding(exposure)];
|
|
334
|
+
}) : [];
|
|
335
|
+
const selected = Object.freeze({
|
|
336
|
+
...prepared,
|
|
337
|
+
filter: deepFreeze(normalized),
|
|
338
|
+
inputAudit: Object.freeze(audit),
|
|
339
|
+
cells,
|
|
340
|
+
exposures,
|
|
341
|
+
findings: orphanFindings.length ? deepFreeze(mergeFindings([...prepared.findings, ...orphanFindings])) : prepared.findings,
|
|
342
|
+
expectedCells: Object.freeze(prepared.expectedCells.filter((cell) => includes(cell.sourceGroup) && includesOrigin(cell.origin) && includesDate(cell.origin, cell.valuation))),
|
|
343
|
+
}) as CompactPreparedDiagnosticData;
|
|
344
|
+
authenticCompact.add(selected);
|
|
345
|
+
if (allowCoordinates) coordinateSelectionParents.set(selected, prepared);
|
|
346
|
+
else sourceSelectionParents.set(selected, prepared);
|
|
347
|
+
return selected;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Private ownership proof: only whole-source projection can register a parent. */
|
|
351
|
+
export function getCompactSourceSelectionParent(
|
|
352
|
+
prepared: CompactPreparedDiagnosticData,
|
|
353
|
+
): CompactPreparedDiagnosticData | undefined {
|
|
354
|
+
assertCompactPreparedDiagnosticData(prepared);
|
|
355
|
+
return sourceSelectionParents.get(prepared);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Private proof of cell reuse; does not imply unchanged development-history checks. */
|
|
359
|
+
export function getCompactCoordinateSelectionParent(
|
|
360
|
+
prepared: CompactPreparedDiagnosticData,
|
|
361
|
+
): CompactPreparedDiagnosticData | undefined {
|
|
362
|
+
assertCompactPreparedDiagnosticData(prepared);
|
|
363
|
+
return coordinateSelectionParents.get(prepared);
|
|
364
|
+
}
|
|
365
|
+
|
|
207
366
|
export interface NormalizedDiagnosticPreparationIdentity {
|
|
208
367
|
readonly definitionIntegrity: string;
|
|
209
368
|
readonly filter: NormalizedDiagnosticsFilterIdentity | null;
|
|
@@ -272,6 +431,18 @@ interface PendingBlocker {
|
|
|
272
431
|
|
|
273
432
|
const authentic = new WeakSet<object>();
|
|
274
433
|
const authenticCompact = new WeakSet<object>();
|
|
434
|
+
const sourceSelectionParents = new WeakMap<CompactPreparedDiagnosticData, CompactPreparedDiagnosticData>();
|
|
435
|
+
const coordinateSelectionParents = new WeakMap<CompactPreparedDiagnosticData, CompactPreparedDiagnosticData>();
|
|
436
|
+
const sourceSelectionMetadata = new WeakMap<CompactPreparedDiagnosticData, {
|
|
437
|
+
groups: ReadonlySet<string>;
|
|
438
|
+
excluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
|
|
439
|
+
originExcluded: ReadonlyMap<object, DiagnosticInputAuditRecord>;
|
|
440
|
+
origins: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
|
|
441
|
+
valuations: ReadonlyMap<string, DiagnosticNormalizedPeriod>;
|
|
442
|
+
allOriginsSelected: boolean;
|
|
443
|
+
allCoordinatesSelected: boolean;
|
|
444
|
+
auditRanks: ReadonlyMap<DiagnosticInputAuditRecord, number>;
|
|
445
|
+
}>();
|
|
275
446
|
const compactIdentityDocuments = new WeakMap<
|
|
276
447
|
object,
|
|
277
448
|
DiagnosticIdentityDocument
|
|
@@ -360,18 +531,6 @@ function own<T>(
|
|
|
360
531
|
: undefined;
|
|
361
532
|
}
|
|
362
533
|
|
|
363
|
-
function deepFreeze<T>(
|
|
364
|
-
value: T,
|
|
365
|
-
seen = new WeakSet<object>(),
|
|
366
|
-
): DiagnosticDeepReadonly<T> {
|
|
367
|
-
if (value === null || typeof value !== "object" || seen.has(value))
|
|
368
|
-
return value as DiagnosticDeepReadonly<T>;
|
|
369
|
-
seen.add(value);
|
|
370
|
-
for (const child of Object.values(value as Record<string, unknown>))
|
|
371
|
-
deepFreeze(child, seen);
|
|
372
|
-
return Object.freeze(value) as DiagnosticDeepReadonly<T>;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
534
|
function uniqueSorted(values: readonly string[]): string[] {
|
|
376
535
|
return [...new Set(values)].sort(codeUnit);
|
|
377
536
|
}
|
|
@@ -382,6 +541,17 @@ function normalizeSources(
|
|
|
382
541
|
return normalizeDiagnosticSourceLocations(values);
|
|
383
542
|
}
|
|
384
543
|
|
|
544
|
+
function exposureWithoutLossFinding(exposure: Extract<ReconciledDiagnosticExposure, { status: "valid" }>): DiagnosticMetricFinding {
|
|
545
|
+
return structuralFinding("exposure-without-loss", {
|
|
546
|
+
measureId: exposure.measureId,
|
|
547
|
+
exposureKey: exposure.key,
|
|
548
|
+
sourceGroup: exposure.sourceGroup,
|
|
549
|
+
origin: exposure.origin,
|
|
550
|
+
...(exposure.valuation ? { valuation: exposure.valuation } : {}),
|
|
551
|
+
sources: exposure.sources,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
385
555
|
function structuralFinding(
|
|
386
556
|
code: StructuralCode,
|
|
387
557
|
context: Omit<
|
|
@@ -1041,7 +1211,7 @@ function createAuditComparator(): (
|
|
|
1041
1211
|
};
|
|
1042
1212
|
return (left, right) =>
|
|
1043
1213
|
rank[left.kind] - rank[right.kind] ||
|
|
1044
|
-
codeUnit(key(left.record), key(right.record)) ||
|
|
1214
|
+
(left.record === right.record ? 0 : codeUnit(key(left.record), key(right.record))) ||
|
|
1045
1215
|
disposition[left.disposition] - disposition[right.disposition];
|
|
1046
1216
|
}
|
|
1047
1217
|
|
|
@@ -1538,6 +1708,7 @@ function prepareDiagnosticDataInMode(
|
|
|
1538
1708
|
coordinate: null,
|
|
1539
1709
|
invalid: false,
|
|
1540
1710
|
}));
|
|
1711
|
+
const rawLossSnapshots = lossCandidates.map((candidate) => candidate.snapshot);
|
|
1541
1712
|
|
|
1542
1713
|
for (const candidate of lossCandidates) {
|
|
1543
1714
|
if (candidate.disposition !== "retained") continue;
|
|
@@ -1666,6 +1837,7 @@ function prepareDiagnosticDataInMode(
|
|
|
1666
1837
|
invalid: false,
|
|
1667
1838
|
}),
|
|
1668
1839
|
);
|
|
1840
|
+
const rawExposureSnapshots = exposureCandidates.map((candidate) => candidate.snapshot);
|
|
1669
1841
|
for (const candidate of exposureCandidates) {
|
|
1670
1842
|
if (candidate.disposition !== "retained") continue;
|
|
1671
1843
|
try {
|
|
@@ -2150,14 +2322,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2150
2322
|
for (const key of keys) addToMap(validExposuresByCell, key, exposure);
|
|
2151
2323
|
if (keys.length === 0)
|
|
2152
2324
|
findings.push(
|
|
2153
|
-
|
|
2154
|
-
measureId: exposure.measureId,
|
|
2155
|
-
exposureKey: exposure.key,
|
|
2156
|
-
sourceGroup: exposure.sourceGroup,
|
|
2157
|
-
origin: exposure.origin,
|
|
2158
|
-
...(exposure.valuation ? { valuation: exposure.valuation } : {}),
|
|
2159
|
-
sources: exposure.sources,
|
|
2160
|
-
}),
|
|
2325
|
+
exposureWithoutLossFinding(exposure),
|
|
2161
2326
|
);
|
|
2162
2327
|
}
|
|
2163
2328
|
|
|
@@ -2169,6 +2334,12 @@ function prepareDiagnosticDataInMode(
|
|
|
2169
2334
|
for (const key of new Set(pending.cellKeys))
|
|
2170
2335
|
addToMap(pendingBlockersByCell, key, pending);
|
|
2171
2336
|
|
|
2337
|
+
// Statistics are immutable scalar records, often identical across measures
|
|
2338
|
+
// and cells. Share only freshly computed owned values, never contributions,
|
|
2339
|
+
// source identities, prepared-cell envelopes, or authenticated SDK handles.
|
|
2340
|
+
// The invocation-scoped pool is bounded and released after preparation.
|
|
2341
|
+
const componentSharing = createDiagnosticEvidenceInterner();
|
|
2342
|
+
|
|
2172
2343
|
for (const [key, rows] of cellRows) {
|
|
2173
2344
|
const [sourceGroup, origin, valuation] = parseCellKey(key);
|
|
2174
2345
|
const coordinate = coordinateFor(definition, origin, valuation).coordinate!;
|
|
@@ -2311,15 +2482,24 @@ function prepareDiagnosticDataInMode(
|
|
|
2311
2482
|
const measure = internals.measuresById.get(measureId)!;
|
|
2312
2483
|
return [
|
|
2313
2484
|
measureId,
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2485
|
+
componentSharing.internOwned(
|
|
2486
|
+
finalizeDiagnosticContributions(
|
|
2487
|
+
contributions[measureId]!,
|
|
2488
|
+
measure.missing,
|
|
2489
|
+
blockers[measureId],
|
|
2490
|
+
),
|
|
2491
|
+
"plain",
|
|
2318
2492
|
),
|
|
2319
2493
|
];
|
|
2320
2494
|
}),
|
|
2321
2495
|
) as Record<string, DiagnosticMeasureStats>;
|
|
2322
2496
|
for (const measureId of allMeasureIds) {
|
|
2497
|
+
const stats = components[measureId]!;
|
|
2498
|
+
// The finalized counts already prove whether any per-status finding is
|
|
2499
|
+
// possible. Avoid allocating three empty partitions and a finding context
|
|
2500
|
+
// for every clean observed measure in large aggregate datasets.
|
|
2501
|
+
if (stats.missing === 0 && stats.nonFinite === 0 &&
|
|
2502
|
+
!(stats.sum === null && stats.structural === 0)) continue;
|
|
2323
2503
|
const measureContributions = contributions[measureId]!;
|
|
2324
2504
|
const context = {
|
|
2325
2505
|
measureId,
|
|
@@ -2445,6 +2625,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2445
2625
|
),
|
|
2446
2626
|
)
|
|
2447
2627
|
.map((candidate) => candidate.row);
|
|
2628
|
+
const compareAudits = createAuditComparator();
|
|
2448
2629
|
const audit: DiagnosticInputAuditRecord[] = [
|
|
2449
2630
|
...lossCandidates.map((candidate): DiagnosticInputAuditRecord => ({
|
|
2450
2631
|
kind: "loss",
|
|
@@ -2461,7 +2642,7 @@ function prepareDiagnosticDataInMode(
|
|
|
2461
2642
|
disposition: candidate.disposition,
|
|
2462
2643
|
record: candidate.snapshot,
|
|
2463
2644
|
})),
|
|
2464
|
-
].sort(
|
|
2645
|
+
].sort(compareAudits);
|
|
2465
2646
|
const normalizedFindings = mergeFindings(findings);
|
|
2466
2647
|
const frozenCells = preparedCells.map((cell) => deepFreeze(cell));
|
|
2467
2648
|
const cutoffValues = normalizedCutoffs.map((cutoff) => cutoff.value);
|
|
@@ -2483,6 +2664,58 @@ function prepareDiagnosticDataInMode(
|
|
|
2483
2664
|
...ownedData,
|
|
2484
2665
|
}) as unknown as CompactPreparedDiagnosticData;
|
|
2485
2666
|
authenticCompact.add(prepared);
|
|
2667
|
+
if (definition.definition.lossRowGrain === "aggregate" && [...knownSourceGroups].every((group) => selectedBySource(filter, group)) &&
|
|
2668
|
+
normalizedFindings.length === 0 && reconciled.every((exposure) => exposure.status === "valid")) {
|
|
2669
|
+
const excluded = new Map<object, DiagnosticInputAuditRecord>();
|
|
2670
|
+
lossCandidates.forEach((candidate, index) => {
|
|
2671
|
+
const raw = rawLossSnapshots[index]!;
|
|
2672
|
+
// Normalization changes only the coordinate labels in these snapshots.
|
|
2673
|
+
// When those were canonical already, reuse the same complete record.
|
|
2674
|
+
const record = raw.origin === candidate.snapshot.origin && raw.valuation === candidate.snapshot.valuation
|
|
2675
|
+
? candidate.snapshot : raw;
|
|
2676
|
+
excluded.set(candidate.snapshot, deepFreeze({ kind: "loss", disposition: "filter", record }));
|
|
2677
|
+
});
|
|
2678
|
+
exposureCandidates.forEach((candidate, index) => {
|
|
2679
|
+
const raw = rawExposureSnapshots[index]!;
|
|
2680
|
+
const record = raw.origin === candidate.snapshot.origin && raw.valuation === candidate.snapshot.valuation
|
|
2681
|
+
? candidate.snapshot : raw;
|
|
2682
|
+
excluded.set(candidate.snapshot, deepFreeze({ kind: "exposure", disposition: "filter", record }));
|
|
2683
|
+
});
|
|
2684
|
+
expectedCandidates.forEach((candidate) => excluded.set(candidate.snapshot, deepFreeze({
|
|
2685
|
+
kind: "expected-cell", disposition: "filter", record: candidate.snapshot,
|
|
2686
|
+
})));
|
|
2687
|
+
// Rank every possible included/excluded audit variant once. Query sorts
|
|
2688
|
+
// then use only numbers; canonical record strings live only in this
|
|
2689
|
+
// preparation's comparator and are not retained by the context.
|
|
2690
|
+
const originExcluded = new Map<object, DiagnosticInputAuditRecord>(audit.map((item) => [item.record,
|
|
2691
|
+
deepFreeze({ ...item, disposition: "filter" as const })]));
|
|
2692
|
+
const origins = new Map<string, DiagnosticNormalizedPeriod>();
|
|
2693
|
+
for (const candidate of lossCandidates) if (candidate.coordinate)
|
|
2694
|
+
origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
|
|
2695
|
+
for (const candidate of exposureCandidates) if (candidate.origin)
|
|
2696
|
+
origins.set(candidate.origin.label, candidate.origin);
|
|
2697
|
+
for (const candidate of expectedCandidates)
|
|
2698
|
+
origins.set(candidate.coordinate.origin.label, candidate.coordinate.origin);
|
|
2699
|
+
const orderedVariants = [...audit, ...excluded.values(), ...originExcluded.values()].sort(compareAudits);
|
|
2700
|
+
const auditRanks = new Map(orderedVariants.map((item, index) => [item, index]));
|
|
2701
|
+
const allOriginsSelected = [...origins.values()].every((origin) =>
|
|
2702
|
+
selectedByOrigin(filter, origin, normalizedFilter.originCoordinates));
|
|
2703
|
+
const valuations = new Map<string, DiagnosticNormalizedPeriod>();
|
|
2704
|
+
let allCoordinatesSelected = true;
|
|
2705
|
+
const rememberCoordinate = (valuation: DiagnosticNormalizedPeriod, age: number) => {
|
|
2706
|
+
valuations.set(valuation.label, valuation);
|
|
2707
|
+
if (!selectedByValuation(filter, valuation, normalizedFilter.valuationCoordinates) || !selectedByAge(filter, age))
|
|
2708
|
+
allCoordinatesSelected = false;
|
|
2709
|
+
};
|
|
2710
|
+
for (const candidate of lossCandidates) if (candidate.coordinate)
|
|
2711
|
+
rememberCoordinate(candidate.coordinate.valuation, candidate.coordinate.developmentAge);
|
|
2712
|
+
for (const candidate of expectedCandidates)
|
|
2713
|
+
rememberCoordinate(candidate.coordinate.valuation, candidate.coordinate.developmentAge);
|
|
2714
|
+
for (const candidate of exposureCandidates) if (candidate.valuation !== null)
|
|
2715
|
+
rememberCoordinate(candidate.valuation, candidate.developmentAge!);
|
|
2716
|
+
sourceSelectionMetadata.set(prepared, { groups: knownSourceGroups, excluded, originExcluded,
|
|
2717
|
+
origins, valuations, allOriginsSelected, allCoordinatesSelected, auditRanks });
|
|
2718
|
+
}
|
|
2486
2719
|
return prepared;
|
|
2487
2720
|
}
|
|
2488
2721
|
return createEagerPreparedDiagnosticData(definition, ownedData);
|
package/src/diagnosticReview.ts
CHANGED
|
@@ -18,12 +18,15 @@ import {
|
|
|
18
18
|
import {
|
|
19
19
|
assertPreparedDiagnosticData,
|
|
20
20
|
assertCompactPreparedDiagnosticData,
|
|
21
|
+
getCompactSourceSelectionParent,
|
|
22
|
+
getCompactCoordinateSelectionParent,
|
|
21
23
|
type CompactPreparedDiagnosticData,
|
|
22
24
|
type PreparedDiagnosticDataContent,
|
|
23
25
|
type PreparedDiagnosticData,
|
|
24
26
|
} from "./diagnosticPreparation.js";
|
|
25
27
|
import {
|
|
26
28
|
createCompactReviewBuilder,
|
|
29
|
+
selectCompactReviewSources,
|
|
27
30
|
type CompactDiagnosticReviewEvaluations,
|
|
28
31
|
} from "./diagnosticReviewStore.js";
|
|
29
32
|
import {
|
|
@@ -704,18 +707,32 @@ export function evaluateDiagnosticReviewRules(
|
|
|
704
707
|
return Object.freeze(results);
|
|
705
708
|
}
|
|
706
709
|
|
|
710
|
+
const compactEvaluationCache = new WeakMap<CompactPreparedDiagnosticData, CompactDiagnosticReviewEvaluations>();
|
|
711
|
+
|
|
707
712
|
/** Keeps every evaluation in compact owned storage; no identity is computed. */
|
|
708
713
|
export function evaluateDiagnosticReviewRulesCompact(
|
|
709
714
|
prepared: CompactPreparedDiagnosticData,
|
|
710
715
|
): CompactDiagnosticReviewEvaluations {
|
|
711
716
|
assertCompactPreparedDiagnosticData(prepared);
|
|
717
|
+
const coordinateParent = getCompactCoordinateSelectionParent(prepared);
|
|
718
|
+
const parent = prepared.cells.length === 0 && prepared.expectedCells.length === 0
|
|
719
|
+
? undefined : coordinateParent ?? getCompactSourceSelectionParent(prepared);
|
|
720
|
+
const invariant = parent
|
|
721
|
+
? compactEvaluationCache.get(parent) ?? evaluateDiagnosticReviewRulesCompact(parent)
|
|
722
|
+
: undefined;
|
|
712
723
|
const compactSources = createCompactCellSourceReader();
|
|
713
724
|
const builder = createCompactReviewBuilder(
|
|
714
725
|
prepared.definition.definition.reviewRules,
|
|
715
726
|
compactSources,
|
|
716
727
|
);
|
|
717
|
-
visitDiagnosticReviewRules(prepared, builder.append, compactSources);
|
|
718
|
-
|
|
728
|
+
visitDiagnosticReviewRules(prepared, builder.append, compactSources, invariant !== undefined, coordinateParent !== undefined);
|
|
729
|
+
const evaluated = builder.finish();
|
|
730
|
+
const result = invariant ? selectCompactReviewSources(invariant, evaluated,
|
|
731
|
+
prepared.filter?.sourceGroups === undefined ? undefined : new Set(prepared.filter.sourceGroups),
|
|
732
|
+
new Set([...prepared.cells.map((cell) => cell.origin), ...prepared.expectedCells.map((cell) => cell.origin)]),
|
|
733
|
+
coordinateParent ? new Set(prepared.cells.map((cell) => JSON.stringify([cell.sourceGroup, cell.origin, cell.valuation]))) : undefined) : evaluated;
|
|
734
|
+
compactEvaluationCache.set(prepared, result);
|
|
735
|
+
return result;
|
|
719
736
|
}
|
|
720
737
|
|
|
721
738
|
/** One traversal and one formula implementation serve both storage contracts. */
|
|
@@ -723,6 +740,8 @@ function visitDiagnosticReviewRules(
|
|
|
723
740
|
prepared: PreparedDiagnosticDataContent,
|
|
724
741
|
appendEvaluation: (value: DiagnosticReviewRuleEvaluation) => void,
|
|
725
742
|
compactSources?: ReturnType<typeof createCompactCellSourceReader>,
|
|
743
|
+
controlsOnly = false,
|
|
744
|
+
includeMonotonic = false,
|
|
726
745
|
): void {
|
|
727
746
|
const evaluateExpression = createCellExpressionEvaluator(
|
|
728
747
|
prepared,
|
|
@@ -741,10 +760,11 @@ function visitDiagnosticReviewRules(
|
|
|
741
760
|
origin: string,
|
|
742
761
|
valuation: string,
|
|
743
762
|
) => canonicalJson([sourceGroup, origin, valuation]);
|
|
763
|
+
const monotonicGroups: Array<{ sourceGroup: string; origin: string; valuations: string[] }> = [];
|
|
744
764
|
// Keep the original traversal and source order; only replace repeated searches.
|
|
745
765
|
// Other rule families do not need these indices.
|
|
746
766
|
if (
|
|
747
|
-
prepared.definition.definition.reviewRules.some(
|
|
767
|
+
(!controlsOnly || includeMonotonic) && prepared.definition.definition.reviewRules.some(
|
|
748
768
|
(rule) => rule.kind === "monotonic",
|
|
749
769
|
)
|
|
750
770
|
) {
|
|
@@ -759,11 +779,36 @@ function visitDiagnosticReviewRules(
|
|
|
759
779
|
sources.push(cell.source);
|
|
760
780
|
expectedSourcesByCoordinate.set(key, sources);
|
|
761
781
|
}
|
|
782
|
+
// Grouping and valuation ordering depend on the selected preparation, not
|
|
783
|
+
// the rule expression. Build them once for all monotonic rules in this run.
|
|
784
|
+
const groups = new Map<string, Set<string>>();
|
|
785
|
+
for (const cell of [...prepared.cells, ...prepared.expectedCells]) {
|
|
786
|
+
const key = canonicalJson([cell.sourceGroup, cell.origin]);
|
|
787
|
+
const values = groups.get(key) ?? new Set<string>();
|
|
788
|
+
values.add(cell.valuation);
|
|
789
|
+
groups.set(key, values);
|
|
790
|
+
}
|
|
791
|
+
const valuationCoordinates = new Map<string, number>();
|
|
792
|
+
const valuationCoordinate = (label: string) => {
|
|
793
|
+
let value = valuationCoordinates.get(label);
|
|
794
|
+
if (value === undefined) {
|
|
795
|
+
value = normalizeDiagnosticPeriod(prepared.definition, "valuation", label).coordinate;
|
|
796
|
+
valuationCoordinates.set(label, value);
|
|
797
|
+
}
|
|
798
|
+
return value;
|
|
799
|
+
};
|
|
800
|
+
for (const [key, values] of [...groups.entries()].sort(([left], [right]) => codeUnit(left, right))) {
|
|
801
|
+
const [sourceGroup, origin] = JSON.parse(key) as [string, string];
|
|
802
|
+
const valuations = [...values].sort((left, right) =>
|
|
803
|
+
valuationCoordinate(left) - valuationCoordinate(right) || codeUnit(left, right));
|
|
804
|
+
monotonicGroups.push({ sourceGroup, origin, valuations });
|
|
805
|
+
}
|
|
762
806
|
}
|
|
763
807
|
for (const [ruleIndex, rule] of (
|
|
764
808
|
prepared.definition.definition
|
|
765
809
|
.reviewRules as unknown as readonly DiagnosticReviewRule[]
|
|
766
810
|
).entries()) {
|
|
811
|
+
if (controlsOnly && rule.kind !== "control-total" && !(includeMonotonic && rule.kind === "monotonic")) continue;
|
|
767
812
|
const basePath = `/reviewRules/${ruleIndex}`;
|
|
768
813
|
if (rule.kind === "control-total") {
|
|
769
814
|
const cells = projectedCells(prepared, rule);
|
|
@@ -907,30 +952,7 @@ function visitDiagnosticReviewRules(
|
|
|
907
952
|
continue;
|
|
908
953
|
}
|
|
909
954
|
if (rule.kind === "monotonic") {
|
|
910
|
-
const
|
|
911
|
-
for (const cell of prepared.cells) {
|
|
912
|
-
const key = canonicalJson([cell.sourceGroup, cell.origin]);
|
|
913
|
-
const values = groups.get(key) ?? new Set<string>();
|
|
914
|
-
values.add(cell.valuation);
|
|
915
|
-
groups.set(key, values);
|
|
916
|
-
}
|
|
917
|
-
for (const expected of prepared.expectedCells) {
|
|
918
|
-
const key = canonicalJson([expected.sourceGroup, expected.origin]);
|
|
919
|
-
const values = groups.get(key) ?? new Set<string>();
|
|
920
|
-
values.add(expected.valuation);
|
|
921
|
-
groups.set(key, values);
|
|
922
|
-
}
|
|
923
|
-
for (const [key, valuations] of [...groups.entries()].sort(
|
|
924
|
-
([left], [right]) => codeUnit(left, right),
|
|
925
|
-
)) {
|
|
926
|
-
const [sourceGroup, origin] = JSON.parse(key) as [string, string];
|
|
927
|
-
const sorted = [...valuations].sort(
|
|
928
|
-
(left, right) =>
|
|
929
|
-
normalizeDiagnosticPeriod(prepared.definition, "valuation", left)
|
|
930
|
-
.coordinate -
|
|
931
|
-
normalizeDiagnosticPeriod(prepared.definition, "valuation", right)
|
|
932
|
-
.coordinate || codeUnit(left, right),
|
|
933
|
-
);
|
|
955
|
+
for (const { sourceGroup, origin, valuations: sorted } of monotonicGroups) {
|
|
934
956
|
for (let index = 1; index < sorted.length; index++) {
|
|
935
957
|
const previousValuation = sorted[index - 1]!;
|
|
936
958
|
const currentValuation = sorted[index]!;
|
|
@@ -40,18 +40,28 @@ export function createDiagnosticReviewSourcePool() {
|
|
|
40
40
|
return entry;
|
|
41
41
|
};
|
|
42
42
|
const collection = () => {
|
|
43
|
-
|
|
43
|
+
// Most cell expressions cite one source row repeatedly across measures.
|
|
44
|
+
// Defer the union map and sorting until a second distinct source exists.
|
|
45
|
+
let first: SourceEntry | undefined;
|
|
46
|
+
let entries: Map<string, DiagnosticSourceLocation> | undefined;
|
|
44
47
|
let owned = true;
|
|
45
48
|
return {
|
|
46
49
|
/** `fromPrepared` is private evaluator authority, not an input option. */
|
|
47
50
|
add(value: DiagnosticSourceLocation, fromPrepared: boolean): void {
|
|
48
51
|
const entry = sourceEntry(value, fromPrepared);
|
|
49
52
|
owned &&= ownedSources.has(entry.value);
|
|
50
|
-
if (!
|
|
53
|
+
if (!first) first = entry;
|
|
54
|
+
else if (entries) {
|
|
55
|
+
if (!entries.has(entry.key)) entries.set(entry.key, entry.value);
|
|
56
|
+
} else if (entry.key !== first.key) {
|
|
57
|
+
entries = new Map([[first.key, first.value], [entry.key, entry.value]]);
|
|
58
|
+
}
|
|
51
59
|
},
|
|
52
60
|
finish(): readonly DiagnosticSourceLocation[] {
|
|
53
61
|
const values = Object.freeze(
|
|
54
|
-
|
|
62
|
+
entries
|
|
63
|
+
? [...entries.values()].sort(compareDiagnosticSourceLocations)
|
|
64
|
+
: first ? [first.value] : [],
|
|
55
65
|
);
|
|
56
66
|
if (owned) ownedLists.add(values);
|
|
57
67
|
return values;
|
|
@@ -61,6 +71,15 @@ export function createDiagnosticReviewSourcePool() {
|
|
|
61
71
|
const union = (
|
|
62
72
|
lists: readonly (readonly DiagnosticSourceLocation[])[],
|
|
63
73
|
): readonly DiagnosticSourceLocation[] => {
|
|
74
|
+
const first = lists[0];
|
|
75
|
+
// Repeated cell expressions often resolve to exactly the same owned source
|
|
76
|
+
// sequence. Reuse it without rebuilding a union or its builder list ID.
|
|
77
|
+
// Reference equality preserves full values and first representatives; no
|
|
78
|
+
// caller-frozen array or merely equal canonical fingerprint is trusted.
|
|
79
|
+
if (first && lists.every((values) => ownedLists.has(values) &&
|
|
80
|
+
values.length === first.length && values.every((value, index) => value === first[index]))) {
|
|
81
|
+
return first;
|
|
82
|
+
}
|
|
64
83
|
const target = collection();
|
|
65
84
|
for (const values of lists)
|
|
66
85
|
for (const source of values) target.add(source, false);
|