@sanning/proof 0.12.0 → 0.13.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 (48) 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 +349 -0
  6. package/dist/case.d.ts +173 -26
  7. package/dist/case.js +469 -123
  8. package/dist/cli.d.ts +38 -5
  9. package/dist/cli.js +557 -208
  10. package/dist/crypto.d.ts +1 -1
  11. package/dist/crypto.js +5 -1
  12. package/dist/declaration.d.ts +58 -52
  13. package/dist/declaration.js +256 -33
  14. package/dist/evidence.d.ts +2 -0
  15. package/dist/evidence.js +134 -19
  16. package/dist/exit-codes.d.ts +17 -0
  17. package/dist/exit-codes.js +50 -0
  18. package/dist/grading.d.ts +28 -13
  19. package/dist/grading.js +90 -31
  20. package/dist/index.d.ts +10 -7
  21. package/dist/index.js +13 -6
  22. package/dist/message-parts.d.ts +11 -9
  23. package/dist/message-parts.js +11 -9
  24. package/dist/types.d.ts +1 -0
  25. package/dist/verifier.d.ts +9 -2
  26. package/dist/verifier.js +28 -12
  27. package/dist/zip-reader.d.ts +50 -0
  28. package/dist/zip-reader.js +220 -0
  29. package/package.json +2 -2
  30. package/src/agent-proof.ts +4 -4
  31. package/src/case-file.ts +419 -0
  32. package/src/case.ts +586 -132
  33. package/src/cli.ts +651 -206
  34. package/src/crypto.ts +5 -1
  35. package/src/declaration.ts +248 -52
  36. package/src/evidence.ts +142 -21
  37. package/src/exit-codes.ts +54 -0
  38. package/src/grading.ts +109 -52
  39. package/src/index.ts +58 -2
  40. package/src/message-parts.ts +11 -9
  41. package/src/types.ts +5 -0
  42. package/src/verifier.ts +35 -13
  43. package/src/zip-reader.ts +229 -0
  44. package/vectors/README.md +23 -5
  45. package/vectors/case-file-index.json +668 -0
  46. package/vectors/case-split.json +218 -9
  47. package/vectors/pack-grading.json +68 -4
  48. package/vectors/stamp-on-chain.json +3 -3
@@ -208,7 +208,7 @@
208
208
  "description": "Packs zipped inside the folder and nothing else. No bundle.json at any path, so the folder is refused, and the hint says to unzip first.",
209
209
  "expected": {
210
210
  "error": "No bundle.json found. An evidence pack contains bundle.json plus logs/.",
211
- "hint": "It holds 2 .zip file(s) (decision-file/evidence/packs/checker.zip, …), and case never opens a .zip. Unzip it first (for example, unzip file.zip -d case/), then pass the folder."
211
+ "hint": "It holds 2 .zip file(s) (decision-file/evidence/packs/checker.zip, …), and case never opens a .zip inside a folder. Pass the .zip itself, or unzip it first (for example, unzip file.zip -d case/) and pass the folder."
212
212
  },
213
213
  "id": "zipped-packs-only",
