@sanning/proof 0.19.0 → 0.20.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 (98) hide show
  1. package/README.md +30 -29
  2. package/dist/agent-record.d.ts +62 -0
  3. package/dist/agent-record.js +527 -0
  4. package/dist/agent-schemas.generated.d.ts +15 -0
  5. package/dist/agent-schemas.generated.js +5759 -0
  6. package/dist/case-file.d.ts +32 -89
  7. package/dist/case-file.js +41 -93
  8. package/dist/case.d.ts +343 -810
  9. package/dist/case.js +693 -1151
  10. package/dist/chain.d.ts +19 -47
  11. package/dist/chain.js +78 -205
  12. package/dist/claims.d.ts +211 -0
  13. package/dist/claims.js +1081 -0
  14. package/dist/cli.d.ts +45 -68
  15. package/dist/cli.js +618 -1022
  16. package/dist/compose.js +23 -65
  17. package/dist/crypto.d.ts +7 -8
  18. package/dist/crypto.js +60 -147
  19. package/dist/declaration.d.ts +68 -194
  20. package/dist/declaration.js +175 -518
  21. package/dist/environment.d.ts +9 -30
  22. package/dist/environment.js +17 -66
  23. package/dist/evidence.d.ts +119 -160
  24. package/dist/evidence.js +744 -1103
  25. package/dist/exit-codes.d.ts +11 -27
  26. package/dist/exit-codes.js +24 -59
  27. package/dist/file-match.d.ts +33 -79
  28. package/dist/file-match.js +33 -69
  29. package/dist/grading.d.ts +98 -135
  30. package/dist/grading.js +224 -380
  31. package/dist/handoff.d.ts +62 -160
  32. package/dist/handoff.js +153 -393
  33. package/dist/index.d.ts +14 -10
  34. package/dist/index.js +60 -167
  35. package/dist/json-schema.d.ts +29 -0
  36. package/dist/json-schema.js +298 -0
  37. package/dist/merkle.js +16 -28
  38. package/dist/message-parts.d.ts +56 -212
  39. package/dist/message-parts.js +142 -502
  40. package/dist/side-content.d.ts +11 -22
  41. package/dist/side-content.js +22 -65
  42. package/dist/types.d.ts +17 -0
  43. package/dist/types.js +19 -5
  44. package/dist/verifier.d.ts +40 -114
  45. package/dist/verifier.js +196 -535
  46. package/dist/witness-cache.d.ts +24 -45
  47. package/dist/witness-cache.js +38 -81
  48. package/dist/witness-lookup.d.ts +36 -57
  49. package/dist/witness-lookup.js +89 -139
  50. package/dist/zip-reader.d.ts +18 -33
  51. package/dist/zip-reader.js +19 -34
  52. package/package.json +11 -3
  53. package/src/agent-record.ts +608 -0
  54. package/src/agent-schemas.generated.ts +5764 -0
  55. package/src/case-file.ts +45 -109
  56. package/src/case.ts +848 -1322
  57. package/src/chain.ts +85 -224
  58. package/src/claims.ts +1253 -0
  59. package/src/cli.ts +714 -1176
  60. package/src/compose.ts +33 -79
  61. package/src/crypto.ts +60 -147
  62. package/src/declaration.ts +219 -656
  63. package/src/environment.ts +17 -65
  64. package/src/evidence.ts +958 -1381
  65. package/src/exit-codes.ts +24 -63
  66. package/src/file-match.ts +40 -89
  67. package/src/grading.ts +287 -421
  68. package/src/handoff.ts +175 -427
  69. package/src/index.ts +132 -177
  70. package/src/json-schema.ts +295 -0
  71. package/src/merkle.ts +16 -28
  72. package/src/message-parts.ts +155 -533
  73. package/src/side-content.ts +25 -68
  74. package/src/types.ts +55 -36
  75. package/src/verifier.ts +202 -526
  76. package/src/witness-cache.ts +45 -94
  77. package/src/witness-lookup.ts +107 -177
  78. package/src/zip-reader.ts +20 -38
  79. package/vectors/README.md +52 -34
  80. package/vectors/agent-record.json +2175 -0
  81. package/vectors/case-file-index.json +1 -1
  82. package/vectors/case-split.json +2 -2
  83. package/vectors/chain-reading.json +3 -3
  84. package/vectors/file-match.json +285 -148
  85. package/vectors/jcs-reader.json +6 -6
  86. package/vectors/pack-grading.json +102 -25
  87. package/vectors/sandbox-evidence.json +597 -580
  88. package/vectors/stamp-on-chain.json +18 -18
  89. package/vectors/witness-cache.json +49 -49
  90. package/dist/agent-proof.d.ts +0 -28
  91. package/dist/agent-proof.js +0 -226
  92. package/dist/run-adapters.d.ts +0 -10
  93. package/dist/run-adapters.js +0 -35
  94. package/dist/run.d.ts +0 -209
  95. package/dist/run.js +0 -1405
  96. package/src/agent-proof.ts +0 -264
  97. package/src/run-adapters.ts +0 -44
  98. package/src/run.ts +0 -1598
