#!/usr/bin/env python3
"""Build a focused execution-route review for the map1_01a scene/event VM goal.

This report does not try to finish the whole scene VM.  It consolidates the
existing evidence around map1_01a scene records and separates:

- direct execution proof,
- selector-root text trace candidates,
- static resource/scene-list adjacency,
- strict transition evidence,
- gaps that need runtime or a stricter dispatch root.
"""
from __future__ import annotations

import argparse
import html
import json
import struct
from collections import Counter
from pathlib import Path
from typing import Any

import sys

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import read_sections  # noqa: E402


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
WEB = ROOT / "web"
SOURCE = "map1_01a"
TARGET = "map2_02d"


def load_json(path: Path, default: Any) -> Any:
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except FileNotFoundError:
        return default


def write_json(path: Path, payload: Any) -> None:
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


def write_text(path: Path, text: str) -> None:
    path.write_text(text, encoding="utf-8")


def h(value: Any) -> str:
    return html.escape("" if value is None else str(value))


def short(value: Any, limit: int = 160) -> str:
    if isinstance(value, (dict, list)):
        value = json.dumps(value, ensure_ascii=False, sort_keys=True)
    text = " ".join((str(value) if value is not None else "").split())
    return text if len(text) <= limit else text[: limit - 1] + "…"


def hex32(value: int | None) -> str:
    return "-" if value is None else f"0x{value:08x}"


def parse_hex(value: Any) -> int | None:
    if isinstance(value, int):
        return value
    if isinstance(value, str) and value.startswith("0x"):
        return int(value, 16)
    return None


def section_for_offset(sections: list[dict[str, Any]], offset: int) -> dict[str, Any] | None:
    for section in sections:
        start = section["raw"]
        end = section["raw"] + section["raw_size"]
        if start <= offset < end:
            return section
    return None


def offset_to_va(sections: list[dict[str, Any]], offset: int) -> int | None:
    for section in sections:
        start = section["raw"]
        end = section["raw"] + section["raw_size"]
        if start <= offset < end:
            return section["va"] + offset - start
    return None


def direct_xrefs(exe: bytes, sections: list[dict[str, Any]], targets: dict[str, int], sample_limit: int = 12) -> dict[str, Any]:
    result: dict[str, Any] = {}
    for name, target in targets.items():
        needle = struct.pack("<I", target)
        counts: Counter[str] = Counter()
        samples = []
        search = 0
        while True:
            hit = exe.find(needle, search)
            if hit < 0:
                break
            search = hit + 1
            section = section_for_offset(sections, hit)
            if not section:
                continue
            va = offset_to_va(sections, hit)
            section_name = section["name"]
            counts[section_name] += 1
            if len(samples) < sample_limit:
                samples.append(
                    {
                        "section": section_name,
                        "refVaHex": hex32(va),
                        "fileOffsetHex": f"0x{hit:06x}",
                    }
                )
        result[name] = {
            "targetVaHex": hex32(target),
            "totalRefCount": sum(counts.values()),
            "sectionCounts": dict(counts),
            "samples": samples,
            "hasTextRef": counts.get(".text", 0) > 0,
            "hasDataRef": counts.get(".data", 0) > 0 or counts.get(".rdata", 0) > 0,
        }
    return result


