@sanning/proof 0.12.0 → 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.
Files changed (48) hide show
  1. package/README.md +25 -12
  2. package/dist/agent-proof.d.ts +2 -2
  3. package/dist/agent-proof.js +4 -4
  4. package/dist/case-file.d.ts +126 -0
  5. package/dist/case-file.js +349 -0
  6. package/dist/case.d.ts +173 -26
  7. package/dist/case.js +469 -123
  8. package/dist/cli.d.ts +38 -5
  9. package/dist/cli.js +557 -208
  10. package/dist/crypto.d.ts +1 -1
  11. package/dist/crypto.js +5 -1
  12. package/dist/declaration.d.ts +58 -52
  13. package/dist/declaration.js +256 -33
  14. package/dist/evidence.d.ts +2 -0
  15. package/dist/evidence.js +134 -19
  16. package/dist/exit-codes.d.ts +17 -0
  17. package/dist/exit-codes.js +50 -0
  18. package/dist/grading.d.ts +28 -13
  19. package/dist/grading.js +90 -31
  20. package/dist/index.d.ts +10 -7
  21. package/dist/index.js +13 -6
  22. package/dist/message-parts.d.ts +11 -9
  23. package/dist/message-parts.js +11 -9
  24. package/dist/types.d.ts +1 -0
  25. package/dist/verifier.d.ts +9 -2
  26. package/dist/verifier.js +28 -12
  27. package/dist/zip-reader.d.ts +50 -0
  28. package/dist/zip-reader.js +220 -0
  29. package/package.json +2 -2
  30. package/src/agent-proof.ts +4 -4
  31. package/src/case-file.ts +419 -0
  32. package/src/case.ts +586 -132
  33. package/src/cli.ts +651 -206
  34. package/src/crypto.ts +5 -1
  35. package/src/declaration.ts +248 -52
  36. package/src/evidence.ts +142 -21
  37. package/src/exit-codes.ts +54 -0
  38. package/src/grading.ts +109 -52
  39. package/src/index.ts +58 -2
  40. package/src/message-parts.ts +11 -9
  41. package/src/types.ts +5 -0
  42. package/src/verifier.ts +35 -13
  43. package/src/zip-reader.ts +229 -0
  44. package/vectors/README.md +23 -5
  45. package/vectors/case-file-index.json +668 -0
  46. package/vectors/case-split.json +218 -9
  47. package/vectors/pack-grading.json +68 -4
  48. package/vectors/stamp-on-chain.json +3 -3
package/dist/case.js 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`, and one that fails
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
@@ -207,7 +209,9 @@
207
209
  * verdict, exactly as `counterpart_absent` and `not_a_handoff_record` are
208
210
  * findings beside a verdict in 4.1.
209
211
  *
210
- * A case with more than one outcome is `mixed` (anchor#198, criterion 4).
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).
211
215
  *
212
216
  * WHAT THIS READER DOES NOT CLAIM
213
217
  * ===============================
@@ -237,9 +241,10 @@
237
241
  * sequence. Narrowing any of it here with `handoffs[]` is precisely the move
238
242
  * that would reopen proof#137's door.
239
243
  */
240
- import { checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
244
+ import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
241
245
  import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
242
246
  import { verifyEvidenceBundle } from "./evidence.js";
247
+ import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
243
248
  // ---- The vocabulary ---------------------------------------------------------
244
249
  /**
245
250
  * The verdicts a case can reach. Every one is already in use in
@@ -578,17 +583,26 @@ export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
578
583
  */
579
584
  export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
580
585
  /**
581
- * The pack's wrapper HOLDS and its own bundle verdict, from
582
- * `verifyEvidenceBundle` run here offline over the pack and the content handed
583
- * in beside it, is not `verified`. The exception carries that verdict
584
- * (`status`) and the verifier's reasons (`reasons`: its bundle errors, then its
585
- * stamp errors, then its checkpoint errors, each as the verifier wrote it), and
586
- * it CONTRIBUTES that verdict, so the case verdict never reads above its
587
- * weakest pack. Gate 3 on proof#140: two unwitnessed (`partial`) packs read as
588
- * a `verified` case with no exception. It does not say who is at fault:
589
- * content handed in beside a pack is not signed by its holder.
586
+ * The pack's wrapper HOLDS and its CASE GRADE is not `verified`
587
+ * (evidence-bundle 5.5.1 step 1 v1.20, A23; it replaces
588
+ * `pack_bundle_not_verified`, which carried the bundle status alone). The case
589
+ * grade is the pack grade of 5.4.5, taken twice: the SIGNED grade over the run
590
+ * with the side map empty, and the FULL grade over the run with it. The signed
591
+ * grade decides where it is `failed` or `malformed`; a failure only the full
592
+ * grade reaches is `incomplete`, because it rests on bytes nobody signed;
593
+ * otherwise the full grade. A body the verifier could not read is `malformed`,
594
+ * a body that is not one of the two event-carrying bodies takes the signed
595
+ * run's status, and a verifier that graded a different number of records than
596
+ * the pack holds takes the signed status, or `malformed` where that status is
597
+ * `verified`.
598
+ *
599
+ * The exception carries `status`, the case grade; `causes`, the 5.4.5 cause
600
+ * kinds of the grade that decided it (none in the first three branches); and
601
+ * `reasons`, the verifier's reasons from the run with the side map empty. It
602
+ * CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
603
+ * `mixed`, so the case verdict never reads above its weakest pack.
590
604
  */
591
- export const PACK_BUNDLE_NOT_VERIFIED = "pack_bundle_not_verified";
605
+ export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
592
606
  /**
593
607
  * A record in a pack whose wrapper holds, whose OWN envelope does not verify
594
608
  * (`EventResult.envelopeOk` is false). Not read by 4.1, and NAMED with the
@@ -609,12 +623,39 @@ export const RECORD_ENVELOPE_DOES_NOT_VERIFY = "record_envelope_does_not_verify"
609
623
  export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its_pack";
610
624
  /**
611
625
  * A record whose envelope verifies and whose disclosed content does not hash to
612
- * the `content_hash` it commits to (`EventResult.contentOk` is false). `failed`,
613
- * and the record STAYS in the pile: content beside a pack is unsigned and often
626
+ * the hash it commits to, its `content_hash` or a hand-off record's
627
+ * `message_hash` (evidence-bundle v1.19, A20) (`EventResult.contentOk` is false), with the
628
+ * side map EMPTY: the content is inside the signed body. The same mismatch from
629
+ * content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
630
+ * `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
614
631
  * supplied by whoever assembled the case, and gate 3 on proof#140 showed that
