@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/dist/grading.js CHANGED
@@ -1,93 +1,42 @@
1
- // Pack grading — what a verification SURFACE may claim about a pack it was
2
- // handed, as distinct from what the wrapper verdict says about a bundle.
1
+ // Pack grading: what a verification surface may claim about a pack (a bundle
2
+ // plus the bytes handed in beside it), as distinct from the bundle verdict.
3
3
  //
4
- // WHY THIS LIVES IN THE KERNEL.
5
- // This rule was, until now, derived twice: once in a CLI trust close and once
6
- // in a browser verify page, in two repos and two languages, held together by a
7
- // conformance table copied byte-for-byte into both. The table caught an
8
- // implementation drifting from the rule. Nothing caught the table itself being
9
- // edited on one side — and the two surfaces had already diverged once, one
10
- // binding disclosed bytes per record and the other believed to bind by set
11
- // membership. Two implementations of one verification rule is the defect; a
12
- // shared table is a mitigation of it.
4
+ // Pure: no `fs`, no `fetch`. A directory read and a browser drop both arrive
5
+ // as a content map and must produce the same grade. Causes are structured so a
6
+ // surface can render its own prose or call `renderCause`.
13
7
  //
14
- // Grading is kernel semantics that leaked into consumers. The rule it encodes
15
- // is already normative in `specs/evidence-bundle.md`: absent disclosure — and
16
- // an absent committed `content_hash` — is **undetermined, not failed** (§5.1),
17
- // a genuine mismatch fails the event and the rollup (§5.1), and no status ever
18
- // means "safe" (§6). What the surfaces added was a sharper word for the
19
- // undetermined case and an account of what the pack failed to disclose. Both
20
- // belong next to the verifier that produced the result.
21
- //
22
- // ── the seam ────────────────────────────────────────────────────────────────
23
- //
24
- // Everything here is PURE: data in, data out. No `fs`, no `fetch`, no key
25
- // lifecycle — the kernel's standing boundary (`specs/architecture.md` §3) is
26
- // unchanged. How disclosed bytes were transported is a consumer concern; a
27
- // directory read and a browser drop both arrive here as a content map and MUST
28
- // produce the same verdict. `causes` come back STRUCTURED, so a surface can
29
- // render its own prose (or call `renderCause` and get the house wording)
30
- // without the kernel taking on presentation.
31
- //
32
- // ── the rule ────────────────────────────────────────────────────────────────
33
- //
34
- // Three outcomes where the old code had two:
35
- //
36
- // verified every record's own bytes hashed to its OWN committed
8
+ // Outcomes (evidence-bundle.md sections 3 and 5.1), read off six claims (`claims.ts`):
9
+ // verified every record's own bytes hashed to its own committed
37
10
  // content_hash, and nothing is missing, unreadable, surplus or
38
11
  // omitted
39
- // failed some record's bytes DISAGREE with what was committed — tamper
40
- // incomplete some record's bytes are absent or unreadable, or the pack
41
- // presents a different number of records than a checkpoint
42
- // signed for — UNKNOWN
12
+ // failed a claim is disproven: some record's bytes disagree with its
13
+ // commitment, or (reason kind nonconforming) an agent record is
14
+ // signed and intact and does not follow its vocabulary
15
+ // incomplete nothing is disproven and a claim is not proven: some record's
16
+ // bytes are absent or unreadable, the pack presents a different
17
+ // record count than a checkpoint signed, or an agent record
18
+ // reads partial
43
19
  //
44
- // `incomplete` is one of the eight words of `evidence-bundle.md` §3 (v1.17,
45
- // A18), and it is a PACK verdict, never a bundle status. The bundle status
46
- // grades a signed bundle; this grades a PACK — a strictly larger object, the
47
- // bundle plus whatever bytes travelled beside it. A pack can be incomplete
48
- // while the bundle it carries is impeccable, which is exactly the case that
49
- // used to render green over zero hashed bytes. One vocabulary, two layers,
50
- // each issuing the subset it can reach.
20
+ // `incomplete` is a pack verdict, never a bundle status: a pack can be
21
+ // incomplete while its bundle is sound.
51
22
  //
52
- // `recordsVerified` counts CONTENT-BOUND records only. The kernel's own
53
- // `event.ok` tolerates undisclosed content by design — a bundle may legitimately
54
- // withhold a record (minimal disclosure, §5.1) — which is right for the kernel
55
- // and wrong for a surface whose headline is that the logs were checked.
56
- // Counting `ok` events is how "26 records verified" got printed over zero
57
- // hashed bytes.
23
+ // `recordsVerified` counts content-bound records only. The kernel's `event.ok`
24
+ // tolerates withheld content (minimal disclosure, §5.1), so counting `ok`
25
+ // events would report records verified over zero hashed bytes.
58
26
  //
59
- // Binding is PER RECORD, never per set. The bytes disclosed for `event_id` are
60
- // hashed against the commitment inside THAT event's signed record; the verifier
61
- // does this and this module never re-implements it. A set-membership check
62
- // ("is this digest anywhere in the bundle?") passes a swap of two records'
63
- // bytes, and that is the shape of the divergence this module exists to end.
27
+ // Binding is per record, never per set: a set-membership check passes a swap
28
+ // of two records' bytes. The verifier does the binding; this module never
29
+ // re-implements it.
64
30
  //
65
- // The conformance statement of all of the above is `../vectors/pack-grading.json`,
66
- // which ships in the published tarball so a consumer can diff its own vendored
67
- // copy against it. If an implementation disagrees with the table, the
68
- // implementation is wrong.
31
+ // Conformance: `../vectors/pack-grading.json`, shipped in the package.
32
+ import { INCOMPLETE_REASONS } from "./agent-record.js";
69
33
  import { hexToBytes } from "./crypto.js";
70
34
  import { renderContentFinding, renderDeviation } from "./declaration.js";