def scene_records(scene_manifest: list[dict[str, Any]], cluster_context: dict[str, Any]) -> list[dict[str, Any]]:
    cluster_by_record = {
        row.get("recordVaHex"): row
        for row in cluster_context.get("sourceSceneRecords", [])
    }
    rows = []
    for record in scene_manifest:
        if record.get("map") != SOURCE:
            continue
        cluster = cluster_by_record.get(record.get("recordVaHex"), {})
        strict = bool(cluster.get("hasStrictEventRecord"))
        selector_refs = int(cluster.get("saveSelectorRefCount") or 0)
        if strict and cluster.get("role") == "incoming strict entry context":
            execution_status = "grounded-incoming-entry"
            promotion = "grounded"
            note = "strict event-linked incoming entry context; not the first field-scene execution root"
        elif selector_refs:
            execution_status = "selector-scene-list-only"
            promotion = "blocked"
            note = "selector scene-list/resource adjacency; no strict event record or executed root proof"
        else:
            execution_status = "manifest-resource-record-only"
            promotion = "gap"
            note = "manifest/resource record with no strict event record and no selector execution refs"
        rows.append(
            {
                "map": record.get("map"),
                "recordVaHex": record.get("recordVaHex"),
                "sceneIdHex": record.get("sceneIdHex"),
                "resourceSource": record.get("resourceSource"),
                "tilesets": record.get("tilesets") or [],
                "sprites": record.get("sprites") or [],
                "classification": cluster.get("classification") or "unclassified",
                "role": cluster.get("role") or "-",
                "hasStrictEventRecord": strict,
                "saveSelectorRefCount": selector_refs,
                "currentFrontierPairCount": cluster.get("currentFrontierPairCount") or 0,
                "eventSources": cluster.get("eventSources") or [],
                "eventFieldLinks": cluster.get("eventFieldLinks") or [],
                "executionStatus": execution_status,
                "promotion": promotion,
                "note": note,
            }
        )
    return rows


def selector_trace_summary(scene_script_player_data: dict[str, Any], opcode_summary: dict[str, Any]) -> list[dict[str, Any]]:
    rows = []
    for item in scene_script_player_data.get("mapIndex", {}).get(SOURCE, []):
        status = item.get("evidenceStatus")
        root_hex = item.get("rootVaHex") or ""
        sample = item.get("sample") or ""
        if status == "selector-root-range-candidate" and "여긴 호랑이권법가" in sample:
            promotion = "partial"
            role = "first text-flow candidate"
            note = "matches observed early Ataho cave prompts, but no direct map1_01a scene dispatch root proof"
        elif status == "nearest-scene-proximity-candidate":
            promotion = "candidate"
            role = "nearby text proximity"
            note = "nearby scene proximity only; sample is not the observed map1_01a opening flow"
        else:
            promotion = "candidate"
            role = "selector/map text association"
            note = "selector/root text association; needs direct execution proof"
        rows.append(
            {
                "groupId": item.get("groupId"),
                "sequenceId": item.get("sequenceId"),
                "contextLabel": item.get("contextLabel"),
                "mapEvidence": item.get("mapEvidence"),
                "recordVaHex": item.get("recordVaHex") or "",
                "rootVaHex": root_hex,
                "evidenceStatus": status,
                "promptCount": item.get("promptCount"),
                "choiceCount": item.get("choiceCount"),
                "promotion": promotion,
                "role": role,
                "sample": sample,
                "note": note,
                "commandTraceRows": (
                    opcode_summary.get("summary", {}).get("map1CommandRowsShown")
                    if item.get("groupId") == "scene-text-group-029" and item.get("sequenceId") == "scene-seq-029-01"
                    else 0
                ),
            }
        )
    return rows


