#!/usr/bin/env python3 """Minimal Plan F3 validator.""" from __future__ import annotations import re from pathlib import Path REQUIRED_SECTIONS = ["purpose", "problem", "solution", "files", "phases", "validation", "references", "notes"] REQUIRED_FRONTMATTER = ["product_id", "purpose", "references"] def parse_frontmatter(text: str) -> dict: if not text.startswith("---\n"): return {} end = text.find("\n---", 4) if end == -1: return {} data = {} for line in text[4:end].splitlines(): if ":" in line and not line.startswith(" "): key, value = line.split(":", 1) data[key.strip()] = value.strip() return data def sections(text: str) -> set[str]: found = set() for match in re.finditer(r"^##\s+(.+)$", text, re.MULTILINE): found.add(match.group(1).strip().lower()) return found def validate_plan(path: str | Path, strict: bool = False) -> dict: path = Path(path) text = path.read_text(encoding="utf-8") front = parse_frontmatter(text) missing_front = [k for k in REQUIRED_FRONTMATTER if k not in front] found_sections = sections(text) missing_sections = [s for s in REQUIRED_SECTIONS if s not in found_sections] references_ok = "references" in front and bool(re.search(r"^## References\n- ", text, re.MULTILINE)) html_path = path.with_suffix(".html") errors = [] if missing_front: errors.append(f"missing frontmatter: {', '.join(missing_front)}") if missing_sections: errors.append(f"missing sections: {', '.join(missing_sections)}") if strict and not references_ok: errors.append("references missing or empty") if strict and not html_path.exists(): errors.append("html plan missing") return {"ok": not errors, "errors": errors, "frontmatter": front, "html": str(html_path)} if __name__ == "__main__": import json, sys print(json.dumps(validate_plan(sys.argv[1], "--strict" in sys.argv), indent=2))