@actuarial-ts/compliance 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 +6 -2
- package/dist/diagnosticArtifactStream.d.ts +30 -0
- package/dist/diagnosticArtifactStream.d.ts.map +1 -0
- package/dist/diagnosticArtifactStream.js +128 -0
- package/dist/diagnosticArtifactStream.js.map +1 -0
- package/dist/diagnosticByteSource.d.ts +10 -0
- package/dist/diagnosticByteSource.d.ts.map +1 -0
- package/dist/diagnosticByteSource.js +70 -0
- package/dist/diagnosticByteSource.js.map +1 -0
- package/dist/diagnosticByteView.d.ts +3 -0
- package/dist/diagnosticByteView.d.ts.map +1 -0
- package/dist/diagnosticByteView.js +27 -0
- package/dist/diagnosticByteView.js.map +1 -0
- package/dist/diagnosticCompactRun.d.ts +43 -0
- package/dist/diagnosticCompactRun.d.ts.map +1 -0
- package/dist/diagnosticCompactRun.js +253 -0
- package/dist/diagnosticCompactRun.js.map +1 -0
- package/dist/diagnosticReplayFrames.d.ts +10 -0
- package/dist/diagnosticReplayFrames.d.ts.map +1 -0
- package/dist/diagnosticReplayFrames.js +99 -0
- package/dist/diagnosticReplayFrames.js.map +1 -0
- package/dist/diagnosticReplayProtocol.d.ts +17 -0
- package/dist/diagnosticReplayProtocol.d.ts.map +1 -0
- package/dist/diagnosticReplayProtocol.js +98 -0
- package/dist/diagnosticReplayProtocol.js.map +1 -0
- package/dist/diagnosticReplayReader.d.ts +41 -0
- package/dist/diagnosticReplayReader.d.ts.map +1 -0
- package/dist/diagnosticReplayReader.js +346 -0
- package/dist/diagnosticReplayReader.js.map +1 -0
- package/dist/diagnosticReplayValue.d.ts +34 -0
- package/dist/diagnosticReplayValue.d.ts.map +1 -0
- package/dist/diagnosticReplayValue.js +269 -0
- package/dist/diagnosticReplayValue.js.map +1 -0
- package/dist/diagnosticReplayWriter.d.ts +25 -0
- package/dist/diagnosticReplayWriter.d.ts.map +1 -0
- package/dist/diagnosticReplayWriter.js +148 -0
- package/dist/diagnosticReplayWriter.js.map +1 -0
- package/dist/diagnosticRun.d.ts.map +1 -1
- package/dist/diagnosticRun.js +1 -193
- package/dist/diagnosticRun.js.map +1 -1
- package/dist/diagnosticRunEvidence.d.ts +18 -0
- package/dist/diagnosticRunEvidence.d.ts.map +1 -0
- package/dist/diagnosticRunEvidence.js +195 -0
- package/dist/diagnosticRunEvidence.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/sha256Stream.d.ts +6 -0
- package/dist/sha256Stream.d.ts.map +1 -0
- package/dist/sha256Stream.js +103 -0
- package/dist/sha256Stream.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/src/diagnosticArtifactStream.ts +148 -0
- package/src/diagnosticByteSource.ts +82 -0
- package/src/diagnosticByteView.ts +27 -0
- package/src/diagnosticCompactRun.ts +462 -0
- package/src/diagnosticReplayFrames.ts +99 -0
- package/src/diagnosticReplayProtocol.ts +99 -0
- package/src/diagnosticReplayReader.ts +428 -0
- package/src/diagnosticReplayValue.ts +269 -0
- package/src/diagnosticReplayWriter.ts +190 -0
- package/src/diagnosticRun.ts +7 -373
- package/src/diagnosticRunEvidence.ts +391 -0
- package/src/index.ts +10 -0
- package/src/sha256Stream.ts +108 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { iterateDiagnosticIdentityJson } from "@actuarial-ts/core";
|
|
2
|
+
import { getCompletedCompactDiagnosticRunInput } from "@actuarial-ts/data";
|
|
3
|
+
import {
|
|
4
|
+
assertVerifiedCompactDiagnosticRunProvenance,
|
|
5
|
+
getCompactDiagnosticRunOwner,
|
|
6
|
+
type VerifiedCompactDiagnosticRunProvenance,
|
|
7
|
+
} from "./diagnosticCompactRun.js";
|
|
8
|
+
import type { DiagnosticArtifactDigest } from "./diagnosticRun.js";
|
|
9
|
+
import { diagnosticByteView } from "./diagnosticByteView.js";
|
|
10
|
+
import { consumeDiagnosticByteSource } from "./diagnosticByteSource.js";
|
|
11
|
+
import {
|
|
12
|
+
encodeReplayFrames,
|
|
13
|
+
REPLAY_TEXT_UNITS,
|
|
14
|
+
type ReplayFrame,
|
|
15
|
+
} from "./diagnosticReplayFrames.js";
|
|
16
|
+
import { replayValueFrames } from "./diagnosticReplayValue.js";
|
|
17
|
+
import { createSha256 } from "./sha256Stream.js";
|
|
18
|
+
import {
|
|
19
|
+
ARTIFACT_CHUNK_BYTES,
|
|
20
|
+
REPLAY_FORMAT,
|
|
21
|
+
REPLAY_VERSION,
|
|
22
|
+
badReplay,
|
|
23
|
+
mismatch,
|
|
24
|
+
replayArray,
|
|
25
|
+
replayCancellation,
|
|
26
|
+
replayFrameBytes,
|
|
27
|
+
replayRecord,
|
|
28
|
+
replayToken,
|
|
29
|
+
encodeArtifactChunk,
|
|
30
|
+
} from "./diagnosticReplayProtocol.js";
|
|
31
|
+
|
|
32
|
+
export interface DiagnosticReplayStreamRun {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly provenance: VerifiedCompactDiagnosticRunProvenance;
|
|
35
|
+
}
|
|
36
|
+
export type DiagnosticReplayByteSource = Iterable<Uint8Array> | AsyncIterable<Uint8Array>;
|
|
37
|
+
export interface WriteDiagnosticReplayStreamInput {
|
|
38
|
+
readonly runs: readonly DiagnosticReplayStreamRun[];
|
|
39
|
+
/** Fresh stream of at most 64 KiB per chunk, for each unique computed artifact. */
|
|
40
|
+
readonly openArtifact: (
|
|
41
|
+
artifact: Extract<DiagnosticArtifactDigest, { assurance: "sdk-computed" }>,
|
|
42
|
+
) => DiagnosticReplayByteSource | Promise<DiagnosticReplayByteSource>;
|
|
43
|
+
readonly signal?: AbortSignal;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function replayInput(provenance: VerifiedCompactDiagnosticRunProvenance) {
|
|
47
|
+
const input = getCompletedCompactDiagnosticRunInput(getCompactDiagnosticRunOwner(provenance));
|
|
48
|
+
// Only a small envelope is new. All dataset/evidence arrays are the immutable
|
|
49
|
+
// originals owned by the validated gateway, including filtered-out inputs.
|
|
50
|
+
// Definition uses the normalized wire shape accepted by the core compiler,
|
|
51
|
+
// not a falsely typed authored definition with its optional-null defaults.
|
|
52
|
+
return {
|
|
53
|
+
definition: input.definition.definition,
|
|
54
|
+
losses: input.losses,
|
|
55
|
+
exposures: input.exposures,
|
|
56
|
+
...(input.filter === null ? {} : { filter: input.filter }),
|
|
57
|
+
completePeriodCutoffs: input.completePeriodCutoffs,
|
|
58
|
+
...(input.expectedCells === null ? {} : { expectedCells: input.expectedCells }),
|
|
59
|
+
reviewEvidence: input.reviewEvidence,
|
|
60
|
+
...(input.runPresetId === null ? {} : { runPresetId: input.runPresetId }),
|
|
61
|
+
...(input.datasetArtifactId === null ? {} : { datasetArtifactId: input.datasetArtifactId }),
|
|
62
|
+
groupMap: input.groupMap,
|
|
63
|
+
groupDimensions: input.groupDimensions,
|
|
64
|
+
policy: {
|
|
65
|
+
allowedReviewStatuses: input.policy.allowedReviewStatuses,
|
|
66
|
+
allowedMetricFindingSeverities: input.policy.allowedMetricFindingSeverities,
|
|
67
|
+
...(input.policy.rationaleRef === null ? {} : { rationaleRef: input.policy.rationaleRef }),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Versioned large-data replay, NOT an interchange BundleDoc or the old JSON
|
|
74
|
+
* provenance DTO. Writes only from genuine immutable provenance owners. The
|
|
75
|
+
* host owns I/O and must publish a file only after the generator completes.
|
|
76
|
+
* All artifact bytes are rechecked against their authenticated digests while
|
|
77
|
+
* writing. Consumer cancellation closes producers; pending host I/O should use
|
|
78
|
+
* the same signal. No full candidate identity or whole archive string exists.
|
|
79
|
+
*/
|
|
80
|
+
export function writeDiagnosticReplayStream(
|
|
81
|
+
input: WriteDiagnosticReplayStreamInput,
|
|
82
|
+
): AsyncGenerator<Uint8Array> {
|
|
83
|
+
// Snapshot at call time, before the first await/yield (not at first next()).
|
|
84
|
+
const options = replayRecord(input, ["runs", "openArtifact", "signal"]);
|
|
85
|
+
const runs = replayArray(options.runs).map((value) => {
|
|
86
|
+
const item = replayRecord(value, ["id", "provenance"]);
|
|
87
|
+
const id = replayToken(item.id);
|
|
88
|
+
assertVerifiedCompactDiagnosticRunProvenance(item.provenance);
|
|
89
|
+
return Object.freeze({ id, provenance: item.provenance });
|
|
90
|
+
});
|
|
91
|
+
if (!runs.length || new Set(runs.map((run) => run.id)).size !== runs.length)
|
|
92
|
+
badReplay("Replay must have at least one run and unique run IDs");
|
|
93
|
+
if (typeof options.openArtifact !== "function")
|
|
94
|
+
badReplay("Replay openArtifact must be a function");
|
|
95
|
+
const openArtifact = options.openArtifact as WriteDiagnosticReplayStreamInput["openArtifact"];
|
|
96
|
+
const checkCancellation = replayCancellation(options.signal);
|
|
97
|
+
checkCancellation();
|
|
98
|
+
const artifacts = new Map<string, DiagnosticArtifactDigest>();
|
|
99
|
+
for (const { provenance } of runs)
|
|
100
|
+
for (const artifact of [...provenance.inputArtifacts, ...provenance.preparationArtifacts]) {
|
|
101
|
+
const prior = artifacts.get(artifact.id);
|
|
102
|
+
if (
|
|
103
|
+
prior &&
|
|
104
|
+
(prior.scope !== artifact.scope ||
|
|
105
|
+
prior.assurance !== artifact.assurance ||
|
|
106
|
+
prior.algorithm !== artifact.algorithm ||
|
|
107
|
+
prior.value !== artifact.value ||
|
|
108
|
+
(prior.assurance === "sdk-computed" &&
|
|
109
|
+
artifact.assurance === "sdk-computed" &&
|
|
110
|
+
prior.byteLength !== artifact.byteLength))
|
|
111
|
+
)
|
|
112
|
+
badReplay("Shared replay artifact IDs have conflicting evidence");
|
|
113
|
+
artifacts.set(artifact.id, artifact);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function* body(): AsyncGenerator<ReplayFrame> {
|
|
117
|
+
yield [REPLAY_FORMAT, REPLAY_VERSION];
|
|
118
|
+
for (const artifact of artifacts.values()) {
|
|
119
|
+
checkCancellation();
|
|
120
|
+
yield ["artifact"];
|
|
121
|
+
yield* replayValueFrames(artifact);
|
|
122
|
+
yield ["artifact-data"];
|
|
123
|
+
if (artifact.assurance === "sdk-computed") {
|
|
124
|
+
const hash = createSha256();
|
|
125
|
+
let length = 0;
|
|
126
|
+
for await (const raw of consumeDiagnosticByteSource(
|
|
127
|
+
await openArtifact(artifact),
|
|
128
|
+
checkCancellation,
|
|
129
|
+
)) {
|
|
130
|
+
checkCancellation();
|
|
131
|
+
const view = diagnosticByteView(raw);
|
|
132
|
+
if (view.byteLength > ARTIFACT_CHUNK_BYTES)
|
|
133
|
+
badReplay("Artifact read chunks must be at most 64 KiB");
|
|
134
|
+
// Snapshot the entire bounded delivered chunk before yielding.
|
|
135
|
+
const owned = new Uint8Array(view);
|
|
136
|
+
length += owned.length;
|
|
137
|
+
if (!Number.isSafeInteger(length) || length > artifact.byteLength)
|
|
138
|
+
mismatch("Artifact grew after its verified digest");
|
|
139
|
+
hash.update(owned);
|
|
140
|
+
if (owned.length) yield ["bytes", encodeArtifactChunk(owned)];
|
|
141
|
+
}
|
|
142
|
+
checkCancellation();
|
|
143
|
+
if (length !== artifact.byteLength || hash.digest() !== artifact.value)
|
|
144
|
+
mismatch("Artifact bytes changed after their verified digest");
|
|
145
|
+
}
|
|
146
|
+
yield ["artifact-end"];
|
|
147
|
+
}
|
|
148
|
+
for (const { id, provenance } of runs) {
|
|
149
|
+
checkCancellation();
|
|
150
|
+
yield ["run"];
|
|
151
|
+
yield* replayValueFrames({
|
|
152
|
+
id,
|
|
153
|
+
input: replayInput(provenance),
|
|
154
|
+
inputArtifactIds: provenance.inputArtifacts.map((item) => item.id),
|
|
155
|
+
preparationArtifactIds: provenance.preparationArtifacts.map((item) => item.id),
|
|
156
|
+
preparationLineage: provenance.preparationLineage,
|
|
157
|
+
});
|
|
158
|
+
yield ["input-end"];
|
|
159
|
+
for (const [channel, document] of [
|
|
160
|
+
["manifest", provenance.manifestIdentityDocument],
|
|
161
|
+
["result", provenance.resultIdentityDocument],
|
|
162
|
+
] as const) {
|
|
163
|
+
for (const text of iterateDiagnosticIdentityJson(document))
|
|
164
|
+
for (let start = 0; start < text.length; start += REPLAY_TEXT_UNITS)
|
|
165
|
+
yield [channel, text.slice(start, start + REPLAY_TEXT_UNITS)];
|
|
166
|
+
yield [`${channel}-end`];
|
|
167
|
+
}
|
|
168
|
+
yield [
|
|
169
|
+
"run-end",
|
|
170
|
+
provenance.runFingerprint,
|
|
171
|
+
provenance.resultFingerprint,
|
|
172
|
+
provenance.runResultFingerprint,
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function* sequenced(): AsyncGenerator<ReplayFrame> {
|
|
177
|
+
let sequence = 0;
|
|
178
|
+
const hash = createSha256();
|
|
179
|
+
for await (const event of body()) {
|
|
180
|
+
checkCancellation();
|
|
181
|
+
if (!Number.isSafeInteger(sequence + 1)) badReplay("Replay sequence exceeded safe integers");
|
|
182
|
+
const frame = [sequence++, ...event];
|
|
183
|
+
hash.update(replayFrameBytes(frame));
|
|
184
|
+
yield frame;
|
|
185
|
+
}
|
|
186
|
+
checkCancellation();
|
|
187
|
+
yield [sequence, "end", artifacts.size, runs.length, hash.digest()];
|
|
188
|
+
}
|
|
189
|
+
return encodeReplayFrames(sequenced());
|
|
190
|
+
}
|
package/src/diagnosticRun.ts
CHANGED
|
@@ -29,6 +29,13 @@ import {
|
|
|
29
29
|
} from "@actuarial-ts/data";
|
|
30
30
|
import { ComplianceError } from "./errors.js";
|
|
31
31
|
import { COMPLIANCE_PACKAGE_VERSION } from "./version.js";
|
|
32
|
+
import {
|
|
33
|
+
token,
|
|
34
|
+
plain,
|
|
35
|
+
exactKeys,
|
|
36
|
+
snapshotLineage,
|
|
37
|
+
validateArtifactGraph,
|
|
38
|
+
} from "./diagnosticRunEvidence.js";
|
|
32
39
|
|
|
33
40
|
export interface DiagnosticArtifactDigestBase {
|
|
34
41
|
readonly id: string;
|
|
@@ -141,36 +148,6 @@ export interface CreateDiagnosticRunIdentityInput {
|
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
const verified = new WeakMap<object, CompletedValidatedMetricDiagnosticsRun>();
|
|
144
|
-
const token = (value: string, path: string) => {
|
|
145
|
-
if (
|
|
146
|
-
value.length === 0 ||
|
|
147
|
-
/^[\t-\r ]|[\t-\r ]$/.test(value) ||
|
|
148
|
-
value.includes("\0")
|
|
149
|
-
)
|
|
150
|
-
throw new ComplianceError(
|
|
151
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
152
|
-
`${path} must be a nonempty token`,
|
|
153
|
-
path,
|
|
154
|
-
);
|
|
155
|
-
for (let index = 0; index < value.length; index++) {
|
|
156
|
-
const unit = value.charCodeAt(index);
|
|
157
|
-
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
158
|
-
const next = value.charCodeAt(index + 1);
|
|
159
|
-
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
160
|
-
throw new ComplianceError(
|
|
161
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
162
|
-
`${path} contains malformed Unicode`,
|
|
163
|
-
path,
|
|
164
|
-
);
|
|
165
|
-
index++;
|
|
166
|
-
} else if (unit >= 0xdc00 && unit <= 0xdfff)
|
|
167
|
-
throw new ComplianceError(
|
|
168
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
169
|
-
`${path} contains malformed Unicode`,
|
|
170
|
-
path,
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
151
|
function freeze<T>(
|
|
175
152
|
value: T,
|
|
176
153
|
seen = new WeakSet<object>(),
|
|
@@ -210,25 +187,6 @@ type SnapshottedArtifact =
|
|
|
210
187
|
readonly assurance: "sdk-computed";
|
|
211
188
|
readonly bytes: Uint8Array;
|
|
212
189
|
};
|
|
213
|
-
function plain(value: unknown): value is Record<string, unknown> {
|
|
214
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
215
|
-
return false;
|
|
216
|
-
const prototype = Object.getPrototypeOf(value);
|
|
217
|
-
return prototype === Object.prototype || prototype === null;
|
|
218
|
-
}
|
|
219
|
-
function exactKeys(
|
|
220
|
-
value: Record<string, unknown>,
|
|
221
|
-
allowed: readonly string[],
|
|
222
|
-
path: string,
|
|
223
|
-
): void {
|
|
224
|
-
for (const key of Object.keys(value))
|
|
225
|
-
if (!allowed.includes(key))
|
|
226
|
-
throw new ComplianceError(
|
|
227
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
228
|
-
`Unknown field ${key}`,
|
|
229
|
-
`${path}.${key}`,
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
190
|
function snapshotArtifacts(
|
|
233
191
|
evidence: unknown,
|
|
234
192
|
scope: "input" | "preparation",
|
|
@@ -348,330 +306,6 @@ async function digestArtifacts(
|
|
|
348
306
|
);
|
|
349
307
|
}
|
|
350
308
|
|
|
351
|
-
function snapshotLineage(
|
|
352
|
-
lineage: unknown,
|
|
353
|
-
): readonly DiagnosticPreparationLineage[] {
|
|
354
|
-
if (!Array.isArray(lineage))
|
|
355
|
-
throw new ComplianceError(
|
|
356
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
357
|
-
"$.preparationLineage must be an array",
|
|
358
|
-
"$.preparationLineage",
|
|
359
|
-
);
|
|
360
|
-
return lineage
|
|
361
|
-
.map((raw, index) => {
|
|
362
|
-
const path = `$.preparationLineage[${index}]`;
|
|
363
|
-
if (!plain(raw))
|
|
364
|
-
throw new ComplianceError(
|
|
365
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
366
|
-
"Lineage edge must be a plain object",
|
|
367
|
-
path,
|
|
368
|
-
);
|
|
369
|
-
exactKeys(
|
|
370
|
-
raw,
|
|
371
|
-
["outputArtifactId", "inputArtifactIds", "transformationArtifactIds"],
|
|
372
|
-
path,
|
|
373
|
-
);
|
|
374
|
-
if (typeof raw.outputArtifactId !== "string")
|
|
375
|
-
throw new ComplianceError(
|
|
376
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
377
|
-
"Lineage output must be a string",
|
|
378
|
-
`${path}.outputArtifactId`,
|
|
379
|
-
);
|
|
380
|
-
token(raw.outputArtifactId, `${path}.outputArtifactId`);
|
|
381
|
-
const readIds = (value: unknown, key: string) => {
|
|
382
|
-
if (!Array.isArray(value))
|
|
383
|
-
throw new ComplianceError(
|
|
384
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
385
|
-
`Lineage ${key} must be an array`,
|
|
386
|
-
`${path}.${key}`,
|
|
387
|
-
);
|
|
388
|
-
const result = value.map((id, inputIndex) => {
|
|
389
|
-
if (typeof id !== "string")
|
|
390
|
-
throw new ComplianceError(
|
|
391
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
392
|
-
"Lineage artifact id must be a string",
|
|
393
|
-
`${path}.${key}[${inputIndex}]`,
|
|
394
|
-
);
|
|
395
|
-
token(id, `${path}.${key}[${inputIndex}]`);
|
|
396
|
-
return id;
|
|
397
|
-
});
|
|
398
|
-
if (new Set(result).size !== result.length)
|
|
399
|
-
throw new ComplianceError(
|
|
400
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
401
|
-
`Lineage ${key} must be unique`,
|
|
402
|
-
`${path}.${key}`,
|
|
403
|
-
);
|
|
404
|
-
return [...result].sort();
|
|
405
|
-
};
|
|
406
|
-
const inputs = readIds(raw.inputArtifactIds, "inputArtifactIds"),
|
|
407
|
-
transformations = readIds(
|
|
408
|
-
raw.transformationArtifactIds,
|
|
409
|
-
"transformationArtifactIds",
|
|
410
|
-
);
|
|
411
|
-
if (inputs.length + transformations.length === 0)
|
|
412
|
-
throw new ComplianceError(
|
|
413
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
414
|
-
"Lineage edge must have at least one upstream artifact",
|
|
415
|
-
path,
|
|
416
|
-
);
|
|
417
|
-
return {
|
|
418
|
-
outputArtifactId: raw.outputArtifactId,
|
|
419
|
-
inputArtifactIds: inputs,
|
|
420
|
-
transformationArtifactIds: transformations,
|
|
421
|
-
};
|
|
422
|
-
})
|
|
423
|
-
.sort((a, b) =>
|
|
424
|
-
a.outputArtifactId < b.outputArtifactId
|
|
425
|
-
? -1
|
|
426
|
-
: a.outputArtifactId > b.outputArtifactId
|
|
427
|
-
? 1
|
|
428
|
-
: 0,
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function validateArtifactGraph(
|
|
433
|
-
run: CompletedValidatedMetricDiagnosticsRun,
|
|
434
|
-
inputArtifacts: readonly Pick<
|
|
435
|
-
DiagnosticArtifactDigest,
|
|
436
|
-
"id" | "scope" | "assurance"
|
|
437
|
-
>[],
|
|
438
|
-
preparationArtifacts: readonly Pick<
|
|
439
|
-
DiagnosticArtifactDigest,
|
|
440
|
-
"id" | "scope" | "assurance"
|
|
441
|
-
>[],
|
|
442
|
-
lineage: readonly DiagnosticPreparationLineage[],
|
|
443
|
-
): void {
|
|
444
|
-
const byId = new Map<
|
|
445
|
-
string,
|
|
446
|
-
Pick<DiagnosticArtifactDigest, "id" | "scope" | "assurance">
|
|
447
|
-
>();
|
|
448
|
-
for (const [index, item] of [
|
|
449
|
-
...inputArtifacts,
|
|
450
|
-
...preparationArtifacts,
|
|
451
|
-
].entries()) {
|
|
452
|
-
if (byId.has(item.id))
|
|
453
|
-
throw new ComplianceError(
|
|
454
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
455
|
-
`Duplicate artifact ID ${item.id}`,
|
|
456
|
-
`$.${index < inputArtifacts.length ? "inputArtifacts" : "preparationArtifacts"}[${index < inputArtifacts.length ? index : index - inputArtifacts.length}].id`,
|
|
457
|
-
);
|
|
458
|
-
byId.set(item.id, item);
|
|
459
|
-
}
|
|
460
|
-
const referenced = new Set<string>();
|
|
461
|
-
const requireArtifact = (
|
|
462
|
-
id: string,
|
|
463
|
-
scope: "input" | "preparation",
|
|
464
|
-
path: string,
|
|
465
|
-
) => {
|
|
466
|
-
const artifact = byId.get(id);
|
|
467
|
-
if (!artifact)
|
|
468
|
-
throw new ComplianceError(
|
|
469
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
470
|
-
`Artifact reference ${id} is unresolved`,
|
|
471
|
-
path,
|
|
472
|
-
);
|
|
473
|
-
if (artifact.scope !== scope)
|
|
474
|
-
throw new ComplianceError(
|
|
475
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
476
|
-
`Artifact ${id} must have ${scope} scope`,
|
|
477
|
-
path,
|
|
478
|
-
);
|
|
479
|
-
referenced.add(id);
|
|
480
|
-
};
|
|
481
|
-
let unsourced = false;
|
|
482
|
-
for (const [index, item] of run.prepared.inputAudit.entries()) {
|
|
483
|
-
const source = item.record.source;
|
|
484
|
-
if (source)
|
|
485
|
-
requireArtifact(
|
|
486
|
-
source.artifactId,
|
|
487
|
-
"input",
|
|
488
|
-
`$.completedRun.prepared.inputAudit[${index}].record.source.artifactId`,
|
|
489
|
-
);
|
|
490
|
-
else unsourced = true;
|
|
491
|
-
}
|
|
492
|
-
const evidence = run.review.evidence;
|
|
493
|
-
if (evidence) {
|
|
494
|
-
for (const [index, item] of evidence.groupingAssignments.entries()) {
|
|
495
|
-
if (item.source)
|
|
496
|
-
requireArtifact(
|
|
497
|
-
item.source.artifactId,
|
|
498
|
-
"input",
|
|
499
|
-
`$.completedRun.review.evidence.groupingAssignments[${index}].source.artifactId`,
|
|
500
|
-
);
|
|
501
|
-
else unsourced = true;
|
|
502
|
-
}
|
|
503
|
-
for (const [index, item] of evidence.cachedFormulas.entries()) {
|
|
504
|
-
if (item.source)
|
|
505
|
-
requireArtifact(
|
|
506
|
-
item.source.artifactId,
|
|
507
|
-
"input",
|
|
508
|
-
`$.completedRun.review.evidence.cachedFormulas[${index}].source.artifactId`,
|
|
509
|
-
);
|
|
510
|
-
else unsourced = true;
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
if (unsourced) {
|
|
514
|
-
if (run.datasetArtifactId === null)
|
|
515
|
-
throw new ComplianceError(
|
|
516
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
517
|
-
"Unsourced diagnostic input requires datasetArtifactId",
|
|
518
|
-
"$.completedRun.datasetArtifactId",
|
|
519
|
-
);
|
|
520
|
-
const fallback = byId.get(run.datasetArtifactId);
|
|
521
|
-
if (
|
|
522
|
-
!fallback ||
|
|
523
|
-
fallback.scope !== "input" ||
|
|
524
|
-
fallback.assurance !== "sdk-computed"
|
|
525
|
-
)
|
|
526
|
-
throw new ComplianceError(
|
|
527
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
528
|
-
"datasetArtifactId must resolve to SDK-computed input evidence",
|
|
529
|
-
"$.completedRun.datasetArtifactId",
|
|
530
|
-
);
|
|
531
|
-
referenced.add(run.datasetArtifactId);
|
|
532
|
-
} else if (run.datasetArtifactId !== null) {
|
|
533
|
-
requireArtifact(
|
|
534
|
-
run.datasetArtifactId,
|
|
535
|
-
"input",
|
|
536
|
-
"$.completedRun.datasetArtifactId",
|
|
537
|
-
);
|
|
538
|
-
if (byId.get(run.datasetArtifactId)!.assurance !== "sdk-computed")
|
|
539
|
-
throw new ComplianceError(
|
|
540
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
541
|
-
"datasetArtifactId must resolve to SDK-computed input evidence",
|
|
542
|
-
"$.completedRun.datasetArtifactId",
|
|
543
|
-
);
|
|
544
|
-
}
|
|
545
|
-
for (const [
|
|
546
|
-
basisIndex,
|
|
547
|
-
basis,
|
|
548
|
-
] of run.prepared.definition.definition.amountBases.entries())
|
|
549
|
-
for (const [componentIndex, component] of basis.components.entries())
|
|
550
|
-
if (
|
|
551
|
-
component.limitation.kind !== "unlimited" &&
|
|
552
|
-
component.limitation.kind !== "unknown" &&
|
|
553
|
-
component.limitation.derivation.kind === "external"
|
|
554
|
-
)
|
|
555
|
-
requireArtifact(
|
|
556
|
-
component.limitation.derivation.transformationRef,
|
|
557
|
-
"preparation",
|
|
558
|
-
`$.definition.amountBases[${basisIndex}].components[${componentIndex}].limitation.derivation.transformationRef`,
|
|
559
|
-
);
|
|
560
|
-
for (const [
|
|
561
|
-
ruleIndex,
|
|
562
|
-
rule,
|
|
563
|
-
] of run.prepared.definition.definition.reviewRules.entries())
|
|
564
|
-
if (
|
|
565
|
-
rule.kind === "layer-order" &&
|
|
566
|
-
rule.comparability.kind === "caller-asserted"
|
|
567
|
-
)
|
|
568
|
-
requireArtifact(
|
|
569
|
-
rule.comparability.rationaleArtifactId,
|
|
570
|
-
"preparation",
|
|
571
|
-
`$.definition.reviewRules[${ruleIndex}].comparability.rationaleArtifactId`,
|
|
572
|
-
);
|
|
573
|
-
if (run.gate.rationaleRef !== null)
|
|
574
|
-
requireArtifact(
|
|
575
|
-
run.gate.rationaleRef,
|
|
576
|
-
"preparation",
|
|
577
|
-
"$.completedRun.gate.rationaleRef",
|
|
578
|
-
);
|
|
579
|
-
const edges = new Map<string, readonly string[]>();
|
|
580
|
-
for (const [index, edge] of lineage.entries()) {
|
|
581
|
-
const path = `$.preparationLineage[${index}]`;
|
|
582
|
-
if (edges.has(edge.outputArtifactId))
|
|
583
|
-
throw new ComplianceError(
|
|
584
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
585
|
-
"An artifact may have only one producing lineage edge",
|
|
586
|
-
`${path}.outputArtifactId`,
|
|
587
|
-
);
|
|
588
|
-
const downstream = byId.get(edge.outputArtifactId);
|
|
589
|
-
if (!downstream)
|
|
590
|
-
throw new ComplianceError(
|
|
591
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
592
|
-
`Lineage artifact ${edge.outputArtifactId} is unresolved`,
|
|
593
|
-
`${path}.outputArtifactId`,
|
|
594
|
-
);
|
|
595
|
-
if (downstream.scope !== "input")
|
|
596
|
-
throw new ComplianceError(
|
|
597
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
598
|
-
"Lineage output artifact must have input scope",
|
|
599
|
-
`${path}.outputArtifactId`,
|
|
600
|
-
);
|
|
601
|
-
for (const [inputIndex, id] of edge.inputArtifactIds.entries()) {
|
|
602
|
-
if (id === edge.outputArtifactId)
|
|
603
|
-
throw new ComplianceError(
|
|
604
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
605
|
-
"Lineage may not reference itself",
|
|
606
|
-
`${path}.inputArtifactIds[${inputIndex}]`,
|
|
607
|
-
);
|
|
608
|
-
const artifact = byId.get(id);
|
|
609
|
-
if (!artifact || artifact.scope !== "input")
|
|
610
|
-
throw new ComplianceError(
|
|
611
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
612
|
-
`Lineage input ${id} must resolve to input evidence`,
|
|
613
|
-
`${path}.inputArtifactIds[${inputIndex}]`,
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
for (const [
|
|
617
|
-
transformIndex,
|
|
618
|
-
id,
|
|
619
|
-
] of edge.transformationArtifactIds.entries()) {
|
|
620
|
-
const artifact = byId.get(id);
|
|
621
|
-
if (!artifact || artifact.scope !== "preparation")
|
|
622
|
-
throw new ComplianceError(
|
|
623
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
624
|
-
`Lineage transformation ${id} must resolve to preparation evidence`,
|
|
625
|
-
`${path}.transformationArtifactIds[${transformIndex}]`,
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
edges.set(edge.outputArtifactId, [
|
|
629
|
-
...edge.inputArtifactIds,
|
|
630
|
-
...edge.transformationArtifactIds,
|
|
631
|
-
]);
|
|
632
|
-
}
|
|
633
|
-
const visiting = new Set<string>(),
|
|
634
|
-
visited = new Set<string>();
|
|
635
|
-
for (const root of [...referenced]) {
|
|
636
|
-
const stack = [{ id: root, exit: false }];
|
|
637
|
-
while (stack.length > 0) {
|
|
638
|
-
const { id, exit } = stack.pop()!;
|
|
639
|
-
if (exit) {
|
|
640
|
-
visiting.delete(id);
|
|
641
|
-
visited.add(id);
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
if (visiting.has(id))
|
|
645
|
-
throw new ComplianceError(
|
|
646
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
647
|
-
"Artifact lineage contains a cycle",
|
|
648
|
-
"$.preparationLineage",
|
|
649
|
-
);
|
|
650
|
-
if (visited.has(id)) continue;
|
|
651
|
-
visiting.add(id);
|
|
652
|
-
stack.push({ id, exit: true });
|
|
653
|
-
for (const upstream of [...(edges.get(id) ?? [])].reverse()) {
|
|
654
|
-
referenced.add(upstream);
|
|
655
|
-
stack.push({ id: upstream, exit: false });
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
for (const [index, artifact] of inputArtifacts.entries())
|
|
660
|
-
if (!referenced.has(artifact.id))
|
|
661
|
-
throw new ComplianceError(
|
|
662
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
663
|
-
`Artifact ${artifact.id} is orphaned`,
|
|
664
|
-
`$.inputArtifacts[${index}].id`,
|
|
665
|
-
);
|
|
666
|
-
for (const [index, artifact] of preparationArtifacts.entries())
|
|
667
|
-
if (!referenced.has(artifact.id))
|
|
668
|
-
throw new ComplianceError(
|
|
669
|
-
"BAD_DIAGNOSTIC_RUN",
|
|
670
|
-
`Artifact ${artifact.id} is orphaned`,
|
|
671
|
-
`$.preparationArtifacts[${index}].id`,
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
309
|
function rerunAndVerify(run: CompletedValidatedMetricDiagnosticsRun): {
|
|
676
310
|
review: DiagnosticReviewReceipt;
|
|
677
311
|
result: DiagnosticDeepReadonly<NormalizedDiagnosticResultIdentity>;
|