def connection_rows(
    opcode_summary: dict[str, Any],
    scene_prompt_probe: dict[str, Any],
    scene_payload_context: dict[str, Any],
    scene_record_sequence: dict[str, Any],
    scene_list_context: dict[str, Any],
    scene_adjacency_index: dict[str, Any],
    route_root_ref_context: dict[str, Any],
) -> list[dict[str, Any]]:
    trace = opcode_summary.get("map1_01aTrace") or {}
    return [
        {
            "connection": "opcode trace",
            "status": "grounded-opcodes / partial-route",
            "promotion": "partial",
            "evidence": f"{opcode_summary.get('summary', {}).get('groundedOpcodeCount', 0)} grounded opcodes; {trace.get('commandTraceRowsShown', 0)} map1 command trace rows",
            "gap": "trace rows are tied to selector-root range, not direct normal field-scene execution",
        },
        {
            "connection": "prompt/choice",
            "status": trace.get("selectedEvidenceStatus") or "unknown",
            "promotion": "partial",
            "evidence": f"group={trace.get('selectedGroupId')} sequence={trace.get('selectedSequenceId')} prompts={trace.get('promptsShown')} choices={trace.get('choiceCountInSequence')}",
            "gap": "direct scene -> prompt pointer reference is absent near scene records",
        },
        {
            "connection": "actor/resource",
            "status": "static-resource-list-grounded / actor-consumer-gap",
            "promotion": "partial",
            "evidence": "map1_01a manifest records list map/cara/face resources; opcode 0x07 object-position is grounded generally",
            "gap": "resource presence alone is not actor placement/action execution proof",
        },
        {
            "connection": "map transition incoming",
            "status": "strict event-linked incoming entry context",
            "promotion": "grounded",
            "evidence": "record 0x005032d8 is a strict event-linked cluster with event source map1_02b and field link map1_01a",
            "gap": "this is incoming map1_02b -> map1_01a context, not map1_01a first scene execution",
        },
        {
            "connection": "map transition outgoing",
            "status": "selector-adjacency-only",
            "promotion": "blocked",
            "evidence": f"selector adjacency occurrences={scene_adjacency_index.get('currentPair', {}).get('occurrenceCount')}; geometryExitWordHitCount={scene_record_sequence.get('geometryExitWordHitCount')}; strictTransitionProofFound={scene_list_context.get('strictTransitionProofFound')}",
            "gap": "strict source hotspot/coordinate or runtime execution proof is missing",
        },
        {
            "connection": "selector root refs",
            "status": route_root_ref_context.get("promotionStatus") or "blocked",
            "promotion": "blocked",
            "evidence": (
                f"tableOnly={route_root_ref_context.get('allRouteSelectorRootsTableOnly')}; "
                f"textRefs={route_root_ref_context.get('anyRouteSelectorRootTextRefs')}; "
                f"predecessorToCurrent={route_root_ref_context.get('predecessorToCurrentRootRefFound')}"
            ),
            "gap": route_root_ref_context.get("conclusion")
            or "selector root dword refs are table membership, not execution-order proof",
        },
        {
            "connection": "scene-local payload pointers",
            "status": scene_payload_context.get("promotionStatus") or "blocked",
            "promotion": "blocked",
            "evidence": f"payloads={len(scene_payload_context.get('payloads') or [])}; strictHotspotFound={scene_payload_context.get('strictHotspotFound')}",
            "gap": scene_payload_context.get("conclusion"),
        },
        {
            "connection": "direct prompt refs near scene records",
            "status": "absent-within-strict-radius",
            "promotion": "gap",
            "evidence": scene_prompt_probe.get("conclusion"),
            "gap": "needs consumer-side VM execution or narrower command root",
        },
        {
            "connection": "BGM/SFX",
            "status": "not-linked-in-map1-route",
            "promotion": "gap",
            "evidence": "no map1_01a scene execution route currently links MLK/WLK commands",
            "gap": "needs audio opcode/scene consumer trace; do not infer from resource adjacency",
        },
    ]


def command_trace_reliability(opcode_summary: dict[str, Any]) -> dict[str, Any]:
    trace_rows = (opcode_summary.get("map1_01aTrace") or {}).get("commandTraceRows") or []
    opcode_rows = opcode_summary.get("opcodeDictionary") or []
    default_opcodes = {
        row.get("opcodeHex")
        for row in opcode_rows
        if row.get("isDefaultHandler") or row.get("role") == "default-handler-or-operand-byte"
    }
    opcode_counts = Counter(row.get("opcodeHex") for row in trace_rows)
    evidence_counts = Counter(row.get("opcodeEvidenceLevel") for row in trace_rows)
    default_rows = [row for row in trace_rows if row.get("opcodeHex") in default_opcodes]
    return {
        "totalRows": len(trace_rows),
        "groundedRows": evidence_counts.get("grounded", 0),
        "partialRows": evidence_counts.get("partial", 0),
        "candidateRows": evidence_counts.get("candidate", 0),
        "defaultHandlerRows": len(default_rows),
        "defaultHandlerOpcodes": sorted(op for op in default_opcodes if op in opcode_counts),
        "opcodeCounts": dict(sorted(opcode_counts.items())),
        "evidenceCounts": dict(sorted(evidence_counts.items())),
        "defaultHandlerSampleRows": default_rows[:12],
        "classification": "review-only-mixed-trace",
        "conclusion": "The map1 trace includes many default-handler/operand-byte candidates. The default handler at 0x0040239f only advances context+0x40 by 4, so these rows are useful for review but are not route execution proof without command-start binding.",
    }


