122 lines
5.7 KiB
Python
122 lines
5.7 KiB
Python
"""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"]
|