@sanning/proof 0.12.0 → 0.14.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 (59) 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 +353 -0
  6. package/dist/case.d.ts +217 -26
  7. package/dist/case.js +535 -124
  8. package/dist/cli.d.ts +64 -8
  9. package/dist/cli.js +799 -219
  10. package/dist/compose.js +6 -1
  11. package/dist/crypto.d.ts +1 -1
  12. package/dist/crypto.js +5 -1
  13. package/dist/declaration.d.ts +58 -52
  14. package/dist/declaration.js +256 -33
  15. package/dist/evidence.d.ts +38 -0
  16. package/dist/evidence.js +238 -22
  17. package/dist/exit-codes.d.ts +17 -0
  18. package/dist/exit-codes.js +50 -0
  19. package/dist/file-match.d.ts +105 -0
  20. package/dist/file-match.js +374 -0
  21. package/dist/grading.d.ts +28 -13
  22. package/dist/grading.js +90 -31
  23. package/dist/index.d.ts +14 -7
  24. package/dist/index.js +20 -6
  25. package/dist/message-parts.d.ts +18 -10
  26. package/dist/message-parts.js +25 -15
  27. package/dist/types.d.ts +1 -0
  28. package/dist/verifier.d.ts +63 -4
  29. package/dist/verifier.js +223 -15
  30. package/dist/witness-cache.d.ts +85 -0
  31. package/dist/witness-cache.js +264 -0
  32. package/dist/zip-reader.d.ts +50 -0
  33. package/dist/zip-reader.js +220 -0
  34. package/package.json +2 -2
  35. package/src/agent-proof.ts +4 -4
  36. package/src/case-file.ts +422 -0
  37. package/src/case.ts +671 -131
  38. package/src/cli.ts +932 -217
  39. package/src/compose.ts +8 -1
  40. package/src/crypto.ts +5 -1
  41. package/src/declaration.ts +248 -52
  42. package/src/evidence.ts +257 -23
  43. package/src/exit-codes.ts +54 -0
  44. package/src/file-match.ts +461 -0
  45. package/src/grading.ts +109 -52
  46. package/src/index.ts +101 -2
  47. package/src/message-parts.ts +25 -15
  48. package/src/types.ts +5 -0
  49. package/src/verifier.ts +225 -16
  50. package/src/witness-cache.ts +306 -0
  51. package/src/zip-reader.ts +229 -0
  52. package/vectors/README.md +66 -8
  53. package/vectors/case-file-index.json +668 -0
  54. package/vectors/case-split.json +218 -9
  55. package/vectors/file-match.json +1808 -0
  56. package/vectors/jcs-reader.json +82 -3
  57. package/vectors/pack-grading.json +68 -4
  58. package/vectors/stamp-on-chain.json +20 -5
  59. package/vectors/witness-cache.json +1304 -0
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "rule": "jcs-reader",
3
- "ruleVersion": 2,
4
- "statement": "How a kernel reads bytes that claim to be JCS(value) (RFC 8785). Four checks, none of them left to a built-in decoder's defaults: strict UTF-8, no LEADING byte-order mark, no non-finite literal, and no UNPAIRED surrogate in the parsed value. The fourth is not the first one twice: a surrogate written as the ASCII escape `\\ud800` survives every byte-level check, because the bytes are ASCII, and the parser produces the malformed code unit afterwards. Everything else a JSON parser rejects is rejected. A reader that accepts any of the rejected cases here is reading a set of bytes some other reader will read differently, and a producer chooses those bytes: that is a split verdict on one pack, not a spelling preference. The accepted cases matter as much as the rejected ones -- an honest body carrying astral characters, noncharacters, a U+FEFF inside a string value or combining marks is conformant and must keep verifying.",
3
+ "ruleVersion": 3,
4
+ "statement": "How a kernel reads bytes that claim to be JCS(value) (RFC 8785). Five checks, none of them left to a built-in decoder's defaults: strict UTF-8, no LEADING byte-order mark, no non-finite literal, no object that names one member twice (I-JSON, RFC 7493; ruleVersion 3, proof#144), and no UNPAIRED surrogate in the parsed value. The fourth is not the first one twice: a surrogate written as the ASCII escape `\\ud800` survives every byte-level check, because the bytes are ASCII, and the parser produces the malformed code unit afterwards. Everything else a JSON parser rejects is rejected. A reader that accepts any of the rejected cases here is reading a set of bytes some other reader will read differently, and a producer chooses those bytes: that is a split verdict on one pack, not a spelling preference. The accepted cases matter as much as the rejected ones -- an honest body carrying astral characters, noncharacters, a U+FEFF inside a string value or combining marks is conformant and must keep verifying.",
5
5
  "home": "@sanning/proof -- ts/vectors/jcs-reader.json. This file is the RULE, not a test fixture of one implementation. TypeScript implements it in ts/src/verifier.ts as jcsParse and Python in src/sanning_proof/canonicalize.py as jcs_loads; ts/test/host-wording.test.ts and tests/test_host_wording.py hold BOTH kernels to this file, so neither can pass by agreeing with the other.",