def gap_rows(record_rows: list[dict[str, Any]], selector_rows: list[dict[str, Any]], connection_rows_: list[dict[str, Any]]) -> list[dict[str, Any]]:
    rows = []
    if not any(row["promotion"] == "confirmed" for row in record_rows):
        rows.append(
            {
                "id": "map1-scene-record-to-executed-vm-stream",
                "status": "gap",
                "reason": "no map1_01a scene record currently has direct executed VM stream proof",
                "nextEvidenceNeeded": "runtime trace or static dispatch root that consumes record 0x0043e798/0x00542b44 as an executed command stream",
            }
        )
    if not any(row["promotion"] == "partial" and "여긴 호랑이권법가" in row.get("sample", "") for row in selector_rows):
        rows.append(
            {
                "id": "map1-first-text-flow-candidate",
                "status": "gap",
                "reason": "observed first Ataho cave text sequence not associated with any selector-root candidate",
                "nextEvidenceNeeded": "find text-flow root containing early map1_01a prompt sequence",
            }
        )
    for row in connection_rows_:
        if row["promotion"] in {"blocked", "gap"}:
            rows.append(
                {
                    "id": row["connection"].replace(" ", "-"),
                    "status": row["promotion"],
                    "reason": row["status"],
                    "nextEvidenceNeeded": row["gap"],
                }
            )
    return rows


