@sanning/proof 0.10.0 → 0.11.1

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/dist/grading.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { DeclarationCheck, DeclarationContentKind, DeclarationDeviationKind, DeclarationVerdict } from "./declaration.js";
2
+ import { type SideContentMap } from "./side-content.js";
1
3
  export type PackVerdict = "verified" | "partial" | "pending" | "not_found" | "mixed" | "failed" | "malformed" | "incomplete";
2
4
  /**
3
5
  * The minimum this module reads out of a verification result.
@@ -54,7 +56,13 @@ export type CompletenessNote = {
54
56
  presented: null;
55
57
  kind: "unreadable";
56
58
  };
57
- export type PackGradeCauseKind = "records-omitted" | "records-surplus" | "leaf-count-unreadable" | "no-disclosure" | "missing-disclosure" | "unreadable-disclosure" | "unaccounted-disclosure";
59
+ export type PackGradeCauseKind = "records-omitted" | "records-surplus" | "leaf-count-unreadable" | "no-disclosure" | "missing-disclosure" | "unreadable-disclosure" | "unaccounted-disclosure" | "declaration-deviation"
60
+ /**
61
+ * The pack and the UNSIGNED content handed in beside it do not line up
62
+ * (proof#147). A cause, so the pack cannot read clean over it and a reader
63
+ * is told which record; NOT a deviation, so it never reaches `failed`.
64
+ */
65
+ | "declaration-content-dependent";
58
66
  /**
59
67
  * One reason a pack is not `verified`, as data.
60
68
  *
@@ -79,6 +87,12 @@ export interface PackGradeCause {
79
87
  firstEventId?: string;
80
88
  /** Why one supplied disclosure could not be used. */
81
89
  reason?: string;
90
+ /**
91
+ * Which deviation from the pack's own declaration, for
92
+ * `declaration-deviation`, or which content-dependent finding, for
93
+ * `declaration-content-dependent`.
94
+ */
95
+ deviation?: DeclarationDeviationKind | DeclarationContentKind;
82
96
  }
