@sanning/proof 0.13.0 → 0.15.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/dist/case-file.js +6 -2
- package/dist/case.d.ts +51 -7
- package/dist/case.js +81 -16
- package/dist/cli.d.ts +8 -27
- package/dist/cli.js +184 -103
- package/dist/compose.js +6 -1
- package/dist/evidence.d.ts +36 -0
- package/dist/evidence.js +104 -3
- package/dist/file-match.d.ts +105 -0
- package/dist/file-match.js +374 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +24 -2
- package/dist/message-parts.d.ts +12 -6
- package/dist/message-parts.js +18 -10
- package/dist/verifier.d.ts +54 -2
- package/dist/verifier.js +195 -3
- package/dist/witness-cache.d.ts +85 -0
- package/dist/witness-cache.js +264 -0
- package/dist/witness-lookup.d.ts +112 -0
- package/dist/witness-lookup.js +300 -0
- package/package.json +1 -1
- package/src/case-file.ts +5 -2
- package/src/case.ts +101 -15
- package/src/cli.ts +191 -123
- package/src/compose.ts +8 -1
- package/src/evidence.ts +115 -2
- package/src/file-match.ts +461 -0
- package/src/index.ts +68 -0
- package/src/message-parts.ts +18 -10
- package/src/verifier.ts +190 -3
- package/src/witness-cache.ts +306 -0
- package/src/witness-lookup.ts +428 -0
- package/vectors/README.md +43 -3
- package/vectors/file-match.json +1808 -0
- package/vectors/jcs-reader.json +82 -3
- package/vectors/stamp-on-chain.json +18 -3
- package/vectors/witness-cache.json +1304 -0
package/dist/case-file.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* Pure: bytes and a file list in, a reading out. No filesystem, no zip, no
|
|
25
25
|
* network. How the files were listed and hashed is the caller's.
|
|
26
26
|
*/
|
|
27
|
-
import { jcsParse } from "./verifier.js";
|
|
27
|
+
import { DuplicateKeyError, jcsParse } from "./verifier.js";
|
|
28
28
|
/** The index's dispatch key (5.6.2). An absent or unknown value makes the index unreadable. */
|
|
29
29
|
export const CASE_FILE_VERSION = "sanning.case-file/v1";
|
|
30
30
|
/** The index, at the root of the case file. */
|
|
@@ -176,7 +176,11 @@ function readIndex(raw) {
|
|
|
176
176
|
try {
|
|
177
177
|
value = jcsParse(raw);
|
|
178
178
|
}
|
|
179
|
-
catch {
|
|
179
|
+
catch (e) {
|
|
180
|
+
// Named, because an index that lists one pack twice under one member name
|
|
181
|
+
// reads as whichever copy came last (I-JSON, proof#144).
|
|
182
|
+
if (e instanceof DuplicateKeyError)
|
|
183
|
+
throw new Unreadable(e.message);
|
|
180
184
|
throw new Unreadable("not readable as JSON");
|
|
181
185
|
}
|
|
182
186
|
if (!isObject(value))
|
package/dist/case.d.ts
CHANGED
|
@@ -638,13 +638,21 @@ export declare const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
|
638
638
|
/** The reader was handed no packs at all. */
|
|
639
639
|
export declare const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
640
640
|
/**
|
|
641
|
-
* The
|
|
642
|
-
* printed in the scope sentence so a clean verdict
|
|
643
|
-
* `pack_undeclared` is a note only where the
|
|
644
|
-
* where it holds one, it is the exception
|
|
641
|
+
* The three NOTEs (5.5.3 v1.20, extended proof#148): findings on the reading
|
|
642
|
+
* with no contribution, printed in the scope sentence so a clean verdict
|
|
643
|
+
* never hides any of them. `pack_undeclared` is a note only where the
|
|
644
|
+
* reading holds no hand-off record; where it holds one, it is the exception
|
|
645
|
+
* of that name.
|
|
645
646
|
*/
|
|
646
647
|
export declare const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
647
|
-
|
|
648
|
+
/**
|
|
649
|
+
* A pack's wrapper carries a present `co_signatures` (evidence-bundle.md §2,
|
|
650
|
+
* decided by Phil 2026-09-24 on proof#148). The field verifies nothing and
|
|
651
|
+
* sits outside the wrapper's signed scope, so the case reading names it
|
|
652
|
+
* rather than presenting it as evidence, and it contributes no verdict.
|
|
653
|
+
*/
|
|
654
|
+
export declare const CO_SIGNATURES_PRESENT = "co_signatures_present";
|
|
655
|
+
export declare const CASE_NOTE_KINDS: readonly ["pack_undeclared", "content_withheld_by_declaration", "co_signatures_present"];
|
|
648
656
|
export type CaseNoteKind = (typeof CASE_NOTE_KINDS)[number];
|
|
649
657
|
/**
|
|
650
658
|
* Every exception this reader can report, as a closed set. A surface may render
|
|
@@ -872,10 +880,30 @@ export declare function renderCaseSummary(reading: CaseReading): string;
|
|
|
872
880
|
export declare const VERDICT_DISPLAY_TITLES: Readonly<Record<string, string>>;
|
|
873
881
|
/**
|
|
874
882
|
* What a reader may rely on, for each of the six words a case reading reaches
|
|
875
|
-
* (5.5.6 v1.20, A26), the same for every
|
|
876
|
-
* never head a case reading: they are
|
|
883
|
+
* (5.5.6 v1.20, A26; the `verified` text at v1.28, A35), the same for every
|
|
884
|
+
* drop. `pending` and `not_found` never head a case reading: they are
|
|
885
|
+
* witness states. `verified` is the base sentence, printed alone where the
|
|
886
|
+
* drop holds no hand-off record; a caller reads `caseReliance` for
|
|
887
|
+
* `verified`, never this mapping directly.
|
|
877
888
|
*/
|
|
878
889
|
export declare const CASE_RELIANCE: Readonly<Record<CaseVerdict, string>>;
|
|
890
|
+
/**
|
|
891
|
+
* The clause a `verified` drop that holds a hand-off record gains, inserted
|
|
892
|
+
* after `CASE_RELIANCE`'s first sentence (5.5.6, A35). Never printed alone,
|
|
893
|
+
* and never where `handoffRecords` is zero: most drops hold one agent's
|
|
894
|
+
* pack and no hand-off to say anything about.
|
|
895
|
+
*/
|
|
896
|
+
export declare const CASE_RELIANCE_HANDOFF_CLAUSE = "Every hand-off with both sides in this drop is intact.";
|
|
897
|
+
/**
|
|
898
|
+
* What a reader may rely on for one case reading (5.5.6 v1.28, A35), byte
|
|
899
|
+
* for byte the text `case_reliance`'s Python sibling returns.
|
|
900
|
+
*
|
|
901
|
+
* For every word but `verified` this is `CASE_RELIANCE` unchanged. For
|
|
902
|
+
* `verified`, the hand-off clause is inserted after the first sentence
|
|
903
|
+
* where `handoffRecords` is not zero, and left out where it is zero: the
|
|
904
|
+
* base sentence never claims a hand-off the drop does not hold.
|
|
905
|
+
*/
|
|
906
|
+
export declare function caseReliance(verdict: CaseVerdict, handoffRecords: number): string;
|
|
879
907
|
/** The three witness states, and how each is printed (5.5.6, D7). No other. */
|
|
880
908
|
export declare const WITNESS_STATES: {
|
|
881
909
|
readonly confirmed: "Confirmed on the public record";
|
|
@@ -894,8 +922,24 @@ export interface WitnessLine {
|
|
|
894
922
|
state: WitnessState;
|
|
895
923
|
reasons: string[];
|
|
896
924
|
services: string[];
|
|
925
|
+
/**
|
|
926
|
+
* Witness times a pack caches that no lookup settled, as `[blockHeight,
|
|
927
|
+
* blockTime]`, distinct and in time order (5.6.6, v1.25, A30). Printed labelled
|
|
928
|
+
* not checked, and never as a time a reader may rely on. Absent reads as none.
|
|
929
|
+
*/
|
|
930
|
+
cached?: [number, number][];
|
|
931
|
+
/**
|
|
932
|
+
* Witness times the public record returned where they matched a pack's
|
|
933
|
+
* cache, distinct and in time order. The gateway's answer, named with the
|
|
934
|
+
* service, never the cache's value. Absent reads as none.
|
|
935
|
+
*/
|
|
936
|
+
recorded?: [number, number][];
|
|
897
937
|
}
|
|
898
938
|
/** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
|
|
899
939
|
export declare const WITNESS_NOT_LOOKED_UP: Readonly<WitnessLine>;
|
|
900
940
|
/** The witness line as both kernels print it. */
|
|
941
|
+
/** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
|
|
942
|
+
export declare const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
|
|
943
|
+
/** How a witness time the public record returned is printed where it matched the cache. */
|
|
944
|
+
export declare const WITNESS_TIME_RECORDED = "the public record dates the witness";
|
|
901
945
|
export declare function renderWitnessLine(line: WitnessLine): string;
|
package/dist/case.js
CHANGED
|
@@ -243,8 +243,9 @@
|
|
|
243
243
|
*/
|
|
244
244
|
import { DECLARATION_VERSION, DECLARATION_VERSION_V2, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
|
|
245
245
|
import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
|
|
246
|
-
import { verifyEvidenceBundle } from "./evidence.js";
|
|
246
|
+
import { coSignaturesPresent, verifyEvidenceBundle } from "./evidence.js";
|
|
247
247
|
import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
|
|
248
|
+
import { renderWitnessTime } from "./witness-cache.js";
|
|
248
249
|
// ---- The vocabulary ---------------------------------------------------------
|
|
249
250
|
/**
|
|
250
251
|
* The verdicts a case can reach. Every one is already in use in
|
|
@@ -665,13 +666,25 @@ export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
|
|
|
665
666
|
/** The reader was handed no packs at all. */
|
|
666
667
|
export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
|
|
667
668
|
/**
|
|
668
|
-
* The
|
|
669
|
-
* printed in the scope sentence so a clean verdict
|
|
670
|
-
* `pack_undeclared` is a note only where the
|
|
671
|
-
* where it holds one, it is the exception
|
|
669
|
+
* The three NOTEs (5.5.3 v1.20, extended proof#148): findings on the reading
|
|
670
|
+
* with no contribution, printed in the scope sentence so a clean verdict
|
|
671
|
+
* never hides any of them. `pack_undeclared` is a note only where the
|
|
672
|
+
* reading holds no hand-off record; where it holds one, it is the exception
|
|
673
|
+
* of that name.
|
|
672
674
|
*/
|
|
673
675
|
export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
|
|
674
|
-
|
|
676
|
+
/**
|
|
677
|
+
* A pack's wrapper carries a present `co_signatures` (evidence-bundle.md §2,
|
|
678
|
+
* decided by Phil 2026-09-24 on proof#148). The field verifies nothing and
|
|
679
|
+
* sits outside the wrapper's signed scope, so the case reading names it
|
|
680
|
+
* rather than presenting it as evidence, and it contributes no verdict.
|
|
681
|
+
*/
|
|
682
|
+
export const CO_SIGNATURES_PRESENT = "co_signatures_present";
|
|
683
|
+
export const CASE_NOTE_KINDS = [
|
|
684
|
+
PACK_UNDECLARED,
|
|
685
|
+
CONTENT_WITHHELD_BY_DECLARATION,
|
|
686
|
+
CO_SIGNATURES_PRESENT,
|
|
687
|
+
];
|
|
675
688
|
/**
|
|
676
689
|
* Every exception this reader can report, as a closed set. A surface may render
|
|
677
690
|
* them and a test may assert over them; nothing outside this array is emitted.
|
|
@@ -1048,6 +1061,15 @@ export async function readCase(packs) {
|
|
|
1048
1061
|
continue;
|
|
1049
1062
|
}
|
|
1050
1063
|
holderKeys.add(holder);
|
|
1064
|
+
if (coSignaturesPresent(pack.bundle)) {
|
|
1065
|
+
// A NOTE, with no contribution (5.5.3 v1.20, proof#148): the wrapper's
|
|
1066
|
+
// co_signatures verifies nothing and sits outside its signed scope, so
|
|
1067
|
+
// the case reading names it rather than moving anything on its account.
|
|
1068
|
+
caseNotes.push({
|
|
1069
|
+
position,
|
|
1070
|
+
note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref },
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1051
1073
|
const bodyType = bodyTypeOf(pack.bundle);
|
|
1052
1074
|
const records = packRecords(pack.bundle, pack.content);
|
|
1053
1075
|
// The per-record gradings below are joined to `records` BY INDEX, so a
|
|
@@ -1591,6 +1613,9 @@ export function renderCaseNote(note) {
|
|
|
1591
1613
|
"checked, their content was not");
|
|
1592
1614
|
case PACK_UNDECLARED:
|
|
1593
1615
|
return `${note.packRef} does not say what it holds`;
|
|
1616
|
+
case CO_SIGNATURES_PRESENT:
|
|
1617
|
+
return (`${note.packRef} carries unverified co-signatures in its wrapper; ` +
|
|
1618
|
+
"they are not part of the evidence");
|
|
1594
1619
|
default:
|
|
1595
1620
|
return note.kind;
|
|
1596
1621
|
}
|
|
@@ -1645,16 +1670,17 @@ export const VERDICT_DISPLAY_TITLES = {
|
|
|
1645
1670
|
};
|
|
1646
1671
|
/**
|
|
1647
1672
|
* 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
|
|
1649
|
-
* never head a case reading: they are
|
|
1673
|
+
* (5.5.6 v1.20, A26; the `verified` text at v1.28, A35), the same for every
|
|
1674
|
+
* drop. `pending` and `not_found` never head a case reading: they are
|
|
1675
|
+
* witness states. `verified` is the base sentence, printed alone where the
|
|
1676
|
+
* drop holds no hand-off record; a caller reads `caseReliance` for
|
|
1677
|
+
* `verified`, never this mapping directly.
|
|
1650
1678
|
*/
|
|
1651
1679
|
export const CASE_RELIANCE = {
|
|
1652
|
-
verified: "Every pack in this drop is genuine
|
|
1653
|
-
"
|
|
1654
|
-
"
|
|
1655
|
-
"
|
|
1656
|
-
"device. It does not say these packs are everything the parties " +
|
|
1657
|
-
"recorded.",
|
|
1680
|
+
verified: "Every pack in this drop is genuine, unaltered, and matches its own " +
|
|
1681
|
+
"signed declaration. Every record in it was signed by its holder's " +
|
|
1682
|
+
"key. This was read offline, on this device. It does not say this " +
|
|
1683
|
+
"drop holds everything that was recorded.",
|
|
1658
1684
|
partial: "Nothing here was proven wrong, and a check could not be completed: a " +
|
|
1659
1685
|
"pack names no witness, or a pack in a hand-off reading does not say " +
|
|
1660
1686
|
"what it holds. The exceptions say which.",
|
|
@@ -1671,6 +1697,32 @@ export const CASE_RELIANCE = {
|
|
|
1671
1697
|
"and some could not be checked. Read each pack's row and each " +
|
|
1672
1698
|
"exception before relying on any of it.",
|
|
1673
1699
|
};
|
|
1700
|
+
/**
|
|
1701
|
+
* The clause a `verified` drop that holds a hand-off record gains, inserted
|
|
1702
|
+
* after `CASE_RELIANCE`'s first sentence (5.5.6, A35). Never printed alone,
|
|
1703
|
+
* and never where `handoffRecords` is zero: most drops hold one agent's
|
|
1704
|
+
* pack and no hand-off to say anything about.
|
|
1705
|
+
*/
|
|
1706
|
+
export const CASE_RELIANCE_HANDOFF_CLAUSE = "Every hand-off with both sides in this drop is intact.";
|
|
1707
|
+
/**
|
|
1708
|
+
* What a reader may rely on for one case reading (5.5.6 v1.28, A35), byte
|
|
1709
|
+
* for byte the text `case_reliance`'s Python sibling returns.
|
|
1710
|
+
*
|
|
1711
|
+
* For every word but `verified` this is `CASE_RELIANCE` unchanged. For
|
|
1712
|
+
* `verified`, the hand-off clause is inserted after the first sentence
|
|
1713
|
+
* where `handoffRecords` is not zero, and left out where it is zero: the
|
|
1714
|
+
* base sentence never claims a hand-off the drop does not hold.
|
|
1715
|
+
*/
|
|
1716
|
+
export function caseReliance(verdict, handoffRecords) {
|
|
1717
|
+
if (verdict === "verified" && handoffRecords > 0) {
|
|
1718
|
+
return ("Every pack in this drop is genuine, unaltered, and matches its own " +
|
|
1719
|
+
"signed declaration. Every hand-off with both sides in this drop is " +
|
|
1720
|
+
"intact. Every record in it was signed by its holder's key. This " +
|
|
1721
|
+
"was read offline, on this device. It does not say this drop holds " +
|
|
1722
|
+
"everything that was recorded.");
|
|
1723
|
+
}
|
|
1724
|
+
return CASE_RELIANCE[verdict];
|
|
1725
|
+
}
|
|
1674
1726
|
/** The three witness states, and how each is printed (5.5.6, D7). No other. */
|
|
1675
1727
|
export const WITNESS_STATES = {
|
|
1676
1728
|
confirmed: "Confirmed on the public record",
|
|
@@ -1684,10 +1736,23 @@ export const WITNESS_NOT_LOOKED_UP = Object.freeze({
|
|
|
1684
1736
|
services: [],
|
|
1685
1737
|
});
|
|
1686
1738
|
/** The witness line as both kernels print it. */
|
|
1739
|
+
/** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
|
|
1740
|
+
export const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
|
|
1741
|
+
/** How a witness time the public record returned is printed where it matched the cache. */
|
|
1742
|
+
export const WITNESS_TIME_RECORDED = "the public record dates the witness";
|
|
1743
|
+
function witnessTimes(times) {
|
|
1744
|
+
return times.map(([h, t]) => renderWitnessTime(h, t)).join(", ");
|
|
1745
|
+
}
|
|
1687
1746
|
export function renderWitnessLine(line) {
|
|
1688
1747
|
let text = `Witness: ${WITNESS_STATES[line.state]}`;
|
|
1689
|
-
|
|
1690
|
-
|
|
1748
|
+
const parts = [...line.reasons];
|
|
1749
|
+
if (line.cached && line.cached.length > 0)
|
|
1750
|
+
parts.push(`${WITNESS_TIME_CACHED}: ${witnessTimes(line.cached)}`);
|
|
1751
|
+
if (line.recorded && line.recorded.length > 0) {
|
|
1752
|
+
parts.push(`${WITNESS_TIME_RECORDED}: ${witnessTimes(line.recorded)}`);
|
|
1753
|
+
}
|
|
1754
|
+
if (parts.length > 0)
|
|
1755
|
+
text += `: ${parts.join("; ")}`;
|
|
1691
1756
|
if (line.services.length > 0)
|
|
1692
1757
|
text += ` (answered: ${line.services.join(", ")})`;
|
|
1693
1758
|
return text;
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { type WitnessLine } from "./case.js";
|
|
3
2
|
interface Cli {
|
|
4
3
|
out: (s: string) => void;
|
|
5
4
|
err: (s: string) => void;
|
|
@@ -33,18 +32,23 @@ export declare class CaseInputError extends Error {
|
|
|
33
32
|
/**
|
|
34
33
|
* `bundle.json` bytes that `readBundleBytes` will not read. `tooDeep` separates
|
|
35
34
|
* the one refusal that is not a parse error; `detail` is the parser's own words,
|
|
36
|
-
* which differ by kernel and are printed by `verify` only.
|
|
35
|
+
* which differ by kernel and are printed by `verify` only. `duplicate` marks the
|
|
36
|
+
* one parse refusal whose `detail` is kernel wording, identical in both kernels
|
|
37
|
+
* (`duplicate key "<name>"`, proof#144), so both commands print it.
|
|
37
38
|
*/
|
|
38
39
|
export declare class BundleReadError extends Error {
|
|
39
40
|
readonly tooDeep: boolean;
|
|
40
41
|
readonly detail: string;
|
|
41
|
-
|
|
42
|
+
readonly duplicate: boolean;
|
|
43
|
+
constructor(tooDeep: boolean, detail: string, duplicate?: boolean);
|
|
42
44
|
}
|
|
45
|
+
export declare const NOT_I_JSON = "is not I-JSON (RFC 7493)";
|
|
43
46
|
/**
|
|
44
47
|
* How both `verify` and `case` turn `bundle.json` bytes into JSON, in both
|
|
45
48
|
* kernels (`read_bundle_bytes` in the Python twin). UTF-8 with each ill-formed
|
|
46
49
|
* sequence read as U+FFFD, and a byte-order mark kept; then nesting deeper than
|
|
47
|
-
* MAX_JSON_DEPTH is refused before any parser walks into it; then `JSON.parse
|
|
50
|
+
* MAX_JSON_DEPTH is refused before any parser walks into it; then `JSON.parse`,
|
|
51
|
+
* refusing an object that repeats a member name (I-JSON, RFC 7493; proof#144).
|
|
48
52
|
* Throws `BundleReadError`.
|
|
49
53
|
*/
|
|
50
54
|
export declare function readBundleBytes(bytes: Uint8Array): unknown;
|
|
@@ -100,29 +104,6 @@ export declare function readFileEntry(entry: TreeEntry, key: string): Promise<Ui
|
|
|
100
104
|
*/
|
|
101
105
|
export declare function treeFromArguments(paths: readonly string[]): Promise<Map<string, TreeEntry>>;
|
|
102
106
|
export declare const CASE_USAGE: string;
|
|
103
|
-
interface OnChainRow {
|
|
104
|
-
ref: string;
|
|
105
|
-
witnesses: number;
|
|
106
|
-
confirmed: number;
|
|
107
|
-
mismatched: number;
|
|
108
|
-
unreachable: number;
|
|
109
|
-
not_checked: number;
|
|
110
|
-
}
|
|
111
|
-
interface OnChainHalf {
|
|
112
|
-
/** Present only where a lookup ran: each gateway that answered, in the order given. */
|
|
113
|
-
answered?: string[];
|
|
114
|
-
checked: boolean;
|
|
115
|
-
gateways: string[];
|
|
116
|
-
packs: OnChainRow[];
|
|
117
|
-
verdict: "not_checked" | "confirmed" | "failed" | "undetermined";
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* The witness line (evidence-bundle 5.5.6 v1.20, A24, D7) from the on-chain
|
|
121
|
-
* half: `confirmed` is `confirmed`, `not_checked` and `undetermined` are
|
|
122
|
-
* `not_checked` with the reason, and `failed` is `differs`. It never moves the
|
|
123
|
-
* headline.
|
|
124
|
-
*/
|
|
125
|
-
export declare function witnessLine(onChain: OnChainHalf): WitnessLine;
|
|
126
107
|
/**
|
|
127
108
|
* The case verdict's code, moved only by an on-chain half that was asked for:
|
|
128
109
|
* a witness that does not match is a failure whatever the reading said, and an
|