@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/src/grading.ts
CHANGED
|
@@ -1,72 +1,36 @@
|
|
|
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
|
-
// which ships in the published tarball so a consumer can diff its own vendored
|
|
67
|
-
// copy against it. If an implementation disagrees with the table, the
|
|
68
|
-
// implementation is wrong.
|
|
31
|
+
// Conformance: `../vectors/pack-grading.json`, shipped in the package.
|
|
69
32
|
|
|
33
|
+
import { INCOMPLETE_REASONS } from "./agent-record.js";
|
|
70
34
|
import { hexToBytes } from "./crypto.js";
|
|
71
35
|
import type {
|
|
72
36
|
DeclarationCheck,
|
|
@@ -76,62 +40,57 @@ import type {
|
|
|
76
40
|
} from "./declaration.js";
|
|
77
41
|
import { renderContentFinding, renderDeviation } from "./declaration.js";
|
|
78
42
|
import { hasSideContent, sideContentKeys, type SideContentMap } from "./side-content.js";
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
43
|
+
import {
|
|
44
|
+
VERDICTS,
|
|
45
|
+
bundleProblems,
|
|
46
|
+
claimsOf,
|
|
47
|
+
problem,
|
|
48
|
+
verdictOf,
|
|
49
|
+
type Problem,
|
|
50
|
+
type ProblemItem,
|
|
51
|
+
type ReasonKind,
|
|
52
|
+
type Verdict,
|
|
53
|
+
} from "./claims.js";
|
|
82
54
|
import { jcsParse } from "./verifier.js";
|
|
83
55
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// pack-level `incomplete`, plus `pending` from the shared §3 vocabulary so a
|
|
88
|
-
// kernel headline this module does not downgrade can pass through unchanged.
|
|
89
|
-
export const PACK_VERDICTS = [
|
|
90
|
-
"verified",
|
|
91
|
-
"partial",
|
|
92
|
-
"pending",
|
|
93
|
-
"not_found",
|
|
94
|
-
"mixed",
|
|
95
|
-
"failed",
|
|
96
|
-
"malformed",
|
|
97
|
-
"incomplete",
|
|
98
|
-
] as const;
|
|
99
|
-
export type PackVerdict = (typeof PACK_VERDICTS)[number];
|
|
100
|
-
|
|
101
|
-
// ---- Inputs ------------------------------------------------------------------
|
|
56
|
+
// The three verdicts (evidence-bundle.md section 3). A pack reads its claims.
|
|
57
|
+
export const PACK_VERDICTS = VERDICTS;
|
|
58
|
+
export type PackVerdict = Verdict;
|
|
102
59
|
|
|
103
60
|
/**
|
|
104
|
-
* The minimum this module reads
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* passes the kernel's own result straight through. It is stated as a minimum
|
|
108
|
-
* rather than an import so a surface running an OLDER pinned kernel — or one
|
|
109
|
-
* grading a result it adapted from another verifier — can still be held to
|
|
110
|
-
* this rule.
|
|
61
|
+
* The minimum this module reads from a verification result.
|
|
62
|
+
* `EvidenceBundleResult` satisfies it, and an adapted result from another
|
|
63
|
+
* verifier can satisfy it too.
|
|
111
64
|
*/
|
|
112
65
|
export interface GradeInput {
|
|
113
66
|
status: string | null | undefined;
|
|
67
|
+
/** The bundle could not be read as one (`EvidenceBundleResult.unreadable`). */
|
|
68
|
+
unreadable?: boolean;
|
|
114
69
|
/**
|
|
115
|
-
* `eventId` and `payloadBindingOk` are read
|
|
116
|
-
*
|
|
117
|
-
*
|
|
70
|
+
* `eventId` and `payloadBindingOk` are read only for declared withholding
|
|
71
|
+
* (evidence-bundle §5.4.5). An input without both is never withheld, so it
|
|
72
|
+
* gets the stricter reading.
|
|
118
73
|
*/
|
|
119
74
|
events: {
|
|
120
75
|
ok: boolean;
|
|
121
76
|
contentOk: boolean | null;
|
|
122
77
|
eventId?: string;
|
|
123
78
|
payloadBindingOk?: boolean | null;
|
|
79
|
+
/** The record's agent reading, where it is one (rule version 9). Its findings move nothing. */
|
|
80
|
+
agent?: {
|
|
81
|
+
verdict: string | null;
|
|
82
|
+
reason?: string;
|
|
83
|
+
at?: string;
|
|
84
|
+
reasons?: readonly { reason: string; at: string }[];
|
|
85
|
+
absent?: readonly string[];
|
|
86
|
+
} | null;
|
|
124
87
|
}[];
|
|
125
88
|
}
|
|
126
89
|
|
|
127
90
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
* on the absence of mismatches. The old readers wrapped the disclosure read in
|
|
132
|
-
* `try {} catch {}` and fell back to an empty map, so deleting every log turned
|
|
133
|
-
* every content check undetermined, left every event `ok`, and printed a
|
|
134
|
-
* verified verdict over zero hashed bytes.
|
|
91
|
+
* Which records disclosed bytes, and what could not be used. An empty account
|
|
92
|
+
* is not a clean result: callers gate on this, never on the absence of
|
|
93
|
+
* mismatches.
|
|
135
94
|
*/
|
|
136
95
|
export interface DisclosureAccount {
|
|
137
96
|
/** `event_id`s the bundle presents, in bundle order. */
|
|
@@ -147,15 +106,13 @@ export interface DisclosureAccount {
|
|
|
147
106
|
}
|
|
148
107
|
|
|
149
108
|
/**
|
|
150
|
-
*
|
|
151
|
-
* checkpoint
|
|
109
|
+
* The pack presents a different number of records for a checkpoint than that
|
|
110
|
+
* checkpoint's signed record commits to.
|
|
152
111
|
*/
|
|
153
112
|
export type CompletenessNote =
|
|
154
113
|
| { checkpointRef: string; signed: number; presented: number; kind: "omitted" | "surplus" }
|
|
155
114
|
| { checkpointRef: string; signed: null; presented: null; kind: "unreadable" };
|
|
156
115
|
|
|
157
|
-
// ---- Output ------------------------------------------------------------------
|
|
158
|
-
|
|
159
116
|
export const PACK_GRADE_CAUSE_KINDS = [
|
|
160
117
|
"records_omitted",
|
|
161
118
|
"records_surplus",
|
|
@@ -166,27 +123,23 @@ export const PACK_GRADE_CAUSE_KINDS = [
|
|
|
166
123
|
"unaccounted_disclosure",
|
|
167
124
|
"declaration_deviation",
|
|
168
125
|
/**
|
|
169
|
-
* The pack and the
|
|
170
|
-
*
|
|
171
|
-
* is told which record; NOT a deviation, so it never reaches `failed`.
|
|
126
|
+
* The pack and the unsigned content beside it do not line up. A cause, so the
|
|
127
|
+
* pack cannot read clean; not a deviation, so it never reaches `failed`.
|
|
172
128
|
*/
|
|
173
129
|
"declaration_content_dependent",
|
|
130
|
+
/** A reason of an agent record that reads `failed` (agent-events.md section 10): the pack is `failed`, reason kind `nonconforming`. */
|
|
131
|
+
"agent_record_nonconforming",
|
|
132
|
+
/** A reason of an agent record that reads `incomplete`: the pack is `incomplete`. */
|
|
133
|
+
"agent_record_incomplete",
|
|
174
134
|
] as const;
|
|
175
135
|
export type PackGradeCauseKind = (typeof PACK_GRADE_CAUSE_KINDS)[number];
|
|
176
136
|
|
|
177
|
-
/**
|
|
178
|
-
* One reason a pack is not `verified`, as data.
|
|
179
|
-
*
|
|
180
|
-
* Structured because the two surfaces that used to derive this rule also
|
|
181
|
-
* derived its prose, and prose is the part that drifts hardest and matters
|
|
182
|
-
* least. `renderCause` produces the house wording; a surface with its own
|
|
183
|
-
* voice reads the fields.
|
|
184
|
-
*/
|
|
137
|
+
/** One reason a pack is not `verified`, as data. `renderCause` gives the house wording. */
|
|
185
138
|
export interface PackGradeCause {
|
|
186
139
|
kind: PackGradeCauseKind;
|
|
187
140
|
/** The checkpoint this is about, for the completeness kinds. */
|
|
188
141
|
checkpointRef?: string;
|
|
189
|
-
/** The count
|
|
142
|
+
/** The count the checkpoint's signed record commits to. */
|
|
190
143
|
signed?: number;
|
|
191
144
|
/** The count the pack actually presents for that checkpoint. */
|
|
192
145
|
presented?: number;
|
|
@@ -204,36 +157,49 @@ export interface PackGradeCause {
|
|
|
204
157
|
* `declaration_content_dependent`.
|
|
205
158
|
*/
|
|
206
159
|
deviation?: DeclarationDeviationKind | DeclarationContentKind;
|
|
160
|
+
/**
|
|
161
|
+
* The JSON Pointer the agent reading's reason is about, for
|
|
162
|
+
* `agent_record_nonconforming` and `agent_record_incomplete`; `reason` is its reason
|
|
163
|
+
* code (agent-events.md section 10.1).
|
|
164
|
+
*/
|
|
165
|
+
at?: string;
|
|
166
|
+
/**
|
|
167
|
+
* For `agent_record_incomplete` with `listed_record_absent`, the `event_id` the
|
|
168
|
+
* closing record lists and the pack does not hold.
|
|
169
|
+
*/
|
|
170
|
+
listedEventId?: string;
|
|
207
171
|
}
|
|
208
172
|
|
|
209
173
|
export interface PackGrade {
|
|
210
174
|
verdict: PackVerdict;
|
|
175
|
+
/** `changed` or `nonconforming` where `verdict` is `failed`. */
|
|
176
|
+
reasonKind?: ReasonKind;
|
|
177
|
+
/**
|
|
178
|
+
* Every problem behind the verdict, each mapped to one claim and one state
|
|
179
|
+
* (`claims.ts`): the bundle's own, then the causes', then the declaration's.
|
|
180
|
+
* Empty exactly where `verdict` is `verified`.
|
|
181
|
+
*/
|
|
182
|
+
problems: Problem[];
|
|
211
183
|
/** Records whose OWN bytes bound to their OWN commitment. Nothing else. */
|
|
212
184
|
recordsVerified: number;
|
|
213
185
|
recordsFailed: number;
|
|
214
186
|
recordsUndetermined: number;
|
|
215
187
|
causes: PackGradeCause[];
|
|
216
188
|
/**
|
|
217
|
-
* The declaration check's
|
|
218
|
-
*
|
|
219
|
-
* check was passed, which is the pre-§5.4 call.
|
|
189
|
+
* The declaration check's verdict, so a consumer can tell a declared pack
|
|
190
|
+
* from an undeclared one. Absent when no check was passed.
|
|
220
191
|
*/
|
|
221
192
|
declaration?: DeclarationVerdict;
|
|
222
193
|
/**
|
|
223
|
-
* Records
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* their `payloadBindingOk` is true. Never counted in `recordsVerified`: no
|
|
227
|
-
* byte of their content was hashed, and a surface MUST NOT print them as
|
|
228
|
-
* verified records.
|
|
194
|
+
* Records withheld by their declaration (evidence-bundle §5.4.5). Never
|
|
195
|
+
* counted in `recordsVerified`: none of their content was hashed, and a
|
|
196
|
+
* surface must not print them as verified.
|
|
229
197
|
*/
|
|
230
198
|
recordsWithheld: number;
|
|
231
199
|
/** Their `event_id`s, in bundle order. */
|
|
232
200
|
withheldEventIds: string[];
|
|
233
201
|
}
|
|
234
202
|
|
|
235
|
-
// ---- Reading a bundle --------------------------------------------------------
|
|
236
|
-
|
|
237
203
|
interface BundleLike {
|
|
238
204
|
body?: {
|
|
239
205
|
events?: {
|
|
@@ -245,22 +211,10 @@ interface BundleLike {
|
|
|
245
211
|
}
|
|
246
212
|
|
|
247
213
|
/**
|
|
248
|
-
* The checkpoint's join key,
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* Grading runs over a PACK, which a consumer may hand us in pieces, so the
|
|
253
|
-
* wrapper is not always in scope here. Reading the key off the entry keeps
|
|
254
|
-
* grading honest for both forms with no wrapper lookup. It is deliberately
|
|
255
|
-
* NOT a fallback chain that would accept either spelling on the same entry:
|
|
256
|
-
* an entry carrying both is a mixed bundle, and `verifyEvidenceBundle`
|
|
257
|
-
* rejects it — grading must not quietly grade what the verifier refuses.
|
|
258
|
-
*
|
|
259
|
-
* Returning `null` (rather than a "(no tx_id)" placeholder that compares
|
|
260
|
-
* equal to itself) matters: before this was dual-form, a v2 pack read every
|
|
261
|
-
* checkpoint key as absent, every `presented` count as zero, and reported
|
|
262
|
-
* EVERY checkpoint as "omitted" — a completeness accusation manufactured by
|
|
263
|
-
* a field rename. A key we cannot read is unreadable, never zero.
|
|
214
|
+
* The checkpoint's join key, `checkpoint_ref` or the retired `tx_id`. Read from
|
|
215
|
+
* the entry because the wrapper may not be in scope. An entry carrying both is
|
|
216
|
+
* a mixed bundle the verifier rejects, so it reads as `null`. `null` means
|
|
217
|
+
* unreadable, never zero, so a key we cannot read is never counted as omitted.
|
|
264
218
|
*/
|
|
265
219
|
function joinKeyOf(o: { checkpoint_ref?: unknown; tx_id?: unknown } | null | undefined): string | null {
|
|
266
220
|
if (!o) return null;
|
|
@@ -280,10 +234,8 @@ function inclusionKeyOf(
|
|
|
280
234
|
return (hasV2 ? incl.checkpoint_ref : incl.checkpoint_tx_id) as string;
|
|
281
235
|
}
|
|
282
236
|
|
|
283
|
-
// The key a checkpoint and an inclusion are
|
|
284
|
-
//
|
|
285
|
-
// A retired `tx_id` / `checkpoint_tx_id` is compared exactly as written, as the
|
|
286
|
-
// retired reader did, even when it happens to look like hex.
|
|
237
|
+
// The key a checkpoint and an inclusion are matched on. `checkpoint_ref` is
|
|
238
|
+
// lowercased, as the verifier does; a retired key is compared exactly.
|
|
287
239
|
function checkpointMatchKey(o: { checkpoint_ref?: unknown; tx_id?: unknown } | null | undefined): string | null {
|
|
288
240
|
const k = joinKeyOf(o);
|
|
289
241
|
return k !== null && typeof o?.checkpoint_ref === "string" ? k.toLowerCase() : k;
|
|
@@ -306,84 +258,32 @@ const checkpointsOf = (bundle: unknown) => {
|
|
|
306
258
|
return Array.isArray(c) ? c : [];
|
|
307
259
|
};
|
|
308
260
|
|
|
309
|
-
// Presence in a
|
|
310
|
-
//
|
|
311
|
-
// entry" inside one kernel is two implementations of one verification rule, and
|
|
312
|
-
// ending that is why this module was brought into the kernel at all.
|
|
313
|
-
//
|
|
314
|
-
// This module used to answer it laxly: own + enumerable, nothing more. The
|
|
315
|
-
// verifier additionally requires a DATA property (an accessor can throw, and a
|
|
316
|
-
// verifier must never raise on adversarial input) holding something other than
|
|
317
|
-
// null/undefined (`{ [id]: undefined }` and `new Map([[id, undefined]])` are
|
|
318
|
-
// how "no entry" is spelled in the two shapes). So on one map this module said
|
|
319
|
-
// PRESENT and the verifier said nothing was offered — and `gradePack` returned
|
|
320
|
-
// `incomplete` with ZERO causes, the very outcome the comment in
|
|
321
|
-
// `accountDisclosure` below says was removed. Now there is one definition and
|
|
322
|
-
// two call sites.
|
|
261
|
+
// Presence in a content map is decided by `side-content.ts`, the same rule the
|
|
262
|
+
// verifier applies, so the grade and the verdict never disagree on an entry.
|
|
323
263
|
|
|
324
264
|
/**
|
|
325
|
-
* `event_id`s name files on every transport
|
|
326
|
-
* cannot
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* ── WHAT THIS IS AND IS NOT ─────────────────────────────────────────────────
|
|
265
|
+
* `event_id`s name files on every transport. An id that cannot safely name one
|
|
266
|
+
* cannot be bound to disclosed bytes, so it reads as unreadable. This is an
|
|
267
|
+
* honesty guard, not a traversal fix (nothing here joins an id into a path);
|
|
268
|
+
* refusing an id can only move a verdict toward unknown, never toward verified.
|
|
330
269
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
270
|
+
* Refused:
|
|
271
|
+
* charset anything outside letters, digits, dot, underscore and dash
|
|
272
|
+
* `.` and `..` path entries, not names
|
|
273
|
+
* trailing dot Windows strips it, so `a.` and `a` collide and one record's
|
|
274
|
+
* bytes would overwrite another's, a mismatch no tamper caused
|
|
275
|
+
* device names `CON`, `PRN`, `AUX`, `NUL`, `COM0-9`, `LPT0-9`, with or
|
|
276
|
+
* without an extension: Windows swallows the write
|
|
277
|
+
* length over 200, leaving room in a 255-byte path component
|
|
336
278
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* and an id that cannot name a file on the holder's filesystem is an id whose
|
|
340
|
-
* bytes will never arrive. Saying so — `unreadable`, and therefore
|
|
341
|
-
* `incomplete` — is the truth about that record. Refusing an id can only move
|
|
342
|
-
* a verdict toward UNKNOWN, never toward verified, which is what makes
|
|
343
|
-
* tightening this safe: the failure mode of a rule that is too strict is a
|
|
344
|
-
* conservative verdict, and the failure mode of one that is too lax is a
|
|
345
|
-
* record silently graded on bytes that were never there.
|
|
346
|
-
*
|
|
347
|
-
* ── WHAT IS REFUSED, AND WHY EACH ONE ───────────────────────────────────────
|
|
348
|
-
*
|
|
349
|
-
* charset — anything outside letters, digits, dot, underscore, dash.
|
|
350
|
-
* No separator of any platform survives it.
|
|
351
|
-
* `.` and `..` — not names but path entries. Under the documented `.json`
|
|
352
|
-
* convention they are harmless (`logs/..json` is a file);
|
|
353
|
-
* under a consumer that writes `logs/<event_id>` or uses
|
|
354
|
-
* the id as a directory, `..` IS the parent. The kernel
|
|
355
|
-
* does not know the convention, so it refuses the two ids
|
|
356
|
-
* that are hazardous under any of them.
|
|
357
|
-
* trailing dot — Windows strips it, so `a.` and `a` are one file. Two
|
|
358
|
-
* distinct records collapsing onto one path means one
|
|
359
|
-
* record's bytes overwrite another's, and the verifier then
|
|
360
|
-
* reports a MISMATCH — an accusation manufactured by a
|
|
361
|
-
* filesystem rather than by a tamper.
|
|
362
|
-
* device names — `CON`, `PRN`, `AUX`, `NUL`, `COM0-9`, `LPT0-9`, with or
|
|
363
|
-
* without an extension. On Windows `CON.json` is still the
|
|
364
|
-
* console device: the write appears to succeed and nothing
|
|
365
|
-
* is on disk.
|
|
366
|
-
* length — a POSIX path component is 255 bytes. The charset is
|
|
367
|
-
* ASCII, so characters are bytes; 200 leaves the documented
|
|
368
|
-
* `.json` and room for a prefix a consumer adds, and sits
|
|
369
|
-
* far above any real id (a UUID is 36).
|
|
370
|
-
*
|
|
371
|
-
* ── WHAT IS DELIBERATELY *NOT* REFUSED ──────────────────────────────────────
|
|
372
|
-
*
|
|
373
|
-
* **Case collisions.** `A` and `a` are distinct ids that are one file on
|
|
374
|
-
* macOS and Windows. They stay legal, because banning uppercase would refuse
|
|
375
|
-
* ordinary ids for a hazard the kernel cannot see: this module takes a content
|
|
376
|
-
* MAP, which is case-sensitive and has already resolved the question. The
|
|
377
|
-
* collision happens in the transport, and `accountDisclosure` is explicitly a
|
|
378
|
-
* convenience rather than the only way to build an account — a consumer
|
|
379
|
-
* reading a DIRECTORY builds its own and is the one that can detect it.
|
|
380
|
-
* Pinned as a decision, not left to be rediscovered.
|
|
279
|
+
* Case collisions (`A` and `a`) stay legal: the content map is case-sensitive,
|
|
280
|
+
* and only a consumer reading a directory can see the collision.
|
|
381
281
|
*/
|
|
382
282
|
const SAFE_EVENT_ID = /^[A-Za-z0-9._-]+$/;
|
|
383
283
|
const MAX_EVENT_ID_LENGTH = 200;
|
|
384
284
|
const WINDOWS_DEVICE_NAME = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\.|$)/i;
|
|
385
285
|
|
|
386
|
-
/** Why this id cannot name a file, or null if it can.
|
|
286
|
+
/** Why this id cannot name a file, or null if it can. */
|
|
387
287
|
function unsafeEventIdReason(eventId: string): string | null {
|
|
388
288
|
if (!SAFE_EVENT_ID.test(eventId)) {
|
|
389
289
|
return "only letters, digits, dot, underscore and dash are admitted";
|
|
@@ -404,33 +304,17 @@ function unsafeEventIdReason(eventId: string): string | null {
|
|
|
404
304
|
}
|
|
405
305
|
|
|
406
306
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
307
|
+
* Accounts for the disclosed bytes in `content` against the records the bundle
|
|
308
|
+
* presents. `content` is the map `verifyEvidenceBundle` takes; prefer a `Map`,
|
|
309
|
+
* since on a plain object an `event_id` of `__proto__` never lands. A zero-byte
|
|
310
|
+
* entry is present, not missing, so a truncated log cannot read as unknown.
|
|
409
311
|
*
|
|
410
|
-
* `
|
|
411
|
-
* `
|
|
412
|
-
* an `event_id` is producer-chosen, and on a plain object `__proto__` is not a
|
|
413
|
-
* key but a prototype setter, so bytes written under that id never land. A
|
|
414
|
-
* ZERO-BYTE entry is present, not missing: bytes were supplied, the check ran,
|
|
415
|
-
* and the mismatch the verifier reports stands. A truncated log must not be
|
|
416
|
-
* able to launder itself into "unknown".
|
|
417
|
-
*
|
|
418
|
-
* A CONVENIENCE, NOT THE ONLY WAY TO BUILD AN ACCOUNT — and the difference
|
|
419
|
-
* matters for one field. `extra` here can only ever name keys the content map
|
|
420
|
-
* carries, so a transport whose surplus is not representable as a map entry
|
|
421
|
-
* cannot report it through this function: a pack directory holding
|
|
422
|
-
* `logs/notes.txt`, or a second file for a record already accounted for, is
|
|
423
|
-
* invisible to a map built by iterating the bundle's own events. A consumer
|
|
424
|
-
* reading a DIRECTORY should build `DisclosureAccount` itself from the real
|
|
425
|
-
* listing and pass that to `gradePack` — the rule takes an account, never a
|
|
426
|
-
* bundle, precisely so the transport can say things the map cannot.
|
|
312
|
+
* `extra` can only name map keys. A consumer reading a directory must build
|
|
313
|
+
* `DisclosureAccount` from the real listing to report surplus files.
|
|
427
314
|
*/
|
|
428
315
|
export function accountDisclosure(
|
|
429
316
|
bundle: unknown,
|
|
430
|
-
//
|
|
431
|
-
// below already take: an entry holding a string that is not hex IS an entry,
|
|
432
|
-
// and typing it out of this signature made the one map a caller builds
|
|
433
|
-
// unusable here (proof#147). A widening, so no caller moves.
|
|
317
|
+
// Any side-content entry counts, including a string that is not hex.
|
|
434
318
|
content: SideContentMap,
|
|
435
319
|
): DisclosureAccount {
|
|
436
320
|
const expected: string[] = [];
|
|
@@ -447,25 +331,13 @@ export function accountDisclosure(
|
|
|
447
331
|
expected.push(eventId);
|
|
448
332
|
const unsafe = unsafeEventIdReason(eventId);
|
|
449
333
|
if (unsafe !== null) {
|
|
450
|
-
//
|
|
451
|
-
// which rule refused it, because "not a safe file name" alone leaves a
|
|
452
|
-
// holder with no idea what to change.
|
|
334
|
+
// A stable prefix to group on, and a suffix naming the rule that refused it.
|
|
453
335
|
unreadable.push({ eventId, reason: `event_id is not a safe file name: ${unsafe}` });
|
|
454
336
|
continue;
|
|
455
337
|
}
|
|
456
|
-
// An
|
|
457
|
-
// Object.prototype
|
|
458
|
-
//
|
|
459
|
-
// dot, underscore and dash, so every one of them is a legal id here) and
|
|
460
|
-
// resolves to an inherited value, so an indexed lookup reported a record as
|
|
461
|
-
// PRESENT when nothing at all was disclosed for it. That is the module's
|
|
462
|
-
// own founding defect (absent evidence read as good evidence) reappearing
|
|
463
|
-
// one layer down: the verdict stayed `incomplete` because the verifier
|
|
464
|
-
// still had no bytes to bind, but with ZERO causes — a surface saying
|
|
465
|
-
// "incomplete" and unable to say why. `__proto__` is worse still: assigning
|
|
466
|
-
// it on a plain object either silently drops the write or replaces the
|
|
467
|
-
// object's prototype outright, so bytes a holder genuinely supplied never
|
|
468
|
-
// reach the map, and the indexed read then claimed they had.
|
|
338
|
+
// An own entry, not an indexed read: `constructor`, `__proto__` and other
|
|
339
|
+
// Object.prototype keys pass SAFE_EVENT_ID and would read as present when
|
|
340
|
+
// nothing was disclosed.
|
|
469
341
|
if (!hasSideContent(content, eventId)) missing.push(eventId);
|
|
470
342
|
else present.push(eventId);
|
|
471
343
|
}
|
|
@@ -477,33 +349,25 @@ export function accountDisclosure(
|
|
|
477
349
|
}
|
|
478
350
|
|
|
479
351
|
/**
|
|
480
|
-
* A JSON number read by its value: an integer within 2**53 - 1, or null.
|
|
481
|
-
* `
|
|
482
|
-
* `src/sanning_proof/chain.py`, so `2.0` is 2 in both kernels and
|
|
483
|
-
* `9007199254740993`, which this parser rounds, is unreadable in both.
|
|
352
|
+
* A JSON number read by its value: an integer within 2**53 - 1, or null. So
|
|
353
|
+
* `2.0` is 2, and `9007199254740993`, which the parser rounds, is unreadable.
|
|
484
354
|
*/
|
|
485
355
|
export function safeInteger(value: unknown): number | null {
|
|
486
|
-
// `+ 0` folds -0 into 0
|
|
356
|
+
// `+ 0` folds -0 into 0.
|
|
487
357
|
return typeof value === "number" && Number.isSafeInteger(value) ? value + 0 : null;
|
|
488
358
|
}
|
|
489
359
|
|
|
490
|
-
/** The `leaf_count` a checkpoint's
|
|
360
|
+
/** The `leaf_count` a checkpoint's signed record commits to, or null if unreadable. */
|
|
491
361
|
export function signedLeafCount(checkpoint: { event_bytes?: unknown } | null | undefined): number | null {
|
|
492
362
|
const hex = checkpoint?.event_bytes;
|
|
493
363
|
if (typeof hex !== "string" || hex.length === 0) return null;
|
|
494
364
|
try {
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
// that held for invalid UTF-8 and nowhere else: `TextDecoder` EATS a
|
|
498
|
-
// leading byte-order mark where Python's `errors="replace"` keeps it, so
|
|
499
|
-
// `BOM + JCS(checkpoint)` yielded a signed leaf count here and unreadable
|
|
500
|
-
// there, and that count decides a completeness note (proof#140 gate 5).
|
|
365
|
+
// The strict signed-bytes reader: a leading byte-order mark is refused, not
|
|
366
|
+
// eaten, since this count decides a verdict.
|
|
501
367
|
const record = jcsParse(hexToBytes(hex)) as {
|
|
502
368
|
event?: { leaf_count?: unknown };
|
|
503
369
|
};
|
|
504
|
-
// Read
|
|
505
|
-
// `30e-1` commits to 3, and since A38 (proof#197) this count decides a
|
|
506
|
-
// verdict, not only a note. Past 2**53 - 1 it is unreadable, as in Python.
|
|
370
|
+
// Read by value, the model JCS signs: `3.0`, `3e0` and `30e-1` all commit to 3.
|
|
507
371
|
return safeInteger(record?.event?.leaf_count);
|
|
508
372
|
} catch {
|
|
509
373
|
return null;
|
|
@@ -511,16 +375,10 @@ export function signedLeafCount(checkpoint: { event_bytes?: unknown } | null | u
|
|
|
511
375
|
}
|
|
512
376
|
|
|
513
377
|
/**
|
|
514
|
-
* Omission
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
* exposes it is already in the pack and already signed — each checkpoint's
|
|
519
|
-
* record commits to the `leaf_count` of the tree it anchored. This compares
|
|
520
|
-
* that against how many records the pack presents for that checkpoint.
|
|
521
|
-
*
|
|
522
|
-
* A checkpoint whose signed count cannot be read is `unreadable`: a
|
|
523
|
-
* completeness check we could not run is UNKNOWN, never a pass.
|
|
378
|
+
* Omission, which a per-record check cannot see: drop records and every
|
|
379
|
+
* survivor still verifies. Compares each checkpoint's signed `leaf_count` with
|
|
380
|
+
* the records the pack presents for it. An unreadable count is `unreadable`:
|
|
381
|
+
* a check we could not run is unknown, never a pass.
|
|
524
382
|
*/
|
|
525
383
|
export function completenessNotes(bundle: unknown): CompletenessNote[] {
|
|
526
384
|
const events = eventsOf(bundle);
|
|
@@ -529,19 +387,13 @@ export function completenessNotes(bundle: unknown): CompletenessNote[] {
|
|
|
529
387
|
const key = joinKeyOf(cp);
|
|
530
388
|
const checkpointRef = key ?? "(no checkpoint_ref)";
|
|
531
389
|
const signed = signedLeafCount(cp);
|
|
532
|
-
// No readable key
|
|
533
|
-
// completeness is UNKNOWN — the same answer as an unreadable leaf_count,
|
|
534
|
-
// and never a shortfall.
|
|
390
|
+
// No readable key: completeness is unknown, never a shortfall.
|
|
535
391
|
if (signed === null || key === null) {
|
|
536
392
|
notes.push({ checkpointRef, signed: null, presented: null, kind: "unreadable" });
|
|
537
393
|
continue;
|
|
538
394
|
}
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
// the form, and the kernel lowercases before comparing), so a pack whose
|
|
542
|
-
// checkpoint spells its ref in upper case still verifies. Grading it
|
|
543
|
-
// case-sensitively reported every record as omitted: a completeness
|
|
544
|
-
// accusation manufactured by letter case. The ref shown is the pack's own.
|
|
395
|
+
// Case-folded, because the verifier accepts `checkpoint_ref` in any case;
|
|
396
|
+
// otherwise an uppercase ref would report every record as omitted.
|
|
545
397
|
const want = checkpointMatchKey(cp);
|
|
546
398
|
const presented = events.filter((e) => inclusionMatchKey(e?.inclusion) === want).length;
|
|
547
399
|
if (presented < signed) notes.push({ checkpointRef, signed, presented, kind: "omitted" });
|
|
@@ -550,22 +402,19 @@ export function completenessNotes(bundle: unknown): CompletenessNote[] {
|
|
|
550
402
|
return notes;
|
|
551
403
|
}
|
|
552
404
|
|
|
553
|
-
// ---- The rule ----------------------------------------------------------------
|
|
554
|
-
|
|
555
405
|
/**
|
|
556
|
-
* Every reason this pack is not `verified`, in
|
|
557
|
-
*
|
|
406
|
+
* Every reason this pack is not `verified`, in reading order: what the pack
|
|
407
|
+
* claimed, then what is missing from the whole, then from the parts.
|
|
558
408
|
*/
|
|
559
409
|
export function gradeCauses(
|
|
560
410
|
account: DisclosureAccount,
|
|
561
411
|
notes: readonly CompletenessNote[],
|
|
562
412
|
declaration?: DeclarationCheck | null,
|
|
563
413
|
withheld: readonly string[] = [],
|
|
414
|
+
events: GradeInput["events"] = [],
|
|
564
415
|
): PackGradeCause[] {
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
// so a pack whose every record its declaration withholds earns neither
|
|
568
|
-
// `no_disclosure` nor `missing_disclosure`.
|
|
416
|
+
// Records withheld by their declaration are not expected, so a pack that
|
|
417
|
+
// withholds every record earns no disclosure cause.
|
|
569
418
|
if (withheld.length > 0) {
|
|
570
419
|
const gone = new Set(withheld);
|
|
571
420
|
account = {
|
|
@@ -576,11 +425,8 @@ export function gradeCauses(
|
|
|
576
425
|
}
|
|
577
426
|
const causes: PackGradeCause[] = [];
|
|
578
427
|
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
// it contains" reads the per-record findings differently. Every deviation
|
|
582
|
-
// gets its own cause (one defect, one line), so a pack that deviates in
|
|
583
|
-
// four ways can never be reported as one vague shortfall.
|
|
428
|
+
// Declaration deviations first, since they frame the rest. One cause per
|
|
429
|
+
// deviation, so several are never reported as one vague shortfall.
|
|
584
430
|
for (const deviation of declaration?.deviations ?? []) {
|
|
585
431
|
causes.push({
|
|
586
432
|
kind: "declaration_deviation",
|
|
@@ -590,9 +436,8 @@ export function gradeCauses(
|
|
|
590
436
|
});
|
|
591
437
|
}
|
|
592
438
|
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
// deviation, so it never reaches `failed`.
|
|
439
|
+
// A mismatch with unsigned side content: a cause, so the pack cannot read
|
|
440
|
+
// clean, but not a deviation, so it never reaches `failed`.
|
|
596
441
|
for (const finding of declaration?.contentFindings ?? []) {
|
|
597
442
|
causes.push({
|
|
598
443
|
kind: "declaration_content_dependent",
|
|
@@ -602,6 +447,26 @@ export function gradeCauses(
|
|
|
602
447
|
});
|
|
603
448
|
}
|
|
604
449
|
|
|
450
|
+
// Then every reason of each agent record that does not read verified (agent-events.md
|
|
451
|
+
// section 10). A reason that reads incomplete is `agent_record_incomplete`; every other
|
|
452
|
+
// reason is `agent_record_nonconforming`. A finding is never a cause: it is reported
|
|
453
|
+
// beside the record and moves nothing.
|
|
454
|
+
for (const e of events) {
|
|
455
|
+
const verdict = e.agent?.verdict;
|
|
456
|
+
if (verdict !== "failed" && verdict !== "incomplete") continue;
|
|
457
|
+
const absent = [...(e.agent?.absent ?? [])];
|
|
458
|
+
for (const r of e.agent?.reasons ?? []) {
|
|
459
|
+
const listed = r.reason === "listed_record_absent" ? absent.shift() : undefined;
|
|
460
|
+
causes.push({
|
|
461
|
+
kind: INCOMPLETE_REASONS.has(r.reason) ? "agent_record_incomplete" : "agent_record_nonconforming",
|
|
462
|
+
...(e.eventId === undefined ? {} : { firstEventId: e.eventId }),
|
|
463
|
+
reason: r.reason,
|
|
464
|
+
at: r.at,
|
|
465
|
+
...(listed === undefined ? {} : { listedEventId: listed }),
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
605
470
|
for (const note of notes) {
|
|
606
471
|
if (note.kind === "omitted") {
|
|
607
472
|
causes.push({
|
|
@@ -624,10 +489,7 @@ export function gradeCauses(
|
|
|
624
489
|
}
|
|
625
490
|
}
|
|
626
491
|
|
|
627
|
-
//
|
|
628
|
-
// regression that rendered a green banner over zero hashed bytes, and a
|
|
629
|
-
// reader deserves to be told the pack disclosed nothing rather than counting
|
|
630
|
-
// N absences.
|
|
492
|
+
// A pack that disclosed nothing is told as that, once, rather than as N absences.
|
|
631
493
|
if (account.expected.length > 0 && account.present.length === 0 && account.unreadable.length === 0) {
|
|
632
494
|
causes.push({ kind: "no_disclosure", count: account.expected.length, of: account.expected.length });
|
|
633
495
|
} else if (account.missing.length > 0) {
|
|
@@ -655,90 +517,34 @@ export function gradeCauses(
|
|
|
655
517
|
}
|
|
656
518
|
|
|
657
519
|
/**
|
|
658
|
-
* The
|
|
520
|
+
* The verdict a surface may print, and the counts beside it.
|
|
659
521
|
*
|
|
660
|
-
* `
|
|
661
|
-
*
|
|
662
|
-
* status used to be cast straight to `PackVerdict`, so `gradePack` would hand
|
|
663
|
-
* back whatever it was given — `"mixed"` and `"not_found"` (legal, if
|
|
664
|
-
* unexpected), but equally `"banana"`, `""` and `"SAFE"`. A surface that trusts
|
|
665
|
-
* the type and renders the verdict verbatim would then print the one word
|
|
666
|
-
* `evidence-bundle.md` §6 forbids, over a pack this rule never graded.
|
|
667
|
-
*
|
|
668
|
-
* So: the five words of the §3 vocabulary that a bundle status can pass up to
|
|
669
|
-
* a pack pass through, and ANYTHING ELSE is
|
|
670
|
-
* `incomplete`. A verdict we cannot interpret is a check we could not run, and
|
|
671
|
-
* the answer to that is UNKNOWN — never a pass, and never an accusation.
|
|
672
|
-
* (`failed` and `malformed` are handled before this is reached.)
|
|
673
|
-
*/
|
|
674
|
-
const PASS_THROUGH: ReadonlySet<string> = new Set([
|
|
675
|
-
"verified",
|
|
676
|
-
"partial",
|
|
677
|
-
"pending",
|
|
678
|
-
"not_found",
|
|
679
|
-
"mixed",
|
|
680
|
-
]);
|
|
681
|
-
|
|
682
|
-
function passThrough(status: string | null | undefined): PackVerdict {
|
|
683
|
-
return typeof status === "string" && PASS_THROUGH.has(status)
|
|
684
|
-
? (status as PackVerdict)
|
|
685
|
-
: "incomplete";
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/**
|
|
689
|
-
* The verdict a surface may print, and the counts it may print beside it.
|
|
690
|
-
*
|
|
691
|
-
* The invariant, stated so a mutation test can kill it: `verified` is reachable
|
|
692
|
-
* ONLY when every record's supplied bytes bound to its own commitment, or
|
|
693
|
-
* (rule version 6) the record is withheld by its declaration, and nothing is
|
|
522
|
+
* Invariant: `verified` only when every record's bytes bound to its own
|
|
523
|
+
* commitment or the record is withheld by its declaration, and nothing is
|
|
694
524
|
* missing, unreadable, surplus or omitted.
|
|
695
525
|
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
* ── the declaration (optional fourth argument) ──────────────────────────────
|
|
703
|
-
*
|
|
704
|
-
* `declaration` is what `checkDeclaration` in `./declaration.ts` made of the
|
|
705
|
-
* pack's own signed statement of what it holds (`evidence-bundle.md` §5.4). A
|
|
706
|
-
* deviation from it is a PROVEN mismatch between what the pack claimed and
|
|
707
|
-
* what it carries, so it FAILS the pack and each deviation becomes its own
|
|
708
|
-
* cause. It must never soften into absence: that is how a trimmed hand-over
|
|
709
|
-
* reads as an innocent gap.
|
|
526
|
+
* Every problem (the bundle's, each cause, the declaration's) maps to one claim
|
|
527
|
+
* and one state (`claims.ts`) and the verdict is read off the claims (rule
|
|
528
|
+
* version 9): `failed` where a claim is disproven, else `incomplete` where one
|
|
529
|
+
* is not proven, else `verified`. Absent bytes are never a disproof (section
|
|
530
|
+
* 5.1), and a proven mismatch is never softened into absence. An agent record's
|
|
531
|
+
* findings move nothing.
|
|
710
532
|
*
|
|
711
|
-
*
|
|
712
|
-
* of
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
* hands.
|
|
727
|
-
* present and unreadable the pack is `incomplete`. `declaration_version` is
|
|
728
|
-
* chosen by the same party that assembles the pack
|
|
729
|
-
* and has no accept-set, so a holder who trims a
|
|
730
|
-
* record and then writes one unreadable field can
|
|
731
|
-
* re-seal the pack with its own key and hand a
|
|
732
|
-
* reader a clean verdict over evidence of the trim
|
|
733
|
-
* sitting inside the signed body. UNKNOWN is the
|
|
734
|
-
* only honest answer to a claim the reader cannot
|
|
735
|
-
* read, and it adds no cause: `PackGrade.declaration`
|
|
736
|
-
* says why.
|
|
737
|
-
*
|
|
738
|
-
* `verified` adds nothing and moves nothing.
|
|
739
|
-
*
|
|
740
|
-
* Omitting the argument entirely is the pre-§5.4 behavior, unchanged, and
|
|
741
|
-
* `PackGrade.declaration` is then absent.
|
|
533
|
+
* `declaration` is `checkDeclaration`'s result for the pack's signed statement
|
|
534
|
+
* of what it holds (§5.4):
|
|
535
|
+
* deviation a proven mismatch: the pack fails, one cause each
|
|
536
|
+
* content-dependent the pack and its unsigned side content disagree:
|
|
537
|
+
* `incomplete`, never `failed`, since whoever builds
|
|
538
|
+
* the side-content map could otherwise fail an intact
|
|
539
|
+
* pack without a key
|
|
540
|
+
* no declaration nothing moves
|
|
541
|
+
* present and unreadable `incomplete`, with no cause and one problem,
|
|
542
|
+
* `pack_declaration_unreadable` (or
|
|
543
|
+
* `declaration_not_checked` for a chain claim this
|
|
544
|
+
* call was given no reading for): the pack's holder
|
|
545
|
+
* picks `declaration_version`, so an unreadable one
|
|
546
|
+
* could otherwise hide a trimmed record
|
|
547
|
+
* Omitting the argument leaves `PackGrade.declaration` absent.
|
|
742
548
|
*/
|
|
743
549
|
export function gradePack(
|
|
744
550
|
result: GradeInput,
|
|
@@ -749,9 +555,8 @@ export function gradePack(
|
|
|
749
555
|
const events = Array.isArray(result?.events) ? result.events : [];
|
|
750
556
|
const recordsVerified = events.filter((e) => e.ok && e.contentOk === true).length;
|
|
751
557
|
const recordsFailed = events.filter((e) => e.ok === false || e.contentOk === false).length;
|
|
752
|
-
//
|
|
753
|
-
//
|
|
754
|
-
// absent, unreadable or malformed declaration.
|
|
558
|
+
// Withheld by its declaration (§5.4.5) only when all four conditions hold.
|
|
559
|
+
// `declaredWithheld` is empty for an absent, unreadable or malformed declaration.
|
|
755
560
|
const declaredWithheld = new Set(declaration?.declaredWithheld ?? []);
|
|
756
561
|
const withheldEventIds = events
|
|
757
562
|
.filter(
|
|
@@ -765,19 +570,21 @@ export function gradePack(
|
|
|
765
570
|
.map((e) => e.eventId as string);
|
|
766
571
|
const recordsWithheld = withheldEventIds.length;
|
|
767
572
|
const recordsUndetermined = events.length - recordsVerified - recordsFailed - recordsWithheld;
|
|
768
|
-
const causes = gradeCauses(account, notes, declaration, withheldEventIds);
|
|
573
|
+
const causes = gradeCauses(account, notes, declaration, withheldEventIds, events);
|
|
769
574
|
|
|
770
|
-
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
575
|
+
const problems = packProblems(result, events, account, causes, declaration, withheldEventIds);
|
|
576
|
+
if (recordsUndetermined > 0 && !problems.some((p) => p.claim === "unchanged")) {
|
|
577
|
+
// Fail closed: a record whose bytes were not bound and that no cause names still
|
|
578
|
+
// holds the pack below verified.
|
|
579
|
+
problems.push(problem("records_not_bound", [[0, null]]));
|
|
580
|
+
}
|
|
581
|
+
const outcome = verdictOf(claimsOf(problems));
|
|
582
|
+
const verdict = outcome.status;
|
|
778
583
|
|
|
779
584
|
const grade: PackGrade = {
|
|
780
585
|
verdict,
|
|
586
|
+
...(outcome.reason_kind === undefined ? {} : { reasonKind: outcome.reason_kind }),
|
|
587
|
+
problems,
|
|
781
588
|
recordsVerified,
|
|
782
589
|
recordsFailed,
|
|
783
590
|
recordsUndetermined,
|
|
@@ -789,41 +596,100 @@ export function gradePack(
|
|
|
789
596
|
return grade;
|
|
790
597
|
}
|
|
791
598
|
|
|
792
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Every problem of one pack, read on its own (pack position 0): the bundle's, then each
|
|
601
|
+
* cause's, then the declaration's.
|
|
602
|
+
*/
|
|
603
|
+
function packProblems(
|
|
604
|
+
result: GradeInput,
|
|
605
|
+
events: GradeInput["events"],
|
|
606
|
+
account: DisclosureAccount,
|
|
607
|
+
causes: readonly PackGradeCause[],
|
|
608
|
+
declaration: DeclarationCheck | null | undefined,
|
|
609
|
+
withheld: readonly string[],
|
|
610
|
+
): Problem[] {
|
|
611
|
+
const out = bundleProblems(result, 0);
|
|
612
|
+
const ids = events.map((e) => e.eventId ?? null);
|
|
613
|
+
const records = (...eventIds: (string | null | undefined)[]): ProblemItem[] => {
|
|
614
|
+
const wanted = eventIds.map((e) => e ?? null);
|
|
615
|
+
const found: ProblemItem[] = [];
|
|
616
|
+
ids.forEach((e, i) => {
|
|
617
|
+
if (wanted.includes(e)) found.push([0, i]);
|
|
618
|
+
});
|
|
619
|
+
return found.length > 0 ? found : [[0, null]];
|
|
620
|
+
};
|
|
621
|
+
const gone = new Set(withheld);
|
|
622
|
+
for (const c of causes) {
|
|
623
|
+
if (c.kind === "agent_record_nonconforming" || c.kind === "agent_record_incomplete") {
|
|
624
|
+
// A record the closing record lists and the pack does not hold is the item, named by
|
|
625
|
+
// its own event_id, so the line counts the records that are not held.
|
|
626
|
+
out.push(
|
|
627
|
+
problem(c.kind, c.listedEventId !== undefined ? [[0, null, c.listedEventId]] : records(c.firstEventId), {
|
|
628
|
+
rule: c.reason ?? null,
|
|
629
|
+
}),
|
|
630
|
+
);
|
|
631
|
+
} else if (c.kind === "records_omitted") {
|
|
632
|
+
out.push(problem(c.kind, [[0, null]], { count: c.count, of: c.signed, presented: c.presented }));
|
|
633
|
+
} else if (c.kind === "no_disclosure") {
|
|
634
|
+
out.push(problem(c.kind, records(...account.expected), { count: c.count }));
|
|
635
|
+
} else if (c.kind === "missing_disclosure") {
|
|
636
|
+
const missing = account.missing.filter((e) => !gone.has(e));
|
|
637
|
+
out.push(problem(c.kind, records(...missing), { count: c.count, of: c.of }));
|
|
638
|
+
} else if (c.kind === "unreadable_disclosure" || c.kind === "declaration_content_dependent") {
|
|
639
|
+
out.push(problem(c.kind, records(c.firstEventId)));
|
|
640
|
+
} else if (c.kind === "unaccounted_disclosure") {
|
|
641
|
+
out.push(problem(c.kind, [[0, null]], { count: c.count }));
|
|
642
|
+
} else if (c.kind === "declaration_deviation" && c.deviation === "declaration_malformed") {
|
|
643
|
+
// A declaration in a version this reader knows, signed with the pack, that does not
|
|
644
|
+
// parse: the producer broke its own format, and nothing was proven altered.
|
|
645
|
+
out.push(problem("declaration_nonconforming", [[0, null]]));
|
|
646
|
+
} else {
|
|
647
|
+
out.push(problem(c.kind, [[0, null]]));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (declaration != null && declaration.present) {
|
|
651
|
+
if (declaration.verdict === "failed" && declaration.deviations.length === 0) {
|
|
652
|
+
out.push(problem("pack_declaration_deviates", [[0, null]]));
|
|
653
|
+
} else if (declaration.verdict === "incomplete" && declaration.contentFindings.length === 0) {
|
|
654
|
+
out.push(
|
|
655
|
+
problem(
|
|
656
|
+
declaration.version == null ? "pack_declaration_unreadable" : "declaration_not_checked",
|
|
657
|
+
[[0, null]],
|
|
658
|
+
),
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return out;
|
|
663
|
+
}
|
|
793
664
|
|
|
794
665
|
/**
|
|
795
|
-
* The house wording for one cause.
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
* `PackGradeCause` instead. What must not vary between surfaces is the verdict
|
|
799
|
-
* and the counts, and those come from `gradePack`.
|
|
800
|
-
*
|
|
801
|
-
* The `subject` names what the pack is from the reader's position — "this pack"
|
|
802
|
-
* on a page someone dropped a file onto, "the pack" in a CLI report.
|
|
666
|
+
* The house wording for one cause. Optional: a surface may read the fields
|
|
667
|
+
* instead; the verdict and counts are what must not vary. `subject` is "this
|
|
668
|
+
* pack" on a page and "the pack" in a CLI report.
|
|
803
669
|
*/
|
|
804
670
|
export function renderCause(cause: PackGradeCause, subject = "this pack"): string {
|
|
805
671
|
switch (cause.kind) {
|
|
806
672
|
case "records_omitted":
|
|
807
673
|
return (
|
|
808
674
|
`checkpoint ${cause.checkpointRef} commits to ${cause.signed} record(s); ${subject} presents ` +
|
|
809
|
-
`${cause.presented}
|
|
675
|
+
`${cause.presented}: ${cause.count} record(s) omitted`
|
|
810
676
|
);
|
|
811
677
|
case "records_surplus":
|
|
812
678
|
return (
|
|
813
679
|
`checkpoint ${cause.checkpointRef} commits to ${cause.signed} record(s); ${subject} presents ` +
|
|
814
|
-
`${cause.presented}
|
|
680
|
+
`${cause.presented}; more records than the signed tree accounts for`
|
|
815
681
|
);
|
|
816
682
|
case "leaf_count_unreadable":
|
|
817
|
-
return `checkpoint ${cause.checkpointRef} has no readable signed leaf_count
|
|
683
|
+
return `checkpoint ${cause.checkpointRef} has no readable signed leaf_count; completeness is unknown`;
|
|
818
684
|
case "no_disclosure":
|
|
819
685
|
return (
|
|
820
|
-
`no event bytes were disclosed
|
|
686
|
+
`no event bytes were disclosed; content binding is unknown for all ${cause.count} ` +
|
|
821
687
|
`record(s), not verified`
|
|
822
688
|
);
|
|
823
689
|
case "missing_disclosure":
|
|
824
690
|
return (
|
|
825
|
-
`${cause.count} of ${cause.of} record(s) disclose no bytes (first: ${cause.firstEventId})
|
|
826
|
-
`content binding is
|
|
691
|
+
`${cause.count} of ${cause.of} record(s) disclose no bytes (first: ${cause.firstEventId}); ` +
|
|
692
|
+
`content binding is unknown, not verified`
|
|
827
693
|
);
|
|
828
694
|
case "unreadable_disclosure":
|
|
829
695
|
return `the disclosure for ${cause.firstEventId} is unreadable: ${cause.reason}`;
|
|
@@ -832,16 +698,16 @@ export function renderCause(cause: PackGradeCause, subject = "this pack"): strin
|
|
|
832
698
|
`${cause.count} disclosed file(s) are not accounted for by any record in this bundle ` +
|
|
833
699
|
`(first: ${cause.firstEventId})`
|
|
834
700
|
);
|
|
835
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
701
|
+
// Worded once, by `renderDeviation`, and carried on `reason`.
|
|
702
|
+
case "agent_record_nonconforming":
|
|
703
|
+
return `the agent record ${cause.firstEventId} does not conform to its vocabulary: ${cause.reason} at ${cause.at}`;
|
|
704
|
+
case "agent_record_incomplete":
|
|
705
|
+
return `the agent record ${cause.firstEventId} could not be fully read: ${cause.reason} at ${cause.at}`;
|
|
839
706
|
case "declaration_deviation":
|
|
840
707
|
return `this pack deviates from its own declaration: ${cause.reason}`;
|
|
841
708
|
case "declaration_content_dependent":
|
|
842
|
-
//
|
|
843
|
-
// holder did: the
|
|
844
|
-
// the case chose them (proof#147).
|
|
709
|
+
// Worded as what the reader could not establish, never as what the
|
|
710
|
+
// holder did: the side content is unsigned.
|
|
845
711
|
return (
|
|
846
712
|
`${subject} and the unsigned content handed in beside it do not line up: ` +
|
|
847
713
|
`${cause.reason}`
|