Files
Cesarus85--ringfall-mjolnir-vr/tools/semantic_progress.py
T

170 lines
6.7 KiB
Python

"""Conservative progress corrections for proven objdiff false negatives.
Entries in config/semantic_matches.json are never trusted on their own. Each
entry is re-verified against the current target and rebuilt COFF objects before
its function is credited. This keeps the ordinary objdiff report authoritative
except where a stricter semantic relocation comparison proves exact equality.
"""
import json
from pathlib import Path
from typing import Any, Dict, List
from .coff_compare import CoffError, load, section_info
class SemanticProgressError(RuntimeError):
pass
def _percent(numerator: int, denominator: int) -> float:
return 100.0 * numerator / denominator if denominator else 0.0
def _credit(measures: Dict[str, Any], code_bytes: int) -> None:
measures["matched_code"] = int(measures.get("matched_code", 0)) + code_bytes
measures["matched_functions"] = int(measures.get("matched_functions", 0)) + 1
measures["matched_code_percent"] = _percent(
measures["matched_code"], int(measures.get("total_code", 0))
)
measures["matched_functions_percent"] = _percent(
measures["matched_functions"], int(measures.get("total_functions", 0))
)
def _debit(measures: Dict[str, Any], code_bytes: int) -> None:
measures["matched_code"] = int(measures.get("matched_code", 0)) - code_bytes
measures["matched_functions"] = int(measures.get("matched_functions", 0)) - 1
if measures["matched_code"] < 0 or measures["matched_functions"] < 0:
raise SemanticProgressError("semantic rejection would make progress negative")
measures["matched_code_percent"] = _percent(
measures["matched_code"], int(measures.get("total_code", 0))
)
measures["matched_functions_percent"] = _percent(
measures["matched_functions"], int(measures.get("total_functions", 0))
)
def apply_semantic_rejections(
report: Dict[str, Any],
semantic_report_path: Path,
) -> List[str]:
"""Remove objdiff credits rejected by the stricter COFF-shape audit."""
if not semantic_report_path.is_file():
return []
semantic_report = json.loads(semantic_report_path.read_text(encoding="utf-8"))
report_units = {unit["name"]: unit for unit in report.get("units", [])}
categories = {item["id"]: item for item in report.get("categories", [])}
rejected = []
for entry in semantic_report.get("ordinary_rejected", []):
unit_name = entry["unit"]
function_name = entry["function"]
report_unit = report_units.get(unit_name)
if report_unit is None:
raise SemanticProgressError(f"semantic rejection unit not found: {unit_name}")
functions = [
function for function in report_unit.get("functions", [])
if function.get("name") == function_name
]
if len(functions) != 1 or functions[0].get("fuzzy_match_percent") != 100.0:
raise SemanticProgressError(
f"semantic rejection is not an objdiff exact function: "
f"{unit_name}:{function_name}"
)
code_bytes = int(functions[0]["size"])
_debit(report["measures"], code_bytes)
_debit(report_unit["measures"], code_bytes)
progress_categories = report_unit.get("metadata", {}).get(
"progress_categories", []
)
if isinstance(progress_categories, str):
progress_categories = [progress_categories]
for category_id in progress_categories:
if category_id not in categories:
raise SemanticProgressError(f"progress category not found: {category_id}")
_debit(categories[category_id]["measures"], code_bytes)
rejected.append(
f"{unit_name}:{function_name} (-{code_bytes} code bytes, -1 function)"
)
return rejected
def apply_semantic_matches(
report: Dict[str, Any],
project_root: Path,
manifest_path: Path,
objdiff_config_path: Path,
) -> List[str]:
"""Verify and credit manifest entries that objdiff did not count exactly.
Returns human-readable notes for credited entries. Missing, ambiguous, or
non-equal evidence raises instead of silently inflating progress.
"""
if not manifest_path.is_file():
return []
entries = json.loads(manifest_path.read_text(encoding="utf-8"))
objdiff = json.loads(objdiff_config_path.read_text(encoding="utf-8"))
report_units = {unit["name"]: unit for unit in report.get("units", [])}
config_units = {unit["name"]: unit for unit in objdiff.get("units", [])}
categories = {item["id"]: item for item in report.get("categories", [])}
credited = []
for entry in entries:
unit_name = entry["unit"]
function_name = entry["function"]
if unit_name not in report_units or unit_name not in config_units:
raise SemanticProgressError(f"semantic match unit not found: {unit_name}")
report_unit = report_units[unit_name]
functions = [
function for function in report_unit.get("functions", [])
if function.get("name") == function_name
]
if len(functions) != 1:
raise SemanticProgressError(
f"expected one report function {unit_name}:{function_name}, found {len(functions)}"
)
function = functions[0]
if function.get("fuzzy_match_percent") == 100.0:
continue
config_unit = config_units[unit_name]
try:
target = load(project_root / config_unit["target_path"])
base = load(project_root / config_unit["base_path"])
target_info = section_info(target, function_name)
base_info = section_info(base, function_name)
except (CoffError, KeyError, OSError) as error:
raise SemanticProgressError(
f"cannot verify semantic match {unit_name}:{function_name}: {error}"
) from error
if target_info != base_info:
raise SemanticProgressError(
f"semantic match is no longer exact: {unit_name}:{function_name}"
)
code_bytes = int(function["size"])
_credit(report["measures"], code_bytes)
_credit(report_unit["measures"], code_bytes)
progress_categories = report_unit.get("metadata", {}).get("progress_categories", [])
if isinstance(progress_categories, str):
progress_categories = [progress_categories]
for category_id in progress_categories:
if category_id not in categories:
raise SemanticProgressError(f"progress category not found: {category_id}")
_credit(categories[category_id]["measures"], code_bytes)
credited.append(
f"{unit_name}:{function_name} (+{code_bytes} code bytes, +1 function)"
)
return credited