package/src/exit-codes.ts CHANGED
@@ -1,82 +1,43 @@
1
- // One exit-code table for every verb (0.13.0).
2
- //
3
- // Both commands, `npx @sanning/proof` and `sanning-proof`, map a verdict word to
4
- // an exit code through this one table, for `verify`, `case` and `export` alike,
5
- // and every verb's `--help` prints `EXIT_CODES_HELP` byte for byte. The twin is
6
- // `src/sanning_proof/exit_codes.py`; the specification is `evidence-export.md`
7
- // §5.
8
- //
9
- // Until 0.13.0 `verify` exited 0 on `partial` unless a gateway was unreachable,
10
- // while `case` exited 3 on it, so a CI gate reading `verify`'s exit code read a
11
- // stamp witnessed by nobody as verified. Every word that means "could not be
12
- // settled" now exits 3, which is the code's name: undetermined.
1
+ // One exit-code table for every verb (`verify`, `case`, `export`), shared by both
2
+ // commands. Every verb's `--help` prints `EXIT_CODES_HELP` byte for byte
3
+ // (evidence-export.md section 5).
13
4
 
14
5
  export const EXIT_VERIFIED = 0;
15
6
  export const EXIT_FAILED = 1;
16
- export const EXIT_MALFORMED = 2;
17
- export const EXIT_UNDETERMINED = 3;
18
- export const EXIT_MIXED = 4;
7
+ export const EXIT_INCOMPLETE = 2;
19
8
  /**
20
- * Sandbox evidence (envelope-spec §2): a run that would exit 0 over a pack or
21
- * case holding a record signed `environment: "dev"`, or a run over records
22
- * that are ALL signed `dev` whose only unsettled witnesses are simulated.
23
- * Not a verdict word: the verdict stays what the checks established
24
- * (`sandboxExit`). No flag turns 5 into 0.
9
+ * Not a verdict: the verdict is `verified` and a record is signed `environment: "dev"`
10
+ * (envelope-spec section 2). A failed or incomplete sandbox run keeps 1 or 2.
25
11
  */
26
- export const EXIT_SANDBOX = 5;
12
+ export const EXIT_SANDBOX = 3;
13
+ /** A bad flag, a missing file, or an input that is not a pack or a case at all. */
14
+ export const EXIT_COULD_NOT_RUN = 4;
27
15
 
28
- /**
29
- * The exit code of each of the eight verdict words (`evidence-bundle.md` §3).
30
- * `undetermined` is the name of 3, and never a word here.
31
- */
16
+ /** The exit code of each verdict (`evidence-bundle.md` section 3). */
32
17
  export const EXIT_CODES: Readonly<Record<string, number>> = Object.freeze({
33
18
  verified: EXIT_VERIFIED,
34
19
  failed: EXIT_FAILED,
35
- malformed: EXIT_MALFORMED,
36
- partial: EXIT_UNDETERMINED,
37
- pending: EXIT_UNDETERMINED,
38
- incomplete: EXIT_UNDETERMINED,
39
- not_found: EXIT_UNDETERMINED,
40
- mixed: EXIT_MIXED,
20
+ incomplete: EXIT_INCOMPLETE,
41
21
  });
