@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 CHANGED
@@ -163,6 +163,28 @@ Every event also appears in `result.events`, in body order, with a status that n
163
163
 
164
164
  Bytes come back as `Uint8Array`, never a string: the spec commits to **raw bytes**, and a UTF-8 decode is lossy for any payload that is not valid UTF-8, re-encoding such a string would change the bytes and break the hash. Decode to text yourself if you know your payload is text.
165
165
 
166
+ ## Check a case: several packs, one verdict
167
+
168
+ When a case spans more than one pack (two agents, two organizations, one hand-off between them), check the packs together. This is `readCase` from a terminal, with no page in the way:
169
+
170
+ ```bash
171
+ npx @sanning/proof case assessor-pack/ challenger-pack/
172
+ unzip decision-file.zip -d case/ && npx @sanning/proof case case/ # a decision file
173
+ npx @sanning/proof case case/ --json # the full reading, for a script
174
+ ```
175
+
176
+ Each argument is a folder: one pack (`bundle.json` plus `logs/`), or a folder that holds several packs, such as an unpacked decision file. `case` does not open archives: given a `.zip`, it exits `2` and says to unzip it first and pass the folder. Packs are found by the verify page's splitting rule, so the page and this command apply the same rule to the same unpacked tree: every `bundle.json` is a pack, a file belongs to the deepest pack whose `bundle.json` or `logs/` it sits in, and every other file is listed as outside every pack and never opened. A `.zip` inside the folder is a file like any other. Several folders are read as one folder holding them, each under its own name. Each `bundle.json` is read with `verify`'s own reader (`readBundleBytes`), so the two commands classify one bundle the same way.
177
+
178
+ The walk is bounded and follows no link. A symbolic link is never followed: it is listed as outside every pack and not read, even under a pack's `logs/`, and a link named on the command line exits `2`. A folder nested more than 32 levels deep, more than 50,000 files and folders in all, or a name that is not valid UTF-8 exits `2`; a bad name is refused rather than escaped, because two different names would then read as one. A `bundle.json` nested more than 512 levels deep exits `2`, from `case` and from `verify` alike. The rules are pinned for both kernels by `vectors/case-split.json`, shipped in this tarball, as directory trees.
179
+
180
+ Known differences from the verify page: the page opens a dropped `.zip`; the page drops a leading byte-order mark from `bundle.json`, which `verify`'s reader refuses; the page reads whatever the browser hands it for a link, a bad name or a very deep tree; and the page loses a record named `__proto__`, which this command keeps.
181
+
182
+ The first line is the case verdict, then the kernel's summary and every exception through `renderException`, then the packs, the files outside them, and the on-chain half. Exit codes: `0` verified · `1` failed · `2` malformed, usage, an input that cannot be read as a case, or an unexpected error in the command · `3` incomplete or partial · `4` mixed (more than one outcome: read the exceptions). `verify` never exits `4`.
183
+
184
+ A case reading establishes that every pack verified offline, that every record every pack supplied was read in one pile under envelope-spec §4.1's hand-off rules, and that a hand-off record with no counterpart is always named. It does **not** establish that the case is complete, that a party which presented no pack under the named key recorded nothing, that two organizations are independent, or that any witness was re-fetched on chain: the output says `on-chain NOT CHECKED` unless you pass `--gateways <url,url>`. With gateways, each pack's witnesses are re-fetched pack by pack and reported beside the reading, which they never change; a witness that does not match exits `1`, and an on-chain half that could not be settled turns `0` into `3`.
185
+
186
+ The Python kernel ships the same command as `sanning-proof case`, and `cross-kernel/run_case_cli.sh` requires the two to print the same bytes, the same `--json` and the same exit code over every `case/` corpus vector and every tree in `vectors/case-split.json`.
187
+
166
188
  ## Verify a Merkle inclusion proof
167
189
 
168
190
  Agents close daily checkpoints over per-cycle leaves (RFC 9162, §2.1 domain separation, not the Bitcoin duplicate-leaf variant). Verify a leaf's inclusion against an anchored root:
