Files
mtgodot-poc/.agents/skills/metin2-40250-parity-audit/scripts/audit_source_coverage.py
T
shenleiandClaude Opus 5 9d0e50de8f docs(parity): mirror 40250 layout for the logic layer; evaluate embedded Python UI
- SKILL.md: logic units port into extension/src/port/<Lib>/<File>, platform
  adapters under extension/src/platform; legacy GDScript owners are migration
  sources; python layer frozen until the embedding decision
- port_map.py: logic/python/platform layers with per-layer progress, queue
  --layer, LEGACY layout check; inventory adds EterBase, EterPythonLib,
  ScriptLib, SpeedTreeLib, SphereLib, EterImageLib
- py_embed_spike.py + docs/PYTHON-EMBED-EVAL.md: the 40250 system.py bootstrap
  loads 67/74 root modules unchanged under CPython 2.7 with stub natives

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:06:32 +09:00

351 lines
18 KiB
Python

#!/usr/bin/env python3
"""Audit active 40250 project sources against parity contract ownership.
This is a coverage gate, not a parity verifier. A file inferred to a contract
still requires that contract's branch-by-branch review; the report deliberately
labels inferred ownership separately from exact manifest references.
"""
from __future__ import annotations
import argparse
import json
import pathlib
import re
import xml.etree.ElementTree as ET
from collections import Counter, defaultdict
import refroot # script directory is on sys.path when run directly
REFERENCE_DIRS = (
"UserInterface",
"GameLib",
"EffectLib",
"EterLib",
"EterGrnLib",
"PRTerrainLib",
"MilesLib",
"EterPack",
"EterLocale",
)
SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".inl"}
IGNORED_BASENAMES = {"stdafx.cpp", "stdafx.h", "stdafx.hxx", "stdafx.hpp"}
REVIEWED_STATIC_GROUPS = {
"EffectLib", "EterGrnLib", "MilesLib",
"GameLib", "PRTerrainLib", "EterPack", "EterLocale", "EterLib",
}
REVIEWED_STATIC_FILES = {
"UserInterface/PythonNetworkStreamCommand.cpp",
"EterLib/Camera.cpp", "EterLib/Camera.h",
"EterLib/IME.cpp", "EterLib/IME.h", "EterLib/Input.h", "EterLib/msctf.h",
"EterLib/Resource.cpp", "EterLib/Resource.h", "EterLib/ResourceManager.cpp", "EterLib/ResourceManager.h",
"EterLib/FileLoaderThread.cpp", "EterLib/FileLoaderThread.h",
"EterLib/TextFileLoader.cpp", "EterLib/TextFileLoader.h", "EterLib/parser.cpp", "EterLib/parser.h",
"EterLib/PathStack.cpp", "EterLib/PathStack.h",
"EterLib/SkyBox.cpp", "EterLib/SkyBox.h", "EterLib/ScreenFilter.cpp", "EterLib/ScreenFilter.h",
"EterLib/LensFlare.cpp", "EterLib/LensFlare.h", "EterLib/ColorTransitionHelper.cpp", "EterLib/ColorTransitionHelper.h",
"EterLib/AttributeData.cpp", "EterLib/AttributeData.h", "EterLib/AttributeInstance.h",
"EterLib/CollisionData.cpp", "EterLib/CollisionData.h", "EterLib/Decal.cpp", "EterLib/Decal.h",
"EterLib/CullingManager.cpp", "EterLib/CullingManager.h",
"EterLib/GrpText.cpp", "EterLib/GrpText.h", "EterLib/GrpTextInstance.cpp", "EterLib/GrpTextInstance.h",
"EterLib/TextBar.cpp", "EterLib/TextBar.h", "EterLib/TextTag.cpp", "EterLib/TextTag.h",
"EterLib/GrpFontTexture.cpp", "EterLib/GrpFontTexture.h", "EterLib/GrpMarkInstance.cpp", "EterLib/GrpMarkInstance.h",
"EterLib/GrpDevice.cpp", "EterLib/GrpDevice.h", "EterLib/GrpScreen.cpp", "EterLib/GrpScreen.h",
"EterLib/GrpShadowTexture.cpp", "EterLib/GrpShadowTexture.h",
"UserInterface/AbstractApplication.h", "UserInterface/AbstractSingleton.h",
"UserInterface/CheckLatestFiles.h",
"UserInterface/ProcessCRC.cpp", "UserInterface/ProcessCRC.h",
"UserInterface/ProcessScanner.cpp", "UserInterface/ProcessScanner.h",
"UserInterface/HackShield.cpp", "UserInterface/Hackshield.h",
"UserInterface/HackshieldLicense.h", "UserInterface/HackshieldLicense_metin2.h",
"UserInterface/HackshieldLicense_metin2client.h",
"UserInterface/NProtectGameGuard.cpp", "UserInterface/NProtectGameGuard.h",
"UserInterface/PythonApplicationLogo.cpp", "UserInterface/PythonApplicationModule.cpp",
"UserInterface/PythonApplicationWebPage.cpp",
"UserInterface/PythonExceptionSender.cpp", "UserInterface/PythonExceptionSender.h",
"UserInterface/PythonProfilerModule.cpp",
"UserInterface/PythonSystem.cpp", "UserInterface/PythonSystem.h", "UserInterface/PythonSystemModule.cpp",
"UserInterface/MovieMan.cpp", "UserInterface/MovieMan.h",
"UserInterface/UserInterface.cpp", "UserInterface/Test.h", "UserInterface/resource.h",
"UserInterface/WiseLogicXTrap.cpp", "UserInterface/WiseLogicXTrap.h",
"UserInterface/AbstractCharacterManager.h", "UserInterface/AbstractChat.h", "UserInterface/AbstractPlayer.h",
"UserInterface/AccountConnector.h", "UserInterface/AffectFlagContainer.h",
"UserInterface/CameraProcedure.cpp", "UserInterface/Discord.h", "UserInterface/GameType.cpp",
"UserInterface/InstanceBaseEvent.cpp", "UserInterface/InstanceBaseTransform.cpp",
"UserInterface/NetworkActorManager.h",
"UserInterface/PythonBackground.h", "UserInterface/PythonBackgroundModule.cpp",
"UserInterface/PythonCharacterManager.h", "UserInterface/PythonCharacterManagerModule.cpp",
"UserInterface/PythonCharacterModule.cpp", "UserInterface/PythonNonPlayer.h",
"UserInterface/PythonNonPlayerModule.cpp", "UserInterface/PythonPlayerEventHandler.h",
"UserInterface/GuildMarkDownloader.h", "UserInterface/GuildMarkUploader.h",
"UserInterface/MarkImage.cpp", "UserInterface/MarkImage.h", "UserInterface/MarkManager.h",
"UserInterface/PythonGuild.h", "UserInterface/PythonMessenger.h",
"UserInterface/InsultChecker.cpp", "UserInterface/InsultChecker.h",
"UserInterface/PythonChatModule.cpp", "UserInterface/PythonTextTailModule.cpp",
"UserInterface/PythonEventManagerMoudle.cpp", "UserInterface/PythonGameEventManagerModule.cpp",
"UserInterface/PythonQuest.h",
"UserInterface/PythonExchange.h", "UserInterface/PythonExchangeModule.cpp",
"UserInterface/PythonSafeBox.h", "UserInterface/PythonShop.h",
"UserInterface/PythonIME.h", "UserInterface/PythonIMEModule.cpp",
"UserInterface/PythonMiniMapModule.cpp",
"UserInterface/PythonNetworkDatagram.cpp", "UserInterface/PythonNetworkDatagram.h",
"UserInterface/PythonNetworkDatagramModule.cpp", "UserInterface/PythonNetworkStream.h",
"UserInterface/PythonNetworkStreamPhaseOffline.cpp",
"UserInterface/ServerStateChecker.h", "UserInterface/ServerStateCheckerModule.cpp",
"UserInterface/PythonSkill.h",
"UserInterface/Locale.cpp", "UserInterface/Locale.h",
"UserInterface/Locale_inc_BR.h", "UserInterface/Locale_inc_CA.h",
"UserInterface/Locale_inc_EU.h", "UserInterface/Locale_inc_EU2.h",
"UserInterface/Locale_inc_GB.h", "UserInterface/Locale_inc_HK.h",
"UserInterface/Locale_inc_JP.h", "UserInterface/Locale_inc_KO.h",
"UserInterface/Locale_inc_KO_DEV.h", "UserInterface/Locale_inc_NEWCIBN.h",
"UserInterface/Locale_inc_SG.h", "UserInterface/Locale_inc_VI.h",
"UserInterface/Locale_inc_dev.h",
}
def normalize(path: pathlib.Path, root: pathlib.Path) -> str:
return path.resolve().relative_to(root.resolve()).as_posix()
def manifest_data(repo: pathlib.Path) -> tuple[set[str], dict[str, list[str]]]:
data = json.loads((repo / "audit/manifest.json").read_text(encoding="utf-8"))
listed: set[str] = set()
owners: dict[str, list[str]] = defaultdict(list)
for contract in data["contracts"]:
cid = contract["id"]
for raw in contract.get("reference", {}).get("files", []):
path = raw.replace("\\", "/")
listed.add(path)
owners[path].append(cid)
return listed, owners
def active_project_sources(reference_root: pathlib.Path, dirs: tuple[str, ...] = REFERENCE_DIRS) -> set[str]:
active: set[str] = set()
for project in sorted((reference_root.parent / "vs_files").glob("*/*.vcxproj")):
try:
tree = ET.parse(project)
except ET.ParseError:
continue
for node in tree.iter():
if not (node.tag.endswith("ClCompile") or node.tag.endswith("ClInclude")):
continue
include = node.attrib.get("Include", "")
if not include:
continue
candidate = (project.parent / pathlib.Path(include.replace("\\", "/"))).resolve()
try:
relative = candidate.relative_to(reference_root.resolve())
except ValueError:
continue
if relative.parts and relative.parts[0] in dirs:
if relative.suffix.lower() in SOURCE_SUFFIXES:
active.add(relative.as_posix())
return active
def infer_owners(path: str) -> list[str]:
"""Infer behavior ownership for a source not explicitly listed in manifest.
These rules are intentionally broad. They identify review queues, never
upgrade a contract status or claim semantic equivalence.
"""
lower = path.lower()
area, _, name = lower.partition("/")
stem = pathlib.PurePosixPath(name).stem
owners: list[str] = []
def add(*ids: str) -> None:
for cid in ids:
if cid not in owners:
owners.append(cid)
if area == "effectlib":
add("render.effect_particle_motion")
elif area == "etergrnlib":
add("render.model_attachment")
elif area == "mileslib":
add("audio.bgm_ambience_3d")
elif area == "eterpack" or area == "eterlocale":
add("resource.pack_locale_proto")
elif area == "prterrainlib":
add("world.terrain_water_collision")
elif area == "eterlib":
if any(x in stem for x in ("net", "packet")):
add("network.packet_dispatch_protocol")
if any(x in stem for x in ("camera", "input", "ime")):
add("ui.input_camera_mobile")
if any(x in stem for x in ("attribute", "collision", "ray", "intersect", "decal")):
add("world.terrain_water_collision", "movement.click_collision", "combat.flying_projectile")
if any(x in stem for x in ("sky", "lensflare", "screenfilter", "colortransition")):
add("world.environment_weather")
if any(x in stem for x in ("text", "font", "tag")):
add("ui.hud_target_minimap_status", "social.chat_text_tail")
if "mark" in stem:
add("social.party_guild_messenger")
if any(x in stem for x in ("resource", "path", "fileloader", "parser", "image", "targa", "jpeg")):
add("resource.pack_locale_proto")
if any(x in stem for x in ("device", "state", "grp", "culling", "shadow", "blocktexture")):
add("render.model_attachment", "lifecycle.platform_release")
if any(x in stem for x in ("msapplication", "mswindow", "mutex", "thread", "referenceobject", "profiler")):
add("lifecycle.platform_release")
if any(x in stem for x in ("dimm", "dynamic", "event", "funcobject", "pool", "ref", "util")):
add("lifecycle.platform_release")
if "dibbar" in stem:
add("resource.pack_locale_proto", "ui.hud_target_minimap_status")
if "msctf" in stem:
add("ui.input_camera_mobile")
elif area == "gamelib":
if any(x in stem for x in ("fly", "flying")):
add("combat.flying_projectile")
if any(x in stem for x in ("weapontrace", "battle")):
add("combat.attack_combo_damage")
if "actorinstance" in stem or any(x in stem for x in ("motion", "blend", "rotation", "position")):
add("movement.motion_resource_events", "render.model_attachment", "skill.cast.effect_timing")
if any(x in stem for x in ("map", "terrain", "physics")):
add("world.terrain_water_collision", "world.map_load_transition")
if "dungeon" in stem:
add("world.dungeon_activity")
if "snow" in stem:
add("world.environment_weather")
if any(x in stem for x in ("property", "monsterarea")):
add("world.map_load_transition", "world.npc_drop_actor_spawn")
if any(x in stem for x in ("race", "item")):
add("resource.pack_locale_proto", "render.model_attachment")
if not owners:
add("world.map_load_transition")
elif area == "userinterface":
if any(x in stem for x in ("chat", "texttail", "insult")):
add("social.chat_text_tail")
if any(x in stem for x in ("character", "character_manager")):
add("network.character_select.create_delete", "social.party_guild_messenger")
if any(x in stem for x in ("eventmanager", "quest")):
add("quest.quest_event_navigation")
if any(x in stem for x in ("ime", "input", "cursor")):
add("ui.input_camera_mobile")
if "serverstate" in stem or "account" in stem:
add("network.error_timeout_server_state", "network.login.phase_flow")
if any(x in stem for x in ("locale", "language")):
add("resource.pack_locale_proto")
if any(x in stem for x in ("mark", "guild", "messenger")):
add("social.party_guild_messenger")
if any(x in stem for x in ("exchange", "safebox", "shop")):
add("social.exchange_safebox", "npc.dialog_shop")
if any(x in stem for x in ("application", "exception", "crc", "process", "movie", "system")):
add("lifecycle.bootstrap_phase_host", "lifecycle.platform_release")
if "background" in stem:
add("world.map_load_transition", "world.environment_weather")
if "minimap" in stem:
add("ui.hud_target_minimap_status")
if "nonplayer" in stem:
add("world.npc_drop_actor_spawn")
if any(x in stem for x in ("networkstream", "networkdatagram")):
add("network.packet_dispatch_protocol", "network.reconnect_warp")
if "player" in stem or "instancebase" in stem:
add("movement.keyboard.motion", "combat.attack_combo_damage", "world.entity_spawn_state_sync")
if "skill" in stem:
add("skill.player_ui_cooldown", "skill.cast.effect_timing")
if "item" in stem:
add("item.inventory_equipment_state", "item.consumable_action", "item.refine_socket_costume")
if "pythontexttail" in stem:
add("ui.hud_target_minimap_status")
if "profiler" in stem:
add("lifecycle.platform_release")
if not owners:
add("lifecycle.bootstrap_phase_host")
return owners
def build(repo: pathlib.Path) -> dict:
reference_root = refroot.reference_root(repo)
listed, exact_owners = manifest_data(repo)
active = active_project_sources(reference_root)
rows = []
for path in sorted(active):
if pathlib.PurePosixPath(path).name.lower() in IGNORED_BASENAMES:
kind = "build_support"
owners = []
elif path in exact_owners:
kind = "manifest_exact"
owners = exact_owners[path]
else:
kind = "inferred_review_queue"
owners = infer_owners(path)
if path in REVIEWED_STATIC_FILES or pathlib.PurePosixPath(path).parts[0] in REVIEWED_STATIC_GROUPS:
kind = "reviewed_static_group"
rows.append({"path": path, "kind": kind, "owners": owners})
counts = Counter(row["kind"] for row in rows)
unowned = [row["path"] for row in rows if not row["owners"] and row["kind"] != "build_support"]
by_contract: dict[str, dict[str, int]] = defaultdict(lambda: Counter())
for row in rows:
for owner in row["owners"]:
by_contract[owner][row["kind"]] += 1
return {
"reference_root": json.loads((repo / "audit/manifest.json").read_text(encoding="utf-8"))["reference_root"],
"active_source_count": len(rows),
"manifest_exact_count": counts["manifest_exact"],
"inferred_review_queue_count": counts["inferred_review_queue"],
"reviewed_static_group_count": counts["reviewed_static_group"],
"build_support_count": counts["build_support"],
"unowned_behavior_candidates": unowned,
"by_contract": {k: dict(v) for k, v in sorted(by_contract.items())},
"files": rows,
}
def markdown(data: dict) -> str:
lines = [
"# Active 40250 source coverage",
"",
"This report is a coverage gate, not proof of parity. `manifest_exact` means the source is explicitly listed by a contract; `inferred_review_queue` means it is assigned by a broad area rule and still requires semantic review.",
"",
f"- Active Visual Studio source/include files: **{data['active_source_count']}**",
f"- Explicitly listed by manifest: **{data['manifest_exact_count']}**",
f"- Static-reviewed groups (still PARTIAL for parity): **{data['reviewed_static_group_count']}**",
f"- Inferred review queue: **{data['inferred_review_queue_count']}**",
f"- Build support (`StdAfx`): **{data['build_support_count']}**",
f"- Unowned behavior candidates: **{len(data['unowned_behavior_candidates'])}**",
"",
"## Review queue by contract",
"",
"| Contract | Exact manifest files | Static-reviewed files | Inferred files |",
"|---|---:|---:|---:|",
]
for cid, counts in data["by_contract"].items():
lines.append(f"| `{cid}` | {counts.get('manifest_exact', 0)} | {counts.get('reviewed_static_group', 0)} | {counts.get('inferred_review_queue', 0)} |")
lines.extend(["", "## Unowned behavior candidates", ""])
if data["unowned_behavior_candidates"]:
lines.extend(f"- `{p}`" for p in data["unowned_behavior_candidates"])
else:
lines.append("None")
lines.extend([
"",
"## Required interpretation",
"",
"The inferred queue is not considered audited merely because a rule assigned it to a contract. Each queue must be reviewed through the contract's reference call chain and either added to exact evidence, explicitly classified as support/disabled/vendor code, or recorded as a remaining gap. Static-reviewed groups have had their active symbols and lifecycle call chain checked in the current round, but their contracts remain PARTIAL until implementation equivalence and tests close the documented gaps.",
"",
])
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--write", action="store_true")
args = parser.parse_args()
repo = pathlib.Path(__file__).resolve().parents[4]
data = build(repo)
if args.write:
(repo / "audit/source-coverage.json").write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
(repo / "audit/reports/source-coverage.md").write_text(markdown(data), encoding="utf-8")
else:
print(markdown(data))
return 0 if not data["unowned_behavior_candidates"] else 2
if __name__ == "__main__":
raise SystemExit(main())