@mikeargento/bitgraph-audit 0.8.0 → 0.10.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 +8 -2
  2. package/dist/audit.d.ts +9 -6
  3. package/dist/audit.d.ts.map +1 -1
  4. package/dist/audit.js +34 -8
  5. package/dist/audit.js.map +1 -1
  6. package/dist/ceilings.d.ts.map +1 -1
  7. package/dist/ceilings.js +1 -0
  8. package/dist/ceilings.js.map +1 -1
  9. package/dist/cli.js +109 -21
  10. package/dist/cli.js.map +1 -1
  11. package/dist/exports.d.ts +39 -0
  12. package/dist/exports.d.ts.map +1 -0
  13. package/dist/exports.js +374 -0
  14. package/dist/exports.js.map +1 -0
  15. package/dist/floors.d.ts +23 -0
  16. package/dist/floors.d.ts.map +1 -0
  17. package/dist/floors.js +203 -0
  18. package/dist/floors.js.map +1 -0
  19. package/dist/index.d.ts +6 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/ingest.d.ts +2 -0
  24. package/dist/ingest.d.ts.map +1 -1
  25. package/dist/ingest.js +193 -19
  26. package/dist/ingest.js.map +1 -1
  27. package/dist/report-json.d.ts.map +1 -1
  28. package/dist/report-json.js +30 -0
  29. package/dist/report-json.js.map +1 -1
  30. package/dist/report-md.d.ts.map +1 -1
  31. package/dist/report-md.js +315 -25
  32. package/dist/report-md.js.map +1 -1
  33. package/dist/temporal.d.ts +12 -0
  34. package/dist/temporal.d.ts.map +1 -1
  35. package/dist/temporal.js +245 -41
  36. package/dist/temporal.js.map +1 -1
  37. package/dist/types.d.ts +348 -7
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +3 -2
  40. package/src/__tests__/base-floor.test.ts +405 -0
  41. package/src/audit.ts +36 -8
  42. package/src/ceilings.ts +1 -0
  43. package/src/cli.ts +117 -22
  44. package/src/exports.ts +482 -0
  45. package/src/floors.ts +242 -0
  46. package/src/index.ts +20 -0
  47. package/src/ingest.ts +207 -18
  48. package/src/report-json.ts +30 -0
  49. package/src/report-md.ts +353 -29
  50. package/src/temporal.ts +279 -48
  51. package/src/types.ts +341 -6
package/src/report-md.ts CHANGED
@@ -21,12 +21,17 @@
21
21
  * remainder line; the JSON report always carries the complete data.
22
22
  */
23
23
 
24
+ import { exportRunClaims } from "./exports.js";
24
25
  import { buildJsonReport } from "./report-json.js";
