@colophon-claims/verify 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +18 -8
  2. package/dist/admission/contracts.d.ts +241 -0
  3. package/dist/admission/contracts.js +116 -0
  4. package/dist/admission/index.d.ts +2 -0
  5. package/dist/admission/index.js +2 -0
  6. package/dist/admission/prompted-commitment.d.ts +19 -0
  7. package/dist/admission/prompted-commitment.js +52 -0
  8. package/dist/admission/prompted-selection.d.ts +24 -0
  9. package/dist/admission/prompted-selection.js +85 -0
  10. package/dist/admission/verification.d.ts +1 -0
  11. package/dist/admission/verification.js +88 -42
  12. package/dist/assets.d.ts +21 -1
  13. package/dist/assets.js +60 -3
  14. package/dist/binding/beacon-binding.d.ts +230 -0
  15. package/dist/binding/beacon-binding.js +325 -0
  16. package/dist/binding/report-face.d.ts +45 -0
  17. package/dist/binding/report-face.js +153 -0
  18. package/dist/cli.js +74 -12
  19. package/dist/index.d.ts +14 -3
  20. package/dist/index.js +16 -3
  21. package/dist/manifest.d.ts +30 -4
  22. package/dist/manifest.js +30 -0
  23. package/dist/materialize.d.ts +7 -0
  24. package/dist/materialize.js +7 -0
  25. package/dist/outcome.d.ts +31 -0
  26. package/dist/outcome.js +45 -0
  27. package/dist/profile/claim-consistency.d.ts +8 -1
  28. package/dist/profile/claim-consistency.js +4 -4
  29. package/dist/profile/claim.d.ts +201 -2
  30. package/dist/profile/claim.js +178 -47
  31. package/dist/profile/disclosure.d.ts +273 -0
  32. package/dist/profile/disclosure.js +240 -0
  33. package/dist/profile/run-results.d.ts +8 -2
  34. package/dist/profile/run-results.js +9 -3
  35. package/dist/profile/task-selection.d.ts +69 -0
  36. package/dist/profile/task-selection.js +140 -0
  37. package/dist/reader-instructions.d.ts +38 -1
  38. package/dist/reader-instructions.js +42 -2
  39. package/dist/schema.d.ts +4 -3
  40. package/dist/schema.js +17 -0
  41. package/dist/signers.d.ts +48 -0
  42. package/dist/signers.js +77 -0
  43. package/dist/verify.d.ts +20 -6
  44. package/dist/verify.js +156 -31
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/package.json +14 -14
@@ -40,6 +40,7 @@ export interface VerifiedBinaryJudgmentAdmissionExclusion {
40
40
  readonly stratum: string;
41
41
  readonly reason: "review-disagreement" | "review-indeterminate" | "review-incomplete" | "screening-disagreement" | "screening-indeterminate" | "screening-hand-excluded";
42
42
  readonly replacementItemSha256: AdmissionSha256;
43
+ readonly receivingSlotId?: string;
43
44
  }
44
45
  export interface VerifiedBinaryJudgmentAdmissionClosure {
45
46
  readonly manifestSha256: AdmissionSha256;
@@ -3,7 +3,9 @@ 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, SCREENING_TABLE_V2_MEDIA_TYPE, HumanReviewOperatorAssertionSchema, HumanReviewPacketSchema, HumanReviewReplacementLedgerSchema, HumanReviewResponseSchema, HumanReviewRevealReceiptSchema, HumanReviewRosterSchema, HumanReviewVisibilityReceiptSchema, ScreeningRevealReceiptSchema, ScreeningTableSchema, ScreeningTableV2Schema, PromptedScreeningProcedureV1Schema, ScreeningPoolV1Schema, ScreeningSampleCommitmentV1Schema, PROMPTED_SCREENING_LIMITATIONS, PROMPTED_SCREENING_PROFILE, } 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
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
@@ -432,22 +434,36 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
432
434
  if (tableEvidence.mediaType !== SCREENING_TABLE_V2_MEDIA_TYPE)