6
- "howToUse": "Base64-decode `bytesB64`, hand the bytes to your kernel's JCS reader, and assert that it returns a value where `accept` is true and throws or raises where it is false. Never decode to a string first: half of these cases are ABOUT the decode, and a reader tested through a string has not been tested.",
6
+ "howToUse": "Base64-decode `bytesB64`, hand the bytes to your kernel's JCS reader, and assert that it returns a value where `accept` is true and throws or raises where it is false. Never decode to a string first: half of these cases are ABOUT the decode, and a reader tested through a string has not been tested. Where a case carries `error`, the refusal's message is that exact string in both kernels: a duplicate's message is kernel wording, and a finding carries it.",
7
7
  "whyItExists": "Round four of proof#140 put a strict decoder on the TypeScript side of three sites and left json.loads(<bytes>) on the Python side of two. CPython hands bytes to detect_encoding and decodes with errors='surrogatepass', so Python became the LENIENT kernel: a sanning.anchor.trace/v3 pack whose checkpoint record bytes were BOM + JCS(record), re-sealed so the pack verifies, read `verified` with zero exceptions in Python and `mixed` in TypeScript. The full ladder, the case leg and CI were green over it. Both guards were source assertions over different module sets, and the TypeScript one stated the false premise in its own comment. One table both kernels read is the fix for that.",
