@sanning/proof 0.19.1 → 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 +339 -822
  9. package/dist/case.js +667 -1160
  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 +814 -1328
  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 +131 -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 +573 -581
  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/case.js CHANGED
@@ -1,684 +1,293 @@
1
1
  /**
2
- * One verdict for a CASE, across packs and across organizations (anchor#198).
3
- *
4
- * The TypeScript half of `src/sanning_proof/case.py`. Both kernels are held to
5
- * the same corpus set, `test-vectors/case/`, so neither can pass by agreeing
6
- * with the other.
7
- *
8
- * WHY THIS LAYER EXISTS, AND WHAT IT IS THE CLOSURE FOR
9
- * =====================================================
10
- *
11
- * `envelope-spec.md` 4.1 reads records. `evidence-bundle.md` 5.4 reads one pack
12
- * against its own declaration. Neither can answer the question an audit team
13
- * actually asks, and 4.1 (v1.10, A15) and 5.4.2 rule 7 (v1.10) now say so in
14
- * these words:
15
- *
16
- * **A pack a party assembles about itself is not self-proving about what
17
- * that party LEFT OUT.**
18
- *
19
- * `handoffs[]` MAY be empty (5.4.1), so a holder declares a record in
20
- * `records[]` and claims it nowhere: rule 2 cannot reach it because it is
21
- * declared, rule 7 cannot reach it because it is unclaimed, and the pack reads
22
- * `verified` with no deviation, which is the same reading the honest pack gets.
23
- * The corpus pins that pack as EXPECTED
24
- * (`test-vectors/declaration/declaration-unclaimed-record-01`) rather than
25
- * leaving it to be rediscovered. Six repairs treated a property of one-sided
26
- * evidence as a defect in a rule (proof#137, ratified by Phil 2026-09-22), and
27
- * the omission happens BEFORE any reader runs, so no rule at either of those
28
- * layers reaches it.
29
- *
30
- * **This module is the closure, and the reason it can be is that it reads
31
- * several parties' packs at once.** A `handoff.sent` with no counterpart among
32
- * the records of the case is always NAMED, and when a pack signed by the key
33
- * that record names is in the case, the reading says that pack holds no
34
- * counterpart. No party can make a missing counterpart read clean.
35
- *
36
- * What it does NOT establish is that a party which presented no pack signed by
37
- * the named key recorded nothing. Nothing binds a pack's wrapper key to the
38
- * keys its records are signed with, so a counterparty that hands over its
39
- * account under any other key, or inside an export an issuer signed, or not at
40
- * all, gets the reading that says only that no pack in the case is signed by
41
- * the named key. Proving that a party did NOT record something needs that
42
- * party's account or a completeness mechanism over its records, and which of
43
- * those this reading waits for is proof#143.
44
- *
45
- * WHAT THAT MEANS FOR HOW THE JOIN IS BUILT
46
- * =========================================
47
- *
48
- * **The case reader does not consult `handoffs[]` to pair anything.** That is
49
- * not an oversight, it is the whole mechanism. The declaration is written by
50
- * the party being audited, so a join keyed on it inherits exactly the hole it
51
- * is meant to close: claim the clean pair, keep the tampered receipt
52
- * unclaimed, and a declaration-keyed reader never grades the receipt at all.
53
- * This reader pairs records with records, through `readHandoff`, over the
54
- * union of every record every pack in the case supplied. `handoffs[]` is read
55
- * for ONE thing and one only: to name a record its own holder DECLARED and
56
- * claimed nowhere (`RECORD_UNCLAIMED`), which is the pinned vector above
57
- * becoming visible. That claim check runs over every record a pack declares,
58
- * and not only over the ones 4.1 governs, because "is this a conformant
59
- * hand-off record" is an answer the audited party writes.
60
- *
61
- * **Matching is on hashes and identifiers alone and NEVER on tenancy**
62
- * (anchor#198, and DL-60: producer identity gates what the plane accepts and
63
- * never whether evidence verifies). The union pile is not partitioned by pack,
64
- * by issuer, by `tenant_id` or by organization anywhere in this module, so a
65
- * pair assembled by two organizations reads exactly as a pair assembled by
66
- * one. There is no code path here that reads a tenant.
67
- *
68
- * THE DEFECT CLASS THIS MODULE IS WRITTEN AGAINST
69
- * ===============================================
70
- *
71
- * Seventeen findings on this program share one generator: **a deviation
72
- * designed as a comparison between two present things, where the deviation
73
- * itself removes one of them, so the code takes the "nothing to compare"
74
- * branch written for the honest case.** A rule shaped that way reads the
75
- * absence of a signal as "fine" rather than as "I could not tell", and a real
76
- * deviation grades clean.
77
- *
78
- * Acceptance criterion 3 of anchor#198 is made of exactly that shape: *a
79
- * one-sided hand-off is an exception, not a failure*. So the four rules below
80
- * are structural and are the thing to preserve in any change here.
81
- *
82
- * 1. **Every exception is NAMED and appears in the output, and every exception
83
- * contributes a verdict.** There is no path through this module that
84
- * removes a record, a pack or a claim from the reading. An empty
85
- * `exceptions` is the ONLY shape a reader may read as clean, and `readCase`
86
- * is invariant: `verified` is reachable only when that array is empty.
87
- * 2. **Nothing a party writes decides whether grading happens.** No graded
88
- * value is part of any key that selects a code path. The pile is everything
89
- * every pack supplied; there is no filter.
90
- * 3. **Every field that selects a code path and is chosen by the party being
91
- * audited has an accept-set, and an unknown value goes to a named exception
92
- * rather than to silence.** `body_type` is `CASE_BODY_TYPES`;
93
- * `declaration_version` is `checkDeclaration`'s; `event_type` and
94
- * `event.counterparty_public_key` are 4.1's, through `readHandoffs`. A pack
95
- * that supplies zero records is `PACK_HOLDS_NO_RECORDS` and a case with no
96
- * hand-off record in it at all is `CASE_HOLDS_NO_HANDOFF_RECORDS`, because
97
- * "nothing to grade" is never "nothing is wrong".
98
- * 4. **The 4.1 predicates are CALLED, never restated.** `readHandoffs`,
99
- * `normalizedSigningKey` and `packRecords` are the one implementation of
100
- * each rule. A validation copied beside the one that enforces it drifts,
101
- * and a copy that is looser blesses a value the real one then rejects.
102
- *
103
- * EVERY EARLY EXIT IN THIS MODULE, AND WHAT IT SKIPS
104
- * ==================================================
105
- *
106
- * Three defects on this layer were one shape: an exit between a record
107
- * entering the pile and a check running over it, with the audited party
108
- * choosing whether the exit was taken. So the exits are enumerated here rather
109
- * than left to be rediscovered a fourth time. `src/sanning_proof/case.py` has
110
- * the same exits in the same order, and the pull request that added this list
111
- * carries the same table.
112
- *
113
- * 1. **No packs** (`return`). Skips everything. Reached by the reader, who
114
- * chose what to hand in. Named: `CASE_HOLDS_NO_PACKS`.
115
- * 2. **A wrapper key this reader cannot read** (`continue`). Skips the bundle
116
- * verification, the declaration check and every record in the pack. The
117
- * holder writes the key, so it steers here freely, and the pack is NAMED
118
- * (`PACK_KEY_UNREADABLE`). Both bundle verifiers now refuse every such
119
- * spelling in the same words (proof#142), so this exit and exit 3 agree on
120
- * which packs they take; it stays first because it names WHY. While any
121
- * pack takes this exit, a one-sided record whose named key signed no pack
122
- * reads `COUNTERPART_ABSENT_SIGNER_UNKNOWN` and never the innocent
123
- * `COUNTERPART_ABSENT`.
124
- * 3. **The wrapper does not hold** (`continue`): `signatureOk` and
125
- * `bodyHashOk` are not both true. Skips the declaration check and every
126
- * record in the pack, and the key it claims is never counted as a pack
127
- * signer. **This is the forger's case and the only exit that turns on
128
- * provenance**: those two fields are the whole of what ties a pack to a
129
- * key. Reached by whoever built the pack, and by anyone who alters its
130
- * unsigned `signature` field, which is indistinguishable from a forgery.
131
- * Named: `PACK_DOES_NOT_VERIFY`, and it moves one-sided records to
132
- * `COUNTERPART_ABSENT_SIGNER_UNKNOWN` exactly as exit 2 does.
133
- *
134
- * A pack whose wrapper HOLDS is never sent here, whatever is wrong inside
135
- * it (gate 3 on proof#140): its key is counted, its own bundle verdict is
136
- * `PACK_BUNDLE_NOT_VERIFIED` when that verdict is not `verified`, and each
137
- * record is graded on its own envelope below.
138
- * 3b. **The bundle verdict is `malformed`** (`continue`, after exit 3 and after
139
- * `PACK_BUNDLE_NOT_VERIFIED`). The body is one the verifier could not read,
140
- * so it graded no record. The key is already counted.
141
- * 4. **`body_type` outside `CASE_BODY_TYPES`** (`continue`). Skips the
142
- * declaration check and every record in the pack. The holder writes
143
- * `body_type`, so the holder steers here freely, which is why it is named
144
- * every time (`PACK_BODY_TYPE_UNKNOWN`) and why the pack's wrapper key is
145
- * counted ABOVE it: a counterparty cannot leave the damning counterpart
146
- * line by choosing a body (F3). `sanning.evidence.export/v2` lands here and
147
- * is not unwrapped.
148
- * 5. **No records** (`continue`). Skips the declaration check, over nothing.
149
- * The holder steers here by handing over an empty `events[]`. Named:
150
- * `PACK_HOLDS_NO_RECORDS`; the wrapper key is already counted.
151
- * 5b. **A record under a quarantined profile** and 5c. **a record whose own
152
- * envelope does not verify** (`continue`). Each skips 4.1 for that record,
153
- * its candidacy for any other record, and the claim check, because 4.1
154
- * compares only envelopes that verified (`HeldRecord`). Named:
155
- * `RECORD_PROFILE_QUARANTINED` (`incomplete`, pending DL-63) and
156
- * `RECORD_ENVELOPE_DOES_NOT_VERIFY` (`failed`). Neither removes a named
157
- * failure from another record: the other records of the pack are still
158
- * graded, and pairs still form over every record that verified.
159
- * 6. **`event_bytes` withheld** and 7. **`event_bytes` unreadable**
160
- * (`continue`). Each skips 4.1 for that record, its candidacy for any other
161
- * record, and the claim check. The holder steers into both by what it
162
- * discloses. Each is named in the claim check's place
163
- * (`RECORD_BYTES_WITHHELD` `incomplete`, `RECORD_BYTES_UNREADABLE`
164
- * `malformed`), so neither buys a cleaner reading than an unclaimed record
165
- * would get, and neither decides which counterpart kind another record
166
- * gets, since that reads wrapper keys.
167
- *
168
- * Not exits: a record whose envelope verifies and whose content does not
169
- * match its commitment is `RECORD_CONTENT_MISMATCH` where that content is
170
- * inside the signed body and `RECORD_CONTENT_DEPENDENT` where it was handed
171
- * in beside the pack (proof#159), and one that fails
172
- * another check inside its pack is `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`.
173
- * Both are named and both STAY in the pile, so neither unsigned content
174
- * handed in beside a pack nor a broken inclusion proof removes a record
175
- * from pairing.
176
- * 8. **`not_a_handoff_record`** (`continue`). Skips the hand-off count and the
177
- * counterpart, ambiguity and pair reporting, which 4.1 does not give a
178
- * record outside its scope. The holder steers here by respelling a field.
179
- * It no longer skips the claim check, which runs ABOVE it because it asks
180
- * what the pack SAID and not what the record is (F1).
181
- * 9. **THIS record is malformed** (`continue`). Skips the counterpart,
182
- * ambiguity and pair reporting, and 4.1 computed none of them for a
183
- * malformed subject, so nothing is lost. A party steers here only by
184
- * breaking a record it signed, and the record is named `RECORD_MALFORMED`.
185
- * A well formed record whose CANDIDATE is malformed does not take this
186
- * exit: it is `COUNTERPART_MALFORMED` and the reporting below still runs
187
- * (F2).
188
- * 10. **`counterpart_absent`** (`continue`). Skips the ambiguity and pair
189
- * reporting, and there is nothing to skip: 4.1 emits that finding only
190
- * where no candidate is left to read, an EMPTY candidate list or one whose
191
- * every candidate another record accounts for (4.1 v1.17, A20). Which of the three counterpart kinds it
192
- * gets reads the keys of the packs that verified, counted before exit 4,
193
- * and whether any pack took exit 2 or 3.
194
- *
195
- * Not an exit, and listed because it is one more place this shape could
196
- * hide: the claim check runs only where the pack's declaration is READABLE. An
197
- * absent or unreadable one is named once for the pack (`PACK_UNDECLARED`,
198
- * `PACK_DECLARATION_UNREADABLE`) rather than once per record.
199
- *
200
- * THE VERDICT VOCABULARY IS CLOSED
201
- * ================================
202
- *
203
- * `verified`, `failed`, `partial`, `mixed`, `incomplete` and `malformed`,
204
- * every one of them already in use (`evidence-bundle.md` 3 and
205
- * `PACK_VERDICTS`). **No new word is coined anywhere in this module**, and
206
- * `CASE_VERDICTS` is asserted against `PACK_VERDICTS` by the test suite so a
207
- * future addition here cannot invent one quietly. Everything this module has
208
- * to say that is not one of those six is an EXCEPTION KIND reported beside the
209
- * verdict, exactly as `counterpart_absent` and `not_a_handoff_record` are
210
- * findings beside a verdict in 4.1.
211
- *
212
- * A case with any `failed` contribution is `failed`: a proven failure keeps
213
- * the headline (5.5.4 v1.16, A17, proof#158). Otherwise a case with more than
214
- * one outcome is `mixed` (anchor#198, criterion 4).
215
- *
216
- * WHAT THIS READER DOES NOT CLAIM
217
- * ===============================
218
- *
219
- * * **Not that the case is complete.** Joining is voluntary. A counterparty
220
- * that never handed over a pack leaves `COUNTERPART_ABSENT`, which is an
221
- * exception and never a failure. It says that no pack in the case is signed
222
- * by the named key, and nothing about what that party recorded.
223
- * * **Not that a party which presented no pack under the named key recorded
224
- * nothing.** See the top of this comment, and proof#143.
225
- * * **Not that two organizations are independent.** One team holding both keys
226
- * proves which agent said what, not that two parties agreed (4.1, 5.4.4).
227
- * * **Not that a `verified` case has no other problem.** It reads hand-offs.
228
- * It runs `verifyEvidenceBundle` over every pack itself, offline and with no
229
- * gateways, and a pack whose own bundle verdict is not `verified` is named
230
- * with that verdict and its reasons, so the case never reads above its
231
- * weakest pack. On-chain re-fetch and `gradePack` are a caller's to run.
232
- *
233
- * THE COST OF 4.1's "GRADE EVERY CANDIDATE", AND WHAT IS LEFT OF IT (4.1
234
- * v1.17, amendment A20, which reopens proof#134). Up to v1.16 a pile holding
235
- * two or more records of the complementary type signed by one counterparty key
236
- * could not reach `verified`. 4.1 now sets such a candidate aside where a
237
- * record the subject's own key signed pairs with it verified at another
238
- * sequence, so two messages in one direction, and the demo's A to B, B to A,
239
- * A to B, read clean. What is left surfaces as `COUNTERPART_AMBIGUOUS` beside
240
- * the failure: fan-in, fan-out, and the same pair in two cases at the same
241
- * sequence. Narrowing any of it here with `handoffs[]` is precisely the move
242
- * that would reopen proof#137's door.
2
+ * One verdict for a case: several packs, possibly from several organizations.
3
+ * Conformance: `test-vectors/case/`.
4
+ *
5
+ * A pack a party assembles about itself is not self-proving about what that
6
+ * party left out: a holder can declare a record and claim it nowhere, and the
7
+ * pack still reads `verified` (`declaration-unclaimed-record-01`). Reading
8
+ * several parties' packs at once closes that. An `agent.handoff.sent` with no
9
+ * counterpart among the case's records is always named, and when a pack signed
10
+ * by the key that record names is in the case, the reading says that pack
11
+ * holds no counterpart. It does not establish that a party which presented no
12
+ * such pack recorded nothing: nothing binds a pack's wrapper key to its
13
+ * records' keys.
14
+ *
15
+ * How the join works:
16
+ * - Records pair with records, through `readHandoffs`, over the union of every
17
+ * record every pack supplied. `handoffs[]` is written by the audited party,
18
+ * so it never selects a pair; it is read only to name a declared record that
19
+ * is claimed nowhere (`RECORD_UNCLAIMED`).
20
+ * - Matching is on hashes and identifiers only, never tenancy. Nothing here
21
+ * partitions the pile by pack, issuer, `tenant_id` or organization.
22
+ *
23
+ * Structural rules, to preserve in any change:
24
+ * 1. Every exception is named in the output and contributes a verdict.
25
+ * Nothing removes a record, pack or claim from the reading. `verified` is
26
+ * reachable only when `exceptions` is empty.
27
+ * 2. Nothing a party writes decides whether grading happens. The pile is
28
+ * everything every pack supplied.
29
+ * 3. Every party-chosen field that selects a code path has an accept-set, and
30
+ * an unknown value is a named exception. "Nothing to grade" is never
31
+ * "nothing is wrong".
32
+ * 4. The §4.1 predicates (`readHandoffs`, `normalizedSigningKey`,
33
+ * `packRecords`) are called, never restated.
34
+ *
35
+ * Every early exit in `readCase` names its exception, so no exit buys a
36
+ * cleaner reading than the record or pack would otherwise get. In particular:
37
+ * - A pack with an unreadable key or a wrapper that does not hold is skipped
38
+ * and its key never counted; while any pack takes that exit, a one-sided
39
+ * record reads `COUNTERPART_ABSENT_SIGNER_UNKNOWN`, never `COUNTERPART_ABSENT`.
40
+ * - A wrapper key is counted before the body-type and no-records exits, so
41
+ * choosing a body cannot dodge the counterpart line.
42
+ * - The claim check runs before the not-a-hand-off and malformed exits, so
43
+ * respelling or breaking a record cannot switch it off.
44
+ * - Content and in-pack failures are named and the record stays in the pile.
45
+ *
46
+ * The verdict vocabulary is closed: `verified`, `failed`, `incomplete`
47
+ * (evidence-bundle.md section 3). Every exception maps to one claim and one
48
+ * state (`claims.ts`), and the case reads its worst pack: any `failed`
49
+ * contribution makes the case `failed`, otherwise any `incomplete` makes it
50
+ * `incomplete`, and a reading with no contribution is `verified`.
51
+ *
52
+ * Not claimed: that the case is complete (joining is voluntary), that two
53
+ * organizations are independent (one team can hold both keys), or that a
54
+ * `verified` case has no other problem. Every pack is verified offline here
55
+ * and a pack whose bundle is not `verified` is named; on-chain re-fetch and
56
+ * `gradePack` are the caller's.
57
+ *
58
+ * Several records of the complementary type under one counterparty key can
59
+ * still be ambiguous (fan-in, fan-out, one pair in two cases at one sequence):
60
+ * that surfaces as `COUNTERPART_AMBIGUOUS`. Narrowing it with `handoffs[]`
61
+ * would reopen the hole above.
243
62
  */
