#!/usr/bin/env python3 """Reuse completed implementations in bounded, executable business workflows.""" import argparse import hashlib import json from collections import Counter from datetime import datetime, timezone from business_acceptance import ROOT, accepted_ids, read, requirements from implementation_first import load_registry from prepare_feature_capability_batch import MODULE_ORDER from scheduler_progress import cooling_ids, retry_details from migration_workflows import DELIVERY_BATCH_SIZE, DELIVERY_MODULE_ORDER, PRODUCT_ORDER, delivery_route, desktop_command, workflow_family, workflow_order def contract_ids(module: str) -> set[str]: contract = read(ROOT / f"docs/business-acceptance/{module}.json") command = contract.get("command", []) if not isinstance(command, list) or len(command) < 2 or command[0] not in {"node", "python3"}: return set() script = (ROOT / str(command[1])).resolve() if not script.is_relative_to(ROOT) or not script.is_file(): return set() return {value for value in contract.get("coveredIds", []) if isinstance(value, str)} def inventory() -> dict: rows = requirements() accepted = accepted_ids(rows) receipts = load_registry().get("records", {}) ready = {record_id for record_id, entry in receipts.items() if entry.get("status") in {"implementation_ready", "implemented_pending_use"}} pending = [row for row in rows if row.get("implementationStatus", "routed") == "routed" and row["id"] not in accepted and (row["id"] in ready or row.get("status") == "passed")] covered = {module: contract_ids(module) for module in {row["module"] for row in pending}} build_overrides = {module: read(ROOT / f"docs/business-acceptance/{module}.json").get("buildOverrides", {}) for module in covered} cooling = cooling_ids(candidates={row["id"] for row in pending}) return {"rows": rows, "accepted": accepted, "pending": pending, "covered": covered, "cooling": cooling, "receipts": receipts, "buildOverrides": build_overrides} def workflow(row: dict, receipts: dict) -> str: if desktop_command(row): return receipts.get(row["id"], {}).get("family") or "desktop-command" return workflow_family(row) def candidates(state: dict, covered_only: bool = False, delivery_only: bool = False, uncovered_only: bool = False, exclude_module: str | None = None) -> list[dict]: groups = {} for row in state["pending"]: if row["module"] == exclude_module: continue if delivery_only and not delivery_route(row): continue if row["id"] in state["cooling"]: continue executable = row["id"] in state["covered"][row["module"]] if covered_only and not executable: continue if uncovered_only and executable: continue family = "existing-business-cases" if executable else workflow(row, state["receipts"]) override = state.get("buildOverrides", {}).get(row["module"], {}).get(row["id"]) if executable else None key = (row["module"], family, json.dumps(override, sort_keys=True)) groups.setdefault(key, {"module": row["module"], "family": family, "executable": executable, "delivery": delivery_route(row), "rows": []})["rows"].append(row) groups[key]["delivery"] = groups[key]["delivery"] or delivery_route(row) def priority(group): family = group["family"] return (not group["executable"], not group["delivery"], family == "supporting-business-route", DELIVERY_MODULE_ORDER.get(group["module"], 20 + MODULE_ORDER.get(group["module"], 50)) if group["delivery"] else MODULE_ORDER.get(group["module"], 50), group["module"], workflow_order(family)) return sorted(groups.values(), key=priority) def plan(state: dict, covered_only: bool = False, delivery_only: bool = False, uncovered_only: bool = False, exclude_module: str | None = None) -> dict: groups = candidates(state, covered_only, delivery_only, uncovered_only, exclude_module) deferred = {row["id"] for row in state["pending"] if row["id"] in state["cooling"] and (not delivery_only or delivery_route(row)) and (not covered_only or row["id"] in state["covered"][row["module"]]) and (not uncovered_only or row["id"] not in state["covered"][row["module"]]) and row["module"] != exclude_module} if not groups: return {"schema": "freecad-web-module-business-batch/v1", "status": "no_ready_batch", "records": [], "reason": "no business acceptance batch ready", **retry_details(deferred)} group = groups[0] module = group["module"] limit = 16 if group["executable"] else DELIVERY_BATCH_SIZE selected = sorted(group["rows"], key=lambda row: (PRODUCT_ORDER.get(row["id"], 100), row["id"]))[:limit] identity = "|".join([module, "business-acceptance", "business-acceptance", *(row["id"] for row in selected)]) return { "schema": "freecad-web-module-business-batch/v1", "status": "ready", "strategy": "module-business-first-deferred-validation", "taskId": read(ROOT / "docs/current/task-state.json").get("active_task"), "batchId": hashlib.sha256(identity.encode()).hexdigest()[:24], "minimumRecords": 1, "singletonFallback": len(selected) == 1, "selected": {"module": module, "tier": "business-acceptance", "family": "business-acceptance", "recordCount": len(selected)}, "businessWorkflow": group["family"], "acceptanceReady": group["executable"], "businessLane": "development" if uncovered_only else "acceptance", "selectionScope": "delivery" if delivery_only else "full", "fields": ["id", "command", "status", "source", "sourceLine"], "records": [[row["id"], row["command"], "acceptance_required", row.get("sourcePath") or row.get("source") or "web/src/module-router.ts", row.get("sourceLine", 1)] for row in selected], "acceptanceContract": f"docs/business-acceptance/{module}.json", "queueGroupCount": len(groups), "pendingBusinessCount": len(state["pending"]), "queue": [{"module": item["module"], "family": item["family"], "unresolved": len(item["rows"]), "acceptanceReady": item["executable"]} for item in groups[:8]], } def write_plan_report(state: dict) -> dict: total = Counter(row["module"] for row in state["rows"]) accepted = Counter(row["module"] for row in state["rows"] if row["id"] in state["accepted"]) pending = Counter(row["module"] for row in state["pending"]) executable = Counter(row["module"] for row in state["pending"] if row["id"] in state["covered"][row["module"]]) modules = [{"module": module, "total": total[module], "accepted": accepted[module], "reusablePending": pending[module], "executablePending": executable[module], "needsCases": pending[module] - executable[module], "implementationGap": total[module] - accepted[module] - pending[module]} for module in sorted(total, key=lambda name: (MODULE_ORDER.get(name, 50), name))] report = {"generatedAt": datetime.now(timezone.utc).isoformat(), "total": len(state["rows"]), "accepted": len(state["accepted"]), "reusablePending": len(state["pending"]), "executablePending": sum(executable.values()), "modules": modules, "nextBatch": plan(state), "policy": "executable-cases-first; alternate acceptance-development and implementation"} output = ROOT / "docs/current/business-acceptance-plan.json" output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") lines = ["# Business Acceptance Plan", "", f"Accepted: {report['accepted']}/{report['total']}. Reusable pending: {report['reusablePending']}; executable pending: {report['executablePending']}.", "", "Implementation receipts are candidates for acceptance, not proof of functional completion.", "", "| Module | Total | Accepted | Reusable Pending | Executable | Needs Cases | Implementation Gap |", "| --- | ---: | ---: | ---: | ---: | ---: | ---: |"] lines.extend("| " + " | ".join(str(row[key]) for key in ("module", "total", "accepted", "reusablePending", "executablePending", "needsCases", "implementationGap")) + " |" for row in modules) output.with_suffix(".md").write_text("\n".join(lines) + "\n", encoding="utf-8") return report def main(argv=None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--covered-only", action="store_true") parser.add_argument("--delivery-only", action="store_true") parser.add_argument("--uncovered-only", action="store_true", help="select existing implementations that need real business cases") parser.add_argument("--exclude-module", help="leave the current blocked module intact while another module advances") parser.add_argument("--report-only", action="store_true") args = parser.parse_args(argv) if args.covered_only and args.uncovered_only: parser.error("--covered-only and --uncovered-only are mutually exclusive") state = inventory() if args.report_only: report = write_plan_report(state) print(json.dumps({key: report[key] for key in ("accepted", "total", "reusablePending", "executablePending")})) return 0 payload = plan(state, args.covered_only, args.delivery_only, args.uncovered_only, args.exclude_module) payload["generatedAt"] = datetime.now(timezone.utc).isoformat() (ROOT / "docs/current/module-batch-manifest.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(json.dumps({"status": payload["status"], "remaining": len(state["pending"]), "selected": payload.get("selected"), "acceptanceReady": payload.get("acceptanceReady")})) return 0 if __name__ == "__main__": raise SystemExit(main())