42
22
 
43
- /** What every verb's help prints, identical in both commands. */
23
+ /** What every verb's help prints. */
44
24
  export const EXIT_CODES_HELP = [
45
25
  "Exit codes, one table for every verb:",
46
- " 0 verified, or the export was written",
47
- " 1 failed",
48
- " 2 usage or malformed",
49
- " 3 undetermined: partial, pending, incomplete or not_found",
50
- " 4 mixed: more than one outcome, none failed; read the exceptions",
51
- " 5 sandbox: a record is signed environment dev; would be 0, or only a simulated witness is unchecked",
26
+ " 0 verified: every claim in scope is proven; export: the export was written",
27
+ " 1 failed: a claim is disproven",
28
+ " 2 incomplete: nothing is disproven and a claim is not proven",
29
+ " 3 sandbox: verified, and a record is signed environment dev",
30
+ " 4 could not run: a bad flag, a missing file, or an input that is not a pack or case",
52
31
  ].join("\n");
53
32
 
54
33
  /**
55
- * The exit code of a verdict word. A word outside the eight is a verdict
56
- * nothing can render, so it exits 2, never 0.
34
+ * The exit code of a verdict. `verified` over a record signed `dev` is 3. A value outside
35
+ * the three verdicts is 4, never 0.
57
36
  */
