@colophon-claims/verify 0.0.0 → 0.2.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 (96) hide show
  1. package/README.md +39 -0
  2. package/dist/admission/application.d.ts +18 -0
  3. package/dist/admission/application.js +53 -0
  4. package/dist/admission/contracts.d.ts +506 -0
  5. package/dist/admission/contracts.js +459 -0
  6. package/dist/admission/index.d.ts +5 -0
  7. package/dist/admission/index.js +5 -0
  8. package/dist/admission/intake.d.ts +75 -0
  9. package/dist/admission/intake.js +70 -0
  10. package/dist/admission/result-evaluation.d.ts +18 -0
  11. package/dist/admission/result-evaluation.js +71 -0
  12. package/dist/admission/screening-sample.d.ts +56 -0
  13. package/dist/admission/screening-sample.js +126 -0
  14. package/dist/admission/verification.d.ts +102 -0
  15. package/dist/admission/verification.js +844 -0
  16. package/dist/anchor/check.d.ts +95 -0
  17. package/dist/anchor/check.js +146 -0
  18. package/dist/anchor/ports.d.ts +6 -0
  19. package/dist/anchor/ports.js +354 -0
  20. package/dist/assets.d.ts +33 -0
  21. package/dist/assets.js +885 -0
  22. package/dist/bin.d.ts +2 -0
  23. package/dist/bin.js +20 -0
  24. package/dist/cli.d.ts +14 -0
  25. package/dist/cli.js +205 -0
  26. package/dist/comparison.d.ts +64 -0
  27. package/dist/comparison.js +235 -0
  28. package/dist/index.d.ts +23 -0
  29. package/dist/index.js +23 -0
  30. package/dist/manifest.d.ts +54 -0
  31. package/dist/manifest.js +240 -0
  32. package/dist/materialize.d.ts +4 -0
  33. package/dist/materialize.js +13 -0
  34. package/dist/profile/admission-receipts.d.ts +11 -0
  35. package/dist/profile/admission-receipts.js +24 -0
  36. package/dist/profile/anchor-claims.d.ts +157 -0
  37. package/dist/profile/anchor-claims.js +330 -0
  38. package/dist/profile/artifacts.d.ts +261 -0
  39. package/dist/profile/artifacts.js +139 -0
  40. package/dist/profile/assembly-ports.d.ts +19 -0
  41. package/dist/profile/assembly-ports.js +42 -0
  42. package/dist/profile/binary-judge-manifest.d.ts +121 -0
  43. package/dist/profile/binary-judge-manifest.js +106 -0
  44. package/dist/profile/binary-qualification.d.ts +9 -0
  45. package/dist/profile/binary-qualification.js +58 -0
  46. package/dist/profile/branding-assets.d.ts +5 -0
  47. package/dist/profile/branding-assets.js +5 -0
  48. package/dist/profile/branding.d.ts +24 -0
  49. package/dist/profile/branding.js +14 -0
  50. package/dist/profile/claim-consistency.d.ts +26 -0
  51. package/dist/profile/claim-consistency.js +91 -0
  52. package/dist/profile/claim.d.ts +327 -0
  53. package/dist/profile/claim.js +742 -0
  54. package/dist/profile/errors.d.ts +49 -0
  55. package/dist/profile/errors.js +78 -0
  56. package/dist/profile/inspect-assurance.d.ts +16 -0
  57. package/dist/profile/inspect-assurance.js +30 -0
  58. package/dist/profile/inspect-disclosure.d.ts +28 -0
  59. package/dist/profile/inspect-disclosure.js +54 -0
  60. package/dist/profile/inspect-manifest.d.ts +706 -0
  61. package/dist/profile/inspect-manifest.js +397 -0
  62. package/dist/profile/isolation.d.ts +21 -0
  63. package/dist/profile/isolation.js +31 -0
  64. package/dist/profile/pinning-evidence.d.ts +41 -0
  65. package/dist/profile/pinning-evidence.js +43 -0
  66. package/dist/profile/ports.d.ts +3 -0
  67. package/dist/profile/ports.js +11 -0
  68. package/dist/profile/preview-log.d.ts +4 -0
  69. package/dist/profile/preview-log.js +3 -0
  70. package/dist/profile/run-results.d.ts +17 -0
  71. package/dist/profile/run-results.js +23 -0
  72. package/dist/profile/signing.d.ts +10 -0
  73. package/dist/profile/signing.js +46 -0
  74. package/dist/profile/trust.d.ts +13 -0
  75. package/dist/profile/trust.js +35 -0
  76. package/dist/profile/venue.d.ts +2 -0
  77. package/dist/profile/venue.js +2 -0
  78. package/dist/profile/verdict.d.ts +28 -0
  79. package/dist/profile/verdict.js +51 -0
  80. package/dist/reader-instructions.d.ts +41 -0
  81. package/dist/reader-instructions.js +52 -0
  82. package/dist/schema.d.ts +380 -0
  83. package/dist/schema.js +498 -0
  84. package/dist/verify.d.ts +72 -0
  85. package/dist/verify.js +1573 -0
  86. package/dist/version.d.ts +2 -0
  87. package/dist/version.js +2 -0
  88. package/package.json +63 -3
  89. package/schemas/assembly-row.schema.json +194 -0
  90. package/schemas/bundle-manifest.schema.json +23 -0
  91. package/schemas/claim-package.schema.json +279 -0
  92. package/schemas/dsse-envelope.schema.json +25 -0
  93. package/schemas/evidence-catalog.schema.json +43 -0
  94. package/schemas/public-trust.schema.json +44 -0
  95. package/schemas/verdict-catalog.schema.json +27 -0
  96. package/scripts/external-verify.py +238 -0
