@mikeargento/bitgraph-audit 0.8.0 → 0.9.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/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,6 +360,32 @@ 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("");
@@ -525,6 +556,7 @@ function detailSections(
525
556
  anchorDetails(lines, report);
526
557
  witnessDetails(lines, report);
527
558
  temporalDetails(lines, report);
559
+ exportDetails(lines, report);
528
560
  attestationDetails(lines, report);
529
561
  unsupportedVersionDetails(lines, report);
530
562
  manifestDetails(lines, report);
@@ -838,6 +870,214 @@ function boundLine(bound: SegmentBound): string {
838
870
  );
839
871
  }
840
872
 
873
+ // ---------------------------------------------------------------------------
874
+ // Exports (bitgraph-export/1)
875
+ // ---------------------------------------------------------------------------
876
+
877
+ /** One summary line for an export: its verdict, path, kind and covered files. */
878
+ function exportSummaryLine(e: ExportCheck): string {
879
+ if (e.status === "malformed") {
880
+ // It says bitgraph-export/1 and is not one: a failed check, not an unknown format.
881
+ return `FALSE ${inlineCode(e.path)}: malformed, ${e.reason ?? e.status}. Fails the audit (exit bit 1).`;
882
+ }
883
+ if (e.status !== "checked") {
884
+ return `NOT CHECKED ${inlineCode(e.path)}: ${e.reason ?? e.status}. Fails the audit (exit bit 1).`;
885
+ }
886
+ const withFile = e.runs.filter((r) => r.file !== null).length;
887
+ const covered =
888
+ e.files.length === 0
889
+ ? "no covered file in this bundle (checked without one)"
890
+ : `${withCommas(e.files.length)} covered ${plural(e.files.length, "file", "files")} in this bundle` +
891
+ (withFile < e.files.length ? `, ${withCommas(withFile)} checked one by one` : "");
892
+ const failed = e.failedClaims.length > 0 ? ` FALSE claims: ${e.failedClaims.map(inlineCode).join(", ")}.` : "";
893
+ return `${e.verdict} ${inlineCode(e.path)}: ${exportKindPhrase(e)}; ${covered}.${failed}`;
894
+ }
895
+
896
+ function exportKindPhrase(e: ExportCheck): string {
897
+ const count = exportLeafCount(e);
898
+ const tree = count !== null ? ` of a tree of ${withCommas(count)} ${plural(count, "leaf", "leaves")}` : "";
899
+ if (e.kind === "member") return `member export${tree}`;
900
+ if (e.kind === "owner") return `the owner's export (the whole list)${tree}`;
901
+ return `an export with neither member evidence nor the owner's list${tree}`;
902
+ }
903
+
904
+ /** The tree size the export's runs established (from the member, else from tree.root's detail). */
905
+ function exportLeafCount(e: ExportCheck): number | null {
906
+ for (const r of e.runs) if (r.member !== null) return r.member.count;
907
+ for (const c of [...e.claims, ...e.runs.flatMap((r) => r.claims)]) {
908
+ const m = c.id === "tree.root" && c.result === "TRUE" ? /a tree of (\d+) leaves/.exec(c.detail) : null;
909
+ if (m) return Number(m[1]);
910
+ }
911
+ return null;
912
+ }
913
+
914
+ function exportFileLabel(f: ExportCoveredFile): string {
915
+ return f.paths.map(inlineCode).join(", ");
916
+ }
917
+
918
+ function exportDetails(lines: string[], report: AuditJsonReport): void {
919
+ const ex = report.exports;
920
+ if (ex === undefined || ex.checks.length === 0) return;
921
+ lines.push("### Exports (bitgraph-export/1)");
922
+ lines.push("");
923
+ lines.push(
924
+ "Each export below was checked with verifyExport from bitgraph-verify. Every claim is TRUE, FALSE, " +
925
+ "UNDETERMINED or NOT_CARRIED, with what it rests on, exactly as the verifier states it. Offline claims " +
926
+ "take block headers as the ones matching their hashes; whether each block is its chain's own is a " +
927
+ "confirmed claim, which this audit never looks up (it makes no network call), so those read " +
928
+ "UNDETERMINED (not checked). The proof inside an export is also in the proof analysis above under its " +
929
+ "canonical hash, where it counts as observed without artifact bytes: its artifact is the tree's " +
930
+ "84-byte root document, which travels inside the export and is checked here (claim tree.root)."
931
+ );
932
+ lines.push("");
933
+ for (const e of ex.checks) exportDetail(lines, e);
934
+ }
935
+
936
+ function exportDetail(lines: string[], e: ExportCheck): void {
937
+ lines.push(`#### Export ${inlineCode(e.path)}: ${e.status === "checked" ? e.verdict : e.status === "malformed" ? "FALSE (malformed)" : "NOT CHECKED"}`);
938
+ lines.push("");
939
+ if (e.status !== "checked") {
940
+ lines.push(
941
+ `Rejected at ingest: ${e.reason ?? e.status}. The file is not checked and is never treated as an ` +
942
+ "artifact; it fails the audit (exit bit 1), as an unsupported proof version does."
943
+ );
944
+ lines.push("");
945
+ return;
946
+ }
947
+ const facts: string[] = [`${exportKindPhrase(e).replace(/^./, (c) => c.toUpperCase())}.`];
948
+ if (e.proofHash !== undefined) facts.push(`Proof ${inlineCode(e.proofHash)}.`);
949
+ if (e.failedClaims.length > 0) facts.push(`FALSE claims: ${e.failedClaims.map(inlineCode).join(", ")}.`);
950
+ lines.push(facts.join(" "));
951
+ lines.push("");
952
+
953
+ // The files it covers.
954
+ if (e.files.length === 0) {
955
+ lines.push(
956
+ "No file in this bundle is covered by this export, so it was checked once without a file: the claims " +
957
+ "about the file read NOT_CARRIED, which is not a failure."
958
+ );
959
+ lines.push("");
960
+ } else {
961
+ lines.push("Covered files in this bundle (matched by SHA-256):");
962
+ lines.push("");
963
+ lines.push(
964
+ ...table(
965
+ ["File", "SHA-256", "Leaf", "Placement", "Matched as", "Name in the export (unsigned)"],
966
+ e.files.slice(0, MAX_TABLE_ROWS).map((f) => [
967
+ exportFileLabel(f),
968
+ inlineCode(f.sha256Hex),
969
+ f.leafIndex !== null ? String(f.leafIndex) : "",
970
+ f.placement ?? "",
971
+ f.matchedAs === "committed-bytes" ? "committed bytes" : f.matchedAs === "original" ? "original" : "as is",
972
+ f.nameInExport ?? "",
973
+ ])
974
+ )
975
+ );
976
+ if (e.files.length > MAX_TABLE_ROWS) lines.push("", `${withCommas(e.files.length - MAX_TABLE_ROWS)} more in the JSON report.`);
977
+ lines.push("");
978
+ if (e.runMode === "member-from-list") {
979
+ lines.push(
980
+ "The owner's list was checked once (claim tree.leaves); each file's member evidence was then derived " +
981
+ "from it exactly as verifyExport derives it, and verifyExport ran once per file with that evidence."
982
+ );
983
+ lines.push("");
984
+ }
985
+ }
986
+ for (const u of e.unchecked ?? []) lines.push(`- ${exportFileLabel(u.file)}: ${u.reason}.`);
987
+ if ((e.unchecked ?? []).length > 0) lines.push("");
988
+
989
+ // The three time claims, each on its own evidence, never merged.
990
+ const firstClaims = exportRunClaims(e, e.runs[0]!);
991
+ const why = (id: string): string => {
992
+ const c = firstClaims.find((x) => x.id === id);
993
+ return c ? `${inlineCode(id)} ${c.result}: ${c.detail}` : `${inlineCode(id)} was not reached`;
994
+ };
995
+ // Whether a block is its chain's own: the confirmed claim, which only an embedder's lookup answers.
996
+ const confirmedNote = (id: string, chain: string): string => {
997
+ const c = firstClaims.find((x) => x.id === id);
998
+ if (c === undefined || c.result === "UNDETERMINED") return ` Whether the block is ${chain}'s own is a confirmed claim, not looked up here.`;
999
+ return c.result === "TRUE"
1000
+ ? ` The caller's own lookup confirms the block is ${chain}'s (${inlineCode(id)} TRUE).`
1001
+ : ` The caller's own lookup says the block is NOT ${chain}'s (${inlineCode(id)} ${c.result}: ${c.detail}).`;
1002
+ };
1003
+ lines.push("Time claims, each on its own evidence and never merged:");
1004
+ lines.push("");
1005
+ const t = e.times;
1006
+ lines.push(
1007
+ t.floor !== null
1008
+ ? `- Floor: Ethereum block ${withCommas(t.floor.blockNumber)}, mined at ${formatTimestamp(t.floor.blockTimestamp)}, ` +
1009
+ `hash ${inlineCode(t.floor.blockHash)}: the proof's signed floor block, its header checked by hash. ` +
1010
+ "Every file in the tree was recorded after it (the record floor). Committed bytes that carry this position's " +
1011
+ "commitment were also finished after it (the content floor); an original inside them, and a file recorded as is, " +
1012
+ "are not dated by it. What the floor covers for each file is stated per run below." +
1013
+ confirmedNote("confirmed.floor", "Ethereum")
1014
+ : `- Floor: not established (${why("floor.header")}).`
1015
+ );
1016
+ lines.push(
1017
+ t.ceilingBase !== null
1018
+ ? `- Ceiling on Base: the record existed by Base block ${withCommas(t.ceilingBase.blockNumber)}, at ` +
1019
+ `${formatTimestamp(t.ceilingBase.blockTimestamp)}, hash ${inlineCode(t.ceilingBase.blockHash)}. ` +
1020
+ (!t.ceilingBase.provisional
1021
+ ? "The caller's own lookup confirmed the block against Base, so the time is no longer provisional."
1022
+ : firstClaims.some((x) => x.id === "confirmed.ceiling.base" && x.result === "FALSE")
1023
+ ? `PROVISIONAL, and the caller's own lookup says the block is NOT Base's (${inlineCode("confirmed.ceiling.base")} FALSE).`
1024
+ : "PROVISIONAL: this time holds once the block is checked against Base, which this offline audit does not do.")
1025
+ : `- Ceiling on Base: not established (${why("ceiling.base")}).`
1026
+ );
1027
+ lines.push(
1028
+ t.ceilingEthereum !== null
1029
+ ? `- Ceiling on Ethereum: the record existed by Ethereum block ${withCommas(t.ceilingEthereum.blockNumber)}, at ` +
1030
+ `${formatTimestamp(t.ceilingEthereum.blockTimestamp)}, hash ${inlineCode(t.ceilingEthereum.blockHash)}, ` +
1031
+ "through Base's output root, whatever Base's claim turns out to be." +
1032
+ confirmedNote("confirmed.ceiling.ethereum", "Ethereum")
1033
+ : `- Ceiling on Ethereum: not established (${why("ceiling.ethereum")}).`
1034
+ );
1035
+ lines.push("");
1036
+
1037
+ // Claims: the ones every run states alike once, then each run's own (the ones about its file).
1038
+ lines.push(e.runs.length > 1 ? "Claims, the same in every run:" : "Claims:");
1039
+ lines.push("");
1040
+ lines.push(...table(["Claim", "Result", "Level", "Rests on", "Detail"], e.claims.map((c) => claimRow(c))));
1041
+ lines.push("");
1042
+ const perFile: string[][] = [];
1043
+ let perFileTotal = 0;
1044
+ for (const r of e.runs) {
1045
+ for (const c of r.claims) {
1046
+ perFileTotal++;
1047
+ if (perFile.length < MAX_TABLE_ROWS) perFile.push([r.file !== null ? exportFileLabel(r.file) : "(no file)", ...claimRow(c)]);
1048
+ }
1049
+ }
1050
+ if (perFileTotal > 0) {
1051
+ lines.push("Claims that differ by file:");
1052
+ lines.push("");
1053
+ lines.push(...table(["File", "Claim", "Result", "Level", "Rests on", "Detail"], perFile));
1054
+ if (perFileTotal > MAX_TABLE_ROWS) lines.push("", `${withCommas(perFileTotal - MAX_TABLE_ROWS)} more rows in the JSON report.`);
1055
+ lines.push("");
1056
+ }
1057
+
1058
+ // Per run: the verdict, what the floor covers, and the verifier's own reading.
1059
+ lines.push(e.runs.length > 1 ? "Runs:" : "Run:");
1060
+ lines.push("");
1061
+ for (const r of e.runs.slice(0, MAX_TABLE_ROWS)) lines.push(`- ${exportRunLine(r)}`);
1062
+ if (e.runs.length > MAX_TABLE_ROWS) lines.push(`- ${withCommas(e.runs.length - MAX_TABLE_ROWS)} more runs in the JSON report.`);
1063
+ lines.push("");
1064
+ }
1065
+
1066
+ function claimRow(c: ExportClaimRecord): string[] {
1067
+ return [inlineCode(c.id), c.result, c.level, c.restsOn, c.detail];
1068
+ }
1069
+
1070
+ function exportRunLine(r: ExportRun): string {
1071
+ const who = r.file !== null ? exportFileLabel(r.file) : "Without a file";
1072
+ const floor =
1073
+ r.floorCovers === "content"
1074
+ ? " Recorded after the floor block, and its committed bytes were finished after it; an original inside them is not dated by it."
1075
+ : r.floorCovers === "record"
1076
+ ? " Recorded as is: recorded after the floor block; the bytes themselves are not dated."
1077
+ : "";
1078
+ return `${who}: ${r.verdict}.${floor} Reading: ${r.reading.length > 0 ? r.reading : "(none)"}`;
1079
+ }
1080
+
841
1081
  function attestationDetails(lines: string[], report: AuditJsonReport): void {
842
1082
  lines.push("### Attestation breakdown");
843
1083
  lines.push("");
@@ -1048,6 +1288,12 @@ function codeMeaning(code: AnomalyCode): string {
1048
1288
  return "A validated attestation document's measurement does not equal the measurement the proof declares.";
1049
1289
  case "attestation-user-data-mismatch":
1050
1290
  return "A validated attestation document is not bound to the specific proof that carries it.";
1291
+ case "export-malformed":
1292
+ 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.";
1293
+ case "export-unsupported-format":
1294
+ 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.";
1295
+ case "export-too-large":
1296
+ 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
1297
  default:
1052
1298
  return "See the anomaly details section.";
1053
1299
  }
package/src/types.ts CHANGED
@@ -123,6 +123,13 @@ export type AnomalyCode =
123
123
  | "attestation-measurement-mismatch"
124
124
  /** A validated attestation document's user_data is not bound to this proof's canonical proof hash. */
125
125
  | "attestation-user-data-mismatch"
126
+ // --- Exports (bitgraph-export/1) ---
127
+ /** A file declares format "bitgraph-export/1" but lacks an export's structure (a proof object and tree.rootDocument). Not checked; fails the audit (exit bit 1). */
128
+ | "export-malformed"
129
+ /** A file declares a bitgraph-export format other than "bitgraph-export/1". Not checked; fails the audit (exit bit 1), as an unsupported proof version does. */
130
+ | "export-unsupported-format"
131
+ /** A file opens an export object but is larger than the audit's cap for one export's JSON (IngestLimits.maxExportJsonBytes). Not checked; fails the audit (exit bit 1). */
132
+ | "export-too-large"
126
133
  | (string & {});
127
134
 
128
135
  /**
@@ -408,6 +415,8 @@ export interface IngestCounts {
408
415
  witnesses: number;
409
416
  /** Ceiling files (bitgraph-ceiling/1) and ceiling status notes (bitgraph-ceiling-status/1). */
410
417
  ceilings?: number;
418
+ /** Export-shaped files (format "bitgraph-export/..."), checked or rejected. */
419
+ exports?: number;
411
420
  /** Container entries skipped for unsafe paths. */
412
421
  skippedUnsafePaths: number;
413
422
  }
