Files
FreeCADWasmDev/tools/implementation_first.py

259 lines
12 KiB
Python

#!/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 continuous_acceptance_enabled() -> bool:
policy = _read_json(EXECUTION_POLICY_PATH)
return (policy.get("continuousDevelopment") is True
and policy.get("completionFocus") == "business-acceptance"
and policy.get("automaticValidation") is True)
def deferred_validation_ids() -> set[str]:
"""Historical passes are scheduling exclusions, never current acceptance proof."""
if _read_json(EXECUTION_POLICY_PATH).get("deferPreviouslyPassedValidation") is not True:
return set()
records = _read_json(ROOT / "docs/current/business-acceptance.json").get("records", {})
return {key for key, entry in records.items() if isinstance(entry, dict)
and isinstance(entry.get("evidencePath"), str) and entry["evidencePath"]}
def implementation_scope_complete() -> bool:
if _read_json(EXECUTION_POLICY_PATH).get("workScope") == "gap-repair-only":
# Waiting for concrete defects is not a newly verified full completion.
return False
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"<missing>")
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 known_later_gaps(records: dict) -> set[str]:
"""An older build receipt cannot hide a later demonstrated missing behavior."""
ledger = _read_json(ROOT / "docs/current/business-acceptance.json").get("records", {})
return {key for key, entry in ledger.items() if isinstance(entry, dict)
and entry.get("status") == "failed" and entry.get("lastFailureEvidencePath")
and str(entry.get("updatedAt", "")) > str(records.get(key, {}).get("updatedAt", ""))}
def open_implementation_gaps() -> set[str]:
"""Explicit source-confirmed gaps survive unrelated build-only receipts."""
gaps = _read_json(EXECUTION_POLICY_PATH).get("openImplementationGaps", {})
return {key for key, value in gaps.items() if isinstance(value, dict) and value.get("status") == "open"}
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"
}
if user_validation_deferred():
# User explicitly requests direct reuse of completed work until the
# implementation phase ends; do not re-open it for a stale test hash.
accepted = _read_json(ROOT / "docs/current/business-acceptance.json").get("records", {})
receipt_ids.update(key for key, entry in accepted.items() if isinstance(entry, dict)
and entry.get("status") == "passed" and entry.get("evidencePath"))
receipt_ids.difference_update(known_later_gaps(records))
return (receipt_ids & currently_routed) - open_implementation_gaps()
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"}
ready = {
str(record_id)
for record_id, entry in records.items()
if isinstance(entry, dict) and str(entry.get("status", "")) in allowed
}
if user_validation_deferred():
accepted = _read_json(ROOT / "docs/current/business-acceptance.json").get("records", {})
ready.update(key for key, entry in accepted.items() if isinstance(entry, dict)
and entry.get("status") == "passed" and entry.get("evidencePath"))
ready.difference_update(known_later_gaps(records))
return ready - open_implementation_gaps()
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
)
# A compiled bundle cannot close a known missing workflow. Preserve its
# historical receipt but do not refresh it as implemented until the
# source-confirmed gap is actually resolved.
record_ids = [record_id for record_id in record_ids if record_id not in open_implementation_gaps()]
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