"""P0-004 CI/CD baseline source readiness. Loads the committed source-readiness snapshot for the dev/prod CI/CD baseline. This is read-only: it checks committed source paths and does not modify workflows, trigger Gitea, create repos, sync refs, read secrets, or touch hosts. """ from __future__ import annotations import json from pathlib import Path from typing import Any from src.services.snapshot_paths import default_operations_dir, resolve_repo_root _DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) _SNAPSHOT_FILE = "p0-cicd-baseline-source-readiness.snapshot.json" _SCHEMA_VERSION = "p0_cicd_baseline_source_readiness_v1" def load_latest_p0_cicd_baseline_source_readiness( operations_dir: Path | None = None, ) -> dict[str, Any]: """Load the committed P0-004 CI/CD baseline source readiness snapshot.""" directory = operations_dir or _DEFAULT_OPERATIONS_DIR path = directory / _SNAPSHOT_FILE with path.open(encoding="utf-8") as handle: payload = json.load(handle) if not isinstance(payload, dict): raise ValueError(f"{path}: expected JSON object") repo_root = resolve_repo_root(path) _enrich_source_presence(payload, repo_root) _require_schema(payload, str(path)) _require_operation_boundaries(payload, str(path)) _require_rollup_consistency(payload, str(path)) return payload def _enrich_source_presence(payload: dict[str, Any], repo_root: Path) -> None: sources = _list(payload.get("required_sources")) required_sources = [source for source in sources if _dict(source).get("required") is True] present_ids: list[str] = [] missing_ids: list[str] = [] for source in required_sources: item = _dict(source) relative_path = str(item.get("path") or "") present = bool(relative_path) and _source_present(repo_root, relative_path) item["present"] = present if present: present_ids.append(str(item.get("id") or relative_path)) else: missing_ids.append(str(item.get("id") or relative_path)) required_count = len(required_sources) present_count = len(present_ids) missing_count = len(missing_ids) rollups = _dict(payload.get("rollups")) rollups["required_source_count"] = required_count rollups["present_required_source_count"] = present_count rollups["missing_required_source_count"] = missing_count rollups["source_readiness_percent"] = round( present_count / max(required_count, 1) * 100 ) rollups["blocked_source_ids"] = missing_ids rollups["hard_blocker_count"] = len(_list(payload.get("blockers"))) rollups["next_action_count"] = len(_list(payload.get("next_actions"))) payload["status"] = ( "ready_for_template_copy_apply_gate" if missing_count == 0 else "blocked_required_sources_missing" ) def _source_present(repo_root: Path, relative_path: str) -> bool: if (repo_root / relative_path).is_file(): return True api_prefix = "apps/api/" if relative_path.startswith(api_prefix): runtime_relative_path = relative_path.removeprefix(api_prefix) return (repo_root / runtime_relative_path).is_file() return False def _require_schema(payload: dict[str, Any], label: str) -> None: actual = payload.get("schema_version") if actual != _SCHEMA_VERSION: raise ValueError( f"{label}: expected schema_version={_SCHEMA_VERSION}, got {actual!r}" ) def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None: boundaries = _dict(payload.get("operation_boundaries")) if boundaries.get("read_only_api_allowed") is not True: raise ValueError(f"{label}: read_only_api_allowed must be true") blocked_flags = { "workflow_modification_allowed", "workflow_trigger_allowed", "repo_creation_allowed", "refs_sync_allowed", "github_api_allowed", "host_or_k8s_write_allowed", "secret_read_allowed", "raw_session_or_sqlite_read_allowed", } allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False) if allowed: raise ValueError(f"{label}: operation boundaries must remain false: {allowed}") def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None: sources = [ source for source in _list(payload.get("required_sources")) if _dict(source).get("required") is True ] blockers = _list(payload.get("blockers")) next_actions = _list(payload.get("next_actions")) rollups = _dict(payload.get("rollups")) missing_ids = [ str(_dict(source).get("id") or _dict(source).get("path") or "") for source in sources if _dict(source).get("present") is not True ] if rollups.get("required_source_count") != len(sources): raise ValueError(f"{label}: required_source_count must match sources") if rollups.get("missing_required_source_count") != len(missing_ids): raise ValueError(f"{label}: missing_required_source_count mismatch") if rollups.get("blocked_source_ids") != missing_ids: raise ValueError(f"{label}: blocked_source_ids must match missing sources") if rollups.get("hard_blocker_count") != len(blockers): raise ValueError(f"{label}: hard_blocker_count must match blockers") if rollups.get("next_action_count") != len(next_actions): raise ValueError(f"{label}: next_action_count must match next_actions") def _dict(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {} def _list(value: Any) -> list[Any]: return value if isinstance(value, list) else []