#!/usr/bin/env python3 """Builds dist/legal-manifest.json (HT-14): renders each project's markdown legal documents to HTML+PDF and computes a content_hash, producing the single canonical manifest every consumer backend imports at deploy time (HT-15) instead of hashing independently. content_hash source (interim, per the HT-3 "Design döntés" comment): the source markdown's git blob hash (`git hash-object`), NOT the rendered artifact's hash yet — that requires a proven byte-for-byte-deterministic render, which this pandoc+WeasyPrint combination hasn't been tested for (ResidentFirst's HRF-34 proved a *different* render pipeline deterministic, not this one). Switching later doesn't invalidate old receipts, since a receipt is bound to document_version, not content_hash alone. effective_from source: parsed from each document's own "Hatálybalépés napja: " line — the one human-controlled field carrying the legally relevant date. A document whose date is still the unfilled "[dátum]" placeholder gets effective_from: null (not yet effective) rather than an invented date. version: the short hash of the last commit that actually touched this file (NOT repo HEAD — see HT-15's belépő ellenőrzés) — same convention as the two consumer sync scripts already use for document_version. Usage: python3 scripts/build-manifest.py """ import json import re import subprocess from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent OUTPUT_DIR = REPO_ROOT / "dist" HUNGARIAN_MONTHS = { "január": 1, "február": 2, "március": 3, "április": 4, "május": 5, "június": 6, "július": 7, "augusztus": 8, "szeptember": 9, "október": 10, "november": 11, "december": 12, } # e.g. "*Hatálybalépés napja: 2026. július 17. · Verzió: 1.0*" DATE_LINE_RE = re.compile(r"Hatálybalépés napja:\s*(.+?)\s*·\s*Verzió:") HU_DATE_RE = re.compile(r"(\d{4})\.\s*([a-záéíóöőúüű]+)\s*(\d{1,2})\.") def parse_effective_from(markdown: str) -> str | None: m = DATE_LINE_RE.search(markdown) if not m: return None hu = HU_DATE_RE.match(m.group(1).strip()) if not hu: return None # still the "[dátum]" placeholder, or an unrecognized format year, month_name, day = hu.groups() month = HUNGARIAN_MONTHS.get(month_name.lower()) if month is None: return None return f"{int(year):04d}-{month:02d}-{int(day):02d}" def _git(*args: str) -> str: return subprocess.run( ["git", *args], cwd=REPO_ROOT, check=True, capture_output=True, text=True ).stdout.strip() def git_blob_hash(path: Path) -> str: return _git("hash-object", str(path)) def last_commit_hash(path: Path) -> str: relative = path.relative_to(REPO_ROOT) # NOTE: requires full history (fetch-depth: 0 in CI) — a shallow clone would silently # resolve every file to the same single available commit. See HT-15. return _git("log", "-1", "--format=%h", "--", str(relative)) def render_html(markdown_path: Path, html_path: Path) -> None: subprocess.run( ["pandoc", str(markdown_path), "-o", str(html_path), "--standalone", "--metadata", "lang=hu"], check=True, ) def render_pdf(html_path: Path, pdf_path: Path) -> None: from weasyprint import HTML # full_fonts=True: WeasyPrint's default font subsetting is not byte-for-byte # deterministic (confirmed empirically while building HRF-34) — full embedding is. HTML(filename=str(html_path)).write_pdf(str(pdf_path), full_fonts=True) def build_manifest_for_project(project_dir: Path) -> dict: manifest = {} for md_path in sorted(project_dir.glob("*.md")): doc_type = md_path.stem markdown = md_path.read_text(encoding="utf-8") out_dir = OUTPUT_DIR / project_dir.name out_dir.mkdir(parents=True, exist_ok=True) html_path = out_dir / f"{doc_type}.html" pdf_path = out_dir / f"{doc_type}.pdf" render_html(md_path, html_path) render_pdf(html_path, pdf_path) manifest[doc_type] = { "version": last_commit_hash(md_path), "effective_from": parse_effective_from(markdown), "content_hash": git_blob_hash(md_path), "html": f"{project_dir.name}/{doc_type}.html", "pdf": f"{project_dir.name}/{doc_type}.pdf", } return manifest def main() -> None: OUTPUT_DIR.mkdir(exist_ok=True) full_manifest = {} for project_dir in sorted(REPO_ROOT.iterdir()): if not project_dir.is_dir() or project_dir.name.startswith(".") or project_dir.name in ("dist", "scripts"): continue if not list(project_dir.glob("*.md")): continue print(f"Renderelés: {project_dir.name}/") full_manifest[project_dir.name] = build_manifest_for_project(project_dir) manifest_path = OUTPUT_DIR / "legal-manifest.json" manifest_path.write_text(json.dumps(full_manifest, indent=2, ensure_ascii=False), encoding="utf-8") print(f"Manifest írva: {manifest_path}") if __name__ == "__main__": main()