#!/usr/bin/env python3 """Audit catalog, mapping, exported shims, and the downstream compile fixture.""" from __future__ import annotations import argparse import csv import json import re from collections import Counter from pathlib import Path import generate_api_shims as shims import generate_rust_mapping as mapping ROOT = Path(__file__).resolve().parents[1] REPORT = ROOT / "api/API-COVERAGE.md" FIXTURE = ROOT / "tests/api-compile/src/lib.rs" FORBIDDEN = { "ShimValue": r"\bShimValue\b", "dynamic argument bag": r"\b(?:DynamicArgumentBag|ArgumentBag)\b", "invented variadic": r"\b(?:Variadic|VarArgs)\b|\.\.\.", "erased Any argument vector": r"Vec\s*<\s*Box\s*<\s*dyn\s+(?:std::any::)?Any", } def require_exact(label: str, expected: set[str], actual: Counter[str]) -> None: actual_ids = set(actual) missing = expected - actual_ids stale = actual_ids - expected duplicates = sorted(item for item, count in actual.items() if count != 1) if missing or stale or duplicates: raise ValueError( f"{label}: missing={len(missing)}, stale={len(stale)}, duplicates={len(duplicates)}" ) def matches(paths: list[Path], pattern: str) -> Counter[str]: found: Counter[str] = Counter() regex = re.compile(pattern, re.MULTILINE) for path in paths: found.update(regex.findall(path.read_text())) return found def audit() -> str: catalog = json.loads(mapping.CATALOG.read_text()) assemblies = {assembly["identity"]["name"]: assembly for assembly in catalog["assemblies"]} catalog_types = { item["doc_id"] for assembly in catalog["assemblies"] for item in assembly["types"] } catalog_members = { member["doc_id"] for assembly in catalog["assemblies"] for item in assembly["types"] for member in item["members"] } external_types = {item["doc_id"] for item in catalog["external_types"]} support_types = {f"T:{name}" for name in mapping.SUPPORT_TYPES} type_rows = list(csv.DictReader(mapping.TYPE_LEDGER.open(), delimiter="\t")) member_rows = list(csv.DictReader(mapping.MEMBER_LEDGER.open(), delimiter="\t")) require_exact( "type mapping", catalog_types | external_types | support_types, Counter(row["csharp_type_id"] for row in type_rows), ) require_exact( "member mapping", catalog_members, Counter(row["csharp_id"] for row in member_rows), ) duplicate_destinations = [ item for item, count in Counter(row["rust_item_path"] for row in member_rows).items() if count != 1 ] if duplicate_destinations: raise ValueError(f"duplicate Rust member destinations: {len(duplicate_destinations)}") generated = [shims.MAIN_TARGET, *shims.TARGETS.values()] require_exact( "exported types", catalog_types, matches(generated, r"^\s*/// C# type: `(T:.+)`\.$"), ) require_exact( "exported members", catalog_members, matches(generated, r"^\s*/// C# member: `(.+)`\.$"), ) require_exact( "fixture types", catalog_types, matches([FIXTURE], r"^\s*// C# type: `(T:.+)`\.$"), ) require_exact( "fixture members", catalog_members, matches([FIXTURE], r"^\s*// C# member: `(.+)`\.$"), ) fixture_text = FIXTURE.read_text() if len(re.findall(r"^fn type_", fixture_text, re.MULTILINE)) != len(catalog_types): raise ValueError("fixture type probe count does not match the catalog") if len(re.findall(r"^fn member_", fixture_text, re.MULTILINE)) != len(catalog_members): raise ValueError("fixture member probe count does not match the catalog") scan_text = "\n".join( path.read_text() for path in [*generated, FIXTURE, mapping.MEMBER_LEDGER] ) forbidden_hits = [name for name, pattern in FORBIDDEN.items() if re.search(pattern, scan_text)] if forbidden_hits: raise ValueError("forbidden erased shim patterns: " + ", ".join(forbidden_hits)) mapping.validate_generated_shims() lines = [ "# Complete API gate coverage", "", "Generated by `python3 tools/check_api_coverage.py --write`; do not edit by hand.", "", "| Assembly | Catalog types | Mapped/exported/fixture types | Catalog members | Mapped/exported/fixture members |", "|---|---:|---:|---:|---:|", ] for name, assembly in assemblies.items(): type_count = len(assembly["types"]) member_count = sum(len(item["members"]) for item in assembly["types"]) lines.append( f"| `{name}` | {type_count:,} | {type_count:,} | {member_count:,} | {member_count:,} |" ) lines += [ f"| **Total** | **{len(catalog_types):,}** | **{len(catalog_types):,}** | **{len(catalog_members):,}** | **{len(catalog_members):,}** |", "", "## Exclusions and boundary records", "", "- Public catalog type exclusions: **0**.", "- Public catalog member exclusions: **0**.", f"- External signature types: **{len(external_types):,}** mapped to core/std, adopted cross-platform crates, or project-owned boundary types; their external member APIs are intentionally not copied.", f"- Referenced non-public support traits: **{len(support_types):,}** mapped because public inheritance signatures require them.", f"- Intentional delegate APM differences: **{sum(row['status'] == 'intentional_difference' for row in member_rows):,}**; every replacement is exported and compiled by the fixture.", "- Forbidden erased shim patterns: **0** (`ShimValue`, dynamic argument bags, invented variadics, or `Vec>` argument bags).", "", "The fixture is a standalone Rust workspace with path dependencies only. Building it requires neither .NET nor compiled LibreMetaverse assemblies.", "", ] return "\n".join(lines) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--write", action="store_true") args = parser.parse_args() report = audit() if args.write: REPORT.write_text(report) print(f"generated {REPORT.relative_to(ROOT)}") elif not REPORT.exists() or REPORT.read_text() != report: raise SystemExit(f"stale coverage report: {REPORT.relative_to(ROOT)}") else: print("API coverage matches the catalog, mappings, exports, and fixture") if __name__ == "__main__": main()