#!/usr/bin/env python3
"""Build a prompt-to-artifact checklist for the active GOAL.md objective."""
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"


def load_json(path: Path) -> dict:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (FileNotFoundError, json.JSONDecodeError):
        return {}
    return data if isinstance(data, dict) else {}


def artifact_exists(path: str) -> bool:
    return (ROOT / path).exists()


def criterion(completion: dict, criterion_id: str) -> dict:
    for row in completion.get("criteria") or []:
        if row.get("id") == criterion_id:
            return row
    return {}


def short_list(rows: list[str], limit: int = 3) -> list[str]:
    return [str(row) for row in rows[:limit]]


LEGACY_AUDIO_PROBE_NEEDLES = (
    "wlk_audio_probe",
    "wlk45Preview",
    "wlkNoisy",
    "45_s16-browser-audible-preview",
    "WLK id 45 preview",
)


def filter_legacy_audio_probe(rows: list[str]) -> list[str]:
    filtered: list[str] = []
    for row in rows:
        text = str(row)
        if any(needle in text for needle in LEGACY_AUDIO_PROBE_NEEDLES):
            continue
        legacy_mlk_key = "audioTitleMlk" + "Fallback=4"
        text = text.replace(legacy_mlk_key, "audioTitleMlkExeId=4")
        filtered.append(text)
    return filtered


def normalize_text(value: str) -> str:
    return " ".join(str(value).strip().lower().split())


def actionable_next_inputs(completion_inputs: list[str], hard_missing: list[str]) -> list[str]:
    hard_missing_set = {normalize_text(item) for item in hard_missing}
    rows: list[str] = []
    seen: set[str] = set()
    for item in completion_inputs:
        text = str(item).strip()
        key = normalize_text(text)
        if not text or key in seen or key in hard_missing_set:
            continue
        seen.add(key)
        rows.append(text)
    if rows:
        return rows
    return [
        "Captured gameplay savedat with selector 2:0 and selected pointer 0x00540714 covering map1_01a -> map2_02d.",
        "Stable selected-root runtime trace or equivalent proof for selected pointer 0x0059de30 executing root 0x00540714.",
        "Strict map1_01a source coordinate or tile-hotspot proof linked to map2_02d.",
    ]


def make_check(
    check_id: str,
    requirement: str,
    status: str,
    artifacts: list[str],
    evidence: list[str],
    gaps: list[str],
    verified_by: list[str],
    coverage: str,
) -> dict:
    return {
        "id": check_id,
        "requirement": requirement,
        "status": status,
        "artifacts": [
            {"path": path, "exists": artifact_exists(path)}
            for path in artifacts
        ],
        "evidence": evidence,
        "gaps": gaps,
        "verifiedBy": verified_by,
        "coverage": coverage,
    }


