@sanning/proof 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -8
- package/dist/agent-proof.d.ts +2 -2
- package/dist/agent-proof.js +4 -4
- package/dist/case-file.d.ts +126 -0
- package/dist/case-file.js +349 -0
- package/dist/case.d.ts +199 -43
- package/dist/case.js +522 -146
- package/dist/cli.d.ts +132 -1
- package/dist/cli.js +1140 -96
- package/dist/crypto.d.ts +1 -1
- package/dist/crypto.js +5 -1
- package/dist/declaration.d.ts +58 -52
- package/dist/declaration.js +256 -33
- package/dist/evidence.d.ts +2 -0
- package/dist/evidence.js +134 -19
- package/dist/exit-codes.d.ts +17 -0
- package/dist/exit-codes.js +50 -0
- package/dist/grading.d.ts +28 -13
- package/dist/grading.js +90 -31
- package/dist/handoff.d.ts +16 -1
- package/dist/handoff.js +91 -16
- package/dist/index.d.ts +10 -7
- package/dist/index.js +15 -7
- package/dist/message-parts.d.ts +11 -9
- package/dist/message-parts.js +11 -9
- package/dist/types.d.ts +1 -0
- package/dist/verifier.d.ts +9 -2
- package/dist/verifier.js +28 -12
- package/dist/zip-reader.d.ts +50 -0
- package/dist/zip-reader.js +220 -0
- package/package.json +2 -2
- package/src/agent-proof.ts +4 -4
- package/src/case-file.ts +419 -0
- package/src/case.ts +651 -156
- package/src/cli.ts +1325 -100
- package/src/crypto.ts +5 -1
- package/src/declaration.ts +248 -52
- package/src/evidence.ts +142 -21
- package/src/exit-codes.ts +54 -0
- package/src/grading.ts +109 -52
- package/src/handoff.ts +108 -17
- package/src/index.ts +60 -3
- package/src/message-parts.ts +11 -9
- package/src/types.ts +5 -0
- package/src/verifier.ts +35 -13
- package/src/zip-reader.ts +229 -0
- package/vectors/README.md +56 -3
- package/vectors/case-file-index.json +668 -0
- package/vectors/case-split.json +1102 -0
- package/vectors/pack-grading.json +68 -4
- package/vectors/stamp-on-chain.json +3 -3
package/src/case.ts
CHANGED
|
@@ -166,7 +166,9 @@
|
|
|
166
166
|
* gets, since that reads wrapper keys.
|
|
167
167
|
*
|
|
168
168
|
* Not exits: a record whose envelope verifies and whose content does not
|
|
169
|
-
* match its commitment is `RECORD_CONTENT_MISMATCH
|
|
169
|
+
* match its commitment is `RECORD_CONTENT_MISMATCH` where that content is
|
|
170
|
+
* inside the signed body and `RECORD_CONTENT_DEPENDENT` where it was handed
|
|
171
|
+
* in beside the pack (proof#159), and one that fails
|
|
170
172
|
* another check inside its pack is `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`.
|
|
171
173
|
* Both are named and both STAY in the pile, so neither unsigned content
|
|
172
174
|
* handed in beside a pack nor a broken inclusion proof removes a record
|
|
@@ -185,7 +187,8 @@
|
|
|
185
187
|
* (F2).
|
|
186
188
|
* 10. **`counterpart_absent`** (`continue`). Skips the ambiguity and pair
|
|
187
189
|
* reporting, and there is nothing to skip: 4.1 emits that finding only
|
|
188
|
-
*
|
|
190
|
+
* where no candidate is left to read, an EMPTY candidate list or one whose
|
|
191
|
+
* every candidate another record accounts for (4.1 v1.17, A20). Which of the three counterpart kinds it
|
|
189
192
|
* gets reads the keys of the packs that verified, counted before exit 4,
|
|
190
193
|
* and whether any pack took exit 2 or 3.
|
|
191
194
|
*
|
|
@@ -206,7 +209,9 @@
|
|
|
206
209
|
* verdict, exactly as `counterpart_absent` and `not_a_handoff_record` are
|
|
207
210
|
* findings beside a verdict in 4.1.
|
|
208
211
|
*
|
|
209
|
-
* A case with
|
|
212
|
+
* A case with any `failed` contribution is `failed`: a proven failure keeps
|
|
213
|
+
* the headline (5.5.4 v1.16, A17, proof#158). Otherwise a case with more than
|
|
214
|
+
* one outcome is `mixed` (anchor#198, criterion 4).
|
|
210
215
|
*
|
|
211
216
|
* WHAT THIS READER DOES NOT CLAIM
|
|
212
217
|
* ===============================
|
|
@@ -225,20 +230,24 @@
|
|
|
225
230
|
* with that verdict and its reasons, so the case never reads above its
|
|
226
231
|
* weakest pack. On-chain re-fetch and `gradePack` are a caller's to run.
|
|
227
232
|
*
|
|
228
|
-
* THE COST OF 4.1's "GRADE EVERY CANDIDATE",
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
233
|
+
* THE COST OF 4.1's "GRADE EVERY CANDIDATE", AND WHAT IS LEFT OF IT (4.1
|
|
234
|
+
* v1.17, amendment A20, which reopens proof#134). Up to v1.16 a pile holding
|
|
235
|
+
* two or more records of the complementary type signed by one counterparty key
|
|
236
|
+
* could not reach `verified`. 4.1 now sets such a candidate aside where a
|
|
237
|
+
* record the subject's own key signed pairs with it verified at another
|
|
238
|
+
* sequence, so two messages in one direction, and the demo's A to B, B to A,
|
|
239
|
+
* A to B, read clean. What is left surfaces as `COUNTERPART_AMBIGUOUS` beside
|
|
240
|
+
* the failure: fan-in, fan-out, and the same pair in two cases at the same
|
|
241
|
+
* sequence. Narrowing any of it here with `handoffs[]` is precisely the move
|
|
242
|
+
* that would reopen proof#137's door.
|
|
238
243
|
*/
|
|
239
244
|
|
|
240
245
|
import {
|
|
246
|
+
type DeclarationCheck,
|
|
241
247
|
type DeclarationClaims,
|
|
248
|
+
type DeclarationRole,
|
|
249
|
+
DECLARATION_VERSION,
|
|
250
|
+
DECLARATION_VERSION_V2,
|
|
242
251
|
checkDeclaration,
|
|
243
252
|
declarationClaims,
|
|
244
253
|
packRecords,
|
|
@@ -253,7 +262,14 @@ import {
|
|
|
253
262
|
} from "./handoff.js";
|
|
254
263
|
import { verifyEvidenceBundle } from "./evidence.js";
|
|
255
264
|
import type { EvidenceBundleResult } from "./evidence.js";
|
|
256
|
-
import
|
|
265
|
+
import {
|
|
266
|
+
type PackGrade,
|
|
267
|
+
type PackGradeCauseKind,
|
|
268
|
+
type PackVerdict,
|
|
269
|
+
accountDisclosure,
|
|
270
|
+
completenessNotes,
|
|
271
|
+
gradePack,
|
|
272
|
+
} from "./grading.js";
|
|
257
273
|
import type { SideContentMap } from "./side-content.js";
|
|
258
274
|
|
|
259
275
|
// ---- The vocabulary ---------------------------------------------------------
|
|
@@ -335,7 +351,9 @@ export const CASE_QUARANTINED_PROFILES = ["sanning.mlflow/v1"] as const;
|
|
|
335
351
|
/**
|
|
336
352
|
* A conformant hand-off record with no counterpart among the records this
|
|
337
353
|
* reader read, where every pack in the case verified under a key this reader
|
|
338
|
-
* can read and NO pack in the case is signed by the key the record names
|
|
354
|
+
* can read and NO pack in the case is signed by the key the record names,
|
|
355
|
+
* or where every candidate it had was accounted for by another record under
|
|
356
|
+
* A20 (the exception then carries `count`, and names no pack).
|
|
339
357
|
* **An exception, never a failure** (anchor#198, criterion 3).
|
|
340
358
|
*
|
|
341
359
|
* That is ALL it says. It does not say the counterparty's account is absent,
|
|
@@ -425,9 +443,11 @@ export const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS =
|
|
|
425
443
|
export const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
|
|
426
444
|
|
|
427
445
|
/**
|
|
428
|
-
* More than one candidate counterpart (4.1
|
|
429
|
-
* reading is the worst of them and the ambiguity is named: a reader
|
|
430
|
-
* to take one match can be handed an honest decoy beside a tampered
|
|
446
|
+
* More than one candidate counterpart that nothing accounts for (4.1 v1.17,
|
|
447
|
+
* A20). The reading is the worst of them and the ambiguity is named: a reader
|
|
448
|
+
* permitted to take one match can be handed an honest decoy beside a tampered
|
|
449
|
+
* record. `count` is how many of the candidates are live, which is the number
|
|
450
|
+
* the ambiguity is about.
|
|
431
451
|
*/
|
|
432
452
|
export const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
|
|
433
453
|
|
|
@@ -618,17 +638,26 @@ export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
|
|
|
618
638
|
export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
|
|
619
639
|
|
|
620
640
|
/**
|
|
621
|
-
* The pack's wrapper HOLDS and its
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
641
|
+
* The pack's wrapper HOLDS and its CASE GRADE is not `verified`
|
|
642
|
+
* (evidence-bundle 5.5.1 step 1 v1.20, A23; it replaces
|
|
643
|
+
* `pack_bundle_not_verified`, which carried the bundle status alone). The case
|
|
644
|
+
* grade is the pack grade of 5.4.5, taken twice: the SIGNED grade over the run
|
|
645
|
+
* with the side map empty, and the FULL grade over the run with it. The signed
|
|
646
|
+
* grade decides where it is `failed` or `malformed`; a failure only the full
|
|
647
|
+
* grade reaches is `incomplete`, because it rests on bytes nobody signed;
|
|
648
|
+
* otherwise the full grade. A body the verifier could not read is `malformed`,
|
|
649
|
+
* a body that is not one of the two event-carrying bodies takes the signed
|
|
650
|
+
* run's status, and a verifier that graded a different number of records than
|
|
651
|
+
* the pack holds takes the signed status, or `malformed` where that status is
|
|
652
|
+
* `verified`.
|
|
653
|
+
*
|
|
654
|
+
* The exception carries `status`, the case grade; `causes`, the 5.4.5 cause
|
|
655
|
+
* kinds of the grade that decided it (none in the first three branches); and
|
|
656
|
+
* `reasons`, the verifier's reasons from the run with the side map empty. It
|
|
657
|
+
* CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
|
|
658
|
+
* `mixed`, so the case verdict never reads above its weakest pack.
|
|
630
659
|
*/
|
|
631
|
-
export const
|
|
660
|
+
export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
|
|
632
661
|
|
|
633
662
|
/**
|
|
634
663
|
* A record in a pack whose wrapper holds, whose OWN envelope does not verify
|
|
@@ -652,13 +681,41 @@ export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its
|
|
|
652
681
|
|
|
653
682
|
/**
|
|
654
683
|
* A record whose envelope verifies and whose disclosed content does not hash to
|
|
655
|
-
* the
|
|
656
|
-
*
|
|
684
|
+
* the hash it commits to, its `content_hash` or a hand-off record's
|
|
685
|
+
* `message_hash` (evidence-bundle v1.19, A20) (`EventResult.contentOk` is false), with the
|
|
686
|
+
* side map EMPTY: the content is inside the signed body. The same mismatch from
|
|
687
|
+
* content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
|
|
688
|
+
* `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
|
|
657
689
|
* supplied by whoever assembled the case, and gate 3 on proof#140 showed that
|
|
658
690
|
* party erasing an honest verified pair by swapping it.
|
|
659
691
|
*/
|
|
660
692
|
export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
|
|
661
693
|
|
|
694
|
+
/**
|
|
695
|
+
* The two readings proof#147 did not reach, given its shape (proof#159, ruled
|
|
696
|
+
* by Phil 2026-09-22, evidence-bundle.md 5.5.4 v1.16, A17). A pack whose
|
|
697
|
+
* bundle verdict is `failed` ONLY when read with the unsigned content handed
|
|
698
|
+
* in beside it, and `failed` no longer when read with that map empty, is
|
|
699
|
+
* `PACK_BUNDLE_CONTENT_DEPENDENT`; a record whose content mismatch comes from
|
|
700
|
+
* that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
|
|
701
|
+
* reader established that the bytes it was handed are not the bytes the record
|
|
702
|
+
* commits to, and nothing about the party that signed the record, because
|
|
703
|
+
* whoever assembled the case chose those bytes with no key. Up to v1.15 both
|
|
704
|
+
* readings carried `failed`, and one flipped nibble of accuser-supplied content
|
|
705
|
+
* put a `failed` against an intact pack; under A17's headline rule that would
|
|
706
|
+
* have become the case's headline.
|
|
707
|
+
*
|
|
708
|
+
* The test that separates the two is the one proof#159 states for the inverse:
|
|
709
|
+
* every deviation a signature covers still reads `failed` with the side map
|
|
710
|
+
* EMPTY. So the pack is verified twice, with the map and without it, and what
|
|
711
|
+
* fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
|
|
712
|
+
* (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
|
|
713
|
+
* each still `failed`. Emptying the map is the most a party that signed
|
|
714
|
+
* nothing can do to starve the check, and it cannot remove any of those.
|
|
715
|
+
*/
|
|
716
|
+
export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
|
|
717
|
+
export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
|
|
718
|
+
|
|
662
719
|
/**
|
|
663
720
|
* A record signed under a ratified profile that is under quarantine
|
|
664
721
|
* (`CASE_QUARANTINED_PROFILES`, pending DL-63). Not read, in any kernel.
|
|
@@ -670,11 +727,14 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
|
670
727
|
export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
671
728
|
|
|
672
729
|
/**
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
730
|
+
* The two NOTEs (5.5.3 v1.20): findings on the reading with no contribution,
|
|
731
|
+
* printed in the scope sentence so a clean verdict never hides either.
|
|
732
|
+
* `pack_undeclared` is a note only where the reading holds no hand-off record;
|
|
733
|
+
* where it holds one, it is the exception of that name.
|
|
676
734
|
*/
|
|
677
|
-
export const
|
|
735
|
+
export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
736
|
+
export const CASE_NOTE_KINDS = [PACK_UNDECLARED, CONTENT_WITHHELD_BY_DECLARATION] as const;
|
|
737
|
+
export type CaseNoteKind = (typeof CASE_NOTE_KINDS)[number];
|
|
678
738
|
|
|
679
739
|
/**
|
|
680
740
|
* Every exception this reader can report, as a closed set. A surface may render
|
|
@@ -699,13 +759,14 @@ export const CASE_EXCEPTION_KINDS = [
|
|
|
699
759
|
PACK_HOLDS_NO_RECORDS,
|
|
700
760
|
PACK_KEY_UNREADABLE,
|
|
701
761
|
PACK_DOES_NOT_VERIFY,
|
|
702
|
-
|
|
762
|
+
PACK_GRADE_NOT_VERIFIED,
|
|
763
|
+
PACK_BUNDLE_CONTENT_DEPENDENT,
|
|
703
764
|
RECORD_ENVELOPE_DOES_NOT_VERIFY,
|
|
704
765
|
RECORD_DOES_NOT_VERIFY_IN_ITS_PACK,
|
|
705
766
|
RECORD_CONTENT_MISMATCH,
|
|
767
|
+
RECORD_CONTENT_DEPENDENT,
|
|
706
768
|
RECORD_PROFILE_QUARANTINED,
|
|
707
769
|
CASE_HOLDS_NO_PACKS,
|
|
708
|
-
CASE_HOLDS_NO_HANDOFF_RECORDS,
|
|
709
770
|
] as const;
|
|
710
771
|
|
|
711
772
|
export type CaseExceptionKind = (typeof CASE_EXCEPTION_KINDS)[number];
|
|
@@ -721,10 +782,10 @@ export type CaseExceptionKind = (typeof CASE_EXCEPTION_KINDS)[number];
|
|
|
721
782
|
* contributes a clean verdict, which is what makes the invariant in `verdictOf`
|
|
722
783
|
* hold.
|
|
723
784
|
*
|
|
724
|
-
* `
|
|
725
|
-
* contributes the pack's
|
|
726
|
-
*
|
|
727
|
-
*
|
|
785
|
+
* `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
|
|
786
|
+
* contributes the pack's case grade, which is never `verified` (the exception
|
|
787
|
+
* is emitted only when it is not). `null` marks that row, and `readCase` fills
|
|
788
|
+
* it from `status`.
|
|
728
789
|
*/
|
|
729
790
|
const EXCEPTION_VERDICT: Record<CaseExceptionKind, CaseVerdict | null> = {
|
|
730
791
|
[COUNTERPART_ABSENT]: "incomplete",
|
|
@@ -745,15 +806,28 @@ const EXCEPTION_VERDICT: Record<CaseExceptionKind, CaseVerdict | null> = {
|
|
|
745
806
|
[PACK_HOLDS_NO_RECORDS]: "incomplete",
|
|
746
807
|
[PACK_KEY_UNREADABLE]: "incomplete",
|
|
747
808
|
[PACK_DOES_NOT_VERIFY]: "failed",
|
|
748
|
-
[
|
|
809
|
+
[PACK_GRADE_NOT_VERIFIED]: null,
|
|
810
|
+
[PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
|
|
749
811
|
[RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
|
|
750
812
|
[RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
|
|
751
813
|
[RECORD_CONTENT_MISMATCH]: "failed",
|
|
814
|
+
[RECORD_CONTENT_DEPENDENT]: "incomplete",
|
|
752
815
|
[RECORD_PROFILE_QUARANTINED]: "incomplete",
|
|
753
816
|
[CASE_HOLDS_NO_PACKS]: "partial",
|
|
754
|
-
[CASE_HOLDS_NO_HANDOFF_RECORDS]: "partial",
|
|
755
817
|
};
|
|
756
818
|
|
|
819
|
+
/**
|
|
820
|
+
* The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
|
|
821
|
+
* other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
|
|
822
|
+
* grade passes through and a case verdict never issues) is `incomplete`.
|
|
823
|
+
*/
|
|
824
|
+
const GRADE_CONTRIBUTES: ReadonlySet<string> = new Set([
|
|
825
|
+
"partial",
|
|
826
|
+
"incomplete",
|
|
827
|
+
"failed",
|
|
828
|
+
"malformed",
|
|
829
|
+
]);
|
|
830
|
+
|
|
757
831
|
// ---- Inputs -----------------------------------------------------------------
|
|
758
832
|
|
|
759
833
|
/**
|
|
@@ -793,13 +867,24 @@ export interface CaseException {
|
|
|
793
867
|
counterpartyKey?: string | null;
|
|
794
868
|
/** Which checks a formed pair failed, in 4.1's fixed order. */
|
|
795
869
|
failedChecks?: string[];
|
|
796
|
-
/**
|
|
870
|
+
/**
|
|
871
|
+
* How many of a thing the exception is about, where it is about a count. On
|
|
872
|
+
* `COUNTERPART_ABSENT` it is set only where 4.1 set every candidate aside
|
|
873
|
+
* (v1.17, A20), and it is how many were set aside.
|
|
874
|
+
*/
|
|
797
875
|
count?: number;
|
|
798
|
-
/** The pack's
|
|
876
|
+
/** The pack's case grade, for `PACK_GRADE_NOT_VERIFIED`. */
|
|
799
877
|
status?: CaseVerdict;
|
|
878
|
+
/**
|
|
879
|
+
* The 5.4.5 cause kinds of the grade that decided the case grade, for
|
|
880
|
+
* `PACK_GRADE_NOT_VERIFIED`, in the grade's order. Empty where the grade was
|
|
881
|
+
* not computed (an unreadable body, a body that is not one of the two
|
|
882
|
+
* event-carrying ones, a verifier that graded too few records).
|
|
883
|
+
*/
|
|
884
|
+
causes?: PackGradeCauseKind[];
|
|
800
885
|
/**
|
|
801
886
|
* What the bundle verifier said, as it said it, for the kinds that carry a
|
|
802
|
-
* verifier's grading: `
|
|
887
|
+
* verifier's grading: `PACK_GRADE_NOT_VERIFIED` and the three record kinds
|
|
803
888
|
* graded on the record's own envelope and content.
|
|
804
889
|
*/
|
|
805
890
|
reasons?: string[];
|
|
@@ -821,6 +906,54 @@ export interface CaseRecordReading {
|
|
|
821
906
|
findings: string[];
|
|
822
907
|
failedChecks: string[];
|
|
823
908
|
counterparts: string[];
|
|
909
|
+
/**
|
|
910
|
+
* The role the pack's v2 declaration signs for this record, `handoff` or
|
|
911
|
+
* `step`, or `null` where the pack's declaration is absent, unreadable or
|
|
912
|
+
* v1, or does not declare the record (5.5.1 v1.20).
|
|
913
|
+
*/
|
|
914
|
+
role: DeclarationRole | null;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* One NOTE on the reading (5.5.3 v1.20): something true a reader needs and
|
|
919
|
+
* nothing a reader could not tell, so it carries no contribution.
|
|
920
|
+
*/
|
|
921
|
+
export interface CaseNote {
|
|
922
|
+
kind: CaseNoteKind;
|
|
923
|
+
/** Which pack, by the reader's own name for it. */
|
|
924
|
+
packRef: string;
|
|
925
|
+
/**
|
|
926
|
+
* For `CONTENT_WITHHELD_BY_DECLARATION`, how many of the pack's records its
|
|
927
|
+
* declaration withholds.
|
|
928
|
+
*/
|
|
929
|
+
count?: number;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* One pack as the reading read it, in the order the reader was given them
|
|
934
|
+
* (5.5.4 v1.20). A surface names a pack by `signingKey`, the key that signed
|
|
935
|
+
* its wrapper, and never by an organization (D10).
|
|
936
|
+
*/
|
|
937
|
+
export interface CasePackReading {
|
|
938
|
+
ref: string;
|
|
939
|
+
/** The wrapper key, normalized, or `null` where this reader could not read it. */
|
|
940
|
+
signingKey: string | null;
|
|
941
|
+
/**
|
|
942
|
+
* The case grade (5.5.1 step 1), or `null` where the wrapper did not hold or
|
|
943
|
+
* the key could not be read.
|
|
944
|
+
*/
|
|
945
|
+
grade: PackVerdict | null;
|
|
946
|
+
/**
|
|
947
|
+
* Records withheld by the pack's declaration (5.4.5): in scope for their
|
|
948
|
+
* signature and commitment only. Never counted as verified records.
|
|
949
|
+
*/
|
|
950
|
+
recordsWithheld: number;
|
|
951
|
+
/**
|
|
952
|
+
* `sanning.declaration/v1`, `sanning.declaration/v2`, or `null` where the
|
|
953
|
+
* pack carries no declaration, one this reader does not know, or a body this
|
|
954
|
+
* reader does not read.
|
|
955
|
+
*/
|
|
956
|
+
declarationVersion: string | null;
|
|
824
957
|
}
|
|
825
958
|
|
|
826
959
|
export interface CaseReading {
|
|
@@ -864,6 +997,10 @@ export interface CaseReading {
|
|
|
864
997
|
* checked packs, which is false.
|
|
865
998
|
*/
|
|
866
999
|
onChainChecked: boolean;
|
|
1000
|
+
/** The NOTEs, in pack order (5.5.3 v1.20). No contribution. */
|
|
1001
|
+
notes: CaseNote[];
|
|
1002
|
+
/** One reading per pack, in the order the reader was given them. */
|
|
1003
|
+
packs: CasePackReading[];
|
|
867
1004
|
}
|
|
868
1005
|
|
|
869
1006
|
// ---- The reading ------------------------------------------------------------
|
|
@@ -910,6 +1047,24 @@ const wrapperHolds = (result: EvidenceBundleResult): boolean =>
|
|
|
910
1047
|
* bundle-level error at all, and an exception that named the verdict and no
|
|
911
1048
|
* reason would send a reader looking for a defect in the pack as a whole.
|
|
912
1049
|
*/
|
|
1050
|
+
/**
|
|
1051
|
+
* The reasons a reading carries only because of the side map, in the order the
|
|
1052
|
+
* verifier wrote them: what the content-dependent kinds are about, and nothing
|
|
1053
|
+
* a signature covers.
|
|
1054
|
+
*/
|
|
1055
|
+
const onlyIn = (withMap: readonly string[], withoutMap: readonly string[]): string[] =>
|
|
1056
|
+
withMap.filter((reason) => !withoutMap.includes(reason));
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Whether a side-content map holds any key. Python's `if pack.content` is
|
|
1060
|
+
* the twin: `None` and an empty map both mean the two verify runs are one run.
|
|
1061
|
+
*/
|
|
1062
|
+
function sideMapHasKeys(content: CasePack["content"]): boolean {
|
|
1063
|
+
if (content === null || content === undefined) return false;
|
|
1064
|
+
if (content instanceof Map) return content.size > 0;
|
|
1065
|
+
return Object.keys(content as object).length > 0;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
913
1068
|
const bundleReasons = (result: EvidenceBundleResult): string[] => [
|
|
914
1069
|
...result.errors,
|
|
915
1070
|
...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
|
|
@@ -946,8 +1101,13 @@ function underQuarantine(specVersion: unknown): boolean {
|
|
|
946
1101
|
/**
|
|
947
1102
|
* The case verdict, from the contributions alone.
|
|
948
1103
|
*
|
|
949
|
-
*
|
|
950
|
-
*
|
|
1104
|
+
* **A proven failure keeps the headline** (`evidence-bundle.md` 5.5.4 v1.16,
|
|
1105
|
+
* A17, proof#158): if any contribution is `failed`, the case is `failed`,
|
|
1106
|
+
* whatever else contributes, and every other exception stays named beside it.
|
|
1107
|
+
* Otherwise one outcome is that outcome, and more than one is `mixed`
|
|
1108
|
+
* (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
|
|
1109
|
+
* does NOT keep the headline: it says a record cannot be read, not that a
|
|
1110
|
+
* check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
|
|
951
1111
|
*
|
|
952
1112
|
* THE INVARIANT, stated so a mutation test has something to kill: `verified` is
|
|
953
1113
|
* reachable ONLY when `exceptions` is empty. Every exception kind carries a
|
|
@@ -959,13 +1119,26 @@ function underQuarantine(specVersion: unknown): boolean {
|
|
|
959
1119
|
function verdictOf(
|
|
960
1120
|
exceptions: readonly CaseException[],
|
|
961
1121
|
contributions: readonly [string, CaseVerdict][],
|
|
1122
|
+
holdsPacks = true,
|
|
962
1123
|
): CaseVerdict {
|
|
963
|
-
//
|
|
964
|
-
//
|
|
965
|
-
//
|
|
966
|
-
|
|
1124
|
+
// A reading that holds at least one pack and no contribution at all is
|
|
1125
|
+
// `verified` (5.5.4 v1.20, A23): every pack in it graded `verified` and
|
|
1126
|
+
// nothing in it was an exception. Every exception contributes, so no
|
|
1127
|
+
// contribution means no exception. A reading with no packs always carries
|
|
1128
|
+
// `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
|
|
1129
|
+
// nothing reading as `verified` is the defect class itself.
|
|
1130
|
+
if (contributions.length === 0) {
|
|
1131
|
+
if (exceptions.length > 0) {
|
|
1132
|
+
throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
|
|
1133
|
+
}
|
|
1134
|
+
return holdsPacks ? "verified" : "partial";
|
|
1135
|
+
}
|
|
967
1136
|
const outcomes = new Set(contributions.map(([, v]) => v));
|
|
968
|
-
const verdict: CaseVerdict = outcomes.
|
|
1137
|
+
const verdict: CaseVerdict = outcomes.has("failed")
|
|
1138
|
+
? "failed"
|
|
1139
|
+
: outcomes.size === 1
|
|
1140
|
+
? [...outcomes][0]
|
|
1141
|
+
: "mixed";
|
|
969
1142
|
if (verdict === "verified" && exceptions.length > 0) {
|
|
970
1143
|
throw new Error(
|
|
971
1144
|
`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`,
|
|
@@ -1004,28 +1177,40 @@ function verdictOf(
|
|
|
1004
1177
|
export async function readCase(
|
|
1005
1178
|
packs: readonly CasePack[],
|
|
1006
1179
|
): Promise<CaseReading> {
|
|
1007
|
-
|
|
1008
|
-
|
|
1180
|
+
// A `null` slot is held for each pack that carries no declaration, and
|
|
1181
|
+
// settled once the pile says whether the reading holds a hand-off record.
|
|
1182
|
+
const exceptions: (CaseException | null)[] = [];
|
|
1183
|
+
const contributions: ([string, CaseVerdict] | null)[] = [];
|
|
1009
1184
|
|
|
1010
1185
|
const note = (
|
|
1011
1186
|
kind: CaseExceptionKind,
|
|
1012
1187
|
fields: Omit<CaseException, "kind"> = {},
|
|
1013
1188
|
) => {
|
|
1014
1189
|
exceptions.push({ kind, ...fields });
|
|
1015
|
-
// `null` is
|
|
1016
|
-
|
|
1190
|
+
// `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
|
|
1191
|
+
// verified, and `incomplete` for a word a pack grade passes through and a
|
|
1192
|
+
// case verdict never issues (5.5.1 step 1 v1.20).
|
|
1193
|
+
let contribution = EXCEPTION_VERDICT[kind];
|
|
1194
|
+
if (contribution === null) {
|
|
1195
|
+
const status = fields.status as string;
|
|
1196
|
+
contribution = GRADE_CONTRIBUTES.has(status) ? (status as CaseVerdict) : "incomplete";
|
|
1197
|
+
}
|
|
1017
1198
|
contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
|
|
1018
1199
|
};
|
|
1019
1200
|
|
|
1020
1201
|
if (packs.length === 0) {
|
|
1021
1202
|
note(CASE_HOLDS_NO_PACKS);
|
|
1203
|
+
const named = exceptions as CaseException[];
|
|
1204
|
+
const contributed = contributions as [string, CaseVerdict][];
|
|
1022
1205
|
return {
|
|
1023
|
-
verdict: verdictOf(
|
|
1024
|
-
exceptions,
|
|
1206
|
+
verdict: verdictOf(named, contributed, false),
|
|
1207
|
+
exceptions: named,
|
|
1025
1208
|
records: [],
|
|
1026
|
-
contributions,
|
|
1209
|
+
contributions: contributed,
|
|
1027
1210
|
handoffRecords: 0,
|
|
1028
1211
|
onChainChecked: false,
|
|
1212
|
+
notes: [],
|
|
1213
|
+
packs: [],
|
|
1029
1214
|
};
|
|
1030
1215
|
}
|
|
1031
1216
|
|
|
@@ -1070,6 +1255,20 @@ export async function readCase(
|
|
|
1070
1255
|
// below, from the pack envelope and never from the body, so no body type, no
|
|
1071
1256
|
// disclosure choice and no record content decides whether it is read.
|
|
1072
1257
|
const holderKeys = new Set<string>();
|
|
1258
|
+
// One reading per pack, in the order the reader was given them (5.5.4 v1.20).
|
|
1259
|
+
const packReadings: CasePackReading[] = [];
|
|
1260
|
+
// The NOTEs (5.5.3 v1.20), with the pack position they are about.
|
|
1261
|
+
const caseNotes: { position: number; note: CaseNote }[] = [];
|
|
1262
|
+
// The role each pack's readable v2 declaration signs for each record, keyed
|
|
1263
|
+
// by the pack's POSITION for the reason `claimsByPack` is.
|
|
1264
|
+
const rolesByPack = new Map<number, Map<string, DeclarationRole>>();
|
|
1265
|
+
// The undeclared packs' slots, settled after the pile is read.
|
|
1266
|
+
const undeclaredSlots: {
|
|
1267
|
+
exceptionAt: number;
|
|
1268
|
+
contributionAt: number;
|
|
1269
|
+
position: number;
|
|
1270
|
+
ref: string;
|
|
1271
|
+
}[] = [];
|
|
1073
1272
|
// How many packs this reader could not tie to a key: an unreadable wrapper
|
|
1074
1273
|
// key, or a wrapper that does not hold. While it is not zero, "no pack in this
|
|
1075
1274
|
// case is signed by the named key" is not established, so the innocent
|
|
@@ -1086,6 +1285,13 @@ export async function readCase(
|
|
|
1086
1285
|
// of these spellings (proof#135) and the reading must not.
|
|
1087
1286
|
note(PACK_KEY_UNREADABLE, { packRef: pack.ref });
|
|
1088
1287
|
unattributed += 1;
|
|
1288
|
+
packReadings.push({
|
|
1289
|
+
ref: pack.ref,
|
|
1290
|
+
signingKey: null,
|
|
1291
|
+
grade: null,
|
|
1292
|
+
recordsWithheld: 0,
|
|
1293
|
+
declarationVersion: null,
|
|
1294
|
+
});
|
|
1089
1295
|
continue;
|
|
1090
1296
|
}
|
|
1091
1297
|
|
|
@@ -1101,30 +1307,127 @@ export async function readCase(
|
|
|
1101
1307
|
| undefined,
|
|
1102
1308
|
});
|
|
1103
1309
|
witnessChecked.push(verified.onChainChecked);
|
|
1310
|
+
// The same pack with the side map EMPTY (proof#159): what fails here is
|
|
1311
|
+
// what a signature covers, and what fails only above rests on bytes nobody
|
|
1312
|
+
// signed. Skipped where there is no map, because the two runs are then the
|
|
1313
|
+
// same run.
|
|
1314
|
+
const signed = sideMapHasKeys(pack.content)
|
|
1315
|
+
? await verifyEvidenceBundle(pack.bundle, {})
|
|
1316
|
+
: verified;
|
|
1104
1317
|
|
|
1105
1318
|
// WHOSE PACK IS IT. The wrapper, and only the wrapper.
|
|
1106
1319
|
if (!wrapperHolds(verified)) {
|
|
1107
1320
|
note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref });
|
|
1108
1321
|
unattributed += 1;
|
|
1322
|
+
packReadings.push({
|
|
1323
|
+
ref: pack.ref,
|
|
1324
|
+
signingKey: holder,
|
|
1325
|
+
grade: null,
|
|
1326
|
+
recordsWithheld: 0,
|
|
1327
|
+
declarationVersion: null,
|
|
1328
|
+
});
|
|
1109
1329
|
continue;
|
|
1110
1330
|
}
|
|
1111
1331
|
holderKeys.add(holder);
|
|
1112
1332
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1333
|
+
const bodyType = bodyTypeOf(pack.bundle);
|
|
1334
|
+
const records = packRecords(pack.bundle, pack.content);
|
|
1335
|
+
// The per-record gradings below are joined to `records` BY INDEX, so a
|
|
1336
|
+
// verifier that graded a different number of records than the pack holds
|
|
1337
|
+
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
1338
|
+
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
1339
|
+
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
1340
|
+
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
1341
|
+
// must never crash on a pack a party can build, so the gap is graded rather
|
|
1342
|
+
// than indexed into, and the pack's records are dropped exactly as a
|
|
1343
|
+
// `malformed` pack's are (gate 4 on proof#140, finding 3).
|
|
1344
|
+
const ungraded =
|
|
1345
|
+
verified.events.length !== records.length || signed.events.length !== records.length;
|
|
1346
|
+
const knownBody = (CASE_BODY_TYPES as readonly unknown[]).includes(bodyType);
|
|
1347
|
+
|
|
1348
|
+
// WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
|
|
1349
|
+
// propagates: the case never reads above its weakest pack (5.5.1 step 1
|
|
1350
|
+
// v1.20, A23; up to v1.19 it was the bundle status).
|
|
1351
|
+
let check: DeclarationCheck | null = null;
|
|
1352
|
+
let full: PackGrade | null = null;
|
|
1353
|
+
let causes: PackGradeCauseKind[] = [];
|
|
1354
|
+
let grade: PackVerdict;
|
|
1355
|
+
if (signed.status === "malformed") {
|
|
1356
|
+
grade = "malformed";
|
|
1357
|
+
} else if (!knownBody) {
|
|
1358
|
+
grade = signed.status as PackVerdict;
|
|
1359
|
+
} else if (ungraded) {
|
|
1360
|
+
grade = signed.status === "verified" ? "malformed" : (signed.status as PackVerdict);
|
|
1361
|
+
} else {
|
|
1362
|
+
check = checkDeclaration(pack.bundle, pack.content);
|
|
1363
|
+
const completeness = completenessNotes(pack.bundle);
|
|
1364
|
+
full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
|
|
1365
|
+
const bareGrade = gradePack(
|
|
1366
|
+
signed,
|
|
1367
|
+
accountDisclosure(pack.bundle, {}),
|
|
1368
|
+
completeness,
|
|
1369
|
+
checkDeclaration(pack.bundle, null),
|
|
1370
|
+
);
|
|
1371
|
+
// NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
|
|
1372
|
+
// where it is `failed` or `malformed`; a failure only the side map
|
|
1373
|
+
// produced is `incomplete`; otherwise the full grade.
|
|
1374
|
+
if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
|
|
1375
|
+
grade = bareGrade.verdict;
|
|
1376
|
+
causes = bareGrade.causes.map((c) => c.kind);
|
|
1377
|
+
} else if (full.verdict === "failed" || full.verdict === "malformed") {
|
|
1378
|
+
grade = "incomplete";
|
|
1379
|
+
causes = full.causes.map((c) => c.kind);
|
|
1380
|
+
} else {
|
|
1381
|
+
grade = full.verdict;
|
|
1382
|
+
causes = full.causes.map((c) => c.kind);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
if (grade !== "verified") {
|
|
1386
|
+
const reasons = bundleReasons(signed);
|
|
1387
|
+
if (knownBody && ungraded && signed.status !== "malformed") {
|
|
1388
|
+
// Said, because nothing else says it: the verifier's own reasons are
|
|
1389
|
+
// about the bundle, and this pack's records are dropped for a reason
|
|
1390
|
+
// of this reader's.
|
|
1391
|
+
reasons.push(
|
|
1392
|
+
`this verifier graded ${verified.events.length} of the ${records.length} ` +
|
|
1393
|
+
"record(s) the pack holds, so none of them is read",
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
note(PACK_GRADE_NOT_VERIFIED, {
|
|
1397
|
+
packRef: pack.ref,
|
|
1398
|
+
status: grade as CaseVerdict,
|
|
1399
|
+
causes,
|
|
1400
|
+
reasons,
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
if (verified.status === "failed" && signed.status !== "failed") {
|
|
1404
|
+
note(PACK_BUNDLE_CONTENT_DEPENDENT, {
|
|
1117
1405
|
packRef: pack.ref,
|
|
1118
|
-
|
|
1119
|
-
|
|
1406
|
+
reasons: onlyIn(bundleReasons(verified), bundleReasons(signed)),
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
const recordsWithheld = full !== null ? full.recordsWithheld : 0;
|
|
1410
|
+
packReadings.push({
|
|
1411
|
+
ref: pack.ref,
|
|
1412
|
+
signingKey: holder,
|
|
1413
|
+
grade,
|
|
1414
|
+
recordsWithheld,
|
|
1415
|
+
declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
|
|
1416
|
+
});
|
|
1417
|
+
if (recordsWithheld > 0) {
|
|
1418
|
+
// A NOTE, with no contribution (5.5.3 v1.20): the holder signed that
|
|
1419
|
+
// these records' content does not travel, and the scope sentence counts
|
|
1420
|
+
// them, so a clean verdict never hides them.
|
|
1421
|
+
caseNotes.push({
|
|
1422
|
+
position,
|
|
1423
|
+
note: { kind: CONTENT_WITHHELD_BY_DECLARATION, packRef: pack.ref, count: recordsWithheld },
|
|
1120
1424
|
});
|
|
1121
1425
|
}
|
|
1122
1426
|
// The body is one the verifier could not read, so it graded no record, and
|
|
1123
1427
|
// nothing below has a grading to read. Named above.
|
|
1124
|
-
if (
|
|
1428
|
+
if (signed.status === "malformed") continue;
|
|
1125
1429
|
|
|
1126
|
-
|
|
1127
|
-
if (!(CASE_BODY_TYPES as readonly unknown[]).includes(bodyType)) {
|
|
1430
|
+
if (!knownBody) {
|
|
1128
1431
|
// `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
|
|
1129
1432
|
// records sit under `body.source_bundle`, and reading them is a change to
|
|
1130
1433
|
// which packs this reader grades, so it is a decision of its own and not
|
|
@@ -1134,58 +1437,37 @@ export async function readCase(
|
|
|
1134
1437
|
note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref });
|
|
1135
1438
|
continue;
|
|
1136
1439
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
// verifier that graded a different number of records than the pack holds
|
|
1141
|
-
// leaves this loop with no grading to read. That is not hypothetical: a
|
|
1142
|
-
// pack whose `body_hash` does not cover its body reads `failed` with
|
|
1143
|
-
// `events` EMPTY while `packRecords` reads every record in it, and only
|
|
1144
|
-
// `wrapperHolds` above stands between that pack and this loop. A reader
|
|
1145
|
-
// must never crash on a pack a party can build, so the gap is NAMED rather
|
|
1146
|
-
// than indexed into, and the pack's records are dropped exactly as a
|
|
1147
|
-
// `malformed` pack's are (gate 4 on proof#140, finding 3: under the
|
|
1148
|
-
// mutation that dropped `bodyHashOk` from the wrapper test, this kernel
|
|
1149
|
-
// threw `Cannot read properties of undefined (reading 'envelopeOk')` here
|
|
1150
|
-
// and nothing in the ladder said so).
|
|
1151
|
-
if (verified.events.length !== records.length) {
|
|
1152
|
-
note(PACK_BUNDLE_NOT_VERIFIED, {
|
|
1153
|
-
packRef: pack.ref,
|
|
1154
|
-
status: verified.status as CaseVerdict,
|
|
1155
|
-
reasons: [
|
|
1156
|
-
`this verifier graded ${verified.events.length} of the ` +
|
|
1157
|
-
`${records.length} record(s) the pack holds, so none of them is read`,
|
|
1158
|
-
],
|
|
1159
|
-
});
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1440
|
+
// Graded above (`malformed` where the verifier called it `verified`), and
|
|
1441
|
+
// none of its records are read.
|
|
1442
|
+
if (ungraded) continue;
|
|
1162
1443
|
if (records.length === 0) {
|
|
1163
1444
|
note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref });
|
|
1164
1445
|
continue;
|
|
1165
1446
|
}
|
|
1166
1447
|
|
|
1167
|
-
const
|
|
1448
|
+
const checked = check as DeclarationCheck;
|
|
1168
1449
|
claimsByPack.set(position, declarationClaims(pack.bundle));
|
|
1169
|
-
|
|
1450
|
+
rolesByPack.set(position, new Map((checked.roles ?? []).map((r) => [r.eventId, r.role])));
|
|
1451
|
+
if (checked.contentFindings.length > 0) {
|
|
1170
1452
|
// BESIDE the deviation exception and never instead of it: a pack can do
|
|
1171
1453
|
// both, over different records, and the two say different things. This
|
|
1172
1454
|
// one is `incomplete` because whoever assembled the case chose the bytes
|
|
1173
1455
|
// it turns on (proof#147).
|
|
1174
1456
|
note(PACK_DECLARATION_CONTENT_DEPENDENT, {
|
|
1175
1457
|
packRef: pack.ref,
|
|
1176
|
-
count:
|
|
1458
|
+
count: checked.contentFindings.length,
|
|
1177
1459
|
});
|
|
1178
1460
|
}
|
|
1179
|
-
if (
|
|
1461
|
+
if (checked.verdict === "failed") {
|
|
1180
1462
|
note(PACK_DECLARATION_DEVIATES, {
|
|
1181
1463
|
packRef: pack.ref,
|
|
1182
|
-
count:
|
|
1464
|
+
count: checked.deviations.length,
|
|
1183
1465
|
});
|
|
1184
|
-
} else if (
|
|
1466
|
+
} else if (checked.contentFindings.length > 0) {
|
|
1185
1467
|
// Already named above. `partial` here is not "the reader cannot read the
|
|
1186
1468
|
// claim": the claim was read, and part of it rests on bytes nobody
|
|
1187
1469
|
// signed.
|
|
1188
|
-
} else if (
|
|
1470
|
+
} else if (checked.verdict === "verified") {
|
|
1189
1471
|
// A pack that agrees with its own claim contributes NOTHING, rather than
|
|
1190
1472
|
// a `verified` of its own. The case verdict is over the hand-off
|
|
1191
1473
|
// readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
|
|
@@ -1193,14 +1475,25 @@ export async function readCase(
|
|
|
1193
1475
|
// every case, so one broken pair beside two honest declarations would
|
|
1194
1476
|
// read `mixed` and the failure would be the quieter half of a two-word
|
|
1195
1477
|
// answer. A pack-level PROBLEM still contributes, through its exception.
|
|
1196
|
-
} else if (
|
|
1478
|
+
} else if (checked.present) {
|
|
1197
1479
|
// `partial` with a declaration present is 5.4.3's "a claim this reader
|
|
1198
1480
|
// cannot read", which `gradePack` already carries to `incomplete`. The
|
|
1199
1481
|
// same posture here, for the same reason: the field has no accept-set and
|
|
1200
1482
|
// the holder writes it.
|
|
1201
1483
|
note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref });
|
|
1202
1484
|
} else {
|
|
1203
|
-
|
|
1485
|
+
// WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
|
|
1486
|
+
// A23, I4): `partial` where the reading holds a hand-off record, and a
|
|
1487
|
+
// NOTE where it holds none. That is not known until the pile is read, so
|
|
1488
|
+
// the slot is held HERE, in pack order, and settled below.
|
|
1489
|
+
undeclaredSlots.push({
|
|
1490
|
+
exceptionAt: exceptions.length,
|
|
1491
|
+
contributionAt: contributions.length,
|
|
1492
|
+
position,
|
|
1493
|
+
ref: pack.ref,
|
|
1494
|
+
});
|
|
1495
|
+
exceptions.push(null);
|
|
1496
|
+
contributions.push(null);
|
|
1204
1497
|
}
|
|
1205
1498
|
|
|
1206
1499
|
// Each record on its OWN envelope. `packRecords` and the verifier both walk
|
|
@@ -1209,6 +1502,7 @@ export async function readCase(
|
|
|
1209
1502
|
for (let index = 0; index < records.length; index += 1) {
|
|
1210
1503
|
const record = records[index];
|
|
1211
1504
|
const graded = verified.events[index];
|
|
1505
|
+
const bare = signed.events[index];
|
|
1212
1506
|
if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
|
|
1213
1507
|
note(RECORD_PROFILE_QUARANTINED, { packRef: pack.ref, eventId: record.eventId });
|
|
1214
1508
|
continue;
|
|
@@ -1235,22 +1529,32 @@ export async function readCase(
|
|
|
1235
1529
|
});
|
|
1236
1530
|
continue;
|
|
1237
1531
|
}
|
|
1238
|
-
// Named, and the record stays in the pile (see the kinds).
|
|
1239
|
-
|
|
1532
|
+
// Named, and the record stays in the pile (see the kinds). A mismatch
|
|
1533
|
+
// the signed body carries on its own is `failed`; one that appears only
|
|
1534
|
+
// with the side map is `incomplete` (proof#159).
|
|
1535
|
+
if (bare.contentOk === false) {
|
|
1240
1536
|
note(RECORD_CONTENT_MISMATCH, {
|
|
1241
1537
|
packRef: pack.ref,
|
|
1242
1538
|
eventId: record.eventId,
|
|
1243
|
-
reasons: [...
|
|
1539
|
+
reasons: [...bare.errors],
|
|
1540
|
+
});
|
|
1541
|
+
} else if (graded.contentOk === false) {
|
|
1542
|
+
note(RECORD_CONTENT_DEPENDENT, {
|
|
1543
|
+
packRef: pack.ref,
|
|
1544
|
+
eventId: record.eventId,
|
|
1545
|
+
reasons: onlyIn(graded.errors, bare.errors),
|
|
1244
1546
|
});
|
|
1245
1547
|
}
|
|
1548
|
+
// Every other check on the SIGNED reading, so side content can neither
|
|
1549
|
+
// add a failure here nor take one away.
|
|
1246
1550
|
if (
|
|
1247
|
-
!(
|
|
1248
|
-
(!
|
|
1551
|
+
!(bare.inclusionOk && bare.checkpointBound) ||
|
|
1552
|
+
(!bare.ok && bare.contentOk !== false)
|
|
1249
1553
|
) {
|
|
1250
1554
|
note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
|
|
1251
1555
|
packRef: pack.ref,
|
|
1252
1556
|
eventId: record.eventId,
|
|
1253
|
-
reasons: [...
|
|
1557
|
+
reasons: [...bare.errors],
|
|
1254
1558
|
});
|
|
1255
1559
|
}
|
|
1256
1560
|
held.push({
|
|
@@ -1281,6 +1585,7 @@ export async function readCase(
|
|
|
1281
1585
|
findings: [...reading.findings],
|
|
1282
1586
|
failedChecks: [...reading.failedChecks],
|
|
1283
1587
|
counterparts: [...reading.candidates],
|
|
1588
|
+
role: rolesByPack.get(position)?.get(eventId) ?? null,
|
|
1284
1589
|
});
|
|
1285
1590
|
// proof#137's door, named. Read from the ONE reader of the declaration
|
|
1286
1591
|
// (`declarationClaims`), never from a second parse here. Only a READABLE
|
|
@@ -1294,10 +1599,14 @@ export async function readCase(
|
|
|
1294
1599
|
// `not_a_handoff_record` exit it was a check the audited party switched off
|
|
1295
1600
|
// by respelling two fields, and behind the `malformed` exit it was one a
|
|
1296
1601
|
// party switched off by breaking its own record.
|
|
1602
|
+
//
|
|
1603
|
+
// v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
|
|
1604
|
+
// the declaration check already ran on the signed body.
|
|
1297
1605
|
const claims = claimsByPack.get(position);
|
|
1298
1606
|
if (
|
|
1299
1607
|
claims !== undefined &&
|
|
1300
1608
|
claims.readable &&
|
|
1609
|
+
claims.version === DECLARATION_VERSION &&
|
|
1301
1610
|
!claims.claimedSides.includes(eventId)
|
|
1302
1611
|
) {
|
|
1303
1612
|
note(RECORD_UNCLAIMED, { packRef, eventId });
|
|
@@ -1343,13 +1652,34 @@ export async function readCase(
|
|
|
1343
1652
|
// that party's pack. Where some pack could not be tied to a key, "no
|
|
1344
1653
|
// pack is signed by the named key" is not established and the reading
|
|
1345
1654
|
// says it could not tell.
|
|
1655
|
+
//
|
|
1656
|
+
// EXCEPT where 4.1 set every candidate aside (v1.17, A20). There the
|
|
1657
|
+
// absence is a finding about the candidates the reader DID read, each
|
|
1658
|
+
// paired `verified` with another record the subject's own signer made,
|
|
1659
|
+
// and not a finding about any pack. 4.1 and evidence-bundle A16 name the
|
|
1660
|
+
// neutral kind for it, and the damning one would put the audited party's
|
|
1661
|
+
// own unfaithful record on the counterparty's pack as a statement
|
|
1662
|
+
// against it. `count` is how many candidates were set aside, which is
|
|
1663
|
+
// also what tells the rendered sentence which absence this is.
|
|
1664
|
+
const graded = reading.candidateReadings;
|
|
1665
|
+
const setAside =
|
|
1666
|
+
graded.length > 0 && graded.every((c) => c.accountedFor !== null)
|
|
1667
|
+
? graded.length
|
|
1668
|
+
: 0;
|
|
1346
1669
|
const kind =
|
|
1347
|
-
|
|
1348
|
-
?
|
|
1349
|
-
:
|
|
1350
|
-
?
|
|
1351
|
-
:
|
|
1352
|
-
|
|
1670
|
+
setAside > 0
|
|
1671
|
+
? COUNTERPART_ABSENT
|
|
1672
|
+
: named !== null && holderKeys.has(named)
|
|
1673
|
+
? COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS
|
|
1674
|
+
: unattributed > 0
|
|
1675
|
+
? COUNTERPART_ABSENT_SIGNER_UNKNOWN
|
|
1676
|
+
: COUNTERPART_ABSENT;
|
|
1677
|
+
note(
|
|
1678
|
+
kind,
|
|
1679
|
+
setAside > 0
|
|
1680
|
+
? { packRef, eventId, counterpartyKey: named, count: setAside }
|
|
1681
|
+
: { packRef, eventId, counterpartyKey: named },
|
|
1682
|
+
);
|
|
1353
1683
|
continue;
|
|
1354
1684
|
}
|
|
1355
1685
|
|
|
@@ -1357,7 +1687,7 @@ export async function readCase(
|
|
|
1357
1687
|
note(COUNTERPART_AMBIGUOUS, {
|
|
1358
1688
|
packRef,
|
|
1359
1689
|
eventId,
|
|
1360
|
-
count: reading.
|
|
1690
|
+
count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
|
|
1361
1691
|
});
|
|
1362
1692
|
}
|
|
1363
1693
|
// ON `failedChecks` AND NOT ON THE VERDICT WORD. 4.1 keeps the UNION of
|
|
@@ -1378,18 +1708,57 @@ export async function readCase(
|
|
|
1378
1708
|
}
|
|
1379
1709
|
}
|
|
1380
1710
|
|
|
1381
|
-
|
|
1711
|
+
// A reading with no hand-off record in it is not an exception (5.5.3 v1.20,
|
|
1712
|
+
// A23, D6). The undeclared packs, settled: the exception `pack_undeclared`,
|
|
1713
|
+
// contributing `partial`, in the slot it was given in pack order where the
|
|
1714
|
+
// reading holds a hand-off record; a NOTE with no contribution where it holds
|
|
1715
|
+
// none.
|
|
1716
|
+
for (const slot of undeclaredSlots) {
|
|
1717
|
+
if (handoffRecords > 0) {
|
|
1718
|
+
exceptions[slot.exceptionAt] = { kind: PACK_UNDECLARED, packRef: slot.ref };
|
|
1719
|
+
contributions[slot.contributionAt] = [slot.ref, EXCEPTION_VERDICT[PACK_UNDECLARED] as CaseVerdict];
|
|
1720
|
+
} else {
|
|
1721
|
+
caseNotes.push({ position: slot.position, note: { kind: PACK_UNDECLARED, packRef: slot.ref } });
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const named = exceptions.filter((e): e is CaseException => e !== null);
|
|
1725
|
+
const contributed = contributions.filter((c): c is [string, CaseVerdict] => c !== null);
|
|
1726
|
+
// Pack order, stable. A pack carries at most one note: one with no
|
|
1727
|
+
// declaration withholds nothing by one.
|
|
1728
|
+
const notes = caseNotes
|
|
1729
|
+
.map((n, i) => ({ ...n, i }))
|
|
1730
|
+
.sort((a, b) => a.position - b.position || a.i - b.i)
|
|
1731
|
+
.map((n) => n.note);
|
|
1382
1732
|
|
|
1383
1733
|
return {
|
|
1384
|
-
verdict: verdictOf(
|
|
1385
|
-
exceptions,
|
|
1734
|
+
verdict: verdictOf(named, contributed),
|
|
1735
|
+
exceptions: named,
|
|
1386
1736
|
records,
|
|
1387
|
-
contributions,
|
|
1737
|
+
contributions: contributed,
|
|
1388
1738
|
handoffRecords,
|
|
1389
1739
|
onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
|
|
1740
|
+
notes,
|
|
1741
|
+
packs: packReadings,
|
|
1390
1742
|
};
|
|
1391
1743
|
}
|
|
1392
1744
|
|
|
1745
|
+
/**
|
|
1746
|
+
* `body.declaration.declaration_version` where it is one this reader knows,
|
|
1747
|
+
* and `null` otherwise.
|
|
1748
|
+
*/
|
|
1749
|
+
function declarationVersionOf(bundle: unknown): string | null {
|
|
1750
|
+
const at = (v: unknown, k: string): unknown =>
|
|
1751
|
+
typeof v === "object" && v !== null && !Array.isArray(v)
|
|
1752
|
+
? (v as Record<string, unknown>)[k]
|
|
1753
|
+
: undefined;
|
|
1754
|
+
const declaration = at(at(bundle, "body"), "declaration");
|
|
1755
|
+
if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
const version = (declaration as Record<string, unknown>)["declaration_version"];
|
|
1759
|
+
return version === DECLARATION_VERSION || version === DECLARATION_VERSION_V2 ? version : null;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1393
1762
|
// ---- Presentation (optional) ------------------------------------------------
|
|
1394
1763
|
|
|
1395
1764
|
/**
|
|
@@ -1406,6 +1775,17 @@ export function renderException(exception: CaseException): string {
|
|
|
1406
1775
|
const where = `${exception.packRef}/${exception.eventId}`;
|
|
1407
1776
|
switch (exception.kind) {
|
|
1408
1777
|
case COUNTERPART_ABSENT:
|
|
1778
|
+
if (exception.count) {
|
|
1779
|
+
const which =
|
|
1780
|
+
exception.count === 1
|
|
1781
|
+
? "its one candidate counterpart pairs"
|
|
1782
|
+
: `each of its ${exception.count} candidate counterparts pairs`;
|
|
1783
|
+
return (
|
|
1784
|
+
`${where} has no counterpart among the records this verifier read: ` +
|
|
1785
|
+
`${which} verified with another record signed by the key that ` +
|
|
1786
|
+
"signed this one, at a different sequence, and is set aside"
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1409
1789
|
return (
|
|
1410
1790
|
`${where} has no counterpart among the records this verifier read, ` +
|
|
1411
1791
|
"and no pack in this case is signed by the key it names"
|
|
@@ -1495,13 +1875,21 @@ export function renderException(exception: CaseException): string {
|
|
|
1495
1875
|
"does not verify, so none of its records are read and the key it " +
|
|
1496
1876
|
"names is not counted as its signer"
|
|
1497
1877
|
);
|
|
1498
|
-
case
|
|
1499
|
-
let head =
|
|
1500
|
-
|
|
1501
|
-
|
|
1878
|
+
case PACK_GRADE_NOT_VERIFIED: {
|
|
1879
|
+
let head = `${exception.packRef} grades ${(exception.status ?? "").toUpperCase()} as a pack`;
|
|
1880
|
+
if (exception.causes !== undefined && exception.causes.length > 0) {
|
|
1881
|
+
head += ` (${exception.causes.join(", ")})`;
|
|
1882
|
+
}
|
|
1502
1883
|
if (exception.status === "malformed") head += ", so none of its records are read";
|
|
1503
1884
|
return head + because(exception.reasons);
|
|
1504
1885
|
}
|
|
1886
|
+
case PACK_BUNDLE_CONTENT_DEPENDENT:
|
|
1887
|
+
return (
|
|
1888
|
+
`${exception.packRef} reads FAILED as a bundle only together with ` +
|
|
1889
|
+
"the unsigned content handed in beside it, which anyone could have " +
|
|
1890
|
+
"supplied, so whether that failure is the pack's is UNKNOWN" +
|
|
1891
|
+
because(exception.reasons)
|
|
1892
|
+
);
|
|
1505
1893
|
case RECORD_ENVELOPE_DOES_NOT_VERIFY:
|
|
1506
1894
|
return (
|
|
1507
1895
|
`${where} is in a pack whose wrapper verifies, and its own envelope ` +
|
|
@@ -1520,54 +1908,161 @@ export function renderException(exception: CaseException): string {
|
|
|
1520
1908
|
"to, and it is still read as a hand-off record" +
|
|
1521
1909
|
because(exception.reasons)
|
|
1522
1910
|
);
|
|
1911
|
+
case RECORD_CONTENT_DEPENDENT:
|
|
1912
|
+
return (
|
|
1913
|
+
`${where} commits to content that the unsigned content handed in ` +
|
|
1914
|
+
"beside its pack does not match, and anyone could have supplied " +
|
|
1915
|
+
"that content, so what it says about the record is UNKNOWN; the " +
|
|
1916
|
+
"record is still read as a hand-off record" +
|
|
1917
|
+
because(exception.reasons)
|
|
1918
|
+
);
|
|
1523
1919
|
case RECORD_PROFILE_QUARANTINED:
|
|
1524
1920
|
return (
|
|
1525
1921
|
`${where} is signed under a profile this case reader does not read ` +
|
|
1526
|
-
"while its admission is undecided
|
|
1922
|
+
"while its admission is undecided, so what it says is UNKNOWN"
|
|
1527
1923
|
);
|
|
1528
1924
|
case CASE_HOLDS_NO_PACKS:
|
|
1529
1925
|
return "this case holds no packs";
|
|
1530
|
-
|
|
1926
|
+
default:
|
|
1927
|
+
return exception.kind;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
/** The house wording for one NOTE. Offered, not imposed, as `renderException` is. */
|
|
1932
|
+
export function renderCaseNote(note: CaseNote): string {
|
|
1933
|
+
switch (note.kind) {
|
|
1934
|
+
case CONTENT_WITHHELD_BY_DECLARATION:
|
|
1531
1935
|
return (
|
|
1532
|
-
|
|
1533
|
-
"
|
|
1936
|
+
`${note.packRef} withholds the content of ${note.count} record(s) by ` +
|
|
1937
|
+
"its own signed declaration: their signatures and commitments were " +
|
|
1938
|
+
"checked, their content was not"
|
|
1534
1939
|
);
|
|
1940
|
+
case PACK_UNDECLARED:
|
|
1941
|
+
return `${note.packRef} does not say what it holds`;
|
|
1535
1942
|
default:
|
|
1536
|
-
return
|
|
1943
|
+
return (note as CaseNote).kind;
|
|
1537
1944
|
}
|
|
1538
1945
|
}
|
|
1539
1946
|
|
|
1540
1947
|
/**
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1948
|
+
* The scope sentence, printed directly under the headline (evidence-bundle
|
|
1949
|
+
* 5.5.6 v1.20, A26), byte for byte the text `render_case_summary` returns.
|
|
1950
|
+
*
|
|
1951
|
+
* The packs in the reading; the distinct keys, compared as keys, that signed
|
|
1952
|
+
* the wrapper of a pack whose wrapper held; the hand-off records; the
|
|
1953
|
+
* exceptions; and that the reading was made offline. Where any pack holds
|
|
1954
|
+
* records withheld by its declaration, their total; where any pack is noted
|
|
1955
|
+
* `pack_undeclared`, how many. Those two lines are part of what the headline
|
|
1956
|
+
* MEANS, and a surface MUST print them with it.
|
|
1957
|
+
*
|
|
1958
|
+
* What was not checked on the public record is no longer a clause here. It is
|
|
1959
|
+
* the witness line (`renderWitnessLine`), printed with every headline.
|
|
1546
1960
|
*/
|
|
1547
1961
|
export function renderCaseSummary(reading: CaseReading): string {
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
: "; no anchor_ref in it was re-fetched on chain, which this reader never does";
|
|
1561
|
-
if (reading.verdict === "verified") {
|
|
1562
|
-
return (
|
|
1563
|
-
`this case verifies across ${reading.records.length} record(s): ` +
|
|
1564
|
-
`${n} hand-off record(s), every one matched, no exceptions` +
|
|
1565
|
-
scope
|
|
1962
|
+
const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
|
|
1963
|
+
const withheld = reading.packs.reduce((n, p) => n + p.recordsWithheld, 0);
|
|
1964
|
+
const undeclared = reading.notes.filter((n) => n.kind === PACK_UNDECLARED).length;
|
|
1965
|
+
const lines = [
|
|
1966
|
+
`${reading.packs.length} pack(s), ${keys.size} signing key(s), ` +
|
|
1967
|
+
`${reading.handoffRecords} hand-off record(s), ` +
|
|
1968
|
+
`${reading.exceptions.length} exception(s), read offline.`,
|
|
1969
|
+
];
|
|
1970
|
+
if (withheld > 0) {
|
|
1971
|
+
lines.push(
|
|
1972
|
+
`${withheld} record(s)' content withheld by their holder: their ` +
|
|
1973
|
+
"signatures and commitments were checked, their content was not.",
|
|
1566
1974
|
);
|
|
1567
1975
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1976
|
+
if (undeclared > 0) lines.push(`${undeclared} pack(s) do not say what they hold.`);
|
|
1977
|
+
return lines.join(" ");
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
|
|
1981
|
+
|
|
1982
|
+
/**
|
|
1983
|
+
* The one display title of each of the eight 3 words. A surface prints the
|
|
1984
|
+
* title, never the word, and never a title of its own (#173, A18). The wording
|
|
1985
|
+
* is Will's to change through the glossary (DL-71); what each MEANS is pinned
|
|
1986
|
+
* in the spec.
|
|
1987
|
+
*/
|
|
1988
|
+
export const VERDICT_DISPLAY_TITLES: Readonly<Record<string, string>> = {
|
|
1989
|
+
verified: "Verified",
|
|
1990
|
+
partial: "Partial",
|
|
1991
|
+
pending: "Pending",
|
|
1992
|
+
incomplete: "Incomplete",
|
|
1993
|
+
failed: "Failed",
|
|
1994
|
+
malformed: "Not readable",
|
|
1995
|
+
not_found: "Not found",
|
|
1996
|
+
mixed: "Mixed",
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
/**
|
|
2000
|
+
* What a reader may rely on, for each of the six words a case reading reaches
|
|
2001
|
+
* (5.5.6 v1.20, A26), the same for every drop. `pending` and `not_found`
|
|
2002
|
+
* never head a case reading: they are witness states.
|
|
2003
|
+
*/
|
|
2004
|
+
export const CASE_RELIANCE: Readonly<Record<CaseVerdict, string>> = {
|
|
2005
|
+
verified:
|
|
2006
|
+
"Every pack in this drop is genuine and unaltered and agrees with its " +
|
|
2007
|
+
"own signed declaration. Every hand-off with both sides here is " +
|
|
2008
|
+
"intact. Every other record is one its holder signed as a step. " +
|
|
2009
|
+
"Nothing here could not be checked. This was read offline, on this " +
|
|
2010
|
+
"device. It does not say these packs are everything the parties " +
|
|
2011
|
+
"recorded.",
|
|
2012
|
+
partial:
|
|
2013
|
+
"Nothing here was proven wrong, and a check could not be completed: a " +
|
|
2014
|
+
"pack names no witness, or a pack in a hand-off reading does not say " +
|
|
2015
|
+
"what it holds. The exceptions say which.",
|
|
2016
|
+
incomplete:
|
|
2017
|
+
"Nothing here was proven wrong, and part of it could not be checked: " +
|
|
2018
|
+
"the other side of a hand-off is not in this drop, bytes were not " +
|
|
2019
|
+
"handed over, or a claim could not be read. Each exception says what " +
|
|
2020
|
+
"is missing and, where a key is known, whose pack would supply it.",
|
|
2021
|
+
failed:
|
|
2022
|
+
"A check ran and did not pass. Do not rely on this drop until the " +
|
|
2023
|
+
"named failure is explained. The failure is named on the pack or " +
|
|
2024
|
+
"record it is about, and every other exception is listed beside it.",
|
|
2025
|
+
malformed:
|
|
2026
|
+
"Part of this drop could not be read as evidence, and nothing was " +
|
|
2027
|
+
"proven wrong. The exceptions name what could not be read.",
|
|
2028
|
+
mixed:
|
|
2029
|
+
"The readings here differ and none of them failed: some are verified " +
|
|
2030
|
+
"and some could not be checked. Read each pack's row and each " +
|
|
2031
|
+
"exception before relying on any of it.",
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
/** The three witness states, and how each is printed (5.5.6, D7). No other. */
|
|
2035
|
+
export const WITNESS_STATES = {
|
|
2036
|
+
confirmed: "Confirmed on the public record",
|
|
2037
|
+
not_checked: "Not checked",
|
|
2038
|
+
differs: "The public record holds different bytes",
|
|
2039
|
+
} as const;
|
|
2040
|
+
export type WitnessState = keyof typeof WITNESS_STATES;
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* The witness line: a lookup of the public record, reported beside the
|
|
2044
|
+
* headline and never in it (D7). It never raises or lowers the headline and
|
|
2045
|
+
* never changes its title. `reasons` says why a `not_checked` line did not
|
|
2046
|
+
* settle, in a fixed order; `services` names each service that answered, as
|
|
2047
|
+
* the URL the lookup used: named, never trusted (6).
|
|
2048
|
+
*/
|
|
2049
|
+
export interface WitnessLine {
|
|
2050
|
+
state: WitnessState;
|
|
2051
|
+
reasons: string[];
|
|
2052
|
+
services: string[];
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
/** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
|
|
2056
|
+
export const WITNESS_NOT_LOOKED_UP: Readonly<WitnessLine> = Object.freeze({
|
|
2057
|
+
state: "not_checked",
|
|
2058
|
+
reasons: ["no lookup was run"],
|
|
2059
|
+
services: [],
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
/** The witness line as both kernels print it. */
|
|
2063
|
+
export function renderWitnessLine(line: WitnessLine): string {
|
|
2064
|
+
let text = `Witness: ${WITNESS_STATES[line.state]}`;
|
|
2065
|
+
if (line.reasons.length > 0) text += `: ${line.reasons.join("; ")}`;
|
|
2066
|
+
if (line.services.length > 0) text += ` (answered: ${line.services.join(", ")})`;
|
|
2067
|
+
return text;
|
|
1573
2068
|
}
|