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