@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.
Files changed (69) hide show
  1. package/README.md +6 -2
  2. package/dist/diagnosticArtifactStream.d.ts +30 -0
  3. package/dist/diagnosticArtifactStream.d.ts.map +1 -0
  4. package/dist/diagnosticArtifactStream.js +128 -0
  5. package/dist/diagnosticArtifactStream.js.map +1 -0
  6. package/dist/diagnosticByteSource.d.ts +10 -0
  7. package/dist/diagnosticByteSource.d.ts.map +1 -0
  8. package/dist/diagnosticByteSource.js +70 -0
  9. package/dist/diagnosticByteSource.js.map +1 -0
  10. package/dist/diagnosticByteView.d.ts +3 -0
  11. package/dist/diagnosticByteView.d.ts.map +1 -0
  12. package/dist/diagnosticByteView.js +27 -0
  13. package/dist/diagnosticByteView.js.map +1 -0
  14. package/dist/diagnosticCompactRun.d.ts +43 -0
  15. package/dist/diagnosticCompactRun.d.ts.map +1 -0
  16. package/dist/diagnosticCompactRun.js +253 -0
  17. package/dist/diagnosticCompactRun.js.map +1 -0
  18. package/dist/diagnosticReplayFrames.d.ts +10 -0
  19. package/dist/diagnosticReplayFrames.d.ts.map +1 -0
  20. package/dist/diagnosticReplayFrames.js +99 -0
  21. package/dist/diagnosticReplayFrames.js.map +1 -0
  22. package/dist/diagnosticReplayProtocol.d.ts +17 -0
  23. package/dist/diagnosticReplayProtocol.d.ts.map +1 -0
  24. package/dist/diagnosticReplayProtocol.js +98 -0
  25. package/dist/diagnosticReplayProtocol.js.map +1 -0
  26. package/dist/diagnosticReplayReader.d.ts +41 -0
  27. package/dist/diagnosticReplayReader.d.ts.map +1 -0
  28. package/dist/diagnosticReplayReader.js +346 -0
  29. package/dist/diagnosticReplayReader.js.map +1 -0
  30. package/dist/diagnosticReplayValue.d.ts +34 -0
  31. package/dist/diagnosticReplayValue.d.ts.map +1 -0
  32. package/dist/diagnosticReplayValue.js +269 -0
  33. package/dist/diagnosticReplayValue.js.map +1 -0
  34. package/dist/diagnosticReplayWriter.d.ts +25 -0
  35. package/dist/diagnosticReplayWriter.d.ts.map +1 -0
  36. package/dist/diagnosticReplayWriter.js +148 -0
  37. package/dist/diagnosticReplayWriter.js.map +1 -0
  38. package/dist/diagnosticRun.d.ts.map +1 -1
  39. package/dist/diagnosticRun.js +1 -193
  40. package/dist/diagnosticRun.js.map +1 -1
  41. package/dist/diagnosticRunEvidence.d.ts +18 -0
  42. package/dist/diagnosticRunEvidence.d.ts.map +1 -0
  43. package/dist/diagnosticRunEvidence.js +195 -0
  44. package/dist/diagnosticRunEvidence.js.map +1 -0
  45. package/dist/index.d.ts +4 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +4 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/sha256Stream.d.ts +6 -0
  50. package/dist/sha256Stream.d.ts.map +1 -0
  51. package/dist/sha256Stream.js +103 -0
  52. package/dist/sha256Stream.js.map +1 -0
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +4 -4
  56. package/src/diagnosticArtifactStream.ts +148 -0
  57. package/src/diagnosticByteSource.ts +82 -0
  58. package/src/diagnosticByteView.ts +27 -0
  59. package/src/diagnosticCompactRun.ts +462 -0
  60. package/src/diagnosticReplayFrames.ts +99 -0
  61. package/src/diagnosticReplayProtocol.ts +99 -0
  62. package/src/diagnosticReplayReader.ts +428 -0
  63. package/src/diagnosticReplayValue.ts +269 -0
  64. package/src/diagnosticReplayWriter.ts +190 -0
  65. package/src/diagnosticRun.ts +7 -373
  66. package/src/diagnosticRunEvidence.ts +391 -0
  67. package/src/index.ts +10 -0
  68. package/src/sha256Stream.ts +108 -0
  69. 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
+ }
@@ -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>;