@colophon-claims/verify 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +23 -9
  2. package/dist/admission/contracts.d.ts +483 -0
  3. package/dist/admission/contracts.js +285 -0
  4. package/dist/admission/index.d.ts +2 -0
  5. package/dist/admission/index.js +2 -0
  6. package/dist/admission/prompted-commitment.d.ts +19 -0
  7. package/dist/admission/prompted-commitment.js +52 -0
  8. package/dist/admission/prompted-selection.d.ts +24 -0
  9. package/dist/admission/prompted-selection.js +85 -0
  10. package/dist/admission/verification.d.ts +17 -3
  11. package/dist/admission/verification.js +230 -49
  12. package/dist/assets.d.ts +21 -1
  13. package/dist/assets.js +60 -3
  14. package/dist/binding/beacon-binding.d.ts +230 -0
  15. package/dist/binding/beacon-binding.js +325 -0
  16. package/dist/binding/report-face.d.ts +45 -0
  17. package/dist/binding/report-face.js +153 -0
  18. package/dist/cli.js +74 -12
  19. package/dist/index.d.ts +14 -3
  20. package/dist/index.js +16 -3
  21. package/dist/manifest.d.ts +30 -4
  22. package/dist/manifest.js +30 -0
  23. package/dist/materialize.d.ts +7 -0
  24. package/dist/materialize.js +7 -0
  25. package/dist/outcome.d.ts +31 -0
  26. package/dist/outcome.js +45 -0
  27. package/dist/profile/binary-qualification.js +6 -0
  28. package/dist/profile/claim-consistency.d.ts +8 -1
  29. package/dist/profile/claim-consistency.js +4 -4
  30. package/dist/profile/claim.d.ts +203 -1
  31. package/dist/profile/claim.js +192 -43
  32. package/dist/profile/disclosure.d.ts +273 -0
  33. package/dist/profile/disclosure.js +240 -0
  34. package/dist/profile/pinning-evidence.d.ts +1 -1
  35. package/dist/profile/run-results.d.ts +8 -2
  36. package/dist/profile/run-results.js +9 -3
  37. package/dist/profile/task-selection.d.ts +69 -0
  38. package/dist/profile/task-selection.js +140 -0
  39. package/dist/reader-instructions.d.ts +38 -1
  40. package/dist/reader-instructions.js +42 -2
  41. package/dist/schema.d.ts +13 -2
  42. package/dist/schema.js +39 -8
  43. package/dist/signers.d.ts +48 -0
  44. package/dist/signers.js +77 -0
  45. package/dist/verify.d.ts +20 -6
  46. package/dist/verify.js +156 -31
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +14 -14
@@ -0,0 +1,273 @@
1
+ /**
2
+ * The disclosed closure's projection and check (disclosure-specification-record design §6.4, §6.6,
3
+ * §7; issue #2839).
4
+ *
5
+ * This module is **single-sourced, not mirrored**, exactly as `anchor-claims.ts` is: `@colophon-
6
+ * claims/core` already depends on this package, so the producer and the standalone verifier call
7
+ * ONE `deriveDisclosureSpecification`. The claim-consistency byte-compare then compares one
8
+ * function's output over two byte sets rather than two implementations' guesses.
9
+ *
10
+ * The one posture that matters more than any mechanism here (design R4):
11
+ *
12
+ * **The verifier AUTHENTICATES measurements and CARRIES assertions.**
13
+ *
14
+ * A `measured-here` variable must cite records the bundle actually carries, under a role admissible
15
+ * for the citation. A `disclosed-by-publisher` variable is checked for internal well-formedness and
16
+ * for nothing else — no lookup, no fetch, no cross-check against the Matrix or the Report, no status
17
+ * upgrade or downgrade. An assertion that turns out to be false is a false assertion in a valid
18
+ * record; that is the correct outcome and the honest one, and a verifier that refused it would be
19
+ * claiming a power it does not have.
20
+ *
21
+ * What this check deliberately does NOT do (design §8, each one a thing a well-meaning implementer
22
+ * would add):
23
+ *
24
+ * - It does not reconcile against `Report.disclosures.perSubject[].pinning`. Those counts answer
25
+ * "how well did each executed axis pin"; this record answers "which variables were executed at
26
+ * all". Two surfaces restating one fact are two surfaces that can disagree.
27
+ * - It does not infer a status from the bundle. A bundle that plainly executed a judge model does
28
+ * not license marking `judge-model` as `measured-here` if the record says otherwise.
29
+ * - It does not fetch anything, ever. A verifier that reached the network would make its own result
30
+ * depend on when it ran.
31
+ * - It does not rank, score, compare, or aggregate. There is no disclosure-completeness score:
32
+ * counting statuses would create a number publishers optimize against, and a six-of-six record
33
+ * with six vague assertions would outscore a two-of-six record with two proofs.
34
+ */
35
+ import { z } from "zod";
36
+ import { SIX_VARIABLE_DISCLOSURE_SPECIFICATION, type DisclosureEvidenceRole, type DisclosureVariableEntry, type DisclosureVariableKey, type DisclosureVariableStatus } from "@jinn-network/benchmarking-records";
37
+ import type { BundleV4EvidenceRole } from "../schema.js";
38
+ /** The evidence-catalog role a disclosure-specification record carries, and the ONLY one it may
39
+ * carry: a record bearing this token together with any second role refuses (§7 step 1). */
40
+ export declare const DISCLOSURE_SPECIFICATION_BUNDLE_ROLE: "disclosure-specification";
41
+ /**
42
+ * §6.4's binding profile: which of this carrier's evidence roles satisfy which of the standard's two
43
+ * portable disclosure roles. Jinn-side mapping, deliberately NOT part of the portable record — the
44
+ * mapping from the standard's roles onto a specific carrier's record roles is the carrier's business.
45
+ *
46
+ * v1 deliberately does not narrow the admissible set per variable. Which record species fixes
47
+ * `judge-model` depends on the judge profile packet P0 is freezing, and encoding a guess here would
48
+ * put this module in P0's lane. Reserved as a v2 tightening once P0's record vocabulary merges.
49
+ */
50
+ export declare const DISCLOSURE_ROLE_BINDING: Readonly<Record<DisclosureEvidenceRole, readonly BundleV4EvidenceRole[]>>;
51
+ /** The claim package's `disclosure` section (§6.6). Every variable entry is its record entry
52
+ * VERBATIM: nothing is summarized, counted, ranked, or reworded (R5). */
53
+ export interface ClaimDisclosureSection {
54
+ readonly recordSha256: string;
55
+ /** The literal standard identifier, not any string: this section names the standard the record
56
+ * claims compliance with, and a section naming a different one is not this record's projection. */
57
+ readonly specification: typeof SIX_VARIABLE_DISCLOSURE_SPECIFICATION;
58
+ readonly subjectSha256: string;
59
+ readonly variables: Readonly<Record<DisclosureVariableKey, DisclosureVariableEntry>>;
60
+ }
61
+ /**
62
+ * The claim section's own grammar, single-sourced from the record's schema so the section cannot
63
+ * describe a variable entry the record could not have carried. It is a second line of defense
64
+ * rather than the primary one: `assertClaimConsistency`'s whole-claim byte-compare against the
65
+ * rebuilt claim is what actually proves the section is this record's projection.
66
+ */
67
+ export declare const ClaimDisclosureSectionSchema: z.ZodObject<{
68
+ recordSha256: z.ZodString;
69
+ specification: z.ZodLiteral<"https://spec.jinn.network/disclosure/six-variable/v1">;
70
+ subjectSha256: z.ZodString;
71
+ variables: z.ZodObject<{
72
+ "ingestion-model": z.ZodDiscriminatedUnion<[z.ZodObject<{
73
+ status: z.ZodLiteral<"measured-here">;
74
+ statement: z.ZodString;
75
+ evidence: z.ZodArray<z.ZodObject<{
76
+ role: z.ZodEnum<{
77
+ "pinned-configuration": "pinned-configuration";
78
+ "execution-observation": "execution-observation";
79
+ }>;
80
+ digest: z.ZodObject<{
81
+ sha256: z.ZodString;
82
+ }, z.core.$strict>;
83
+ }, z.core.$strict>>;
84
+ }, z.core.$strict>, z.ZodObject<{
85
+ status: z.ZodLiteral<"disclosed-by-publisher">;
86
+ statement: z.ZodString;
87
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
88
+ uri: z.ZodString;
89
+ }, z.core.$strict>>>;
90
+ }, z.core.$strict>, z.ZodObject<{
91
+ status: z.ZodLiteral<"undisclosed">;
92
+ reason: z.ZodEnum<{
93
+ "not-stated": "not-stated";
94
+ "stated-without-identifiers": "stated-without-identifiers";
95
+ "outside-this-experiment": "outside-this-experiment";
96
+ }>;
97
+ }, z.core.$strict>], "status">;
98
+ "retrieval-config": z.ZodDiscriminatedUnion<[z.ZodObject<{
99
+ status: z.ZodLiteral<"measured-here">;
100
+ statement: z.ZodString;
101
+ evidence: z.ZodArray<z.ZodObject<{
102
+ role: z.ZodEnum<{
103
+ "pinned-configuration": "pinned-configuration";
104
+ "execution-observation": "execution-observation";
105
+ }>;
106
+ digest: z.ZodObject<{
107
+ sha256: z.ZodString;
108
+ }, z.core.$strict>;
109
+ }, z.core.$strict>>;
110
+ }, z.core.$strict>, z.ZodObject<{
111
+ status: z.ZodLiteral<"disclosed-by-publisher">;
112
+ statement: z.ZodString;
113
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
114
+ uri: z.ZodString;
115
+ }, z.core.$strict>>>;
116
+ }, z.core.$strict>, z.ZodObject<{
117
+ status: z.ZodLiteral<"undisclosed">;
118
+ reason: z.ZodEnum<{
119
+ "not-stated": "not-stated";
120
+ "stated-without-identifiers": "stated-without-identifiers";
121
+ "outside-this-experiment": "outside-this-experiment";
122
+ }>;
123
+ }, z.core.$strict>], "status">;
124
+ "answer-model": z.ZodDiscriminatedUnion<[z.ZodObject<{
125
+ status: z.ZodLiteral<"measured-here">;
126
+ statement: z.ZodString;
127
+ evidence: z.ZodArray<z.ZodObject<{
128
+ role: z.ZodEnum<{
129
+ "pinned-configuration": "pinned-configuration";
130
+ "execution-observation": "execution-observation";
131
+ }>;
132
+ digest: z.ZodObject<{
133
+ sha256: z.ZodString;
134
+ }, z.core.$strict>;
135
+ }, z.core.$strict>>;
136
+ }, z.core.$strict>, z.ZodObject<{
137
+ status: z.ZodLiteral<"disclosed-by-publisher">;
138
+ statement: z.ZodString;
139
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
140
+ uri: z.ZodString;
141
+ }, z.core.$strict>>>;
142
+ }, z.core.$strict>, z.ZodObject<{
143
+ status: z.ZodLiteral<"undisclosed">;
144
+ reason: z.ZodEnum<{
145
+ "not-stated": "not-stated";
146
+ "stated-without-identifiers": "stated-without-identifiers";
147
+ "outside-this-experiment": "outside-this-experiment";
148
+ }>;
149
+ }, z.core.$strict>], "status">;
150
+ "answer-prompt": z.ZodDiscriminatedUnion<[z.ZodObject<{
151
+ status: z.ZodLiteral<"measured-here">;
152
+ statement: z.ZodString;
153
+ evidence: z.ZodArray<z.ZodObject<{
154
+ role: z.ZodEnum<{
155
+ "pinned-configuration": "pinned-configuration";
156
+ "execution-observation": "execution-observation";
157
+ }>;
158
+ digest: z.ZodObject<{
159
+ sha256: z.ZodString;
160
+ }, z.core.$strict>;
161
+ }, z.core.$strict>>;
162
+ }, z.core.$strict>, z.ZodObject<{
163
+ status: z.ZodLiteral<"disclosed-by-publisher">;
164
+ statement: z.ZodString;
165
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
166
+ uri: z.ZodString;
167
+ }, z.core.$strict>>>;
168
+ }, z.core.$strict>, z.ZodObject<{
169
+ status: z.ZodLiteral<"undisclosed">;
170
+ reason: z.ZodEnum<{
171
+ "not-stated": "not-stated";
172
+ "stated-without-identifiers": "stated-without-identifiers";
173
+ "outside-this-experiment": "outside-this-experiment";
174
+ }>;
175
+ }, z.core.$strict>], "status">;
176
+ "judge-model": z.ZodDiscriminatedUnion<[z.ZodObject<{
177
+ status: z.ZodLiteral<"measured-here">;
178
+ statement: z.ZodString;
179
+ evidence: z.ZodArray<z.ZodObject<{
180
+ role: z.ZodEnum<{
181
+ "pinned-configuration": "pinned-configuration";
182
+ "execution-observation": "execution-observation";
183
+ }>;
184
+ digest: z.ZodObject<{
185
+ sha256: z.ZodString;
186
+ }, z.core.$strict>;
187
+ }, z.core.$strict>>;
188
+ }, z.core.$strict>, z.ZodObject<{
189
+ status: z.ZodLiteral<"disclosed-by-publisher">;
190
+ statement: z.ZodString;
191
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
192
+ uri: z.ZodString;
193
+ }, z.core.$strict>>>;
194
+ }, z.core.$strict>, z.ZodObject<{
195
+ status: z.ZodLiteral<"undisclosed">;
196
+ reason: z.ZodEnum<{
197
+ "not-stated": "not-stated";
198
+ "stated-without-identifiers": "stated-without-identifiers";
199
+ "outside-this-experiment": "outside-this-experiment";
200
+ }>;
201
+ }, z.core.$strict>], "status">;
202
+ "judge-prompt": z.ZodDiscriminatedUnion<[z.ZodObject<{
203
+ status: z.ZodLiteral<"measured-here">;
204
+ statement: z.ZodString;
205
+ evidence: z.ZodArray<z.ZodObject<{
206
+ role: z.ZodEnum<{
207
+ "pinned-configuration": "pinned-configuration";
208
+ "execution-observation": "execution-observation";
209
+ }>;
210
+ digest: z.ZodObject<{
211
+ sha256: z.ZodString;
212
+ }, z.core.$strict>;
213
+ }, z.core.$strict>>;
214
+ }, z.core.$strict>, z.ZodObject<{
215
+ status: z.ZodLiteral<"disclosed-by-publisher">;
216
+ statement: z.ZodString;
217
+ sources: z.ZodOptional<z.ZodArray<z.ZodObject<{
218
+ uri: z.ZodString;
219
+ }, z.core.$strict>>>;
220
+ }, z.core.$strict>, z.ZodObject<{
221
+ status: z.ZodLiteral<"undisclosed">;
222
+ reason: z.ZodEnum<{
223
+ "not-stated": "not-stated";
224
+ "stated-without-identifiers": "stated-without-identifiers";
225
+ "outside-this-experiment": "outside-this-experiment";
226
+ }>;
227
+ }, z.core.$strict>], "status">;
228
+ }, z.core.$strict>;
229
+ }, z.core.$strict>;
230
+ export declare class DisclosureProjectionError extends Error {
231
+ constructor(message: string);
232
+ }
233
+ /**
234
+ * The one shared projection, called by BOTH the workspace producer and the standalone verifier.
235
+ *
236
+ * It carries only facts embedded in the record's own bytes, plus the record's digest — which is the
237
+ * digest of the very bytes handed in, never a separately supplied one. It exists at all so a reader
238
+ * of `claim-package.json` alone sees all six statuses without opening an evidence record, and so
239
+ * `assertClaimConsistency`'s existing whole-claim byte-compare covers the disclosure without a
240
+ * second bespoke comparison.
241
+ */
242
+ export declare function deriveDisclosureSpecification(recordBytes: Uint8Array): ClaimDisclosureSection;
243
+ /** The disclosed statuses, surfaced on the verification result (§7 step 11). Disclosed facts, never
244
+ * folded into a single badge — the same posture `anchors` takes. */
245
+ export interface DisclosureSpecificationReport {
246
+ readonly recordSha256: string;
247
+ readonly specification: string;
248
+ readonly subjectSha256: string;
249
+ readonly statuses: Readonly<Record<DisclosureVariableKey, DisclosureVariableStatus>>;
250
+ }
251
+ export interface AssertDisclosureSpecificationInput {
252
+ /** The Report extension's descriptor digest — what the carrier says the record is. */
253
+ readonly extensionDigestSha256: string;
254
+ /** Every evidence-catalog record's declared roles, keyed by record digest. */
255
+ readonly catalogRoles: ReadonlyMap<string, ReadonlySet<string>>;
256
+ /** The exact carried bytes for every evidence record, keyed by digest. */
257
+ readonly recordBytes: ReadonlyMap<string, Uint8Array>;
258
+ /** The bundle's own Matrix digest — the record's one legal subject (R1). */
259
+ readonly matrixSha256: string;
260
+ /** The verified Report's author IRI. */
261
+ readonly reportAuthor: string;
262
+ /** Refuses with the verifier's own typed refusal, so this module raises no error class of its own
263
+ * at the call boundary and every path lands in the caller's issue shape. */
264
+ readonly refuse: (path: string, message: string) => never;
265
+ }
266
+ /**
267
+ * §7 steps 1–8: authenticate what the Report extension names.
268
+ *
269
+ * Step 9 (claim-id pairing) and step 10 (projection equality) live in `verify.ts` and
270
+ * `assertClaimConsistency` respectively, because both compare against documents this function is
271
+ * deliberately not handed — keeping this function a statement about the RECORD and its evidence.
272
+ */
273
+ export declare function assertDisclosureSpecification(input: AssertDisclosureSpecificationInput): DisclosureSpecificationReport;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * The disclosed closure's projection and check (disclosure-specification-record design §6.4, §6.6,
3
+ * §7; issue #2839).
4
+ *
5
+ * This module is **single-sourced, not mirrored**, exactly as `anchor-claims.ts` is: `@colophon-
6
+ * claims/core` already depends on this package, so the producer and the standalone verifier call
7
+ * ONE `deriveDisclosureSpecification`. The claim-consistency byte-compare then compares one
8
+ * function's output over two byte sets rather than two implementations' guesses.
9
+ *
10
+ * The one posture that matters more than any mechanism here (design R4):
11
+ *
12
+ * **The verifier AUTHENTICATES measurements and CARRIES assertions.**
13
+ *
14
+ * A `measured-here` variable must cite records the bundle actually carries, under a role admissible
15
+ * for the citation. A `disclosed-by-publisher` variable is checked for internal well-formedness and
16
+ * for nothing else — no lookup, no fetch, no cross-check against the Matrix or the Report, no status
17
+ * upgrade or downgrade. An assertion that turns out to be false is a false assertion in a valid
18
+ * record; that is the correct outcome and the honest one, and a verifier that refused it would be
19
+ * claiming a power it does not have.
20
+ *
21
+ * What this check deliberately does NOT do (design §8, each one a thing a well-meaning implementer
22
+ * would add):
23
+ *
24
+ * - It does not reconcile against `Report.disclosures.perSubject[].pinning`. Those counts answer
25
+ * "how well did each executed axis pin"; this record answers "which variables were executed at
26
+ * all". Two surfaces restating one fact are two surfaces that can disagree.
27
+ * - It does not infer a status from the bundle. A bundle that plainly executed a judge model does
28
+ * not license marking `judge-model` as `measured-here` if the record says otherwise.
29
+ * - It does not fetch anything, ever. A verifier that reached the network would make its own result
30
+ * depend on when it ran.
31
+ * - It does not rank, score, compare, or aggregate. There is no disclosure-completeness score:
32
+ * counting statuses would create a number publishers optimize against, and a six-of-six record
33
+ * with six vague assertions would outscore a two-of-six record with two proofs.
34
+ */
35
+ import { createHash } from "node:crypto";
36
+ import { z } from "zod";
37
+ import { DISCLOSURE_VARIABLE_KEYS, DisclosureVariablesSchema, MATRIX_RECORD_KIND, SIX_VARIABLE_DISCLOSURE_SPECIFICATION, parseDisclosureSpecification, } from "@jinn-network/benchmarking-records";
38
+ /** The evidence-catalog role a disclosure-specification record carries, and the ONLY one it may
39
+ * carry: a record bearing this token together with any second role refuses (§7 step 1). */
40
+ export const DISCLOSURE_SPECIFICATION_BUNDLE_ROLE = "disclosure-specification";
41
+ /**
42
+ * §6.4's binding profile: which of this carrier's evidence roles satisfy which of the standard's two
43
+ * portable disclosure roles. Jinn-side mapping, deliberately NOT part of the portable record — the
44
+ * mapping from the standard's roles onto a specific carrier's record roles is the carrier's business.
45
+ *
46
+ * v1 deliberately does not narrow the admissible set per variable. Which record species fixes
47
+ * `judge-model` depends on the judge profile packet P0 is freezing, and encoding a guess here would
48
+ * put this module in P0's lane. Reserved as a v2 tightening once P0's record vocabulary merges.
49
+ */
50
+ export const DISCLOSURE_ROLE_BINDING = {
51
+ "pinned-configuration": [
52
+ "task",
53
+ "item-bank",
54
+ "source-item",
55
+ "evaluation-spec",
56
+ "judge-instrument",
57
+ "runtime-selection",
58
+ "admission-manifest",
59
+ ],
60
+ "execution-observation": [
61
+ "run-pinning-evidence",
62
+ "solve-submission",
63
+ "solve-delivery",
64
+ "solve-output",
65
+ "evaluation-submission",
66
+ "evaluation-delivery",
67
+ "verdict",
68
+ ],
69
+ };
70
+ /**
71
+ * The claim section's own grammar, single-sourced from the record's schema so the section cannot
72
+ * describe a variable entry the record could not have carried. It is a second line of defense
73
+ * rather than the primary one: `assertClaimConsistency`'s whole-claim byte-compare against the
74
+ * rebuilt claim is what actually proves the section is this record's projection.
75
+ */
76
+ export const ClaimDisclosureSectionSchema = z.strictObject({
77
+ recordSha256: z.string().regex(/^[a-f0-9]{64}$/u),
78
+ specification: z.literal(SIX_VARIABLE_DISCLOSURE_SPECIFICATION),
79
+ subjectSha256: z.string().regex(/^[a-f0-9]{64}$/u),
80
+ variables: DisclosureVariablesSchema,
81
+ });
82
+ export class DisclosureProjectionError extends Error {
83
+ constructor(message) {
84
+ super(message);
85
+ this.name = "DisclosureProjectionError";
86
+ }
87
+ }
88
+ function sha256Hex(bytes) {
89
+ return createHash("sha256").update(bytes).digest("hex");
90
+ }
91
+ /**
92
+ * The one shared projection, called by BOTH the workspace producer and the standalone verifier.
93
+ *
94
+ * It carries only facts embedded in the record's own bytes, plus the record's digest — which is the
95
+ * digest of the very bytes handed in, never a separately supplied one. It exists at all so a reader
96
+ * of `claim-package.json` alone sees all six statuses without opening an evidence record, and so
97
+ * `assertClaimConsistency`'s existing whole-claim byte-compare covers the disclosure without a
98
+ * second bespoke comparison.
99
+ */
100
+ export function deriveDisclosureSpecification(recordBytes) {
101
+ let record;
102
+ try {
103
+ record = parseDisclosureSpecification(recordBytes);
104
+ }
105
+ catch (cause) {
106
+ throw new DisclosureProjectionError(`disclosure-specification record is not a valid sealed record: ${cause instanceof Error ? cause.message : String(cause)}`);
107
+ }
108
+ return {
109
+ recordSha256: sha256Hex(recordBytes),
110
+ specification: record.specification,
111
+ subjectSha256: record.subject.digest.sha256,
112
+ // Verbatim, in the frozen key order. Building the object key by key rather than spreading
113
+ // `record.variables` is what makes the order a property of this function rather than of
114
+ // whatever order the record's own bytes happened to carry.
115
+ variables: Object.fromEntries(DISCLOSURE_VARIABLE_KEYS.map((key) => [key, record.variables[key]])),
116
+ };
117
+ }
118
+ /**
119
+ * §7 steps 1–8: authenticate what the Report extension names.
120
+ *
121
+ * Step 9 (claim-id pairing) and step 10 (projection equality) live in `verify.ts` and
122
+ * `assertClaimConsistency` respectively, because both compare against documents this function is
123
+ * deliberately not handed — keeping this function a statement about the RECORD and its evidence.
124
+ */
125
+ export function assertDisclosureSpecification(input) {
126
+ // Explicitly annotated so TypeScript's control-flow analysis treats every call below as
127
+ // terminating; a destructured member with an inferred type does not narrow through `never`.
128
+ const refuse = input.refuse;
129
+ // ── Step 1: carrier binding, in exact one-to-one correspondence ─────────────────────────────
130
+ //
131
+ // The extension's digest must resolve to EXACTLY ONE catalog record — none and two both refuse —
132
+ // and that record's declared roles must be exactly the one disclosure role. Conversely any catalog
133
+ // record bearing the role that the extension does not name refuses: without the second direction,
134
+ // a second disclosure record could ride along with nothing checking it.
135
+ const bearers = [...input.catalogRoles]
136
+ .filter(([, roles]) => roles.has(DISCLOSURE_SPECIFICATION_BUNDLE_ROLE))
137
+ .map(([digest]) => digest);
138
+ if (bearers.length !== 1 || bearers[0] !== input.extensionDigestSha256) {
139
+ refuse("disclosure-specification", bearers.length === 0
140
+ ? "the Report names a disclosure-specification record that no evidence-catalog record carries"
141
+ : bearers.length > 1
142
+ ? "the evidence catalog carries more than one disclosure-specification record; the Report names exactly one"
143
+ : "the evidence catalog's disclosure-specification record is not the one the Report extension names");
144
+ }
145
+ const declared = input.catalogRoles.get(input.extensionDigestSha256);
146
+ if (declared === undefined || declared.size !== 1) {
147
+ refuse("disclosure-specification", "the disclosure-specification record must declare exactly that one role and no second");
148
+ }
149
+ // ── Step 2: exact bytes ────────────────────────────────────────────────────────────────────
150
+ //
151
+ // The digest equality with the catalog entry is already established upstream (the bundle walk
152
+ // recomputes every `records/<sha256>.bin`), so what is proved here is that those bytes parse
153
+ // strictly AND are the one exact canonical encoding of what they parse to.
154
+ const bytes = input.recordBytes.get(input.extensionDigestSha256);
155
+ if (bytes === undefined) {
156
+ refuse("disclosure-specification", "the bundle does not carry the disclosure-specification record it names");
157
+ }
158
+ const record = (() => {
159
+ try {
160
+ return parseDisclosureSpecification(bytes);
161
+ }
162
+ catch (cause) {
163
+ refuse(`records/${input.extensionDigestSha256}.bin`, `disclosure-specification record is invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
164
+ }
165
+ })();
166
+ // ── Step 3: subject binding (R1) ───────────────────────────────────────────────────────────
167
+ //
168
+ // The Matrix rather than the Report, because the Report cannot name its own digest inside its own
169
+ // signed payload, and because the record must be sealable before the Report is (§5).
170
+ if (record.subject.kind !== MATRIX_RECORD_KIND) {
171
+ refuse("disclosure-specification", `the record's subject kind must be ${MATRIX_RECORD_KIND}`);
172
+ }
173
+ if (record.subject.digest.sha256 !== input.matrixSha256) {
174
+ refuse("disclosure-specification", "the record's subject digest is not this bundle's Matrix digest");
175
+ }
176
+ if (record.specification !== SIX_VARIABLE_DISCLOSURE_SPECIFICATION) {
177
+ refuse("disclosure-specification", `the record must claim ${SIX_VARIABLE_DISCLOSURE_SPECIFICATION}`);
178
+ }
179
+ // ── Step 4: author binding ─────────────────────────────────────────────────────────────────
180
+ //
181
+ // A disclosure record asserting under one identity inside a bundle signed by another is a carrier
182
+ // mismatch, not an extra fact.
183
+ if (record.author !== input.reportAuthor) {
184
+ refuse("disclosure-specification", "the record's author is not the bundle's verified Report author");
185
+ }
186
+ // ── Step 5: vocabulary completeness (R2) ───────────────────────────────────────────────────
187
+ //
188
+ // Structural via the strict object; restated so the refusal names the KEY rather than a schema
189
+ // path a reader would have to decode.
190
+ for (const key of DISCLOSURE_VARIABLE_KEYS) {
191
+ if (record.variables[key] === undefined) {
192
+ refuse("disclosure-specification", `the record does not state the variable "${key}"`);
193
+ }
194
+ }
195
+ for (const key of Object.keys(record.variables)) {
196
+ if (!DISCLOSURE_VARIABLE_KEYS.includes(key)) {
197
+ refuse("disclosure-specification", `the record states an unknown variable "${key}"; the six-variable set is closed`);
198
+ }
199
+ }
200
+ // ── Steps 6–8: authenticate measurements, carry assertions, and let undisclosed carry nothing ─
201
+ const statuses = {};
202
+ for (const key of DISCLOSURE_VARIABLE_KEYS) {
203
+ const entry = record.variables[key];
204
+ statuses[key] = entry.status;
205
+ if (entry.status === "measured-here") {
206
+ // Step 6. THE WHOLE SUBSTANCE of "variables the venue actually ran must match actual pinning
207
+ // evidence in the bundle": no measured-here variable may cite a record the bundle does not
208
+ // carry, and a citation resolving to bytes of the wrong species is as bad as a dangling one.
209
+ for (const citation of entry.evidence) {
210
+ const cited = input.catalogRoles.get(citation.digest.sha256);
211
+ if (cited === undefined) {
212
+ refuse("disclosure-specification", `"${key}" is measured here but cites ${citation.digest.sha256}, which this bundle does not carry`);
213
+ }
214
+ const citedRoles = cited;
215
+ const admissible = DISCLOSURE_ROLE_BINDING[citation.role];
216
+ if (!admissible.some((role) => citedRoles.has(role))) {
217
+ refuse("disclosure-specification", `"${key}" cites ${citation.digest.sha256} as ${citation.role}, but that record's bundle roles are`
218
+ + ` outside the admissible set for it`);
219
+ }
220
+ }
221
+ continue;
222
+ }
223
+ if (entry.status === "disclosed-by-publisher") {
224
+ // Step 7. Internal consistency ONLY. The schema has already established the statement bound
225
+ // and the sorted, unique, absolute-IRI source list; there is nothing further to check here and
226
+ // — R4 — nothing further this verifier is entitled to check. Restated as a branch so a later
227
+ // reader sees the deliberate emptiness rather than an omission.
228
+ continue;
229
+ }
230
+ // Step 8. Structural via the union; restated so the refusal would name the variable. Reaching
231
+ // this branch at all means the entry is `undisclosed`, which by construction carries a reason
232
+ // token and nothing else.
233
+ }
234
+ return {
235
+ recordSha256: input.extensionDigestSha256,
236
+ specification: record.specification,
237
+ subjectSha256: record.subject.digest.sha256,
238
+ statuses: statuses,
239
+ };
240
+ }
@@ -16,8 +16,8 @@ export declare const RunPinningEvidenceSchema: z.ZodObject<{
16
16
  }, z.core.$strict>>;
17
17
  observations: z.ZodOptional<z.ZodArray<z.ZodObject<{
18
18
  axis: z.ZodEnum<{
19
- harness: "harness";
20
19
  model: "model";
20
+ harness: "harness";
21
21
  loadout: "loadout";
22
22
  isolation: "isolation";
23
23
  }>;
@@ -1,10 +1,16 @@
1
1
  import type { MatrixCell, RunRecord } from "@jinn-network/benchmarking-records";
2
2
  import { type ClaimAnchor } from "./anchor-claims.js";
3
+ import type { VerifiedRunBinding } from "../binding/beacon-binding.js";
3
4
  export declare const LOCAL_VENUE_LIMITS: readonly string[];
4
5
  export declare function localVenueLimitsForRun(run: Pick<RunRecord, "policy">): readonly string[];
5
6
  /** `anchors` is the same derived section the producer used (anchor-evidence §7.4); the conditional
6
- * copy is the same pure function on both sides, so claim-consistency stays an exact byte-compare. */
7
- export declare function buildLocalVenueHonesty(cells: readonly MatrixCell[], run: Pick<RunRecord, "policy">, anchors?: readonly ClaimAnchor[]): {
7
+ * copy is the same pure function on both sides, so claim-consistency stays an exact byte-compare.
8
+ *
9
+ * `binding` is the run's verified `beacon-binding/1` record (issue #2976) and obeys the same rule:
10
+ * omitting it is the identity, so every run that carries no binding keeps its exact limits bytes.
11
+ * It is only ever passed by a caller that can show BOTH sides the same binding — today that means
12
+ * it is not passed, because the public bundle carries no binding record yet. */
13
+ export declare function buildLocalVenueHonesty(cells: readonly MatrixCell[], run: Pick<RunRecord, "policy">, anchors?: readonly ClaimAnchor[], binding?: VerifiedRunBinding): {
8
14
  venue: "self-run";
9
15
  preRegistration: "structural-and-append-order-only" | "structural-append-order-and-anchored-time";
10
16
  limits: readonly string[];
@@ -1,5 +1,6 @@
1
1
  import { venueIsolationPostureForPolicy } from "./isolation.js";
2
2
  import { anchoredPreRegistration, anchoredVenueLimits } from "./anchor-claims.js";
3
+ import { runBoundVenueLimits } from "../binding/report-face.js";
3
4
  export const LOCAL_VENUE_LIMITS = [
4
5
  "This is a local, self-run venue: the same operator controls task dispatch, execution, and evaluation.",
5
6
  "Pre-registration here is a discipline enforced by this tool, not a proof against the run's own owner — nothing prevents the owner from having altered the record before publishing it.",
@@ -12,12 +13,17 @@ export function localVenueLimitsForRun(run) {
12
13
  return venueIsolationPostureForPolicy(run.policy.submissionBaseline?.isolationPolicy).inventory.length === 1 ? LOCAL_VENUE_LIMITS : [LOCAL_VENUE_LIMITS[0], LOCAL_VENUE_LIMITS[1], MULTI, ...LOCAL_VENUE_LIMITS.slice(3)];
13
14
  }
14
15
  /** `anchors` is the same derived section the producer used (anchor-evidence §7.4); the conditional
15
- * copy is the same pure function on both sides, so claim-consistency stays an exact byte-compare. */
16
- export function buildLocalVenueHonesty(cells, run, anchors = []) {
16
+ * copy is the same pure function on both sides, so claim-consistency stays an exact byte-compare.
17
+ *
18
+ * `binding` is the run's verified `beacon-binding/1` record (issue #2976) and obeys the same rule:
19
+ * omitting it is the identity, so every run that carries no binding keeps its exact limits bytes.
20
+ * It is only ever passed by a caller that can show BOTH sides the same binding — today that means
21
+ * it is not passed, because the public bundle carries no binding record yet. */
22
+ export function buildLocalVenueHonesty(cells, run, anchors = [], binding) {
17
23
  const counts = { harness: 0, model: 0, loadout: 0, isolation: 0 };
18
24
  for (const cell of cells)
19
25
  for (const axis of Object.keys(counts))
20
26
  if (cell.verification[axis] === "unverifiable")
21
27
  counts[axis] += 1;
22
- return { venue: "self-run", preRegistration: anchoredPreRegistration(anchors), limits: anchoredVenueLimits(localVenueLimitsForRun(run), anchors), unverifiableAxisCounts: counts };
28
+ return { venue: "self-run", preRegistration: anchoredPreRegistration(anchors), limits: runBoundVenueLimits(anchoredVenueLimits(localVenueLimitsForRun(run), anchors), binding), unverifiableAxisCounts: counts };
23
29
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Task-selection provenance, verified against the records that carry it (issue #2980).
3
+ *
4
+ * The declaration is a closed-vocabulary value sealed into the Run record's
5
+ * `task-selection/v1` extension. Sealing makes it unforgeable after the lock; it does not make it
6
+ * true. What remains for a cold verifier is the part sealing cannot settle: whether the *other*
7
+ * sealed records are consistent with what the declaration asserts.
8
+ *
9
+ * In this record model the *selection is the Benchmark record itself*: the Run seals a Benchmark
10
+ * digest, and `expectedCellSet` is the full cartesian product `items x arms x replicates`, so the
11
+ * Matrix always covers every Benchmark item exactly. Comparing the Matrix's task digests against
12
+ * the Benchmark's items — the first thing this module tried — is therefore dead code. What is left
13
+ * is the Benchmark's own reveal policy and whether anyone declared the set at all.
14
+ *
15
+ * These checks are refusals, not endorsements, and the asymmetry is the point: each one names a
16
+ * declaration the records positively contradict. None of them can establish that a declaration is
17
+ * TRUE, because the bundle carries no independent witness of an upstream set. `assertTaskSelection-
18
+ * Consistency` below says exactly where that boundary falls and why the tempting rule on the other
19
+ * side of it is unsound.
20
+ */
21
+ import { type BenchmarkRecord, type RunRecord, type TaskSelectionMode } from "@jinn-network/benchmarking-records";
22
+ export interface TaskSelectionConsistencyInput {
23
+ readonly benchmarkRecord: BenchmarkRecord;
24
+ readonly runRecord: RunRecord;
25
+ }
26
+ /**
27
+ * The declared mode, refusing rather than throwing raw on bytes the Run schema would not have
28
+ * sealed. Exported so any reader that resolves the mode does so through this refusal posture
29
+ * rather than re-deriving it with its own error handling. No presentation asset consumes it today
30
+ * -- the report face renders nothing for task selection until issue #3416 -- so its only callers
31
+ * are `taskSelectionContradiction` below and its own tests.
32
+ */
33
+ export declare function declaredTaskSelectionMode(runRecord: RunRecord): TaskSelectionMode | undefined;
34
+ /**
35
+ * The way the sealed records contradict a declared task-selection mode, or `undefined` when they
36
+ * do not. Pure, and shared deliberately: the producer calls it BEFORE the lock so a contradiction
37
+ * is a draft-validation refusal the claimant can still act on, and the cold verifier calls it after
38
+ * the fact through {@link assertTaskSelectionConsistency}. One rule, two postures — a second copy
39
+ * would be free to drift into refusing at publish what it accepted at lock, which is the worst
40
+ * possible place to disagree.
41
+ *
42
+ * Two contradictions, both provable from bundle bytes:
43
+ *
44
+ * 1. **`fixed-public-set` over an undeclared set.** A set nobody declared is not a publicly
45
+ * declared set, so the Benchmark record must at least name an `author`.
46
+ * 2. **Reveal policy that cannot coexist with the mode.** `fixed-public-set` is refused when the
47
+ * items were provably withheld at the lock; `drawn-post-lock` is refused when they were
48
+ * `immediate` — open the moment the record existed, so the Run sealed against a set the
49
+ * claimant could already read, and nothing was drawn afterwards.
50
+ *
51
+ * What this deliberately does NOT do is decide the stronger modes from `benchmark.author` versus
52
+ * `run.owner`. That rule looks decisive and is not: `author` is a self-declaration the design spec
53
+ * marks non-authoritative, and every task-set intake in this product re-authors the Benchmark under
54
+ * the workspace's own key (`intake/workspace-authored.ts`), which is also the Run's owner. Enforcing
55
+ * on it would refuse every bundle this product can produce, making two of the three vocabulary
56
+ * values dead letters.
57
+ *
58
+ * So the honest boundary: these checks catch declarations the records positively contradict, but
59
+ * none can prove a `fixed-public-set` claim TRUE — the bundle carries no independent witness of the
60
+ * upstream set. `PUBLIC-BUNDLE.md` says so in the same words, because a gap a reader can see is
61
+ * worth more than one a rule pretends to close.
62
+ *
63
+ * `claimant-chosen` is unconstrained on purpose. It asserts nothing about anyone but the claimant,
64
+ * so nothing can contradict it, and constraining it would only make the honest answer the
65
+ * expensive one.
66
+ */
67
+ export declare function taskSelectionContradiction(input: TaskSelectionConsistencyInput): string | undefined;
68
+ /** The cold verifier's posture over {@link taskSelectionContradiction}: a typed record refusal. */
69
+ export declare function assertTaskSelectionConsistency(input: TaskSelectionConsistencyInput): void;