@actuarial-ts/data 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 +58 -3
- package/dist/customizationContracts.d.ts +2473 -0
- package/dist/customizationContracts.d.ts.map +1 -0
- package/dist/customizationContracts.js +845 -0
- package/dist/customizationContracts.js.map +1 -0
- package/dist/customizationExternalState.d.ts +42 -0
- package/dist/customizationExternalState.d.ts.map +1 -0
- package/dist/customizationExternalState.js +121 -0
- package/dist/customizationExternalState.js.map +1 -0
- package/dist/customizationHistoryStream.d.ts +26 -0
- package/dist/customizationHistoryStream.d.ts.map +1 -0
- package/dist/customizationHistoryStream.js +66 -0
- package/dist/customizationHistoryStream.js.map +1 -0
- package/dist/diagnosticInput.d.ts +33 -0
- package/dist/diagnosticInput.d.ts.map +1 -1
- package/dist/diagnosticInput.js +71 -1
- package/dist/diagnosticInput.js.map +1 -1
- package/dist/diagnosticPreparedReview.d.ts.map +1 -1
- package/dist/diagnosticPreparedReview.js +38 -0
- package/dist/diagnosticPreparedReview.js.map +1 -1
- package/dist/historyMapping.d.ts +898 -0
- package/dist/historyMapping.d.ts.map +1 -0
- package/dist/historyMapping.js +395 -0
- package/dist/historyMapping.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/customizationContracts.ts +963 -0
- package/src/customizationExternalState.ts +208 -0
- package/src/customizationHistoryStream.ts +90 -0
- package/src/diagnosticInput.ts +101 -0
- package/src/diagnosticPreparedReview.ts +48 -0
- package/src/historyMapping.ts +514 -0
- package/src/index.ts +4 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDiagnosticToken,
|
|
3
|
+
isWellFormedDiagnosticString,
|
|
4
|
+
snapshotDiagnosticJson,
|
|
5
|
+
type CustomizationResourceLimits,
|
|
6
|
+
type JsonValue,
|
|
7
|
+
} from "@actuarial-ts/core";
|
|
8
|
+
import { parseCustomizationResourceLimits } from "./customizationContracts.js";
|
|
9
|
+
|
|
10
|
+
export type CustomizationExecutionErrorCode =
|
|
11
|
+
| "CANCELLED"
|
|
12
|
+
| "RESOURCE_LIMIT"
|
|
13
|
+
| "EXTERNAL_STATE_FAILURE"
|
|
14
|
+
| "EXTERNAL_STATE_ORDER"
|
|
15
|
+
| "EXTERNAL_STATE_TRUNCATED";
|
|
16
|
+
|
|
17
|
+
export class CustomizationExecutionError extends Error {
|
|
18
|
+
readonly code: CustomizationExecutionErrorCode;
|
|
19
|
+
|
|
20
|
+
constructor(code: CustomizationExecutionErrorCode, message: string) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "CustomizationExecutionError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
Object.freeze(this);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CustomizationExternalRecord<T extends JsonValue = JsonValue> {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly partitionKey: string;
|
|
31
|
+
readonly orderKey: string;
|
|
32
|
+
readonly value: T;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CustomizationExternalStateSession<T extends JsonValue = JsonValue> {
|
|
36
|
+
/** Store one SDK-owned record. The adapter must not mutate it. */
|
|
37
|
+
readonly append: (record: CustomizationExternalRecord<T>) => void | Promise<void>;
|
|
38
|
+
/** Finish writes before reads begin. */
|
|
39
|
+
readonly seal: () => void | Promise<void>;
|
|
40
|
+
/** Reopen all records exactly once in partitionKey/orderKey/id order. */
|
|
41
|
+
readonly read: () => AsyncIterable<CustomizationExternalRecord<T>>;
|
|
42
|
+
/** Release the run's temporary namespace for every terminal outcome. */
|
|
43
|
+
readonly dispose: (outcome: "completed" | "failed" | "cancelled") => void | Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CustomizationExternalStateAdapter {
|
|
47
|
+
readonly open: <T extends JsonValue>(request: {
|
|
48
|
+
readonly runId: string;
|
|
49
|
+
readonly signal?: AbortSignal;
|
|
50
|
+
}) => CustomizationExternalStateSession<T> | Promise<CustomizationExternalStateSession<T>>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ExternalSortCustomizationRecordsInput<T extends JsonValue = JsonValue> {
|
|
54
|
+
readonly runId: string;
|
|
55
|
+
readonly records: Iterable<CustomizationExternalRecord<T>> | AsyncIterable<CustomizationExternalRecord<T>>;
|
|
56
|
+
readonly adapter: CustomizationExternalStateAdapter;
|
|
57
|
+
readonly limits: CustomizationResourceLimits;
|
|
58
|
+
readonly signal?: AbortSignal;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const encoder = new TextEncoder();
|
|
62
|
+
|
|
63
|
+
function cancelled(signal: AbortSignal | undefined): void {
|
|
64
|
+
if (signal?.aborted)
|
|
65
|
+
throw new CustomizationExecutionError("CANCELLED", "Customization execution was cancelled");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateRecord<T extends JsonValue>(
|
|
69
|
+
value: CustomizationExternalRecord<T>,
|
|
70
|
+
maximumRecordBytes: number,
|
|
71
|
+
): CustomizationExternalRecord<T> {
|
|
72
|
+
if (!isDiagnosticToken(value.id))
|
|
73
|
+
throw new CustomizationExecutionError("EXTERNAL_STATE_FAILURE", "External record id is invalid");
|
|
74
|
+
if (
|
|
75
|
+
!isWellFormedDiagnosticString(value.partitionKey) ||
|
|
76
|
+
!isWellFormedDiagnosticString(value.orderKey)
|
|
77
|
+
)
|
|
78
|
+
throw new CustomizationExecutionError(
|
|
79
|
+
"EXTERNAL_STATE_FAILURE",
|
|
80
|
+
"External record sort keys must be valid strings without U+0000",
|
|
81
|
+
);
|
|
82
|
+
const owned = snapshotDiagnosticJson(value);
|
|
83
|
+
const bytes = encoder.encode(JSON.stringify(owned)).byteLength;
|
|
84
|
+
if (bytes > maximumRecordBytes)
|
|
85
|
+
throw new CustomizationExecutionError(
|
|
86
|
+
"RESOURCE_LIMIT",
|
|
87
|
+
`External record ${value.id} requires ${bytes} encoded bytes, exceeding maximumRecordBytes ${maximumRecordBytes}`,
|
|
88
|
+
);
|
|
89
|
+
return owned;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compareRecords(
|
|
93
|
+
left: CustomizationExternalRecord,
|
|
94
|
+
right: CustomizationExternalRecord,
|
|
95
|
+
): number {
|
|
96
|
+
for (const key of ["partitionKey", "orderKey", "id"] as const) {
|
|
97
|
+
const result = left[key] === right[key] ? 0 : left[key] < right[key] ? -1 : 1;
|
|
98
|
+
if (result !== 0) return result;
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validates, owns, externally stages, and deterministically rereads records.
|
|
105
|
+
* The adapter supplies storage mechanics only; it cannot choose actuarial
|
|
106
|
+
* ordering or publish a completed run. Early consumer return is cancellation.
|
|
107
|
+
*/
|
|
108
|
+
export async function* externalSortCustomizationRecords<T extends JsonValue>(
|
|
109
|
+
input: ExternalSortCustomizationRecordsInput<T>,
|
|
110
|
+
): AsyncGenerator<CustomizationExternalRecord<T>> {
|
|
111
|
+
const runId = input.runId;
|
|
112
|
+
const records = input.records;
|
|
113
|
+
const adapter = input.adapter;
|
|
114
|
+
const signal = input.signal;
|
|
115
|
+
let limits: CustomizationResourceLimits;
|
|
116
|
+
try {
|
|
117
|
+
limits = parseCustomizationResourceLimits(input.limits);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new CustomizationExecutionError(
|
|
120
|
+
"RESOURCE_LIMIT",
|
|
121
|
+
error instanceof Error ? error.message : "Invalid customization resource limits",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (!isDiagnosticToken(runId))
|
|
125
|
+
throw new CustomizationExecutionError("EXTERNAL_STATE_FAILURE", "runId is invalid");
|
|
126
|
+
cancelled(signal);
|
|
127
|
+
|
|
128
|
+
let session: CustomizationExternalStateSession<T> | undefined;
|
|
129
|
+
let outcome: "completed" | "failed" | "cancelled" = "failed";
|
|
130
|
+
let primary: unknown;
|
|
131
|
+
try {
|
|
132
|
+
session = await adapter.open<T>({
|
|
133
|
+
runId,
|
|
134
|
+
...(signal === undefined ? {} : { signal }),
|
|
135
|
+
});
|
|
136
|
+
let written = 0;
|
|
137
|
+
for await (const raw of records) {
|
|
138
|
+
cancelled(signal);
|
|
139
|
+
written += 1;
|
|
140
|
+
if (written > limits.maximumInputRecords)
|
|
141
|
+
throw new CustomizationExecutionError(
|
|
142
|
+
"RESOURCE_LIMIT",
|
|
143
|
+
`Input exceeds maximumInputRecords ${limits.maximumInputRecords}`,
|
|
144
|
+
);
|
|
145
|
+
await session.append(validateRecord(raw, limits.maximumRecordBytes));
|
|
146
|
+
}
|
|
147
|
+
cancelled(signal);
|
|
148
|
+
await session.seal();
|
|
149
|
+
|
|
150
|
+
let read = 0;
|
|
151
|
+
let partitionRecords = 0;
|
|
152
|
+
let prior: CustomizationExternalRecord<T> | undefined;
|
|
153
|
+
for await (const raw of session.read()) {
|
|
154
|
+
cancelled(signal);
|
|
155
|
+
const record = validateRecord(raw, limits.maximumRecordBytes);
|
|
156
|
+
if (prior !== undefined && compareRecords(prior, record) > 0)
|
|
157
|
+
throw new CustomizationExecutionError(
|
|
158
|
+
"EXTERNAL_STATE_ORDER",
|
|
159
|
+
"External records are not in nondecreasing partitionKey/orderKey/id order",
|
|
160
|
+
);
|
|
161
|
+
partitionRecords = prior?.partitionKey === record.partitionKey ? partitionRecords + 1 : 1;
|
|
162
|
+
if (partitionRecords > limits.maximumPartitionRecords)
|
|
163
|
+
throw new CustomizationExecutionError(
|
|
164
|
+
"RESOURCE_LIMIT",
|
|
165
|
+
`Partition ${record.partitionKey} exceeds maximumPartitionRecords ${limits.maximumPartitionRecords}`,
|
|
166
|
+
);
|
|
167
|
+
read += 1;
|
|
168
|
+
if (read > written)
|
|
169
|
+
throw new CustomizationExecutionError(
|
|
170
|
+
"EXTERNAL_STATE_FAILURE",
|
|
171
|
+
"External state returned more records than were written",
|
|
172
|
+
);
|
|
173
|
+
prior = record;
|
|
174
|
+
yield record;
|
|
175
|
+
}
|
|
176
|
+
if (read !== written)
|
|
177
|
+
throw new CustomizationExecutionError(
|
|
178
|
+
"EXTERNAL_STATE_TRUNCATED",
|
|
179
|
+
`External state returned ${read} of ${written} records`,
|
|
180
|
+
);
|
|
181
|
+
outcome = "completed";
|
|
182
|
+
} catch (error) {
|
|
183
|
+
primary = error;
|
|
184
|
+
outcome =
|
|
185
|
+
error instanceof CustomizationExecutionError && error.code === "CANCELLED"
|
|
186
|
+
? "cancelled"
|
|
187
|
+
: "failed";
|
|
188
|
+
throw error;
|
|
189
|
+
} finally {
|
|
190
|
+
if (session !== undefined) {
|
|
191
|
+
try {
|
|
192
|
+
const terminal =
|
|
193
|
+
outcome === "completed"
|
|
194
|
+
? "completed"
|
|
195
|
+
: primary === undefined
|
|
196
|
+
? "cancelled"
|
|
197
|
+
: outcome;
|
|
198
|
+
await session.dispose(terminal);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (primary === undefined)
|
|
201
|
+
throw new CustomizationExecutionError(
|
|
202
|
+
"EXTERNAL_STATE_FAILURE",
|
|
203
|
+
`External state cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalJson,
|
|
3
|
+
fnv1a64,
|
|
4
|
+
prepareHistoricalDataset,
|
|
5
|
+
type CustomizationCapabilityFinding,
|
|
6
|
+
type CustomizationResourceLimits,
|
|
7
|
+
type HistoricalDatasetInput,
|
|
8
|
+
type HistoricalObservationRecord,
|
|
9
|
+
type HistoricalRecordDisposition,
|
|
10
|
+
type JsonValue,
|
|
11
|
+
type PreparedHistoricalObservation,
|
|
12
|
+
} from "@actuarial-ts/core";
|
|
13
|
+
import { parseHistoricalDatasetInput, parseHistoricalObservationRecord } from "./customizationContracts.js";
|
|
14
|
+
import {
|
|
15
|
+
externalSortCustomizationRecords,
|
|
16
|
+
type CustomizationExternalRecord,
|
|
17
|
+
type CustomizationExternalStateAdapter,
|
|
18
|
+
} from "./customizationExternalState.js";
|
|
19
|
+
|
|
20
|
+
export type PreparedHistoryStreamEvent =
|
|
21
|
+
| { readonly kind: "observation"; readonly observation: PreparedHistoricalObservation }
|
|
22
|
+
| { readonly kind: "disposition"; readonly disposition: HistoricalRecordDisposition }
|
|
23
|
+
| { readonly kind: "finding"; readonly finding: CustomizationCapabilityFinding };
|
|
24
|
+
|
|
25
|
+
function partitionKey(record: HistoricalObservationRecord): string {
|
|
26
|
+
return canonicalJson([record.sourceNamespace, record.claimId]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function orderKey(record: HistoricalObservationRecord, precedence: HistoricalDatasetInput["revisionPrecedence"]): string {
|
|
30
|
+
const order = precedence === "sequence"
|
|
31
|
+
? String(record.revision.sequence ?? -1).padStart(20, "0")
|
|
32
|
+
: record.revision.correctedAt ?? "";
|
|
33
|
+
return canonicalJson([record.recordId, record.evaluationDate ?? record.effectiveDate ?? "", order, record.revision.id]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sorts raw revisions in host-owned state and prepares one claim partition at
|
|
38
|
+
* a time. Retained SDK state is bounded by maximumPartitionRecords; oversized
|
|
39
|
+
* claims fail rather than silently spilling or dropping observations.
|
|
40
|
+
*/
|
|
41
|
+
export async function* streamPreparedHistoricalDataset(input: {
|
|
42
|
+
readonly runId: string;
|
|
43
|
+
readonly header: Omit<HistoricalDatasetInput, "records">;
|
|
44
|
+
readonly records: AsyncIterable<unknown> | Iterable<unknown>;
|
|
45
|
+
readonly adapter: CustomizationExternalStateAdapter;
|
|
46
|
+
readonly limits: CustomizationResourceLimits;
|
|
47
|
+
readonly signal?: AbortSignal;
|
|
48
|
+
}): AsyncGenerator<PreparedHistoryStreamEvent> {
|
|
49
|
+
const header = parseHistoricalDatasetInput({ ...input.header, records: [] });
|
|
50
|
+
async function* externalRecords(): AsyncGenerator<CustomizationExternalRecord<JsonValue>> {
|
|
51
|
+
for await (const raw of input.records) {
|
|
52
|
+
const record = parseHistoricalObservationRecord(raw);
|
|
53
|
+
const stable = canonicalJson([record.sourceNamespace, record.claimId, record.recordId, record.revision.id]);
|
|
54
|
+
yield {
|
|
55
|
+
id: `record-${fnv1a64(stable)}`,
|
|
56
|
+
partitionKey: partitionKey(record),
|
|
57
|
+
orderKey: orderKey(record, header.revisionPrecedence),
|
|
58
|
+
value: record as unknown as JsonValue,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let currentPartition: string | undefined;
|
|
63
|
+
let current: HistoricalObservationRecord[] = [];
|
|
64
|
+
const flush = function* (): Generator<PreparedHistoryStreamEvent> {
|
|
65
|
+
if (current.length === 0) return;
|
|
66
|
+
const sourceNamespace = current[0]!.sourceNamespace;
|
|
67
|
+
const claimId = current[0]!.claimId;
|
|
68
|
+
const prepared = prepareHistoricalDataset({
|
|
69
|
+
...header,
|
|
70
|
+
...(header.openingBalances === undefined ? {} : { openingBalances: header.openingBalances.filter((balance) => balance.sourceNamespace === sourceNamespace && balance.claimId === claimId) }),
|
|
71
|
+
records: current,
|
|
72
|
+
});
|
|
73
|
+
for (const disposition of prepared.dispositions) yield { kind: "disposition", disposition };
|
|
74
|
+
for (const finding of prepared.findings) yield { kind: "finding", finding };
|
|
75
|
+
for (const observation of prepared.observations) yield { kind: "observation", observation };
|
|
76
|
+
current = [];
|
|
77
|
+
};
|
|
78
|
+
for await (const external of externalSortCustomizationRecords({
|
|
79
|
+
runId: input.runId,
|
|
80
|
+
records: externalRecords(),
|
|
81
|
+
adapter: input.adapter,
|
|
82
|
+
limits: input.limits,
|
|
83
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
84
|
+
})) {
|
|
85
|
+
if (currentPartition !== undefined && external.partitionKey !== currentPartition) yield* flush();
|
|
86
|
+
currentPartition = external.partitionKey;
|
|
87
|
+
current.push(parseHistoricalObservationRecord(external.value));
|
|
88
|
+
}
|
|
89
|
+
yield* flush();
|
|
90
|
+
}
|
package/src/diagnosticInput.ts
CHANGED
|
@@ -3,6 +3,10 @@ import {
|
|
|
3
3
|
compileDiagnosticDefinition,
|
|
4
4
|
prepareDiagnosticData,
|
|
5
5
|
prepareDiagnosticDataCompact,
|
|
6
|
+
reselectCompactDiagnosticMetrics,
|
|
7
|
+
selectCompactDiagnosticSourceGroups,
|
|
8
|
+
selectCompactDiagnosticOrigins,
|
|
9
|
+
selectCompactDiagnosticCoordinates,
|
|
6
10
|
runMetricDiagnostics,
|
|
7
11
|
runMetricDiagnosticsCompact,
|
|
8
12
|
validateDiagnosticGroupingConfiguration,
|
|
@@ -307,6 +311,38 @@ const compactPreparedByInput = new WeakMap<
|
|
|
307
311
|
CompactValidatedDiagnosticRunInput,
|
|
308
312
|
CompactPreparedDiagnosticData
|
|
309
313
|
>();
|
|
314
|
+
|
|
315
|
+
declare const queryContextBrand: unique symbol;
|
|
316
|
+
/** SDK-owned source snapshot. Cloning or serializing this handle loses authority. */
|
|
317
|
+
export interface DiagnosticQueryContext {
|
|
318
|
+
readonly [queryContextBrand]: true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Omitted fields inherit the context; null clears filter/expected-cell selection.
|
|
323
|
+
* Group maps/dimensions must match the selected scope; empty records clear them.
|
|
324
|
+
*/
|
|
325
|
+
export interface DiagnosticQuery {
|
|
326
|
+
/** Per-query evidence labels; null clears the inherited label. */
|
|
327
|
+
readonly runPresetId?: string | null;
|
|
328
|
+
readonly datasetArtifactId?: string | null;
|
|
329
|
+
readonly filter?: DiagnosticsFilter | null;
|
|
330
|
+
readonly completePeriodCutoffs?: readonly DiagnosticCompletePeriodCutoff[];
|
|
331
|
+
readonly expectedCells?: readonly DiagnosticExpectedCell[] | null;
|
|
332
|
+
readonly groupMap?: Readonly<Record<string, string>>;
|
|
333
|
+
readonly groupDimensions?: Readonly<Record<string, JsonValue>>;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const querySchema = z.object({
|
|
337
|
+
runPresetId: tokenSchema.nullable().optional(),
|
|
338
|
+
datasetArtifactId: tokenSchema.nullable().optional(),
|
|
339
|
+
filter: filterSchema.nullable().optional(),
|
|
340
|
+
completePeriodCutoffs: z.array(cutoffSchema).optional(),
|
|
341
|
+
expectedCells: z.array(expectedSchema).nullable().optional(),
|
|
342
|
+
groupMap: recordSchema(tokenSchema).optional(),
|
|
343
|
+
groupDimensions: recordSchema(jsonSchema).optional(),
|
|
344
|
+
}).strict();
|
|
345
|
+
const queryContexts = new WeakMap<DiagnosticQueryContext, CompactValidatedDiagnosticRunInput>();
|
|
310
346
|
const compactCompleted = new WeakSet<object>();
|
|
311
347
|
// Retain the exact immutable validated input only while its completed run lives.
|
|
312
348
|
// Reconstructing from the audit would lose the original optional/raw-value form.
|
|
@@ -509,6 +545,71 @@ export function validateDiagnosticRunInputCompact(
|
|
|
509
545
|
return result;
|
|
510
546
|
}
|
|
511
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Own and validate source rows and compile the definition once. Queries keep the
|
|
550
|
+
* definition, execution policy, source order and review evidence fixed.
|
|
551
|
+
* Metric selections and eligible clean aggregate source/origin/date/age queries
|
|
552
|
+
* share prepared cells; ineligible inputs still perform full core preparation.
|
|
553
|
+
* Source/origin queries reuse invariant owned review evidence. Date/age queries
|
|
554
|
+
* reuse cell-local checks but recompute monotonic comparisons and controls. Every
|
|
555
|
+
* query enforces review and metric gates, including unchanged evidence validation.
|
|
556
|
+
*/
|
|
557
|
+
export function createDiagnosticQueryContext(value: unknown): DiagnosticQueryContext {
|
|
558
|
+
const validated = validateDiagnosticRunInputCompact(value);
|
|
559
|
+
const handle = Object.freeze({}) as DiagnosticQueryContext;
|
|
560
|
+
queryContexts.set(handle, validated);
|
|
561
|
+
return handle;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Create an authentic per-query input without cloning/reparsing owned source rows. */
|
|
565
|
+
export function validateDiagnosticQueryCompact(
|
|
566
|
+
context: DiagnosticQueryContext,
|
|
567
|
+
query: unknown = {},
|
|
568
|
+
): CompactValidatedDiagnosticRunInput {
|
|
569
|
+
const base = queryContexts.get(context);
|
|
570
|
+
if (!base) throw new DiagnosticValidationError([{
|
|
571
|
+
domain: "input",
|
|
572
|
+
code: "invalid-input-relationship",
|
|
573
|
+
path: "$",
|
|
574
|
+
message: "Value is not an authentic diagnostic query context",
|
|
575
|
+
}]);
|
|
576
|
+
const undefinedIssues = explicitUndefinedIssues(query);
|
|
577
|
+
if (undefinedIssues.length) throw new DiagnosticValidationError(undefinedIssues);
|
|
578
|
+
const parsed = querySchema.safeParse(query);
|
|
579
|
+
if (!parsed.success) throw issues(parsed.error);
|
|
580
|
+
if (Object.keys(parsed.data).length === 0) return base;
|
|
581
|
+
const ownedQuery = freeze({
|
|
582
|
+
...parsed.data,
|
|
583
|
+
...(parsed.data.groupMap === undefined ? {} : { groupMap: sortedRecord<string>(parsed.data.groupMap) }),
|
|
584
|
+
...(parsed.data.groupDimensions === undefined ? {} : { groupDimensions: sortedRecord<JsonValue>(parsed.data.groupDimensions) }),
|
|
585
|
+
});
|
|
586
|
+
// Only the new query fields require freezing. Every inherited graph belongs
|
|
587
|
+
// to the authentic validated base; never trust caller Object.freeze markers.
|
|
588
|
+
const result = Object.freeze({ ...base, ...ownedQuery }) as CompactValidatedDiagnosticRunInput;
|
|
589
|
+
const projected = Object.keys(ownedQuery).every((key) => ["filter", "groupMap", "groupDimensions", "runPresetId", "datasetArtifactId"].includes(key))
|
|
590
|
+
? reselectCompactDiagnosticMetrics(compactPreparedByInput.get(base)!, result.filter ?? undefined) ??
|
|
591
|
+
selectCompactDiagnosticSourceGroups(compactPreparedByInput.get(base)!, result.filter ?? undefined) ??
|
|
592
|
+
selectCompactDiagnosticOrigins(compactPreparedByInput.get(base)!, result.filter ?? undefined) ??
|
|
593
|
+
selectCompactDiagnosticCoordinates(compactPreparedByInput.get(base)!, result.filter ?? undefined)
|
|
594
|
+
: undefined;
|
|
595
|
+
const prepared = projected ?? prepareDiagnosticDataCompact(preparationInput(result));
|
|
596
|
+
validateCompactDiagnosticGroupingConfiguration({
|
|
597
|
+
prepared,
|
|
598
|
+
groupMap: result.groupMap,
|
|
599
|
+
groupDimensions: result.groupDimensions,
|
|
600
|
+
});
|
|
601
|
+
compactPreparedByInput.set(result, prepared);
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Execute all review and metric gates for this scope, even if another scope passed. */
|
|
606
|
+
export function runDiagnosticQueryCompact(
|
|
607
|
+
context: DiagnosticQueryContext,
|
|
608
|
+
query: DiagnosticQuery = {},
|
|
609
|
+
): CompactMetricDiagnosticsOutcome {
|
|
610
|
+
return runValidatedMetricDiagnosticsCompact(validateDiagnosticQueryCompact(context, query));
|
|
611
|
+
}
|
|
612
|
+
|
|
512
613
|
export function assertCompactValidatedDiagnosticRunInput(
|
|
513
614
|
value: unknown,
|
|
514
615
|
): asserts value is CompactValidatedDiagnosticRunInput {
|
|
@@ -747,6 +747,30 @@ const compactReceiptFingerprints = new WeakMap<
|
|
|
747
747
|
CompactDiagnosticReviewReceipt,
|
|
748
748
|
string
|
|
749
749
|
>();
|
|
750
|
+
|
|
751
|
+
// At most one review per live immutable cell graph. Weak ownership releases the
|
|
752
|
+
// retained report with its preparation; changed evidence replaces, never grows,
|
|
753
|
+
// the entry. Metric selection does not affect rule evaluation or structural
|
|
754
|
+
// checks, but every source, definition and evidence dependency below does.
|
|
755
|
+
const reusableCompactReviews = new WeakMap<object, {
|
|
756
|
+
prepared: CompactPreparedDiagnosticData;
|
|
757
|
+
evidenceKey: string;
|
|
758
|
+
scopeKey: string;
|
|
759
|
+
receipt: CompactDiagnosticReviewReceipt;
|
|
760
|
+
}>();
|
|
761
|
+
|
|
762
|
+
function reviewScopeKey(prepared: CompactPreparedDiagnosticData): string {
|
|
763
|
+
const { instanceIds: _instances, ...scope } = prepared.filter ?? {};
|
|
764
|
+
return canonicalJson(scope);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function sameReviewSourceGraph(left: CompactPreparedDiagnosticData, right: CompactPreparedDiagnosticData): boolean {
|
|
768
|
+
return left.definition === right.definition && left.cells === right.cells &&
|
|
769
|
+
left.inputAudit === right.inputAudit && left.exposures === right.exposures &&
|
|
770
|
+
left.completePeriodCutoffs === right.completePeriodCutoffs &&
|
|
771
|
+
left.expectedCellsProvided === right.expectedCellsProvided &&
|
|
772
|
+
left.expectedCells === right.expectedCells && left.findings === right.findings;
|
|
773
|
+
}
|
|
750
774
|
function compactError(message: string, path = "$"): never {
|
|
751
775
|
throw new DiagnosticValidationError([
|
|
752
776
|
{ domain: "input", code: "invalid-input-relationship", path, message },
|
|
@@ -935,6 +959,24 @@ export function reviewPreparedDiagnosticDataCompact(
|
|
|
935
959
|
input.evidence === null
|
|
936
960
|
? null
|
|
937
961
|
: validateDiagnosticReviewEvidence(input.evidence);
|
|
962
|
+
// Validated evidence has a fixed JSON schema. Preserve signed zero in its
|
|
963
|
+
// numeric slots; identity JSON intentionally normalizes it, cache ownership
|
|
964
|
+
// must not silently substitute a different caller's raw evidence.
|
|
965
|
+
const evidenceKey = JSON.stringify(evidence, (_key, value: unknown) =>
|
|
966
|
+
typeof value === "number" && Object.is(value, -0) ? { negativeZero: true } : value,
|
|
967
|
+
);
|
|
968
|
+
const scopeKey = reviewScopeKey(input.prepared);
|
|
969
|
+
const previous = reusableCompactReviews.get(input.prepared.cells);
|
|
970
|
+
if (previous && previous.evidenceKey === evidenceKey && previous.scopeKey === scopeKey &&
|
|
971
|
+
sameReviewSourceGraph(previous.prepared, input.prepared)) {
|
|
972
|
+
// The content is genuinely evaluated evidence, not a copied passing flag.
|
|
973
|
+
// Bind the new receipt to this preparation so its exported fingerprint
|
|
974
|
+
// includes this query's actual metric selection and preparation identity.
|
|
975
|
+
const receipt = Object.freeze({ ...previous.receipt, evidence });
|
|
976
|
+
compactReceipts.add(receipt);
|
|
977
|
+
compactReceiptPreparations.set(receipt, input.prepared);
|
|
978
|
+
return receipt;
|
|
979
|
+
}
|
|
938
980
|
const evaluations = evaluateDiagnosticReviewRulesCompact(input.prepared);
|
|
939
981
|
const table = new CompactDiagnosticJson();
|
|
940
982
|
const blocks: FindingBlock[] = [];
|
|
@@ -1136,6 +1178,12 @@ export function reviewPreparedDiagnosticDataCompact(
|
|
|
1136
1178
|
});
|
|
1137
1179
|
compactReceipts.add(receipt);
|
|
1138
1180
|
compactReceiptPreparations.set(receipt, input.prepared);
|
|
1181
|
+
// Very large supplied evidence remains fully reviewed, simply not cached.
|
|
1182
|
+
if (evidenceKey.length <= 65_536 && scopeKey.length <= 65_536) {
|
|
1183
|
+
reusableCompactReviews.set(input.prepared.cells, {
|
|
1184
|
+
prepared: input.prepared, evidenceKey, scopeKey, receipt,
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1139
1187
|
return receipt;
|
|
1140
1188
|
}
|
|
1141
1189
|
|