@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.
- package/README.md +18 -8
- package/dist/admission/contracts.d.ts +241 -0
- package/dist/admission/contracts.js +116 -0
- package/dist/admission/index.d.ts +2 -0
- package/dist/admission/index.js +2 -0
- package/dist/admission/prompted-commitment.d.ts +19 -0
- package/dist/admission/prompted-commitment.js +52 -0
- package/dist/admission/prompted-selection.d.ts +24 -0
- package/dist/admission/prompted-selection.js +85 -0
- package/dist/admission/verification.d.ts +1 -0
- package/dist/admission/verification.js +88 -42
- package/dist/assets.d.ts +21 -1
- package/dist/assets.js +60 -3
- package/dist/binding/beacon-binding.d.ts +230 -0
- package/dist/binding/beacon-binding.js +325 -0
- package/dist/binding/report-face.d.ts +45 -0
- package/dist/binding/report-face.js +153 -0
- package/dist/cli.js +74 -12
- package/dist/index.d.ts +14 -3
- package/dist/index.js +16 -3
- package/dist/manifest.d.ts +30 -4
- package/dist/manifest.js +30 -0
- package/dist/materialize.d.ts +7 -0
- package/dist/materialize.js +7 -0
- package/dist/outcome.d.ts +31 -0
- package/dist/outcome.js +45 -0
- package/dist/profile/claim-consistency.d.ts +8 -1
- package/dist/profile/claim-consistency.js +4 -4
- package/dist/profile/claim.d.ts +201 -2
- package/dist/profile/claim.js +178 -47
- package/dist/profile/disclosure.d.ts +273 -0
- package/dist/profile/disclosure.js +240 -0
- package/dist/profile/run-results.d.ts +8 -2
- package/dist/profile/run-results.js +9 -3
- package/dist/profile/task-selection.d.ts +69 -0
- package/dist/profile/task-selection.js +140 -0
- package/dist/reader-instructions.d.ts +38 -1
- package/dist/reader-instructions.js +42 -2
- package/dist/schema.d.ts +4 -3
- package/dist/schema.js +17 -0
- package/dist/signers.d.ts +48 -0
- package/dist/signers.js +77 -0
- package/dist/verify.d.ts +20 -6
- package/dist/verify.js +156 -31
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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,
|
|
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(
|
|
436
|
-
|
|
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
|
-
||
|
|
444
|
-
||
|
|
445
|
-
||
|
|
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(
|
|
448
|
-
|| Date.parse(
|
|
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.
|
|
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
|
|
462
|
-
if (!sameStrings(rowIdentities,
|
|
463
|
-
fail("screeningTable.rows", "v2 table must cover all 664 pool
|
|
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(
|
|
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 =
|
|
474
|
-
sampleSize =
|
|
489
|
+
sampleSeed = commitmentView.sampleSeed;
|
|
490
|
+
sampleSize = commitmentView.sampleSize;
|
|
475
491
|
sampleIdentities = poolIdentities;
|
|
476
492
|
promptedScreeningProcedureSha256 = screeningTable.procedureSha256;
|
|
477
493
|
promptedScreeningPool = pool;
|
|
478
|
-
promptedCommittedSample =
|
|
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 ||
|
|
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 = [...
|
|
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 /
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
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
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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;
|