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