8
8
  "cases": [
9
9
  {
@@ -161,6 +161,85 @@
161
161
  "bytesB64": "WzEsMiwzXQ==",
162
162
  "accept": true,
163
163
  "why": "a JSON array at the root. The reader returns it; whether a given site accepts a non-object is that site's check, not this one's"
164
+ },
165
+ {
166
+ "id": "duplicate-top-level",
167
+ "bytesB64": "eyJzcGVjX3ZlcnNpb24iOiJqdW5rIiwic3BlY192ZXJzaW9uIjoic2FubmluZy5ldmVudHMvdjIifQ==",
168
+ "accept": false,
169
+ "error": "duplicate key \"spec_version\"",
170
+ "why": "an object that names one member twice (proof#144). I-JSON (RFC 7493 section 2.3), which RFC 8785 is defined over, forbids it, and RFC 8785 cannot emit it. `json.loads` and `JSON.parse` both keep the LAST value and say nothing, so a reader that looks at the first sees junk while the kernel verifies the second"
171
+ },
172
+ {
173
+ "id": "duplicate-nested",
174
+ "bytesB64": "eyJldmVudCI6eyJtZXJrbGVfcm9vdCI6ImFhIiwibWVya2xlX3Jvb3QiOiJiYiJ9LCJwcmV2aW91c19oYXNoIjoiR0VORVNJUyJ9",
175
+ "accept": false,
176
+ "error": "duplicate key \"merkle_root\"",
177
+ "why": "the repeat is inside a nested object. Every level is checked, not only the root"
178
+ },
179
+ {
180
+ "id": "duplicate-escaped-spelling",
181
+ "bytesB64": "eyJhIjoxLCJcdTAwNjEiOjJ9",
182
+ "accept": false,
183
+ "error": "duplicate key \"a\"",
184
+ "why": "`\\u0061` is `a`. Names compare after unescaping, as both parsers compare them, so a second spelling of one name is still one name"
185
+ },
186
+ {
187
+ "id": "duplicate-innermost-first",
188
+ "bytesB64": "eyJhIjoxLCJhIjoyLCJiIjp7ImMiOjEsImMiOjJ9fQ==",
189
+ "accept": false,
190
+ "error": "duplicate key \"c\"",
191
+ "why": "two objects repeat a name. Objects are checked as they CLOSE, innermost first, so both kernels name `c` and not `a`: the rule for WHICH duplicate a message names, pinned because a finding carries it"
192
+ },
193
+ {
194
+ "id": "duplicate-name-rendered",
195
+ "bytesB64": "eyJcdTAwMDFcdWQ4M2RcdWRlMDBcIiI6MSwiXHUwMDAxXHVkODNkXHVkZTAwXCIiOjJ9",
196
+ "accept": false,
197
+ "error": "duplicate key \"\\u0001\\ud83d\\ude00\\\"\"",
198
+ "why": "the name in the message is rendered pure ASCII in both kernels: printable ASCII kept, a quote and a backslash escaped, every other UTF-16 code unit written \\uXXXX in lower case, so an astral character is its surrogate pair"
199
+ },
200
+ {
201
+ "id": "duplicate-name-truncated",
202
+ "bytesB64": "eyJra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2trayI6MSwia2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2tra2siOjJ9",
203
+ "accept": false,
204
+ "error": "duplicate key \"kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk\"...",
205
+ "why": "a rendered name longer than 64 characters is cut there and `...` follows the closing quote, so a hostile name cannot flood a finding"
206
+ },
207
+ {
208
+ "id": "duplicate-backslash-at-string-end",
209
+ "bytesB64": "eyJrIjoiXFwiLCJrIjoxfQ==",
210
+ "accept": false,
211
+ "error": "duplicate key \"k\"",
212
+ "why": "a string value ending in an escaped backslash. A scanner that reads `\\\\\"` as an escaped quote runs past the end of the string and misses the repeat"
213
+ },
214
+ {
215
+ "id": "duplicate-then-non-finite",
216
+ "bytesB64": "eyJ4Ijp7ImEiOjEsImEiOjJ9LCJiIjpOYU59",
217
+ "accept": false,
218
+ "why": "a repeat AND a non-finite literal. The parse runs to the end before any repeat is reported, so these bytes fail as JSON in both kernels and never as a duplicate: `JSON.parse` has to finish before a scan can start, and a Python hook that raised on the repeat would name a duplicate where TypeScript names a syntax error"
219
+ },
220
+ {
221
+ "id": "same-name-sibling-objects",
222
+ "bytesB64": "W3siYSI6MX0seyJhIjoyfV0=",
223
+ "accept": true,
224
+ "why": "one name in two different objects is not a repeat"
225
+ },
226
+ {
227
+ "id": "same-name-nested",
228
+ "bytesB64": "eyJhIjp7ImEiOjF9fQ==",
229
+ "accept": true,
230
+ "why": "one name at two depths is not a repeat"
231
+ },
232
+ {
233
+ "id": "braces-and-quotes-inside-strings",
234
+ "bytesB64": "eyJhXCJ9eyI6MSwiYiI6IntcImFcIjoxLFwiYVwiOjJ9In0=",
235
+ "accept": true,
236
+ "why": "a name holding a quote and braces, and a string VALUE holding JSON text with a repeat in it. Neither is an object, so neither is a repeat"
237
+ },
238
+ {
239
+ "id": "case-variant-names",
240
+ "bytesB64": "eyJzcGVjX3ZlcnNpb24iOiJzYW5uaW5nLmV2ZW50cy92MiIsIlNwZWNfVmVyc2lvbiI6Imp1bmsiLCJcdTAxN2ZwZWNfdmVyc2lvbiI6Imp1bmsifQ==",
241
+ "accept": true,
242
+ "why": "names that differ only in case, or by a look-alike such as the long s U+017F, are DIFFERENT names to I-JSON and to both parsers, and this rule does not refuse them. No spec in this repository pins how a reader treats a key that folds to a defined one; to every reader here it is an unknown member, and unknown members are ignored. The Go bundle reader in sanning-io/agent refuses these at the top level of a bundle, which is a recorded Go divergence (proof#144)"
164
243
  }
165
244
  ]