package/dist/case.d.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
- * for an EMPTY candidate list. Which of the three counterpart kinds it
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", INHERITED HERE AND STATED RATHER
229
- * THAN LEFT TO BE FOUND (proof#134, ratified by Phil 2026-09-22). A reader
230
- * whose pile holds two or more records of the complementary type signed by one
231
- * counterparty key cannot reach `verified`: the other exchanges' records are
232
- * candidates too, they commit to other messages, and they fail. One agent
233
- * sending two messages to the same counterparty inside one case lands there,
234
- * and it surfaces as `COUNTERPART_AMBIGUOUS` beside the failure. An
235
- * alternating exchange (A sends, B replies) does NOT, because a reply is
236
- * signed by the other key and is the other type. Narrowing that here with
237
- * `handoffs[]` is precisely the move that would reopen proof#137's door.
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
  import type { SideContentMap } from "./side-content.js";
240
241
  /**
@@ -281,7 +282,9 @@ export declare const CASE_QUARANTINED_PROFILES: readonly ["sanning.mlflow/v1"];
281
282
  /**
282
283
  * A conformant hand-off record with no counterpart among the records this
283
284
  * reader read, where every pack in the case verified under a key this reader
284
- * can read and NO pack in the case is signed by the key the record names.
285
+ * can read and NO pack in the case is signed by the key the record names,
286
+ * or where every candidate it had was accounted for by another record under
287
+ * A20 (the exception then carries `count`, and names no pack).
285
288
  * **An exception, never a failure** (anchor#198, criterion 3).
286
289
  *
287
290
  * That is ALL it says. It does not say the counterparty's account is absent,
@@ -367,9 +370,11 @@ export declare const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS = "counterpart
367
370
  */
368
371
  export declare const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
369
372
  /**
370
- * More than one candidate counterpart (4.1, and proof#134's stated cost). The
371
- * reading is the worst of them and the ambiguity is named: a reader permitted
372
- * to take one match can be handed an honest decoy beside a tampered record.
373
+ * More than one candidate counterpart that nothing accounts for (4.1 v1.17,
374
+ * A20). The reading is the worst of them and the ambiguity is named: a reader
375
+ * permitted to take one match can be handed an honest decoy beside a tampered
376
+ * record. `count` is how many of the candidates are live, which is the number
377
+ * the ambiguity is about.
373
378
  */
374
379
  export declare const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
375
380
  /**
@@ -636,7 +641,11 @@ export interface CaseException {
636
641
  counterpartyKey?: string | null;
637
642
  /** Which checks a formed pair failed, in 4.1's fixed order. */
638
643
  failedChecks?: string[];
639
- /** How many of a thing the exception is about, where it is about a count. */
644
+ /**
645
+ * How many of a thing the exception is about, where it is about a count. On
646
+ * `COUNTERPART_ABSENT` it is set only where 4.1 set every candidate aside
647
+ * (v1.17, A20), and it is how many were set aside.
648
+ */
640
649
  count?: number;
641
650
  /** The pack's own bundle verdict, for `PACK_BUNDLE_NOT_VERIFIED`. */
642
651
  status?: CaseVerdict;
package/dist/case.js 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
- * for an EMPTY candidate list. Which of the three counterpart kinds it
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", INHERITED HERE AND STATED RATHER
229
- * THAN LEFT TO BE FOUND (proof#134, ratified by Phil 2026-09-22). A reader
230
- * whose pile holds two or more records of the complementary type signed by one
231
- * counterparty key cannot reach `verified`: the other exchanges' records are
232
- * candidates too, they commit to other messages, and they fail. One agent
233
- * sending two messages to the same counterparty inside one case lands there,
234
- * and it surfaces as `COUNTERPART_AMBIGUOUS` beside the failure. An
235
- * alternating exchange (A sends, B replies) does NOT, because a reply is
236
- * signed by the other key and is the other type. Narrowing that here with
237
- * `handoffs[]` is precisely the move that would reopen proof#137's door.
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
  import { checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
240
241
  import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
@@ -309,7 +310,9 @@ export const CASE_QUARANTINED_PROFILES = ["sanning.mlflow/v1"];
309
310
  /**
310
311
  * A conformant hand-off record with no counterpart among the records this
311
312
  * reader read, where every pack in the case verified under a key this reader
312
- * can read and NO pack in the case is signed by the key the record names.
313
+ * can read and NO pack in the case is signed by the key the record names,
314
+ * or where every candidate it had was accounted for by another record under
315
+ * A20 (the exception then carries `count`, and names no pack).
313
316
  * **An exception, never a failure** (anchor#198, criterion 3).
314
317
  *
315
318
  * That is ALL it says. It does not say the counterparty's account is absent,
@@ -395,9 +398,11 @@ export const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS = "counterpart_absent_
395
398
  */
396
399
  export const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
397
400
  /**
398
- * More than one candidate counterpart (4.1, and proof#134's stated cost). The
399
- * reading is the worst of them and the ambiguity is named: a reader permitted
400
- * to take one match can be handed an honest decoy beside a tampered record.
401
+ * More than one candidate counterpart that nothing accounts for (4.1 v1.17,
402
+ * A20). The reading is the worst of them and the ambiguity is named: a reader
403
+ * permitted to take one match can be handed an honest decoy beside a tampered
404
+ * record. `count` is how many of the candidates are live, which is the number
405
+ * the ambiguity is about.
401
406
  */
402
407
  export const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
403
408
  /**
@@ -1135,19 +1140,36 @@ export async function readCase(packs) {
1135
1140
  // that party's pack. Where some pack could not be tied to a key, "no
1136
1141
  // pack is signed by the named key" is not established and the reading
1137
1142
  // says it could not tell.
1138
- const kind = named !== null && holderKeys.has(named)
1139
- ? COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS
1140
- : unattributed > 0
1141
- ? COUNTERPART_ABSENT_SIGNER_UNKNOWN
1142
- : COUNTERPART_ABSENT;
1143
- note(kind, { packRef, eventId, counterpartyKey: named });
1143
+ //
1144
+ // EXCEPT where 4.1 set every candidate aside (v1.17, A20). There the
1145
+ // absence is a finding about the candidates the reader DID read, each
1146
+ // paired `verified` with another record the subject's own signer made,
1147
+ // and not a finding about any pack. 4.1 and evidence-bundle A16 name the
1148
+ // neutral kind for it, and the damning one would put the audited party's
1149
+ // own unfaithful record on the counterparty's pack as a statement
1150
+ // against it. `count` is how many candidates were set aside, which is
1151
+ // also what tells the rendered sentence which absence this is.
1152
+ const graded = reading.candidateReadings;
1153
+ const setAside = graded.length > 0 && graded.every((c) => c.accountedFor !== null)
1154
+ ? graded.length
1155
+ : 0;
1156
+ const kind = setAside > 0
1157
+ ? COUNTERPART_ABSENT
1158
+ : named !== null && holderKeys.has(named)
1159
+ ? COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS
1160
+ : unattributed > 0
1161
+ ? COUNTERPART_ABSENT_SIGNER_UNKNOWN
1162
+ : COUNTERPART_ABSENT;
1163
+ note(kind, setAside > 0
1164
+ ? { packRef, eventId, counterpartyKey: named, count: setAside }
1165
+ : { packRef, eventId, counterpartyKey: named });
1144
1166
  continue;
1145
1167
  }
1146
1168
  if (reading.findings.includes(COUNTERPART_AMBIGUOUS)) {
1147
1169
  note(COUNTERPART_AMBIGUOUS, {
1148
1170
  packRef,
1149
1171
  eventId,
1150
- count: reading.candidates.length,
1172
+ count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
1151
1173
  });
1152
1174
  }
1153
1175
  // ON `failedChecks` AND NOT ON THE VERDICT WORD. 4.1 keeps the UNION of
@@ -1192,6 +1214,14 @@ export function renderException(exception) {
1192
1214
  const where = `${exception.packRef}/${exception.eventId}`;
1193
1215
  switch (exception.kind) {
1194
1216
  case COUNTERPART_ABSENT:
1217
+ if (exception.count) {
1218
+ const which = exception.count === 1
1219
+ ? "its one candidate counterpart pairs"
1220
+ : `each of its ${exception.count} candidate counterparts pairs`;
1221
+ return (`${where} has no counterpart among the records this verifier read: ` +
1222
+ `${which} verified with another record signed by the key that ` +
1223
+ "signed this one, at a different sequence, and is set aside");
1224
+ }
1195
1225
  return (`${where} has no counterpart among the records this verifier read, ` +
1196
1226
  "and no pack in this case is signed by the key it names");
1197
1227
  case COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS:
@@ -1276,7 +1306,7 @@ export function renderException(exception) {
1276
1306
  because(exception.reasons));
1277
1307
  case RECORD_PROFILE_QUARANTINED:
1278
1308
  return (`${where} is signed under a profile this case reader does not read ` +
1279
- "while its admission is undecided (DL-63), so what it says is UNKNOWN");
1309
+ "while its admission is undecided, so what it says is UNKNOWN");
1280
1310
  case CASE_HOLDS_NO_PACKS:
1281
1311
  return "this case holds no packs";
1282
1312
  case CASE_HOLDS_NO_HANDOFF_RECORDS:
package/dist/cli.d.ts CHANGED
@@ -5,4 +5,102 @@ interface Cli {
5
5
  fetchImpl?: typeof fetch;
6
6
  }
7
7
  export declare function runCli(argv: string[], io: Cli): Promise<number>;
8
+ /** The error text for an input with no pack in it, word for word the page's. */
9
+ export declare const NO_BUNDLE = "No bundle.json found. An evidence pack contains bundle.json plus logs/.";
10
+ /** A directory nested deeper than this below an argument refuses the input. */
11
+ export declare const MAX_WALK_DEPTH = 32;
12
+ /** More names than this, across every argument, refuse the input. */
13
+ export declare const MAX_WALK_ENTRIES = 50000;
14
+ /**
15
+ * `bundle.json` nested deeper than this is refused before it is parsed, by
16
+ * `verify` and by `case` alike (see `readBundleBytes`).
17
+ */
18
+ export declare const MAX_JSON_DEPTH = 512;
19
+ /**
20
+ * The input could not be read as a case. The message is the whole report,
21
+ * identical in both kernels, and the command exits 2 on it. `hint` is an
22
+ * optional second line naming a way through.
23
+ */
24
+ export declare class CaseInputError extends Error {
25
+ readonly hint: string | null;
26
+ constructor(message: string, hint?: string | null);
27
+ }
28
+ /**
29
+ * `bundle.json` bytes that `readBundleBytes` will not read. `tooDeep` separates
30
+ * the one refusal that is not a parse error; `detail` is the parser's own words,
31
+ * which differ by kernel and are printed by `verify` only.
32
+ */
33
+ export declare class BundleReadError extends Error {
34
+ readonly tooDeep: boolean;
35
+ readonly detail: string;
36
+ constructor(tooDeep: boolean, detail: string);
37
+ }
38
+ /**
39
+ * How both `verify` and `case` turn `bundle.json` bytes into JSON, in both
40
+ * kernels (`read_bundle_bytes` in the Python twin). UTF-8 with each ill-formed
41
+ * sequence read as U+FFFD, and a byte-order mark kept; then nesting deeper than
42
+ * MAX_JSON_DEPTH is refused before any parser walks into it; then `JSON.parse`.
43
+ * Throws `BundleReadError`.
44
+ */
45
+ export declare function readBundleBytes(bytes: Uint8Array): unknown;
46
+ export interface SplitPack {
47
+ ref: string;
48
+ bundlePath: string;
49
+ bundleBytes: Uint8Array;
50
+ /**
51
+ * `logs/<event_id>.json` bytes by event id. A `Map` and not an object: an
52
+ * event id is written by the audited party, and `obj["__proto__"] = bytes`
53
+ * replaces the prototype instead of storing the disclosure.
54
+ */
55
+ content: Map<string, Uint8Array>;
56
+ unaccounted: string[];
57
+ }
58
+ export interface SplitCase {
59
+ packs: SplitPack[];
60
+ /** Paths that belong to no pack, sorted. Never opened. */
61
+ outside: string[];
62
+ /** The part of `outside` that is not a regular file. Never followed, never opened. */
63
+ notRegular: string[];
64
+ }
65
+ /** One name the walk found that is not a directory. */
66
+ export interface TreeEntry {
67
+ fsPath: string;
68
+ /** `true` for a regular file; `false` for a link or anything else. */
69
+ regular: boolean;
70
+ }
71
+ /**
72
+ * The page's `splitCase` followed by its `assemble` per pack, over the walk's
73
+ * tree. `read` opens a file; it is called for the files a pack owns and for
74
+ * nothing else.
75
+ */
76
+ export declare function splitCase(tree: Map<string, TreeEntry>, read: (entry: TreeEntry, key: string) => Promise<Uint8Array>): Promise<SplitCase>;
77
+ /** `verify`'s reader, with the refusal named by the pack's path. */
78
+ export declare function loadBundle(pack: SplitPack): unknown;
79
+ /**
80
+ * A regular file's bytes. Opened without following a link and without
81
+ * blocking, then checked again, so a file swapped for a link or a pipe after
82
+ * the walk listed it is refused rather than followed or waited on.
83
+ */
84
+ export declare function readFileEntry(entry: TreeEntry, key: string): Promise<Uint8Array>;
85
+ /**
86
+ * Every name under the directories named, keyed `<name>/<path inside>`. Each
87
+ * argument must be a directory, and not a link to one. A `.zip` is refused
88
+ * with the way through.
89
+ */
90
+ export declare function treeFromArguments(paths: readonly string[]): Promise<Map<string, TreeEntry>>;
91
+ export declare const CASE_EXIT_CODES: Readonly<Record<string, number>>;
92
+ export declare const CASE_USAGE = "sanning-proof case -- one verdict over several evidence packs, offline\n\n sanning-proof case <folder>... [--gateways <url,url>] [--json]\n\n <folder> a pack folder (bundle.json plus logs/), or a folder that holds\n several packs, such as an unpacked decision file. case reads\n folders only: given a .zip, it exits 2 and says to unzip it\n first (for example, unzip file.zip -d case/) and pass the folder.\n Packs are found by the verify page's splitting rule: every\n bundle.json is a pack, a file belongs to the deepest pack whose\n bundle.json or logs/ it sits in, and every other file is listed\n as outside every pack and never opened. A .zip inside a folder is\n a file like any other. A symbolic link is never followed: it is\n listed as outside and not read, even under a pack's logs/.\n Several folders are read as one folder holding them, each under\n its own name. A folder nested more than 32 levels deep, more than\n 50000 files and folders in all, or a name that is not valid UTF-8\n exits 2.\n --gateways comma-separated gateway URLs. Each pack's witnesses are then\n re-fetched on chain, pack by pack, and reported beside the case\n reading. Without it the on-chain half is NOT checked, and the\n output says so.\n --json the full reading as JSON: the verdict, every exception with its\n rendered line, every record, every contribution, the packs, the\n files outside them, and the on-chain half.\n\nEach pack's bundle.json is read as verify reads it, so the two commands\nclassify one bundle the same way.\n\nWhat a case reading establishes: every pack is verified offline by this\nkernel; every record every pack supplied is read, in one pile, under the\nhand-off rules of envelope-spec section 4.1; a hand-off record with no\ncounterpart among the case's records is always named, and when a pack signed by\nthe key it names is in the case, the reading says that pack holds none, unless\nevery candidate it had was accounted for by another record (A20), which reads\nas no counterpart and names no pack.\n\nWhat it does not establish: that the case is complete (a party can decline to\nhand over a pack); that a party which presented no pack under the named key\nrecorded nothing; that two organizations are independent of each other; or\nthat any witness was re-fetched on chain, which the reading never does.\n\nExit codes: 0 verified \u00B7 1 failed \u00B7 2 malformed, usage, an input that cannot\nbe read as a case, or an unexpected error in this command \u00B7 3 incomplete or\npartial (undetermined, not failed) \u00B7 4 mixed (more than one outcome: read the\nexceptions). With --gateways, a witness that does not match on chain exits 1,\nand an on-chain half that could not be settled turns 0 into 3.";
93
+ /**
94
+ * The case verdict's code, moved only by an on-chain half that was asked for:
95
+ * a witness that does not match is a failure whatever the reading said, and an
96
+ * on-chain half that could not be settled is never a clean 0.
97
+ */
98
+ export declare function caseExitCode(verdict: string, onChainVerdict: string): number;
99
+ /**
100
+ * `proof case <folder>... [--gateways <urls>] [--json]`. Every path out of the
101
+ * command is an exit code from the table: an exception nobody expected exits 2,
102
+ * never 1, because 1 would name the evidence failed over a defect in this
103
+ * command.
104
+ */
105
+ export declare function runCase(argv: readonly string[], io: Cli): Promise<number>;
8
106
  export {};