def build(args: argparse.Namespace) -> dict[str, Any]:
    scene_manifest = load_json(args.scene_manifest, [])
    cluster_context = load_json(args.cluster_context, {})
    scene_script_player_data = load_json(args.scene_script_player_data, {})
    opcode_summary = load_json(args.opcode_dictionary, {})
    scene_prompt_probe = load_json(args.scene_prompt_probe, {})
    scene_payload_context = load_json(args.scene_payload_context, {})
    scene_record_sequence = load_json(args.scene_record_sequence, {})
    scene_list_context = load_json(args.scene_list_context, {})
    scene_adjacency_index = load_json(args.scene_adjacency_index, {})
    selected_root_gap = load_json(args.selected_root_gap, {})
    route_root_ref_context = load_json(args.route_root_ref_context, {})
    trace_reliability = command_trace_reliability(opcode_summary)

    records = scene_records(scene_manifest, cluster_context)
    selector_rows = selector_trace_summary(scene_script_player_data, opcode_summary)
    connections = connection_rows(
        opcode_summary,
        scene_prompt_probe,
        scene_payload_context,
        scene_record_sequence,
        scene_list_context,
        scene_adjacency_index,
        route_root_ref_context,
    )
    gaps = gap_rows(records, selector_rows, connections)

    targets: dict[str, int] = {}
    for row in records:
        va = parse_hex(row.get("recordVaHex"))
        if va is not None:
            targets[f"record:{row['recordVaHex']}"] = va
    for row in selector_rows:
        va = parse_hex(row.get("rootVaHex"))
        if va is not None:
            targets[f"root:{row['rootVaHex']}:{row['groupId']}:{row['sequenceId']}"] = va
    for key in ["currentRootHex", "selectedPointerGlobalHex"]:
        va = parse_hex(selected_root_gap.get(key))
        if va is not None:
            targets[f"selectedRootGap:{key}"] = va

    xrefs = {}
    if args.exe.exists():
        exe = args.exe.read_bytes()
        sections = read_sections(exe)
        xrefs = direct_xrefs(exe, sections, targets)

    summary = {
        "sceneRecordCount": len(records),
        "selectorTraceCandidateCount": len(selector_rows),
        "groundedConnectionCount": sum(1 for row in connections if row["promotion"] == "grounded"),
        "partialConnectionCount": sum(1 for row in connections if row["promotion"] == "partial"),
        "blockedOrGapConnectionCount": sum(1 for row in connections if row["promotion"] in {"blocked", "gap"}),
        "gapCount": len(gaps),
        "directExecutionRootFound": False,
        "map1PromptCommandRows": opcode_summary.get("summary", {}).get("map1CommandRowsShown", 0),
        "map1CommandGroundedRows": trace_reliability["groundedRows"],
        "map1CommandCandidateRows": trace_reliability["candidateRows"],
        "map1CommandDefaultHandlerRows": trace_reliability["defaultHandlerRows"],
        "selectorAdjacencyOccurrenceCount": (scene_adjacency_index.get("currentPair") or {}).get("occurrenceCount", 0),
        "routeRootRefProofFound": bool(route_root_ref_context.get("routeRootRefProofFound")),
        "routeSelectorRootsTableOnly": bool(route_root_ref_context.get("allRouteSelectorRootsTableOnly")),
        "routeSelectorRootTextRefsFound": bool(route_root_ref_context.get("anyRouteSelectorRootTextRefs")),
        "routeRootRefMissingEvidenceCount": len(route_root_ref_context.get("missingEvidence") or []),
    }
    return {
        "scope": "map1_01a scene/event VM execution route review",
        "source": SOURCE,
        "target": TARGET,
        "promotionStatus": "partial-execution-route-blocked-direct-root-missing",
        "summary": summary,
        "sceneRecords": records,
        "selectorTraceCandidates": selector_rows,
        "connectionDecisions": connections,
        "directXrefs": xrefs,
        "commandTraceReliability": trace_reliability,
        "routeRootRefContext": {
            "promotionStatus": route_root_ref_context.get("promotionStatus") or "missing",
            "proofFound": bool(route_root_ref_context.get("routeRootRefProofFound")),
            "allRouteSelectorRootsTableOnly": bool(route_root_ref_context.get("allRouteSelectorRootsTableOnly")),
            "anyRouteSelectorRootTextRefs": bool(route_root_ref_context.get("anyRouteSelectorRootTextRefs")),
            "predecessorToCurrentRootRefFound": bool(route_root_ref_context.get("predecessorToCurrentRootRefFound")),
            "routeOrderProven": bool(route_root_ref_context.get("routeOrderProven")),
            "selectors": route_root_ref_context.get("selectors") or [],
            "missingEvidence": route_root_ref_context.get("missingEvidence") or [],
            "conclusion": route_root_ref_context.get("conclusion"),
        },
        "gaps": gaps,
        "sourceConclusions": {
            "clusterContext": cluster_context.get("conclusion"),
            "scenePayloadContext": scene_payload_context.get("conclusion"),
            "sceneRecordSequence": scene_record_sequence.get("conclusion"),
            "sceneListContext": scene_list_context.get("conclusion"),
            "sceneAdjacencyIndex": scene_adjacency_index.get("conclusion"),
            "selectedRootExecutionGap": selected_root_gap.get("selectedRootExecutionRejectionClassification"),
        },
    }


