#!/usr/bin/env python3
"""Summarize grounded event/object VM text opcode semantics."""
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"
TEXT_SOURCE_OPCODES = {"0x0b", "0x0c", "0x0d"}
DISPLAY_CURSOR_OPCODES = {"0x02"}
CONTROL_OPCODES = {
    "0x03": {
        "handlerVaHex": "0x0041b7bb",
        "semanticStatus": "display-state-opcode-grounded",
        "decodedLength": 4,
        "decodedLengthHex": "0x4",
        "operandLayout": "stream byte at +2 selects context+0xde display/font row",
        "effect": "context+0xde = stream u8+2; advance stream +4",
        "promotionImpact": "display state evidence only; browser replay does not execute full VM control flow",
    },
    "0x04": {
        "handlerVaHex": "0x0041b7ed",
        "semanticStatus": "display-timer-opcode-grounded",
        "decodedLength": 4,
        "decodedLengthHex": "0x4",
        "operandLayout": "stream word at +2 seeds context+0xca",
        "effect": "context+0xca = stream u16+2; context+0xcc = 0; advance stream +4",
        "promotionImpact": "display/timer state evidence only; browser replay does not execute full VM timing",
    },
    "0x06": {
        "handlerVaHex": "0x0041b8ee",
        "semanticStatus": "input-wait-opcode-grounded",
        "decodedLength": 4,
        "decodedLengthHex": "0x4",
        "operandLayout": "no promoted text operand; waits on global 0x47f1d4 and input mask 0x0059e312",
        "effect": "toggle press/release latch; set 0x55b280 stop flag; advance stream +4 only after release condition",
        "promotionImpact": "dialogue wait evidence only; full browser event VM wait loop remains unimplemented",
    },
    "0x07": {
        "handlerVaHex": "0x0041b956",
        "semanticStatus": "object-position-opcode-grounded",
        "decodedLength": 8,
        "decodedLengthHex": "0x8",
        "operandLayout": "stream words at +2/+4 become fixed-point x/y",
        "effect": "context+0x1c = stream u16+2 << 16; context+0x20 = stream u16+4 << 16; advance stream +8",
        "promotionImpact": "object placement evidence only; route-linked event object execution still missing",
    },
    "0x08": {
        "handlerVaHex": "0x0041b9a0",
        "semanticStatus": "display-style-opcode-grounded",
        "decodedLength": None,
        "decodedLengthHex": "mode0=0x4/mode1=0x8",
        "decodedLengthVariants": {"mode0": 4, "mode1": 8},
        "operandLayout": "stream byte +2 mode; mode0 uses table index byte +3 at 0x59daf0, mode1 uses immediate bytes +5/+6/+7",
        "effect": "write context+0xe0/+0xe2/+0xe4 from palette/style table or immediate RGB-like triplet",
        "promotionImpact": "display style evidence only; variable-length VM execution remains partial",
    },
    "0x09": {
        "handlerVaHex": "0x0041bac6",
        "semanticStatus": "control-flow-call-opcode-grounded",
        "decodedLength": 8,
        "decodedLengthHex": "0x8",
        "operandLayout": "stream dword at +4 is branch target; stream+8 is pushed as continuation",
        "effect": "push continuation pointer to context+0xb4 stack; increment context+0xc8; set stream to target dword",
        "promotionImpact": "VM control-flow evidence only; browser replay does not follow original event branches",
    },
    "0x0a": {
        "handlerVaHex": "0x0041bb15",
        "semanticStatus": "control-flow-return-opcode-grounded",
        "decodedLength": None,
        "decodedLengthHex": "stack-pop",
        "operandLayout": "no stream operand; pops continuation from context+0xb4 stack using context+0xc8",
        "effect": "decrement context+0xc8; set stream to saved continuation pointer context+0xb4[index]",
        "promotionImpact": "VM control-flow evidence only; browser replay does not execute original call/return branches",
    },
    "0x0e": {
        "handlerVaHex": "0x0041bd4f",
        "semanticStatus": "cursor-relative-opcode-grounded",
        "decodedLength": 8,
        "decodedLengthHex": "0x8",
        "operandLayout": "stream words at +2/+4 optionally offset current text origin unless 0xffff sentinel",
        "effect": "context+0xd6 = context+0xce + u16+2 when not 0xffff; context+0xda = context+0xd2 + u16+4 when not 0xffff; advance +8",
        "promotionImpact": "cursor positioning evidence only; browser replay does not execute full layout VM",
    },
    "0x15": {
        "handlerVaHex": "0x0041d476",
        "semanticStatus": "cursor-origin-relative-opcode-grounded",
        "decodedLength": 8,
        "decodedLengthHex": "0x8",
        "operandLayout": "stream words at +2/+4 optionally offset and update text origin unless 0xffff sentinel",
        "effect": "context+0xce/context+0xd6 and context+0xd2/context+0xda are moved by stream offsets; advance +8",
        "promotionImpact": "cursor origin evidence only; browser replay does not execute full layout VM",
    },
    "0x1b": {
        "handlerVaHex": "0x0041d89d",
        "semanticStatus": "data-bank-select-opcode-grounded",
        "decodedLength": 4,
        "decodedLengthHex": "0x4",
        "operandLayout": "stream byte at +2 selects context+0xa8 backing table; mode 2 also uses stream byte +3",
        "effect": "context+0xa8 becomes 0x59e310, 0x4576d8, 0x457750 + shaped index, or table 0x59db30[context+0xf2]",
        "promotionImpact": "state-table selection evidence only; full object/script data VM remains partial",
    },
    "0x37": {
        "handlerVaHex": "0x00420028",
        "semanticStatus": "global-timer-opcode-grounded",
        "decodedLength": 4,
        "decodedLengthHex": "0x4",
        "operandLayout": "no stream operand; reads global byte 0x457745",
        "effect": "context+0xca = byte[0x457745]; context+0xcc = 0; advance stream +4",
        "promotionImpact": "global display/timer state evidence only; not event route execution proof",
    },
}
PARTIAL_BROWSER_OPCODES = ["0x02", "0x0b", "0x0d", "0x35"]


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


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 table_entry(entries: list[dict], opcode_hex: str) -> dict:
    return next((row for row in entries if row.get("opcodeHex") == opcode_hex), {})