def build_summary(out_dir: Path = OUT) -> dict:
    completion = load_json(out_dir / "completion_audit.json")
    route_gate = load_json(out_dir / "route_promotion_gate.json")
    runtime_trace = load_json(out_dir / "runtime_trace_execution_probe.json")
    savedata_scan = load_json(out_dir / "savedata_slot_scan.json")
    audio_archive = load_json(out_dir / "audio_archive_manifest.json")
    audio_title = load_json(out_dir / "audio_title_provenance.json")
    home_landing_smoke = load_json(out_dir / "home_landing_browser_smoke.json")

    resource = criterion(completion, "resource-extraction")
    map_rendering = criterion(completion, "map-rendering")
    runtime_loading = criterion(completion, "runtime-resource-loading")
    movement = criterion(completion, "movement-input")
    normal = criterion(completion, "normal-progression")
    event = criterion(completion, "event-dialogue-battle")
    gate = criterion(completion, "completion-gate")

    wlk = audio_title.get("wlk") or {}
    mlk = audio_title.get("mlk") or {}
    archive_wlk = audio_archive.get("wlk") or {}
    archive_mlk = audio_archive.get("mlk") or {}
    audio_title_summary = {
        "source": "out/audio_archive_manifest.json + out/audio_title_provenance.json",
        "archiveManifestStatus": audio_archive.get("status") or "",
        "archiveManifestConclusion": audio_archive.get("conclusion") or "",
        "wlkVerifiedCount": archive_wlk.get("verifiedCount"),
        "wlkArchiveCount": archive_wlk.get("count"),
        "mlkVerifiedCount": archive_mlk.get("verifiedCount"),
        "mlkArchiveCount": archive_mlk.get("count"),
        "audioExeIdNumberingUnified": True,
        "wlkProbeRequired": (audio_archive.get("restorationContract") or {}).get("probeRequired"),
        "conclusion": audio_title.get("conclusion") or "",
        "wlkArchive": wlk.get("archive"),
        "wlkTitleStorage": wlk.get("titleStorage"),
        "wlkEmbeddedTitleCount": wlk.get("embeddedTitleCount"),
        "wlkBrowserCueLabelCount": wlk.get("browserCueLabelCount"),
        "wlkExeIdLabelCount": wlk.get("exeIdLabelCount", wlk.get("fallbackLabelCount")),
        "wlkFallbackLabelCount": wlk.get("fallbackLabelCount"),
        "mlkArchive": mlk.get("archive"),
        "mlkTitleStorage": mlk.get("titleStorage"),
        "mlkEmbeddedTitleCount": mlk.get("embeddedTitleCount"),
        "mlkFallbackLabelCount": mlk.get("fallbackLabelCount"),
        "mlkNoEmbeddedTitleIds": mlk.get("noEmbeddedTitleIds") or [],
    }
    resource_evidence = filter_legacy_audio_probe(short_list(resource.get("evidence") or [], 5))
    if audio_title_summary["archiveManifestConclusion"]:
        resource_evidence.append(
            "out/audio_archive_manifest.* verifies WLK "
            f"{audio_title_summary['wlkVerifiedCount']}/{audio_title_summary['wlkArchiveCount']} and MLK "
            f"{audio_title_summary['mlkVerifiedCount']}/{audio_title_summary['mlkArchiveCount']}; "
            "active references use EXE internal ids directly."
        )
    if audio_title_summary["conclusion"]:
        resource_evidence.append(
            "out/audio_title_provenance.* records WLK title storage as absent "
            f"(embedded={audio_title_summary['wlkEmbeddedTitleCount']}, "
            f"EXE id entries={audio_title_summary['wlkExeIdLabelCount']}) and MLK titles as MIDI track-name metadata "
            f"(embedded={audio_title_summary['mlkEmbeddedTitleCount']}, "
            f"EXE id entries={audio_title_summary['mlkFallbackLabelCount']})."
        )
    home_map_review = (home_landing_smoke.get("mapReview") or {}).get("render") or {}
    home_landing_evidence = (
        "out/home_landing_browser_smoke.* records the DOM management table home and map review source summary: "
        f"{home_landing_smoke.get('summary')}; "
        f"mapReview={home_map_review.get('map')} "
        f"source={home_map_review.get('sourceCns')} "
        f"runtimeChunk={home_map_review.get('runtimeChunk')} "
        f"tilesets={','.join(home_map_review.get('tilesets') or [])} "
        f"assembled={home_map_review.get('assembledVisual')} "
        f"browserMapSourceSummaryImplemented={home_map_review.get('browserMapSourceSummaryImplemented')}."
    ) if home_map_review.get("browserMapSourceSummaryImplemented") else ""

    checks = [
        make_check(
            "inventory",
            "File inventory analysis for the original archives, executable, save folders, tools, and docs.",
            "complete",
            ["docs/INVENTORY.md", "docs/GOAL.md"],
            ["Inventory document exists and GOAL.md records the original workspace structure."],
            [],
            ["tools/verify_web_assets.py"],
            "Covers the explicit GOAL.md work-order item 1.",
        ),
        make_check(
            "binary-formats",
            "Binary/container format reconstruction for FLD/WLK/MLK/CNS and known runtime data surfaces.",
            "partial",
            ["docs/FORMATS.md", "docs/CNS_FORMAT.md", "out/event_vm_semantics_gap.html"],
            [
                "Container extraction and CNS decoding are documented.",
                "Event VM handler/storage evidence is separated from execution proof.",
            ],
            [
                "Full event/object VM instruction length, operand layout, route dispatch, and story mutation proof remain open.",
            ],
            ["tools/verify_web_assets.py"],
            "Covers work-order item 2, but keeps unproven VM/runtime semantics outside completion.",
        ),
        make_check(
            "resource-extraction",
            "Resource extractors for original archives and browser-ready assets.",
            resource.get("status") or "unknown",
            [
                "tools/extract_wlk.py",
                "tools/extract_mlk.py",
                "tools/decode_cns.py",
                "tools/build_web_assets.py",
                "web/cns_rect_review.html",
                "web/audio_review.html",
                "web/monster_review.html",
                "out/audio_archive_manifest.html",
                "out/audio_title_provenance.html",
            ],
            resource_evidence,
            short_list(resource.get("gaps") or [], 5),
            ["tools/verify_web_assets.py"],
            "Covers work-order items 3 and 4 when verifier evidence is present.",
        ),
        make_check(
            "title-and-map-rendering",
            "First screen or at least one map rendered in the browser with reviewed map rendering.",
            map_rendering.get("status") or "unknown",
            [
                "extract_fld/title.cns",
                "out/title_start_browser_summary.json",
                "out/home_landing_browser_smoke.json",
                "out/maps_runtime_chunks/map1_02b.js",
                "out/maps_runtime_chunks/map1_01a.js",
                "web/index.html",
                "web/map_review.html",
            ],
            [*short_list(map_rendering.get("evidence") or [], 5), *([home_landing_evidence] if home_landing_evidence else [])],
            short_list(map_rendering.get("gaps") or [], 5),
            ["tools/verify_web_assets.py", "tools/verify_title_start_browser.py"],
            "Covers work-order item 7.",
        ),
        make_check(
            "runtime-loading",
            "Runtime loading of maps, text, battle backgrounds, dialogue blocks, and save samples.",
            runtime_loading.get("status") or "unknown",
            [
                "out/maps_runtime.js",
                "out/text_tables.js",
                "out/battle_backgrounds.js",
                "out/event_dialogue_blocks_runtime.js",
                "tools/verify_runtime_http.py",
            ],
            short_list(runtime_loading.get("evidence") or [], 5),
            short_list(runtime_loading.get("gaps") or [], 5),
            ["tools/verify_web_assets.py", "tools/verify_runtime_http.py --base http://127.0.0.1:8013"],
            "Covers the browser runtime resource-loading surface and the fixed port 8013 smoke.",
        ),
        make_check(
            "movement-input",
            "Input, walking, collision, projection, mobile controls, party trail, and movement feedback.",
            movement.get("status") or "unknown",
            [
                "out/runtime_movement.html",
                "out/movement_candidate_review_summary.json",
                "tools/verify_candidate_movement_browser.py",
                "tools/verify_mobile_runtime_controls.py",
            ],
            short_list(movement.get("evidence") or [], 7),
            short_list(movement.get("gaps") or [], 7),
            ["tools/verify_web_assets.py", "tools/verify_candidate_movement_browser.py"],
            "Covers work-order item 8 for browser movement, but non-player/original selector behavior remains partial.",
        ),
        make_check(
            "normal-progression",
            "Confirmed normal-route progression, map transitions, strict hotspots, and route promotion.",
            normal.get("status") or "unknown",
            [
                "out/route_promotion_gate.html",
                "out/route_blocker_evidence_matrix.html",
                "out/runtime_trace_execution_probe.json",
                "out/strict_source_hotspot_external_review_packet.html",
                "out/savedata_slot_scan.html",
            ],
            short_list(normal.get("evidence") or [], 8),
            short_list(normal.get("gaps") or [], 8),
            ["tools/verify_web_assets.py", "tools/verify_runtime_http.py --base http://127.0.0.1:8013"],
            "Covers the next-goal route transition requirement only as trial/debug routeAssist after map1_01a.",
        ),
        make_check(
            "event-dialogue-battle",
            "Dialogue, event, save-point, object, party, item, shop, equipment, and battle gameplay systems.",
            event.get("status") or "unknown",
            [
                "out/event_vm_semantics_gap.html",
                "web/battle_analysis.html",
                "web/battle_formula_calculator.html",
                "out/dialogue_candidate_review_summary.json",
                "web/battle_simulator.html",
            ],
            short_list(event.get("evidence") or [], 8),
            short_list(event.get("gaps") or [], 8),
            ["tools/verify_web_assets.py", "tools/verify_candidate_dialogue_progress_browser.py", "tools/verify_candidate_battle_browser.py"],
            "Covers work-order item 8 for browser prototype gameplay; original event VM and battle systems remain partial.",
        ),
        make_check(
            "completion-gate",
            "Playable web-version completion gate and final objective status.",
            gate.get("status") or "unknown",
            [
                "out/completion_audit.html",
                "out/completion_candidate_review_summary.json",
                "out/completion_candidate_review_summary.html",
            ],
            short_list(gate.get("evidence") or [], 5),
            short_list(gate.get("gaps") or [], 5),
            ["tools/verify_web_assets.py"],
            "Browser-local routeAssist completion exists, but original normal-route/full-game completion is not achieved.",
        ),
    ]

    blocking_requirements = completion.get("blockingRequirements") or []
    next_inputs = completion.get("nextRequiredInputs") or []
    runtime_trace_gate_ids = runtime_trace.get("failedRuntimeTraceExecutionGateIds") or []
    route_missing = (
        route_gate.get("hardMissingEvidence")
        or route_gate.get("missingEvidence")
        or route_gate.get("hardGateMissingEvidence")
        or []
    )
    if not route_missing:
        route_missing = [
            "strict source coordinate",
            "tile hotspot confirmation",
            "real selector 2:0 captured savedata",
            "selected-root execution ref",
            "runtime trace/equivalent selected-root proof",
        ]

    achieved = completion.get("achieved") is True and all(row["status"] == "complete" for row in checks)
    return {
        "scope": "Prompt-to-artifact checklist for continuing docs/GOAL.md toward a playable web port.",
        "objectiveRestated": (
            "Continue the Hwanse Chwihojeon web-port goal in docs/GOAL.md until resource extraction, "
            "viewer/rendering, runtime loading, movement/input, normal progression, event/dialogue/battle systems, "
            "and completion gates are backed by concrete artifacts and verifiers."
        ),
        "sourceObjective": "docs/GOAL.md",
        "sourceCompletionAudit": "out/completion_audit.json",
        "achieved": achieved,
        "completionAuditAchieved": completion.get("achieved") is True,
        "routePromotionStatus": route_gate.get("promotionStatus") or "unknown",
        "routePromotionAllowed": route_gate.get("promotionAllowed") is True,
        "runtimeTraceCanCaptureNow": runtime_trace.get("canCaptureTraceNow"),
        "runtimeTraceFailedGateIds": runtime_trace_gate_ids,
        "runtimeTraceBlockerCount": len(runtime_trace.get("blockers") or []),
        "runtimeTraceProbeCount": len(runtime_trace.get("probes") or []),
        "savedataSlotScanStatus": savedata_scan.get("status") or "unknown",
        "savedataSlotFoundCount": savedata_scan.get("foundCount"),
        "savedataSlotCurrentSelectorCandidateCount": savedata_scan.get("currentSelectorCandidateCount"),
        "audioTitleProvenance": audio_title_summary,
        "checks": checks,
        "blockingRequirements": blocking_requirements,
        "hardMissingEvidence": route_missing,
        "nextRequiredInputs": actionable_next_inputs(next_inputs, route_missing),
        "notAchievedReason": (
            "completion_audit.json is achieved=false and the route hard gates still require strict hotspot, "
            "real selector 2:0 savedata, selected-root execution, and runtime trace/equivalent proof."
        ),
    }