615
632
  * party erasing an honest verified pair by swapping it.
616
633
  */
617
634
  export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
635
+ /**
636
+ * The two readings proof#147 did not reach, given its shape (proof#159, ruled
637
+ * by Phil 2026-09-22, evidence-bundle.md 5.5.4 v1.16, A17). A pack whose
638
+ * bundle verdict is `failed` ONLY when read with the unsigned content handed
639
+ * in beside it, and `failed` no longer when read with that map empty, is
640
+ * `PACK_BUNDLE_CONTENT_DEPENDENT`; a record whose content mismatch comes from
641
+ * that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
642
+ * reader established that the bytes it was handed are not the bytes the record
643
+ * commits to, and nothing about the party that signed the record, because
644
+ * whoever assembled the case chose those bytes with no key. Up to v1.15 both
645
+ * readings carried `failed`, and one flipped nibble of accuser-supplied content
646
+ * put a `failed` against an intact pack; under A17's headline rule that would
647
+ * have become the case's headline.
648
+ *
649
+ * The test that separates the two is the one proof#159 states for the inverse:
650
+ * every deviation a signature covers still reads `failed` with the side map
651
+ * EMPTY. So the pack is verified twice, with the map and without it, and what
652
+ * fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
653
+ * (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
654
+ * each still `failed`. Emptying the map is the most a party that signed
655
+ * nothing can do to starve the check, and it cannot remove any of those.
656
+ */
657
+ export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
658
+ export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
618
659
  /**
619
660
  * A record signed under a ratified profile that is under quarantine
620
661
  * (`CASE_QUARANTINED_PROFILES`, pending DL-63). Not read, in any kernel.
@@ -624,11 +665,13 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
624
665
  /** The reader was handed no packs at all. */
625
666
  export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
626
667
  /**
627
- * Every pack was read and not one hand-off record came out of any of them.
628
- * There is no hand-off to have a verdict about, and saying `verified` over that
629
- * would be the defect class in its purest form.
668
+ * The two NOTEs (5.5.3 v1.20): findings on the reading with no contribution,
669
+ * printed in the scope sentence so a clean verdict never hides either.
670
+ * `pack_undeclared` is a note only where the reading holds no hand-off record;
671
+ * where it holds one, it is the exception of that name.
630
672
  */
631
- export const CASE_HOLDS_NO_HANDOFF_RECORDS = "case_holds_no_handoff_records";
673
+ export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
674
+ export const CASE_NOTE_KINDS = [PACK_UNDECLARED, CONTENT_WITHHELD_BY_DECLARATION];
632
675
  /**
633
676
  * Every exception this reader can report, as a closed set. A surface may render
634
677
  * them and a test may assert over them; nothing outside this array is emitted.
@@ -652,13 +695,14 @@ export const CASE_EXCEPTION_KINDS = [
652
695
  PACK_HOLDS_NO_RECORDS,
653
696
  PACK_KEY_UNREADABLE,
654
697
  PACK_DOES_NOT_VERIFY,
655
- PACK_BUNDLE_NOT_VERIFIED,
698
+ PACK_GRADE_NOT_VERIFIED,
699
+ PACK_BUNDLE_CONTENT_DEPENDENT,
656
700
  RECORD_ENVELOPE_DOES_NOT_VERIFY,
657
701
  RECORD_DOES_NOT_VERIFY_IN_ITS_PACK,
658
702
  RECORD_CONTENT_MISMATCH,
703
+ RECORD_CONTENT_DEPENDENT,
659
704
  RECORD_PROFILE_QUARANTINED,
660
705
  CASE_HOLDS_NO_PACKS,
661
- CASE_HOLDS_NO_HANDOFF_RECORDS,
662
706
  ];
663
707
  /**
664
708
  * What each exception kind contributes to the case verdict. It is a TABLE and
@@ -671,10 +715,10 @@ export const CASE_EXCEPTION_KINDS = [
671
715
  * contributes a clean verdict, which is what makes the invariant in `verdictOf`
672
716
  * hold.
673
717
  *
674
- * `PACK_BUNDLE_NOT_VERIFIED` is the one row that is not a constant: it
675
- * contributes the pack's own bundle verdict, which is `partial`, `failed` or
676
- * `malformed` and never `verified` (the exception is emitted only when it is
677
- * not). `null` marks that row, and `readCase` fills it from `status`.
718
+ * `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
719
+ * contributes the pack's case grade, which is never `verified` (the exception
720
+ * is emitted only when it is not). `null` marks that row, and `readCase` fills
721
+ * it from `status`.
678
722
  */
