@colophon-claims/verify 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +35 -0
  2. package/dist/admission/application.d.ts +18 -0
  3. package/dist/admission/application.js +53 -0
  4. package/dist/admission/contracts.d.ts +264 -0
  5. package/dist/admission/contracts.js +290 -0
  6. package/dist/admission/index.d.ts +5 -0
  7. package/dist/admission/index.js +5 -0
  8. package/dist/admission/intake.d.ts +75 -0
  9. package/dist/admission/intake.js +70 -0
  10. package/dist/admission/result-evaluation.d.ts +18 -0
  11. package/dist/admission/result-evaluation.js +71 -0
  12. package/dist/admission/screening-sample.d.ts +56 -0
  13. package/dist/admission/screening-sample.js +126 -0
  14. package/dist/admission/verification.d.ts +89 -0
  15. package/dist/admission/verification.js +709 -0
  16. package/dist/anchor/check.d.ts +95 -0
  17. package/dist/anchor/check.js +146 -0
  18. package/dist/anchor/ports.d.ts +6 -0
  19. package/dist/anchor/ports.js +354 -0
  20. package/dist/assets.d.ts +33 -0
  21. package/dist/assets.js +885 -0
  22. package/dist/bin.d.ts +2 -0
  23. package/dist/bin.js +20 -0
  24. package/dist/cli.d.ts +14 -0
  25. package/dist/cli.js +205 -0
  26. package/dist/comparison.d.ts +64 -0
  27. package/dist/comparison.js +235 -0
  28. package/dist/index.d.ts +23 -0
  29. package/dist/index.js +23 -0
  30. package/dist/manifest.d.ts +54 -0
  31. package/dist/manifest.js +240 -0
  32. package/dist/materialize.d.ts +4 -0
  33. package/dist/materialize.js +13 -0
  34. package/dist/profile/admission-receipts.d.ts +11 -0
  35. package/dist/profile/admission-receipts.js +24 -0
  36. package/dist/profile/anchor-claims.d.ts +157 -0
  37. package/dist/profile/anchor-claims.js +330 -0
  38. package/dist/profile/artifacts.d.ts +261 -0
  39. package/dist/profile/artifacts.js +139 -0
  40. package/dist/profile/assembly-ports.d.ts +19 -0
  41. package/dist/profile/assembly-ports.js +42 -0
  42. package/dist/profile/binary-judge-manifest.d.ts +121 -0
  43. package/dist/profile/binary-judge-manifest.js +106 -0
  44. package/dist/profile/binary-qualification.d.ts +9 -0
  45. package/dist/profile/binary-qualification.js +52 -0
  46. package/dist/profile/branding-assets.d.ts +5 -0
  47. package/dist/profile/branding-assets.js +5 -0
  48. package/dist/profile/branding.d.ts +24 -0
  49. package/dist/profile/branding.js +14 -0
  50. package/dist/profile/claim-consistency.d.ts +26 -0
  51. package/dist/profile/claim-consistency.js +91 -0
  52. package/dist/profile/claim.d.ts +324 -0
  53. package/dist/profile/claim.js +724 -0
  54. package/dist/profile/errors.d.ts +49 -0
  55. package/dist/profile/errors.js +78 -0
  56. package/dist/profile/inspect-assurance.d.ts +16 -0
  57. package/dist/profile/inspect-assurance.js +30 -0
  58. package/dist/profile/inspect-disclosure.d.ts +28 -0
  59. package/dist/profile/inspect-disclosure.js +54 -0
  60. package/dist/profile/inspect-manifest.d.ts +706 -0
  61. package/dist/profile/inspect-manifest.js +397 -0
  62. package/dist/profile/isolation.d.ts +21 -0
  63. package/dist/profile/isolation.js +31 -0
  64. package/dist/profile/pinning-evidence.d.ts +41 -0
  65. package/dist/profile/pinning-evidence.js +43 -0
  66. package/dist/profile/ports.d.ts +3 -0
  67. package/dist/profile/ports.js +11 -0
  68. package/dist/profile/preview-log.d.ts +4 -0
  69. package/dist/profile/preview-log.js +3 -0
  70. package/dist/profile/run-results.d.ts +17 -0
  71. package/dist/profile/run-results.js +23 -0
  72. package/dist/profile/signing.d.ts +10 -0
  73. package/dist/profile/signing.js +46 -0
  74. package/dist/profile/trust.d.ts +13 -0
  75. package/dist/profile/trust.js +35 -0
  76. package/dist/profile/venue.d.ts +2 -0
  77. package/dist/profile/venue.js +2 -0
  78. package/dist/profile/verdict.d.ts +28 -0
  79. package/dist/profile/verdict.js +51 -0
  80. package/dist/reader-instructions.d.ts +41 -0
  81. package/dist/reader-instructions.js +52 -0
  82. package/dist/schema.d.ts +370 -0
  83. package/dist/schema.js +484 -0
  84. package/dist/verify.d.ts +72 -0
  85. package/dist/verify.js +1573 -0
  86. package/dist/version.d.ts +2 -0
  87. package/dist/version.js +2 -0
  88. package/package.json +63 -3
  89. package/schemas/assembly-row.schema.json +194 -0
  90. package/schemas/bundle-manifest.schema.json +23 -0
  91. package/schemas/claim-package.schema.json +279 -0
  92. package/schemas/dsse-envelope.schema.json +25 -0
  93. package/schemas/evidence-catalog.schema.json +43 -0
  94. package/schemas/public-trust.schema.json +44 -0
  95. package/schemas/verdict-catalog.schema.json +27 -0
  96. package/scripts/external-verify.py +238 -0