166
245
  }
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "rule": "pack-grading",
3
- "ruleVersion": 5,
4
- "statement": "What a verification surface may CLAIM about a pack it was handed. verified is reachable ONLY when every record's own bytes bound to its own commitment and nothing is missing, unreadable, surplus or omitted. Absent or unreadable evidence is incomplete (UNKNOWN), never failed and never verified. A hard kernel verdict wins: malformed > failed > incomplete > the kernel's own headline. A pack that DEVIATES from its own signed declaration (evidence-bundle.md \u00a75.4) is failed, and every deviation is a cause: a declared record that is absent, a present record nobody declared, or a record the pack declared withheld whose bytes its own SIGNED BODY supplies. A reading that rests instead on the UNSIGNED content handed in beside a pack is not a deviation and never reaches failed (proof#147): each content-dependent finding is its own cause and carries the pack to incomplete, because whoever assembles a case fills that map in, so an accuser holding an intact pack could otherwise reach failed with no key at all. A deviation and a content-dependent finding together are still failed, because the deviation reads off the signed body and a failing check wins. A declaration check that came back partial therefore splits in THREE, and only two of the three turn on the declaration's readability: no declaration member in the body at all moves nothing, because every pack built before \u00a75.4 is that case; a declaration the reader CANNOT READ makes the pack incomplete, because declaration_version is chosen by the party that assembles the pack and has no accept-set, so an unreadable claim must never reach a clean verdict; and a declaration the reader read perfectly well whose disclosure only unsigned bytes could settle makes the pack incomplete as well, named as what the reader could not establish rather than as what the holder did. The grade carries the declaration check verdict in its own output field, so a consumer that keeps only the grade can still tell a declared pack from an undeclared one.",
3
+ "ruleVersion": 6,
4
+ "statement": "What a verification surface may CLAIM about a pack it was handed. verified is reachable ONLY when every record's own bytes bound to its own commitment and nothing is missing, unreadable, surplus or omitted. Absent or unreadable evidence is incomplete (UNKNOWN), never failed and never verified. A hard kernel verdict wins: malformed > failed > incomplete > the kernel's own headline. A pack that DEVIATES from its own signed declaration (evidence-bundle.md \u00a75.4) is failed, and every deviation is a cause: a declared record that is absent, a present record nobody declared, or a record the pack declared withheld whose bytes its own SIGNED BODY supplies. A reading that rests instead on the UNSIGNED content handed in beside a pack is not a deviation and never reaches failed (proof#147): each content-dependent finding is its own cause and carries the pack to incomplete, because whoever assembles a case fills that map in, so an accuser holding an intact pack could otherwise reach failed with no key at all. A deviation and a content-dependent finding together are still failed, because the deviation reads off the signed body and a failing check wins. A declaration check that came back partial therefore splits in THREE, and only two of the three turn on the declaration's readability: no declaration member in the body at all moves nothing, because every pack built before \u00a75.4 is that case; a declaration the reader CANNOT READ makes the pack incomplete, because declaration_version is chosen by the party that assembles the pack and has no accept-set, so an unreadable claim must never reach a clean verdict; and a declaration the reader read perfectly well whose disclosure only unsigned bytes could settle makes the pack incomplete as well, named as what the reader could not establish rather than as what the holder did. The grade carries the declaration check verdict in its own output field, so a consumer that keeps only the grade can still tell a declared pack from an undeclared one. Rule version 6 (evidence-bundle.md \u00a75.4.5 v1.20, A22): a record is WITHHELD BY ITS DECLARATION when a readable declaration lists it in declared_withheld, its ok is true, its contentOk is null and its payloadBindingOk is true; it counts in recordsWithheld and never in recordsVerified, it is taken out of expected and missing before the disclosure causes are computed, and it holds the pack at nothing, so a pack whose every record its declaration withholds grades verified with recordsVerified zero. A record that fails any of the four, a pack with no readable declaration, and an input that does not carry eventId and payloadBindingOk keep rule version 5's reading.",
5
5
  "home": "@sanning/proof — ts/vectors/pack-grading.json. This file is the RULE, not a test fixture of one implementation. The kernel implements it in ts/src/grading.ts and is held to it by ts/test/grading.test.ts.",
6
6
  "consumers": "A surface that grades packs SHOULD call the kernel's gradePack rather than derive the rule again. A surface that cannot yet (an older pinned kernel, another language) vendors this file byte-for-byte, asserts its own derivation against it, and runs a drift check that compares its copy against the copy shipped in the @sanning/proof tarball at vectors/pack-grading.json. A vendored copy that is not byte-identical to this one is a defect, not a variant.",
