Files
FreeCADWasmDev/tools/report_business_placeholders.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

134 lines
7.8 KiB
Python

#!/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())