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