7
- "howToUse": "Adapt a kernel verification result and a disclosure account into the shapes below, call gradePack, and assert the expectations. `kernel` gives the per-event outcome classes the kernel returns; `logs` gives the account of which records disclosed bytes; `omissions` gives the signed-leaf_count completeness notes. Nothing here depends on how the bytes were transported: a directory read and a browser drop produce the same account and must produce the same verdict. `declaration`, where a case carries one, gives the declaration check's verdict, whether the body carried a declaration member at all, and how many deviations it found; pass a check of that shape as gradePack's optional fourth argument. A case with no `declaration` key calls gradePack with three arguments, which is the pre-§5.4 behavior and must not move, and its `expect.declaration` is absent.",
7
+ "howToUse": "Adapt a kernel verification result and a disclosure account into the shapes below, call gradePack, and assert the expectations. `kernel` gives the per-event outcome classes the kernel returns; `logs` gives the account of which records disclosed bytes; `omissions` gives the signed-leaf_count completeness notes. Nothing here depends on how the bytes were transported: a directory read and a browser drop produce the same account and must produce the same verdict. `declaration`, where a case carries one, gives the declaration check's verdict, whether the body carried a declaration member at all, and how many deviations it found; pass a check of that shape as gradePack's optional fourth argument. A case with no `declaration` key calls gradePack with three arguments, which is the pre-§5.4 behavior and must not move, and its `expect.declaration` is absent. Rule version 6: `kernel.withheldBound` and `kernel.withheldUnbound` events carry eventId withheld-0, withheld-1 and so on, in that order, and payloadBindingOk true and null respectively, unless `kernel.v5Adapter` is true, in which case they carry neither field; `logs.withheld` adds that many of those ids to the account's expected AND missing lists, after the counted ones; `declaration.declaredWithheld` is how many of them, from withheld-0, the check lists in declared_withheld (0 where absent); and `expect.recordsWithheld`, where present, is the grade's count of records withheld by their declaration.",
8
8
  "kernelEventClasses": {
9
9
  "okBound": "{ ok: true, contentOk: true } — the record's own bytes hashed to its own commitment",
10
10
  "okUnknown": "{ ok: true, contentOk: null } — structurally fine, but no bytes were disclosed",
11
11
  "mismatched": "{ ok: false, contentOk: false } — the bytes disagree with the commitment",
12
- "brokenNoContent": "{ ok: false, contentOk: null } — the record itself does not verify"
12
+ "brokenNoContent": "{ ok: false, contentOk: null } — the record itself does not verify",
13
+ "withheldBound": "{ eventId: withheld-<i>, ok: true, contentOk: null, payloadBindingOk: true } — no content bound, the record bytes bound to the envelope (rule version 6)",
14
+ "withheldUnbound": "{ eventId: withheld-<i>, ok: true, contentOk: null, payloadBindingOk: null } — no content bound, and the record bytes did not travel either (rule version 6)"
13
15
  },
