"""Business-scoped acceptance, separate from reusable implementation receipts.""" from __future__ import annotations import hashlib import json from functools import lru_cache from pathlib import Path from urllib.parse import urlsplit ROOT = Path(__file__).resolve().parents[1] LEDGER = ROOT / "docs/current/business-acceptance.json" def read(path: Path) -> dict: try: value = json.loads(path.read_text(encoding="utf-8")) return value if isinstance(value, dict) else {} except (OSError, ValueError): return {} def requirements() -> list[dict]: rows = list(read(ROOT / "docs/current/feature-capability-registry.json").get("records", [])) formats = {} for row in read(ROOT / "docs/p6/format-coverage-kernel.json").get("records", []): if row.get("direction") in {"import", "export"}: record_id = f"business-format:{row['module']}:{row['direction']}:{row['extension']}" passed = row.get("status") == "passed" or formats.get(record_id, {}).get("status") == "passed" formats[record_id] = {**row, "id": record_id, "command": f"{row['direction']} {row['extension']}", "sourcePath": row.get("path"), "sourceLine": row.get("line", 1), "status": "passed" if passed else row.get("status")} rows.extend(formats.values()) rows.extend({**row, "module": "Web", "command": row["name"]} for row in read(ROOT / "docs/current/product-business-backlog.json").get("records", [])) return rows @lru_cache(maxsize=512) def digest(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else "missing" @lru_cache(maxsize=64) def module_inputs(module: str) -> dict: slug = "".join(character for character in module.lower() if character.isalnum()) paths = set((ROOT / "web/src").glob(f"{slug}-*.ts*")) paths.update((ROOT / "web/src").glob(f"{slug}.*.ts")) modules = read(ROOT / "web/public/modules/manifest.json").get("modules", {}) def owns(path: Path) -> bool: matches = [name for name in modules if path.stem.lower().startswith(name.lower())] return max(matches, key=len, default=module) == module and path.stem.lower().startswith(slug) paths.update(path for path in (ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe").glob("*.*") if owns(path)) artifact = modules.get(module, {}) for worker in [artifact, *artifact.get("workers", {}).values()] if isinstance(artifact, dict) else []: runtime = worker.get("runtimeUrl") if isinstance(runtime, str): stem = Path(urlsplit(runtime).path).stem.removeprefix("FreeCADWeb") source = ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe" / f"{stem}.cpp" if source.is_file(): paths.add(source) paths.update(ROOT / f"web/src/{name}" for name in ( "module-router.ts", "native-source-policy.ts", "desktop-command-dispatcher.ts", "facade-envelope.ts")) if module == "Web": paths.update((ROOT / "web/src").glob("*-shell.tsx")) paths.update((ROOT / "web/src").glob("*-desktop-adapter.ts")) return { "sources": {str(path.relative_to(ROOT)): digest(path) for path in sorted(paths) if path.is_file()}, "artifact": read(ROOT / "web/public/modules/manifest.json").get("modules", {}).get(module), } def fingerprint(row: dict) -> str: route = row.get("route") or {} module = (route.get("module") if isinstance(route, dict) else None) or row["module"] source = str(row.get("sourcePath") or row.get("source") or "") source_path = ROOT / source if source and not source_path.is_file(): source_path = ROOT / "freecad-source" / source value = {"id": row["id"], "route": route, "inputs": module_inputs(module), "source": digest(source_path)} return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() def successful_result(result: dict) -> bool: cases = result.get("cases", []) return bool(result.get("status") == "passed" and cases and all( isinstance(case, dict) and "input" in case and case.get("assertions") and all( isinstance(item, dict) and item.get("name") and item.get("passed") is True for item in case["assertions"]) for case in cases)) def accepted_ids(rows: list[dict] | None = None) -> set[str]: accepted = set() evidence_cache = {} entries = read(LEDGER).get("records", {}) for row in rows if rows is not None else requirements(): if "implementationStatus" in row and row["implementationStatus"] != "routed": continue entry = entries.get(row["id"], {}) if entry.get("status") != "passed" or entry.get("fingerprint") != fingerprint(row): continue path = (ROOT / str(entry.get("evidencePath", ""))).resolve() if not path.is_relative_to(ROOT) or not path.is_file() or digest(path) != entry.get("evidenceSha256"): continue if path not in evidence_cache: report = read(path) evidence_cache[path] = {item.get("capabilityId"): item for item in report.get("results", [])} if report.get("schema") == "freecad-web-business-cases/v1" else {} if successful_result(evidence_cache[path].get(row["id"], {})): accepted.add(row["id"]) return accepted def business_complete() -> bool: from failure_reconciliation import summary_current audit = read(ROOT / "docs/current/web-business-completion-audit.json") if audit.get("businessFunctionalComplete") is not True: return False digest.cache_clear() module_inputs.cache_clear() rows = requirements() return bool(rows) and len(accepted_ids(rows)) == len(rows) and summary_current()["complete"]