@sanning/proof 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +43 -8
  2. package/dist/agent-proof.d.ts +2 -2
  3. package/dist/agent-proof.js +4 -4
  4. package/dist/case-file.d.ts +126 -0
  5. package/dist/case-file.js +349 -0
  6. package/dist/case.d.ts +199 -43
  7. package/dist/case.js +522 -146
  8. package/dist/cli.d.ts +132 -1
  9. package/dist/cli.js +1140 -96
  10. package/dist/crypto.d.ts +1 -1
  11. package/dist/crypto.js +5 -1
  12. package/dist/declaration.d.ts +58 -52
  13. package/dist/declaration.js +256 -33
  14. package/dist/evidence.d.ts +2 -0
  15. package/dist/evidence.js +134 -19
  16. package/dist/exit-codes.d.ts +17 -0
  17. package/dist/exit-codes.js +50 -0
  18. package/dist/grading.d.ts +28 -13
  19. package/dist/grading.js +90 -31
  20. package/dist/handoff.d.ts +16 -1
  21. package/dist/handoff.js +91 -16
  22. package/dist/index.d.ts +10 -7
  23. package/dist/index.js +15 -7
  24. package/dist/message-parts.d.ts +11 -9
  25. package/dist/message-parts.js +11 -9
  26. package/dist/types.d.ts +1 -0
  27. package/dist/verifier.d.ts +9 -2
  28. package/dist/verifier.js +28 -12
  29. package/dist/zip-reader.d.ts +50 -0
  30. package/dist/zip-reader.js +220 -0
  31. package/package.json +2 -2
  32. package/src/agent-proof.ts +4 -4
  33. package/src/case-file.ts +419 -0
  34. package/src/case.ts +651 -156
  35. package/src/cli.ts +1325 -100
  36. package/src/crypto.ts +5 -1
  37. package/src/declaration.ts +248 -52
  38. package/src/evidence.ts +142 -21
  39. package/src/exit-codes.ts +54 -0
  40. package/src/grading.ts +109 -52
  41. package/src/handoff.ts +108 -17
  42. package/src/index.ts +60 -3
  43. package/src/message-parts.ts +11 -9
  44. package/src/types.ts +5 -0
  45. package/src/verifier.ts +35 -13
  46. package/src/zip-reader.ts +229 -0
  47. package/vectors/README.md +56 -3
  48. package/vectors/case-file-index.json +668 -0
  49. package/vectors/case-split.json +1102 -0
  50. package/vectors/pack-grading.json +68 -4
  51. package/vectors/stamp-on-chain.json +3 -3
