@sanning/proof 0.11.0 → 0.12.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 +22 -0
- package/dist/case.d.ts +25 -16
- package/dist/case.js +53 -23
- package/dist/cli.d.ts +98 -0
- package/dist/cli.js +701 -6
- package/dist/handoff.d.ts +16 -1
- package/dist/handoff.js +111 -17
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/src/case.ts +65 -24
- package/src/cli.ts +787 -7
- package/src/handoff.ts +129 -18
- package/src/index.ts +2 -1
- package/vectors/README.md +35 -0
- package/vectors/case-split.json +893 -0
package/dist/handoff.d.ts
CHANGED
|
@@ -126,6 +126,16 @@ export interface CandidateReading {
|
|
|
126
126
|
ref: string;
|
|
127
127
|
verdict: HandoffVerdict;
|
|
128
128
|
failedChecks: string[];
|
|
129
|
+
/**
|
|
130
|
+
* The ref of the record that ACCOUNTS FOR this candidate, or `null` (4.1
|
|
131
|
+
* v1.17, A20). Set only on a candidate that graded `failed` and that the
|
|
132
|
+
* reader holds a verified pair for elsewhere; such a candidate is still
|
|
133
|
+
* reported here and takes no part in the subject's verdict, failed checks or
|
|
134
|
+
* ambiguity. Every reading this kernel returns carries it. It is optional in
|
|
135
|
+
* the type only so that code which builds a `CandidateReading` itself keeps
|
|
136
|
+
* compiling: the field is an addition.
|
|
137
|
+
*/
|
|
138
|
+
accountedFor?: string | null;
|
|
129
139
|
}
|
|
130
140
|
export interface HandoffReading {
|
|
131
141
|
ref: string;
|
|
@@ -135,7 +145,12 @@ export interface HandoffReading {
|
|
|
135
145
|
findings: string[];
|
|
136
146
|
/** Ordered, never filtered. */
|
|
137
147
|
candidates: string[];
|
|
138
|
-
/**
|
|
148
|
+
/**
|
|
149
|
+
* One entry per candidate, in the same order, and nothing left out of it. A
|
|
150
|
+
* candidate another record accounts for is here with `accountedFor` set, and
|
|
151
|
+
* where every candidate is accounted for the reading is `counterpart_absent`
|
|
152
|
+
* with the candidates still listed.
|
|
153
|
+
*/
|
|
139
154
|
candidateReadings: CandidateReading[];
|
|
140
155
|
}
|
|
141
156
|
/**
|
package/dist/handoff.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The hand-off pairing rule of specs/envelope-spec.md §4.1 (v1.
|
|
1
|
+
// The hand-off pairing rule of specs/envelope-spec.md §4.1 (v1.17).
|
|
2
2
|
//
|
|
3
3
|
// Two agents exchange a message and each side records what it held. This module
|
|
4
4
|
// is the TypeScript half of the reference reading of those two records: which
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
// compared absent to absent and found them equal.
|
|
41
41
|
// - A pair whose two envelope signing keys are equal does not verify.
|
|
42
42
|
// - Every hand-off record a reader holds is read, not only the ones that pair.
|
|
43
|
+
// - A formed pair verifies only when each record names the other's signer
|
|
44
|
+
// (v1.16, A21), or it fails `counterparty_named`. Up to 0.11.0 grading
|
|
45
|
+
// never read whom either record names.
|
|
43
46
|
//
|
|
44
47
|
// THE THREE ATTACKS THE ADVERSARIAL GATE LANDED (proof#133, rulings 1 to 3),
|
|
45
48
|
// and they are one root cause: candidacy and the self-hand-off check compared
|
|
@@ -116,14 +119,23 @@
|
|
|
116
119
|
// `not_a_handoff_record` are findings reported beside a verdict rather than
|
|
117
120
|
// verdicts.
|
|
118
121
|
//
|
|
119
|
-
// THE COST OF GRADING EVERY CANDIDATE,
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// than one message
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
122
|
+
// THE COST OF GRADING EVERY CANDIDATE, AND THE ACCOUNTED-FOR RULE THAT PAYS
|
|
123
|
+
// MOST OF IT (envelope-spec.md 4.1 v1.17, amendment A20). Grading every
|
|
124
|
+
// candidate means the records of a party's OTHER exchanges are candidates too,
|
|
125
|
+
// and they fail. Up to v1.16 that put `verified` out of reach for any two agents
|
|
126
|
+
// that exchanged more than one message in one direction, and the declaration
|
|
127
|
+
// named as the belt cannot be one: evidence-bundle.md 5.5 forbids the reading
|
|
128
|
+
// to consult `handoffs[]` to pair anything. So a candidate that grades `failed`
|
|
129
|
+
// is SET ASIDE from the subject's verdict, failed checks and ambiguity when,
|
|
130
|
+
// and only when, the reader holds another record `o` that accounts for it
|
|
131
|
+
// (`accountedFor` below): `o` is signed by the subject's own key and is the
|
|
132
|
+
// subject's type; `o` names the candidate's signer and the candidate names the
|
|
133
|
+
// subject's key back; `o`'s sequence differs from the subject's; and the
|
|
134
|
+
// candidate grades `verified` against `o`. The candidate stays in `candidates`
|
|
135
|
+
// and `candidateReadings`, with `accountedFor` set. Setting one aside needs a
|
|
136
|
+
// verified pair with a record the OTHER side signed, so a tampered record
|
|
137
|
+
// stays live. Fan-in, fan-out, and one pair in two cases at one sequence still
|
|
138
|
+
// read `failed`: the Python half's module docstring says why.
|
|
127
139
|
export const CANDIDATE_NAMESPACE = "handoff.";
|
|
128
140
|
export const DEFINED_EVENT_TYPES = ["handoff.sent", "handoff.received"];
|
|
129
141
|
/**
|
|
@@ -144,7 +156,7 @@ const HEX64 = /^[0-9a-fA-F]{64}$/;
|
|
|
144
156
|
const LOWER_HEX64 = /^[0-9a-f]{64}$/;
|
|
145
157
|
// The order a reader names the checks a formed pair failed, fixed so two
|
|
146
158
|
// kernels report one list rather than two orderings of it.
|
|
147
|
-
const CHECK_ORDER = ["message_hash", "sequence", "self_handoff"];
|
|
159
|
+
const CHECK_ORDER = ["message_hash", "sequence", "self_handoff", "counterparty_named"];
|
|
148
160
|
const VERDICT_RANK = { verified: 0, failed: 1, malformed: 2 };
|
|
149
161
|
// ASCII whitespace, listed rather than delegated: `String.trim()` and Python's
|
|
150
162
|
// `str.strip()` do not strip the same set (`U+FEFF` is whitespace to one and
|
|
@@ -478,6 +490,22 @@ function gradePair(subject, counterpart) {
|
|
|
478
490
|
failed.add("message_hash");
|
|
479
491
|
if (!sameNumber(subjectEvent.sequence, counterpartEvent.sequence))
|
|
480
492
|
failed.add("sequence");
|
|
493
|
+
// Each record names the OTHER's signer (v1.16, A21). Candidacy reads the key
|
|
494
|
+
// the subject names and nothing the candidate says, so up to and including
|
|
495
|
+
// 0.11.0 a sent record addressed to C paired `verified` with B's receipt of
|
|
496
|
+
// it, and a receipt naming C as its sender paired `verified` with A's send.
|
|
497
|
+
// Both halves of the naming are graded, each against the envelope key the
|
|
498
|
+
// other record was SIGNED with, compared as a key. The Python half is
|
|
499
|
+
// `_grade_pair`.
|
|
500
|
+
const [sent, received] = eventType(subject) === "handoff.sent" ? [subject, counterpart] : [counterpart, subject];
|
|
501
|
+
const sentNames = normalizedSigningKey(section(sent.record, "event").counterparty_public_key);
|
|
502
|
+
const receivedNames = normalizedSigningKey(section(received.record, "event").counterparty_public_key);
|
|
503
|
+
// The send names who received it.
|
|
504
|
+
if (sentNames !== normalizedSigningKey(received.publicKey))
|
|
505
|
+
failed.add("counterparty_named");
|
|
506
|
+
// The receipt names who sent it.
|
|
507
|
+
if (receivedNames !== normalizedSigningKey(sent.publicKey))
|
|
508
|
+
failed.add("counterparty_named");
|
|
481
509
|
const checks = CHECK_ORDER.filter((name) => failed.has(name));
|
|
482
510
|
return { verdict: checks.length > 0 ? "failed" : "verified", checks: [...checks] };
|
|
483
511
|
}
|
|
@@ -527,21 +555,39 @@ export function readHandoff(subject, held) {
|
|
|
527
555
|
// and took none of them out of this list.
|
|
528
556
|
const graded = candidates.map((candidate) => {
|
|
529
557
|
const { verdict, checks } = gradePair(subject, candidate);
|
|
530
|
-
|
|
558
|
+
const by = verdict === "failed" ? accountedFor(subject, candidate, held) : null;
|
|
559
|
+
return { ref: candidate.ref, verdict, failedChecks: checks, accountedFor: by };
|
|
531
560
|
});
|
|
532
|
-
|
|
561
|
+
// The verdict, the failed checks and the ambiguity are taken over the
|
|
562
|
+
// candidates nothing accounts for. The others stay in the report.
|
|
563
|
+
const live = graded.filter((g) => g.accountedFor === null);
|
|
564
|
+
if (live.length === 0) {
|
|
565
|
+
// Every candidate is another exchange's record, each proven so by a
|
|
566
|
+
// verified pair with a record the subject's own key signed: an absence,
|
|
567
|
+
// reported with the candidates it looked at.
|
|
568
|
+
return {
|
|
569
|
+
ref: subject.ref,
|
|
570
|
+
verdict: null,
|
|
571
|
+
failedChecks: [],
|
|
572
|
+
findings: ["counterpart_absent"],
|
|
573
|
+
candidates: candidates.map((c) => c.ref),
|
|
574
|
+
candidateReadings: graded,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const worst = Math.max(...live.map((g) => VERDICT_RANK[g.verdict]));
|
|
533
578
|
const verdict = Object.keys(VERDICT_RANK).find((name) => VERDICT_RANK[name] === worst);
|
|
534
|
-
// Over every candidate, not only the ones at the winning verdict:
|
|
535
|
-
// outranks `failed`, so a single junk record would otherwise
|
|
536
|
-
// `message_hash` failure out of the report (proof#133
|
|
579
|
+
// Over every live candidate, not only the ones at the winning verdict:
|
|
580
|
+
// `malformed` outranks `failed`, so a single junk record would otherwise
|
|
581
|
+
// strip the named `message_hash` failure out of the report (proof#133
|
|
582
|
+
// review, ruling 6).
|
|
537
583
|
const failed = new Set();
|
|
538
|
-
for (const g of
|
|
584
|
+
for (const g of live)
|
|
539
585
|
for (const check of g.failedChecks)
|
|
540
586
|
failed.add(check);
|
|
541
587
|
const findings = [];
|
|
542
588
|
// Never a free choice: a reader permitted to take one match can be handed an
|
|
543
589
|
// honest decoy beside a tampered record.
|
|
544
|
-
if (
|
|
590
|
+
if (live.length > 1)
|
|
545
591
|
findings.push("counterpart_ambiguous");
|
|
546
592
|
if (verdict === "malformed")
|
|
547
593
|
findings.push("malformed_record");
|
|
@@ -554,6 +600,54 @@ export function readHandoff(subject, held) {
|
|
|
554
600
|
candidateReadings: graded,
|
|
555
601
|
};
|
|
556
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* The ref of the record that accounts for a FAILED candidate, or `null` (4.1
|
|
605
|
+
* v1.17, A20). The candidate is another exchange's record, and not a deviation
|
|
606
|
+
* on this one, only where the reader holds a record `o`, other than the
|
|
607
|
+
* subject, for which all four hold:
|
|
608
|
+
*
|
|
609
|
+
* 1. `o` is signed by the SUBJECT's own key, and is the subject's type;
|
|
610
|
+
* 2. `o` names the candidate's signer, and the candidate names the subject's
|
|
611
|
+
* key back;
|
|
612
|
+
* 3. `o`'s `sequence` differs from the subject's;
|
|
613
|
+
* 4. the candidate grades `verified` against `o`.
|
|
614
|
+
*
|
|
615
|
+
* Condition 1 is what the audited party cannot supply: a verified pair needs
|
|
616
|
+
* the OTHER side's signature over the digest and the number. Condition 3 is
|
|
617
|
+
* the position guard (proof#129): a second record at the subject's own number
|
|
618
|
+
* is a second claim about the subject's message. The first `o` in the order
|
|
619
|
+
* the records are held is reported, so two kernels name one record. The
|
|
620
|
+
* Python half, `_accounted_for`, carries the full reasoning.
|
|
621
|
+
*/
|
|
622
|
+
function accountedFor(subject, candidate, held) {
|
|
623
|
+
const own = normalizedSigningKey(subject.publicKey);
|
|
624
|
+
const theirs = normalizedSigningKey(candidate.publicKey);
|
|
625
|
+
// Condition 2: the candidate names the subject's key back.
|
|
626
|
+
if (normalizedSigningKey(section(candidate.record, "event").counterparty_public_key) !== own) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
const subjectSequence = section(subject.record, "event").sequence;
|
|
630
|
+
for (const other of held) {
|
|
631
|
+
if (other === subject)
|
|
632
|
+
continue;
|
|
633
|
+
// Condition 1: signed by the subject's own key, of the subject's own type.
|
|
634
|
+
if (normalizedSigningKey(other.publicKey) !== own)
|
|
635
|
+
continue;
|
|
636
|
+
if (eventType(other) !== eventType(subject))
|
|
637
|
+
continue;
|
|
638
|
+
// Condition 2: o names the candidate's signer.
|
|
639
|
+
if (normalizedSigningKey(section(other.record, "event").counterparty_public_key) !== theirs) {
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
// Condition 3: another position than the subject's.
|
|
643
|
+
if (sameNumber(section(other.record, "event").sequence, subjectSequence))
|
|
644
|
+
continue;
|
|
645
|
+
// Condition 4: a verified pair.
|
|
646
|
+
if (gradePair(other, candidate).verdict === "verified")
|
|
647
|
+
return other.ref;
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
557
651
|
/**
|
|
558
652
|
* EVERY record the reader holds gets a reading, in the order given.
|
|
559
653
|
*
|
package/dist/index.js
CHANGED
|
@@ -77,7 +77,8 @@ export { DECLARATION_VERSION, checkDeclaration, declarationClaims, packRecords,
|
|
|
77
77
|
// because the omission happens before any reader runs. Reading several parties'
|
|
78
78
|
// packs at once is what closes it: a `handoff.sent` with no counterpart is
|
|
79
79
|
// always named, and where a pack signed by the key it names is in the case, the
|
|
80
|
-
// reading says that pack holds no counterpart
|
|
80
|
+
// reading says that pack holds no counterpart (unless every candidate was
|
|
81
|
+
// accounted for under A20, which reads as no counterpart and names no pack). Whether a party that presented
|
|
81
82
|
// no such pack recorded anything is not established (proof#143). `readCase`
|
|
82
83
|
// verifies every pack itself, offline, so it is async. Matching is on keys and hashes alone and NEVER on tenancy, so a pair from two
|
|
83
84
|
// organizations verifies (DL-60).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1 and sanning.events/v2. Conformance-gated byte-for-byte against the test-vectors corpus.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/case.ts
CHANGED
|
@@ -185,7 +185,8 @@
|
|
|
185
185
|
* (F2).
|
|
186
186
|
* 10. **`counterpart_absent`** (`continue`). Skips the ambiguity and pair
|
|
187
187
|
* reporting, and there is nothing to skip: 4.1 emits that finding only
|
|
188
|
-
*
|
|
188
|
+
* where no candidate is left to read, an EMPTY candidate list or one whose
|
|
189
|
+
* every candidate another record accounts for (4.1 v1.17, A20). Which of the three counterpart kinds it
|
|
189
190
|
* gets reads the keys of the packs that verified, counted before exit 4,
|
|
190
191
|
* and whether any pack took exit 2 or 3.
|
|
191
192
|
*
|
|
@@ -225,16 +226,16 @@
|
|
|
225
226
|
* with that verdict and its reasons, so the case never reads above its
|
|
226
227
|
* weakest pack. On-chain re-fetch and `gradePack` are a caller's to run.
|
|
227
228
|
*
|
|
228
|
-
* THE COST OF 4.1's "GRADE EVERY CANDIDATE",
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
229
|
+
* THE COST OF 4.1's "GRADE EVERY CANDIDATE", AND WHAT IS LEFT OF IT (4.1
|
|
230
|
+
* v1.17, amendment A20, which reopens proof#134). Up to v1.16 a pile holding
|
|
231
|
+
* two or more records of the complementary type signed by one counterparty key
|
|
232
|
+
* could not reach `verified`. 4.1 now sets such a candidate aside where a
|
|
233
|
+
* record the subject's own key signed pairs with it verified at another
|
|
234
|
+
* sequence, so two messages in one direction, and the demo's A to B, B to A,
|
|
235
|
+
* A to B, read clean. What is left surfaces as `COUNTERPART_AMBIGUOUS` beside
|
|
236
|
+
* the failure: fan-in, fan-out, and the same pair in two cases at the same
|
|
237
|
+
* sequence. Narrowing any of it here with `handoffs[]` is precisely the move
|
|
238
|
+
* that would reopen proof#137's door.
|
|
238
239
|
*/
|
|
239
240
|
|
|
240
241
|
import {
|
|
@@ -335,7 +336,9 @@ export const CASE_QUARANTINED_PROFILES = ["sanning.mlflow/v1"] as const;
|
|
|
335
336
|
/**
|
|
336
337
|
* A conformant hand-off record with no counterpart among the records this
|
|
337
338
|
* 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
|
|
339
|
+
* can read and NO pack in the case is signed by the key the record names,
|
|
340
|
+
* or where every candidate it had was accounted for by another record under
|
|
341
|
+
* A20 (the exception then carries `count`, and names no pack).
|
|
339
342
|
* **An exception, never a failure** (anchor#198, criterion 3).
|
|
340
343
|
*
|
|
341
344
|
* That is ALL it says. It does not say the counterparty's account is absent,
|
|
@@ -425,9 +428,11 @@ export const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS =
|
|
|
425
428
|
export const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
|
|
426
429
|
|
|
427
430
|
/**
|
|
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
|
|
431
|
+
* More than one candidate counterpart that nothing accounts for (4.1 v1.17,
|
|
432
|
+
* A20). The reading is the worst of them and the ambiguity is named: a reader
|
|
433
|
+
* permitted to take one match can be handed an honest decoy beside a tampered
|
|
434
|
+
* record. `count` is how many of the candidates are live, which is the number
|
|
435
|
+
* the ambiguity is about.
|
|
431
436
|
*/
|
|
432
437
|
export const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
|
|
433
438
|
|
|
@@ -793,7 +798,11 @@ export interface CaseException {
|
|
|
793
798
|
counterpartyKey?: string | null;
|
|
794
799
|
/** Which checks a formed pair failed, in 4.1's fixed order. */
|
|
795
800
|
failedChecks?: string[];
|
|
796
|
-
/**
|
|
801
|
+
/**
|
|
802
|
+
* How many of a thing the exception is about, where it is about a count. On
|
|
803
|
+
* `COUNTERPART_ABSENT` it is set only where 4.1 set every candidate aside
|
|
804
|
+
* (v1.17, A20), and it is how many were set aside.
|
|
805
|
+
*/
|
|
797
806
|
count?: number;
|
|
798
807
|
/** The pack's own bundle verdict, for `PACK_BUNDLE_NOT_VERIFIED`. */
|
|
799
808
|
status?: CaseVerdict;
|
|
@@ -1343,13 +1352,34 @@ export async function readCase(
|
|
|
1343
1352
|
// that party's pack. Where some pack could not be tied to a key, "no
|
|
1344
1353
|
// pack is signed by the named key" is not established and the reading
|
|
1345
1354
|
// says it could not tell.
|
|
1355
|
+
//
|
|
1356
|
+
// EXCEPT where 4.1 set every candidate aside (v1.17, A20). There the
|
|
1357
|
+
// absence is a finding about the candidates the reader DID read, each
|
|
1358
|
+
// paired `verified` with another record the subject's own signer made,
|
|
1359
|
+
// and not a finding about any pack. 4.1 and evidence-bundle A16 name the
|
|
1360
|
+
// neutral kind for it, and the damning one would put the audited party's
|
|
1361
|
+
// own unfaithful record on the counterparty's pack as a statement
|
|
1362
|
+
// against it. `count` is how many candidates were set aside, which is
|
|
1363
|
+
// also what tells the rendered sentence which absence this is.
|
|
1364
|
+
const graded = reading.candidateReadings;
|
|
1365
|
+
const setAside =
|
|
1366
|
+
graded.length > 0 && graded.every((c) => c.accountedFor !== null)
|
|
1367
|
+
? graded.length
|
|
1368
|
+
: 0;
|
|
1346
1369
|
const kind =
|
|
1347
|
-
|
|
1348
|
-
?
|
|
1349
|
-
:
|
|
1350
|
-
?
|
|
1351
|
-
:
|
|
1352
|
-
|
|
1370
|
+
setAside > 0
|
|
1371
|
+
? COUNTERPART_ABSENT
|
|
1372
|
+
: named !== null && holderKeys.has(named)
|
|
1373
|
+
? COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS
|
|
1374
|
+
: unattributed > 0
|
|
1375
|
+
? COUNTERPART_ABSENT_SIGNER_UNKNOWN
|
|
1376
|
+
: COUNTERPART_ABSENT;
|
|
1377
|
+
note(
|
|
1378
|
+
kind,
|
|
1379
|
+
setAside > 0
|
|
1380
|
+
? { packRef, eventId, counterpartyKey: named, count: setAside }
|
|
1381
|
+
: { packRef, eventId, counterpartyKey: named },
|
|
1382
|
+
);
|
|
1353
1383
|
continue;
|
|
1354
1384
|
}
|
|
1355
1385
|
|
|
@@ -1357,7 +1387,7 @@ export async function readCase(
|
|
|
1357
1387
|
note(COUNTERPART_AMBIGUOUS, {
|
|
1358
1388
|
packRef,
|
|
1359
1389
|
eventId,
|
|
1360
|
-
count: reading.
|
|
1390
|
+
count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
|
|
1361
1391
|
});
|
|
1362
1392
|
}
|
|
1363
1393
|
// ON `failedChecks` AND NOT ON THE VERDICT WORD. 4.1 keeps the UNION of
|
|
@@ -1406,6 +1436,17 @@ export function renderException(exception: CaseException): string {
|
|
|
1406
1436
|
const where = `${exception.packRef}/${exception.eventId}`;
|
|
1407
1437
|
switch (exception.kind) {
|
|
1408
1438
|
case COUNTERPART_ABSENT:
|
|
1439
|
+
if (exception.count) {
|
|
1440
|
+
const which =
|
|
1441
|
+
exception.count === 1
|
|
1442
|
+
? "its one candidate counterpart pairs"
|
|
1443
|
+
: `each of its ${exception.count} candidate counterparts pairs`;
|
|
1444
|
+
return (
|
|
1445
|
+
`${where} has no counterpart among the records this verifier read: ` +
|
|
1446
|
+
`${which} verified with another record signed by the key that ` +
|
|
1447
|
+
"signed this one, at a different sequence, and is set aside"
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1409
1450
|
return (
|
|
1410
1451
|
`${where} has no counterpart among the records this verifier read, ` +
|
|
1411
1452
|
"and no pack in this case is signed by the key it names"
|
|
@@ -1523,7 +1564,7 @@ export function renderException(exception: CaseException): string {
|
|
|
1523
1564
|
case RECORD_PROFILE_QUARANTINED:
|
|
1524
1565
|
return (
|
|
1525
1566
|
`${where} is signed under a profile this case reader does not read ` +
|
|
1526
|
-
"while its admission is undecided
|
|
1567
|
+
"while its admission is undecided, so what it says is UNKNOWN"
|
|
1527
1568
|
);
|
|
1528
1569
|
case CASE_HOLDS_NO_PACKS:
|
|
1529
1570
|
return "this case holds no packs";
|