def handler_effects_by_opcode(event_text_source_flow: dict) -> dict[str, dict]:
    return {
        row.get("opcodeHex"): row
        for row in event_text_source_flow.get("handlerEffects") or []
        if row.get("opcodeHex")
    }


def opcode_semantic_row(
    opcode_hex: str,
    event_entries: list[dict],
    effects: dict[str, dict],
    command_counts: dict,
    dialogue_counts: dict,
) -> dict:
    entry = table_entry(event_entries, opcode_hex)
    effect = effects.get(opcode_hex) or {}
    if opcode_hex == "0x35":
        return {
            "opcodeHex": opcode_hex,
            "handlerVaHex": entry.get("handlerVaHex"),
            "handlerSection": entry.get("handlerSection"),
            "semanticStatus": "literal-storage-marker",
            "decodedLength": None,
            "decodedLengthHex": None,
            "operandLayout": "CP949 text bytes are preserved as literalTextEvents from dialogue block storage; handler semantics are not promoted.",
            "effect": "storage-indexed literal payload marker used by partial browser replay evidence",
            "source": "out/event_dialogue_blocks.json",
            "commandCount": dialogue_counts.get(opcode_hex, 0),
            "browserReplayEnabled": True,
            "promotionImpact": "storage evidence only; not route-linked VM execution",
        }
    if opcode_hex == "0x02":
        return {
            "opcodeHex": opcode_hex,
            "handlerVaHex": entry.get("handlerVaHex") or "0x0041b771",
            "handlerSection": entry.get("handlerSection") or ".text",
            "semanticStatus": "display-cursor-opcode-grounded",
            "decodedLength": 4,
            "decodedLengthHex": "0x4",
            "operandLayout": "no promoted text id operand; updates cursor fields from current context and font-row table",
            "effect": "context+0xd6=context+0xce; context+0xda += table[context+0xde]; advance stream +4",
            "source": "Hwanse2.exe handler 0x0041b771 plus out/event_dialogue_blocks.json storage scan",
            "commandCount": dialogue_counts.get(opcode_hex, 0),
            "browserReplayEnabled": True,
            "promotionImpact": "supports partial dialogue cursor/separator trace only; route-linked execution still missing",
        }
    if opcode_hex in CONTROL_OPCODES:
        row = dict(CONTROL_OPCODES[opcode_hex])
        return {
            "opcodeHex": opcode_hex,
            "handlerVaHex": entry.get("handlerVaHex") or row["handlerVaHex"],
            "handlerSection": entry.get("handlerSection") or ".text",
            "semanticStatus": row["semanticStatus"],
            "decodedLength": row.get("decodedLength"),
            "decodedLengthHex": row.get("decodedLengthHex"),
            "decodedLengthVariants": row.get("decodedLengthVariants"),
            "operandLayout": row["operandLayout"],
            "effect": row["effect"],
            "source": f"Hwanse2.exe handler {row['handlerVaHex']} plus out/event_dialogue_blocks.json storage scan",
            "commandCount": dialogue_counts.get(opcode_hex, 0),
            "browserReplayEnabled": False,
            "promotionImpact": row["promotionImpact"],
        }
    decoded_length = effect.get("length")
    return {
        "opcodeHex": opcode_hex,
        "handlerVaHex": effect.get("handlerVaHex") or entry.get("handlerVaHex"),
        "handlerSection": entry.get("handlerSection"),
        "semanticStatus": "text-source-opcode-grounded",
        "decodedLength": decoded_length,
        "decodedLengthHex": f"0x{decoded_length:x}" if isinstance(decoded_length, int) else None,
        "operandLayout": effect.get("source"),
        "effect": effect.get("effect"),
        "source": "out/event_text_source_flow.json",
        "commandCount": command_counts.get(opcode_hex, 0),
        "browserReplayEnabled": opcode_hex in {"0x0b", "0x0d"},
        "promotionImpact": (
            "0x0c route-linked hits are pointer overlap, so this opcode stays non-promoting"
            if opcode_hex == "0x0c"
            else "supports partial dialogue replay only; route-linked execution still missing"
        ),
    }