@@ -437,6 +446,14 @@ export interface IngestLimits {
437
446
  * declaring a larger size aborts ingest before any allocation.
438
447
  */
439
448
  maxMetadataEntryBytes: number;
449
+ /**
450
+ * Ceiling on one export's JSON (bitgraph-export/1), for every container,
451
+ * directories included. Other JSON candidates stop at 8 MiB; an entry that
452
+ * opens an export object may run to this size (the owner's export of a
453
+ * large tree lists every leaf). A larger export is reported
454
+ * (export-too-large) and not checked. Default 192 MiB.
455
+ */
456
+ maxExportJsonBytes?: number;
440
457
  }
441
458
 
442
459
  /**
@@ -473,6 +490,11 @@ export interface IngestResult {
473
490
  ceilings?: CeilingFile[];
474
491
  /** Ceiling status notes (bitgraph-ceiling-status/1): a package saying why a ceiling is absent. */
475
492
  ceilingStatuses?: CeilingFile[];
493
+ /**
494
+ * Export-shaped files (format "bitgraph-export/..."), in observation order.
495
+ * Evidence, never artifacts. Optional so older embedders' IngestResults still type.
496
+ */
497
+ exports?: ExportFile[];
476
498
  /** Present when a root manifest.json entry existed. */
477
499
  manifest?: ManifestReport;