244
63
  import { DECLARATION_VERSION, DECLARATION_VERSIONS, checkDeclaration, declarationClaims, packRecords, } from "./declaration.js";
64
+ import { readAgentRecords } from "./agent-record.js";
245
65
  import { readChainLinks } from "./chain.js";
246
- import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, } from "./handoff.js";
66
+ import { NOT_A_HANDOFF_FINDING, normalizedSigningKey, readHandoffs, recordIsMalformed, recordIsNonconforming, } from "./handoff.js";
247
67
  import { coSignaturesPresent, resolveTraceEvents, verifyEvidenceBundle } from "./evidence.js";
248
68
  import { accountDisclosure, completenessNotes, gradePack, } from "./grading.js";
249
69
  import { renderWitnessTime } from "./witness-cache.js";
250
- // ---- The vocabulary ---------------------------------------------------------
251
- /**
252
- * The verdicts a case can reach. Every one is already in use in
253
- * `evidence-bundle.md` 3 or `PACK_VERDICTS`, and anchor#198 says in terms that
254
- * no new verdict word is introduced anywhere.
255
- */
256
- export const CASE_VERDICTS = [
257
- "verified",
258
- "failed",
259
- "partial",
260
- "mixed",
261
- "incomplete",
262
- "malformed",
263
- ];
264
- /**
265
- * anchor#198 says in terms that no new verdict word is introduced anywhere, and
266
- * this line is what ENFORCES it rather than asserting it in prose.
267
- *
268
- * `tsc` refuses to compile if any member of `CASE_VERDICTS` is not already a
269
- * `PackVerdict`, so a seventh word added here fails the BUILD rather than
270
- * reaching a test that might not cover it. The Python half asserts the same
271
- * containment at run time against `grading.PACK_VERDICTS`, which is that
272
- * language's equivalent gate.
273
- */
274
- const _caseVerdictsAreSharedVocabulary = [...CASE_VERDICTS];
275
- void _caseVerdictsAreSharedVocabulary;
70
+ import { KIND_CLAIMS, VERDICTS, VERDICT_TITLES, bundleProblems, claimsOf, problem, restOnSigned, verdictOf as claimsVerdictOf, withItems, } from "./claims.js";
71
+ /** The three verdicts (evidence-bundle.md section 3). A case reads its worst pack. */
72
+ export const CASE_VERDICTS = VERDICTS;
276
73
  /**
277
- * The two pack bodies that carry `events[]` and may carry a `declaration`
278
- * (`evidence-bundle.md` 5.4.1). This is an ACCEPT-SET and not a hint.
279
- *
280
- * `body_type` selects a code path and is written by the party assembling the
281
- * pack. Without this set, a pack whose records live under a key this reader
282
- * does not know yields zero records, contributes nothing, and lets the rest of
283
- * the case read clean over it: the exact defect class this module is written
284
- * against. An unknown value is `PACK_BODY_TYPE_UNKNOWN`.
74
+ * The pack bodies that carry `events[]` and may carry a `declaration`
75
+ * (evidence-bundle.md §5.4.1). An accept-set: `body_type` is party-written, so
76
+ * an unknown value is `PACK_BODY_TYPE_UNKNOWN`, never zero records read clean.
285
77
  *
286
- * **`sanning.evidence.export/v2` is NOT in this set and is NOT unwrapped.** It
287
- * verifies as a bundle and carries a whole `sanning.anchor.trace/v3` pack under
288
- * `body.source_bundle`, so its records do not reach the pile and the export is
289
- * named `PACK_BODY_TYPE_UNKNOWN`, `incomplete`. Reading inside it changes which
290
- * packs this reader grades, so it is a decision of its own. A reader who needs
291
- * the records hands in the source bundle as a pack. The export's wrapper key is
292
- * still read, so wrapping a pack in one does not change which counterpart kind
293
- * another record gets.
78
+ * `sanning.evidence.export/v2` is not unwrapped: it is named
79
+ * `PACK_BODY_TYPE_UNKNOWN`, and a reader who needs its records hands in the
80
+ * source bundle. Its wrapper key is still read.
294
81
  */
295
82
  export const CASE_BODY_TYPES = [
296
83
  "sanning.anchor.trace/v3",
297
84
  "sanning.stamp.evidence/v2",
298
85
  ];
299
86
  /**
300
- * The envelope profiles the case reader reads records under: the ratified
301
- * accept-set (`envelope-spec.md` 5) minus every profile under quarantine in
302
- * `cross-kernel/accept-set.json`. A test in each kernel derives both arrays
303
- * from that file, and a test here pins this one equal to this kernel's own
304
- * `ACCEPTED_SPEC_MAJORS`, which is what `verifyEvidenceBundle` reads under:
305
- * the Python kernel narrows its verifier to this set for the case reader, and
306
- * this kernel's verifier already reads exactly it. If this kernel ever admits
307
- * a quarantined profile, that test fails, and the case reader has to narrow
308
- * its verifier the way the Python one does before it can pass.
87
+ * The profiles records are read under: the accept-set (envelope-spec.md §5)
88
+ * minus quarantined profiles in `cross-kernel/accept-set.json`. A test pins
89
+ * this equal to `ACCEPTED_SPEC_MAJORS`, which `verifyEvidenceBundle` reads under.
309
90
  */
310
- export const CASE_RECORD_PROFILES = ["sanning.agent/v1", "sanning.events/v2"];
311
- /** The ratified profiles under quarantine, pending DL-63. */
91
+ export const CASE_RECORD_PROFILES = ["sanning.events/v2"];
92
+ /** Accepted profiles under quarantine. */
312
93
  export const CASE_QUARANTINED_PROFILES = ["sanning.mlflow/v1"];
313
- // ---- The exceptions ---------------------------------------------------------
314
- //
315
- // Each of these is a NAME for something a reader would otherwise have to infer
316
- // from an absence. None of them is a verdict word.
317
94
  /**
318
- * A conformant hand-off record with no counterpart among the records this
319
- * reader read, where every pack in the case verified under a key this reader
320
- * can read and NO pack in the case is signed by the key the record names,
321
- * or where every candidate it had was accounted for by another record under
322
- * A20 (the exception then carries `count`, and names no pack).
323
- * **An exception, never a failure** (anchor#198, criterion 3).
324
- *
325
- * That is ALL it says. It does not say the counterparty's account is absent,
326
- * and it does not say nothing was tampered with: a counterparty that signs its
327
- * own pack under a second key, or hands over an export an issuer signed,
328
- * reaches this reading too, and nothing offline binds a pack's key to the keys
329
- * of the records inside it (proof#143). The rendered text states the fact and
330
- * stops.
331
- *
332
- * It says nothing about RECORDS signed by that key either. A party may hold records
333
- * its counterparty signed, and those are still graded as candidates, but they
334
- * are not the counterparty's account of itself, so they do not move this
335
- * record to `COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS`.
95
+ * A conformant hand-off record with no counterpart, where every pack verified
96
+ * under a readable key and none is signed by the key the record names, or every
97
+ * candidate was accounted for by another record (then `count` is set and no
98
+ * pack is named). An exception, never a failure. It says only that no pack is
99
+ * signed by the named key; records that key signed and others hold are not that
100
+ * party's own account and do not change it.
336
101
  */
337
102
  export const COUNTERPART_ABSENT = "counterpart_absent";
338
103
  /**
339
- * THE CLOSURE anchor#198 exists for.
340
- *
341
- * A conformant hand-off record with no counterpart, where a pack that VERIFIES
342
- * and is SIGNED by the key this record names is in the case, and not one
343
- * record this reader could read is this record's counterpart. It states that a
344
- * pack signed by the named key is present and holds no counterpart this reader
345
- * could read, and nothing more. 4.1 (v1.10, A15) and 5.4.2 rule 7 (v1.10) both point here, and
346
- * `declaration/declaration-unclaimed-record-01` is the pack that reaches this
347
- * line.
348
- *
349
- * **"Its own account" is read from the key that signed the PACK, and never
350
- * from the keys that signed records.** A pack's wrapper key signs `body_hash`
351
- * and `body` carries the declaration (`evidence-bundle.md` 5.4, "Who signs
352
- * it"), so a wrapper signed by B is provenance the accusing party cannot forge
353
- * without B's private key. Records signed by B that A merely HOLDS are not B's
354
- * account of itself: keyed on record keys, one pack from A plus one unrelated
355
- * record B signed at some other time reached this line when B had handed over
356
- * nothing at all.
357
- *
358
- * What it does NOT prove, stated so no surface claims it: that B handed the
359
- * pack over for THIS case. A pack B signed for some other purpose and a third
360
- * party replayed reads the same, because the only values that could say
361
- * otherwise (the declaration's `case_id` and `period`) are written by B and
362
- * would let B leave this line by writing them differently. The cost the other
363
- * way: a pack B assembles under any key other than the one its records name, a
364
- * per-organization hand-over key (anchor#196) included, reads
365
- * `COUNTERPART_ABSENT`, because nothing in the case binds that key to B. That
366
- * reading is still true: no pack signed by the named key is here. So a
367
- * counterparty can AVOID this line by choosing the key it signs under; what it
368
- * cannot do is make the missing counterpart read clean.
369
- *
370
- * It is deliberately NOT also conditioned on a record signed by B being in the
371
- * pile. B's pack can hold none this reader reads (a body it does not unwrap,
372
- * bytes B withheld, an empty `events[]`), and each of those is a choice B makes
373
- * and is named on its own. A second conjunct on them hands B the innocent line
374
- * for making one.
375
- *
376
- * It is a DISTINCT kind from `COUNTERPART_ABSENT` and not a louder spelling of
377
- * it. Collapsing the two would be the defect class again: "the counterparty
378
- * never joined" and "the counterparty joined and its own account does not
379
- * contain this hand-off" are the honest reading and the damning one, and one
380
- * name for both lets the second read as the first.
381
- *
382
- * It is still an EXCEPTION and not a failure, which criterion 3 requires and
383
- * which is also the honest call: the counterparty's pack is a selection over
384
- * its own interval, and a hand-off outside that interval is legitimately not
385
- * in it. What this names is that the question can now be ASKED of the other
386
- * party's own evidence, which is what no single pack could ever support.
104
+ * A conformant hand-off record with no counterpart, where a verified pack
105
+ * signed by the key the record names is in the case and holds no counterpart.
106
+ * The closure this module exists for (`declaration-unclaimed-record-01`).
107
+ *
108
+ * "Its own account" is read from the key that signed the pack, never from
109
+ * record keys: a wrapper signed by B is provenance an accuser cannot forge, but
110
+ * records signed by B that A holds are not B's account.
111
+ *
112
+ * Not proven: that B handed the pack over for this case. A pack B signs under
113
+ * any other key reads `COUNTERPART_ABSENT`, which is still true; B can avoid
114
+ * this line by choice of key, but cannot make the missing counterpart read
115
+ * clean. Not conditioned on B's records being readable, since each way B could
116
+ * make them unreadable is its own named choice.
117
+ *
118
+ * A distinct kind from `COUNTERPART_ABSENT`: one name for both would let the
119
+ * damning reading pass as the honest one. Still `incomplete`, not a failure: a
120
+ * hand-off outside the interval B's pack covers is legitimately absent.
387
121
  */
388
122
  export const COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS = "counterpart_absent_from_counterparty_records";
389
123
  /**
390
- * A conformant hand-off record with no counterpart, no VERIFIED pack in the
391
- * case signed by the key it names, and at least one pack in the case that this
392
- * reader could not tie to a key: its wrapper key is unreadable
393
- * (`PACK_KEY_UNREADABLE`) or it does not verify (`PACK_DOES_NOT_VERIFY`).
394
- * Whether a pack signed by the named key is in the case is UNKNOWN, so neither
395
- * of the other two kinds is true.
396
- *
397
- * The gate on the previous version of this reader showed why it exists. A
398
- * counterparty that signed its own pack under `" " + key` got
399
- * `PACK_KEY_UNREADABLE` for the pack and the innocent `COUNTERPART_ABSENT` for
400
- * the record, and the output contradicted itself: whose pack it was is
401
- * UNKNOWN, and then no pack is signed by the named key. The second sentence
402
- * was not established.
403
- *
404
- * `incomplete`, like the other two one-sided kinds: "I could not tell".
124
+ * A conformant hand-off record with no counterpart, no verified pack signed by
125
+ * the key it names, and at least one pack whose signer is unknown
126
+ * (`PACK_KEY_UNREADABLE` or `PACK_DOES_NOT_VERIFY`). Whether the named key
127
+ * signed a pack is unknown, so neither other kind is true. `incomplete`.
405
128
  */
406
129
  export const COUNTERPART_ABSENT_SIGNER_UNKNOWN = "counterpart_absent_signer_unknown";
407
130
  /**
408
- * More than one candidate counterpart that nothing accounts for (4.1 v1.17,
409
- * A20). The reading is the worst of them and the ambiguity is named: a reader
410
- * permitted to take one match can be handed an honest decoy beside a tampered
411
- * record. `count` is how many of the candidates are live, which is the number
412
- * the ambiguity is about.
131
+ * More than one live candidate counterpart (§4.1). The reading is the worst of
132
+ * them and the ambiguity is named, so an honest decoy cannot hide a tampered
133
+ * record. `count` is the number of live candidates.
413
134
  */
414
135
  export const COUNTERPART_AMBIGUOUS = "counterpart_ambiguous";
136
+ /** A formed pair that failed checks, listed in §4.1's fixed order. */
137
+ export const PAIR_FAILED = "pair_failed";
138
+ /** This record itself is malformed under §4.1 (`recordIsMalformed`). */
139
+ export const RECORD_MALFORMED = "record_malformed";
415
140
  /**
416
- * A formed pair that failed a check. Carries which checks, from 4.1's own
417
- * fixed order, so a reader is told the record AND the check.
141
+ * A candidate counterpart is malformed and this record is not. §4.1 reports
142
+ * the worst candidate, so without this kind one junk record signed by the
143
+ * counterparty's key would blame the honest record and drop its other failures.
144
+ * `incomplete`: the defect is the candidate's, which carries its own
145
+ * `RECORD_MALFORMED`.
418
146
  */
419
- export const PAIR_FAILED = "pair_failed";
147
+ export const COUNTERPART_MALFORMED = "counterpart_malformed";
420
148
  /**
421
- * THIS record is wrong: 4.1 governs it and `recordIsMalformed` says so of the
422
- * record itself. A statement about the record this exception names, and about
423
- * no other.
149
+ * This record is one of the two hand-off types, signed and intact, and does
150
+ * not follow its vocabulary (`recordIsNonconforming`). A producer's error,
151
+ * never a proven alteration.
424
152
  */
425
- export const RECORD_MALFORMED = "record_malformed";
153
+ export const RECORD_NONCONFORMING = "record_nonconforming";
426
154
  /**
427
- * A CANDIDATE counterpart of this record is malformed, and this record is not.
428
- *
429
- * 4.1 grades every candidate and reports the worst of them, and `malformed`
430
- * outranks `failed`, so one junk record signed by the counterparty's key makes
431
- * the reading of a perfectly well formed record `malformed` too. Filed under
432
- * `RECORD_MALFORMED`, that read as "this record is not a well formed hand-off
433
- * record" about the HONEST record, and the branch it took skipped the
434
- * counterpart, ambiguity and `failedChecks` reporting entirely: one junk record
435
- * both blamed the wrong party and stripped a named `message_hash` failure out
436
- * of the case. That is proof#133 ruling 6, which 4.1 closed by keeping the
437
- * UNION of `failedChecks`, reappearing one layer out because this layer
438
- * discarded that union.
439
- *
440
- * A record's own conformance and its candidate's are different facts and do
441
- * not share a kind. This one is `incomplete` and not `malformed`: the proven
442
- * defect belongs to the candidate, which carries its own `RECORD_MALFORMED`
443
- * line, and what is true of THIS record is that its pairing could not be
444
- * determined. Contributing `malformed` here would attribute the candidate's
445
- * defect to the record a party being accused did not write.
155
+ * A candidate counterpart is nonconforming and this record is not.
156
+ * `incomplete` for the reason `COUNTERPART_MALFORMED` is: the defect is the
157
+ * candidate's, which carries its own `RECORD_NONCONFORMING`.
446
158
  */
