#!/usr/bin/env python3 """ Generate a provenance manifest for the Nexus browser surface. Hashes all frontend files so smoke tests can verify the app comes from a clean Timmy_Foundation/the-nexus checkout, not stale sources. Usage: python bin/generate_provenance.py # writes provenance.json python bin/generate_provenance.py --check # verify existing manifest matches """ import hashlib import json import subprocess import sys import os from datetime import datetime, timezone from pathlib import Path # Files that constitute the browser-facing contract CONTRACT_FILES = [ "index.html", "app.js", "style.css", "gofai_worker.js", "server.py", "portals.json", "vision.json", "manifest.json", ] # Component files imported by app.js COMPONENT_FILES = [ "nexus/components/spatial-memory.js", "nexus/components/session-rooms.js", "nexus/components/timeline-scrubber.js", "nexus/components/memory-particles.js", ] ALL_FILES = CONTRACT_FILES + COMPONENT_FILES def sha256_file(path: Path) -> str: h = hashlib.sha256() h.update(path.read_bytes()) return h.hexdigest() def get_git_info(repo_root: Path) -> dict: """Capture git state for provenance.""" def git(*args): try: r = subprocess.run( ["git", *args], cwd=repo_root, capture_output=True, text=True, timeout=10, ) return r.stdout.strip() if r.returncode == 0 else None except Exception: return None return { "commit": git("rev-parse", "HEAD"), "branch": git("rev-parse", "--abbrev-ref", "HEAD"), "remote": git("remote", "get-url", "origin"), "dirty": git("status", "--porcelain") != "", } def generate_manifest(repo_root: Path) -> dict: files = {} missing = [] for rel in ALL_FILES: p = repo_root / rel if p.exists(): files[rel] = { "sha256": sha256_file(p), "size": p.stat().st_size, } else: missing.append(rel) return { "generated_at": datetime.now(timezone.utc).isoformat(), "repo": "Timmy_Foundation/the-nexus", "git": get_git_info(repo_root), "files": files, "missing": missing, "file_count": len(files), } def check_manifest(repo_root: Path, existing: dict) -> tuple[bool, list[str]]: """Check if current files match the stored manifest. Returns (ok, mismatches).""" mismatches = [] for rel, expected in existing.get("files", {}).items(): p = repo_root / rel if not p.exists(): mismatches.append(f"MISSING: {rel}") elif sha256_file(p) != expected["sha256"]: mismatches.append(f"CHANGED: {rel}") return (len(mismatches) == 0, mismatches) def main(): repo_root = Path(__file__).resolve().parent.parent manifest_path = repo_root / "provenance.json" if "--check" in sys.argv: if not manifest_path.exists(): print("FAIL: provenance.json does not exist") sys.exit(1) existing = json.loads(manifest_path.read_text()) ok, mismatches = check_manifest(repo_root, existing) if ok: print(f"OK: All {len(existing['files'])} files match provenance manifest") sys.exit(0) else: print(f"FAIL: {len(mismatches)} file(s) differ:") for m in mismatches: print(f" {m}") sys.exit(1) manifest = generate_manifest(repo_root) manifest_path.write_text(json.dumps(manifest, indent=2) + "\n") print(f"Wrote provenance.json: {manifest['file_count']} files hashed") if manifest["missing"]: print(f" Missing (not yet created): {', '.join(manifest['missing'])}") if __name__ == "__main__": main()