@@ -0,0 +1,724 @@
1
+ /**
2
+ * The claim package (BP-13 deliverable 2, spec §8.2): a pure builder + a zod schema + a writer
3
+ * for the standalone, portable document a skeptic reads to check a benchmark result without
4
+ * re-running anything.
5
+ *
6
+ * `buildClaimPackage` is PURE — no filesystem, no digest recomputation, no re-derivation of the
7
+ * sealed Report's own judgment. Every headline-or-comparison number, disclosure, limitation, and
8
+ * attrition figure is EXTRACTED verbatim from the records the caller already sealed and verified
9
+ * elsewhere (`../operations/report.ts`), never recomputed here — a claim package that silently
10
+ * disagreed with its own cited report would be worse than one that failed to build. Nothing is
11
+ * dropped: `conflicted`, `completeness`, and `attrition` are always present, even when they carry
12
+ * unwelcome numbers (a conflicted cell, an incomplete run) — spec §8.2's whole point is that the
13
+ * claim package cannot be more flattering than the sealed records it cites.
14
+ *
15
+ * P4b Task 5: `buildClaimPackage` DISPATCHES on the produced Report's `method.id` — wilson@1's
16
+ * per-arm `headline` and paired-delta@1's `comparison` are mutually exclusive siblings, exactly
17
+ * one of which is present on any given claim (the schema refuses a claim carrying neither). Same
18
+ * "extracted, never recomputed" posture regardless of which method produced the Report.
19
+ *
20
+ * BP-20 (spec §7.2): the optional `rehearsal` block names any disclosed preview that preceded
21
+ * this run's lock, verbatim from the caller's `previewDisclosure` input — used previews are named
22
+ * in the official record; its absence means no preview preceded lock. Same "extracted, never
23
+ * recomputed" posture as everything else this builder emits.
24
+ *
25
+ * BP-21 (spec §6): the required `assurance` block states the draft's preset (a product-policy
26
+ * label) AND the platform primitives it resolved to, plus the fixed agent-distinctness
27
+ * disclosure. The one deliberate exception to "never cross-check here": `buildClaimPackage`
28
+ * THROWS when the stated primitives disagree with what the sealed Run record's own policy
29
+ * carries — a claim stating primitives the sealed Run does not carry would be dishonest.
30
+ */
31
+ import { z } from "zod";
32
+ import { Buffer } from "node:buffer";
33
+ import { validateBinaryInstrumentQualificationProjection } from "@jinn-network/benchmarking-aggregate";
34
+ import { BENCHMARKING_METHOD_IDS, BENCHMARKING_METHOD_VERSION } from "@jinn-network/benchmarking-records";
35
+ import { canonicalJsonBytes } from "@jinn-network/trust-core";
36
+ import { PUBLIC_BUNDLE_COMPATIBLE_VERIFICATION_COMMAND, PUBLIC_BUNDLE_VERIFICATION_CHECKS as READER_VERIFICATION_CHECKS, PUBLIC_BUNDLE_VERIFICATION_COMMAND as READER_VERIFICATION_COMMAND, PUBLIC_BUNDLE_V6_CHECKS as READER_ANCHORED_VERIFICATION_CHECKS, PUBLIC_BUNDLE_V6_COMPATIBLE_VERIFICATION_COMMAND, PUBLIC_BUNDLE_V6_VERIFICATION_COMMAND, } from "../reader-instructions.js";
37
+ import { ClaimAnchorSchema, SELF_RUN_TRUST_ROOT, anchoredTrustRoot } from "./anchor-claims.js";
38
+ export const CLAIM_PACKAGE_SCHEMA_ID = "benchmark-product.claim-package/1";
39
+ export const BINARY_QUALIFICATION_CLAIM_PACKAGE_SCHEMA_ID = "benchmark-product.claim-package/2";
40
+ /**
41
+ * The anchored claim package (anchor-evidence design §7.4): claim-package/1 plus the `anchors`
42
+ * section, carried by `benchmark-product-public-bundle/6`. The allocation is the next free number
43
+ * in the classic path; the evidence-native claim-package/3 adopts the same anchor surface in its
44
+ * own later allocation.
45
+ */
46
+ export const ANCHORED_CLAIM_PACKAGE_SCHEMA_ID = "benchmark-product.claim-package/4";
47
+ export const BINARY_QUALIFICATION_VERIFICATION_COMMAND = "npx @colophon-claims/verify@0.1.0 <bundle-dir>";
48
+ export const BINARY_QUALIFICATION_COMPATIBLE_VERIFICATION_COMMAND = "npx @colophon-claims/verify@0.1 <bundle-dir>";
49
+ const Sha256HexSchema = z.string().regex(/^[a-f0-9]{64}$/, "must be a lowercase sha256 hex digest");
50
+ /** Mirrors `../run/preview-log.ts`'s own (unexported) `Rfc3339Schema` — restated here rather than
51
+ * reached across the module boundary for one regex. */
52
+ const Rfc3339Schema = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/, "must be an RFC3339 timestamp");
53
+ const WilsonIntervalSchema = z.object({ low: z.string(), high: z.string() });
54
+ const HeadlineArmSchema = z.object({
55
+ n: z.number().int().nonnegative(),
56
+ passRate: z.string(),
57
+ wilsonInterval: WilsonIntervalSchema,
58
+ });
59
+ const ConflictedSchema = z.object({
60
+ count: z.number().int().nonnegative(),
61
+ cellKeys: z.array(z.string()),
62
+ });
63
+ const PairedIntervalSchema = z.object({ alpha: z.string(), low: z.string(), high: z.string() });
64
+ /**
65
+ * paired-delta@1's comparison block (P4b Task 5), sibling to `headline` (BP-13 method dispatch).
66
+ * Carried verbatim from the method's own compute() output (`benchmarking/aggregate/src/
67
+ * registry.ts`'s pairedDeltaMethod) — never recomputed, per this module's header. `baseline`,
68
+ * `candidate`, and `verdictRule` are deliberately NOT repeated here: they already live in the
69
+ * claim's `method.parameters` block, sealed verbatim by `run/compile.ts`'s buildAnalysisPlan.
70
+ * `pairing`, `clustering`, and `bootstrap` carry richer nested shapes than a claim reader needs
71
+ * pinned exactly, so — like this schema's own `results` field — they are typed loosely rather
72
+ * than re-specified field-by-field.
73
+ */
74
+ const ComparisonSchema = z.object({
75
+ pairs: z.number().int().nonnegative(),
76
+ delta: z.string().nullable(),
77
+ interval: PairedIntervalSchema.nullable(),
78
+ reasons: z.array(z.string()),
79
+ pairing: z.object({ taskDigests: z.array(z.string()) }),
80
+ clustering: z.object({ basis: z.string(), clusters: z.number().int().nonnegative() }),
81
+ excluded: ConflictedSchema,
82
+ conflicted: ConflictedSchema,
83
+ bootstrap: z.unknown(),
84
+ });
85
+ /** `pairwise-disagreement@1` and `paired-majority-delta@1` (packet #2837, spec §7.1/§7.2a) both
86
+ * report intervals keyed `{lower, upper, alpha}` -- NOT `PairedIntervalSchema`'s `{low, high,
87
+ * alpha}`, which is `paired-delta@1`'s own key spelling. Reusing `PairedIntervalSchema` here would
88
+ * silently drop `lower`/`upper` at parse time (zod strips unrecognized keys by default) rather than
89
+ * refuse. Mirrors the identically-named schema in core's `report/claim.ts`. */
90
+ const JudgeIntervalSchema = z.object({ lower: z.string(), upper: z.string(), alpha: z.string() });
91
+ const ExclusionTripleSchema = z.object({
92
+ taskDigest: z.string().min(1),
93
+ armId: z.string().min(1),
94
+ reason: z.string().min(1),
95
+ });
96
+ /**
97
+ * `pairwise-disagreement@1`'s panel projection (spec §7.1), sibling to `comparison`/`headline`/
98
+ * `qualification` (BP-13 method dispatch). Carried verbatim from the method's own compute() output
99
+ * (`aggregate/src/pairwise-disagreement-method.ts`'s `computePairwiseDisagreement`) — never
100
+ * recomputed, per this module's header. No `baseline`/`candidate`: the method computes all
101
+ * unordered arm pairs in one pass and carries no chosen pair.
102
+ */
103
+ const DisagreementCountSchema = z.object({
104
+ n: z.number().int().nonnegative(),
105
+ disagreements: z.number().int().nonnegative(),
106
+ rate: z.string().nullable(),
107
+ interval: JudgeIntervalSchema.nullable(),
108
+ });
109
+ const PairwiseDisagreementSchema = z.object({
110
+ pairs: z.array(DisagreementCountSchema.extend({
111
+ armA: z.string().min(1),
112
+ armB: z.string().min(1),
113
+ byCandidateClass: z.array(DisagreementCountSchema.extend({ candidateClass: z.string().min(1) })),
114
+ byStratum: z.array(DisagreementCountSchema.extend({ stratum: z.string().min(1) })),
115
+ exclusions: z.array(ExclusionTripleSchema),
116
+ })),
117
+ conflicted: ConflictedSchema,
118
+ });
119
+ /**
120
+ * `paired-majority-delta@1`'s evidence-contrast projection (spec §7.2a), sibling to `comparison`/
121
+ * `pairwiseDisagreement`. Carried verbatim from the method's own compute() output (`aggregate/src/
122
+ * paired-majority-delta-method.ts`'s `computePairedMajorityDelta`) — never recomputed. `baseline`
123
+ * is the evidence-declaring arm's evidence-free twin and `candidate` is the evidence-declaring arm
124
+ * itself (coordinator ruling, packet #2837): a positive `delta` means declaring evidence increased
125
+ * agreement.
126
+ */
127
+ const DeltaProjectionSchema = z.object({
128
+ n: z.number().int().nonnegative(),
129
+ delta: z.string().nullable(),
130
+ interval: JudgeIntervalSchema.nullable(),
131
+ reasons: z.array(z.string()),
132
+ });
133
+ const PairedMajorityDeltaSchema = DeltaProjectionSchema.extend({
134
+ baseline: z.string().min(1),
135
+ candidate: z.string().min(1),
136
+ // `manifest` is optional (M4): the projection omits the key entirely when the method emitted
137
+ // none, rather than writing `manifest: undefined` into an object that is canonical-JSON encoded
138
+ // on both sides of the mirror. Present-and-absent must both be expressible for the two copies to
139
+ // agree byte-for-byte.
140
+ clusters: z.object({ count: z.number().int().nonnegative(), manifest: z.unknown().optional() }),
141
+ byCandidateClass: z.array(DeltaProjectionSchema.extend({ candidateClass: z.string().min(1) })),
142
+ byStratum: z.array(DeltaProjectionSchema.extend({ stratum: z.string().min(1) })),
143
+ exclusions: z.array(ExclusionTripleSchema),
144
+ conflicted: ConflictedSchema,
145
+ });
146
+ const IntegrityTierCountsSchema = z.object({
147
+ "re-derivable": z.number().int().nonnegative(),
148
+ "attested-only": z.number().int().nonnegative(),
149
+ });
150
+ const PinningUnverifiableCountsSchema = z.object({
151
+ harness: z.number().int().nonnegative(),
152
+ model: z.number().int().nonnegative(),
153
+ loadout: z.number().int().nonnegative(),
154
+ isolation: z.number().int().nonnegative(),
155
+ });
156
+ /** The report's own disclosures block, carried whole, plus the two convenience summaries this
157
+ * module adds (never a substitute for reading `perSubject` itself). */
158
+ const DisclosuresSchema = z.object({
159
+ perSubject: z.array(z.unknown()),
160
+ integrityTierCounts: IntegrityTierCountsSchema,
161
+ pinningUnverifiableCounts: PinningUnverifiableCountsSchema,
162
+ });
163
+ /** The fixed sentence every claim package's assurance block carries (BP-21, spec §6/§7.1): what
164
+ * distinct evaluator identities on this venue actually prove. Never softened per-claim. */
165
+ export const ASSURANCE_DISCLOSURE = "Distinct evaluator identities are workspace-minted keys; they prove agent-distinctness, "
166
+ + "not party-independence, on this self-run venue.";
167
+ const ResolvedAssuranceSchema = z.object({
168
+ independence: z.enum(["gating", "disclosed"]),
169
+ minVerdicts: z.number().int().min(1),
170
+ distinctEvaluator: z.boolean(),
171
+ verdictRule: z.enum(["sole", "majority", "unanimous"]),
172
+ });
173
+ /** BP-21 (spec §6): the claim states the assurance preset AND the platform primitives it resolved
174
+ * to, never the label alone — preset names are product policy, the primitives are what the sealed
175
+ * Run actually carries (`buildClaimPackage` throws if the two disagree). */
176
+ const AssuranceBlockSchema = z.object({
177
+ /** Product preset label (product policy) — never a substitute for `resolved`. */
178
+ preset: z.string().min(1),
179
+ resolved: ResolvedAssuranceSchema,
180
+ /** The fixed agent-distinctness disclosure (`ASSURANCE_DISCLOSURE`). */
181
+ disclosure: z.string().min(1),
182
+ });
183
+ const ClaimPackageWireSchema = z.object({
184
+ claimSchema: z.union([
185
+ z.literal(CLAIM_PACKAGE_SCHEMA_ID),
186
+ z.literal(BINARY_QUALIFICATION_CLAIM_PACKAGE_SCHEMA_ID),
187
+ z.literal(ANCHORED_CLAIM_PACKAGE_SCHEMA_ID),
188
+ ]),
189
+ scope: z.object({
190
+ draftId: z.string().min(1),
191
+ benchmarkSha256: Sha256HexSchema,
192
+ taskCount: z.number().int().nonnegative(),
193
+ arms: z.array(z.object({ armId: z.string().min(1), pinning: z.record(z.string(), z.unknown()) })),
194
+ replicates: z.number().int().positive(),
195
+ venue: z.literal("self-run"),
196
+ }),
197
+ records: z.object({
198
+ benchmarkSha256: Sha256HexSchema,
199
+ runSha256: Sha256HexSchema,
200
+ matrixSha256: Sha256HexSchema,
201
+ reportSha256: Sha256HexSchema,
202
+ reportEnvelopeSha256: Sha256HexSchema,
203
+ }),
204
+ method: z.object({
205
+ id: z.string().min(1),
206
+ version: z.string().min(1),
207
+ parameters: z.record(z.string(), z.unknown()),
208
+ preregistered: z.boolean(),
209
+ }),
210
+ results: z.unknown(),
211
+ /** wilson@1's per-arm headline. Optional and additive (P4b Task 5, mirrors the `rehearsal`
212
+ * precedent below): a paired claim carries `comparison` instead — see the schema-level refine
213
+ * at the bottom of this object, which refuses a claim carrying neither. */
214
+ headline: z.record(z.string(), HeadlineArmSchema).optional(),
215
+ completeness: z.unknown(),
216
+ attrition: z.unknown(),
217
+ conflicted: ConflictedSchema,
218
+ assurance: AssuranceBlockSchema,
219
+ disclosures: DisclosuresSchema,
220
+ limitations: z.array(z.string()),
221
+ venueHonesty: z.unknown(),
222
+ verification: z.object({
223
+ command: z.string().min(1),
224
+ compatibleCommand: z.string().min(1),
225
+ checks: z.array(z.string().min(1)).min(1),
226
+ trustRoot: z.string().min(1),
227
+ }),
228
+ /** BP-20 (spec §7.2): present only when a preview preceded this run's lock — see module
229
+ * header. Optional and additive; absent on every claim package built before this change. */
230
+ rehearsal: z.object({
231
+ previewCount: z.number().int().positive(),
232
+ timestamps: z.array(Rfc3339Schema).min(1),
233
+ }).optional(),
234
+ /** P4b Task 5: present only for a paired-delta@1 Report, sibling to `headline`. See
235
+ * `ComparisonSchema`'s own comment. */
236
+ comparison: ComparisonSchema.optional(),
237
+ /** packet #2837: present only for a pairwise-disagreement@1 Report, sibling to `comparison`. See
238
+ * `PairwiseDisagreementSchema`'s own comment. */
239
+ pairwiseDisagreement: PairwiseDisagreementSchema.optional(),
240
+ /** packet #2837: present only for a paired-majority-delta@1 Report, sibling to `comparison`. See
241
+ * `PairedMajorityDeltaSchema`'s own comment. */
242
+ pairedMajorityDelta: PairedMajorityDeltaSchema.optional(),
243
+ /** binary-instrument@1's complete per-subject F6 result. This is copied exactly; it is not a
244
+ * headline, comparison, threshold, selection, or ranking projection. */
245
+ qualification: z.unknown().optional(),
246
+ /** anchor-evidence §7.4: one entry per AnchorEvidence record the bundle carries, in record-digest
247
+ * order, each carrying only the facts embedded in the proof's own bytes. Present exactly on
248
+ * claim-package/4 — the schema-level refine below refuses it on /1 and /2, so an unanchored claim
249
+ * cannot grow an anchors section and an anchored one cannot be published without it. */
250
+ anchors: z.array(ClaimAnchorSchema).optional(),
251
+ }).superRefine((claim, ctx) => {
252
+ if (claim.claimSchema !== ANCHORED_CLAIM_PACKAGE_SCHEMA_ID && claim.anchors !== undefined) {
253
+ ctx.addIssue({
254
+ code: "custom",
255
+ message: `only ${ANCHORED_CLAIM_PACKAGE_SCHEMA_ID} carries an anchors section`,
256
+ path: ["anchors"],
257
+ });
258
+ }
259
+ if (claim.claimSchema === ANCHORED_CLAIM_PACKAGE_SCHEMA_ID) {
260
+ if (claim.anchors === undefined) {
261
+ // Present, not necessarily non-empty. An empty section is legal on exactly one bundle: one
262
+ // whose sealed Run declared anchoring intent that no carried anchor satisfies (§7.3). That
263
+ // bundle must still be on the anchored closure so the check that reports the absence runs,
264
+ // and its claim must therefore still state the closure's seven checks. What no claim may do
265
+ // is omit the section while claiming the closure, or carry one while claiming an earlier one.
266
+ ctx.addIssue({
267
+ code: "custom",
268
+ message: `${ANCHORED_CLAIM_PACKAGE_SCHEMA_ID} must carry its anchors section, even when the section is empty`,
269
+ path: ["anchors"],
270
+ });
271
+ }
272
+ if (claim.qualification !== undefined) {
273
+ ctx.addIssue({
274
+ code: "custom",
275
+ message: "the anchored binary-qualification closure is a later allocation, not claim-package/4",
276
+ path: ["qualification"],
277
+ });
278
+ }
279
+ if (claim.headline === undefined
280
+ && claim.comparison === undefined
281
+ && claim.pairwiseDisagreement === undefined
282
+ && claim.pairedMajorityDelta === undefined) {
283
+ ctx.addIssue({
284
+ code: "custom",
285
+ message: "claim package must carry headline (wilson@1), comparison (paired-delta@1), "
286
+ + "pairwiseDisagreement (pairwise-disagreement@1), or pairedMajorityDelta (paired-majority-delta@1)",
287
+ path: ["headline"],
288
+ });
289
+ }
290
+ if (claim.verification.command !== PUBLIC_BUNDLE_V6_VERIFICATION_COMMAND
291
+ || claim.verification.compatibleCommand !== PUBLIC_BUNDLE_V6_COMPATIBLE_VERIFICATION_COMMAND) {
292
+ ctx.addIssue({ code: "custom", message: "anchored claim package must pin verifier 0.1.0/@0.1", path: ["verification"] });
293
+ }
294
+ if (claim.verification.checks.length !== READER_ANCHORED_VERIFICATION_CHECKS.length
295
+ || claim.verification.checks.some((check, index) => check !== READER_ANCHORED_VERIFICATION_CHECKS[index])) {
296
+ ctx.addIssue({
297
+ code: "custom",
298
+ message: "anchored claim package must retain the six frozen verification checks plus integrity-anchors, in order",
299
+ path: ["verification", "checks"],
300
+ });
301
+ }
302
+ return;
303
+ }
304
+ if (claim.claimSchema === CLAIM_PACKAGE_SCHEMA_ID) {
305
+ if (claim.headline === undefined
306
+ && claim.comparison === undefined
307
+ && claim.pairwiseDisagreement === undefined
308
+ && claim.pairedMajorityDelta === undefined) {
309
+ ctx.addIssue({
310
+ code: "custom",
311
+ message: "claim package must carry headline (wilson@1), comparison (paired-delta@1), "
312
+ + "pairwiseDisagreement (pairwise-disagreement@1), or pairedMajorityDelta (paired-majority-delta@1)",
313
+ path: ["headline"],
314
+ });
315
+ }
316
+ return;
317
+ }
318
+ if (claim.method.id !== BENCHMARKING_METHOD_IDS.binaryInstrument
319
+ || claim.method.version !== BENCHMARKING_METHOD_VERSION
320
+ || claim.qualification === undefined
321
+ || claim.headline !== undefined
322
+ || claim.comparison !== undefined
323
+ || claim.pairwiseDisagreement !== undefined
324
+ || claim.pairedMajorityDelta !== undefined) {
325
+ ctx.addIssue({
326
+ code: "custom",
327
+ message: "binary claim package must carry only the exact binary-instrument qualification projection",
328
+ path: ["qualification"],
329
+ });
330
+ }
331
+ if (claim.qualification === undefined)
332
+ return;
333
+ const qualificationValidation = validateBinaryInstrumentQualificationProjection(claim.qualification);
334
+ if (!qualificationValidation.ok) {
335
+ ctx.addIssue({
336
+ code: "custom",
337
+ message: `binary qualification is outside the exact F6 schema: ${qualificationValidation.issues.join("; ")}`,
338
+ path: ["qualification"],
339
+ });
340
+ return;
341
+ }
342
+ const reportResults = claim.results;
343
+ const subject = reportResults?.perSubject;
344
+ let exactResult = false;
345
+ const exactWrapper = (typeof reportResults === "object"
346
+ && reportResults !== null
347
+ && Object.keys(reportResults).join("\0") === "perSubject"
348
+ && subject?.length === 1
349
+ && typeof subject[0] === "object"
350
+ && subject[0] !== null
351
+ && Object.keys(subject[0]).sort().join("\0") === "results\0subjectSha256"
352
+ && subject[0].subjectSha256 === claim.records.matrixSha256);
353
+ if (exactWrapper) {
354
+ try {
355
+ exactResult = Buffer.from(canonicalJsonBytes(subject[0].results)).equals(Buffer.from(canonicalJsonBytes(claim.qualification)));
356
+ }
357
+ catch {
358
+ exactResult = false;
359
+ }
360
+ }
361
+ if (!exactResult) {
362
+ ctx.addIssue({ code: "custom", message: "qualification must exactly equal the Report's one F6 per-subject result", path: ["qualification"] });
363
+ }
364
+ if (claim.verification.command !== BINARY_QUALIFICATION_VERIFICATION_COMMAND
365
+ || claim.verification.compatibleCommand !== BINARY_QUALIFICATION_COMPATIBLE_VERIFICATION_COMMAND) {
366
+ ctx.addIssue({ code: "custom", message: "binary claim package must pin verifier 0.1.0/@0.1", path: ["verification"] });
367
+ }
368
+ if (claim.verification.checks.length !== READER_VERIFICATION_CHECKS.length
369
+ || claim.verification.checks.some((check, index) => check !== READER_VERIFICATION_CHECKS[index])) {
370
+ ctx.addIssue({ code: "custom", message: "binary claim package must retain the six frozen verification checks in order", path: ["verification", "checks"] });
371
+ }
372
+ });
373
+ /** claim-package/1 predates `qualification`; its original non-strict object grammar accepted and
374
+ * stripped that unknown key. Remove it before the additive v1/v2 wire schema sees the document so
375
+ * v1 retains that exact behavior while claim-package/2 validates and preserves the field. */
376
+ function exactKeys(value, allowed) {
377
+ return typeof value === "object" && value !== null && !Array.isArray(value)
378
+ && Object.keys(value).every((key) => allowed.includes(key));
379
+ }
380
+ function exactBinaryClaimControls(input) {
381
+ const scope = input.scope;
382
+ const records = input.records;
383
+ const method = input.method;
384
+ const assurance = input.assurance;
385
+ const disclosures = input.disclosures;
386
+ const verification = input.verification;
387
+ // `anchors`, `pairwiseDisagreement`, and `pairedMajorityDelta` are admitted here so a binary
388
+ // claim that grew one of them reaches the schema-level refine and is refused BY NAME (all three
389
+ // are checked there, alongside `headline`/`comparison`), rather than collapsing into the generic
390
+ // control-shape failure. Neither judge field is ever set on an actual binary-instrument claim
391
+ // (`methodProjection`'s dispatch is exclusive), so admitting them here is defense in depth, not
392
+ // a widening any real claim exercises.
393
+ return exactKeys(input, ["claimSchema", "scope", "records", "method", "results", "completeness", "attrition", "conflicted", "assurance", "disclosures", "limitations", "venueHonesty", "verification", "rehearsal", "qualification", "anchors", "pairwiseDisagreement", "pairedMajorityDelta"])
394
+ && exactKeys(scope, ["draftId", "benchmarkSha256", "taskCount", "arms", "replicates", "venue"])
395
+ && Array.isArray(scope.arms)
396
+ && (scope.arms).every((arm) => exactKeys(arm, ["armId", "pinning"]))
397
+ && exactKeys(records, ["benchmarkSha256", "runSha256", "matrixSha256", "reportSha256", "reportEnvelopeSha256"])
398
+ && exactKeys(method, ["id", "version", "parameters", "preregistered"])
399
+ && exactKeys(assurance, ["preset", "resolved", "disclosure"])
400
+ && exactKeys(assurance?.resolved, ["independence", "minVerdicts", "distinctEvaluator", "verdictRule"])
401
+ && exactKeys(disclosures, ["perSubject", "integrityTierCounts", "pinningUnverifiableCounts"])
402
+ && exactKeys(disclosures?.integrityTierCounts, ["re-derivable", "attested-only"])
403
+ && exactKeys(disclosures?.pinningUnverifiableCounts, ["harness", "model", "loadout", "isolation"])
404
+ && exactKeys(input.conflicted, ["count", "cellKeys"])
405
+ && exactKeys(verification, ["command", "compatibleCommand", "checks", "trustRoot"])
406
+ && (input.rehearsal === undefined || exactKeys(input.rehearsal, ["previewCount", "timestamps"]));
407
+ }
408
+ export const ClaimPackageSchema = z.preprocess((input) => {
409
+ if (typeof input === "object" && input !== null && !Array.isArray(input)
410
+ && input.claimSchema === BINARY_QUALIFICATION_CLAIM_PACKAGE_SCHEMA_ID
411
+ && !exactBinaryClaimControls(input)) {
412
+ return { claimSchema: "invalid-binary-claim-control-shape" };
413
+ }
414
+ if (typeof input === "object" && input !== null && !Array.isArray(input)
415
+ && input.claimSchema === CLAIM_PACKAGE_SCHEMA_ID) {
416
+ const { qualification: _legacyUnknown, ...legacy } = input;
417
+ return legacy;
418
+ }
419
+ return input;
420
+ }, ClaimPackageWireSchema);
421
+ /** Every method this product wires publishes its single Matrix subject's results at this fixed
422
+ * path — the wrapper itself is method-agnostic; only what's inside `results` differs by method. */
423
+ function singleSubjectResults(reportRecord) {
424
+ const results = reportRecord.results;
425
+ const perSubject = results?.perSubject;
426
+ if (!Array.isArray(perSubject) || perSubject.length !== 1) {
427
+ throw new Error("claim package: expected exactly one Report subject (this product's single-Matrix Report shape)");
428
+ }
429
+ return perSubject[0]?.results;
430
+ }
431
+ /** wilson@1's per-arm headline projection. A narrow, local shape check — deliberately not a
432
+ * general Method-results parser; a mismatch here means the sealed Report was not produced by
433
+ * wilson@1. */
434
+ function wilsonProjection(subjectResults) {
435
+ const shape = subjectResults;
436
+ if (typeof shape?.arms !== "object" || shape.arms === null
437
+ || typeof shape.conflicted?.count !== "number"
438
+ || !Array.isArray(shape.conflicted.cellKeys)) {
439
+ throw new Error("claim package: Report results do not carry wilson@1's arms/conflicted shape");
440
+ }
441
+ return {
442
+ headline: shape.arms,
443
+ conflicted: { count: shape.conflicted.count, cellKeys: shape.conflicted.cellKeys },
444
+ };
445
+ }
446
+ /** paired-delta@1's comparison projection (P4b Task 5), sibling to `wilsonProjection`. Carried
447
+ * verbatim from the method's own compute() output (`benchmarking/aggregate/src/registry.ts`'s
448
+ * pairedDeltaMethod) — never recomputed. Same narrow, local-shape-check posture as
449
+ * `wilsonProjection`: a mismatch here means the sealed Report was not produced by paired-delta@1. */
450
+ function comparisonProjection(subjectResults) {
451
+ const shape = subjectResults;
452
+ if (typeof shape?.pairs !== "number"
453
+ || !(typeof shape.delta === "string" || shape.delta === null)
454
+ || typeof shape.interval !== "object"
455
+ || !Array.isArray(shape.reasons)
456
+ || !Array.isArray(shape.pairing?.taskDigests)
457
+ || typeof shape.clustering?.basis !== "string"
458
+ || typeof shape.clustering.clusters !== "number"
459
+ || typeof shape.excluded?.count !== "number"
460
+ || !Array.isArray(shape.excluded.cellKeys)
461
+ || typeof shape.conflicted?.count !== "number"
462
+ || !Array.isArray(shape.conflicted.cellKeys)
463
+ || shape.bootstrap === undefined) {
464
+ throw new Error("claim package: Report results do not carry paired-delta@1's comparison shape");
465
+ }
466
+ const conflicted = { count: shape.conflicted.count, cellKeys: shape.conflicted.cellKeys };
467
+ return {
468
+ comparison: {
469
+ pairs: shape.pairs,
470
+ delta: shape.delta,
471
+ interval: shape.interval,
472
+ reasons: [...shape.reasons],
473
+ pairing: { taskDigests: [...shape.pairing.taskDigests] },
474
+ clustering: { basis: shape.clustering.basis, clusters: shape.clustering.clusters },
475
+ excluded: { count: shape.excluded.count, cellKeys: [...shape.excluded.cellKeys] },
476
+ conflicted: { count: conflicted.count, cellKeys: [...conflicted.cellKeys] },
477
+ bootstrap: shape.bootstrap,
478
+ },
479
+ conflicted,
480
+ };
481
+ }
482
+ function binaryQualificationProjection(subjectResults) {
483
+ const shape = subjectResults;
484
+ if (typeof shape?.conflicted?.count !== "number" || !Array.isArray(shape.conflicted.cellKeys)) {
485
+ throw new Error("claim package: Report results do not carry binary-instrument@1's conflicted shape");
486
+ }
487
+ return {
488
+ qualification: subjectResults,
489
+ conflicted: { count: shape.conflicted.count, cellKeys: shape.conflicted.cellKeys },
490
+ };
491
+ }
492
+ /** `pairwise-disagreement@1`'s panel projection (packet #2837, spec §7.1), sibling to
493
+ * `comparisonProjection`. Carried verbatim from the method's own compute() output
494
+ * (`aggregate/src/pairwise-disagreement-method.ts`'s `computePairwiseDisagreement`) — never
495
+ * recomputed. Same narrow, local-shape-check posture as `wilsonProjection`/`comparisonProjection`:
496
+ * a mismatch here means the sealed Report was not produced by pairwise-disagreement@1. This
497
+ * method's own output already carries `pairs` and top-level `conflicted` and nothing else, so the
498
+ * whole object is carried through rather than field-by-field reassembled the way
499
+ * `comparisonProjection` does for its richer sibling shape. Mirrors the identically-named function
500
+ * in core's `report/claim.ts`. */
501
+ function pairwiseDisagreementProjection(subjectResults) {
502
+ const shape = subjectResults;
503
+ if (!Array.isArray(shape?.pairs)
504
+ || typeof shape.conflicted?.count !== "number"
505
+ || !Array.isArray(shape.conflicted.cellKeys)) {
506
+ throw new Error("claim package: Report results do not carry pairwise-disagreement@1's pairs/conflicted shape");
507
+ }
508
+ const conflicted = { count: shape.conflicted.count, cellKeys: shape.conflicted.cellKeys };
509
+ return {
510
+ pairwiseDisagreement: {
511
+ pairs: shape.pairs,
512
+ conflicted: { count: conflicted.count, cellKeys: [...conflicted.cellKeys] },
513
+ },
514
+ conflicted,
515
+ };
516
+ }
517
+ /** `paired-majority-delta@1`'s evidence-contrast projection (packet #2837, spec §7.2a), sibling to
518
+ * `comparisonProjection`/`pairwiseDisagreementProjection`. Carried verbatim from the method's own
519
+ * compute() output (`aggregate/src/paired-majority-delta-method.ts`'s
520
+ * `computePairedMajorityDelta`) — never recomputed. Same narrow, local-shape-check posture as its
521
+ * siblings. Mirrors the identically-named function in core's `report/claim.ts`. */
522
+ function pairedMajorityDeltaProjection(subjectResults) {
523
+ const shape = subjectResults;
524
+ if (typeof shape?.baseline !== "string"
525
+ || typeof shape.candidate !== "string"
526
+ || typeof shape.n !== "number"
527
+ || !(typeof shape.delta === "string" || shape.delta === null)
528
+ || typeof shape.interval !== "object"
529
+ || !Array.isArray(shape.reasons)
530
+ || typeof shape.clusters?.count !== "number"
531
+ || !Array.isArray(shape.byCandidateClass)
532
+ || !Array.isArray(shape.byStratum)
533
+ || !Array.isArray(shape.exclusions)
534
+ || typeof shape.conflicted?.count !== "number"
535
+ || !Array.isArray(shape.conflicted.cellKeys)) {
536
+ throw new Error("claim package: Report results do not carry paired-majority-delta@1's baseline/candidate/delta shape");
537
+ }
538
+ const conflicted = { count: shape.conflicted.count, cellKeys: shape.conflicted.cellKeys };
539
+ return {
540
+ pairedMajorityDelta: {
541
+ baseline: shape.baseline,
542
+ candidate: shape.candidate,
543
+ n: shape.n,
544
+ delta: shape.delta,
545
+ interval: shape.interval,
546
+ reasons: [...shape.reasons],
547
+ // `manifest` is spread in only when present (M4): writing `manifest: undefined` puts an
548
+ // explicit undefined-valued key into an object that is canonical-JSON encoded on both sides
549
+ // of the mirror, and the two copies must agree byte-for-byte. An absent manifest stays
550
+ // absent rather than becoming a key with no value.
551
+ clusters: {
552
+ count: shape.clusters.count,
553
+ ...(shape.clusters.manifest === undefined ? {} : { manifest: shape.clusters.manifest }),
554
+ },
555
+ byCandidateClass: shape.byCandidateClass,
556
+ byStratum: shape.byStratum,
557
+ exclusions: shape.exclusions,
558
+ conflicted: { count: conflicted.count, cellKeys: [...conflicted.cellKeys] },
559
+ },
560
+ conflicted,
561
+ };
562
+ }
563
+ /** Dispatches on the produced Report's method (P4b Task 5) — the claim package's headline/
564
+ * comparison projection is no longer a mandatory wilson@1 gate. Any method this product has not
565
+ * wired a projection for throws rather than silently building an incomplete claim. */
566
+ function methodProjection(reportRecord) {
567
+ const subjectResults = singleSubjectResults(reportRecord);
568
+ switch (reportRecord.method.id) {
569
+ case BENCHMARKING_METHOD_IDS.wilson:
570
+ return wilsonProjection(subjectResults);
571
+ case BENCHMARKING_METHOD_IDS.pairedDelta:
572
+ return comparisonProjection(subjectResults);
573
+ case BENCHMARKING_METHOD_IDS.binaryInstrument:
574
+ if (reportRecord.method.version !== BENCHMARKING_METHOD_VERSION) {
575
+ throw new Error(`claim package: binary-instrument version "${reportRecord.method.version}" is not supported`);
576
+ }
577
+ return binaryQualificationProjection(subjectResults);
578
+ case BENCHMARKING_METHOD_IDS.pairwiseDisagreement:
579
+ if (reportRecord.method.version !== BENCHMARKING_METHOD_VERSION) {
580
+ throw new Error(`claim package: pairwise-disagreement version "${reportRecord.method.version}" is not supported`);
581
+ }
582
+ return pairwiseDisagreementProjection(subjectResults);
583
+ case BENCHMARKING_METHOD_IDS.pairedMajorityDelta:
584
+ if (reportRecord.method.version !== BENCHMARKING_METHOD_VERSION) {
585
+ throw new Error(`claim package: paired-majority-delta version "${reportRecord.method.version}" is not supported`);
586
+ }
587
+ return pairedMajorityDeltaProjection(subjectResults);
588
+ default:
589
+ throw new Error(`claim package: method "${reportRecord.method.id}" has no claim-package projection`);
590
+ }
591
+ }
592
+ function summedIntegrityTierCounts(perSubject) {
593
+ const counts = { "re-derivable": 0, "attested-only": 0 };
594
+ for (const subject of perSubject) {
595
+ counts["re-derivable"] += subject.integrityTiers["re-derivable"];
596
+ counts["attested-only"] += subject.integrityTiers["attested-only"];
597
+ }
598
+ return counts;
599
+ }
600
+ const PINNING_AXES = ["harness", "model", "loadout", "isolation"];
601
+ function summedPinningUnverifiableCounts(perSubject) {
602
+ const counts = { harness: 0, model: 0, loadout: 0, isolation: 0 };
603
+ for (const subject of perSubject) {
604
+ for (const axis of PINNING_AXES)
605
+ counts[axis] += subject.pinning[axis].unverifiable;
606
+ }
607
+ return counts;
608
+ }
609
+ export const CLAIM_VERIFICATION_CHECKS = READER_VERIFICATION_CHECKS;
610
+ /** The anchored closure's list: the six frozen checks plus `integrity-anchors` (§8). */
611
+ export const ANCHORED_CLAIM_VERIFICATION_CHECKS = READER_ANCHORED_VERIFICATION_CHECKS;
612
+ export const PUBLIC_BUNDLE_VERIFICATION_COMMAND = READER_VERIFICATION_COMMAND;
613
+ /** The unconditional trust-root sentence, re-exported unchanged. It is DEFINED once beside its
614
+ * anchored replacement in `anchor-claims.ts`: one sentence written out twice, in two mirrored
615
+ * files, is exactly the drift the conditional copy cannot afford. */
616
+ export { SELF_RUN_TRUST_ROOT };
617
+ /** Builds the claim package document (spec §8.2) from explicit, already-sealed record data. Pure
618
+ * — no filesystem access, no recomputation of anything the cited records already settled. */
619
+ export function buildClaimPackage(input) {
620
+ const { reportRecord } = input;
621
+ // BP-21: the assurance block may only state primitives the sealed Run record itself carries.
622
+ // `../run/compile.ts` seals the first three into `policy` at lock time and `verdictRule` into
623
+ // the analysisPlan entry matching the produced Report's method (BP-13 F2, P4b Task 5) — all
624
+ // four are checked against the sealed Run. The sealed plan may carry more than one entry (one
625
+ // per candidate method), so the entry is SELECTED by method match, not read at a fixed index.
626
+ const { resolved } = input.assurance;
627
+ const policy = input.runRecord.policy;
628
+ const matchingPlanEntry = input.runRecord.analysisPlan?.find((entry) => entry.method === reportRecord.method.id && entry.version === reportRecord.method.version);
629
+ const analysisParameters = matchingPlanEntry?.parameters;
630
+ if (policy.independence !== resolved.independence
631
+ || policy.evaluation.minVerdicts !== resolved.minVerdicts
632
+ || policy.evaluation.distinctEvaluator !== resolved.distinctEvaluator
633
+ || analysisParameters?.verdictRule !== resolved.verdictRule) {
634
+ throw new Error("claim package: the stated assurance primitives do not match what the sealed Run record carries"
635
+ + " — a claim stating primitives the sealed Run does not carry would be dishonest");
636
+ }
637
+ const projection = methodProjection(reportRecord);
638
+ const disclosuresPerSubject = (reportRecord.disclosures?.perSubject ?? []);
639
+ const taskCount = new Set(input.matrixRecord.cells.map((cell) => cell.taskDigest)).size;
640
+ // anchor-evidence §7.4: the anchored closure is entered by the CALLER, by supplying the section
641
+ // at all -- a run that carries an anchor, or one whose sealed Run declared intent whose absence
642
+ // the bundle must still report (§7.3). A caller that supplies nothing produces exactly the bytes
643
+ // this builder produced before the feature existed.
644
+ const anchors = input.anchors ?? [];
645
+ const anchored = input.anchors !== undefined;
646
+ if (anchored && projection.qualification !== undefined) {
647
+ throw new Error("claim package: the anchored binary-qualification closure is a later allocation"
648
+ + " — claim-package/4 has no qualification projection");
649
+ }
650
+ return {
651
+ claimSchema: anchored
652
+ ? ANCHORED_CLAIM_PACKAGE_SCHEMA_ID
653
+ : projection.qualification === undefined
654
+ ? CLAIM_PACKAGE_SCHEMA_ID
655
+ : BINARY_QUALIFICATION_CLAIM_PACKAGE_SCHEMA_ID,
656
+ scope: {
657
+ draftId: input.draftId,
658
+ benchmarkSha256: input.benchmarkSha256,
659
+ taskCount,
660
+ arms: input.runRecord.arms.map((arm) => ({ armId: arm.armId, pinning: arm.pinning })),
661
+ replicates: input.runRecord.replicates,
662
+ venue: "self-run",
663
+ },
664
+ records: {
665
+ benchmarkSha256: input.benchmarkSha256,
666
+ runSha256: input.runSha256,
667
+ matrixSha256: input.matrixSha256,
668
+ reportSha256: input.reportSha256,
669
+ reportEnvelopeSha256: input.reportEnvelopeSha256,
670
+ },
671
+ method: {
672
+ id: reportRecord.method.id,
673
+ version: reportRecord.method.version,
674
+ parameters: reportRecord.method.parameters,
675
+ preregistered: reportRecord.preregistered ?? false,
676
+ },
677
+ results: reportRecord.results,
678
+ ...(projection.headline !== undefined ? { headline: projection.headline } : {}),
679
+ ...(projection.qualification !== undefined ? { qualification: projection.qualification } : {}),
680
+ completeness: input.matrixRecord.completeness,
681
+ attrition: input.matrixRecord.attrition,
682
+ conflicted: { count: projection.conflicted.count, cellKeys: [...projection.conflicted.cellKeys] },
683
+ assurance: {
684
+ preset: input.assurance.preset,
685
+ resolved: { ...resolved },
686
+ disclosure: ASSURANCE_DISCLOSURE,
687
+ },
688
+ disclosures: {
689
+ perSubject: (reportRecord.disclosures?.perSubject ?? []),
690
+ integrityTierCounts: summedIntegrityTierCounts(disclosuresPerSubject),
691
+ pinningUnverifiableCounts: summedPinningUnverifiableCounts(disclosuresPerSubject),
692
+ },
693
+ limitations: [...(reportRecord.limitations ?? [])],
694
+ venueHonesty: input.venueHonesty,
695
+ verification: {
696
+ command: anchored
697
+ ? PUBLIC_BUNDLE_V6_VERIFICATION_COMMAND
698
+ : projection.qualification === undefined
699
+ ? PUBLIC_BUNDLE_VERIFICATION_COMMAND
700
+ : BINARY_QUALIFICATION_VERIFICATION_COMMAND,
701
+ compatibleCommand: anchored
702
+ ? PUBLIC_BUNDLE_V6_COMPATIBLE_VERIFICATION_COMMAND
703
+ : projection.qualification === undefined
704
+ ? PUBLIC_BUNDLE_COMPATIBLE_VERIFICATION_COMMAND
705
+ : BINARY_QUALIFICATION_COMPATIBLE_VERIFICATION_COMMAND,
706
+ checks: anchored ? [...ANCHORED_CLAIM_VERIFICATION_CHECKS] : [...CLAIM_VERIFICATION_CHECKS],
707
+ // §9.2: the trust-root sentence is replaced only by a governing lock anchor. A bundle whose
708
+ // only anchors are pending, or cover the Matrix alone, keeps the unconditional sentence.
709
+ trustRoot: anchoredTrustRoot(anchors),
710
+ },
711
+ ...(anchored ? { anchors: anchors.map((anchor) => ({ ...anchor })) } : {}),
712
+ ...(input.previewDisclosure !== undefined
713
+ ? {
714
+ rehearsal: {
715
+ previewCount: input.previewDisclosure.previewCount,
716
+ timestamps: [...input.previewDisclosure.timestamps],
717
+ },
718
+ }
719
+ : {}),
720
+ ...(projection.comparison !== undefined ? { comparison: projection.comparison } : {}),
721
+ ...(projection.pairwiseDisagreement !== undefined ? { pairwiseDisagreement: projection.pairwiseDisagreement } : {}),
722
+ ...(projection.pairedMajorityDelta !== undefined ? { pairedMajorityDelta: projection.pairedMajorityDelta } : {}),
723
+ };
724
+ }