86 lines
4.2 KiB
Python
86 lines
4.2 KiB
Python
"""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))
|