#!/usr/bin/env python3 """Archive the two stopped observer runs; verify launch sources and never overwrite.""" from collections import Counter from pathlib import Path import hashlib import json import re import shutil import sys HERE = Path(__file__).resolve().parent ROOT = HERE.parents[2] sys.path.insert(0, str(ROOT / 'tools')) from native_provenance import archive_source, encoded, read_launch from native_report import analyze RELEVANT = [ 'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c', 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverTargets.c', 'addons/core/Scripts/Game/ERR/00_Core/ERR_ObserverPlayerView.c', 'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c', 'addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c', 'addons/core/Prefabs/ERR/SpectatorCamera.et', 'addons/core/Configs/ERR/ObserverInput.conf', 'addons/core/Configs/ERR/Tests/LegacySites.conf', ] def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest() def copy(source, destination): destination.parent.mkdir(parents=True, exist_ok=True) with destination.open('xb') as stream: stream.write(source.read_bytes()) assert digest(source) == digest(destination) def normalized(errors): return Counter(re.sub(r'\s+', ' ', re.sub(r'^\d\d:\d\d:\d\d\.\d+\s+', '', line)) for line in errors) runs = {} for tag in ('a01', 'a02'): profile = Path('/tmp/erri-observer-orbit-run-' + tag) source = Path('/tmp/erri-observer-orbit-source-' + tag) sessions = list((profile / 'logs').glob('logs_*')) assert len(sessions) == 1 session = sessions[0] manifest, metadata, runner = read_launch(session) assert runner['cleanup_confirmed'] is True assert runner['external_addons_unchanged_after_run'] is True manifest_bytes, provenance, launched = archive_source(session, source) assert launched and metadata['source_root'] == str(source) destination = HERE / tag destination.mkdir(exist_ok=False) for name in ('console.log', 'script.log', 'error.log', 'runner.json', 'external-addons-inputs.json', 'headless-launch.log'): copy(session / name, destination / (name + '.txt')) for name in ('launch-source-sha256.json', 'launch-source.json'): copy(session / name, destination / name) (destination / 'source-sha256.json').write_bytes(manifest_bytes) (destination / 'source-provenance.json').write_bytes(encoded(provenance)) for capture in sorted([*session.glob('err-*.png'), *session.glob('err-*.bmp')]): copy(capture, destination / capture.name) if (profile / 'fixture-inputs.json').exists(): copy(profile / 'fixture-inputs.json', destination / 'fixture-inputs.json') runner_output = Path('/tmp/erri-observer-orbit-' + tag + '-runner.log') if runner_output.exists(): copy(runner_output, destination / 'runner-output.log.txt') source_matches = {} for name in RELEVANT: assert digest(source / name) == manifest[name] copy(source / name, destination / 'source' / name) source_matches[name] = { 'launch_sha256': manifest[name], 'archived_sha256': digest(destination / 'source' / name), 'current_repository_matches_launch': digest(ROOT / name) == manifest[name], } report = analyze((destination / 'console.log.txt').read_text(errors='replace'), str((destination / 'console.log.txt').relative_to(ROOT))) report['scope'] = 'Local Workbench; native action values and production frame output, then actual camera application. No physical mouse, remote streaming or multiplayer qualification.' (destination / 'report.json').write_bytes(encoded(report)) (destination / 'relevant-source-matches.json').write_bytes(encoded(source_matches)) runs[tag] = {'manifest': manifest, 'report': report, 'runner': runner} baseline = Path('/tmp/err-observer-orbit-before.c') copy(baseline, HERE / 'baseline/ERR_Observer.c') copy(ROOT / 'tests/test_observer_orbit.py', HERE / 'regression/test_observer_orbit.py') copy(Path('/tmp/erri-observer-orbit-check-unsandboxed.log'), HERE / 'full-check.log.txt') one, two = runs['a01'], runs['a02'] changed = sorted(path for path in set(one['manifest']) | set(two['manifest']) if one['manifest'].get(path) != two['manifest'].get(path)) assert changed == ['addons/core/Configs/ERR/Tests/LegacySites.conf'] before, after = normalized(one['report']['engine_errors']), normalized(two['report']['engine_errors']) review = { 'finding': 'Orbit input and pose calculation were inside the selected-target branch, freezing the fallback anchor when alone or all surviving players were unconscious.', 'fix': 'Keep yaw, pitch and zoom around the authorized fallback anchor without requiring a locally resolved target; preserve collision, input and player-view restrictions.', 'native_a01': {'result': one['runner']['result'], 'assertions': one['report']['assertions_passed'], 'reason': 'Historical hillside extraction test marker has no second US rescue landing footprint; mission preparation fails before camera regression.'}, 'native_a02': {'result': two['runner']['result'], 'assertions': two['report']['assertions_passed'], 'completions': two['report']['smoke_completions'], 'script_or_runtime_failure_count': len(two['report']['script_or_runtime_failures']), 'engine_error_count': len(two['report']['engine_errors'])}, 'a01_to_a02_changed_launch_sources': changed, 'a02_setup_only_arrangement': 'Move only the legacy fixture extraction marker from [7000,0,7500] to previously surveyed flat airport grounds [4776.55,0,11528.9]. No camera or casualty source changes between native runs.', 'a02_engine_error_signatures_absent_from_a01': sorted(set(after) - set(before)), 'a02_additional_occurrences_of_existing_engine_errors': dict(after - before), 'baseline_source': {'path': 'baseline/ERR_Observer.c', 'sha256': digest(baseline), 'qualification': 'Pre-fix source retained for host regression reproduction; neither a01 nor a02 is a native pre-fix run.'}, 'regression_snapshot': {'path': 'regression/test_observer_orbit.py', 'sha256': digest(ROOT / 'tests/test_observer_orbit.py'), 'qualification': 'Host regression copied at archive time, outside native launch manifest.'}, 'full_check_limitation': 'Full check stops at the existing ERRM_AtlasLeaf.et missing saved root ID. Exact failure output retained in full-check.log.txt.', 'cleanup_source_and_external_inputs': 'Both runners confirm cleanup and unchanged launched addon and external sources; source manifests and exact raw logs verified at archive time.', 'limitations': 'Synthetic native action/frame inputs, not physical mouse delivery. One local player and a physical stand-in; no dedicated multiplayer target streaming or live-server deployment.', } (HERE / 'review.json').write_bytes(encoded(review)) print(json.dumps(review, indent=2))