14
16
  "cases": [
15
17
  {
@@ -259,6 +261,68 @@
259
261
  "omissions": [],
260
262
  "declaration": { "verdict": "partial", "present": true, "deviations": 0 },
261
263
  "expect": { "verdict": "incomplete", "recordsVerified": 26, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 0, "declaration": "partial" }
264
+ },
265
+ {
266
+ "id": "withheld-by-declaration",
267
+ "why": "rule version 6, D5. One record the pack's readable declaration lists as content_withheld, whose record bytes bound to its envelope and whose content did not travel. Withholding it is the holder's signed act, so it holds the pack at nothing: verified, counted apart from the two records whose content bound. `logs.withheld` names the records that are both in the account's expected list and in its missing list under the withheld records' own ids",
268
+ "kernel": { "status": "verified", "okBound": 2, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0, "withheldBound": 1, "withheldUnbound": 0 },
269
+ "logs": { "expected": 3, "present": 2, "missing": 0, "unreadable": 0, "extra": 0, "withheld": 1 },
270
+ "omissions": [],
271
+ "declaration": { "verdict": "verified", "present": true, "deviations": 0, "declaredWithheld": 1 },
272
+ "expect": { "verdict": "verified", "recordsVerified": 2, "recordsFailed": 0, "recordsUndetermined": 0, "recordsWithheld": 1, "causeCount": 0, "declaration": "verified" }
273
+ },
274
+ {
275
+ "id": "every-record-withheld-by-declaration",
276
+ "why": "the cost of D5, stated rather than left to be found: every record withheld by the declaration grades verified with recordsVerified ZERO and no content byte hashed. It is safe to print only because the count travels, and a surface MUST NOT count these as verified records",
277
+ "kernel": { "status": "verified", "okBound": 0, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0, "withheldBound": 26, "withheldUnbound": 0 },
278
+ "logs": { "expected": 26, "present": 0, "missing": 0, "unreadable": 0, "extra": 0, "withheld": 26 },
279
+ "omissions": [],
280
+ "declaration": { "verdict": "verified", "present": true, "deviations": 0, "declaredWithheld": 26 },
281
+ "expect": { "verdict": "verified", "recordsVerified": 0, "recordsFailed": 0, "recordsUndetermined": 0, "recordsWithheld": 26, "causeCount": 0, "declaration": "verified" }
282
+ },
283
+ {
284
+ "id": "withheld-record-bytes-too",
285
+ "why": "the declaration withholds the record's content, and the pack withholds its record bytes as well, so its payload binding is undetermined: what is withheld is not only its content, and the fourth condition fails. Rule version 5's reading: incomplete, no_disclosure",
286
+ "kernel": { "status": "verified", "okBound": 0, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0, "withheldBound": 0, "withheldUnbound": 1 },
287
+ "logs": { "expected": 1, "present": 0, "missing": 0, "unreadable": 0, "extra": 0, "withheld": 1 },
288
+ "omissions": [],
289
+ "declaration": { "verdict": "verified", "present": true, "deviations": 0, "declaredWithheld": 1 },
290
+ "expect": { "verdict": "incomplete", "recordsVerified": 0, "recordsFailed": 0, "recordsUndetermined": 1, "recordsWithheld": 0, "causeCount": 1, "declaration": "verified" }
291
+ },
292
+ {
293
+ "id": "withheld-no-declaration",
294
+ "why": "the records of withheld-by-declaration with NO declaration. Only a declaration makes withholding a signed act; without one, absent content is indistinguishable from content lost in transit, so the record is undetermined and the pack is incomplete (missing_disclosure), exactly as rule version 5 graded it",
295
+ "kernel": { "status": "verified", "okBound": 2, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0, "withheldBound": 1, "withheldUnbound": 0 },
296
+ "logs": { "expected": 3, "present": 2, "missing": 0, "unreadable": 0, "extra": 0, "withheld": 1 },
297
+ "omissions": [],
298
+ "expect": { "verdict": "incomplete", "recordsVerified": 2, "recordsFailed": 0, "recordsUndetermined": 1, "recordsWithheld": 0, "causeCount": 1 }
299
+ },
300
+ {
301
+ "id": "withheld-unreadable-declaration",
302
+ "why": "the records of withheld-by-declaration beside a declaration that is present and that this reader cannot read. Its declared_withheld is empty, so nothing is withheld by it, and the pack is incomplete for the absent content and for the unreadable claim both",
303
+ "kernel": { "status": "verified", "okBound": 2, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0, "withheldBound": 1, "withheldUnbound": 0 },
304
+ "logs": { "expected": 3, "present": 2, "missing": 0, "unreadable": 0, "extra": 0, "withheld": 1 },
305
+ "omissions": [],
306
+ "declaration": { "verdict": "partial", "present": true, "deviations": 0, "declaredWithheld": 0 },
307
+ "expect": { "verdict": "incomplete", "recordsVerified": 2, "recordsFailed": 0, "recordsUndetermined": 1, "recordsWithheld": 0, "causeCount": 1, "declaration": "partial" }
308
+ },
309
+ {
310
+ "id": "withheld-and-side-bytes",
311
+ "why": "a record withheld by its declaration, and a content-dependent finding on the pack: bytes nobody signed that do not line up with the claim. The record is still withheld by the declaration and counted so, and the finding is its own cause, so the pack is incomplete and never failed",
312
+ "kernel": { "status": "verified", "okBound": 2, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0, "withheldBound": 1, "withheldUnbound": 0 },
313
+ "logs": { "expected": 3, "present": 2, "missing": 0, "unreadable": 0, "extra": 0, "withheld": 1 },
314
+ "omissions": [],
315
+ "declaration": { "verdict": "partial", "present": true, "deviations": 0, "contentFindings": 1, "declaredWithheld": 1 },
316
+ "expect": { "verdict": "incomplete", "recordsVerified": 2, "recordsFailed": 0, "recordsUndetermined": 0, "recordsWithheld": 1, "causeCount": 1, "declaration": "partial" }
317
+ },
318
+ {
319
+ "id": "withheld-v5-adapter",
320
+ "why": "withheld-by-declaration graded through an adapter written for rule version 5, whose events carry neither eventId nor payloadBindingOk (`kernel.v5Adapter`). Such an input is never withheld by its declaration, so the adapter loses the exemption and keeps the stricter reading: incomplete, missing_disclosure",
321
+ "kernel": { "status": "verified", "okBound": 2, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0, "withheldBound": 1, "withheldUnbound": 0, "v5Adapter": true },
322
+ "logs": { "expected": 3, "present": 2, "missing": 0, "unreadable": 0, "extra": 0, "withheld": 1 },
323
+ "omissions": [],
324
+ "declaration": { "verdict": "verified", "present": true, "deviations": 0, "declaredWithheld": 1 },
325
+ "expect": { "verdict": "incomplete", "recordsVerified": 2, "recordsFailed": 0, "recordsUndetermined": 1, "recordsWithheld": 0, "causeCount": 1, "declaration": "verified" }
262
326
  }
263
327
  ]
