134 lines
7.8 KiB
Python
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())
|