132 lines
3.8 KiB
Python
Executable File
132 lines
3.8 KiB
Python
Executable File
#!/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()
|