from __future__ import annotations import argparse import difflib import json import tempfile from pathlib import Path from techviz import __version__ from techviz.document import build_context from techviz.layout import build_layout from techviz.prompt import build_agent_prompt from techviz.reference_catalog import selection_payload from techviz.renderers import render_formats from techviz.spec import load_spec, stable_hash from techviz.validate import has_errors, validate_spec ROOT = Path(__file__).resolve().parents[1] DOCUMENT = ROOT / "examples/docs/payment-flow.md" CONTEXT = ROOT / "examples/work/payment/context.json" PROMPT = ROOT / "examples/work/payment/prompt.md" SPEC = ROOT / "examples/work/payment/spec.json" ASSETS = ROOT / "examples/assets" FORMATS = ["svg", "mermaid", "d2", "dot", "drawio", "excalidraw", "a11y"] def _manifest(spec: object, files: list[Path]) -> str: payload = { "harness_version": __version__, "spec_id": spec.id, "spec_version": spec.version, "spec_sha256": stable_hash([json.dumps(spec.as_dict(), ensure_ascii=False, sort_keys=True)]), "source_context": spec.source_context, "outputs": [path.name for path in files], "lint_issue_count": 0, "assumption_count": 0, "assumptions_allowed": False, "composition_profile": spec.profile, "reference_ids": spec.composition.reference_ids if spec.composition else [], "diagram_only": bool(spec.composition.diagram_only) if spec.composition else True, } return json.dumps(payload, ensure_ascii=False, indent=2) + "\n" def _diff(label: str, expected: str, actual: str) -> str: return "".join( difflib.unified_diff( expected.splitlines(keepends=True), actual.splitlines(keepends=True), fromfile=f"committed/{label}", tofile=f"generated/{label}", ) ) def main() -> int: parser = argparse.ArgumentParser(description="Verify committed example artifacts are reproducible.") parser.add_argument("--update", action="store_true") args = parser.parse_args() # Keep the path stored in context relative and portable. relative_document = DOCUMENT.relative_to(ROOT) generated_context = build_context(relative_document, marker_id="payment-request") generated_context["visual_reference_candidates"] = selection_payload(generated_context, limit=5) context_text = json.dumps(generated_context, ensure_ascii=False, indent=2) + "\n" spec = load_spec(SPEC) issues = validate_spec(spec, generated_context) if has_errors(issues): for issue in issues: print(issue.as_dict()) return 1 prompt_text = build_agent_prompt(generated_context) differences: list[str] = [] if args.update: CONTEXT.write_text(context_text, encoding="utf-8") PROMPT.write_text(prompt_text, encoding="utf-8") else: committed_context = CONTEXT.read_text(encoding="utf-8") if committed_context != context_text: differences.append(_diff(str(CONTEXT.relative_to(ROOT)), committed_context, context_text)) committed_prompt = PROMPT.read_text(encoding="utf-8") if committed_prompt != prompt_text: differences.append(_diff(str(PROMPT.relative_to(ROOT)), committed_prompt, prompt_text)) with tempfile.TemporaryDirectory() as tmp: temp_dir = Path(tmp) files = render_formats(spec, build_layout(spec), temp_dir, FORMATS) manifest_path = temp_dir / f"{spec.id}.manifest.json" manifest_path.write_text(_manifest(spec, files), encoding="utf-8") files.append(manifest_path) for generated in files: committed = ASSETS / generated.name if args.update: committed.write_bytes(generated.read_bytes()) continue expected = committed.read_text(encoding="utf-8") actual = generated.read_text(encoding="utf-8") if expected != actual: differences.append(_diff(str(committed.relative_to(ROOT)), expected, actual)) if differences: print("\n".join(differences)) print("Generated artifacts differ. Run: PYTHONPATH=src python scripts/check_generated.py --update") return 1 print("PASS committed context and diagram artifacts are reproducible") return 0 if __name__ == "__main__": raise SystemExit(main())