package/dist/verify.js ADDED
@@ -0,0 +1,1573 @@
1
+ import { createHash, createPublicKey, verify as verifySignature } from "node:crypto";
2
+ import { verifyReport } from "@jinn-network/benchmarking-aggregate";
3
+ import { verifyEvidenceNativePortableBundle, } from "@jinn-network/benchmarking-evidence";
4
+ import { exportStaticBundle } from "@jinn-network/benchmarking-interop";
5
+ import { cellIdempotencyKey, expectedCellSet, parseBenchmark, parseMatrix, parseReport, parseRun, readRunAnchorIntentExtension, readRunPublicationExtension, } from "@jinn-network/benchmarking-records";
6
+ import { verifyMatrix } from "@jinn-network/benchmarking-run";
7
+ import { BINARY_JUDGMENT_INSTRUMENT_REQUIREMENT_KEY, BINARY_JUDGMENT_PROFILE_URI, BinaryJudgmentSnapshotProbeSchema, deriveEvaluationTask, parseBinaryJudgmentAnalysisContext, parseBinaryJudgmentInstrument, parseEvaluationSpec, } from "@jinn-network/task-execution-profiles";
8
+ import { DeliveryRecordSchema, SubmissionRecordSchema, TaskSpecificationSchema } from "@jinn-network/task-execution-protocol";
9
+ import { canonicalJsonBytes, dssePreAuthEncoding, parseExactDsseEnvelope } from "@jinn-network/trust-core";
10
+ import { refuse } from "./profile/errors.js";
11
+ import { ClaimPackageSchema } from "./profile/claim.js";
12
+ import { buildMethodPortsFromResolver } from "./profile/ports.js";
13
+ import { didKeyFromEd25519PublicKey } from "./profile/signing.js";
14
+ import { buildPublicReportTrustDeps } from "./profile/trust.js";
15
+ import { buildAssemblyPortsFromFacts } from "./profile/assembly-ports.js";
16
+ import { parseRunPinningEvidenceArtifact, pinningEvidenceFacts, } from "./profile/pinning-evidence.js";
17
+ import { parsePredictionSnapshotAdmissionReceipt, } from "./profile/admission-receipts.js";
18
+ import { EVALUATOR_REQUIREMENT_KEY } from "./profile/venue.js";
19
+ import { INSPECT_SELECTION_CORRELATION_ROLE, InspectBinaryJudgeSelectionManifestSchema, } from "./profile/binary-judge-manifest.js";
20
+ import { readOrderedVerdictMeasurements, readVerdictEnvelope, verdictKeyIdFromEd25519PublicKey, } from "./profile/verdict.js";
21
+ import { INSPECT_EMBEDDED_EVALUATOR_ID, InspectCellSummarySchema, INSPECT_TASK_PROFILE_URI, projectInspectCellVerdict, } from "./profile/artifacts.js";
22
+ import { InspectSelectionManifestSchema, isInspectMultiScorerSelection, } from "./profile/inspect-manifest.js";
23
+ import { describeInspectRuntimeMethod, } from "./profile/inspect-disclosure.js";
24
+ import { deriveInspectEvaluationStrategy, INSPECT_SEPARATE_ASSURANCE_LIMITATIONS, inspectLogVerifierMethod, } from "./profile/inspect-assurance.js";
25
+ import { assertClaimConsistency } from "./profile/claim-consistency.js";
26
+ import { buildPublicAssets } from "./assets.js";
27
+ import { derivePublicComparison } from "./comparison.js";
28
+ import { verifyBundleSnapshot, } from "./manifest.js";
29
+ import { PUBLIC_BUNDLE_FILES, PUBLIC_BUNDLE_V4_FILES } from "./materialize.js";
30
+ import { BUNDLE_V4_FORMAT, BUNDLE_V5_FORMAT, BUNDLE_V6_FORMAT } from "./manifest.js";
31
+ import { evaluateIntegrityAnchors, } from "./anchor/check.js";
32
+ import { ClaimAnchorProjectionError, deriveClaimAnchors } from "./profile/anchor-claims.js";
33
+ import { verifyBinaryJudgmentAdmissionClosure, } from "./admission/verification.js";
34
+ import { readAdmissionVerdictEnvelope } from "./admission/result-evaluation.js";
35
+ import { BINARY_ITEM_BANK_INTAKE_EXTENSION, BinaryAdmissionIndexEntrySchema, BinaryItemBankEntrySchema, BinaryItemBankIntakeExtensionSchema, BinarySourceManifestEntrySchema, } from "./admission/intake.js";
36
+ import { BundleAssemblyCellSchema, BundleAssemblyHeaderSchema, BundleCancelMarkerSchema, BundleEvidenceCatalogSchema, BundleQualificationSchema, BundleTrustSchema, BundleV4EvidenceCatalogSchema, BundleV4TrustSchema, BundleVerdictCatalogSchema, } from "./schema.js";
37
+ function sha256(bytes) {
38
+ return createHash("sha256").update(bytes).digest("hex");
39
+ }
40
+ function equalBytes(left, right) {
41
+ return left.length === right.length && left.every((byte, index) => byte === right[index]);
42
+ }
43
+ function parseJson(bytes, schema, path) {
44
+ let raw;
45
+ try {
46
+ raw = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
47
+ }
48
+ catch {
49
+ refuse("record-integrity", path, `${path} is not valid UTF-8 JSON`);
50
+ }
51
+ const parsed = schema.safeParse(raw);
52
+ if (!parsed.success)
53
+ refuse("record-integrity", path, `${path} does not satisfy its public bundle schema`);
54
+ return parsed.data;
55
+ }
56
+ function parseRecord(bytes, schema, path) {
57
+ return parseJson(bytes, schema, path);
58
+ }
59
+ function requireCanonical(bytes, value, path) {
60
+ if (!equalBytes(bytes, canonicalJsonBytes(value)))
61
+ refuse("record-integrity", path, `${path} is not the exact canonical encoding`);
62
+ }
63
+ function analysisContextDigestFromEvalSpec(spec) {
64
+ const block = spec.familyBlock;
65
+ if (typeof block !== "object" || block === null || Array.isArray(block))
66
+ return undefined;
67
+ const testMaterial = block.testMaterial;
68
+ if (!Array.isArray(testMaterial) || testMaterial.length !== 1)
69
+ return undefined;
70
+ const entry = testMaterial[0];
71
+ if (typeof entry !== "object" || entry === null)
72
+ return undefined;
73
+ const record = entry;
74
+ if (record.name !== "analysis-context.json" || typeof record.digest?.sha256 !== "string")
75
+ return undefined;
76
+ if (!/^[a-f0-9]{64}$/u.test(record.digest.sha256))
77
+ return undefined;
78
+ return record.digest.sha256;
79
+ }
80
+ function unique(values, path) {
81
+ if (new Set(values).size !== values.length)
82
+ refuse("record-integrity", path, `${path} contains duplicate identities`);
83
+ }
84
+ function publicKey(spkiDerBase64, path) {
85
+ let key;
86
+ try {
87
+ key = createPublicKey({ key: Buffer.from(spkiDerBase64, "base64"), format: "der", type: "spki" });
88
+ }
89
+ catch {
90
+ refuse("record-integrity", path, `${path} is not a valid SPKI public key`);
91
+ }
92
+ if (key.asymmetricKeyType !== "ed25519")
93
+ refuse("record-integrity", path, `${path} is not an Ed25519 public key`);
94
+ return key;
95
+ }
96
+ function parseAssembly(bytes) {
97
+ let text;
98
+ try {
99
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
100
+ }
101
+ catch {
102
+ refuse("record-integrity", "verification/assembly.jsonl", "assembly JSONL is not valid UTF-8");
103
+ }
104
+ if (!text.endsWith("\n"))
105
+ refuse("record-integrity", "verification/assembly.jsonl", "assembly JSONL must end with one newline");
106
+ const lines = text.slice(0, -1).split("\n");
107
+ if (lines.length < 1 || lines.some((line) => line.length === 0)) {
108
+ refuse("record-integrity", "verification/assembly.jsonl", "assembly JSONL contains an empty line");
109
+ }
110
+ const headerBytes = new TextEncoder().encode(lines[0]);
111
+ const header = parseJson(headerBytes, BundleAssemblyHeaderSchema, "verification/assembly.jsonl#1");
112
+ requireCanonical(headerBytes, header, "verification/assembly.jsonl#1");
113
+ const cells = lines.slice(1).map((line, index) => {
114
+ const lineBytes = new TextEncoder().encode(line);
115
+ const cell = parseJson(lineBytes, BundleAssemblyCellSchema, `verification/assembly.jsonl#${index + 2}`);
116
+ requireCanonical(lineBytes, cell, `verification/assembly.jsonl#${index + 2}`);
117
+ return cell;
118
+ });
119
+ unique(cells.map((cell) => cell.cellKey), "verification/assembly.jsonl.cellKey");
120
+ return { header, cells };
121
+ }
122
+ function addRole(expected, digest, role) {
123
+ const roles = expected.get(digest) ?? new Set();
124
+ roles.add(role);
125
+ expected.set(digest, roles);
126
+ }
127
+ function sorted(values) {
128
+ return [...values].sort();
129
+ }
130
+ function exactCanonicalJsonl(bytes, path, schema) {
131
+ let text;
132
+ try {
133
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
134
+ }
135
+ catch {
136
+ refuse("record-integrity", path, `${path} is not UTF-8 canonical JSONL`);
137
+ }
138
+ if (!text.endsWith("\n") || text.includes("\r"))
139
+ refuse("record-integrity", path, `${path} must end in one LF and contain no CR`);
140
+ const lines = text.slice(0, -1).split("\n");
141
+ if (lines.length === 0 || lines.some((line) => line.length === 0))
142
+ refuse("record-integrity", path, `${path} contains an empty JSONL row`);
143
+ return lines.map((line, index) => {
144
+ let value;
145
+ try {
146
+ value = JSON.parse(line);
147
+ }
148
+ catch {
149
+ refuse("record-integrity", `${path}#${index + 1}`, "JSONL row is not JSON");
150
+ }
151
+ const parsed = schema.safeParse(value);
152
+ if (!parsed.success)
153
+ refuse("record-integrity", `${path}#${index + 1}`, "JSONL row is outside its strict registered schema");
154
+ if (!equalBytes(new TextEncoder().encode(line), canonicalJsonBytes(parsed.data))) {
155
+ refuse("record-integrity", `${path}#${index + 1}`, "JSONL row is not exact canonical JSON");
156
+ }
157
+ return parsed.data;
158
+ });
159
+ }
160
+ function sameCanonical(left, right) {
161
+ return equalBytes(canonicalJsonBytes(left), canonicalJsonBytes(right));
162
+ }
163
+ function verifyEvidenceNativeSignature(input) {
164
+ try {
165
+ const key = createPublicKey({ key: Buffer.from(input.publicKeyBytes), format: "der", type: "spki" });
166
+ return key.asymmetricKeyType === "ed25519" && verifySignature(null, Buffer.from(input.preAuthEncoding), key, Buffer.from(input.signature));
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
172
+ /**
173
+ * The cold verifier's item-bank/source-manifest closure, over already-parsed canonical rows.
174
+ *
175
+ * Exported so the closure can be exercised directly. It has to be: a bundle whose cluster key is
176
+ * decoupled from its declared sources cannot be produced by this repository's own import path,
177
+ * because the importer's code-unit-least rule is strictly stronger than the membership rule below.
178
+ * This check exists for bundles produced by other implementations, so its test has to construct
179
+ * the rows rather than round-trip them through the importer.
180
+ */
181
+ export function checkItemBankSourceClosure(itemRows, sourceRows) {
182
+ const itemDigests = new Set();
183
+ const coveredSourceDigests = new Set();
184
+ for (const [index, row] of itemRows.entries()) {
185
+ const item = row.item;
186
+ const digest = `sha256:${sha256(canonicalJsonBytes(item))}`;
187
+ if (itemDigests.has(digest))
188
+ refuse("record-integrity", "item-bank.jsonl", "item bank contains duplicate payloads");
189
+ itemDigests.add(digest);
190
+ const itemSourceDigests = new Set();
191
+ for (const descriptor of item.sources) {
192
+ const digestHex = descriptor.digest.sha256;
193
+ itemSourceDigests.add(`sha256:${digestHex}`);
194
+ coveredSourceDigests.add(`sha256:${digestHex}`);
195
+ }
196
+ // Membership only. The cluster key must name one of this item's own declared sources, or a
197
+ // bundle could ship provenance decoupled from the sources it claims to draw on. Deliberately
198
+ // NOT the code-unit-least rule and NOT the timestamp equality: both are enforced once, at
199
+ // import. Membership is a different property, so this is not a second enforcement point.
200
+ if (!itemSourceDigests.has(item.provenance.sourceCommitment)) {
201
+ refuse("record-integrity", `item-bank.jsonl.${index + 1}.item.provenance.sourceCommitment`, "item provenance sourceCommitment is not one of the item's declared sources");
202
+ }
203
+ }
204
+ // The covered set is derived from `sources` alone. Folding the cluster key in here would let a
205
+ // source row count as used when only a cluster key names it, weakening this exact-equality
206
+ // refusal against an unused source row.
207
+ const sourceDigests = sourceRows.map((row) => row.provenanceSha256);
208
+ if (!sameCanonical([...coveredSourceDigests].sort(), [...sourceDigests].sort())) {
209
+ refuse("record-integrity", "source-manifest.jsonl", "source manifest does not exactly cover item-bank provenance");
210
+ }
211
+ return { itemDigests };
212
+ }
213
+ /** Verifies a copied public bundle using one authenticated byte snapshot and only bundle-carried
214
+ * public keys. No pathname is reopened after manifest authentication. */
215
+ export async function verifyPublicBundleSnapshot(bundleDir, deps = {}) {
216
+ const checked = verifyBundleSnapshot(bundleDir, deps);
217
+ if (checked.manifest.format === BUNDLE_V5_FORMAT) {
218
+ try {
219
+ return {
220
+ verification: await verifyEvidenceNativePortableBundle({
221
+ files: checked.fileBytes,
222
+ verifySignature: verifyEvidenceNativeSignature,
223
+ }),
224
+ snapshot: checked,
225
+ };
226
+ }
227
+ catch (cause) {
228
+ refuse("record-integrity", "evidence-native-bundle", cause instanceof Error ? cause.message : "evidence-native bundle verification failed");
229
+ }
230
+ }
231
+ const read = (path) => {
232
+ const bytes = checked.fileBytes.get(path);
233
+ if (bytes === undefined)
234
+ refuse("record-integrity", path, `authenticated bundle snapshot is missing "${path}"`);
235
+ return bytes;
236
+ };
237
+ const checks = ["manifest"];
238
+ const manifestPaths = new Set(checked.manifest.files.map((file) => file.path));
239
+ const isV4 = checked.manifest.format === BUNDLE_V4_FORMAT;
240
+ // The anchored closure is v2's graph plus `anchors/`, so it takes v2's mandatory member list; the
241
+ // binary qualification projection has its own later anchored allocation and is not this one.
242
+ const isV6 = checked.manifest.format === BUNDLE_V6_FORMAT;
243
+ const mandatoryFiles = isV4 ? PUBLIC_BUNDLE_V4_FILES : PUBLIC_BUNDLE_FILES;
244
+ for (const path of mandatoryFiles) {
245
+ if (!manifestPaths.has(path))
246
+ refuse("record-integrity", path, `mandatory public bundle file "${path}" is missing`);
247
+ }
248
+ const evidenceBytes = read("evidence.json");
249
+ const evidence = isV4
250
+ ? parseJson(evidenceBytes, BundleV4EvidenceCatalogSchema, "evidence.json")
251
+ : parseJson(evidenceBytes, BundleEvidenceCatalogSchema, "evidence.json");
252
+ requireCanonical(evidenceBytes, evidence, "evidence.json");
253
+ unique(evidence.records.map((record) => record.sha256), "evidence.json.records");
254
+ for (const record of evidence.records)
255
+ unique(record.roles, `evidence.json.records.${record.sha256}.roles`);
256
+ const expectedPaths = new Set([
257
+ ...mandatoryFiles,
258
+ ...evidence.records.map((record) => `records/${record.sha256}.bin`),
259
+ ]);
260
+ if (manifestPaths.has("verification/cancel-requested.json"))
261
+ expectedPaths.add("verification/cancel-requested.json");
262
+ for (const path of manifestPaths) {
263
+ if (/^native\/inspect\/[a-f0-9]{64}\.eval$/u.test(path))
264
+ expectedPaths.add(path);
265
+ }
266
+ // `anchors/<sha256>.bin` is allowlisted only by the closure version that defines it: an anchor
267
+ // member in a v2 or v4 bundle is a non-allowlisted file, exactly as it was before this format.
268
+ const anchorPaths = [];
269
+ if (isV6) {
270
+ for (const path of manifestPaths) {
271
+ if (/^anchors\/[a-f0-9]{64}\.bin$/u.test(path)) {
272
+ expectedPaths.add(path);
273
+ anchorPaths.push(path);
274
+ }
275
+ }
276
+ }
277
+ for (const path of manifestPaths)
278
+ if (!expectedPaths.has(path))
279
+ refuse("record-integrity", path, `public bundle contains non-allowlisted file "${path}"`);
280
+ for (const path of expectedPaths)
281
+ if (!manifestPaths.has(path))
282
+ refuse("record-integrity", path, `public bundle closure is missing "${path}"`);
283
+ const records = new Map();
284
+ const declaredRoles = new Map();
285
+ for (const record of evidence.records) {
286
+ const bytes = read(`records/${record.sha256}.bin`);
287
+ if (sha256(bytes) !== record.sha256)
288
+ refuse("record-integrity", `records/${record.sha256}.bin`, "evidence record digest mismatch");
289
+ records.set(record.sha256, bytes);
290
+ declaredRoles.set(record.sha256, new Set(record.roles));
291
+ }
292
+ const trustBytes = read("trust/public-keys.json");
293
+ const trust = isV4
294
+ ? parseJson(trustBytes, BundleV4TrustSchema, "trust/public-keys.json")
295
+ : parseJson(trustBytes, BundleTrustSchema, "trust/public-keys.json");
296
+ requireCanonical(trustBytes, trust, "trust/public-keys.json");
297
+ const reportKey = publicKey(trust.report.spkiDerBase64, "trust.report.spkiDerBase64");
298
+ const carriedEvaluatorKeys = new Map(trust.evaluators.map((entry) => [
299
+ entry.evaluator,
300
+ { keyId: entry.keyId, publicKey: publicKey(entry.spkiDerBase64, `trust.evaluators.${entry.evaluator}`) },
301
+ ]));
302
+ let qualification;
303
+ if (isV4) {
304
+ const qualificationBytes = read("qualification.json");
305
+ qualification = parseJson(qualificationBytes, BundleQualificationSchema, "qualification.json");
306
+ requireCanonical(qualificationBytes, qualification, "qualification.json");
307
+ }
308
+ const benchmarkBytes = read("benchmark.json");
309
+ const runBytes = read("run.json");
310
+ const matrixBytes = read("matrix.json");
311
+ const reportBytes = read("report.json");
312
+ const envelopeBytes = read("report-envelope.json");
313
+ const claimBytes = read("claim-package.json");
314
+ const claim = parseJson(claimBytes, ClaimPackageSchema, "claim-package.json");
315
+ requireCanonical(claimBytes, claim, "claim-package.json");
316
+ let benchmark;
317
+ let run;
318
+ let matrix;
319
+ let report;
320
+ try {
321
+ benchmark = parseBenchmark(benchmarkBytes);
322
+ run = parseRun(runBytes);
323
+ matrix = parseMatrix(matrixBytes);
324
+ report = parseReport(reportBytes);
325
+ }
326
+ catch (cause) {
327
+ refuse("record-integrity", "evidence-closure", `primary benchmark record is invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
328
+ }
329
+ const identities = {
330
+ benchmarkSha256: sha256(benchmarkBytes),
331
+ runSha256: sha256(runBytes),
332
+ matrixSha256: sha256(matrixBytes),
333
+ reportSha256: sha256(reportBytes),
334
+ reportEnvelopeSha256: sha256(envelopeBytes),
335
+ };
336
+ const staticBytes = read("static-bundle.json");
337
+ if (!equalBytes(staticBytes, canonicalJsonBytes(exportStaticBundle(matrix, [report])))) {
338
+ refuse("record-integrity", "static-bundle.json", "static bundle is not the exact platform metadata projection");
339
+ }
340
+ // ── integrity-anchors (anchor-evidence design §8) ──────────────────────────────────────────
341
+ //
342
+ // Evaluated here, before the claim is rebuilt, because an `invalid` anchor is affirmative
343
+ // evidence of substitution and must be the refusal a reader sees — not a downstream claim
344
+ // mismatch caused by it. Once nothing is invalid, the same bytes project the claim's anchors
345
+ // section through the same function the producer used.
346
+ let anchorReport;
347
+ // Supplied to the claim rebuild only for the anchored closure, and then even when empty: an
348
+ // empty section and an omitted one are different claims, and §7.3's declared-but-absent bundle
349
+ // carries the first.
350
+ let claimAnchors;
351
+ if (isV6) {
352
+ const anchorRecords = anchorPaths
353
+ .map((path) => {
354
+ const bytes = read(path);
355
+ const recordSha256 = path.slice("anchors/".length, -".bin".length);
356
+ if (sha256(bytes) !== recordSha256) {
357
+ refuse("record-integrity", path, "anchor record digest mismatch");
358
+ }
359
+ return { recordSha256, bytes };
360
+ });
361
+ anchorReport = evaluateIntegrityAnchors({
362
+ records: anchorRecords,
363
+ runSha256: identities.runSha256,
364
+ matrixSha256: identities.matrixSha256,
365
+ closeAt: run.closeAt,
366
+ declaredProfiles: readRunAnchorIntentExtension(run)?.providers ?? [],
367
+ ...(deps.anchorTrust === undefined ? {} : { trust: deps.anchorTrust }),
368
+ });
369
+ const firstInvalid = anchorReport.invalid[0];
370
+ if (firstInvalid !== undefined) {
371
+ refuse("record-integrity", `anchors/${firstInvalid.recordSha256}.bin`, `carried anchor is invalid: ${firstInvalid.reason ?? "the proof does not verify"}`);
372
+ }
373
+ try {
374
+ claimAnchors = deriveClaimAnchors({
375
+ records: anchorRecords,
376
+ runSha256: identities.runSha256,
377
+ matrixSha256: identities.matrixSha256,
378
+ });
379
+ }
380
+ catch (cause) {
381
+ if (cause instanceof ClaimAnchorProjectionError) {
382
+ refuse("record-integrity", `anchors/${cause.recordSha256}.bin`, cause.message);
383
+ }
384
+ throw cause;
385
+ }
386
+ }
387
+ const assembly = parseAssembly(read("verification/assembly.jsonl"));
388
+ const expectedNativePaths = new Set(assembly.header.graph.solveDeliveries.flatMap((delivery) => delivery.outputs
389
+ .filter((output) => output.name === "inspect-log")
390
+ .map((output) => `native/inspect/${output.sha256}.eval`)));
391
+ const actualNativePaths = new Set([...manifestPaths].filter((path) => path.startsWith("native/inspect/")));
392
+ if (!equalBytes(canonicalJsonBytes(sorted(expectedNativePaths)), canonicalJsonBytes(sorted(actualNativePaths)))) {
393
+ refuse("record-integrity", "native/inspect", "native Inspect log paths do not exactly match the run's delivered artifacts");
394
+ }
395
+ for (const path of expectedNativePaths) {
396
+ const digest = path.slice("native/inspect/".length, -".eval".length);
397
+ const recordBytes = records.get(digest);
398
+ if (recordBytes === undefined || !equalBytes(read(path), recordBytes)) {
399
+ refuse("record-integrity", path, "viewer-ready Inspect log differs from its content-addressed evidence record");
400
+ }
401
+ }
402
+ if (manifestPaths.has("verification/cancel-requested.json")) {
403
+ const cancelBytes = read("verification/cancel-requested.json");
404
+ const cancelMarker = parseJson(cancelBytes, BundleCancelMarkerSchema, "verification/cancel-requested.json");
405
+ requireCanonical(cancelBytes, cancelMarker, "verification/cancel-requested.json");
406
+ if (!assembly.header.runCancelled)
407
+ refuse("record-integrity", "verification/assembly.jsonl", "cancel marker and assembly cancellation fact disagree");
408
+ }
409
+ else if (assembly.header.runCancelled) {
410
+ refuse("record-integrity", "verification/cancel-requested.json", "assembly declares cancellation without its public marker");
411
+ }
412
+ const expectedCoordinates = expectedCellSet(benchmark, run);
413
+ if (assembly.cells.length !== expectedCoordinates.length)
414
+ refuse("record-integrity", "evidence-closure", "assembly does not cover every expected cell");
415
+ const cellsByKey = new Map(assembly.cells.map((cell) => [cell.cellKey, cell]));
416
+ const coordinatesByKey = new Map(expectedCoordinates.map((coord) => [coord.cellKey, coord]));
417
+ const expectedRoles = new Map();
418
+ const evaluationSpecs = new Map();
419
+ const taskSpecs = new Map();
420
+ const inspectSelections = new Map();
421
+ const minVerdicts = run.policy.evaluation?.minVerdicts ?? 1;
422
+ let verifiedAdmission;
423
+ let binaryAssetQualification;
424
+ if (qualification !== undefined) {
425
+ const v4Trust = BundleV4TrustSchema.parse(trust);
426
+ const ports = {
427
+ resolveExactRecord: (digest) => {
428
+ const bytes = records.get(digest.slice("sha256:".length));
429
+ if (bytes === undefined)
430
+ throw new Error(`bundle record ${digest} is missing`);
431
+ return bytes;
432
+ },
433
+ verifyReviewerSignature: (input) => {
434
+ const key = carriedEvaluatorKeys.get(input.evaluatorId);
435
+ if (key === undefined || key.keyId !== input.keyId)
436
+ return false;
437
+ try {
438
+ const envelope = parseExactDsseEnvelope(input.envelopeBytes);
439
+ const signature = envelope.signatures[0];
440
+ return envelope.signatures.length === 1 && signature?.keyid === input.keyId
441
+ && verifySignature(null, Buffer.from(dssePreAuthEncoding(envelope.payloadType, envelope.payloadBytes)), key.publicKey, Buffer.from(signature.sig, "base64"));
442
+ }
443
+ catch {
444
+ return false;
445
+ }
446
+ },
447
+ verifyAuthoritySignature: (input) => {
448
+ const binding = v4Trust.admission.authorities.find((candidate) => candidate.role === input.role);
449
+ if (binding?.keyId !== input.keyId || input.keyId !== v4Trust.report.keyId)
450
+ return false;
451
+ try {
452
+ const envelope = parseExactDsseEnvelope(input.envelopeBytes);
453
+ const signature = envelope.signatures[0];
454
+ return envelope.signatures.length === 1 && signature?.keyid === input.keyId
455
+ && verifySignature(null, Buffer.from(dssePreAuthEncoding(envelope.payloadType, envelope.payloadBytes)), reportKey, Buffer.from(signature.sig, "base64"));
456
+ }
457
+ catch {
458
+ return false;
459
+ }
460
+ },
461
+ };
462
+ try {
463
+ verifiedAdmission = verifyBinaryJudgmentAdmissionClosure({
464
+ admissionManifestSha256: qualification.admissionManifestSha256,
465
+ expectedDraftId: assembly.header.draftId,
466
+ }, ports);
467
+ }
468
+ catch (cause) {
469
+ refuse("record-integrity", "evidence-closure", `binary admission replay failed: ${cause instanceof Error ? cause.message : String(cause)}`);
470
+ }
471
+ if (!sameCanonical(verifiedAdmission.reachableSha256s, qualification.reachableSha256s)
472
+ || !sameCanonical(verifiedAdmission.reachableRecords, qualification.admissionRecords)
473
+ || verifiedAdmission.publicationGrade !== qualification.publicationGrade
474
+ || verifiedAdmission.manifest.truthAdmission !== qualification.truthAdmission
475
+ || !sameCanonical(verifiedAdmission.classes, qualification.candidateClasses)
476
+ || !sameCanonical(verifiedAdmission.strata, qualification.strata))
477
+ refuse("record-integrity", "qualification.json", "qualification admission projection differs from portable replay");
478
+ for (const record of verifiedAdmission.reachableRecords) {
479
+ for (const role of record.roles)
480
+ addRole(expectedRoles, record.sha256.slice("sha256:".length), role);
481
+ }
482
+ const intakeParsed = BinaryItemBankIntakeExtensionSchema.safeParse(benchmark[BINARY_ITEM_BANK_INTAKE_EXTENSION]);
483
+ if (!intakeParsed.success)
484
+ refuse("record-integrity", "evidence-closure", "Benchmark binary intake extension is outside its strict schema");
485
+ const intake = intakeParsed.data;
486
+ const itemBankSha256 = intake.itemBankSha256;
487
+ const sourceManifestSha256 = intake.sourceManifestSha256;
488
+ const admissionIndexSha256 = intake.admissionIndexSha256;
489
+ if (typeof itemBankSha256 !== "string" || typeof sourceManifestSha256 !== "string"
490
+ || typeof admissionIndexSha256 !== "string"
491
+ || sourceManifestSha256 !== qualification.sourceManifestSha256
492
+ || intake.admissionManifestSha256 !== qualification.admissionManifestSha256
493
+ || intake.replacementLedgerSha256 !== verifiedAdmission.manifest.replacementLedgerSha256)
494
+ refuse("record-integrity", "evidence-closure", "Benchmark binary intake roots differ from qualification/admission replay");
495
+ const roots = [
496
+ [itemBankSha256, "item-bank"],
497
+ [sourceManifestSha256, "source-manifest"],
498
+ [admissionIndexSha256, "admission-index"],
499
+ ];
500
+ for (const [digest, role] of roots) {
501
+ if (!/^sha256:[a-f0-9]{64}$/u.test(digest) || !records.has(digest.slice("sha256:".length))) {
502
+ refuse("record-integrity", "evidence-closure", `${role} root ${digest} is missing`);
503
+ }
504
+ addRole(expectedRoles, digest.slice("sha256:".length), role);
505
+ }
506
+ const itemRows = exactCanonicalJsonl(records.get(itemBankSha256.slice("sha256:".length)), "item-bank.jsonl", BinaryItemBankEntrySchema);
507
+ const sourceRows = exactCanonicalJsonl(records.get(sourceManifestSha256.slice("sha256:".length)), "source-manifest.jsonl", BinarySourceManifestEntrySchema);
508
+ const admissionRows = exactCanonicalJsonl(records.get(admissionIndexSha256.slice("sha256:".length)), "admission-index.jsonl", BinaryAdmissionIndexEntrySchema);
509
+ const strictOrder = (values, path) => {
510
+ for (let index = 1; index < values.length; index += 1) {
511
+ if (values[index - 1] >= values[index])
512
+ refuse("record-integrity", path, `${path} rows are not sorted and unique`);
513
+ }
514
+ };
515
+ strictOrder(itemRows.map((row) => row.item.itemId), "item-bank.jsonl");
516
+ strictOrder(sourceRows.map((row) => row.provenanceSha256), "source-manifest.jsonl");
517
+ strictOrder(admissionRows.map((row) => row.itemSha256), "admission-index.jsonl");
518
+ const admittedByItem = new Map(verifiedAdmission.accepted.map((entry) => [entry.itemSha256, entry]));
519
+ const { itemDigests } = checkItemBankSourceClosure(itemRows, sourceRows);
520
+ const expectedAdmissions = verifiedAdmission.accepted.map((entry) => ({
521
+ admissionManifestSha256: verifiedAdmission.manifestSha256,
522
+ itemSha256: entry.itemSha256,
523
+ labelResolutionSha256: entry.labelResolutionSha256,
524
+ analysisContextSha256: entry.analysisContextSha256,
525
+ })).sort((left, right) => left.itemSha256 < right.itemSha256 ? -1 : left.itemSha256 > right.itemSha256 ? 1 : 0);
526
+ const actualAdmissions = admissionRows.map((row) => ({
527
+ admissionManifestSha256: row.admissionManifestSha256,
528
+ itemSha256: row.itemSha256,
529
+ labelResolutionSha256: row.labelResolutionSha256,
530
+ analysisContextSha256: row.analysisContextSha256,
531
+ }));
532
+ if (!sameCanonical(actualAdmissions, expectedAdmissions))
533
+ refuse("record-integrity", "admission-index.jsonl", "admission index differs from authenticated accepted closure");
534
+ const expectedTasks = new Set(benchmark.items.map((item) => item.task.digest.sha256));
535
+ if (qualification.items.length !== expectedTasks.size || qualification.items.length !== verifiedAdmission.accepted.length) {
536
+ refuse("record-integrity", "qualification.items", "qualification Task coverage cardinality differs from Benchmark/admission");
537
+ }
538
+ for (const entry of qualification.items) {
539
+ const taskHex = entry.taskSha256.slice("sha256:".length);
540
+ const taskBytes = records.get(taskHex);
541
+ if (!expectedTasks.has(taskHex) || taskBytes === undefined)
542
+ refuse("record-integrity", "qualification.items", `Task ${entry.taskSha256} is outside Benchmark evidence`);
543
+ const task = parseRecord(taskBytes, TaskSpecificationSchema, `records/${taskHex}.bin`);
544
+ if (task["network.jinn.binary-judgment.item-sha256"] !== entry.itemSha256) {
545
+ refuse("record-integrity", "qualification.items", `Task ${entry.taskSha256} item commitment differs`);
546
+ }
547
+ const admitted = admittedByItem.get(entry.itemSha256);
548
+ if (admitted === undefined || admitted.labelResolutionSha256 !== entry.labelResolutionSha256
549
+ || admitted.analysisContextSha256 !== entry.analysisContextSha256 || !itemDigests.has(entry.itemSha256)) {
550
+ refuse("record-integrity", "qualification.items", `Task ${entry.taskSha256} admission joins differ`);
551
+ }
552
+ }
553
+ const runArms = new Map(run.arms.map((arm) => [arm.armId, arm]));
554
+ if (!sameCanonical([...runArms.keys()].sort(), qualification.arms.map((entry) => entry.armId))) {
555
+ refuse("record-integrity", "qualification.arms", "qualification does not exactly cover the Run arm set");
556
+ }
557
+ const instruments = [];
558
+ for (const entry of qualification.arms) {
559
+ const arm = runArms.get(entry.armId);
560
+ if (arm?.pinning[BINARY_JUDGMENT_INSTRUMENT_REQUIREMENT_KEY] !== entry.instrumentSha256) {
561
+ refuse("record-integrity", "qualification.arms", `Run arm ${entry.armId} instrument pin differs`);
562
+ }
563
+ const instrumentHex = entry.instrumentSha256.slice("sha256:".length);
564
+ const instrumentBytes = records.get(instrumentHex);
565
+ if (instrumentBytes === undefined)
566
+ refuse("record-integrity", "qualification.arms", `instrument ${entry.instrumentSha256} is missing`);
567
+ let instrument;
568
+ try {
569
+ instrument = parseBinaryJudgmentInstrument(instrumentBytes);
570
+ }
571
+ catch (cause) {
572
+ refuse("record-integrity", "qualification.arms", `instrument ${entry.instrumentSha256} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
573
+ }
574
+ if (!equalBytes(instrumentBytes, canonicalJsonBytes(instrument)))
575
+ refuse("record-integrity", "qualification.arms", "instrument is not exact canonical profile bytes");
576
+ addRole(expectedRoles, instrumentHex, "judge-instrument");
577
+ instruments.push({
578
+ armId: entry.armId,
579
+ instrumentSha256: entry.instrumentSha256,
580
+ promptTemplateSha256: instrument.promptTemplateSha256,
581
+ model: instrument.model.requested,
582
+ generation: instrument.model.generation,
583
+ });
584
+ }
585
+ if (verifiedAdmission.screening !== undefined
586
+ && instruments.some((entry) => entry.instrumentSha256 === verifiedAdmission.screening.instrumentSha256)) {
587
+ refuse("record-integrity", "qualification.arms", "the screening instrument is reused as a run judge arm");
588
+ }
589
+ const registeredSelections = readRunPublicationExtension(run)
590
+ ?.registrationArtifacts.filter((artifact) => artifact.role === INSPECT_SELECTION_CORRELATION_ROLE) ?? [];
591
+ if (registeredSelections.length !== 1
592
+ || registeredSelections[0].artifact.mediaType !== "application/json") {
593
+ refuse("record-integrity", "evidence-closure", "binary Run must register exactly one JSON Inspect runtime selection");
594
+ }
595
+ const registeredSelectionSha256 = registeredSelections[0].artifact.digest.sha256;
596
+ const runtimeSelectionRecords = evidence.records.filter((record) => record.roles.includes("runtime-selection"));
597
+ if (runtimeSelectionRecords.length !== 1) {
598
+ refuse("record-integrity", "evidence-closure", "binary qualification requires exactly one runtime-selection record");
599
+ }
600
+ const binarySelectionRecord = runtimeSelectionRecords[0];
601
+ if (binarySelectionRecord.sha256 !== registeredSelectionSha256) {
602
+ refuse("record-integrity", "evidence-closure", "runtime-selection evidence differs from the selection frozen in Run registration");
603
+ }
604
+ const binarySelectionBytes = records.get(binarySelectionRecord.sha256);
605
+ const binarySelection = parseRecord(binarySelectionBytes, InspectBinaryJudgeSelectionManifestSchema, `records/${binarySelectionRecord.sha256}.bin`);
606
+ requireCanonical(binarySelectionBytes, binarySelection, `records/${binarySelectionRecord.sha256}.bin`);
607
+ const expectedSelectionArms = instruments.map((entry) => ({
608
+ armId: entry.armId,
609
+ instrumentSha256: entry.instrumentSha256,
610
+ model: entry.model,
611
+ generation: entry.generation,
612
+ }));
613
+ const actualSelectionArms = binarySelection.arms;
614
+ if (!sameCanonical(actualSelectionArms, expectedSelectionArms)) {
615
+ refuse("record-integrity", "evidence-closure", "binary runtime selection arms differ from qualification and Run pins");
616
+ }
617
+ addRole(expectedRoles, binarySelectionRecord.sha256, "runtime-selection");
618
+ if (binarySelection.snapshotProbeSha256 !== undefined) {
619
+ const probeHex = binarySelection.snapshotProbeSha256.slice("sha256:".length);
620
+ const probeBytes = records.get(probeHex);
621
+ if (probeBytes === undefined)
622
+ refuse("record-integrity", "evidence-closure", `snapshot-serving probe ${binarySelection.snapshotProbeSha256} is missing`);
623
+ const probe = parseRecord(probeBytes, BinaryJudgmentSnapshotProbeSchema, `records/${probeHex}.bin`);
624
+ requireCanonical(probeBytes, probe, `records/${probeHex}.bin`);
625
+ // This is the cold-verify SECOND COPY of the bind-time rule (spec §1.5 rule 4), not a
626
+ // second enforcement point (§0.5): a cold verifier re-derives from bytes without trusting
627
+ // the producer, which is the same reason this package already carries second copies of
628
+ // other producer-side rules. Freshness (§1.5 rule 3) is deliberately NOT re-checked here —
629
+ // it scopes to bind, where the bind clock exists.
630
+ if (!instruments.some((entry) => entry.model === probe.requestedModel)) {
631
+ refuse("record-integrity", "evidence-closure", "snapshot-serving probe model is not the model of any bound arm");
632
+ }
633
+ if (probe.outcome !== "serving") {
634
+ refuse("record-integrity", "evidence-closure", "snapshot-serving probe outcome is not serving");
635
+ }
636
+ addRole(expectedRoles, probeHex, "snapshot-probe");
637
+ }
638
+ const reviewerBindingMap = new Map();
639
+ for (const entry of verifiedAdmission.reachableRecords
640
+ .filter((candidate) => candidate.roles.includes("human-review-verdict"))) {
641
+ const bytes = records.get(entry.sha256.slice("sha256:".length));
642
+ const view = readAdmissionVerdictEnvelope(bytes);
643
+ const keyId = parseExactDsseEnvelope(bytes).signatures[0]?.keyid;
644
+ if (typeof keyId !== "string") {
645
+ refuse("record-integrity", "trust.admission.reviewers", `reviewer ${view.evaluatorId} has no signer key id`);
646
+ }
647
+ const prior = reviewerBindingMap.get(view.evaluatorId);
648
+ if (prior !== undefined && prior !== keyId) {
649
+ refuse("record-integrity", "trust.admission.reviewers", `reviewer ${view.evaluatorId} uses more than one key`);
650
+ }
651
+ reviewerBindingMap.set(view.evaluatorId, keyId);
652
+ }
653
+ const reviewerBindings = [...reviewerBindingMap].map(([evaluator, keyId]) => ({ evaluator, keyId }))
654
+ .sort((left, right) => left.evaluator < right.evaluator ? -1 : left.evaluator > right.evaluator ? 1 : 0);
655
+ if (!sameCanonical(reviewerBindings, v4Trust.admission.reviewers)) {
656
+ refuse("record-integrity", "trust.admission.reviewers", "reviewer registry differs from authenticated signed review closure");
657
+ }
658
+ binaryAssetQualification = {
659
+ publicationGrade: qualification.publicationGrade,
660
+ truthAdmission: qualification.truthAdmission,
661
+ sourceManifestSha256: qualification.sourceManifestSha256,
662
+ admissionManifestSha256: qualification.admissionManifestSha256,
663
+ exclusions: qualification.exclusions,
664
+ instruments: instruments.map(({ armId, instrumentSha256, promptTemplateSha256 }) => ({
665
+ armId,
666
+ instrumentSha256,
667
+ promptTemplateSha256,
668
+ })),
669
+ };
670
+ }
671
+ for (const cell of assembly.cells) {
672
+ unique(cell.verdicts.map((verdict) => String(verdict.evalIndex)), `${cell.cellKey}.verdicts.evalIndex`);
673
+ if (cell.verdicts.some((verdict) => verdict.evalIndex < 1 || verdict.evalIndex > minVerdicts)) {
674
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} verdict index is outside Run policy`);
675
+ }
676
+ }
677
+ for (const coord of expectedCoordinates) {
678
+ addRole(expectedRoles, coord.taskDigest, "task");
679
+ const taskBytes = records.get(coord.taskDigest);
680
+ if (taskBytes === undefined)
681
+ refuse("record-integrity", "evidence-closure", `${coord.cellKey} has no exact Task bytes`);
682
+ const task = taskSpecs.get(coord.taskDigest) ?? parseRecord(taskBytes, TaskSpecificationSchema, `records/${coord.taskDigest}.bin`);
683
+ taskSpecs.set(coord.taskDigest, task);
684
+ if (task.profile.uri === INSPECT_TASK_PROFILE_URI) {
685
+ const payload = task.payload;
686
+ const selectionSha256 = payload.selectionManifestSha256;
687
+ if (typeof selectionSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(selectionSha256)) {
688
+ refuse("record-integrity", "evidence-closure", `${coord.cellKey} Inspect Task has no sealed runtime selection identity`);
689
+ }
690
+ addRole(expectedRoles, selectionSha256, "runtime-selection");
691
+ if (!inspectSelections.has(selectionSha256)) {
692
+ const selectionBytes = records.get(selectionSha256);
693
+ if (selectionBytes === undefined) {
694
+ refuse("record-integrity", "evidence-closure", `${coord.cellKey} has no exact Inspect runtime selection bytes`);
695
+ }
696
+ const selection = parseRecord(selectionBytes, InspectSelectionManifestSchema, `records/${selectionSha256}.bin`);
697
+ requireCanonical(selectionBytes, selection, `records/${selectionSha256}.bin`);
698
+ inspectSelections.set(selectionSha256, selection);
699
+ }
700
+ }
701
+ const evaluationSpecSha256 = task.evaluation?.digest?.sha256;
702
+ if (typeof evaluationSpecSha256 !== "string")
703
+ refuse("record-integrity", "evidence-closure", `${coord.cellKey} Task has no EvaluationSpec digest`);
704
+ addRole(expectedRoles, evaluationSpecSha256, "evaluation-spec");
705
+ if (!evaluationSpecs.has(evaluationSpecSha256)) {
706
+ const specBytes = records.get(evaluationSpecSha256);
707
+ if (specBytes === undefined)
708
+ refuse("record-integrity", "evidence-closure", `${coord.cellKey} has no exact EvaluationSpec bytes`);
709
+ try {
710
+ evaluationSpecs.set(evaluationSpecSha256, parseEvaluationSpec(specBytes));
711
+ }
712
+ catch {
713
+ refuse("record-integrity", "evidence-closure", `${coord.cellKey} EvaluationSpec bytes are invalid`);
714
+ }
715
+ }
716
+ }
717
+ // Binary-judgment Tasks (the family that includes binary-instrument@1 and the two judge
718
+ // readouts) catalog a runtime-selection, arm instruments, analysis-context, and
719
+ // label-resolution even when the Report is not binary-instrument@1 and there is therefore no
720
+ // qualification.json. The V4 qualification block above already expects those roles; this is
721
+ // the same producer-side `binaryInspectRuntime` catalog for the non-V4 additional-analysis
722
+ // bundles. Snapshot-probe is V4-only (stripped from the /2 catalog).
723
+ if (qualification === undefined
724
+ && [...taskSpecs.values()].some((task) => task.profile.uri === BINARY_JUDGMENT_PROFILE_URI)) {
725
+ const registeredSelections = readRunPublicationExtension(run)
726
+ ?.registrationArtifacts.filter((artifact) => artifact.role === INSPECT_SELECTION_CORRELATION_ROLE) ?? [];
727
+ if (registeredSelections.length !== 1
728
+ || registeredSelections[0].artifact.mediaType !== "application/json") {
729
+ refuse("record-integrity", "evidence-closure", "binary Run must register exactly one JSON Inspect runtime selection");
730
+ }
731
+ const registeredSelectionSha256 = registeredSelections[0].artifact.digest.sha256;
732
+ const selectionBytes = records.get(registeredSelectionSha256);
733
+ if (selectionBytes === undefined) {
734
+ refuse("record-integrity", "evidence-closure", "binary runtime-selection bytes are missing");
735
+ }
736
+ const binarySelection = parseRecord(selectionBytes, InspectBinaryJudgeSelectionManifestSchema, `records/${registeredSelectionSha256}.bin`);
737
+ requireCanonical(selectionBytes, binarySelection, `records/${registeredSelectionSha256}.bin`);
738
+ addRole(expectedRoles, registeredSelectionSha256, "runtime-selection");
739
+ for (const arm of run.arms) {
740
+ const instrumentSha256 = arm.pinning[BINARY_JUDGMENT_INSTRUMENT_REQUIREMENT_KEY];
741
+ if (typeof instrumentSha256 !== "string" || !/^sha256:[a-f0-9]{64}$/u.test(instrumentSha256)) {
742
+ refuse("record-integrity", "evidence-closure", `Run arm ${arm.armId} has no exact judge-instrument pin`);
743
+ }
744
+ const instrumentHex = instrumentSha256.slice("sha256:".length);
745
+ const instrumentBytes = records.get(instrumentHex);
746
+ if (instrumentBytes === undefined) {
747
+ refuse("record-integrity", "evidence-closure", `instrument ${instrumentSha256} is missing`);
748
+ }
749
+ let instrument;
750
+ try {
751
+ instrument = parseBinaryJudgmentInstrument(instrumentBytes);
752
+ }
753
+ catch (cause) {
754
+ refuse("record-integrity", "evidence-closure", `instrument ${instrumentSha256} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
755
+ }
756
+ requireCanonical(instrumentBytes, instrument, `records/${instrumentHex}.bin`);
757
+ addRole(expectedRoles, instrumentHex, "judge-instrument");
758
+ }
759
+ for (const task of taskSpecs.values()) {
760
+ if (task.profile.uri !== BINARY_JUDGMENT_PROFILE_URI)
761
+ continue;
762
+ const specSha256 = task.evaluation?.digest?.sha256;
763
+ if (typeof specSha256 !== "string") {
764
+ refuse("record-integrity", "evidence-closure", "binary Task has no EvaluationSpec digest");
765
+ }
766
+ const spec = evaluationSpecs.get(specSha256);
767
+ if (spec === undefined) {
768
+ refuse("record-integrity", "evidence-closure", `EvaluationSpec ${specSha256} is not in the catalog`);
769
+ }
770
+ const analysisHex = analysisContextDigestFromEvalSpec(spec);
771
+ if (analysisHex === undefined) {
772
+ refuse("record-integrity", "evidence-closure", `EvaluationSpec ${specSha256} has no analysis-context`);
773
+ }
774
+ const analysisBytes = records.get(analysisHex);
775
+ if (analysisBytes === undefined) {
776
+ refuse("record-integrity", "evidence-closure", `analysis context ${analysisHex} is missing`);
777
+ }
778
+ let analysis;
779
+ try {
780
+ analysis = parseBinaryJudgmentAnalysisContext(analysisBytes);
781
+ }
782
+ catch (cause) {
783
+ refuse("record-integrity", "evidence-closure", `analysis context ${analysisHex} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
784
+ }
785
+ requireCanonical(analysisBytes, analysis, `records/${analysisHex}.bin`);
786
+ addRole(expectedRoles, analysisHex, "analysis-context");
787
+ const labelHex = analysis.labelResolutionSha256.slice("sha256:".length);
788
+ if (records.get(labelHex) === undefined) {
789
+ refuse("record-integrity", "evidence-closure", `label resolution ${analysis.labelResolutionSha256} is missing`);
790
+ }
791
+ addRole(expectedRoles, labelHex, "label-resolution");
792
+ }
793
+ }
794
+ unique(assembly.header.graph.admissions.map((entry) => entry.taskSha256), "verification.graph.admissions.taskSha256");
795
+ const admissionByTask = new Map();
796
+ for (const edge of assembly.header.graph.admissions) {
797
+ if (![...coordinatesByKey.values()].some((coord) => coord.taskDigest === edge.taskSha256)) {
798
+ refuse("record-integrity", "evidence-closure", `admission receipt is unreachable from Benchmark Task ${edge.taskSha256}`);
799
+ }
800
+ addRole(expectedRoles, edge.receiptSha256, "admission-receipt");
801
+ const bytes = records.get(edge.receiptSha256);
802
+ const parsed = bytes === undefined ? undefined : parsePredictionSnapshotAdmissionReceipt(bytes, edge.receiptSha256);
803
+ if (parsed === undefined || parsed.taskSha256 !== edge.taskSha256) {
804
+ refuse("record-integrity", "evidence-closure", `admission receipt ${edge.receiptSha256} does not bind Task ${edge.taskSha256}`);
805
+ }
806
+ admissionByTask.set(edge.taskSha256, { receiptSha256: edge.receiptSha256, fact: parsed.fact });
807
+ }
808
+ unique(assembly.header.graph.solveSubmissions.map((edge) => `${edge.cellKey}:${edge.dispatch}`), "verification.graph.solveSubmissions.coordinates");
809
+ const solveSubmissionByCoordinate = new Map();
810
+ const pinningEvidenceByDigest = new Map();
811
+ for (const edge of assembly.header.graph.solveSubmissions) {
812
+ const coord = coordinatesByKey.get(edge.cellKey);
813
+ if (coord === undefined)
814
+ refuse("record-integrity", "evidence-closure", `solve Submission ${edge.sha256} names an unknown cell`);
815
+ const cell = cellsByKey.get(edge.cellKey);
816
+ if (edge.dispatch < 1 || edge.dispatch > cell.dispatches) {
817
+ refuse("record-integrity", "evidence-closure", `solve Submission ${edge.sha256} dispatch is outside its cell lineage`);
818
+ }
819
+ addRole(expectedRoles, edge.sha256, "solve-submission");
820
+ const bytes = records.get(edge.sha256);
821
+ if (bytes === undefined)
822
+ refuse("record-integrity", "evidence-closure", `solve Submission ${edge.sha256} bytes are missing`);
823
+ const submission = parseRecord(bytes, SubmissionRecordSchema, `records/${edge.sha256}.bin`);
824
+ const expectedNonce = `${edge.cellKey}:${edge.dispatch}`;
825
+ if (submission.task.digest?.sha256 !== coord.taskDigest
826
+ || submission.nonce !== expectedNonce
827
+ || submission.idempotencyKey !== cellIdempotencyKey(`sha256:${identities.runSha256}`, edge.cellKey, edge.dispatch))
828
+ refuse("record-integrity", "evidence-closure", `solve Submission ${edge.sha256} does not bind its Task/cell/dispatch`);
829
+ if (edge.pinningEvidenceSha256 !== undefined) {
830
+ addRole(expectedRoles, edge.pinningEvidenceSha256, "run-pinning-evidence");
831
+ const evidenceBytes = records.get(edge.pinningEvidenceSha256);
832
+ if (evidenceBytes === undefined) {
833
+ refuse("record-integrity", "evidence-closure", `run-pinning evidence ${edge.pinningEvidenceSha256} bytes are missing`);
834
+ }
835
+ let evidence;
836
+ try {
837
+ evidence = parseRunPinningEvidenceArtifact(evidenceBytes);
838
+ }
839
+ catch {
840
+ refuse("record-integrity", "evidence-closure", `run-pinning evidence ${edge.pinningEvidenceSha256} is invalid`);
841
+ }
842
+ requireCanonical(evidenceBytes, evidence, `records/${edge.pinningEvidenceSha256}.bin`);
843
+ if (evidence.submissionDigest !== `sha256:${edge.sha256}`) {
844
+ refuse("record-integrity", "evidence-closure", `run-pinning evidence ${edge.pinningEvidenceSha256} names another Submission`);
845
+ }
846
+ pinningEvidenceByDigest.set(edge.pinningEvidenceSha256, evidence);
847
+ }
848
+ solveSubmissionByCoordinate.set(`${edge.cellKey}:${edge.dispatch}`, edge);
849
+ }
850
+ for (const cell of assembly.cells) {
851
+ for (let dispatch = 1; dispatch <= cell.dispatches; dispatch += 1) {
852
+ if (!solveSubmissionByCoordinate.has(`${cell.cellKey}:${dispatch}`)) {
853
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} is missing solve Submission dispatch ${dispatch}`);
854
+ }
855
+ }
856
+ }
857
+ unique(assembly.header.graph.solveDeliveries.map((edge) => edge.cellKey), "verification.graph.solveDeliveries.cells");
858
+ const solveDeliveryByCell = new Map();
859
+ const inspectClosureByCell = new Map();
860
+ for (const edge of assembly.header.graph.solveDeliveries) {
861
+ const coord = coordinatesByKey.get(edge.cellKey);
862
+ if (coord === undefined)
863
+ refuse("record-integrity", "evidence-closure", `solve Delivery ${edge.sha256} names an unknown cell`);
864
+ const cell = cellsByKey.get(edge.cellKey);
865
+ if (edge.dispatch !== cell.dispatches
866
+ || solveSubmissionByCoordinate.get(`${edge.cellKey}:${edge.dispatch}`) === undefined
867
+ || cell.deliverySha256 !== edge.sha256) {
868
+ refuse("record-integrity", "evidence-closure", `solve Delivery ${edge.sha256} is outside its exact cell lineage`);
869
+ }
870
+ addRole(expectedRoles, edge.sha256, "solve-delivery");
871
+ const bytes = records.get(edge.sha256);
872
+ if (bytes === undefined)
873
+ refuse("record-integrity", "evidence-closure", `solve Delivery ${edge.sha256} bytes are missing`);
874
+ const delivery = parseRecord(bytes, DeliveryRecordSchema, `records/${edge.sha256}.bin`);
875
+ if (delivery.task !== `sha256:${coord.taskDigest}` || delivery.attempt !== edge.attempt) {
876
+ refuse("record-integrity", "evidence-closure", `solve Delivery ${edge.sha256} does not bind its cell Task/attempt`);
877
+ }
878
+ const actualOutputs = delivery.outputs.flatMap((output) => typeof output.digest?.sha256 === "string"
879
+ ? [{ name: output.name, sha256: output.digest.sha256 }]
880
+ : []);
881
+ if (!equalBytes(canonicalJsonBytes(actualOutputs), canonicalJsonBytes(edge.outputs))) {
882
+ refuse("record-integrity", "evidence-closure", `solve Delivery ${edge.sha256} outputs disagree with the journal graph`);
883
+ }
884
+ for (const output of edge.outputs)
885
+ addRole(expectedRoles, output.sha256, "solve-output");
886
+ solveDeliveryByCell.set(edge.cellKey, edge);
887
+ const task = taskSpecs.get(cell.taskDigest);
888
+ if (task?.profile.uri === INSPECT_TASK_PROFILE_URI) {
889
+ const nativeLog = edge.outputs.find((output) => output.name === "inspect-log");
890
+ const summaryOutput = edge.outputs.find((output) => output.name === "inspect-summary");
891
+ const verdictOutput = edge.outputs.find((output) => output.name === "verdict");
892
+ if (nativeLog === undefined || summaryOutput === undefined) {
893
+ refuse("record-integrity", "evidence-closure", "Inspect Delivery lacks its native log or summary");
894
+ }
895
+ const nativeBytes = records.get(nativeLog.sha256);
896
+ const summaryBytes = records.get(summaryOutput.sha256);
897
+ if (nativeBytes === undefined || summaryBytes === undefined) {
898
+ refuse("record-integrity", "evidence-closure", "Inspect native log or summary bytes are absent");
899
+ }
900
+ let summary;
901
+ try {
902
+ summary = InspectCellSummarySchema.parse(JSON.parse(new TextDecoder("utf8", { fatal: true }).decode(summaryBytes)));
903
+ }
904
+ catch {
905
+ refuse("record-integrity", "evidence-closure", "Inspect summary is invalid");
906
+ }
907
+ if (summary.nativeLogSha256 !== nativeLog.sha256
908
+ || summary.nativeLogBytes !== nativeBytes.length
909
+ || sha256(nativeBytes) !== nativeLog.sha256)
910
+ refuse("record-integrity", "evidence-closure", "Inspect summary does not bind the exact native log");
911
+ const selectionSha256 = task.payload.selectionManifestSha256;
912
+ const selection = typeof selectionSha256 === "string" ? inspectSelections.get(selectionSha256) : undefined;
913
+ const arm = selection?.arms.find((candidate) => candidate.armId === cell.armId);
914
+ if (selection === undefined || arm === undefined) {
915
+ refuse("record-integrity", "evidence-closure", "Inspect Delivery has no exact selected arm configuration");
916
+ }
917
+ if (summary.schema === "jinn.network/benchmark-product/inspect-cell-summary/2") {
918
+ let projectedVerdict;
919
+ try {
920
+ projectedVerdict = projectInspectCellVerdict(summary, selection);
921
+ }
922
+ catch {
923
+ refuse("record-integrity", "evidence-closure", "Inspect summary differs from its sealed scorer projections");
924
+ }
925
+ if (summary.verdict !== projectedVerdict || !isInspectMultiScorerSelection(selection)) {
926
+ refuse("record-integrity", "evidence-closure", "Inspect summary verdict does not match its sealed scoring rule");
927
+ }
928
+ const evaluationSpecSha256 = task.evaluation?.digest?.sha256;
929
+ const spec = evaluationSpecSha256 === undefined ? undefined : evaluationSpecs.get(evaluationSpecSha256);
930
+ const expectedDeclarations = summary.measurements.map((measurement) => ({
931
+ name: measurement.measurementName,
932
+ type: "boolean",
933
+ required: true,
934
+ }));
935
+ if (spec === undefined
936
+ || !equalBytes(canonicalJsonBytes(spec.measurements), canonicalJsonBytes(expectedDeclarations))
937
+ || !equalBytes(canonicalJsonBytes(spec.verdictRule), canonicalJsonBytes(selection.scoring.verdictRule))) {
938
+ refuse("record-integrity", "evidence-closure", "Inspect summary measurements differ from the sealed EvaluationSpec");
939
+ }
940
+ }
941
+ else if (isInspectMultiScorerSelection(selection) || summary.scorer !== selection.scorer.name) {
942
+ refuse("record-integrity", "evidence-closure", "legacy Inspect summary differs from its sealed scorer selection");
943
+ }
944
+ if (arm.provider === undefined) {
945
+ if (summary.provider !== undefined) {
946
+ refuse("record-integrity", "evidence-closure", "credential-free Inspect arm carries provider evidence");
947
+ }
948
+ }
949
+ else {
950
+ const provider = summary.provider;
951
+ const completed = provider?.terminalStatus === "completed";
952
+ const noCall = provider?.terminalStatus === "no-call";
953
+ const failedWithMethodConflict = provider?.terminalStatus === "method-conflict";
954
+ if (provider?.surface !== arm.provider.surface
955
+ || provider.brokerProtocol !== selection.runtime.execution?.broker?.protocol
956
+ || provider.brokerSourceSha256 !== selection.runtime.execution?.brokerSourceSha256
957
+ || (noCall ? provider.callCount !== 0 : provider.callCount !== 1)
958
+ || (noCall && provider.resolvedModel !== null)
959
+ || (completed && provider.resolvedModel !== arm.provider.upstreamModel)
960
+ || (!completed && !failedWithMethodConflict && provider.resolvedModel !== null
961
+ && provider.resolvedModel !== arm.provider.upstreamModel)
962
+ || (summary.terminal === "scored" && !completed)) {
963
+ refuse("record-integrity", "evidence-closure", "Inspect provider evidence differs from the sealed arm/runtime selection");
964
+ }
965
+ }
966
+ inspectClosureByCell.set(edge.cellKey, {
967
+ summary,
968
+ selection,
969
+ selectionSha256: selectionSha256,
970
+ arm,
971
+ nativeLog,
972
+ ...(verdictOutput === undefined ? {} : { verdictOutput }),
973
+ });
974
+ }
975
+ }
976
+ unique(assembly.header.graph.evaluationSubmissions.map((edge) => `${edge.cellKey}:${edge.evalIndex}:${edge.evaluationAttempt ?? 1}`), "verification.graph.evaluationSubmissions.coordinates");
977
+ const evaluationSubmissionByDigest = new Map();
978
+ for (const edge of assembly.header.graph.evaluationSubmissions) {
979
+ const cell = cellsByKey.get(edge.cellKey);
980
+ if (cell === undefined)
981
+ refuse("record-integrity", "evidence-closure", `evaluation Submission ${edge.sha256} names an unknown cell`);
982
+ if (edge.dispatch < 1 || edge.dispatch > cell.dispatches || edge.evalIndex < 1 || edge.evalIndex > minVerdicts) {
983
+ refuse("record-integrity", "evidence-closure", `evaluation Submission ${edge.sha256} is outside its cell/evaluator domain`);
984
+ }
985
+ const bytes = records.get(edge.sha256);
986
+ if (bytes === undefined)
987
+ refuse("record-integrity", "evidence-closure", `evaluation Submission ${edge.sha256} bytes are missing`);
988
+ const submission = parseRecord(bytes, SubmissionRecordSchema, `records/${edge.sha256}.bin`);
989
+ const evaluationAttempt = edge.evaluationAttempt ?? 1;
990
+ const expectedNonce = evaluationAttempt === 1
991
+ ? `eval:${identities.runSha256}:e${edge.evalIndex}:${edge.cellKey}:${edge.dispatch}`
992
+ : `eval:${identities.runSha256}:e${edge.evalIndex}:r${evaluationAttempt}:${edge.cellKey}:${edge.dispatch}`;
993
+ if (submission.task.digest?.sha256 !== edge.evalTaskSha256
994
+ || submission.nonce !== expectedNonce || submission.idempotencyKey !== expectedNonce
995
+ || submission.requirements?.[EVALUATOR_REQUIREMENT_KEY] !== edge.evaluator)
996
+ refuse("record-integrity", "evidence-closure", `evaluation Submission ${edge.sha256} does not bind its Task/cell/evaluator`);
997
+ evaluationSubmissionByDigest.set(edge.sha256, edge);
998
+ }
999
+ unique(assembly.header.graph.evaluations.map((edge) => `${edge.cellKey}:${edge.evalIndex}:${edge.evaluationAttempt ?? 1}`), "verification.graph.evaluations.coordinates");
1000
+ const evaluationsByVerdict = new Map();
1001
+ const consumedEvaluationSubmissions = new Set();
1002
+ const inspectEvaluationStrategy = deriveInspectEvaluationStrategy(run.policy.evaluation);
1003
+ for (const edge of assembly.header.graph.evaluations) {
1004
+ const cell = cellsByKey.get(edge.cellKey);
1005
+ if (cell === undefined)
1006
+ refuse("record-integrity", "evidence-closure", "evaluation graph names an unknown cell");
1007
+ if (edge.evalIndex < 1 || edge.evalIndex > minVerdicts)
1008
+ refuse("record-integrity", "evidence-closure", "evaluation graph index is outside Run policy");
1009
+ const successful = edge.verdictSha256 !== undefined;
1010
+ const embedded = edge.relationship === "same-execution-scorer";
1011
+ const separateInspectVerifier = edge.relationship === "separate-log-verifier";
1012
+ const inspectClosure = inspectClosureByCell.get(cell.cellKey);
1013
+ if (successful
1014
+ && inspectClosure !== undefined
1015
+ && inspectEvaluationStrategy === "separate-log-verification"
1016
+ && !separateInspectVerifier) {
1017
+ refuse("record-integrity", "evidence-closure", "separate Inspect verdict omits its verifier relationship");
1018
+ }
1019
+ if (successful) {
1020
+ if (embedded) {
1021
+ if (inspectEvaluationStrategy !== "embedded") {
1022
+ refuse("record-integrity", "evidence-closure", "separate Inspect assurance contains an embedded Matrix vote");
1023
+ }
1024
+ if (edge.evaluator !== INSPECT_EMBEDDED_EVALUATOR_ID
1025
+ || edge.evalTaskSha256 !== undefined || edge.evalSubmissionSha256 !== undefined
1026
+ || edge.evalAttempt !== undefined || edge.evalDeliverySha256 !== undefined
1027
+ || edge.evaluationTerminal !== undefined)
1028
+ refuse("record-integrity", "evidence-closure", "same-execution Inspect score carries false separate-evaluator lineage");
1029
+ // Binary Tasks (binary-instrument@1 and the two judge-family readouts that share its
1030
+ // execution) are runtime-neutral and carry no generic Inspect summary. `qualification.json`
1031
+ // is present only for binary-instrument@1's V4 bundle; pairwise-disagreement@1 and
1032
+ // paired-majority-delta@1 ride the same Binary Tasks without that file, so the
1033
+ // discriminator is the Task profile URI (with qualification.json as the V4 alias).
1034
+ const binaryJudgmentCell = qualification !== undefined
1035
+ || taskSpecs.get(cell.taskDigest)?.profile.uri === BINARY_JUDGMENT_PROFILE_URI;
1036
+ if (binaryJudgmentCell) {
1037
+ // The solve Delivery plus signed verdict are joined here; verifyReport is the sole
1038
+ // authority that replays its exact response/observation/instrument semantics below.
1039
+ if (solveDeliveryByCell.get(cell.cellKey) === undefined) {
1040
+ refuse("record-integrity", "evidence-closure", "binary same-execution verdict has no solve Delivery lineage");
1041
+ }
1042
+ }
1043
+ else {
1044
+ const closure = inspectClosureByCell.get(cell.cellKey);
1045
+ const summary = closure?.summary;
1046
+ const verdictOutput = closure?.verdictOutput;
1047
+ if (closure === undefined || summary === undefined
1048
+ || verdictOutput?.sha256 !== edge.verdictSha256)
1049
+ refuse("record-integrity", "evidence-closure", "same-execution score is not bound to one Inspect Task/Delivery/log/summary/verdict closure");
1050
+ if (summary.terminal !== "scored")
1051
+ refuse("record-integrity", "evidence-closure", "Inspect verdict is attached to an unscorable summary");
1052
+ if (summary.schema === "jinn.network/benchmark-product/inspect-cell-summary/2") {
1053
+ const verdictBytes = verdictOutput === undefined ? undefined : records.get(verdictOutput.sha256);
1054
+ if (verdictBytes === undefined) {
1055
+ refuse("record-integrity", "evidence-closure", "Inspect Result Evaluation bytes are absent");
1056
+ }
1057
+ const verdictView = readVerdictEnvelope(verdictBytes);
1058
+ const expectedMeasurements = summary.measurements.map((measurement) => ({
1059
+ name: measurement.measurementName,
1060
+ value: measurement.value,
1061
+ }));
1062
+ if (verdictView.verdict !== summary.verdict
1063
+ || !equalBytes(canonicalJsonBytes(readOrderedVerdictMeasurements(verdictBytes)), canonicalJsonBytes(expectedMeasurements))) {
1064
+ refuse("record-integrity", "evidence-closure", "Inspect Result Evaluation differs from its projected measurements");
1065
+ }
1066
+ }
1067
+ }
1068
+ }
1069
+ else if (edge.evaluator === undefined || edge.evalTaskSha256 === undefined
1070
+ || edge.evalSubmissionSha256 === undefined || edge.evalAttempt === undefined
1071
+ || edge.evalDeliverySha256 === undefined || edge.evaluationTerminal !== undefined)
1072
+ refuse("record-integrity", "evidence-closure", "successful evaluation lacks its exact Task/Submission/attempt/Delivery closure");
1073
+ if (separateInspectVerifier) {
1074
+ const closure = inspectClosureByCell.get(cell.cellKey);
1075
+ if (inspectEvaluationStrategy !== "separate-log-verification"
1076
+ || closure === undefined
1077
+ || closure.verdictOutput !== undefined
1078
+ || closure.summary.terminal !== "scored"
1079
+ || edge.evaluator === INSPECT_EMBEDDED_EVALUATOR_ID) {
1080
+ refuse("record-integrity", "evidence-closure", "separate Inspect verification is not bound to a scored solve-only native-log closure");
1081
+ }
1082
+ }
1083
+ }
1084
+ else if (edge.evaluationTerminal !== "could-not-grade"
1085
+ || cell.evaluationTerminal !== "could-not-grade"
1086
+ || cell.verdicts.some((verdict) => verdict.evalIndex === edge.evalIndex))
1087
+ refuse("record-integrity", "evidence-closure", "terminal evaluation is outside its exact cell/index lineage");
1088
+ const carriesLineage = edge.evalTaskSha256 !== undefined
1089
+ || edge.evalSubmissionSha256 !== undefined || edge.evalAttempt !== undefined
1090
+ || edge.evalDeliverySha256 !== undefined;
1091
+ if (!embedded && !carriesLineage && edge.evaluator !== undefined) {
1092
+ refuse("record-integrity", "evidence-closure", "pre-evaluation terminal cannot claim an evaluator identity");
1093
+ }
1094
+ if (carriesLineage && !embedded && (edge.evaluator === undefined || edge.evalTaskSha256 === undefined)) {
1095
+ refuse("record-integrity", "evidence-closure", "partial evaluation lineage must name its evaluator and exact Task");
1096
+ }
1097
+ if (edge.evalAttempt !== undefined && edge.evalSubmissionSha256 === undefined) {
1098
+ refuse("record-integrity", "evidence-closure", "evaluation attempt has no accepted Submission lineage");
1099
+ }
1100
+ if (edge.evalDeliverySha256 !== undefined && edge.evalAttempt === undefined) {
1101
+ refuse("record-integrity", "evidence-closure", "evaluation Delivery has no attempt lineage");
1102
+ }
1103
+ const submission = edge.evalSubmissionSha256 === undefined
1104
+ ? undefined
1105
+ : evaluationSubmissionByDigest.get(edge.evalSubmissionSha256);
1106
+ if (edge.evalSubmissionSha256 !== undefined && submission === undefined) {
1107
+ refuse("record-integrity", "evidence-closure", "evaluation graph names an unknown accepted Submission");
1108
+ }
1109
+ if (submission !== undefined) {
1110
+ if (submission.cellKey !== edge.cellKey || submission.evalIndex !== edge.evalIndex
1111
+ || (submission.evaluationAttempt ?? 1) !== (edge.evaluationAttempt ?? 1)
1112
+ || submission.evalTaskSha256 !== edge.evalTaskSha256 || submission.evaluator !== edge.evaluator)
1113
+ refuse("record-integrity", "evidence-closure", "evaluation graph and Submission linkage disagree");
1114
+ consumedEvaluationSubmissions.add(submission.sha256);
1115
+ addRole(expectedRoles, submission.sha256, "evaluation-submission");
1116
+ }
1117
+ if (edge.evalTaskSha256 !== undefined)
1118
+ addRole(expectedRoles, edge.evalTaskSha256, "evaluation-task");
1119
+ if (edge.evalTaskSha256 !== undefined) {
1120
+ const solveDelivery = solveDeliveryByCell.get(cell.cellKey);
1121
+ if (solveDelivery === undefined || cell.evaluationSpecSha256 === undefined) {
1122
+ refuse("record-integrity", "evidence-closure", `evaluation Task ${edge.evalTaskSha256} has no solve artifacts/spec source`);
1123
+ }
1124
+ const derived = deriveEvaluationTask({
1125
+ subjectTask: { name: "subject-task.json", digest: `sha256:${cell.taskDigest}` },
1126
+ subjectDelivery: { name: "subject-delivery.json", digest: `sha256:${solveDelivery.sha256}` },
1127
+ subjectResults: solveDelivery.outputs.map((output) => ({ name: output.name, digest: `sha256:${output.sha256}` })),
1128
+ evaluationSpecDigest: `sha256:${cell.evaluationSpecSha256}`,
1129
+ });
1130
+ const carried = records.get(edge.evalTaskSha256);
1131
+ if (carried === undefined || !equalBytes(carried, derived.bytes) || derived.digest !== `sha256:${edge.evalTaskSha256}`) {
1132
+ refuse("record-integrity", "evidence-closure", `evaluation Task ${edge.evalTaskSha256} is not the exact derivation from solve artifacts/spec`);
1133
+ }
1134
+ }
1135
+ if (edge.evalDeliverySha256 !== undefined) {
1136
+ addRole(expectedRoles, edge.evalDeliverySha256, "evaluation-delivery");
1137
+ const bytes = records.get(edge.evalDeliverySha256);
1138
+ if (bytes === undefined)
1139
+ refuse("record-integrity", "evidence-closure", `evaluation Delivery ${edge.evalDeliverySha256} bytes are missing`);
1140
+ const delivery = parseRecord(bytes, DeliveryRecordSchema, `records/${edge.evalDeliverySha256}.bin`);
1141
+ const verdictOutput = delivery.outputs.find((output) => output.name === "verdict");
1142
+ if (delivery.task !== `sha256:${edge.evalTaskSha256}` || delivery.attempt !== edge.evalAttempt
1143
+ || (successful && verdictOutput?.digest?.sha256 !== edge.verdictSha256))
1144
+ refuse("record-integrity", "evidence-closure", `evaluation Delivery ${edge.evalDeliverySha256} does not bind Task/attempt/verdict`);
1145
+ }
1146
+ if (edge.verdictSha256 !== undefined) {
1147
+ addRole(expectedRoles, edge.verdictSha256, "verdict");
1148
+ const edges = evaluationsByVerdict.get(edge.verdictSha256) ?? [];
1149
+ edges.push(edge);
1150
+ evaluationsByVerdict.set(edge.verdictSha256, edges);
1151
+ }
1152
+ }
1153
+ const maxInfrastructureRetries = run.policy.evaluation?.maxInfrastructureRetries ?? 0;
1154
+ const retriesByLeg = new Map();
1155
+ const evaluationTaskByLeg = new Map();
1156
+ for (const retry of assembly.header.graph.evaluationRetries ?? []) {
1157
+ const cell = cellsByKey.get(retry.cellKey);
1158
+ if (cell === undefined || retry.dispatch < 1 || retry.dispatch > cell.dispatches
1159
+ || retry.evalIndex < 1 || retry.evalIndex > minVerdicts) {
1160
+ refuse("record-integrity", "evidence-closure", "evaluation retry names an unknown or out-of-domain leg");
1161
+ }
1162
+ if (retry.evaluationAttempt < 1 || retry.evaluationAttempt > maxInfrastructureRetries) {
1163
+ refuse("record-integrity", "evidence-closure", "evaluation retry exceeds the sealed infrastructure retry policy");
1164
+ }
1165
+ if (retry.failureCategory !== "backend-unavailable"
1166
+ && retry.failureCategory !== "dependency-unavailable"
1167
+ && retry.failureCategory !== "transport-failure") {
1168
+ refuse("record-integrity", "evidence-closure", "evaluation retry carries an ineligible failure category");
1169
+ }
1170
+ const key = `${retry.cellKey}:${retry.evalIndex}`;
1171
+ const attempts = retriesByLeg.get(key) ?? [];
1172
+ attempts.push(retry.evaluationAttempt);
1173
+ retriesByLeg.set(key, attempts);
1174
+ const submission = retry.evalSubmissionSha256 === undefined
1175
+ ? undefined
1176
+ : evaluationSubmissionByDigest.get(retry.evalSubmissionSha256);
1177
+ if (retry.evalSubmissionSha256 !== undefined && submission === undefined) {
1178
+ refuse("record-integrity", "evidence-closure", "evaluation retry names an unknown accepted Submission");
1179
+ }
1180
+ if (submission !== undefined) {
1181
+ if (submission.cellKey !== retry.cellKey || submission.dispatch !== retry.dispatch
1182
+ || submission.evalIndex !== retry.evalIndex
1183
+ || (submission.evaluationAttempt ?? 1) !== retry.evaluationAttempt
1184
+ || submission.evaluator !== retry.evaluator
1185
+ || submission.evalTaskSha256 !== retry.evalTaskSha256) {
1186
+ refuse("record-integrity", "evidence-closure", "evaluation retry and Submission lineage disagree");
1187
+ }
1188
+ consumedEvaluationSubmissions.add(submission.sha256);
1189
+ addRole(expectedRoles, submission.sha256, "evaluation-submission");
1190
+ }
1191
+ if (retry.evalTaskSha256 !== undefined) {
1192
+ const priorTask = evaluationTaskByLeg.get(key);
1193
+ if (priorTask !== undefined && priorTask !== retry.evalTaskSha256) {
1194
+ refuse("record-integrity", "evidence-closure", "evaluation retry substituted a different derived Task");
1195
+ }
1196
+ evaluationTaskByLeg.set(key, retry.evalTaskSha256);
1197
+ addRole(expectedRoles, retry.evalTaskSha256, "evaluation-task");
1198
+ const solveDelivery = solveDeliveryByCell.get(retry.cellKey);
1199
+ if (solveDelivery === undefined || cell.evaluationSpecSha256 === undefined) {
1200
+ refuse("record-integrity", "evidence-closure", "evaluation retry Task has no solve artifacts/spec source");
1201
+ }
1202
+ const derived = deriveEvaluationTask({
1203
+ subjectTask: { name: "subject-task.json", digest: `sha256:${cell.taskDigest}` },
1204
+ subjectDelivery: { name: "subject-delivery.json", digest: `sha256:${solveDelivery.sha256}` },
1205
+ subjectResults: solveDelivery.outputs.map((output) => ({ name: output.name, digest: `sha256:${output.sha256}` })),
1206
+ evaluationSpecDigest: `sha256:${cell.evaluationSpecSha256}`,
1207
+ });
1208
+ const carried = records.get(retry.evalTaskSha256);
1209
+ if (carried === undefined || !equalBytes(carried, derived.bytes) || derived.digest !== `sha256:${retry.evalTaskSha256}`) {
1210
+ refuse("record-integrity", "evidence-closure", "evaluation retry Task is not the exact solve-derived Task");
1211
+ }
1212
+ }
1213
+ }
1214
+ for (const [key, attempts] of retriesByLeg) {
1215
+ const ordered = [...attempts].sort((left, right) => left - right);
1216
+ if (ordered.some((attempt, index) => attempt !== index + 1)) {
1217
+ refuse("record-integrity", "evidence-closure", `evaluation retry sequence is non-contiguous for ${key}`);
1218
+ }
1219
+ }
1220
+ for (const edge of assembly.header.graph.evaluations) {
1221
+ const evaluationAttempt = edge.evaluationAttempt ?? 1;
1222
+ if (evaluationAttempt > maxInfrastructureRetries + 1) {
1223
+ refuse("record-integrity", "evidence-closure", "evaluation terminal exceeds the sealed infrastructure retry policy");
1224
+ }
1225
+ if (evaluationAttempt > 1) {
1226
+ const key = `${edge.cellKey}:${edge.evalIndex}`;
1227
+ const attempts = [...(retriesByLeg.get(key) ?? [])].sort((left, right) => left - right);
1228
+ if (attempts.length !== evaluationAttempt - 1
1229
+ || attempts.some((attempt, index) => attempt !== index + 1)) {
1230
+ refuse("record-integrity", "evidence-closure", "evaluation retry terminal lacks its complete failed-attempt lineage");
1231
+ }
1232
+ const priorTask = evaluationTaskByLeg.get(key);
1233
+ if (priorTask !== undefined && edge.evalTaskSha256 !== undefined && priorTask !== edge.evalTaskSha256) {
1234
+ refuse("record-integrity", "evidence-closure", "evaluation retry terminal substituted a different derived Task");
1235
+ }
1236
+ }
1237
+ }
1238
+ if (consumedEvaluationSubmissions.size !== evaluationSubmissionByDigest.size) {
1239
+ refuse("record-integrity", "evidence-closure", "evaluation graph does not consume every exact Submission lineage");
1240
+ }
1241
+ const receipts = new Map();
1242
+ const consumedSuccessfulEvaluations = new Set();
1243
+ const inScopeCells = expectedCoordinates.map((coord) => {
1244
+ const cell = cellsByKey.get(coord.cellKey);
1245
+ if (cell === undefined || cell.armId !== coord.armId || cell.replicate !== coord.replicate || cell.taskDigest !== coord.taskDigest) {
1246
+ refuse("record-integrity", "evidence-closure", `assembly coordinate mismatch for ${coord.cellKey}`);
1247
+ }
1248
+ const task = taskSpecs.get(cell.taskDigest);
1249
+ if (task.evaluation?.digest?.sha256 !== cell.evaluationSpecSha256)
1250
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} Task and EvaluationSpec identities disagree`);
1251
+ const admission = admissionByTask.get(cell.taskDigest);
1252
+ if ((admission === undefined) !== (cell.admission === undefined)
1253
+ || admission?.receiptSha256 !== cell.admissionReceiptSha256
1254
+ || (admission !== undefined && !equalBytes(canonicalJsonBytes(admission.fact), canonicalJsonBytes(cell.admission))))
1255
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} admission receipt/fact linkage disagrees`);
1256
+ if (admission !== undefined)
1257
+ receipts.set(cell.taskDigest, admission.fact);
1258
+ const finalSubmission = cell.submissionSha256 === undefined ? undefined : solveSubmissionByCoordinate.get(`${cell.cellKey}:${cell.dispatches}`);
1259
+ if ((cell.submissionSha256 === undefined) !== (finalSubmission === undefined))
1260
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} final solve Submission linkage disagrees`);
1261
+ if (finalSubmission !== undefined && finalSubmission.sha256 !== cell.submissionSha256)
1262
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} final solve Submission digest disagrees`);
1263
+ if (finalSubmission?.pinningEvidenceSha256 !== cell.pinningEvidenceSha256) {
1264
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} final run-pinning evidence linkage disagrees`);
1265
+ }
1266
+ let pinningEvidence;
1267
+ if (cell.pinningEvidenceSha256 !== undefined) {
1268
+ pinningEvidence = pinningEvidenceByDigest.get(cell.pinningEvidenceSha256);
1269
+ if (pinningEvidence === undefined)
1270
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} run-pinning evidence bytes are missing or unreachable`);
1271
+ }
1272
+ const finalDelivery = solveDeliveryByCell.get(cell.cellKey);
1273
+ if ((cell.deliverySha256 === undefined) !== (finalDelivery === undefined))
1274
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} final solve Delivery linkage disagrees`);
1275
+ if (finalDelivery !== undefined && (finalDelivery.cellKey !== cell.cellKey || finalDelivery.attempt !== cell.attempt
1276
+ || !equalBytes(canonicalJsonBytes(finalDelivery.outputs), canonicalJsonBytes(cell.solveOutputs ?? []))))
1277
+ refuse("record-integrity", "evidence-closure", `${cell.cellKey} solve Delivery/output linkage disagrees`);
1278
+ const evaluationSpec = cell.evaluationSpecSha256 === undefined ? undefined : evaluationSpecs.get(cell.evaluationSpecSha256);
1279
+ const verdicts = cell.verdicts.map((declared) => {
1280
+ const edges = evaluationsByVerdict.get(declared.sha256)?.filter((candidate) => candidate.cellKey === cell.cellKey
1281
+ && candidate.evalIndex === declared.evalIndex
1282
+ && candidate.evaluator === declared.evaluator
1283
+ && candidate.relationship === declared.relationship
1284
+ && candidate.evalTaskSha256 === declared.evalTaskSha256
1285
+ && candidate.evalSubmissionSha256 === declared.evalSubmissionSha256
1286
+ && candidate.evalDeliverySha256 === declared.evalDeliverySha256
1287
+ && candidate.evalAttempt === declared.evalAttempt);
1288
+ const edge = edges?.[0];
1289
+ if (edges?.length !== 1 || edge === undefined || edge.relationship !== declared.relationship
1290
+ || edge.evalTaskSha256 !== declared.evalTaskSha256
1291
+ || edge.evalSubmissionSha256 !== declared.evalSubmissionSha256
1292
+ || edge.evalDeliverySha256 !== declared.evalDeliverySha256
1293
+ || edge.evalAttempt !== declared.evalAttempt || edge.evalIndex !== declared.evalIndex) {
1294
+ refuse("record-integrity", "evidence-closure", `verdict ${declared.sha256} evaluation graph linkage disagrees`);
1295
+ }
1296
+ consumedSuccessfulEvaluations.add(`${edge.cellKey}:${edge.evalIndex}`);
1297
+ const verdictBytes = records.get(declared.sha256);
1298
+ if (verdictBytes === undefined)
1299
+ refuse("record-integrity", "evidence-closure", `verdict ${declared.sha256} bytes are missing`);
1300
+ const view = readVerdictEnvelope(verdictBytes);
1301
+ const binaryJudgmentCell = qualification !== undefined
1302
+ || taskSpecs.get(cell.taskDigest)?.profile.uri === BINARY_JUDGMENT_PROFILE_URI;
1303
+ if (!binaryJudgmentCell && declared.relationship === "same-execution-scorer" && (view.evaluatorExtensions?.["jinn.network/relationship"] !== "same-execution-scorer"
1304
+ || !view.limitations?.includes("same-execution-scorer"))) {
1305
+ refuse("record-integrity", "evidence-closure", `verdict ${declared.sha256} does not disclose its same-execution scorer relationship`);
1306
+ }
1307
+ if (declared.relationship === "separate-log-verifier") {
1308
+ const closure = inspectClosureByCell.get(cell.cellKey);
1309
+ if (closure === undefined) {
1310
+ refuse("record-integrity", "evidence-closure", `verdict ${declared.sha256} has no Inspect solve closure`);
1311
+ }
1312
+ const requiredLimitations = [
1313
+ "score-source:same-execution-scorer",
1314
+ "verification-process:separate",
1315
+ "self-run-operator-custody",
1316
+ "not-independent-rescoring",
1317
+ "not-separate-real-world-party",
1318
+ "not-method-diversity",
1319
+ ];
1320
+ const expectedMethod = inspectLogVerifierMethod(closure.selection, closure.selectionSha256);
1321
+ const expectedMeasurements = closure.summary.schema === "jinn.network/benchmark-product/inspect-cell-summary/1"
1322
+ ? [{ name: "inspect-score-pass", value: closure.summary.measurement }]
1323
+ : closure.summary.measurements.map((measurement) => ({
1324
+ name: measurement.measurementName,
1325
+ value: measurement.value,
1326
+ }));
1327
+ if (closure.summary.verdict !== view.verdict
1328
+ || view.evaluatorId === INSPECT_EMBEDDED_EVALUATOR_ID
1329
+ || view.evaluationMethod?.name !== expectedMethod.name
1330
+ || view.evaluationMethod.sha256 !== expectedMethod.digest.sha256
1331
+ || requiredLimitations.some((limitation) => !view.limitations?.includes(limitation))
1332
+ || !equalBytes(canonicalJsonBytes(readOrderedVerdictMeasurements(verdictBytes)), canonicalJsonBytes(expectedMeasurements))
1333
+ || !view.evidence?.some((evidence) => evidence.name === "inspect-native-log"
1334
+ && evidence.sha256 === closure.nativeLog.sha256
1335
+ && evidence.mediaType === "application/vnd.inspect-ai.eval")) {
1336
+ refuse("record-integrity", "evidence-closure", `verdict ${declared.sha256} is not the locked separate Inspect log verification claim`);
1337
+ }
1338
+ }
1339
+ if (view.evaluatorId !== declared.evaluator || view.verdict !== declared.verdict
1340
+ || view.evaluationSpecificationSha256 !== declared.evaluationSpecSha256
1341
+ || !equalBytes(canonicalJsonBytes(view.measurements), canonicalJsonBytes(declared.measurements)))
1342
+ refuse("record-integrity", "evidence-closure", `assembly facts disagree with verdict ${declared.sha256}`);
1343
+ return {
1344
+ digest: `sha256:${declared.sha256}`,
1345
+ record: { evaluationSpecification: `sha256:${view.evaluationSpecificationSha256}`, evaluator: view.evaluatorId, verdict: view.verdict },
1346
+ measurements: view.measurements,
1347
+ ...(evaluationSpec === undefined ? {} : { evaluationSpec }),
1348
+ };
1349
+ });
1350
+ return {
1351
+ cellKey: cell.cellKey,
1352
+ armId: cell.armId,
1353
+ replicate: cell.replicate,
1354
+ taskDigest: cell.taskDigest,
1355
+ dispatches: cell.dispatches,
1356
+ ...(cell.accounted === undefined ? {} : { accounted: cell.accounted }),
1357
+ ...(cell.submissionSha256 === undefined ? {} : { submissionDigest: `sha256:${cell.submissionSha256}` }),
1358
+ ...(pinningEvidence === undefined ? {} : { evidenceRef: pinningEvidenceFacts(pinningEvidence) }),
1359
+ ...(cell.attempt === undefined ? {} : { attempt: cell.attempt }),
1360
+ ...(cell.deliverySha256 === undefined ? {} : { deliveryDigest: `sha256:${cell.deliverySha256}`, deliveryBytes: records.get(cell.deliverySha256) }),
1361
+ ...(cell.evaluationSpecSha256 === undefined ? {} : { evaluationSpecDigest: `sha256:${cell.evaluationSpecSha256}` }),
1362
+ ...(evaluationSpec === undefined ? {} : { evaluationSpec }),
1363
+ ...(cell.evaluationTerminal === undefined ? {} : { evaluationTerminal: cell.evaluationTerminal }),
1364
+ verdicts,
1365
+ };
1366
+ });
1367
+ const successfulEvaluationCount = assembly.header.graph.evaluations.filter((edge) => edge.verdictSha256 !== undefined).length;
1368
+ if (successfulEvaluationCount !== consumedSuccessfulEvaluations.size) {
1369
+ refuse("record-integrity", "evidence-closure", "evaluation graph contains a successful lineage not consumed by an exact cell verdict");
1370
+ }
1371
+ if (expectedRoles.size !== declaredRoles.size)
1372
+ refuse("record-integrity", "evidence-closure", "evidence catalog contains missing or unreachable records");
1373
+ for (const [digest, roles] of expectedRoles) {
1374
+ const declared = declaredRoles.get(digest);
1375
+ if (declared === undefined || !equalBytes(canonicalJsonBytes(sorted(roles)), canonicalJsonBytes(sorted(declared)))) {
1376
+ refuse("record-integrity", "evidence-closure", `record ${digest} roles do not equal its derived graph roles`);
1377
+ }
1378
+ }
1379
+ const verdictCatalogBytes = read("verdicts.json");
1380
+ const verdictCatalog = parseJson(verdictCatalogBytes, BundleVerdictCatalogSchema, "verdicts.json");
1381
+ requireCanonical(verdictCatalogBytes, verdictCatalog, "verdicts.json");
1382
+ unique(verdictCatalog.verdicts.map((verdict) => verdict.sha256), "verdicts.json.verdicts");
1383
+ const expectedVerdictCells = new Map();
1384
+ for (const cell of matrix.cells)
1385
+ for (const digest of cell.verdicts) {
1386
+ const hex = digest.slice("sha256:".length);
1387
+ const set = expectedVerdictCells.get(hex) ?? new Set();
1388
+ set.add(cell.cellKey);
1389
+ expectedVerdictCells.set(hex, set);
1390
+ }
1391
+ if (expectedVerdictCells.size !== verdictCatalog.verdicts.length)
1392
+ refuse("record-integrity", "verdicts.json", "verdict catalog does not match Matrix verdict closure");
1393
+ for (const verdict of verdictCatalog.verdicts) {
1394
+ const cells = expectedVerdictCells.get(verdict.sha256);
1395
+ if (cells === undefined || !equalBytes(canonicalJsonBytes([...cells].sort()), canonicalJsonBytes(verdict.cellKeys))) {
1396
+ refuse("record-integrity", "verdicts.json", `verdict ${verdict.sha256} cell references do not match Matrix`);
1397
+ }
1398
+ const bytes = records.get(verdict.sha256);
1399
+ if (bytes === undefined || readVerdictEnvelope(bytes).evaluatorId !== verdict.evaluator)
1400
+ refuse("record-integrity", "verdicts.json", `verdict ${verdict.sha256} evaluator mismatch`);
1401
+ }
1402
+ checks.push("evidence-closure");
1403
+ const derivedReportDid = didKeyFromEd25519PublicKey(reportKey);
1404
+ if (trust.report.author !== report.author || trust.report.keyId !== derivedReportDid || trust.report.didKey !== derivedReportDid) {
1405
+ refuse("record-integrity", "trust", "Report author/keyId/didKey are not derived from the bundled Report SPKI");
1406
+ }
1407
+ unique(trust.evaluators.map((entry) => entry.evaluator), "trust.evaluators.evaluator");
1408
+ const admissionReviewerEvaluators = qualification === undefined
1409
+ ? []
1410
+ : BundleV4TrustSchema.parse(trust).admission.reviewers.map((entry) => entry.evaluator);
1411
+ const referencedEvaluators = [...new Set([
1412
+ ...verdictCatalog.verdicts.map((verdict) => verdict.evaluator),
1413
+ ...admissionReviewerEvaluators,
1414
+ ])].sort();
1415
+ const trustedEvaluators = trust.evaluators.map((entry) => entry.evaluator).sort();
1416
+ if (!equalBytes(canonicalJsonBytes(referencedEvaluators), canonicalJsonBytes(trustedEvaluators))) {
1417
+ refuse("record-integrity", "trust", "evaluator trust set must exactly equal Matrix-referenced evaluators");
1418
+ }
1419
+ const evaluatorKeys = new Map();
1420
+ const spkiIdentities = new Set([sha256(Buffer.from(trust.report.spkiDerBase64, "base64"))]);
1421
+ for (const entry of trust.evaluators) {
1422
+ const key = publicKey(entry.spkiDerBase64, `trust.evaluators.${entry.evaluator}`);
1423
+ const derivedKeyId = verdictKeyIdFromEd25519PublicKey(key);
1424
+ if (entry.keyId !== derivedKeyId)
1425
+ refuse("record-integrity", "trust", `evaluator ${entry.evaluator} keyId is not derived from its SPKI`);
1426
+ const spkiIdentity = sha256(Buffer.from(entry.spkiDerBase64, "base64"));
1427
+ if (spkiIdentities.has(spkiIdentity))
1428
+ refuse("record-integrity", "trust", "one SPKI is reused under multiple bundled identities");
1429
+ spkiIdentities.add(spkiIdentity);
1430
+ evaluatorKeys.set(entry.evaluator, { keyId: entry.keyId, publicKey: key });
1431
+ }
1432
+ for (const verdict of verdictCatalog.verdicts) {
1433
+ const key = evaluatorKeys.get(verdict.evaluator);
1434
+ if (key === undefined || key.keyId !== verdict.keyId)
1435
+ refuse("record-integrity", "trust", `verdict ${verdict.sha256} keyId cross-validation failed`);
1436
+ }
1437
+ checks.push("trust");
1438
+ const resolveBytes = (digest) => {
1439
+ const hex = digest.startsWith("sha256:") ? digest.slice("sha256:".length) : digest;
1440
+ const bytes = records.get(hex)
1441
+ ?? (hex === identities.benchmarkSha256 ? benchmarkBytes : undefined)
1442
+ ?? (hex === identities.runSha256 ? runBytes : undefined)
1443
+ ?? (hex === identities.matrixSha256 ? matrixBytes : undefined)
1444
+ ?? (hex === identities.reportSha256 ? reportBytes : undefined)
1445
+ ?? (hex === identities.reportEnvelopeSha256 ? envelopeBytes : undefined);
1446
+ if (bytes === undefined)
1447
+ refuse("record-integrity", "evidence-closure", `bundle does not carry referenced record ${digest}`);
1448
+ return bytes;
1449
+ };
1450
+ const ports = buildAssemblyPortsFromFacts({
1451
+ runRecord: run,
1452
+ cells: inScopeCells,
1453
+ owner: run.owner,
1454
+ runCancelled: assembly.header.runCancelled,
1455
+ receiptsByTaskDigest: receipts,
1456
+ resolveBytes,
1457
+ evaluatorKeys: () => evaluatorKeys,
1458
+ });
1459
+ const verifiedMatrix = await verifyMatrix(matrix, benchmark, run, ports, undefined, matrixBytes);
1460
+ if (!verifiedMatrix.ok)
1461
+ refuse("record-integrity", "matrix-rederivation", `${verifiedMatrix.check}: ${verifiedMatrix.detail}`);
1462
+ checks.push("matrix-rederivation");
1463
+ const verifiedReport = await verifyReport({ envelopeBytes, subjects: [matrixBytes], effectiveTime: trust.report.validFrom }, {
1464
+ ...buildMethodPortsFromResolver((digest) => {
1465
+ try {
1466
+ return resolveBytes(digest);
1467
+ }
1468
+ catch {
1469
+ return undefined;
1470
+ }
1471
+ }),
1472
+ trust: buildPublicReportTrustDeps({ report: trust.report, bindingEnvelopeBytes: trustBytes }),
1473
+ });
1474
+ if (!verifiedReport.ok)
1475
+ refuse("record-integrity", "report-verification", `${verifiedReport.check}: ${verifiedReport.detail}`);
1476
+ if (!equalBytes(canonicalJsonBytes(verifiedReport.record), reportBytes))
1477
+ refuse("record-integrity", "report-verification", "verified Report payload does not match report.json");
1478
+ checks.push("report-verification");
1479
+ assertClaimConsistency({
1480
+ claim,
1481
+ identities,
1482
+ benchmarkRecord: benchmark,
1483
+ runRecord: run,
1484
+ matrixRecord: matrix,
1485
+ reportRecord: verifiedReport.record,
1486
+ draftId: assembly.header.draftId,
1487
+ assurancePreset: assembly.header.assurancePreset,
1488
+ ...(inspectSelections.size > 0 && inspectEvaluationStrategy === "separate-log-verification"
1489
+ ? { additionalLimitations: INSPECT_SEPARATE_ASSURANCE_LIMITATIONS }
1490
+ : {}),
1491
+ ...(assembly.header.rehearsal === undefined ? {} : { rehearsal: assembly.header.rehearsal }),
1492
+ ...(claimAnchors === undefined ? {} : { anchors: claimAnchors }),
1493
+ });
1494
+ checks.push("claim-consistency");
1495
+ // Always present for this closure version, and never for any earlier one: an anchored bundle
1496
+ // whose anchors were stripped is a closure failure above, not a shorter check list here.
1497
+ if (isV6)
1498
+ checks.push("integrity-anchors");
1499
+ const dissentCellKeys = assembly.cells
1500
+ .filter((cell) => new Set(cell.verdicts.map((verdict) => verdict.verdict)).size > 1)
1501
+ .map((cell) => cell.cellKey)
1502
+ .sort();
1503
+ const comparison = qualification === undefined
1504
+ ? derivePublicComparison({
1505
+ benchmark,
1506
+ matrix,
1507
+ assemblyCells: assembly.cells,
1508
+ recordBytes: records,
1509
+ })
1510
+ : undefined;
1511
+ const assetFacts = {
1512
+ claim,
1513
+ matrix,
1514
+ report: verifiedReport.record,
1515
+ reportSha256: identities.reportSha256,
1516
+ matrixSha256: identities.matrixSha256,
1517
+ recordSha256s: checked.manifest.files.flatMap((file) => {
1518
+ const match = /^records\/([a-f0-9]{64})\.bin$/u.exec(file.path);
1519
+ return match === null ? [] : [match[1]];
1520
+ }),
1521
+ dissentCellKeys,
1522
+ };
1523
+ // Reader v1 remains able to authenticate bundles published before the
1524
+ // human comparison projection existed. A bundle must match one complete,
1525
+ // deterministic presentation profile; individual assets cannot be mixed.
1526
+ const legacyAssets = qualification === undefined ? buildPublicAssets(assetFacts) : undefined;
1527
+ const isLegacyPresentation = legacyAssets !== undefined
1528
+ && Object.entries(legacyAssets).every(([path, bytes]) => equalBytes(read(path), bytes));
1529
+ const expectedAssets = qualification !== undefined
1530
+ ? buildPublicAssets({ ...assetFacts, binaryQualification: binaryAssetQualification })
1531
+ : isLegacyPresentation
1532
+ ? legacyAssets
1533
+ : buildPublicAssets({ ...assetFacts, comparison });
1534
+ for (const [path, bytes] of Object.entries(expectedAssets)) {
1535
+ if (!equalBytes(read(path), bytes))
1536
+ refuse("record-integrity", path, `${path} is not the exact projection of verified public facts`);
1537
+ }
1538
+ const bundledInspectSelections = [...inspectSelections.entries()];
1539
+ const bundledInspectSelection = bundledInspectSelections.length === 1
1540
+ ? bundledInspectSelections[0]
1541
+ : undefined;
1542
+ const runtimeMethod = bundledInspectSelection === undefined
1543
+ ? undefined
1544
+ : describeInspectRuntimeMethod(bundledInspectSelection[1], bundledInspectSelection[0], run.policy.evaluation);
1545
+ return {
1546
+ verification: {
1547
+ format: checked.manifest.format,
1548
+ identity: checked.identity,
1549
+ checks,
1550
+ ...identities,
1551
+ ...(runtimeMethod === undefined ? {} : { runtimeMethod }),
1552
+ ...(anchorReport === undefined ? {} : { anchors: anchorReport }),
1553
+ ...(qualification === undefined ? {} : {
1554
+ qualification: {
1555
+ publicationGrade: qualification.publicationGrade,
1556
+ truthAdmission: qualification.truthAdmission,
1557
+ candidateClasses: qualification.candidateClasses,
1558
+ strata: qualification.strata,
1559
+ // Derived, not declared (spec §1.6 rule 4): a count that is a constant is not a count.
1560
+ armCount: qualification.arms.length,
1561
+ itemCount: qualification.items.length,
1562
+ exclusionCount: qualification.exclusions.length,
1563
+ },
1564
+ }),
1565
+ },
1566
+ ...(comparison === undefined ? {} : { comparison }),
1567
+ snapshot: checked,
1568
+ };
1569
+ }
1570
+ /** Verifies a bundle without exposing its authenticated byte snapshot. */
1571
+ export async function verifyPublicBundle(bundleDir, deps = {}) {
1572
+ return (await verifyPublicBundleSnapshot(bundleDir, deps)).verification;
1573
+ }