Files
ARCADIA-Studio/harness/experiments/text-navigation-native-20260911/repair-save-native-v4/helper.py
T

282 lines
15 KiB
Python

"""Explicit native-v4 launch/checkpoint helper. Never operates GUI or compiles.
Launch/check/closed require the corresponding root-observed boundary. Merely
copying a snapshot never claims Apply, Save, recovery or model acceptance.
"""
import argparse
from datetime import datetime, timezone
import hashlib
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import zipfile
ROOT = Path(__file__).resolve().parents[2]
BASE = Path(__file__).resolve().parent
TRIAL = BASE / 'trial'
FIXTURES = BASE / 'fixtures'
ORIGINAL = ROOT / '.local/text-navigation-native-20260911-preparation'
GATES = ROOT / '.local/text-repair-save-20260911'
ZIP_SHA = '213fdb39ea7c3601b4baac674c3ab3ecc863ac44db1b092d922d3e24f26887b7'
SOURCE_COMMIT = '33c6d83e9ba9a5b1310b1b627d7c40affcbcf15c'
FIXTURE_MANIFEST_SHA = '7bccf622c8e2ff42f9e71ac8e5d08ea27b3493c9ebc689ce5f7fbac9aa63f851'
ORIGINAL_MANIFEST_SHA = 'edd4157fc31823f4494dee91862728b87f12ef51c49b8d1fd86a954bc9ecc1ed'
DLL_SHA = 'f45604a12e8bcd839fd86f667c1be1cc7c4f9da2b3c69ff381ac708f0e1ae7d4'
ORACLE_SHA = '967d9ba3c6fb3a7efb410833903ec94556ca2d48a67d5072c8bb160f40a00f9c'
RUNTIME = ROOT / '.local/toolchain/llvm-mingw-20260826-ucrt-x86_64/bin/libunwind.dll'
ORACLE = ROOT / '.local/staged-source-oracle-20260911/staged-source-authoring-contract.exe'
MODELS = ('valid', 'release1', 'ambiguous', 'missing-include', 'recovery')
JOURNAL = '.arcadia-save-project.arcadia.journal'
COMMANDS = {
'desktop-tests': ['cargo', 'test', '--locked', '--offline', '-p', 'oa-desktop', '--bin', 'arcadia-studio', '--', '--test-threads=1'],
'desktop-clippy': ['cargo', 'clippy', '--locked', '--offline', '-p', 'oa-desktop', '--all-targets', '--', '-D', 'warnings'],
'scoped-fmt': ['rustfmt', '--check', '--edition', '2024', '--config', 'skip_children=true', 'crates/oa-desktop/src/app.rs', 'crates/oa-desktop/src/document.rs', 'crates/oa-desktop/src/app/repair_save_state_tests.rs'],
'diff-check': ['git', 'diff', '--check'],
'gui-build': ['cargo', 'build', '--locked', '--offline', '-p', 'oa-desktop', '--bin', 'arcadia-studio', '--message-format=json-render-diagnostics'],
}
def now():
return datetime.now(timezone.utc).isoformat()
def ordinary(path):
path = Path(path).absolute()
for p in (path, *path.parents):
if p.exists():
assert not p.is_symlink() and not (getattr(p.lstat(), 'st_file_attributes', 0) & 0x400), str(p)
return path
def sha(path):
with ordinary(path).open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def load(path):
return json.loads(ordinary(path).read_text(encoding='utf-8'))
def record(path, value):
ordinary(path)
with path.open('x', encoding='utf-8', newline='\n') as out:
json.dump(value, out, ensure_ascii=False, indent=2)
out.write('\n')
def inventory(folder):
ordinary(folder)
result = {}
for p in folder.rglob('*'):
ordinary(p)
if p.is_file():
result[p.relative_to(folder).as_posix()] = sha(p)
assert len({p.casefold() for p in result}) == len(result)
return result
def copy_exact(source, destination):
before = inventory(source)
assert not destination.exists()
shutil.copytree(source, destination)
assert inventory(source) == inventory(destination) == before
return before
def git(*args):
return subprocess.check_output(['git', *args], cwd=ROOT)
def fixture_admission():
assert sha(ORIGINAL / 'fixture-manifest.json') == ORIGINAL_MANIFEST_SHA
for item in load(ORIGINAL / 'fixture-manifest.json'):
path = (ORIGINAL / item['path']).resolve()
assert path.is_relative_to(ORIGINAL)
assert sha(path) == item['sha256'] and path.stat().st_size == item['bytes']
assert sha(BASE / 'fixture-manifest.json') == FIXTURE_MANIFEST_SHA
frozen = load(BASE / 'fixture-manifest.json')
expected = {item['path']: item['sha256'] for item in frozen['files']}
assert len(expected) == len(frozen['files']) and inventory(FIXTURES) == expected
mapping = {'valid': 'valid-baseline', 'release1': 'release1-baseline', 'ambiguous': 'ambiguous-open', 'missing-include': 'missing-include-open'}
for destination, original in mapping.items():
assert inventory(FIXTURES / destination) == inventory(ORIGINAL / original)
after = FIXTURES / 'recovery-expected-after'
before = FIXTURES / 'recovery-expected-before'
assert inventory(after) == inventory(FIXTURES / 'valid')
a = (after / 'project.arcadia').read_bytes()
b = (before / 'project.arcadia').read_bytes()
title = '"Navigation — 救援"'.encode('utf-8')
old_title = '"Before interrupted save — 救援"'.encode('utf-8')
assert a.count(title) == 1 and b == a.replace(title, old_title, 1)
assert {p: h for p, h in inventory(before).items() if p != 'project.arcadia'} == {p: h for p, h in inventory(after).items() if p != 'project.arcadia'}
recovery = FIXTURES / 'recovery'
assert load(recovery / JOURNAL / 'journal.json') == {'version': 1, 'entry': 'project.arcadia', 'files': [{'path': 'project.arcadia', 'before': True, 'write': True}]}
assert (recovery / JOURNAL / '0.before').read_bytes() == b
assert (recovery / JOURNAL / '0.after').read_bytes() == a
assert {p: h for p, h in inventory(recovery).items() if not p.startswith(JOURNAL + '/')} == inventory(after)
return frozen
def gate_admission(source_commit):
assert source_commit == SOURCE_COMMIT
assert git('rev-parse', 'HEAD').decode().strip() == source_commit
manifest = load(GATES / 'inputs1.json')
assert manifest['archiveSha256'] == sha(GATES / 'inputs1.zip') == ZIP_SHA
entries = {e['path']: e for e in manifest['files']}
assert len(entries) == len(manifest['files']) == 500
selectors = ['crates', '.cargo', 'Cargo.toml', 'Cargo.lock', 'docs', 'examples', 'harness/fixtures']
assert not git('status', '--porcelain', '--', *selectors).strip(), 'Source must be committed before launch.'
paths = git('ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', *selectors).decode().split('\0')[:-1]
assert sorted(set(paths)) == sorted(entries)
with zipfile.ZipFile(GATES / 'inputs1.zip') as archive:
assert len(archive.namelist()) == len(entries) and set(archive.namelist()) == set(entries)
for name, item in entries.items():
path = (ROOT / name).resolve()
assert path.is_relative_to(ROOT)
raw = ordinary(path).read_bytes()
assert len(raw) == item['bytes'] and hashlib.sha256(raw).hexdigest() == item['sha256']
assert archive.read(name) == raw
complete = load(GATES / 'complete.json')
assert complete['archiveSha256'] == ZIP_SHA and complete['inputsUnchanged'] is True
assert complete['gates'] == list(COMMANDS) and complete['sourceHead'] == manifest['head']
receipts = {}
for name, command in COMMANDS.items():
receipt = load(GATES / (name + '-result.json'))
assert receipt['exitCode'] == 0 and receipt['inputsUnchanged'] is True and receipt['command'] == command
assert sha(GATES / (name + '-stdout.log')) == receipt['stdoutSha256']
assert sha(GATES / (name + '-stderr.log')) == receipt['stderrSha256']
receipts[name] = sha(GATES / (name + '-result.json'))
summaries = re.findall(r'test result: ok\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;', (GATES / 'desktop-tests-stdout.log').read_text(encoding='utf-8'))
assert summaries == [('755', '0', '1', '0', '0')]
build = load(GATES / 'gui-build-result.json')['binary']
artifact = build['artifact']
assert artifact['target']['name'] == 'arcadia-studio' and artifact['profile']['test'] is False
assert artifact['profile']['opt_level'] == '0' and artifact['profile']['debug_assertions'] is True
assert Path(artifact['executable']).resolve() == Path(build['path']).resolve()
assert Path(build['path']).resolve().is_relative_to(ROOT / '.local/target-gnullvm')
binary = GATES / 'gui-repair.exe'
assert sha(binary) == build['sha256'] and binary.stat().st_size == build['bytes']
assert sha(RUNTIME) == DLL_SHA and sha(ORACLE) == ORACLE_SHA
return binary, build, receipts
def environment():
parent = {k.upper(): v for k, v in os.environ.items()}
names = ('SystemRoot', 'SystemDrive', 'ProgramData', 'ALLUSERSPROFILE', 'PUBLIC')
standard = {name: parent[name.upper()] for name in names}
assert all(value and '%' not in value for value in standard.values())
system = standard['SystemRoot']
return {**standard, 'WINDIR': system, 'PATH': system + '\\System32;' + system,
'USERPROFILE': str(TRIAL / 'profile'), 'LOCALAPPDATA': str(TRIAL / 'profile/local'),
'APPDATA': str(TRIAL / 'profile/roaming'), 'TEMP': str(TRIAL / 'tmp'), 'TMP': str(TRIAL / 'tmp')}
def launch(args):
assert not TRIAL.exists(), 'A previous trial must remain untouched.'
frozen = fixture_admission()
binary, build, receipts = gate_admission(args.source_commit)
ordinary(BASE)
TRIAL.mkdir()
for name in ('bin', 'profile/local', 'profile/roaming', 'profile/Desktop', 'tmp', 'screenshots', 'checkpoints'):
(TRIAL / name).mkdir(parents=True)
for source, name, checksum in ((binary, 'arcadia-studio.exe', build['sha256']), (RUNTIME, 'libunwind.dll', DLL_SHA)):
with source.open('rb') as src, (TRIAL / 'bin' / name).open('xb') as dst:
shutil.copyfileobj(src, dst)
assert sha(source) == sha(TRIAL / 'bin' / name) == checksum
sources = {model: copy_exact(FIXTURES / model, TRIAL / model) for model in MODELS}
record(TRIAL / 'source-before.json', sources)
record(TRIAL / 'binary-before.json', inventory(TRIAL / 'bin'))
record(TRIAL / 'admission.json', {'atUtc': now(), 'sourceCommit': args.source_commit, 'inputArchiveSha256': ZIP_SHA,
'gateReceipts': receipts, 'completeReceiptSha256': sha(GATES / 'complete.json'), 'fixtureManifestSha256': sha(BASE / 'fixture-manifest.json'),
'originalFixtureManifestSha256': ORIGINAL_MANIFEST_SHA, 'binary': build, 'scope': 'Gate/fixture admission only, not native acceptance.'})
env = environment()
startup = subprocess.STARTUPINFO()
startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startup.wShowWindow = 0
with (TRIAL / 'stdout.log').open('xb') as out, (TRIAL / 'stderr.log').open('xb') as err:
process = subprocess.Popen([str(TRIAL / 'bin/arcadia-studio.exe')], cwd=TRIAL / 'bin', env=env, stdout=out, stderr=err, startupinfo=startup)
receipt = {'atUtc': now(), 'pid': process.pid, 'sourceCommit': args.source_commit, 'binarySha256': build['sha256'],
'guiBuildReceiptSha256': receipts['gui-build'], 'environment': env, 'modelEntries': {m: str(TRIAL / m / 'project.arcadia') for m in MODELS},
'scope': 'Explicitly launched identified Windows GUI; native results pending.'}
record(TRIAL / 'launch.json', receipt)
print(json.dumps(receipt, ensure_ascii=False, indent=2))
def checkpoint(args, check):
assert args.label and re.fullmatch('[a-z0-9][a-z0-9-]{0,79}', args.label)
if check:
assert args.after_visible_save, 'Oracle check requires root-observed successful Save.'
fixture_admission()
launch_receipt = load(TRIAL / 'launch.json')
before_bin = load(TRIAL / 'binary-before.json')
assert inventory(TRIAL / 'bin') == before_bin and before_bin['arcadia-studio.exe'] == launch_receipt['binarySha256']
folder = TRIAL / 'checkpoints' / args.label
assert not folder.exists()
folder.mkdir()
source = TRIAL / args.model
files = copy_exact(source, folder / 'sources')
journals = {p: h for p, h in files.items() if p.split('/')[0].startswith('.arcadia-save-')}
state = {'atUtc': now(), 'label': args.label, 'model': args.model, 'files': files,
'arcadiaFiles': sum(p.endswith('.arcadia') for p in files), 'journalFiles': journals,
'pendingJournalExists': (source / JOURNAL).exists(), 'binaryFilesUnchanged': True,
'scope': 'On-disk snapshot only; no accepted graph, Apply, Save or recovery success inferred.'}
record(folder / 'snapshot.json', state)
if not check:
print(json.dumps(state, ensure_ascii=False, indent=2))
return
assert not journals, 'Final saved oracle cannot bypass a retained transaction journal.'
assert sha(ORACLE) == ORACLE_SHA
expected = FIXTURES / ('release1' if args.model == 'release1' else 'recovery-expected-after' if args.model == 'recovery' else 'valid')
env = environment()
env['PATH'] = str(TRIAL / 'bin') + ';' + env['PATH']
command = [str(ORACLE), 'same', str(expected / 'project.arcadia'), str(folder / 'sources/project.arcadia')]
result = subprocess.run(command, capture_output=True, env=env)
with (folder / 'oracle.stdout.log').open('xb') as out:
out.write(result.stdout)
with (folder / 'oracle.stderr.log').open('xb') as err:
err.write(result.stderr)
receipt = {'atUtc': now(), 'command': command, 'exitCode': result.returncode, 'oracleSha256': ORACLE_SHA,
'afterVisibleSaveConfirmedByCaller': True, 'model': args.model, 'expected': str(expected), 'files': files,
'binaryFilesUnchanged': inventory(TRIAL / 'bin') == before_bin,
'scope': 'Actual saved oracle result; complete Project/identity/reachable and physical Arcadia equality only if exitCode is zero.'}
record(folder / 'oracle.json', receipt)
print(result.stdout.decode('utf-8', errors='replace'))
print(json.dumps(receipt, ensure_ascii=False, indent=2))
assert receipt['binaryFilesUnchanged']
if result.returncode:
print(result.stderr.decode('utf-8', errors='replace'))
raise SystemExit(result.returncode)
def closed(args):
assert args.observed_absent, 'Root must first verify the process/window is absent.'
before = load(TRIAL / 'binary-before.json')
after = inventory(TRIAL / 'bin')
record(TRIAL / 'binary-after-close.json', {'atUtc': now(), 'files': after, 'unchanged': before == after,
'absenceObservedByCaller': True, 'scope': 'Binary readback only, not a process/exit-code observation by this helper.'})
assert before == after
print(json.dumps({'unchanged': True, 'files': len(after)}))
if __name__ == '__main__':
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
parser = argparse.ArgumentParser()
parser.add_argument('action', choices=('launch', 'snapshot', 'check', 'closed'))
parser.add_argument('--source-commit')
parser.add_argument('--model', choices=MODELS, default='valid')
parser.add_argument('--label')
parser.add_argument('--after-visible-save', action='store_true')
parser.add_argument('--observed-absent', action='store_true')
arguments = parser.parse_args()
if arguments.action == 'launch':
launch(arguments)
elif arguments.action in ('snapshot', 'check'):
checkpoint(arguments, arguments.action == 'check')
else:
closed(arguments)