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)}
    Source
    ${escapeHtml(classification.sourceName)} — ${escapeHtml(classification.sourceSection)}

    ${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)}
    ` : ""; 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 ? `

    Classification

    ` : ""} ${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)}

    PSBT Interop Lab

    Run ${escapeHtml(manifest.runId)} · ${runtimeSummary}

    ${selectionSummary}
    ${counts.passed}Passed
    ${counts.failed}Failed
    ${counts.unsupported}Unsupported
    ${counts.skipped}Skipped
    ${counts.findings}Findings

    Scenarios

    ${scenarios}

    Implementations

    ${adapters}
    NameVersionSource revisionArtifact digest

    Private checkpoints

    ${checkpoints}
    ScenarioStageSizeSHA256
    `; }