71
35
  import { hasSideContent, sideContentKeys } from "./side-content.js";
72
- // The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
73
- // byte-order mark, no non-finite literal. `jcs_loads` in
74
- // `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
36
+ import { VERDICTS, bundleProblems, claimsOf, problem, verdictOf, } from "./claims.js";
75
37
  import { jcsParse } from "./verifier.js";
76
- // ---- The verdict a surface may print ----------------------------------------
77
- // A superset of `EvidenceStatus`: the four the kernel can return, plus the
78
- // pack-level `incomplete`, plus `pending` from the shared §3 vocabulary so a
79
- // kernel headline this module does not downgrade can pass through unchanged.
80
- export const PACK_VERDICTS = [
81
- "verified",
82
- "partial",
83
- "pending",
84
- "not_found",
85
- "mixed",
86
- "failed",
87
- "malformed",
88
- "incomplete",
89
- ];
90
- // ---- Output ------------------------------------------------------------------
38
+ // The three verdicts (evidence-bundle.md section 3). A pack reads its claims.
39
+ export const PACK_VERDICTS = VERDICTS;
91
40
  export const PACK_GRADE_CAUSE_KINDS = [
92
41
  "records_omitted",
93
42
  "records_surplus",
@@ -98,29 +47,20 @@ export const PACK_GRADE_CAUSE_KINDS = [
98
47
  "unaccounted_disclosure",
99
48
  "declaration_deviation",
100
49
  /**
101
- * The pack and the UNSIGNED content handed in beside it do not line up
102
- * (proof#147). A cause, so the pack cannot read clean over it and a reader
103
- * is told which record; NOT a deviation, so it never reaches `failed`.
50
+ * The pack and the unsigned content beside it do not line up. A cause, so the
51
+ * pack cannot read clean; not a deviation, so it never reaches `failed`.
104
52
  */
105
53
  "declaration_content_dependent",
54
+ /** A reason of an agent record that reads `failed` (agent-events.md section 10): the pack is `failed`, reason kind `nonconforming`. */
55
+ "agent_record_nonconforming",
56
+ /** A reason of an agent record that reads `incomplete`: the pack is `incomplete`. */
57
+ "agent_record_incomplete",
106
58
  ];
107
59
  /**
108
- * The checkpoint's join key, in whichever vocabulary the bundle speaks.
109
- *
110
- * ── Why this is read from the entry and not from the body_type ──
111
- *
112
- * Grading runs over a PACK, which a consumer may hand us in pieces, so the
113
- * wrapper is not always in scope here. Reading the key off the entry keeps
114
- * grading honest for both forms with no wrapper lookup. It is deliberately
115
- * NOT a fallback chain that would accept either spelling on the same entry:
116
- * an entry carrying both is a mixed bundle, and `verifyEvidenceBundle`
117
- * rejects it — grading must not quietly grade what the verifier refuses.
118
- *
119
- * Returning `null` (rather than a "(no tx_id)" placeholder that compares
120
- * equal to itself) matters: before this was dual-form, a v2 pack read every
121
- * checkpoint key as absent, every `presented` count as zero, and reported
122
- * EVERY checkpoint as "omitted" — a completeness accusation manufactured by
123
- * a field rename. A key we cannot read is unreadable, never zero.
60
+ * The checkpoint's join key, `checkpoint_ref` or the retired `tx_id`. Read from
61
+ * the entry because the wrapper may not be in scope. An entry carrying both is
62
+ * a mixed bundle the verifier rejects, so it reads as `null`. `null` means
63
+ * unreadable, never zero, so a key we cannot read is never counted as omitted.
124
64
  */
125
65
  function joinKeyOf(o) {
126
66
  if (!o)
@@ -140,10 +80,8 @@ function inclusionKeyOf(incl) {
140
80
  return null;
141
81
  return (hasV2 ? incl.checkpoint_ref : incl.checkpoint_tx_id);
142
82
  }
143
- // The key a checkpoint and an inclusion are MATCHED on. A `checkpoint_ref` is
144
- // folded to lower case, because the verifier lowercases it before comparing.
145
- // A retired `tx_id` / `checkpoint_tx_id` is compared exactly as written, as the
146
- // retired reader did, even when it happens to look like hex.
83
+ // The key a checkpoint and an inclusion are matched on. `checkpoint_ref` is
84
+ // lowercased, as the verifier does; a retired key is compared exactly.
147
85
  function checkpointMatchKey(o) {
148
86
  const k = joinKeyOf(o);
149
87
  return k !== null && typeof o?.checkpoint_ref === "string" ? k.toLowerCase() : k;
@@ -160,82 +98,30 @@ const checkpointsOf = (bundle) => {
160
98
  const c = bundle?.body?.checkpoints;
161
99
  return Array.isArray(c) ? c : [];
162
100
  };
163
- // Presence in a caller-built content map — DEFERRED to `side-content.ts`, which
164
- // is the same rule the VERIFIER applies, because two answers to "is there an
165
- // entry" inside one kernel is two implementations of one verification rule, and
166
- // ending that is why this module was brought into the kernel at all.
167
- //
168
- // This module used to answer it laxly: own + enumerable, nothing more. The
169
- // verifier additionally requires a DATA property (an accessor can throw, and a
170
- // verifier must never raise on adversarial input) holding something other than
171
- // null/undefined (`{ [id]: undefined }` and `new Map([[id, undefined]])` are
172
- // how "no entry" is spelled in the two shapes). So on one map this module said
173
- // PRESENT and the verifier said nothing was offered — and `gradePack` returned
174
- // `incomplete` with ZERO causes, the very outcome the comment in
175
- // `accountDisclosure` below says was removed. Now there is one definition and
176
- // two call sites.
101
+ // Presence in a content map is decided by `side-content.ts`, the same rule the
102
+ // verifier applies, so the grade and the verdict never disagree on an entry.
177
103
  /**
178
- * `event_id`s name files on every transport this rule has met. An id that
179
- * cannot safely name one cannot be bound to disclosed bytes, so it is refused
180
- * rather than joined into a path.
181
- *
182
- * ── WHAT THIS IS AND IS NOT ─────────────────────────────────────────────────
183
- *
184
- * **Not a traversal fix, and it must not be described as one.** Nothing in
185
- * this kernel or in the `proof` CLI joins an `event_id` into a path: the CLI's
186
- * only join is `join(dirPath, name)` where `name` came from `readdir`, so the
187
- * id is DERIVED from a real directory entry rather than used to reach one.
188
- * Claiming otherwise would misrepresent a hardening as a vulnerability fix.
189
- *
190
- * It is an **honesty guard**, and it points one way only. The documented pack
191
- * layout is `logs/<event_id>.json`, so a downstream consumer WILL join these,
192
- * and an id that cannot name a file on the holder's filesystem is an id whose
193
- * bytes will never arrive. Saying so — `unreadable`, and therefore
194
- * `incomplete` — is the truth about that record. Refusing an id can only move
195
- * a verdict toward UNKNOWN, never toward verified, which is what makes
196
- * tightening this safe: the failure mode of a rule that is too strict is a
197
- * conservative verdict, and the failure mode of one that is too lax is a
198
- * record silently graded on bytes that were never there.
199
- *
200
- * ── WHAT IS REFUSED, AND WHY EACH ONE ───────────────────────────────────────
104
+ * `event_id`s name files on every transport. An id that cannot safely name one
105
+ * cannot be bound to disclosed bytes, so it reads as unreadable. This is an
106
+ * honesty guard, not a traversal fix (nothing here joins an id into a path);
107
+ * refusing an id can only move a verdict toward unknown, never toward verified.
201
108
  *
202
- * charset — anything outside letters, digits, dot, underscore, dash.
203
- * No separator of any platform survives it.
204
- * `.` and `..` — not names but path entries. Under the documented `.json`
205
- * convention they are harmless (`logs/..json` is a file);
206
- * under a consumer that writes `logs/<event_id>` or uses
207
- * the id as a directory, `..` IS the parent. The kernel
208
- * does not know the convention, so it refuses the two ids
209
- * that are hazardous under any of them.
210
- * trailing dot — Windows strips it, so `a.` and `a` are one file. Two
211
- * distinct records collapsing onto one path means one
212
- * record's bytes overwrite another's, and the verifier then
213
- * reports a MISMATCH — an accusation manufactured by a
214
- * filesystem rather than by a tamper.
215
- * device names — `CON`, `PRN`, `AUX`, `NUL`, `COM0-9`, `LPT0-9`, with or
216
- * without an extension. On Windows `CON.json` is still the
217
- * console device: the write appears to succeed and nothing
218
- * is on disk.
219
- * length — a POSIX path component is 255 bytes. The charset is
220
- * ASCII, so characters are bytes; 200 leaves the documented
221
- * `.json` and room for a prefix a consumer adds, and sits
222
- * far above any real id (a UUID is 36).
109
+ * Refused:
110
+ * charset anything outside letters, digits, dot, underscore and dash
111
+ * `.` and `..` path entries, not names
112
+ * trailing dot Windows strips it, so `a.` and `a` collide and one record's
113
+ * bytes would overwrite another's, a mismatch no tamper caused
114
+ * device names `CON`, `PRN`, `AUX`, `NUL`, `COM0-9`, `LPT0-9`, with or
115
+ * without an extension: Windows swallows the write
116
+ * length over 200, leaving room in a 255-byte path component
223
117
  *
224
- * ── WHAT IS DELIBERATELY *NOT* REFUSED ──────────────────────────────────────
225
- *
226
- * **Case collisions.** `A` and `a` are distinct ids that are one file on
227
- * macOS and Windows. They stay legal, because banning uppercase would refuse
228
- * ordinary ids for a hazard the kernel cannot see: this module takes a content
229
- * MAP, which is case-sensitive and has already resolved the question. The
230
- * collision happens in the transport, and `accountDisclosure` is explicitly a
231
- * convenience rather than the only way to build an account — a consumer
232
- * reading a DIRECTORY builds its own and is the one that can detect it.
233
- * Pinned as a decision, not left to be rediscovered.
118
+ * Case collisions (`A` and `a`) stay legal: the content map is case-sensitive,
119
+ * and only a consumer reading a directory can see the collision.
234
120
  */
235
121
  const SAFE_EVENT_ID = /^[A-Za-z0-9._-]+$/;
236
122
  const MAX_EVENT_ID_LENGTH = 200;
237
123
  const WINDOWS_DEVICE_NAME = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\.|$)/i;
238
- /** Why this id cannot name a file, or null if it can. Prose, for a surface. */
124
+ /** Why this id cannot name a file, or null if it can. */
239
125
  function unsafeEventIdReason(eventId) {
240
126
  if (!SAFE_EVENT_ID.test(eventId)) {
241
127
  return "only letters, digits, dot, underscore and dash are admitted";
@@ -255,32 +141,16 @@ function unsafeEventIdReason(eventId) {
255
141
  return null;
256
142
  }
257
143
  /**
258
- * Account for the disclosed bytes a pack handed us against the records its
259
- * bundle presents.
260
- *
261
- * `content` is the same map `verifyEvidenceBundle` takes — a plain object or a
262
- * `Map` — so a caller builds it once and uses it for both. Prefer the `Map`:
263
- * an `event_id` is producer-chosen, and on a plain object `__proto__` is not a
264
- * key but a prototype setter, so bytes written under that id never land. A
265
- * ZERO-BYTE entry is present, not missing: bytes were supplied, the check ran,
266
- * and the mismatch the verifier reports stands. A truncated log must not be
267
- * able to launder itself into "unknown".
144
+ * Accounts for the disclosed bytes in `content` against the records the bundle
145
+ * presents. `content` is the map `verifyEvidenceBundle` takes; prefer a `Map`,
146
+ * since on a plain object an `event_id` of `__proto__` never lands. A zero-byte
147
+ * entry is present, not missing, so a truncated log cannot read as unknown.
268
148
  *
269
- * A CONVENIENCE, NOT THE ONLY WAY TO BUILD AN ACCOUNT — and the difference
270
- * matters for one field. `extra` here can only ever name keys the content map
271
- * carries, so a transport whose surplus is not representable as a map entry
272
- * cannot report it through this function: a pack directory holding
273
- * `logs/notes.txt`, or a second file for a record already accounted for, is
274
- * invisible to a map built by iterating the bundle's own events. A consumer
275
- * reading a DIRECTORY should build `DisclosureAccount` itself from the real
276
- * listing and pass that to `gradePack` — the rule takes an account, never a
277
- * bundle, precisely so the transport can say things the map cannot.
149
+ * `extra` can only name map keys. A consumer reading a directory must build
150
+ * `DisclosureAccount` from the real listing to report surplus files.
278
151
  */
279
152
  export function accountDisclosure(bundle,
280
- // `SideContentMap`, which is what `hasSideContent` and `sideContentKeys`
281
- // below already take: an entry holding a string that is not hex IS an entry,
282
- // and typing it out of this signature made the one map a caller builds
283
- // unusable here (proof#147). A widening, so no caller moves.
153
+ // Any side-content entry counts, including a string that is not hex.
284
154
  content) {
285
155
  const expected = [];
286
156
  const present = [];
@@ -295,25 +165,13 @@ content) {
295
165
  expected.push(eventId);
296
166
  const unsafe = unsafeEventIdReason(eventId);
297
167
  if (unsafe !== null) {
298
- // The prefix is stable so a surface can group these; the suffix says
299
- // which rule refused it, because "not a safe file name" alone leaves a
300
- // holder with no idea what to change.
168
+ // A stable prefix to group on, and a suffix naming the rule that refused it.
301
169
  unreadable.push({ eventId, reason: `event_id is not a safe file name: ${unsafe}` });
302
170
  continue;
303
171
  }
304
- // An OWN entry, NOT `content[eventId] === undefined`. Every key on
305
- // Object.prototype — `constructor`, `toString`, `valueOf`, `__proto__` —
306
- // passes SAFE_EVENT_ID (see the regex above: it admits letters, digits,
307
- // dot, underscore and dash, so every one of them is a legal id here) and
308
- // resolves to an inherited value, so an indexed lookup reported a record as
309
- // PRESENT when nothing at all was disclosed for it. That is the module's
310
- // own founding defect (absent evidence read as good evidence) reappearing
311
- // one layer down: the verdict stayed `incomplete` because the verifier
312
- // still had no bytes to bind, but with ZERO causes — a surface saying
313
- // "incomplete" and unable to say why. `__proto__` is worse still: assigning
314
- // it on a plain object either silently drops the write or replaces the
315
- // object's prototype outright, so bytes a holder genuinely supplied never
316
- // reach the map, and the indexed read then claimed they had.
172
+ // An own entry, not an indexed read: `constructor`, `__proto__` and other
173
+ // Object.prototype keys pass SAFE_EVENT_ID and would read as present when
174
+ // nothing was disclosed.
317
175
  if (!hasSideContent(content, eventId))
318
176
  missing.push(eventId);
319
177
  else
@@ -324,31 +182,23 @@ content) {
324
182
  return { expected, present, missing, unreadable, extra };
325
183
  }
326
184
  /**
327
- * A JSON number read by its value: an integer within 2**53 - 1, or null.
328
- * `Number.isSafeInteger`, mirrored by `_js_safe_integer` in
329
- * `src/sanning_proof/chain.py`, so `2.0` is 2 in both kernels and
330
- * `9007199254740993`, which this parser rounds, is unreadable in both.
185
+ * A JSON number read by its value: an integer within 2**53 - 1, or null. So
186
+ * `2.0` is 2, and `9007199254740993`, which the parser rounds, is unreadable.
331
187
  */
332
188
  export function safeInteger(value) {
333
- // `+ 0` folds -0 into 0, which Python's `int(-0.0)` already is.
189
+ // `+ 0` folds -0 into 0.
334
190
  return typeof value === "number" && Number.isSafeInteger(value) ? value + 0 : null;
335
191
  }
336
- /** The `leaf_count` a checkpoint's SIGNED record commits to, or null if unreadable. */
192
+ /** The `leaf_count` a checkpoint's signed record commits to, or null if unreadable. */
337
193
  export function signedLeafCount(checkpoint) {
338
194
  const hex = checkpoint?.event_bytes;
339
195
  if (typeof hex !== "string" || hex.length === 0)
340
196
  return null;
341
197
  try {
342
- // `jcsParse`, matching `signed_leaf_count` in `src/sanning_proof/grading.py`.
343
- // The pair used to agree by pointing two LENIENT decoders at one input, and
344
- // that held for invalid UTF-8 and nowhere else: `TextDecoder` EATS a
345
- // leading byte-order mark where Python's `errors="replace"` keeps it, so
346
- // `BOM + JCS(checkpoint)` yielded a signed leaf count here and unreadable
347
- // there, and that count decides a completeness note (proof#140 gate 5).
198
+ // The strict signed-bytes reader: a leading byte-order mark is refused, not
199
+ // eaten, since this count decides a verdict.
348
200
  const record = jcsParse(hexToBytes(hex));
349
- // Read BY VALUE, the model JCS signs: a record spelling `3.0`, `3e0` or
350
- // `30e-1` commits to 3, and since A38 (proof#197) this count decides a
351
- // verdict, not only a note. Past 2**53 - 1 it is unreadable, as in Python.
201
+ // Read by value, the model JCS signs: `3.0`, `3e0` and `30e-1` all commit to 3.
352
202
  return safeInteger(record?.event?.leaf_count);
353
203
  }
354
204
  catch {
@@ -356,16 +206,10 @@ export function signedLeafCount(checkpoint) {
356
206
  }
357
207
  }
358
208
  /**
359
- * Omission — the failure a per-record check structurally cannot see.
360
- *
361
- * Drop records from a pack and every SURVIVOR still verifies perfectly:
362
- * signatures, inclusion proofs, content hashes, all green. The number that
363
- * exposes it is already in the pack and already signed — each checkpoint's
364
- * record commits to the `leaf_count` of the tree it anchored. This compares
365
- * that against how many records the pack presents for that checkpoint.
366
- *
367
- * A checkpoint whose signed count cannot be read is `unreadable`: a
368
- * completeness check we could not run is UNKNOWN, never a pass.
209
+ * Omission, which a per-record check cannot see: drop records and every
210
+ * survivor still verifies. Compares each checkpoint's signed `leaf_count` with
211
+ * the records the pack presents for it. An unreadable count is `unreadable`:
212
+ * a check we could not run is unknown, never a pass.
369
213
  */
370
214
  export function completenessNotes(bundle) {
371
215
  const events = eventsOf(bundle);
@@ -374,19 +218,13 @@ export function completenessNotes(bundle) {
374
218
  const key = joinKeyOf(cp);
375
219
  const checkpointRef = key ?? "(no checkpoint_ref)";
376
220
  const signed = signedLeafCount(cp);
377
- // No readable key means we cannot count what was presented against it, so
378
- // completeness is UNKNOWN — the same answer as an unreadable leaf_count,
379
- // and never a shortfall.
221
+ // No readable key: completeness is unknown, never a shortfall.
380
222
  if (signed === null || key === null) {
381
223
  notes.push({ checkpointRef, signed: null, presented: null, kind: "unreadable" });
382
224
  continue;
383
225
  }
384
- // Compared case-folded. The verifier recomputes `checkpoint_ref` and
385
- // accepts it in any case (evidence-bundle.md §5.1 names lowercase hex as
386
- // the form, and the kernel lowercases before comparing), so a pack whose
387
- // checkpoint spells its ref in upper case still verifies. Grading it
388
- // case-sensitively reported every record as omitted: a completeness
389
- // accusation manufactured by letter case. The ref shown is the pack's own.
226
+ // Case-folded, because the verifier accepts `checkpoint_ref` in any case;
227
+ // otherwise an uppercase ref would report every record as omitted.
390
228
  const want = checkpointMatchKey(cp);
391
229
  const presented = events.filter((e) => inclusionMatchKey(e?.inclusion) === want).length;
392
230
  if (presented < signed)
@@ -396,16 +234,13 @@ export function completenessNotes(bundle) {
396
234
  }
397
235
  return notes;
398
236
  }
399
- // ---- The rule ----------------------------------------------------------------
400
237
  /**
401
- * Every reason this pack is not `verified`, in the order a reader should hear
402
- * them: what is missing from the whole, then what is missing from the parts.
238
+ * Every reason this pack is not `verified`, in reading order: what the pack
239
+ * claimed, then what is missing from the whole, then from the parts.
403
240
  */
404
- export function gradeCauses(account, notes, declaration, withheld = []) {
405
- // The `event_id`s WITHHELD BY THEIR DECLARATION (rule version 6) are taken
406
- // out of `expected` and `missing` before the disclosure causes are computed,
407
- // so a pack whose every record its declaration withholds earns neither
408
- // `no_disclosure` nor `missing_disclosure`.
241
+ export function gradeCauses(account, notes, declaration, withheld = [], events = []) {
242
+ // Records withheld by their declaration are not expected, so a pack that
243
+ // withholds every record earns no disclosure cause.
409
244
  if (withheld.length > 0) {
410
245
  const gone = new Set(withheld);
411
246
  account = {
@@ -415,11 +250,8 @@ export function gradeCauses(account, notes, declaration, withheld = []) {
415
250
  };
416
251
  }
417
252
  const causes = [];
418
- // What the pack CLAIMED to hold comes first, because it frames everything
419
- // below it: a reader told "four records are missing from what this pack says
420
- // it contains" reads the per-record findings differently. Every deviation
421
- // gets its own cause (one defect, one line), so a pack that deviates in
422
- // four ways can never be reported as one vague shortfall.
253
+ // Declaration deviations first, since they frame the rest. One cause per
254
+ // deviation, so several are never reported as one vague shortfall.
423
255
  for (const deviation of declaration?.deviations ?? []) {
424
256
  causes.push({
425
257
  kind: "declaration_deviation",
@@ -428,9 +260,8 @@ export function gradeCauses(account, notes, declaration, withheld = []) {
428
260
  reason: renderDeviation(deviation),
429
261
  });
430
262
  }
431
- // Then what the pack and the UNSIGNED content beside it do not agree on
432
- // (proof#147). A cause, so the pack cannot read clean over it; NOT a
433
- // deviation, so it never reaches `failed`.
263
+ // A mismatch with unsigned side content: a cause, so the pack cannot read
264
+ // clean, but not a deviation, so it never reaches `failed`.
434
265
  for (const finding of declaration?.contentFindings ?? []) {
435
266
  causes.push({
436
267
  kind: "declaration_content_dependent",
@@ -439,6 +270,26 @@ export function gradeCauses(account, notes, declaration, withheld = []) {
439
270
  reason: renderContentFinding(finding),
440
271
  });
441
272
  }
273
+ // Then every reason of each agent record that does not read verified (agent-events.md
274
+ // section 10). A reason that reads incomplete is `agent_record_incomplete`; every other
275
+ // reason is `agent_record_nonconforming`. A finding is never a cause: it is reported
276
+ // beside the record and moves nothing.
277
+ for (const e of events) {
278
+ const verdict = e.agent?.verdict;
279
+ if (verdict !== "failed" && verdict !== "incomplete")
280
+ continue;
281
+ const absent = [...(e.agent?.absent ?? [])];
282
+ for (const r of e.agent?.reasons ?? []) {
283
+ const listed = r.reason === "listed_record_absent" ? absent.shift() : undefined;
284
+ causes.push({
285
+ kind: INCOMPLETE_REASONS.has(r.reason) ? "agent_record_incomplete" : "agent_record_nonconforming",
286
+ ...(e.eventId === undefined ? {} : { firstEventId: e.eventId }),
287
+ reason: r.reason,
288
+ at: r.at,
289
+ ...(listed === undefined ? {} : { listedEventId: listed }),
290
+ });
291
+ }
292
+ }
442
293
  for (const note of notes) {
443
294
  if (note.kind === "omitted") {
444
295
  causes.push({
@@ -462,10 +313,7 @@ export function gradeCauses(account, notes, declaration, withheld = []) {
462
313
  causes.push({ kind: "leaf_count_unreadable", checkpointRef: note.checkpointRef });
463
314
  }
464
315
  }
465
- // "Nothing at all was disclosed" is reported once, as itself. It is the
466
- // regression that rendered a green banner over zero hashed bytes, and a
467
- // reader deserves to be told the pack disclosed nothing rather than counting
468
- // N absences.
316
+ // A pack that disclosed nothing is told as that, once, rather than as N absences.
469
317
  if (account.expected.length > 0 && account.present.length === 0 && account.unreadable.length === 0) {
470
318
  causes.push({ kind: "no_disclosure", count: account.expected.length, of: account.expected.length });
471
319
  }
@@ -490,96 +338,41 @@ export function gradeCauses(account, notes, declaration, withheld = []) {
490
338
  return causes;
491
339
  }
492
340
  /**
493
- * The kernel headline this rule is willing to repeat, as an ALLOW-LIST.
494
- *
495
- * `GradeInput.status` is deliberately `string` so a surface can grade a result
496
- * from a verifier this kernel did not write. The cost of that was a hole: the
497
- * status used to be cast straight to `PackVerdict`, so `gradePack` would hand
498
- * back whatever it was given — `"mixed"` and `"not_found"` (legal, if
499
- * unexpected), but equally `"banana"`, `""` and `"SAFE"`. A surface that trusts
500
- * the type and renders the verdict verbatim would then print the one word
501
- * `evidence-bundle.md` §6 forbids, over a pack this rule never graded.
502
- *
503
- * So: the five words of the §3 vocabulary that a bundle status can pass up to
504
- * a pack pass through, and ANYTHING ELSE is
505
- * `incomplete`. A verdict we cannot interpret is a check we could not run, and
506
- * the answer to that is UNKNOWN — never a pass, and never an accusation.
507
- * (`failed` and `malformed` are handled before this is reached.)
508
- */
509
- const PASS_THROUGH = new Set([
510
- "verified",
511
- "partial",
512
- "pending",
513
- "not_found",
514
- "mixed",
515
- ]);
516
- function passThrough(status) {
517
- return typeof status === "string" && PASS_THROUGH.has(status)
518
- ? status
519
- : "incomplete";
520
- }
521
- /**
522
- * The verdict a surface may print, and the counts it may print beside it.
341
+ * The verdict a surface may print, and the counts beside it.
523
342
  *
524
- * The invariant, stated so a mutation test can kill it: `verified` is reachable
525
- * ONLY when every record's supplied bytes bound to its own commitment, or
526
- * (rule version 6) the record is withheld by its declaration, and nothing is
343
+ * Invariant: `verified` only when every record's bytes bound to its own
344
+ * commitment or the record is withheld by its declaration, and nothing is
527
345
  * missing, unreadable, surplus or omitted.
528
346
  *
529
- * Precedence: malformed > failed > incomplete > the kernel's own headline.
530
- * A hard verdict wins — "the bytes are absent" is never reported as failure
531
- * (§5.1: undetermined, not failed), and a proven mismatch is never softened
532
- * into absence. The last branch passes the kernel's headline through unchanged:
533
- * this rule DOWNGRADES a verdict, it never invents one.
534
- *
535
- * ── the declaration (optional fourth argument) ──────────────────────────────
536
- *
537
- * `declaration` is what `checkDeclaration` in `./declaration.ts` made of the
538
- * pack's own signed statement of what it holds (`evidence-bundle.md` §5.4). A
539
- * deviation from it is a PROVEN mismatch between what the pack claimed and
540
- * what it carries, so it FAILS the pack and each deviation becomes its own
541
- * cause. It must never soften into absence: that is how a trimmed hand-over
542
- * reads as an innocent gap.
543
- *
544
- * A check that came back `partial` splits in THREE (proof#147), and the first
545
- * of the three is not about the declaration's readability at all. The other
546
- * two split on whether the body carried a `declaration` member.
347
+ * Every problem (the bundle's, each cause, the declaration's) maps to one claim
348
+ * and one state (`claims.ts`) and the verdict is read off the claims (rule
349
+ * version 9): `failed` where a claim is disproven, else `incomplete` where one
350
+ * is not proven, else `verified`. Absent bytes are never a disproof (section
351
+ * 5.1), and a proven mismatch is never softened into absence. An agent record's
352
+ * findings move nothing.
547
353
  *
548
- * content-dependent the pack and the UNSIGNED content beside it do not
549
- * line up (proof#147). Each finding is its own cause
550
- * and the pack is `incomplete`. It must never reach
551
- * `failed`: whoever assembles a case fills in the
552
- * side-content map, so an accuser holding an intact
553
- * pack could otherwise put its holder into `failed`
554
- * with no key at all, by dropping one file or adding
555
- * one. `failed` is left to what a signature covers.
556
- * no declaration at all nothing moves. Every pack built before §5.4 is
557
- * this case, and re-grading them would move pinned
558
- * exit codes for artifacts already in customers'
559
- * hands.
560
- * present and unreadable the pack is `incomplete`. `declaration_version` is
561
- * chosen by the same party that assembles the pack
562
- * and has no accept-set, so a holder who trims a
563
- * record and then writes one unreadable field can
564
- * re-seal the pack with its own key and hand a
565
- * reader a clean verdict over evidence of the trim
566
- * sitting inside the signed body. UNKNOWN is the
567
- * only honest answer to a claim the reader cannot
568
- * read, and it adds no cause: `PackGrade.declaration`
569
- * says why.
570
- *
571
- * `verified` adds nothing and moves nothing.
572
- *
573
- * Omitting the argument entirely is the pre-§5.4 behavior, unchanged, and
574
- * `PackGrade.declaration` is then absent.
354
+ * `declaration` is `checkDeclaration`'s result for the pack's signed statement
355
+ * of what it holds (§5.4):
356
+ * deviation a proven mismatch: the pack fails, one cause each
357
+ * content-dependent the pack and its unsigned side content disagree:
358
+ * `incomplete`, never `failed`, since whoever builds
359
+ * the side-content map could otherwise fail an intact
360
+ * pack without a key
361
+ * no declaration nothing moves
362
+ * present and unreadable `incomplete`, with no cause and one problem,
363
+ * `pack_declaration_unreadable` (or
364
+ * `declaration_not_checked` for a chain claim this
365
+ * call was given no reading for): the pack's holder
366
+ * picks `declaration_version`, so an unreadable one
367
+ * could otherwise hide a trimmed record
368
+ * Omitting the argument leaves `PackGrade.declaration` absent.
575
369
  */
576
370
  export function gradePack(result, account, notes, declaration) {
577
371
  const events = Array.isArray(result?.events) ? result.events : [];
578
372
  const recordsVerified = events.filter((e) => e.ok && e.contentOk === true).length;
579
373
  const recordsFailed = events.filter((e) => e.ok === false || e.contentOk === false).length;
580
- // WITHHELD BY ITS DECLARATION (rule version 6, 5.4.5 v1.20, A22, D5). All
581
- // four conditions, each on its own. `declaredWithheld` is empty for an
582
- // absent, unreadable or malformed declaration.
374
+ // Withheld by its declaration (§5.4.5) only when all four conditions hold.
375
+ // `declaredWithheld` is empty for an absent, unreadable or malformed declaration.
583
376
  const declaredWithheld = new Set(declaration?.declaredWithheld ?? []);
584
377
  const withheldEventIds = events
585
378
  .filter((e) => typeof e.eventId === "string" &&
@@ -590,22 +383,19 @@ export function gradePack(result, account, notes, declaration) {
590
383
  .map((e) => e.eventId);
591
384
  const recordsWithheld = withheldEventIds.length;
592
385
  const recordsUndetermined = events.length - recordsVerified - recordsFailed - recordsWithheld;
593
- const causes = gradeCauses(account, notes, declaration, withheldEventIds);
594
- let verdict;
595
- if (result?.status === "malformed")
596
- verdict = "malformed";
597
- else if (result?.status === "failed" || recordsFailed > 0)
598
- verdict = "failed";
599
- else if (declaration?.verdict === "failed")
600
- verdict = "failed";
601
- else if (recordsUndetermined > 0 || causes.length > 0)
602
- verdict = "incomplete";
603
- else if (declaration?.present === true && declaration.verdict === "partial")
604
- verdict = "incomplete";
605
- else
606
- verdict = passThrough(result?.status);
386
+ const causes = gradeCauses(account, notes, declaration, withheldEventIds, events);
387
+ const problems = packProblems(result, events, account, causes, declaration, withheldEventIds);
388
+ if (recordsUndetermined > 0 && !problems.some((p) => p.claim === "unchanged")) {
389
+ // Fail closed: a record whose bytes were not bound and that no cause names still
390
+ // holds the pack below verified.
391
+ problems.push(problem("records_not_bound", [[0, null]]));
392
+ }
393
+ const outcome = verdictOf(claimsOf(problems));
394
+ const verdict = outcome.status;
607
395
  const grade = {
608
396
  verdict,
397
+ ...(outcome.reason_kind === undefined ? {} : { reasonKind: outcome.reason_kind }),
398
+ problems,
609
399
  recordsVerified,
610
400
  recordsFailed,
611
401
  recordsUndetermined,
@@ -617,48 +407,102 @@ export function gradePack(result, account, notes, declaration) {
617
407
  grade.declaration = declaration.verdict;
618
408
  return grade;
619
409
  }
620
- // ---- Presentation (optional) -------------------------------------------------
621
410
  /**
622
- * The house wording for one cause.
623
- *
624
- * Offered, not imposed: a surface with its own voice reads the fields on
625
- * `PackGradeCause` instead. What must not vary between surfaces is the verdict
626
- * and the counts, and those come from `gradePack`.
627
- *
628
- * The `subject` names what the pack is from the reader's position — "this pack"
629
- * on a page someone dropped a file onto, "the pack" in a CLI report.
411
+ * Every problem of one pack, read on its own (pack position 0): the bundle's, then each
412
+ * cause's, then the declaration's.
413
+ */
414
+ function packProblems(result, events, account, causes, declaration, withheld) {
415
+ const out = bundleProblems(result, 0);
416
+ const ids = events.map((e) => e.eventId ?? null);
417
+ const records = (...eventIds) => {
418
+ const wanted = eventIds.map((e) => e ?? null);
419
+ const found = [];
420
+ ids.forEach((e, i) => {
421
+ if (wanted.includes(e))
422
+ found.push([0, i]);
423
+ });
424
+ return found.length > 0 ? found : [[0, null]];
425
+ };
426
+ const gone = new Set(withheld);
427
+ for (const c of causes) {
428
+ if (c.kind === "agent_record_nonconforming" || c.kind === "agent_record_incomplete") {
429
+ // A record the closing record lists and the pack does not hold is the item, named by
430
+ // its own event_id, so the line counts the records that are not held.
431
+ out.push(problem(c.kind, c.listedEventId !== undefined ? [[0, null, c.listedEventId]] : records(c.firstEventId), {
432
+ rule: c.reason ?? null,
433
+ }));
434
+ }
435
+ else if (c.kind === "records_omitted") {
436
+ out.push(problem(c.kind, [[0, null]], { count: c.count, of: c.signed, presented: c.presented }));
437
+ }
438
+ else if (c.kind === "no_disclosure") {
439
+ out.push(problem(c.kind, records(...account.expected), { count: c.count }));
440
+ }
441
+ else if (c.kind === "missing_disclosure") {
442
+ const missing = account.missing.filter((e) => !gone.has(e));
443
+ out.push(problem(c.kind, records(...missing), { count: c.count, of: c.of }));
444
+ }
445
+ else if (c.kind === "unreadable_disclosure" || c.kind === "declaration_content_dependent") {
446
+ out.push(problem(c.kind, records(c.firstEventId)));
447
+ }
448
+ else if (c.kind === "unaccounted_disclosure") {
449
+ out.push(problem(c.kind, [[0, null]], { count: c.count }));
450
+ }
451
+ else if (c.kind === "declaration_deviation" && c.deviation === "declaration_malformed") {
452
+ // A declaration in a version this reader knows, signed with the pack, that does not
453
+ // parse: the producer broke its own format, and nothing was proven altered.
454
+ out.push(problem("declaration_nonconforming", [[0, null]]));
455
+ }
456
+ else {
457
+ out.push(problem(c.kind, [[0, null]]));
458
+ }
459
+ }
460
+ if (declaration != null && declaration.present) {
461
+ if (declaration.verdict === "failed" && declaration.deviations.length === 0) {
462
+ out.push(problem("pack_declaration_deviates", [[0, null]]));
463
+ }
464
+ else if (declaration.verdict === "incomplete" && declaration.contentFindings.length === 0) {
465
+ out.push(problem(declaration.version == null ? "pack_declaration_unreadable" : "declaration_not_checked", [[0, null]]));
466
+ }
467
+ }
468
+ return out;
469
+ }
470
+ /**
471
+ * The house wording for one cause. Optional: a surface may read the fields
472
+ * instead; the verdict and counts are what must not vary. `subject` is "this
473
+ * pack" on a page and "the pack" in a CLI report.
630
474
  */
631
475
  export function renderCause(cause, subject = "this pack") {
632
476
  switch (cause.kind) {
633
477
  case "records_omitted":
634
478
  return (`checkpoint ${cause.checkpointRef} commits to ${cause.signed} record(s); ${subject} presents ` +
635
- `${cause.presented} — ${cause.count} record(s) OMITTED`);
479
+ `${cause.presented}: ${cause.count} record(s) omitted`);
636
480
  case "records_surplus":
637
481
  return (`checkpoint ${cause.checkpointRef} commits to ${cause.signed} record(s); ${subject} presents ` +
638
- `${cause.presented} — more records than the signed tree accounts for`);
482
+ `${cause.presented}; more records than the signed tree accounts for`);
639
483
  case "leaf_count_unreadable":
640
- return `checkpoint ${cause.checkpointRef} has no readable signed leaf_count — completeness is UNKNOWN`;
484
+ return `checkpoint ${cause.checkpointRef} has no readable signed leaf_count; completeness is unknown`;
641
485
  case "no_disclosure":
642
- return (`no event bytes were disclosed — content binding is UNKNOWN for all ${cause.count} ` +
486
+ return (`no event bytes were disclosed; content binding is unknown for all ${cause.count} ` +
643
487
  `record(s), not verified`);
644
488
  case "missing_disclosure":
645
- return (`${cause.count} of ${cause.of} record(s) disclose no bytes (first: ${cause.firstEventId}) — ` +
646
- `content binding is UNKNOWN, not verified`);
489
+ return (`${cause.count} of ${cause.of} record(s) disclose no bytes (first: ${cause.firstEventId}); ` +
490
+ `content binding is unknown, not verified`);
647
491
  case "unreadable_disclosure":
648
492
  return `the disclosure for ${cause.firstEventId} is unreadable: ${cause.reason}`;
649
493
  case "unaccounted_disclosure":
650
494
  return (`${cause.count} disclosed file(s) are not accounted for by any record in this bundle ` +
651
495
  `(first: ${cause.firstEventId})`);
652
- // Rendered once, by `renderDeviation` in `./declaration.ts`, and carried
653
- // here on `reason`. Re-deriving the sentence from the cause fields would
654
- // put the declaration's wording in two places, and one of the two would be
655
- // edited alone.
496
+ // Worded once, by `renderDeviation`, and carried on `reason`.
497
+ case "agent_record_nonconforming":
498
+ return `the agent record ${cause.firstEventId} does not conform to its vocabulary: ${cause.reason} at ${cause.at}`;
499
+ case "agent_record_incomplete":
500
+ return `the agent record ${cause.firstEventId} could not be fully read: ${cause.reason} at ${cause.at}`;
656
501
  case "declaration_deviation":
657
502
  return `this pack deviates from its own declaration: ${cause.reason}`;
658
503
  case "declaration_content_dependent":
659
- // Named as what the reader could not establish, never as what the
660
- // holder did: the bytes it turns on are unsigned and whoever assembled
661
- // the case chose them (proof#147).
504
+ // Worded as what the reader could not establish, never as what the
505
+ // holder did: the side content is unsigned.
662
506
  return (`${subject} and the unsigned content handed in beside it do not line up: ` +
663
507
  `${cause.reason}`);
664
508
  }