433
435
  fail("screeningTable", "v2 table protocol must use the v2 media type");
434
436
  const procedure = exactCanonical(PromptedScreeningProcedureV1Schema, resolve(state, screeningTable.procedureSha256, "screeningTable.procedureSha256", "screening-procedure"), "screeningProcedure");
435
- const pool = exactCanonical(ScreeningPoolV1Schema, resolve(state, screeningTable.poolSha256, "screeningTable.poolSha256", "screening-pool"), "screeningPool");
436
- const commitment = exactCanonical(ScreeningSampleCommitmentV1Schema, resolve(state, screeningTable.sampleCommitmentSha256, "screeningTable.sampleCommitmentSha256", "screening-sample-commitment"), "screeningSampleCommitment");
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);
437
446
  resolve(state, screeningTable.coordinatorPromptSha256, "screeningTable.coordinatorPromptSha256", "screening-prompt");
438
447
  resolve(state, screeningTable.samplingScriptSha256, "screeningTable.samplingScriptSha256", "screening-sampling-script");
439
448
  resolve(state, screeningTable.transcriptSha256, "screeningTable.transcriptSha256", "screening-transcript");
440
449
  if (procedure.coordinatorPromptSha256 !== screeningTable.coordinatorPromptSha256
441
450
  || procedure.transcriptSha256 !== screeningTable.transcriptSha256
442
451
  || pool.draftId !== input.expectedDraftId
443
- || commitment.draftId !== input.expectedDraftId
444
- || commitment.poolSha256 !== screeningTable.poolSha256
445
- || commitment.poolIdentityCommitmentSha256 !== pool.identityCommitmentSha256
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)
446
456
  || Date.parse(procedure.sealedAt) > Date.parse(screeningTable.sealedAt)
447
- || Date.parse(pool.sealedAt) > Date.parse(commitment.committedAt)
448
- || Date.parse(commitment.committedAt) > 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))
449
460
  fail("screeningTable", "prompted procedure, prompt, transcript, pool, commitment, draft, or timestamps do not join");
450
- const poolIdentities = pool.items.map((item) => item.itemSha256);
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");
451
467
  if (computeScreeningPoolDigest(poolIdentities) !== pool.identityCommitmentSha256)
452
468
  fail("screeningPool.identityCommitmentSha256", "identity commitment does not match the sorted unique pool identities");
453
469
  const itemIds = new Set();
@@ -458,24 +474,24 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
458
474
  itemIds.add(item.itemId);
459
475
  }
460
476
  const rowIdentities = screeningTable.rows.map((row) => row.itemSha256);
461
- const sortedPoolIdentities = [...poolIdentities].sort(compareCodeUnitStrings);
462
- if (!sameStrings(rowIdentities, sortedPoolIdentities))
463
- fail("screeningTable.rows", "v2 table must cover all 664 pool identities in sorted order");
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");
464
480
  const poolByItem = new Map(pool.items.map((item) => [item.itemSha256, item]));
