57 lines
1.9 KiB
Python
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))
|