214
214
  "tree": [
@@ -230,15 +230,25 @@
230
230
  "args": [
231
231
  "pack.zip"
232
232
  ],
233
- "description": "A .zip named on the command line is refused with one line: unzip it first, then pass the folder. Nothing in it is read.",
233
+ "description": "A .zip named on the command line is read as a case file (evidence-bundle §5.6, v1.20 A25): <name>.zip is read at <name>/<entry>, the folder unzip <name>.zip -d <name> makes, so the archive and its unpacked folder give one reading.",
234
234
  "expected": {
235
- "error": "<root>/pack.zip is a .zip archive. Unzip it first (for example, unzip file.zip -d case/), then pass the folder.",
236
- "hint": null
235
+ "not_regular": [],
236
+ "outside": [],
237
+ "packs": [
238
+ {
239
+ "bundle_path": "pack/bundle.json",
240
+ "content": {
241
+ "x": "78"
242
+ },
243
+ "ref": "pack",
244
+ "unaccounted": []
245
+ }
246
+ ]
237
247
  },
238
- "id": "zip-argument-refused",
248
+ "id": "zip-argument-read",
239
249
  "tree": [
240
250
  {
241
- "hex": "504b03041400000008000000375d408bd69528000000260000000b00000062756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500504b010214031400000008000000375d408bd69528000000260000000b000000000000000000000080010000000062756e646c652e6a736f6e504b0506000000000100010039000000510000000000",
251
+ "hex": "504b03041400000008000000375d408bd69528000000260000000b00000062756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500504b03041400000008000000375d8316dc8c03000000010000000b0000006c6f67732f782e6a736f6eab0000504b010214031400000008000000375d408bd69528000000260000000b000000000000000000000080010000000062756e646c652e6a736f6e504b010214031400000008000000375d8316dc8c03000000010000000b00000000000000000000008001510000006c6f67732f782e6a736f6e504b05060000000002000200720000007d0000000000",
242
252
  "path": "pack.zip"
243
253
  }
244
254
  ]
@@ -247,9 +257,9 @@
247
257
  "args": [
248
258
  "PACK.Zip"
249
259
  ],
250
- "description": "The extension is matched without regard to ASCII case.",
260
+ "description": "The extension is matched without regard to ASCII case, and bytes that are not a .zip archive refuse the input.",
251
261
  "expected": {
252
- "error": "<root>/PACK.Zip is a .zip archive. Unzip it first (for example, unzip file.zip -d case/), then pass the folder.",
262
+ "error": "<root>/PACK.Zip is not a readable .zip archive",
253
263
  "hint": null
254
264
  },
255
265
  "id": "zip-argument-any-case",
@@ -266,7 +276,7 @@
266
276
  ],
267
277
  "description": "A file that is not a .zip is not a folder either.",
268
278
  "expected": {
269
- "error": "<root>/bundle.json is not a folder. Pass pack folders, or the folder that holds them.",
279
+ "error": "<root>/bundle.json is not a folder or a .zip. Pass pack folders, the folder that holds them, or a case file.",
270
280
  "hint": null
271
281
  },
272
282
  "id": "file-argument-refused",
@@ -277,6 +287,205 @@
277
287
  }
278
288
  ]
279
289
  },
