209 lines
8.6 KiB
Python
209 lines
8.6 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 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"<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 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
|