import type { RunManifest } from "./artifacts.js";
import {
classifyScenario,
type ReportClassification,
type ReportRepairability,
} from "./classification.js";
const SECRET_KEY = /(private|secret|password|mnemonic|seed|wif)/i;
const PSBT_VALUE = /cHNidP8[A-Za-z0-9+/]*={0,2}/g;
const LABELED_MULTIWORD_SECRET = /\b(mnemonic|seed(?:\s+phrase)?)\s*[:=]\s*[^,;\r\n]+/gi;
const LABELED_SECRET =
/\b(private(?:\s+key)?|secret|password|mnemonic|seed|wif)\s*[:=]\s*[^\s,;]+/gi;
const WIF_VALUE = /\b[5KLc9][1-9A-HJ-NP-Za-km-z]{50,51}\b/g;
const EXTENDED_PRIVATE_KEY = /\b[xyzuvt]prv[1-9A-HJ-NP-Za-km-z]{100,110}\b/gi;
export function redactSensitiveText(value: string): string {
return value
.replace(PSBT_VALUE, "[redacted:psbt]")
.replace(
LABELED_MULTIWORD_SECRET,
(match) => `${match.slice(0, match.search(/[:=]/) + 1)}[redacted:secret]`,
)
.replace(
LABELED_SECRET,
(match) => `${match.slice(0, match.search(/[:=]/) + 1)}[redacted:secret]`,
)
.replace(WIF_VALUE, "[redacted:secret]")
.replace(EXTENDED_PRIVATE_KEY, "[redacted:secret]");
}
export function redactValue(value: unknown, key = "", depth = 0): unknown {
if (depth > 20) {
return "[redacted:depth-limit]";
}
if (SECRET_KEY.test(key)) {
return "[redacted:secret]";
}
if (key.toLowerCase() === "psbt") {
return "[redacted:psbt]";
}
if (typeof value === "string") {
return redactSensitiveText(value);
}
if (Array.isArray(value)) {
return value.map((item) => redactValue(item, "", depth + 1));
}
if (typeof value === "object" && value !== null) {
return Object.fromEntries(
Object.entries(value).map(([childKey, childValue]) => [
childKey,
redactValue(childValue, childKey, depth + 1),
]),
);
}
return value;
}
export function generateJsonReport(manifest: RunManifest): unknown {
return redactValue({
...manifest,
scenarios: manifest.scenarios.map((scenario) => ({
...scenario,
classifications: classifyScenario(scenario),
})),
note: "Raw PSBTs are intentionally stored only in private checkpoint files.",
});
}
function repairabilityLabel(repairability: ReportRepairability): string {
switch (repairability) {
case "code-or-dependency-change":
return "Code or dependency change";
case "investigation-required":
return "Investigation required";
case "not-a-code-defect":
return "Not classified as a code defect";
}
}
function normalizedMarkdownValue(value: string | number): string {
return redactSensitiveText(String(value)).replace(/[\r\n]+/g, " ");
}
function markdownText(value: string | number): string {
return normalizedMarkdownValue(value)
.replaceAll("[redacted:secret]", "\u0000secret\u0000")
.replaceAll("[redacted:psbt]", "\u0000psbt\u0000")
.replaceAll("[redacted:depth-limit]", "\u0000depth\u0000")
.replaceAll("\\", "\\\\")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replace(/([`*_[\]{}()#+\-!|])/g, "\\$1")
.replaceAll("\u0000secret\u0000", "[redacted:secret]")
.replaceAll("\u0000psbt\u0000", "[redacted:psbt]")
.replaceAll("\u0000depth\u0000", "[redacted:depth-limit]");
}
function markdownCode(value: string | number): string {
const normalized = normalizedMarkdownValue(value);
const backtickRuns = normalized.match(/`+/g) ?? [];
if (backtickRuns.length === 0) return `\`${normalized}\``;
const fence = "`".repeat(Math.max(...backtickRuns.map((run) => run.length)) + 1);
return `${fence} ${normalized} ${fence}`;
}
function markdownClassification(classification: ReportClassification): string[] {
return [
`- **${markdownText(classification.label)}** (${markdownCode(classification.id)}, rule ${markdownCode(classification.ruleId)})`,
` - Severity: **${classification.severity.toUpperCase()}**`,
` - Normative level: ${markdownCode(classification.normativeLevel)}`,
` - Observed at: ${markdownCode(classification.observedAt)}`,
` - Repairability: ${markdownCode(classification.repairability)}`,
` - Confidence: ${markdownCode(classification.confidence)}`,
` - Source: [${markdownText(classification.sourceName)} — ${markdownText(classification.sourceSection)}](${classification.sourceUrl})`,
` - Expected: ${markdownText(classification.expected)}`,
` - Observed: ${classification.actual.map(markdownText).join("; ")}`,
` - ${markdownText(classification.summary)}`,
` - Evidence: ${classification.evidence.map(markdownCode).join(", ")}`,
];
}
export function generateMarkdownReport(manifest: RunManifest): string {
const redactedManifest = redactValue(manifest) as RunManifest;
const filtered =
(redactedManifest.selectors?.requested.scenarios?.length ?? 0) > 0 ||
redactedManifest.selectors?.requested.category !== undefined;
const selection = filtered
? `Filtered run: requested ${redactedManifest.selectors?.requested.scenarios?.map(markdownCode).join(", ") || "all scenarios"}${redactedManifest.selectors?.requested.category ? ` in category ${markdownCode(redactedManifest.selectors.requested.category)}` : ""}; executed ${redactedManifest.selectors?.executed.scenarios.length ?? 0} scenario(s).`
: undefined;
const lines = [
"# PSBT Interop Lab Proof",
"",
`Run: ${markdownCode(redactedManifest.runId)}`,
`Outcome: **${redactedManifest.outcome.toUpperCase()}**`,
redactedManifest.core
? `Bitcoin Core: ${markdownCode(redactedManifest.core.subversion)} on regtest at height ${redactedManifest.core.blocks}`
: "Bitcoin Core: not required by selected scenarios",
...(selection ? [selection] : []),
"",
"## Scenarios",
"",
];
for (const scenario of redactedManifest.scenarios) {
const classifications = classifyScenario(scenario);
lines.push(
`### ${markdownText(scenario.title)}`,
"",
`Scenario: ${markdownCode(scenario.id)}`,
`Outcome: **${scenario.outcome.toUpperCase()}**`,
`Duration: ${scenario.durationMs.toFixed(3)} ms`,
"",
markdownText(scenario.summary),
"",
);
if (classifications.length > 0) {
lines.push("Classifications:", "", ...classifications.flatMap(markdownClassification), "");
}
if (scenario.infrastructureError) {
lines.push(
`Infrastructure error: ${markdownCode(scenario.infrastructureError.errorClass)} - ${markdownText(scenario.infrastructureError.message)}`,
"",
);
}
if (scenario.missingCapabilities) {
lines.push(
"Missing capabilities:",
"",
...scenario.missingCapabilities.map(
(missing) =>
`- ${markdownCode(missing.adapter)}: ${markdownText(missing.kind)} ${markdownCode(missing.value)}`,
),
"",
);
}
if (scenario.adapterCells?.length) {
lines.push("Adapter cells:", "");
for (const cell of scenario.adapterCells) {
const diagnostics = [
`request=${markdownCode(cell.requestId)}`,
`duration=${markdownText(cell.durationMs.toFixed(3))} ms`,
cell.errorClass ? `error=${markdownCode(cell.errorClass)}` : undefined,
cell.restarted !== undefined ? `restarted=${cell.restarted ? "yes" : "no"}` : undefined,
].filter((value): value is string => value !== undefined);
lines.push(
`- **${cell.status.toUpperCase()}** ${markdownCode(cell.adapter)} ${markdownCode(cell.operation)} (${diagnostics.join(", ")}): ${markdownText(cell.detail)}`,
);
}
lines.push("");
}
if (scenario.assertions.length > 0) {
lines.push("Assertions:", "");
for (const assertion of scenario.assertions) {
const diagnostics = [
assertion.policy ? `policy=${markdownText(assertion.policy)}` : undefined,
assertion.exactBytesEqual !== undefined
? `exact-bytes=${assertion.exactBytesEqual ? "yes" : "no"}`
: undefined,
assertion.likelyImplementation
? `observed-implementation=${markdownText(assertion.likelyImplementation)}`
: undefined,
].filter((value): value is string => value !== undefined);
lines.push(
`- **${assertion.passed ? "PASS" : "FAIL"}** ${markdownCode(assertion.name)}${diagnostics.length > 0 ? ` (${diagnostics.join(", ")})` : ""}`,
);
for (const failure of assertion.failures ?? []) {
const location =
failure.location.kind === "global"
? "global"
: `${failure.location.kind}[${failure.location.index}]`;
const field = failure.field;
lines.push(
field
? ` - ${markdownCode(failure.code)} at ${markdownText(location)}: ${markdownCode(field.symbol)} (${markdownText(field.displayName)}, ${markdownCode(field.keyTypeHex)}${field.bip ? `, ${markdownText(field.bip)}` : ""})`
: ` - ${markdownCode(failure.code)} at ${markdownText(location)}, key type ${markdownCode(`0x${failure.keyType.toString(16).padStart(2, "0")}`)}`,
);
if (failure.guidance) {
lines.push(
` - Guidance **${failure.guidance.severity.toUpperCase()}** ${markdownCode(failure.guidance.code)}: ${markdownText(failure.guidance.summary)}`,
...failure.guidance.nextSteps.map((step) => ` - ${markdownText(step)}`),
);
}
}
}
lines.push("");
}
if (scenario.expectedFailure) {
lines.push(
`Expected historical failure: ${markdownCode(scenario.expectedFailure.implementation)} returned ${markdownCode(scenario.expectedFailure.errorClass)}.`,
"",
);
}
if (scenario.findings?.length) {
lines.push(
"Compatibility findings:",
"",
...scenario.findings.map(
(finding) =>
`- ${markdownCode(finding.id)} in ${markdownCode(finding.implementation)}: ${markdownText(finding.summary)}`,
),
"",
);
}
if (scenario.transactionId) {
const transactionIdLabel =
scenario.policyAccepted === true ? "Policy-accepted txid" : "Core-confirmed txid";
lines.push(`${transactionIdLabel}: ${markdownCode(scenario.transactionId)}`, "");
}
if (scenario.skipReason) {
lines.push(`Skip reason: ${markdownText(scenario.skipReason)}`, "");
}
}
lines.push(
"## Checkpoints",
"",
...redactedManifest.checkpoints.map(
(checkpoint) =>
`- ${markdownCode(`${checkpoint.scenario}/${checkpoint.stage}`)}: ${checkpoint.facts.byteLength} bytes, SHA256 ${markdownCode(checkpoint.facts.sha256)}`,
),
"",
"Raw PSBTs are stored only in the private checkpoint files beside this report.",
);
return lines.join("\n");
}
function escapeHtml(value: string | number): string {
return redactSensitiveText(String(value))
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
function failureLocation(
location: RunManifest["scenarios"][number]["assertions"][number]["failures"] extends
| readonly (infer Failure)[]
| undefined
? Failure extends { location: infer Location }
? Location
: never
: never,
): string {
return location.kind === "global" ? "global" : `${location.kind}[${location.index}]`;
}
export function generateHtmlReport(manifest: RunManifest): string {
const counts = {
passed: manifest.scenarios.filter((scenario) => scenario.outcome === "passed").length,
failed: manifest.scenarios.filter((scenario) => scenario.outcome === "failed").length,
unsupported: manifest.scenarios.filter((scenario) => scenario.outcome === "unsupported").length,
skipped: manifest.scenarios.filter((scenario) => scenario.outcome === "skipped").length,
findings: manifest.scenarios.reduce(
(count, scenario) => count + (scenario.findings?.length ?? 0),
0,
),
};
const scenarios = manifest.scenarios
.map((scenario) => {
const classifications = classifyScenario(scenario);
const classificationHtml = classifications
.map(
(
classification,
) => `
${escapeHtml(classification.label)}${escapeHtml(classification.ruleId)}
- Category
${escapeHtml(classification.id)}
- Severity
- ${escapeHtml(classification.severity.toUpperCase())}
- Normative level
${escapeHtml(classification.normativeLevel)}
- Observed at
${escapeHtml(classification.observedAt)}
- Repairability
- ${escapeHtml(repairabilityLabel(classification.repairability))}
- Confidence
- ${escapeHtml(classification.confidence)}
${escapeHtml(classification.summary)}
Expected: ${escapeHtml(classification.expected)}
Observed: ${escapeHtml(classification.actual.join("; "))}
Evidence: ${classification.evidence.map((evidence) => `${escapeHtml(evidence)}`).join(" · ")}
`,
)
.join("");
const assertions = scenario.assertions
.map((assertion) => {
const diagnostics = [
assertion.policy ? `policy ${assertion.policy}` : undefined,
assertion.exactBytesEqual !== undefined
? `exact bytes ${assertion.exactBytesEqual ? "yes" : "no"}`
: undefined,
assertion.likelyImplementation
? `Observed implementation ${assertion.likelyImplementation}`
: undefined,
].filter((value): value is string => value !== undefined);
const failures = (assertion.failures ?? [])
.map((failure) => {
const field = failure.field;
const fieldDescription = field
? `${escapeHtml(field.symbol)} (${escapeHtml(field.displayName)}, ${escapeHtml(field.keyTypeHex)}${field.bip ? `, ${escapeHtml(field.bip)}` : ""})`
: `key 0x${escapeHtml(failure.keyType.toString(16).padStart(2, "0"))}`;
const guidance = failure.guidance
? `${escapeHtml(failure.guidance.severity.toUpperCase())} ${escapeHtml(failure.guidance.code)}: ${escapeHtml(failure.guidance.summary)}
${failure.guidance.nextSteps.map((step) => `- ${escapeHtml(step)}
`).join("")}
`
: "";
return `${escapeHtml(failure.code)} at ${escapeHtml(failureLocation(failure.location))}: ${fieldDescription}${guidance}`;
})
.join("");
return `
${assertion.passed ? "PASS" : "FAIL"} ${escapeHtml(assertion.name)}${diagnostics.length > 0 ? ` ${escapeHtml(diagnostics.join(" · "))}` : ""}
${assertion.summary ? `${escapeHtml(assertion.summary)}
` : ""}
${failures ? `` : ""}
`;
})
.join("");
const missing = (scenario.missingCapabilities ?? [])
.map(
(capability) =>
`${escapeHtml(capability.adapter)} lacks ${escapeHtml(capability.kind)} ${escapeHtml(capability.value)}`,
)
.join("");
const findings = (scenario.findings ?? [])
.map(
(finding) =>
`${escapeHtml(finding.id)} in ${escapeHtml(finding.implementation)}: ${escapeHtml(finding.summary)}`,
)
.join("");
const adapterCells = (scenario.adapterCells ?? [])
.map((cell) => {
const diagnostics = [
`request ${cell.requestId}`,
`${cell.durationMs.toFixed(3)} ms`,
cell.errorClass ? `error ${cell.errorClass}` : undefined,
cell.restarted !== undefined ? `restarted ${cell.restarted ? "yes" : "no"}` : undefined,
].filter((value): value is string => value !== undefined);
return `
${escapeHtml(cell.status.toUpperCase())} ${escapeHtml(cell.adapter)} ${escapeHtml(cell.operation)}
${escapeHtml(diagnostics.join(" · "))}
${escapeHtml(cell.detail)}
`;
})
.join("");
const infrastructure = scenario.infrastructureError
? `Infrastructure error: ${escapeHtml(scenario.infrastructureError.errorClass)} ${escapeHtml(scenario.infrastructureError.message)}
`
: "";
return `
${escapeHtml(scenario.outcome.toUpperCase())}${escapeHtml(scenario.category)}
${escapeHtml(scenario.title)}
${escapeHtml(scenario.id)} · ${escapeHtml(scenario.durationMs.toFixed(3))} ms
${escapeHtml(scenario.summary)}
${classificationHtml ? `` : ""}
${infrastructure}
${scenario.expectedFailure ? `Expected failure: ${escapeHtml(scenario.expectedFailure.implementation)} · ${escapeHtml(scenario.expectedFailure.errorClass)}
` : ""}
${findings ? `Compatibility findings
` : ""}
${missing ? `Missing capabilities
` : ""}
${adapterCells ? `Adapter cells
` : ""}
${assertions ? `Assertions (${scenario.assertions.length})
` : ""}
`;
})
.join("\n");
const adapters = manifest.adapters
.map(
(adapter) => `
${escapeHtml(adapter.name)} |
${escapeHtml(adapter.version)} |
${escapeHtml(adapter.sourceRevision ?? "not declared")} |
${escapeHtml(adapter.artifactDigest)} |
`,
)
.join("\n");
const checkpoints = manifest.checkpoints
.map(
(checkpoint) => `
${escapeHtml(checkpoint.scenario)} |
${escapeHtml(checkpoint.stage)} |
${escapeHtml(checkpoint.facts.byteLength)} bytes |
${escapeHtml(checkpoint.facts.sha256)} |
`,
)
.join("\n");
const runtimeSummary = manifest.core
? `${escapeHtml(manifest.core.subversion)} · regtest height ${escapeHtml(manifest.core.blocks)} · ${escapeHtml(manifest.core.connections)} peers`
: "Bitcoin Core not required by selected scenarios";
const filtered =
(manifest.selectors?.requested.scenarios?.length ?? 0) > 0 ||
manifest.selectors?.requested.category !== undefined;
const selectionSummary = filtered
? `Filtered run · requested ${escapeHtml(manifest.selectors?.requested.scenarios?.join(", ") || "all scenarios")}${manifest.selectors?.requested.category ? ` · category ${escapeHtml(manifest.selectors.requested.category)}` : ""} · executed ${escapeHtml(manifest.selectors?.executed.scenarios.length ?? 0)} scenario(s)
`
: "";
return `
PSBT Interop Lab · ${escapeHtml(manifest.runId)}
${counts.passed}Passed
${counts.failed}Failed
${counts.unsupported}Unsupported
${counts.skipped}Skipped
${counts.findings}Findings
Scenarios
${scenarios}
Implementations
| Name | Version | Source revision | Artifact digest |
${adapters}
Private checkpoints
| Scenario | Stage | Size | SHA256 |
${checkpoints}
`;
}