@mikeargento/bitgraph-audit 0.1.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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/anchors.d.ts +31 -0
  4. package/dist/anchors.d.ts.map +1 -0
  5. package/dist/anchors.js +116 -0
  6. package/dist/anchors.js.map +1 -0
  7. package/dist/anomalies.d.ts +42 -0
  8. package/dist/anomalies.d.ts.map +1 -0
  9. package/dist/anomalies.js +642 -0
  10. package/dist/anomalies.js.map +1 -0
  11. package/dist/attestation.d.ts +20 -0
  12. package/dist/attestation.d.ts.map +1 -0
  13. package/dist/attestation.js +795 -0
  14. package/dist/attestation.js.map +1 -0
  15. package/dist/audit.d.ts +19 -0
  16. package/dist/audit.d.ts.map +1 -0
  17. package/dist/audit.js +116 -0
  18. package/dist/audit.js.map +1 -0
  19. package/dist/authority.d.ts +37 -0
  20. package/dist/authority.d.ts.map +1 -0
  21. package/dist/authority.js +126 -0
  22. package/dist/authority.js.map +1 -0
  23. package/dist/aws-nitro-root-ca.d.ts +24 -0
  24. package/dist/aws-nitro-root-ca.d.ts.map +1 -0
  25. package/dist/aws-nitro-root-ca.js +44 -0
  26. package/dist/aws-nitro-root-ca.js.map +1 -0
  27. package/dist/cli.d.ts +3 -0
  28. package/dist/cli.d.ts.map +1 -0
  29. package/dist/cli.js +257 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/contents-hash.d.ts +30 -0
  32. package/dist/contents-hash.d.ts.map +1 -0
  33. package/dist/contents-hash.js +73 -0
  34. package/dist/contents-hash.js.map +1 -0
  35. package/dist/export.d.ts +64 -0
  36. package/dist/export.d.ts.map +1 -0
  37. package/dist/export.js +192 -0
  38. package/dist/export.js.map +1 -0
  39. package/dist/index.d.ts +31 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +17 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/ingest.d.ts +36 -0
  44. package/dist/ingest.d.ts.map +1 -0
  45. package/dist/ingest.js +828 -0
  46. package/dist/ingest.js.map +1 -0
  47. package/dist/reconstruct.d.ts +40 -0
  48. package/dist/reconstruct.d.ts.map +1 -0
  49. package/dist/reconstruct.js +400 -0
  50. package/dist/reconstruct.js.map +1 -0
  51. package/dist/report-json.d.ts +3 -0
  52. package/dist/report-json.d.ts.map +1 -0
  53. package/dist/report-json.js +290 -0
  54. package/dist/report-json.js.map +1 -0
  55. package/dist/report-md.d.ts +3 -0
  56. package/dist/report-md.d.ts.map +1 -0
  57. package/dist/report-md.js +853 -0
  58. package/dist/report-md.js.map +1 -0
  59. package/dist/rlp.d.ts +37 -0
  60. package/dist/rlp.d.ts.map +1 -0
  61. package/dist/rlp.js +120 -0
  62. package/dist/rlp.js.map +1 -0
  63. package/dist/tar.d.ts +47 -0
  64. package/dist/tar.d.ts.map +1 -0
  65. package/dist/tar.js +483 -0
  66. package/dist/tar.js.map +1 -0
  67. package/dist/temporal.d.ts +55 -0
  68. package/dist/temporal.d.ts.map +1 -0
  69. package/dist/temporal.js +461 -0
  70. package/dist/temporal.js.map +1 -0
  71. package/dist/types.d.ts +1389 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/types.js +3 -0
  74. package/dist/types.js.map +1 -0
  75. package/dist/validity.d.ts +21 -0
  76. package/dist/validity.d.ts.map +1 -0
  77. package/dist/validity.js +86 -0
  78. package/dist/validity.js.map +1 -0
  79. package/dist/verify-tiers.d.ts +11 -0
  80. package/dist/verify-tiers.d.ts.map +1 -0
  81. package/dist/verify-tiers.js +120 -0
  82. package/dist/verify-tiers.js.map +1 -0
  83. package/dist/witness.d.ts +25 -0
  84. package/dist/witness.d.ts.map +1 -0
  85. package/dist/witness.js +308 -0
  86. package/dist/witness.js.map +1 -0
  87. package/package.json +42 -0
  88. package/src/anchors.ts +168 -0
  89. package/src/anomalies.ts +773 -0
  90. package/src/attestation.ts +878 -0
  91. package/src/audit.ts +136 -0
  92. package/src/authority.ts +165 -0
  93. package/src/aws-nitro-root-ca.ts +47 -0
  94. package/src/cli.ts +284 -0
  95. package/src/contents-hash.ts +95 -0
  96. package/src/export.ts +278 -0
  97. package/src/index.ts +122 -0
  98. package/src/ingest.ts +967 -0
  99. package/src/reconstruct.ts +491 -0
  100. package/src/report-json.ts +331 -0
  101. package/src/report-md.ts +1067 -0
  102. package/src/rlp.ts +142 -0
  103. package/src/tar.ts +587 -0
  104. package/src/temporal.ts +629 -0
  105. package/src/types.ts +1528 -0
  106. package/src/validity.ts +90 -0
  107. package/src/verify-tiers.ts +137 -0
  108. package/src/witness.ts +407 -0
