@colophon-claims/verify 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +23 -9
  2. package/dist/admission/contracts.d.ts +483 -0
  3. package/dist/admission/contracts.js +285 -0
  4. package/dist/admission/index.d.ts +2 -0
  5. package/dist/admission/index.js +2 -0
  6. package/dist/admission/prompted-commitment.d.ts +19 -0
  7. package/dist/admission/prompted-commitment.js +52 -0
  8. package/dist/admission/prompted-selection.d.ts +24 -0
  9. package/dist/admission/prompted-selection.js +85 -0
  10. package/dist/admission/verification.d.ts +17 -3
  11. package/dist/admission/verification.js +230 -49
  12. package/dist/assets.d.ts +21 -1
  13. package/dist/assets.js +60 -3
  14. package/dist/binding/beacon-binding.d.ts +230 -0
  15. package/dist/binding/beacon-binding.js +325 -0
  16. package/dist/binding/report-face.d.ts +45 -0
  17. package/dist/binding/report-face.js +153 -0
  18. package/dist/cli.js +74 -12
  19. package/dist/index.d.ts +14 -3
  20. package/dist/index.js +16 -3
  21. package/dist/manifest.d.ts +30 -4
  22. package/dist/manifest.js +30 -0
  23. package/dist/materialize.d.ts +7 -0
  24. package/dist/materialize.js +7 -0
  25. package/dist/outcome.d.ts +31 -0
  26. package/dist/outcome.js +45 -0
  27. package/dist/profile/binary-qualification.js +6 -0
  28. package/dist/profile/claim-consistency.d.ts +8 -1
  29. package/dist/profile/claim-consistency.js +4 -4
  30. package/dist/profile/claim.d.ts +203 -1
  31. package/dist/profile/claim.js +192 -43
  32. package/dist/profile/disclosure.d.ts +273 -0
  33. package/dist/profile/disclosure.js +240 -0
  34. package/dist/profile/pinning-evidence.d.ts +1 -1
  35. package/dist/profile/run-results.d.ts +8 -2
  36. package/dist/profile/run-results.js +9 -3
  37. package/dist/profile/task-selection.d.ts +69 -0
  38. package/dist/profile/task-selection.js +140 -0
  39. package/dist/reader-instructions.d.ts +38 -1
  40. package/dist/reader-instructions.js +42 -2
  41. package/dist/schema.d.ts +13 -2
  42. package/dist/schema.js +39 -8
  43. package/dist/signers.d.ts +48 -0
  44. package/dist/signers.js +77 -0
  45. package/dist/verify.d.ts +20 -6
  46. package/dist/verify.js +156 -31
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +14 -14
@@ -3,9 +3,11 @@ import { z } from "zod";
3
3
  import { VERDICT_DSSE_PAYLOAD_TYPE, canonicalJsonBytes, compareCodeUnitStrings, parseBinaryJudgmentAnalysisContext, parseBinaryJudgmentInstrument, parseBinaryJudgmentLabelResolution, parseBinaryJudgmentPayload, recordDigest, sealBinaryJudgmentInstrument, } from "@jinn-network/task-execution-profiles";
4
4
  import { parseExactDsseEnvelope } from "@jinn-network/trust-core";
5
5
  import { BINARY_JUDGMENT_HUMAN_REVIEW_EVALUATION_SPEC_SEALED, HUMAN_REVIEW_FORM_SEALED, } from "./application.js";
6
- import { BinaryJudgmentAdmissionManifestSchema, HUMAN_REVIEW_OPERATOR_ASSERTION_MEDIA_TYPE, HUMAN_REVIEW_PACKET_MEDIA_TYPE, HUMAN_REVIEW_RESPONSE_MEDIA_TYPE, HUMAN_REVIEW_REVEAL_RECEIPT_MEDIA_TYPE, HUMAN_REVIEW_ROSTER_MEDIA_TYPE, HUMAN_REVIEW_VISIBILITY_RECEIPT_MEDIA_TYPE, SCREENING_REVEAL_RECEIPT_MEDIA_TYPE, SCREENING_TABLE_MEDIA_TYPE, HumanReviewOperatorAssertionSchema, HumanReviewPacketSchema, HumanReviewReplacementLedgerSchema, HumanReviewResponseSchema, HumanReviewRevealReceiptSchema, HumanReviewRosterSchema, HumanReviewVisibilityReceiptSchema, ScreeningRevealReceiptSchema, ScreeningTableSchema, } from "./contracts.js";
6
+ import { BinaryJudgmentAdmissionManifestSchema, HUMAN_REVIEW_OPERATOR_ASSERTION_MEDIA_TYPE, HUMAN_REVIEW_PACKET_MEDIA_TYPE, HUMAN_REVIEW_RESPONSE_MEDIA_TYPE, HUMAN_REVIEW_REVEAL_RECEIPT_MEDIA_TYPE, HUMAN_REVIEW_ROSTER_MEDIA_TYPE, HUMAN_REVIEW_VISIBILITY_RECEIPT_MEDIA_TYPE, SCREENING_REVEAL_RECEIPT_MEDIA_TYPE, SCREENING_TABLE_MEDIA_TYPE, SCREENING_TABLE_V2_MEDIA_TYPE, HumanReviewOperatorAssertionSchema, HumanReviewPacketSchema, HumanReviewReplacementLedgerSchema, HumanReviewResponseSchema, HumanReviewRevealReceiptSchema, HumanReviewRosterSchema, HumanReviewVisibilityReceiptSchema, ScreeningRevealReceiptSchema, ScreeningTableSchema, ScreeningTableV2Schema, PromptedScreeningProcedureV1Schema, ScreeningPoolSchema, SCREENING_POOL_V2_PROTOCOL, PROMPTED_SCREENING_LIMITATIONS, PROMPTED_SCREENING_PROFILE, } from "./contracts.js";
7
+ import { selectPromptedScreeningPool, PromptedScreeningSelectionError } from "./prompted-selection.js";
8
+ import { parseScreeningSampleCommitmentBytes, promptedScreeningCommitmentView, } from "./prompted-commitment.js";
7
9
  import { readOrderedAdmissionVerdictMeasurements, readAdmissionVerdictEnvelope } from "./result-evaluation.js";
8
- import { computeScreeningSample, ScreeningSampleError } from "./screening-sample.js";
10
+ import { computeScreeningPoolDigest, computeScreeningSample, ScreeningSampleError } from "./screening-sample.js";
9
11
  // Verbatim duplicate of `schema.ts`'s `BUNDLE_V4_ADMISSION_EVIDENCE_ROLES`, hand-kept in sync
