#!/usr/bin/env python3
"""Build an external proof packet for the predecessor fill/order blocker."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
PREDECESSOR_FILL_EXTERNAL_EVIDENCE_REFS = [
    {
        "path": "out/save_selector_predecessor_fill_execution_order_gap.json",
        "fields": [
            "predecessorFillProofGateRows",
            "proofFound",
            "failedPredecessorFillOrderGateIds",
            "missingEvidence",
            "remainingProofs",
        ],
    },
    {
        "path": "out/save_selector_predecessor_fill_site_execution_context.json",
        "fields": [
            "requiredProofGates",
            "fillSiteExecutionContextProven",
            "failedPredecessorFillGateIds",
            "missingEvidence",
            "remainingProofs",
        ],
    },
    {
        "path": "out/save_selector_predecessor_fill_opcode10_context.json",
        "fields": [
            "opcodeHandlerHex",
            "decodedOpcode10Rows",
            "proofFound",
            "failedPredecessorFillOpcode10GateIds",
            "missingEvidence",
        ],
    },
    {
        "path": "out/save_selector_predecessor_branch_state_execution_gap.json",
        "fields": [
            "fillExecutionOrderProofFound",
            "runtimePredecessorFillObserved",
            "failedBranchStateExecutionGateIds",
            "missingEvidence",
        ],
    },
    {
        "path": "out/save_selector_predecessor_descriptor_bridge_gap.json",
        "fields": [
            "descriptorBridgeProofFound",
            "descriptorEdgeRejectionClassification",
            "failedDescriptorBridgeGateIds",
            "missingEvidence",
        ],
    },
    {
        "path": "out/save_selector_merge_runtime_context.json",
        "fields": [
            "selectorMergeGapOpen",
            "selectorMergeRuntimeProofFound",
            "failedSelectorMergeRuntimeGateIds",
            "missingEvidence",
            "routeOrderProven",
        ],
    },
]


def load_json(path: Path, fallback: Any) -> Any:
    if not path.exists():
        return fallback
    return json.loads(path.read_text(encoding="utf-8"))


def runtime_observation_summary(context: dict, branch: dict) -> dict:
    split = branch.get("runtimeBranchStateSplit") or {}
    fill_match_count = context.get("branchStatePollFillMatchCount")
    current_root_hits = context.get("branchStatePollCurrentRootHitCount")
    route_hits = context.get("branchStatePollRouteSelectorHitCount")
    target_fill_matches = context.get("branchStatePollTargetObservationFillMatchCount")
    accepted_signal_count = sum(
        value or 0
        for value in [
            fill_match_count,
            current_root_hits,
            route_hits,
            target_fill_matches,
        ]
        if isinstance(value, int)
    )
    return {
        "classification": split.get("classification"),
        "pollCount": context.get("branchStatePollCount"),
        "sequenceCount": context.get("branchStatePollSequenceCount"),
        "sampleCount": context.get("branchStatePollSampleCount"),
        "publicPredecessorHitCount": context.get("branchStatePollPublicPredecessorHitCount"),
        "currentRootHitCount": current_root_hits,
        "routeSelectorHitCount": route_hits,
        "allZeroCount": context.get("branchStatePollAllZeroCount"),
        "fillMatchCount": fill_match_count,
        "movementOrTargetCount": context.get("branchStatePollMovementOrTargetCount"),
        "movementOrTargetSampleCount": context.get("branchStatePollMovementOrTargetSampleCount"),
        "targetObservationCount": context.get("branchStatePollTargetObservationCount"),
        "targetObservationSampleCount": context.get("branchStatePollTargetObservationSampleCount"),
        "targetObservationFillMatchCount": target_fill_matches,
        "targetObservationCurrentRootHitCount": context.get("branchStatePollTargetObservationCurrentRootHitCount"),
        "targetObservationRouteSelectorHitCount": context.get("branchStatePollTargetObservationRouteSelectorHitCount"),
        "targetObservationStatus": context.get("branchStatePollTargetObservationStatus"),
        "observedStateHexes": split.get("observedStateHexes") or [],
        "expectedFillHexes": split.get("expectedFillHexes") or [],
        "observedMatchesFill": split.get("observedMatchesFill"),
        "observedAllZero": split.get("observedAllZero"),
        "activeSelectionFlagHex": split.get("activeSelectionFlagHex"),
        "staticResetScopeClosed": split.get("staticResetScopeClosed"),
        "selectorOrderResetGapClosed": split.get("selectorOrderResetGapClosed"),
        "acceptedSignalCount": accepted_signal_count,
        "acceptedSignalPresent": accepted_signal_count > 0,
        "nextProofFocus": split.get("nextProofFocus"),
        "nonPromotingReason": (
            "public predecessor selector is observed, but branch-state values stay all-zero "
            "and never match the predecessor fill hypothesis before the current reader"
        ),
    }


def build_summary(out_dir: Path = OUT) -> dict:
    order = load_json(out_dir / "save_selector_predecessor_fill_execution_order_gap.json", {})
    context = load_json(out_dir / "save_selector_predecessor_fill_site_execution_context.json", {})
    opcode10 = load_json(out_dir / "save_selector_predecessor_fill_opcode10_context.json", {})
    branch = load_json(out_dir / "save_selector_predecessor_branch_state_execution_gap.json", {})
    descriptor = load_json(out_dir / "save_selector_predecessor_descriptor_bridge_gap.json", {})
    merge = load_json(out_dir / "save_selector_merge_runtime_context.json", {})

    gate_rows = order.get("predecessorFillProofGateRows") or context.get("requiredProofGates") or []
    failed_gate_ids = (
        order.get("failedPredecessorFillOrderGateIds")
        or order.get("predecessorFillProofGateBlockedIds")
        or []
    )
    missing_evidence = order.get("missingEvidence") or context.get("missingEvidence") or []
    return {
        "source": SOURCE,
        "target": TARGET,
        "promotionStatus": "blocked",
        "predecessorSelector": order.get("predecessorSelector") or context.get("predecessorSelector"),
        "currentSelector": order.get("currentSelector") or context.get("currentSelector"),
        "predecessorRootHex": order.get("predecessorRootHex") or context.get("predecessorRootHex"),
        "currentRootHex": order.get("currentRootHex") or context.get("currentRootHex"),
        "currentReaderHex": order.get("currentReaderHex") or context.get("currentReaderHex"),
        "fillSites": order.get("fillSites") or context.get("fillSites") or [],
        "proofFound": order.get("proofFound"),
        "predecessorFillExternalProofFound": order.get("proofFound"),
        "failedPredecessorFillExternalGateIds": failed_gate_ids,
        "missingEvidence": missing_evidence,
        "fillSiteExecutionContextProven": context.get("fillSiteExecutionContextProven"),
        "fillExecutionOrderProofFound": branch.get("fillExecutionOrderProofFound"),
        "runtimePredecessorFillObserved": branch.get("runtimePredecessorFillObserved"),
        "runtimeBranchStateSplit": branch.get("runtimeBranchStateSplit"),
        "publicPredecessorBranchStateAllZero": branch.get("publicPredecessorBranchStateAllZero"),
        "runtimeObservationSummary": runtime_observation_summary(context, branch),
        "localFillTrace": {
            "startHex": order.get("localFillTraceStartHex"),
            "stopHex": order.get("localFillTraceStopHex"),
            "stopReason": order.get("localFillTraceStopReason"),
            "containsAllFillSites": order.get("localFillTraceContainsAllFillSites"),
            "reachesCurrentReader": order.get("localFillTraceReachesCurrentReader"),
        },
        "proofGateCount": order.get("predecessorFillProofGateCount"),
        "proofGatePassCount": order.get("predecessorFillProofGatePassCount"),
        "proofGateBlockedCount": order.get("predecessorFillProofGateBlockedCount"),
        "proofGateBlockedIds": order.get("predecessorFillProofGateBlockedIds") or [],
        "proofGatesAllBlocked": order.get("predecessorFillAllProofGatesBlocked"),
        "proofGateRows": gate_rows,
        "opcode10Evidence": {
            "handlerHex": opcode10.get("opcodeHandlerHex"),
            "handlerMatchesExpected": opcode10.get("opcodeHandlerMatchesExpected"),
            "streamEffect": opcode10.get("opcodeHandlerStreamEffectText"),
            "decodedRows": opcode10.get("decodedOpcode10Rows") or [],
            "expectedFillHexes": opcode10.get("decodedOpcode10ExpectedFillHexes") or [],
            "expectedStatePrefix": opcode10.get("decodedOpcode10ExpectedStatePrefix") or [],
            "runtimeObservedAllZero": opcode10.get("runtimeObservedAllZero"),
            "branchStatePollSampleCount": opcode10.get("branchStatePollSampleCount"),
            "branchStatePollFillMatchCount": opcode10.get("branchStatePollFillMatchCount"),
            "proofFound": opcode10.get("proofFound"),
        },
        "dispatchAndDescriptorEvidence": {
            "descriptorBridgeProofFound": descriptor.get("descriptorBridgeProofFound"),
            "rootStopToFillBridgeFound": descriptor.get("rootStopToFillBridgeFound"),
            "fillStopToCurrentBridgeFound": descriptor.get("fillStopToCurrentBridgeFound"),
            "descriptorEdgeRejectionClassification": descriptor.get("descriptorEdgeRejectionClassification"),
            "sliceRuntimeProofFound": order.get("predecessorDispatchSliceRuntimeProofFound"),
            "tableBaseRejectionClassification": order.get("predecessorDispatchTableBaseRejectionClassification"),
            "dynamicTableBaseCandidateCount": order.get(
                "predecessorDispatchDynamicSaveSelectorTableBaseCandidateCount"
            ),
        },
        "selectorMergeEvidence": {
            "selectorMergeGapOpen": merge.get("selectorMergeGapOpen"),
            "selectorMergeRuntimeProofFound": merge.get("selectorMergeRuntimeProofFound"),
            "forwardBridgeAbsent": merge.get("forwardBridgeAbsent"),
            "reverseReuseBeforeFillOnly": merge.get("reverseReuseBeforeFillOnly"),
            "routeOrderProven": order.get("routeOrderProven") or context.get("routeOrderProven"),
        },
        "acceptedEvidenceChecklist": [
            {
                "requirement": "predecessor fill fragment executes before current reader 0x00542b0c",
                "currentStatus": "missing",
                "acceptedSignal": "runtimeObservedPredecessorFill == true or localFillTraceReachesCurrentReader == true",
            },
            {
                "requirement": "save-selector slice dispatch is proven for the predecessor descriptor boundary",
                "currentStatus": "missing",
                "acceptedSignal": "predecessorDispatchSliceRuntimeProofFound == true",
            },
            {
                "requirement": "predecessor-to-current selector merge/order is proven",
                "currentStatus": "missing",
                "acceptedSignal": "routeOrderProven == true and selectorMergeRuntimeProofFound == true",
            },
        ],
        "notAcceptedEvidence": [
            "opcode 0x10 fill semantics without execution order",
            "public predecessor selector 1:0 observations with all-zero branch state",
            "descriptor/data-boundary reachability without a save-selector table-base runtime proof",
            "reverse current-to-predecessor data reuse before the fill sites",
        ],
        "evidenceRefs": PREDECESSOR_FILL_EXTERNAL_EVIDENCE_REFS,
        "evidenceRefCount": len(PREDECESSOR_FILL_EXTERNAL_EVIDENCE_REFS),
        "relatedReports": [
            "out/save_selector_predecessor_fill_execution_order_gap.html",
            "out/save_selector_predecessor_fill_site_execution_context.json",
            "out/save_selector_predecessor_fill_opcode10_context.json",
            "out/save_selector_predecessor_branch_state_execution_gap.html",
            "out/save_selector_predecessor_descriptor_bridge_gap.html",
            "out/save_selector_merge_runtime_context.json",
        ],
        "regenerateAndVerifyCommands": [
            "python3 tools/summarize_predecessor_fill_external_proof_packet.py",
            "python3 tools/verify_web_assets.py",
        ],
        "remainingProofs": order.get("remainingProofs") or context.get("remainingProofs") or [],
        "conclusion": (
            "Predecessor fill/order remains blocked: the opcode 0x10 fill bytes are decoded, "
            "but no normal execution/order proof carries them to the current reader."
        ),
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Predecessor Fill External Proof Packet",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- predecessor selector: `{summary.get('predecessorSelector')}`",
        f"- current selector: `{summary.get('currentSelector')}`",
        f"- current reader: `{summary.get('currentReaderHex')}`",
        f"- fill sites: `{summary.get('fillSites')}`",
        f"- proof found: {summary.get('proofFound')}",
        f"- predecessorFillExternalProofFound: {summary.get('predecessorFillExternalProofFound')}",
        f"- failed predecessor-fill external gates: `{', '.join(summary.get('failedPredecessorFillExternalGateIds') or [])}`",
        f"- missing evidence count: {len(summary.get('missingEvidence') or [])}",
        f"- evidence refs: {summary.get('evidenceRefCount')}",
        "",
        "## Proof Gates",
        "",
        f"- gate pass/block: {summary.get('proofGatePassCount')}/{summary.get('proofGateBlockedCount')}",
        f"- all blocked: {summary.get('proofGatesAllBlocked')}",
        f"- blocked ids: `{', '.join(summary.get('proofGateBlockedIds') or [])}`",
        "",
        "| gate | pass | status | detail |",
        "| --- | --- | --- | --- |",
    ]
    for row in summary.get("proofGateRows") or []:
        lines.append(
            f"| `{row.get('id')}` | {row.get('pass')} | `{row.get('status')}` | {row.get('detail')} |"
        )
    runtime = summary.get("runtimeObservationSummary") or {}
    lines.extend([
        "",
        "## Runtime Observation Summary",
        "",
        f"- classification: `{runtime.get('classification')}`",
        f"- polls/sequences/samples: {runtime.get('pollCount')}/{runtime.get('sequenceCount')}/{runtime.get('sampleCount')}",
        f"- public predecessor/current root/route hits: {runtime.get('publicPredecessorHitCount')}/{runtime.get('currentRootHitCount')}/{runtime.get('routeSelectorHitCount')}",
        f"- all-zero/fill matches: {runtime.get('allZeroCount')}/{runtime.get('fillMatchCount')}",
        f"- movement-or-target samples: {runtime.get('movementOrTargetCount')}@{runtime.get('movementOrTargetSampleCount')}",
        f"- target observations/samples/fill matches/current/root route hits: {runtime.get('targetObservationCount')}/{runtime.get('targetObservationSampleCount')}/{runtime.get('targetObservationFillMatchCount')}/{runtime.get('targetObservationCurrentRootHitCount')}/{runtime.get('targetObservationRouteSelectorHitCount')}",
        f"- target observation status: `{runtime.get('targetObservationStatus')}`",
        f"- observed state: `{runtime.get('observedStateHexes')}`",
        f"- expected fill state: `{runtime.get('expectedFillHexes')}`",
        f"- accepted signal present: {runtime.get('acceptedSignalPresent')}",
        f"- non-promoting reason: {runtime.get('nonPromotingReason')}",
        "",
        "## Local Trace",
        "",
    ])
    trace = summary.get("localFillTrace") or {}
    lines.extend([
        f"- start: `{trace.get('startHex')}`",
        f"- stop: `{trace.get('stopHex')}`",
        f"- stop reason: `{trace.get('stopReason')}`",
        f"- contains fill sites: {trace.get('containsAllFillSites')}",
        f"- reaches current reader: {trace.get('reachesCurrentReader')}",
        "",
        "## Opcode 0x10 Fill Semantics",
        "",
    ])
    op10 = summary.get("opcode10Evidence") or {}
    lines.extend([
        f"- handler: `{op10.get('handlerHex')}`",
        f"- expected fill hexes: `{op10.get('expectedFillHexes')}`",
        f"- runtime observed all zero: {op10.get('runtimeObservedAllZero')}",
        f"- branch poll samples/fill matches: {op10.get('branchStatePollSampleCount')}/{op10.get('branchStatePollFillMatchCount')}",
        "",
        "## Accepted Evidence Checklist",
        "",
        "| requirement | current status | accepted signal |",
        "| --- | --- | --- |",
    ])
    for row in summary.get("acceptedEvidenceChecklist") or []:
        lines.append(f"| {row.get('requirement')} | {row.get('currentStatus')} | {row.get('acceptedSignal')} |")
    lines.extend(["", "## Missing Evidence", ""])
    lines.extend(f"- {item}" for item in summary.get("missingEvidence") or [])
    lines.extend(["", "## Evidence Refs", ""])
    lines.extend(
        f"- `{ref.get('path')}`: {', '.join(ref.get('fields') or [])}"
        for ref in summary.get("evidenceRefs") or []
    )
    lines.extend(["", "## Not Accepted Evidence", ""])
    lines.extend(f"- {item}" for item in summary.get("notAcceptedEvidence") or [])
    lines.extend(["", "## Related Reports", ""])
    lines.extend(f"- `{item}`" for item in summary.get("relatedReports") or [])
    lines.extend(["", "## Regenerate And Verify", ""])
    lines.extend(f"- `{item}`" for item in summary.get("regenerateAndVerifyCommands") or [])
    lines.extend(["", summary.get("conclusion") or "", ""])
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    def esc(value: Any) -> str:
        return html.escape(str(value))

    checklist_rows = "".join(
        "<tr>"
        f"<td>{esc(row.get('requirement'))}</td>"
        f"<td>{esc(row.get('currentStatus'))}</td>"
        f"<td>{esc(row.get('acceptedSignal'))}</td>"
        "</tr>"
        for row in summary.get("acceptedEvidenceChecklist") or []
    )
    not_accepted = "".join(f"<li>{esc(item)}</li>" for item in summary.get("notAcceptedEvidence") or [])
    missing = "".join(f"<li>{esc(item)}</li>" for item in summary.get("missingEvidence") or [])
    evidence_refs = "".join(
        f"<li><code>{esc(ref.get('path'))}</code>: {esc(', '.join(ref.get('fields') or []))}</li>"
        for ref in summary.get("evidenceRefs") or []
    )
    related = "".join(f"<li><code>{esc(item)}</code></li>" for item in summary.get("relatedReports") or [])
    commands = "".join(f"<li><code>{esc(item)}</code></li>" for item in summary.get("regenerateAndVerifyCommands") or [])
    trace = summary.get("localFillTrace") or {}
    op10 = summary.get("opcode10Evidence") or {}
    runtime = summary.get("runtimeObservationSummary") or {}
    gate_rows = "".join(
        "<tr>"
        f"<td><code>{esc(row.get('id'))}</code></td>"
        f"<td>{esc(row.get('pass'))}</td>"
        f"<td><code>{esc(row.get('status'))}</code></td>"
        f"<td>{esc(row.get('detail'))}</td>"
        "</tr>"
        for row in summary.get("proofGateRows") or []
    )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Predecessor Fill External Proof Packet</title>",
        "  <style>body{margin:24px;background:#101010;color:#eee;font:14px system-ui,sans-serif}table{border-collapse:collapse;width:100%;margin:16px 0 28px}th,td{border:1px solid #333;padding:6px 8px;vertical-align:top}code{color:#f5d76e}</style>",
        "</head>",
        "<body>",
        "  <h1>Predecessor Fill External Proof Packet</h1>",
        f"  <p>route <code>{esc(summary['source'])}</code> -&gt; <code>{esc(summary['target'])}</code>; "
        f"predecessor <code>{esc(summary.get('predecessorSelector'))}</code>; current "
        f"<code>{esc(summary.get('currentSelector'))}</code>; reader <code>{esc(summary.get('currentReaderHex'))}</code>.</p>",
        f"  <p>proof found {esc(summary.get('proofFound'))}; "
        f"predecessorFillExternalProofFound {esc(summary.get('predecessorFillExternalProofFound'))}; "
        "failed predecessor-fill external gates "
        f"<code>{esc(', '.join(summary.get('failedPredecessorFillExternalGateIds') or []))}</code>; "
        f"missing evidence count <code>{esc(len(summary.get('missingEvidence') or []))}</code>; "
        f"evidence refs <code>{esc(summary.get('evidenceRefCount'))}</code>.</p>",
        f"  <p>proof gates pass/block {esc(summary.get('proofGatePassCount'))}/{esc(summary.get('proofGateBlockedCount'))}; "
        f"all blocked {esc(summary.get('proofGatesAllBlocked'))}; blocked ids <code>{esc(', '.join(summary.get('proofGateBlockedIds') or []))}</code>.</p>",
        "  <table><thead><tr><th>gate</th><th>pass</th><th>status</th><th>detail</th></tr></thead>",
        f"  <tbody>{gate_rows}</tbody></table>",
        "  <h2>Runtime Observation Summary</h2>",
        f"  <p>classification <code>{esc(runtime.get('classification'))}</code>; "
        f"polls/sequences/samples {esc(runtime.get('pollCount'))}/{esc(runtime.get('sequenceCount'))}/{esc(runtime.get('sampleCount'))}; "
        f"public predecessor/current root/route hits {esc(runtime.get('publicPredecessorHitCount'))}/{esc(runtime.get('currentRootHitCount'))}/{esc(runtime.get('routeSelectorHitCount'))}; "
        f"all-zero/fill matches {esc(runtime.get('allZeroCount'))}/{esc(runtime.get('fillMatchCount'))}; "
        f"movement-or-target {esc(runtime.get('movementOrTargetCount'))}@{esc(runtime.get('movementOrTargetSampleCount'))}; "
        f"target observations {esc(runtime.get('targetObservationCount'))}/{esc(runtime.get('targetObservationSampleCount'))}; "
        f"target observation status <code>{esc(runtime.get('targetObservationStatus'))}</code>; "
        f"accepted signal present {esc(runtime.get('acceptedSignalPresent'))}.</p>",
        f"  <p>observed state <code>{esc(runtime.get('observedStateHexes'))}</code>; expected fill state "
        f"<code>{esc(runtime.get('expectedFillHexes'))}</code>; reason {esc(runtime.get('nonPromotingReason'))}.</p>",
        "  <h2>Local Trace</h2>",
        f"  <p>start <code>{esc(trace.get('startHex'))}</code>; stop <code>{esc(trace.get('stopHex'))}</code>; "
        f"reason <code>{esc(trace.get('stopReason'))}</code>; reaches reader {esc(trace.get('reachesCurrentReader'))}.</p>",
        "  <h2>Opcode 0x10 Fill Semantics</h2>",
        f"  <p>handler <code>{esc(op10.get('handlerHex'))}</code>; expected fills "
        f"<code>{esc(op10.get('expectedFillHexes'))}</code>; runtime all-zero "
        f"{esc(op10.get('runtimeObservedAllZero'))}; poll/fill matches "
        f"{esc(op10.get('branchStatePollSampleCount'))}/{esc(op10.get('branchStatePollFillMatchCount'))}.</p>",
        "  <h2>Accepted Evidence Checklist</h2>",
        f"  <table><thead><tr><th>requirement</th><th>status</th><th>accepted signal</th></tr></thead><tbody>{checklist_rows}</tbody></table>",
        "  <h2>Missing Evidence</h2>",
        f"  <ul>{missing}</ul>",
        "  <h2>Evidence Refs</h2>",
        f"  <ul>{evidence_refs}</ul>",
        "  <h2>Not Accepted Evidence</h2>",
        f"  <ul>{not_accepted}</ul>",
        "  <h2>Related Reports</h2>",
        f"  <ul>{related}</ul>",
        "  <h2>Regenerate And Verify</h2>",
        f"  <ul>{commands}</ul>",
        f"  <p>{esc(summary.get('conclusion'))}</p>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "predecessor_fill_external_proof_packet.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "predecessor_fill_external_proof_packet.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(args.out_dir)
    write_outputs(summary, args.out_dir)
    print(f"wrote predecessor fill external proof packet -> {args.out_dir / 'predecessor_fill_external_proof_packet.html'}")


if __name__ == "__main__":
    main()