478
500
 
@@ -1314,6 +1336,8 @@ export interface AuditOptions {
1314
1336
  trustedRootCaDer?: Uint8Array;
1315
1337
  /** Ceilings in time: the declared writer, the chain, and an optional Base RPC for the one online check. */
1316
1338
  ceilings?: import("./ceilings.js").CeilingAuditOptions;
1339
+ /** Exports (bitgraph-export/1): extra spec hashes, PCR0 pins, and optional lookups for embedders that allow network. */
1340
+ exports?: import("./exports.js").ExportAuditOptions;
1317
1341
  }
1318
1342
 
1319
1343
  /**
@@ -1351,7 +1375,7 @@ export interface CeilingCheck {
1351
1375
  */
1352
1376
  status: "verified" | "failed" | "pending" | "unmatched";
1353
1377
  reason?: string;
1354
- /** One line for people, e.g. "Ceiling: Base block N at hh:mm:ss UTC, settled on Ethereum." */
1378
+ /** One line for people, e.g. "Ceiling: included in Base block N at hh:mm:ss UTC. ..."; it never claims settlement the file does not prove. */
1355
1379
  label?: string;
1356
1380
  window?: {
1357
1381
  floorBlock: number | null;
@@ -1406,6 +1430,160 @@ export interface CeilingAnalysis {
1406
1430
  statuses: Array<{ path: string; status: string; note: string }>;
1407
1431
  }
1408
1432
 
1433
+ // ---------------------------------------------------------------------------
1434
+ // Exports (bitgraph-export/1)
1435
+ // ---------------------------------------------------------------------------
1436
+
1437
+ /**
1438
+ * An export-shaped file as found in the bundle: a JSON object whose format
1439
+ * field starts with "bitgraph-export/". Found by structure, never by name.
1440
+ * Evidence, never an artifact.
1441
+ */
1442
+ export interface ExportFile {
1443
+ path: string;
1444
+ fileSha256Hex: string;
1445
+ /** The format the file declares. */
1446
+ format: string;
1447
+ /**
1448
+ * ok: a bitgraph-export/1 document (bitgraph-verify's parseExport accepts it).
1449
+ * malformed: declares bitgraph-export/1 but lacks a proof object or tree.rootDocument.
1450
+ * unsupported-format: declares another bitgraph-export version.
1451
+ * too-large: opens an export object but is larger than the audit's export JSON cap; not parsed (json is empty).
1452
+ */
1453
+ status: "ok" | "malformed" | "unsupported-format" | "too-large";
1454
+ json: Record<string, unknown>;
1455
+ /** Canonical hash of the proof the export carries, when that proof joined the proof analysis (a proof-shaped bitgraph/1 object). */
1456
+ proofHash?: string;
1457
+ }
1458
+
1459
+ /** One claim exactly as bitgraph-verify's verifyExport states it. */
1460
+ export interface ExportClaimRecord {
1461
+ /** Stable id, dotted: "proof.signature", "tree.member", "bytes.floor", "ceiling.base", "confirmed.floor" ... */
1462
+ id: string;
1463
+ name: string;
1464
+ result: "TRUE" | "FALSE" | "UNDETERMINED" | "NOT_CARRIED";
1465
+ /** What the result rests on (the primitive or the evidence), empty when nothing was checked. */
1466
+ restsOn: string;
1467
+ detail: string;
1468
+ /** offline: recomputed from the bytes in hand. confirmed: from the caller's own chain lookups (never made by the CLI). */
1469
+ level: "offline" | "confirmed";
1470
+ }
1471
+
1472
+ /** The three time claims as verifyExport establishes them. Never merged; a null field was not established. */
1473
+ export interface ExportTimes {
1474
+ /** The committed bytes were finished after this Ethereum block (the proof's signed floor block, its header checked by hash). */
1475
+ floor: { blockNumber: number; blockHash: string; blockTimestamp: number } | null;
1476
+ /** The record existed by this Base block, at its time. Provisional until the block is checked against Base (offline, always provisional). */
1477
+ ceilingBase: { blockNumber: number; blockHash: string; blockTimestamp: number; provisional: boolean } | null;
1478
+ /** The record existed by this Ethereum block (through Base's output root; needs no trust in Base). */
1479
+ ceilingEthereum: { blockNumber: number; blockHash: string; blockTimestamp: number } | null;
1480
+ }
1481
+
1482
+ /** A file in the bundle that an export covers, matched by SHA-256 against a leaf's digests. */
1483
+ export interface ExportCoveredFile {
1484
+ /** SHA-256 of the file, lowercase hex. */
1485
+ sha256Hex: string;
1486
+ /** Every bundle path holding these bytes. */
1487
+ paths: string[];
1488
+ byteLength: number;
1489
+ /** The leaf it matched, in tree order: the first leaf whose committed or original digest is the file's, as verifyExport picks it. */
1490
+ leafIndex: number | null;
1491
+ /** That leaf's placement: "as-is", "trailer/1", "container/1" or "container/2". */
1492
+ placement: string | null;
1493
+ /**
1494
+ * committed-bytes: the file is the leaf's committed bytes (its artifact digest).
1495
+ * original: the file is the original the committed bytes were made from (its origin digest).
1496
+ * as-is: the leaf records the file exactly as it is (the two digests are the same).
1497
+ */
1498
+ matchedAs: "committed-bytes" | "original" | "as-is";
1499
+ /** The name an owner's export lists for that leaf. Unsigned and informational. */
1500
+ nameInExport?: string;
1501
+ }
1502
+
1503
+ /** A claim of one run that is not common to every run of its export, with its place in verifyExport's order. */
1504
+ export interface ExportOwnClaim extends ExportClaimRecord {
1505
+ /** Index of this claim in the run's whole claim list, in verifyExport's order. */
1506
+ position: number;
1507
+ }
1508
+
1509
+ /**
1510
+ * One verifyExport run: with one covered file, or once without a file when
1511
+ * the bundle holds none. The run's whole claim list, in verifyExport's
1512
+ * order, is the export's common claims (ExportCheck.claims) with this run's
1513
+ * own claims put back at their positions: exportRunClaims(check, run).
1514
+ * verifyExport's reasons are that list's FALSE and UNDETERMINED claims, and
1515
+ * its time claims are the export's (ExportCheck.times): they do not depend
1516
+ * on the file.
1517
+ */
1518
+ export interface ExportRun {
1519
+ /** The covered file this run checked; null for the run without a file. */
1520
+ file: ExportCoveredFile | null;
1521
+ /** verifyExport's verdict over its offline claims. */
1522
+ verdict: "TRUE" | "FALSE" | "UNDETERMINED";
1523
+ /** This run's claims that differ from the export's common claims (in practice the ones about its file: tree.member, bytes.member, bytes.floor). */
1524
+ claims: ExportOwnClaim[];
1525
+ /** The member the run established (index, count, placement, digests), when it did. */
1526
+ member: { index: number; count: number; placement: string; artifactHex: string; originHex: string } | null;
1527
+ /**
1528
+ * What the floor covers for this run's file, read from its claims: content
1529
+ * (the committed bytes carry the commitment, so they were finished after the
1530
+ * floor block; an original inside them is not dated by it), record (an as-is
1531
+ * leaf: the record was made after the floor block, the bytes themselves are
1532
+ * not dated), or null (the file was not established as a member, or no file
1533
+ * was checked).
1534
+ */
1535
+ floorCovers: "content" | "record" | null;
1536
+ /** verifyExport's plain-language reading, written from its claims. */
1537
+ reading: string;
1538
+ }
1539
+
1540
+ /** One export file, checked. */
1541
+ export interface ExportCheck {
1542
+ path: string;
1543
+ fileSha256Hex: string;
1544
+ format: string;
1545
+ /** checked: verifyExport ran. malformed / unsupported-format / too-large: rejected at ingest, not checked (exit bit 1). */
1546
+ status: "checked" | "malformed" | "unsupported-format" | "too-large";
1547
+ /** Why the file was not checked. */
1548
+ reason?: string;
1549
+ /** member: carries one member's evidence (tree.member). owner: carries the whole list (tree.leaves). root-only: neither. */
1550
+ kind?: "member" | "owner" | "root-only";
1551
+ /** Canonical hash of the export's proof, which also appears in the proof analysis. */
1552
+ proofHash?: string;
1553
+ /**
1554
+ * The audit's judgment of the export: FALSE when any run's verdict is FALSE
1555
+ * or any claim of any run is FALSE (a confirmed claim included, from an
1556
+ * embedder's lookup); else UNDETERMINED when any run is; else TRUE. A FALSE
1557
+ * export fails the audit (exit bit 1), as a bad proof does.
1558
+ */
1559
+ verdict: "TRUE" | "FALSE" | "UNDETERMINED";
1560
+ /** Ids of the claims that are FALSE in any run, in first-seen order. */
1561
+ failedClaims: string[];
1562
+ /** The three time claims, never merged. They do not depend on the file, so every run states the same; read from the first. */
1563
+ times: ExportTimes;
1564
+ /** The claims every run states alike (same id, result, level, restsOn and detail), in verifyExport's order. With one run, all of its claims. */
1565
+ claims: ExportClaimRecord[];
1566
+ /** Files in the bundle this export covers, sorted by first path. */
1567
+ files: ExportCoveredFile[];
1568
+ /** One run per covered file (in the order of files), or one run without a file when the bundle holds none. */
1569
+ runs: ExportRun[];
1570
+ /**
1571
+ * How the per-file runs of an owner's export were made. as-given: verifyExport
1572
+ * on the export itself, once per file. member-from-list: the owner's list was
1573
+ * checked once (claim tree.leaves TRUE), then each file's member evidence was
1574
+ * derived from it exactly as verifyExport derives it, verifyExport ran on that
1575
+ * member form, and the list claim was put back in its place; the runs equal
1576
+ * the as-given runs, without rebuilding the whole tree once per file.
1577
+ */
1578
+ runMode?: "as-given" | "member-from-list";
1579
+ /** Covered files not checked one by one: only when an owner's list fails and the per-file budget ran out, or a file could not be re-read. */
1580
+ unchecked?: Array<{ file: ExportCoveredFile; reason: string }>;
1581
+ }
1582
+
1583
+ export interface ExportAnalysis {
1584
+ checks: ExportCheck[];
1585
+ }
1586
+
1409
1587
  export interface AuditResult {
1410
1588
  runMetadata: AuditRunMetadata;
1411
1589
  ingest: IngestResult;
@@ -1419,6 +1597,8 @@ export interface AuditResult {
1419
1597
  attestations: AttestationAnalysis;
1420
1598
  /** Ceilings in time on Base. Absent from results made before this stage existed. */
1421
1599
  ceilings?: CeilingAnalysis;
1600
+ /** Exports (bitgraph-export/1), each checked with verifyExport per covered file. Absent from results made before this stage existed. */
1601
+ exports?: ExportAnalysis;
1422
1602
  }
1423
1603
 
1424
1604
  /**
@@ -1426,7 +1606,13 @@ export interface AuditResult {
1426
1606
  *
1427
1607
  * bit 1 (value 1): verification failures. Set when any proof's canonical
1428
1608
  * checks failed at either tier, or any proof-shaped input was rejected
1429
- * as an unsupported version. artifact-unavailable is NOT a failure: a
1609
+ * as an unsupported version, or any export (bitgraph-export/1) has a
1610
+ * FALSE claim (its verdict is FALSE), or an export-shaped file was
1611
+ * rejected as malformed or of an unsupported format. An export's
1612
+ * attestation claims are part of its verdict: an export is checked as
1613
+ * one self-contained object, as a carrier is. NOT_CARRIED claims (a
1614
+ * pending ceiling, the covered file absent) never fail an export.
1615
+ * artifact-unavailable is NOT a failure: a
1430
1616
  * proof without artifact bytes passes or fails on its bytes-free checks
1431
1617
  * alone, unless a supplied trust policy makes those checks fail (for
1432
1618
  * example requireSlot), in which case its status is "failed" and it
@@ -1616,6 +1802,19 @@ export interface ReportSummary {
1616
1802
  segmentsUpperBounded: number;
1617
1803
  segmentsUnanchored: number;
1618
1804
  };
1805
+ /** Exports (bitgraph-export/1), as counts. Present only when the bundle carries export-shaped files. */
1806
+ exports?: {
1807
+ /** Export-shaped files found. */
1808
+ files: number;
1809
+ /** Exports checked, by the audit's verdict. */
1810
+ verdictTrue: number;
1811
+ verdictFalse: number;
1812
+ verdictUndetermined: number;
1813
+ /** Export-shaped files rejected (malformed or an unsupported format). */
1814
+ rejected: number;
1815
+ /** verifyExport runs made with a covered file from the bundle. */
1816
+ coveredFilesChecked: number;
1817
+ };
1619
1818
  exit: ExitFlags;
1620
1819
  }
1621
1820
 
@@ -1662,6 +1861,8 @@ export interface AuditJsonReport {
1662
1861
  };
1663
1862
  /** Ceilings in time on Base (bitgraph-ceiling/1). Absent when the bundle carries none. */
1664
1863
  ceilings?: import("./types.js").CeilingAnalysis;
1864
+ /** Exports (bitgraph-export/1), one entry per export file in observation order. Absent when the bundle carries none. */
1865
+ exports?: ExportAnalysis;
1665
1866
  attestations: {
1666
1867
  records: ProofAttestationRecord[];
1667
1868
  counts: AttestationAnalysis["counts"];