package/dist/evidence.js CHANGED
@@ -21,13 +21,15 @@ import { deriveOperatorAddress, ed25519Verify, hexToBytes,
21
21
  // body: two copies of one rule is how two kernels drift apart one edit at a
22
22
  // time, which is the whole defect class this fix is about.
23
23
  isHexField, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
24
+ import { isConformantHandoffRecord, isHandoffNamespace } from "./handoff.js";
24
25
  import { leafHash, verifyInclusion } from "./merkle.js";
25
26
  import { MAX_CANONICAL_DEPTH, contentHashes, exceedsDepth, jcs, jcsParse, verifyEnvelope, } from "./verifier.js";
26
27
  import { inBodyContentOffered, sideContentEntry, } from "./side-content.js";
27
28
  // Only the evidence-bundle major(s) this kernel verifies. A new major is a
28
29
  // deliberate one-entry addition HERE — same fail-closed discipline as the
29
30
  // envelope verifier's ACCEPTED_SPEC_MAJORS.
30
- const ACCEPTED_EVIDENCE_MAJORS = ["sanning.evidence/v1"];
31
+ /** The accepted `sanning.evidence/v1` wrapper majors; exported in both kernels (P-01, 0.13.0). */
32
+ export const ACCEPTED_EVIDENCE_MAJORS = Object.freeze(["sanning.evidence/v1"]);
31
33
  // The body_type this module knows how to verify structurally. An unknown
32
34
  // body_type is not a hard failure of the WRAPPER (the signature + body_hash
33
35
  // still verify), but its body cannot be re-derived, so the per-body verdict is
@@ -254,12 +256,16 @@ async function verifyBundle(bundle, options, exportSource) {
254
256
  const b = bundle;
255
257
  const specVersionOk = isEvidenceMajorAccepted(b.spec_version);
256
258
  if (!specVersionOk) {
257
- return malformed(`unsupported evidence spec_version: ${JSON.stringify(b.spec_version)}`, false);
259
+ // `?? null`: an ABSENT field is spelled `null`, as the Python twin's
260
+ // `_json_str` spells it. `JSON.stringify(undefined)` is `undefined`, and
261
+ // the two kernels printed two different sentences for one bundle until
262
+ // `verify --json` compared them byte for byte (0.13.0).
263
+ return malformed(`unsupported evidence spec_version: ${JSON.stringify(b.spec_version ?? null)}`, false);
258
264
  }
259
265
  if (typeof b.signature_alg === "string" && b.signature_alg !== "ed25519") {
260
266
  // The reference verifier implements ed25519 only (evidence-bundle §2 +
261
267
  // decision #2); other algs are a deliberate future addition.
262
- return malformed(`unsupported signature_alg: ${JSON.stringify(b.signature_alg)}`, true);
268
+ return malformed(`unsupported signature_alg: ${JSON.stringify(b.signature_alg ?? null)}`, true);
263
269
  }
264
270
  if (typeof b.public_key !== "string" || typeof b.signature !== "string") {
265
271
  return malformed("evidence bundle is missing public_key/signature", true);
@@ -308,7 +314,7 @@ async function verifyBundle(bundle, options, exportSource) {
308
314
  if (keyFormOk && signatureFormOk) {
309
315
  try {
310
316
  const { signature: _sig, co_signatures: _co, ...bundleForSig } = b;
311
- signatureOk = await ed25519Verify(b.signature, utf8(jcs(bundleForSig)), b.public_key);
317
+ signatureOk = await ed25519Verify(utf8(jcs(bundleForSig)), b.signature, b.public_key);
312
318
  if (!signatureOk)
313
319
  errors.push("wrapper Ed25519 signature verification failed");
314
320
  }
@@ -524,7 +530,7 @@ function parseAnchorRef(raw, errors) {
524
530
  }
525
531
  const o = raw;
526
532
  if (typeof o.kind !== "string" || !/^[a-z0-9][a-z0-9._-]*$/.test(o.kind)) {
527
- errors.push(`anchor_ref.kind is missing or malformed: ${JSON.stringify(o.kind)}`);
533
+ errors.push(`anchor_ref.kind is missing or malformed: ${JSON.stringify(o.kind ?? null)}`);
528
534
  return { ref: null, malformed: true };
529
535
  }
530
536
  if (typeof o.id !== "string" || o.id === "") {
@@ -1076,21 +1082,62 @@ function isNonNegInt(n) {
1076
1082
  async function verifyDisclosedContent(ev, eventId, eventBytes, sideContent) {
1077
1083
  const evErrors = [];
1078
1084
  let contentOk = null;
1085
+ // WHICH hash in the record is decided by the record's STRUCTURE
1086
+ // (evidence-bundle §5.1 v1.19, A20). A record `envelope-spec.md` §4.1
1087
+ // governs (`isHandoffNamespace`) commits to its message through
1088
+ // `event.message_hash`, SHA-256 of `JCS(message object)`, and has no
1089
+ // `content_hash`, so until A20 its disclosure bound to nothing and an honest
1090
+ // hand-off pack could never grade `verified`. For such a record the committed
1091
+ // hash is `event.message_hash` and nothing else: not a `content_hash` it also
1092
+ // carries, and not the envelope's. It binds only when the record is a
1093
+ // CONFORMANT hand-off record under the key that signed it
1094
+ // (`isConformantHandoffRecord`) and the value is 64 characters of lowercase
1095
+ // hex. Anything short of that commits nothing this reader will bind to, so
1096
+ // the reading is undetermined and never true: a record dressed as a hand-off
1097
+ // record gains no binding from the dress, and a record outside §4.1 keeps its
1098
+ // `content_hash` exactly as before, whatever `message_hash` field it writes.
1099
+ // The Python twin is `evidence._evaluate_content`.
1079
1100
  const committed = [];
1080
1101
  let recordContentHash;
1102
+ let governed = false;
1103
+ let handoffHash;
1081
1104
  if (eventBytes !== undefined) {
1082
1105
  try {
1083
- const record = jcsParse(eventBytes);
1084
- const ch = record.event?.content_hash;
1106
+ const parsed = jcsParse(eventBytes);
1107
+ const record = parsed;
1108
+ const ch = record?.event?.content_hash;
1085
1109
  if (typeof ch === "string" && ch)
1086
1110
  recordContentHash = ch.toLowerCase();
1111
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
1112
+ const envelope = ev.envelope;
1113
+ const held = {
1114
+ publicKey: envelope !== null && typeof envelope === "object" && !Array.isArray(envelope)
1115
+ ? envelope.public_key
1116
+ : undefined,
1117
+ record: parsed,
1118
+ ref: "",
1119
+ };
1120
+ if (isHandoffNamespace(held)) {
1121
+ governed = true;
1122
+ const event = parsed.event;
1123
+ const mh = event !== null && typeof event === "object" && !Array.isArray(event)
1124
+ ? event.message_hash
1125
+ : undefined;
1126
+ if (isConformantHandoffRecord(held) && isLowerHex(mh, 64))
1127
+ handoffHash = mh;
1128
+ }
1129
+ }
1087
1130
  }
1088
1131
  catch {
1089
1132
  // record not JSON — the payload binding already reflects that; content
1090
1133
  // stays undetermined rather than throwing.
1091
1134
  }
1092
1135
  }
1093
- if (recordContentHash !== undefined) {
1136
+ if (governed) {
1137
+ if (handoffHash !== undefined)
1138
+ committed.push(handoffHash);
1139
+ }
1140
+ else if (recordContentHash !== undefined) {
1094
1141
  committed.push(recordContentHash);
1095
1142
  }
1096
1143
  else if (ev.envelope && typeof ev.envelope === "object") {
@@ -1112,15 +1159,20 @@ async function verifyDisclosedContent(ev, eventId, eventBytes, sideContent) {
1112
1159
  if (disclosed === undefined || committed.length === 0) {
1113
1160
  contentOk = null;
1114
1161
  if (disclosed !== undefined && committed.length === 0) {
1115
- evErrors.push("disclosed content present but no committed content_hash to bind to " +
1116
- "(record withheld or record has no event.content_hash) -- undetermined");
1162
+ evErrors.push(governed
1163
+ ? "disclosed content present but this hand-off record commits no " +
1164
+ "message_hash a reader can bind to (not a conformant hand-off " +
1165
+ "record, or message_hash is not 64 lowercase hex) -- undetermined"
1166
+ : "disclosed content present but no committed content_hash to bind to " +
1167
+ "(record withheld or record has no event.content_hash) -- undetermined");
1117
1168
  }
1118
1169
  }
1119
1170
  else {
1120
1171
  const got = await sha256Hex(disclosed);
1121
1172
  contentOk = committed.includes(got);
1122
1173
  if (!contentOk) {
1123
- evErrors.push("disclosed content does not match the committed content_hash: " +
1174
+ const fieldName = governed ? "message_hash" : "content_hash";
1175
+ evErrors.push(`disclosed content does not match the committed ${fieldName}: ` +
1124
1176
  `sha256(disclosed)=${got} committed=${committed.join("|")}`);
1125
1177
  }
1126
1178
  }
@@ -1217,7 +1269,7 @@ async function verifyStampEntry(raw, seen) {
1217
1269
  // two answers to one question inside one signature. The body_type is the
1218
1270
  // reader's dispatch key and it said v1; honouring the other spelling is
1219
1271
  // how a reader half-parses a shape it does not know.
1220
- errors.push(`unsupported stamp_version ${JSON.stringify(body.stamp_version)} in a ` +
1272
+ errors.push(`unsupported stamp_version ${JSON.stringify(body.stamp_version ?? null)} in a ` +
1221
1273
  `${STAMP_TRACE_BODY_TYPE} body (expected ${STAMP_BODY_VERSION})`);
1222
1274
  return { result: base, witnessedHex };
1223
1275
  }
@@ -1702,8 +1754,8 @@ async function verifyStampEvidence(b, specVersionOk, assertedStatus, errors, opt
1702
1754
  * synthesis exists to drive `gatewayUnavailable`, which is what downgrades that
1703
1755
  * run to `partial`. Here §5.2.4 already downgrades an unwitnessed stamp to
1704
1756
  * `partial` on its own, with a better error. Copying the synthesis would add
1705
- * nothing to the status and would silently move the CLI exit code from 0 to 3
1706
- * for an unwitnessed stamp — changing what an auditor's script does, based on
1757
+ * nothing to the status, which is `partial` either way, and would add an
1758
+ * unreachable-gateway note for a network event that never happened, based on
1707
1759
  * nothing but whether the caller happened to pass a gateway list.
1708
1760
  */
1709
1761
  async function refetchStamps(stamps, witnessed, options, errors) {
@@ -2016,8 +2068,11 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
2016
2068
  // step-5 erratum flagged in the slice report.
2017
2069
  let verdictAgreementOk = false;
2018
2070
  try {
2019
- const cachedProjection = jcs(projectVerdictForAgreement(body.kernel_verdict));
2020
- const freshProjection = jcs(projectVerdictForAgreement(verdict));
2071
+ const cached = projectVerdictForAgreement(body.kernel_verdict);
2072
+ const fresh = projectVerdictForAgreement(verdict);
2073
+ toleratePreA20Content(cached, fresh, sourceEventsGoverned(inline));
2074
+ const cachedProjection = jcs(cached);
2075
+ const freshProjection = jcs(fresh);
2021
2076
  verdictAgreementOk = cachedProjection === freshProjection;
2022
2077
  if (!verdictAgreementOk) {
2023
2078
  errors.push("recomputed verdict disagrees with the cached kernel_verdict " +
@@ -2046,8 +2101,8 @@ async function verifyExportBody(b, specVersionOk, assertedStatus, errors, option
2046
2101
  status = "failed";
2047
2102
  }
2048
2103
  else if (sourceResult.status === "partial") {
2049
- // Undetermined source (withheld record → exit 0; gateway-unreachable →
2050
- // exit 3 via the propagated "unreachable" note). Never a failure.
2104
+ // Undetermined source (a withheld record, or an unreachable gateway):
2105
+ // exit 3 either way since 0.13.0. Never a failure.
2051
2106
  status = "partial";
2052
2107
  }
2053
2108
  else {
@@ -2193,7 +2248,7 @@ async function verifyAttestation(rec, sourceCheckpoints) {
2193
2248
  if (rec.signature_alg !== "rsa-pss-sha256") {
2194
2249
  return {
2195
2250
  result,
2196
- malformed: `unsupported attestation signature_alg: ${JSON.stringify(rec.signature_alg)}`,
2251
+ malformed: `unsupported attestation signature_alg: ${JSON.stringify(rec.signature_alg ?? null)}`,
2197
2252
  };
2198
2253
  }
2199
2254
  const pub = rec.public_key;
@@ -2421,6 +2476,66 @@ function projectVerdictForAgreement(v) {
2421
2476
  }),
2422
2477
  };
2423
2478
  }
2479
+ // Per event of an export's inline `sanning.anchor.trace/v3` source, in order:
2480
+ // whether `envelope-spec.md` §4.1 governs its record. Decoded the way the
2481
+ // content binding decodes it, so an event whose record is withheld, not hex or
2482
+ // not JCS is not governed here either. The Python twin is
2483
+ // `evidence._source_events_governed`.
2484
+ function sourceEventsGoverned(source) {
2485
+ if (source === null || typeof source !== "object" || Array.isArray(source))
2486
+ return [];
2487
+ const s = source;
2488
+ if (s.body_type !== ANCHOR_TRACE_BODY_TYPE)
2489
+ return [];
2490
+ const body = s.body;
2491
+ const events = body !== null && typeof body === "object" && !Array.isArray(body) ? body.events : undefined;
2492
+ if (!Array.isArray(events))
2493
+ return [];
2494
+ return events.map((raw) => {
2495
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
2496
+ return false;
2497
+ const ev = raw;
2498
+ if (typeof ev.event_bytes !== "string")
2499
+ return false;
2500
+ try {
2501
+ const parsed = jcsParse(hexToBytes(ev.event_bytes));
2502
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
2503
+ return false;
2504
+ const envelope = ev.envelope;
2505
+ const held = {
2506
+ publicKey: envelope !== null && typeof envelope === "object" && !Array.isArray(envelope)
2507
+ ? envelope.public_key
2508
+ : undefined,
2509
+ record: parsed,
2510
+ ref: "",
2511
+ };
2512
+ return isHandoffNamespace(held);
2513
+ }
2514
+ catch {
2515
+ return false;
2516
+ }
2517
+ });
2518
+ }
2519
+ // The one difference §5 step 5 does not read as a disagreement
2520
+ // (evidence-export.md, evidence-bundle.md v1.19, A20): a cached `content_ok` of
2521
+ // `null` against a recomputed `true`, on an event whose record §4.1 governs. A
2522
+ // kernel before A20 could not bind a hand-off record's disclosed message, so an
2523
+ // export it composed claims less than the recompute proves, and reading that
2524
+ // as tamper failed an honest export. Only that direction: a cached `true`
2525
+ // against a recomputed `false` or `null`, `null` against `false`, and any
2526
+ // difference on a record outside §4.1 still disagree. Edits `fresh` in place
2527
+ // to match `cached` on those events. The Python twin is
2528
+ // `evidence._tolerate_pre_a20_content`.
2529
+ function toleratePreA20Content(cached, fresh, governed) {
2530
+ const n = Math.min(cached.events.length, fresh.events.length);
2531
+ for (let i = 0; i < n; i++) {
2532
+ if (governed[i] === true &&
2533
+ cached.events[i].content_ok === null &&
2534
+ fresh.events[i].content_ok === true) {
2535
+ fresh.events[i].content_ok = null;
2536
+ }
2537
+ }
2538
+ }
2424
2539
  function summarizeExport(status, source, attestations) {
2425
2540
  const attOk = attestations.filter((a) => a.ok).length;
2426
2541
  return (`Export ${status}: source bundle ${source.status} ` +
@@ -0,0 +1,17 @@
1
+ export declare const EXIT_VERIFIED = 0;
2
+ export declare const EXIT_FAILED = 1;
3
+ export declare const EXIT_MALFORMED = 2;
4
+ export declare const EXIT_UNDETERMINED = 3;
5
+ export declare const EXIT_MIXED = 4;
6
+ /**
7
+ * The exit code of each of the eight verdict words (`evidence-bundle.md` §3).
8
+ * `undetermined` is the name of 3, and never a word here.
9
+ */
10
+ export declare const EXIT_CODES: Readonly<Record<string, number>>;
11
+ /** What every verb's help prints, identical in both commands. */
12
+ export declare const EXIT_CODES_HELP: string;
13
+ /**
14
+ * The exit code of a verdict word. A word outside the eight is a verdict
15
+ * nothing can render, so it exits 2, never 0.
16
+ */
17
+ export declare function exitCodeFor(verdict: unknown): number;
@@ -0,0 +1,50 @@
1
+ // One exit-code table for every verb (0.13.0).
2
+ //
3
+ // Both commands, `npx @sanning/proof` and `sanning-proof`, map a verdict word to
4
+ // an exit code through this one table, for `verify`, `case` and `export` alike,
5
+ // and every verb's `--help` prints `EXIT_CODES_HELP` byte for byte. The twin is
6
+ // `src/sanning_proof/exit_codes.py`; the specification is `evidence-export.md`
7
+ // §5.
8
+ //
9
+ // Until 0.13.0 `verify` exited 0 on `partial` unless a gateway was unreachable,
10
+ // while `case` exited 3 on it, so a CI gate reading `verify`'s exit code read a
11
+ // stamp witnessed by nobody as verified. Every word that means "could not be
12
+ // settled" now exits 3, which is the code's name: undetermined.
13
+ export const EXIT_VERIFIED = 0;
14
+ export const EXIT_FAILED = 1;
15
+ export const EXIT_MALFORMED = 2;
16
+ export const EXIT_UNDETERMINED = 3;
17
+ export const EXIT_MIXED = 4;
18
+ /**
19
+ * The exit code of each of the eight verdict words (`evidence-bundle.md` §3).
20
+ * `undetermined` is the name of 3, and never a word here.
21
+ */
22
+ export const EXIT_CODES = Object.freeze({
23
+ verified: EXIT_VERIFIED,
24
+ failed: EXIT_FAILED,
25
+ malformed: EXIT_MALFORMED,
26
+ partial: EXIT_UNDETERMINED,
27
+ pending: EXIT_UNDETERMINED,
28
+ incomplete: EXIT_UNDETERMINED,
29
+ not_found: EXIT_UNDETERMINED,
30
+ mixed: EXIT_MIXED,
31
+ });
32
+ /** What every verb's help prints, identical in both commands. */
33
+ export const EXIT_CODES_HELP = [
34
+ "Exit codes, one table for every verb:",
35
+ " 0 verified, or the export was written",
36
+ " 1 failed",
37
+ " 2 usage or malformed",
38
+ " 3 undetermined: partial, pending, incomplete or not_found",
39
+ " 4 mixed: more than one outcome, none failed; read the exceptions",
40
+ ].join("\n");
41
+ /**
42
+ * The exit code of a verdict word. A word outside the eight is a verdict
43
+ * nothing can render, so it exits 2, never 0.
44
+ */
45
+ export function exitCodeFor(verdict) {
46
+ if (typeof verdict === "string" && Object.prototype.hasOwnProperty.call(EXIT_CODES, verdict)) {
47
+ return EXIT_CODES[verdict];
48
+ }
49
+ return EXIT_MALFORMED;
50
+ }
package/dist/grading.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { DeclarationCheck, DeclarationContentKind, DeclarationDeviationKind, DeclarationVerdict } from "./declaration.js";
2
2
  import { type SideContentMap } from "./side-content.js";
3
- export type PackVerdict = "verified" | "partial" | "pending" | "not_found" | "mixed" | "failed" | "malformed" | "incomplete";
3
+ export declare const PACK_VERDICTS: readonly ["verified", "partial", "pending", "not_found", "mixed", "failed", "malformed", "incomplete"];
4
+ export type PackVerdict = (typeof PACK_VERDICTS)[number];
4
5
  /**
5
6
  * The minimum this module reads out of a verification result.
6
7
  *
@@ -12,9 +13,16 @@ export type PackVerdict = "verified" | "partial" | "pending" | "not_found" | "mi
12
13
  */
13
14
  export interface GradeInput {
14
15
  status: string | null | undefined;
16
+ /**
17
+ * `eventId` and `payloadBindingOk` are read by rule version 6 alone (A22):
18
+ * an input that does not carry both is never withheld by its declaration,
19
+ * so an adapter written for rule version 5 keeps the stricter reading.
20
+ */
15
21
  events: {
16
22
  ok: boolean;
17
23
  contentOk: boolean | null;
24
+ eventId?: string;
25
+ payloadBindingOk?: boolean | null;
18
26
  }[];
19
27
  }
20
28
  /**
@@ -56,13 +64,8 @@ export type CompletenessNote = {
56
64
  presented: null;
57
65
  kind: "unreadable";
58
66
  };
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";
67
+ export declare const PACK_GRADE_CAUSE_KINDS: readonly ["records_omitted", "records_surplus", "leaf_count_unreadable", "no_disclosure", "missing_disclosure", "unreadable_disclosure", "unaccounted_disclosure", "declaration_deviation", "declaration_content_dependent"];
68
+ export type PackGradeCauseKind = (typeof PACK_GRADE_CAUSE_KINDS)[number];
66
69
  /**
67
70
  * One reason a pack is not `verified`, as data.
68
71
  *
@@ -89,8 +92,8 @@ export interface PackGradeCause {
89
92
  reason?: string;
90
93
  /**
91
94
  * Which deviation from the pack's own declaration, for
92
- * `declaration-deviation`, or which content-dependent finding, for
93
- * `declaration-content-dependent`.
95
+ * `declaration_deviation`, or which content-dependent finding, for
96
+ * `declaration_content_dependent`.
94
97
  */
95
98
  deviation?: DeclarationDeviationKind | DeclarationContentKind;
96
99
  }
@@ -107,6 +110,17 @@ export interface PackGrade {
107
110
  * check was passed, which is the pre-§5.4 call.
108
111
  */
109
112
  declaration?: DeclarationVerdict;
113
+ /**
114
+ * Records WITHHELD BY THEIR DECLARATION (rule version 6, evidence-bundle
115
+ * 5.4.5 v1.20, A22): a readable declaration lists them in
116
+ * `declaredWithheld`, their `ok` is true, their `contentOk` is `null` and
117
+ * their `payloadBindingOk` is true. Never counted in `recordsVerified`: no
118
+ * byte of their content was hashed, and a surface MUST NOT print them as
119
+ * verified records.
120
+ */
121
+ recordsWithheld: number;
122
+ /** Their `event_id`s, in bundle order. */
123
+ withheldEventIds: string[];
110
124
  }
111
125
  /**
112
126
  * Account for the disclosed bytes a pack handed us against the records its
@@ -152,13 +166,14 @@ export declare function completenessNotes(bundle: unknown): CompletenessNote[];
152
166
  * Every reason this pack is not `verified`, in the order a reader should hear
153
167
  * them: what is missing from the whole, then what is missing from the parts.
154
168
  */
155
- export declare function gradeCauses(account: DisclosureAccount, notes: readonly CompletenessNote[], declaration?: DeclarationCheck | null): PackGradeCause[];
169
+ export declare function gradeCauses(account: DisclosureAccount, notes: readonly CompletenessNote[], declaration?: DeclarationCheck | null, withheld?: readonly string[]): PackGradeCause[];
156
170
  /**
157
171
  * The verdict a surface may print, and the counts it may print beside it.
158
172
  *
159
173
  * The invariant, stated so a mutation test can kill it: `verified` is reachable
160
- * ONLY when every record's supplied bytes bound to its own commitment and
161
- * nothing is missing, unreadable, surplus or omitted.
174
+ * ONLY when every record's supplied bytes bound to its own commitment, or
175
+ * (rule version 6) the record is withheld by its declaration, and nothing is
176
+ * missing, unreadable, surplus or omitted.
162
177
  *
163
178
  * Precedence: malformed > failed > incomplete > the kernel's own headline.
164
179
  * A hard verdict wins — "the bytes are absent" is never reported as failure