10
12
  // (spec §6.8a Group C).
11
13
  export const BINARY_JUDGMENT_ADMISSION_RECORD_ROLES = [
@@ -21,7 +23,17 @@ export const BINARY_JUDGMENT_ADMISSION_RECORD_ROLES = [
21
23
  "screening-instrument",
22
24
  "screening-sampling-script",
23
25
  "screening-raw-outputs",
26
+ "screening-prompt",
27
+ "screening-procedure",
28
+ "screening-pool",
29
+ "screening-sample-commitment",
30
+ "screening-transcript",
24
31
  ];
32
+ export const PROMPTED_SCREENING_CAPABILITY_BOUNDARY = {
33
+ status: "not-machine-verified",
34
+ machineVerified: ["sealed-pool", "sealed-sample", "signed-table", "operator-decisions", "artifact-integrity"],
35
+ notMachineVerified: PROMPTED_SCREENING_LIMITATIONS,
36
+ };
25
37
  export class BinaryJudgmentAdmissionClosureError extends Error {
26
38
  path;
27
39
  constructor(path, detail, options) {
@@ -244,14 +256,15 @@ function authorityPayload(state, options) {
244
256
  catch (cause) {
245
257
  fail(path, "authority evidence is not an exact compact DSSE envelope", cause);
246
258
  }
247
- if (envelope.payloadType !== mediaType || envelope.signatures.length !== 1)
259
+ const allowedMediaTypes = typeof mediaType === "string" ? [mediaType] : mediaType;
260
+ if (!allowedMediaTypes.includes(envelope.payloadType) || envelope.signatures.length !== 1)
248
261
  fail(path, "authority evidence has the wrong payload type or signature count");
249
262
  const signature = envelope.signatures[0];
250
263
  const signatureKeyId = signature.keyid;
251
264
  if (typeof signatureKeyId !== "string" || !state.ports.verifyAuthoritySignature({ envelopeBytes, keyId: signatureKeyId, role })) {
252
265
  fail(path, `authority signature is invalid for role ${role}`);
253
266
  }
254
- return { value: exactCanonical(schema, envelope.payloadBytes, `${path}.payload`), keyId: signatureKeyId };
267
+ return { value: exactCanonical(schema, envelope.payloadBytes, `${path}.payload`), keyId: signatureKeyId, mediaType: envelope.payloadType };
255
268
  }
256
269
  function verifyHumanEvidence(state, refs, expectedItemSha256, expectedDraftId, admittedAt, path) {
257
270
  const reviews = refs.reviewVerdictSha256s.map((digest, index) => verifyReview(state, digest, expectedItemSha256, `${path}.reviews.${index}`));
@@ -314,27 +327,29 @@ function expectedExclusionReason(reviews) {
314
327
  return "review-disagreement";
315
328
  return undefined;
316
329
  }
317
- /**
318
- * The screened branch's sibling of `expectedExclusionReason` above, derived from §6.4's admission
319
- * rule instead of two signed reviews (spec §6.4, §6.5 check (4)). The seventh copy of the
320
- * replacement-ledger reason vocabulary this file carries (contracts.ts's own comment names the
321
- * other six and defers this one to S3, "recomputation logic").
322
- *
323
- * A row this file already knows is admitted returns `undefined` (no exclusion). Among excluded
324
- * rows: a FLAGGED row (`screeningVerdict !== intendedLabel`) is always hand-checked in a passing
325
- * closure (check (2) requires it), so its reason names the screen's own finding regardless of
326
- * that hand check having run -- `screening-indeterminate` when the screen itself was
327
- * indeterminate, `screening-disagreement` for a definite-label disagreement. `screening-hand-
328
- * excluded` is reserved for the one case a flagged-first read would miss: R-3's tie-break, a row
329
- * the screen AGREED with that the hand check overrode to exclude.
330
- */
330
+ function screeningRowAgreed(row) {
331
+ return row.screeningVerdict !== "UNSURE"
332
+ && row.screeningVerdict !== "indeterminate"
333
+ && row.screeningVerdict === row.intendedLabel;
334
+ }
335
+ function screeningRowChecked(row) {
336
+ return "ritsuDecision" in row ? row.ritsuDecision.checked : row.handChecked;
337
+ }
338
+ function screeningRowHandVerdict(row) {
339
+ if ("ritsuDecision" in row)
340
+ return row.ritsuDecision.checked ? row.ritsuDecision.verdict : undefined;
341
+ return row.handVerdict;
342
+ }
343
+ function screeningRowAdmitted(row) {
344
+ return screeningRowChecked(row) ? screeningRowHandVerdict(row) === "confirm" : screeningRowAgreed(row);
345
+ }
331
346
  function expectedScreeningExclusionReason(row) {
332
- const agreed = row.screeningVerdict === row.intendedLabel;
333
- const admitted = row.handChecked ? row.handVerdict === "confirm" : agreed;
347
+ const agreed = screeningRowAgreed(row);
348
+ const admitted = screeningRowAdmitted(row);
334
349
  if (admitted)
335
350
  return undefined;
336
351
  if (!agreed)
337
- return row.screeningVerdict === "indeterminate" ? "screening-indeterminate" : "screening-disagreement";
352
+ return row.screeningVerdict === "indeterminate" || row.screeningVerdict === "UNSURE" ? "screening-indeterminate" : "screening-disagreement";
338
353
  return "screening-hand-excluded";
339
354
  }
340
355
  /** The reviewer Result Evaluation verifier shared by admission construction and closure replay. */
@@ -365,6 +380,10 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
365
380
  let screeningSample = new Set();
366
381
  let screeningSampleAgreementRate;
367
382
  let screeningInstrumentSha256;
383
+ let promptedScreeningProcedureSha256;
384
+ let promptedScreeningPool;
385
+ let promptedCommittedSample;
386
+ let screeningTableAuthorityKeyId;
368
387
  if (manifest.truthAdmission === "screened-operator-sampled") {
369
388
  if (manifest.screeningTableSha256 === undefined) {
370
389
  fail("admissionManifest.screeningTableSha256", "screened admission manifest carries no screening table reference");
@@ -380,28 +399,100 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
380
399
  // branch's sole legal authority set is `["truth-reveal-attestor"]` alone (§6.8a Group C), so
381
400
  // that is the one key that signs both the bank-scoped table and the bank-scoped reveal
382
401
  // receipt: "signed once per bank" as D1 describes.
383
- screeningTable = authorityPayload(state, {
402
+ const tableEvidence = authorityPayload(state, {
384
403
  digest: manifest.screeningTableSha256,
385
- mediaType: SCREENING_TABLE_MEDIA_TYPE,
404
+ mediaType: [SCREENING_TABLE_MEDIA_TYPE, SCREENING_TABLE_V2_MEDIA_TYPE],
386
405
  role: "truth-reveal-attestor",
387
406
  evidenceRole: "screening-table",
388
- schema: ScreeningTableSchema,
407
+ schema: z.union([ScreeningTableSchema, ScreeningTableV2Schema]),
389
408
  path: "admissionManifest.screeningTableSha256",
390
- }).value;
409
+ });
410
+ screeningTable = tableEvidence.value;
411
+ screeningTableAuthorityKeyId = tableEvidence.keyId;
391
412
  if (screeningTable.draftId !== input.expectedDraftId || screeningTable.sealedAt !== manifest.admittedAt) {
392
413
  fail("screeningTable", "table draft/time does not bind the manifest");
393
414
  }
394
- const screeningInstrument = parseProfileRecord(state, screeningTable.screeningInstrumentSha256, "screeningTable.screeningInstrumentSha256", parseBinaryJudgmentInstrument, "screening-instrument");
395
- const resealedScreeningInstrument = sealBinaryJudgmentInstrument(screeningInstrument.value);
396
- if (resealedScreeningInstrument.digest !== screeningTable.screeningInstrumentSha256
397
- || !bytesEqual(resealedScreeningInstrument.bytes, screeningInstrument.bytes)) {
398
- fail("screeningTable.screeningInstrumentSha256", "instrument does not reseal to its table commitment");
415
+ let sampleSeed;
416
+ let sampleSize;
417
+ let sampleIdentities;
418
+ if (screeningTable.protocol === "https://spec.jinn.network/binary-judgment/screening-table/v1") {
419
+ if (tableEvidence.mediaType !== SCREENING_TABLE_MEDIA_TYPE)
420
+ fail("screeningTable", "v1 table protocol must use the v1 media type");
421
+ const screeningInstrument = parseProfileRecord(state, screeningTable.screeningInstrumentSha256, "screeningTable.screeningInstrumentSha256", parseBinaryJudgmentInstrument, "screening-instrument");
422
+ const resealedScreeningInstrument = sealBinaryJudgmentInstrument(screeningInstrument.value);
423
+ if (resealedScreeningInstrument.digest !== screeningTable.screeningInstrumentSha256 || !bytesEqual(resealedScreeningInstrument.bytes, screeningInstrument.bytes)) {
424
+ fail("screeningTable.screeningInstrumentSha256", "instrument does not reseal to its table commitment");
425
+ }
426
+ screeningInstrumentSha256 = screeningTable.screeningInstrumentSha256;
427
+ resolve(state, screeningTable.samplingScriptSha256, "screeningTable.samplingScriptSha256", "screening-sampling-script");
428
+ resolve(state, screeningTable.rawOutputsSha256, "screeningTable.rawOutputsSha256", "screening-raw-outputs");
429
+ sampleSeed = screeningTable.sampleSeed;
430
+ sampleSize = screeningTable.sampleSize;
431
+ sampleIdentities = screeningTable.rows.map((row) => row.itemSha256);
432
+ }
433
+ else {
434
+ if (tableEvidence.mediaType !== SCREENING_TABLE_V2_MEDIA_TYPE)
435
+ fail("screeningTable", "v2 table protocol must use the v2 media type");
436
+ const procedure = exactCanonical(PromptedScreeningProcedureV1Schema, resolve(state, screeningTable.procedureSha256, "screeningTable.procedureSha256", "screening-procedure"), "screeningProcedure");
437
+ const pool = exactCanonical(ScreeningPoolSchema, resolve(state, screeningTable.poolSha256, "screeningTable.poolSha256", "screening-pool"), "screeningPool");
438
+ let commitment;
439
+ try {
440
+ commitment = parseScreeningSampleCommitmentBytes(resolve(state, screeningTable.sampleCommitmentSha256, "screeningTable.sampleCommitmentSha256", "screening-sample-commitment"));
441
+ }
442
+ catch (cause) {
443
+ fail("screeningSampleCommitment", cause instanceof Error ? cause.message : "screening sample commitment is invalid", cause);
444
+ }
445
+ const commitmentView = promptedScreeningCommitmentView(commitment);
446
+ resolve(state, screeningTable.coordinatorPromptSha256, "screeningTable.coordinatorPromptSha256", "screening-prompt");
447
+ resolve(state, screeningTable.samplingScriptSha256, "screeningTable.samplingScriptSha256", "screening-sampling-script");
448
+ resolve(state, screeningTable.transcriptSha256, "screeningTable.transcriptSha256", "screening-transcript");
449
+ if (procedure.coordinatorPromptSha256 !== screeningTable.coordinatorPromptSha256
450
+ || procedure.transcriptSha256 !== screeningTable.transcriptSha256
451
+ || pool.draftId !== input.expectedDraftId
452
+ || (commitmentView.draftId !== undefined && commitmentView.draftId !== input.expectedDraftId)
453
+ || (commitmentView.poolSha256 !== undefined && commitmentView.poolSha256 !== screeningTable.poolSha256)
454
+ || commitmentView.poolIdentityCommitmentSha256 !== pool.identityCommitmentSha256
455
+ || (commitmentView.samplingScriptSha256 !== undefined && commitmentView.samplingScriptSha256 !== screeningTable.samplingScriptSha256)
456
+ || Date.parse(procedure.sealedAt) > Date.parse(screeningTable.sealedAt)
457
+ || Date.parse(pool.sealedAt) > Date.parse(screeningTable.sealedAt)
458
+ || (commitmentView.poolSha256 !== undefined && Date.parse(pool.sealedAt) > Date.parse(commitmentView.committedAt))
459
+ || Date.parse(commitmentView.committedAt) > Date.parse(screeningTable.sealedAt))
460
+ fail("screeningTable", "prompted procedure, prompt, transcript, pool, commitment, draft, or timestamps do not join");
461
+ const poolIdentities = pool.protocol === SCREENING_POOL_V2_PROTOCOL
462
+ ? pool.items.map((item) => item.screeningIdentitySha256)
463
+ : pool.items.map((item) => item.itemSha256);
464
+ if (commitmentView.registeredIdentitySha256s !== undefined
465
+ && !sameStrings([...poolIdentities].sort(compareCodeUnitStrings), commitmentView.registeredIdentitySha256s))
466
+ fail("screeningSampleCommitment.candidateItemDigests", "registered identities do not exactly cover the later pool bridge");
467
+ if (computeScreeningPoolDigest(poolIdentities) !== pool.identityCommitmentSha256)
468
+ fail("screeningPool.identityCommitmentSha256", "identity commitment does not match the sorted unique pool identities");
469
+ const itemIds = new Set();
470
+ for (const [index, poolItem] of pool.items.entries()) {
471
+ const item = parseProfileRecord(state, poolItem.itemSha256, `screeningPool.items.${index}.itemSha256`, (bytes) => parseBinaryJudgmentPayload(bytes), "source-item").value;
472
+ if (itemIds.has(item.itemId))
473
+ fail("screeningPool.items", `duplicate source itemId ${item.itemId}`);
474
+ itemIds.add(item.itemId);
475
+ }
476
+ const rowIdentities = screeningTable.rows.map((row) => row.itemSha256);
477
+ const sortedPoolItemSha256s = pool.items.map((item) => item.itemSha256).sort(compareCodeUnitStrings);
478
+ if (!sameStrings(rowIdentities, sortedPoolItemSha256s))
479
+ fail("screeningTable.rows", "v2 table must cover all 664 pool item digests in sorted order");
480
+ const poolByItem = new Map(pool.items.map((item) => [item.itemSha256, item]));
481
+ for (const row of screeningTable.rows) {
482
+ const poolItem = poolByItem.get(row.itemSha256);
483
+ if (row.intendedLabel !== poolItem.intendedLabel)
484
+ fail("screeningTable.rows", `row ${row.itemSha256} intended label differs from the sealed pool`);
485
+ if (row.ritsuDecision.checked && (Date.parse(row.ritsuDecision.decidedAt) < Date.parse(commitmentView.committedAt)
486
+ || Date.parse(row.ritsuDecision.decidedAt) > Date.parse(screeningTable.sealedAt)))
487
+ fail("screeningTable.rows", `row ${row.itemSha256} has a Ritsu decision outside the committed-sample-to-table interval`);
488
+ }
489
+ sampleSeed = commitmentView.sampleSeed;
490
+ sampleSize = commitmentView.sampleSize;
491
+ sampleIdentities = poolIdentities;
492
+ promptedScreeningProcedureSha256 = screeningTable.procedureSha256;
493
+ promptedScreeningPool = pool;
494
+ promptedCommittedSample = commitmentView.sampleItemSha256s;
399
495
  }
400
- screeningInstrumentSha256 = screeningTable.screeningInstrumentSha256;
401
- // These two sidecars are intentionally opaque. Resolving them is sufficient to prove exact
402
- // digest-bound reachability; screening-sample/1 remains the independent sampling authority.
403
- resolve(state, screeningTable.samplingScriptSha256, "screeningTable.samplingScriptSha256", "screening-sampling-script");
404
- resolve(state, screeningTable.rawOutputsSha256, "screeningTable.rawOutputsSha256", "screening-raw-outputs");
405
496
  screeningRowsByItem = new Map(screeningTable.rows.map((row) => [row.itemSha256, row]));
406
497
  // (1) Sample membership: recomputed by screening-sample/1, never by executing the sealed
407
498
  // script (R-4). The recomputation is authoritative for checks (2) and (3) below -- there is
@@ -409,9 +500,9 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
409
500
  let sampleResult;
410
501
  try {
411
502
  sampleResult = computeScreeningSample({
412
- itemSha256s: screeningTable.rows.map((row) => row.itemSha256),
413
- sampleSeed: screeningTable.sampleSeed,
414
- sampleSize: screeningTable.sampleSize,
503
+ itemSha256s: sampleIdentities,
504
+ sampleSeed,
505
+ sampleSize,
415
506
  });
416
507
  }
417
508
  catch (cause) {
@@ -420,21 +511,28 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
420
511
  : "sample recomputation (screening-sample/1) failed", cause);
421
512
  }
422
513
  screeningSample = new Set(sampleResult.sample);
514
+ if (promptedCommittedSample !== undefined && !sameStrings([...sampleResult.sample].sort(compareCodeUnitStrings), promptedCommittedSample))
515
+ fail("screeningSampleCommitment.sampleItemSha256s", "public sample membership differs from screening-sample/1 recomputation");
516
+ const sampledItemSha256s = promptedScreeningPool?.protocol === SCREENING_POOL_V2_PROTOCOL
517
+ ? new Set(promptedScreeningPool.items
518
+ .filter((item) => screeningSample.has(item.screeningIdentitySha256))
519
+ .map((item) => item.itemSha256))
520
+ : screeningSample;
423
521
  // (2) Required hand checks: flagged := rows where NOT agreed. Every row in flagged ∪ sample
424
522
  // must carry handChecked === true.
425
523
  for (const row of screeningTable.rows) {
426
- const flagged = row.screeningVerdict !== row.intendedLabel;
427
- if ((flagged || screeningSample.has(row.itemSha256)) && !row.handChecked) {
524
+ const flagged = !screeningRowAgreed(row);
525
+ if ((flagged || sampledItemSha256s.has(row.itemSha256)) && !screeningRowChecked(row)) {
428
526
  fail("screeningTable.rows", `row ${row.itemSha256} is flagged or sampled but was never hand-checked`);
429
527
  }
430
528
  }
431
529
  // (3) Sample agreement rate -- the one definition (spec §6.5(3)), symmetric on purpose. Every
432
530
  // row in `screeningSample` is hand-checked by check (2) above, so `handVerdict` is defined.
433
- const sampleMatches = [...screeningSample].filter((sampledItemSha256) => {
531
+ const sampleMatches = [...sampledItemSha256s].filter((sampledItemSha256) => {
434
532
  const row = screeningRowsByItem.get(sampledItemSha256);
435
- return (row.screeningVerdict === row.intendedLabel) === (row.handVerdict === "confirm");
533
+ return screeningRowAgreed(row) === (screeningRowHandVerdict(row) === "confirm");
436
534
  }).length;
437
- screeningSampleAgreementRate = sampleMatches / screeningSample.size;
535
+ screeningSampleAgreementRate = sampleMatches / sampledItemSha256s.size;
438
536
  }
439
537
  const resolutions = manifest.labelResolutionSha256s.map((digest, index) => ({
440
538
  digest,
@@ -528,6 +626,7 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
528
626
  const reveal = revealEvidence.value;
529
627
  if (reveal.attestorRole !== "truth-reveal-attestor"
530
628
  || reveal.attestorKeyId !== revealEvidence.keyId
629
+ || revealEvidence.keyId !== screeningTableAuthorityKeyId
531
630
  || reveal.draftId !== input.expectedDraftId
532
631
  || reveal.screeningTableSha256 !== resolution.screeningTableSha256
533
632
  || reveal.judgeExecutionState !== "not-started"
@@ -539,12 +638,13 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
539
638
  const row = screeningRowsByItem.get(resolution.itemSha256);
540
639
  if (row === undefined)
541
640
  fail(path, "accepted screened item has no covering screening-table row");
542
- const agreed = row.screeningVerdict === row.intendedLabel;
543
- const admitted = row.handChecked ? row.handVerdict === "confirm" : agreed;
544
- if (!admitted)
545
- fail(path, "admitted screened item's table row is not itself admitted per §6.4's admission rule");
641
+ if (!screeningRowAdmitted(row))
642
+ fail(path, "admitted screened item's table row is not itself admitted per the sealed admission rule");
546
643
  if (row.intendedLabel !== resolution.truthLabel)
547
644
  fail(path, "resolution truth label differs from the screening table's intended label");
645
+ const poolItem = promptedScreeningPool?.items.find((item) => item.itemSha256 === resolution.itemSha256);
646
+ if (poolItem !== undefined && (poolItem.candidateClass !== resolution.candidateClass || poolItem.stratum !== resolution.stratum))
647
+ fail(path, "resolution class or stratum differs from the prompted screening pool");
548
648
  break;
549
649
  }
550
650
  default: {
@@ -566,6 +666,36 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
566
666
  if (contextByResolution.size !== resolutions.length || contexts.some((entry) => !manifest.labelResolutionSha256s.includes(entry.value.labelResolutionSha256))) {
567
667
  fail("admissionManifest.analysisContextSha256s", "analysis-context coverage has an orphan or omission");
568
668
  }
669
+ const promptedExpectedReplacements = new Map();
670
+ if (promptedScreeningPool !== undefined) {
671
+ let selection;
672
+ try {
673
+ selection = selectPromptedScreeningPool(promptedScreeningPool, screeningRowsByItem);
674
+ }
675
+ catch (cause) {
676
+ fail("screeningPool.items", cause instanceof PromptedScreeningSelectionError ? cause.message : "prompted reserve selection failed", cause);
677
+ }
678
+ const expectedAccepted = new Set(selection.winners.map((item) => item.itemSha256));
679
+ for (const replacement of selection.replacements) {
680
+ promptedExpectedReplacements.set(replacement.excludedMain.itemSha256, {
681
+ replacementItemSha256: replacement.replacement.itemSha256,
682
+ receivingSlotId: replacement.receivingSlotId,
683
+ });
684
+ }
685
+ const actualAccepted = [...acceptedItems].sort(compareCodeUnitStrings);
686
+ if (!sameStrings(actualAccepted, [...expectedAccepted].sort(compareCodeUnitStrings)))
687
+ fail("admissionManifest.labelResolutionSha256s", "accepted items are not the first admissible candidate in every sealed slot");
688
+ if (accepted.length !== 240)
689
+ fail("admissionManifest.labelResolutionSha256s", "prompted screening must admit exactly 240 items");
690
+ for (const candidateClass of ["correct", "specific-wrong", "vague-topical-wrong"]) {
691
+ if (accepted.filter((item) => item.candidateClass === candidateClass).length !== 80)
692
+ fail("admissionManifest.labelResolutionSha256s", `final bank must contain exactly 80 ${candidateClass} items`);
693
+ for (const stratum of ["category-1", "category-2", "category-3", "category-4"]) {
694
+ if (accepted.filter((item) => item.candidateClass === candidateClass && item.stratum === stratum).length !== 20)
695
+ fail("admissionManifest.labelResolutionSha256s", `final bank must contain exactly 20 ${candidateClass}/${stratum} items`);
696
+ }
697
+ }
698
+ }
569
699
  const excludedDigests = ledger.entries.map((entry) => entry.excludedItemSha256).sort(compareCodeUnitStrings);
570
700
  if (!sameStrings(excludedDigests, manifest.excludedItemSha256s))
571
701
  fail("replacementLedger.entries", "ledger exclusion coverage differs from manifest");
@@ -577,7 +707,14 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
577
707
  let previousPosition = 0;
578
708
  for (const [index, entry] of ledger.entries.entries()) {
579
709
  const path = `replacementLedger.entries.${index}`;
580
- if (entry.excludedPoolPosition <= previousPosition)
710
+ const sharedPool = promptedScreeningPool?.protocol === SCREENING_POOL_V2_PROTOCOL;
711
+ if (sharedPool) {
712
+ const previousSlotId = index === 0 ? undefined : ledger.entries[index - 1].receivingSlotId;
713
+ if (entry.receivingSlotId === undefined || (previousSlotId !== undefined && compareCodeUnitStrings(previousSlotId, entry.receivingSlotId) >= 0)) {
714
+ fail(path, "shared-reserve ledger entries are not in deterministic receiving-slot order");
715
+ }
716
+ }
717
+ else if (entry.excludedPoolPosition <= previousPosition)
581
718
  fail(path, "ledger entries are not in deterministic pool order");
582
719
  previousPosition = entry.excludedPoolPosition;
583
720
  if (excludedItems.has(entry.excludedItemSha256) || acceptedItems.has(entry.excludedItemSha256))
@@ -593,6 +730,35 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
593
730
  || replacement.candidateClass !== entry.candidateClass
594
731
  || replacement.stratum !== entry.stratum)
595
732
  fail(path, "replacement is not a later admitted reserve in the same class and stratum");
733
+ if (promptedScreeningPool !== undefined) {
734
+ const excludedPoolItem = promptedScreeningPool.items.find((item) => item.itemSha256 === entry.excludedItemSha256);
735
+ const replacementPoolItem = promptedScreeningPool.items.find((item) => item.itemSha256 === entry.replacementItemSha256);
736
+ const expected = promptedExpectedReplacements.get(entry.excludedItemSha256);
737
+ let sealedPoolItemsMatch = false;
738
+ let actualReceivingSlotId;
739
+ if (promptedScreeningPool.protocol === SCREENING_POOL_V2_PROTOCOL) {
740
+ if (excludedPoolItem?.poolKind === "main" && replacementPoolItem?.poolKind === "reserve") {
741
+ actualReceivingSlotId = entry.receivingSlotId;
742
+ sealedPoolItemsMatch = excludedPoolItem.candidateClass === replacementPoolItem.candidateClass
743
+ && excludedPoolItem.stratum === replacementPoolItem.stratum
744
+ && entry.receivingSlotId === excludedPoolItem.slotId;
745
+ }
746
+ }
747
+ else if (excludedPoolItem?.poolKind === "main"
748
+ && replacementPoolItem?.poolKind === "reserve"
749
+ && "slotId" in excludedPoolItem
750
+ && "slotId" in replacementPoolItem) {
751
+ actualReceivingSlotId = excludedPoolItem.slotId;
752
+ sealedPoolItemsMatch = excludedPoolItem.slotId === replacementPoolItem.slotId
753
+ && entry.receivingSlotId === undefined;
754
+ }
755
+ if (!sealedPoolItemsMatch
756
+ || entry.excludedPoolPosition !== excludedPoolItem?.poolPosition
757
+ || entry.replacementPoolPosition !== replacementPoolItem?.poolPosition
758
+ || expected?.replacementItemSha256 !== entry.replacementItemSha256
759
+ || expected.receivingSlotId !== actualReceivingSlotId)
760
+ fail(path, "ledger does not name the deterministic first-admissible reserve and its receiving slot at the exact sealed positions");
761
+ }
596
762
  // Branches on admission mode (spec §6.4, §6.5 check (4); ratified resolution to G-1): a
597
763
  // screened exclusion is verified against the screening table's own per-row admission rule
598
764
  // rather than against two signed reviews. §6.9 deliberately DROPS the four per-item two-human
@@ -650,8 +816,15 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
650
816
  stratum: entry.stratum,
651
817
  reason: entry.reason,
652
818
  replacementItemSha256: prefixed(entry.replacementItemSha256),
819
+ ...(entry.receivingSlotId === undefined ? {} : { receivingSlotId: entry.receivingSlotId }),
653
820
  });
654
821
  }
822
+ if (promptedScreeningPool !== undefined) {
823
+ const ledgerReplacements = new Map(ledger.entries.map((entry) => [entry.excludedItemSha256, entry.replacementItemSha256]));
824
+ if (ledgerReplacements.size !== promptedExpectedReplacements.size || [...promptedExpectedReplacements].some(([excludedItem, expected]) => ledgerReplacements.get(excludedItem) !== expected.replacementItemSha256)) {
825
+ fail("replacementLedger.entries", "ledger does not exactly cover every prompted main-slot replacement");
826
+ }
827
+ }
655
828
  // (0) Coverage, the other half (spec §6.5 check (0)): the table's rows must cover the frozen
656
829
  // bank's candidate pool EXACTLY. The pool itself is not a separate input to this function --
657
830
  // it is reconstructed as accepted ∪ excluded, the closure's own admission decision, which by
@@ -659,7 +832,7 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
659
832
  // with a same-class same-stratum replacement, and every replacement is itself an admitted pool
660
833
  // member). `sampleSize <= rows.length` (check (0)'s other clause) is enforced transitively:
661
834
  // `computeScreeningSample` above refuses a `sampleSize` exceeding the pool size on its own.
662
- if (manifest.truthAdmission === "screened-operator-sampled") {
835
+ if (manifest.truthAdmission === "screened-operator-sampled" && promptedScreeningPool === undefined) {
663
836
  if (screeningTable === undefined) {
664
837
  // Unreachable today: the screened arm resolves the table before any row is admitted and fails
665
838
  // closed when it is absent. Written as a refusal anyway, because the `&& screeningTable !==
@@ -705,5 +878,13 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
705
878
  instrumentSha256: screeningInstrumentSha256,
706
879
  },
707
880
  }),
881
+ ...(screeningSampleAgreementRate === undefined || promptedScreeningProcedureSha256 === undefined ? {} : {
882
+ promptedScreening: {
883
+ sampleAgreementRate: screeningSampleAgreementRate,
884
+ procedureSha256: promptedScreeningProcedureSha256,
885
+ profile: PROMPTED_SCREENING_PROFILE,
886
+ capabilityBoundary: PROMPTED_SCREENING_CAPABILITY_BOUNDARY,
887
+ },
888
+ }),
708
889
  };
709
890
  }
package/dist/assets.d.ts CHANGED
@@ -10,7 +10,12 @@ export interface PublicAssetInput {
10
10
  /** Canonically sorted identities for every authenticated `records/<sha>.bin` closure member. */
11
11
  readonly recordSha256s: readonly string[];
12
12
  readonly dissentCellKeys: readonly string[];
13
- /** Verifier-derived, authenticated human projection. Absent only for legacy bundle assets. */
13
+ /**
14
+ * Verifier-derived, authenticated human projection. Absent only for the
15
+ * qualification-projecting profile (`benchmark-product-public-bundle/4`, `/7`, and `/8`), which
16
+ * carries `binaryQualification` instead. There is no third, comparison-free profile: a
17
+ * bundle rendered with neither field is refused at verification (issue #2984).
18
+ */
14
19
  readonly comparison?: PublicComparisonView;
15
20
  /** Producer-verified binary admission/instrument facts. Required for claim-package/2 only. */
16
21
  readonly binaryQualification?: {
@@ -26,6 +31,21 @@ export interface PublicAssetInput {
26
31
  }[];
27
32
  };
28
33
  }
34
+ /**
35
+ * The six-variable disclosure table (issue #2839, ruling Q4: the bundle's own `index.html` and
36
+ * `README.md` are S2's; the site template is packet R1's).
37
+ *
38
+ * Rendered from `claim.disclosure` — a verified fact the claim already carries — so this hangs off
39
+ * the REPORT/claim facts rather than off a format test, and every bundle without a declaration
40
+ * renders byte-identically to what it rendered before this section existed.
41
+ *
42
+ * `measured-here` and `disclosed-by-publisher` are kept visually distinct and are never merged into
43
+ * one list: the whole point of the record is that a reader can tell at a glance which variables this
44
+ * venue proved and which it only carries. Nothing here counts, scores, or ranks the statuses (design
45
+ * §8), and there is no explanatory caption — the status token and the statement are the content.
46
+ */
47
+ export declare function disclosureSpecificationHtml(input: PublicAssetInput): string;
48
+ export declare function disclosureSpecificationMarkdown(input: PublicAssetInput): string;
29
49
  export declare function binaryAdmissionHtml(input: PublicAssetInput): string;
30
50
  export declare function binaryAdmissionMarkdown(input: PublicAssetInput): string;
31
51
  /** Fixed, deterministic public-bundle/2 presentation bytes. The builder only projects already
package/dist/assets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BENCHMARKING_METHOD_IDS } from "@jinn-network/benchmarking-records";
1
+ import { BENCHMARKING_METHOD_IDS, DISCLOSURE_VARIABLE_KEYS } from "@jinn-network/benchmarking-records";
2
2
  import { validateBinaryInstrumentQualificationProjection } from "@jinn-network/benchmarking-aggregate";
3
3
  import { canonicalJsonBytes } from "@jinn-network/trust-core";
4
4
  import { readFileSync } from "node:fs";
@@ -450,6 +450,57 @@ function publicationGradeWording(truthAdmission) {
450
450
  }
451
451
  }
452
452
  }
453
+ /**
454
+ * The six-variable disclosure table (issue #2839, ruling Q4: the bundle's own `index.html` and
455
+ * `README.md` are S2's; the site template is packet R1's).
456
+ *
457
+ * Rendered from `claim.disclosure` — a verified fact the claim already carries — so this hangs off
458
+ * the REPORT/claim facts rather than off a format test, and every bundle without a declaration
459
+ * renders byte-identically to what it rendered before this section existed.
460
+ *
461
+ * `measured-here` and `disclosed-by-publisher` are kept visually distinct and are never merged into
462
+ * one list: the whole point of the record is that a reader can tell at a glance which variables this
463
+ * venue proved and which it only carries. Nothing here counts, scores, or ranks the statuses (design
464
+ * §8), and there is no explanatory caption — the status token and the statement are the content.
465
+ */
466
+ export function disclosureSpecificationHtml(input) {
467
+ const disclosure = input.claim.disclosure;
468
+ if (disclosure === undefined)
469
+ return "";
470
+ const rows = DISCLOSURE_VARIABLE_KEYS.map((key) => {
471
+ const entry = disclosure.variables[key];
472
+ const detail = entry.status === "undisclosed"
473
+ ? `<p class="disclosure-reason">${escapeMarkup(entry.reason)}</p>`
474
+ : `<p class="disclosure-statement">${escapeMarkup(entry.statement)}</p>`;
475
+ const sources = entry.status === "disclosed-by-publisher" && entry.sources !== undefined
476
+ ? `<ul class="disclosure-sources">${entry.sources.map((source) => `<li>${escapeMarkup(source.uri)}</li>`).join("")}</ul>`
477
+ : "";
478
+ const evidence = entry.status === "measured-here"
479
+ ? `<ul class="disclosure-evidence">${entry.evidence.map((citation) => `<li><span class="role">${escapeMarkup(citation.role)}</span> <span class="digest">${escapeMarkup(citation.digest.sha256)}</span></li>`).join("")}</ul>`
480
+ : "";
481
+ return `<div class="disclosure-variable disclosure-${escapeMarkup(entry.status)}"><dt>${escapeMarkup(key)}</dt><dd><span class="disclosure-status">${escapeMarkup(entry.status)}</span>${detail}${sources}${evidence}</dd></div>`;
482
+ }).join("");
483
+ return `<section id="disclosure-specification" aria-labelledby="disclosure-specification-heading"><h2 id="disclosure-specification-heading">Six-variable disclosure</h2><dl class="facts disclosure">${rows}</dl><p class="digest">${escapeMarkup(disclosure.specification)} · record ${escapeMarkup(disclosure.recordSha256)} · subject ${escapeMarkup(disclosure.subjectSha256)}</p></section>`;
484
+ }
485
+ export function disclosureSpecificationMarkdown(input) {
486
+ const disclosure = input.claim.disclosure;
487
+ if (disclosure === undefined)
488
+ return "";
489
+ const rows = DISCLOSURE_VARIABLE_KEYS.map((key) => {
490
+ const entry = disclosure.variables[key];
491
+ const detail = entry.status === "undisclosed"
492
+ ? `\n - ${escapeMarkdown(entry.reason)}`
493
+ : `\n - ${escapeMarkdown(entry.statement)}`;
494
+ const sources = entry.status === "disclosed-by-publisher" && entry.sources !== undefined
495
+ ? entry.sources.map((source) => `\n - source: ${escapeMarkdown(source.uri)}`).join("")
496
+ : "";
497
+ const evidence = entry.status === "measured-here"
498
+ ? entry.evidence.map((citation) => `\n - ${escapeMarkdown(citation.role)}: ${escapeMarkdown(citation.digest.sha256)}`).join("")
499
+ : "";
500
+ return `- ${escapeMarkdown(key)}: ${escapeMarkdown(entry.status)}${detail}${sources}${evidence}`;
501
+ }).join("\n");
502
+ return `## Six-variable disclosure\n\n${rows}\n\n- ${escapeMarkdown(disclosure.specification)}\n- record: ${escapeMarkdown(disclosure.recordSha256)}\n- subject: ${escapeMarkdown(disclosure.subjectSha256)}\n\n`;
503
+ }
453
504
  export function binaryAdmissionHtml(input) {
454
505
  const admission = input.binaryQualification;
455
506
  if (admission === undefined)
@@ -493,6 +544,12 @@ function neutralClaimHtml(facts) {
493
544
  }
494
545
  return '<p class="neutral">Verified binary-instrument qualification. Facts are presented per instrument without comparative conclusions.</p>';
495
546
  }
547
+ // Task-selection provenance (issue #2980) is sealed into the Run and verified under
548
+ // `claim-consistency`, but deliberately renders NOTHING here, and no asset below projects it.
549
+ // The classic and anchored allocations pin `@colophon-claims/verify@0.1.0`, whose
550
+ // `verifyPublicBundleSnapshot` byte-compares every presentation asset against its own rebuild, so
551
+ // a bundle that rendered the sentence would carry an instruction to run a verifier that refuses
552
+ // it. Restoring the render is issue #3416, once the reader line that derives it is re-pinned.
496
553
  function buildIndex(input, reportFacts, claimFacts) {
497
554
  const outcome = input.matrix.completeness.runOutcome;
498
555
  const status = reportFacts.kind === "binary" ? qualificationOutcomeLabel(outcome) : outcomeLabel(outcome);
@@ -543,7 +600,7 @@ ${neutralClaimHtml(reportFacts)}
543
600
  </header>
544
601
  <main>
545
602
  <section class="adverse" aria-labelledby="adverse-heading"><h2 id="adverse-heading">Prominent adverse facts</h2>${list(adverse, "No adverse facts stated.")}</section>${input.comparison === undefined ? "" : `\n${comparisonSectionHtml(input.comparison)}`}
546
- <section aria-labelledby="scope-heading"><h2 id="scope-heading">Benchmark and configuration scope</h2><dl class="facts"><div><dt>Benchmark digest</dt><dd class="digest">${input.claim.scope.benchmarkSha256}</dd></div><div><dt>Tasks</dt><dd>${input.claim.scope.taskCount}</dd></div><div><dt>Replicates</dt><dd>${input.claim.scope.replicates}</dd></div><div><dt>Venue</dt><dd>${escapeMarkup(input.claim.scope.venue)}</dd></div></dl><h3>Arms and pinned configuration</h3><ul>${arms}</ul></section>${reportFacts.kind === "binary" ? binaryAdmissionHtml(input) : ""}
603
+ <section aria-labelledby="scope-heading"><h2 id="scope-heading">Benchmark and configuration scope</h2><dl class="facts"><div><dt>Benchmark digest</dt><dd class="digest">${input.claim.scope.benchmarkSha256}</dd></div><div><dt>Tasks</dt><dd>${input.claim.scope.taskCount}</dd></div><div><dt>Replicates</dt><dd>${input.claim.scope.replicates}</dd></div><div><dt>Venue</dt><dd>${escapeMarkup(input.claim.scope.venue)}</dd></div></dl><h3>Arms and pinned configuration</h3><ul>${arms}</ul></section>${reportFacts.kind === "binary" ? binaryAdmissionHtml(input) : ""}${disclosureSpecificationHtml(input)}
547
604
  <section aria-labelledby="matrix-heading"><h2 id="matrix-heading">Sealed Matrix accounting</h2><p class="source-label">Source: authenticated <a href="matrix.json">matrix.json</a>; values below are copied without reconciliation.</p><pre>${escapeMarkup(canonicalText({ completeness: input.matrix.completeness, attrition: input.matrix.attrition }))}</pre><h3>Completeness and attrition</h3><dl class="facts"><div><dt>Matrix run outcome</dt><dd>${escapeMarkup(outcome)}</dd></div><div><dt>Matrix expected</dt><dd>${input.matrix.completeness.expected}</dd></div><div><dt>Matrix judged</dt><dd>${input.matrix.completeness.judged}</dd></div><div><dt>Matrix floor</dt><dd>${escapeMarkup(input.matrix.completeness.floor)}</dd></div></dl><div class="table-scroll" tabindex="0" role="region" aria-label="Per-arm Matrix attrition"><table><caption>Exact per-arm attrition stored in the Matrix</caption><thead><tr><th scope="col">Arm</th><th scope="col">Expected</th><th scope="col">Judged</th><th scope="col">Unjudged</th><th scope="col">Unscorable</th><th scope="col">Expired</th><th scope="col">Invalidated</th><th scope="col">Excluded</th><th scope="col">Replacements</th></tr></thead><tbody>${attritionRows(input)}</tbody></table></div><h3>Matrix asymmetry flags</h3>${list(input.matrix.attrition.asymmetryFlags, "None recorded in the Matrix.")}</section>
548
605
  <section aria-labelledby="report-heading"><h2 id="report-heading">Sealed Report facts</h2><p class="source-label">Source: authenticated <a href="report.json">report.json</a>; values below are copied without reconciliation.</p><h3>${factsHeading(reportFacts, "report")}</h3>${armResultsHtml(reportFacts, "Exact wilson@1 values from the sealed Report")}<h3>Method and assurance facts stored in the Report</h3><dl class="facts"><div><dt>Report method</dt><dd>${escapeMarkup(input.report.method.id)} @ ${escapeMarkup(input.report.method.version)}</dd></div><div><dt>Report preregistered</dt><dd>${input.report.preregistered === true ? "Yes" : "No"}</dd></div></dl><h3>Report parameters</h3><pre>${escapeMarkup(canonicalText(input.report.method.parameters))}</pre><h3>Report conflicts</h3><pre>${escapeMarkup(canonicalText(reportFacts.conflicted))}</pre><h3>Report disclosures</h3><pre>${escapeMarkup(canonicalText(input.report.disclosures))}</pre></section>
549
606
  <section aria-labelledby="claim-heading"><h2 id="claim-heading">Stored Claim facts</h2><p class="source-label">Source: authenticated <a href="claim-package.json">claim-package.json</a>; values below are copied without reconciliation.</p><h3>${factsHeading(claimFacts, "claim")}</h3>${armResultsHtml(claimFacts, "Exact arm values stored in the Claim package")}<h3>Claim method and preregistration</h3><dl class="facts"><div><dt>Claim method</dt><dd>${escapeMarkup(input.claim.method.id)} @ ${escapeMarkup(input.claim.method.version)}</dd></div><div><dt>Claim preregistered</dt><dd>${input.claim.method.preregistered ? "Yes" : "No"}</dd></div><div><dt>Assurance preset</dt><dd>${escapeMarkup(input.claim.assurance.preset)}</dd></div></dl><h3>Claim parameters</h3><pre>${escapeMarkup(canonicalText(input.claim.method.parameters))}</pre><h3>Claim completeness</h3><pre>${escapeMarkup(canonicalText(input.claim.completeness))}</pre><h3>Claim attrition</h3><pre>${escapeMarkup(canonicalText(input.claim.attrition))}</pre><h3>Claim conflicts</h3><pre>${escapeMarkup(canonicalText(input.claim.conflicted))}</pre><h3>Claim disclosures</h3><h4>Unverifiable axes, integrity tiers, and per-subject disclosures</h4><pre>${escapeMarkup(canonicalText(input.claim.disclosures))}</pre><h3>Resolved assurance primitives</h3><pre>${escapeMarkup(canonicalText(input.claim.assurance.resolved))}</pre><p>${escapeMarkup(input.claim.assurance.disclosure)}</p><h3>Rehearsal disclosure</h3>${rehearsalHtml}</section>
@@ -749,7 +806,7 @@ ${adverse.map((value) => `- ${escapeMarkdown(value)}`).join("\n")}
749
806
 
750
807
  ## Configurations
751
808
 
752
- ${arms}${reportFacts.kind === "binary" ? `\n\n${binaryAdmissionMarkdown(input).trimEnd()}` : ""}
809
+ ${arms}${reportFacts.kind === "binary" ? `\n\n${binaryAdmissionMarkdown(input).trimEnd()}` : ""}${input.claim.disclosure === undefined ? "" : `\n\n${disclosureSpecificationMarkdown(input).trimEnd()}`}
753
810
 
754
811
  ## Sealed Matrix accounting
755
812