@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.
- package/README.md +39 -0
- package/dist/admission/application.d.ts +18 -0
- package/dist/admission/application.js +53 -0
- package/dist/admission/contracts.d.ts +506 -0
- package/dist/admission/contracts.js +459 -0
- package/dist/admission/index.d.ts +5 -0
- package/dist/admission/index.js +5 -0
- package/dist/admission/intake.d.ts +75 -0
- package/dist/admission/intake.js +70 -0
- package/dist/admission/result-evaluation.d.ts +18 -0
- package/dist/admission/result-evaluation.js +71 -0
- package/dist/admission/screening-sample.d.ts +56 -0
- package/dist/admission/screening-sample.js +126 -0
- package/dist/admission/verification.d.ts +102 -0
- package/dist/admission/verification.js +844 -0
- package/dist/anchor/check.d.ts +95 -0
- package/dist/anchor/check.js +146 -0
- package/dist/anchor/ports.d.ts +6 -0
- package/dist/anchor/ports.js +354 -0
- package/dist/assets.d.ts +33 -0
- package/dist/assets.js +885 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +20 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +205 -0
- package/dist/comparison.d.ts +64 -0
- package/dist/comparison.js +235 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/manifest.d.ts +54 -0
- package/dist/manifest.js +240 -0
- package/dist/materialize.d.ts +4 -0
- package/dist/materialize.js +13 -0
- package/dist/profile/admission-receipts.d.ts +11 -0
- package/dist/profile/admission-receipts.js +24 -0
- package/dist/profile/anchor-claims.d.ts +157 -0
- package/dist/profile/anchor-claims.js +330 -0
- package/dist/profile/artifacts.d.ts +261 -0
- package/dist/profile/artifacts.js +139 -0
- package/dist/profile/assembly-ports.d.ts +19 -0
- package/dist/profile/assembly-ports.js +42 -0
- package/dist/profile/binary-judge-manifest.d.ts +121 -0
- package/dist/profile/binary-judge-manifest.js +106 -0
- package/dist/profile/binary-qualification.d.ts +9 -0
- package/dist/profile/binary-qualification.js +58 -0
- package/dist/profile/branding-assets.d.ts +5 -0
- package/dist/profile/branding-assets.js +5 -0
- package/dist/profile/branding.d.ts +24 -0
- package/dist/profile/branding.js +14 -0
- package/dist/profile/claim-consistency.d.ts +26 -0
- package/dist/profile/claim-consistency.js +91 -0
- package/dist/profile/claim.d.ts +327 -0
- package/dist/profile/claim.js +742 -0
- package/dist/profile/errors.d.ts +49 -0
- package/dist/profile/errors.js +78 -0
- package/dist/profile/inspect-assurance.d.ts +16 -0
- package/dist/profile/inspect-assurance.js +30 -0
- package/dist/profile/inspect-disclosure.d.ts +28 -0
- package/dist/profile/inspect-disclosure.js +54 -0
- package/dist/profile/inspect-manifest.d.ts +706 -0
- package/dist/profile/inspect-manifest.js +397 -0
- package/dist/profile/isolation.d.ts +21 -0
- package/dist/profile/isolation.js +31 -0
- package/dist/profile/pinning-evidence.d.ts +41 -0
- package/dist/profile/pinning-evidence.js +43 -0
- package/dist/profile/ports.d.ts +3 -0
- package/dist/profile/ports.js +11 -0
- package/dist/profile/preview-log.d.ts +4 -0
- package/dist/profile/preview-log.js +3 -0
- package/dist/profile/run-results.d.ts +17 -0
- package/dist/profile/run-results.js +23 -0
- package/dist/profile/signing.d.ts +10 -0
- package/dist/profile/signing.js +46 -0
- package/dist/profile/trust.d.ts +13 -0
- package/dist/profile/trust.js +35 -0
- package/dist/profile/venue.d.ts +2 -0
- package/dist/profile/venue.js +2 -0
- package/dist/profile/verdict.d.ts +28 -0
- package/dist/profile/verdict.js +51 -0
- package/dist/reader-instructions.d.ts +41 -0
- package/dist/reader-instructions.js +52 -0
- package/dist/schema.d.ts +380 -0
- package/dist/schema.js +498 -0
- package/dist/verify.d.ts +72 -0
- package/dist/verify.js +1573 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +63 -3
- package/schemas/assembly-row.schema.json +194 -0
- package/schemas/bundle-manifest.schema.json +23 -0
- package/schemas/claim-package.schema.json +279 -0
- package/schemas/dsse-envelope.schema.json +25 -0
- package/schemas/evidence-catalog.schema.json +43 -0
- package/schemas/public-trust.schema.json +44 -0
- package/schemas/verdict-catalog.schema.json +27 -0
- package/scripts/external-verify.py +238 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { VERDICT_DSSE_PAYLOAD_TYPE, canonicalJsonBytes, compareCodeUnitStrings, parseBinaryJudgmentAnalysisContext, parseBinaryJudgmentInstrument, parseBinaryJudgmentLabelResolution, parseBinaryJudgmentPayload, recordDigest, sealBinaryJudgmentInstrument, } from "@jinn-network/task-execution-profiles";
|
|
4
|
+
import { parseExactDsseEnvelope } from "@jinn-network/trust-core";
|
|
5
|
+
import { BINARY_JUDGMENT_HUMAN_REVIEW_EVALUATION_SPEC_SEALED, HUMAN_REVIEW_FORM_SEALED, } from "./application.js";
|
|
6
|
+
import { BinaryJudgmentAdmissionManifestSchema, HUMAN_REVIEW_OPERATOR_ASSERTION_MEDIA_TYPE, HUMAN_REVIEW_PACKET_MEDIA_TYPE, HUMAN_REVIEW_RESPONSE_MEDIA_TYPE, HUMAN_REVIEW_REVEAL_RECEIPT_MEDIA_TYPE, HUMAN_REVIEW_ROSTER_MEDIA_TYPE, HUMAN_REVIEW_VISIBILITY_RECEIPT_MEDIA_TYPE, SCREENING_REVEAL_RECEIPT_MEDIA_TYPE, SCREENING_TABLE_MEDIA_TYPE, SCREENING_TABLE_V2_MEDIA_TYPE, HumanReviewOperatorAssertionSchema, HumanReviewPacketSchema, HumanReviewReplacementLedgerSchema, HumanReviewResponseSchema, HumanReviewRevealReceiptSchema, HumanReviewRosterSchema, HumanReviewVisibilityReceiptSchema, ScreeningRevealReceiptSchema, ScreeningTableSchema, ScreeningTableV2Schema, PromptedScreeningProcedureV1Schema, ScreeningPoolV1Schema, ScreeningSampleCommitmentV1Schema, PROMPTED_SCREENING_LIMITATIONS, PROMPTED_SCREENING_PROFILE, } from "./contracts.js";
|
|
7
|
+
import { readOrderedAdmissionVerdictMeasurements, readAdmissionVerdictEnvelope } from "./result-evaluation.js";
|
|
8
|
+
import { computeScreeningPoolDigest, computeScreeningSample, ScreeningSampleError } from "./screening-sample.js";
|
|
9
|
+
// Verbatim duplicate of `schema.ts`'s `BUNDLE_V4_ADMISSION_EVIDENCE_ROLES`, hand-kept in sync
|
|
10
|
+
// (spec §6.8a Group C).
|
|
11
|
+
export const BINARY_JUDGMENT_ADMISSION_RECORD_ROLES = [
|
|
12
|
+
"admission-manifest", "replacement-ledger", "source-item", "label-resolution",
|
|
13
|
+
"analysis-context", "human-review-evaluation-spec", "human-review-form",
|
|
14
|
+
"human-review-packet", "human-review-response", "human-review-verdict",
|
|
15
|
+
"reviewer-roster", "review-visibility-receipt", "review-reveal-receipt",
|
|
16
|
+
"operator-assertion",
|
|
17
|
+
// Appended after operator-assertion (spec §6.8a Group C, first bullet; packet P6).
|
|
18
|
+
"screening-table",
|
|
19
|
+
"screening-reveal-receipt",
|
|
20
|
+
// Appended only: these exact nested records are authenticated by the signed screening table.
|
|
21
|
+
"screening-instrument",
|
|
22
|
+
"screening-sampling-script",
|
|
23
|
+
"screening-raw-outputs",
|
|
24
|
+
"screening-prompt",
|
|
25
|
+
"screening-procedure",
|
|
26
|
+
"screening-pool",
|
|
27
|
+
"screening-sample-commitment",
|
|
28
|
+
"screening-transcript",
|
|
29
|
+
];
|
|
30
|
+
export const PROMPTED_SCREENING_CAPABILITY_BOUNDARY = {
|
|
31
|
+
status: "not-machine-verified",
|
|
32
|
+
machineVerified: ["sealed-pool", "sealed-sample", "signed-table", "operator-decisions", "artifact-integrity"],
|
|
33
|
+
notMachineVerified: PROMPTED_SCREENING_LIMITATIONS,
|
|
34
|
+
};
|
|
35
|
+
export class BinaryJudgmentAdmissionClosureError extends Error {
|
|
36
|
+
path;
|
|
37
|
+
constructor(path, detail, options) {
|
|
38
|
+
super(`${path}: ${detail}`, options);
|
|
39
|
+
this.name = "BinaryJudgmentAdmissionClosureError";
|
|
40
|
+
this.path = path;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function fail(path, detail, cause) {
|
|
44
|
+
throw new BinaryJudgmentAdmissionClosureError(path, detail, cause === undefined ? undefined : { cause });
|
|
45
|
+
}
|
|
46
|
+
function bytesEqual(left, right) {
|
|
47
|
+
return left.byteLength === right.byteLength && left.every((byte, index) => byte === right[index]);
|
|
48
|
+
}
|
|
49
|
+
function exactCanonical(schema, bytes, path) {
|
|
50
|
+
let json;
|
|
51
|
+
try {
|
|
52
|
+
json = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
53
|
+
}
|
|
54
|
+
catch (cause) {
|
|
55
|
+
fail(path, "record is not UTF-8 JSON", cause);
|
|
56
|
+
}
|
|
57
|
+
const parsed = schema.safeParse(json);
|
|
58
|
+
if (!parsed.success)
|
|
59
|
+
fail(path, parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; "));
|
|
60
|
+
if (!bytesEqual(bytes, canonicalJsonBytes(parsed.data)))
|
|
61
|
+
fail(path, "record is not in exact canonical JSON encoding");
|
|
62
|
+
return parsed.data;
|
|
63
|
+
}
|
|
64
|
+
function prefixed(value) {
|
|
65
|
+
return (value.startsWith("sha256:") ? value : `sha256:${value}`);
|
|
66
|
+
}
|
|
67
|
+
function bare(value) {
|
|
68
|
+
return value.startsWith("sha256:") ? value.slice("sha256:".length) : value;
|
|
69
|
+
}
|
|
70
|
+
function sameStrings(left, right) {
|
|
71
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
72
|
+
}
|
|
73
|
+
function resolve(state, digest, path, role) {
|
|
74
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(digest))
|
|
75
|
+
fail(path, "record digest is not canonical sha256");
|
|
76
|
+
const exactDigest = digest;
|
|
77
|
+
let bytes;
|
|
78
|
+
try {
|
|
79
|
+
bytes = state.ports.resolveExactRecord(exactDigest);
|
|
80
|
+
}
|
|
81
|
+
catch (cause) {
|
|
82
|
+
fail(path, `cannot resolve ${digest}`, cause);
|
|
83
|
+
}
|
|
84
|
+
if (recordDigest(bytes) !== digest)
|
|
85
|
+
fail(path, `resolved bytes do not hash to ${digest}`);
|
|
86
|
+
state.reachable.add(exactDigest);
|
|
87
|
+
const roles = state.roles.get(exactDigest) ?? new Set();
|
|
88
|
+
roles.add(role);
|
|
89
|
+
state.roles.set(exactDigest, roles);
|
|
90
|
+
return bytes;
|
|
91
|
+
}
|
|
92
|
+
function parseProfileRecord(state, digest, path, parse, role) {
|
|
93
|
+
const bytes = resolve(state, digest, path, role);
|
|
94
|
+
let value;
|
|
95
|
+
try {
|
|
96
|
+
value = parse(bytes);
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
fail(path, "record does not satisfy its registered schema", cause);
|
|
100
|
+
}
|
|
101
|
+
if (!bytesEqual(bytes, canonicalJsonBytes(value)))
|
|
102
|
+
fail(path, "record is not in exact canonical JSON encoding");
|
|
103
|
+
return { value, bytes };
|
|
104
|
+
}
|
|
105
|
+
function verifyFrozenHumanReviewSpec(state, path) {
|
|
106
|
+
const specBytes = resolve(state, BINARY_JUDGMENT_HUMAN_REVIEW_EVALUATION_SPEC_SEALED.digest, `${path}.evaluationSpecification`, "human-review-evaluation-spec");
|
|
107
|
+
if (!bytesEqual(specBytes, BINARY_JUDGMENT_HUMAN_REVIEW_EVALUATION_SPEC_SEALED.bytes)) {
|
|
108
|
+
fail(path, "frozen human-review EvaluationSpec bytes do not match the registered contract");
|
|
109
|
+
}
|
|
110
|
+
const formBytes = resolve(state, HUMAN_REVIEW_FORM_SEALED.digest, `${path}.reviewForm`, "human-review-form");
|
|
111
|
+
if (!bytesEqual(formBytes, HUMAN_REVIEW_FORM_SEALED.bytes)) {
|
|
112
|
+
fail(path, "frozen human-review form bytes do not match the registered contract");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function verifyReview(state, verdictSha256, expectedItemSha256, path) {
|
|
116
|
+
const envelopeBytes = resolve(state, verdictSha256, path, "human-review-verdict");
|
|
117
|
+
let envelope;
|
|
118
|
+
try {
|
|
119
|
+
envelope = parseExactDsseEnvelope(envelopeBytes);
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
fail(path, "review verdict is not an exact compact DSSE envelope", cause);
|
|
123
|
+
}
|
|
124
|
+
if (envelope.payloadType !== VERDICT_DSSE_PAYLOAD_TYPE || envelope.signatures.length !== 1) {
|
|
125
|
+
fail(path, "review verdict must be one Result Evaluation envelope with one signature");
|
|
126
|
+
}
|
|
127
|
+
let statement;
|
|
128
|
+
try {
|
|
129
|
+
statement = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(envelope.payloadBytes));
|
|
130
|
+
}
|
|
131
|
+
catch (cause) {
|
|
132
|
+
fail(path, "Result Evaluation payload is not UTF-8 JSON", cause);
|
|
133
|
+
}
|
|
134
|
+
if (!bytesEqual(canonicalJsonBytes(statement), envelope.payloadBytes))
|
|
135
|
+
fail(path, "Result Evaluation payload is not exact canonical JSON");
|
|
136
|
+
let view;
|
|
137
|
+
let ordered;
|
|
138
|
+
try {
|
|
139
|
+
view = readAdmissionVerdictEnvelope(envelopeBytes);
|
|
140
|
+
ordered = readOrderedAdmissionVerdictMeasurements(envelopeBytes);
|
|
141
|
+
}
|
|
142
|
+
catch (cause) {
|
|
143
|
+
fail(path, "Result Evaluation payload is outside the closed verdict grammar", cause);
|
|
144
|
+
}
|
|
145
|
+
const signature = envelope.signatures[0];
|
|
146
|
+
const signatureKeyId = signature.keyid;
|
|
147
|
+
if (typeof signatureKeyId !== "string" || !state.ports.verifyReviewerSignature({ envelopeBytes, evaluatorId: view.evaluatorId, keyId: signatureKeyId })) {
|
|
148
|
+
fail(path, "reviewer signature or evaluator/key binding is invalid");
|
|
149
|
+
}
|
|
150
|
+
if (view.evaluationSpecificationSha256 !== bare(BINARY_JUDGMENT_HUMAN_REVIEW_EVALUATION_SPEC_SEALED.digest)) {
|
|
151
|
+
fail(path, "review names a different human-review EvaluationSpec");
|
|
152
|
+
}
|
|
153
|
+
verifyFrozenHumanReviewSpec(state, path);
|
|
154
|
+
const expectedNames = ["truthLabel", "reviewComplete", "reviewPacketSha256", "visibilityReceiptSha256", "responseSha256"];
|
|
155
|
+
if (ordered.length !== expectedNames.length || ordered.some((entry, index) => entry.name !== expectedNames[index])) {
|
|
156
|
+
fail(path, "review measurements do not match the frozen human-review EvaluationSpec");
|
|
157
|
+
}
|
|
158
|
+
const [labelRaw, completeRaw, packetRaw, visibilityRaw, responseRaw] = ordered.map((entry) => entry.value);
|
|
159
|
+
const labelResult = z.enum(["CORRECT", "WRONG", "indeterminate"]).safeParse(labelRaw);
|
|
160
|
+
if (!labelResult.success || typeof completeRaw !== "boolean" || typeof packetRaw !== "string" || typeof visibilityRaw !== "string" || typeof responseRaw !== "string") {
|
|
161
|
+
fail(path, "review measurement values are invalid");
|
|
162
|
+
}
|
|
163
|
+
const packetSha256 = prefixed(packetRaw);
|
|
164
|
+
const visibilityReceiptSha256 = prefixed(visibilityRaw);
|
|
165
|
+
const responseSha256 = prefixed(responseRaw);
|
|
166
|
+
const packet = exactCanonical(HumanReviewPacketSchema, resolve(state, packetSha256, `${path}.packet`, "human-review-packet"), `${path}.packet`);
|
|
167
|
+
const visibility = exactCanonical(HumanReviewVisibilityReceiptSchema, resolve(state, visibilityReceiptSha256, `${path}.visibility`, "review-visibility-receipt"), `${path}.visibility`);
|
|
168
|
+
const response = exactCanonical(HumanReviewResponseSchema, resolve(state, responseSha256, `${path}.response`, "human-review-response"), `${path}.response`);
|
|
169
|
+
const itemBytes = resolve(state, expectedItemSha256, `${path}.item`, "source-item");
|
|
170
|
+
const item = parseProfileRecord(state, expectedItemSha256, `${path}.item`, (bytes) => parseBinaryJudgmentPayload(bytes), "source-item").value;
|
|
171
|
+
if (!bytesEqual(itemBytes, canonicalJsonBytes(packet.item)) || packet.item.itemId !== item.itemId) {
|
|
172
|
+
fail(path, "packet embedded item does not equal the exact referenced item record");
|
|
173
|
+
}
|
|
174
|
+
const subjects = statement.subject;
|
|
175
|
+
const predicate = statement.predicate;
|
|
176
|
+
if (!Array.isArray(subjects) || subjects.length !== 2 || typeof predicate !== "object" || predicate === null || Array.isArray(predicate)) {
|
|
177
|
+
fail(path, "Result Evaluation subjects or predicate are malformed");
|
|
178
|
+
}
|
|
179
|
+
const subjectView = subjects;
|
|
180
|
+
const predicateView = predicate;
|
|
181
|
+
const expectedVerdict = !completeRaw || labelResult.data === "indeterminate"
|
|
182
|
+
? "inconclusive"
|
|
183
|
+
: labelResult.data === "CORRECT" ? "pass" : "fail";
|
|
184
|
+
const expectedEvidence = [
|
|
185
|
+
["human-review-packet", bare(packetSha256), HUMAN_REVIEW_PACKET_MEDIA_TYPE],
|
|
186
|
+
["visibility-receipt", bare(visibilityReceiptSha256), HUMAN_REVIEW_VISIBILITY_RECEIPT_MEDIA_TYPE],
|
|
187
|
+
["human-review-response", bare(responseSha256), HUMAN_REVIEW_RESPONSE_MEDIA_TYPE],
|
|
188
|
+
];
|
|
189
|
+
const evidenceMatches = Array.isArray(predicateView.evidence)
|
|
190
|
+
&& predicateView.evidence.length === expectedEvidence.length
|
|
191
|
+
&& predicateView.evidence.every((raw, index) => {
|
|
192
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
193
|
+
return false;
|
|
194
|
+
const entry = raw;
|
|
195
|
+
const expected = expectedEvidence[index];
|
|
196
|
+
return entry.name === expected[0]
|
|
197
|
+
&& entry.digest?.sha256 === expected[1]
|
|
198
|
+
&& entry.mediaType === expected[2]
|
|
199
|
+
&& Object.keys(entry).every((key) => ["name", "digest", "mediaType"].includes(key));
|
|
200
|
+
});
|
|
201
|
+
if (packet.itemSha256 !== expectedItemSha256
|
|
202
|
+
|| recordDigest(canonicalJsonBytes(packet.item)) !== expectedItemSha256
|
|
203
|
+
|| packet.evaluationSpecSha256 !== BINARY_JUDGMENT_HUMAN_REVIEW_EVALUATION_SPEC_SEALED.digest
|
|
204
|
+
|| packet.reviewerId !== view.evaluatorId
|
|
205
|
+
|| visibility.packetSha256 !== packetSha256
|
|
206
|
+
|| visibility.itemSha256 !== expectedItemSha256
|
|
207
|
+
|| visibility.reviewerId !== view.evaluatorId
|
|
208
|
+
|| response.packetSha256 !== packetSha256
|
|
209
|
+
|| response.visibilityReceiptSha256 !== visibilityReceiptSha256
|
|
210
|
+
|| response.itemSha256 !== expectedItemSha256
|
|
211
|
+
|| response.evaluatorId !== view.evaluatorId
|
|
212
|
+
|| response.label !== labelResult.data
|
|
213
|
+
|| response.complete !== completeRaw
|
|
214
|
+
|| response.completedAt !== view.evaluatedAt
|
|
215
|
+
|| Date.parse(response.completedAt) < Date.parse(visibility.issuedAt)
|
|
216
|
+
|| subjectView[0]?.name !== "binary-judgment-item"
|
|
217
|
+
|| subjectView[0]?.digest?.sha256 !== bare(expectedItemSha256)
|
|
218
|
+
|| Object.keys(subjectView[0] ?? {}).some((key) => !["name", "digest"].includes(key))
|
|
219
|
+
|| subjectView[1]?.name !== "human-review-response"
|
|
220
|
+
|| subjectView[1]?.digest?.sha256 !== bare(responseSha256)
|
|
221
|
+
|| subjectView[1]?.mediaType !== HUMAN_REVIEW_RESPONSE_MEDIA_TYPE
|
|
222
|
+
|| Object.keys(subjectView[1] ?? {}).some((key) => !["name", "digest", "mediaType"].includes(key))
|
|
223
|
+
|| predicateView.taskSubject !== "binary-judgment-item"
|
|
224
|
+
|| !Array.isArray(predicateView.resultSubjects)
|
|
225
|
+
|| predicateView.resultSubjects.length !== 1
|
|
226
|
+
|| predicateView.resultSubjects[0] !== "human-review-response"
|
|
227
|
+
|| predicateView.evaluator?.id !== view.evaluatorId
|
|
228
|
+
|| predicateView.evaluator?.["network.jinn.reviewer.keyid"] !== signatureKeyId
|
|
229
|
+
|| predicateView.verdict !== expectedVerdict
|
|
230
|
+
|| predicateView.evaluatedAt !== response.completedAt
|
|
231
|
+
|| !evidenceMatches
|
|
232
|
+
|| !Array.isArray(predicateView.limitations)
|
|
233
|
+
|| !sameStrings(predicateView.limitations, ["reviewer-person-distinctness-is-roster-attested"])
|
|
234
|
+
|| view.evidence?.length !== 3)
|
|
235
|
+
fail(path, "Result Evaluation packet/response/visibility/item/evaluator joins do not close");
|
|
236
|
+
return {
|
|
237
|
+
evaluatorId: view.evaluatorId,
|
|
238
|
+
keyId: signatureKeyId,
|
|
239
|
+
label: labelResult.data,
|
|
240
|
+
complete: completeRaw,
|
|
241
|
+
completedAt: response.completedAt,
|
|
242
|
+
itemId: item.itemId,
|
|
243
|
+
packetSha256,
|
|
244
|
+
visibilityReceiptSha256,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function authorityPayload(state, options) {
|
|
248
|
+
const { digest, mediaType, role, evidenceRole, schema, path } = options;
|
|
249
|
+
const envelopeBytes = resolve(state, digest, path, evidenceRole);
|
|
250
|
+
let envelope;
|
|
251
|
+
try {
|
|
252
|
+
envelope = parseExactDsseEnvelope(envelopeBytes);
|
|
253
|
+
}
|
|
254
|
+
catch (cause) {
|
|
255
|
+
fail(path, "authority evidence is not an exact compact DSSE envelope", cause);
|
|
256
|
+
}
|
|
257
|
+
const allowedMediaTypes = typeof mediaType === "string" ? [mediaType] : mediaType;
|
|
258
|
+
if (!allowedMediaTypes.includes(envelope.payloadType) || envelope.signatures.length !== 1)
|
|
259
|
+
fail(path, "authority evidence has the wrong payload type or signature count");
|
|
260
|
+
const signature = envelope.signatures[0];
|
|
261
|
+
const signatureKeyId = signature.keyid;
|
|
262
|
+
if (typeof signatureKeyId !== "string" || !state.ports.verifyAuthoritySignature({ envelopeBytes, keyId: signatureKeyId, role })) {
|
|
263
|
+
fail(path, `authority signature is invalid for role ${role}`);
|
|
264
|
+
}
|
|
265
|
+
return { value: exactCanonical(schema, envelope.payloadBytes, `${path}.payload`), keyId: signatureKeyId, mediaType: envelope.payloadType };
|
|
266
|
+
}
|
|
267
|
+
function verifyHumanEvidence(state, refs, expectedItemSha256, expectedDraftId, admittedAt, path) {
|
|
268
|
+
const reviews = refs.reviewVerdictSha256s.map((digest, index) => verifyReview(state, digest, expectedItemSha256, `${path}.reviews.${index}`));
|
|
269
|
+
if (reviews[0].evaluatorId === reviews[1].evaluatorId || reviews[0].keyId === reviews[1].keyId) {
|
|
270
|
+
fail(path, "reviewers must have distinct evaluator identities and keys");
|
|
271
|
+
}
|
|
272
|
+
if (reviews[0].itemId !== reviews[1].itemId)
|
|
273
|
+
fail(path, "review packets disagree on itemId");
|
|
274
|
+
const visibilityDigests = reviews.map((review) => review.visibilityReceiptSha256).sort(compareCodeUnitStrings);
|
|
275
|
+
if (!sameStrings(visibilityDigests, refs.visibilityReceiptSha256s))
|
|
276
|
+
fail(path, "declared visibility receipt coverage differs from signed reviews");
|
|
277
|
+
const rosterEvidence = authorityPayload(state, {
|
|
278
|
+
digest: refs.reviewerRosterSha256,
|
|
279
|
+
mediaType: HUMAN_REVIEW_ROSTER_MEDIA_TYPE,
|
|
280
|
+
role: "roster-attestor",
|
|
281
|
+
evidenceRole: "reviewer-roster",
|
|
282
|
+
schema: HumanReviewRosterSchema,
|
|
283
|
+
path: `${path}.roster`,
|
|
284
|
+
});
|
|
285
|
+
const roster = rosterEvidence.value;
|
|
286
|
+
if (roster.attestorRole !== "roster-attestor" || roster.attestorKeyId !== rosterEvidence.keyId || roster.itemSha256 !== expectedItemSha256) {
|
|
287
|
+
fail(path, "roster authority role/key/item binding is invalid");
|
|
288
|
+
}
|
|
289
|
+
const declarations = [...roster.reviewers];
|
|
290
|
+
if (compareCodeUnitStrings(declarations[0].evaluatorId, declarations[1].evaluatorId) >= 0)
|
|
291
|
+
fail(path, "roster reviewers are not sorted and unique");
|
|
292
|
+
const reviewByEvaluator = new Map(reviews.map((review) => [review.evaluatorId, review]));
|
|
293
|
+
if (declarations.some((declaration) => reviewByEvaluator.get(declaration.evaluatorId)?.keyId !== declaration.keyId)) {
|
|
294
|
+
fail(path, "roster does not bind the exact reviewer evaluator/key pairs");
|
|
295
|
+
}
|
|
296
|
+
if (declarations[0].personId === declarations[1].personId || declarations.some((declaration) => declaration.conflicts.length > 0)) {
|
|
297
|
+
fail(path, "roster does not attest distinct conflict-free people");
|
|
298
|
+
}
|
|
299
|
+
const revealEvidence = authorityPayload(state, {
|
|
300
|
+
digest: refs.revealReceiptSha256,
|
|
301
|
+
mediaType: HUMAN_REVIEW_REVEAL_RECEIPT_MEDIA_TYPE,
|
|
302
|
+
role: "truth-reveal-attestor",
|
|
303
|
+
evidenceRole: "review-reveal-receipt",
|
|
304
|
+
schema: HumanReviewRevealReceiptSchema,
|
|
305
|
+
path: `${path}.reveal`,
|
|
306
|
+
});
|
|
307
|
+
const reveal = revealEvidence.value;
|
|
308
|
+
if (reveal.attestorRole !== "truth-reveal-attestor"
|
|
309
|
+
|| reveal.attestorKeyId !== revealEvidence.keyId
|
|
310
|
+
|| revealEvidence.keyId !== rosterEvidence.keyId
|
|
311
|
+
|| reveal.draftId !== expectedDraftId
|
|
312
|
+
|| reveal.itemSha256 !== expectedItemSha256
|
|
313
|
+
|| reveal.judgeExecutionState !== "not-started"
|
|
314
|
+
|| reveal.truthFrozenAt !== admittedAt
|
|
315
|
+
|| reviews.some((review) => Date.parse(review.completedAt) > Date.parse(reveal.truthFrozenAt)))
|
|
316
|
+
fail(path, "signed reveal does not follow both completed reviews under the same authority");
|
|
317
|
+
return { reviews, itemId: reviews[0].itemId };
|
|
318
|
+
}
|
|
319
|
+
function expectedExclusionReason(reviews) {
|
|
320
|
+
if (reviews.some((review) => !review.complete))
|
|
321
|
+
return "review-incomplete";
|
|
322
|
+
if (reviews.some((review) => review.label === "indeterminate"))
|
|
323
|
+
return "review-indeterminate";
|
|
324
|
+
if (reviews[0].label !== reviews[1].label)
|
|
325
|
+
return "review-disagreement";
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
function screeningRowAgreed(row) {
|
|
329
|
+
return row.screeningVerdict !== "UNSURE"
|
|
330
|
+
&& row.screeningVerdict !== "indeterminate"
|
|
331
|
+
&& row.screeningVerdict === row.intendedLabel;
|
|
332
|
+
}
|
|
333
|
+
function screeningRowChecked(row) {
|
|
334
|
+
return "ritsuDecision" in row ? row.ritsuDecision.checked : row.handChecked;
|
|
335
|
+
}
|
|
336
|
+
function screeningRowHandVerdict(row) {
|
|
337
|
+
if ("ritsuDecision" in row)
|
|
338
|
+
return row.ritsuDecision.checked ? row.ritsuDecision.verdict : undefined;
|
|
339
|
+
return row.handVerdict;
|
|
340
|
+
}
|
|
341
|
+
function screeningRowAdmitted(row) {
|
|
342
|
+
return screeningRowChecked(row) ? screeningRowHandVerdict(row) === "confirm" : screeningRowAgreed(row);
|
|
343
|
+
}
|
|
344
|
+
function expectedScreeningExclusionReason(row) {
|
|
345
|
+
const agreed = screeningRowAgreed(row);
|
|
346
|
+
const admitted = screeningRowAdmitted(row);
|
|
347
|
+
if (admitted)
|
|
348
|
+
return undefined;
|
|
349
|
+
if (!agreed)
|
|
350
|
+
return row.screeningVerdict === "indeterminate" || row.screeningVerdict === "UNSURE" ? "screening-indeterminate" : "screening-disagreement";
|
|
351
|
+
return "screening-hand-excluded";
|
|
352
|
+
}
|
|
353
|
+
/** The reviewer Result Evaluation verifier shared by admission construction and closure replay. */
|
|
354
|
+
export function verifyBinaryJudgmentReviewerResult(input, ports) {
|
|
355
|
+
return verifyReview({ ports, reachable: new Set(), roles: new Map() }, input.verdictSha256, input.expectedItemSha256, "reviewVerdictSha256");
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Portable, fail-closed replay of the entire binary-judgment admission graph. The caller supplies
|
|
359
|
+
* immutable record resolution and role-separated trust; no candidate truth is caller-authored.
|
|
360
|
+
*/
|
|
361
|
+
export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
|
|
362
|
+
const state = { ports, reachable: new Set(), roles: new Map() };
|
|
363
|
+
const manifest = exactCanonical(BinaryJudgmentAdmissionManifestSchema, resolve(state, input.admissionManifestSha256, "admissionManifestSha256", "admission-manifest"), "admissionManifest");
|
|
364
|
+
if (manifest.draftId !== input.expectedDraftId)
|
|
365
|
+
fail("admissionManifest.draftId", "manifest belongs to a different draft");
|
|
366
|
+
const ledger = exactCanonical(HumanReviewReplacementLedgerSchema, resolve(state, manifest.replacementLedgerSha256, "admissionManifest.replacementLedgerSha256", "replacement-ledger"), "replacementLedger");
|
|
367
|
+
if (ledger.draftId !== input.expectedDraftId || ledger.sealedAt !== manifest.admittedAt)
|
|
368
|
+
fail("replacementLedger", "ledger draft/time does not bind the manifest");
|
|
369
|
+
// Screened-operator-sampled admission (spec §6.4, §6.5; packet P6 item A): the table and S2's
|
|
370
|
+
// `screening-sample/1` recomputation are resolved and checked ONCE per bank, ahead of the
|
|
371
|
+
// resolutions and ledger loops below, which read `screeningRowsByItem` per item. Checks (1)-(3)
|
|
372
|
+
// run here, over `rows` alone; check (0)'s coverage half runs after both loops, once
|
|
373
|
+
// `acceptedItems`/`excludedItems` (the closure's own reconstruction of the frozen bank's
|
|
374
|
+
// candidate pool -- see the coverage check below) are fully known; check (4) runs per item in
|
|
375
|
+
// both loops.
|
|
376
|
+
let screeningTable;
|
|
377
|
+
let screeningRowsByItem = new Map();
|
|
378
|
+
let screeningSample = new Set();
|
|
379
|
+
let screeningSampleAgreementRate;
|
|
380
|
+
let screeningInstrumentSha256;
|
|
381
|
+
let promptedScreeningProcedureSha256;
|
|
382
|
+
let promptedScreeningPool;
|
|
383
|
+
let promptedCommittedSample;
|
|
384
|
+
let screeningTableAuthorityKeyId;
|
|
385
|
+
if (manifest.truthAdmission === "screened-operator-sampled") {
|
|
386
|
+
if (manifest.screeningTableSha256 === undefined) {
|
|
387
|
+
fail("admissionManifest.screeningTableSha256", "screened admission manifest carries no screening table reference");
|
|
388
|
+
}
|
|
389
|
+
// The table is signed ONCE, DSSE-sealed on the same path as the other admission records
|
|
390
|
+
// (spec §6.3, final sentence, verbatim) -- this is what makes §6.9's dropping of 240 per-item
|
|
391
|
+
// signatures sound ("one key signing 240 rows separately proves nothing beyond one signature
|
|
392
|
+
// on the whole table"). The table schema carries no attestorRole field (unlike the roster and
|
|
393
|
+
// reveal receipt), but `sealRoleEvidence` (core/src/operations/human-review.ts:218-233) DSSE-
|
|
394
|
+
// wraps arbitrary bytes under any payload type regardless of the payload's own shape, so
|
|
395
|
+
// `screeningTableSha256` names the DSSE ENVELOPE digest, exactly like `reviewerRosterSha256`
|
|
396
|
+
// and `revealReceiptSha256` already do -- not the bare canonical table bytes. The screened
|
|
397
|
+
// branch's sole legal authority set is `["truth-reveal-attestor"]` alone (§6.8a Group C), so
|
|
398
|
+
// that is the one key that signs both the bank-scoped table and the bank-scoped reveal
|
|
399
|
+
// receipt: "signed once per bank" as D1 describes.
|
|
400
|
+
const tableEvidence = authorityPayload(state, {
|
|
401
|
+
digest: manifest.screeningTableSha256,
|
|
402
|
+
mediaType: [SCREENING_TABLE_MEDIA_TYPE, SCREENING_TABLE_V2_MEDIA_TYPE],
|
|
403
|
+
role: "truth-reveal-attestor",
|
|
404
|
+
evidenceRole: "screening-table",
|
|
405
|
+
schema: z.union([ScreeningTableSchema, ScreeningTableV2Schema]),
|
|
406
|
+
path: "admissionManifest.screeningTableSha256",
|
|
407
|
+
});
|
|
408
|
+
screeningTable = tableEvidence.value;
|
|
409
|
+
screeningTableAuthorityKeyId = tableEvidence.keyId;
|
|
410
|
+
if (screeningTable.draftId !== input.expectedDraftId || screeningTable.sealedAt !== manifest.admittedAt) {
|
|
411
|
+
fail("screeningTable", "table draft/time does not bind the manifest");
|
|
412
|
+
}
|
|
413
|
+
let sampleSeed;
|
|
414
|
+
let sampleSize;
|
|
415
|
+
let sampleIdentities;
|
|
416
|
+
if (screeningTable.protocol === "https://spec.jinn.network/binary-judgment/screening-table/v1") {
|
|
417
|
+
if (tableEvidence.mediaType !== SCREENING_TABLE_MEDIA_TYPE)
|
|
418
|
+
fail("screeningTable", "v1 table protocol must use the v1 media type");
|
|
419
|
+
const screeningInstrument = parseProfileRecord(state, screeningTable.screeningInstrumentSha256, "screeningTable.screeningInstrumentSha256", parseBinaryJudgmentInstrument, "screening-instrument");
|
|
420
|
+
const resealedScreeningInstrument = sealBinaryJudgmentInstrument(screeningInstrument.value);
|
|
421
|
+
if (resealedScreeningInstrument.digest !== screeningTable.screeningInstrumentSha256 || !bytesEqual(resealedScreeningInstrument.bytes, screeningInstrument.bytes)) {
|
|
422
|
+
fail("screeningTable.screeningInstrumentSha256", "instrument does not reseal to its table commitment");
|
|
423
|
+
}
|
|
424
|
+
screeningInstrumentSha256 = screeningTable.screeningInstrumentSha256;
|
|
425
|
+
resolve(state, screeningTable.samplingScriptSha256, "screeningTable.samplingScriptSha256", "screening-sampling-script");
|
|
426
|
+
resolve(state, screeningTable.rawOutputsSha256, "screeningTable.rawOutputsSha256", "screening-raw-outputs");
|
|
427
|
+
sampleSeed = screeningTable.sampleSeed;
|
|
428
|
+
sampleSize = screeningTable.sampleSize;
|
|
429
|
+
sampleIdentities = screeningTable.rows.map((row) => row.itemSha256);
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
if (tableEvidence.mediaType !== SCREENING_TABLE_V2_MEDIA_TYPE)
|
|
433
|
+
fail("screeningTable", "v2 table protocol must use the v2 media type");
|
|
434
|
+
const procedure = exactCanonical(PromptedScreeningProcedureV1Schema, resolve(state, screeningTable.procedureSha256, "screeningTable.procedureSha256", "screening-procedure"), "screeningProcedure");
|
|
435
|
+
const pool = exactCanonical(ScreeningPoolV1Schema, resolve(state, screeningTable.poolSha256, "screeningTable.poolSha256", "screening-pool"), "screeningPool");
|
|
436
|
+
const commitment = exactCanonical(ScreeningSampleCommitmentV1Schema, resolve(state, screeningTable.sampleCommitmentSha256, "screeningTable.sampleCommitmentSha256", "screening-sample-commitment"), "screeningSampleCommitment");
|
|
437
|
+
resolve(state, screeningTable.coordinatorPromptSha256, "screeningTable.coordinatorPromptSha256", "screening-prompt");
|
|
438
|
+
resolve(state, screeningTable.samplingScriptSha256, "screeningTable.samplingScriptSha256", "screening-sampling-script");
|
|
439
|
+
resolve(state, screeningTable.transcriptSha256, "screeningTable.transcriptSha256", "screening-transcript");
|
|
440
|
+
if (procedure.coordinatorPromptSha256 !== screeningTable.coordinatorPromptSha256
|
|
441
|
+
|| procedure.transcriptSha256 !== screeningTable.transcriptSha256
|
|
442
|
+
|| pool.draftId !== input.expectedDraftId
|
|
443
|
+
|| commitment.draftId !== input.expectedDraftId
|
|
444
|
+
|| commitment.poolSha256 !== screeningTable.poolSha256
|
|
445
|
+
|| commitment.poolIdentityCommitmentSha256 !== pool.identityCommitmentSha256
|
|
446
|
+
|| Date.parse(procedure.sealedAt) > Date.parse(screeningTable.sealedAt)
|
|
447
|
+
|| Date.parse(pool.sealedAt) > Date.parse(commitment.committedAt)
|
|
448
|
+
|| Date.parse(commitment.committedAt) > Date.parse(screeningTable.sealedAt))
|
|
449
|
+
fail("screeningTable", "prompted procedure, prompt, transcript, pool, commitment, draft, or timestamps do not join");
|
|
450
|
+
const poolIdentities = pool.items.map((item) => item.itemSha256);
|
|
451
|
+
if (computeScreeningPoolDigest(poolIdentities) !== pool.identityCommitmentSha256)
|
|
452
|
+
fail("screeningPool.identityCommitmentSha256", "identity commitment does not match the sorted unique pool identities");
|
|
453
|
+
const itemIds = new Set();
|
|
454
|
+
for (const [index, poolItem] of pool.items.entries()) {
|
|
455
|
+
const item = parseProfileRecord(state, poolItem.itemSha256, `screeningPool.items.${index}.itemSha256`, (bytes) => parseBinaryJudgmentPayload(bytes), "source-item").value;
|
|
456
|
+
if (itemIds.has(item.itemId))
|
|
457
|
+
fail("screeningPool.items", `duplicate source itemId ${item.itemId}`);
|
|
458
|
+
itemIds.add(item.itemId);
|
|
459
|
+
}
|
|
460
|
+
const rowIdentities = screeningTable.rows.map((row) => row.itemSha256);
|
|
461
|
+
const sortedPoolIdentities = [...poolIdentities].sort(compareCodeUnitStrings);
|
|
462
|
+
if (!sameStrings(rowIdentities, sortedPoolIdentities))
|
|
463
|
+
fail("screeningTable.rows", "v2 table must cover all 664 pool identities in sorted order");
|
|
464
|
+
const poolByItem = new Map(pool.items.map((item) => [item.itemSha256, item]));
|
|
465
|
+
for (const row of screeningTable.rows) {
|
|
466
|
+
const poolItem = poolByItem.get(row.itemSha256);
|
|
467
|
+
if (row.intendedLabel !== poolItem.intendedLabel)
|
|
468
|
+
fail("screeningTable.rows", `row ${row.itemSha256} intended label differs from the sealed pool`);
|
|
469
|
+
if (row.ritsuDecision.checked && (Date.parse(row.ritsuDecision.decidedAt) < Date.parse(commitment.committedAt)
|
|
470
|
+
|| Date.parse(row.ritsuDecision.decidedAt) > Date.parse(screeningTable.sealedAt)))
|
|
471
|
+
fail("screeningTable.rows", `row ${row.itemSha256} has a Ritsu decision outside the committed-sample-to-table interval`);
|
|
472
|
+
}
|
|
473
|
+
sampleSeed = commitment.sampleSeed;
|
|
474
|
+
sampleSize = commitment.sampleSize;
|
|
475
|
+
sampleIdentities = poolIdentities;
|
|
476
|
+
promptedScreeningProcedureSha256 = screeningTable.procedureSha256;
|
|
477
|
+
promptedScreeningPool = pool;
|
|
478
|
+
promptedCommittedSample = commitment.sampleItemSha256s;
|
|
479
|
+
}
|
|
480
|
+
screeningRowsByItem = new Map(screeningTable.rows.map((row) => [row.itemSha256, row]));
|
|
481
|
+
// (1) Sample membership: recomputed by screening-sample/1, never by executing the sealed
|
|
482
|
+
// script (R-4). The recomputation is authoritative for checks (2) and (3) below -- there is
|
|
483
|
+
// no separate "compare against the operator's script" step, because the script is never run.
|
|
484
|
+
let sampleResult;
|
|
485
|
+
try {
|
|
486
|
+
sampleResult = computeScreeningSample({
|
|
487
|
+
itemSha256s: sampleIdentities,
|
|
488
|
+
sampleSeed,
|
|
489
|
+
sampleSize,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (cause) {
|
|
493
|
+
fail("admissionManifest.screeningTableSha256", cause instanceof ScreeningSampleError
|
|
494
|
+
? `sample recomputation refused its own input: ${cause.message}`
|
|
495
|
+
: "sample recomputation (screening-sample/1) failed", cause);
|
|
496
|
+
}
|
|
497
|
+
screeningSample = new Set(sampleResult.sample);
|
|
498
|
+
if (promptedCommittedSample !== undefined && !sameStrings([...sampleResult.sample].sort(compareCodeUnitStrings), promptedCommittedSample))
|
|
499
|
+
fail("screeningSampleCommitment.sampleItemSha256s", "public sample membership differs from screening-sample/1 recomputation");
|
|
500
|
+
// (2) Required hand checks: flagged := rows where NOT agreed. Every row in flagged ∪ sample
|
|
501
|
+
// must carry handChecked === true.
|
|
502
|
+
for (const row of screeningTable.rows) {
|
|
503
|
+
const flagged = !screeningRowAgreed(row);
|
|
504
|
+
if ((flagged || screeningSample.has(row.itemSha256)) && !screeningRowChecked(row)) {
|
|
505
|
+
fail("screeningTable.rows", `row ${row.itemSha256} is flagged or sampled but was never hand-checked`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// (3) Sample agreement rate -- the one definition (spec §6.5(3)), symmetric on purpose. Every
|
|
509
|
+
// row in `screeningSample` is hand-checked by check (2) above, so `handVerdict` is defined.
|
|
510
|
+
const sampleMatches = [...screeningSample].filter((sampledItemSha256) => {
|
|
511
|
+
const row = screeningRowsByItem.get(sampledItemSha256);
|
|
512
|
+
return screeningRowAgreed(row) === (screeningRowHandVerdict(row) === "confirm");
|
|
513
|
+
}).length;
|
|
514
|
+
screeningSampleAgreementRate = sampleMatches / screeningSample.size;
|
|
515
|
+
}
|
|
516
|
+
const resolutions = manifest.labelResolutionSha256s.map((digest, index) => ({
|
|
517
|
+
digest,
|
|
518
|
+
...parseProfileRecord(state, digest, `admissionManifest.labelResolutionSha256s.${index}`, (bytes) => parseBinaryJudgmentLabelResolution(bytes), "label-resolution"),
|
|
519
|
+
}));
|
|
520
|
+
const contexts = manifest.analysisContextSha256s.map((digest, index) => ({
|
|
521
|
+
digest,
|
|
522
|
+
...parseProfileRecord(state, digest, `admissionManifest.analysisContextSha256s.${index}`, (bytes) => parseBinaryJudgmentAnalysisContext(bytes), "analysis-context"),
|
|
523
|
+
}));
|
|
524
|
+
if (resolutions.length !== contexts.length)
|
|
525
|
+
fail("admissionManifest", "resolution and analysis-context coverage differs");
|
|
526
|
+
const contextByResolution = new Map(contexts.map((entry) => [entry.value.labelResolutionSha256, entry]));
|
|
527
|
+
if (contextByResolution.size !== contexts.length)
|
|
528
|
+
fail("admissionManifest.analysisContextSha256s", "multiple contexts cover one resolution");
|
|
529
|
+
const acceptedItems = new Set();
|
|
530
|
+
const accepted = [];
|
|
531
|
+
for (const [index, entry] of resolutions.entries()) {
|
|
532
|
+
const resolution = entry.value;
|
|
533
|
+
const path = `resolutions.${index}`;
|
|
534
|
+
if (acceptedItems.has(resolution.itemSha256))
|
|
535
|
+
fail(path, "item is admitted more than once");
|
|
536
|
+
acceptedItems.add(resolution.itemSha256);
|
|
537
|
+
if (resolution.truthAdmission !== manifest.truthAdmission || resolution.resolvedAt !== manifest.admittedAt)
|
|
538
|
+
fail(path, "resolution admission kind/time differs from manifest");
|
|
539
|
+
// A screened resolution carries no humanReviewEvaluationSpecSha256 (spec §6.7: a row never
|
|
540
|
+
// hand-checked has no human-review evaluation), so this equality check only applies to the
|
|
541
|
+
// two branches that still carry the field. `verifyFrozenHumanReviewSpec` stays unconditional
|
|
542
|
+
// below regardless: the frozen spec+form are required admission-record roles for every mode
|
|
543
|
+
// (§6.8a's ratified G-5), not only the two that reference the spec by digest.
|
|
544
|
+
if (resolution.truthAdmission !== "screened-operator-sampled") {
|
|
545
|
+
if (resolution.humanReviewEvaluationSpecSha256 !== BINARY_JUDGMENT_HUMAN_REVIEW_EVALUATION_SPEC_SEALED.digest) {
|
|
546
|
+
fail(path, "resolution names a different human-review EvaluationSpec");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
verifyFrozenHumanReviewSpec(state, path);
|
|
550
|
+
const item = parseProfileRecord(state, resolution.itemSha256, `${path}.itemSha256`, (bytes) => parseBinaryJudgmentPayload(bytes), "source-item").value;
|
|
551
|
+
if (item.itemId !== resolution.itemId)
|
|
552
|
+
fail(path, "resolution itemId differs from exact item record");
|
|
553
|
+
const contextEntry = contextByResolution.get(entry.digest);
|
|
554
|
+
if (contextEntry === undefined)
|
|
555
|
+
fail(path, "resolution has no analysis context");
|
|
556
|
+
const context = contextEntry.value;
|
|
557
|
+
if (context.itemSha256 !== resolution.itemSha256
|
|
558
|
+
|| context.itemId !== resolution.itemId
|
|
559
|
+
|| context.truthLabel !== resolution.truthLabel
|
|
560
|
+
|| context.candidateClass !== resolution.candidateClass
|
|
561
|
+
|| context.stratum !== resolution.stratum)
|
|
562
|
+
fail(path, "analysis context does not exactly project the resolution");
|
|
563
|
+
// Exhaustive switch over the three-member truthAdmission union, never-typed default (spec
|
|
564
|
+
// §6.8a Group B / this packet's G-3): an unrecognized future mode is a compile error here,
|
|
565
|
+
// not a silent reroute into the two-human or operator-only path.
|
|
566
|
+
switch (resolution.truthAdmission) {
|
|
567
|
+
case "two-human-unanimous": {
|
|
568
|
+
const human = verifyHumanEvidence(state, resolution, resolution.itemSha256, input.expectedDraftId, manifest.admittedAt, path);
|
|
569
|
+
if (human.itemId !== resolution.itemId
|
|
570
|
+
|| human.reviews.some((review) => !review.complete || review.label === "indeterminate" || review.label !== resolution.truthLabel))
|
|
571
|
+
fail(path, "admitted human truth is not two complete unanimous reviews");
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
case "operator-only": {
|
|
575
|
+
const assertionEvidence = authorityPayload(state, {
|
|
576
|
+
digest: resolution.operatorAssertionSha256,
|
|
577
|
+
mediaType: HUMAN_REVIEW_OPERATOR_ASSERTION_MEDIA_TYPE,
|
|
578
|
+
role: "operator-truth-attestor",
|
|
579
|
+
evidenceRole: "operator-assertion",
|
|
580
|
+
schema: HumanReviewOperatorAssertionSchema,
|
|
581
|
+
path: `${path}.operatorAssertion`,
|
|
582
|
+
});
|
|
583
|
+
const assertion = assertionEvidence.value;
|
|
584
|
+
if (assertion.attestorRole !== "operator-truth-attestor"
|
|
585
|
+
|| assertion.attestorKeyId !== assertionEvidence.keyId
|
|
586
|
+
|| assertion.itemSha256 !== resolution.itemSha256
|
|
587
|
+
|| assertion.truthLabel !== resolution.truthLabel
|
|
588
|
+
|| assertion.assertedAt !== resolution.resolvedAt
|
|
589
|
+
|| assertion.limitation !== "operator-only-not-publication-grade")
|
|
590
|
+
fail(path, "operator assertion does not exactly bind the non-publication resolution");
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case "screened-operator-sampled": {
|
|
594
|
+
if (resolution.screeningTableSha256 !== manifest.screeningTableSha256) {
|
|
595
|
+
fail(path, "resolution names a different screening table than the manifest");
|
|
596
|
+
}
|
|
597
|
+
const revealEvidence = authorityPayload(state, {
|
|
598
|
+
digest: resolution.screeningRevealReceiptSha256,
|
|
599
|
+
mediaType: SCREENING_REVEAL_RECEIPT_MEDIA_TYPE,
|
|
600
|
+
role: "truth-reveal-attestor",
|
|
601
|
+
evidenceRole: "screening-reveal-receipt",
|
|
602
|
+
schema: ScreeningRevealReceiptSchema,
|
|
603
|
+
path: `${path}.screeningReveal`,
|
|
604
|
+
});
|
|
605
|
+
const reveal = revealEvidence.value;
|
|
606
|
+
if (reveal.attestorRole !== "truth-reveal-attestor"
|
|
607
|
+
|| reveal.attestorKeyId !== revealEvidence.keyId
|
|
608
|
+
|| revealEvidence.keyId !== screeningTableAuthorityKeyId
|
|
609
|
+
|| reveal.draftId !== input.expectedDraftId
|
|
610
|
+
|| reveal.screeningTableSha256 !== resolution.screeningTableSha256
|
|
611
|
+
|| reveal.judgeExecutionState !== "not-started"
|
|
612
|
+
|| reveal.truthFrozenAt !== manifest.admittedAt)
|
|
613
|
+
fail(path, "signed screening reveal does not bind the screening table under the same authority");
|
|
614
|
+
// (4) Per-row admission (spec §6.4): an admitted item's own table row must itself be
|
|
615
|
+
// admitted, and the resolution's truth label must be the row's intended label -- the
|
|
616
|
+
// screened branch never corrects a label, only confirms or excludes.
|
|
617
|
+
const row = screeningRowsByItem.get(resolution.itemSha256);
|
|
618
|
+
if (row === undefined)
|
|
619
|
+
fail(path, "accepted screened item has no covering screening-table row");
|
|
620
|
+
if (!screeningRowAdmitted(row))
|
|
621
|
+
fail(path, "admitted screened item's table row is not itself admitted per the sealed admission rule");
|
|
622
|
+
if (row.intendedLabel !== resolution.truthLabel)
|
|
623
|
+
fail(path, "resolution truth label differs from the screening table's intended label");
|
|
624
|
+
const poolItem = promptedScreeningPool?.items.find((item) => item.itemSha256 === resolution.itemSha256);
|
|
625
|
+
if (poolItem !== undefined && (poolItem.candidateClass !== resolution.candidateClass || poolItem.stratum !== resolution.stratum))
|
|
626
|
+
fail(path, "resolution class or stratum differs from the prompted screening pool");
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
default: {
|
|
630
|
+
const exhaustive = resolution;
|
|
631
|
+
fail(path, `unsupported truthAdmission ${String(exhaustive.truthAdmission)}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
accepted.push({
|
|
635
|
+
itemSha256: prefixed(resolution.itemSha256),
|
|
636
|
+
itemId: resolution.itemId,
|
|
637
|
+
truthLabel: resolution.truthLabel,
|
|
638
|
+
candidateClass: resolution.candidateClass,
|
|
639
|
+
stratum: resolution.stratum,
|
|
640
|
+
truthAdmission: resolution.truthAdmission,
|
|
641
|
+
labelResolutionSha256: prefixed(entry.digest),
|
|
642
|
+
analysisContextSha256: prefixed(contextEntry.digest),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (contextByResolution.size !== resolutions.length || contexts.some((entry) => !manifest.labelResolutionSha256s.includes(entry.value.labelResolutionSha256))) {
|
|
646
|
+
fail("admissionManifest.analysisContextSha256s", "analysis-context coverage has an orphan or omission");
|
|
647
|
+
}
|
|
648
|
+
const promptedExpectedReplacements = new Map();
|
|
649
|
+
if (promptedScreeningPool !== undefined) {
|
|
650
|
+
const slotIds = [...new Set(promptedScreeningPool.items.map((item) => item.slotId))].sort(compareCodeUnitStrings);
|
|
651
|
+
const expectedAccepted = new Set();
|
|
652
|
+
for (const slotId of slotIds) {
|
|
653
|
+
const slotItems = promptedScreeningPool.items.filter((item) => item.slotId === slotId);
|
|
654
|
+
const main = slotItems.find((item) => item.poolKind === "main");
|
|
655
|
+
const reserves = slotItems
|
|
656
|
+
.filter((item) => item.poolKind === "reserve")
|
|
657
|
+
.sort((left, right) => left.reserveOrder - right.reserveOrder);
|
|
658
|
+
const winner = [main, ...reserves].find((item) => screeningRowAdmitted(screeningRowsByItem.get(item.itemSha256)));
|
|
659
|
+
if (winner === undefined)
|
|
660
|
+
fail("screeningPool.items", `${slotId} has no admissible main or reserve candidate`);
|
|
661
|
+
expectedAccepted.add(winner.itemSha256);
|
|
662
|
+
if (winner.itemSha256 !== main.itemSha256)
|
|
663
|
+
promptedExpectedReplacements.set(main.itemSha256, winner.itemSha256);
|
|
664
|
+
}
|
|
665
|
+
const actualAccepted = [...acceptedItems].sort(compareCodeUnitStrings);
|
|
666
|
+
if (!sameStrings(actualAccepted, [...expectedAccepted].sort(compareCodeUnitStrings)))
|
|
667
|
+
fail("admissionManifest.labelResolutionSha256s", "accepted items are not the first admissible candidate in every sealed slot");
|
|
668
|
+
if (accepted.length !== 240)
|
|
669
|
+
fail("admissionManifest.labelResolutionSha256s", "prompted screening must admit exactly 240 items");
|
|
670
|
+
for (const candidateClass of ["correct", "specific-wrong", "vague-topical-wrong"]) {
|
|
671
|
+
if (accepted.filter((item) => item.candidateClass === candidateClass).length !== 80)
|
|
672
|
+
fail("admissionManifest.labelResolutionSha256s", `final bank must contain exactly 80 ${candidateClass} items`);
|
|
673
|
+
for (const stratum of ["category-1", "category-2", "category-3", "category-4"]) {
|
|
674
|
+
if (accepted.filter((item) => item.candidateClass === candidateClass && item.stratum === stratum).length !== 20)
|
|
675
|
+
fail("admissionManifest.labelResolutionSha256s", `final bank must contain exactly 20 ${candidateClass}/${stratum} items`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const excludedDigests = ledger.entries.map((entry) => entry.excludedItemSha256).sort(compareCodeUnitStrings);
|
|
680
|
+
if (!sameStrings(excludedDigests, manifest.excludedItemSha256s))
|
|
681
|
+
fail("replacementLedger.entries", "ledger exclusion coverage differs from manifest");
|
|
682
|
+
if (manifest.truthAdmission === "operator-only" && ledger.entries.length > 0)
|
|
683
|
+
fail("replacementLedger.entries", "operator-only admission cannot claim human-review exclusions");
|
|
684
|
+
const excludedItems = new Set();
|
|
685
|
+
const replacementItems = new Set();
|
|
686
|
+
const excluded = [];
|
|
687
|
+
let previousPosition = 0;
|
|
688
|
+
for (const [index, entry] of ledger.entries.entries()) {
|
|
689
|
+
const path = `replacementLedger.entries.${index}`;
|
|
690
|
+
if (entry.excludedPoolPosition <= previousPosition)
|
|
691
|
+
fail(path, "ledger entries are not in deterministic pool order");
|
|
692
|
+
previousPosition = entry.excludedPoolPosition;
|
|
693
|
+
if (excludedItems.has(entry.excludedItemSha256) || acceptedItems.has(entry.excludedItemSha256))
|
|
694
|
+
fail(path, "excluded item is duplicated or also admitted");
|
|
695
|
+
excludedItems.add(entry.excludedItemSha256);
|
|
696
|
+
if (replacementItems.has(entry.replacementItemSha256))
|
|
697
|
+
fail(path, "one admitted reserve is reused for multiple exclusions");
|
|
698
|
+
replacementItems.add(entry.replacementItemSha256);
|
|
699
|
+
const replacement = accepted.find((candidate) => candidate.itemSha256 === entry.replacementItemSha256);
|
|
700
|
+
if (replacement === undefined)
|
|
701
|
+
fail(path, "replacement is not in the admitted resolution set");
|
|
702
|
+
if (entry.replacementPoolPosition <= entry.excludedPoolPosition
|
|
703
|
+
|| replacement.candidateClass !== entry.candidateClass
|
|
704
|
+
|| replacement.stratum !== entry.stratum)
|
|
705
|
+
fail(path, "replacement is not a later admitted reserve in the same class and stratum");
|
|
706
|
+
if (promptedScreeningPool !== undefined) {
|
|
707
|
+
const excludedPoolItem = promptedScreeningPool.items.find((item) => item.itemSha256 === entry.excludedItemSha256);
|
|
708
|
+
const replacementPoolItem = promptedScreeningPool.items.find((item) => item.itemSha256 === entry.replacementItemSha256);
|
|
709
|
+
if (excludedPoolItem?.poolKind !== "main"
|
|
710
|
+
|| replacementPoolItem?.poolKind !== "reserve"
|
|
711
|
+
|| excludedPoolItem.slotId !== replacementPoolItem.slotId
|
|
712
|
+
|| entry.excludedPoolPosition !== excludedPoolItem.poolPosition
|
|
713
|
+
|| entry.replacementPoolPosition !== replacementPoolItem.poolPosition
|
|
714
|
+
|| promptedExpectedReplacements.get(entry.excludedItemSha256) !== entry.replacementItemSha256)
|
|
715
|
+
fail(path, "ledger does not name the deterministic first-admissible reserve for the exact sealed slot positions");
|
|
716
|
+
}
|
|
717
|
+
// Branches on admission mode (spec §6.4, §6.5 check (4); ratified resolution to G-1): a
|
|
718
|
+
// screened exclusion is verified against the screening table's own per-row admission rule
|
|
719
|
+
// rather than against two signed reviews. §6.9 deliberately DROPS the four per-item two-human
|
|
720
|
+
// digests for the screened branch (one signature on the whole table, not 240 on individual
|
|
721
|
+
// rows), so those four fields are never read here for a screened entry. `operator-only` is
|
|
722
|
+
// unreachable in this loop body: any operator-only manifest with ledger.entries.length > 0
|
|
723
|
+
// already refused above, before this loop began.
|
|
724
|
+
let itemId;
|
|
725
|
+
let derivedReason;
|
|
726
|
+
switch (manifest.truthAdmission) {
|
|
727
|
+
case "screened-operator-sampled": {
|
|
728
|
+
const row = screeningRowsByItem.get(entry.excludedItemSha256);
|
|
729
|
+
if (row === undefined)
|
|
730
|
+
fail(path, "excluded item has no covering screening-table row");
|
|
731
|
+
derivedReason = expectedScreeningExclusionReason(row);
|
|
732
|
+
const item = parseProfileRecord(state, entry.excludedItemSha256, `${path}.item`, (bytes) => parseBinaryJudgmentPayload(bytes), "source-item").value;
|
|
733
|
+
itemId = item.itemId;
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case "two-human-unanimous": {
|
|
737
|
+
// TypeScript narrows each optional property read below, but not `entry`'s own declared
|
|
738
|
+
// (still-optional) structural type, so the narrowed fields are passed on explicitly
|
|
739
|
+
// rather than passing `entry` itself. Defensive: the schema's own present-iff refinement
|
|
740
|
+
// (checked at `exactCanonical` above) already guarantees these four digests are present
|
|
741
|
+
// for a two-human reason, so this guard should be unreachable, not load-bearing.
|
|
742
|
+
if (entry.reviewVerdictSha256s === undefined
|
|
743
|
+
|| entry.visibilityReceiptSha256s === undefined
|
|
744
|
+
|| entry.reviewerRosterSha256 === undefined
|
|
745
|
+
|| entry.revealReceiptSha256 === undefined)
|
|
746
|
+
fail(path, "two-human ledger entry is missing its required review evidence digests");
|
|
747
|
+
const human = verifyHumanEvidence(state, {
|
|
748
|
+
reviewVerdictSha256s: entry.reviewVerdictSha256s,
|
|
749
|
+
visibilityReceiptSha256s: entry.visibilityReceiptSha256s,
|
|
750
|
+
reviewerRosterSha256: entry.reviewerRosterSha256,
|
|
751
|
+
revealReceiptSha256: entry.revealReceiptSha256,
|
|
752
|
+
}, entry.excludedItemSha256, input.expectedDraftId, manifest.admittedAt, path);
|
|
753
|
+
derivedReason = expectedExclusionReason(human.reviews);
|
|
754
|
+
itemId = human.itemId;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "operator-only":
|
|
758
|
+
fail(path, "operator-only admission cannot claim a replacement-ledger entry");
|
|
759
|
+
break;
|
|
760
|
+
default: {
|
|
761
|
+
const exhaustive = manifest.truthAdmission;
|
|
762
|
+
fail(path, `unsupported truthAdmission ${String(exhaustive)}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (derivedReason === undefined || derivedReason !== entry.reason)
|
|
766
|
+
fail(path, "ledger reason does not derive from the admitted evidence");
|
|
767
|
+
excluded.push({
|
|
768
|
+
itemSha256: prefixed(entry.excludedItemSha256),
|
|
769
|
+
itemId,
|
|
770
|
+
candidateClass: entry.candidateClass,
|
|
771
|
+
stratum: entry.stratum,
|
|
772
|
+
reason: entry.reason,
|
|
773
|
+
replacementItemSha256: prefixed(entry.replacementItemSha256),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
if (promptedScreeningPool !== undefined) {
|
|
777
|
+
const ledgerReplacements = new Map(ledger.entries.map((entry) => [entry.excludedItemSha256, entry.replacementItemSha256]));
|
|
778
|
+
if (ledgerReplacements.size !== promptedExpectedReplacements.size || [...promptedExpectedReplacements].some(([excludedItem, replacementItem]) => ledgerReplacements.get(excludedItem) !== replacementItem)) {
|
|
779
|
+
fail("replacementLedger.entries", "ledger does not exactly cover every prompted main-slot replacement");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
// (0) Coverage, the other half (spec §6.5 check (0)): the table's rows must cover the frozen
|
|
783
|
+
// bank's candidate pool EXACTLY. The pool itself is not a separate input to this function --
|
|
784
|
+
// it is reconstructed as accepted ∪ excluded, the closure's own admission decision, which by
|
|
785
|
+
// construction is exactly the frozen bank (every candidate item is either admitted or excluded
|
|
786
|
+
// with a same-class same-stratum replacement, and every replacement is itself an admitted pool
|
|
787
|
+
// member). `sampleSize <= rows.length` (check (0)'s other clause) is enforced transitively:
|
|
788
|
+
// `computeScreeningSample` above refuses a `sampleSize` exceeding the pool size on its own.
|
|
789
|
+
if (manifest.truthAdmission === "screened-operator-sampled" && promptedScreeningPool === undefined) {
|
|
790
|
+
if (screeningTable === undefined) {
|
|
791
|
+
// Unreachable today: the screened arm resolves the table before any row is admitted and fails
|
|
792
|
+
// closed when it is absent. Written as a refusal anyway, because the `&& screeningTable !==
|
|
793
|
+
// undefined` conjunct this replaces was a type-narrowing device whose false branch silently
|
|
794
|
+
// SKIPPED the coverage check. A coverage check with a skip path is not a coverage check, and a
|
|
795
|
+
// future caller that reordered the resolution would have been passed rather than refused.
|
|
796
|
+
fail("screeningTable", "screened admission reached the coverage check with no resolved screening table");
|
|
797
|
+
}
|
|
798
|
+
const pool = [...acceptedItems, ...excludedItems].sort(compareCodeUnitStrings);
|
|
799
|
+
const rowIdentities = screeningTable.rows.map((row) => row.itemSha256);
|
|
800
|
+
if (!sameStrings(pool, rowIdentities)) {
|
|
801
|
+
fail("screeningTable.rows", "screening table rows do not cover the admitted-plus-excluded closure exactly");
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const classes = [...new Set([...accepted.map((entry) => entry.candidateClass), ...excluded.map((entry) => entry.candidateClass)])].sort(compareCodeUnitStrings);
|
|
805
|
+
const strata = [...new Set([...accepted.map((entry) => entry.stratum), ...excluded.map((entry) => entry.stratum)])].sort(compareCodeUnitStrings);
|
|
806
|
+
return {
|
|
807
|
+
manifestSha256: input.admissionManifestSha256,
|
|
808
|
+
manifest,
|
|
809
|
+
replacementLedger: ledger,
|
|
810
|
+
// Derived by exclusion, not by equality (spec §6.8a Group A, first bullet; ratified fix):
|
|
811
|
+
// `=== "two-human-unanimous"` derives `false` for a screened manifest, which is
|
|
812
|
+
// publication-grade under §6.8, and would make schema.ts's third `superRefine` branch (which
|
|
813
|
+
// REQUIRES `true`) refuse every screened bundle. `operator-only` is the one mode that is
|
|
814
|
+
// never publication-grade.
|
|
815
|
+
publicationGrade: manifest.truthAdmission !== "operator-only",
|
|
816
|
+
classes,
|
|
817
|
+
strata,
|
|
818
|
+
accepted,
|
|
819
|
+
excluded,
|
|
820
|
+
reachableSha256s: [...state.reachable].sort(compareCodeUnitStrings),
|
|
821
|
+
reachableRecords: [...state.roles]
|
|
822
|
+
.sort(([left], [right]) => compareCodeUnitStrings(left, right))
|
|
823
|
+
.map(([sha256, roles]) => ({
|
|
824
|
+
sha256,
|
|
825
|
+
roles: BINARY_JUDGMENT_ADMISSION_RECORD_ROLES.filter((role) => roles.has(role)),
|
|
826
|
+
})),
|
|
827
|
+
// Present iff screened (spec §6.5 check (3)): the one definition of the screen-versus-hand
|
|
828
|
+
// agreement rate on the random sample, so no caller recomputes it independently.
|
|
829
|
+
...(screeningSampleAgreementRate === undefined || screeningInstrumentSha256 === undefined ? {} : {
|
|
830
|
+
screening: {
|
|
831
|
+
sampleAgreementRate: screeningSampleAgreementRate,
|
|
832
|
+
instrumentSha256: screeningInstrumentSha256,
|
|
833
|
+
},
|
|
834
|
+
}),
|
|
835
|
+
...(screeningSampleAgreementRate === undefined || promptedScreeningProcedureSha256 === undefined ? {} : {
|
|
836
|
+
promptedScreening: {
|
|
837
|
+
sampleAgreementRate: screeningSampleAgreementRate,
|
|
838
|
+
procedureSha256: promptedScreeningProcedureSha256,
|
|
839
|
+
profile: PROMPTED_SCREENING_PROFILE,
|
|
840
|
+
capabilityBoundary: PROMPTED_SCREENING_CAPABILITY_BOUNDARY,
|
|
841
|
+
},
|
|
842
|
+
}),
|
|
843
|
+
};
|
|
844
|
+
}
|