142 lines
7.8 KiB
Python
142 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Root-only native collection. Does not run regression or claim acceptance."""
|
|
import argparse
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
sys.dont_write_bytecode = True
|
|
HERE = Path(__file__).resolve().parent
|
|
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
|
|
CONDITIONS = ('mixed_construction', 'occupancy_change', 'actual_actor_positions',
|
|
'combined_rescue_logistics_ev', 'no_overlap_orphan_loss', 'unsolvable_preservation')
|
|
|
|
def module(name, path):
|
|
spec = importlib.util.spec_from_file_location(name, path)
|
|
obj = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(obj)
|
|
return obj
|
|
|
|
def absolute(value):
|
|
path = Path(value)
|
|
if not path.is_absolute():
|
|
raise argparse.ArgumentTypeError('absolute path required')
|
|
return path.resolve()
|
|
|
|
def collect(args):
|
|
root, out = args.project_root, args.output
|
|
r = module('p08_standard_runner', root/'tools/run_all_tests.py')
|
|
commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
|
|
r.require(commit == args.commit, 'actual final submitted commit required')
|
|
r.require(not out.exists(), 'fresh output; never overwrite earlier proof')
|
|
refs = r.source_refs(root)
|
|
outer = r.module('p08_owned_outer', r.SUPPORT/'owned_outer.py')
|
|
r.require(not outer.native.helper['live_engines'](), 'one native engine')
|
|
engine = shutil.which(args.godot)
|
|
r.require(engine is not None, 'native engine exists')
|
|
engine = str(Path(engine).resolve())
|
|
out.mkdir(parents=True)
|
|
stage = out/'stage'; stage.mkdir()
|
|
(out/'starters').mkdir(); (out/'bootstrap').mkdir()
|
|
for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items():
|
|
data = pin['text'].encode()
|
|
r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin')
|
|
(out/'bootstrap'/name).write_bytes(data)
|
|
for name, pin in refs.items():
|
|
target = stage/name; target.parent.mkdir(parents=True, exist_ok=True)
|
|
r.copy_source(Path(pin['path']), target)
|
|
r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name)
|
|
expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()}
|
|
selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180}
|
|
r.require(r.inventory(stage, stage=True) == expected, 'complete private source')
|
|
protected = r.default_player(root); public = (stage/'project.godot').read_bytes()
|
|
paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')]
|
|
r.write(out/'parse-paths.json', paths)
|
|
markers = r.module('p08_strict_markers', root/'tools/strict_test_markers.py')
|
|
contracts = markers.load_contracts(root/CONTRACT)
|
|
for role in ('writer','reader'):
|
|
name = 'tests/fixtures/p08-02/'+role+'.gd'
|
|
contracts[name] = {'test':name, 'template':'P08 mixed '+role+': <checks> checks, <failures> failures', 'required':True}
|
|
policy = r.read(r.SUPPORT/'suite-policy.json')
|
|
diagnostics = r.module('p08_diagnostics', r.SUPPORT/'diagnostic_gate.py')
|
|
owners = r.module('p08_owners', r.SUPPORT/'ordinary_proof_gate.py')
|
|
audio = r.module('p08_audio', r.SUPPORT/'audio_evidence_gate.py')
|
|
r.write(out/'runner-input.json', {'protocol':'p08-mixed-v1', 'commit':commit,
|
|
'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine),
|
|
'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':42588})
|
|
phases = []; previous = {}; cases = []; error = None
|
|
deadline = time.monotonic()+900
|
|
saved_env = {k:os.environ.get(k) for k in ('P08_PAIR_ROOT','P08_OPERATIONS','P08_CASE')}
|
|
def phase(label, test):
|
|
remaining = int(deadline-time.monotonic())
|
|
r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline')
|
|
selected['timeout'] = min(180,remaining)
|
|
phase_out = out/'runs'/label
|
|
phase_out.mkdir(parents=True)
|
|
(phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir()
|
|
for original in (out/'bootstrap').iterdir():
|
|
r.copy_source(original,phase_out/'bootstrap'/original.name)
|
|
r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy')
|
|
r.write(phase_out/'parse-paths.json',paths)
|
|
row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous,
|
|
policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio)
|
|
phases.append('runs/'+label+'/'+label+'-receipt.json')
|
|
r.require(not row['failed'], 'strict phase failed: '+label)
|
|
try:
|
|
phase('import', None); phase('parse', None)
|
|
for operations in ('off','on'):
|
|
for case in ('positive','refusal'):
|
|
name = operations+'-'+case; pair = out/'pairs'/name
|
|
os.environ.update(P08_PAIR_ROOT=str(pair), P08_OPERATIONS=operations, P08_CASE=case)
|
|
case_phases = []
|
|
preserved = {}
|
|
for role in ('writer','reader'):
|
|
label = name+'-'+role
|
|
test = 'tests/fixtures/p08-02/'+role+'.gd'
|
|
phase(label, test)
|
|
actual = r.inventory(pair)
|
|
r.require(all(actual.get(n) == pin for n,pin in preserved.items()), 'reader preserved every writer byte')
|
|
preserved = actual
|
|
case_phases.append(phases[-1])
|
|
cases.append({'id':name, 'operations':operations, 'kind':case,
|
|
'directory':'pairs/'+name, 'phases':case_phases, 'files':preserved})
|
|
except BaseException as caught:
|
|
error = type(caught).__name__+': '+str(caught)
|
|
finally:
|
|
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
|
|
for key,value in saved_env.items():
|
|
if value is None: os.environ.pop(key, None)
|
|
else: os.environ[key] = value
|
|
r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'protected root/profile unchanged')
|
|
r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors,
|
|
'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'})
|
|
inputs = {str(p.relative_to(root)):r.sha(p) for p in (root/'docs/spec/inputs/p08-02').iterdir() if p.is_file()}
|
|
# The collector is intentionally incomplete until Root supplies OS and regression artifacts.
|
|
r.write(out/'index.json', {'schema':'p08-evidence-v1', 'commit':commit,
|
|
'source_map':selected['map'], 'inputs':inputs, 'marker_contract_sha256':r.sha(root/CONTRACT),
|
|
'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P08-02.json').read_text().strip().encode()).hexdigest(),
|
|
'cases':cases, 'phases':phases, 'conditions':{c:[x['id'] for x in cases if c != 'unsolvable_preservation' or x['kind']=='refusal'] for c in CONDITIONS},
|
|
'artifacts':{str(p.relative_to(out)):{'bytes':p.stat().st_size,'sha256':r.sha(p)}
|
|
for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts},
|
|
'os_proof':None, 'regression':None,
|
|
'acceptance':'pending', 'collection_error':error})
|
|
return 1 if error or cleanup_errors else 0
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--project-root', type=absolute, required=True)
|
|
parser.add_argument('--output', type=absolute, required=True)
|
|
parser.add_argument('--commit', required=True)
|
|
parser.add_argument('--godot', default='godot')
|
|
parser.add_argument('--collect', action='store_true', required=True)
|
|
return collect(parser.parse_args(argv))
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|