465
481
  for (const row of screeningTable.rows) {
466
482
  const poolItem = poolByItem.get(row.itemSha256);
467
483
  if (row.intendedLabel !== poolItem.intendedLabel)
468
484
  fail("screeningTable.rows", `row ${row.itemSha256} intended label differs from the sealed pool`);
469
- if (row.ritsuDecision.checked && (Date.parse(row.ritsuDecision.decidedAt) < Date.parse(commitment.committedAt)
485
+ if (row.ritsuDecision.checked && (Date.parse(row.ritsuDecision.decidedAt) < Date.parse(commitmentView.committedAt)
470
486
  || Date.parse(row.ritsuDecision.decidedAt) > Date.parse(screeningTable.sealedAt)))
471
487
  fail("screeningTable.rows", `row ${row.itemSha256} has a Ritsu decision outside the committed-sample-to-table interval`);
472
488
  }
473
- sampleSeed = commitment.sampleSeed;
474
- sampleSize = commitment.sampleSize;
489
+ sampleSeed = commitmentView.sampleSeed;
490
+ sampleSize = commitmentView.sampleSize;
475
491
  sampleIdentities = poolIdentities;
476
492
  promptedScreeningProcedureSha256 = screeningTable.procedureSha256;
477
493
  promptedScreeningPool = pool;
478
- promptedCommittedSample = commitment.sampleItemSha256s;
494
+ promptedCommittedSample = commitmentView.sampleItemSha256s;
479
495
  }
480
496
  screeningRowsByItem = new Map(screeningTable.rows.map((row) => [row.itemSha256, row]));
481
497
  // (1) Sample membership: recomputed by screening-sample/1, never by executing the sealed
@@ -497,21 +513,26 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
497
513
  screeningSample = new Set(sampleResult.sample);
498
514
  if (promptedCommittedSample !== undefined && !sameStrings([...sampleResult.sample].sort(compareCodeUnitStrings), promptedCommittedSample))
499
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;
500
521
  // (2) Required hand checks: flagged := rows where NOT agreed. Every row in flagged ∪ sample
501
522
  // must carry handChecked === true.
502
523
  for (const row of screeningTable.rows) {
503
524
  const flagged = !screeningRowAgreed(row);
504
- if ((flagged || screeningSample.has(row.itemSha256)) && !screeningRowChecked(row)) {
525
+ if ((flagged || sampledItemSha256s.has(row.itemSha256)) && !screeningRowChecked(row)) {
505
526
  fail("screeningTable.rows", `row ${row.itemSha256} is flagged or sampled but was never hand-checked`);
506
527
  }
507
528
  }
508
529
  // (3) Sample agreement rate -- the one definition (spec §6.5(3)), symmetric on purpose. Every
509
530
  // row in `screeningSample` is hand-checked by check (2) above, so `handVerdict` is defined.
510
- const sampleMatches = [...screeningSample].filter((sampledItemSha256) => {
531
+ const sampleMatches = [...sampledItemSha256s].filter((sampledItemSha256) => {
511
532
  const row = screeningRowsByItem.get(sampledItemSha256);
512
533
  return screeningRowAgreed(row) === (screeningRowHandVerdict(row) === "confirm");
513
534
  }).length;
514
- screeningSampleAgreementRate = sampleMatches / screeningSample.size;
535
+ screeningSampleAgreementRate = sampleMatches / sampledItemSha256s.size;
515
536
  }
516
537
  const resolutions = manifest.labelResolutionSha256s.map((digest, index) => ({
517
538
  digest,
@@ -647,20 +668,19 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
647
668
  }
648
669
  const promptedExpectedReplacements = new Map();
649
670
  if (promptedScreeningPool !== undefined) {
650
- const slotIds = [...new Set(promptedScreeningPool.items.map((item) => item.slotId))].sort(compareCodeUnitStrings);
651
- const expectedAccepted = new Set();
652
- for (const slotId of slotIds) {
653
- const slotItems = promptedScreeningPool.items.filter((item) => item.slotId === slotId);
654
- const main = slotItems.find((item) => item.poolKind === "main");
655
- const reserves = slotItems
656
- .filter((item) => item.poolKind === "reserve")
657
- .sort((left, right) => left.reserveOrder - right.reserveOrder);
658
- const winner = [main, ...reserves].find((item) => screeningRowAdmitted(screeningRowsByItem.get(item.itemSha256)));
659
- if (winner === undefined)
660
- fail("screeningPool.items", `${slotId} has no admissible main or reserve candidate`);
661
- expectedAccepted.add(winner.itemSha256);
662
- if (winner.itemSha256 !== main.itemSha256)
663
- promptedExpectedReplacements.set(main.itemSha256, winner.itemSha256);
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
+ });
664
684
  }
665
685
  const actualAccepted = [...acceptedItems].sort(compareCodeUnitStrings);
666
686
  if (!sameStrings(actualAccepted, [...expectedAccepted].sort(compareCodeUnitStrings)))
@@ -687,7 +707,14 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
687
707
  let previousPosition = 0;
688
708
  for (const [index, entry] of ledger.entries.entries()) {
689
709
  const path = `replacementLedger.entries.${index}`;
690
- 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)
691
718
  fail(path, "ledger entries are not in deterministic pool order");
692
719
  previousPosition = entry.excludedPoolPosition;
693
720
  if (excludedItems.has(entry.excludedItemSha256) || acceptedItems.has(entry.excludedItemSha256))
@@ -706,13 +733,31 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
706
733
  if (promptedScreeningPool !== undefined) {
707
734
  const excludedPoolItem = promptedScreeningPool.items.find((item) => item.itemSha256 === entry.excludedItemSha256);
708
735
  const replacementPoolItem = promptedScreeningPool.items.find((item) => item.itemSha256 === entry.replacementItemSha256);
709
- if (excludedPoolItem?.poolKind !== "main"
710
- || replacementPoolItem?.poolKind !== "reserve"
711
- || excludedPoolItem.slotId !== replacementPoolItem.slotId
712
- || entry.excludedPoolPosition !== excludedPoolItem.poolPosition
713
- || entry.replacementPoolPosition !== replacementPoolItem.poolPosition
714
- || promptedExpectedReplacements.get(entry.excludedItemSha256) !== entry.replacementItemSha256)
715
- fail(path, "ledger does not name the deterministic first-admissible reserve for the exact sealed slot positions");
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");
716
761
  }
717
762
  // Branches on admission mode (spec §6.4, §6.5 check (4); ratified resolution to G-1): a
718
763
  // screened exclusion is verified against the screening table's own per-row admission rule
@@ -771,11 +816,12 @@ export function verifyBinaryJudgmentAdmissionClosure(input, ports) {
771
816
  stratum: entry.stratum,
772
817
  reason: entry.reason,
773
818
  replacementItemSha256: prefixed(entry.replacementItemSha256),
819
+ ...(entry.receivingSlotId === undefined ? {} : { receivingSlotId: entry.receivingSlotId }),
774
820
  });
775
821
  }
776
822
  if (promptedScreeningPool !== undefined) {
777
823
  const ledgerReplacements = new Map(ledger.entries.map((entry) => [entry.excludedItemSha256, entry.replacementItemSha256]));
778
- if (ledgerReplacements.size !== promptedExpectedReplacements.size || [...promptedExpectedReplacements].some(([excludedItem, replacementItem]) => ledgerReplacements.get(excludedItem) !== replacementItem)) {
824
+ if (ledgerReplacements.size !== promptedExpectedReplacements.size || [...promptedExpectedReplacements].some(([excludedItem, expected]) => ledgerReplacements.get(excludedItem) !== expected.replacementItemSha256)) {
779
825
  fail("replacementLedger.entries", "ledger does not exactly cover every prompted main-slot replacement");
780
826
  }
781
827
  }
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
 
@@ -0,0 +1,230 @@
1
+ import { z } from "zod";
2
+ /** The procedure identifier a binding record must carry. */
3
+ export declare const BEACON_BINDING_PROCEDURE: "beacon-binding/1";
4
+ /**
5
+ * How strongly a source's own round index proves that its value postdates a given instant.
6
+ *
7
+ * `deterministic-round-time` sources index rounds by a published arithmetic schedule, so the
8
+ * proof is offline arithmetic. `attributive-height` sources index by block height, whose time
9
+ * needs headers the reader must obtain separately -- the claim is then what the chain asserts,
10
+ * checked elsewhere, and never an offline proof.
11
+ */
12
+ export type BeaconSourceTimeBasis = "deterministic-round-time" | "attributive-height";
13
+ export interface BeaconSourceDefinition {
14
+ readonly timeBasis: BeaconSourceTimeBasis;
15
+ /** Unix seconds of round 1. Present only on `deterministic-round-time` sources. */
16
+ readonly genesisTimeSeconds?: number;
17
+ /** Seconds between rounds. Present only on `deterministic-round-time` sources. */
18
+ readonly periodSeconds?: number;
19
+ /** Reader-facing name used by the report face. */
20
+ readonly displayName: string;
21
+ }
22
+ /**
23
+ * The beacons this procedure admits. Values are the sources' own published chain parameters and
24
+ * are part of the derivation: a reader recomputing `beaconRoundInstant` needs exactly these
25
+ * numbers, so they live in the code rather than in a comment.
26
+ */
27
+ export declare const BEACON_SOURCES: {
28
+ readonly "drand/quicknet": {
29
+ readonly timeBasis: "deterministic-round-time";
30
+ readonly genesisTimeSeconds: 1692803367;
31
+ readonly periodSeconds: 3;
32
+ readonly displayName: "drand quicknet";
33
+ };
34
+ readonly "drand/default": {
35
+ readonly timeBasis: "deterministic-round-time";
36
+ readonly genesisTimeSeconds: 1595431050;
37
+ readonly periodSeconds: 30;
38
+ readonly displayName: "drand default chain";
39
+ };
40
+ readonly "bitcoin/mainnet": {
41
+ readonly timeBasis: "attributive-height";
42
+ readonly displayName: "Bitcoin mainnet";
43
+ };
44
+ };
45
+ export type BeaconSourceId = keyof typeof BEACON_SOURCES;
46
+ export declare const BEACON_SOURCE_IDS: readonly BeaconSourceId[];
47
+ /**
48
+ * A schema-level sanity ceiling on a round index. Not a beacon limit, and deliberately not the
49
+ * representability guarantee either: the ceiling that matters is per-source, because it falls out
50
+ * of each source's own period, and one shared number cannot be sound for all of them. Quicknet's
51
+ * 3-second period puts round 1,000,000,000,000 some 95,000 years out and inside what `Date`
52
+ * represents; the default chain's 30-second period puts the same round ten times further out and
53
+ * outside it. So this bound only rejects the absurd, and `beaconRoundInstant` -- not the schema --
54
+ * owns the guarantee that the arithmetic stays representable. Every real Bitcoin height is eight
55
+ * orders of magnitude below this.
56
+ */
57
+ export declare const MAX_BEACON_ROUND = 1000000000000;
58
+ /**
59
+ * A public beacon reference: which beacon, which round or height, and the value it published
60
+ * there. `round` is the source's own index -- a drand round number, a Bitcoin block height.
61
+ */
62
+ export declare const BeaconReferenceSchema: z.ZodObject<{
63
+ source: z.ZodEnum<{
64
+ "drand/quicknet": "drand/quicknet";
65
+ "drand/default": "drand/default";
66
+ "bitcoin/mainnet": "bitcoin/mainnet";
67
+ }>;
68
+ round: z.ZodNumber;
69
+ value: z.ZodString;
70
+ }, z.core.$strict>;
71
+ export type BeaconReference = z.infer<typeof BeaconReferenceSchema>;
72
+ /**
73
+ * The two shapes, disjoint by construction so no reader can mistake the weaker binding for the
74
+ * stronger one:
75
+ *
76
+ * - `sampled` -- a slate drawn from a larger pool. `sample` is the claim; the verifier recomputes
77
+ * it and fails on mismatch.
78
+ * - `census` -- the whole declared population runs, so there is no draw. `order` is the claim, and
79
+ * it binds execution order only.
80
+ */
81
+ export declare const RunBindingSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
82
+ mode: z.ZodLiteral<"sampled">;
83
+ poolItemSha256s: z.ZodArray<z.ZodString>;
84
+ sampleSize: z.ZodNumber;
85
+ sample: z.ZodArray<z.ZodString>;
86
+ procedure: z.ZodLiteral<"beacon-binding/1">;
87
+ sealDigest: z.ZodString;
88
+ sealedAt: z.ZodString;
89
+ beacon: z.ZodObject<{
90
+ source: z.ZodEnum<{
91
+ "drand/quicknet": "drand/quicknet";
92
+ "drand/default": "drand/default";
93
+ "bitcoin/mainnet": "bitcoin/mainnet";
94
+ }>;
95
+ round: z.ZodNumber;
96
+ value: z.ZodString;
97
+ }, z.core.$strict>;
98
+ }, z.core.$strict>, z.ZodObject<{
99
+ mode: z.ZodLiteral<"census">;
100
+ itemSha256s: z.ZodArray<z.ZodString>;
101
+ order: z.ZodArray<z.ZodString>;
102
+ procedure: z.ZodLiteral<"beacon-binding/1">;
103
+ sealDigest: z.ZodString;
104
+ sealedAt: z.ZodString;
105
+ beacon: z.ZodObject<{
106
+ source: z.ZodEnum<{
107
+ "drand/quicknet": "drand/quicknet";
108
+ "drand/default": "drand/default";
109
+ "bitcoin/mainnet": "bitcoin/mainnet";
110
+ }>;
111
+ round: z.ZodNumber;
112
+ value: z.ZodString;
113
+ }, z.core.$strict>;
114
+ }, z.core.$strict>], "mode">;
115
+ export type RunBinding = z.infer<typeof RunBindingSchema>;
116
+ export declare class RunBindingError extends Error {
117
+ readonly name = "RunBindingError";
118
+ readonly path: string;
119
+ constructor(path: string, detail: string);
120
+ }
121
+ /**
122
+ * The instant a `deterministic-round-time` beacon published `round`, as an RFC 3339 UTC string, or
123
+ * `undefined` for a source whose round index carries no offline time. The schedule is
124
+ * `genesis + (round - 1) * period`: round 1 is published at genesis.
125
+ *
126
+ * Refuses (throws `RunBindingError`) when that instant falls outside the range `Date` represents.
127
+ * The check lives here rather than on the schema because the largest representable round is a
128
+ * function of the source's own period -- the default chain runs out ten times earlier than
129
+ * quicknet does -- so a single schema ceiling would leave the slower source's tail passing
130
+ * validation and then throwing an untyped `RangeError` from `toISOString` deep inside
131
+ * verification. Guarding the arithmetic where the arithmetic happens makes the typed refusal a
132
+ * property of this function, and therefore true for every source and every caller.
133
+ */
134
+ export declare function beaconRoundInstant(beacon: Pick<BeaconReference, "source" | "round">): string | undefined;
135
+ /** The one round a run sealed at a given instant may bind to, and when that round is published. */
136
+ export interface RequiredBeaconRound {
137
+ readonly round: number;
138
+ /** RFC 3339 UTC, from the source's own schedule -- the same arithmetic `beaconRoundInstant` does. */
139
+ readonly publishedAt: string;
140
+ }
141
+ /**
142
+ * The one round a run sealed at `sealedAt` may bind to on a `deterministic-round-time` source: the
143
+ * first round that source publishes STRICTLY after the seal (issue #3322).
144
+ *
145
+ * The point is that the seal already fixes it. `verifyRunBinding` admits any round whose instant
146
+ * postdates the seal, which makes the beacon VALUE unpredictable but leaves the CHOICE among
147
+ * realized values open: between seal and binding an operator sees many published rounds, can derive
148
+ * what each would produce, and can bind the one they prefer. The standard construction is to commit
149
+ * at seal time to a specific future round -- and for a scheduled source no separate commitment
150
+ * record is needed, because `(source, sealedAt)` already determines exactly one such round, and both
151
+ * are fixed at seal time and carried by the binding itself.
152
+ *
153
+ * From `instant(r) = genesis + (r - 1) * period`, the smallest `r` with `instant(r) > sealedAt` is
154
+ * `floor((sealedAt - genesis) / period) + 2`, clamped to round 1 for a seal that predates genesis.
155
+ *
156
+ * `undefined` -- meaning no round is derivable, so the operator's choice remains and the report face
157
+ * says so -- when the source indexes by block height rather than by a schedule, when `sealedAt` is
158
+ * unparseable, or when the required round leaves `MAX_BEACON_ROUND` or the representable range.
159
+ */
160
+ export declare function requiredBeaconRound(source: BeaconSourceId, sealedAt: string): RequiredBeaconRound | undefined;
161
+ export interface BeaconOrderParams {
162
+ /** `sha256:<64 lowercase hex>` -- the sealed record the beacon postdates. */
163
+ readonly sealDigest: string;
164
+ /** The beacon's published value, 64 lowercase hex characters. */
165
+ readonly beaconValue: string;
166
+ /** The identity set to order. Non-empty, unique, each `sha256:<64 lowercase hex>`. */
167
+ readonly itemSha256s: readonly string[];
168
+ }
169
+ export interface BeaconOrderResult {
170
+ /** `sha256:<64 lowercase hex>` of the sorted, unique identity set. */
171
+ readonly poolDigest: string;
172
+ /** Every item, ascending by HMAC stream (unsigned byte order), ties by code-unit order. */
173
+ readonly order: readonly string[];
174
+ }
175
+ /**
176
+ * The derivation itself. Refuses (throws `RunBindingError`) when `sealDigest` or `beaconValue` is
177
+ * malformed, or when `itemSha256s` is empty, contains a duplicate, or contains a malformed entry.
178
+ */
179
+ export declare function computeBeaconOrder(params: BeaconOrderParams): BeaconOrderResult;
180
+ /** Whether the beacon's postdating of the seal was proven here, or only asserted by its chain. */
181
+ export type BeaconPostSealBasis = "proven-offline" | "attributive";
182
+ /**
183
+ * Whether the seal fixed WHICH post-seal value applied, or the operator picked it (issue #3322).
184
+ *
185
+ * `seal-derived` -- the named round is `requiredBeaconRound` for this source and seal, so there was
186
+ * exactly one round to bind to and no choosing happened. `operator-chosen` -- the round postdates
187
+ * the seal (that is still checked) but was selected afterwards from among those published since, so
188
+ * the derivation is one of several the operator could have realized. Every `attributive-height`
189
+ * source is `operator-chosen` by construction: a block height carries no schedule, so no round
190
+ * follows from the seal.
191
+ *
192
+ * This is derived and reported, never enforced here. A verifier states what the bytes are; the
193
+ * choosing happens in the producer, which is where the refusal belongs (`bind` refuses a round other
194
+ * than the derivable one). Refusing here would also make every already-sealed record unreadable.
195
+ */
196
+ export type BeaconRoundBasis = "seal-derived" | "operator-chosen";
197
+ /** What every verified binding carries, whichever mode produced it. */
198
+ export interface VerifiedRunBindingBase {
199
+ readonly procedure: typeof BEACON_BINDING_PROCEDURE;
200
+ readonly beacon: BeaconReference;
201
+ readonly sealDigest: string;
202
+ readonly sealedAt: string;
203
+ /** The recomputed identity-set digest of the pool (sampled) or population (census). */
204
+ readonly poolDigest: string;
205
+ readonly poolSize: number;
206
+ /** The recomputed full order. In census mode this is the execution order. */
207
+ readonly order: readonly string[];
208
+ readonly postSeal: BeaconPostSealBasis;
209
+ /** Whether the seal fixed which post-seal round applied, or the operator chose it. */
210
+ readonly roundBasis: BeaconRoundBasis;
211
+ /** The beacon's own publication instant, when its source's round index determines one. */
212
+ readonly beaconInstant?: string;
213
+ }
214
+ /**
215
+ * Discriminated on `mode` so `sample` is present exactly when there was a draw. A single optional
216
+ * `sample` would make every reader of the stronger binding write a fallback for a case that cannot
217
+ * happen, and the fallback is where a "0 items" sentence gets shipped.
218
+ */
219
+ export type VerifiedRunBinding = (VerifiedRunBindingBase & {
220
+ readonly mode: "census";
221
+ }) | (VerifiedRunBindingBase & {
222
+ readonly mode: "sampled";
223
+ readonly sample: readonly string[];
224
+ });
225
+ /**
226
+ * Verifies one binding record: the beacon postdates the seal, and the declared draw or order is
227
+ * exactly what `beacon-binding/1` derives. Throws `RunBindingError` on any disagreement -- the
228
+ * recomputation wins, always; a stored field never does.
229
+ */
230
+ export declare function verifyRunBinding(candidate: unknown): VerifiedRunBinding;