@colophon-claims/verify 0.2.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 (47) hide show
  1. package/README.md +18 -8
  2. package/dist/admission/contracts.d.ts +241 -0
  3. package/dist/admission/contracts.js +116 -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 +1 -0
  11. package/dist/admission/verification.js +88 -42
  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/claim-consistency.d.ts +8 -1
  28. package/dist/profile/claim-consistency.js +4 -4
  29. package/dist/profile/claim.d.ts +201 -2
  30. package/dist/profile/claim.js +178 -47
  31. package/dist/profile/disclosure.d.ts +273 -0
  32. package/dist/profile/disclosure.js +240 -0
  33. package/dist/profile/run-results.d.ts +8 -2
  34. package/dist/profile/run-results.js +9 -3
  35. package/dist/profile/task-selection.d.ts +69 -0
  36. package/dist/profile/task-selection.js +140 -0
  37. package/dist/reader-instructions.d.ts +38 -1
  38. package/dist/reader-instructions.js +42 -2
  39. package/dist/schema.d.ts +4 -3
  40. package/dist/schema.js +17 -0
  41. package/dist/signers.d.ts +48 -0
  42. package/dist/signers.js +77 -0
  43. package/dist/verify.d.ts +20 -6
  44. package/dist/verify.js +156 -31
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +14 -14
@@ -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
+ }
@@ -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;
@@ -0,0 +1,140 @@
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 { compareCalendarStrictRfc3339Instants, readTaskSelectionMode, } from "@jinn-network/benchmarking-records";
22
+ import { refuse } from "./errors.js";
23
+ /** The verification path every refusal here is reported under. Adding a new named check would be a
24
+ * bundle-format bump — the claim pins the check list byte-for-byte — so these refusals join the
25
+ * existing `claim-consistency` check, which is exactly the question they answer. */
26
+ const PATH = "claim-consistency";
27
+ /**
28
+ * Whether the Benchmark's items were PROVABLY still withheld when the Run sealed against them.
29
+ *
30
+ * Deliberately one-directional, and the reason is a real trap. The only instant a cold verifier
31
+ * can read out of a sealed Run is `closeAt`, which is the run's CLOSE, not its lock —
32
+ * `closeAt = lockedAt + policy.closeAfterMs` with a strictly positive interval (24h by default),
33
+ * and `lockedAt` lives only in product-local state that no bundle carries. So `notBefore <= closeAt`
34
+ * establishes nothing about the lock: a schedule opening twelve hours into a twenty-four-hour run
35
+ * satisfies it while the items were plainly withheld at the lock. Only the far side is safe —
36
+ * `notBefore >= closeAt > lockedAt` proves withholding — so that is the only comparison made.
37
+ *
38
+ * The comparator is the records package's own, not `Date.parse`, and that is a correctness
39
+ * requirement rather than a preference. These fields are validated by `isCalendarStrictRfc3339`,
40
+ * which accepts leap seconds; V8's `Date.parse` returns `NaN` for exactly those spellings, and
41
+ * `NaN >= x` is `false`. Left on `Date.parse`, a claimant could seal
42
+ * `notBefore: "2026-12-31T23:59:60Z"` — items withheld past the run's end — declare
43
+ * `fixed-public-set`, and have both this check and the producer's pre-lock gate wave it through.
44
+ * An uncomparable pair fails CLOSED for the same reason: it cannot arise from schema-valid records,
45
+ * so treating it as proof of withholding costs nothing and removes the fail-open shape entirely.
46
+ *
47
+ * A `scheduled` reveal with no `notBefore` fails closed on the same principle. The Benchmark schema
48
+ * leaves `notBefore` optional under every policy, so `{ "policy": "scheduled" }` seals cleanly while
49
+ * announcing no instant at which the items become readable — strictly more withheld than
50
+ * `after-run`, which at least names the run's end. Reading it as open would hand a claimant a
51
+ * one-key evasion of the only reveal-policy teeth `fixed-public-set` has: delete the field, and a
52
+ * privately assembled set passes the check that `after-run` fails.
53
+ */
54
+ function withheldAtLock(benchmark, closeAt) {
55
+ const { policy, notBefore } = benchmark.reveal;
56
+ if (policy === "after-run")
57
+ return true;
58
+ if (policy !== "scheduled")
59
+ return false;
60
+ if (notBefore === undefined)
61
+ return true;
62
+ const order = compareCalendarStrictRfc3339Instants(notBefore, closeAt);
63
+ return order === undefined || order >= 0;
64
+ }
65
+ /**
66
+ * The declared mode, refusing rather than throwing raw on bytes the Run schema would not have
67
+ * sealed. Exported so any reader that resolves the mode does so through this refusal posture
68
+ * rather than re-deriving it with its own error handling. No presentation asset consumes it today
69
+ * -- the report face renders nothing for task selection until issue #3416 -- so its only callers
70
+ * are `taskSelectionContradiction` below and its own tests.
71
+ */
72
+ export function declaredTaskSelectionMode(runRecord) {
73
+ try {
74
+ return readTaskSelectionMode(runRecord);
75
+ }
76
+ catch (cause) {
77
+ refuse("record-integrity", PATH, `the Run's declared task selection is not one of the recorded selection modes: ${cause instanceof Error ? cause.message : String(cause)}`);
78
+ }
79
+ }
80
+ /**
81
+ * The way the sealed records contradict a declared task-selection mode, or `undefined` when they
82
+ * do not. Pure, and shared deliberately: the producer calls it BEFORE the lock so a contradiction
83
+ * is a draft-validation refusal the claimant can still act on, and the cold verifier calls it after
84
+ * the fact through {@link assertTaskSelectionConsistency}. One rule, two postures — a second copy
85
+ * would be free to drift into refusing at publish what it accepted at lock, which is the worst
86
+ * possible place to disagree.
87
+ *
88
+ * Two contradictions, both provable from bundle bytes:
89
+ *
90
+ * 1. **`fixed-public-set` over an undeclared set.** A set nobody declared is not a publicly
91
+ * declared set, so the Benchmark record must at least name an `author`.
92
+ * 2. **Reveal policy that cannot coexist with the mode.** `fixed-public-set` is refused when the
93
+ * items were provably withheld at the lock; `drawn-post-lock` is refused when they were
94
+ * `immediate` — open the moment the record existed, so the Run sealed against a set the
95
+ * claimant could already read, and nothing was drawn afterwards.
96
+ *
97
+ * What this deliberately does NOT do is decide the stronger modes from `benchmark.author` versus
98
+ * `run.owner`. That rule looks decisive and is not: `author` is a self-declaration the design spec
99
+ * marks non-authoritative, and every task-set intake in this product re-authors the Benchmark under
100
+ * the workspace's own key (`intake/workspace-authored.ts`), which is also the Run's owner. Enforcing
101
+ * on it would refuse every bundle this product can produce, making two of the three vocabulary
102
+ * values dead letters.
103
+ *
104
+ * So the honest boundary: these checks catch declarations the records positively contradict, but
105
+ * none can prove a `fixed-public-set` claim TRUE — the bundle carries no independent witness of the
106
+ * upstream set. `PUBLIC-BUNDLE.md` says so in the same words, because a gap a reader can see is
107
+ * worth more than one a rule pretends to close.
108
+ *
109
+ * `claimant-chosen` is unconstrained on purpose. It asserts nothing about anyone but the claimant,
110
+ * so nothing can contradict it, and constraining it would only make the honest answer the
111
+ * expensive one.
112
+ */
113
+ export function taskSelectionContradiction(input) {
114
+ const { benchmarkRecord, runRecord } = input;
115
+ const declared = declaredTaskSelectionMode(runRecord);
116
+ if (declared === undefined || declared === "claimant-chosen")
117
+ return undefined;
118
+ if (declared === "fixed-public-set") {
119
+ if (benchmarkRecord.author === undefined) {
120
+ return "task selection is declared fixed-public-set but the Benchmark record names no author,"
121
+ + " so the set it describes was never publicly declared by anyone";
122
+ }
123
+ if (withheldAtLock(benchmarkRecord, runRecord.closeAt)) {
124
+ return "task selection is declared fixed-public-set but the Benchmark's reveal policy withholds"
125
+ + " its items past the end of the run, so they were not public when the run was locked";
126
+ }
127
+ return undefined;
128
+ }
129
+ if (benchmarkRecord.reveal.policy === "immediate") {
130
+ return "task selection is declared drawn-post-lock but the Benchmark reveals its items"
131
+ + " immediately, so the run was locked against a set the claimant could already read";
132
+ }
133
+ return undefined;
134
+ }
135
+ /** The cold verifier's posture over {@link taskSelectionContradiction}: a typed record refusal. */
136
+ export function assertTaskSelectionConsistency(input) {
137
+ const contradiction = taskSelectionContradiction(input);
138
+ if (contradiction !== undefined)
139
+ refuse("record-integrity", PATH, contradiction);
140
+ }
@@ -20,7 +20,36 @@ export declare const PUBLIC_BUNDLE_V6_COMPATIBLE_VERIFICATION_COMMAND: "npx @col
20
20
  * `EVIDENCE_NATIVE_BUNDLE_V5_CHECKS`.
