Files
StarPilot/roadscore/judging/export_handoff.py
T

104 lines
5.8 KiB
Python

"""Offline, private export adapter. Never changes integration manifests or attempts."""
import argparse
import json
import math
from pathlib import Path
from prepare import ASSETS, LETTERS, copy_with_reserve, digest, normalize, write_json
def export_handoff(manifest, output, materialize=False, reserve_gib=8):
manifest, output = Path(manifest), Path(output)
batch = json.loads(manifest.read_text())
rows = batch['routes']
if sorted(r['label'] for r in rows) != LETTERS or len({r['route'] for r in rows}) != len(rows):
raise ValueError('Require eleven unique routes with preserved A-K labels')
if not isinstance(batch.get('configuration'), dict) or not batch['configuration']:
raise ValueError('Inherited configuration is required')
if output.exists():
raise ValueError('Export must use a fresh directory; existing evidence is immutable')
# Validate all identities/ranges before creating any output. No inferred range edits.
for row in rows:
if normalize(row['route'])['route'] != row['route']:
raise ValueError('Noncanonical route')
selection = row['selection']
if selection['segments'] is not None or not selection.get('end_exclusive'):
raise ValueError('Resolve selection to an end-exclusive time interval before export')
start, end = selection['start_s'], selection['end_s']
if not math.isfinite(start) or start < 0 or (end is not None and (not math.isfinite(end) or end <= start)):
raise ValueError('Invalid time interval')
if not isinstance(row['seed'], int) or isinstance(row['seed'], bool) or not 0 <= row['seed'] < 2**32:
raise ValueError('Invalid preserved seed')
if 'pending' in row.get('range_resolution', '').lower():
raise ValueError('Range resolution is still pending')
output.mkdir(parents=True, mode=0o700)
submissions, ranges, plans = [], {}, []
for row in rows:
label, route, selection = row['label'], row['route'], row['selection']
cache = row.get('cache', {})
selected = cache.get('selected_segments', [])
needed = set(selected) | {0}
blockers = []
if not selected or not cache.get('extent_verified_against_remote_listing'):
blockers.append('Selected extent unverified')
if row.get('analysis_status') != 'characterized':
blockers.append('Offline analysis incomplete or blocked')
route_root = output / 'routes' / route
native_files, assets = [], set()
for item in cache.get('files', []):
segment = item['segment']
if segment not in needed:
continue
source = (manifest.parent / item['path']).resolve()
if not source.is_relative_to(manifest.parent.resolve()) or source.name not in ASSETS:
raise ValueError('Cache path is outside this batch or has an unsupported asset')
if not source.is_file() or source.stat().st_size != item['bytes'] or digest(source) != item['sha256']:
blockers.append('Missing or corrupt cached asset')
continue
relative = Path(route.split('/')[1] + '--' + str(segment)) / source.name
plans.append({'label': label, 'source': str(source), 'destination': str(Path('routes') / route / relative),
'bytes': item['bytes'], 'sha256': item['sha256']})
if materialize:
copy_with_reserve(source, route_root / relative, reserve_gib)
native_files.append({'path': str(relative), 'bytes': item['bytes'], 'sha256': item['sha256']})
assets.add((segment, source.name))
if any(not any((s, a) in assets for a in ASSETS[:3]) for s in needed):
blockers.append('Full logs missing, including route clock reference')
if any((s, 'fcamera.hevc') not in assets for s in selected):
blockers.append('Full front camera missing')
if not materialize:
blockers.append('Cache transfer plan only; assets not materialized')
ready = not blockers
if materialize:
route_root.mkdir(parents=True, exist_ok=True)
if ready:
write_json(route_root / 'cache_manifest.json', {'route': route, 'native_layout': True,
'files': native_files, 'complete_available_files': False, 'complete_selected_files': True,
'selection': selection, 'source': 'Verified private preparation export'})
else:
(route_root / '.acquiring').write_text('Preparation incomplete. See private export manifest.\n')
interval = {'start_seconds': selection['start_s'],
'duration_seconds': 86400 if selection['end_s'] is None else selection['end_s'] - selection['start_s'],
'range_resolution': 'resolved explicit preparation selection'}
submissions.append({'label': label, 'route': route, 'seed': row['seed'], **interval,
'preparation_ready': ready, 'preparation_blockers': sorted(set(blockers))})
ranges[label] = interval
result = {'schema': 'roadscore-judging-handoff-v1', 'generation_authorized': False,
'configuration': batch['configuration'], 'source_manifest_sha256': digest(manifest),
'submissions': submissions}
write_json(output / 'manifest.private.json', result)
write_json(output / 'judging_ranges.private.json', ranges)
write_json(output / 'cache_transfer.private.json', {'materialized': materialize, 'files': plans})
return result
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--manifest', type=Path, required=True)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--materialize', action='store_true', help='Copy verified assets into a NEW native-layout export')
parser.add_argument('--reserve-gib', type=float, default=8)
args = parser.parse_args()
result = export_handoff(args.manifest, args.output, args.materialize, args.reserve_gib)
print('Private export created;', sum(r['preparation_ready'] for r in result['submissions']), 'ready; no generation authorized.')