@@ -0,0 +1,853 @@
1
+ // Copyright (c) 2024-2026 Mike Argento. Licensed under the MIT License. See LICENSE.
2
+ /**
3
+ * Markdown report builder: turns an AuditResult into audit-report.md.
4
+ *
5
+ * Two audiences, one document. The executive summary is written for a
6
+ * reader with no cryptography background: every cryptographic term is
7
+ * explained by its consequence, gap language follows the two-position
8
+ * counter reality (positions neither commit positions nor referenced
9
+ * slot positions; absence from the bundle does not, by itself, prove the
10
+ * authority failed to create them), and divergences are presented
11
+ * without choosing a winner. The detail sections that follow are for
12
+ * engineers: full tables and per-object walkthroughs.
13
+ *
14
+ * The markdown is rendered from the same buildJsonReport object, so its
15
+ * ordering is identical to the JSON report's. It is deterministic given
16
+ * the same bundle except for the run line naming the wall-clock start
17
+ * time, which is the only nondeterministic content.
18
+ *
19
+ * Long tables are capped at MAX_TABLE_ROWS rows with an explicit
20
+ * remainder line; the JSON report always carries the complete data.
21
+ */
22
+ import { buildJsonReport } from "./report-json.js";
23
+ const MAX_TABLE_ROWS = 200;
24
+ // ---------------------------------------------------------------------------
25
+ // Public API
26
+ // ---------------------------------------------------------------------------
27
+ export function buildMarkdownReport(result) {
28
+ const report = buildJsonReport(result);
29
+ const proofByHash = new Map(report.proofs.map((p) => [p.proofHash, p]));
30
+ const lines = [];
31
+ header(lines, report);
32
+ executiveSummary(lines, report, proofByHash);
33
+ detailSections(lines, report, proofByHash);
34
+ return lines.join("\n") + "\n";
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // Header
38
+ // ---------------------------------------------------------------------------
39
+ function header(lines, report) {
40
+ lines.push("# BitGraph Audit Report");
41
+ lines.push("");
42
+ lines.push(`Generated by bitgraph-audit ${report.toolVersion}. ` +
43
+ `Bundle: \`${report.runMetadata.bundlePath}\` (${report.runMetadata.container} container). ` +
44
+ `Run started ${report.runMetadata.startedAt}.`);
45
+ lines.push("");
46
+ lines.push("The start time above is the only content of this report that changes " +
47
+ "between runs on the same bundle. Everything else is derived entirely " +
48
+ "from the files supplied, with no network access of any kind. The " +
49
+ "machine-readable companion, audit-report.json, carries the complete " +
50
+ "data behind every table here (schema bitgraph-audit-report/1).");
51
+ lines.push("");
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Executive summary
55
+ // ---------------------------------------------------------------------------
56
+ function executiveSummary(lines, report, proofByHash) {
57
+ const s = report.summary;
58
+ lines.push("## Executive summary");
59
+ lines.push("");
60
+ lines.push("This report examines a bundle of BitGraph proofs offline. A BitGraph " +
61
+ "proof is a small signed record stating that a specific file, " +
62
+ "identified by its digital fingerprint, was committed at a specific " +
63
+ "position in a sequence kept by an authority. Because each proof " +
64
+ "names the fingerprint of the proof before it, the sequence forms a " +
65
+ "chain whose order can be checked from the objects themselves: this " +
66
+ "is what is meant by causal order. The audit verifies what can be " +
67
+ "verified, reconstructs that order, and reports anything missing or " +
68
+ "conflicting. Where evidence is incomplete, it says so rather than " +
69
+ "guessing, and where valid records conflict, it presents all of them " +
70
+ "and chooses none.");
71
+ lines.push("");
72
+ // Proofs and verification tiers.
73
+ lines.push("### Proofs");
74
+ lines.push("");
75
+ lines.push(...table(["Category", "Count"], [
76
+ ["Proof objects observed", withCommas(s.proofsObserved)],
77
+ ["Fully verified (artifact bytes present and matched)", withCommas(s.fullyVerified)],
78
+ ["Failed verification", withCommas(s.failed)],
79
+ ["Observed without artifact bytes", withCommas(s.artifactUnavailable)],
80
+ ["Rejected as unsupported legacy versions", withCommas(s.unsupportedVersion)],
81
+ ["Duplicate copies (byte-identical)", withCommas(s.exactDuplicates)],
82
+ ["Duplicate copies (re-encoded, same identity)", withCommas(s.semanticDuplicates)],
83
+ ]));
84
+ lines.push("");
85
+ lines.push("Fully verified means two things held at once: the proof's internal " +
86
+ "cryptography checked out (its signature is valid and its parts are " +
87
+ "cryptographically bound together), and the original file was present " +
88
+ "in the bundle with contents matching the fingerprint recorded inside " +
89
+ "the signed proof. Such a proof is detectably invalid if altered: " +
90
+ "changing any byte of the proof or of the file breaks the check.");
91
+ if (s.artifactUnavailable > 0) {
92
+ lines.push("");
93
+ lines.push(`${withCommas(s.artifactUnavailable)} ${plural(s.artifactUnavailable, "proof", "proofs")} ` +
94
+ "arrived without the original file bytes. Their internal " +
95
+ "cryptography was checked and passed, but the link between proof " +
96
+ "and file could not be independently confirmed because there was no " +
97
+ "file to compare against. They are reported as observed, never as " +
98
+ "fully verified.");
99
+ }
100
+ if (s.failed > 0) {
101
+ lines.push("");
102
+ lines.push(`${withCommas(s.failed)} ${plural(s.failed, "proof", "proofs")} failed verification. ` +
103
+ "The exact reason for each failure is recorded in the detail " +
104
+ "sections and in the JSON report.");
105
+ }
106
+ if (s.unsupportedVersion > 0) {
107
+ lines.push("");
108
+ lines.push(`${withCommas(s.unsupportedVersion)} proof-shaped ${plural(s.unsupportedVersion, "file was", "files were")} ` +
109
+ "rejected because the version field is not bitgraph/1. Only " +
110
+ "bitgraph/1 proofs are supported; earlier pre-release data is " +
111
+ "permanently out of scope. Rejected files are listed with their " +
112
+ "paths and version strings in the detail sections.");
113
+ }
114
+ if (s.chainless > 0) {
115
+ lines.push("");
116
+ lines.push(`${withCommas(s.chainless)} ${plural(s.chainless, "proof carries", "proofs carry")} ` +
117
+ "neither a counter nor an epoch identifier. The schema permits " +
118
+ "this; such proofs are observed and verified but sit outside every " +
119
+ "chain, so no ordering claims are made about them.");
120
+ }
121
+ lines.push("");
122
+ // Causal chain.
123
+ lines.push("### Causal chain");
124
+ lines.push("");
125
+ lines.push("Proofs are grouped by the authority key, epoch, and chain they were " +
126
+ "committed under; each group is called a partition. Every proof " +
127
+ "occupies two counter positions: one reserved in advance (the slot) " +
128
+ "and one at the moment of commit. Slot positions never produce " +
129
+ "stored proof files of their own, so a healthy chain shows only " +
130
+ "committed proofs, each referencing its own slot position from " +
131
+ "inside its signed content.");
132
+ lines.push("");
133
+ if (report.partitions.length === 0) {
134
+ lines.push("No chain partitions were observed in this bundle.");
135
+ lines.push("");
136
+ }
137
+ for (const partition of report.partitions) {
138
+ lines.push(`**Partition ${partitionLabel(partition)}.** Chain intact: ${partition.intact ? "yes" : "no"}.`);
139
+ lines.push("");
140
+ lines.push(...partitionSummarySentences(report, partition));
141
+ lines.push("");
142
+ }
143
+ if (report.unchainedProofHashes.length > 0) {
144
+ lines.push(`${withCommas(report.unchainedProofHashes.length)} observed ` +
145
+ `${plural(report.unchainedProofHashes.length, "proof is", "proofs are")} ` +
146
+ "outside all partitions (no counter and no epoch identifier).");
147
+ lines.push("");
148
+ }
149
+ // Epochs.
150
+ lines.push("### Epochs");
151
+ lines.push("");
152
+ const epochCount = report.epochRelationships.epochs.length;
153
+ lines.push(`${withCommas(epochCount)} ${plural(epochCount, "epoch was", "epochs were")} observed. ` +
154
+ "An epoch is one uninterrupted run of the authority: every restart " +
155
+ "creates a fresh signing key and restarts the counter, so counters " +
156
+ "never order proofs across epochs. Epochs are ordered against each " +
157
+ "other only by verified evidence: a signed lineage link from one " +
158
+ "epoch's first proof back to the previous epoch's last proof, or " +
159
+ "independently verified Ethereum anchor evidence.");
160
+ lines.push("");
161
+ const hardPairs = report.epochRelationships.orderedPairs.length;
162
+ const anchorPairs = report.epochRelationships.anchorOrderedPairs.length;
163
+ const unorderedPairs = report.epochRelationships.unorderedPairs.length;
164
+ if (epochCount > 1 || hardPairs + anchorPairs > 0) {
165
+ lines.push(`Ordering evidence: ${withCommas(hardPairs)} epoch ${plural(hardPairs, "pair", "pairs")} ` +
166
+ `ordered by signed lineage links, ${withCommas(anchorPairs)} by anchor time bounds ` +
167
+ `(assumption-dependent, see the time evidence section), and ` +
168
+ `${withCommas(unorderedPairs)} ${plural(unorderedPairs, "pair", "pairs")} with no ordering ` +
169
+ "evidence in either direction. Epochs without ordering evidence " +
170
+ "are concurrent or unordered as far as this bundle shows, which " +
171
+ "is not by itself a conflict.");
172
+ lines.push("");
173
+ }
174
+ // Anomalies.
175
+ lines.push("### Anomalies");
176
+ lines.push("");
177
+ const codes = Object.entries(report.summary.anomalyCountsByCode);
178
+ if (codes.length === 0) {
179
+ lines.push("No anomalies of any type were detected.");
180
+ }
181
+ else {
182
+ lines.push(...table(["Code", "Count", "What it means"], codes.map(([code, count]) => [
183
+ inlineCode(code),
184
+ withCommas(count),
185
+ codeMeaning(code),
186
+ ])));
187
+ }
188
+ lines.push("");
189
+ // Divergences.
190
+ lines.push("### Divergences");
191
+ lines.push("");
192
+ if (report.divergences.length === 0) {
193
+ lines.push("No divergences were found: no two valid proofs claim the same " +
194
+ "position, the same predecessor, or the same epoch boundary.");
195
+ lines.push("");
196
+ }
197
+ else {
198
+ lines.push(`${withCommas(report.divergences.length)} ${plural(report.divergences.length, "divergence was", "divergences were")} ` +
199
+ "found. A divergence is a conflict between proofs that are each " +
200
+ "individually valid; it is presented as evidence, never resolved " +
201
+ "by this tool.");
202
+ lines.push("");
203
+ for (const divergence of report.divergences) {
204
+ lines.push(`- ${divergenceSummarySentence(divergence)}`);
205
+ }
206
+ lines.push("");
207
+ }
208
+ // Authorities and measurements.
209
+ lines.push("### Authorities and measurements");
210
+ lines.push("");
211
+ lines.push(`The observed proofs were signed under ${withCommas(s.distinctSignerCount)} ` +
212
+ `${plural(s.distinctSignerCount, "authority key", "authority keys")} and declare ` +
213
+ `${withCommas(s.distinctDeclaredMeasurementCount)} distinct software ` +
214
+ `${plural(s.distinctDeclaredMeasurementCount, "measurement", "measurements")}, in ` +
215
+ `${withCommas(s.authorityGroupCount)} authority ${plural(s.authorityGroupCount, "group", "groups")}. ` +
216
+ "A declared measurement is what a proof says about the software that " +
217
+ "produced it; on its own it is a self-description. An attested " +
218
+ "measurement comes from a hardware attestation document that was " +
219
+ "cryptographically validated; only that ties proofs to a specific " +
220
+ "program. The two are tracked separately throughout this report.");
221
+ lines.push("");
222
+ // Attestation coverage.
223
+ lines.push("### Attestation coverage");
224
+ lines.push("");
225
+ lines.push(...table(["Fact (tracked separately)", "Proofs"], [
226
+ ["Declared measurement present (self-reported)", withCommas(s.attestation.declaredMeasurementPresent)],
227
+ ["Attestation document present (presence alone proves nothing)", withCommas(s.attestation.documentsPresent)],
228
+ ["Document cryptographically validated offline", withCommas(s.attestation.documentsValidated)],
229
+ ["Validated document's measurement matches the declared one", withCommas(s.attestation.pcr0MatchesDeclared)],
230
+ ["Validated document bound to its specific proof", withCommas(s.attestation.userDataBound)],
231
+ ]));
232
+ lines.push("");
233
+ lines.push("These five facts are never merged. Carrying an attestation document " +
234
+ "is not the same as the document validating, and a validated " +
235
+ "document supports a proof only when it is bound to that specific " +
236
+ "proof and its measurement matches what the proof declares.");
237
+ lines.push("");
238
+ // External time evidence.
239
+ lines.push("### External time evidence");
240
+ lines.push("");
241
+ const t = s.temporal;
242
+ if (t.anchorsIdentified === 0) {
243
+ lines.push("No Ethereum anchor proofs were observed. The causal order of the " +
244
+ "proofs stands on its own; this report makes no wall-clock claims.");
245
+ lines.push("");
246
+ }
247
+ else {
248
+ lines.push(`${withCommas(t.anchorsIdentified)} Ethereum anchor ${plural(t.anchorsIdentified, "proof was", "proofs were")} ` +
249
+ "identified. An anchor is an ordinary chain member that committed " +
250
+ "the fingerprint of a public Ethereum block, giving the chain a " +
251
+ "contact point with a public timeline. " +
252
+ `${withCommas(t.anchorsWithVerifiedWitness)} ${plural(t.anchorsWithVerifiedWitness, "anchor", "anchors")} ` +
253
+ "came with witness material (the block header) that let this tool " +
254
+ "reconstruct the block fingerprint locally and confirm it against " +
255
+ "the signed anchor, entirely offline.");
256
+ lines.push("");
257
+ lines.push("A verified anchor bounds time in one direction at a time. Proofs " +
258
+ "that come after an anchor in the chain were committed no earlier " +
259
+ "than that block's timestamp, because the block's fingerprint " +
260
+ "could not have been known before the block existed. That lower " +
261
+ "bound additionally assumes the anchored header is a genuine, " +
262
+ "publicly published Ethereum block: this offline audit checks the " +
263
+ "header's structure and hash binding, not proof-of-work, " +
264
+ "consensus, or chain membership, so it cannot confirm the block " +
265
+ "is real. Proofs that come before an anchor existed before the " +
266
+ "commit that consumed the block; reading that as a wall-clock " +
267
+ "ceiling additionally assumes the anchor consumed a recently " +
268
+ "published block. Every such bound in this report states its " +
269
+ "assumption. No individual proof's exact creation time is ever " +
270
+ "stated.");
271
+ lines.push("");
272
+ lines.push(...table(["Temporal coverage", "Segments"], [
273
+ ["Bracketed (verified evidence on both sides)", withCommas(t.segmentsBracketed)],
274
+ ["Lower-bounded only (committed no earlier than a block time)", withCommas(t.segmentsLowerBounded)],
275
+ ["Upper-bounded only (existed before an anchor commit)", withCommas(t.segmentsUpperBounded)],
276
+ ["Ordered but unanchored (causal order only, no wall-clock evidence)", withCommas(t.segmentsUnanchored)],
277
+ ]));
278
+ lines.push("");
279
+ }
280
+ }
281
+ /** Plain sentences describing one partition's chain state, in the brief's style. */
282
+ function partitionSummarySentences(report, partition) {
283
+ const out = [];
284
+ const memberCount = partition.memberProofHashes.length;
285
+ if (partition.components.length === 1) {
286
+ out.push(`${withCommas(memberCount)} ${plural(memberCount, "proof forms", "proofs form")} one ` +
287
+ "continuous observed causal sequence.");
288
+ }
289
+ else {
290
+ out.push(`The ${withCommas(memberCount)} observed ${plural(memberCount, "proof forms", "proofs form")} ` +
291
+ `${withCommas(partition.components.length)} disconnected sequences. The record between ` +
292
+ "them cannot be reconstructed from the supplied evidence.");
293
+ }
294
+ for (const anomaly of report.anomalies) {
295
+ if (anomaly.stage !== "chain" || anomaly.partition === undefined)
296
+ continue;
297
+ if (!samePartition(anomaly.partition, partition.key))
298
+ continue;
299
+ if (anomaly.code === "unexplained-counter-positions") {
300
+ const detail = anomaly.details;
301
+ const count = detail.count;
302
+ const singular = count === "1";
303
+ out.push(`${withCommas(count)} counter ${singular ? "position is neither a commit position nor a referenced slot position" : "positions are neither commit positions nor referenced slot positions"} ` +
304
+ "in the supplied bundle. An unexplained interior position may " +
305
+ "mean a proof is absent from the bundle, or a slot that was " +
306
+ "allocated but never committed (a routine, benign occurrence); " +
307
+ "this offline audit cannot tell the two apart. It does not, by " +
308
+ "itself, prove that the BitGraph authority failed to create or " +
309
+ "withheld any proof.");
310
+ }
311
+ else if (anomaly.code === "chain-break-missing") {
312
+ out.push("At least one proof names a predecessor that is absent from the " +
313
+ "bundle, so the history before that point cannot be " +
314
+ "reconstructed from the supplied evidence. The absence does " +
315
+ "not, by itself, prove that the predecessor never existed.");
316
+ }
317
+ }
318
+ return [out.join(" ")];
319
+ }
320
+ /** One-sentence executive-summary line for a divergence, matching the brief's style. */
321
+ function divergenceSummarySentence(divergence) {
322
+ const n = divergence.parties.length;
323
+ const head = n === 2 ? "Two independently valid proof objects" : `${withCommas(n)} independently valid proof objects`;
324
+ const both = n === 2 ? "Both objects" : "All objects";
325
+ const tail = ` The audit tool does not choose between them. ${both} and their predecessor ` +
326
+ "relationships are shown in the divergence details for adjudication.";
327
+ switch (divergence.kind) {
328
+ case "counter-collision":
329
+ return (`${head} ${n === 2 ? "both claim" : "all claim"} commit counter ` +
330
+ `${withCommas(divergence.contested["counter"] ?? "?")} in the same epoch.` +
331
+ tail);
332
+ case "slot-collision":
333
+ return (`${head} ${n === 2 ? "both reference" : "all reference"} slot counter ` +
334
+ `${withCommas(divergence.contested["slotCounter"] ?? "?")} in the same epoch.` +
335
+ tail);
336
+ case "cross-kind-position-reuse":
337
+ return (`${head} allocate one causal position ` +
338
+ `${withCommas(divergence.contested["position"] ?? "?")} in the same epoch, at least one ` +
339
+ "committing it and at least one reserving it as a slot." +
340
+ tail);
341
+ case "predecessor-reuse":
342
+ return (`${head} name the same predecessor proof: the chain forks at that point.` + tail);
343
+ case "multiple-genesis":
344
+ return (`${head} each begin a chain without naming a predecessor in the same epoch.` + tail);
345
+ case "epochlink-fork":
346
+ return (`${head} claim the same predecessor epoch's terminal proof for different successor epochs.` +
347
+ tail);
348
+ default:
349
+ return `${head} conflict over ${JSON.stringify(divergence.contested)}.` + tail;
350
+ }
351
+ }
352
+ // ---------------------------------------------------------------------------
353
+ // Detail sections (for engineers)
354
+ // ---------------------------------------------------------------------------
355
+ function detailSections(lines, report, proofByHash) {
356
+ lines.push("## Details");
357
+ lines.push("");
358
+ lines.push("The sections below are for technical readers. All hashes are " +
359
+ "canonical proof hashes (base64 SHA-256 over the canonical signed " +
360
+ "body) unless stated otherwise. The JSON report carries the " +
361
+ "complete, uncapped data.");
362
+ lines.push("");
363
+ perPartitionChains(lines, report, proofByHash);
364
+ anomalyDetails(lines, report);
365
+ divergenceDetails(lines, report);
366
+ anchorDetails(lines, report);
367
+ witnessDetails(lines, report);
368
+ temporalDetails(lines, report);
369
+ attestationDetails(lines, report);
370
+ unsupportedVersionDetails(lines, report);
371
+ manifestDetails(lines, report);
372
+ }
373
+ function perPartitionChains(lines, report, proofByHash) {
374
+ lines.push("### Per-partition chains");
375
+ lines.push("");
376
+ if (report.partitions.length === 0) {
377
+ lines.push("No partitions were observed.");
378
+ lines.push("");
379
+ return;
380
+ }
381
+ for (const partition of report.partitions) {
382
+ lines.push(`#### Partition ${partitionLabel(partition)}`);
383
+ lines.push("");
384
+ lines.push(`Signer key: ${inlineCode(partition.key.publicKeyB64)}. ` +
385
+ `Epoch: ${partition.key.epochId !== undefined ? inlineCode(partition.key.epochId) : "(none)"}. ` +
386
+ `Chain: ${inlineCode(partition.key.chainId)}. ` +
387
+ `Members: ${withCommas(partition.memberProofHashes.length)}. ` +
388
+ `Components: ${withCommas(partition.components.length)}. ` +
389
+ `Intact: ${partition.intact ? "yes" : "no"}.`);
390
+ lines.push("");
391
+ for (let c = 0; c < partition.components.length; c++) {
392
+ const component = partition.components[c];
393
+ if (component === undefined)
394
+ continue;
395
+ lines.push(`Component ${c + 1} of ${partition.components.length}` +
396
+ (component.positionRange !== undefined
397
+ ? ` (positions ${withCommas(component.positionRange.min)} to ${withCommas(component.positionRange.max)})`
398
+ : "") +
399
+ ":");
400
+ lines.push("");
401
+ const rows = [];
402
+ for (const hash of component.memberProofHashes.slice(0, MAX_TABLE_ROWS)) {
403
+ const proof = proofByHash.get(hash);
404
+ rows.push([
405
+ proof?.counter !== undefined ? proof.counter : "",
406
+ proof?.slotCounter !== undefined ? proof.slotCounter : "",
407
+ inlineCode(hash),
408
+ proof?.prevB64 !== undefined
409
+ ? component.brokenLinkProofHashes.includes(hash)
410
+ ? "unresolved"
411
+ : "resolved"
412
+ : "none (genesis)",
413
+ proof?.verificationStatus ?? "unverified",
414
+ ]);
415
+ }
416
+ lines.push(...table(["Counter", "Slot", "Proof hash", "Predecessor", "Status"], rows));
417
+ if (component.memberProofHashes.length > MAX_TABLE_ROWS) {
418
+ lines.push("");
419
+ lines.push(`... and ${withCommas(component.memberProofHashes.length - MAX_TABLE_ROWS)} more members ` +
420
+ "(complete list in the JSON report).");
421
+ }
422
+ lines.push("");
423
+ }
424
+ }
425
+ if (report.unchainedProofHashes.length > 0) {
426
+ lines.push("#### Unchained proofs");
427
+ lines.push("");
428
+ lines.push("Observed proofs with neither counter nor epoch identifier " +
429
+ "(permitted by the schema, outside all partitions, not anomalies):");
430
+ lines.push("");
431
+ for (const hash of report.unchainedProofHashes.slice(0, MAX_TABLE_ROWS)) {
432
+ lines.push(`- ${inlineCode(hash)}`);
433
+ }
434
+ lines.push("");
435
+ }
436
+ if (report.unpartitionedProofHashes.length > 0) {
437
+ lines.push("#### Unpartitioned proofs");
438
+ lines.push("");
439
+ lines.push("Observed proofs carrying chain fields but no signer key; they " +
440
+ "cannot join a signer lineage and their structural failure is " +
441
+ "recorded on the verification dimension:");
442
+ lines.push("");
443
+ for (const hash of report.unpartitionedProofHashes.slice(0, MAX_TABLE_ROWS)) {
444
+ lines.push(`- ${inlineCode(hash)}`);
445
+ }
446
+ lines.push("");
447
+ }
448
+ }
449
+ function anomalyDetails(lines, report) {
450
+ lines.push("### Anomaly details");
451
+ lines.push("");
452
+ if (report.anomalies.length === 0) {
453
+ lines.push("No anomalies of any type were detected.");
454
+ lines.push("");
455
+ return;
456
+ }
457
+ for (const anomaly of report.anomalies.slice(0, MAX_TABLE_ROWS)) {
458
+ lines.push(`- [${anomaly.stage}] ${inlineCode(anomaly.code)}: ${anomaly.message}`);
459
+ if (anomaly.path !== undefined)
460
+ lines.push(` - path: ${inlineCode(anomaly.path)}`);
461
+ if (anomaly.partition !== undefined) {
462
+ lines.push(` - partition: signer ${inlineCode(anomaly.partition.publicKeyB64)}, epoch ` +
463
+ `${anomaly.partition.epochId !== undefined ? inlineCode(anomaly.partition.epochId) : "(none)"}, ` +
464
+ `chain ${inlineCode(anomaly.partition.chainId)}`);
465
+ }
466
+ if (anomaly.proofHashes !== undefined && anomaly.proofHashes.length > 0) {
467
+ lines.push(` - proofs: ${anomaly.proofHashes.map(inlineCode).join(", ")}`);
468
+ }
469
+ if (anomaly.details !== undefined) {
470
+ lines.push(` - details: ${inlineCode(JSON.stringify(anomaly.details))}`);
471
+ }
472
+ }
473
+ if (report.anomalies.length > MAX_TABLE_ROWS) {
474
+ lines.push("");
475
+ lines.push(`... and ${withCommas(report.anomalies.length - MAX_TABLE_ROWS)} more anomalies ` +
476
+ "(complete list in the JSON report).");
477
+ }
478
+ lines.push("");
479
+ }
480
+ function divergenceDetails(lines, report) {
481
+ lines.push("### Divergence walkthroughs");
482
+ lines.push("");
483
+ if (report.divergences.length === 0) {
484
+ lines.push("No divergences were found.");
485
+ lines.push("");
486
+ return;
487
+ }
488
+ for (let i = 0; i < report.divergences.length && i < MAX_TABLE_ROWS; i++) {
489
+ const divergence = report.divergences[i];
490
+ if (divergence === undefined)
491
+ continue;
492
+ lines.push(`#### Divergence ${i + 1}: ${inlineCode(divergence.kind)}`);
493
+ lines.push("");
494
+ lines.push(divergence.explanation);
495
+ lines.push("");
496
+ lines.push(`Contested: ${inlineCode(JSON.stringify(divergence.contested))}.` +
497
+ (divergence.partition !== undefined
498
+ ? ` Partition: signer ${inlineCode(divergence.partition.publicKeyB64)}, epoch ` +
499
+ `${divergence.partition.epochId !== undefined ? inlineCode(divergence.partition.epochId) : "(none)"}, ` +
500
+ `chain ${inlineCode(divergence.partition.chainId)}.`
501
+ : ""));
502
+ lines.push("");
503
+ lines.push("Competing valid parties (this tool chooses none of them):");
504
+ lines.push("");
505
+ lines.push(...partyTable(divergence.parties));
506
+ if (divergence.invalidContext.length > 0) {
507
+ lines.push("");
508
+ lines.push("Cryptographically invalid proofs involved in the same conflict " +
509
+ "(context only, never competing branches):");
510
+ lines.push("");
511
+ lines.push(...partyTable(divergence.invalidContext));
512
+ }
513
+ lines.push("");
514
+ }
515
+ }
516
+ function partyTable(parties) {
517
+ return table(["Proof hash", "Counter", "Slot", "Predecessor", "Status", "Sources"], parties.map((party) => [
518
+ inlineCode(party.proofHash),
519
+ party.counter ?? "",
520
+ party.slotCounter ?? "",
521
+ party.prevB64 !== undefined ? inlineCode(party.prevB64) : "",
522
+ party.verificationStatus ?? "unverified",
523
+ party.sourcePaths.map(inlineCode).join(", "),
524
+ ]));
525
+ }
526
+ function anchorDetails(lines, report) {
527
+ lines.push("### Ethereum anchors");
528
+ lines.push("");
529
+ if (report.anchors.records.length === 0) {
530
+ lines.push("No anchor proofs were identified.");
531
+ lines.push("");
532
+ return;
533
+ }
534
+ lines.push(...table(["Proof hash", "Epoch", "Chain", "Counter", "Block number", "Block hash (signed)", "Metadata", "Status"], report.anchors.records
535
+ .slice(0, MAX_TABLE_ROWS)
536
+ .map((anchor) => [
537
+ inlineCode(anchor.proofHash),
538
+ anchor.epochId !== undefined ? inlineCode(anchor.epochId) : "",
539
+ inlineCode(anchor.chainId),
540
+ anchor.counter ?? "",
541
+ anchor.blockNumber ?? "(unparsed)",
542
+ anchor.blockHash !== undefined ? inlineCode(anchor.blockHash) : "",
543
+ anchor.metadataCorroboration,
544
+ anchor.verificationStatus ?? "unverified",
545
+ ])));
546
+ lines.push("");
547
+ if (report.anchors.metadataOnlyProofHashes.length > 0) {
548
+ lines.push("Proofs whose unsigned metadata claims to be an anchor while the " +
549
+ "signed content does not (never treated as anchors): " +
550
+ report.anchors.metadataOnlyProofHashes.map(inlineCode).join(", "));
551
+ lines.push("");
552
+ }
553
+ }
554
+ function witnessDetails(lines, report) {
555
+ lines.push("### Anchor witnesses");
556
+ lines.push("");
557
+ if (report.witnesses.outcomes.length === 0) {
558
+ lines.push("No anchor witness files were present. Anchors without a verified " +
559
+ "witness still establish causal order; they confer no wall-clock " +
560
+ "evidence.");
561
+ lines.push("");
562
+ return;
563
+ }
564
+ lines.push(...table(["Witness path", "Anchor proof", "Verified", "Reason", "Block number", "Block timestamp"], report.witnesses.outcomes
565
+ .slice(0, MAX_TABLE_ROWS)
566
+ .map((outcome) => [
567
+ inlineCode(outcome.witnessPath),
568
+ outcome.anchorProofHash !== undefined ? inlineCode(outcome.anchorProofHash) : "(unmatched)",
569
+ outcome.verified ? "yes" : "no",
570
+ outcome.reason !== undefined ? inlineCode(outcome.reason) : "",
571
+ outcome.blockNumber ?? "",
572
+ outcome.timestamp !== undefined ? formatTimestamp(outcome.timestamp) : "",
573
+ ])));
574
+ lines.push("");
575
+ lines.push("A verified witness means this tool decoded the supplied block " +
576
+ "header, recomputed the block fingerprint locally, and confirmed it " +
577
+ "against the fingerprint the authority signed into the anchor. A " +
578
+ "failed witness confers nothing and does not change the anchor " +
579
+ "proof's own standing.");
580
+ lines.push("");
581
+ }
582
+ function temporalDetails(lines, report) {
583
+ lines.push("### Temporal bounds");
584
+ lines.push("");
585
+ if (report.temporal.segments.length === 0) {
586
+ lines.push("No partitioned proofs exist, so no temporal segments were derived.");
587
+ lines.push("");
588
+ return;
589
+ }
590
+ for (let i = 0; i < report.temporal.segments.length && i < MAX_TABLE_ROWS; i++) {
591
+ const segment = report.temporal.segments[i];
592
+ if (segment === undefined)
593
+ continue;
594
+ lines.push(`Segment ${i + 1}: ${withCommas(segment.memberProofHashes.length)} ` +
595
+ `${plural(segment.memberProofHashes.length, "proof", "proofs")}` +
596
+ (segment.positionRange !== undefined
597
+ ? ` at positions ${withCommas(segment.positionRange.min)} to ${withCommas(segment.positionRange.max)}`
598
+ : "") +
599
+ ` in partition signer ${inlineCode(segment.partition.publicKeyB64)}, epoch ` +
600
+ `${segment.partition.epochId !== undefined ? inlineCode(segment.partition.epochId) : "(none)"}, ` +
601
+ `chain ${inlineCode(segment.partition.chainId)}. Status: ${segment.status}.`);
602
+ for (const bound of [...segment.lowerBounds, ...segment.upperBounds]) {
603
+ lines.push(`- ${boundLine(bound)}`);
604
+ }
605
+ lines.push("");
606
+ }
607
+ if (report.epochRelationships.anchorOrderedPairs.length > 0) {
608
+ lines.push("Anchor-derived epoch ordering (assumption-dependent, covered portions only):");
609
+ lines.push("");
610
+ for (const pair of report.epochRelationships.anchorOrderedPairs) {
611
+ lines.push(`- Covered portion of epoch ${inlineCode(pair.beforeEpochId)} ` +
612
+ `(${withCommas(pair.beforeCoveredProofCount)} of ${withCommas(pair.beforeTotalProofCount)} proofs) ` +
613
+ `before covered portion of epoch ${inlineCode(pair.afterEpochId)} ` +
614
+ `(${withCommas(pair.afterCoveredProofCount)} of ${withCommas(pair.afterTotalProofCount)} proofs) ` +
615
+ `(evidence: not-after ${pair.upperEvidence}, not-before ${pair.lowerEvidence}` +
616
+ `${pair.weaker ? ", weaker" : ""}). ${pair.note}`);
617
+ }
618
+ lines.push("");
619
+ }
620
+ }
621
+ function boundLine(bound) {
622
+ const direction = bound.kind === "not-before"
623
+ ? "Committed no earlier than"
624
+ : "Existed before the anchor commit that consumed a block published at";
625
+ return (`${direction} ${formatTimestamp(bound.timestamp)} ` +
626
+ `(block ${bound.blockNumber ?? "?"}, anchor ${inlineCode(bound.anchorProofHash)}, ` +
627
+ `evidence: ${bound.evidence}${bound.weaker ? ", weaker" : ""}). ${bound.claim}`);
628
+ }
629
+ function attestationDetails(lines, report) {
630
+ lines.push("### Attestation breakdown");
631
+ lines.push("");
632
+ const withDocs = report.attestations.records.filter((r) => r.documentPresent);
633
+ if (withDocs.length === 0) {
634
+ lines.push("No observed proof carries an attestation document. Declared " +
635
+ "measurements remain self-reported; no hardware-backed execution " +
636
+ "claim is made anywhere in this report.");
637
+ lines.push("");
638
+ return;
639
+ }
640
+ lines.push(...table(["Proof hash", "Declared measurement", "Document", "Validated", "Measurement match", "Bound to proof"], withDocs
641
+ .slice(0, MAX_TABLE_ROWS)
642
+ .map((record) => [
643
+ inlineCode(record.proofHash),
644
+ record.declaredMeasurementPresent ? "present" : "absent",
645
+ record.documentPresent ? "present" : "absent",
646
+ record.documentValidated ? "yes" : "no",
647
+ record.pcr0MatchesDeclared === undefined ? "not evaluated" : record.pcr0MatchesDeclared ? "yes" : "no",
648
+ record.userDataBoundToProof === undefined ? "not evaluated" : record.userDataBoundToProof ? "yes" : "no",
649
+ ])));
650
+ lines.push("");
651
+ const failures = withDocs.filter((r) => !r.documentValidated);
652
+ if (failures.length > 0) {
653
+ lines.push("Validation failures:");
654
+ lines.push("");
655
+ for (const record of failures.slice(0, MAX_TABLE_ROWS)) {
656
+ lines.push(`- ${inlineCode(record.proofHash)}: ${record.validationFailure ?? "validation failed"}`);
657
+ }
658
+ lines.push("");
659
+ }
660
+ lines.push("Measurement match and proof binding are evaluated only on documents " +
661
+ "that validated; values parsed from an unvalidated document prove " +
662
+ "nothing and are never compared.");
663
+ lines.push("");
664
+ }
665
+ function unsupportedVersionDetails(lines, report) {
666
+ lines.push("### Unsupported version inputs");
667
+ lines.push("");
668
+ if (report.unsupportedVersions.length === 0) {
669
+ lines.push("No proof-shaped files were rejected for their version.");
670
+ lines.push("");
671
+ return;
672
+ }
673
+ lines.push("The audit system supports exactly one proof schema, bitgraph/1. The " +
674
+ "following proof-shaped files were rejected at ingest and excluded " +
675
+ "from verification, chain reconstruction, and anomaly analysis:");
676
+ lines.push("");
677
+ lines.push(...table(["Path", "Version string", "File SHA-256"], report.unsupportedVersions
678
+ .slice(0, MAX_TABLE_ROWS)
679
+ .map((record) => [
680
+ inlineCode(record.path),
681
+ inlineCode(record.version),
682
+ inlineCode(record.fileSha256Hex),
683
+ ])));
684
+ lines.push("");
685
+ }
686
+ function manifestDetails(lines, report) {
687
+ lines.push("### Bundle manifest");
688
+ lines.push("");
689
+ const manifest = report.input.manifest;
690
+ if (manifest === undefined) {
691
+ lines.push("No manifest.json was present at the bundle root. The manifest is " +
692
+ "optional and advisory; its absence changes nothing about the " +
693
+ "evidence above.");
694
+ lines.push("");
695
+ lines.push(`Computed deterministic contents hash: ${inlineCode(report.input.computedContentsHashB64)}.`);
696
+ lines.push("");
697
+ return;
698
+ }
699
+ lines.push(`A manifest was present (parsed: ${manifest.parsed ? "yes" : "no"}, ` +
700
+ `recognized: ${manifest.recognized ? "yes" : "no"}). All manifest fields ` +
701
+ "are unsigned and advisory; the audit never treats them as evidence.");
702
+ lines.push("");
703
+ if (manifest.contentsHash !== undefined) {
704
+ lines.push(`Declared contents hash ${manifest.contentsHash.match ? "matches" : "does NOT match"} ` +
705
+ `the computed value. Declared: ${inlineCode(manifest.contentsHash.declaredB64)}. ` +
706
+ `Computed: ${inlineCode(manifest.contentsHash.computedB64)}.`);
707
+ }
708
+ else {
709
+ lines.push(`Computed deterministic contents hash: ${inlineCode(report.input.computedContentsHashB64)} ` +
710
+ "(the manifest declared none).");
711
+ }
712
+ if (manifest.problems.length > 0) {
713
+ lines.push("");
714
+ lines.push("Manifest field problems (advisory):");
715
+ for (const problem of manifest.problems)
716
+ lines.push(`- ${problem}`);
717
+ }
718
+ lines.push("");
719
+ }
720
+ // ---------------------------------------------------------------------------
721
+ // Plain-language meaning per stable code
722
+ // ---------------------------------------------------------------------------
723
+ function codeMeaning(code) {
724
+ switch (code) {
725
+ case "unsupported-version":
726
+ return "A proof-shaped file whose version is not bitgraph/1 was rejected at ingest and excluded from every analysis.";
727
+ case "proofhash-mismatch":
728
+ return "A stored copy carries a proofHash label that does not match the identity computed from the proof's own signed content. The computed identity governs; the label is never trusted.";
729
+ case "exact-duplicate":
730
+ return "The same proof file appears more than once, byte for byte. Harmless; copies collapse into one observed proof.";
731
+ case "semantic-duplicate":
732
+ return "The same proof appears in more than one byte encoding. Harmless; identity is computed from the signed content, not the file bytes.";
733
+ case "unsafe-path":
734
+ return "A container entry declared a path that could escape the bundle root. The entry was skipped and never interpreted.";
735
+ case "duplicate-path":
736
+ return "Two container entries normalized to the same path; the later entry was used.";
737
+ case "manifest-unparseable":
738
+ return "The root manifest.json did not parse. The manifest is advisory; nothing about the proofs changes.";
739
+ case "manifest-unrecognized-version":
740
+ return "The manifest's version field is missing or unrecognized, so the manifest was reported but not interpreted.";
741
+ case "manifest-field-invalid":
742
+ return "A manifest field fails the bundle specification's type rules. Advisory only.";
743
+ case "manifest-contents-hash-mismatch":
744
+ return "The manifest's declared contents hash does not match the computed one. The manifest is advisory; the computed value governs.";
745
+ case "unexplained-counter-positions":
746
+ return "Counter positions inside the observed range are neither commit positions nor referenced slot positions. Such a position may be a proof absent from the bundle, or a slot that was allocated but never committed (routine and benign); this offline audit cannot distinguish them, and it does not, by itself, prove the authority failed to create or withheld any proof.";
747
+ case "counter-collision":
748
+ return "Two or more valid proofs claim the same commit counter. All are preserved; the audit tool does not choose between them.";
749
+ case "slot-collision":
750
+ return "Two or more valid proofs reference the same slot counter. All are preserved; the audit tool does not choose between them.";
751
+ case "cross-kind-position-reuse":
752
+ return "A commit counter in one proof equals a different valid proof's slot position in the same partition: one causal position allocated twice across kinds, which only enclave malfunction, replay, or compromise produces. All parties are preserved; the audit tool does not choose between them.";
753
+ case "predecessor-reuse":
754
+ return "Two or more valid proofs name the same predecessor: the chain forks at that point. All branches are preserved; the audit tool does not choose between them.";
755
+ case "chain-break-missing":
756
+ return "A proof names a predecessor absent from the bundle. History before that point cannot be reconstructed from the supplied evidence; the absence does not, by itself, prove the predecessor never existed.";
757
+ case "chain-break-malformed":
758
+ return "A proof's predecessor reference is not a well-formed hash and can never match any proof.";
759
+ case "chain-break-cross-partition":
760
+ return "A proof's predecessor reference resolves to a proof under a different signer, epoch, or chain. Predecessor links never bridge those boundaries.";
761
+ case "multiple-genesis":
762
+ return "More than one proof in the same partition starts a chain without naming a predecessor. A single such proof is the normal start of an epoch; several need adjudication.";
763
+ case "slot-order-violation":
764
+ return "A proof's slot position is not strictly before its commit position, violating the slot-then-commit ordering.";
765
+ case "epochlink-terminal-missing":
766
+ return "An epoch's first proof links to a prior epoch that is observed, but the specific proof it names is absent from the bundle.";
767
+ case "epochlink-dangling":
768
+ return "An epoch's first proof links to a prior epoch and proof, neither of which appears in the bundle.";
769
+ case "epochlink-fork":
770
+ return "Two or more successor epochs claim the same predecessor terminal proof. All claims are preserved; the audit tool does not choose between them.";
771
+ case "epochlink-cycle":
772
+ return "Epoch lineage links form a cycle, so the claimed epoch ordering is self-contradictory. No ordering is asserted for the affected pairs.";
773
+ case "epochlink-mismatch":
774
+ return "An epoch lineage link matches an observed proof, but the link's declared epoch, key, or counter disagrees with that proof. The link is not used as ordering evidence.";
775
+ case "mid-epoch-signer-change":
776
+ return "More than one signer key appears within a single epoch. An epoch is one uninterrupted run of the authority under one key, so this requires explanation.";
777
+ case "mid-epoch-measurement-change":
778
+ return "More than one declared software measurement appears within a single epoch, which a single uninterrupted run does not produce.";
779
+ case "anchor-metadata-disagreement":
780
+ return "A proof's signed content identifies it as an Ethereum anchor while its unsigned metadata says otherwise. The signed content governs.";
781
+ case "anchor-metadata-only-claim":
782
+ return "Unsigned metadata claims a proof is an Ethereum anchor, but the signed content does not. Metadata alone never makes a proof an anchor.";
783
+ case "anchor-title-unparseable":
784
+ return "An anchor's signed block reference does not parse as an Etherscan block URL, so the block number is treated as absent rather than guessed.";
785
+ case "witness-malformed":
786
+ return "An anchor witness file fails the bundle specification's field rules. It confers nothing.";
787
+ case "witness-rlp-invalid":
788
+ return "An anchor witness's block header bytes are not a well-formed header encoding. It confers nothing.";
789
+ case "witness-header-shape":
790
+ return "A witness header decoded, but the block number or timestamp field is missing or malformed where the specification requires it.";
791
+ case "witness-hash-mismatch":
792
+ return "The block fingerprint recomputed from the witness header does not equal the fingerprint the authority signed. The witness confers nothing.";
793
+ case "witness-digest-mismatch":
794
+ return "The signed block fingerprint is not correctly bound to the anchor proof's artifact digest. The witness confers nothing.";
795
+ case "witness-block-number-mismatch":
796
+ return "The witness header's block number disagrees with the witness's claim or the signed block reference.";
797
+ case "witness-claimed-hash-mismatch":
798
+ return "The witness's claimed block fingerprint does not equal the one recomputed from its own header bytes.";
799
+ case "witness-anchor-invalid":
800
+ return "The candidate anchor proof is not cryptographically valid, and a witness cannot rescue an invalid proof.";
801
+ case "witness-unmatched":
802
+ return "A witness's block fingerprint matches no observed anchor. It confers nothing.";
803
+ case "attestation-invalid":
804
+ return "An attestation document is present but failed offline cryptographic validation. Its contents prove nothing.";
805
+ case "attestation-measurement-mismatch":
806
+ return "A validated attestation document's measurement does not equal the measurement the proof declares.";
807
+ case "attestation-user-data-mismatch":
808
+ return "A validated attestation document is not bound to the specific proof that carries it.";
809
+ default:
810
+ return "See the anomaly details section.";
811
+ }
812
+ }
813
+ // ---------------------------------------------------------------------------
814
+ // Formatting helpers
815
+ // ---------------------------------------------------------------------------
816
+ function partitionLabel(partition) {
817
+ return (`signer ${inlineCode(shorten(partition.key.publicKeyB64))}, ` +
818
+ `epoch ${partition.key.epochId !== undefined ? inlineCode(partition.key.epochId) : "(none)"}, ` +
819
+ `chain ${inlineCode(partition.key.chainId)}`);
820
+ }
821
+ function samePartition(a, b) {
822
+ return a.publicKeyB64 === b.publicKeyB64 && a.epochId === b.epochId && a.chainId === b.chainId;
823
+ }
824
+ function shorten(value) {
825
+ return value.length > 16 ? `${value.slice(0, 12)}...` : value;
826
+ }
827
+ function inlineCode(value) {
828
+ return `\`${value.replace(/`/g, "'")}\``;
829
+ }
830
+ /** Unix seconds as "seconds (ISO UTC)". Pure conversion; no clock read. */
831
+ function formatTimestamp(unixSeconds) {
832
+ return `${unixSeconds} (${new Date(unixSeconds * 1000).toISOString()})`;
833
+ }
834
+ /** Thousands separators for prose, matching the brief's style examples. */
835
+ function withCommas(value) {
836
+ const s = String(value);
837
+ if (!/^\d+$/.test(s))
838
+ return s;
839
+ return s.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
840
+ }
841
+ function plural(count, singular, pluralForm) {
842
+ return String(count) === "1" ? singular : pluralForm;
843
+ }
844
+ function table(headers, rows) {
845
+ const escape = (cell) => cell.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
846
+ const out = [];
847
+ out.push(`| ${headers.map(escape).join(" | ")} |`);
848
+ out.push(`| ${headers.map(() => "---").join(" | ")} |`);
849
+ for (const row of rows)
850
+ out.push(`| ${row.map(escape).join(" | ")} |`);
851
+ return out;
852
+ }
853
+ //# sourceMappingURL=report-md.js.map