264
328
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "rule": "stamp-on-chain",
3
- "ruleVersion": 1,
3
+ "ruleVersion": 3,
4
4
  "statement": "What re-fetching a stamp's witness from a gateway DOES and DOES NOT settle. A stamp body carries no producer signature and no inclusion proof of its own, so the witness is the entire independent part of it (evidence-bundle.md §5.2.4): confirming that a gateway holds the exact bytes named by anchor_ref is the only check that turns Sanning's own assertion into evidence. It is an ONLINE act and always optional — an offline verdict is unchanged by it and never claims it happened. Four states, never three: not-checked (no gateway was asked, or the stamp names no witness), confirm (a gateway returned the witnessed bytes), mismatch (a gateway returned DIFFERENT bytes — the only outcome that fails anything), unreachable (we asked and nobody answered — an availability gap, never an accusation). The expected bytes are stamps[].stamp_bytes VERBATIM, because §5.2.2 defines that field as the exact bytes handed to the witness; re-canonicalizing them would compare the chain against bytes nobody signed.",
5
5
  "home": "@sanning/proof — ts/vectors/stamp-on-chain.json. This file is the RULE, not a test fixture of one implementation. The TypeScript kernel implements it in ts/src/evidence.ts (refetchStamps / refetchStamp / probeGateway / rollupOnChain) and is held to it by ts/test/stamp-on-chain-vectors.test.ts; the Python kernel implements it in src/sanning_proof/evidence.py (_refetch_stamps / _refetch_stamp / _probe_gateway / _rollup_on_chain) and is held to the SAME file by tests/test_stamp_on_chain_vectors.py. Neither kernel can pass by agreeing with the other.",
6
6
  "consumers": "A surface that re-fetches a stamp witness SHOULD call the kernel's verifyEvidenceBundle with `gateways` rather than derive the rule again. A surface that cannot yet (an older pinned kernel, another language) vendors this file byte-for-byte, asserts its own derivation against it, and runs a drift check against the copy shipped in the @sanning/proof tarball at vectors/stamp-on-chain.json — failing when it cannot find a copy to compare against. A vendored copy that is not byte-identical to this one is a defect, not a variant. In particular: a surface MUST NOT collapse the four states into a boolean. A reader that cannot distinguish not-checked from unreachable will report that no gateway could be reached when none was tried, which is a false accusation manufactured by a missing state, and is the defect this table was written after.",