def build_summary(
    branch_state_dispatch: dict,
    event_text_source_flow: dict,
    event_dialogue_blocks: dict,
) -> dict:
    event_table = branch_state_dispatch.get("eventHandlerTable") or {}
    event_entries = branch_state_dispatch.get("tableEntries") or []
    effects = handler_effects_by_opcode(event_text_source_flow)
    command_counts = event_text_source_flow.get("commandCountsByOpcode") or {}
    dialogue_counts = event_dialogue_blocks.get("commandOpcodeCounts") or {}
    opcode_rows = [
        opcode_semantic_row(opcode, event_entries, effects, command_counts, dialogue_counts)
        for opcode in [
            "0x02",
            "0x03",
            "0x04",
            "0x06",
            "0x07",
            "0x08",
            "0x09",
            "0x0a",
            "0x0b",
            "0x0c",
            "0x0d",
            "0x0e",
            "0x15",
            "0x1b",
            "0x35",
            "0x37",
        ]
    ]
    decoded_text_rows = [
        row
        for row in opcode_rows
        if row["opcodeHex"] in TEXT_SOURCE_OPCODES and isinstance(row.get("decodedLength"), int)
    ]
    dispatcher_checks = event_table.get("dispatcherByteChecks") or []
    dispatcher_verified = (
        event_table.get("dispatcherVerified") is True
        and bool(dispatcher_checks)
        and all(row.get("matches") is True for row in dispatcher_checks)
    )
    browser_opcode_set = sorted({
        opcode
        for block in event_dialogue_blocks.get("blocks") or []
        for opcode in ((block.get("vmTrace") or {}).get("implementedOpcodes") or [])
    })
    grounded_control_event_count = sum(
        int((block.get("vmTrace") or {}).get("groundedControlEventCount") or 0)
        for block in event_dialogue_blocks.get("blocks") or []
    )
    grounded_control_event_opcode_set = sorted({
        opcode
        for block in event_dialogue_blocks.get("blocks") or []
        for opcode in ((block.get("vmTrace") or {}).get("groundedControlOpcodeSet") or [])
    })
    grounded_control_sample_effect_count = sum(
        1
        for block in event_dialogue_blocks.get("blocks") or []
        for event in ((block.get("vmTrace") or {}).get("groundedControlEvents") or [])
        if event.get("browserExecutesEffect") is True
    )
    control_opcode_command_count = sum(
        int(dialogue_counts.get(opcode, 0))
        for opcode in CONTROL_OPCODES
    )
    opcode0c_pointer_overlap = event_text_source_flow.get("opcode0cAllPointerOverlap") is True
    coverage = {
        "dispatcherVerified": dispatcher_verified,
        "dispatcherTableVaHex": event_table.get("tableVaHex"),
        "dispatcherVaHex": event_table.get("dispatcherVaHex"),
        "dispatchCallVaHex": event_table.get("dispatchCallVaHex"),
        "textSourceOpcodeCount": len(TEXT_SOURCE_OPCODES),
        "decodedTextSourceOpcodeCount": len(decoded_text_rows),
        "decodedTextSourceOpcodeLengths": {
            row["opcodeHex"]: row["decodedLength"]
            for row in decoded_text_rows
        },
        "textSourceOpcodeLengthsDecoded": len(decoded_text_rows) == len(TEXT_SOURCE_OPCODES),
        "textSourceOperandSemanticsGrounded": all(row.get("effect") and row.get("operandLayout") for row in decoded_text_rows),
        "displayCursorOpcodeCount": len(DISPLAY_CURSOR_OPCODES),
        "decodedDisplayCursorOpcodeCount": sum(
            1
            for row in opcode_rows
            if row["opcodeHex"] in DISPLAY_CURSOR_OPCODES and isinstance(row.get("decodedLength"), int)
        ),
        "decodedDisplayCursorOpcodeLengths": {
            row["opcodeHex"]: row["decodedLength"]
            for row in opcode_rows
            if row["opcodeHex"] in DISPLAY_CURSOR_OPCODES and isinstance(row.get("decodedLength"), int)
        },
        "displayCursorOpcodeGrounded": (table_entry(event_entries, "0x02").get("handlerVaHex") == "0x0041b771"),
        "displayCursorCommandCount": dialogue_counts.get("0x02", 0),
        "controlOpcodeCount": len(CONTROL_OPCODES),
        "groundedControlOpcodeCount": sum(
            1
            for opcode, row in CONTROL_OPCODES.items()
            if table_entry(event_entries, opcode).get("handlerVaHex") == row["handlerVaHex"]
        ),
        "controlOpcodeSemanticsGrounded": all(
            table_entry(event_entries, opcode).get("handlerVaHex") == row["handlerVaHex"]
            for opcode, row in CONTROL_OPCODES.items()
        ),
        "decodedControlOpcodeLengths": {
            row["opcodeHex"]: row["decodedLength"]
            for row in opcode_rows
            if row["opcodeHex"] in CONTROL_OPCODES and isinstance(row.get("decodedLength"), int)
        },
        "variableLengthControlOpcodes": {
            row["opcodeHex"]: row.get("decodedLengthVariants")
            for row in opcode_rows
            if row["opcodeHex"] in CONTROL_OPCODES and row.get("decodedLengthVariants")
        },
        "controlOpcodeCommandCounts": {
            opcode: dialogue_counts.get(opcode, 0)
            for opcode in CONTROL_OPCODES
        },
        "groundedControlCommandCount": control_opcode_command_count,
        "groundedControlEventCount": grounded_control_event_count,
        "groundedControlEventOpcodeSet": grounded_control_event_opcode_set,
        "groundedControlSampleEffectExecutionCount": grounded_control_sample_effect_count,
        "literalStorageOpcodeObserved": dialogue_counts.get("0x35", 0) > 0,
        "literalStorageEventCount": sum(
            int((block.get("vmTrace") or {}).get("literalTextEventCount") or 0)
            for block in event_dialogue_blocks.get("blocks") or []
        ),
        "opcode0cPointerOverlapNotPromoted": opcode0c_pointer_overlap,
        "routeLinkedCommandCount": event_text_source_flow.get("routeLinkedCommandCount", 0),
        "routeLinkedCommandsArePointerOverlap": opcode0c_pointer_overlap,
        "browserPartialReplayOpcodeSet": browser_opcode_set,
        "browserPartialReplayMatchesTextSemantics": browser_opcode_set == PARTIAL_BROWSER_OPCODES,
        "browserGroundedControlEventsPreserved": (
            grounded_control_event_count == control_opcode_command_count
            and grounded_control_event_opcode_set == sorted(CONTROL_OPCODES)
            and grounded_control_sample_effect_count == 0
        ),
        "browserFullEventVmImplemented": False,
        "routeLinkedEventVmExecution": False,
        "fullInstructionLengthFullyDecoded": False,
        "fullOperandLayoutFullyDecoded": False,
    }
    return {
        "scope": "Grounded text-related event/object VM opcode semantics from Hwanse2.exe handler evidence.",
        "source": [
            "out/save_selector_branch_state_dispatch.json",
            "out/event_text_source_flow.json",
            "out/event_dialogue_blocks.json",
        ],
        "promotionStatus": "text-opcode-semantics-grounded-route-execution-missing",
        "eventObjectDispatcher": {
            "tableVaHex": event_table.get("tableVaHex"),
            "entryCount": event_table.get("entryCount"),
            "dispatcherVaHex": event_table.get("dispatcherVaHex"),
            "dispatchCallVaHex": event_table.get("dispatchCallVaHex"),
            "dispatcherVerified": dispatcher_verified,
            "byteCheckCount": len(dispatcher_checks),
        },
        "opcodeSemantics": opcode_rows,
        "coverage": coverage,
        "checks": {
            "eventObjectDispatcherVerified": dispatcher_verified,
            "textSourceOpcodeLengthsDecoded": coverage["textSourceOpcodeLengthsDecoded"],
            "textSourceOperandSemanticsGrounded": coverage["textSourceOperandSemanticsGrounded"],
            "displayCursorOpcodeGrounded": coverage["displayCursorOpcodeGrounded"],
            "controlOpcodeSemanticsGrounded": coverage["controlOpcodeSemanticsGrounded"],
            "browserGroundedControlEventsPreserved": coverage["browserGroundedControlEventsPreserved"],
            "literalStorageOpcodeObserved": coverage["literalStorageOpcodeObserved"],
            "opcode0cPointerOverlapNotPromoted": coverage["opcode0cPointerOverlapNotPromoted"],
            "browserPartialReplayMatchesTextSemantics": coverage["browserPartialReplayMatchesTextSemantics"],
            "routeLinkedEventVmExecution": False,
            "browserFullEventVmImplemented": False,
            "fullInstructionLengthFullyDecoded": False,
            "fullOperandLayoutFullyDecoded": False,
        },
        "nextEvidenceNeeded": [
            "Decode non-text event/object opcodes that appear in dialogue blocks.",
            "Capture route-linked event/object VM execution from a normal map interaction.",
            "Bind scene event records to a dialogue block through executed VM control flow.",
        ],
        "conclusion": (
            "Event/object VM dispatch is byte-table grounded, and text-source opcodes 0x0b, 0x0c, and 0x0d "
            "now have explicit length/operand/effect rows; opcode 0x02 also has a grounded cursor/separator "
            "length/effect row, and opcodes 0x03/0x04/0x06/0x07/0x08/0x09/0x0a/0x0e/0x15/0x1b/0x37 have "
            "handler-grounded display, wait, position, style, data-bank, and control-flow rows. This narrows the "
            "VM semantics gap without promoting "
            "route-linked event execution: the route-linked 0x0c hits remain pointer overlaps, 0x35 is literal "
            "storage evidence, browser replay remains partial, and full event VM instruction decoding is still "
            "incomplete."
        ),
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Event VM Opcode Semantics",
        "",
        summary["conclusion"],
        "",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        "## Dispatcher",
        "",
        "| field | value |",
        "| --- | --- |",
    ]
    for key, value in summary["eventObjectDispatcher"].items():
        lines.append(f"| {key} | {json_value(value)} |")
    lines.extend([
        "",
        "## Opcode Semantics",
        "",
        "| opcode | handler | status | length | operand layout | command count | browser replay | promotion impact |",
        "| --- | --- | --- | --- | --- | ---: | --- | --- |",
    ])
    for row in summary["opcodeSemantics"]:
        lines.append(
            f"| `{row['opcodeHex']}` | `{row.get('handlerVaHex') or '-'}` | {row['semanticStatus']} | "
            f"{row.get('decodedLengthHex') or '-'} | {row.get('operandLayout') or '-'} | "
            f"{row.get('commandCount') or 0} | {row.get('browserReplayEnabled')} | {row['promotionImpact']} |"
        )
    lines.extend([
        "",
        "## Coverage",
        "",
        "| field | value |",
        "| --- | --- |",
    ])
    for key, value in summary["coverage"].items():
        lines.append(f"| {key} | {json_value(value)} |")
    lines.extend([
        "",
        "## Next Evidence Needed",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["nextEvidenceNeeded"])
    return "\n".join(lines) + "\n"


def html_table(rows: dict) -> str:
    return "\n".join(
        f"<tr><td>{html.escape(key)}</td><td>{html.escape(json_value(value))}</td></tr>"
        for key, value in rows.items()
    )


def html_page(summary: dict) -> str:
    opcode_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(row['opcodeHex'])}</code></td>"
        f"<td><code>{html.escape(row.get('handlerVaHex') or '-')}</code></td>"
        f"<td>{html.escape(row['semanticStatus'])}</td>"
        f"<td>{html.escape(row.get('decodedLengthHex') or '-')}</td>"
        f"<td>{html.escape(str(row.get('operandLayout') or '-'))}</td>"
        f"<td>{html.escape(str(row.get('commandCount') or 0))}</td>"
        f"<td>{html.escape(str(row.get('browserReplayEnabled')))}</td>"
        f"<td>{html.escape(row['promotionImpact'])}</td>"
        "</tr>"
        for row in summary["opcodeSemantics"]
    )
    next_items = "\n".join(f"<li>{html.escape(item)}</li>" for item in summary["nextEvidenceNeeded"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        "  <title>Event VM Opcode Semantics</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}</style>",
        "</head>",
        "<body>",
        "  <h1>Event VM Opcode Semantics</h1>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        f"  <p><b>Promotion status:</b> <code>{html.escape(summary['promotionStatus'])}</code></p>",
        "  <h2>Dispatcher</h2>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{html_table(summary['eventObjectDispatcher'])}</tbody></table>",
        "  <h2>Opcode Semantics</h2>",
        "  <table><thead><tr><th>opcode</th><th>handler</th><th>status</th><th>length</th><th>operand layout</th><th>commands</th><th>browser replay</th><th>promotion impact</th></tr></thead>",
        f"  <tbody>{opcode_rows}</tbody></table>",
        "  <h2>Coverage</h2>",
        f"  <table><thead><tr><th>field</th><th>value</th></tr></thead><tbody>{html_table(summary['coverage'])}</tbody></table>",
        "  <h2>Next Evidence Needed</h2>",
        f"  <ul>{next_items}</ul>",
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "event_vm_opcode_semantics.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "event_vm_opcode_semantics.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(
        load_json(args.out_dir / "save_selector_branch_state_dispatch.json", {}),
        load_json(args.out_dir / "event_text_source_flow.json", {}),
        load_json(args.out_dir / "event_dialogue_blocks.json", {}),
    )
    write_outputs(summary, args.out_dir)
    print(f"wrote event VM opcode semantics -> {args.out_dir / 'event_vm_opcode_semantics.html'}")


if __name__ == "__main__":
    main()
