262 lines
18 KiB
Python
262 lines
18 KiB
Python
"""Verify portable captured evidence, without Git, extraction or executing captured code."""
|
|
from datetime import datetime
|
|
import hashlib
|
|
import io
|
|
import json
|
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
import re
|
|
import zipfile
|
|
|
|
SOURCE = '4ffff0d9d94bd1875e6b316b84ac11e5c5c9657c'
|
|
FAILED = '836f9e14e7fae726e8dc16f3e1fb22b352e8e366'
|
|
PREFIX = ['cargo', 'test', '--locked', '--offline', '-p', 'oa-desktop', '--bin', 'arcadia-studio']
|
|
|
|
|
|
def require(condition, message):
|
|
if not condition:
|
|
raise RuntimeError(message)
|
|
|
|
|
|
def pin(data):
|
|
return {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
|
|
|
|
|
def safe_name(name):
|
|
path = PurePosixPath(name)
|
|
require(name and not path.is_absolute() and '..' not in path.parts and '\\' not in name,
|
|
'Unsafe member name: ' + name)
|
|
|
|
|
|
def archive(raw):
|
|
z = zipfile.ZipFile(io.BytesIO(raw))
|
|
names = z.namelist()
|
|
require(len(names) == len(set(names)), 'Duplicate ZIP entries')
|
|
for entry in z.infolist():
|
|
safe_name(entry.filename)
|
|
require(not entry.is_dir(), 'Unexpected directory entry')
|
|
require((entry.external_attr >> 16) & 0o170000 != 0o120000, 'Linked ZIP entry')
|
|
return z
|
|
|
|
|
|
def timestamp(value):
|
|
result = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
require(result.tzinfo is not None, 'Timestamp requires timezone')
|
|
return result
|
|
|
|
|
|
def names(log):
|
|
found = re.findall(r'^([^\r\n]+): test\r?$', log, re.M)
|
|
require(len(found) == len(set(found)), 'Duplicate listed test')
|
|
return found
|
|
|
|
|
|
def outcomes(log):
|
|
rows = re.findall(r'^test ([^\r\n]+) \.\.\. (ok|FAILED|ignored)(?:, ([^\r\n]*))?\r?$', log, re.M)
|
|
require(len(rows) == len({n for n, _, _ in rows}), 'Duplicate test outcome')
|
|
return {n: (status, reason) for n, status, reason in rows}
|
|
|
|
|
|
def summary(log, passed, ignored, filtered):
|
|
rows = re.findall(r'^test result: ok\. (\d+) passed; (\d+) failed; (\d+) ignored; (\d+) measured; (\d+) filtered out;', log, re.M)
|
|
require(rows == [(str(passed), '0', str(ignored), '0', str(filtered))], 'Unexpected test summary')
|
|
|
|
|
|
def main():
|
|
root = Path(__file__).resolve().parent
|
|
expected_outer = {'.gitattributes', 'README.md', 'verify.py', 'manifest.json', 'files.json', 'evidence.zip'}
|
|
require({p.name for p in root.iterdir()} == expected_outer, 'Exact outer package membership')
|
|
require(all(p.is_file() and not p.is_symlink() for p in root.iterdir()), 'Ordinary outer files required')
|
|
pins = json.loads((root / 'files.json').read_bytes())
|
|
require(set(pins) == expected_outer - {'files.json'}, 'Outer hash inventory')
|
|
for name, wanted in pins.items():
|
|
require(pin((root / name).read_bytes()) == wanted, 'Outer hash: ' + name)
|
|
manifest = json.loads((root / 'manifest.json').read_bytes())
|
|
require(manifest['source'] == SOURCE and manifest['failedSource'] == FAILED, 'Source identifiers')
|
|
z = archive((root / 'evidence.zip').read_bytes())
|
|
require(set(z.namelist()) == set(manifest['members']), 'Exact ZIP membership')
|
|
for name, wanted in manifest['members'].items():
|
|
require(pin(z.read(name)) == {k: wanted[k] for k in ('bytes', 'sha256')}, 'Member hash: ' + name)
|
|
read = z.read
|
|
doc = lambda name: json.loads(read(name))
|
|
text = lambda name: read(name).decode('utf-8')
|
|
|
|
# Source snapshots were compared with Git blobs at assembly time. Portable
|
|
# verification recomputes their bytes and recorded Git blob IDs, not Git ancestry.
|
|
inputs, source_zips = {}, {}
|
|
for commit in (FAILED, SOURCE):
|
|
setting = manifest['sourceSets'][commit]
|
|
inventory = doc(setting['manifest'])
|
|
require(inventory['source'] == commit, 'Inventory source')
|
|
rows = inventory['inputs']
|
|
require(len(rows) == len({r['path'] for r in rows}) == 711, 'Exact 711 input identities')
|
|
inputs[commit] = {r['path']: {k: r[k] for k in ('bytes', 'sha256')} for r in rows}
|
|
source_zip = archive(read(setting['archive']))
|
|
source_zips[commit] = source_zip
|
|
require(set(source_zip.namelist()) == set(inputs[commit]), 'Source ZIP scope')
|
|
blobs = doc(setting['gitBlobs'])
|
|
require(set(blobs) == set(inputs[commit]), 'Git blob inventory scope')
|
|
for path, wanted in inputs[commit].items():
|
|
content = source_zip.read(path)
|
|
require(pin(content) == wanted, 'Source bytes: ' + path)
|
|
git_hash = hashlib.sha1(b'blob ' + str(len(content)).encode() + b'\0' + content).hexdigest()
|
|
require(git_hash == blobs[path], 'Recorded Git blob: ' + path)
|
|
changed = sorted(p for p in inputs[SOURCE] if inputs[SOURCE][p] != inputs[FAILED].get(p))
|
|
require(set(inputs[SOURCE]) == set(inputs[FAILED]) and changed == [
|
|
'crates/oa-desktop/src/app/source_indentation_tests.rs',
|
|
'crates/oa-desktop/src/source_indentation/keyboard_tests.rs'], 'Two test-only compilation fixes')
|
|
|
|
old_names = names(text('prior/desktop-list-1.stdout.log'))
|
|
old_outcomes = outcomes(text('prior/desktop-1.stdout.log'))
|
|
require(len(old_names) == len(old_outcomes) == 1185 and set(old_names) == set(old_outcomes), 'Prior actual membership')
|
|
require(all(s in ('ok', 'ignored') for s, _ in old_outcomes.values()), 'Prior outcomes')
|
|
ignored = {n: reason for n, (status, reason) in old_outcomes.items() if status == 'ignored'}
|
|
require(len(ignored) == 6, 'Six prior ignored probes')
|
|
new_names = []
|
|
for path, prefix in [
|
|
('crates/oa-desktop/src/source_indentation/tests.rs', 'source_indentation::tests::'),
|
|
('crates/oa-desktop/src/source_indentation/keyboard_tests.rs', 'source_indentation::keyboard_tests::'),
|
|
('crates/oa-desktop/src/app/source_indentation_tests.rs', 'app::source_indentation_tests::'),
|
|
]:
|
|
code = source_zips[SOURCE].read(path).decode('utf-8')
|
|
new_names.extend(prefix + name for name in re.findall(r'#\[test\]\s*fn (\w+)\(', code))
|
|
require(len(new_names) == len(set(new_names)) == 9 and not set(new_names) & set(old_names), 'Nine source-derived additions')
|
|
listing = names(text('gates/list-4ffff0d.log'))
|
|
require(len(listing) == 1194 and set(listing) == set(old_names) | set(new_names), 'Retained suite plus nine additions')
|
|
require(re.findall(r'^(\d+) tests, (\d+) benchmarks\s*$', text('gates/list-4ffff0d.log'), re.M) == [('1194', '0')], 'Actual listing summary')
|
|
runner = pin(read('helpers/run-text-indentation-gate-20260922.py'))['sha256']
|
|
admitter = pin(read('helpers/admit-text-indentation-20260922.py'))['sha256']
|
|
artifact = None
|
|
environment = None
|
|
last_end = None
|
|
for stem, mode in [('list-836f9e14', 'failed'), ('list-4ffff0d', 'list'),
|
|
('indent-4ffff0d', 'focused'), ('full-4ffff0d', 'full'), ('strict-4ffff0d', 'strict')]:
|
|
result = doc('gates/' + stem + '.result.json')
|
|
log = text('gates/' + stem + '.log')
|
|
commit = FAILED if mode == 'failed' else SOURCE
|
|
require(result['source'] == result['head_after'] == commit and result['changed_inputs'] == [], 'Unchanged gate inputs: ' + stem)
|
|
require(result['launcher_sha256'] == runner, 'Gate launcher binding')
|
|
require(result['inputs'] == stem + '.inputs.json', 'Gate manifest name')
|
|
require(doc('gates/' + result['inputs']) == doc(manifest['sourceSets'][commit]['manifest']), 'Exact gate input inventory')
|
|
start, end = timestamp(result['start']), timestamp(result['end'])
|
|
require(start <= end and (last_end is None or last_end <= start), 'Closed ordered gate interval')
|
|
last_end = end
|
|
if environment is None:
|
|
environment = result['environment']
|
|
require(result['environment'] == environment, 'Same recorded gate environment')
|
|
command = (['cargo', 'clippy', '--locked', '--offline', '-p', 'oa-desktop', '--all-targets', '--', '-D', 'warnings'] if mode == 'strict'
|
|
else PREFIX + (['--', '--list'] if mode in ('failed', 'list') else
|
|
(['source_indentation'] if mode == 'focused' else []) + ['--', '--test-threads=1']))
|
|
require(result['command'] == command and result['exit'] == (101 if mode == 'failed' else 0), 'Gate command/outcome')
|
|
if mode in ('failed', 'strict'):
|
|
require(result['artifacts'] == [] and result['listing_members'] == [] and result['listing_count'] == 0, 'No executed/listed tests for this gate')
|
|
if mode == 'failed':
|
|
require(re.findall(r'^error\[(E\d+)\]:', log, re.M) == ['E0308', 'E0599'] and 'due to 2 previous errors' in log, 'Retained initial compilation failure')
|
|
require(not names(log) and not outcomes(log), 'Failed listing is not execution')
|
|
continue
|
|
if mode == 'strict':
|
|
require('Finished `dev` profile [unoptimized]' in log and not re.search(r'^\s*(warning|error)(\[|:)', log, re.M), 'Closed strict desktop all-targets gate')
|
|
continue
|
|
require(len(result['artifacts']) == 1, 'One recorded test binary')
|
|
artifact = result['artifacts'] if artifact is None else artifact
|
|
require(result['artifacts'] == artifact, 'Same recorded test binary')
|
|
emitted = re.findall(r'Running unittests [^\r\n]+ \(([^\r\n]+\.exe)\)', log)
|
|
require(len(emitted) == 1, 'One raw test executable')
|
|
observed = PureWindowsPath(emitted[0])
|
|
if not observed.is_absolute():
|
|
target = PureWindowsPath(environment['CARGO_TARGET_DIR'])
|
|
require(observed.parts[0] == target.name and '..' not in observed.parts, 'Relative Cargo target path')
|
|
observed = target.parent / observed
|
|
require(observed == PureWindowsPath(artifact[0]['path']), 'Raw test executable binding')
|
|
selected, rows = listing, None
|
|
if mode == 'list':
|
|
require(result['listing_members'] == listing and result['listing_count'] == 1194, 'Listing receipt membership')
|
|
else:
|
|
require(result['listing_members'] == [] and result['listing_count'] == 0, 'Runtime receipt is not a listing')
|
|
selected = [n for n in listing if 'source_indentation' in n] if mode == 'focused' else listing
|
|
actual = outcomes(log)
|
|
require(list(actual) == selected, 'Actual ordered executed membership')
|
|
for name, (status, reason) in actual.items():
|
|
require(status == ('ignored' if name in ignored else 'ok'), 'Actual result: ' + name)
|
|
require(reason == ignored.get(name, ''), 'Preserved ignored reason: ' + name)
|
|
nignored = len(set(selected) & set(ignored))
|
|
summary(log, len(selected) - nignored, nignored, 1194 - len(selected))
|
|
rows = [[n, actual[n][0]] for n in selected]
|
|
admission = doc('gates/' + stem + '.admission.json')
|
|
require(admission['source'] == SOURCE and admission['listed'] == 1194 and admission['new_names'] == new_names, 'Admission source-derived membership')
|
|
require(admission['selected'] == len(selected) and admission['outcomes'] == rows and admission['unchanged_inputs'] == 711, 'Admission actual outcomes')
|
|
require(admission['helper_sha256'] == admitter, 'Admission helper identity')
|
|
require(admission['records'] == {suffix: pin(read('gates/' + stem + suffix))['sha256'] for suffix in ('.result.json', '.inputs.json', '.log')}, 'Admission raw bindings')
|
|
|
|
build = doc('build/result.json')
|
|
require(build['source'] == build['head_after'] == SOURCE and build['exit'] == 0 and build['outcome'] == 'passed' and build['failure'] is None, 'Successful identified GUI build')
|
|
require(build['changed_inputs'] == [] and build['status_after'] == '' and len(build['artifacts']) == 1, 'Unchanged GUI build inputs')
|
|
require(timestamp(build['start']) <= timestamp(build['end']), 'Closed GUI build')
|
|
require(build['command'][1:] == ['build', '--locked', '--offline', '-p', 'oa-desktop', '--bin', 'arcadia-studio', '--message-format=json-render-diagnostics'], 'GUI build command')
|
|
for field, member in [('launcher', 'helpers/build-text-gui-20260922.py'), ('inputs', 'build/inputs.json'), ('environment', 'build/environment.json'), ('stdout', 'build/cargo.jsonl'), ('stderr', 'build/cargo.stderr.log')]:
|
|
require(build[field] == pin(read(member)), 'GUI build binding: ' + field)
|
|
require(doc('build/inputs.json') == doc(manifest['sourceSets'][SOURCE]['manifest']), 'GUI build same 711 inputs')
|
|
env = doc('build/environment.json')
|
|
require(env['launcher'] == build['launcher'], 'Build environment launcher')
|
|
cargo = [json.loads(line) for line in text('build/cargo.jsonl').splitlines() if line.strip()]
|
|
require([r['success'] for r in cargo if r.get('reason') == 'build-finished'] == [True], 'Cargo success record')
|
|
gui = build['artifacts'][0]
|
|
require(gui['cargo_artifact'] in cargo and gui['name'] == 'arcadia-studio', 'Actual Cargo GUI artifact')
|
|
require(PureWindowsPath(gui['origin']) == PureWindowsPath(gui['cargo_artifact']['executable']), 'GUI executable path binding')
|
|
require(not re.search(r'^\s*(warning|error)(\[|:)', text('build/cargo.stderr.log'), re.M), 'GUI build diagnostics')
|
|
|
|
baseline = None
|
|
native_summary = {}
|
|
for date, count, success in [('20260922', 29, False), ('20260923', 30, True)]:
|
|
prefix = 'native/' + date + '/'
|
|
prep, request, launch, close = [doc(prefix + name + '.json') for name in ('preparation', 'launch-request', 'launch', 'closure')]
|
|
require(all(r['source'] == SOURCE for r in (prep, request, launch, close)), 'Native source chain')
|
|
require(prep['build'] == request['build'] == pin(read('build/result.json')) and prep['build_name'] == 'gui-4ffff0d', 'Native build binding')
|
|
require(prep['gui'] == {k: gui[k] for k in ('bytes', 'sha256')}, 'Recorded GUI receipt identity')
|
|
require(request['preparation'] == pin(read(prefix + 'preparation.json')) and request['helper'] == pin(read('helpers/text-indentation-native-' + date + '.py')), 'Native launch helper/preparation')
|
|
require(len(request['command']) == 1 and PureWindowsPath(request['command'][0]).name == 'arcadia-studio.exe', 'Native launch target')
|
|
require(launch['pid'] == close['pid'] and close['exit'] == 0 and close['gui_unchanged'] is True and close['baseline_unchanged'] is True, 'Native closed receipt')
|
|
times = [timestamp(r['utc']) for r in (prep, request, launch, close)]
|
|
require(times == sorted(times) and timestamp(build['end']) <= times[0], 'Native chronological chain')
|
|
for stream in ('stdout', 'stderr'):
|
|
require(read(prefix + stream + '.log') == b'' and close[stream] == pin(b''), 'Empty native process logs')
|
|
original = read(prefix + 'baseline.arcadia')
|
|
require(prep['fixture'] == pin(original), 'Original fixture pin')
|
|
baseline = original if baseline is None else baseline
|
|
require(original == baseline, 'Both trials use identical original fixture')
|
|
expected = b''.join(b' ' + line for line in original.splitlines(keepends=True))
|
|
actual = read(prefix + 'source-work/project.arcadia')
|
|
require(close['saved'] == pin(actual) and close['expected'] == pin(expected), 'Saved and expected byte receipts')
|
|
require(close['saved_bytes_equal_expected'] is success and (actual == expected) is success, 'Distinct native outcome scopes')
|
|
require(actual == (expected if success else original), 'Exact restored/saved byte oracle')
|
|
require(len(original) == 525 and len(expected) == 577 and len(original.splitlines()) == 13, 'Native fixture physical lines')
|
|
require(original.count(b'\r\n') == 2 and original.count(b'\n') - original.count(b'\r\n') == 11, 'Mixed CRLF/LF baseline')
|
|
require('e\u0301'.encode() in actual and '\U0001f469\u200d\U0001f680'.encode() in actual, 'Combining and ZWJ bytes')
|
|
captures = manifest['native'][date]['captureMetadata']
|
|
require(len(captures) == count and captures == sorted(captures), 'Native observation membership')
|
|
require(set(captures) == {n for n in z.namelist() if n.startswith(prefix + 'evidence/') and n.endswith('.json')}, 'Exact capture metadata set')
|
|
images, capture_times = [], []
|
|
for path in captures:
|
|
frame = doc(path)
|
|
require(frame['source'] == SOURCE, 'Capture source')
|
|
capture_times.append(timestamp(frame['time']))
|
|
for image in frame['images']:
|
|
safe_name(image['file'])
|
|
require('/' not in image['file'] and image['width'] > 0 and image['height'] > 0, 'Capture image metadata')
|
|
name = prefix + 'evidence/' + image['file']
|
|
require(read(name).startswith(b'\xff\xd8'), 'Original JPEG bytes')
|
|
images.append(name)
|
|
require(capture_times == sorted(capture_times) and times[2] <= capture_times[0] <= capture_times[-1] <= times[3], 'Capture interval/order')
|
|
require(len(images) == len(set(images)) == (33 if not success else 41), 'Distinct original image membership')
|
|
require(set(images) == {n for n in z.namelist() if n.startswith(prefix + 'evidence/') and n.endswith('.jpg')}, 'No orphan original frames')
|
|
native_summary[date] = {'observations': count, 'images': len(images), 'exit': 0, 'saved_bytes': len(actual), 'equals_indented_expectation': success}
|
|
original = read('prior/native-input.arcadia').replace(b'Compact source workflow', b'Tab indentation workflow')
|
|
require(baseline == '// Tab check: e\u0301 / \U0001f469\u200d\U0001f680\r\n// first selection\n// second selection\r\n'.encode() + original, 'Fixture preparation oracle')
|
|
print(json.dumps({'source': SOURCE, 'source_inputs': 711, 'initial_listing_compile_errors': 2,
|
|
'listed': 1194, 'focused_passed': 9, 'full_passed': 1188, 'full_ignored': 6,
|
|
'strict_exit': 0, 'gui_build_exit': 0, 'native': native_summary,
|
|
'scope': 'Recorded evidence integrity/relationships, not runtime reproduction or release acceptance.'}))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|