def html_page(payload: dict[str, Any]) -> str:
    def chip(status: str) -> str:
        cls = {"confirmed": "good", "grounded": "good", "partial": "warn", "candidate": "muted", "blocked": "bad", "gap": "bad"}.get(status, "muted")
        return f'<span class="tag {cls}">{h(status)}</span>'

    def rows_table(rows: list[dict[str, Any]], columns: list[tuple[str, str]]) -> str:
        body = []
        for row in rows:
            cells = []
            for key, label in columns:
                value = row.get(key)
                if isinstance(value, list):
                    value = ", ".join(map(str, value))
                if key == "promotion":
                    cells.append(f"<td>{chip(str(value))}</td>")
                elif str(key).endswith("Hex") or key in {"recordVaHex", "rootVaHex", "sceneIdHex"}:
                    cells.append(f"<td><code>{h(value or '-')}</code></td>")
                else:
                    cells.append(f"<td>{h(short(value, 220))}</td>")
            body.append("<tr>" + "".join(cells) + "</tr>")
        head = "".join(f"<th>{h(label)}</th>" for _key, label in columns)
        return f"<table><thead><tr>{head}</tr></thead><tbody>{''.join(body)}</tbody></table>"

    xref_rows = [
        {
            "target": name,
            "refs": row["totalRefCount"],
            "textRef": row["hasTextRef"],
            "sections": row["sectionCounts"],
            "samples": ", ".join(f"{s['section']}:{s['refVaHex']}" for s in row.get("samples", [])[:6]),
        }
        for name, row in payload.get("directXrefs", {}).items()
    ]
    root_ref = payload.get("routeRootRefContext") or {}
    root_ref_rows = [
        {
            "selector": row.get("selector"),
            "selectedRootHex": row.get("selectedRootHex"),
            "fieldMaps": row.get("fieldMaps") or [],
            "refs": f"{row.get('rowPointerRefCount')}/{row.get('selectedRootRefCount')}",
            "textRefs": row.get("textRefCount"),
            "tableOnly": row.get("tableOnlyChain"),
        }
        for row in root_ref.get("selectors") or []
    ]
    trace = payload.get("commandTraceReliability") or {}
    trace_metric_rows = [
        {"metric": "total rows", "value": trace.get("totalRows")},
        {"metric": "grounded rows", "value": trace.get("groundedRows")},
        {"metric": "candidate rows", "value": trace.get("candidateRows")},
        {"metric": "default-handler rows", "value": trace.get("defaultHandlerRows")},
        {"metric": "default-handler opcodes", "value": ", ".join(trace.get("defaultHandlerOpcodes") or [])},
    ]
    trace_opcode_rows = [
        {"opcode": opcode, "rows": count}
        for opcode, count in (trace.get("opcodeCounts") or {}).items()
    ]

    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="icon" href="../favicon.ico" />
  <title>Scene/Event VM 실행 루트 검토</title>
  <style>
    body {{ margin:0; background:#f6f7f9; color:#17202a; font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; line-height:1.45; }}
    main {{ max-width:1500px; margin:0 auto; padding:18px; }}
    header {{ display:flex; justify-content:space-between; gap:16px; align-items:flex-start; }}
    h1 {{ margin:0; font-size:24px; }}
    h2 {{ margin:0; font-size:17px; }}
    nav {{ display:flex; flex-wrap:wrap; gap:10px; justify-content:flex-end; }}
    a {{ color:#185abc; text-decoration:none; font-weight:700; }}
    a:hover {{ text-decoration:underline; }}
    section, details {{ background:#fff; border:1px solid #d8dee6; border-radius:8px; margin:14px 0; overflow:hidden; }}
    summary, .head {{ padding:12px 14px; background:#eef2f6; border-bottom:1px solid #d8dee6; font-weight:800; }}
    summary {{ cursor:pointer; }}
    .body {{ padding:14px; }}
    .metrics {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; }}
    .metric {{ border:1px solid #d8dee6; border-radius:6px; background:#f8fafc; padding:10px; }}
    .metric strong {{ display:block; font-size:22px; }}
    table {{ width:100%; border-collapse:collapse; }}
    th,td {{ padding:8px 10px; border-bottom:1px solid #d8dee6; vertical-align:top; text-align:left; font-size:13px; }}
    th {{ background:#f8fafc; color:#344050; }}
    code {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
    .tag {{ display:inline-block; padding:2px 7px; border-radius:999px; background:#edf2f7; color:#334155; font-size:12px; white-space:nowrap; }}
    .tag.good {{ color:#0f766e; background:#e6f4f1; }}
    .tag.warn {{ color:#a15c00; background:#fff4df; }}
    .tag.bad {{ color:#b42318; background:#fdebea; }}
    .tag.muted {{ color:#607080; background:#edf2f7; }}
    .muted {{ color:#607080; }}
  </style>
</head>
<body>
<main data-page="scene-event-vm-execution-route-review">
  <header>
    <div>
      <h1>Scene/Event VM 실행 루트 검토</h1>
      <p class="muted">map1_01a scene record가 실제 VM stream으로 실행되는지, prompt/choice/actor/map transition 연결을 분리한다.</p>
    </div>
    <nav>
      <a href="index.html">관리 홈</a>
      <a href="scene_event_vm_review.html">opcode/trace review</a>
      <a href="scene_event_vm_branch_flag_review.html">branch/flag</a>
      <a href="scene_event_vm_prompt_sequence_review.html">prompt sequence</a>
      <a href="scene_event_vm_choice_target_review.html">choice target</a>
      <a href="scene_event_vm_random_gate_review.html">random gate</a>
      <a href="selected_scene_text_root_consumer_review.html">selected root consumer</a>
      <a href="scene_event_runtime_evidence_handoff.html">runtime handoff</a>
      <a href="../out/scene_event_vm_execution_route_review.json">JSON</a>
      <a href="../docs/SCENE_EVENT_VM_REFERENCE.md">reference</a>
    </nav>
  </header>
  <section>
    <div class="head">요약</div>
    <div class="body metrics">
      <div class="metric"><strong>{h(payload['summary']['sceneRecordCount'])}</strong><span>map1_01a scene records</span></div>
      <div class="metric"><strong>{h(payload['summary']['map1PromptCommandRows'])}</strong><span>selector prompt command rows</span></div>
      <div class="metric"><strong>{h(payload['summary']['map1CommandGroundedRows'])}</strong><span>grounded command rows</span></div>
      <div class="metric"><strong>{h(payload['summary']['map1CommandDefaultHandlerRows'])}</strong><span>default-handler rows</span></div>
      <div class="metric"><strong>{h(payload['summary']['selectorAdjacencyOccurrenceCount'])}</strong><span>selector adjacency occurrences</span></div>
      <div class="metric"><strong>{h(payload['summary']['routeSelectorRootsTableOnly'])}</strong><span>selector roots table-only</span></div>
      <div class="metric"><strong>{h(payload['summary']['routeRootRefProofFound'])}</strong><span>root-ref route proof</span></div>
      <div class="metric"><strong>{h(payload['summary']['gapCount'])}</strong><span>execution route gaps</span></div>
    </div>
  </section>
  <section><div class="head">Scene Records</div>{rows_table(payload['sceneRecords'], [('recordVaHex','record'),('sceneIdHex','scene'),('promotion','promotion'),('executionStatus','execution status'),('role','role'),('saveSelectorRefCount','selector refs'),('tilesets','tilesets'),('sprites','sprites'),('note','note')])}</section>
  <section><div class="head">Selector/Text Trace Candidates</div>{rows_table(payload['selectorTraceCandidates'], [('groupId','group'),('sequenceId','sequence'),('promotion','promotion'),('evidenceStatus','evidence'),('rootVaHex','root'),('promptCount','prompts'),('choiceCount','choices'),('commandTraceRows','command rows'),('sample','sample')])}</section>
  <section>
    <div class="head">Command Trace Reliability</div>
    <div class="body"><p class="muted">{h(trace.get('conclusion'))}</p></div>
    {rows_table(trace_metric_rows, [('metric','metric'),('value','value')])}
  </section>
  <details><summary>Command Trace Opcode Counts</summary>{rows_table(trace_opcode_rows, [('opcode','opcode'),('rows','rows')])}</details>
  <section><div class="head">Connection Decisions</div>{rows_table(payload['connectionDecisions'], [('connection','connection'),('promotion','promotion'),('status','status'),('evidence','evidence'),('gap','gap')])}</section>
  <details open><summary>Direct Xrefs</summary>{rows_table(xref_rows, [('target','target'),('refs','refs'),('textRef','.text ref'),('sections','sections'),('samples','samples')])}</details>
  <section>
    <div class="head">Selector Root Ref Context</div>
    <div class="body">
      <p class="muted">{h(root_ref.get('conclusion'))}</p>
    </div>
    {rows_table(root_ref_rows, [('selector','selector'),('selectedRootHex','selected root'),('fieldMaps','field maps'),('refs','row/root refs'),('textRefs','.text refs'),('tableOnly','table only')])}
  </section>
  <section><div class="head">Gaps</div>{rows_table(payload['gaps'], [('id','id'),('status','status'),('reason','reason'),('nextEvidenceNeeded','next evidence')])}</section>
</main>
<script>
window.HWANSE_SCENE_EVENT_VM_EXECUTION_ROUTE_REVIEW_READY = {{
  directExecutionRootFound: {str(payload['summary']['directExecutionRootFound']).lower()},
  sceneRecordCount: {payload['summary']['sceneRecordCount']},
  map1PromptCommandRows: {payload['summary']['map1PromptCommandRows']},
  map1CommandGroundedRows: {payload['summary']['map1CommandGroundedRows']},
  map1CommandCandidateRows: {payload['summary']['map1CommandCandidateRows']},
  map1CommandDefaultHandlerRows: {payload['summary']['map1CommandDefaultHandlerRows']},
  selectorAdjacencyOccurrenceCount: {payload['summary']['selectorAdjacencyOccurrenceCount']},
  routeSelectorRootsTableOnly: {str(payload['summary']['routeSelectorRootsTableOnly']).lower()},
  routeSelectorRootTextRefsFound: {str(payload['summary']['routeSelectorRootTextRefsFound']).lower()},
  routeRootRefProofFound: {str(payload['summary']['routeRootRefProofFound']).lower()},
  routeRootRefMissingEvidenceCount: {payload['summary']['routeRootRefMissingEvidenceCount']},
  gapCount: {payload['summary']['gapCount']},
  candidatesSeparated: true,
  routeLinkedExecutionStillGap: true
}};
</script>
</body>
</html>
"""


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--scene-manifest", type=Path, default=OUT / "scene_manifest.json")
    parser.add_argument("--cluster-context", type=Path, default=OUT / "map1_01a_scene_record_cluster_context.json")
    parser.add_argument("--scene-script-player-data", type=Path, default=OUT / "scene_script_player_data.json")
    parser.add_argument("--opcode-dictionary", type=Path, default=OUT / "scene_event_vm_opcode_dictionary.json")
    parser.add_argument("--scene-prompt-probe", type=Path, default=OUT / "scene_prompt_reference_probe.json")
    parser.add_argument("--scene-payload-context", type=Path, default=OUT / "map1_01a_scene_payload_context.json")
    parser.add_argument("--scene-record-sequence", type=Path, default=OUT / "save_selector_scene_record_sequence.json")
    parser.add_argument("--scene-list-context", type=Path, default=OUT / "save_selector_scene_list_context.json")
    parser.add_argument("--scene-adjacency-index", type=Path, default=OUT / "save_selector_scene_adjacency_index.json")
    parser.add_argument("--selected-root-gap", type=Path, default=OUT / "save_selector_selected_root_execution_gap.json")
    parser.add_argument("--route-root-ref-context", type=Path, default=OUT / "save_selector_route_root_ref_context.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    args = parser.parse_args()

    payload = build(args)
    args.out_dir.mkdir(parents=True, exist_ok=True)
    write_json(args.out_dir / "scene_event_vm_execution_route_review.json", payload)
    write_text(args.out_dir / "scene_event_vm_execution_route_review.html", html_page(payload))
    write_text(WEB / "scene_event_vm_execution_route_review.html", html_page(payload))
    print(f"wrote scene/event VM execution route review -> {WEB / 'scene_event_vm_execution_route_review.html'}")


if __name__ == "__main__":
    main()
