@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,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";
package/dist/schema.d.ts CHANGED
@@ -6,9 +6,9 @@ export declare const BUNDLE_ASSEMBLY_FORMAT: "benchmark-product-assembly/2";
6
6
  export declare const BUNDLE_V4_TRUST_FORMAT: "benchmark-product-public-trust/4";
7
7
  export declare const BUNDLE_V4_EVIDENCE_FORMAT: "benchmark-product-evidence-catalog/4";
8
8
  export declare const BUNDLE_QUALIFICATION_FORMAT: "benchmark-product-binary-qualification/1";
9
- export declare const BUNDLE_V4_EVIDENCE_ROLES: readonly ["task", "runtime-selection", "evaluation-spec", "admission-receipt", "solve-submission", "run-pinning-evidence", "evaluation-submission", "solve-delivery", "solve-output", "evaluation-task", "evaluation-delivery", "verdict", "item-bank", "source-manifest", "admission-index", "admission-manifest", "replacement-ledger", "source-item", "judge-instrument", "analysis-context", "label-resolution", "human-review-evaluation-spec", "human-review-form", "human-review-packet", "human-review-response", "human-review-verdict", "reviewer-roster", "review-visibility-receipt", "review-reveal-receipt", "operator-assertion", "snapshot-probe", "screening-table", "screening-reveal-receipt", "screening-instrument", "screening-sampling-script", "screening-raw-outputs"];
9
+ export declare const BUNDLE_V4_EVIDENCE_ROLES: readonly ["task", "runtime-selection", "evaluation-spec", "admission-receipt", "solve-submission", "run-pinning-evidence", "evaluation-submission", "solve-delivery", "solve-output", "evaluation-task", "evaluation-delivery", "verdict", "item-bank", "source-manifest", "admission-index", "admission-manifest", "replacement-ledger", "source-item", "judge-instrument", "analysis-context", "label-resolution", "human-review-evaluation-spec", "human-review-form", "human-review-packet", "human-review-response", "human-review-verdict", "reviewer-roster", "review-visibility-receipt", "review-reveal-receipt", "operator-assertion", "snapshot-probe", "screening-table", "screening-reveal-receipt", "screening-instrument", "screening-sampling-script", "screening-raw-outputs", "screening-prompt", "screening-procedure", "screening-pool", "screening-sample-commitment", "screening-transcript", "disclosure-specification"];
10
10
  export type BundleV4EvidenceRole = (typeof BUNDLE_V4_EVIDENCE_ROLES)[number];
