#!/usr/bin/env python3 """Inventory placeholder candidates without executing business code or changing coverage.""" from __future__ import annotations import hashlib import json import re import time from collections import Counter from datetime import datetime, timezone from pathlib import Path ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "docs/current/business-placeholder-inventory.json" REPORT = OUTPUT.with_suffix(".md") CACHE = ROOT / "docs/current/business-placeholder-scan-cache.json" PROBES = ROOT / "freecad-source/src/WebAssembly" FRONTEND = ROOT / "web/src" RULES = [ ("backend-identity-claim", re.compile(r"_freecad_import_stub\s*=\s*False|_freecad_wasm_backend\s*=\s*True")), ("runtime-shim", re.compile(r"types\.ModuleType\(|declarationOnly|_freecad_import_stub\s*=\s*True")), ("fixture-or-inspection", re.compile(r"methodFixture|fixtureOnly|sourceConstructor|fixed.fixture|inspection.only", re.I)), ("explicit-boundary", re.compile(r"implementationBoundary|not implemented|remain unavailable|placeholder|\bTODO\b|\bFIXME\b", re.I)), ("failure-status-review", re.compile(r"not_implemented|implementation_required")), ] def load(path: Path) -> dict: try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError): return {} def write_json(path: Path, value: dict) -> None: temporary = path.with_suffix(".tmp") temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") temporary.replace(path) def main() -> int: started = time.monotonic() rules_digest = hashlib.sha256(Path(__file__).read_bytes()).hexdigest() cached = load(CACHE) previous = cached.get("files", {}) if cached.get("rulesDigest") == rules_digest else {} files = sorted({path for root, suffixes in ((PROBES, {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"}), (FRONTEND, {".js", ".mjs", ".ts", ".tsx"})) for path in root.rglob("*") if path.is_file() and path.suffix in suffixes and ".generated." not in path.name and ".test." not in path.name and ".spec." not in path.name}) matrix = load(ROOT / "docs/p6/workbench-command-matrix.json") modules = sorted({row["module"] for row in matrix.get("records", []) if row.get("module")}, key=len, reverse=True) scanned = {} findings = [] changed = 0 for path in files: relative = str(path.relative_to(ROOT)) data = path.read_bytes() digest = hashlib.sha256(data).hexdigest() retained = previous.get(relative, {}) if retained.get("sha256") == digest: rows = retained.get("findings", []) else: changed += 1 lines = data.decode("utf-8", errors="replace").splitlines() rows = [] stem = re.sub(r"[^a-z0-9]", "", path.stem.lower()) module = next((name for name in modules if stem.startswith(re.sub(r"[^a-z0-9]", "", name.lower()))), "Shared") for number, line in enumerate(lines, 1): kinds = [name for name, pattern in RULES if pattern.search(line)] if not kinds: continue context = "\n".join(lines[max(0, number - 3):number + 2]) rows.append({"id": hashlib.sha256(f"{relative}:{number}:{line}".encode()).hexdigest()[:20], "path": relative, "line": number, "moduleHint": module, "kinds": kinds, "sourceSha256": digest, "status": "source-review-required", "excerpt": context[:2400]}) scanned[relative] = {"sha256": digest, "findings": rows} findings.extend(rows) kinds = Counter(kind for row in findings for kind in row["kinds"]) owners = Counter(row["moduleHint"] for row in findings) summary = {"filesScanned": len(files), "filesRescanned": changed, "filesReused": len(files) - changed, "candidateLocations": len(findings), "byKind": dict(kinds), "byModuleHint": dict(owners), "seconds": round(time.monotonic() - started, 3)} reviewed = {} for group in load(ROOT / "docs/current/native-semantic-decisions.json").get("groups", []): hashes = group.get("sourceDigests", {}) current = bool(hashes) for name, expected in hashes.items(): path = (ROOT / name).resolve() if not path.is_relative_to(ROOT) or not path.is_file(): current = False break actual = scanned.get(name, {}).get("sha256") or hashlib.sha256(path.read_bytes()).hexdigest() current = current and actual == expected for record_id, decision in group.get("records", {}).items(): reviewed[record_id] = {**decision, "sourceSnapshotCurrent": current} payload = {"schema": "freecad-web-business-placeholder-inventory/v1", "generatedAt": datetime.now(timezone.utc).isoformat(), "scope": [str(PROBES.relative_to(ROOT)), str(FRONTEND.relative_to(ROOT))], "limitations": ["Lexical candidates, not confirmed business placeholders or completed semantic review.", "Normal failure handling, comments and diagnostic fixtures require call-path attribution.", "Unmarked no-op behavior may not match these rules; desktop source semantics remain authoritative.", "Module names are filename hints, not verified ownership. No tests or acceptance executed."], "summary": summary, "files": {name: row["sha256"] for name, row in scanned.items()}, "reviewedCommandFindings": reviewed, "findings": findings} OUTPUT.parent.mkdir(parents=True, exist_ok=True) write_json(OUTPUT, payload) write_json(CACHE, {"rulesDigest": rules_digest, "files": scanned}) report = ["# Business Placeholder Candidates", "", "Static source inventory only; not a count of confirmed missing commands.", "No automatic acceptance, runtime execution or coverage promotion.", "", f"Scanned {len(files)} source files; {len(findings)} candidate locations.", f"Reused {len(files) - changed} unchanged file scans; scan took {summary['seconds']} seconds.", "", "Full locations, excerpts and fingerprints: business-placeholder-inventory.json.", "Normal errors, diagnostics and declaration shims must be distinguished from active business placeholders.", "", "| Candidate Kind | Locations |", "| --- | ---: |"] report.extend(f"| {name} | {count} |" for name, count in kinds.most_common()) report.extend(["", "## Review Order", "", "1. Claimed real backends and fixed outputs on business paths.", "2. Explicit unimplemented branches with supplied native implementations.", "3. Runtime/package dependencies, using the retained local source index.", "4. Diagnostic fixtures and normal errors: preserve them unless they leak into business execution.", "", "No blanket status changes, blanket registrations or removal of failure guards.", "", "| File | Candidate Locations |", "| --- | ---: |"]) counts = Counter(row["path"] for row in findings) report.extend(f"| {name} | {count} |" for name, count in counts.most_common()) report.extend(["", "## Retained Source Decisions", "", "Routed commands with remaining semantic/dependency gaps are retained here too.", "", "| Command | Source Decision | Fingerprint Current |", "| --- | --- | --- |"]) report.extend(f"| {key} | {value['classification']} | {value['sourceSnapshotCurrent']} |" for key, value in sorted(reviewed.items())) REPORT.write_text("\n".join(report) + "\n", encoding="utf-8") print(json.dumps(summary, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())