#!/usr/bin/env python3 """Shared state for the implementation-first, deferred-validation scheduler mode.""" from __future__ import annotations import json import hashlib import os from datetime import datetime, timezone from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] CHECKPOINT_PATH = ROOT / "docs/current/continuation-state.json" REGISTRY_PATH = ROOT / "docs/current/implementation-first-registry.json" RUNTIME_ISSUE_PATH = ROOT / "docs/current/runtime-issue-ledger.json" CAPABILITY_PATH = ROOT / "docs/current/feature-capability-registry.json" EXECUTION_POLICY_PATH = ROOT / "docs/current/execution-policy.json" def _read_json(path: Path) -> dict[str, Any]: try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {} return value if isinstance(value, dict) else {} def implementation_first_enabled() -> bool: """Return the strategy without making environment-only state authoritative.""" if user_validation_deferred(): return True override = os.environ.get("FREECAD_IMPLEMENTATION_FIRST") if override is not None: return override.strip().lower() in {"1", "true", "yes", "on"} checkpoint = _read_json(CHECKPOINT_PATH) strategy = checkpoint.get("implementationStrategy") return isinstance(strategy, dict) and str(strategy.get("mode", "")) == "feature-first-deferred-validation" def user_validation_deferred() -> bool: policy = _read_json(EXECUTION_POLICY_PATH) return (policy.get("mode") == "implementation-only-user-validation" and policy.get("automaticValidation") is False) def implementation_scope_complete() -> bool: from business_acceptance import requirements rows = requirements() records = load_registry().get("records", {}) ready = {key for key, row in records.items() if row.get("status") in {"implementation_ready", "implemented_pending_use"}} ready.update(row["id"] for row in rows if row.get("status") == "passed") unrouted = {row["id"] for row in rows if row.get("implementationStatus", "routed") != "routed"} pending = _read_json(ROOT / "docs/current/module-build-pending.json").get("targets", {}) issues = _read_json(RUNTIME_ISSUE_PATH).get("issues", []) return bool(rows and not unrouted and {row["id"] for row in rows} <= ready and not any(pending.values()) and not any(row.get("status", "open") in {"open", "confirmed", "reopened"} for row in issues)) def capability_source_fingerprint() -> str: digest = hashlib.sha256() for path in (CAPABILITY_PATH, RUNTIME_ISSUE_PATH, ROOT / "docs/current/scheduler-progress.json"): digest.update(str(path.relative_to(ROOT)).encode("utf-8")) try: digest.update(path.read_bytes()) except OSError: digest.update(b"") return digest.hexdigest() def load_registry() -> dict[str, Any]: value = _read_json(REGISTRY_PATH) if value.get("schema") != "freecad-web-implementation-first-registry/v1": return { "schema": "freecad-web-implementation-first-registry/v1", "mode": "feature-first-deferred-validation", "updatedAt": None, "batches": {}, "records": {}, } value.setdefault("batches", {}) value.setdefault("records", {}) return value def implementation_ready_ids() -> set[str]: """Return records with an implementation receipt, not validation evidence.""" if not implementation_first_enabled(): return set() records = load_registry().get("records", {}) if not isinstance(records, dict): return set() allowed = {"implementation_ready", "implementation_pending_build", "implemented_pending_use"} receipt_ids = { str(record_id) for record_id, entry in records.items() if isinstance(entry, dict) and str(entry.get("status", "")) in allowed } capabilities = _read_json(CAPABILITY_PATH) capability_rows = capabilities.get("records", []) if isinstance(capabilities.get("records"), list) else [] currently_routed = { str(row.get("id")) for row in capability_rows if isinstance(row, dict) and row.get("implementationStatus") == "routed" } return receipt_ids & currently_routed def all_implementation_ready_ids() -> set[str]: """Return every current implementation receipt, including API/object batches.""" if not implementation_first_enabled(): return set() records = load_registry().get("records", {}) if not isinstance(records, dict): return set() allowed = {"implementation_ready", "implementation_pending_build", "implemented_pending_use"} return { str(record_id) for record_id, entry in records.items() if isinstance(entry, dict) and str(entry.get("status", "")) in allowed } def record_implementation_batch( batch: dict[str, Any], *, target: str, build_tree: str, artifacts: list[str], source: str = "outer-build", status: str = "implementation_ready", require_routed_capability: bool = True, ) -> dict[str, Any]: """Persist a build/接线 receipt while leaving API dispositions untouched.""" registry = load_registry() batch_id = str(batch.get("batchId") or "") selected = batch.get("selected") if isinstance(batch.get("selected"), dict) else {} rows = batch.get("records") if isinstance(batch.get("records"), list) else [] selected_ids = [str(row[0]) for row in rows if isinstance(row, list) and row and row[0]] capabilities = _read_json(CAPABILITY_PATH) capability_rows = capabilities.get("records", []) if isinstance(capabilities.get("records"), list) else [] ready_ids = { str(row.get("id")) for row in capability_rows if isinstance(row, dict) and row.get("implementationStatus") == "routed" } record_ids = ( [record_id for record_id in selected_ids if record_id in ready_ids] if require_routed_capability else selected_ids ) now = datetime.now(timezone.utc).isoformat() receipt = { "batchId": batch_id, "module": selected.get("module"), "tier": selected.get("tier"), "family": selected.get("family"), "recordIds": record_ids, "recordCount": len(record_ids), "selectedRecordCount": len(selected_ids), "remainingRecordCount": len(selected_ids) - len(record_ids), "status": status, "validation": "user_during_use" if user_validation_deferred() else "deferred_until_runtime_issue_or_explicit_full_validation", "target": target, "buildTree": build_tree, "artifacts": artifacts, "source": source, "scope": "feature-capability" if require_routed_capability else "business-family", "updatedAt": now, } batches = registry.setdefault("batches", {}) records = registry.setdefault("records", {}) if isinstance(batches, dict) and batch_id: batches[batch_id] = receipt if isinstance(records, dict): for record_id in record_ids: records[record_id] = { "batchId": batch_id, "module": selected.get("module"), "tier": selected.get("tier"), "family": selected.get("family"), "status": status, "scope": "feature-capability" if require_routed_capability else "business-family", "updatedAt": now, } registry["updatedAt"] = now REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True) temporary = REGISTRY_PATH.with_suffix(".tmp") temporary.write_text(json.dumps(registry, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") temporary.replace(REGISTRY_PATH) issue_ledger = _read_json(RUNTIME_ISSUE_PATH) issues = issue_ledger.get("issues", []) if isinstance(issue_ledger.get("issues"), list) else [] changed = False for issue in issues: if not isinstance(issue, dict) or str(issue.get("capabilityId") or "") not in record_ids: continue if status == "implementation_ready" and str(issue.get("status", "open")) not in {"resolved", "closed", "fix_deployed_pending_use"}: issue["status"] = "fix_deployed_pending_use" issue["deployedAt"] = now issue["implementationBatchId"] = batch_id changed = True if changed: issue_ledger["updatedAt"] = now issue_temporary = RUNTIME_ISSUE_PATH.with_suffix(".tmp") issue_temporary.write_text(json.dumps(issue_ledger, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") issue_temporary.replace(RUNTIME_ISSUE_PATH) return receipt