83
97
  export interface PackGrade {
84
98
  verdict: PackVerdict;
@@ -87,6 +101,12 @@ export interface PackGrade {
87
101
  recordsFailed: number;
88
102
  recordsUndetermined: number;
89
103
  causes: PackGradeCause[];
104
+ /**
105
+ * The declaration check's own verdict, carried so a consumer that reads only
106
+ * this value can tell a declared pack from an undeclared one. Absent when no
107
+ * check was passed, which is the pre-§5.4 call.
108
+ */
109
+ declaration?: DeclarationVerdict;
90
110
  }
91
111
  /**
92
112
  * Account for the disclosed bytes a pack handed us against the records its
@@ -110,7 +130,7 @@ export interface PackGrade {
110
130
  * listing and pass that to `gradePack` — the rule takes an account, never a
111
131
  * bundle, precisely so the transport can say things the map cannot.
112
132
  */
113
- export declare function accountDisclosure(bundle: unknown, content: Readonly<Record<string, Uint8Array>> | ReadonlyMap<string, Uint8Array>): DisclosureAccount;
133
+ export declare function accountDisclosure(bundle: unknown, content: SideContentMap): DisclosureAccount;
114
134
  /** The `leaf_count` a checkpoint's SIGNED record commits to, or null if unreadable. */
115
135
  export declare function signedLeafCount(checkpoint: {
116
136
  event_bytes?: unknown;
@@ -132,7 +152,7 @@ export declare function completenessNotes(bundle: unknown): CompletenessNote[];
132
152
  * Every reason this pack is not `verified`, in the order a reader should hear
133
153
  * them: what is missing from the whole, then what is missing from the parts.
134
154
  */
135
- export declare function gradeCauses(account: DisclosureAccount, notes: readonly CompletenessNote[]): PackGradeCause[];
155
+ export declare function gradeCauses(account: DisclosureAccount, notes: readonly CompletenessNote[], declaration?: DeclarationCheck | null): PackGradeCause[];
136
156
  /**
137
157
  * The verdict a surface may print, and the counts it may print beside it.
138
158
  *
@@ -145,8 +165,49 @@ export declare function gradeCauses(account: DisclosureAccount, notes: readonly
145
165
  * (§5.1: undetermined, not failed), and a proven mismatch is never softened
146
166
  * into absence. The last branch passes the kernel's headline through unchanged:
147
167
  * this rule DOWNGRADES a verdict, it never invents one.
168
+ *
169
+ * ── the declaration (optional fourth argument) ──────────────────────────────
170
+ *
171
+ * `declaration` is what `checkDeclaration` in `./declaration.ts` made of the
172
+ * pack's own signed statement of what it holds (`evidence-bundle.md` §5.4). A
173
+ * deviation from it is a PROVEN mismatch between what the pack claimed and
174
+ * what it carries, so it FAILS the pack and each deviation becomes its own
175
+ * cause. It must never soften into absence: that is how a trimmed hand-over
176
+ * reads as an innocent gap.
177
+ *
178
+ * A check that came back `partial` splits in THREE (proof#147), and the first
179
+ * of the three is not about the declaration's readability at all. The other
180
+ * two split on whether the body carried a `declaration` member.
181
+ *
182
+ * content-dependent the pack and the UNSIGNED content beside it do not
183
+ * line up (proof#147). Each finding is its own cause
184
+ * and the pack is `incomplete`. It must never reach
185
+ * `failed`: whoever assembles a case fills in the
186
+ * side-content map, so an accuser holding an intact
187
+ * pack could otherwise put its holder into `failed`
188
+ * with no key at all, by dropping one file or adding
189
+ * one. `failed` is left to what a signature covers.
190
+ * no declaration at all nothing moves. Every pack built before §5.4 is
191
+ * this case, and re-grading them would move pinned
192
+ * exit codes for artifacts already in customers'
193
+ * hands.
194
+ * present and unreadable the pack is `incomplete`. `declaration_version` is
195
+ * chosen by the same party that assembles the pack
196
+ * and has no accept-set, so a holder who trims a
197
+ * record and then writes one unreadable field can
198
+ * re-seal the pack with its own key and hand a
199
+ * reader a clean verdict over evidence of the trim
200
+ * sitting inside the signed body. UNKNOWN is the
201
+ * only honest answer to a claim the reader cannot
202
+ * read, and it adds no cause: `PackGrade.declaration`
203
+ * says why.
204
+ *
205
+ * `verified` adds nothing and moves nothing.
206
+ *
207
+ * Omitting the argument entirely is the pre-§5.4 behavior, unchanged, and
208
+ * `PackGrade.declaration` is then absent.
148
209
  */
149
- export declare function gradePack(result: GradeInput, account: DisclosureAccount, notes: readonly CompletenessNote[]): PackGrade;
210
+ export declare function gradePack(result: GradeInput, account: DisclosureAccount, notes: readonly CompletenessNote[], declaration?: DeclarationCheck | null): PackGrade;
150
211
  /**
151
212
  * The house wording for one cause.
152
213
  *
package/dist/grading.js CHANGED
@@ -67,7 +67,12 @@
67
67
  // copy against it. If an implementation disagrees with the table, the
68
68
  // implementation is wrong.
69
69
  import { hexToBytes } from "./crypto.js";
70
+ import { renderContentFinding, renderDeviation } from "./declaration.js";
70
71
  import { hasSideContent, sideContentKeys } from "./side-content.js";
72
+ // The kernel's one reader for bytes a producer signed: strict UTF-8, no leading
73
+ // byte-order mark, no non-finite literal. `jcs_loads` in
74
+ // `src/sanning_proof/canonicalize.py` is the twin (proof#140 gate 5).
75
+ import { jcsParse } from "./verifier.js";
71
76
  /**
72
77
  * The checkpoint's join key, in whichever vocabulary the bundle speaks.
73
78
  *
@@ -240,7 +245,12 @@ function unsafeEventIdReason(eventId) {
240
245
  * listing and pass that to `gradePack` — the rule takes an account, never a
241
246
  * bundle, precisely so the transport can say things the map cannot.
242
247
  */
243
- export function accountDisclosure(bundle, content) {
248
+ export function accountDisclosure(bundle,
249
+ // `SideContentMap`, which is what `hasSideContent` and `sideContentKeys`
250
+ // below already take: an entry holding a string that is not hex IS an entry,
251
+ // and typing it out of this signature made the one map a caller builds
252
+ // unusable here (proof#147). A widening, so no caller moves.
253
+ content) {
244
254
  const expected = [];
245
255
  const present = [];
246
256
  const missing = [];
@@ -288,7 +298,13 @@ export function signedLeafCount(checkpoint) {
288
298
  if (typeof hex !== "string" || hex.length === 0)
289
299
  return null;
290
300
  try {
291
- const record = JSON.parse(new TextDecoder().decode(hexToBytes(hex)));
301
+ // `jcsParse`, matching `signed_leaf_count` in `src/sanning_proof/grading.py`.
302
+ // The pair used to agree by pointing two LENIENT decoders at one input, and
303
+ // that held for invalid UTF-8 and nowhere else: `TextDecoder` EATS a
304
+ // leading byte-order mark where Python's `errors="replace"` keeps it, so
305
+ // `BOM + JCS(checkpoint)` yielded a signed leaf count here and unreadable
306
+ // there, and that count decides a completeness note (proof#140 gate 5).
307
+ const record = jcsParse(hexToBytes(hex));
292
308
  const n = record?.event?.leaf_count;
293
309
  return Number.isInteger(n) ? n : null;
294
310
  }
@@ -342,8 +358,32 @@ export function completenessNotes(bundle) {
342
358
  * Every reason this pack is not `verified`, in the order a reader should hear
343
359
  * them: what is missing from the whole, then what is missing from the parts.
344
360
  */
345
- export function gradeCauses(account, notes) {
361
+ export function gradeCauses(account, notes, declaration) {
346
362
  const causes = [];
363
+ // What the pack CLAIMED to hold comes first, because it frames everything
364
+ // below it: a reader told "four records are missing from what this pack says
365
+ // it contains" reads the per-record findings differently. Every deviation
366
+ // gets its own cause (one defect, one line), so a pack that deviates in
367
+ // four ways can never be reported as one vague shortfall.
368
+ for (const deviation of declaration?.deviations ?? []) {
369
+ causes.push({
370
+ kind: "declaration-deviation",
371
+ deviation: deviation.kind,
372
+ ...(deviation.eventId === undefined ? {} : { firstEventId: deviation.eventId }),
373
+ reason: renderDeviation(deviation),
374
+ });
375
+ }
376
+ // Then what the pack and the UNSIGNED content beside it do not agree on
377
+ // (proof#147). A cause, so the pack cannot read clean over it; NOT a
378
+ // deviation, so it never reaches `failed`.
379
+ for (const finding of declaration?.contentFindings ?? []) {
380
+ causes.push({
381
+ kind: "declaration-content-dependent",
382
+ deviation: finding.kind,
383
+ ...(finding.eventId === undefined ? {} : { firstEventId: finding.eventId }),
384
+ reason: renderContentFinding(finding),
385
+ });
386
+ }
347
387
  for (const note of notes) {
348
388
  if (note.kind === "omitted") {
349
389
  causes.push({
@@ -434,23 +474,77 @@ function passThrough(status) {
434
474
  * (§5.1: undetermined, not failed), and a proven mismatch is never softened
435
475
  * into absence. The last branch passes the kernel's headline through unchanged:
436
476
  * this rule DOWNGRADES a verdict, it never invents one.
477
+ *
478
+ * ── the declaration (optional fourth argument) ──────────────────────────────
479
+ *
480
+ * `declaration` is what `checkDeclaration` in `./declaration.ts` made of the
481
+ * pack's own signed statement of what it holds (`evidence-bundle.md` §5.4). A
482
+ * deviation from it is a PROVEN mismatch between what the pack claimed and
483
+ * what it carries, so it FAILS the pack and each deviation becomes its own
484
+ * cause. It must never soften into absence: that is how a trimmed hand-over
485
+ * reads as an innocent gap.
486
+ *
487
+ * A check that came back `partial` splits in THREE (proof#147), and the first
488
+ * of the three is not about the declaration's readability at all. The other
489
+ * two split on whether the body carried a `declaration` member.
490
+ *
491
+ * content-dependent the pack and the UNSIGNED content beside it do not
492
+ * line up (proof#147). Each finding is its own cause
493
+ * and the pack is `incomplete`. It must never reach
494
+ * `failed`: whoever assembles a case fills in the
495
+ * side-content map, so an accuser holding an intact
496
+ * pack could otherwise put its holder into `failed`
497
+ * with no key at all, by dropping one file or adding
498
+ * one. `failed` is left to what a signature covers.
499
+ * no declaration at all nothing moves. Every pack built before §5.4 is
500
+ * this case, and re-grading them would move pinned
501
+ * exit codes for artifacts already in customers'
502
+ * hands.
503
+ * present and unreadable the pack is `incomplete`. `declaration_version` is
504
+ * chosen by the same party that assembles the pack
505
+ * and has no accept-set, so a holder who trims a
506
+ * record and then writes one unreadable field can
507
+ * re-seal the pack with its own key and hand a
508
+ * reader a clean verdict over evidence of the trim
509
+ * sitting inside the signed body. UNKNOWN is the
510
+ * only honest answer to a claim the reader cannot
511
+ * read, and it adds no cause: `PackGrade.declaration`
512
+ * says why.
513
+ *
514
+ * `verified` adds nothing and moves nothing.
515
+ *
516
+ * Omitting the argument entirely is the pre-§5.4 behavior, unchanged, and
517
+ * `PackGrade.declaration` is then absent.
437
518
  */
438
- export function gradePack(result, account, notes) {
519
+ export function gradePack(result, account, notes, declaration) {
439
520
  const events = Array.isArray(result?.events) ? result.events : [];
440
521
  const recordsVerified = events.filter((e) => e.ok && e.contentOk === true).length;
441
522
  const recordsFailed = events.filter((e) => e.ok === false || e.contentOk === false).length;
442
523
  const recordsUndetermined = events.length - recordsVerified - recordsFailed;
443
- const causes = gradeCauses(account, notes);
524
+ const causes = gradeCauses(account, notes, declaration);
444
525
  let verdict;
445
526
  if (result?.status === "malformed")
446
527
  verdict = "malformed";
447
528
  else if (result?.status === "failed" || recordsFailed > 0)
448
529
  verdict = "failed";
530
+ else if (declaration?.verdict === "failed")
531
+ verdict = "failed";
449
532
  else if (recordsUndetermined > 0 || causes.length > 0)
450
533
  verdict = "incomplete";
534
+ else if (declaration?.present === true && declaration.verdict === "partial")
535
+ verdict = "incomplete";
451
536
  else
452
537
  verdict = passThrough(result?.status);
453
- return { verdict, recordsVerified, recordsFailed, recordsUndetermined, causes };
538
+ const grade = {
539
+ verdict,
540
+ recordsVerified,
541
+ recordsFailed,
542
+ recordsUndetermined,
543
+ causes,
544
+ };
545
+ if (declaration != null)
546
+ grade.declaration = declaration.verdict;
547
+ return grade;
454
548
  }
455
549
  // ---- Presentation (optional) -------------------------------------------------
456
550
  /**
@@ -484,5 +578,17 @@ export function renderCause(cause, subject = "this pack") {
484
578
  case "unaccounted-disclosure":
485
579
  return (`${cause.count} disclosed file(s) are not accounted for by any record in this bundle ` +
486
580
  `(first: ${cause.firstEventId})`);
581
+ // Rendered once, by `renderDeviation` in `./declaration.ts`, and carried
582
+ // here on `reason`. Re-deriving the sentence from the cause fields would
583
+ // put the declaration's wording in two places, and one of the two would be
584
+ // edited alone.
585
+ case "declaration-deviation":
586
+ return `this pack deviates from its own declaration: ${cause.reason}`;
587
+ case "declaration-content-dependent":
588
+ // Named as what the reader could not establish, never as what the
589
+ // holder did: the bytes it turns on are unsigned and whoever assembled
590
+ // the case chose them (proof#147).
591
+ return (`${subject} and the unsigned content handed in beside it do not line up: ` +
592
+ `${cause.reason}`);
487
593
  }
488
594
  }
@@ -0,0 +1,209 @@
1
+ export declare const CANDIDATE_NAMESPACE = "handoff.";
2
+ export declare const DEFINED_EVENT_TYPES: readonly ["handoff.sent", "handoff.received"];
3
+ /**
4
+ * The finding a record that is not a hand-off record at all reads with.
5
+ *
6
+ * A FINDING and not a verdict: the ratified verdict words are closed
7
+ * (anchor#188) and this reading names an outcome beside one, exactly as
8
+ * `counterpart_absent`, `counterpart_ambiguous` and `malformed_record` do. It
9
+ * exists because 4.1 had a SILENT branch, and a rule with a silent branch is a
10
+ * rule the party being audited steers into (proof#136). A record TRYING to be
11
+ * a hand-off and getting it wrong reads `malformed`; a record that is not a
12
+ * hand-off at all reads this. Both are readings. Neither is silence.
13
+ */
14
+ export declare const NOT_A_HANDOFF_FINDING = "not_a_handoff_record";
15
+ /**
16
+ * An envelope `public_key` as a KEY rather than as a string: the 32 key bytes
17
+ * written as lowercase hex, or `null` where the value is not 64 characters of
18
+ * hex and so names no key this reading can compare.
19
+ *
20
+ * Case is the whole point: §2 states lowercase as a PRODUCER convention, the
21
+ * envelope layer accepts either case in every kernel, and the party being
22
+ * audited is the producer (proof#133 adversarial gate, ruling 1). Nothing
23
+ * wider is folded: whitespace inside the value is not stripped, because
24
+ * Python's `bytes.fromhex` skips ASCII whitespace and this kernel's charset
25
+ * check does not, a live divergence at the ENVELOPE layer and a separate
26
+ * founder call (proof#135). A reading rule that silently repaired it would
27
+ * hide it.
28
+ */
29
+ export declare function normalizedSigningKey(value: unknown): string | null;
30
+ /**
31
+ * Whether two envelope keys are the SAME KEY. Two values that name a key are
32
+ * equal when their key bytes are equal.
33
+ *
34
+ * Where either side names no key, the question cannot be answered from key
35
+ * bytes and the answer is the conservative one: the two are reported as one
36
+ * key, so the pair reads `failed[self_handoff]` rather than clean. Exact
37
+ * string equality was the answer here and it was the wrong way round: one key
38
+ * that signed both ends, spelled `abcd...` on one envelope and `abcd ...` on
39
+ * the other, is one party, and string equality called it two and read the
40
+ * self-hand-off `verified` (proof#133 fourth hunt, finding 1). A record whose
41
+ * own envelope key this reading cannot read is `malformed` under
42
+ * `recordIsMalformed` and a formed pair reaches `malformed` before it reaches
43
+ * this line, so the two rules are belt and braces rather than one rule stated
44
+ * twice.
45
+ */
46
+ export declare function sameSigningKey(left: unknown, right: unknown): boolean;
47
+ export type HandoffVerdict = "verified" | "failed" | "malformed";
48
+ export interface HandoffRecord {
49
+ event_type?: unknown;
50
+ event?: unknown;
51
+ context?: unknown;
52
+ metadata?: unknown;
53
+ [key: string]: unknown;
54
+ }
55
+ /**
56
+ * One hand-off record as a reader holds it: the committed record, plus the
57
+ * `public_key` of the envelope that carries it. The key comes from the ENVELOPE
58
+ * and never from the record, which is the whole point of the amended rule.
59
+ *
60
+ * PRECONDITION: THE ENVELOPE MUST ALREADY HAVE VERIFIED. "Unforgeable" is a
61
+ * property of a VERIFIED envelope and of nothing else. The key on an envelope
62
+ * whose signature has not been checked is an assertion like any other, and
63
+ * every reading in this module is then worth exactly as much as that
64
+ * assertion. A reader builds a `HeldRecord` only from an envelope that
65
+ * `verifyEnvelope` returned `ok` for, bound to the canonical bytes of this
66
+ * record. Nothing in this module can check that for itself, which is why it is
67
+ * stated here (proof#133 adversarial gate, ruling 3), and why candidacy and
68
+ * the self-hand-off check read `normalizedSigningKey(publicKey)` rather than
69
+ * the spelling the audited party chose.
70
+ */
71
+ export interface HeldRecord {
72
+ publicKey: string;
73
+ record: HandoffRecord;
74
+ ref: string;
75
+ }
76
+ /**
77
+ * Whether a reader reads this record as a hand-off record at all.
78
+ *
79
+ * §4.1 says a reader reads EVERY hand-off record it holds, and an exact
80
+ * `handoff.` prefix did not deliver that: `HANDOFF.RECEIVED`,
81
+ * `sanning.handoff.received`, `handoff-received` and a non-string `event_type`
82
+ * all vanished from the reading entirely, not even as malformed, so a receipt
83
+ * typed one character outside the prefix was never read and, beside a clean decoy,
84
+ * published `verified` over proven tampering (proof#133 adversarial gate,
85
+ * ruling 2).
86
+ *
87
+ * The rule is checkable rather than a judgement, and it is exactly two
88
+ * clauses. The FIRST is the one the adversary cannot evade: the record's
89
+ * `event` carries `counterparty_public_key`, the field §4.1 defines and no
90
+ * other section does, WHATEVER its `event_type` says. The second keeps a
91
+ * record that omits the field in scope: an `event_type` that is a string whose
92
+ * folded form has `handoff` as one of its `.`-separated segments, which leaves
93
+ * `asset_missing` and the other families alone.
94
+ *
95
+ * The order is the fix, not decoration. Every earlier version of this test was
96
+ * an ALLOW-LIST OVER A NAME THE AUDITED PARTY WRITES, so each repair narrowed
97
+ * the class and the next spelling walked around it: an exact `handoff.` prefix
98
+ * fell to `HANDOFF.RECEIVED`, and the ASCII fold that replaced it fell to the
99
+ * `ff` ligature, full-width letters, a Cyrillic `a`, `hand-off`, `handoffs`, a
100
+ * zero-width space, a BOM and a non-breaking space, each of which left a
101
+ * tampered receipt with no reading at all beside a clean decoy (proof#133
102
+ * fourth hunt, finding 2). The structural field closes all of them in one rule
103
+ * rather than by chasing Unicode, because the attack needs the field: a
104
+ * receipt that drops `counterparty_public_key` says nothing about who it came
105
+ * from.
106
+ *
107
+ * Anything in scope that is not exactly `handoff.sent` or `handoff.received`
108
+ * is `malformed`, so a spelling is never a way out.
109
+ *
110
+ * THIS IS NO LONGER A GATE ON BEING READ (proof#136). Both clauses are written
111
+ * by the party being audited, and one code point on each put a record in scope
112
+ * by neither, which produced silence. `readHandoffs` reads every record it
113
+ * holds, and this test now decides only WHICH reading a record gets: in scope
114
+ * and wrong reads `malformed`, out of scope reads `NOT_A_HANDOFF_FINDING`. A
115
+ * respelling buys a different line rather than no line.
116
+ */
117
+ export declare function isHandoffNamespace(held: HeldRecord): boolean;
118
+ /**
119
+ * What one candidate graded to, kept beside the aggregate. 4.1 requires a
120
+ * reader to name the record and which check failed, and `malformed` outranks
121
+ * `failed`, so one junk record signed by the counterparty key would otherwise
122
+ * turn a named `message_hash` failure into a `malformed` with nothing named
123
+ * (proof#133 review, ruling 6).
124
+ */
125
+ export interface CandidateReading {
126
+ ref: string;
127
+ verdict: HandoffVerdict;
128
+ failedChecks: string[];
129
+ }
130
+ export interface HandoffReading {
131
+ ref: string;
132
+ /** `null` where no pair formed: there is no verdict to give, and the finding says why. */
133
+ verdict: HandoffVerdict | null;
134
+ failedChecks: string[];
135
+ findings: string[];
136
+ /** Ordered, never filtered. */
137
+ candidates: string[];
138
+ /** One entry per candidate, in the same order, and nothing left out of it. */
139
+ candidateReadings: CandidateReading[];
140
+ }
141
+ /**
142
+ * A record missing a REQUIRED field of its own type, or carrying one that is
143
+ * not the type §4.1 states, is malformed and fails. Stated as a rule because
144
+ * §4.1 had none: a pair where both sides omitted `message_hash` compared absent
145
+ * to absent, found them equal, and graded intact (proof#132).
146
+ *
147
+ * A record whose ENVELOPE key this reading cannot read is malformed too, and
148
+ * that one is a repair rather than an omission. The rule used to be "a value
149
+ * that is not 64 characters of hex names no key and matches nothing", so such a
150
+ * record dropped out of candidacy in silence and the subject read
151
+ * `counterpart_absent`, which §4.1 obliges a reader to call innocent. Nine
152
+ * whitespace spellings of a real key produce a VERIFIED envelope in exactly
153
+ * that state in the Python kernel, because `bytes.fromhex` skips ASCII
154
+ * whitespace, which reopened the whole of the first attack through the repair
155
+ * rather than around it (proof#133 fourth hunt, finding 1). A record a reader
156
+ * HOLDS, whose own key it cannot read, is a defect in the record and not an
157
+ * absence of one. This holds whatever the envelope layer decides about its hex
158
+ * charset (proof#135).
159
+ */
160
+ export declare function recordIsMalformed(held: HeldRecord): boolean;
161
+ /**
162
+ * Read ONE record against everything the reader holds.
163
+ *
164
+ * Every record gets a reading, and the two findings are how the distinction a
165
+ * reader needs survives (proof#136): a record that is not a hand-off record at
166
+ * all reads `NOT_A_HANDOFF_FINDING` with no verdict, and a record 4.1 DOES
167
+ * govern and that is wrong reads `malformed`. The scope test lives HERE and
168
+ * not in `readHandoffs`, so the two entry points cannot answer differently
169
+ * about one record.
170
+ */
171
+ export declare function readHandoff(subject: HeldRecord, held: readonly HeldRecord[]): HandoffReading;
172
+ /**
173
+ * EVERY record the reader holds gets a reading, in the order given.
174
+ *
175
+ * This function does not filter, and the branch that used to decide whether a
176
+ * record was read at all is gone (proof#136, ratified by Phil 2026-09-22).
177
+ * Scope was two clauses, an exact test for the key name
178
+ * `counterparty_public_key` and an ASCII fold of `event_type`, and both
179
+ * operands are written by the party being audited: one code point on each put
180
+ * a record in scope by NEITHER route, so it had no reading at all and the pile
181
+ * read clean. Five repairs narrowed which values reached that silent branch
182
+ * and five hunts found a value outside it, so the branch is removed rather
183
+ * than narrowed again.
184
+ *
185
+ * The distinction a reader needs survives as two findings rather than as a
186
+ * reading and a silence: a record TRYING to be a hand-off and getting it wrong
187
+ * reads `malformed` with `malformed_record`, and a record that is not a
188
+ * hand-off record at all reads `NOT_A_HANDOFF_FINDING` with no verdict.
189
+ * `isHandoffNamespace` decides which of the two, in `readHandoff`, and no
190
+ * longer whether a record is read.
191
+ *
192
+ * THE COST, stated rather than left to be found: a reader given a mixed pile
193
+ * produces a line per record, including for records that have nothing to do
194
+ * with hand-offs. 4.1 says so too.
195
+ */
196
+ export declare function readHandoffs(held: readonly HeldRecord[]): HandoffReading[];
197
+ /**
198
+ * Whether this record is a conformant hand-off record of 4.1: it is read as a
199
+ * hand-off record at all (`isHandoffNamespace`) and it is not `malformed`
200
+ * (`recordIsMalformed`), which between them mean it is exactly a
201
+ * `handoff.sent` or a `handoff.received` carrying every REQUIRED field of its
202
+ * own type, under an envelope key this reading can read.
203
+ *
204
+ * `./declaration.ts` reads this for evidence-bundle.md 5.4: a claimed
205
+ * `sent_event_id` or `received_event_id` naming a record that is not one of
206
+ * these is a DEVIATION, so a record cannot be unreadable at the record layer
207
+ * and usable at the pack layer at the same time (proof#136).
208
+ */
209
+ export declare function isConformantHandoffRecord(held: HeldRecord): boolean;