679
723
  const EXCEPTION_VERDICT = {
680
724
  [COUNTERPART_ABSENT]: "incomplete",
@@ -695,14 +739,26 @@ const EXCEPTION_VERDICT = {
695
739
  [PACK_HOLDS_NO_RECORDS]: "incomplete",
696
740
  [PACK_KEY_UNREADABLE]: "incomplete",
697
741
  [PACK_DOES_NOT_VERIFY]: "failed",
698
- [PACK_BUNDLE_NOT_VERIFIED]: null,
742
+ [PACK_GRADE_NOT_VERIFIED]: null,
743
+ [PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
699
744
  [RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
700
745
  [RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
701
746
  [RECORD_CONTENT_MISMATCH]: "failed",
747
+ [RECORD_CONTENT_DEPENDENT]: "incomplete",
702
748
  [RECORD_PROFILE_QUARANTINED]: "incomplete",
703
749
  [CASE_HOLDS_NO_PACKS]: "partial",
704
- [CASE_HOLDS_NO_HANDOFF_RECORDS]: "partial",
705
750
  };
751
+ /**
752
+ * The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
753
+ * other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
754
+ * grade passes through and a case verdict never issues) is `incomplete`.
755
+ */
756
+ const GRADE_CONTRIBUTES = new Set([
757
+ "partial",
758
+ "incomplete",
759
+ "failed",
760
+ "malformed",
761
+ ]);
706
762
  // ---- The reading ------------------------------------------------------------
707
763
  const bodyTypeOf = (bundle) => typeof bundle === "object" && bundle !== null
708
764
  ? bundle["body_type"]
@@ -740,6 +796,23 @@ const wrapperHolds = (result) => result.signatureOk && result.bodyHashOk;
740
796
  * bundle-level error at all, and an exception that named the verdict and no
741
797
  * reason would send a reader looking for a defect in the pack as a whole.
742
798
  */
799
+ /**
800
+ * The reasons a reading carries only because of the side map, in the order the
801
+ * verifier wrote them: what the content-dependent kinds are about, and nothing
802
+ * a signature covers.
803
+ */
804
+ const onlyIn = (withMap, withoutMap) => withMap.filter((reason) => !withoutMap.includes(reason));
805
+ /**
806
+ * Whether a side-content map holds any key. Python's `if pack.content` is
807
+ * the twin: `None` and an empty map both mean the two verify runs are one run.
808
+ */
809
+ function sideMapHasKeys(content) {
810
+ if (content === null || content === undefined)
811
+ return false;
812
+ if (content instanceof Map)
813
+ return content.size > 0;
814
+ return Object.keys(content).length > 0;
815
+ }
743
816
  const bundleReasons = (result) => [
744
817
  ...result.errors,
745
818
  ...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
@@ -775,8 +848,13 @@ function underQuarantine(specVersion) {
775
848
  /**
776
849
  * The case verdict, from the contributions alone.
777
850
  *
778
- * One outcome is that outcome; more than one is `mixed` (anchor#198,
779
- * criterion 4). `mixed` is in the 3 vocabulary and coins no word.
851
+ * **A proven failure keeps the headline** (`evidence-bundle.md` 5.5.4 v1.16,
852
+ * A17, proof#158): if any contribution is `failed`, the case is `failed`,
853
+ * whatever else contributes, and every other exception stays named beside it.
854
+ * Otherwise one outcome is that outcome, and more than one is `mixed`
855
+ * (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
856
+ * does NOT keep the headline: it says a record cannot be read, not that a
857
+ * check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
780
858
  *
781
859
  * THE INVARIANT, stated so a mutation test has something to kill: `verified` is
782
860
  * reachable ONLY when `exceptions` is empty. Every exception kind carries a
@@ -785,14 +863,25 @@ function underQuarantine(specVersion) {
785
863
  * The assertion is here anyway, because that is the property every one of the
786
864
  * seventeen findings on this program violated.
787
865
  */
788
- function verdictOf(exceptions, contributions) {
789
- // Unreachable through `readCase`, which names a case with no contributions
790
- // rather than returning one. Defended anyway: an empty aggregate reading as
791
- // `verified` is the defect class itself.
792
- if (contributions.length === 0)
793
- return "partial";
866
+ function verdictOf(exceptions, contributions, holdsPacks = true) {
867
+ // A reading that holds at least one pack and no contribution at all is
868
+ // `verified` (5.5.4 v1.20, A23): every pack in it graded `verified` and
869
+ // nothing in it was an exception. Every exception contributes, so no
870
+ // contribution means no exception. A reading with no packs always carries
871
+ // `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
872
+ // nothing reading as `verified` is the defect class itself.
873
+ if (contributions.length === 0) {
874
+ if (exceptions.length > 0) {
875
+ throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
876
+ }
877
+ return holdsPacks ? "verified" : "partial";
878
+ }
794
879
  const outcomes = new Set(contributions.map(([, v]) => v));
795
- const verdict = outcomes.size === 1 ? [...outcomes][0] : "mixed";
880
+ const verdict = outcomes.has("failed")
881
+ ? "failed"
882
+ : outcomes.size === 1
883
+ ? [...outcomes][0]
884
+ : "mixed";
796
885
  if (verdict === "verified" && exceptions.length > 0) {
797
886
  throw new Error(`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`);
798
887
  }
@@ -826,23 +915,35 @@ function verdictOf(exceptions, contributions) {
826
915
  * see is the counterpart that is missing.
827
916
  */
828
917
  export async function readCase(packs) {
918
+ // A `null` slot is held for each pack that carries no declaration, and
919
+ // settled once the pile says whether the reading holds a hand-off record.
829
920
  const exceptions = [];
830
921
  const contributions = [];
831
922
  const note = (kind, fields = {}) => {
832
923
  exceptions.push({ kind, ...fields });
833
- // `null` is PACK_BUNDLE_NOT_VERIFIED: the pack's own verdict, never verified.
834
- const contribution = EXCEPTION_VERDICT[kind] ?? fields.status;
924
+ // `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
925
+ // verified, and `incomplete` for a word a pack grade passes through and a
926
+ // case verdict never issues (5.5.1 step 1 v1.20).
927
+ let contribution = EXCEPTION_VERDICT[kind];
928
+ if (contribution === null) {
929
+ const status = fields.status;
930
+ contribution = GRADE_CONTRIBUTES.has(status) ? status : "incomplete";
931
+ }
835
932
  contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
836
933
  };
837
934
  if (packs.length === 0) {
838
935
  note(CASE_HOLDS_NO_PACKS);
936
+ const named = exceptions;
937
+ const contributed = contributions;
839
938
  return {
840
- verdict: verdictOf(exceptions, contributions),
841
- exceptions,
939
+ verdict: verdictOf(named, contributed, false),
940
+ exceptions: named,
842
941
  records: [],
843
- contributions,
942
+ contributions: contributed,
844
943
  handoffRecords: 0,
845
944
  onChainChecked: false,
945
+ notes: [],
946
+ packs: [],
846
947
  };
847
948
  }
848
949
  // Step 1 and 2, pack by pack, in the order the reader was given them.
@@ -884,6 +985,15 @@ export async function readCase(packs) {
884
985
  // below, from the pack envelope and never from the body, so no body type, no
885
986
  // disclosure choice and no record content decides whether it is read.
886
987
  const holderKeys = new Set();
988
+ // One reading per pack, in the order the reader was given them (5.5.4 v1.20).
989
+ const packReadings = [];
990
+ // The NOTEs (5.5.3 v1.20), with the pack position they are about.
991
+ const caseNotes = [];
992
+ // The role each pack's readable v2 declaration signs for each record, keyed
993
+ // by the pack's POSITION for the reason `claimsByPack` is.
994
+ const rolesByPack = new Map();
995
+ // The undeclared packs' slots, settled after the pile is read.
996
+ const undeclaredSlots = [];
887
997
  // How many packs this reader could not tie to a key: an unreadable wrapper
888
998
  // key, or a wrapper that does not hold. While it is not zero, "no pack in this
889
999
  // case is signed by the named key" is not established, so the innocent
@@ -899,6 +1009,13 @@ export async function readCase(packs) {
899
1009
  // of these spellings (proof#135) and the reading must not.
900
1010
  note(PACK_KEY_UNREADABLE, { packRef: pack.ref });
901
1011
  unattributed += 1;
1012
+ packReadings.push({
1013
+ ref: pack.ref,
1014
+ signingKey: null,
1015
+ grade: null,
1016
+ recordsWithheld: 0,
1017
+ declarationVersion: null,
1018
+ });
902
1019
  continue;
903
1020
  }
904
1021
  // The precondition `HeldRecord` states, met HERE rather than trusted to a
@@ -910,28 +1027,121 @@ export async function readCase(packs) {
910
1027
  content: (pack.content ?? undefined),
911
1028
  });
912
1029
  witnessChecked.push(verified.onChainChecked);
1030
+ // The same pack with the side map EMPTY (proof#159): what fails here is
1031
+ // what a signature covers, and what fails only above rests on bytes nobody
1032
+ // signed. Skipped where there is no map, because the two runs are then the
1033
+ // same run.
1034
+ const signed = sideMapHasKeys(pack.content)
1035
+ ? await verifyEvidenceBundle(pack.bundle, {})
1036
+ : verified;
913
1037
  // WHOSE PACK IS IT. The wrapper, and only the wrapper.
914
1038
  if (!wrapperHolds(verified)) {
915
1039
  note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref });
916
1040
  unattributed += 1;
1041
+ packReadings.push({
1042
+ ref: pack.ref,
1043
+ signingKey: holder,
1044
+ grade: null,
1045
+ recordsWithheld: 0,
1046
+ declarationVersion: null,
1047
+ });
917
1048
  continue;
918
1049
  }
919
1050
  holderKeys.add(holder);
920
- // WHAT IS WRONG INSIDE IT, from here on. First the pack's own assurance,
921
- // which propagates: the case never reads above its weakest pack.
922
- if (verified.status !== "verified") {
923
- note(PACK_BUNDLE_NOT_VERIFIED, {
1051
+ const bodyType = bodyTypeOf(pack.bundle);
1052
+ const records = packRecords(pack.bundle, pack.content);
1053
+ // The per-record gradings below are joined to `records` BY INDEX, so a
1054
+ // verifier that graded a different number of records than the pack holds
1055
+ // leaves this loop with no grading to read. That is not hypothetical: a
1056
+ // pack whose `body_hash` does not cover its body reads `failed` with
1057
+ // `events` EMPTY while `packRecords` reads every record in it, and only
1058
+ // `wrapperHolds` above stands between that pack and this loop. A reader
1059
+ // must never crash on a pack a party can build, so the gap is graded rather
1060
+ // than indexed into, and the pack's records are dropped exactly as a
1061
+ // `malformed` pack's are (gate 4 on proof#140, finding 3).
1062
+ const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
1063
+ const knownBody = CASE_BODY_TYPES.includes(bodyType);
1064
+ // WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
1065
+ // propagates: the case never reads above its weakest pack (5.5.1 step 1
1066
+ // v1.20, A23; up to v1.19 it was the bundle status).
1067
+ let check = null;
1068
+ let full = null;
1069
+ let causes = [];
1070
+ let grade;
1071
+ if (signed.status === "malformed") {
1072
+ grade = "malformed";
1073
+ }
1074
+ else if (!knownBody) {
1075
+ grade = signed.status;
1076
+ }
1077
+ else if (ungraded) {
1078
+ grade = signed.status === "verified" ? "malformed" : signed.status;
1079
+ }
1080
+ else {
1081
+ check = checkDeclaration(pack.bundle, pack.content);
1082
+ const completeness = completenessNotes(pack.bundle);
1083
+ full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
1084
+ const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null));
1085
+ // NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
1086
+ // where it is `failed` or `malformed`; a failure only the side map
1087
+ // produced is `incomplete`; otherwise the full grade.
1088
+ if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
1089
+ grade = bareGrade.verdict;
1090
+ causes = bareGrade.causes.map((c) => c.kind);
1091
+ }
1092
+ else if (full.verdict === "failed" || full.verdict === "malformed") {
1093
+ grade = "incomplete";
1094
+ causes = full.causes.map((c) => c.kind);
1095
+ }
1096
+ else {
1097
+ grade = full.verdict;
1098
+ causes = full.causes.map((c) => c.kind);
1099
+ }
1100
+ }
1101
+ if (grade !== "verified") {
1102
+ const reasons = bundleReasons(signed);
1103
+ if (knownBody && ungraded && signed.status !== "malformed") {
1104
+ // Said, because nothing else says it: the verifier's own reasons are
1105
+ // about the bundle, and this pack's records are dropped for a reason
1106
+ // of this reader's.
1107
+ reasons.push(`this verifier graded ${verified.events.length} of the ${records.length} ` +
1108
+ "record(s) the pack holds, so none of them is read");
1109
+ }
1110
+ note(PACK_GRADE_NOT_VERIFIED, {
924
1111
  packRef: pack.ref,
925
- status: verified.status,
926
- reasons: bundleReasons(verified),
1112
+ status: grade,
1113
+ causes,
1114
+ reasons,
1115
+ });
1116
+ }
1117
+ if (verified.status === "failed" && signed.status !== "failed") {
1118
+ note(PACK_BUNDLE_CONTENT_DEPENDENT, {
1119
+ packRef: pack.ref,
1120
+ reasons: onlyIn(bundleReasons(verified), bundleReasons(signed)),
1121
+ });
1122
+ }
1123
+ const recordsWithheld = full !== null ? full.recordsWithheld : 0;
1124
+ packReadings.push({
1125
+ ref: pack.ref,
1126
+ signingKey: holder,
1127
+ grade,
1128
+ recordsWithheld,
1129
+ declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
1130
+ });
1131
+ if (recordsWithheld > 0) {
1132
+ // A NOTE, with no contribution (5.5.3 v1.20): the holder signed that
1133
+ // these records' content does not travel, and the scope sentence counts
1134
+ // them, so a clean verdict never hides them.
1135
+ caseNotes.push({
1136
+ position,
1137
+ note: { kind: CONTENT_WITHHELD_BY_DECLARATION, packRef: pack.ref, count: recordsWithheld },
927
1138
  });
928
1139
  }
929
1140
  // The body is one the verifier could not read, so it graded no record, and
930
1141
  // nothing below has a grading to read. Named above.
931
- if (verified.status === "malformed")
1142
+ if (signed.status === "malformed")
932
1143
  continue;
933
- const bodyType = bodyTypeOf(pack.bundle);
934
- if (!CASE_BODY_TYPES.includes(bodyType)) {
1144
+ if (!knownBody) {
935
1145
  // `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
936
1146
  // records sit under `body.source_bundle`, and reading them is a change to
937
1147
  // which packs this reader grades, so it is a decision of its own and not
@@ -941,58 +1151,39 @@ export async function readCase(packs) {
941
1151
  note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref });
942
1152
  continue;
943
1153
  }
944
- const records = packRecords(pack.bundle, pack.content);
945
- // The per-record gradings below are joined to `records` BY INDEX, so a
946
- // verifier that graded a different number of records than the pack holds
947
- // leaves this loop with no grading to read. That is not hypothetical: a
948
- // pack whose `body_hash` does not cover its body reads `failed` with
949
- // `events` EMPTY while `packRecords` reads every record in it, and only
950
- // `wrapperHolds` above stands between that pack and this loop. A reader
951
- // must never crash on a pack a party can build, so the gap is NAMED rather
952
- // than indexed into, and the pack's records are dropped exactly as a
953
- // `malformed` pack's are (gate 4 on proof#140, finding 3: under the
954
- // mutation that dropped `bodyHashOk` from the wrapper test, this kernel
955
- // threw `Cannot read properties of undefined (reading 'envelopeOk')` here
956
- // and nothing in the ladder said so).
957
- if (verified.events.length !== records.length) {
958
- note(PACK_BUNDLE_NOT_VERIFIED, {
959
- packRef: pack.ref,
960
- status: verified.status,
961
- reasons: [
962
- `this verifier graded ${verified.events.length} of the ` +
963
- `${records.length} record(s) the pack holds, so none of them is read`,
964
- ],
965
- });
1154
+ // Graded above (`malformed` where the verifier called it `verified`), and
1155
+ // none of its records are read.
1156
+ if (ungraded)
966
1157
  continue;
967
- }
968
1158
  if (records.length === 0) {
969
1159
  note(PACK_HOLDS_NO_RECORDS, { packRef: pack.ref });
970
1160
  continue;
971
1161
  }
972
- const check = checkDeclaration(pack.bundle, pack.content);
1162
+ const checked = check;
973
1163
  claimsByPack.set(position, declarationClaims(pack.bundle));
974
- if (check.contentFindings.length > 0) {
1164
+ rolesByPack.set(position, new Map((checked.roles ?? []).map((r) => [r.eventId, r.role])));
1165
+ if (checked.contentFindings.length > 0) {
975
1166
  // BESIDE the deviation exception and never instead of it: a pack can do
976
1167
  // both, over different records, and the two say different things. This
977
1168
  // one is `incomplete` because whoever assembled the case chose the bytes
978
1169
  // it turns on (proof#147).
979
1170
  note(PACK_DECLARATION_CONTENT_DEPENDENT, {
980
1171
  packRef: pack.ref,
981
- count: check.contentFindings.length,
1172
+ count: checked.contentFindings.length,
982
1173
  });
983
1174
  }
984
- if (check.verdict === "failed") {
1175
+ if (checked.verdict === "failed") {
985
1176
  note(PACK_DECLARATION_DEVIATES, {
986
1177
  packRef: pack.ref,
987
- count: check.deviations.length,
1178
+ count: checked.deviations.length,
988
1179
  });
989
1180
  }
990
- else if (check.contentFindings.length > 0) {
1181
+ else if (checked.contentFindings.length > 0) {
991
1182
  // Already named above. `partial` here is not "the reader cannot read the
992
1183
  // claim": the claim was read, and part of it rests on bytes nobody
993
1184
  // signed.
994
1185
  }
995
- else if (check.verdict === "verified") {
1186
+ else if (checked.verdict === "verified") {
996
1187
  // A pack that agrees with its own claim contributes NOTHING, rather than
997
1188
  // a `verified` of its own. The case verdict is over the hand-off
998
1189
  // readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
@@ -1001,7 +1192,7 @@ export async function readCase(packs) {
1001
1192
  // read `mixed` and the failure would be the quieter half of a two-word
1002
1193
  // answer. A pack-level PROBLEM still contributes, through its exception.
1003
1194
  }
1004
- else if (check.present) {
1195
+ else if (checked.present) {
1005
1196
  // `partial` with a declaration present is 5.4.3's "a claim this reader
1006
1197
  // cannot read", which `gradePack` already carries to `incomplete`. The
1007
1198
  // same posture here, for the same reason: the field has no accept-set and
@@ -1009,7 +1200,18 @@ export async function readCase(packs) {
1009
1200
  note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref });
1010
1201
  }
1011
1202
  else {
1012
- note(PACK_UNDECLARED, { packRef: pack.ref });
1203
+ // WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
1204
+ // A23, I4): `partial` where the reading holds a hand-off record, and a
1205
+ // NOTE where it holds none. That is not known until the pile is read, so
1206
+ // the slot is held HERE, in pack order, and settled below.
1207
+ undeclaredSlots.push({
1208
+ exceptionAt: exceptions.length,
1209
+ contributionAt: contributions.length,
1210
+ position,
1211
+ ref: pack.ref,
1212
+ });
1213
+ exceptions.push(null);
1214
+ contributions.push(null);
1013
1215
  }
1014
1216
  // Each record on its OWN envelope. `packRecords` and the verifier both walk
1015
1217
  // `body.events` in order and emit one entry per element, so the index is
@@ -1017,6 +1219,7 @@ export async function readCase(packs) {
1017
1219
  for (let index = 0; index < records.length; index += 1) {
1018
1220
  const record = records[index];
1019
1221
  const graded = verified.events[index];
1222
+ const bare = signed.events[index];
1020
1223
  if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
1021
1224
  note(RECORD_PROFILE_QUARANTINED, { packRef: pack.ref, eventId: record.eventId });
1022
1225
  continue;
@@ -1043,20 +1246,31 @@ export async function readCase(packs) {
1043
1246
  });
1044
1247
  continue;
1045
1248
  }
1046
- // Named, and the record stays in the pile (see the kinds).
1047
- if (graded.contentOk === false) {
1249
+ // Named, and the record stays in the pile (see the kinds). A mismatch
1250
+ // the signed body carries on its own is `failed`; one that appears only
1251
+ // with the side map is `incomplete` (proof#159).
1252
+ if (bare.contentOk === false) {
1048
1253
  note(RECORD_CONTENT_MISMATCH, {
1049
1254
  packRef: pack.ref,
1050
1255
  eventId: record.eventId,
1051
- reasons: [...graded.errors],
1256
+ reasons: [...bare.errors],
1052
1257
  });
1053
1258
  }
1054
- if (!(graded.inclusionOk && graded.checkpointBound) ||
1055
- (!graded.ok && graded.contentOk !== false)) {
1259
+ else if (graded.contentOk === false) {
1260
+ note(RECORD_CONTENT_DEPENDENT, {
1261
+ packRef: pack.ref,
1262
+ eventId: record.eventId,
1263
+ reasons: onlyIn(graded.errors, bare.errors),
1264
+ });
1265
+ }
1266
+ // Every other check on the SIGNED reading, so side content can neither
1267
+ // add a failure here nor take one away.
1268
+ if (!(bare.inclusionOk && bare.checkpointBound) ||
1269
+ (!bare.ok && bare.contentOk !== false)) {
1056
1270
  note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
1057
1271
  packRef: pack.ref,
1058
1272
  eventId: record.eventId,
1059
- reasons: [...graded.errors],
1273
+ reasons: [...bare.errors],
1060
1274
  });
1061
1275
  }
1062
1276
  held.push({
@@ -1085,6 +1299,7 @@ export async function readCase(packs) {
1085
1299
  findings: [...reading.findings],
1086
1300
  failedChecks: [...reading.failedChecks],
1087
1301
  counterparts: [...reading.candidates],
1302
+ role: rolesByPack.get(position)?.get(eventId) ?? null,
1088
1303
  });
1089
1304
  // proof#137's door, named. Read from the ONE reader of the declaration
1090
1305
  // (`declarationClaims`), never from a second parse here. Only a READABLE
@@ -1098,9 +1313,13 @@ export async function readCase(packs) {
1098
1313
  // `not_a_handoff_record` exit it was a check the audited party switched off
1099
1314
  // by respelling two fields, and behind the `malformed` exit it was one a
1100
1315
  // party switched off by breaking its own record.
1316
+ //
1317
+ // v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
1318
+ // the declaration check already ran on the signed body.
1101
1319
  const claims = claimsByPack.get(position);
1102
1320
  if (claims !== undefined &&
1103
1321
  claims.readable &&
1322
+ claims.version === DECLARATION_VERSION &&
1104
1323
  !claims.claimedSides.includes(eventId)) {
1105
1324
  note(RECORD_UNCLAIMED, { packRef, eventId });
1106
1325
  }
@@ -1190,17 +1409,54 @@ export async function readCase(packs) {
1190
1409
  contributions.push([`${packRef}/${eventId}`, "verified"]);
1191
1410
  }
1192
1411
  }
1193
- if (handoffRecords === 0)
1194
- note(CASE_HOLDS_NO_HANDOFF_RECORDS);
1412
+ // A reading with no hand-off record in it is not an exception (5.5.3 v1.20,
1413
+ // A23, D6). The undeclared packs, settled: the exception `pack_undeclared`,
1414
+ // contributing `partial`, in the slot it was given in pack order where the
1415
+ // reading holds a hand-off record; a NOTE with no contribution where it holds
1416
+ // none.
1417
+ for (const slot of undeclaredSlots) {
1418
+ if (handoffRecords > 0) {
1419
+ exceptions[slot.exceptionAt] = { kind: PACK_UNDECLARED, packRef: slot.ref };
1420
+ contributions[slot.contributionAt] = [slot.ref, EXCEPTION_VERDICT[PACK_UNDECLARED]];
1421
+ }
1422
+ else {
1423
+ caseNotes.push({ position: slot.position, note: { kind: PACK_UNDECLARED, packRef: slot.ref } });
1424
+ }
1425
+ }
1426
+ const named = exceptions.filter((e) => e !== null);
1427
+ const contributed = contributions.filter((c) => c !== null);
1428
+ // Pack order, stable. A pack carries at most one note: one with no
1429
+ // declaration withholds nothing by one.
1430
+ const notes = caseNotes
1431
+ .map((n, i) => ({ ...n, i }))
1432
+ .sort((a, b) => a.position - b.position || a.i - b.i)
1433
+ .map((n) => n.note);
1195
1434
  return {
1196
- verdict: verdictOf(exceptions, contributions),
1197
- exceptions,
1435
+ verdict: verdictOf(named, contributed),
1436
+ exceptions: named,
1198
1437
  records,
1199
- contributions,
1438
+ contributions: contributed,
1200
1439
  handoffRecords,
1201
1440
  onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
1441
+ notes,
1442
+ packs: packReadings,
1202
1443
  };
1203
1444
  }
1445
+ /**
1446
+ * `body.declaration.declaration_version` where it is one this reader knows,
1447
+ * and `null` otherwise.
1448
+ */
1449
+ function declarationVersionOf(bundle) {
1450
+ const at = (v, k) => typeof v === "object" && v !== null && !Array.isArray(v)
1451
+ ? v[k]
1452
+ : undefined;
1453
+ const declaration = at(at(bundle, "body"), "declaration");
1454
+ if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) {
1455
+ return null;
1456
+ }
1457
+ const version = declaration["declaration_version"];
1458
+ return version === DECLARATION_VERSION || version === DECLARATION_VERSION_V2 ? version : null;
1459
+ }
1204
1460
  // ---- Presentation (optional) ------------------------------------------------
1205
1461
  /**
1206
1462
  * The house wording for one exception.
@@ -1285,13 +1541,20 @@ export function renderException(exception) {
1285
1541
  return (`${exception.packRef} carries a wrapper signature over its body that ` +
1286
1542
  "does not verify, so none of its records are read and the key it " +
1287
1543
  "names is not counted as its signer");
1288
- case PACK_BUNDLE_NOT_VERIFIED: {
1289
- let head = `${exception.packRef} reads ${(exception.status ?? "").toUpperCase()} as ` +
1290
- "a bundle, together with any content handed in beside it";
1544
+ case PACK_GRADE_NOT_VERIFIED: {
1545
+ let head = `${exception.packRef} grades ${(exception.status ?? "").toUpperCase()} as a pack`;
1546
+ if (exception.causes !== undefined && exception.causes.length > 0) {
1547
+ head += ` (${exception.causes.join(", ")})`;
1548
+ }
1291
1549
  if (exception.status === "malformed")
1292
1550
  head += ", so none of its records are read";
1293
1551
  return head + because(exception.reasons);
1294
1552
  }
1553
+ case PACK_BUNDLE_CONTENT_DEPENDENT:
1554
+ return (`${exception.packRef} reads FAILED as a bundle only together with ` +
1555
+ "the unsigned content handed in beside it, which anyone could have " +
1556
+ "supplied, so whether that failure is the pack's is UNKNOWN" +
1557
+ because(exception.reasons));
1295
1558
  case RECORD_ENVELOPE_DOES_NOT_VERIFY:
1296
1559
  return (`${where} is in a pack whose wrapper verifies, and its own envelope ` +
1297
1560
  "does not verify, so it is not read as a hand-off record" +
@@ -1304,45 +1567,128 @@ export function renderException(exception) {
1304
1567
  return (`${where} is read with content that does not match what it commits ` +
1305
1568
  "to, and it is still read as a hand-off record" +
1306
1569
  because(exception.reasons));
1570
+ case RECORD_CONTENT_DEPENDENT:
1571
+ return (`${where} commits to content that the unsigned content handed in ` +
1572
+ "beside its pack does not match, and anyone could have supplied " +
1573
+ "that content, so what it says about the record is UNKNOWN; the " +
1574
+ "record is still read as a hand-off record" +
1575
+ because(exception.reasons));
1307
1576
  case RECORD_PROFILE_QUARANTINED:
1308
1577
  return (`${where} is signed under a profile this case reader does not read ` +
1309
1578
  "while its admission is undecided, so what it says is UNKNOWN");
1310
1579
  case CASE_HOLDS_NO_PACKS:
1311
1580
  return "this case holds no packs";
1312
- case CASE_HOLDS_NO_HANDOFF_RECORDS:
1313
- return ("no record this verifier read in this case is a hand-off record, " +
1314
- "so no hand-off was verified");
1315
1581
  default:
1316
1582
  return exception.kind;
1317
1583
  }
1318
1584
  }
1585
+ /** The house wording for one NOTE. Offered, not imposed, as `renderException` is. */
1586
+ export function renderCaseNote(note) {
1587
+ switch (note.kind) {
1588
+ case CONTENT_WITHHELD_BY_DECLARATION:
1589
+ return (`${note.packRef} withholds the content of ${note.count} record(s) by ` +
1590
+ "its own signed declaration: their signatures and commitments were " +
1591
+ "checked, their content was not");
1592
+ case PACK_UNDECLARED:
1593
+ return `${note.packRef} does not say what it holds`;
1594
+ default:
1595
+ return note.kind;
1596
+ }
1597
+ }
1319
1598
  /**
1320
- * One line a surface can print for a whole case.
1599
+ * The scope sentence, printed directly under the headline (evidence-bundle
1600
+ * 5.5.6 v1.20, A26), byte for byte the text `render_case_summary` returns.
1601
+ *
1602
+ * The packs in the reading; the distinct keys, compared as keys, that signed
1603
+ * the wrapper of a pack whose wrapper held; the hand-off records; the
1604
+ * exceptions; and that the reading was made offline. Where any pack holds
1605
+ * records withheld by its declaration, their total; where any pack is noted
1606
+ * `pack_undeclared`, how many. Those two lines are part of what the headline
1607
+ * MEANS, and a surface MUST print them with it.
1321
1608
  *
1322
- * Both kernels print this sentence, so a reader who runs the Python command and
1323
- * the TypeScript one over one case archive reads one sentence rather than two
1324
- * that differ by a word.
1609
+ * What was not checked on the public record is no longer a clause here. It is
1610
+ * the witness line (`renderWitnessLine`), printed with every headline.
1325
1611
  */
1326
1612
  export function renderCaseSummary(reading) {
1327
- const n = reading.handoffRecords;
1328
- // What was NOT checked, in the same sentence as what was, and in the
1329
- // `verified` one above all (gate 4 on proof#140, finding 3). It is a
1330
- // statement of SCOPE and not a doubt: every pack here was verified offline,
1331
- // and no `anchor_ref` any of them names was fetched from a chain, because
1332
- // this reader makes no network call. Left unsaid, a clean case reads as
1333
- // though a witness had been checked, which nobody established. It is not an
1334
- // exception, and it must never become one: an empty exception list is the
1335
- // only shape a reader may read as clean, so a standing exception here would
1336
- // put `verified` out of reach for good.
1337
- const scope = reading.onChainChecked
1338
- ? ""
1339
- : "; no anchor_ref in it was re-fetched on chain, which this reader never does";
1340
- if (reading.verdict === "verified") {
1341
- return (`this case verifies across ${reading.records.length} record(s): ` +
1342
- `${n} hand-off record(s), every one matched, no exceptions` +
1343
- scope);
1613
+ const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
1614
+ const withheld = reading.packs.reduce((n, p) => n + p.recordsWithheld, 0);
1615
+ const undeclared = reading.notes.filter((n) => n.kind === PACK_UNDECLARED).length;
1616
+ const lines = [
1617
+ `${reading.packs.length} pack(s), ${keys.size} signing key(s), ` +
1618
+ `${reading.handoffRecords} hand-off record(s), ` +
1619
+ `${reading.exceptions.length} exception(s), read offline.`,
1620
+ ];
1621
+ if (withheld > 0) {
1622
+ lines.push(`${withheld} record(s)' content withheld by their holder: their ` +
1623
+ "signatures and commitments were checked, their content was not.");
1344
1624
  }
1345
- return (`this case reads ${reading.verdict.toUpperCase()} over ${n} hand-off ` +
1346
- `record(s), with ${reading.exceptions.length} exception(s)` +
1347
- scope);
1625
+ if (undeclared > 0)
1626
+ lines.push(`${undeclared} pack(s) do not say what they hold.`);
1627
+ return lines.join(" ");
1628
+ }
1629
+ // ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
1630
+ /**
1631
+ * The one display title of each of the eight 3 words. A surface prints the
1632
+ * title, never the word, and never a title of its own (#173, A18). The wording
1633
+ * is Will's to change through the glossary (DL-71); what each MEANS is pinned
1634
+ * in the spec.
1635
+ */
1636
+ export const VERDICT_DISPLAY_TITLES = {
1637
+ verified: "Verified",
1638
+ partial: "Partial",
1639
+ pending: "Pending",
1640
+ incomplete: "Incomplete",
1641
+ failed: "Failed",
1642
+ malformed: "Not readable",
1643
+ not_found: "Not found",
1644
+ mixed: "Mixed",
1645
+ };
1646
+ /**
1647
+ * What a reader may rely on, for each of the six words a case reading reaches
1648
+ * (5.5.6 v1.20, A26), the same for every drop. `pending` and `not_found`
1649
+ * never head a case reading: they are witness states.
1650
+ */
1651
+ export const CASE_RELIANCE = {
1652
+ verified: "Every pack in this drop is genuine and unaltered and agrees with its " +
1653
+ "own signed declaration. Every hand-off with both sides here is " +
1654
+ "intact. Every other record is one its holder signed as a step. " +
1655
+ "Nothing here could not be checked. This was read offline, on this " +
1656
+ "device. It does not say these packs are everything the parties " +
1657
+ "recorded.",
1658
+ partial: "Nothing here was proven wrong, and a check could not be completed: a " +
1659
+ "pack names no witness, or a pack in a hand-off reading does not say " +
1660
+ "what it holds. The exceptions say which.",
1661
+ incomplete: "Nothing here was proven wrong, and part of it could not be checked: " +
1662
+ "the other side of a hand-off is not in this drop, bytes were not " +
1663
+ "handed over, or a claim could not be read. Each exception says what " +
1664
+ "is missing and, where a key is known, whose pack would supply it.",
1665
+ failed: "A check ran and did not pass. Do not rely on this drop until the " +
1666
+ "named failure is explained. The failure is named on the pack or " +
1667
+ "record it is about, and every other exception is listed beside it.",
1668
+ malformed: "Part of this drop could not be read as evidence, and nothing was " +
1669
+ "proven wrong. The exceptions name what could not be read.",
1670
+ mixed: "The readings here differ and none of them failed: some are verified " +
1671
+ "and some could not be checked. Read each pack's row and each " +
1672
+ "exception before relying on any of it.",
1673
+ };
1674
+ /** The three witness states, and how each is printed (5.5.6, D7). No other. */
1675
+ export const WITNESS_STATES = {
1676
+ confirmed: "Confirmed on the public record",
1677
+ not_checked: "Not checked",
1678
+ differs: "The public record holds different bytes",
1679
+ };
1680
+ /** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
1681
+ export const WITNESS_NOT_LOOKED_UP = Object.freeze({
1682
+ state: "not_checked",
1683
+ reasons: ["no lookup was run"],
1684
+ services: [],
1685
+ });
1686
+ /** The witness line as both kernels print it. */
1687
+ export function renderWitnessLine(line) {
1688
+ let text = `Witness: ${WITNESS_STATES[line.state]}`;
1689
+ if (line.reasons.length > 0)
1690
+ text += `: ${line.reasons.join("; ")}`;
1691
+ if (line.services.length > 0)
1692
+ text += ` (answered: ${line.services.join(", ")})`;
1693
+ return text;
1348
1694
  }