447
- export const COUNTERPART_MALFORMED = "counterpart_malformed";
159
+ export const COUNTERPART_NONCONFORMING = "counterpart_nonconforming";
448
160
  /**
449
- * A record that its own pack DECLARES and its own pack's READABLE declaration
450
- * claims in no `handoffs[]` entry. This is proof#137's door, named at the layer
451
- * that can see it: the holder declared the record and claimed it nowhere, so
452
- * neither 5.4.2 rule 2 nor rule 7 reaches it and the pack reads `verified`.
453
- *
454
- * **It turns on what the PACK said about the record, and never on whether 4.1
455
- * governs it.** Gating it on "is this a conformant hand-off record" put it
456
- * behind a value the audited party writes: the same second receipt, respelled
457
- * with an `ff` ligature in `event_type` and a Cyrillic `е` in
458
- * `counterparty_public_key`, left 4.1's scope, took the `not_a_handoff_record`
459
- * branch, and bought a case reading of `verified` with zero exceptions where
460
- * the conformant spelling read `mixed` with four. proof#136 ratified that a
461
- * respelling buys a DIFFERENT LINE rather than no line; at this layer it
462
- * bought no line at all, and a strictly better outcome than honesty.
463
- *
464
- * It is not an accusation. `handoffs[]` MAY be empty and an unclaimed record
465
- * may be entirely innocent. What it is, is the end of the silence: the reader
466
- * cannot tell whether an unclaimed record is part of the hand-over the holder
467
- * meant to give or one it is keeping out of its own account, and that is "I
468
- * could not tell", which is why it carries `incomplete` and not `verified`.
469
- *
470
- * THE COST, stated rather than left to be found: a pack that declares records
471
- * which are not hand-off sides at all, an ordinary log line among them, is
472
- * named once per such record, so a case holding one cannot read `verified`.
473
- * That is the price of a rule with no branch the holder selects, and the only
474
- * narrower rule available keys on a value the holder writes. A surface that
475
- * wants the two apart reads `CaseRecordReading.findings`, where
476
- * `not_a_handoff_record` still says which is which.
161
+ * A record its pack declares and its pack's readable declaration claims in no
162
+ * `handoffs[]` entry (§5.4.2 rules 2 and 7 cannot reach it). Turns on what the
163
+ * pack said, never on whether §4.1 governs the record, so respelling
164
+ * `event_type` cannot move a record out of this check.
165
+ *
166
+ * Not an accusation: `handoffs[]` may be empty and the record innocent, so it
167
+ * is `incomplete`. The cost is that a declared non-hand-off record (a log line)
168
+ * is named too; `CaseRecordReading.findings` tells the two apart.
477
169
  */
478
170
  export const RECORD_UNCLAIMED = "record_unclaimed_by_its_pack";
479
171
  /**
480
- * The pack supplied no `event_bytes` for a record, so this reader cannot see
481
- * what the record IS in either direction.
482
- *
483
- * `event_bytes` is OPTIONAL (5.1, 5.3) and withholding it is minimum
484
- * disclosure, which the corpus calls a customer's right and not a defect
485
- * (proof#137, defect 3), so this is not a deviation and does not fail. It is
486
- * still SAID, because a withheld record is exactly where a missing counterpart
487
- * would hide, and a reader that dropped it silently would read a trimmed pack
488
- * as an honest one.
172
+ * The pack withheld `event_bytes` for a record. Withholding is minimal
173
+ * disclosure (§5.1, §5.3), not a deviation, but it is said: a withheld record is
174
+ * where a missing counterpart would hide.
489
175
  */
490
176
  export const RECORD_BYTES_WITHHELD = "record_bytes_withheld";
491
177
  /**
492
- * The pack SUPPLIED `event_bytes` this reader cannot read as
493
- * `JCS(event record)` (5.4.2 rule 7's parser rule, one implementation in
494
- * `packRecords`). Bytes the holder handed over that do not decode are a defect
495
- * in what was handed over, not an absence, so this one is `malformed`.
178
+ * The pack supplied `event_bytes` that do not read as `JCS(event record)`.
179
+ * A defect in what was handed over, so `malformed`.
496
180
  */
497
181
  export const RECORD_BYTES_UNREADABLE = "record_bytes_unreadable";
498
- /**
499
- * The pack deviates from its own declaration (5.4.2). A proven mismatch, so it
500
- * fails: a case cannot be clean over a pack that is proven to be trimmed.
501
- */
182
+ /** The pack deviates from its own declaration (§5.4.2). A proven mismatch: `failed`. */
502
183
  export const PACK_DECLARATION_DEVIATES = "pack_declaration_deviates";
503
184
  /**
504
- * The pack and the UNSIGNED content handed in beside it do not line up (5.4.2
505
- * rules 3 and 4, proof#147, ratified by Phil 2026-09-22). A record the pack
506
- * declares `disclosed` for which no bytes reached this reader, or one it
507
- * declares `content_withheld` for which bytes reached this reader from the
508
- * side-content map alone.
185
+ * The pack and the unsigned content beside it do not line up (§5.4.2 rules 3
186
+ * and 4): a record declared `disclosed` with no bytes, or declared
187
+ * `content_withheld` with bytes from the side-content map alone.
509
188
  *
510
- * `incomplete` and never `failed`, which is the whole point of the kind. 5.4
511
- * grades a pack's declaration partly against content the READER was handed,
512
- * and whoever assembles a case fills that map in. Under
513
- * `PACK_DECLARATION_DEVIATES` an accuser could put an intact pack into a
514
- * `failed` case WITH NO KEY AT ALL: supply bytes for a record its holder
515
- * declared withheld, or drop the bytes of one it declared disclosed. The pack
516
- * was intact, its wrapper verified, and its holder had done nothing. This is
517
- * the mirror of proof#143, where a counterparty makes ITSELF look better by
518
- * choosing what it hands over.
519
- *
520
- * It is emitted BESIDE `PACK_DECLARATION_DEVIATES` where a pack does both,
521
- * because the two say different things about different records.
189
+ * `incomplete`, never `failed`: whoever assembles a case fills in that map, so
190
+ * otherwise an accuser could fail an intact pack with no key. Emitted beside
191
+ * `PACK_DECLARATION_DEVIATES` when a pack does both.
522
192
  */
523
193
  export const PACK_DECLARATION_CONTENT_DEPENDENT = "pack_declaration_content_dependent";
524
194
  /**
525
- * The pack carries a `declaration` this reader cannot read (5.4.3): an unknown
526
- * `declaration_version`, an absent or `null` one, a non-object, or one that
527
- * does not satisfy 5.4.1. `declaration_version` is chosen by the same party
528
- * that assembles the pack and has no accept-set of its own, so this is UNKNOWN
529
- * and never clean.
195
+ * The pack's `declaration` cannot be read (§5.4.3). Its version is chosen by
196
+ * the pack's assembler with no accept-set, so this is unknown, never clean.
530
197
  */
531
198
  export const PACK_DECLARATION_UNREADABLE = "pack_declaration_unreadable";
532
199
  /**
533
- * The pack carries no `declaration` member at all, so it never said what it
534
- * holds. `partial` is 5.4.3's own word for a claim nobody made: never
535
- * `verified`, because there is nothing to check the pack against, and never
536
- * `failed`, because nothing is wrong.
200
+ * The pack carries no `declaration`. `partial` (§5.4.3): nothing to check
201
+ * against, and nothing wrong.
537
202
  */
538
203
  export const PACK_UNDECLARED = "pack_undeclared";
539
- /**
540
- * The pack's `body_type` is not one this reader knows to carry `events[]`
541
- * (`CASE_BODY_TYPES`).
542
- */
204
+ /** The pack's `body_type` is not in `CASE_BODY_TYPES`. */
543
205
  export const PACK_BODY_TYPE_UNKNOWN = "pack_body_type_unknown";
544
- /**
545
- * The pack carries a body this reader knows and no records in it. A pack that
546
- * contributes nothing is named rather than passed over.
547
- */
206
+ /** The pack carries a known body with no records. */
548
207
  export const PACK_HOLDS_NO_RECORDS = "pack_holds_no_records";
549
208
  /**
550
- * The key the pack's wrapper names is not one this reader can read as a key
551
- * (`normalizedSigningKey` returns nothing for it), so who signed the pack is
552
- * UNKNOWN. The pack is not verified and none of its records are read, and
553
- * every one-sided record whose named key signed no verified pack reads
554
- * `COUNTERPART_ABSENT_SIGNER_UNKNOWN`.
555
- *
556
- * Found by constructing it, while enumerating this module's exits: the wrapper
557
- * key is the ONE input that separates the two counterpart kinds, and a key this
558
- * reader could not read used to drop out of that set in silence. The Python
559
- * `verify_evidence_bundle` accepts a wrapper `public_key` with ASCII
560
- * whitespace inside it or around it (`bytes.fromhex` skips it; this kernel's
561
- * verifier rejects it), so a counterparty that signed its own pack under
562
- * `" " + key` handed a Python reader a pack that verified, contributed no key,
563
- * and moved the subject from `COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS` to
564
- * the innocent `COUNTERPART_ABSENT`. The same repair 4.1 made for a RECORD
565
- * whose envelope key it cannot read (proof#133, fourth hunt): a key a reader
566
- * holds and cannot read is named, never treated as absent. It is `incomplete`
567
- * rather than `malformed` because whether the spelling verifies is the envelope
568
- * layer's call (proof#135), and this reader only knows it cannot read it.
209
+ * The pack's wrapper key cannot be read (`normalizedSigningKey` returns
210
+ * nothing), so its signer is unknown. None of its records are read, and a
211
+ * one-sided record whose named key signed no verified pack reads
212
+ * `COUNTERPART_ABSENT_SIGNER_UNKNOWN`. A key a reader holds and cannot read is
213
+ * named, never treated as absent. `incomplete`: whether the spelling verifies
214
+ * is the envelope layer's call.
569
215
  */
570
216
  export const PACK_KEY_UNREADABLE = "pack_key_unreadable";
571
217
  /**
572
- * The pack does not verify as a bundle: `verifyEvidenceBundle`, run here
573
- * offline over the pack and its side content, returned `failed` or
574
- * `malformed`. None of its records are read, because a record from it would
575
- * reach 4.1 without the verified envelope `HeldRecord` requires, and the key
576
- * its wrapper claims is never counted as a pack signer: an unverified wrapper
577
- * claiming the counterparty's key is exactly what an accusing party would
578
- * forge.
579
- *
580
- * `partial` is not this exception. `partial` means the wrapper signature and
581
- * `body_hash` held and part of the body could not be determined, which is what
582
- * an unknown body or withheld bytes produce, and those are named on their own.
583
- *
584
- * `failed`: a pack in the case is proven broken.
218
+ * The pack's `bundle.json` cannot be read as JSON: it does not parse, nests too deep, or
219
+ * names one member twice. The pack is not verified and none of its records are read, and
220
+ * every other pack in the case still is. `incomplete`: the signature could not be checked.
221
+ */
222
+ export const PACK_UNREADABLE = "pack_unreadable";
223
+ /**
224
+ * `verifyEvidenceBundle`, run offline here, returned `failed` or `malformed`.
225
+ * None of its records are read (§4.1 requires verified envelopes), and its
226
+ * wrapper key is never counted as a pack signer: an unverified wrapper claiming
227
+ * the counterparty's key is what an accuser would forge. `failed`.
585
228
  */
586
229
  export const PACK_DOES_NOT_VERIFY = "pack_does_not_verify";
587
230
  /**
588
- * The pack's wrapper HOLDS and its CASE GRADE is not `verified`
589
- * (evidence-bundle 5.5.1 step 1 v1.20, A23; it replaces
590
- * `pack_bundle_not_verified`, which carried the bundle status alone). The case
591
- * grade is the pack grade of 5.4.5, taken twice: the SIGNED grade over the run
592
- * with the side map empty, and the FULL grade over the run with it. The signed
593
- * grade decides where it is `failed` or `malformed`; a failure only the full
594
- * grade reaches is `incomplete`, because it rests on bytes nobody signed;
595
- * otherwise the full grade. A body the verifier could not read is `malformed`,
596
- * a body that is not one of the two event-carrying bodies takes the signed
597
- * run's status, and a verifier that graded a different number of records than
598
- * the pack holds takes the signed status, or `malformed` where that status is
231
+ * The pack's wrapper holds and its case grade is not `verified`
232
+ * (evidence-bundle §5.5.1). The case grade is the §5.4.5 pack grade taken
233
+ * twice: signed (side map empty) and full (with it). The signed grade decides
234
+ * where it is `failed` or `malformed`; a failure only the full grade reaches is
235
+ * `incomplete`, since it rests on unsigned bytes; otherwise the full grade.
236
+ * An unreadable body is `malformed`; a non-event body takes the signed status;
237
+ * a record-count mismatch takes the signed status, or `malformed` where that is
599
238
  * `verified`.
600
239
  *
601
- * The exception carries `status`, the case grade; `causes`, the 5.4.5 cause
602
- * kinds of the grade that decided it (none in the first three branches); and
603
- * `reasons`, the verifier's reasons from the run with the side map empty. It
604
- * CONTRIBUTES the grade, and `incomplete` for `pending`, `not_found` or
605
- * `mixed`, so the case verdict never reads above its weakest pack.
240
+ * Carries `status`, `causes` (the deciding grade's cause kinds) and `reasons`
241
+ * (from the signed run). Contributes the grade, or `incomplete` for `pending`,
242
+ * `not_found` or `mixed`, so the case never reads above its weakest pack.
606
243
  */
607
244
  export const PACK_GRADE_NOT_VERIFIED = "pack_grade_not_verified";
608
245
  /**
609
- * A record in a pack whose wrapper holds, whose OWN envelope does not verify
610
- * (`EventResult.envelopeOk` is false). Not read by 4.1, and NAMED with the
611
- * verifier's reasons. `failed`: a pack its holder signed carries a record that
612
- * is proven not to verify. As a reason to drop the whole pack it let the
613
- * counterparty add one junk event to its own pack, re-seal it, and strip a
614
- * proven `message_hash` failure out of the case (gate 3 on proof#140).
246
+ * A record whose own envelope does not verify, in a pack whose wrapper holds.
247
+ * Not read by §4.1. `failed`. Scoped to the record so one junk event cannot
248
+ * drop the whole pack and a proven failure with it.
615
249
  */
616
250
  export const RECORD_ENVELOPE_DOES_NOT_VERIFY = "record_envelope_does_not_verify";
617
251
  /**
618
- * A record whose envelope verifies and which fails another check the bundle
619
- * verifier grades it on inside its pack (its inclusion proof, its join to a
620
- * checkpoint or stamp, a retired field name). NAMED with the verifier's
621
- * reasons, `failed`, and the record STAYS in the pile: a holder that could
622
- * remove its own tampered receipt from pairing by breaking that receipt's
623
- * inclusion proof would be choosing its own reading.
252
+ * A record whose envelope verifies but which fails another check in its pack
253
+ * (inclusion proof, checkpoint or stamp join, a retired field). `failed`, and
254
+ * the record stays in the pile, so breaking its own proof cannot remove a
255
+ * tampered receipt from pairing.
624
256
  */
625
257
  export const RECORD_DOES_NOT_VERIFY_IN_ITS_PACK = "record_does_not_verify_in_its_pack";
626
258
  /**
627
- * A record whose envelope verifies and whose disclosed content does not hash to
628
- * the hash it commits to, its `content_hash` or a hand-off record's
629
- * `message_hash` (evidence-bundle v1.19, A20) (`EventResult.contentOk` is false), with the
630
- * side map EMPTY: the content is inside the signed body. The same mismatch from
631
- * content handed in beside the pack is `RECORD_CONTENT_DEPENDENT` (proof#159).
632
- * `failed`, and the record STAYS in the pile: content beside a pack is unsigned and often
633
- * supplied by whoever assembled the case, and gate 3 on proof#140 showed that
634
- * party erasing an honest verified pair by swapping it.
259
+ * A record whose disclosed content does not hash to its `content_hash` (or a
260
+ * hand-off record's `message_hash`) with the side map empty: the content is in
261
+ * the signed body. `failed`, and the record stays in the pile.
635
262
  */
636
263
  export const RECORD_CONTENT_MISMATCH = "record_content_mismatch";
637
264
  /**
638
- * The two readings proof#147 did not reach, given its shape (proof#159, ruled
639
- * by Phil 2026-09-22, evidence-bundle.md 5.5.4 v1.16, A17). A pack whose
640
- * bundle verdict is `failed` ONLY when read with the unsigned content handed
641
- * in beside it, and `failed` no longer when read with that map empty, is
642
- * `PACK_BUNDLE_CONTENT_DEPENDENT`; a record whose content mismatch comes from
643
- * that map alone is `RECORD_CONTENT_DEPENDENT`. Both are `incomplete`: the
644
- * reader established that the bytes it was handed are not the bytes the record
645
- * commits to, and nothing about the party that signed the record, because
646
- * whoever assembled the case chose those bytes with no key. Up to v1.15 both
647
- * readings carried `failed`, and one flipped nibble of accuser-supplied content
648
- * put a `failed` against an intact pack; under A17's headline rule that would
649
- * have become the case's headline.
650
- *
651
- * The test that separates the two is the one proof#159 states for the inverse:
652
- * every deviation a signature covers still reads `failed` with the side map
653
- * EMPTY. So the pack is verified twice, with the map and without it, and what
654
- * fails without it is `PACK_GRADE_NOT_VERIFIED`, `RECORD_CONTENT_MISMATCH`
655
- * (content inside the signed body) or `RECORD_DOES_NOT_VERIFY_IN_ITS_PACK`,
656
- * each still `failed`. Emptying the map is the most a party that signed
657
- * nothing can do to starve the check, and it cannot remove any of those.
265
+ * A pack whose bundle verdict is `failed` only with the unsigned side content,
266
+ * and a record whose content mismatch comes from that content alone
267
+ * (evidence-bundle §5.5.4). Both `incomplete`: whoever assembled the case chose
268
+ * those bytes with no key. Every deviation a signature covers still fails with
269
+ * the side map empty, so starving the check cannot hide one.
658
270
  */