290
+ {
291
+ "args": [
292
+ "case-4b19de"
293
+ ],
294
+ "description": "A case file, unzipped: packs a and b under packs/<ref>/, and the index, the command and every report/ file outside every pack. The case file needs no new splitting rule. The zip under report/ is a file like any other and is never opened, although it holds a bundle.json.",
295
+ "expected": {
296
+ "not_regular": [],
297
+ "outside": [
298
+ "case-4b19de/HOW-TO-VERIFY.txt",
299
+ "case-4b19de/contents.json",
300
+ "case-4b19de/report/README.txt",
301
+ "case-4b19de/report/memo.pdf",
302
+ "case-4b19de/report/nested.zip"
303
+ ],
304
+ "packs": [
305
+ {
306
+ "bundle_path": "case-4b19de/packs/a/bundle.json",
307
+ "content": {
308
+ "x": "78"
309
+ },
310
+ "ref": "case-4b19de/packs/a",
311
+ "unaccounted": []
312
+ },
313
+ {
314
+ "bundle_path": "case-4b19de/packs/b/bundle.json",
315
+ "content": {},
316
+ "ref": "case-4b19de/packs/b",
317
+ "unaccounted": []
318
+ }
319
+ ]
320
+ },
321
+ "id": "case-file-unpacked",
322
+ "tree": [
323
+ {
324
+ "hex": "7b22737065635f76657273696f6e223a2273616e6e696e672e65766964656e63652f7631227d",
325
+ "path": "case-4b19de/packs/a/bundle.json"
326
+ },
327
+ {
328
+ "hex": "78",
329
+ "path": "case-4b19de/packs/a/logs/x.json"
330
+ },
331
+ {
332
+ "hex": "7b22737065635f76657273696f6e223a2273616e6e696e672e65766964656e63652f7631227d",
333
+ "path": "case-4b19de/packs/b/bundle.json"
334
+ },
335
+ {
336
+ "hex": "7b7d",
337
+ "path": "case-4b19de/contents.json"
338
+ },
339
+ {
340
+ "hex": "756e7a6970",
341
+ "path": "case-4b19de/HOW-TO-VERIFY.txt"
342
+ },
343
+ {
344
+ "hex": "4e6f742065766964656e6365",
345
+ "path": "case-4b19de/report/README.txt"
346
+ },
347
+ {
348
+ "hex": "25504446",
349
+ "path": "case-4b19de/report/memo.pdf"
350
+ },
351
+ {
352
+ "hex": "504b03041400000008000000375d408bd69528000000260000000b00000062756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500504b010214031400000008000000375d408bd69528000000260000000b000000000000000000000080010000000062756e646c652e6a736f6e504b0506000000000100010039000000510000000000",
353
+ "path": "case-4b19de/report/nested.zip"
354
+ }
355
+ ]
356
+ },
357
+ {
358
+ "args": [
359
+ "case-4b19de.zip"
360
+ ],
361
+ "description": "The same case file named as case-4b19de.zip: the same split, key for key.",
362
+ "expected": {
363
+ "not_regular": [],
364
+ "outside": [
365
+ "case-4b19de/HOW-TO-VERIFY.txt",
366
+ "case-4b19de/contents.json",
367
+ "case-4b19de/report/README.txt",
368
+ "case-4b19de/report/memo.pdf",
369
+ "case-4b19de/report/nested.zip"
370
+ ],
371
+ "packs": [
372
+ {
373
+ "bundle_path": "case-4b19de/packs/a/bundle.json",
374
+ "content": {
375
+ "x": "78"
376
+ },
377
+ "ref": "case-4b19de/packs/a",
378
+ "unaccounted": []
379
+ },
380
+ {
381
+ "bundle_path": "case-4b19de/packs/b/bundle.json",
382
+ "content": {},
383
+ "ref": "case-4b19de/packs/b",
384
+ "unaccounted": []
385
+ }
386
+ ]
387
+ },
388
+ "id": "case-file-zip",
389
+ "tree": [
390
+ {
391
+ "hex": "504b03041400000008000000375d408bd6952800000026000000130000007061636b732f612f62756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500504b03041400000008000000375d8316dc8c0300000001000000130000007061636b732f612f6c6f67732f782e6a736f6eab0000504b03041400000008000000375d408bd6952800000026000000130000007061636b732f622f62756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500504b03041400000008000000375d43bfa6a304000000020000000d000000636f6e74656e74732e6a736f6eabae0500504b03041400000008000000375dc654b591070000000500000011000000484f572d544f2d5645524946592e7478742bcdabca2c0000504b03041400000008000000375d47efbb750e0000000c000000110000007265706f72742f524541444d452e747874f3cb2f51482dcb4c49cd4b4e0500504b03041400000008000000375d04c0bed106000000040000000f0000007265706f72742f6d656d6f2e706466530d70710300504b03041400000008000000375d88567fd66c000000a0000000110000007265706f72742f6e65737465642e7a69700bf0666611616060e00062f35887ee6b5335802c3520e606e2a4d2bc949c54bdace2fcbcd5615a7a1ebe7dfadeba5ae7ce9c0fde14a4e577e6f4e933ded7bd754ffb789ef5f6bbaa6f1cb28a9521c09b91498419b79130d0c0c8806a4180372b1b488811082d817420581900504b010214031400000008000000375d408bd69528000000260000001300000000000000000000008001000000007061636b732f612f62756e646c652e6a736f6e504b010214031400000008000000375d8316dc8c03000000010000001300000000000000000000008001590000007061636b732f612f6c6f67732f782e6a736f6e504b010214031400000008000000375d408bd695280000002600000013000000000000000000000080018d0000007061636b732f622f62756e646c652e6a736f6e504b010214031400000008000000375d43bfa6a304000000020000000d00000000000000000000008001e6000000636f6e74656e74732e6a736f6e504b010214031400000008000000375dc654b5910700000005000000110000000000000000000000800115010000484f572d544f2d5645524946592e747874504b010214031400000008000000375d47efbb750e0000000c00000011000000000000000000000080014b0100007265706f72742f524541444d452e747874504b010214031400000008000000375d04c0bed106000000040000000f00000000000000000000008001880100007265706f72742f6d656d6f2e706466504b010214031400000008000000375d88567fd66c000000a00000001100000000000000000000008001bb0100007265706f72742f6e65737465642e7a6970504b05060000000008000800f8010000560200000000",
392
+ "path": "case-4b19de.zip"
393
+ }
394
+ ]
395
+ },
396
+ {
397
+ "args": [
398
+ "p.zip"
399
+ ],
400
+ "description": "Entries a reader refuses are listed as outside every pack and never read, even under the pack's logs/: an absolute name, a .. segment, a backslash, a symbolic link, a compression method other than stored or deflate, and an encrypted entry. A stored entry is read.",
401
+ "expected": {
402
+ "not_regular": [
403
+ "p//abs.json",
404
+ "p/logs/../up.json",
405
+ "p/logs/bz.json",
406
+ "p/logs/enc.json",
407
+ "p/logs/link.json",
408
+ "p/logs\\back.json"
409
+ ],
410
+ "outside": [
411
+ "p//abs.json",
412
+ "p/logs/../up.json",
413
+ "p/logs/bz.json",
414
+ "p/logs/enc.json",
415
+ "p/logs/link.json",
416
+ "p/logs\\back.json"
417
+ ],
418
+ "packs": [
419
+ {
420
+ "bundle_path": "p/bundle.json",
421
+ "content": {
422
+ "ok": "6f6b",
423
+ "stored": "73"
424
+ },
425
+ "ref": "p",
426
+ "unaccounted": []
427
+ }
428
+ ]
429
+ },
430
+ "id": "zip-entries-refused",
431
+ "tree": [
432
+ {
433
+ "hex": "504b03041400000008000000375d408bd69528000000260000000b00000062756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500504b03041400000008000000375d47dddc7904000000020000000c0000006c6f67732f6f6b2e6a736f6ecbcf0600504b03041400000008000000375d43beb7e80300000001000000090000002f6162732e6a736f6e4b0400504b03041400000008000000375d3e6a6df203000000010000000f0000006c6f67732f2e2e2f75702e6a736f6e2b0500504b03041400000008000000375df9efbe7103000000010000000e0000006c6f67735c6261636b2e6a736f6e4b0200504b03041400000008000000375dfc2f6f4608000000060000000e0000006c6f67732f6c696e6b2e6a736f6e2b492c4a4f2d0100504b03041400000000000000375d0bcf0e1b0100000001000000100000006c6f67732f73746f7265642e6a736f6e73504b03042e0000000c000000375daf77d26225000000010000000c0000006c6f67732f627a2e6a736f6e425a68393141592653597ec98b7a00000000800010200021184682ee48a70a120fd9316f40504b03041400010008000000375d5a7adaef03000000010000000d0000006c6f67732f656e632e6a736f6e4b0500504b010214031400000008000000375d408bd69528000000260000000b000000000000000000000080010000000062756e646c652e6a736f6e504b010214031400000008000000375d47dddc7904000000020000000c00000000000000000000008001510000006c6f67732f6f6b2e6a736f6e504b010214031400000008000000375d43beb7e8030000000100000009000000000000000000000080017f0000002f6162732e6a736f6e504b010214031400000008000000375d3e6a6df203000000010000000f00000000000000000000008001a90000006c6f67732f2e2e2f75702e6a736f6e504b010214031400000008000000375df9efbe7103000000010000000e00000000000000000000008001d90000006c6f67735c6261636b2e6a736f6e504b010214031400000008000000375dfc2f6f4608000000060000000e0000000000000000000000ffa1080100006c6f67732f6c696e6b2e6a736f6e504b010214031400000000000000375d0bcf0e1b010000000100000010000000000000000000000080013c0100006c6f67732f73746f7265642e6a736f6e504b01022e032e0000000c000000375daf77d26225000000010000000c000000000000000000000080016b0100006c6f67732f627a2e6a736f6e504b010214031400010008000000375d5a7adaef03000000010000000d00000000000000000000008001ba0100006c6f67732f656e632e6a736f6e504b0506000000000900090012020000e80100000000",
434
+ "path": "p.zip"
435
+ }
436
+ ]
437
+ },
438
+ {
439
+ "args": [
440
+ "p.zip"
441
+ ],
442
+ "description": "An entry whose bytes do not match the CRC-32 the archive records for them refuses the archive: which bytes are the file is not settled.",
443
+ "expected": {
444
+ "error": "<root>/p.zip holds bundle.json, which cannot be read",
445
+ "hint": null
446
+ },
447
+ "id": "zip-crc-mismatch",
448
+ "tree": [
449
+ {
450
+ "hex": "504b03041400000008000000215a418bd69528000000260000000b00000062756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500504b010214001400000008000000215a418bd69528000000260000000b000000000000000000000000000000000062756e646c652e6a736f6e504b0506000000000100010039000000510000000000",
451
+ "path": "p.zip"
452
+ }
453
+ ]
454
+ },
455
+ {
456
+ "args": [
457
+ "p.zip"
458
+ ],
459
+ "description": "Data after the end of an entry's deflate stream, inside the size the archive records, refuses the archive: one zip library reads past it and another stops, so the two kernels would read two files.",
460
+ "expected": {
461
+ "error": "<root>/p.zip holds bundle.json, which cannot be read",
462
+ "hint": null
463
+ },
464
+ "id": "zip-trailing-deflate-data",
465
+ "tree": [
466
+ {
467
+ "hex": "504b03041400000008000000215a408bd6952d000000260000000b00000062756e646c652e6a736f6eab562a2e484d8e2f4b2d2acecccf53b2522a4ecccbcbcc4bd74b2dcb4c49cd4b4ed52f3354aa0500006a756e6b504b010214001400000008000000215a408bd6952d000000260000000b000000000000000000000000000000000062756e646c652e6a736f6e504b0506000000000100010039000000560000000000",
468
+ "path": "p.zip"
469
+ }
470
+ ]
471
+ },
472
+ {
473
+ "args": [
474
+ "p.zip"
475
+ ],
476
+ "description": "An archive holding one name twice is refused whole: which of the two is the file is a choice two zip libraries make differently.",
477
+ "expected": {
478
+ "error": "<root>/p.zip holds bundle.json twice",
479
+ "hint": null
480
+ },
481
+ "id": "zip-name-twice",
482
+ "tree": [
483
+ {
484
+ "hex": "504b03041400000000000000375d408bd69526000000260000000b00000062756e646c652e6a736f6e7b22737065635f76657273696f6e223a2273616e6e696e672e65766964656e63652f7631227d504b03041400000000000000375d43bfa6a302000000020000000b00000062756e646c652e6a736f6e7b7d504b010214031400000000000000375d408bd69526000000260000000b000000000000000000000080010000000062756e646c652e6a736f6e504b010214031400000000000000375d43bfa6a302000000020000000b000000000000000000000080014f00000062756e646c652e6a736f6e504b05060000000002000200720000007a0000000000",
485
+ "path": "p.zip"
486
+ }
487
+ ]
488
+ },
280
489
  {
281
490
  "args": [
282
491
  "nope"
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "rule": "stamp-on-chain",
3
- "ruleVersion": 1,
3
+ "ruleVersion": 2,
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.",
@@ -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
  }