#!/usr/bin/env python3
"""Summarize the remaining execution boundary for map animations.

The visual previews in map_review are useful, but they are not the same as an
EXE-proven runtime animation stream.  This report keeps the proven EXE pieces
separate from the still-unbound visible motion hypothesis.
"""
from __future__ import annotations

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


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


def read_json(name: str) -> dict[str, Any]:
    try:
        data = json.loads((OUT / name).read_text(encoding="utf-8"))
    except FileNotFoundError:
        return {}
    return data if isinstance(data, dict) else {}


def summary(data: dict[str, Any]) -> dict[str, Any]:
    value = data.get("summary")
    return value if isinstance(value, dict) else {}


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


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


def build_report() -> dict[str, Any]:
    tile = read_json("map_animation_tile_review.json")
    tile_summary = summary(tile)
    exe = read_json("map_animation_exe_pattern_review.json")
    exe_summary = summary(exe)
    palette = read_json("map_palette_handler_trace_review.json")
    palette_summary = summary(palette)
    shift = read_json("map_animation_shift_hypothesis_review.json")
    shift_summary = summary(shift)
    resource_bridge = read_json("resource_command_vm_bridge_review.json")
    resource_summary = summary(resource_bridge)
    resource_probe = read_json("map_animation_resource_binding_probe.json")
    resource_probe_summary = summary(resource_probe)
    live_writer = read_json("map_animation_live_buffer_writer_review.json")
    live_writer_summary = summary(live_writer)
    object_relative = read_json("map_animation_object_relative_tile_write_review.json")
    object_relative_summary = summary(object_relative)
    redraw_consumer = read_json("map_animation_redraw_consumer_review.json")
    redraw_summary = summary(redraw_consumer)
    draw_transform = read_json("map_animation_draw_tile_transform_review.json")
    draw_transform_summary = summary(draw_transform)
    tick_route = read_json("map_animation_tick_route_review.json")
    tick_route_summary = summary(tick_route)
    active_script_tile_write = read_json("map_animation_active_object_script_tile_write_review.json")
    active_script_tile_write_summary = summary(active_script_tile_write)
    frame_script_tile_write = read_json("map_animation_frame_script_tile_write_review.json")
    frame_script_tile_write_summary = summary(frame_script_tile_write)
    non_object_tile_write = read_json("map_animation_non_object_tile_write_root_review.json")
    non_object_tile_write_summary = summary(non_object_tile_write)
    specialized_loop = read_json("map_animation_specialized_loop_boundary_review.json")
    specialized_loop_summary = summary(specialized_loop)

    animated_maps = [
        {
            "map": row.get("map"),
            "sourceTileset": row.get("sourceTileset"),
            "animatedCellCount": row.get("animatedCellCount"),
            "componentCount": row.get("componentCount"),
            "paletteRanges": (row.get("paletteUsage") or {}).get("ranges") or [],
        }
        for row in tile.get("maps", [])
    ]

    surfaces = [
        {
            "surface": "layer1 0x40 redraw coverage",
            "promotion": "confirmed",
            "artifact": "map_animation_tile_review.json",
            "finding": (
                f"{tile_summary.get('animatedMapCount', 0)} maps / "
                f"{tile_summary.get('animatedCellCount', 0)} cells are flagged. "
                "EXE reads layer1 and treats bit 0x40 as dirty redraw coverage."
            ),
            "gap": "이 단계는 어느 타일/팔레트 프레임으로 바꿀지 고르지 않는다.",
        },
        {
            "surface": "palette VM handlers",
            "promotion": "confirmed-handler / unbound-map-root",
            "artifact": "map_palette_handler_trace_review.json",
            "finding": (
                f"opcode 0x38/0x39 handlers are table-grounded; "
                f"exactPerMapPaletteRootProven={palette_summary.get('exactPerMapPaletteRootProven')}."
            ),
            "gap": "사용자 관찰상 팔레트만으로는 폭포/모닥불의 공간 이동을 설명하지 못하므로 visible motion proof로 승격하지 않는다.",
        },
        {
            "surface": "tile-grid write handlers",
            "promotion": "confirmed-handler / no animated write",
            "artifact": "map_animation_exe_pattern_review.json",
            "finding": (
                f"plausible opcode 0x58 hits {exe_summary.get('tileWritePlausibleAlignedOpcode58Count', 0)}, "
                f"direct animated tile writes {exe_summary.get('tileWriteDirectAnimatedTileWriteCount', 0)}, "
                f"coordinate-only hits {exe_summary.get('tileWriteCoordinateOnlyAlignedHitCount', 0)}."
            ),
            "gap": "known 0x40 source tile id를 직접 쓰는 실행 stream은 아직 없다.",
        },
        {
            "surface": "simple tile replacement table",
            "promotion": "negative-scan",
            "artifact": "map_animation_exe_pattern_review.json",
            "finding": (
                f"known fire/waterfall sequences scanned {exe_summary.get('tileSequenceRowsScanned', 0)}, "
                f"contiguous hits {exe_summary.get('tileSequenceRowsWithContiguousHits', 0)}."
            ),
            "gap": "단순 u8/u16/u32 연속 테이블 가설은 현재 음성이다. 압축/산술/VM 실행 가능성은 남는다.",
        },
        {
            "surface": "visual shift hypothesis",
            "promotion": "human-review-candidate",
            "artifact": "map_animation_shift_hypothesis_review.json",
            "finding": (
                f"component classes {shift_summary.get('classes', {})}; "
                "map_review preview용 후보로는 유효하지만 EXE 실행 근거는 아니다."
            ),
            "gap": "offset/row-cycle 후보를 실제 map root command stream과 연결해야 한다.",
        },
        {
            "surface": "map resource loader / VM bridge",
            "promotion": "resource-loader-grounded / animation-root-unbound",
            "artifact": "resource_command_vm_bridge_review.json",
            "finding": (
                f"field map opcode10 records {resource_summary.get('fieldMapOpcode10RecordCount', 0)}, "
                f"direct resource runner calls {resource_summary.get('directResourceRunnerCallCount', 0)}, "
                f"routeProofFound={resource_summary.get('routeProofFound')}."
            ),
            "gap": "map resource loading root가 animation frame loop를 실행한다는 증거가 아직 없다.",
        },
        {
            "surface": "animated-map resource roots vs animation commands",
            "promotion": "negative-binding-probe",
            "artifact": "map_animation_resource_binding_probe.json",
            "finding": (
                f"animated maps with exact resource roots {resource_probe_summary.get('mapsWithExactResourceRoot', 0)}/"
                f"{resource_probe_summary.get('animatedMapCount', 0)}, "
                f"palette commands inside exact roots {resource_probe_summary.get('paletteCommandInsideExactResourceRootCount', 0)}, "
                f"tile-write commands inside exact roots {resource_probe_summary.get('tileWriteCommandInsideExactResourceRootCount', 0)}."
            ),
            "gap": "맵 리소스 root 자체가 per-frame animation loop root는 아닌 것으로 좁혀졌다.",
        },
        {
            "surface": "live map buffer writers",
            "promotion": "confirmed-handler / producer-unbound",
            "artifact": "map_animation_live_buffer_writer_review.json",
            "finding": (
                f"VM write-like live-buffer refs {live_writer_summary.get('candidateVmTileWriteRefCount', 0)}, "
                f"refs inside animated roots {live_writer_summary.get('liveBufferRefsInsideAnimatedResourceRoots', 0)}, "
                f"handler opcodes {live_writer_summary.get('handlerOpcodeCount', 0)}."
            ),
            "gap": "live layer0/layer1 writer handlers are grounded, but no animated-map tick/root producer binds them to visible 0x40 motion.",
        },
        {
            "surface": "object-relative live tile-write command scan",
            "promotion": "negative-binding-scan",
            "artifact": "map_animation_object_relative_tile_write_review.json",
            "finding": (
                f"0x6b/0x76 candidates {object_relative_summary.get('candidateCount', 0)}, "
                f"animated tile hits {object_relative_summary.get('animatedTileDirectHitCount', 0)}, "
                f"animated root candidates {object_relative_summary.get('animatedRootCandidateCount', 0)}."
            ),
            "gap": "object-relative tile-write streams currently bind to nonanimated/unbound resource groups, not the visible fire/waterfall loop.",
        },
        {
            "surface": "viewport redraw consumer",
            "promotion": "confirmed-redraw-consumer / producer-unbound",
            "artifact": "map_animation_redraw_consumer_review.json",
            "finding": (
                f"37x21 viewport scan grounded={redraw_summary.get('redrawPathGrounded')}; "
                f"alternate tile selection found={redraw_summary.get('alternateTileSelectionFound')}."
            ),
            "gap": "0x40 cells are invalidated/redrawn here, but visible frame selection still has to come from a producer or draw-time transform.",
        },
        {
            "surface": "draw-time tile id to source rect transform",
            "promotion": "confirmed-render-transform / negative-animation-selector",
            "artifact": "map_animation_draw_tile_transform_review.json",
            "finding": (
                f"tile rect converter grounded={draw_transform_summary.get('tileIdToSourceRectConverterGrounded')}; "
                f"uses frame counter={draw_transform_summary.get('usesFrameCounter')}; "
                f"uses 0x40={draw_transform_summary.get('usesLayer1AnimatedFlag0x40')}."
            ),
            "gap": "The inspected draw path converts tile ids by fixed grid math and does not implement fire/waterfall frame selection.",
        },
        {
            "surface": "field tick route / active-object script runner",
            "promotion": "grounded-route / indirect-producer-candidate",
            "artifact": "map_animation_tick_route_review.json",
            "finding": (
                f"tick route {tick_route_summary.get('fieldTickRouteVaHex', 'unknown')} is grounded; "
                f"direct live-buffer refs in tick/update windows "
                f"{tick_route_summary.get('directLiveBufferRefsInTickRoute', 0)}+"
                f"{tick_route_summary.get('directLiveBufferRefsInPerFrameUpdates', 0)}; "
                f"active-object delayed script route grounded="
                f"{tick_route_summary.get('activeObjectDelayedScriptRouteGrounded', False)}."
            ),
            "gap": "Need the specific animated-map active-object script/root that executes tile-write opcodes or an equivalent producer.",
        },
        {
            "surface": "strict active-object +0xec scripts",
            "promotion": "negative-script-inventory-scan",
            "artifact": "map_animation_active_object_script_tile_write_review.json",
            "finding": (
                f"strict scripts {active_script_tile_write_summary.get('strictObjectEcScriptCount', 0)}, "
                f"command-boundary tile writes {active_script_tile_write_summary.get('commandBoundaryTileWriteCount', 0)}, "
                f"raw unpromoted candidates {active_script_tile_write_summary.get('rawTileWriteCandidateCount', 0)}."
            ),
            "gap": "The remaining producer, if static, is likely outside the strict +0xec inventory or is runtime-installed into object +0x64/+0x40.",
        },
        {
            "surface": "runtime-installed object +0x64 frame scripts",
            "promotion": "negative-frame-script-scan",
            "artifact": "map_animation_frame_script_tile_write_review.json",
            "finding": (
                f"initializer +0x40/+0x64/+0xec writes "
                f"{frame_script_tile_write_summary.get('initializerField40VaWriteCount', 0)}/"
                f"{frame_script_tile_write_summary.get('initializerField64VaWriteCount', 0)}/"
                f"{frame_script_tile_write_summary.get('initializerFieldEcVaWriteCount', 0)}; "
                f"opcode20 targets {frame_script_tile_write_summary.get('opcode20AttachTargetCount', 0)}, "
                f"opcode18 state entries {frame_script_tile_write_summary.get('opcode18StateEntryTargetCount', 0)}, "
                f"command-boundary tile writes "
                f"{frame_script_tile_write_summary.get('opcode20CommandBoundaryTileWriteCount', 0)}+"
                f"{frame_script_tile_write_summary.get('opcode18CommandBoundaryTileWriteCount', 0)}."
            ),
            "gap": "The object frame-script route is grounded for actor/frame animation, but not for map visible-tile mutation.",
        },
        {
            "surface": "non-object / remaining tile-write VM roots",
            "promotion": "negative-root-scan",
            "artifact": "map_animation_non_object_tile_write_root_review.json",
            "finding": (
                f"aligned tile-write commands {non_object_tile_write_summary.get('alignedTileWriteCommandCount', 0)}, "
                f"roots {non_object_tile_write_summary.get('uniqueRootCount', 0)}, "
                f"direct animated evidence commands {non_object_tile_write_summary.get('directAnimatedEvidenceCommandCount', 0)}, "
                f"promoted commands {non_object_tile_write_summary.get('promotedAnimatedProducerCommandCount', 0)}."
            ),
            "gap": "Remaining VM-shaped tile-write roots do not both bind to an animated map and write known animated tile/coordinate evidence.",
        },
        {
            "surface": "specialized tick/draw loop",
            "promotion": "negative-specialized-loop-scan",
            "artifact": "map_animation_specialized_loop_boundary_review.json",
            "finding": (
                f"inspected windows {specialized_loop_summary.get('inspectedWindowCount', 0)}, "
                f"direct callees {specialized_loop_summary.get('directCalleeCount', 0)}, "
                f"callee tile-state refs {specialized_loop_summary.get('calleeTileStateRefCount', 0)}, "
                f"strong candidates {specialized_loop_summary.get('strongSpecializedCandidateCount', 0)}."
            ),
            "gap": "Tick/redraw/draw 주변 direct callee 안에서도 별도 live layer0/layer1 producer가 승격되지 않았다.",
        },
    ]

    return {
        "kind": "hwanse-map-animation-execution-boundary-review",
        "status": "visible-motion-execution-binding-unproven",
        "source": [
            "out/map_animation_tile_review.json",
            "out/map_animation_exe_pattern_review.json",
            "out/map_palette_handler_trace_review.json",
            "out/map_animation_shift_hypothesis_review.json",
            "out/resource_command_vm_bridge_review.json",
            "out/map_animation_resource_binding_probe.json",
            "tools/build_map_animation_execution_boundary_review.py",
            "out/map_animation_live_buffer_writer_review.json",
            "out/map_animation_object_relative_tile_write_review.json",
            "out/map_animation_redraw_consumer_review.json",
            "out/map_animation_draw_tile_transform_review.json",
            "out/map_animation_tick_route_review.json",
            "out/map_animation_active_object_script_tile_write_review.json",
            "out/map_animation_frame_script_tile_write_review.json",
            "out/map_animation_non_object_tile_write_root_review.json",
            "out/map_animation_specialized_loop_boundary_review.json",
        ],
        "summary": {
            "animatedMapCount": tile_summary.get("animatedMapCount", 0),
            "animatedCellCount": tile_summary.get("animatedCellCount", 0),
            "surfaceCount": len(surfaces),
            "confirmedSurfaceCount": sum(1 for row in surfaces if str(row["promotion"]).startswith("confirmed")),
            "liveBufferCandidateVmTileWriteRefCount": live_writer_summary.get("candidateVmTileWriteRefCount", 0),
            "liveBufferAnimatedRootBindingCount": live_writer_summary.get("liveBufferRefsInsideAnimatedResourceRoots", 0),
            "objectRelativeTileWriteCandidateCount": object_relative_summary.get("candidateCount", 0),
            "objectRelativeAnimatedRootCandidateCount": object_relative_summary.get("animatedRootCandidateCount", 0),
            "redrawConsumerGrounded": redraw_summary.get("redrawPathGrounded", False),
            "redrawAlternateTileSelectionFound": redraw_summary.get("alternateTileSelectionFound", False),
            "drawTileTransformGrounded": draw_transform_summary.get("tileIdToSourceRectConverterGrounded", False),
            "drawTimeVisibleMotionTransformFound": draw_transform_summary.get("drawTimeVisibleMotionTransformFound", False),
            "tickRouteGrounded": bool(tick_route_summary.get("fieldTickRouteVaHex")),
            "tickDirectLiveBufferRefs": (
                int(tick_route_summary.get("directLiveBufferRefsInTickRoute", 0) or 0)
                + int(tick_route_summary.get("directLiveBufferRefsInPerFrameUpdates", 0) or 0)
            ),
            "activeObjectScriptCandidateGrounded": tick_route_summary.get(
                "activeObjectDelayedScriptRouteGrounded",
                False,
            ),
            "strictActiveObjectScriptTileWriteCount": active_script_tile_write_summary.get(
                "commandBoundaryTileWriteCount",
                0,
            ),
            "strictActiveObjectScriptRawTileWriteCandidateCount": active_script_tile_write_summary.get(
                "rawTileWriteCandidateCount",
                0,
            ),
            "frameScriptOpcode20TargetCount": frame_script_tile_write_summary.get(
                "opcode20AttachTargetCount",
                0,
            ),
            "frameScriptOpcode18StateEntryTargetCount": frame_script_tile_write_summary.get(
                "opcode18StateEntryTargetCount",
                0,
            ),
            "frameScriptTileWriteProducerFound": frame_script_tile_write_summary.get(
                "frameScriptTileWriteProducerFound",
                False,
            ),
            "nonObjectAlignedTileWriteCommandCount": non_object_tile_write_summary.get(
                "alignedTileWriteCommandCount",
                0,
            ),
            "nonObjectTileWriteRootCount": non_object_tile_write_summary.get("uniqueRootCount", 0),
            "nonObjectPromotedAnimatedProducerCommandCount": non_object_tile_write_summary.get(
                "promotedAnimatedProducerCommandCount",
                0,
            ),
            "specializedLoopStrongCandidateCount": specialized_loop_summary.get(
                "strongSpecializedCandidateCount",
                0,
            ),
            "visibleMotionExecutionBindingProven": False,
            "decision": (
                "layer1 0x40 redraw, palette VM, tile-write handlers, viewport redraw consumer, draw-time tile rect 변환은 각각 근거가 있지만 "
                "특정 맵의 모닥불/폭포 visible motion을 실행하는 command stream/root는 아직 증명되지 않았다. "
                "field tick의 active-object delayed script 경로는 가능하지만, strict +0xec inventory, runtime-installed +0x64 frame script, "
                "remaining tile-write VM root scan, specialized tick/draw loop scan 안에서는 tile-write producer가 발견되지 않았다."
            ),
        },
        "animatedMaps": animated_maps,
        "surfaces": surfaces,
        "nextFrontier": [
            "opcode 0x58/0x6b/0x76 live-buffer writer를 호출하게 하는 animated-map tick/root chain을 찾는다.",
            "map_review의 shift 후보를 EXE command stream의 산술/인덱스 생성 패턴과 대조한다.",
            "0x00425163/0x004199a0 일반 tile draw path 밖에 별도 map-animation specialized draw callback이 있는지 확인한다.",
            "strict +0xec 밖의 runtime-installed object +0x64/+0x40 script 또는 non-object VM root에서 animated-map tile-write root를 찾는다.",
            "runtime-installed +0x64 script scan도 음성이므로 non-object VM root 또는 specialized map animation loop를 우선 확인한다.",
            "remaining tile-write VM root scan도 음성이므로 0x58/0x6b/0x76 command stream이 아닌 specialized loop/산술 생성 경로를 우선 확인한다.",
            "specialized loop scan도 음성이므로 animated-map load/root dispatcher에서 0x40 dirty cell을 소비하는 정적 command chain을 다시 찾는다.",
        ],
    }


