226 lines
21 KiB
Python
226 lines
21 KiB
Python
"""Narrow one-launch Windows trial; prepare/launch require actual closed feab202 gates."""
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
import argparse, hashlib, json, os, re, subprocess, sys
|
|
|
|
def require(condition, reason):
|
|
if not condition:
|
|
raise RuntimeError(reason)
|
|
|
|
require(__debug__, 'Python -O is forbidden')
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
SOURCE = 'feab2020d8e5b6bfdb59c86ab4aee161617b8b6f'
|
|
PENDING_FINAL_SOURCE = False # Reviewed feab202 test-only correction; actual gate admission remains mandatory.
|
|
CONFIG = ROOT / '.local/rename-scroll-native-feab202.config.json'
|
|
def now(): return datetime.now(timezone.utc).isoformat()
|
|
def digest(raw): return {'bytes': len(raw), 'sha256': hashlib.sha256(raw).hexdigest()}
|
|
def save(path, value):
|
|
with path.open('x', encoding='utf-8', newline='\n') as stream:
|
|
stream.write(json.dumps(value, indent=2) + '\n')
|
|
def git(cwd, *args, data=None):
|
|
return subprocess.check_output(['git', '-c', 'core.longpaths=true', *args], cwd=cwd, input=data)
|
|
def files(directory):
|
|
require(directory.is_dir() and not directory.is_symlink(), 'Missing/nonordinary directory: ' + str(directory))
|
|
result = {}
|
|
for path in sorted(directory.rglob('*')):
|
|
require(not path.is_symlink() and not (getattr(path.lstat(), 'st_file_attributes', 0) & 0x400), 'Reparse fixture/stage path')
|
|
if path.is_file(): result[path.relative_to(directory).as_posix()] = digest(path.read_bytes())
|
|
return result
|
|
|
|
def admission():
|
|
require(not PENDING_FINAL_SOURCE, 'Pending final source admission')
|
|
watched = {}
|
|
def read(path):
|
|
raw = path.read_bytes()
|
|
require(path not in watched or watched[path] == raw, 'Input changed while read: ' + str(path))
|
|
watched[path] = raw
|
|
return raw
|
|
def value(path): return json.loads(read(path))
|
|
def check(item):
|
|
require(digest(read(ROOT / item['path'])) == {k: item[k] for k in ('bytes', 'sha256')}, 'Pinned input changed: ' + item['path'])
|
|
cfg = value(CONFIG)
|
|
require(cfg['source'] == SOURCE, 'Wrong source')
|
|
work, gates, prepared = [ROOT / cfg[k] for k in ('source_worktree', 'gate_directory', 'gate_preparation')]
|
|
for item in cfg['build_helpers'] + cfg['historical_provenance'] + cfg['old_failure'] + cfg['fixture'] + [cfg['runtime']]: check(item)
|
|
expected = value(prepared / 'expected.json')
|
|
require(expected['source'] == SOURCE and len(expected['listing_members']) == 1099, 'Wrong prepared listing')
|
|
require(git(work, 'rev-parse', 'HEAD').decode().strip() == SOURCE, 'Source worktree HEAD changed')
|
|
scopes = ['Cargo.toml', 'Cargo.lock', 'rust-toolchain.toml', '.cargo', 'build', 'crates', 'assets', 'docs', 'examples', 'harness/fixtures']
|
|
changed = git(work, 'diff', '--name-only', '44095f78c28b79d6d085863dc928adba8e24d955', SOURCE, '--', *scopes, 'scripts').decode().splitlines()
|
|
require(changed == expected['changed_inputs_from_357'] and len(changed) == 3 and all(p.startswith('crates/oa-desktop/') for p in changed), 'Source delta is not the three reviewed desktop files')
|
|
gate_specs = [('list','list',None), ('preview','focused','preview_scroll_tests'), ('rename','focused','app::source_rename::tests::'), ('navigation','focused','navigation_union'), ('source-navigation','focused','source_navigation::'), ('full','full',None), ('strict','clippy',None)]
|
|
manifests, test_artifact = [], None
|
|
for stem, mode, selector in gate_specs:
|
|
name = stem + '-feab202'
|
|
record = value(gates / (name + '.result.json'))
|
|
admitted = value(gates / (name + '.admission.json'))
|
|
require(record['source'] == record['head_after'] == admitted['source'] == SOURCE, 'Wrong gate source: ' + name)
|
|
require(type(record['exit']) is int and record['exit'] == 0 and record['changed_inputs'] == [] and record['status_after'] == '', 'Gate not closed clean PASS: ' + name)
|
|
require(datetime.fromisoformat(record['end']) >= datetime.fromisoformat(record['start']), 'Gate has no valid closure')
|
|
require(admitted['mode'] == mode and admitted['verified_inputs'] == 581 and datetime.fromisoformat(admitted['utc']) >= datetime.fromisoformat(record['end']), 'Wrong admission mode/count/time')
|
|
require(admitted['boundary'] == 'Desktop source gate only; native/package acceptance is separate.', 'Unexpected admission boundary')
|
|
require(record['launcher_sha256'] == digest(read(prepared / 'run-fixed.py'))['sha256'], 'Wrong gate runner')
|
|
require(admitted['helper_sha256'] == digest(read(prepared / 'admit-fixed.py'))['sha256'] and admitted['config_sha256'] == digest(read(prepared / 'expected.json'))['sha256'], 'Wrong admission helper/config')
|
|
for suffix, key in [('result.json','result_sha256'),('inputs.json','inputs_sha256'),('log','log_sha256')]:
|
|
require(admitted[key] == digest(read(gates / (name + '.' + suffix)))['sha256'], 'Admission raw-file mismatch')
|
|
require(record['inputs'] == name + '.inputs.json', 'Wrong input manifest path')
|
|
manifests.append(value(gates / record['inputs']))
|
|
log = read(gates / (name + '.log')).decode('utf-8')
|
|
prefix = ['cargo','test','--locked','-p','oa-desktop','--bin','arcadia-studio']
|
|
command = ['cargo','clippy','--locked','-p','oa-desktop','--all-targets','--','-D','warnings'] if mode == 'clippy' else prefix + (['--','--list'] if mode == 'list' else ([selector] if selector else []) + ['--','--test-threads=1'])
|
|
require(record['command'] == command, 'Wrong gate command')
|
|
names = [] if mode == 'clippy' else expected['focused_groups'][selector] if selector else expected['listing_members']
|
|
require(set(admitted['names']) == set(names) and len(admitted['names']) == len(names), 'Admission membership differs')
|
|
if mode == 'clippy':
|
|
require(record['artifacts'] == [] and admitted['artifact'] is None and 'Finished `dev` profile' in log, 'Strict gate malformed')
|
|
else:
|
|
require(len(record['artifacts']) == 1 and record['artifacts'][0] == admitted['artifact'], 'Wrong gate artifact')
|
|
artifact = record['artifacts'][0]
|
|
if test_artifact is None: test_artifact = artifact
|
|
require(artifact == test_artifact, 'Gate executable identities differ')
|
|
launches = re.findall(r'Running unittests [^\r\n]+ \(([^\r\n]+\.exe)\)', log)
|
|
require(len(launches) == 1 and (work / launches[0]).resolve() == Path(artifact['path']).resolve(), 'Raw executable launch differs')
|
|
if mode == 'list':
|
|
listed = re.findall(r'^([^\r\n]+): test\r?$', log, re.M)
|
|
require(listed == record['listing_members'] and len(listed) == len(set(listed)) == record['listing_count'] == 1099 and set(listed) == set(names), 'Raw listing differs')
|
|
else:
|
|
rows = re.findall(r'^test ([^\r\n]+) \.\.\. (ok|FAILED|ignored)(?:, [^\r\n]*)?\r?$', log, re.M)
|
|
ignored = expected['ignored'] if mode == 'full' else []
|
|
require(len(rows) == len(names) and set(n for n, _ in rows) == set(names), 'Runtime membership differs')
|
|
require(all(outcome == ('ignored' if n in ignored else 'ok') for n, outcome in rows), 'Runtime failure/ignore mismatch')
|
|
totals = re.findall(r'^test result: ok\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;', log, re.M)
|
|
require(totals == [(str(len(names)-len(ignored)), '0', str(len(ignored)), '0', str(1099-len(names)))], 'Runtime totals differ')
|
|
require(digest(read(Path(test_artifact['path']))) == {k:test_artifact[k] for k in ('bytes','sha256')}, 'Test executable changed')
|
|
gui = gates / 'gui-feab202'
|
|
build = value(gui / 'result.json'); environment = value(gui / 'environment.json')
|
|
require(build['source'] == build['head_after'] == SOURCE and type(build['exit']) is int and build['exit'] == 0 and build['outcome'] == 'passed' and build['failure'] is None and not build['changed_inputs'] and build['status_after'] == '', 'GUI build not closed PASS')
|
|
require(datetime.fromisoformat(build['end']) >= datetime.fromisoformat(build['start']), 'Invalid GUI closure')
|
|
require(Path(build['command'][0]).name.casefold().removesuffix('.exe') == 'cargo' and build['command'][0] == environment['tools']['cargo']['path'] and build['command'][1:] == ['build','--locked','--offline','-p','oa-desktop','--bin','arcadia-studio','--message-format=json-render-diagnostics'], 'Wrong GUI command')
|
|
require(build['launcher'] == environment['launcher'] == digest(read(prepared / 'build-gui.py')), 'Wrong GUI producer')
|
|
for key, filename in [('inputs','inputs.json'),('environment','environment.json'),('stdout','cargo.jsonl'),('stderr','cargo.stderr.log')]: require(build[key] == digest(read(gui / filename)), 'GUI raw binding mismatch')
|
|
require('host: x86_64-pc-windows-gnullvm' in environment['tools']['rustc']['version'], 'Wrong target host')
|
|
historical_env = value(ROOT / '.local/navigation-palette-gates-20260913/artifacts-44095f7/environment.json')
|
|
require(environment['tools']['clang'] == historical_env['tools']['clang'], 'Runtime origin linker tool differs from pinned baseline')
|
|
runtime_origin = Path(environment['tools']['clang']['path']).parent / 'libunwind.dll'
|
|
require(digest(read(runtime_origin)) == digest(read(ROOT / cfg['runtime']['path'])), 'Runtime bytes differ from pinned toolchain/runtime')
|
|
messages = [json.loads(line) for line in read(gui / 'cargo.jsonl').splitlines()]
|
|
require([m for m in messages if m.get('reason') == 'build-finished'] == [{'reason':'build-finished','success':True}], 'GUI Cargo completion differs')
|
|
require(len(build['artifacts']) == 1, 'Expected only GUI artifact')
|
|
artifact = build['artifacts'][0]; meta = artifact['cargo_artifact']
|
|
require(artifact['name'] == meta['target']['name'] == 'arcadia-studio' and meta in messages and meta['target']['kind'] == ['bin'] and Path(meta['manifest_path']).resolve() == (work / 'crates/oa-desktop/Cargo.toml').resolve(), 'Wrong GUI artifact source')
|
|
require(meta['profile'] == {'opt_level':'0','debuginfo':0,'debug_assertions':True,'overflow_checks':True,'test':False}, 'Unexpected GUI profile')
|
|
require(Path(artifact['preserved']).resolve() == (gui / 'arcadia-studio.exe').resolve() and Path(artifact['origin']).resolve() == Path(meta['executable']).resolve(), 'Unexpected GUI paths')
|
|
executable = read(gui / 'arcadia-studio.exe')
|
|
require(digest(executable) == {k:artifact[k] for k in ('bytes','sha256')}, 'Preserved GUI bytes differ')
|
|
require(executable[:2] == b'MZ' and len(executable) >= 64, 'Not PE')
|
|
pe = int.from_bytes(executable[60:64], 'little')
|
|
require(executable[pe:pe+6] == b'PE\0\0\x64\x86', 'Not x64 PE')
|
|
manifests.append(value(gui / 'inputs.json'))
|
|
paths = git(work, 'ls-tree','-r','--name-only',SOURCE,*scopes,'scripts').decode().splitlines()
|
|
require(len(paths) == 615 and [i['path'] for i in manifests[-1]['inputs']] == paths, 'Wrong GUI source scope')
|
|
expected_gate_paths = git(work, 'ls-tree','-r','--name-only',SOURCE,*scopes).decode().splitlines()
|
|
require(len(expected_gate_paths) == 581, 'Wrong gate scope')
|
|
for manifest in manifests[:-1]: require(manifest['source'] == SOURCE and [i['path'] for i in manifest['inputs']] == expected_gate_paths and manifest == manifests[0], 'Gate input maps differ')
|
|
require(manifests[-1]['source'] == SOURCE, 'Wrong GUI manifest source')
|
|
blobs = git(work, 'cat-file','--batch',data=''.join(f'{SOURCE}:{p}\n' for p in paths).encode()); offset=0
|
|
entries = {i['path']:i for i in manifests[-1]['inputs']}
|
|
for path in paths:
|
|
end=blobs.index(b'\n',offset); _,kind,size=blobs[offset:end].split(); size=int(size); raw=blobs[end+1:end+1+size]
|
|
require(blobs[end+1+size:end+2+size] == b'\n', 'Missing Git blob terminator: '+path)
|
|
offset=end+size+2
|
|
require(kind == b'blob' and digest(raw) == {k:entries[path][k] for k in ('bytes','sha256')} and read(work/path) == raw, 'Source bytes differ: '+path)
|
|
require(offset == len(blobs) and all(entries[i['path']] == i for i in manifests[0]['inputs']), 'Gate/GUI source disagreement')
|
|
require(not git(work,'status','--porcelain','--untracked-files=all','--',*scopes,'scripts'), 'Source worktree dirty')
|
|
baseline = {Path(i['path']).relative_to(cfg['fixture_root']).as_posix():{k:i[k] for k in ('bytes','sha256')} for i in cfg['fixture']}
|
|
require(files(ROOT/cfg['fixture_root']) == baseline, 'Original fixture membership changed')
|
|
require(all(path.read_bytes() == raw for path,raw in watched.items()), 'Admission inputs changed before completion')
|
|
attestation = {'source':SOURCE,'config':digest(read(CONFIG)),'helper':digest(Path(__file__).read_bytes()),'records':{str(p.relative_to(ROOT)):digest(raw) for p,raw in watched.items() if p.is_relative_to(ROOT) and not p.name.endswith('.exe') and not p.is_relative_to(work)},'gui':digest(executable),'runtime':digest(read(runtime_origin)),'baseline':baseline,'scope':'Closed source/build admission only; native outcome remains manual. No semantic oracle reuse.'}
|
|
return cfg, attestation, executable, read(runtime_origin)
|
|
|
|
def capture_checkpoint_files(work, snapshot):
|
|
# Record every observed map, including failures; never describe an unchecked copy.
|
|
def observe(directory):
|
|
try: return files(directory), None
|
|
except Exception as error: return None, repr(error)
|
|
before, before_error = observe(work)
|
|
copy_error = None
|
|
snapshot.mkdir()
|
|
if before is not None:
|
|
try:
|
|
for name in before:
|
|
path=snapshot/name;path.parent.mkdir(parents=True,exist_ok=True)
|
|
with path.open('xb') as stream: stream.write((work/name).read_bytes())
|
|
except Exception as error:
|
|
copy_error=repr(error)
|
|
copied, snapshot_error = observe(snapshot)
|
|
after, after_error = observe(work)
|
|
consistent = before is not None and before == copied == after and not any((before_error,copy_error,snapshot_error,after_error))
|
|
return {'before':before,'snapshot':copied,'after':after,'before_error':before_error,
|
|
'copy_error':copy_error,'snapshot_error':snapshot_error,'after_error':after_error,
|
|
'copy_consistent':consistent}
|
|
|
|
def checkpoint(trial, stage, label):
|
|
require(re.fullmatch(r'[a-z0-9-]+',label) is not None, 'Invalid checkpoint label')
|
|
require((trial/'launch.json').is_file(), 'No owned launch')
|
|
raw=(trial/'preparation.json').read_bytes();preparation=json.loads(raw);launch=json.loads((trial/'launch.json').read_bytes())
|
|
require(raw==(stage/'manifest.json').read_bytes() and digest(raw)==launch['manifest'], 'Preparation/launch binding differs')
|
|
require(preparation['source']==launch['source']==SOURCE and launch['helper']==digest(Path(__file__).read_bytes()) and launch['config']==digest(CONFIG.read_bytes()), 'Checkpoint source/helper/config changed')
|
|
destination=trial/'checkpoints'/label
|
|
destination.mkdir(parents=True,exist_ok=False)
|
|
capture=capture_checkpoint_files(trial/'source-work',destination/'snapshot')
|
|
verification_error=None
|
|
try:
|
|
baseline_unchanged=files(trial/'source-baseline')==preparation['baseline']
|
|
stage_unchanged=stage_payload(stage)==preparation['payloads']
|
|
except Exception as error:
|
|
baseline_unchanged=stage_unchanged=False;verification_error=repr(error)
|
|
unchanged=capture['copy_consistent'] and capture['before']==preparation['baseline'] and baseline_unchanged
|
|
result={'utc':now(),'source':SOURCE,'label':label,**capture,'expected':preparation['baseline'],'baseline_unchanged':baseline_unchanged,'verification_error':verification_error,'exact_disk_bytes_equal':unchanged,'stage_unchanged':stage_unchanged,'scope':'Disk membership and complete-byte equality only; does not assert transient model/history or UI acceptance.'}
|
|
save(destination/'result.json',result);print(json.dumps(result),flush=True)
|
|
require(unchanged and result['stage_unchanged'], 'Checkpoint byte mismatch retained')
|
|
|
|
def stage_payload(stage):
|
|
return {k:v for k,v in files(stage).items() if k!='manifest.json'}
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('mode',choices=['check','prepare','launch','checkpoint']);parser.add_argument('--label');args=parser.parse_args()
|
|
cfg=json.loads(CONFIG.read_bytes());trial=ROOT/cfg['trial'];stage=ROOT/cfg['stage']
|
|
require(not PENDING_FINAL_SOURCE, 'Pending final source admission')
|
|
require(trial.parent == ROOT/'.local/native-qa' and stage.parent == trial.parent and trial != stage, 'Unexpected output containment')
|
|
if args.mode=='checkpoint': checkpoint(trial,stage,args.label or '');return
|
|
cfg,attestation,executable,runtime=admission()
|
|
if args.mode=='check': print(json.dumps(attestation));return
|
|
if args.mode=='prepare':
|
|
require(not trial.exists() and not stage.exists(),'Fresh trial and stage required');stage.mkdir();trial.mkdir()
|
|
for name,raw in [('arcadia-studio.exe',executable),('libunwind.dll',runtime)]:
|
|
with (stage/name).open('xb') as stream:stream.write(raw)
|
|
for name in ('source-baseline','source-work'):
|
|
for rel in attestation['baseline']:
|
|
dest=trial/name/rel;dest.parent.mkdir(parents=True,exist_ok=True);dest.write_bytes((ROOT/cfg['fixture_root']/rel).read_bytes())
|
|
require(files(trial/name)==attestation['baseline'],'Fixture copy differs')
|
|
payloads={'arcadia-studio.exe':attestation['gui'],'libunwind.dll':attestation['runtime']}
|
|
require(stage_payload(stage)==payloads,'Stage copy differs')
|
|
preparation={**attestation,'created_utc':now(),'payloads':payloads};save(stage/'manifest.json',preparation);save(trial/'preparation.json',preparation);print(json.dumps({'prepared':str(trial),'stage':str(stage),'scope':'Not launched'}));return
|
|
manifest=(stage/'manifest.json').read_bytes();preparation=json.loads(manifest)
|
|
require((trial/'preparation.json').read_bytes()==manifest and all(preparation[k]==v for k,v in attestation.items()),'Stage/config/gates changed')
|
|
require(stage_payload(stage)==preparation['payloads'] and files(trial/'source-baseline')==files(trial/'source-work')==preparation['baseline'],'Stage or fixture changed')
|
|
for name in ('launch-request.json','launch.json','closure.json','supervisor-error.json','stdout.log','stderr.log','profile','temp','working-directory'): require(not (trial/name).exists(),'Trial already attempted: '+name)
|
|
profile=trial/'profile';temp=trial/'temp';cwd=trial/'working-directory'
|
|
for path in (profile/'AppData/Local',profile/'AppData/Roaming',temp,cwd):path.mkdir(parents=True)
|
|
windows=Path(os.environ['SystemRoot']);env={k:os.environ[k] for k in ('SystemRoot','WINDIR','SystemDrive','ProgramData','ALLUSERSPROFILE','PUBLIC','COMSPEC','PATHEXT','NUMBER_OF_PROCESSORS','PROCESSOR_ARCHITECTURE','OS') if k in os.environ}
|
|
env.update(PATH=os.pathsep.join(map(str,(stage,windows/'System32',windows))),USERPROFILE=str(profile),HOMEDRIVE=profile.drive,HOMEPATH=str(profile)[len(profile.drive):],LOCALAPPDATA=str(profile/'AppData/Local'),APPDATA=str(profile/'AppData/Roaming'),TEMP=str(temp),TMP=str(temp))
|
|
command=[str(stage/'arcadia-studio.exe')];request={'utc':now(),'source':SOURCE,'command':command,'cwd':str(cwd),'environment':env,'manifest':digest(manifest),'config':attestation['config'],'helper':attestation['helper']};save(trial/'launch-request.json',request)
|
|
process=None
|
|
try:
|
|
with (trial/'stdout.log').open('xb') as out,(trial/'stderr.log').open('xb') as err:
|
|
process=subprocess.Popen(command,cwd=cwd,env=env,stdin=subprocess.DEVNULL,stdout=out,stderr=err,creationflags=subprocess.CREATE_NO_WINDOW)
|
|
save(trial/'launch.json',{**request,'pid':process.pid,'spawned_utc':now()});print(json.dumps({'pid':process.pid,'trial':str(trial)}),flush=True);code=process.wait()
|
|
unchanged=stage_payload(stage)==preparation['payloads'] and (stage/'manifest.json').read_bytes()==manifest
|
|
closure={'utc':now(),'source':SOURCE,'pid':process.pid,'exit':code,'stage_unchanged':unchanged,'stdout':digest((trial/'stdout.log').read_bytes()),'stderr':digest((trial/'stderr.log').read_bytes()),'exact_disk_bytes_equal':files(trial/'source-work')==files(trial/'source-baseline')==preparation['baseline'],'scope':'Observed owned process exit and disk-byte preservation only; native acceptance requires separate evidence review.'}
|
|
save(trial/'closure.json',closure);print(json.dumps(closure),flush=True)
|
|
require(code==0 and unchanged and closure['exact_disk_bytes_equal'],'Abnormal exit or changed stage/fixture; receipt retained')
|
|
except (Exception,KeyboardInterrupt) as error:
|
|
save(trial/'supervisor-error.json',{'utc':now(),'error':repr(error),'pid':None if process is None else process.pid,'observed_exit':None if process is None else process.poll()});raise
|
|
|
|
if __name__=='__main__': main()
|