7
- "howToUse": "Each case describes one sanning.stamp.evidence/v2 pack and one set of scripted gateway responses. Build the pack from `packScaffold`: for each entry of `stamps` (index i, 0-based) sign ONE sanning.events/v2 envelope over the record {\"event\":{\"content_hash\":\"<64×a>\"},\"i\":i} with event_id `ev-<i>` and the fixed seed, take its leaf under RFC 9162 (SHA-256(0x00 ‖ JCS(envelope))), make a one-leaf tree, and build the stamp body from `stampBodyScaffold` with merkle_root = that root, leaf_count = 1 and one sub-tree covering [0,1). stamp_bytes is JCS(stamp body) hex-encoded; stamp_ref is SHA-256 of the DECODED bytes. When the entry's `witnessed` is true attach anchor_ref = {kind: \"arweave\", id: `<anchorIdPrefix><i>`}, otherwise omit anchor_ref entirely. Give each stamp one event whose inclusion is {stamp_ref, leaf_index: 0, leaf_count: 1, leaf_hash, audit_path: []}. Wrap the body per `wrapperScaffold`, set body_hash = SHA-256(JCS(body)) and sign the wrapper (minus `signature`) with the same seed. Then call the kernel with the case's `gateways` and a fetch seam that answers `{gateway}/{anchor id}` per the `returns` entry for that stamp at that gateway's index, and assert `expect`. A case with an empty `gateways` list is called with no gateway list at all, that is the not-checked state and it must be reachable without a fetch seam.",
7
+ "howToUse": "Each case describes one sanning.stamp.evidence/v2 pack and one set of scripted gateway responses. Build the pack from `packScaffold`: for each entry of `stamps` (index i, 0-based) sign ONE sanning.events/v2 envelope over the record {\"event\":{\"content_hash\":\"<64×a>\"},\"i\":i} with event_id `ev-<i>` and the fixed seed, take its leaf under RFC 9162 (SHA-256(0x00 ‖ JCS(envelope))), make a one-leaf tree, and build the stamp body from `stampBodyScaffold` with merkle_root = that root, leaf_count = 1 and one sub-tree covering [0,1). stamp_bytes is JCS(stamp body) hex-encoded; stamp_ref is SHA-256 of the DECODED bytes. When the entry's `witnessed` is true attach anchor_ref = {kind: entry's `kind` (default \"arweave\"), id: `<anchorIdPrefix><i>`}, otherwise omit anchor_ref entirely. Give each stamp one event whose inclusion is {stamp_ref, leaf_index: 0, leaf_count: 1, leaf_hash, audit_path: []}. Wrap the body per `wrapperScaffold`, set body_hash = SHA-256(JCS(body)) and sign the wrapper (minus `signature`) with the same seed. Then call the kernel with the case's `gateways` and a fetch seam that answers `{gateway}/{anchor id}` per the `returns` entry for that stamp at that gateway's index, and assert `expect`. A case with an empty `gateways` list is called with no gateway list at all, that is the not-checked state and it must be reachable without a fetch seam. Only an `arweave` witness whose id is a 43-character base64url string is ever fetched (proof#115): a stamp naming any other `kind` is never probed, whatever its id looks like, and stays `ok` with `onChain: null`.",
8
8
  "responseKinds": {
9
9
  "witnessed": "The exact decoded stamp_bytes. The honest chain.",
10
10
  "altered": "JCS of the same stamp body with interval_end changed to 2026-09-09T13:00:00.000Z — a well-formed, canonical, semantically different stamp. This is the forgery shape that matters: a pack built on it is fully self-consistent OFFLINE and lies only about what was witnessed.",
@@ -16,7 +16,7 @@
16
16
  "anchorIdPrefix": "stampOnChainFixtureAnchorId00000000000000-",
17
17
  "packScaffold": {
18
18
  "seedHex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
19
- "eventIdPrefix": "ev-",
19
+ "eventIdPrefix": "00000000-0000-4000-8000-00000000000",
20
20
  "contentHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
21
21
  "signedAt": "2026-09-09T12:00:00Z",
22
22
  "environment": "production"
@@ -150,13 +150,13 @@
150
150
  },
151
151
  {
152
152
  "id": "unwitnessed-stamp-is-not-checked-never-unreachable",
153
- "why": "A stamp with no anchor_ref names no witness, so there is nothing to ask any gateway for. onChain stays null — not-checked — even though gateways were supplied and every one of them is reachable. Reporting all-unreachable here would invent a network event that did not occur, and would move this pack's exit code from 0 to 3 purely because the caller passed a gateway list. The bundle is already `partial` for the right reason, said in the right words, by §5.2.4.",
153
+ "why": "A stamp with no anchor_ref names no witness, so there is nothing to ask any gateway for. onChain stays null — not-checked — even though gateways were supplied and every one of them is reachable. Reporting all-unreachable here would invent a network event that did not occur. The bundle is already `partial` for the right reason, said in the right words, by §5.2.4, and exits 3 for that reason with or without a gateway list (0.13.0: `partial` exits 3 in every verb).",
154
154
  "gateways": ["https://g1.example"],
155
155
  "stamps": [{ "witnessed": false, "returns": ["witnessed"] }],
156
156
  "expect": {
157
157
  "onChainChecked": true,
158
158
  "status": "partial",
159
- "exit": 0,
159
+ "exit": 3,
160
160
  "stamps": [{ "rollup": null, "onChainOk": null, "ok": true }],
161
161
  "bundleErrorsContain": ["sealed by Sanning, witnessed by nobody"]
162
162
  }
@@ -224,6 +224,21 @@
224
224
  "sealed by Sanning, witnessed by nobody"
225
225
  ]
226
226
  }
227
+ },
228
+ {
229
+ "id": "unknown-kind-witness-is-not-checked-never-mismatch",
230
+ "why": "proof#115: a witness whose kind names no adapter this verifier has is never fetched, whatever gateways were passed and whatever its id looks like. Fetching it anyway, and comparing the response against stamp_bytes, is how a gateway that has never heard of the kind gets read as a mismatch or a 404 gets read as unreachable — both an accusation this verifier has no grounds to make. The bundle stays partial, said in the kind note, and the stamp itself stays ok with no on-chain result at all: not-checked, not unreachable.",
231
+ "gateways": ["https://g1.example"],
232
+ "stamps": [{ "witnessed": true, "kind": "memory", "returns": ["witnessed"] }],
233
+ "expect": {
234
+ "onChainChecked": true,
235
+ "status": "partial",
236
+ "exit": 3,
237
+ "stamps": [{ "rollup": null, "onChainOk": null, "ok": true }],
238
+ "bundleErrorsContain": [
239
+ "one or more stamps name a witness kind this verifier has no adapter for, so it could not be re-fetched"
240
+ ]
241
+ }
227
242
  }
228
243
  ]
229
244
  }