11
- export declare const BUNDLE_V4_ADMISSION_EVIDENCE_ROLES: readonly ["admission-manifest", "replacement-ledger", "source-item", "label-resolution", "analysis-context", "human-review-evaluation-spec", "human-review-form", "human-review-packet", "human-review-response", "human-review-verdict", "reviewer-roster", "review-visibility-receipt", "review-reveal-receipt", "operator-assertion", "screening-table", "screening-reveal-receipt", "screening-instrument", "screening-sampling-script", "screening-raw-outputs"];
11
+ export declare const BUNDLE_V4_ADMISSION_EVIDENCE_ROLES: readonly ["admission-manifest", "replacement-ledger", "source-item", "label-resolution", "analysis-context", "human-review-evaluation-spec", "human-review-form", "human-review-packet", "human-review-response", "human-review-verdict", "reviewer-roster", "review-visibility-receipt", "review-reveal-receipt", "operator-assertion", "screening-table", "screening-reveal-receipt", "screening-instrument", "screening-sampling-script", "screening-raw-outputs", "screening-prompt", "screening-procedure", "screening-pool", "screening-sample-commitment", "screening-transcript"];
12
12
  export declare const BundleTrustSchema: z.ZodObject<{
13
13
  format: z.ZodLiteral<"benchmark-product-public-trust/2">;
14
14
  selfRun: z.ZodObject<{
@@ -100,6 +100,7 @@ export declare const BundleV4EvidenceCatalogSchema: z.ZodObject<{
100
100
  records: z.ZodArray<z.ZodObject<{
101
101
  sha256: z.ZodString;
102
102
  roles: z.ZodArray<z.ZodEnum<{
103
+ "disclosure-specification": "disclosure-specification";
103
104
  task: "task";
104
105
  "runtime-selection": "runtime-selection";
105
106
  "evaluation-spec": "evaluation-spec";
@@ -136,6 +137,11 @@ export declare const BundleV4EvidenceCatalogSchema: z.ZodObject<{
136
137
  "screening-instrument": "screening-instrument";
137
138
  "screening-sampling-script": "screening-sampling-script";
138
139
  "screening-raw-outputs": "screening-raw-outputs";
140
+ "screening-prompt": "screening-prompt";
141
+ "screening-procedure": "screening-procedure";
142
+ "screening-pool": "screening-pool";
143
+ "screening-sample-commitment": "screening-sample-commitment";
144
+ "screening-transcript": "screening-transcript";
139
145
  }>>;
140
146
  }, z.core.$strict>>;
141
147
  }, z.core.$strict>;
@@ -197,6 +203,11 @@ export declare const BundleQualificationSchema: z.ZodObject<{
197
203
  "screening-instrument": "screening-instrument";
198
204
  "screening-sampling-script": "screening-sampling-script";
199
205
  "screening-raw-outputs": "screening-raw-outputs";
206
+ "screening-prompt": "screening-prompt";
207
+ "screening-procedure": "screening-procedure";
208
+ "screening-pool": "screening-pool";
209
+ "screening-sample-commitment": "screening-sample-commitment";
210
+ "screening-transcript": "screening-transcript";
200
211
  }>>;
201
212
  }, z.core.$strict>>;
202
213
  reachableSha256s: z.ZodArray<z.ZodString>;
package/dist/schema.js CHANGED
@@ -52,6 +52,22 @@ export const BUNDLE_V4_EVIDENCE_ROLES = [
52
52
  "screening-instrument",
53
53
  "screening-sampling-script",
54
54
  "screening-raw-outputs",
55
+ "screening-prompt",
56
+ "screening-procedure",
57
+ "screening-pool",
58
+ "screening-sample-commitment",
59
+ "screening-transcript",
60
+ // Appended at the very end (disclosure-specification-record design §6.2, issue #2839). Appending
61
+ // is the ONLY additive move available here: this array's index order is the frozen role-ordering
62
+ // map used by both the catalog schema's ordering refinement and the bundle writer, so inserting
63
+ // anywhere earlier would re-order existing bundles' role arrays and move their bytes. The /2
64
+ // catalog is a SEPARATE constant (`BUNDLE_EVIDENCE_ROLES` below) and is untouched by this append.
65
+ //
66
+ // The token is admitted on every v4-graph closure, because the enum is one shared constant and
67
+ // there is no per-closure role vocabulary. What keeps a disclosure record off an earlier closure
68
+ // is that nothing DERIVES the role there: off `/8` the Report extension is never read, so the
69
+ // evidence closure's own size and per-digest guards refuse the record as unreachable (§6.5.2).
70
+ "disclosure-specification",
55
71
  ];
56
72
  export const BUNDLE_V4_ADMISSION_EVIDENCE_ROLES = [
57
73
  "admission-manifest", "replacement-ledger", "source-item", "label-resolution",
@@ -68,6 +84,11 @@ export const BUNDLE_V4_ADMISSION_EVIDENCE_ROLES = [
68
84
  "screening-instrument",
69
85
  "screening-sampling-script",
70
86
  "screening-raw-outputs",
87
+ "screening-prompt",
88
+ "screening-procedure",
89
+ "screening-pool",
90
+ "screening-sample-commitment",
91
+ "screening-transcript",
71
92
  ];
72
93
  const PublicKeySchema = z.object({
73
94
  keyId: z.string().min(1),
@@ -205,6 +226,12 @@ const PrefixedSha256Schema = z.string().regex(/^sha256:[a-f0-9]{64}$/u);
205
226
  const IDENTIFIER_NAME = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/u;
206
227
  export const BundleQualificationSchema = z.strictObject({
207
228
  format: z.literal(BUNDLE_QUALIFICATION_FORMAT),
229
+ // Deliberately NOT widened when a later closure allocates a new claim id (issue #3205's
230
+ // claim-package/5; the same resolution the disclosure-record design reached in its §6.5.1).
231
+ // This field names WHICH CLAIM PROJECTION SHAPE the qualification graph was built for, and the
232
+ // anchors section changes nothing about that graph: the qualification projection under /5 is
233
+ // byte-identical to the one under /2. Widening the literal would make this schema co-vary with
234
+ // an unrelated section, and would need widening again for every future closure.
208
235
  claimSchema: z.literal("benchmark-product.claim-package/2"),
209
236
  sourceManifestSha256: PrefixedSha256Schema,
210
237
  admissionManifestSha256: PrefixedSha256Schema,
@@ -317,12 +344,11 @@ export const BundleQualificationSchema = z.strictObject({
317
344
  "human-review-packet", "human-review-response", "human-review-verdict", "reviewer-roster",
318
345
  "review-visibility-receipt", "review-reveal-receipt",
319
346
  ];
320
- const screeningEvidenceRoles = [
321
- "screening-table",
322
- "screening-reveal-receipt",
323
- "screening-instrument",
324
- "screening-sampling-script",
325
- "screening-raw-outputs",
347
+ const commonScreeningEvidenceRoles = ["screening-table", "screening-reveal-receipt", "screening-sampling-script"];
348
+ const legacyScreeningEvidenceRoles = ["screening-instrument", "screening-raw-outputs"];
349
+ const promptedScreeningEvidenceRoles = [
350
+ "screening-prompt", "screening-procedure", "screening-pool",
351
+ "screening-sample-commitment", "screening-transcript",
326
352
  ];
327
353
  if (qualification.truthAdmission === "two-human-unanimous") {
328
354
  if (humanEvidenceRoles.some((role) => (roleCounts.get(role) ?? 0) === 0)
@@ -340,10 +366,15 @@ export const BundleQualificationSchema = z.strictObject({
340
366
  // silently inheriting the screened one's evidence rules.
341
367
  }
342
368
  else if (qualification.truthAdmission === "screened-operator-sampled") {
343
- if (screeningEvidenceRoles.some((role) => (roleCounts.get(role) ?? 0) !== 1)
369
+ const legacyRoleSet = legacyScreeningEvidenceRoles.every((role) => roleCounts.get(role) === 1)
370
+ && promptedScreeningEvidenceRoles.every((role) => roleCounts.get(role) === 0);
371
+ const promptedRoleSet = promptedScreeningEvidenceRoles.every((role) => roleCounts.get(role) === 1)
372
+ && legacyScreeningEvidenceRoles.every((role) => roleCounts.get(role) === 0);
373
+ if (commonScreeningEvidenceRoles.some((role) => (roleCounts.get(role) ?? 0) !== 1)
374
+ || (!legacyRoleSet && !promptedRoleSet)
344
375
  || humanEvidenceRoles.some((role) => (roleCounts.get(role) ?? 0) !== 0)
345
376
  || (roleCounts.get("operator-assertion") ?? 0) !== 0) {
346
- ctx.addIssue({ code: "custom", path: ["admissionRecords"], message: "screened admission must carry exactly one of every screening record and no human-review evidence or operator assertion" });
377
+ ctx.addIssue({ code: "custom", path: ["admissionRecords"], message: "screened admission must carry exactly one complete legacy-v1 or prompted-v2 screening record set and no human-review evidence or operator assertion" });
347
378
  }
348
379
  const screeningInstrument = qualification.admissionRecords.find((entry) => (entry.roles.includes("screening-instrument")));
349
380
  if (screeningInstrument !== undefined
@@ -0,0 +1,48 @@
1
+ import type { BundleTrust, BundleV4Trust } from "./schema.js";
2
+ /**
3
+ * Who signed, in reader terms. The bundle's own identifiers select signatures and join records,
4
+ * so they stay exact on the machine surface; the human surface is given the role instead
5
+ * (issue #3024).
6
+ */
7
+ export type PublicBundleSignerRole = "publisher" | "automated-grader" | "human-reviewer" | "label-admission";
8
+ export interface PublicBundleSigner {
9
+ readonly role: PublicBundleSignerRole;
10
+ /** The load-bearing signer identity, verbatim. Machine surface only. */
11
+ readonly identity: string;
12
+ /** The exact key the signature selection resolved to. Machine surface only. */
13
+ readonly keyId: string;
14
+ /**
15
+ * `same-operator` where the bundle itself declares that the key is workspace-minted under the
16
+ * publisher's own custody; `undeclared` where it makes no custody statement. Never a claim that
17
+ * a signer is an independent party -- no bundle format can establish that.
18
+ */
19
+ readonly custody: "same-operator" | "undeclared";
20
+ }
21
+ /**
22
+ * Signers of a v2/v4/v6/v7 bundle, read from its already-authenticated `trust/public-keys.json`
23
+ * and the Matrix verdict closure the `trust` check validated it against.
24
+ *
25
+ * Every group this returns is ground-truthed rather than merely declared: `trust.evaluators` is
26
+ * pinned to exactly the union of the Matrix-referenced evaluators and the admission reviewers, and
27
+ * each reviewer binding to the authenticated signed review closure. That union is why the two roles
28
+ * are read from the two sets separately -- an identity that both graded cells and signed reviews is
29
+ * in both, and classifying reviewer-first would print one fewer grader than actually graded. Both
30
+ * trust grammars pin `selfRun.custody: "workspace-minted"` and `partyIndependence:
31
+ * "not-established"`, so same-operator custody is a fact of the parsed record, not an inference.
32
+ *
33
+ * Admission authorities are deliberately absent: the v4 grammar pins every authority keyId to the
34
+ * report key, so listing them would count the publisher's one key twice under two headings.
35
+ */
36
+ export declare function legacyBundleSigners(trust: BundleTrust | BundleV4Trust, verdictEvaluators: ReadonlySet<string>): readonly PublicBundleSigner[];
37
+ /**
38
+ * Signers of a v5 evidence-native bundle, read from the same authenticated `claim-package.json`
39
+ * bytes the closure verified.
40
+ *
41
+ * `claim.trust.signers` is a publisher-written declaration used by the closure only as a lookup
42
+ * table: a surplus entry there is never contradicted, because no signature ever selects it. So this
43
+ * keeps only the keys that actually carried a signature the closure accepted
44
+ * (`verifiedSignerKeyIds`). Without that filter a bundle could declare a human reviewer, or a dozen
45
+ * graders, that signed nothing and have the checker print them as fact. The claim package declares
46
+ * no custody, so nothing here upgrades a signer to same-operator either.
47
+ */
48
+ export declare function evidenceNativeBundleSigners(claimPackageBytes: Uint8Array, verifiedSignerKeyIds: readonly string[]): readonly PublicBundleSigner[];
@@ -0,0 +1,77 @@
1
+ import { parseEvidenceNativeClaimPackageV3 } from "@jinn-network/benchmarking-protocol";
2
+ function deduplicate(signers) {
3
+ const seen = new Set();
4
+ return signers.filter((signer) => {
5
+ const key = `${signer.role} ${signer.keyId}`;
6
+ if (seen.has(key))
7
+ return false;
8
+ seen.add(key);
9
+ return true;
10
+ });
11
+ }
12
+ /**
13
+ * Signers of a v2/v4/v6/v7 bundle, read from its already-authenticated `trust/public-keys.json`
14
+ * and the Matrix verdict closure the `trust` check validated it against.
15
+ *
16
+ * Every group this returns is ground-truthed rather than merely declared: `trust.evaluators` is
17
+ * pinned to exactly the union of the Matrix-referenced evaluators and the admission reviewers, and
18
+ * each reviewer binding to the authenticated signed review closure. That union is why the two roles
19
+ * are read from the two sets separately -- an identity that both graded cells and signed reviews is
20
+ * in both, and classifying reviewer-first would print one fewer grader than actually graded. Both
21
+ * trust grammars pin `selfRun.custody: "workspace-minted"` and `partyIndependence:
22
+ * "not-established"`, so same-operator custody is a fact of the parsed record, not an inference.
23
+ *
24
+ * Admission authorities are deliberately absent: the v4 grammar pins every authority keyId to the
25
+ * report key, so listing them would count the publisher's one key twice under two headings.
26
+ */
27
+ export function legacyBundleSigners(trust, verdictEvaluators) {
28
+ const reviewers = new Set("admission" in trust ? trust.admission.reviewers.map((entry) => entry.evaluator) : []);
29
+ return deduplicate([
30
+ { role: "publisher", identity: trust.report.author, keyId: trust.report.keyId, custody: "same-operator" },
31
+ ...trust.evaluators.flatMap((entry) => {
32
+ const roles = [];
33
+ // The trust set equals verdicts + reviewers, so "in neither" is unreachable; if a future
34
+ // widening makes it reachable, a signer must still be shown rather than silently dropped.
35
+ if (verdictEvaluators.has(entry.evaluator) || !reviewers.has(entry.evaluator))
36
+ roles.push("automated-grader");
37
+ if (reviewers.has(entry.evaluator))
38
+ roles.push("human-reviewer");
39
+ return roles.map((role) => ({
40
+ role,
41
+ identity: entry.evaluator,
42
+ keyId: entry.keyId,
43
+ custody: "same-operator",
44
+ }));
45
+ }),
46
+ ]);
47
+ }
48
+ // Keyed to the purpose union rather than to `string`, so a protocol widening is a build failure
49
+ // here instead of an `undefined` role reaching the reader's screen.
50
+ const EVIDENCE_NATIVE_PURPOSE_ROLES = {
51
+ report: "publisher",
52
+ "automated-evaluator": "automated-grader",
53
+ "human-reviewer": "human-reviewer",
54
+ "label-admission": "label-admission",
55
+ };
56
+ /**
57
+ * Signers of a v5 evidence-native bundle, read from the same authenticated `claim-package.json`
58
+ * bytes the closure verified.
59
+ *
60
+ * `claim.trust.signers` is a publisher-written declaration used by the closure only as a lookup
61
+ * table: a surplus entry there is never contradicted, because no signature ever selects it. So this
62
+ * keeps only the keys that actually carried a signature the closure accepted
63
+ * (`verifiedSignerKeyIds`). Without that filter a bundle could declare a human reviewer, or a dozen
64
+ * graders, that signed nothing and have the checker print them as fact. The claim package declares
65
+ * no custody, so nothing here upgrades a signer to same-operator either.
66
+ */
67
+ export function evidenceNativeBundleSigners(claimPackageBytes, verifiedSignerKeyIds) {
68
+ const verified = new Set(verifiedSignerKeyIds);
69
+ return deduplicate(parseEvidenceNativeClaimPackageV3(claimPackageBytes).trust.signers
70
+ .filter((signer) => verified.has(signer.keyId))
71
+ .map((signer) => ({
72
+ role: EVIDENCE_NATIVE_PURPOSE_ROLES[signer.purpose],
73
+ identity: signer.identity,
74
+ keyId: signer.keyId,
75
+ custody: "undeclared",
76
+ })));
77
+ }
package/dist/verify.d.ts CHANGED
@@ -1,15 +1,21 @@
1
1
  import { type EvidenceNativePortableBundleVerification } from "@jinn-network/benchmarking-evidence";
2
+ import { type PublicBundleSigner } from "./signers.js";
2
3
  import { type InspectRuntimeMethodDisclosure } from "./profile/inspect-disclosure.js";
3
4
  import { type PublicComparisonView } from "./comparison.js";
4
5
  import { type VerifiedBundleSnapshot, type VerifyBundleSnapshotDeps } from "./manifest.js";
6
+ import { type DisclosureSpecificationReport } from "./profile/disclosure.js";
5
7
  import { type IntegrityAnchorsReport, type PublicBundleAnchorTrustMaterial } from "./anchor/check.js";
6
8
  import { type BinaryItemBankEntry, type BinarySourceManifestEntry } from "./admission/intake.js";
7
9
  export type PublicBundleVerificationCheck = "manifest" | "evidence-closure" | "trust" | "matrix-rederivation" | "report-verification" | "claim-consistency"
8
- /** Always present for `benchmark-product-public-bundle/6`, never for any earlier closure
9
- * (anchor-evidence design §8, §12). */
10
- | "integrity-anchors";
11
- export interface LegacyPublicBundleVerificationResult {
12
- readonly format: "benchmark-product-public-bundle/2" | "benchmark-product-public-bundle/4" | "benchmark-product-public-bundle/6";
10
+ /** Always present for the anchored closures, `benchmark-product-public-bundle/6`, `/7`, and `/8`,
11
+ * never for any earlier one (anchor-evidence design §8, §12). */
12
+ | "integrity-anchors"
13
+ /** Always present for `benchmark-product-public-bundle/8`, never for any earlier closure
14
+ * (disclosure-specification-record design §7, issue #2839). Runs last: the claim's `disclosure`
15
+ * section is among the things it depends on having already been byte-compared. */
16
+ | "disclosure-specification";
17
+ export interface LegacyPublicBundleVerificationResult extends PublicBundleSignerDisclosure {
18
+ readonly format: "benchmark-product-public-bundle/2" | "benchmark-product-public-bundle/4" | "benchmark-product-public-bundle/6" | "benchmark-product-public-bundle/7" | "benchmark-product-public-bundle/8";
13
19
  readonly identity: string;
14
20
  readonly checks: readonly PublicBundleVerificationCheck[];
15
21
  readonly benchmarkSha256: string;
@@ -22,6 +28,10 @@ export interface LegacyPublicBundleVerificationResult {
22
28
  * subject's context outcome (anchor-evidence design §8). Statuses are disclosed facts, not a
23
29
  * summary — nothing here is folded into a single verified badge. */
24
30
  readonly anchors?: IntegrityAnchorsReport;
31
+ /** Present exactly for the disclosed closure (issue #2839, design §7 step 11): the sealed record's
32
+ * identity and all six variables' statuses. Statuses are DISCLOSED FACTS, never folded into a
33
+ * single badge and never counted — the same posture `anchors` takes, and for the same reason. */
34
+ readonly disclosure?: DisclosureSpecificationReport;
25
35
  readonly qualification?: {
26
36
  readonly publicationGrade: boolean;
27
37
  readonly truthAdmission: "two-human-unanimous" | "operator-only" | "screened-operator-sampled";
@@ -32,7 +42,11 @@ export interface LegacyPublicBundleVerificationResult {
32
42
  readonly exclusionCount: number;
33
43
  };
34
44
  }
35
- export type PublicBundleVerificationResult = LegacyPublicBundleVerificationResult | EvidenceNativePortableBundleVerification;
45
+ /** Who signed the bundle, with the identifiers the human surface deliberately does not print. */
46
+ export interface PublicBundleSignerDisclosure {
47
+ readonly signers?: readonly PublicBundleSigner[];
48
+ }
49
+ export type PublicBundleVerificationResult = LegacyPublicBundleVerificationResult | (EvidenceNativePortableBundleVerification & PublicBundleSignerDisclosure);
36
50
  export interface VerifyPublicBundleDeps extends VerifyBundleSnapshotDeps {
37
51
  /**
38
52
  * Trust material for the `integrity-anchors` check (anchor-evidence design §8 step 3): timestamp