"""Read-only source guard; write receipt only to a new publication path.""" import argparse from datetime import datetime, timezone import hashlib import json from pathlib import Path import shutil import subprocess from publish_archives import no_links, sha_file, canonical ROOT = Path('/data/digital-drosophila') REGISTERED = 'b63e6281b6505ca3b60acb467dd0b3fae274cf513dcbfd653649ba52e9ba9740' SOURCE_COMMIT = '80aeb2093db655329774eff7b32fb6e417b8b40b' def git(*args, cwd=ROOT): return subprocess.check_output(['git', '-C', str(cwd), *args], text=True).strip() def capture(plan_path, output): output = no_links(output) if output == ROOT or ROOT in output.parents or output.exists(): raise ValueError('Receipt must be new and outside the scientific source') plan = json.loads(Path(plan_path).read_bytes()) path = ROOT / 'experiments/phase4ar-normal-conditioning/protocol/preregistered.json' raw = path.read_bytes() if hashlib.sha256(raw).hexdigest() != REGISTERED: raise ValueError('Phase 4AR registration hash mismatch') registration = json.loads(raw) records = [] for filename, expected in sorted(registration['source_sha256'].items()): source = no_links(filename) if ROOT not in source.parents: raise ValueError('Registered source outside project') actual = sha_file(source) if actual['sha256'] != expected: raise ValueError('Phase 4AR source mismatch: ' + filename) records.append(dict(path=filename, **actual)) if len(records) != 16: raise ValueError('Registered source inventory differs from reviewed scope') if git('cat-file', '-t', SOURCE_COMMIT) != 'commit': raise ValueError('Registered scientific commit unavailable') evidence = [] for group in plan['groups']: licence = group['license'] if 'evidence_file' in licence: source = ROOT / group['paths'][0] / licence['evidence_file'] actual = sha_file(source) if actual['sha256'] != licence['evidence_sha256']: raise ValueError('License evidence hash mismatch: ' + group['id']) evidence.append(dict(group=group['id'], path=str(source), **actual)) provenance = group['provenance'] source = ROOT / group['paths'][0] if (source / '.git').exists(): if git('rev-parse', 'HEAD', cwd=source) != provenance['commit']: raise ValueError('Upstream commit mismatch: ' + group['id']) if git('status', '--porcelain', cwd=source): raise ValueError('Upstream checkout changed: ' + group['id']) result = dict(schema='lfs-publication-source-guard-v1', observed_utc=datetime.now(timezone.utc).isoformat(), registration_sha256=REGISTERED, registered_scientific_commit=SOURCE_COMMIT, phase4ar_registered_sources=records, license_evidence=evidence, original_checkout_head=git('rev-parse', 'HEAD'), original_checkout_status=git('status', '--porcelain'), original_checkout_branch=git('branch', '--show-current'), disk_free_bytes={str(p): shutil.disk_usage(p).free for p in (ROOT, output.parent)}, pass_all=True, scope='16 registered Phase 4AR source files, registration, pinned upstream checkout and license evidence; not a scientific rerun or full historical checksum audit') if result['original_checkout_status']: raise ValueError('Original checkout is no longer clean; preserve and investigate') if any(x < 100 * 1024**3 for x in result['disk_free_bytes'].values()): raise ValueError('Less than 100 GiB free reserve') with output.open('xb') as handle: handle.write(canonical(result)) return dict(pass_all=True, receipt=str(output), registered_sources=len(records), original_checkout_head=result['original_checkout_head']) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--plan', required=True) parser.add_argument('--out', required=True) args = parser.parse_args() print(json.dumps(capture(args.plan, args.out), sort_keys=True))