"""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