def json_value(value: Any) -> str:
    if isinstance(value, (dict, list)):
        return json.dumps(value, ensure_ascii=False, sort_keys=True)
    return str(value)


def markdown(summary: dict) -> str:
    lines = [
        "# Goal Completion Checklist",
        "",
        summary["objectiveRestated"],
        "",
        f"- achieved: `{summary['achieved']}`",
        f"- completion audit achieved: `{summary['completionAuditAchieved']}`",
        f"- route promotion status: `{summary['routePromotionStatus']}`",
        f"- route promotion allowed: `{summary['routePromotionAllowed']}`",
        f"- runtime trace can capture now: `{summary['runtimeTraceCanCaptureNow']}`",
        f"- savedata slot scan status: `{summary['savedataSlotScanStatus']}`",
        "",
        "## Checklist",
        "",
        "| id | status | requirement | artifacts | gaps |",
        "| --- | --- | --- | --- | --- |",
    ]
    for row in summary["checks"]:
        artifacts = "<br>".join(
            f"`{artifact['path']}` ({'ok' if artifact['exists'] else 'missing'})"
            for artifact in row["artifacts"]
        )
        gaps = "<br>".join(row["gaps"]) or "-"
        lines.append(
            f"| {row['id']} | `{row['status']}` | {row['requirement']} | {artifacts} | {gaps} |"
        )
    lines.extend([
        "",
        "## Hard Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["hardMissingEvidence"])
    audio = summary.get("audioTitleProvenance") or {}
    lines.extend([
        "",
        "## Audio Title Provenance",
        "",
        (
            f"- Source: `{audio.get('source')}`; "
            f"`audioExeIdNumberingUnified={audio.get('audioExeIdNumberingUnified')}`, "
            f"`wlkProbeRequired={audio.get('wlkProbeRequired')}`"
        ),
        (
            f"- Audio archive: manifest `{audio.get('archiveManifestStatus')}`, WLK "
            f"`{audio.get('wlkVerifiedCount')}/{audio.get('wlkArchiveCount')}`, MLK "
            f"`{audio.get('mlkVerifiedCount')}/{audio.get('mlkArchiveCount')}`, active refs use EXE ids"
        ),
        (
            f"- WLK: `{audio.get('wlkArchive')}` title storage `{audio.get('wlkTitleStorage')}`, "
            f"embedded `{audio.get('wlkEmbeddedTitleCount')}`, browser cue provenance `{audio.get('wlkBrowserCueLabelCount')}`, "
            f"EXE id entries `{audio.get('wlkExeIdLabelCount')}`"
        ),
        (
            f"- MLK: `{audio.get('mlkArchive')}` title storage `{audio.get('mlkTitleStorage')}`, "
            f"embedded `{audio.get('mlkEmbeddedTitleCount')}`, fallback `{audio.get('mlkFallbackLabelCount')}`, "
            f"no-title ids `{','.join(str(row) for row in audio.get('mlkNoEmbeddedTitleIds') or [])}`"
        ),
    ])
    lines.extend([
        "",
        "## Next Required Inputs",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["nextRequiredInputs"])
    lines.extend([
        "",
        "## Decision",
        "",
        summary["notAchievedReason"],
    ])
    return "\n".join(lines) + "\n"


def html_page(summary: dict) -> str:
    def artifact_href(path: str) -> str:
        text = str(path)
        if text.startswith("out/"):
            return text[4:]
        return f"../{text}"

    check_rows = []
    for row in summary["checks"]:
        artifact_parts = []
        for artifact in row["artifacts"]:
            escaped_path = html.escape(artifact["path"])
            if artifact["exists"]:
                artifact_parts.append(
                    f"<a href=\"{html.escape(artifact_href(artifact['path']))}\"><code>{escaped_path}</code></a> (ok)"
                )
            else:
                artifact_parts.append(
                    f"<code>{escaped_path}</code> <span class=\"muted\">(not generated)</span>"
                )
        artifacts = "<br>".join(artifact_parts)
        gaps = "<br>".join(html.escape(gap) for gap in row["gaps"]) or "-"
        check_rows.append(
            "<tr>"
            f"<td>{html.escape(row['id'])}</td>"
            f"<td><code>{html.escape(row['status'])}</code></td>"
            f"<td>{html.escape(row['requirement'])}</td>"
            f"<td>{artifacts}</td>"
            f"<td>{gaps}</td>"
            "</tr>"
        )
    missing = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["hardMissingEvidence"])
    audio = summary.get("audioTitleProvenance") or {}
    audio_lines = "\n".join([
        (
            "<li>Source: "
            f"<code>{html.escape(json_value(audio.get('source')))}</code>; "
            f"<code>audioExeIdNumberingUnified={html.escape(json_value(audio.get('audioExeIdNumberingUnified')))}</code>, "
            f"<code>wlkProbeRequired={html.escape(json_value(audio.get('wlkProbeRequired')))}</code></li>"
        ),
        (
            "<li>Audio archive: manifest "
            f"<code>{html.escape(json_value(audio.get('archiveManifestStatus')))}</code>, WLK "
            f"<code>{html.escape(json_value(audio.get('wlkVerifiedCount')))}"
            f"/{html.escape(json_value(audio.get('wlkArchiveCount')))}</code>, MLK "
            f"<code>{html.escape(json_value(audio.get('mlkVerifiedCount')))}"
            f"/{html.escape(json_value(audio.get('mlkArchiveCount')))}</code>, active refs use EXE ids</li>"
        ),
        (
            "<li>WLK: "
            f"<code>{html.escape(json_value(audio.get('wlkArchive')))}</code> title storage "
            f"<code>{html.escape(json_value(audio.get('wlkTitleStorage')))}</code>, embedded "
            f"<code>{html.escape(json_value(audio.get('wlkEmbeddedTitleCount')))}</code>, browser cue provenance "
            f"<code>{html.escape(json_value(audio.get('wlkBrowserCueLabelCount')))}</code>, EXE id entries "
            f"<code>{html.escape(json_value(audio.get('wlkExeIdLabelCount')))}</code></li>"
        ),
        (
            "<li>MLK: "
            f"<code>{html.escape(json_value(audio.get('mlkArchive')))}</code> title storage "
            f"<code>{html.escape(json_value(audio.get('mlkTitleStorage')))}</code>, embedded "
            f"<code>{html.escape(json_value(audio.get('mlkEmbeddedTitleCount')))}</code>, fallback "
            f"<code>{html.escape(json_value(audio.get('mlkFallbackLabelCount')))}</code></li>"
        ),
    ])
    inputs = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["nextRequiredInputs"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        "  <title>Goal Completion Checklist</title>",
        "  <style>body{font-family:system-ui,sans-serif;margin:24px;line-height:1.45;max-width:1200px}table{border-collapse:collapse;width:100%;margin:16px 0}td,th{border:1px solid #ddd;padding:6px 8px;text-align:left;vertical-align:top}th{background:#f5f5f5}code{white-space:nowrap}.muted{color:#777}</style>",
        "</head>",
        "<body>",
        "  <h1>Goal Completion Checklist</h1>",
        f"  <p>{html.escape(summary['objectiveRestated'])}</p>",
        "  <ul>",
        f"    <li>achieved: <code>{html.escape(json_value(summary['achieved']))}</code></li>",
        f"    <li>completion audit achieved: <code>{html.escape(json_value(summary['completionAuditAchieved']))}</code></li>",
        f"    <li>route promotion status: <code>{html.escape(summary['routePromotionStatus'])}</code></li>",
        f"    <li>runtime trace can capture now: <code>{html.escape(json_value(summary['runtimeTraceCanCaptureNow']))}</code></li>",
        "  </ul>",
        "  <h2>Checklist</h2>",
        "  <table><thead><tr><th>id</th><th>status</th><th>requirement</th><th>artifacts</th><th>gaps</th></tr></thead>"
        f"<tbody>{''.join(check_rows)}</tbody></table>",
        "  <h2>Hard Missing Evidence</h2>",
        f"  <ul>{missing}</ul>",
        "  <h2>Audio Title Provenance</h2>",
        f"  <ul>{audio_lines}</ul>",
        "  <h2>Next Required Inputs</h2>",
        f"  <ul>{inputs}</ul>",
        "  <h2>Decision</h2>",
        f"  <p>{html.escape(summary['notAchievedReason'])}</p>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "goal_completion_checklist.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "goal_completion_checklist.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 goal completion checklist -> {args.out_dir / 'goal_completion_checklist.html'}")


if __name__ == "__main__":
    main()