25
26
  import type {
26
27
  AnomalyCode,
27
28
  AuditJsonReport,
28
29
  AuditResult,
29
30
  DivergenceRecord,
31
+ ExportCheck,
32
+ ExportClaimRecord,
33
+ ExportCoveredFile,
34
+ ExportRun,
30
35
  ReportAnomaly,
31
36
  ReportPartition,
32
37
  ReportProofRecord,
@@ -355,45 +360,81 @@ function executiveSummary(
355
360
  lines.push("");
356
361
  }
357
362
 
363
+ // Exports (bitgraph-export/1), only when the bundle carries any.
364
+ const ex = report.exports;
365
+ if (ex && ex.checks.length > 0) {
366
+ lines.push("### Exports (bitgraph-export/1)");
367
+ lines.push("");
368
+ lines.push(
369
+ `${withCommas(ex.checks.length)} export ${plural(ex.checks.length, "file was", "files were")} found. ` +
370
+ "An export is one JSON file that, with the file it covers, checks a tree/1 BitGraph on its own: " +
371
+ "the signed proof and its attestation, the tree's root document, the member's path or the owner's " +
372
+ "whole list, and the time evidence. Each was checked with verifyExport from bitgraph-verify, once " +
373
+ "for every file in this bundle it covers (matched by SHA-256 to a leaf's committed bytes or to its " +
374
+ "original), or once without a file when the bundle holds none. A claim the export does not carry, " +
375
+ "or one about a file that is not here, reads NOT_CARRIED and is never a failure. Any FALSE claim " +
376
+ "fails the audit (exit bit 1), as a bad proof does; the attestation claims count, because an export " +
377
+ "is checked as one self-contained object."
378
+ );
379
+ lines.push("");
380
+ for (const e of ex.checks) lines.push(`- ${exportSummaryLine(e)}`);
381
+ lines.push("");
382
+ lines.push(
383
+ "Each export's claims, the files it covers and its three time claims (the floor, the ceiling on " +
384
+ "Base, the ceiling on Ethereum, stated separately and never merged) are in the details below."
385
+ );
386
+ lines.push("");
387
+ }
388
+
358
389
  // External time evidence.
359
390
  lines.push("### External time evidence");
360
391
  lines.push("");
361
392
  const t = s.temporal;
362
- if (t.anchorsIdentified === 0) {
393
+ const bf = t.baseFloors;
394
+ if (t.anchorsIdentified === 0 && bf === undefined) {
363
395
  lines.push(
364
396
  "No Ethereum anchor proofs were observed. The causal order of the " +
365
397
  "proofs stands on its own; this report makes no wall-clock claims."
366
398
  );
367
399
  lines.push("");
368
400
  } else {
369
- lines.push(
370
- `${withCommas(t.anchorsIdentified)} Ethereum anchor ${plural(t.anchorsIdentified, "proof was", "proofs were")} ` +
371
- "identified. An anchor is an ordinary chain member that committed " +
372
- "the fingerprint of a public Ethereum block, giving the chain a " +
373
- "contact point with a public timeline. " +
374
- `${withCommas(t.anchorsWithVerifiedWitness)} ${plural(t.anchorsWithVerifiedWitness, "anchor", "anchors")} ` +
375
- "came with witness material (the block header) that let this tool " +
376
- "reconstruct the block fingerprint locally and confirm it against " +
377
- "the signed anchor, entirely offline."
378
- );
379
- lines.push("");
380
- lines.push(
381
- "A verified anchor bounds time in one direction at a time. Proofs " +
382
- "that come after an anchor in the chain were committed no earlier " +
383
- "than that block's timestamp, because the block's fingerprint " +
384
- "could not have been known before the block existed. That lower " +
385
- "bound additionally assumes the anchored header is a genuine, " +
386
- "publicly published Ethereum block: this offline audit checks the " +
387
- "header's structure and hash binding, not proof-of-work, " +
388
- "consensus, or chain membership, so it cannot confirm the block " +
389
- "is real. Proofs that come before an anchor existed before the " +
390
- "commit that consumed the block; reading that as a wall-clock " +
391
- "ceiling additionally assumes the anchor consumed a recently " +
392
- "published block. Every such bound in this report states its " +
393
- "assumption. No individual proof's exact creation time is ever " +
394
- "stated."
395
- );
396
- lines.push("");
401
+ if (t.anchorsIdentified > 0) {
402
+ lines.push(
403
+ `${withCommas(t.anchorsIdentified)} Ethereum anchor ${plural(t.anchorsIdentified, "proof was", "proofs were")} ` +
404
+ "identified. An anchor is an ordinary chain member that committed " +
405
+ "the fingerprint of a public Ethereum block, giving the chain a " +
406
+ "contact point with a public timeline. " +
407
+ `${withCommas(t.anchorsWithVerifiedWitness)} ${plural(t.anchorsWithVerifiedWitness, "anchor", "anchors")} ` +
408
+ "came with witness material (the block header) that let this tool " +
409
+ "reconstruct the block fingerprint locally and confirm it against " +
410
+ "the signed anchor, entirely offline."
411
+ );
412
+ lines.push("");
413
+ lines.push(
414
+ "A verified anchor bounds time in one direction at a time. Proofs " +
415
+ "that come after an anchor in the chain were committed no earlier " +
416
+ "than that block's timestamp, because the block's fingerprint " +
417
+ "could not have been known before the block existed. That lower " +
418
+ "bound additionally assumes the anchored header is a genuine, " +
419
+ "publicly published Ethereum block: this offline audit checks the " +
420
+ "header's structure and hash binding, not proof-of-work, " +
421
+ "consensus, or chain membership, so it cannot confirm the block " +
422
+ "is real. Proofs that come before an anchor existed before the " +
423
+ "commit that consumed the block; reading that as a wall-clock " +
424
+ "ceiling additionally assumes the anchor consumed a recently " +
425
+ "published block. Every such bound in this report states its " +
426
+ "assumption. No individual proof's exact creation time is ever " +
427
+ "stated."
428
+ );
429
+ lines.push("");
430
+ } else {
431
+ lines.push(
432
+ "No Ethereum anchor proofs were observed. After the cutover to Base floors (enclave v10) there " +
433
+ "are none: order after a record is the chain of proof hashes."
434
+ );
435
+ lines.push("");
436
+ }
437
+ if (bf !== undefined) baseFloorSummary(lines, report);
397
438
  lines.push(
398
439
  ...table(
399
440
  ["Temporal coverage", "Segments"],
@@ -409,6 +450,50 @@ function executiveSummary(
409
450
  }
410
451
  }
411
452
 
453
+ /** The executive summary's paragraph on Base floors (enclave v10). Only written when a proof signs one or a floor problem was found. */
454
+ function baseFloorSummary(lines: string[], report: AuditJsonReport): void {
455
+ const bf = report.summary.temporal.baseFloors;
456
+ if (bf === undefined) return;
457
+ if (bf.signed > 0) {
458
+ const rest = bf.signed - bf.headersChecked;
459
+ lines.push(
460
+ `${withCommas(bf.signed)} ${plural(bf.signed, "proof signs", "proofs sign")} a Base block as ` +
461
+ `${plural(bf.signed, "its", "their")} floor (commit.slotFloor, enclave v10). A proof that signs a floor was ` +
462
+ "committed no earlier than that block's time: the block's hash could not exist before the block, and the " +
463
+ "proof signs it. Proofs after it in the chain carry the same bound. A floor is a lower bound only: it " +
464
+ "bounds nothing from above, and it orders no epoch. " +
465
+ `${withCommas(bf.headersChecked)} of ${withCommas(bf.signed)} came with the block's header, checked against ` +
466
+ "the signed hash, number and time" +
467
+ (rest > 0
468
+ ? `; for the other ${withCommas(rest)} the time is the one the proof signs, on Base mainnet's schedule for ` +
469
+ "that block, and confirming the block needs a Base lookup. "
470
+ : ". ") +
471
+ "Like an anchored block, a floor additionally assumes the block is a genuine, publicly published Base " +
472
+ "block, which this offline audit cannot confirm." +
473
+ (bf.withheld > 0
474
+ ? ` ${withCommas(bf.withheld)} ${plural(bf.withheld, "floor is", "floors are")} not used as a bound; ` +
475
+ "the reasons are in the temporal bounds section."
476
+ : "")
477
+ );
478
+ lines.push("");
479
+ }
480
+ const problems = report.temporal.floorProblems ?? [];
481
+ if (problems.length > 0) {
482
+ lines.push(
483
+ `${withCommas(problems.length)} floor ${plural(problems.length, "problem was", "problems were")} found ` +
484
+ "(exit bit 2). A floor with a problem bounds nothing:"
485
+ );
486
+ lines.push("");
487
+ for (const p of problems.slice(0, MAX_TABLE_ROWS)) {
488
+ lines.push(
489
+ `- ${inlineCode(p.code)}${p.path !== undefined ? ` ${inlineCode(p.path)}` : ""}` +
490
+ `${p.proofHash !== undefined ? `, proof ${inlineCode(p.proofHash)}` : ""}: ${p.message}`
491
+ );
492
+ }
493
+ lines.push("");
494
+ }
495
+ }
496
+
412
497
  /** Plain sentences describing one partition's chain state, in the brief's style. */
413
498
  function partitionSummarySentences(report: AuditJsonReport, partition: ReportPartition): string[] {
414
499
  const out: string[] = [];
@@ -525,6 +610,7 @@ function detailSections(
525
610
  anchorDetails(lines, report);
526
611
  witnessDetails(lines, report);
527
612
  temporalDetails(lines, report);
613
+ exportDetails(lines, report);
528
614
  attestationDetails(lines, report);
529
615
  unsupportedVersionDetails(lines, report);
530
616
  manifestDetails(lines, report);
@@ -809,6 +895,22 @@ function temporalDetails(lines: string[], report: AuditJsonReport): void {
809
895
  }
810
896
  lines.push("");
811
897
  }
898
+ const floors = report.temporal.signedFloors ?? [];
899
+ if (floors.length > 0) {
900
+ lines.push("Base floors the proofs sign (enclave v10), each a not-before for its proof and never an upper bound:");
901
+ lines.push("");
902
+ for (const f of floors.slice(0, MAX_TABLE_ROWS)) {
903
+ lines.push(
904
+ `- Base block ${withCommas(f.blockNumber)} (${formatTimestamp(f.blockTimestamp)}, hash ${inlineCode(f.blockHash)}), ` +
905
+ `signed by ${inlineCode(f.proofHash)}: ` +
906
+ (f.header === "checked"
907
+ ? `header checked (${inlineCode(f.headerPath ?? "")})`
908
+ : "no header in the bundle; confirming the block needs a Base lookup") +
909
+ (f.bound === "not-before" ? "; bounds the proof not before that time." : `; not used as a bound: ${f.withheldReason ?? "withheld"}.`)
910
+ );
911
+ }
912
+ lines.push("");
913
+ }
812
914
  if (report.epochRelationships.anchorOrderedPairs.length > 0) {
813
915
  lines.push("Anchor-derived epoch ordering (assumption-dependent, covered portions only):");
814
916
  lines.push("");
@@ -827,6 +929,13 @@ function temporalDetails(lines: string[], report: AuditJsonReport): void {
827
929
  }
828
930
 
829
931
  function boundLine(bound: SegmentBound): string {
932
+ if (bound.source === "signed-floor") {
933
+ return (
934
+ `Committed no earlier than ${formatTimestamp(bound.timestamp)} ` +
935
+ `(Base block ${bound.blockNumber ?? "?"}, the floor signed by ${inlineCode(bound.anchorProofHash)}, ` +
936
+ `evidence: ${bound.evidence}${bound.weaker ? ", weaker" : ""}). ${bound.claim}`
937
+ );
938
+ }
830
939
  const direction =
831
940
  bound.kind === "not-before"
832
941
  ? "Committed no earlier than"
@@ -838,6 +947,215 @@ function boundLine(bound: SegmentBound): string {
838
947
  );
839
948
  }
840
949
 
950
+ // ---------------------------------------------------------------------------
951
+ // Exports (bitgraph-export/1)
952
+ // ---------------------------------------------------------------------------
953
+
954
+ /** One summary line for an export: its verdict, path, kind and covered files. */
955
+ function exportSummaryLine(e: ExportCheck): string {
956
+ if (e.status === "malformed") {
957
+ // It says bitgraph-export/1 and is not one: a failed check, not an unknown format.
958
+ return `FALSE ${inlineCode(e.path)}: malformed, ${e.reason ?? e.status}. Fails the audit (exit bit 1).`;
959
+ }
960
+ if (e.status !== "checked") {
961
+ return `NOT CHECKED ${inlineCode(e.path)}: ${e.reason ?? e.status}. Fails the audit (exit bit 1).`;
962
+ }
963
+ const withFile = e.runs.filter((r) => r.file !== null).length;
964
+ const covered =
965
+ e.files.length === 0
966
+ ? "no covered file in this bundle (checked without one)"
967
+ : `${withCommas(e.files.length)} covered ${plural(e.files.length, "file", "files")} in this bundle` +
968
+ (withFile < e.files.length ? `, ${withCommas(withFile)} checked one by one` : "");
969
+ const failed = e.failedClaims.length > 0 ? ` FALSE claims: ${e.failedClaims.map(inlineCode).join(", ")}.` : "";
970
+ return `${e.verdict} ${inlineCode(e.path)}: ${exportKindPhrase(e)}; ${covered}.${failed}`;
971
+ }
972
+
973
+ function exportKindPhrase(e: ExportCheck): string {
974
+ const count = exportLeafCount(e);
975
+ const tree = count !== null ? ` of a tree of ${withCommas(count)} ${plural(count, "leaf", "leaves")}` : "";
976
+ if (e.kind === "member") return `member export${tree}`;
977
+ if (e.kind === "owner") return `the owner's export (the whole list)${tree}`;
978
+ return `an export with neither member evidence nor the owner's list${tree}`;
979
+ }
980
+
981
+ /** The tree size the export's runs established (from the member, else from tree.root's detail). */
982
+ function exportLeafCount(e: ExportCheck): number | null {
983
+ for (const r of e.runs) if (r.member !== null) return r.member.count;
984
+ for (const c of [...e.claims, ...e.runs.flatMap((r) => r.claims)]) {
985
+ const m = c.id === "tree.root" && c.result === "TRUE" ? /a tree of (\d+) leaves/.exec(c.detail) : null;
986
+ if (m) return Number(m[1]);
987
+ }
988
+ return null;
989
+ }
990
+
991
+ function exportFileLabel(f: ExportCoveredFile): string {
992
+ return f.paths.map(inlineCode).join(", ");
993
+ }
994
+
995
+ function exportDetails(lines: string[], report: AuditJsonReport): void {
996
+ const ex = report.exports;
997
+ if (ex === undefined || ex.checks.length === 0) return;
998
+ lines.push("### Exports (bitgraph-export/1)");
999
+ lines.push("");
1000
+ lines.push(
1001
+ "Each export below was checked with verifyExport from bitgraph-verify. Every claim is TRUE, FALSE, " +
1002
+ "UNDETERMINED or NOT_CARRIED, with what it rests on, exactly as the verifier states it. Offline claims " +
1003
+ "take block headers as the ones matching their hashes; whether each block is its chain's own is a " +
1004
+ "confirmed claim, which this audit never looks up (it makes no network call), so those read " +
1005
+ "UNDETERMINED (not checked). The proof inside an export is also in the proof analysis above under its " +
1006
+ "canonical hash, where it counts as observed without artifact bytes: its artifact is the tree's " +
1007
+ "84-byte root document, which travels inside the export and is checked here (claim tree.root)."
1008
+ );
1009
+ lines.push("");
1010
+ for (const e of ex.checks) exportDetail(lines, e);
1011
+ }
1012
+
1013
+ function exportDetail(lines: string[], e: ExportCheck): void {
1014
+ lines.push(`#### Export ${inlineCode(e.path)}: ${e.status === "checked" ? e.verdict : e.status === "malformed" ? "FALSE (malformed)" : "NOT CHECKED"}`);
1015
+ lines.push("");
1016
+ if (e.status !== "checked") {
1017
+ lines.push(
1018
+ `Rejected at ingest: ${e.reason ?? e.status}. The file is not checked and is never treated as an ` +
1019
+ "artifact; it fails the audit (exit bit 1), as an unsupported proof version does."
1020
+ );
1021
+ lines.push("");
1022
+ return;
1023
+ }
1024
+ const facts: string[] = [`${exportKindPhrase(e).replace(/^./, (c) => c.toUpperCase())}.`];
1025
+ if (e.proofHash !== undefined) facts.push(`Proof ${inlineCode(e.proofHash)}.`);
1026
+ if (e.failedClaims.length > 0) facts.push(`FALSE claims: ${e.failedClaims.map(inlineCode).join(", ")}.`);
1027
+ lines.push(facts.join(" "));
1028
+ lines.push("");
1029
+
1030
+ // The files it covers.
1031
+ if (e.files.length === 0) {
1032
+ lines.push(
1033
+ "No file in this bundle is covered by this export, so it was checked once without a file: the claims " +
1034
+ "about the file read NOT_CARRIED, which is not a failure."
1035
+ );
1036
+ lines.push("");
1037
+ } else {
1038
+ lines.push("Covered files in this bundle (matched by SHA-256):");
1039
+ lines.push("");
1040
+ lines.push(
1041
+ ...table(
1042
+ ["File", "SHA-256", "Leaf", "Placement", "Matched as", "Name in the export (unsigned)"],
1043
+ e.files.slice(0, MAX_TABLE_ROWS).map((f) => [
1044
+ exportFileLabel(f),
1045
+ inlineCode(f.sha256Hex),
1046
+ f.leafIndex !== null ? String(f.leafIndex) : "",
1047
+ f.placement ?? "",
1048
+ f.matchedAs === "committed-bytes" ? "committed bytes" : f.matchedAs === "original" ? "original" : "as is",
1049
+ f.nameInExport ?? "",
1050
+ ])
1051
+ )
1052
+ );
1053
+ if (e.files.length > MAX_TABLE_ROWS) lines.push("", `${withCommas(e.files.length - MAX_TABLE_ROWS)} more in the JSON report.`);
1054
+ lines.push("");
1055
+ if (e.runMode === "member-from-list") {
1056
+ lines.push(
1057
+ "The owner's list was checked once (claim tree.leaves); each file's member evidence was then derived " +
1058
+ "from it exactly as verifyExport derives it, and verifyExport ran once per file with that evidence."
1059
+ );
1060
+ lines.push("");
1061
+ }
1062
+ }
1063
+ for (const u of e.unchecked ?? []) lines.push(`- ${exportFileLabel(u.file)}: ${u.reason}.`);
1064
+ if ((e.unchecked ?? []).length > 0) lines.push("");
1065
+
1066
+ // The three time claims, each on its own evidence, never merged.
1067
+ const firstClaims = exportRunClaims(e, e.runs[0]!);
1068
+ const why = (id: string): string => {
1069
+ const c = firstClaims.find((x) => x.id === id);
1070
+ return c ? `${inlineCode(id)} ${c.result}: ${c.detail}` : `${inlineCode(id)} was not reached`;
1071
+ };
1072
+ // Whether a block is its chain's own: the confirmed claim, which only an embedder's lookup answers.
1073
+ const confirmedNote = (id: string, chain: string): string => {
1074
+ const c = firstClaims.find((x) => x.id === id);
1075
+ if (c === undefined || c.result === "UNDETERMINED") return ` Whether the block is ${chain}'s own is a confirmed claim, not looked up here.`;
1076
+ return c.result === "TRUE"
1077
+ ? ` The caller's own lookup confirms the block is ${chain}'s (${inlineCode(id)} TRUE).`
1078
+ : ` The caller's own lookup says the block is NOT ${chain}'s (${inlineCode(id)} ${c.result}: ${c.detail}).`;
1079
+ };
1080
+ lines.push("Time claims, each on its own evidence and never merged:");
1081
+ lines.push("");
1082
+ const t = e.times;
1083
+ lines.push(
1084
+ t.floor !== null
1085
+ ? `- Floor: ${t.floor.chain === "base" ? "Base" : "Ethereum"} block ${withCommas(t.floor.blockNumber)}, ` +
1086
+ `${t.floor.chain === "base" ? "stamped" : "mined"} at ${formatTimestamp(t.floor.blockTimestamp)}, ` +
1087
+ `hash ${inlineCode(t.floor.blockHash)}: the proof's signed floor block, its header checked by hash. ` +
1088
+ "Every file in the tree was recorded after it (the record floor). Committed bytes that carry this position's " +
1089
+ "commitment were also finished after it (the content floor); an original inside them, and a file recorded as is, " +
1090
+ "are not dated by it. What the floor covers for each file is stated per run below." +
1091
+ confirmedNote("confirmed.floor", t.floor.chain === "base" ? "Base" : "Ethereum")
1092
+ : `- Floor: not established (${why("floor.header")}).`
1093
+ );
1094
+ lines.push(
1095
+ t.ceilingBase !== null
1096
+ ? `- Ceiling on Base: the record existed by Base block ${withCommas(t.ceilingBase.blockNumber)}, at ` +
1097
+ `${formatTimestamp(t.ceilingBase.blockTimestamp)}, hash ${inlineCode(t.ceilingBase.blockHash)}. ` +
1098
+ (!t.ceilingBase.provisional
1099
+ ? "The caller's own lookup confirmed the block against Base, so the time is no longer provisional."
1100
+ : firstClaims.some((x) => x.id === "confirmed.ceiling.base" && x.result === "FALSE")
1101
+ ? `PROVISIONAL, and the caller's own lookup says the block is NOT Base's (${inlineCode("confirmed.ceiling.base")} FALSE).`
1102
+ : "PROVISIONAL: this time holds once the block is checked against Base, which this offline audit does not do.")
1103
+ : `- Ceiling on Base: not established (${why("ceiling.base")}).`
1104
+ );
1105
+ lines.push(
1106
+ t.ceilingEthereum !== null
1107
+ ? `- Ceiling on Ethereum: the record existed by Ethereum block ${withCommas(t.ceilingEthereum.blockNumber)}, at ` +
1108
+ `${formatTimestamp(t.ceilingEthereum.blockTimestamp)}, hash ${inlineCode(t.ceilingEthereum.blockHash)}, ` +
1109
+ "through Base's output root, whatever Base's claim turns out to be." +
1110
+ confirmedNote("confirmed.ceiling.ethereum", "Ethereum")
1111
+ : `- Ceiling on Ethereum: not established (${why("ceiling.ethereum")}).`
1112
+ );
1113
+ lines.push("");
1114
+
1115
+ // Claims: the ones every run states alike once, then each run's own (the ones about its file).
1116
+ lines.push(e.runs.length > 1 ? "Claims, the same in every run:" : "Claims:");
1117
+ lines.push("");
1118
+ lines.push(...table(["Claim", "Result", "Level", "Rests on", "Detail"], e.claims.map((c) => claimRow(c))));
1119
+ lines.push("");
1120
+ const perFile: string[][] = [];
1121
+ let perFileTotal = 0;
1122
+ for (const r of e.runs) {
1123
+ for (const c of r.claims) {
1124
+ perFileTotal++;
1125
+ if (perFile.length < MAX_TABLE_ROWS) perFile.push([r.file !== null ? exportFileLabel(r.file) : "(no file)", ...claimRow(c)]);
1126
+ }
1127
+ }
1128
+ if (perFileTotal > 0) {
1129
+ lines.push("Claims that differ by file:");
1130
+ lines.push("");
1131
+ lines.push(...table(["File", "Claim", "Result", "Level", "Rests on", "Detail"], perFile));
1132
+ if (perFileTotal > MAX_TABLE_ROWS) lines.push("", `${withCommas(perFileTotal - MAX_TABLE_ROWS)} more rows in the JSON report.`);
1133
+ lines.push("");
1134
+ }
1135
+
1136
+ // Per run: the verdict, what the floor covers, and the verifier's own reading.
1137
+ lines.push(e.runs.length > 1 ? "Runs:" : "Run:");
1138
+ lines.push("");
1139
+ for (const r of e.runs.slice(0, MAX_TABLE_ROWS)) lines.push(`- ${exportRunLine(r)}`);
1140
+ if (e.runs.length > MAX_TABLE_ROWS) lines.push(`- ${withCommas(e.runs.length - MAX_TABLE_ROWS)} more runs in the JSON report.`);
1141
+ lines.push("");
1142
+ }
1143
+
1144
+ function claimRow(c: ExportClaimRecord): string[] {
1145
+ return [inlineCode(c.id), c.result, c.level, c.restsOn, c.detail];
1146
+ }
1147
+
1148
+ function exportRunLine(r: ExportRun): string {
1149
+ const who = r.file !== null ? exportFileLabel(r.file) : "Without a file";
1150
+ const floor =
1151
+ r.floorCovers === "content"
1152
+ ? " Recorded after the floor block, and its committed bytes were finished after it; an original inside them is not dated by it."
1153
+ : r.floorCovers === "record"
1154
+ ? " Recorded as is: recorded after the floor block; the bytes themselves are not dated."
1155
+ : "";
1156
+ return `${who}: ${r.verdict}.${floor} Reading: ${r.reading.length > 0 ? r.reading : "(none)"}`;
1157
+ }
1158
+
841
1159
  function attestationDetails(lines: string[], report: AuditJsonReport): void {
842
1160
  lines.push("### Attestation breakdown");
843
1161
  lines.push("");
@@ -1048,6 +1366,12 @@ function codeMeaning(code: AnomalyCode): string {
1048
1366
  return "A validated attestation document's measurement does not equal the measurement the proof declares.";
1049
1367
  case "attestation-user-data-mismatch":
1050
1368
  return "A validated attestation document is not bound to the specific proof that carries it.";
1369
+ case "export-malformed":
1370
+ return "A file declares the bitgraph-export/1 format but lacks an export's structure (a proof and the tree's root document). It was not checked, and it fails the audit.";
1371
+ case "export-unsupported-format":
1372
+ return "A file declares a bitgraph-export format this audit does not support (only bitgraph-export/1). It was not checked, and it fails the audit.";
1373
+ case "export-too-large":
1374
+ return "A file opens an export but is larger than this audit reads for one export. It was not checked, and it fails the audit.";
1051
1375
  default:
1052
1376
  return "See the anomaly details section.";
1053
1377
  }