"""Parse a construction plan file's YAML frontmatter. See docs/construction-plan-format.md. The frontmatter's status field is the phase-4 hard gate: the build executor (executor.py) refuses to run against anything but status: approved with both approved_by/approved_at set. """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any import yaml class PlanError(RuntimeError): pass @dataclass class ConstructionPlan: id: str status: str approved_by: str | None approved_at: str | None demand_source: str consumer_repo: str credential_type: str path: Path @classmethod def load(cls, path: str | Path) -> ConstructionPlan: path = Path(path) text = path.read_text() parts = text.split("---", 2) if len(parts) < 3 or not text.startswith("---"): raise PlanError(f"{path}: missing YAML frontmatter") data: dict[str, Any] = yaml.safe_load(parts[1]) or {} missing = {"id", "status"} - set(data) if missing: raise PlanError(f"{path}: frontmatter missing field(s): {', '.join(sorted(missing))}") return cls( id=data["id"], status=data["status"], approved_by=data.get("approved_by"), approved_at=data.get("approved_at"), demand_source=data.get("demand_source", ""), consumer_repo=data.get("consumer_repo", ""), credential_type=data.get("credential_type", ""), path=path, ) def is_approved(self) -> bool: return self.status == "approved" and bool(self.approved_by) and bool(self.approved_at)