659
271
  export const PACK_BUNDLE_CONTENT_DEPENDENT = "pack_bundle_content_dependent";
660
272
  export const RECORD_CONTENT_DEPENDENT = "record_content_dependent";
661
273
  /**
662
- * A record signed under a ratified profile that is under quarantine
663
- * (`CASE_QUARANTINED_PROFILES`, pending DL-63). Not read, in any kernel.
664
- * `incomplete`: whether it verifies is what the kernels disagree about.
274
+ * A record under a quarantined profile (`CASE_QUARANTINED_PROFILES`). Not read.
275
+ * `incomplete`.
665
276
  */
666
277
  export const RECORD_PROFILE_QUARANTINED = "record_profile_quarantined";
667
278
  /** The reader was handed no packs at all. */
668
279
  export const CASE_HOLDS_NO_PACKS = "case_holds_no_packs";
669
280
  /**
670
- * The three NOTEs (5.5.3 v1.20, extended proof#148): findings on the reading
671
- * with no contribution, printed in the scope sentence so a clean verdict
672
- * never hides any of them. `pack_undeclared` is a note only where the
673
- * reading holds no hand-off record; where it holds one, it is the exception
674
- * of that name.
281
+ * Notes (§5.5.3): findings with no verdict contribution, printed in the scope
282
+ * sentence so a clean verdict never hides them. `pack_undeclared` is a note
283
+ * only where the reading holds no hand-off record; otherwise it is the
284
+ * exception.
675
285
  */
676
286
  export const CONTENT_WITHHELD_BY_DECLARATION = "content_withheld_by_declaration";
677
287
  /**
678
- * A pack's wrapper carries a present `co_signatures` (evidence-bundle.md §2,
679
- * decided by Phil 2026-09-24 on proof#148). The field verifies nothing and
680
- * sits outside the wrapper's signed scope, so the case reading names it
681
- * rather than presenting it as evidence, and it contributes no verdict.
288
+ * A pack's wrapper carries `co_signatures` (evidence-bundle section 2). It verifies
289
+ * nothing and sits outside the signed scope, so it is named, not presented as
290
+ * evidence.
682
291
  */
683
292
  export const CO_SIGNATURES_PRESENT = "co_signatures_present";