58
- export function exitCodeFor(verdict: unknown): number {
59
- if (typeof verdict === "string" && Object.prototype.hasOwnProperty.call(EXIT_CODES, verdict)) {
60
- return EXIT_CODES[verdict]!;
37
+ export function exitCodeFor(verdict: unknown, environmentMarker: string | null = null): number {
38
+ if (typeof verdict !== "string" || !Object.prototype.hasOwnProperty.call(EXIT_CODES, verdict)) {
39
+ return EXIT_COULD_NOT_RUN;
61
40
  }
62
- return EXIT_MALFORMED;
63
- }
64
-
65
- /**
66
- * The code a verb exits with once the environment marker is read. 5 where the
67
- * run would otherwise exit 0 and the pack or case holds a record signed `dev`;
68
- * 5 where the run is undetermined only because a simulated witness could not
69
- * be looked up (`onlySimulatedUnsettled`, which the caller works out) and
70
- * EVERY record is signed `dev`, so a production or mixed pack carrying a
71
- * simulated witness keeps 3; the code unchanged everywhere else. A failure
72
- * stays 1, malformed 2 and mixed 4.
73
- */
74
- export function sandboxExit(
75
- code: number,
76
- environmentMarker: string | null,
77
- onlySimulatedUnsettled = false,
78
- ): number {
79
- if (code === EXIT_VERIFIED && environmentMarker !== null) return EXIT_SANDBOX;
80
- if (code === EXIT_UNDETERMINED && environmentMarker === "dev" && onlySimulatedUnsettled) return EXIT_SANDBOX;
81
- return code;
41
+ if (verdict === "verified" && environmentMarker !== null) return EXIT_SANDBOX;
42
+ return EXIT_CODES[verdict]!;
82
43
  }
package/src/file-match.ts CHANGED
@@ -1,45 +1,20 @@
1
1
  /**
2
- * Files handed in beside a pack, matched against what its records signed (A29).
2
+ * Matches files handed in beside a pack against the commitments its signed
3
+ * records make (evidence-bundle.md §5.7). Conformance: `vectors/file-match.json`.
3
4
  *
4
- * The TypeScript half of `src/sanning_proof/file_match.py`. Both kernels are
5
- * held to one table, `vectors/file-match.json`, so neither can pass by agreeing
6
- * with the other, and `cross-kernel/run_file_match.sh` holds the two readings,
7
- * and the two commands' `--files` output, to the same bytes.
5
+ * - Matching is by SHA-256 of the exact bytes only. A file's name is a label
6
+ * and is never compared with an `event_id`, a part name or a path.
7
+ * - Unmatched is reported, never guessed.
8
+ * - Only what a record's own key signed counts: a verified envelope whose
9
+ * `event_bytes` bind to its `payload_hash`, or a hand-off message whose bytes
10
+ * bind to that record's `message_hash`. No wrapper field is read.
11
+ * - The verdict does not move (`FILE_MATCH_SCOPE`).
8
12
  *
9
- * WHAT THIS ANSWERS (evidence-bundle.md 5.7 v1.24, proof#92, decided by Phil
10
- * 2026-09-23 as option 2). A reader holds a pack and some files beside it and
11
- * asks whether the pack vouches for them. Each offered file is hashed, SHA-256
12
- * over its exact bytes, and the digest is looked for among the commitments the
13
- * pack's SIGNED records make. The reading says which offered files a record
14
- * commits to (naming the record, its signer and its `signed_at`), which match
15
- * no record, and which commitments had no file offered.
16
- *
17
- * THE RULES THAT CARRY THE WEIGHT:
18
- *
19
- * - By hash, and by nothing else. A file's name is carried through as the
20
- * caller's label and is never compared with an `event_id`, a part's name or
21
- * a `logs/` path. There is no layout convention.
22
- * - Unmatched is reported, never guessed.
23
- * - Only what a record's own key signed: a verified envelope whose
24
- * `event_bytes` bind to its `payload_hash`, or a hand-off message whose
25
- * bytes bind to that record's `message_hash`. No wrapper field is read.
26
- * - The verdict does not move. A match proves that a signed record in the
27
- * pack committed to those exact bytes, and nothing more (`FILE_MATCH_SCOPE`).
28
- *
29
- * WHAT COUNTS AS A COMMITMENT, in record order and then part order:
30
- *
31
- * 1. `record`: a record outside envelope-spec.md 4.1 commits through
32
- * `event.content_hash`, 64 hex characters in either case.
33
- * 2. `file_part`: a record 4.1 governs commits to its MESSAGE through
34
- * `event.message_hash` (5.1, A20), never through a `content_hash` it also
35
- * writes. Where the message's JCS bytes are in hand (in the body, or in
36
- * the side content) and bind to it, each file part of that message (A18)
37
- * commits by its `content_hash`.
38
- *
39
- * Side content is read for ONE thing, to find a hand-off message that binds to
40
- * a signed `message_hash`. It is never an offered file.
41
- *
42
- * Pure: a bundle, the files and the optional side content in, a reading out.
13
+ * A record outside envelope-spec.md §4.1 commits through `event.content_hash`.
14
+ * A hand-off record commits to its message through `event.message_hash`, and,
15
+ * where the message bytes are in hand and bind, each file part of that message
16
+ * commits by its `content_hash`. Side content is read only to find such a
17
+ * message; it is never an offered file.
43
18
  */
44
19
 
45
20
  import { hexToBytes, isHexField, sha256Hex } from "./crypto.js";
@@ -52,7 +27,7 @@ import type { SideContentMap } from "./side-content.js";
52
27
  import type { Envelope } from "./types.js";
53
28
  import { jcsParse, verifyEnvelope } from "./verifier.js";
54
29
 
55
- /** The one sentence every surface prints with the reading (5.7). */
30
+ /** The scope sentence every surface prints with the reading (§5.7). */
56
31
  export const FILE_MATCH_SCOPE =
57
32
  "A match proves that a signed record in the pack committed to these exact " +
58
33
  "bytes, and nothing more.";
@@ -71,9 +46,8 @@ export const NOT_READ_REASONS = [
71
46
  export type NotReadReason = (typeof NOT_READ_REASONS)[number];
72
47
 
73
48
  /**
74
- * Why a file was named in the report and never hashed (5.7.5). A skipped file
75
- * is listed, never dropped: every file a command found is matched, unmatched or
76
- * skipped, so "no match" is never read as "no file was there".
49
+ * Why a found file was not hashed (§5.7.5). A skipped file is listed, never
50
+ * dropped, so "no match" is never read as "no file was there".
77
51
  */
78
52
  export const SKIPPED_REASONS = [
79
53
  "symbolic_link",
@@ -90,7 +64,6 @@ const SKIPPED_TEXT: Record<SkippedReason, string> = {
90
64
  not_regular: "not a regular file: not read",
91
65
  unreadable: "could not be read",
92
66
  too_large: "past the 1 GiB read from folders in one run: not read",
93
- // Followed by the reader's own words, `renderSkipped`.
94
67
  refused: "a .zip entry the reader refused",
95
68
  };
96
69
 
@@ -105,9 +78,8 @@ const REASON_TEXT: Record<NotReadReason, string> = {
105
78
  message_withheld: "its message is not in hand, so its file parts cannot be read",
106
79
  };
107
80
 
108
- // The accept-set minus the quarantined `sanning.mlflow/v1` (DL-63), the set
109
- // both kernels read a case under.
110
- const RECORD_PROFILES = ["sanning.agent/v1", "sanning.events/v2"] as const;
81
+ // The accepted profiles minus the quarantined `sanning.mlflow/v1`.
82
+ const RECORD_PROFILES = ["sanning.events/v2"] as const;
111
83
 
112
84
  /** One commitment a signed record makes to a file's bytes. */
113
85
  export interface FileCommitment {
@@ -118,13 +90,9 @@ export interface FileCommitment {
118
90
  contentHash: string;
119
91
  signer: string;
120
92
  signedAt: string | null;
121
- /**
122
- * The pack, by its position in the packs `matchCaseFiles` was handed (the
123
- * same position the case reading's `packIndex` names), or `null` from
124
- * `matchFiles`, which reads one pack (A43).
125
- */
93
+ /** Position in the packs `matchCaseFiles` was handed, or `null` from `matchFiles`. */
126
94
  packIndex: number | null;
127
- /** The record, by its position in the events the pack presents (A43). */
95
+ /** Position in the events the pack presents. */
128
96
  recordIndex: number;
129
97
  }
130
98
 
@@ -143,7 +111,7 @@ export interface UnmatchedFile {
143
111
  export interface SkippedFile {
144
112
  name: string;
145
113
  reason: SkippedReason;
146
- /** The reader's own words, for a `.zip` entry it refused (`refused`). */
114
+ /** The reader's own words, for a refused `.zip` entry. */
147
115
  detail?: string;
148
116
  }
149
117
 
@@ -151,12 +119,8 @@ export interface NotRead {
151
119
  packRef: string | null;
152
120
  eventId: string | null;
153
121
  reason: NotReadReason;
154
- /** As on `FileCommitment` (A43). */
155
122
  packIndex: number | null;
156
- /**
157
- * As on `FileCommitment`, or `null` where the pack presents no events at all
158
- * (`no_records`).
159
- */
123
+ /** `null` where the pack presents no events (`no_records`). */
160
124
  recordIndex: number | null;
161
125
  }
162
126
 
@@ -167,18 +131,13 @@ export interface FileMatchReading {
167
131
  notOffered: FileCommitment[];
168
132
  notRead: NotRead[];
169
133
  scope: string;
170
- /**
171
- * Files found and not hashed. The matching functions leave it empty: it is
172
- * the caller's, because only a caller that reads a folder knows a link.
173
- */
134
+ /** Files found and not hashed. Filled by the caller, which alone reads folders. */
174
135
  skipped?: SkippedFile[];
175
136
  }
176
137
 
177
138
  /**
178
- * A file handed in beside a pack. `name` is a label and is never compared. The
179
- * file is its `bytes`, or its `sha256` (64 lowercase hex characters) where the
180
- * caller has already hashed it: a command streams a file of any size rather
181
- * than hold it.
139
+ * A file handed in beside a pack. `name` is a label, never compared. Give
140
+ * `bytes`, or `sha256` (64 lowercase hex) when the caller streamed the file.
182
141
  */
183
142
  export type OfferedFile =
184
143
  | { name: string; bytes: Uint8Array; sha256?: undefined }
@@ -203,8 +162,8 @@ function own(o: Record<string, unknown>, key: string): unknown {
203
162
  return Object.prototype.hasOwnProperty.call(o, key) ? o[key] : undefined;
204
163
  }
205
164
 
206
- // The envelope's key and signature in the hex charset at their lengths, the
207
- // reading every body verifier applies inside a bundle (proof#142).
165
+ // The key and signature must be hex at their lengths, as every body verifier
166
+ // requires inside a bundle.
208
167
  function envelopeSpelled(envelope: Record<string, unknown>): boolean {
209
168
  for (const [name, nibbles] of [
210
169
  ["public_key", 64],
@@ -369,8 +328,8 @@ async function readPack(
369
328
  return { commitments, notRead };
370
329
  }
371
330
 
372
- // A malformed file entry is the CALLER's mistake, not the evidence's, so it
373
- // throws rather than reading as a file that matched nothing.
331
+ // A malformed entry is the caller's mistake, not the evidence's, so it throws
332
+ // rather than reading as an unmatched file.
374
333
  async function offeredDigests(files: readonly OfferedFile[]): Promise<[string, string][]> {
375
334
  const out: [string, string][] = [];
376
335
  for (const entry of files) {
@@ -427,10 +386,8 @@ async function match(
427
386
  }
428
387
 
429
388
  /**
430
- * Match each offered file against the commitments one pack's signed records
431
- * make. `content` is the side content handed in beside the pack, read only to
432
- * find a hand-off message that binds to its record's `message_hash`; it is
433
- * never an offered file.
389
+ * Matches each offered file against one pack's signed commitments. `content`
390
+ * is the pack's side content, read only to bind hand-off messages.
434
391
  */
435
392
  export async function matchFiles(
436
393
  bundle: unknown,
@@ -441,7 +398,7 @@ export async function matchFiles(
441
398
  return match(commitments, notRead, files);
442
399
  }
443
400
 
444
- /** The same reading over every pack of a case, each commitment naming its pack's `ref`. */
401
+ /** `matchFiles` over every pack of a case. */
445
402
  export async function matchCaseFiles(
446
403
  packs: readonly FileMatchPack[],
447
404
  files: readonly OfferedFile[],
@@ -472,7 +429,7 @@ function commitmentJson(c: FileCommitment): Record<string, unknown> {
472
429
  };
473
430
  }
474
431
 
475
- /** The reading as the `files` member of `--json`, identical in both kernels. */
432
+ /** The reading as the `files` member of `--json`. */
476
433
  export function fileMatchToJson(reading: FileMatchReading): Record<string, unknown> {
477
434
  const skipped = reading.skipped ?? [];
478
435
  return {
@@ -492,8 +449,7 @@ export function fileMatchToJson(reading: FileMatchReading): Record<string, unkno
492
449
  reason: n.reason,
493
450
  rendered: renderNotRead(n),
494
451
  })),
495
- // Present only when something was skipped, so a reading with no skipped
496
- // file is the object it was before a folder could be offered (A40).
452
+ // Omitted when empty, so a reading with nothing skipped keeps its shape.
497
453
  ...(skipped.length > 0
498
454
  ? {
499
455
  skipped: skipped.map((k) => ({
@@ -535,11 +491,9 @@ function renderNotRead(n: NotRead): string {
535
491
 
536
492
  /**
537
493
  * The commitments no offered file matched and no disclosed log answers.
538
- *
539
- * `contents` is each pack's side content by `pack_ref`. A record whose own pack
540
- * discloses bytes that hash to its `content_hash` is accounted for by the pack,
541
- * and is not returned. Only the text report's count reads this; the reading, and
542
- * `--json`, still list every commitment no file matched.
494
+ * A record whose own pack discloses bytes hashing to its `content_hash` is
495
+ * accounted for. Only the text report's count reads this; `--json` still lists
496
+ * every unmatched commitment.
543
497
  */
544
498
  export async function notAccountedFor(
545
499
  reading: FileMatchReading,
@@ -559,11 +513,8 @@ export async function notAccountedFor(
559
513
  }
560
514
 
561
515
  /**
562
- * The text report's lines for the reading, identical in both kernels.
563
- *
564
- * By default every commitment no file matched is a line. `notAmong` is the case
565
- * command's report when no `--files` was given: one count line for the recorded
566
- * files no loose file matched, and no line for each (A41).
516
+ * The text report's lines for the reading. With `notAmong` (the case command
517
+ * with no `--files`), unmatched commitments collapse to one count line.
567
518
  */
568
519
  export function renderFileMatch(reading: FileMatchReading, notAmong?: number): string[] {
569
520
  const skipped = reading.skipped ?? [];