agentic-ai-engineering/products/plan_validator.py

57 lines
1.9 KiB
Python

#!/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))