684
293
  export const CASE_NOTE_KINDS = [
@@ -686,18 +295,17 @@ export const CASE_NOTE_KINDS = [
686
295
  CONTENT_WITHHELD_BY_DECLARATION,
687
296
  CO_SIGNATURES_PRESENT,
688
297
  ];
689
- /**
690
- * Every exception this reader can report, as a closed set. A surface may render
691
- * them and a test may assert over them; nothing outside this array is emitted.
692
- */
298
+ /** Every exception this reader can report. Nothing outside this set is emitted. */
693
299
  export const CASE_EXCEPTION_KINDS = [
694
300
  COUNTERPART_ABSENT,
695
301
  COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS,
696
302
  COUNTERPART_ABSENT_SIGNER_UNKNOWN,
697
303
  COUNTERPART_AMBIGUOUS,
698
304
  COUNTERPART_MALFORMED,
305
+ COUNTERPART_NONCONFORMING,
699
306
  PAIR_FAILED,
700
307
  RECORD_MALFORMED,
308
+ RECORD_NONCONFORMING,
701
309
  RECORD_UNCLAIMED,
702
310
  RECORD_BYTES_WITHHELD,
703
311
  RECORD_BYTES_UNREADABLE,
@@ -707,6 +315,7 @@ export const CASE_EXCEPTION_KINDS = [
707
315
  PACK_UNDECLARED,
708
316
  PACK_BODY_TYPE_UNKNOWN,
709
317
  PACK_HOLDS_NO_RECORDS,
318
+ PACK_UNREADABLE,
710
319
  PACK_KEY_UNREADABLE,
711
320
  PACK_DOES_NOT_VERIFY,
712
321
  PACK_GRADE_NOT_VERIFIED,
@@ -719,141 +328,57 @@ export const CASE_EXCEPTION_KINDS = [
719
328
  CASE_HOLDS_NO_PACKS,
720
329
  ];
721
330
  /**
722
- * The status every record in every pack carries (evidence-bundle 5.5.4
723
- * v1.35, A43, proof#204), as a closed set. A record is listed for each element
724
- * of its pack's `body.events` as `packRecords` reads them, or, for a
725
- * `sanning.evidence.export/v2` pack, of the events of the inline source
726
- * bundle it wraps (`resolveTraceEvents`), and gets exactly one:
727
- *
728
- * - `read_clean`: the record went into the pile, no exception carries its
729
- * `packIndex` and `recordIndex`, and its pack's declaration check
730
- * names it by nothing (`declarationFindings` is empty). It says nothing
731
- * about its PACK: the pack's own grade and exceptions are on the pack, and a
732
- * surface MUST show that grade beside the status.
733
- * - `read_flagged`: the record went into the pile, and at least one
734
- * exception carries its `packIndex` and `recordIndex`, or its pack's
735
- * declaration check names its `eventId` by a deviation or a content
736
- * finding (`declarationFindings` is not empty; under an `eventId` the
737
- * pack carries twice, every record with that id). An exception about the
738
- * pack alone flags no record.
739
- * - `not_read`: the record never went into the pile. `reason` is the kind
740
- * of the exception that says why, on the record
741
- * (`record_profile_quarantined`, `record_envelope_does_not_verify`,
742
- * `record_bytes_withheld`, `record_bytes_unreadable`) or on its pack
743
- * (`pack_key_unreadable`, `pack_does_not_verify`,
744
- * `pack_body_type_unknown`, `pack_grade_not_verified` where the body
745
- * could not be read or the verifier graded a different number of records).
746
- * An export's wrapped records carry the reason of the exception their pack
747
- * actually got: `pack_body_type_unknown` where its wrapper holds, and
748
- * otherwise `pack_key_unreadable`, `pack_does_not_verify` or
749
- * `pack_grade_not_verified`.
750
- *
751
- * Before A43 the reading listed only the records it read, so a surface had to
752
- * work out which records were missing, and why, by a rule of its own.
331
+ * The status each record of each pack gets (evidence-bundle §5.5.4), one per
332
+ * element of `body.events` (or, for an export, its source bundle's events):
333
+ *
334
+ * - `read_clean`: in the pile, named by no exception and no declaration
335
+ * finding. Says nothing about its pack; a surface must show the pack grade
336
+ * beside it.
337
+ * - `read_flagged`: in the pile, and named by an exception or a declaration
338
+ * finding (every record sharing a duplicated `eventId`). A pack-only
339
+ * exception flags no record.
340
+ * - `not_read`: never in the pile. `reason` is the record or pack exception
341
+ * that says why; an export's wrapped records take their pack's exception.
753
342
  */
754
343
  export const CASE_RECORD_STATUSES = ["read_clean", "read_flagged", "not_read"];
755
344
  /**
756
- * What each exception kind contributes to the case verdict. It is a TABLE and
757
- * not a chain of branches, so "which exceptions move the verdict" is one thing
758
- * a reader can read, and so that adding a kind without deciding what it means
759
- * is a missing entry rather than a quiet `verified`.
760
- *
761
- * Read the `incomplete` rows as "I could not tell", which is the distinction
762
- * this whole module turns on. None of them is `verified`: an exception never
763
- * contributes a clean verdict, which is what makes the invariant in `verdictOf`
764
- * hold.
765
- *
766
- * `PACK_GRADE_NOT_VERIFIED` is the one row that is not a constant: it
767
- * contributes the pack's case grade, which is never `verified` (the exception
768
- * is emitted only when it is not). `null` marks that row, and `readCase` fills
769
- * it from `status`.
770
- */
771
- const EXCEPTION_VERDICT = {
772
- [COUNTERPART_ABSENT]: "incomplete",
773
- [COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS]: "incomplete",
774
- [COUNTERPART_ABSENT_SIGNER_UNKNOWN]: "incomplete",
775
- [COUNTERPART_AMBIGUOUS]: "incomplete",
776
- [COUNTERPART_MALFORMED]: "incomplete",
777
- [PAIR_FAILED]: "failed",
778
- [RECORD_MALFORMED]: "malformed",
779
- [RECORD_UNCLAIMED]: "incomplete",
780
- [RECORD_BYTES_WITHHELD]: "incomplete",
781
- [RECORD_BYTES_UNREADABLE]: "malformed",
782
- [PACK_DECLARATION_DEVIATES]: "failed",
783
- [PACK_DECLARATION_CONTENT_DEPENDENT]: "incomplete",
784
- [PACK_DECLARATION_UNREADABLE]: "incomplete",
785
- [PACK_UNDECLARED]: "partial",
786
- [PACK_BODY_TYPE_UNKNOWN]: "incomplete",
787
- [PACK_HOLDS_NO_RECORDS]: "incomplete",
788
- [PACK_KEY_UNREADABLE]: "incomplete",
789
- [PACK_DOES_NOT_VERIFY]: "failed",
790
- [PACK_GRADE_NOT_VERIFIED]: null,
791
- [PACK_BUNDLE_CONTENT_DEPENDENT]: "incomplete",
792
- [RECORD_ENVELOPE_DOES_NOT_VERIFY]: "failed",
793
- [RECORD_DOES_NOT_VERIFY_IN_ITS_PACK]: "failed",
794
- [RECORD_CONTENT_MISMATCH]: "failed",
795
- [RECORD_CONTENT_DEPENDENT]: "incomplete",
796
- [RECORD_PROFILE_QUARANTINED]: "incomplete",
797
- [CASE_HOLDS_NO_PACKS]: "partial",
798
- };
799
- /**
800
- * The case grades `PACK_GRADE_NOT_VERIFIED` contributes as themselves. Every
801
- * other grade a pack can reach (`pending`, `not_found`, `mixed`, which a pack
802
- * grade passes through and a case verdict never issues) is `incomplete`.
345
+ * What each exception kind contributes to the case verdict, read off the claim it maps to
346
+ * (`KIND_CLAIMS` in `claims.ts`): a disproof is `failed`, anything not proven
347
+ * `incomplete`. A kind with no mapping throws rather than reading as a quiet `verified`.
348
+ * `null` marks `PACK_GRADE_NOT_VERIFIED`, which contributes the pack's case grade from
349
+ * `status`.
803
350
  */
804
- const GRADE_CONTRIBUTES = new Set([
805
- "partial",
806
- "incomplete",
807
- "failed",
808
- "malformed",
809
- ]);
810
- // ---- The reading ------------------------------------------------------------
351
+ const EXCEPTION_VERDICT = Object.fromEntries(CASE_EXCEPTION_KINDS.map((kind) => {
352
+ if (kind === PACK_GRADE_NOT_VERIFIED)
353
+ return [kind, null];
354
+ const mapping = KIND_CLAIMS[kind];
355
+ if (mapping === undefined)
356
+ throw new Error(`KeyError: '${kind}'`);
357
+ return [kind, mapping[1] === "disproven" ? "failed" : "incomplete"];
358
+ }));
811
359
  const bodyTypeOf = (bundle) => typeof bundle === "object" && bundle !== null
812
360
  ? bundle["body_type"]
813
361
  : undefined;
814
362
  /**
815
- * The key that SIGNED the pack wrapper, which is the key that signed the pack's
816
- * declaration.
817
- *
818
- * The wrapper signature covers `body_hash` and `body` carries the
819
- * `declaration` (`evidence-bundle.md` 5.1, 5.4.1), so this key is the party
820
- * whose signed account of itself this pack is. Read from the ENVELOPE of the
821
- * pack and never from `issuer`: `issuer` is a label and DL-60 keeps it out of
822
- * every reading, while `public_key` is the value the signature binds.
823
- *
824
- * It means something only for a pack whose wrapper HOLDS, which is why
825
- * `readCase` counts it only after its own `verifyEvidenceBundle` call over the
826
- * pack returned `signatureOk` and `bodyHashOk`.
363
+ * The key that signed the pack wrapper, and so the pack's declaration
364
+ * (evidence-bundle §5.1, §5.4.1). Read from `public_key`, never `issuer`, which
365
+ * is a label. Meaningful only once the wrapper is verified to hold.
827
366
  */
828
367
  const holderKeyOf = (bundle) => typeof bundle === "object" && bundle !== null
829
368
  ? bundle["public_key"]
830
369
  : undefined;
831
370
  /**
832
- * Provenance, and nothing else: the wrapper signature over `body_hash` verified
833
- * and `body_hash` is the hash of the body (evidence-bundle.md 5.5.1 step 1).
834
- * Nothing inside the body is part of this answer.
835
- */
836
- const wrapperHolds = (result) => result.signatureOk && result.bodyHashOk;
837
- /**
838
- * Why the pack reads what it reads, as the verifier said it: its bundle errors,
839
- * then each stamp's, then each checkpoint's, then each event's behind the
840
- * event's own id.
841
- *
842
- * The event errors are here as well as on the records' own exceptions on
843
- * purpose. A pack whose only defect is one record reads `failed` with no
844
- * bundle-level error at all, and an exception that named the verdict and no
845
- * reason would send a reader looking for a defect in the pack as a whole.
371
+ * Provenance only: the wrapper signature verified and `body_hash` matches the
372
+ * body (evidence-bundle §5.5.1). Under a version this verifier does not know, the
373
+ * signature alone ties the pack to its key, and the body is not read.
846
374
  */
375
+ const wrapperHolds = (result) => result.signatureOk && (result.bodyHashOk || result.versionUnknown === true);
847
376
  /**
848
- * The reasons a reading carries only because of the side map, in the order the
849
- * verifier wrote them: what the content-dependent kinds are about, and nothing
850
- * a signature covers.
377
+ * Reasons present only with the side map: what the content-dependent kinds are
378
+ * about, and nothing a signature covers.
851
379
  */
852
380
  const onlyIn = (withMap, withoutMap) => withMap.filter((reason) => !withoutMap.includes(reason));
853
- /**
854
- * Whether a side-content map holds any key. Python's `if pack.content` is
855
- * the twin: `None` and an empty map both mean the two verify runs are one run.
856
- */
381
+ /** `null`, `undefined` and an empty map all mean the two verify runs are one. */
857
382
  function sideMapHasKeys(content) {
858
383
  if (content === null || content === undefined)
859
384
  return false;
@@ -861,6 +386,11 @@ function sideMapHasKeys(content) {
861
386
  return content.size > 0;
862
387
  return Object.keys(content).length > 0;
863
388
  }
389
+ /**
390
+ * The verifier's reasons for a pack: bundle, then stamps, checkpoints and
391
+ * events (prefixed with the event id). Event errors are included because a pack
392
+ * whose only defect is one record has no bundle-level error.
393
+ */
864
394
  const bundleReasons = (result) => [
865
395
  ...result.errors,
866
396
  ...(result.stamps ?? []).flatMap((stamp) => stamp.errors),
@@ -868,9 +398,8 @@ const bundleReasons = (result) => [
868
398
  ...result.events.flatMap((ev) => ev.errors.map((e) => `${ev.eventId}: ${e}`)),
869
399
  ];
870
400
  /**
871
- * The `spec_version` of the envelope of `body.events[index]`, as written.
872
- * `packRecords` and the bundle verifier both walk `body.events` in order, so
873
- * `index` is the same record in both.
401
+ * The `spec_version` of `body.events[index]`'s envelope. `packRecords` and the
402
+ * verifier walk events in the same order.
874
403
  */
875
404
  function recordSpecVersion(bundle, index) {
876
405
  const at = (v, k) => typeof v === "object" && v !== null ? v[k] : undefined;
@@ -878,8 +407,8 @@ function recordSpecVersion(bundle, index) {
878
407
  return Array.isArray(events) ? at(at(events[index], "envelope"), "spec_version") : undefined;
879
408
  }
880
409
  /**
881
- * `specVersion` is a quarantined profile, on the same token boundary the
882
- * envelope layer's own accept-check uses (`specVersionSupported`).
410
+ * Whether `specVersion` is quarantined, on the token boundary
411
+ * `specVersionSupported` uses.
883
412
  */
884
413
  function underQuarantine(specVersion) {
885
414
  if (typeof specVersion !== "string")
@@ -894,96 +423,235 @@ function underQuarantine(specVersion) {
894
423
  });
895
424
  }
896
425
  /**
897
- * The case verdict, from the contributions alone.
898
- *
899
- * **A proven failure keeps the headline** (`evidence-bundle.md` 5.5.4 v1.16,
900
- * A17, proof#158): if any contribution is `failed`, the case is `failed`,
901
- * whatever else contributes, and every other exception stays named beside it.
902
- * Otherwise one outcome is that outcome, and more than one is `mixed`
903
- * (anchor#198, criterion 4), so `mixed` never holds a `failed`. `malformed`
904
- * does NOT keep the headline: it says a record cannot be read, not that a
905
- * check ran and did not pass. `mixed` is in the 3 vocabulary and coins no word.
426
+ * The case verdict, from the contributions alone: `failed` where any contribution is,
427
+ * else `incomplete` where any is, else `verified`. A case reads its worst pack, and every
428
+ * exception is still named beside it.
906
429
  *
907
- * THE INVARIANT, stated so a mutation test has something to kill: `verified` is
908
- * reachable ONLY when `exceptions` is empty. Every exception kind carries a
909
- * non-`verified` contribution (`EXCEPTION_VERDICT`), so this falls out of the
910
- * table rather than being enforced by a second rule that could drift from it.
911
- * The assertion is here anyway, because that is the property every one of the
912
- * seventeen findings on this program violated.
430
+ * Invariant: `verified` only when `exceptions` is empty. It follows from
431
+ * `EXCEPTION_VERDICT` and is asserted anyway.
913
432
  */
914
433
  function verdictOf(exceptions, contributions, holdsPacks = true) {
915
- // A reading that holds at least one pack and no contribution at all is
916
- // `verified` (5.5.4 v1.20, A23): every pack in it graded `verified` and
917
- // nothing in it was an exception. Every exception contributes, so no
918
- // contribution means no exception. A reading with no packs always carries
919
- // `case_holds_no_packs`, and is defended here anyway: an empty aggregate over
920
- // nothing reading as `verified` is the defect class itself.
434
+ // No contribution means every pack graded `verified` and no exception. A
435
+ // reading with no packs is `incomplete`, never `verified` over nothing.
921
436
  if (contributions.length === 0) {
922
437
  if (exceptions.length > 0) {
923
438
  throw new Error(`exceptions with no contribution: ${exceptions.map((e) => e.kind).join(", ")}`);
924
439
  }
925
- return holdsPacks ? "verified" : "partial";
440
+ return holdsPacks ? "verified" : "incomplete";
926
441
  }
927
442
  const outcomes = new Set(contributions.map(([, v]) => v));
928
443
  const verdict = outcomes.has("failed")
929
444
  ? "failed"
930
- : outcomes.size === 1
931
- ? [...outcomes][0]
932
- : "mixed";
445
+ : outcomes.has("incomplete")
446
+ ? "incomplete"
447
+ : "verified";
933
448
  if (verdict === "verified" && exceptions.length > 0) {
934
449
  throw new Error(`verified over a non-empty exception list: ${exceptions.map((e) => e.kind).join(", ")}`);
935
450
  }
936
451
  return verdict;
937
452
  }
453
+ function at(pack, record = null) {
454
+ return [[pack, record]];
455
+ }
456
+ // The envelope faults a record's pack already names with their own claim, so the case does
457
+ // not also call the envelope one whose signature fails.
458
+ const ENVELOPE_FAULTS_OF_THE_PACK = [
459
+ "payload_unbound",
460
+ "not_an_envelope",
461
+ "key_unreadable",
462
+ "spec_version_unknown",
463
+ "nonconforming",
464
+ ];
465
+ /** A pack's own problems (read at position 0) at its position in the case. */
466
+ function moved(problems, position) {
467
+ return problems.map((p) => withItems(p, p.items.map(([, ...rest]) => [position, ...rest])));
468
+ }
938
469
  /**
939
- * Read a whole case: several packs in, one verdict and its exceptions out.
940
- *
941
- * The order of the work, and why it is this order:
942
- *
943
- * 1. **Each pack is read into records**, through `packRecords`, which is the
944
- * one implementation of "what does this pack hold". A pack whose `body_type`
945
- * this reader does not know, or which holds no records, is NAMED here and
946
- * its records are not silently zero.
947
- * 2. **Each pack is graded against its own declaration**, through
948
- * `checkDeclaration`, which is the one implementation of 5.4. A pack proven
949
- * to deviate from its own claim fails the case.
950
- * 3. **Every record every pack supplied goes into ONE pile**, with no partition
951
- * by pack, by issuer or by organization, and the pile is read by
952
- * `readHandoffs`, which is the one implementation of 4.1. This is where
953
- * matching happens, and it is on keys and hashes alone.
954
- * 4. **Each reading becomes an exception or a clean contribution**, and the two
955
- * counterpart kinds are separated by asking whether a pack SIGNED by the
956
- * counterparty's key is in the case, a key read from every pack's wrapper
957
- * before any exit can skip it.
958
- *
959
- * A record whose bytes the pack did not supply never reaches the pile, because
960
- * there is nothing to put in it. That is exactly the shape this module has to
961
- * refuse to pass over in silence, so it is named at step 1 and carries
962
- * `incomplete`: the reader genuinely cannot tell whether the record it cannot
963
- * see is the counterpart that is missing.
470
+ * Every exception, mapped to its claim and state, in exception order. A pack grade
471
+ * carries its own problems; a pack grade with none (a defence) reads as its status.
472
+ */
473
+ export function caseProblems(exceptions) {
474
+ const out = [];
475
+ for (const e of exceptions) {
476
+ let items;
477
+ if (e.recordIndex !== undefined)
478
+ items = at(e.packIndex ?? null, e.recordIndex);
479
+ else if (e.packIndex !== undefined)
480
+ items = at(e.packIndex);
481
+ else
482
+ items = at(null);
483
+ if (e.kind === PACK_GRADE_NOT_VERIFIED) {
484
+ if (e.problems !== undefined && e.problems.length > 0)
485
+ out.push(...e.problems);
486
+ else if (e.status === "failed")
487
+ out.push(problem("bundle_does_not_verify", items));
488
+ else
489
+ out.push(problem("bundle_not_checked", items));
490
+ continue;
491
+ }
492
+ if (e.kind === PAIR_FAILED && e.failedChecks !== undefined && e.failedChecks.length > 0) {
493
+ // One problem per check that failed, so each is said in its own words.
494
+ for (const c of e.failedChecks)
495
+ out.push(problem(e.kind, items, { rule: c }));
496
+ continue;
497
+ }
498
+ out.push(problem(e.kind, items, {
499
+ key: e.kind.startsWith("counterpart_absent") ? (e.counterpartyKey ?? null) : null,
500
+ }));
501
+ }
502
+ return out;
503
+ }
504
+ /**
505
+ * The reading, with its problems and the verdict they give, which must be the verdict
506
+ * the contributions give: two derivations of one answer, held to agree.
507
+ */
508
+ function withProblems(reading) {
509
+ const problems = caseProblems(reading.exceptions);
510
+ const outcome = claimsVerdictOf(claimsOf(problems));
511
+ if (outcome.status !== reading.verdict) {
512
+ throw new Error(`two derivations of one verdict disagree: ${outcome.status} and ${reading.verdict}`);
513
+ }
514
+ return {
515
+ ...reading,
516
+ ...(outcome.reason_kind === undefined ? {} : { reasonKind: outcome.reason_kind }),
517
+ problems,
518
+ };
519
+ }
520
+ /**
521
+ * The pack's grade, read as `verify --logs` reads it: no disproof rests on bytes nobody
522
+ * signed.
523
+ */
524
+ async function gradeInCase(pack, verified, signed, position) {
525
+ const bodyType = bodyTypeOf(pack.bundle);
526
+ const records = packRecords(pack.bundle, pack.content);
527
+ // Per-record gradings are joined to `records` by index. A pack whose
528
+ // `body_hash` does not cover its body reads `failed` with no events, so a
529
+ // count mismatch is graded, never indexed into.
530
+ const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
531
+ const knownBody = CASE_BODY_TYPES.includes(bodyType);
532
+ // The pack's grade propagates: the case never reads above its weakest pack.
533
+ let check = null;
534
+ let full = null;
535
+ let causes = [];
536
+ let grade;
537
+ let packProblems = bundleProblems(signed, position);
538
+ if (signed.unreadable === true) {
539
+ // A body that cannot be read under a wrapper whose time breaks its format fails.
540
+ grade = signed.status === "failed" ? "failed" : "incomplete";
541
+ }
542
+ else if (!knownBody) {
543
+ grade = signed.status;
544
+ }
545
+ else if (ungraded) {
546
+ grade = signed.status === "verified" ? "incomplete" : signed.status;
547
+ if (signed.status === "verified")
548
+ packProblems = [problem("bundle_not_checked", at(position))];
549
+ }
550
+ else {
551
+ // The declaration's chain rule reads the same signed chain links for both
552
+ // checks (evidence-bundle §5.4.2).
553
+ const chainLinks = await readChainLinks(pack.bundle);
554
+ check = checkDeclaration(pack.bundle, pack.content, chainLinks);
555
+ const completeness = completenessNotes(pack.bundle);
556
+ full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
557
+ const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null, chainLinks));
558
+ // No disproof here rests on unsigned bytes: each disproof the pack read with the
559
+ // bytes beside it makes and the signed reading does not is not proven, as `verify`
560
+ // reads it, and everything else is the reading with those bytes, so a pack's logs/
561
+ // count in `case` as they do in `verify --logs`.
562
+ packProblems = moved(restOnSigned(full.problems, bareGrade.problems), position);
563
+ grade = claimsVerdictOf(claimsOf(packProblems)).status;
564
+ causes = full.causes.map((c) => c.kind);
565
+ }
566
+ return { grade, problems: packProblems, causes, check, full, records, ungraded, knownBody };
567
+ }
568
+ /**
569
+ * Each pack that reads as a pack, verified twice: with the side map, and with
570
+ * it empty. What fails only in the first rests on bytes nobody
571
+ * signed. One run where there is no map. Offline, no gateways.
572
+ */
573
+ async function verifyPacks(packs) {
574
+ const runs = new Map();
575
+ for (const [position, pack] of packs.entries()) {
576
+ if (typeof pack.unreadable === "string")
577
+ continue;
578
+ const verified = await verifyEvidenceBundle(pack.bundle, {
579
+ content: (pack.content ?? undefined),
580
+ });
581
+ const signed = sideMapHasKeys(pack.content)
582
+ ? await verifyEvidenceBundle(pack.bundle, {})
583
+ : verified;
584
+ runs.set(position, [verified, signed]);
585
+ }
586
+ return runs;
587
+ }
588
+ /**
589
+ * Read the agent records of every pack as one pile and put each reading on
590
+ * both runs of its pack, in place of the reading over its own pack alone
591
+ * (agent-events.md section 10 step 7): pairing, chains and closing lists span packs.
592
+ * A record takes part where its pack's wrapper holds, its pack's records are
593
+ * all graded, and its envelope verified with its record bytes bound to it.
594
+ */
595
+ async function readAgentRecordsAcross(packs, runs) {
596
+ const items = [];
597
+ const where = [];
598
+ for (const [position, [verified, signed]] of runs) {
599
+ if (!wrapperHolds(verified))
600
+ continue;
601
+ const records = packRecords(packs[position].bundle, packs[position].content);
602
+ if (verified.events.length !== records.length || signed.events.length !== records.length)
603
+ continue;
604
+ for (const [index, record] of records.entries()) {
605
+ const graded = verified.events[index];
606
+ graded.agent = null;
607
+ signed.events[index].agent = null;
608
+ if (graded.envelopeOk && graded.payloadBindingOk === true && record.record !== null) {
609
+ items.push({ event_id: record.eventId, public_key: record.publicKey, record: record.record });
610
+ where.push([position, index]);
611
+ }
612
+ }
613
+ }
614
+ const readings = await readAgentRecords(items);
615
+ for (const [i, [position, index]] of where.entries()) {
616
+ const [verified, signed] = runs.get(position);
617
+ verified.events[index].agent = readings[i];
618
+ signed.events[index].agent = readings[i];
619
+ }
620
+ }
621
+ /**
622
+ * Reads a case: packs in, one verdict and its exceptions out.
623
+ *
624
+ * 1. Each pack is read into records (`packRecords`); an unknown body or no
625
+ * records is named.
626
+ * 2. Each pack is graded against its own declaration (`checkDeclaration`).
627
+ * 3. Every record goes into one pile, unpartitioned, read by `readHandoffs`.
628
+ * 4. Each reading becomes an exception or a clean contribution. The
629
+ * counterpart kinds are told apart by whether a pack signed by the
630
+ * counterparty's key is in the case, read from every wrapper before any exit.
631
+ *
632
+ * Before step 1 the agent records of every pack are read as one pile
633
+ * (agent-events.md section 10), and each pack is graded with those readings
634
+ * (evidence-bundle.md section 5.4.5, section 5.5.1).
964
635
  */
965
636
  export async function readCase(packs) {
966
- // A `null` slot is held for each pack that carries no declaration, and
967
- // settled once the pile says whether the reading holds a hand-off record.
637
+ // A `null` slot is held for each undeclared pack until the pile says whether
638
+ // the reading holds a hand-off record.
968
639
  const exceptions = [];
969
640
  const contributions = [];
970
641
  const note = (kind, fields = {}) => {
971
642
  exceptions.push({ kind, ...fields });
972
- // `null` is PACK_GRADE_NOT_VERIFIED: the pack's case grade, never
973
- // verified, and `incomplete` for a word a pack grade passes through and a
974
- // case verdict never issues (5.5.1 step 1 v1.20).
643
+ // `null` is PACK_GRADE_NOT_VERIFIED: contribute the pack's case grade, never
644
+ // verified (section 5.5.1 step 1).
975
645
  let contribution = EXCEPTION_VERDICT[kind];
976
- if (contribution === null) {
977
- const status = fields.status;
978
- contribution = GRADE_CONTRIBUTES.has(status) ? status : "incomplete";
979
- }
646
+ if (contribution === null)
647
+ contribution = fields.status;
980
648
  contributions.push([fields.eventId ?? fields.packRef ?? "case", contribution]);
981
649
  };
982
650
  if (packs.length === 0) {
983
651
  note(CASE_HOLDS_NO_PACKS);
984
652
  const named = exceptions;
985
653
  const contributed = contributions;
986
- return {
654
+ return withProblems({
987
655
  verdict: verdictOf(named, contributed, false),
988
656
  exceptions: named,
989
657
  records: [],
@@ -992,85 +660,81 @@ export async function readCase(packs) {
992
660
  onChainChecked: false,
993
661
  notes: [],
994
662
  packs: [],
995
- };
663
+ });
996
664
  }
997
- // Step 1 and 2, pack by pack, in the order the reader was given them.
998
- // Whether every pack's witness was re-fetched on chain, ALL over the packs
999
- // this reader verified. It passes no gateways, so this stays false and the
1000
- // reading says so (see `CaseReading.onChainChecked`).
665
+ // Steps 1 and 2, pack by pack. No gateways, so `witnessChecked` stays false.
1001
666
  const witnessChecked = [];
1002
667
  const held = [];
1003
- // The pack and the `event_id` behind each pile member, kept beside the pile
1004
- // rather than parsed back out of a reference string. A reference is for a
1005
- // human to read; a join rebuilt by splitting one is a join on a value a
1006
- // holder can put a separator in.
668
+ // Pack and `event_id` per pile member, kept beside the pile rather than
669
+ // parsed from a reference string a holder could put a separator in.
1007
670
  const provenance = [];
1008
- // Every record of every pack, in pack order and bundle order (A43): where it
1009
- // went into the pile, its place there (`heldAt`), and where it did not, the
1010
- // kind of the exception that says why. Nothing is listed only where the pack
1011
- // body carries no `events` array, and that pack's own exception says why.
671
+ // Every record of every pack, in order: its pile position, or the exception
672
+ // kind that kept it out.
1012
673
  const slots = [];
1013
674
  const unread = (position, packRef, listed, reason) => {
1014
675
  listed.forEach((record, recordIndex) => slots.push({ position, packRef, recordIndex, eventId: record.eventId, heldAt: null, reason }));
1015
676
  };
1016
- // The kinds each pack's declaration check names each record by, keyed by the
1017
- // pack's POSITION and then by `eventId` (A43). The check names a record by
1018
- // `event_id` alone, so under an id the pack carries twice every record with
1019
- // that id is named: failing closed, rather than guessing which of the two the
1020
- // check meant.
677
+ // Declaration-check findings per pack position, then `eventId`. A duplicated
678
+ // id names every record with it: fail closed rather than guess.
1021
679
  const declarationFindings = new Map();
1022
- // Keyed by the pack's POSITION and never by its `ref`. `ref` is the reader's
1023
- // own name for a pack, and nothing stops two packs in one archive carrying
1024
- // the same one: keyed by ref, the second pack's claims silently replaced the
1025
- // first's and records from one pack were graded against another pack's
1026
- // declaration. Found by a constructed archive rather than by reading, which
1027
- // is the only way this class of defect is ever found.
680
+ // Keyed by pack position, never `ref`: two packs can carry one ref, and a
681
+ // record must never be graded against another pack's declaration.
1028
682
  const claimsByPack = new Map();
1029
- // Every key that SIGNED A PACK in this case: the wrapper key, which is the
1030
- // key that signs the pack's declaration. This is the ONE input that separates
1031
- // the two counterpart kinds (see `COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS`),
1032
- // and it replaces a set of every key that signed a RECORD anywhere in the
1033
- // case.
1034
- //
1035
- // The record-key set was wrong twice over, and both times in the direction
1036
- // that matters. It let the ACCUSER manufacture the damning line: one pack,
1037
- // plus one unrelated record the counterparty signed at some other time, read
1038
- // "the counterparty's OWN records ARE here" when the counterparty had handed
1039
- // over nothing (F4). And it let the COUNTERPARTY choose the innocent line: it
1040
- // was filled inside the record loop, which the `body_type` and no-records
1041
- // exits `continue` past, so a counterparty that wrapped its own pack in a body
1042
- // this reader does not read took its keys out of the set (F3), the third
1043
- // instance of a route already closed once for withheld `event_bytes`.
1044
- //
1045
- // A wrapper key closes both. The accuser cannot forge one without the
1046
- // counterparty's private key, and it is read HERE, first, before any exit
1047
- // below, from the pack envelope and never from the body, so no body type, no
1048
- // disclosure choice and no record content decides whether it is read.
683
+ // Every key that signed a pack wrapper (and so its declaration): the one
684
+ // input that separates the counterpart kinds. Wrapper keys, not record keys:
685
+ // an accuser cannot forge one, and it is read before any exit, from the
686
+ // envelope, so no body type or disclosure choice decides whether it counts.
1049
687
  const holderKeys = new Set();
1050
- // One reading per pack, in the order the reader was given them (5.5.4 v1.20).
1051
688
  const packReadings = [];
1052
- // The NOTEs (5.5.3 v1.20), with the pack position they are about.
1053
689
  const caseNotes = [];
1054
- // The role each pack's readable v2 declaration signs for each record, keyed
1055
- // by the pack's POSITION for the reason `claimsByPack` is.
690
+ // Keyed by pack position, as `claimsByPack` is.
1056
691
  const rolesByPack = new Map();
1057
- // The undeclared packs' slots, settled after the pile is read.
692
+ // Undeclared packs' held slots, settled after the pile is read.
1058
693
  const undeclaredSlots = [];
1059
- // How many packs this reader could not tie to a key: an unreadable wrapper
1060
- // key, or a wrapper that does not hold. While it is not zero, "no pack in this
1061
- // case is signed by the named key" is not established, so the innocent
1062
- // one-sided kind is not available (COUNTERPART_ABSENT_SIGNER_UNKNOWN).
694
+ // Packs not tied to a key (unreadable key, or wrapper does not hold). While
695
+ // non-zero, the innocent `COUNTERPART_ABSENT` is not established.
1063
696
  let unattributed = 0;
697
+ const runs = await verifyPacks(packs);
698
+ await readAgentRecordsAcross(packs, runs);
699
+ // The agent reading of each record that went into the pile, by pack and record position.
700
+ const agentReadings = new Map();
1064
701
  for (let position = 0; position < packs.length; position += 1) {
1065
702
  const pack = packs[position];
703
+ if (typeof pack.unreadable === "string") {
704
+ note(PACK_UNREADABLE, { packRef: pack.ref, packIndex: position, reasons: [pack.unreadable] });
705
+ unattributed += 1;
706
+ packReadings.push({
707
+ index: position,
708
+ ref: pack.ref,
709
+ signingKey: null,
710
+ grade: null,
711
+ recordsWithheld: 0,
712
+ declarationVersion: null,
713
+ });
714
+ continue;
715
+ }
1066
716
  const holder = normalizedSigningKey(holderKeyOf(pack.bundle));
1067
717
  if (holder === null) {
1068
- // Named, never skipped: a pack whose key this reader cannot read is a
1069
- // pack whose signer is UNKNOWN, and not a pack nobody signed. Checked
1070
- // BEFORE verification, because the two bundle verifiers disagree on some
1071
- // of these spellings (proof#135) and the reading must not.
718
+ // Signer unknown, not unsigned. Checked before verification so verifier
719
+ // differences over key spelling cannot change the reading.
1072
720
  note(PACK_KEY_UNREADABLE, { packRef: pack.ref, packIndex: position });
1073
721
  unattributed += 1;
722
+ // `signed` is not proven on that claim only: a pack that can be read is still graded,
723
+ // so a forged record inside it, or a `body_hash` that does not hold, fails it. Its
724
+ // records do not join the pile, since no key ties them to a party.
725
+ const [keyless, keylessSigned] = runs.get(position);
726
+ const keylessGrade = await gradeInCase(pack, keyless, keylessSigned, position);
727
+ const inside = keylessGrade.problems.filter((p) => p.kind !== PACK_KEY_UNREADABLE);
728
+ if (inside.length > 0 && keylessSigned.unreadable !== true) {
729
+ note(PACK_GRADE_NOT_VERIFIED, {
730
+ packRef: pack.ref,
731
+ packIndex: position,
732
+ status: claimsVerdictOf(claimsOf(inside)).status,
733
+ causes: keylessGrade.causes,
734
+ reasons: bundleReasons(keylessSigned),
735
+ problems: inside,
736
+ });
737
+ }
1074
738
  unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_KEY_UNREADABLE);
1075
739
  packReadings.push({
1076
740
  index: position,
@@ -1082,23 +746,9 @@ export async function readCase(packs) {
1082
746
  });
1083
747
  continue;
1084
748
  }
1085
- // The precondition `HeldRecord` states, met HERE rather than trusted to a
1086
- // caller: every key this module compares is an envelope `public_key`, and
1087
- // "unforgeable" is a property of a VERIFIED envelope. Offline, no
1088
- // gateways: this reader makes no network call. This kernel's verifier
1089
- // reads envelopes under exactly CASE_RECORD_PROFILES (pinned by a test).
1090
- const verified = await verifyEvidenceBundle(pack.bundle, {
1091
- content: (pack.content ?? undefined),
1092
- });
749
+ const [verified, signed] = runs.get(position);
1093
750
  witnessChecked.push(verified.onChainChecked);
1094
- // The same pack with the side map EMPTY (proof#159): what fails here is
1095
- // what a signature covers, and what fails only above rests on bytes nobody
1096
- // signed. Skipped where there is no map, because the two runs are then the
1097
- // same run.
1098
- const signed = sideMapHasKeys(pack.content)
1099
- ? await verifyEvidenceBundle(pack.bundle, {})
1100
- : verified;
1101
- // WHOSE PACK IS IT. The wrapper, and only the wrapper.
751
+ // Whose pack it is: the wrapper, and only the wrapper.
1102
752
  if (!wrapperHolds(verified)) {
1103
753
  note(PACK_DOES_NOT_VERIFY, { packRef: pack.ref, packIndex: position });
1104
754
  unattributed += 1;
@@ -1115,74 +765,19 @@ export async function readCase(packs) {
1115
765
  }
1116
766
  holderKeys.add(holder);
1117
767
  if (coSignaturesPresent(pack.bundle)) {
1118
- // A NOTE, with no contribution (5.5.3 v1.20, proof#148): the wrapper's
1119
- // co_signatures verifies nothing and sits outside its signed scope, so
1120
- // the case reading names it rather than moving anything on its account.
768
+ // A note, no contribution: `co_signatures` verifies nothing.
1121
769
  caseNotes.push({
1122
770
  position,
1123
771
  note: { kind: CO_SIGNATURES_PRESENT, packRef: pack.ref, packIndex: position },
1124
772
  });
1125
773
  }
1126
- const bodyType = bodyTypeOf(pack.bundle);
1127
- const records = packRecords(pack.bundle, pack.content);
1128
- // The per-record gradings below are joined to `records` BY INDEX, so a
1129
- // verifier that graded a different number of records than the pack holds
1130
- // leaves this loop with no grading to read. That is not hypothetical: a
1131
- // pack whose `body_hash` does not cover its body reads `failed` with
1132
- // `events` EMPTY while `packRecords` reads every record in it, and only
1133
- // `wrapperHolds` above stands between that pack and this loop. A reader
1134
- // must never crash on a pack a party can build, so the gap is graded rather
1135
- // than indexed into, and the pack's records are dropped exactly as a
1136
- // `malformed` pack's are (gate 4 on proof#140, finding 3).
1137
- const ungraded = verified.events.length !== records.length || signed.events.length !== records.length;
1138
- const knownBody = CASE_BODY_TYPES.includes(bodyType);
1139
- // WHAT IS WRONG INSIDE IT, from here on. First the pack's own GRADE, which
1140
- // propagates: the case never reads above its weakest pack (5.5.1 step 1
1141
- // v1.20, A23; up to v1.19 it was the bundle status).
1142
- let check = null;
1143
- let full = null;
1144
- let causes = [];
1145
- let grade;
1146
- if (signed.status === "malformed") {
1147
- grade = "malformed";
1148
- }
1149
- else if (!knownBody) {
1150
- grade = signed.status;
1151
- }
1152
- else if (ungraded) {
1153
- grade = signed.status === "verified" ? "malformed" : signed.status;
1154
- }
1155
- else {
1156
- // Rule 11 reads the chain links of the same pack, and the reader reads
1157
- // only the signed body, so the bare check below takes the same reading
1158
- // (evidence-bundle.md 5.4.2 v1.36, A45).
1159
- const chainLinks = await readChainLinks(pack.bundle);
1160
- check = checkDeclaration(pack.bundle, pack.content, chainLinks);
1161
- const completeness = completenessNotes(pack.bundle);
1162
- full = gradePack(verified, accountDisclosure(pack.bundle, pack.content ?? {}), completeness, check);
1163
- const bareGrade = gradePack(signed, accountDisclosure(pack.bundle, {}), completeness, checkDeclaration(pack.bundle, null, chainLinks));
1164
- // NO `failed` HERE RESTS ON BYTES NOBODY SIGNED. The signed grade decides
1165
- // where it is `failed` or `malformed`; a failure only the side map
1166
- // produced is `incomplete`; otherwise the full grade.
1167
- if (bareGrade.verdict === "failed" || bareGrade.verdict === "malformed") {
1168
- grade = bareGrade.verdict;
1169
- causes = bareGrade.causes.map((c) => c.kind);
1170
- }
1171
- else if (full.verdict === "failed" || full.verdict === "malformed") {
1172
- grade = "incomplete";
1173
- causes = full.causes.map((c) => c.kind);
1174
- }
1175
- else {
1176
- grade = full.verdict;
1177
- causes = full.causes.map((c) => c.kind);
1178
- }
1179
- }
774
+ const graded = await gradeInCase(pack, verified, signed, position);
775
+ const { grade, check, full, causes, records, ungraded, knownBody } = graded;
776
+ const packProblems = graded.problems;
1180
777
  if (grade !== "verified") {
1181
778
  const reasons = bundleReasons(signed);
1182
- if (knownBody && ungraded && signed.status !== "malformed") {
1183
- // Said, because nothing else says it: the verifier's own reasons are
1184
- // about the bundle, and this pack's records are dropped for a reason
1185
- // of this reader's.
779
+ if (knownBody && ungraded && signed.unreadable !== true) {
780
+ // The verifier's reasons are about the bundle; this one is ours.
1186
781
  reasons.push(`this verifier graded ${verified.events.length} of the ${records.length} ` +
1187
782
  "record(s) the pack holds, so none of them is read");
1188
783
  }
@@ -1192,6 +787,7 @@ export async function readCase(packs) {
1192
787
  status: grade,
1193
788
  causes,
1194
789
  reasons,
790
+ problems: packProblems,
1195
791
  });
1196
792
  }
1197
793
  if (verified.status === "failed" && signed.status !== "failed") {
@@ -1211,9 +807,8 @@ export async function readCase(packs) {
1211
807
  declarationVersion: knownBody ? declarationVersionOf(pack.bundle) : null,
1212
808
  });
1213
809
  if (recordsWithheld > 0) {
1214
- // A NOTE, with no contribution (5.5.3 v1.20): the holder signed that
1215
- // these records' content does not travel, and the scope sentence counts
1216
- // them, so a clean verdict never hides them.
810
+ // A note, no contribution: the holder signed that this content does not
811
+ // travel, and the scope sentence counts it.
1217
812
  caseNotes.push({
1218
813
  position,
1219
814
  note: {
@@ -1224,28 +819,20 @@ export async function readCase(packs) {
1224
819
  },
1225
820
  });
1226
821
  }
1227
- // The body is one the verifier could not read, so it graded no record, and
1228
- // nothing below has a grading to read. Named above.
1229
- if (signed.status === "malformed") {
822
+ // The verifier graded no record; named above.
823
+ if (signed.unreadable === true) {
1230
824
  unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_GRADE_NOT_VERIFIED);
1231
825
  continue;
1232
826
  }
1233
827
  if (!knownBody) {
1234
- // `sanning.evidence.export/v2` lands here, and it is NOT unwrapped: its
1235
- // records sit under `body.source_bundle`, and reading them is a change to
1236
- // which packs this reader grades, so it is a decision of its own and not
1237
- // a repair (evidence-bundle.md 5.5.1 step 1 says so too). What this exit
1238
- // costs is that pack's RECORDS, and it is named every time. It no longer
1239
- // costs the pack's KEY: that was read above.
828
+ // Not unwrapped (evidence-bundle §5.5.1). Costs the pack's records, never
829
+ // its key, which was read above.
1240
830
  note(PACK_BODY_TYPE_UNKNOWN, { packRef: pack.ref, packIndex: position });
1241
- // Listed, and not read: the records a `sanning.evidence.export/v2` wraps
1242
- // are listed from its inline source bundle, so a reading never says "0
1243
- // not read" of a pack that holds records (A43).
831
+ // Listed, not read, so a pack holding records never reads "0 not read".
1244
832
  unread(position, pack.ref, listedRecords(pack.bundle, pack.content), PACK_BODY_TYPE_UNKNOWN);
1245
833
  continue;
1246
834
  }
1247
- // Graded above (`malformed` where the verifier called it `verified`), and
1248
- // none of its records are read.
835
+ // Graded above; none of its records are read.
1249
836
  if (ungraded) {
1250
837
  unread(position, pack.ref, records, PACK_GRADE_NOT_VERIFIED);
1251
838
  continue;
@@ -1267,10 +854,8 @@ export async function readCase(packs) {
1267
854
  }
1268
855
  declarationFindings.set(position, namedByCheck);
1269
856
  if (checked.contentFindings.length > 0) {
1270
- // BESIDE the deviation exception and never instead of it: a pack can do
1271
- // both, over different records, and the two say different things. This
1272
- // one is `incomplete` because whoever assembled the case chose the bytes
1273
- // it turns on (proof#147).
857
+ // Beside the deviation exception, never instead: they concern different
858
+ // records. `incomplete` because the case assembler chose the bytes.
1274
859
  note(PACK_DECLARATION_CONTENT_DEPENDENT, {
1275
860
  packRef: pack.ref,
1276
861
  packIndex: position,
@@ -1278,38 +863,31 @@ export async function readCase(packs) {
1278
863
  });
1279
864
  }
1280
865
  if (checked.verdict === "failed") {
1281
- note(PACK_DECLARATION_DEVIATES, {
1282
- packRef: pack.ref,
1283
- packIndex: position,
1284
- count: checked.deviations.length,
1285
- });
866
+ // A declaration that does not parse deviates from nothing: the pack grade names it as
867
+ // a declaration that breaks its own format.
868
+ if (checked.deviations.length === 0 ||
869
+ checked.deviations.some((d) => d.kind !== "declaration_malformed")) {
870
+ note(PACK_DECLARATION_DEVIATES, {
871
+ packRef: pack.ref,
872
+ packIndex: position,
873
+ count: checked.deviations.length,
874
+ });
875
+ }
1286
876
  }
1287
877
  else if (checked.contentFindings.length > 0) {
1288
- // Already named above. `partial` here is not "the reader cannot read the
1289
- // claim": the claim was read, and part of it rests on bytes nobody
1290
- // signed.
878
+ // Named above: the claim was read, and part of it rests on unsigned bytes.
1291
879
  }
1292
880
  else if (checked.verdict === "verified") {
1293
- // A pack that agrees with its own claim contributes NOTHING, rather than
1294
- // a `verified` of its own. The case verdict is over the hand-off
1295
- // readings, and criterion 4's "mixed" is about mixed HAND-OFF outcomes: a
1296
- // pack-level clean contribution would put `verified` into the set on
1297
- // every case, so one broken pair beside two honest declarations would
1298
- // read `mixed` and the failure would be the quieter half of a two-word
1299
- // answer. A pack-level PROBLEM still contributes, through its exception.
881
+ // A clean pack contributes nothing, so the case verdict reflects hand-off
882
+ // outcomes: a `verified` per pack would turn one broken pair into `mixed`.
1300
883
  }
1301
884
  else if (checked.present) {
1302
- // `partial` with a declaration present is 5.4.3's "a claim this reader
1303
- // cannot read", which `gradePack` already carries to `incomplete`. The
1304
- // same posture here, for the same reason: the field has no accept-set and
1305
- // the holder writes it.
885
+ // A claim this reader cannot read (§5.4.3); the holder writes the field.
1306
886
  note(PACK_DECLARATION_UNREADABLE, { packRef: pack.ref, packIndex: position });
1307
887
  }
1308
888
  else {
1309
- // WHAT IT CONTRIBUTES TURNS ON THE WHOLE READING (5.5.1 step 2 v1.20,
1310
- // A23, I4): `partial` where the reading holds a hand-off record, and a
1311
- // NOTE where it holds none. That is not known until the pile is read, so
1312
- // the slot is held HERE, in pack order, and settled below.
889
+ // `partial` if the reading holds a hand-off record, else a note (§5.5.1).
890
+ // Not known until the pile is read, so hold the slot.
1313
891
  undeclaredSlots.push({
1314
892
  exceptionAt: exceptions.length,
1315
893
  contributionAt: contributions.length,
@@ -1319,25 +897,27 @@ export async function readCase(packs) {
1319
897
  exceptions.push(null);
1320
898
  contributions.push(null);
1321
899
  }
1322
- // Each record on its OWN envelope. `packRecords` and the verifier both walk
1323
- // `body.events` in order and emit one entry per element, so the index is
1324
- // the join, exactly as `held`, `provenance` and `readings` below.
900
+ // Each record on its own envelope, joined by index.
1325
901
  for (let index = 0; index < records.length; index += 1) {
1326
902
  const record = records[index];
1327
903
  const graded = verified.events[index];
1328
904
  const bare = signed.events[index];
1329
- // Which pack and which record, by position: the two names that are
1330
- // unique in the reading whatever the reader called the pack and whatever
1331
- // the holder wrote as `event_id` (A43).
905
+ // Pack and record by position: unique whatever `ref` or `event_id` say.
1332
906
  const at = {
1333
907
  packRef: pack.ref,
1334
908
  packIndex: position,
1335
909
  eventId: record.eventId,
1336
910
  recordIndex: index,
1337
911
  };
1338
- // Not read: listed with the kind of the exception that says why.
912
+ // Not read: listed with the kind of the exception that says why. Signed bytes that
913
+ // bind and are not JSON, and an envelope that cannot be read, names a version this
914
+ // verifier does not know or breaks its format under a signature that holds, are their
915
+ // pack's own problems, so no exception is noted here.
1339
916
  const skip = (kind, fields = {}) => {
1340
- note(kind, { ...at, ...fields });
917
+ if (!(kind === RECORD_BYTES_UNREADABLE && bare.recordNotJson === true) &&
918
+ !(kind === RECORD_ENVELOPE_DOES_NOT_VERIFY && ENVELOPE_FAULTS_OF_THE_PACK.includes(graded.envelopeFault ?? ""))) {
919
+ note(kind, { ...at, ...fields });
920
+ }
1341
921
  slots.push({ position, packRef: pack.ref, recordIndex: index, eventId: record.eventId, heldAt: null, reason: kind });
1342
922
  };
1343
923
  if (underQuarantine(recordSpecVersion(pack.bundle, index))) {
@@ -1356,9 +936,8 @@ export async function readCase(packs) {
1356
936
  skip(RECORD_BYTES_UNREADABLE);
1357
937
  continue;
1358
938
  }
1359
- // Named, and the record stays in the pile (see the kinds). A mismatch
1360
- // the signed body carries on its own is `failed`; one that appears only
1361
- // with the side map is `incomplete` (proof#159).
939
+ // Named, and the record stays in the pile. A mismatch in the signed body
940
+ // is `failed`; one only with the side map is `incomplete`.
1362
941
  if (bare.contentOk === false) {
1363
942
  note(RECORD_CONTENT_MISMATCH, {
1364
943
  ...at,
@@ -1371,10 +950,12 @@ export async function readCase(packs) {
1371
950
  reasons: onlyIn(graded.errors, bare.errors),
1372
951
  });
1373
952
  }
1374
- // Every other check on the SIGNED reading, so side content can neither
1375
- // add a failure here nor take one away.
1376
- if (!(bare.inclusionOk && bare.checkpointBound) ||
1377
- (!bare.ok && bare.contentOk !== false)) {
953
+ // Every other check on the signed reading, so side content can neither
954
+ // add a failure here nor remove one. A stamp the pack does not hold, and signed
955
+ // bytes that are not JSON, are their pack's own problems (not proven, and a
956
+ // producer's error), never a record that does not match its pack.
957
+ if (!(bare.stampAbsent === true || bare.recordNotJson === true) &&
958
+ (!(bare.inclusionOk && bare.checkpointBound) || (!bare.ok && bare.contentOk !== false))) {
1378
959
  note(RECORD_DOES_NOT_VERIFY_IN_ITS_PACK, {
1379
960
  ...at,
1380
961
  reasons: [...bare.errors],
@@ -1388,41 +969,30 @@ export async function readCase(packs) {
1388
969
  heldAt: held.length,
1389
970
  reason: null,
1390
971
  });
972
+ const agent = graded.agent;
973
+ agentReadings.set(`${position}:${index}`, agent);
1391
974
  held.push({
1392
975
  publicKey: record.publicKey,
1393
976
  record: record.record,
1394
977
  ref: `${pack.ref}/${record.eventId}`,
978
+ agentNonconforming: agent !== null && agent.verdict === "failed",
1395
979
  });
1396
980
  provenance.push({ position, packRef: pack.ref, eventId: record.eventId, recordIndex: index });
1397
981
  }
1398
982
  }
1399
- // Step 3. ONE pile, no partition. This is the line anchor#198 turns on and
1400
- // the line DL-60 constrains: there is nothing here that reads a tenant, an
1401
- // issuer or an organization, so a pair across two organizations' packs is
1402
- // read exactly as a pair inside one.
983
+ // Step 3: one pile, no partition by tenant, issuer or organization.
1403
984
  const readings = readHandoffs(held);
1404
985
  // Step 4.
1405
986
  let handoffRecords = 0;
1406
987
  for (let index = 0; index < readings.length; index += 1) {
1407
988
  const reading = readings[index];
1408
989
  const { position, packRef, eventId, recordIndex } = provenance[index];
1409
- // The record this reading is about, by position (A43).
1410
990
  const at = { packRef, packIndex: position, eventId, recordIndex };
1411
- // proof#137's door, named. Read from the ONE reader of the declaration
1412
- // (`declarationClaims`), never from a second parse here. Only a READABLE
1413
- // declaration can make a record unclaimed: where the claim itself is absent
1414
- // or unreadable, that pack already carries its own exception and naming
1415
- // every record in it again would report one defect many times.
1416
- //
1417
- // ABOVE the two `continue`s below, deliberately. This check asks what the
1418
- // PACK said about the record, which is independent of whether 4.1 governs
1419
- // the record and of whether the record is well formed. Behind the
1420
- // `not_a_handoff_record` exit it was a check the audited party switched off
1421
- // by respelling two fields, and behind the `malformed` exit it was one a
1422
- // party switched off by breaking its own record.
1423
- //
1424
- // v1's ALONE (5.5.1 v1.20, A21). A v2 pack's claim check is rule 9, which
1425
- // the declaration check already ran on the signed body.
991
+ // Read from `declarationClaims`, never a second parse. Only a readable
992
+ // declaration makes a record unclaimed; otherwise the pack's own exception
993
+ // covers it. Runs above both `continue`s below, so neither respelling a
994
+ // record out of §4.1 nor breaking it switches the check off. v1 only: a v2
995
+ // pack's claim check already ran inside the declaration check.
1426
996
  const claims = claimsByPack.get(position);
1427
997
  if (claims !== undefined &&
1428
998
  claims.readable &&
@@ -1431,50 +1001,41 @@ export async function readCase(packs) {
1431
1001
  note(RECORD_UNCLAIMED, at);
1432
1002
  }
1433
1003
  if (reading.findings.includes(NOT_A_HANDOFF_FINDING)) {
1434
- // 4.1 (v1.9, A14): information, not absence. A record 4.1 does not govern
1435
- // is not a hand-off exception, and it is still in `records` and still
1436
- // subject to the claim check above.
1004
+ // Not a hand-off exception; still listed and still claim-checked above.
1437
1005
  continue;
1438
1006
  }
1439
1007
  handoffRecords += 1;
1440
- if (reading.verdict === "malformed") {
1441
- // Whose defect is it? `recordIsMalformed` is 4.1's own predicate, CALLED
1442
- // and not restated, and it answers about THIS record. A `malformed`
1443
- // aggregate over a record that is itself well formed is a malformed
1444
- // CANDIDATE, so it is a different kind, it does not `continue`, and the
1445
- // counterpart, ambiguity and `failedChecks` reporting below all keep
1446
- // running over it.
1008
+ if (reading.findings.includes("malformed_record")) {
1009
+ // `recordIsMalformed` answers about this record. A malformed aggregate
1010
+ // over a well-formed record is a malformed candidate, and reporting
1011
+ // continues for it.
1447
1012
  if (recordIsMalformed(held[index])) {
1448
1013
  note(RECORD_MALFORMED, at);
1449
1014
  continue;
1450
1015
  }
1451
1016
  note(COUNTERPART_MALFORMED, at);
1452
1017
  }
1018
+ else if (reading.findings.includes("nonconforming_record")) {
1019
+ // The same question for the other defect: this record's own, or a candidate's.
1020
+ if (recordIsNonconforming(held[index])) {
1021
+ note(RECORD_NONCONFORMING, at);
1022
+ continue;
1023
+ }
1024
+ note(COUNTERPART_NONCONFORMING, at);
1025
+ }
1453
1026
  if (reading.findings.includes(COUNTERPART_ABSENT)) {
1454
- // `held`, `provenance` and `readings` are three views of one list in one
1455
- // order, built together and never filtered, so the index is the join.
1456
- // Deliberately not a lookup by reference string: a reference is for a
1457
- // person to read, and a join rebuilt by splitting one is a join on a
1458
- // value a holder can put a separator inside.
1027
+ // `held`, `provenance` and `readings` share one order, never filtered, so
1028
+ // the index is the join.
1459
1029
  const subject = held[index];
1460
1030
  const event = subject.record["event"];
1461
1031
  const named = normalizedSigningKey(typeof event === "object" && event !== null
1462
1032
  ? event["counterparty_public_key"]
1463
1033
  : undefined);
1464
- // A VERIFIED pack SIGNED by the named key is in the case, and nothing
1465
- // else. Records signed by that key which some other party holds are not
1466
- // that party's pack. Where some pack could not be tied to a key, "no
1467
- // pack is signed by the named key" is not established and the reading
1468
- // says it could not tell.
1469
- //
1470
- // EXCEPT where 4.1 set every candidate aside (v1.17, A20). There the
1471
- // absence is a finding about the candidates the reader DID read, each
1472
- // paired `verified` with another record the subject's own signer made,
1473
- // and not a finding about any pack. 4.1 and evidence-bundle A16 name the
1474
- // neutral kind for it, and the damning one would put the audited party's
1475
- // own unfaithful record on the counterparty's pack as a statement
1476
- // against it. `count` is how many candidates were set aside, which is
1477
- // also what tells the rendered sentence which absence this is.
1034
+ // The damning kind needs a verified pack signed by the named key; with
1035
+ // any unattributed pack, the reading says it could not tell. Where every
1036
+ // candidate was set aside (each paired with another record by the
1037
+ // subject's own signer), the absence is about those candidates, not any
1038
+ // pack, so the neutral kind carries `count`.
1478
1039
  const graded = reading.candidateReadings;
1479
1040
  const setAside = graded.length > 0 && graded.every((c) => c.accountedFor !== null)
1480
1041
  ? graded.length
@@ -1497,13 +1058,9 @@ export async function readCase(packs) {
1497
1058
  count: reading.candidateReadings.filter((c) => c.accountedFor === null).length,
1498
1059
  });
1499
1060
  }
1500
- // ON `failedChecks` AND NOT ON THE VERDICT WORD. 4.1 keeps the UNION of
1501
- // every candidate's failed checks precisely so a junk record cannot strip a
1502
- // named failure out of the report (proof#133 ruling 6), and reading the
1503
- // verdict instead threw that union away: `malformed` outranks `failed`, so
1504
- // one junk candidate took the branch below and a proven `message_hash`
1505
- // failure disappeared from the case. A named failed check is a named failed
1506
- // check whatever the aggregate word is.
1061
+ // On `failedChecks`, not the verdict word: `malformed` outranks `failed`,
1062
+ // and §4.1 keeps the union of failed checks so a junk candidate cannot hide
1063
+ // a proven failure.
1507
1064
  if (reading.failedChecks.length > 0) {
1508
1065
  note(PAIR_FAILED, {
1509
1066
  ...at,
@@ -1514,11 +1071,8 @@ export async function readCase(packs) {
1514
1071
  contributions.push([`${packRef}/${eventId}`, "verified"]);
1515
1072
  }
1516
1073
  }
1517
- // A reading with no hand-off record in it is not an exception (5.5.3 v1.20,
1518
- // A23, D6). The undeclared packs, settled: the exception `pack_undeclared`,
1519
- // contributing `partial`, in the slot it was given in pack order where the
1520
- // reading holds a hand-off record; a NOTE with no contribution where it holds
1521
- // none.
1074
+ // Undeclared packs: the `pack_undeclared` exception, contributing `incomplete`,
1075
+ // where the reading holds a hand-off record; otherwise a note (§5.5.3).
1522
1076
  for (const slot of undeclaredSlots) {
1523
1077
  if (handoffRecords > 0) {
1524
1078
  exceptions[slot.exceptionAt] = {
@@ -1537,17 +1091,13 @@ export async function readCase(packs) {
1537
1091
  }
1538
1092
  const named = exceptions.filter((e) => e !== null);
1539
1093
  const contributed = contributions.filter((c) => c !== null);
1540
- // Pack order, stable. A pack carries at most one note: one with no
1541
- // declaration withholds nothing by one.
1094
+ // Pack order, stable.
1542
1095
  const notes = caseNotes
1543
1096
  .map((n, i) => ({ ...n, i }))
1544
1097
  .sort((a, b) => a.position - b.position || a.i - b.i)
1545
1098
  .map((n) => n.note);
1546
- // Every record of every pack, with its status (A43). A record that went
1547
- // into the pile is `read_flagged` where any exception names it by pack and
1548
- // record position, or its pack's declaration check names it (by deviation
1549
- // or by content finding), and `read_clean` where neither does; every other
1550
- // record is `not_read`, with the kind of the exception that says why.
1099
+ // Read records are `read_flagged` when an exception or declaration finding
1100
+ // names them, else `read_clean`; the rest are `not_read`.
1551
1101
  const flagged = new Set(named
1552
1102
  .filter((e) => e.packIndex !== undefined && e.recordIndex !== undefined)
1553
1103
  .map((e) => `${e.packIndex}:${e.recordIndex}`));
@@ -1570,6 +1120,7 @@ export async function readCase(packs) {
1570
1120
  failedChecks: [],
1571
1121
  counterparts: [],
1572
1122
  role: null,
1123
+ agent: null,
1573
1124
  };
1574
1125
  }
1575
1126
  const reading = readings[slot.heldAt];
@@ -1585,9 +1136,10 @@ export async function readCase(packs) {
1585
1136
  failedChecks: [...reading.failedChecks],
1586
1137
  counterparts: [...reading.candidates],
1587
1138
  role: rolesByPack.get(slot.position)?.get(slot.eventId) ?? null,
1139
+ agent: agentReadings.get(`${slot.position}:${slot.recordIndex}`) ?? null,
1588
1140
  };
1589
1141
  });
1590
- return {
1142
+ return withProblems({
1591
1143
  verdict: verdictOf(named, contributed),
1592
1144
  exceptions: named,
1593
1145
  records,
@@ -1596,14 +1148,11 @@ export async function readCase(packs) {
1596
1148
  onChainChecked: witnessChecked.length > 0 && witnessChecked.every((c) => c),
1597
1149
  notes,
1598
1150
  packs: packReadings,
1599
- };
1151
+ });
1600
1152
  }
1601
1153
  /**
1602
- * The records a pack LISTS where this reader does not read them one by one
1603
- * (A43): the events `resolveTraceEvents` presents, which unwraps a
1604
- * `sanning.evidence.export/v2` to its inline source bundle, and otherwise
1605
- * `body.events` as `packRecords` reads it. Listing is not reading: every record
1606
- * listed here is `not_read`.
1154
+ * The records a pack lists when this reader does not read them one by one,
1155
+ * unwrapping an export to its source bundle. Every one is `not_read`.
1607
1156
  */
1608
1157
  function listedRecords(bundle, content) {
1609
1158
  const events = resolveTraceEvents(bundle);
@@ -1611,10 +1160,7 @@ function listedRecords(bundle, content) {
1611
1160
  return packRecords(bundle, content);
1612
1161
  return packRecords({ body: { events } }, content);
1613
1162
  }
1614
- /**
1615
- * `body.declaration.declaration_version` where it is one this reader knows,
1616
- * and `null` otherwise.
1617
- */
1163
+ /** The pack's known `declaration_version`, or `null`. */
1618
1164
  function declarationVersionOf(bundle) {
1619
1165
  const at = (v, k) => typeof v === "object" && v !== null && !Array.isArray(v)
1620
1166
  ? v[k]
@@ -1628,15 +1174,11 @@ function declarationVersionOf(bundle) {
1628
1174
  ? version
1629
1175
  : null;
1630
1176
  }
1631
- // ---- Presentation (optional) ------------------------------------------------
1177
+ const because = (reasons) => reasons !== undefined && reasons.length > 0 ? `: ${reasons.join("; ")}` : "";
1632
1178
  /**
1633
- * The house wording for one exception.
1634
- *
1635
- * Offered, not imposed, exactly as `renderDeviation` is: what must not vary
1636
- * between surfaces is the verdict and the set of exceptions, and those come
1637
- * from `readCase`. Both kernels return this same text for the same exception.
1179
+ * The house wording for one exception. Optional: the verdict and the set of
1180
+ * exceptions are what must not vary between surfaces.
1638
1181
  */
1639
- const because = (reasons) => reasons !== undefined && reasons.length > 0 ? `: ${reasons.join("; ")}` : "";
1640
1182
  export function renderException(exception) {
1641
1183
  const where = `${exception.packRef}/${exception.eventId}`;
1642
1184
  switch (exception.kind) {
@@ -1653,78 +1195,82 @@ export function renderException(exception) {
1653
1195
  "and no pack in this case is signed by the key it names");
1654
1196
  case COUNTERPART_ABSENT_FROM_COUNTERPARTY_RECORDS:
1655
1197
  return (`${where} has no counterpart among the records this verifier read, ` +
1656
- "and a pack signed by the key it names IS in this case and holds " +
1198
+ "and a pack signed by the key it names is in this case and holds " +
1657
1199
  "no counterpart this verifier could read");
1658
1200
  case COUNTERPART_ABSENT_SIGNER_UNKNOWN:
1659
1201
  return (`${where} has no counterpart among the records this verifier read, ` +
1660
1202
  "and a pack in this case could not be tied to a signing key, so " +
1661
1203
  "whether a pack signed by the key it names is in this case is " +
1662
- "UNKNOWN");
1204
+ "unknown");
1663
1205
  case COUNTERPART_AMBIGUOUS:
1664
1206
  return (`${where} has ${exception.count} candidate counterparts, so the ` +
1665
1207
  "reading is the worst of them and no one match was chosen");
1666
1208
  case PAIR_FAILED:
1667
- return `${where} FAILED on ${(exception.failedChecks ?? []).join(", ")}`;
1209
+ return `${where} failed on ${(exception.failedChecks ?? []).join(", ")}`;
1668
1210
  case RECORD_MALFORMED:
1669
1211
  return `${where} is not a well formed hand-off record`;
1670
1212
  case COUNTERPART_MALFORMED:
1671
1213
  return (`${where} is well formed and a candidate counterpart of it is not, ` +
1672
- "so what this record pairs with is UNKNOWN");
1214
+ "so what this record pairs with is unknown");
1215
+ case RECORD_NONCONFORMING:
1216
+ return (`${where} is signed and intact and does not follow its vocabulary, ` +
1217
+ "so nothing is proven altered and it cannot be paired");
1218
+ case COUNTERPART_NONCONFORMING:
1219
+ return (`${where} conforms and a candidate counterpart of it does not, ` +
1220
+ "so what this record pairs with is unknown");
1673
1221
  case RECORD_UNCLAIMED:
1674
1222
  return (`${where} is a record its own pack declares and claims in no ` +
1675
1223
  "hand-off pair, so whether the holder meant to hand it over is " +
1676
- "UNKNOWN");
1224
+ "unknown");
1677
1225
  case RECORD_BYTES_WITHHELD:
1678
- return `${where} travels with no record bytes, so what that record IS is UNKNOWN`;
1226
+ return `${where} travels with no record bytes, so what that record is, is unknown`;
1679
1227
  case RECORD_BYTES_UNREADABLE:
1680
1228
  return `${where} travels with record bytes this verifier cannot read`;
1681
1229
  case PACK_DECLARATION_DEVIATES:
1682
- // "read together with" and not "deviates", flat: 5.4 grades the pack
1683
- // against the content the reader was handed, and content beside a pack is
1684
- // unsigned. Whoever assembles a case can swap it, so the sentence names
1685
- // what was compared rather than only the pack.
1230
+ // Names what was compared: the side content is unsigned and swappable.
1686
1231
  return (`${exception.packRef}, read together with any content handed in ` +
1687
- `beside it, DEVIATES from its own declaration, in ` +
1232
+ `beside it, deviates from its own declaration, in ` +
1688
1233
  `${exception.count} way(s)`);
1689
1234
  case PACK_DECLARATION_CONTENT_DEPENDENT:
1690
- // Says what this verifier could not establish, and names the content
1691
- // rather than the pack's holder: the bytes are unsigned and whoever
1692
- // assembled the case chose them.
1235
+ // Names what could not be established and the content, never the holder:
1236
+ // the bytes are unsigned and the case assembler chose them.
1693
1237
  return (`${exception.packRef} agrees with its own declaration everywhere its ` +
1694
1238
  `signature reaches, and in ${exception.count} way(s) it does not line ` +
1695
1239
  "up with the unsigned content handed in beside it, which anyone could " +
1696
- "have supplied, so whether it holds what it declares is UNKNOWN");
1240
+ "have supplied, so whether it holds what it declares is unknown");
1697
1241
  case PACK_DECLARATION_UNREADABLE:
1698
1242
  return (`${exception.packRef} says what it holds in a declaration this ` +
1699
- "verifier cannot read, so whether it is complete is UNKNOWN");
1243
+ "verifier cannot read, so whether it is complete is unknown");
1700
1244
  case PACK_UNDECLARED:
1701
1245
  return (`${exception.packRef} does not say what it holds, so whether it is ` +
1702
- "complete is UNKNOWN");
1246
+ "complete is unknown");
1703
1247
  case PACK_BODY_TYPE_UNKNOWN:
1704
1248
  return (`${exception.packRef} is a pack body this verifier does not read, so ` +
1705
- "what it holds is UNKNOWN");
1249
+ "what it holds is unknown");
1706
1250
  case PACK_HOLDS_NO_RECORDS:
1707
1251
  return `${exception.packRef} holds no records this verifier can read`;
1252
+ case PACK_UNREADABLE:
1253
+ return (`${exception.packRef} cannot be read: its bundle.json ` +
1254
+ `${(exception.reasons ?? []).join("; ")}, so who signed it is unknown and none of its ` +
1255
+ "records are read");
1708
1256
  case PACK_KEY_UNREADABLE:
1709
1257
  return (`${exception.packRef} names a signing key this verifier cannot ` +
1710
- "read, so who signed it is UNKNOWN and none of its records are read");
1258
+ "read, so who signed it is unknown and none of its records are read");
1711
1259
  case PACK_DOES_NOT_VERIFY:
1712
1260
  return (`${exception.packRef} carries a wrapper signature over its body that ` +
1713
1261
  "does not verify, so none of its records are read and the key it " +
1714
1262
  "names is not counted as its signer");
1715
1263
  case PACK_GRADE_NOT_VERIFIED: {
1716
- let head = `${exception.packRef} grades ${(exception.status ?? "").toUpperCase()} as a pack`;
1264
+ let head = `${exception.packRef} grades ${exception.status} as a pack`;
1717
1265
  if (exception.causes !== undefined && exception.causes.length > 0) {
1718
1266
  head += ` (${exception.causes.join(", ")})`;
1719
1267
  }
1720
- if (exception.status === "malformed")
1721
- head += ", so none of its records are read";
1722
1268
  return head + because(exception.reasons);
1723
1269
  }
1724
1270
  case PACK_BUNDLE_CONTENT_DEPENDENT:
1725
- return (`${exception.packRef} reads FAILED as a bundle only together with ` +
1271
+ return (`${exception.packRef} reads failed as a bundle only together with ` +
1726
1272
  "the unsigned content handed in beside it, which anyone could have " +
1727
- "supplied, so whether that failure is the pack's is UNKNOWN" +
1273
+ "supplied, so whether that failure is the pack's is unknown" +
1728
1274
  because(exception.reasons));
1729
1275
  case RECORD_ENVELOPE_DOES_NOT_VERIFY:
1730
1276
  return (`${where} is in a pack whose wrapper verifies, and its own envelope ` +
@@ -1741,23 +1287,24 @@ export function renderException(exception) {
1741
1287
  case RECORD_CONTENT_DEPENDENT:
1742
1288
  return (`${where} commits to content that the unsigned content handed in ` +
1743
1289
  "beside its pack does not match, and anyone could have supplied " +
1744
- "that content, so what it says about the record is UNKNOWN; the " +
1290
+ "that content, so what it says about the record is unknown; the " +
1745
1291
  "record is still read as a hand-off record" +
1746
1292
  because(exception.reasons));
1747
1293
  case RECORD_PROFILE_QUARANTINED:
1748
1294
  return (`${where} is signed under a profile this case reader does not read ` +
1749
- "while its admission is undecided, so what it says is UNKNOWN");
1295
+ "while its admission is undecided, so what it says is unknown");
1750
1296
  case CASE_HOLDS_NO_PACKS:
1751
1297
  return "this case holds no packs";
1752
1298
  default:
1753
1299
  return exception.kind;
1754
1300
  }
1755
1301
  }
1756
- /** The house wording for one NOTE. Offered, not imposed, as `renderException` is. */
1302
+ /** The house wording for one note. */
1757
1303
  export function renderCaseNote(note) {
1758
1304
  switch (note.kind) {
1759
1305
  case CONTENT_WITHHELD_BY_DECLARATION:
1760
- return (`${note.packRef} withholds the content of ${note.count} record(s) by ` +
1306
+ return (`${note.packRef} withholds the content of ${note.count} ` +
1307
+ `${note.count === 1 ? "record" : "records"} by ` +
1761
1308
  "its own signed declaration: their signatures and commitments were " +
1762
1309
  "checked, their content was not");
1763
1310
  case PACK_UNDECLARED:
@@ -1770,18 +1317,11 @@ export function renderCaseNote(note) {
1770
1317
  }
1771
1318
  }
1772
1319
  /**
1773
- * The scope sentence, printed directly under the headline (evidence-bundle
1774
- * 5.5.6 v1.20, A26), byte for byte the text `render_case_summary` returns.
1775
- *
1776
- * The packs in the reading; the distinct keys, compared as keys, that signed
1777
- * the wrapper of a pack whose wrapper held; the hand-off records; the
1778
- * exceptions; and that the reading was made offline. Where any pack holds
1779
- * records withheld by its declaration, their total; where any pack is noted
1780
- * `pack_undeclared`, how many. Those two lines are part of what the headline
1781
- * MEANS, and a surface MUST print them with it.
1782
- *
1783
- * What was not checked on the public record is no longer a clause here. It is
1784
- * the witness line (`renderWitnessLine`), printed with every headline.
1320
+ * The scope sentence printed under the headline (evidence-bundle §5.5.6): packs,
1321
+ * distinct signing keys of packs whose wrapper held, hand-off records,
1322
+ * exceptions, and that it was read offline, plus withheld and undeclared
1323
+ * counts where non-zero. Those counts are part of what the headline means, and
1324
+ * a surface must print them with it.
1785
1325
  */
1786
1326
  export function renderCaseSummary(reading) {
1787
1327
  const keys = new Set(reading.packs.filter((p) => p.grade !== null).map((p) => p.signingKey));
@@ -1800,66 +1340,43 @@ export function renderCaseSummary(reading) {
1800
1340
  lines.push(`${undeclared} pack(s) do not say what they hold.`);
1801
1341
  return lines.join(" ");
1802
1342
  }
1803
- // ---- The headline and the witness line (evidence-bundle 5.5.6 v1.20) -------
1804
1343
  /**
1805
- * The one display title of each of the eight 3 words. A surface prints the
1806
- * title, never the word, and never a title of its own (#173, A18). The wording
1807
- * is Will's to change through the glossary (DL-71); what each MEANS is pinned
1808
- * in the spec.
1344
+ * The one display title of each of the three section 3 verdicts. A surface prints the
1345
+ * title, never the word, and never a title of its own.
1809
1346
  */
1810
- export const VERDICT_DISPLAY_TITLES = {
1811
- verified: "Verified",
1812
- partial: "Partial",
1813
- pending: "Pending",
1814
- incomplete: "Incomplete",
1815
- failed: "Failed",
1816
- malformed: "Not readable",
1817
- not_found: "Not found",
1818
- mixed: "Mixed",
1819
- };
1347
+ export const VERDICT_DISPLAY_TITLES = { ...VERDICT_TITLES };
1820
1348
  /**
1821
- * What a reader may rely on, for each of the six words a case reading reaches
1822
- * (5.5.6 v1.20, A26; the `verified` text at v1.28, A35), the same for every
1823
- * drop. `pending` and `not_found` never head a case reading: they are
1824
- * witness states. `verified` is the base sentence, printed alone where the
1825
- * drop holds no hand-off record; a caller reads `caseReliance` for
1826
- * `verified`, never this mapping directly.
1349
+ * What a reader may rely on, for each verdict a case reading reaches (section 5.5.6), the
1350
+ * same for every drop, keyed by the verdict and, for `failed`, its reason kind. Call
1351
+ * `caseReliance` rather than reading `verified` here directly.
1827
1352
  */
1828
1353
  export const CASE_RELIANCE = {
1829
1354
  verified: "Every pack in this drop is genuine, unaltered, and matches its own " +
1830
1355
  "signed declaration. Every record in it was signed by its holder's " +
1831
1356
  "key. This was read offline, on this device. It does not say this " +
1832
1357
  "drop holds everything that was recorded.",
1833
- partial: "Nothing here was proven wrong, and a check could not be completed: a " +
1834
- "pack names no witness, or a pack in a hand-off reading does not say " +
1835
- "what it holds. The exceptions say which.",
1836
1358
  incomplete: "Nothing here was proven wrong, and part of it could not be checked: " +
1837
1359
  "the other side of a hand-off is not in this drop, bytes were not " +
1838
- "handed over, or a claim could not be read. Each exception says what " +
1839
- "is missing and, where a key is known, whose pack would supply it.",
1360
+ "handed over or could not be read, a pack names no witness, or a " +
1361
+ "claim could not be read. Do not rely on the parts that are not " +
1362
+ "proven. Each exception says what is missing and, where a key is " +
1363
+ "known, whose pack would supply it.",
1840
1364
  failed: "A check ran and did not pass. Do not rely on this drop until the " +
1841
1365
  "named failure is explained. The failure is named on the pack or " +
1842
1366
  "record it is about, and every other exception is listed beside it.",
1843
- malformed: "Part of this drop could not be read as evidence, and nothing was " +
1844
- "proven wrong. The exceptions name what could not be read.",
1845
- mixed: "The readings here differ and none of them failed: some are verified " +
1846
- "and some could not be checked. Read each pack's row and each " +
1847
- "exception before relying on any of it.",
1367
+ failed_nonconforming: "A record in this drop is signed and unaltered and does not follow " +
1368
+ "the vocabulary it names: its producer wrote something the " +
1369
+ "specification does not allow. Nothing was proven altered. The " +
1370
+ "exceptions name the record and the rule it breaks.",
1848
1371
  };
1849
1372
  /**
1850
- * The clause a `verified` drop that holds a hand-off record gains, inserted
1851
- * after `CASE_RELIANCE`'s first sentence (5.5.6, A35). Never printed alone,
1852
- * and never where `handoffRecords` is zero: most drops hold one agent's
1853
- * pack and no hand-off to say anything about.
1373
+ * Added to a `verified` reliance line after its first sentence, only where the
1374
+ * drop holds a hand-off record.
1854
1375
  */
1855
1376
  export const CASE_RELIANCE_HANDOFF_CLAUSE = "Every hand-off with both sides in this drop is intact.";
1856
1377
  /**
1857
- * The sentence a case reading's reliance line gains where its packs hold an
1858
- * envelope signed `environment: "dev"` (5.5.6, A48), keyed by the environment
1859
- * marker (`environmentMarker`): `dev` where every envelope is signed dev,
1860
- * `mixed` where only some are. Inserted after the line's first sentence, for
1861
- * every verdict word. A drop with no `dev` envelope has no marker and gains
1862
- * nothing.
1378
+ * Added after the reliance line's first sentence, for every verdict, where the
1379
+ * drop holds `environment: "dev"` envelopes: `dev` if all are, `mixed` if some.
1863
1380
  */
1864
1381
  export const CASE_RELIANCE_SANDBOX = {
1865
1382
  dev: "The records in this drop are sandbox records, signed environment " +
@@ -1868,21 +1385,12 @@ export const CASE_RELIANCE_SANDBOX = {
1868
1385
  "dev: those are not production evidence.",
1869
1386
  };
1870
1387
  /**
1871
- * What a reader may rely on for one case reading (5.5.6 v1.38, A35 and A48),
1872
- * byte for byte the text `case_reliance`'s Python sibling returns.
1873
- *
1874
- * For every word but `verified` the base is `CASE_RELIANCE` unchanged. For
1875
- * `verified`, the hand-off clause is inserted after the first sentence
1876
- * where `handoffRecords` is not zero, and left out where it is zero: the
1877
- * base sentence never claims a hand-off the drop does not hold.
1878
- *
1879
- * Where `environmentMarker` is `dev` or `mixed`, the `CASE_RELIANCE_SANDBOX`
1880
- * sentence for it is inserted after the first sentence, for every word, so
1881
- * it directly follows the claim it qualifies (and precedes the hand-off
1882
- * clause). Where it is null or absent, the text is exactly what it was
1883
- * before A48.
1388
+ * What a reader may rely on for one case reading (§5.5.6). The sandbox
1389
+ * sentence, if any, follows the first sentence and precedes the hand-off
1390
+ * clause, which a `verified` reading gains only with `handoffRecords > 0`, so
1391
+ * the line never claims a hand-off the drop does not hold.
1884
1392
  */
1885
- export function caseReliance(verdict, handoffRecords, environmentMarker = null) {
1393
+ export function caseReliance(verdict, handoffRecords, environmentMarker = null, reasonKind = null) {
1886
1394
  let first;
1887
1395
  let rest;
1888
1396
  if (verdict === "verified" && handoffRecords > 0) {
@@ -1893,13 +1401,13 @@ export function caseReliance(verdict, handoffRecords, environmentMarker = null)
1893
1401
  "device. It does not say this drop holds everything that was recorded.";
1894
1402
  }
1895
1403
  else {
1896
- const base = CASE_RELIANCE[verdict];
1404
+ const key = verdict === "failed" && reasonKind === "nonconforming" ? "failed_nonconforming" : verdict;
1405
+ const base = CASE_RELIANCE[key];
1897
1406
  const cut = base.indexOf(". ");
1898
1407
  first = base.slice(0, cut + 1);
1899
1408
  rest = base.slice(cut + 2);
1900
1409
  }
1901
- // Only `dev` and `mixed` add the sentence. Any other value, which the type
1902
- // forbids but an untyped caller can pass, reads as no marker.
1410
+ // An untyped caller can pass any value; only `dev` and `mixed` add a sentence.
1903
1411
  const sandbox = environmentMarker === "dev" || environmentMarker === "mixed"
1904
1412
  ? CASE_RELIANCE_SANDBOX[environmentMarker]
1905
1413
  : null;
@@ -1907,22 +1415,21 @@ export function caseReliance(verdict, handoffRecords, environmentMarker = null)
1907
1415
  return `${first} ${rest}`;
1908
1416
  return `${first} ${sandbox} ${rest}`;
1909
1417
  }
1910
- /** The three witness states, and how each is printed (5.5.6, D7). No other. */
1418
+ /** The three witness states and how each is printed (§5.5.6). No others. */
1911
1419
  export const WITNESS_STATES = {
1912
1420
  confirmed: "Confirmed on the public record",
1913
1421
  not_checked: "Not checked",
1914
1422
  differs: "The public record holds different bytes",
1915
1423
  };
1916
- /** The line printed where no lookup ran, which is every reading this kernel produces on its own. */
1424
+ /** The line where no lookup ran: every reading this module produces on its own. */
1917
1425
  export const WITNESS_NOT_LOOKED_UP = Object.freeze({
1918
1426
  state: "not_checked",
1919
1427
  reasons: ["no lookup was run"],
1920
1428
  services: [],
1921
1429
  });
1922
- /** The witness line as both kernels print it. */
1923
- /** How a witness time a pack caches is printed where nothing checked it (5.5.6, 5.6.6, v1.25, A30). */
1430
+ /** A cached witness time no lookup checked (§5.6.6). */
1924
1431
  export const WITNESS_TIME_CACHED = "the pack dates the witness, not checked";
1925
- /** How a witness time the public record returned is printed where it matched the cache. */
1432
+ /** A witness time the public record returned that matched the cache. */
1926
1433
  export const WITNESS_TIME_RECORDED = "the public record dates the witness";
1927
1434
  function witnessTimes(times) {
1928
1435
  return times.map(([h, t]) => renderWitnessTime(h, t)).join(", ");