21
21
  */
22
22
  export declare const PUBLIC_BUNDLE_V6_CHECKS: readonly ["manifest", "evidence-closure", "trust", "matrix-rederivation", "report-verification", "claim-consistency", "integrity-anchors"];
23
- /** Every current format stamps the same first public 0.1 line. */
23
+ /**
24
+ * The anchored binary-qualification closure runs exactly the anchored list (issue #3205): the
25
+ * qualification projection changes what `evidence-closure` and `claim-consistency` examine, not
26
+ * which checks run.
27
+ */
28
+ export declare const PUBLIC_BUNDLE_V7_CHECKS: readonly ["manifest", "evidence-closure", "trust", "matrix-rederivation", "report-verification", "claim-consistency", "integrity-anchors"];
29
+ /**
30
+ * Unlike every earlier closure, this one does NOT stamp the first public 0.1 line: no released
31
+ * verifier before 0.2.1 understands `benchmark-product-public-bundle/7`, and a claim naming a
32
+ * reader that cannot read it would be an instruction to fail. 0.2.1 is also the line that carries
33
+ * the prompted-screening admission surface a binary claim may need.
34
+ */
35
+ export declare const PUBLIC_BUNDLE_V7_VERIFICATION_COMMAND: "npx @colophon-claims/verify@0.2.1 <bundle-dir>";
36
+ export declare const PUBLIC_BUNDLE_V7_COMPATIBLE_VERIFICATION_COMMAND: "npx @colophon-claims/verify@0.2 <bundle-dir>";
37
+ /**
38
+ * The disclosed closure's check list (disclosure-specification-record design §7, issue #2839): v7's
39
+ * seven plus `disclosure-specification`, **last**. It runs after `claim-consistency` because the
40
+ * claim's `disclosure` section is among the things it compares, and it is **always present** on this
41
+ * format — a disclosed bundle whose record was stripped is a closure failure, not a shorter list.
42
+ */
43
+ export declare const PUBLIC_BUNDLE_V8_CHECKS: readonly ["manifest", "evidence-closure", "trust", "matrix-rederivation", "report-verification", "claim-consistency", "integrity-anchors", "disclosure-specification"];
44
+ /**
45
+ * Like v7 and for the same reason: no released reader before 0.2.1 understands
46
+ * `benchmark-product-public-bundle/8`, and a claim naming a reader that cannot read it would be an
47
+ * instruction to fail. The disclosed closure ships in the same unpublished 0.2.1 line as v7, so it
48
+ * pins that line rather than minting a third.
49
+ */
50
+ export declare const PUBLIC_BUNDLE_V8_VERIFICATION_COMMAND: "npx @colophon-claims/verify@0.2.1 <bundle-dir>";
51
+ export declare const PUBLIC_BUNDLE_V8_COMPATIBLE_VERIFICATION_COMMAND: "npx @colophon-claims/verify@0.2 <bundle-dir>";
52
+ /** Every format through v6 stamps the same first public 0.1 line; v7 is the first that cannot. */
24
53
  export declare const PUBLIC_BUNDLE_VERIFICATION_INSTRUCTIONS: {
25
54
  readonly "benchmark-product-public-bundle/2": {
26
55
  readonly command: "npx @colophon-claims/verify@0.1.0 <bundle-dir>";
@@ -38,4 +67,12 @@ export declare const PUBLIC_BUNDLE_VERIFICATION_INSTRUCTIONS: {
38
67
  readonly command: "npx @colophon-claims/verify@0.1.0 <bundle-dir>";
39
68
  readonly compatibleCommand: "npx @colophon-claims/verify@0.1 <bundle-dir>";
40
69
  };
70
+ readonly "benchmark-product-public-bundle/7": {
71
+ readonly command: "npx @colophon-claims/verify@0.2.1 <bundle-dir>";
72
+ readonly compatibleCommand: "npx @colophon-claims/verify@0.2 <bundle-dir>";
73
+ };
74
+ readonly "benchmark-product-public-bundle/8": {
75
+ readonly command: "npx @colophon-claims/verify@0.2.1 <bundle-dir>";
76
+ readonly compatibleCommand: "npx @colophon-claims/verify@0.2 <bundle-dir>";
77
+ };
41
78
  };
@@ -30,7 +30,39 @@ export const PUBLIC_BUNDLE_V6_CHECKS = [
30
30
  ...PUBLIC_BUNDLE_VERIFICATION_CHECKS,
31
31
  "integrity-anchors",
32
32
  ];
33
- /** Every current format stamps the same first public 0.1 line. */
33
+ /**
34
+ * The anchored binary-qualification closure runs exactly the anchored list (issue #3205): the
35
+ * qualification projection changes what `evidence-closure` and `claim-consistency` examine, not
36
+ * which checks run.
37
+ */
38
+ export const PUBLIC_BUNDLE_V7_CHECKS = PUBLIC_BUNDLE_V6_CHECKS;
39
+ /**
40
+ * Unlike every earlier closure, this one does NOT stamp the first public 0.1 line: no released
41
+ * verifier before 0.2.1 understands `benchmark-product-public-bundle/7`, and a claim naming a
42
+ * reader that cannot read it would be an instruction to fail. 0.2.1 is also the line that carries
43
+ * the prompted-screening admission surface a binary claim may need.
44
+ */
45
+ export const PUBLIC_BUNDLE_V7_VERIFICATION_COMMAND = "npx @colophon-claims/verify@0.2.1 <bundle-dir>";
46
+ export const PUBLIC_BUNDLE_V7_COMPATIBLE_VERIFICATION_COMMAND = "npx @colophon-claims/verify@0.2 <bundle-dir>";
47
+ /**
48
+ * The disclosed closure's check list (disclosure-specification-record design §7, issue #2839): v7's
49
+ * seven plus `disclosure-specification`, **last**. It runs after `claim-consistency` because the
50
+ * claim's `disclosure` section is among the things it compares, and it is **always present** on this
51
+ * format — a disclosed bundle whose record was stripped is a closure failure, not a shorter list.
52
+ */
53
+ export const PUBLIC_BUNDLE_V8_CHECKS = [
54
+ ...PUBLIC_BUNDLE_V7_CHECKS,
55
+ "disclosure-specification",
56
+ ];
57
+ /**
58
+ * Like v7 and for the same reason: no released reader before 0.2.1 understands
59
+ * `benchmark-product-public-bundle/8`, and a claim naming a reader that cannot read it would be an
60
+ * instruction to fail. The disclosed closure ships in the same unpublished 0.2.1 line as v7, so it
61
+ * pins that line rather than minting a third.
62
+ */
63
+ export const PUBLIC_BUNDLE_V8_VERIFICATION_COMMAND = PUBLIC_BUNDLE_V7_VERIFICATION_COMMAND;
64
+ export const PUBLIC_BUNDLE_V8_COMPATIBLE_VERIFICATION_COMMAND = PUBLIC_BUNDLE_V7_COMPATIBLE_VERIFICATION_COMMAND;
65
+ /** Every format through v6 stamps the same first public 0.1 line; v7 is the first that cannot. */
34
66
  export const PUBLIC_BUNDLE_VERIFICATION_INSTRUCTIONS = {
35
67
  [BUNDLE_FORMAT]: {
36
68
  command: PUBLIC_BUNDLE_VERIFICATION_COMMAND,
@@ -48,5 +80,13 @@ export const PUBLIC_BUNDLE_VERIFICATION_INSTRUCTIONS = {
48
80
  command: PUBLIC_BUNDLE_V6_VERIFICATION_COMMAND,
49
81
  compatibleCommand: PUBLIC_BUNDLE_V6_COMPATIBLE_VERIFICATION_COMMAND,
50
82
  },
83
+ [BUNDLE_V7_FORMAT]: {
84
+ command: PUBLIC_BUNDLE_V7_VERIFICATION_COMMAND,
85
+ compatibleCommand: PUBLIC_BUNDLE_V7_COMPATIBLE_VERIFICATION_COMMAND,
86
+ },
87
+ [BUNDLE_V8_FORMAT]: {
88
+ command: PUBLIC_BUNDLE_V8_VERIFICATION_COMMAND,
89
+ compatibleCommand: PUBLIC_BUNDLE_V8_COMPATIBLE_VERIFICATION_COMMAND,
90
+ },
51
91
  };
52
- import { BUNDLE_FORMAT, BUNDLE_V4_FORMAT, BUNDLE_V5_FORMAT, BUNDLE_V6_FORMAT } from "./manifest.js";
92
+ import { BUNDLE_FORMAT, BUNDLE_V4_FORMAT, BUNDLE_V5_FORMAT, BUNDLE_V6_FORMAT, BUNDLE_V7_FORMAT, BUNDLE_V8_FORMAT, } from "./manifest.js";