Files

223 lines
12 KiB
Python

#!/usr/bin/env python3
"""Read-only project inventory; writes only to a NEW explicit output directory.
No model imports, simulations, data migration, deletion, or dependency installation.
Python >=3.10, standard library only. Snapshot excludes reproducible environment/cache
directories and nested Git databases, never follows symlinks. Verify checks additions too.
"""
import argparse
import concurrent.futures as cf
import gzip
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
from datetime import datetime, timezone
ROOTS = ("datasets", "experiments", "models", "backend", "frontend", "game", "logs", "recordings")
EXCLUDE = {".git", ".venv", "venv", "__pycache__", ".pytest_cache", ".mypy_cache", ".cache", "node_modules"}
TEXT_EXT = {".py", ".md", ".json", ".yaml", ".yml", ".toml", ".txt", ".sh", ".csv", ".tsv"}
def dump(path, obj):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("x", encoding="utf-8", newline="\n") as f:
json.dump(obj, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
def sha(path):
h = hashlib.sha256()
with path.open("rb") as f:
for block in iter(lambda: f.read(4 * 1024 * 1024), b""):
h.update(block)
return h.hexdigest()
def entries(root):
for name in ROOTS:
base = root / name
if not base.exists():
continue
for folder, dirs, files in os.walk(base, followlinks=False):
dirs[:] = sorted(d for d in dirs if d not in EXCLUDE)
for d in dirs[:]:
p = Path(folder) / d
if p.is_symlink():
yield p
dirs.remove(d)
for f in sorted(files):
yield Path(folder) / f
def record(root, path):
st = path.lstat()
row = {"path": path.relative_to(root).as_posix(), "bytes": st.st_size,
"mtime_ns": st.st_mtime_ns, "mode": oct(st.st_mode & 0o7777)}
if path.is_symlink():
row.update(kind="symlink", target=os.readlink(path))
elif path.is_file():
row.update(kind="file", sha256=sha(path))
after = path.stat()
if (st.st_size, st.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
raise RuntimeError("File changed during hashing: " + row["path"])
else:
raise RuntimeError("Unexpected special file: " + str(path))
return row
def snapshot(args):
root, out = args.root.resolve(), args.output.resolve()
if out.exists():
raise RuntimeError("Refusing existing output directory")
out.mkdir(parents=True)
paths = sorted(entries(root))
counts = {}
with gzip.open(out / "files.jsonl.gz", "xt", encoding="utf-8") as f, cf.ThreadPoolExecutor(max_workers=args.workers) as ex:
for n, row in enumerate(ex.map(lambda p: record(root, p), paths), 1):
f.write(json.dumps(row, ensure_ascii=False, sort_keys=True) + "\n")
key = row["path"].split("/")[0]
c = counts.setdefault(key, {"files": 0, "logical_bytes": 0})
c["files"] += 1
c["logical_bytes"] += row["bytes"]
if n % 25000 == 0:
print(f"hashed {n}/{len(paths)}", flush=True)
summary = {"schema_version": 1, "created_utc": datetime.now(timezone.utc).isoformat(),
"root": str(root), "roots": ROOTS, "excluded_directory_names": sorted(EXCLUDE),
"files": len(paths), "groups": counts, "symlinks_followed": False,
"files_jsonl_gz_sha256": sha(out / "files.jsonl.gz"),
"not_a_data_backup": True, "historical_authenticity_claim": False}
dump(out / "summary.json", summary)
print(json.dumps(summary, ensure_ascii=False), flush=True)
def verify(args):
root = args.root.resolve()
summary = json.loads((args.snapshot / "summary.json").read_text())
manifest = args.snapshot / "files.jsonl.gz"
if sha(manifest) != summary["files_jsonl_gz_sha256"]:
raise RuntimeError("Snapshot manifest checksum mismatch")
with gzip.open(manifest, "rt", encoding="utf-8") as f:
rows = [json.loads(line) for line in f]
def check(row):
p = root / row["path"]
if p.is_absolute() and not p.absolute().is_relative_to(root):
return {"path": row["path"], "problem": "unsafe path"}
if ".." in Path(row["path"]).parts:
return {"path": row["path"], "problem": "unsafe path"}
try:
now = record(root, p)
keys = ("kind", "target", "mode") if row["kind"] == "symlink" else ("kind", "bytes", "sha256", "mode")
different = [k for k in keys if row.get(k) != now.get(k)]
return {"path": row["path"], "changed_fields": different} if different else None
except (OSError, RuntimeError) as e:
return {"path": row["path"], "problem": str(e)}
with cf.ThreadPoolExecutor(max_workers=args.workers) as ex:
mismatches = [r for r in ex.map(check, rows) if r]
before = {r["path"] for r in rows}
after = {p.relative_to(root).as_posix() for p in entries(root)}
result = {"checked": len(rows), "mismatches": mismatches, "added": sorted(after-before),
"missing": sorted(before-after), "checked_utc": datetime.now(timezone.utc).isoformat(),
"snapshot_sha256": sha(manifest), "excluded_directory_names": sorted(EXCLUDE),
"pass": not mismatches and before == after}
if args.output:
dump(args.output, result)
print(json.dumps(result, ensure_ascii=False), flush=True)
return 0 if result["pass"] else 1
def run(root, *cmd):
p = subprocess.run(cmd, cwd=root, text=True, capture_output=True, check=False)
return {"exit_code": p.returncode, "stdout": p.stdout.strip(), "stderr": p.stderr.strip()}
def catalog(args):
root, out = args.root.resolve(), args.output.resolve()
if out.exists():
raise RuntimeError("Refusing existing output directory")
out.mkdir(parents=True)
stages, evidence, refs, resources = [], [], [], []
for d in sorted((root / "experiments").iterdir()):
if not d.is_dir():
continue
selected = []
# Small audit/config/result evidence only. Retain originals byte-for-byte.
for p in sorted(d.rglob("*")):
rel = p.relative_to(d)
if "external-resources" in rel.parts or any(x in EXCLUDE for x in rel.parts):
continue
if not p.is_file() or p.is_symlink() or p.stat().st_size > 512 * 1024:
continue
suffix = p.suffix.lower()
include = suffix in {".md", ".yaml", ".yml"} or (
suffix == ".json" and (len(rel.parts) == 1 or rel.parts[0] in {
"audit", "regression", "literature-audit", "numerical-contract", "configs",
"config", "revalidation", "phase4c-v2", "manifests"} or "summary" in p.name or "gate" in p.name))
if include:
dest = out / "evidence" / d.name / rel
dest.parent.mkdir(parents=True, exist_ok=True)
with p.open("rb") as src, dest.open("xb") as dst:
shutil.copyfileobj(src, dst)
item = {"source_path": p.relative_to(root).as_posix(), "copy_path": dest.relative_to(out).as_posix(),
"sha256": sha(p), "bytes": p.stat().st_size}
evidence.append(item)
selected.append(item["copy_path"])
stages.append({"stage_directory": d.name, "evidence": selected,
"status_policy": "Use original evidence; completion is not a scientific PASS"})
# Index citations in project-authored sources, configs, provenance and literature notes.
for p in entries(root):
rel = p.relative_to(root)
if p.is_symlink() or p.suffix.lower() not in TEXT_EXT or p.stat().st_size > 2 * 1024 * 1024:
continue
if "external-resources" in rel.parts or "skeletons" in rel.parts:
continue
body = p.read_text(encoding="utf-8", errors="replace")
urls = sorted(set(re.findall(r"https?://[^\s<>\"'`]+", body)))
dois = sorted(set(x.rstrip(".,;)]}") for x in re.findall(r"10\.\d{4,9}/[-._;()/:A-Za-z0-9]+", body)))
# Never export URLs containing user credentials or common secret query parameters.
urls = [u.rstrip(".,;)]}") for u in urls if not re.search(r"https?://[^/]*@|[?&](?:token|key|secret|password|signature)=", u, re.I)]
if urls or dois:
refs.append({"source_path": rel.as_posix(), "sha256": sha(p), "urls": urls, "doi_candidates": dois,
"review_status": "extracted-not-automatically-endorsed"})
known = [
("experiments/phase4f-retinotopic-vision/external-resources/celltype-explorer", "CC-BY-4.0", "LICENSE"),
("experiments/phase4f-retinotopic-vision/external-resources/visualpathways", "GPL-3.0 license text; grant/version options require upstream review", "LICENSE"),
("experiments/phase4f-retinotopic-vision/external-resources/flyvis", "MIT declared by pyproject classifier; standalone LICENSE absent in frozen checkout", "pyproject.toml"),
("experiments/phase4l-olfactory-grounding/external-resources/DoOR.data", "CC BY-SA 4.0 declared in DESCRIPTION", "DESCRIPTION"),
]
for value, license_id, license_file in known:
p = root / value
if not p.exists():
resources.append({"path": value, "status": "MISSING"})
continue
lp = p / license_file
resources.append({"path": value, "commit": run(p, "git", "rev-parse", "HEAD"),
"origin": run(p, "git", "remote", "get-url", "origin"),
"worktree_status": run(p, "git", "--no-optional-locks", "status", "--short"),
"license_observation": license_id, "license_evidence": str(lp.relative_to(root)),
"license_evidence_sha256": sha(lp) if lp.is_file() else None,
"vendored_in_parent_git": False})
dump(out / "stages.json", stages)
dump(out / "evidence-index.json", evidence)
dump(out / "source-citations.json", refs)
dump(out / "external-resources.lock.json", resources)
env = {"captured_utc": datetime.now(timezone.utc).isoformat(),
"python": run(root, str(root / "models/male-cns-lif/.venv/bin/python"), "--version"),
"packages": run(root, str(root / "models/male-cns-lif/.venv/bin/python"), "-c",
"import importlib.metadata as m,json;print(json.dumps(sorted((x.metadata['Name'],x.version) for x in m.distributions())))"),
"gpu": run(root, "nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader"),
"os": run(root, "uname", "-a"), "git": run(root, "git", "--version"),
"nvcc": run(root, "sh", "-c", "command -v nvcc >/dev/null && nvcc --version || true"),
"historical_environment_guaranteed": False}
dump(out / "environment.json", env)
print(json.dumps({"stages":len(stages), "evidence_files":len(evidence), "citation_source_files":len(refs), "external_resources":len(resources)}))
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--root", type=Path, default=Path("."))
subs = ap.add_subparsers(dest="cmd", required=True)
s = subs.add_parser("snapshot"); s.add_argument("--output", type=Path, required=True); s.add_argument("--workers", type=int, default=8)
v = subs.add_parser("verify"); v.add_argument("--snapshot", type=Path, required=True); v.add_argument("--output", type=Path); v.add_argument("--workers", type=int, default=8)
c = subs.add_parser("catalog"); c.add_argument("--output", type=Path, required=True)
args = ap.parse_args()
return {"snapshot":snapshot, "verify":verify, "catalog":catalog}[args.cmd](args)
if __name__ == "__main__":
sys.exit(main())