def render_html(report: dict[str, Any]) -> str:
    summary = report["summary"]
    cards = [
        ("status", report["status"]),
        ("animated maps", summary["animatedMapCount"]),
        ("animated cells", summary["animatedCellCount"]),
        ("confirmed surfaces", f"{summary['confirmedSurfaceCount']}/{summary['surfaceCount']}"),
        ("VM write refs", summary.get("liveBufferCandidateVmTileWriteRefCount", 0)),
        ("root-bound refs", summary.get("liveBufferAnimatedRootBindingCount", 0)),
        ("0x6b/0x76 candidates", summary.get("objectRelativeTileWriteCandidateCount", 0)),
        ("tick direct refs", summary.get("tickDirectLiveBufferRefs", 0)),
        ("active script route", "grounded" if summary.get("activeObjectScriptCandidateGrounded") else "unbound"),
        ("strict +0xec writes", summary.get("strictActiveObjectScriptTileWriteCount", 0)),
        ("+0x64 frame writes", "found" if summary.get("frameScriptTileWriteProducerFound") else "0"),
        ("remaining roots", summary.get("nonObjectTileWriteRootCount", 0)),
        ("specialized loop", summary.get("specializedLoopStrongCandidateCount", 0)),
        ("visible binding", "proven" if summary["visibleMotionExecutionBindingProven"] else "unproven"),
    ]
    card_html = "".join(f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>" for k, v in cards)
    surface_rows = []
    for row in report["surfaces"]:
        surface_rows.append(
            "<tr>"
            f"<td><b>{h(row['surface'])}</b><br><code>{h(row['artifact'])}</code></td>"
            f"<td>{h(row['promotion'])}</td>"
            f"<td>{h(row['finding'])}</td>"
            f"<td>{h(row['gap'])}</td>"
            "</tr>"
        )
    map_rows = []
    for row in report["animatedMaps"]:
        ranges = ", ".join(f"{item.get('startHex')}..{item.get('endHex')}" for item in row.get("paletteRanges", []))
        map_rows.append(
            "<tr>"
            f"<td><code>{h(row['map'])}</code></td>"
            f"<td><code>{h(row['sourceTileset'])}</code></td>"
            f"<td>{h(row['animatedCellCount'])}</td>"
            f"<td>{h(row['componentCount'])}</td>"
            f"<td>{h(ranges)}</td>"
            "</tr>"
        )
    frontier = "".join(f"<li>{h(item)}</li>" for item in report["nextFrontier"])
    payload = json.dumps(report, ensure_ascii=False)
    return f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Map Animation Execution Boundary Review</title>
  <style>
    body {{ margin:0; background:#101318; color:#edf1f7; font-family:system-ui,sans-serif; }}
    main {{ max-width:1240px; margin:0 auto; padding:24px; }}
    a {{ color:#8ecbff; }}
    .nav {{ display:flex; flex-wrap:wrap; gap:8px; margin-bottom:16px; }}
    .chip {{ border:1px solid #334155; border-radius:999px; padding:6px 10px; text-decoration:none; background:#161b22; }}
    .summary {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px; margin:16px 0; }}
    .card {{ border:1px solid #2b3544; border-radius:8px; padding:12px; background:#161b22; }}
    .card b {{ display:block; color:#9fb1c9; font-size:12px; text-transform:uppercase; }}
    .card span {{ display:block; margin-top:8px; font-size:18px; overflow-wrap:anywhere; }}
    section {{ border:1px solid #273244; border-radius:10px; padding:16px; margin:16px 0; background:#141922; overflow:auto; }}
    table {{ border-collapse:collapse; width:100%; min-width:980px; font-size:13px; }}
    th,td {{ border-bottom:1px solid #283342; padding:8px; text-align:left; vertical-align:top; }}
    th {{ color:#b9c7dc; background:#111722; }}
    code {{ color:#dbeafe; overflow-wrap:anywhere; }}
    pre {{ white-space:pre-wrap; background:#0b0f14; border:1px solid #253044; border-radius:8px; padding:12px; max-height:360px; overflow:auto; }}
  </style>
</head>
<body>
<main>
  <div class="nav">
    <a class="chip" href="index.html">index</a>
    <a class="chip" href="map_review.html">map review</a>
    <a class="chip" href="../out/map_animation_tile_review.html">tile review</a>
    <a class="chip" href="../out/map_animation_execution_boundary_review.json">execution JSON</a>
    <a class="chip" href="../out/map_animation_shift_hypothesis_review.html">shift hypothesis</a>
  </div>
  <h1>Map Animation Execution Boundary Review</h1>
  <p>{h(summary["decision"])}</p>
  <div class="summary">{card_html}</div>
  <section>
    <h2>Animated Maps</h2>
    <table>
      <thead><tr><th>map</th><th>source tileset</th><th>cells</th><th>components</th><th>palette ranges</th></tr></thead>
      <tbody>{''.join(map_rows)}</tbody>
    </table>
  </section>
  <section>
    <h2>Execution Surfaces</h2>
    <table>
      <thead><tr><th>surface</th><th>promotion</th><th>finding</th><th>gap</th></tr></thead>
      <tbody>{''.join(surface_rows)}</tbody>
    </table>
  </section>
  <section>
    <h2>Next Frontier</h2>
    <ul>{frontier}</ul>
  </section>
  <section>
    <h2>Raw JSON</h2>
    <pre id="json"></pre>
  </section>
</main>
<script>
window.HWANSE_MAP_ANIMATION_EXECUTION_BOUNDARY_REVIEW = {payload};
document.getElementById('json').textContent = JSON.stringify(window.HWANSE_MAP_ANIMATION_EXECUTION_BOUNDARY_REVIEW, null, 2);
</script>
</body>
</html>
"""


def main() -> int:
    report = build_report()
    write_json(OUT / "map_animation_execution_boundary_review.json", report)
    html_text = render_html(report)
    (WEB / "map_animation_execution_boundary_review.html").write_text(html_text, encoding="utf-8")
    print("map_animation_execution_boundary_review ok")
    print(json.dumps(report["summary"], ensure_ascii=False, indent=2))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
