@actuarial-ts/data 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/compactDiagnosticJson.d.ts +24 -0
- package/dist/compactDiagnosticJson.d.ts.map +1 -0
- package/dist/compactDiagnosticJson.js +200 -0
- package/dist/compactDiagnosticJson.js.map +1 -0
- package/dist/diagnosticInput.d.ts +53 -2
- package/dist/diagnosticInput.d.ts.map +1 -1
- package/dist/diagnosticInput.js +177 -61
- package/dist/diagnosticInput.js.map +1 -1
- package/dist/diagnosticPreparedReview.d.ts +64 -1
- package/dist/diagnosticPreparedReview.d.ts.map +1 -1
- package/dist/diagnosticPreparedReview.js +384 -64
- package/dist/diagnosticPreparedReview.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/src/compactDiagnosticJson.ts +216 -0
- package/src/diagnosticInput.ts +298 -151
- package/src/diagnosticPreparedReview.ts +649 -111
- package/src/version.ts +1 -1
package/src/diagnosticInput.ts
CHANGED
|
@@ -2,8 +2,13 @@ import {
|
|
|
2
2
|
DiagnosticValidationError,
|
|
3
3
|
compileDiagnosticDefinition,
|
|
4
4
|
prepareDiagnosticData,
|
|
5
|
+
prepareDiagnosticDataCompact,
|
|
5
6
|
runMetricDiagnostics,
|
|
7
|
+
runMetricDiagnosticsCompact,
|
|
6
8
|
validateDiagnosticGroupingConfiguration,
|
|
9
|
+
validateCompactDiagnosticGroupingConfiguration,
|
|
10
|
+
type CompactMetricDiagnosticsResult,
|
|
11
|
+
type CompactPreparedDiagnosticData,
|
|
7
12
|
type CompiledDiagnosticDefinition,
|
|
8
13
|
type DiagnosticCompletePeriodCutoff,
|
|
9
14
|
type DiagnosticDeepReadonly,
|
|
@@ -14,6 +19,7 @@ import {
|
|
|
14
19
|
type DiagnosticsFilter,
|
|
15
20
|
type JsonValue,
|
|
16
21
|
type MetricDiagnosticsResult,
|
|
22
|
+
type PreparedDiagnosticData,
|
|
17
23
|
type DiagnosticValidationIssue,
|
|
18
24
|
diagnosticRecord,
|
|
19
25
|
isDiagnosticToken,
|
|
@@ -22,9 +28,11 @@ import {
|
|
|
22
28
|
import { z } from "zod";
|
|
23
29
|
import {
|
|
24
30
|
reviewPreparedDiagnosticData,
|
|
31
|
+
reviewPreparedDiagnosticDataCompact,
|
|
25
32
|
validateDiagnosticReviewEvidence,
|
|
26
33
|
type DiagnosticReviewEvidence,
|
|
27
34
|
type DiagnosticReviewReceipt,
|
|
35
|
+
type CompactDiagnosticReviewReceipt,
|
|
28
36
|
} from "./diagnosticPreparedReview.js";
|
|
29
37
|
|
|
30
38
|
// Zod 3 validates but drops the literal __proto__ key while assembling records.
|
|
@@ -46,16 +54,10 @@ function recordSchema<T extends z.ZodTypeAny>(value: T) {
|
|
|
46
54
|
|
|
47
55
|
const tokenSchema = z
|
|
48
56
|
.string()
|
|
49
|
-
.refine(
|
|
50
|
-
isDiagnosticToken,
|
|
51
|
-
"Expected a nonempty token with valid Unicode and no U+0000",
|
|
52
|
-
);
|
|
57
|
+
.refine(isDiagnosticToken, "Expected a nonempty token with valid Unicode and no U+0000");
|
|
53
58
|
const jsonStringSchema = z
|
|
54
59
|
.string()
|
|
55
|
-
.refine(
|
|
56
|
-
isWellFormedDiagnosticString,
|
|
57
|
-
"Expected valid Unicode without U+0000",
|
|
58
|
-
);
|
|
60
|
+
.refine(isWellFormedDiagnosticString, "Expected valid Unicode without U+0000");
|
|
59
61
|
const sourceSchema = z
|
|
60
62
|
.object({
|
|
61
63
|
artifactId: tokenSchema,
|
|
@@ -65,10 +67,7 @@ const sourceSchema = z
|
|
|
65
67
|
sourceCell: tokenSchema.optional(),
|
|
66
68
|
})
|
|
67
69
|
.strict();
|
|
68
|
-
const rawNumberSchema = z.custom<number>(
|
|
69
|
-
(value) => typeof value === "number",
|
|
70
|
-
"Expected number",
|
|
71
|
-
);
|
|
70
|
+
const rawNumberSchema = z.custom<number>((value) => typeof value === "number", "Expected number");
|
|
72
71
|
const measuresSchema = recordSchema(z.union([rawNumberSchema, z.null()]));
|
|
73
72
|
const lossBase = {
|
|
74
73
|
recordId: tokenSchema,
|
|
@@ -80,9 +79,7 @@ const lossBase = {
|
|
|
80
79
|
measures: measuresSchema,
|
|
81
80
|
};
|
|
82
81
|
const lossSchema = z.discriminatedUnion("rowType", [
|
|
83
|
-
z
|
|
84
|
-
.object({ ...lossBase, rowType: z.literal("claim"), claimId: tokenSchema })
|
|
85
|
-
.strict(),
|
|
82
|
+
z.object({ ...lossBase, rowType: z.literal("claim"), claimId: tokenSchema }).strict(),
|
|
86
83
|
z.object({ ...lossBase, rowType: z.literal("aggregate") }).strict(),
|
|
87
84
|
]);
|
|
88
85
|
const exposureSchema = z
|
|
@@ -139,12 +136,8 @@ const jsonSchema: z.ZodType<JsonValue> = z.lazy(() =>
|
|
|
139
136
|
);
|
|
140
137
|
const policySchema = z
|
|
141
138
|
.object({
|
|
142
|
-
allowedReviewStatuses: z
|
|
143
|
-
|
|
144
|
-
.optional(),
|
|
145
|
-
allowedMetricFindingSeverities: z
|
|
146
|
-
.array(z.enum(["info", "warning", "fail"]))
|
|
147
|
-
.optional(),
|
|
139
|
+
allowedReviewStatuses: z.array(z.enum(["pass", "warning", "not-evaluated", "fail"])).optional(),
|
|
140
|
+
allowedMetricFindingSeverities: z.array(z.enum(["info", "warning", "fail"])).optional(),
|
|
148
141
|
rationaleRef: tokenSchema.optional(),
|
|
149
142
|
})
|
|
150
143
|
.strict();
|
|
@@ -165,18 +158,10 @@ const runSchema = z
|
|
|
165
158
|
})
|
|
166
159
|
.strict();
|
|
167
160
|
|
|
168
|
-
export type DiagnosticAllowedReviewStatus =
|
|
169
|
-
| "pass"
|
|
170
|
-
| "warning"
|
|
171
|
-
| "not-evaluated"
|
|
172
|
-
| "fail";
|
|
161
|
+
export type DiagnosticAllowedReviewStatus = "pass" | "warning" | "not-evaluated" | "fail";
|
|
173
162
|
export interface DiagnosticExecutionPolicyInput {
|
|
174
163
|
readonly allowedReviewStatuses?: readonly DiagnosticAllowedReviewStatus[];
|
|
175
|
-
readonly allowedMetricFindingSeverities?: readonly (
|
|
176
|
-
| "info"
|
|
177
|
-
| "warning"
|
|
178
|
-
| "fail"
|
|
179
|
-
)[];
|
|
164
|
+
readonly allowedMetricFindingSeverities?: readonly ("info" | "warning" | "fail")[];
|
|
180
165
|
readonly rationaleRef?: string;
|
|
181
166
|
}
|
|
182
167
|
export interface DiagnosticRunInput {
|
|
@@ -209,21 +194,23 @@ export interface ValidatedDiagnosticRunInput {
|
|
|
209
194
|
readonly groupDimensions: Readonly<Record<string, JsonValue>>;
|
|
210
195
|
readonly policy: {
|
|
211
196
|
readonly allowedReviewStatuses: readonly DiagnosticAllowedReviewStatus[];
|
|
212
|
-
readonly allowedMetricFindingSeverities: readonly (
|
|
213
|
-
| "info"
|
|
214
|
-
| "warning"
|
|
215
|
-
| "fail"
|
|
216
|
-
)[];
|
|
197
|
+
readonly allowedMetricFindingSeverities: readonly ("info" | "warning" | "fail")[];
|
|
217
198
|
readonly rationaleRef: string | null;
|
|
218
199
|
};
|
|
219
200
|
}
|
|
201
|
+
|
|
202
|
+
type DiagnosticRunInputContent = Omit<
|
|
203
|
+
ValidatedDiagnosticRunInput,
|
|
204
|
+
typeof validatedDiagnosticRunInputBrand
|
|
205
|
+
>;
|
|
206
|
+
declare const compactValidatedDiagnosticRunInputBrand: unique symbol;
|
|
207
|
+
/** Validated owned input whose preparation does not eagerly expand identity evidence. */
|
|
208
|
+
export interface CompactValidatedDiagnosticRunInput extends DiagnosticRunInputContent {
|
|
209
|
+
readonly [compactValidatedDiagnosticRunInputBrand]: true;
|
|
210
|
+
}
|
|
220
211
|
export interface DiagnosticExecutionGateReceipt {
|
|
221
212
|
readonly allowedReviewStatuses: readonly DiagnosticAllowedReviewStatus[];
|
|
222
|
-
readonly allowedMetricFindingSeverities: readonly (
|
|
223
|
-
| "info"
|
|
224
|
-
| "warning"
|
|
225
|
-
| "fail"
|
|
226
|
-
)[];
|
|
213
|
+
readonly allowedMetricFindingSeverities: readonly ("info" | "warning" | "fail")[];
|
|
227
214
|
readonly rationaleRef: string | null;
|
|
228
215
|
readonly reviewGate: "passed" | "blocked";
|
|
229
216
|
readonly metricGate: "not-run" | "passed" | "blocked";
|
|
@@ -275,16 +262,63 @@ export type ValidatedMetricDiagnosticsOutcome =
|
|
|
275
262
|
};
|
|
276
263
|
};
|
|
277
264
|
|
|
265
|
+
/** A distinct authenticated run; it cannot be substituted for an eager receipt. */
|
|
266
|
+
export interface CompletedCompactMetricDiagnosticsRun {
|
|
267
|
+
readonly status: "completed";
|
|
268
|
+
readonly prepared: CompactPreparedDiagnosticData;
|
|
269
|
+
readonly review: CompactDiagnosticReviewReceipt;
|
|
270
|
+
readonly result: DiagnosticDeepReadonly<CompactMetricDiagnosticsResult>;
|
|
271
|
+
readonly runPresetId: string | null;
|
|
272
|
+
readonly datasetArtifactId: string | null;
|
|
273
|
+
readonly groupMap: Readonly<Record<string, string>>;
|
|
274
|
+
readonly groupDimensions: Readonly<Record<string, JsonValue>>;
|
|
275
|
+
readonly gate: DiagnosticExecutionGateReceipt & {
|
|
276
|
+
readonly reviewGate: "passed";
|
|
277
|
+
readonly metricGate: "passed";
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
type CompactRunMetadata = Omit<CompletedCompactMetricDiagnosticsRun, "status" | "result" | "gate">;
|
|
281
|
+
export type CompactMetricDiagnosticsOutcome =
|
|
282
|
+
| CompletedCompactMetricDiagnosticsRun
|
|
283
|
+
| (CompactRunMetadata & {
|
|
284
|
+
readonly status: "blocked";
|
|
285
|
+
readonly stage: "review";
|
|
286
|
+
readonly result: null;
|
|
287
|
+
readonly gate: DiagnosticExecutionGateReceipt & {
|
|
288
|
+
readonly reviewGate: "blocked";
|
|
289
|
+
readonly metricGate: "not-run";
|
|
290
|
+
};
|
|
291
|
+
})
|
|
292
|
+
| (CompactRunMetadata & {
|
|
293
|
+
readonly status: "blocked";
|
|
294
|
+
readonly stage: "metric";
|
|
295
|
+
readonly result: DiagnosticDeepReadonly<CompactMetricDiagnosticsResult>;
|
|
296
|
+
readonly gate: DiagnosticExecutionGateReceipt & {
|
|
297
|
+
readonly reviewGate: "passed";
|
|
298
|
+
readonly metricGate: "blocked";
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
|
|
278
302
|
const authentic = new WeakSet<object>();
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
303
|
+
// Only owned, frozen inputs can enter this cache. Weak keys do not retain a
|
|
304
|
+
// completed analysis after its caller releases it, and JSON cannot restore it.
|
|
305
|
+
const preparedByInput = new WeakMap<ValidatedDiagnosticRunInput, PreparedDiagnosticData>();
|
|
306
|
+
const compactPreparedByInput = new WeakMap<
|
|
307
|
+
CompactValidatedDiagnosticRunInput,
|
|
308
|
+
CompactPreparedDiagnosticData
|
|
309
|
+
>();
|
|
310
|
+
const compactCompleted = new WeakSet<object>();
|
|
311
|
+
// Retain the exact immutable validated input only while its completed run lives.
|
|
312
|
+
// Reconstructing from the audit would lose the original optional/raw-value form.
|
|
313
|
+
const compactInputByCompletedRun = new WeakMap<
|
|
314
|
+
CompletedCompactMetricDiagnosticsRun,
|
|
315
|
+
CompactValidatedDiagnosticRunInput
|
|
316
|
+
>();
|
|
317
|
+
function freeze<T>(value: T, seen = new WeakSet<object>()): DiagnosticDeepReadonly<T> {
|
|
283
318
|
if (value === null || typeof value !== "object" || seen.has(value))
|
|
284
319
|
return value as DiagnosticDeepReadonly<T>;
|
|
285
320
|
seen.add(value);
|
|
286
|
-
for (const child of Object.values(value as Record<string, unknown>))
|
|
287
|
-
freeze(child, seen);
|
|
321
|
+
for (const child of Object.values(value as Record<string, unknown>)) freeze(child, seen);
|
|
288
322
|
return Object.freeze(value) as DiagnosticDeepReadonly<T>;
|
|
289
323
|
}
|
|
290
324
|
function issues(error: z.ZodError): DiagnosticValidationError {
|
|
@@ -306,28 +340,19 @@ function issues(error: z.ZodError): DiagnosticValidationError {
|
|
|
306
340
|
function codeUnit(left: string, right: string): number {
|
|
307
341
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
308
342
|
}
|
|
309
|
-
function sortedRecord<T>(
|
|
310
|
-
value: Readonly<Record<string, T>>,
|
|
311
|
-
): Readonly<Record<string, T>> {
|
|
343
|
+
function sortedRecord<T>(value: Readonly<Record<string, T>>): Readonly<Record<string, T>> {
|
|
312
344
|
const result = diagnosticRecord<T>();
|
|
313
|
-
for (const key of Object.keys(value).sort(codeUnit))
|
|
314
|
-
result[key] = value[key]!;
|
|
345
|
+
for (const key of Object.keys(value).sort(codeUnit)) result[key] = value[key]!;
|
|
315
346
|
return result;
|
|
316
347
|
}
|
|
317
348
|
|
|
318
349
|
function explicitUndefinedIssues(value: unknown): DiagnosticValidationIssue[] {
|
|
319
350
|
const found: DiagnosticValidationIssue[] = [];
|
|
320
|
-
const stack: { readonly value: unknown; readonly path: string }[] = [
|
|
321
|
-
{ value, path: "$" },
|
|
322
|
-
];
|
|
351
|
+
const stack: { readonly value: unknown; readonly path: string }[] = [{ value, path: "$" }];
|
|
323
352
|
const seen = new WeakSet<object>();
|
|
324
353
|
while (stack.length > 0) {
|
|
325
354
|
const current = stack.pop()!;
|
|
326
|
-
if (
|
|
327
|
-
current.value === null ||
|
|
328
|
-
typeof current.value !== "object" ||
|
|
329
|
-
seen.has(current.value)
|
|
330
|
-
)
|
|
355
|
+
if (current.value === null || typeof current.value !== "object" || seen.has(current.value))
|
|
331
356
|
continue;
|
|
332
357
|
seen.add(current.value);
|
|
333
358
|
for (const [key, child] of Object.entries(current.value)) {
|
|
@@ -355,38 +380,29 @@ function explicitUndefinedIssues(value: unknown): DiagnosticValidationIssue[] {
|
|
|
355
380
|
return found;
|
|
356
381
|
}
|
|
357
382
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
):
|
|
383
|
+
// Both public gateways share the same full validation/ownership boundary.
|
|
384
|
+
// Selecting compact storage never invokes the eager preparation first.
|
|
385
|
+
function validateRunInputContent(value: unknown): DiagnosticRunInputContent {
|
|
361
386
|
const undefinedIssues = explicitUndefinedIssues(value);
|
|
362
|
-
if (undefinedIssues.length > 0)
|
|
363
|
-
throw new DiagnosticValidationError(undefinedIssues);
|
|
387
|
+
if (undefinedIssues.length > 0) throw new DiagnosticValidationError(undefinedIssues);
|
|
364
388
|
const parsed = runSchema.safeParse(value);
|
|
365
389
|
if (!parsed.success) throw issues(parsed.error);
|
|
366
|
-
const definition = compileDiagnosticDefinition(
|
|
367
|
-
|
|
390
|
+
const definition = compileDiagnosticDefinition(parsed.data.definition as DiagnosticDefinition);
|
|
391
|
+
const relationIssues: DiagnosticValidationIssue[] = parsed.data.losses.flatMap((row, index) =>
|
|
392
|
+
row.rowType === definition.definition.lossRowGrain
|
|
393
|
+
? []
|
|
394
|
+
: [
|
|
395
|
+
{
|
|
396
|
+
domain: "input" as const,
|
|
397
|
+
code: "invalid-input-relationship" as const,
|
|
398
|
+
path: `$.losses[${index}].rowType`,
|
|
399
|
+
message: "Loss row type does not match definition grain",
|
|
400
|
+
},
|
|
401
|
+
],
|
|
368
402
|
);
|
|
369
|
-
const relationIssues: DiagnosticValidationIssue[] =
|
|
370
|
-
parsed.data.losses.flatMap((row, index) =>
|
|
371
|
-
row.rowType === definition.definition.lossRowGrain
|
|
372
|
-
? []
|
|
373
|
-
: [
|
|
374
|
-
{
|
|
375
|
-
domain: "input" as const,
|
|
376
|
-
code: "invalid-input-relationship" as const,
|
|
377
|
-
path: `$.losses[${index}].rowType`,
|
|
378
|
-
message: "Loss row type does not match definition grain",
|
|
379
|
-
},
|
|
380
|
-
],
|
|
381
|
-
);
|
|
382
403
|
for (const [index, row] of (parsed.data.exposures ?? []).entries()) {
|
|
383
|
-
const measure = definition.definition.measures.find(
|
|
384
|
-
|
|
385
|
-
);
|
|
386
|
-
if (
|
|
387
|
-
measure?.exposureTiming === "valuation-specific" &&
|
|
388
|
-
row.valuation === undefined
|
|
389
|
-
)
|
|
404
|
+
const measure = definition.definition.measures.find((item) => item.id === row.measureId);
|
|
405
|
+
if (measure?.exposureTiming === "valuation-specific" && row.valuation === undefined)
|
|
390
406
|
relationIssues.push({
|
|
391
407
|
domain: "input",
|
|
392
408
|
code: "missing-required",
|
|
@@ -394,22 +410,11 @@ export function validateDiagnosticRunInput(
|
|
|
394
410
|
message: "Valuation-specific exposure requires valuation",
|
|
395
411
|
});
|
|
396
412
|
}
|
|
397
|
-
if (relationIssues.length)
|
|
398
|
-
|
|
399
|
-
const
|
|
400
|
-
"pass",
|
|
401
|
-
"warning",
|
|
402
|
-
"not-evaluated",
|
|
403
|
-
];
|
|
404
|
-
const metric = parsed.data.policy?.allowedMetricFindingSeverities ?? [
|
|
405
|
-
"info",
|
|
406
|
-
"warning",
|
|
407
|
-
];
|
|
413
|
+
if (relationIssues.length) throw new DiagnosticValidationError(relationIssues);
|
|
414
|
+
const review = parsed.data.policy?.allowedReviewStatuses ?? ["pass", "warning", "not-evaluated"];
|
|
415
|
+
const metric = parsed.data.policy?.allowedMetricFindingSeverities ?? ["info", "warning"];
|
|
408
416
|
const rationale = parsed.data.policy?.rationaleRef ?? null;
|
|
409
|
-
if (
|
|
410
|
-
(review.includes("fail") || metric.includes("fail")) &&
|
|
411
|
-
rationale === null
|
|
412
|
-
)
|
|
417
|
+
if ((review.includes("fail") || metric.includes("fail")) && rationale === null)
|
|
413
418
|
throw new DiagnosticValidationError([
|
|
414
419
|
{
|
|
415
420
|
domain: "configuration",
|
|
@@ -419,24 +424,16 @@ export function validateDiagnosticRunInput(
|
|
|
419
424
|
},
|
|
420
425
|
]);
|
|
421
426
|
const reviewEvidence =
|
|
422
|
-
parsed.data.reviewEvidence === undefined ||
|
|
423
|
-
parsed.data.reviewEvidence === null
|
|
427
|
+
parsed.data.reviewEvidence === undefined || parsed.data.reviewEvidence === null
|
|
424
428
|
? null
|
|
425
|
-
: validateDiagnosticReviewEvidence(
|
|
426
|
-
parsed.data.reviewEvidence,
|
|
427
|
-
"$.reviewEvidence",
|
|
428
|
-
);
|
|
429
|
+
: validateDiagnosticReviewEvidence(parsed.data.reviewEvidence, "$.reviewEvidence");
|
|
429
430
|
const reviewOrder: readonly DiagnosticAllowedReviewStatus[] = [
|
|
430
431
|
"pass",
|
|
431
432
|
"warning",
|
|
432
433
|
"not-evaluated",
|
|
433
434
|
"fail",
|
|
434
435
|
];
|
|
435
|
-
const metricOrder: readonly ("info" | "warning" | "fail")[] = [
|
|
436
|
-
"info",
|
|
437
|
-
"warning",
|
|
438
|
-
"fail",
|
|
439
|
-
];
|
|
436
|
+
const metricOrder: readonly ("info" | "warning" | "fail")[] = ["info", "warning", "fail"];
|
|
440
437
|
for (const key of [
|
|
441
438
|
...Object.keys(parsed.data.groupMap ?? {}),
|
|
442
439
|
...Object.keys(parsed.data.groupDimensions ?? {}),
|
|
@@ -447,8 +444,7 @@ export function validateDiagnosticRunInput(
|
|
|
447
444
|
domain: "configuration",
|
|
448
445
|
code: "invalid-string",
|
|
449
446
|
path: `$.groupMap[${JSON.stringify(key)}]`,
|
|
450
|
-
message:
|
|
451
|
-
"Group key must be a nonempty token with valid Unicode and no U+0000",
|
|
447
|
+
message: "Group key must be a nonempty token with valid Unicode and no U+0000",
|
|
452
448
|
},
|
|
453
449
|
]);
|
|
454
450
|
const result = freeze({
|
|
@@ -461,39 +457,76 @@ export function validateDiagnosticRunInput(
|
|
|
461
457
|
reviewEvidence,
|
|
462
458
|
runPresetId: parsed.data.runPresetId ?? null,
|
|
463
459
|
datasetArtifactId: parsed.data.datasetArtifactId ?? null,
|
|
464
|
-
groupMap: sortedRecord(parsed.data.groupMap ?? diagnosticRecord()),
|
|
465
|
-
groupDimensions: sortedRecord(
|
|
466
|
-
parsed.data.groupDimensions ?? diagnosticRecord(),
|
|
460
|
+
groupMap: sortedRecord<string>(parsed.data.groupMap ?? diagnosticRecord<string>()),
|
|
461
|
+
groupDimensions: sortedRecord<JsonValue>(
|
|
462
|
+
parsed.data.groupDimensions ?? diagnosticRecord<JsonValue>(),
|
|
467
463
|
),
|
|
468
464
|
policy: {
|
|
469
|
-
allowedReviewStatuses: reviewOrder.filter((status) =>
|
|
470
|
-
|
|
471
|
-
),
|
|
472
|
-
allowedMetricFindingSeverities: metricOrder.filter((severity) =>
|
|
473
|
-
metric.includes(severity),
|
|
474
|
-
),
|
|
465
|
+
allowedReviewStatuses: reviewOrder.filter((status) => review.includes(status)),
|
|
466
|
+
allowedMetricFindingSeverities: metricOrder.filter((severity) => metric.includes(severity)),
|
|
475
467
|
rationaleRef: rationale,
|
|
476
468
|
},
|
|
477
|
-
}) as unknown as ValidatedDiagnosticRunInput;
|
|
478
|
-
const prepared = prepareDiagnosticData({
|
|
479
|
-
definition: result.definition,
|
|
480
|
-
losses: result.losses,
|
|
481
|
-
exposures: result.exposures,
|
|
482
|
-
...(result.filter === null ? {} : { filter: result.filter }),
|
|
483
|
-
completePeriodCutoffs: result.completePeriodCutoffs,
|
|
484
|
-
...(result.expectedCells === null
|
|
485
|
-
? {}
|
|
486
|
-
: { expectedCells: result.expectedCells }),
|
|
487
469
|
});
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function preparationInput(input: DiagnosticRunInputContent) {
|
|
474
|
+
return {
|
|
475
|
+
definition: input.definition,
|
|
476
|
+
losses: input.losses,
|
|
477
|
+
exposures: input.exposures,
|
|
478
|
+
...(input.filter === null ? {} : { filter: input.filter }),
|
|
479
|
+
completePeriodCutoffs: input.completePeriodCutoffs,
|
|
480
|
+
...(input.expectedCells === null ? {} : { expectedCells: input.expectedCells }),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function validateDiagnosticRunInput(value: unknown): ValidatedDiagnosticRunInput {
|
|
485
|
+
const result = validateRunInputContent(value) as ValidatedDiagnosticRunInput;
|
|
486
|
+
const prepared = prepareDiagnosticData(preparationInput(result));
|
|
488
487
|
validateDiagnosticGroupingConfiguration({
|
|
489
488
|
prepared,
|
|
490
489
|
groupMap: result.groupMap,
|
|
491
490
|
groupDimensions: result.groupDimensions,
|
|
492
491
|
});
|
|
493
492
|
authentic.add(result);
|
|
493
|
+
preparedByInput.set(result, prepared);
|
|
494
494
|
return result;
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
/** Validate, own and prepare inputs without eagerly materializing identity graphs. */
|
|
498
|
+
export function validateDiagnosticRunInputCompact(
|
|
499
|
+
value: unknown,
|
|
500
|
+
): CompactValidatedDiagnosticRunInput {
|
|
501
|
+
const result = validateRunInputContent(value) as CompactValidatedDiagnosticRunInput;
|
|
502
|
+
const prepared = prepareDiagnosticDataCompact(preparationInput(result));
|
|
503
|
+
validateCompactDiagnosticGroupingConfiguration({
|
|
504
|
+
prepared,
|
|
505
|
+
groupMap: result.groupMap,
|
|
506
|
+
groupDimensions: result.groupDimensions,
|
|
507
|
+
});
|
|
508
|
+
compactPreparedByInput.set(result, prepared);
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function assertCompactValidatedDiagnosticRunInput(
|
|
513
|
+
value: unknown,
|
|
514
|
+
): asserts value is CompactValidatedDiagnosticRunInput {
|
|
515
|
+
if (
|
|
516
|
+
value === null ||
|
|
517
|
+
typeof value !== "object" ||
|
|
518
|
+
!compactPreparedByInput.has(value as CompactValidatedDiagnosticRunInput)
|
|
519
|
+
)
|
|
520
|
+
throw new DiagnosticValidationError([
|
|
521
|
+
{
|
|
522
|
+
domain: "input",
|
|
523
|
+
code: "invalid-input-relationship",
|
|
524
|
+
path: "$",
|
|
525
|
+
message: "Value is not an authentic compact validated diagnostic run input",
|
|
526
|
+
},
|
|
527
|
+
]);
|
|
528
|
+
}
|
|
529
|
+
|
|
497
530
|
export function assertValidatedDiagnosticRunInput(
|
|
498
531
|
value: unknown,
|
|
499
532
|
): asserts value is ValidatedDiagnosticRunInput {
|
|
@@ -513,16 +546,9 @@ export function runValidatedMetricDiagnostics(
|
|
|
513
546
|
input: ValidatedDiagnosticRunInput,
|
|
514
547
|
): ValidatedMetricDiagnosticsOutcome {
|
|
515
548
|
assertValidatedDiagnosticRunInput(input);
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
exposures: input.exposures,
|
|
520
|
-
...(input.filter === null ? {} : { filter: input.filter }),
|
|
521
|
-
completePeriodCutoffs: input.completePeriodCutoffs,
|
|
522
|
-
...(input.expectedCells === null
|
|
523
|
-
? {}
|
|
524
|
-
: { expectedCells: input.expectedCells }),
|
|
525
|
-
});
|
|
549
|
+
// Validation already prepared these exact immutable inputs and checked their
|
|
550
|
+
// grouping. Reuse the authentic result without skipping any execution gate.
|
|
551
|
+
const prepared = preparedByInput.get(input)!;
|
|
526
552
|
validateDiagnosticGroupingConfiguration({
|
|
527
553
|
prepared,
|
|
528
554
|
groupMap: input.groupMap,
|
|
@@ -547,10 +573,7 @@ export function runValidatedMetricDiagnostics(
|
|
|
547
573
|
(check) => !input.policy.allowedReviewStatuses.includes(check.status),
|
|
548
574
|
) ||
|
|
549
575
|
review.evaluations.some(
|
|
550
|
-
(evaluation) =>
|
|
551
|
-
!input.policy.allowedReviewStatuses.includes(
|
|
552
|
-
evaluationStatus(evaluation),
|
|
553
|
-
),
|
|
576
|
+
(evaluation) => !input.policy.allowedReviewStatuses.includes(evaluationStatus(evaluation)),
|
|
554
577
|
);
|
|
555
578
|
const base = {
|
|
556
579
|
prepared,
|
|
@@ -560,8 +583,13 @@ export function runValidatedMetricDiagnostics(
|
|
|
560
583
|
groupMap: input.groupMap,
|
|
561
584
|
groupDimensions: input.groupDimensions,
|
|
562
585
|
};
|
|
586
|
+
// These graphs were deeply frozen by their authentic SDK constructors. Walk
|
|
587
|
+
// only the new outcome envelope; a shallow-frozen caller object never enters
|
|
588
|
+
// this set, so the public input boundary still checks/freezes every child.
|
|
589
|
+
const frozenSdkGraphs = new WeakSet<object>([prepared, review]);
|
|
590
|
+
const freezeOutcome = <T>(value: T) => freeze(value, frozenSdkGraphs);
|
|
563
591
|
if (disallowedReview)
|
|
564
|
-
return
|
|
592
|
+
return freezeOutcome({
|
|
565
593
|
...base,
|
|
566
594
|
status: "blocked" as const,
|
|
567
595
|
stage: "review" as const,
|
|
@@ -577,13 +605,14 @@ export function runValidatedMetricDiagnostics(
|
|
|
577
605
|
groupMap: input.groupMap,
|
|
578
606
|
groupDimensions: input.groupDimensions,
|
|
579
607
|
});
|
|
608
|
+
frozenSdkGraphs.add(result);
|
|
580
609
|
const disallowedMetric = result.findings.some(
|
|
581
610
|
(finding) =>
|
|
582
611
|
finding.category !== "structural" &&
|
|
583
612
|
!input.policy.allowedMetricFindingSeverities.includes(finding.severity),
|
|
584
613
|
);
|
|
585
614
|
if (disallowedMetric)
|
|
586
|
-
return
|
|
615
|
+
return freezeOutcome({
|
|
587
616
|
...base,
|
|
588
617
|
status: "blocked" as const,
|
|
589
618
|
stage: "metric" as const,
|
|
@@ -594,7 +623,7 @@ export function runValidatedMetricDiagnostics(
|
|
|
594
623
|
metricGate: "blocked" as const,
|
|
595
624
|
},
|
|
596
625
|
}) as ValidatedMetricDiagnosticsOutcome;
|
|
597
|
-
const outcome =
|
|
626
|
+
const outcome = freezeOutcome({
|
|
598
627
|
...base,
|
|
599
628
|
status: "completed" as const,
|
|
600
629
|
result,
|
|
@@ -620,3 +649,121 @@ export function assertCompletedValidatedMetricDiagnosticsRun(
|
|
|
620
649
|
},
|
|
621
650
|
]);
|
|
622
651
|
}
|
|
652
|
+
|
|
653
|
+
/** Run every review and metric gate while retaining complete compact evidence. */
|
|
654
|
+
export function runValidatedMetricDiagnosticsCompact(
|
|
655
|
+
input: CompactValidatedDiagnosticRunInput,
|
|
656
|
+
): CompactMetricDiagnosticsOutcome {
|
|
657
|
+
assertCompactValidatedDiagnosticRunInput(input);
|
|
658
|
+
const prepared = compactPreparedByInput.get(input)!;
|
|
659
|
+
validateCompactDiagnosticGroupingConfiguration({
|
|
660
|
+
prepared,
|
|
661
|
+
groupMap: input.groupMap,
|
|
662
|
+
groupDimensions: input.groupDimensions,
|
|
663
|
+
});
|
|
664
|
+
const review = reviewPreparedDiagnosticDataCompact({
|
|
665
|
+
prepared,
|
|
666
|
+
evidence: input.reviewEvidence as DiagnosticReviewEvidence | null,
|
|
667
|
+
});
|
|
668
|
+
const counts = review.evaluations.summary;
|
|
669
|
+
// Aggregate check status and individual effective status are both necessary:
|
|
670
|
+
// an allowed failure must not hide a disallowed not-evaluated row (or vice versa).
|
|
671
|
+
const effectiveCounts: Readonly<Record<DiagnosticAllowedReviewStatus, number>> = {
|
|
672
|
+
pass: counts.pass,
|
|
673
|
+
warning: counts.warning,
|
|
674
|
+
"not-evaluated": counts.notEvaluated,
|
|
675
|
+
fail: counts.fail,
|
|
676
|
+
};
|
|
677
|
+
const disallowedReview =
|
|
678
|
+
review.report.checks.some(
|
|
679
|
+
(check) => !input.policy.allowedReviewStatuses.includes(check.status),
|
|
680
|
+
) ||
|
|
681
|
+
(Object.keys(effectiveCounts) as DiagnosticAllowedReviewStatus[]).some(
|
|
682
|
+
(status) =>
|
|
683
|
+
effectiveCounts[status] > 0 && !input.policy.allowedReviewStatuses.includes(status),
|
|
684
|
+
);
|
|
685
|
+
const base = {
|
|
686
|
+
prepared,
|
|
687
|
+
review,
|
|
688
|
+
runPresetId: input.runPresetId,
|
|
689
|
+
datasetArtifactId: input.datasetArtifactId,
|
|
690
|
+
groupMap: input.groupMap,
|
|
691
|
+
groupDimensions: input.groupDimensions,
|
|
692
|
+
};
|
|
693
|
+
const frozenSdkGraphs = new WeakSet<object>([prepared, review]);
|
|
694
|
+
const freezeOutcome = <T>(value: T) => freeze(value, frozenSdkGraphs);
|
|
695
|
+
if (disallowedReview)
|
|
696
|
+
return freezeOutcome({
|
|
697
|
+
...base,
|
|
698
|
+
status: "blocked" as const,
|
|
699
|
+
stage: "review" as const,
|
|
700
|
+
result: null,
|
|
701
|
+
gate: {
|
|
702
|
+
...input.policy,
|
|
703
|
+
reviewGate: "blocked" as const,
|
|
704
|
+
metricGate: "not-run" as const,
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
const result = runMetricDiagnosticsCompact({
|
|
708
|
+
prepared,
|
|
709
|
+
groupMap: input.groupMap,
|
|
710
|
+
groupDimensions: input.groupDimensions,
|
|
711
|
+
});
|
|
712
|
+
frozenSdkGraphs.add(result);
|
|
713
|
+
const disallowedMetric = result.findings.some(
|
|
714
|
+
(finding) =>
|
|
715
|
+
finding.category !== "structural" &&
|
|
716
|
+
!input.policy.allowedMetricFindingSeverities.includes(finding.severity),
|
|
717
|
+
);
|
|
718
|
+
if (disallowedMetric)
|
|
719
|
+
return freezeOutcome({
|
|
720
|
+
...base,
|
|
721
|
+
status: "blocked" as const,
|
|
722
|
+
stage: "metric" as const,
|
|
723
|
+
result,
|
|
724
|
+
gate: {
|
|
725
|
+
...input.policy,
|
|
726
|
+
reviewGate: "passed" as const,
|
|
727
|
+
metricGate: "blocked" as const,
|
|
728
|
+
},
|
|
729
|
+
});
|
|
730
|
+
const outcome = freezeOutcome({
|
|
731
|
+
...base,
|
|
732
|
+
status: "completed" as const,
|
|
733
|
+
result,
|
|
734
|
+
gate: {
|
|
735
|
+
...input.policy,
|
|
736
|
+
reviewGate: "passed" as const,
|
|
737
|
+
metricGate: "passed" as const,
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
compactCompleted.add(outcome);
|
|
741
|
+
compactInputByCompletedRun.set(outcome, input);
|
|
742
|
+
return outcome;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function assertCompletedCompactMetricDiagnosticsRun(
|
|
746
|
+
value: unknown,
|
|
747
|
+
): asserts value is CompletedCompactMetricDiagnosticsRun {
|
|
748
|
+
if (value === null || typeof value !== "object" || !compactCompleted.has(value))
|
|
749
|
+
throw new DiagnosticValidationError([
|
|
750
|
+
{
|
|
751
|
+
domain: "input",
|
|
752
|
+
code: "invalid-input-relationship",
|
|
753
|
+
path: "$",
|
|
754
|
+
message: "Value is not an authentic completed compact diagnostic run",
|
|
755
|
+
},
|
|
756
|
+
]);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Return the original SDK-owned validated input for an authentic completed run.
|
|
761
|
+
* This is the same deeply immutable input owner, not a reconstruction from its
|
|
762
|
+
* normalized audit or a mutable copy of the caller's upload values.
|
|
763
|
+
*/
|
|
764
|
+
export function getCompletedCompactDiagnosticRunInput(
|
|
765
|
+
run: CompletedCompactMetricDiagnosticsRun,
|
|
766
|
+
): CompactValidatedDiagnosticRunInput {
|
|
767
|
+
assertCompletedCompactMetricDiagnosticsRun(run);
|
|
768
|
+
return compactInputByCompletedRun.get(run)!;
|
|
769
|
+
}
|