296 lines
19 KiB
Python
296 lines
19 KiB
Python
#!/usr/bin/env python3
|
|
"""Read-only artifact integrity gate. Success NEVER accepts original P08-02."""
|
|
import argparse
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
sys.dont_write_bytecode = True
|
|
HERE = Path(__file__).resolve().parent
|
|
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
|
|
SPEC_SHA = '19f02bb7912a4d7354e565186f02366fc1d4dd698468d26142e8204fd14ebfb0'
|
|
CONDITIONS = ('mixed_construction','occupancy_change','actual_actor_positions',
|
|
'combined_rescue_logistics_ev','no_overlap_orphan_loss','unsolvable_preservation')
|
|
MEMBERS = ('world.bin','manifest.json','thumbnail.png','journal.log')
|
|
CASES = ('off-positive','off-refusal','on-positive','on-refusal')
|
|
LABELS = ['checkpoint'] + ['minute-%03d' % n for n in range(1,161)] + ['events']
|
|
|
|
def sha(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
def load(path): return json.loads(Path(path).read_bytes())
|
|
def module(name, path):
|
|
spec = importlib.util.spec_from_file_location(name,path)
|
|
obj = importlib.util.module_from_spec(spec); spec.loader.exec_module(obj); return obj
|
|
|
|
def decode(node):
|
|
"""Only decode for semantic checks; whole typed nodes remain comparison authority."""
|
|
t = node['type']
|
|
if t == 'Dictionary':
|
|
pairs = [(decode(k),decode(v)) for k,v in node['entries']]
|
|
keys = [json.dumps(k,sort_keys=True) for k,v in pairs]
|
|
if len(keys) != len(set(keys)): raise ValueError('duplicate typed dictionary key')
|
|
return {k:v for k,v in pairs}
|
|
if t == 'Array': return [decode(n) for n in node['items']]
|
|
text = node['text']
|
|
if t in ('String','StringName'): return json.loads(text.removeprefix('&'))
|
|
if t == 'int': return int(text)
|
|
if t == 'float': return float(text)
|
|
if t == 'bool':
|
|
if text not in ('true','false'): raise ValueError('invalid bool')
|
|
return text == 'true'
|
|
if t == 'Nil': return None
|
|
if t in ('Vector2i','Vector3i','Vector2','Vector3'):
|
|
return [float(x.strip()) if '.' in x else int(x.strip()) for x in text.strip('()').split(',')]
|
|
# Preserve unfamiliar scalar types verbatim; never silently drop them.
|
|
return (t,text)
|
|
|
|
def indexed(rows, key):
|
|
result = {row[key]:row for row in rows}
|
|
if len(result) != len(rows): raise ValueError('duplicate '+key)
|
|
return result
|
|
|
|
def semantics(snapshot, identity, initial=False):
|
|
"""Independent ID/cell/reference checks on actual serialized Runtime state."""
|
|
r = snapshot['systems']['runtime']
|
|
guests = indexed(r['guests']['guests'],'guest_id')
|
|
staff = indexed(r['staff']['staff'],'staff_id')
|
|
if set(guests) & set(staff): raise ValueError('duplicate actor across domains')
|
|
jobs = indexed(r['construction']['jobs'],'job_id')
|
|
facilities = indexed(r['construction']['facilities'],'instance_id')
|
|
operational = indexed(r['facilities']['instances'],'instance_id')
|
|
owners = {k:v['occupied_cells'] for k,v in facilities.items()}
|
|
owners.update({k:v['reserved_cells'] for k,v in jobs.items() if v['state'] not in ('COMPLETED','CANCELLED')})
|
|
occupied = indexed(r['world']['occupancies'],'entity_id')
|
|
cells = set()
|
|
for owner,row in occupied.items():
|
|
actual = {tuple(c) for c in row['cells']}
|
|
if len(actual) != len(row['cells']) or cells & actual: raise ValueError('overlap occupancy')
|
|
cells |= actual
|
|
if owner not in owners or actual != {tuple(c) for c in owners[owner]}: raise ValueError('orphan occupancy/ownership')
|
|
if set(owners) != set(occupied): raise ValueError('lost occupancy owner')
|
|
for fid,facility in operational.items():
|
|
if fid not in facilities or facility['occupied_cells'] != facilities[fid]['occupied_cells']:
|
|
raise ValueError('operational/construction ownership mismatch')
|
|
for jid in identity['jobs']:
|
|
if jid not in jobs: raise ValueError('lost paid job')
|
|
if initial and (jobs[jid]['progress'] <= 0 or jobs[jid]['state'] != 'BUILDING' or len(jobs[jid]['reserved_cells']) != 6):
|
|
raise ValueError('missing paid old-size progress/reservation')
|
|
if len(identity['jobs']) < 2 or identity['dorm'] not in facilities: raise ValueError('mixed jobs/completed facility missing')
|
|
tasks = indexed(r['staff']['tasks'],'task_id')
|
|
if 'patient' not in staff or 'carrier' not in staff: raise ValueError('lost patient/carrier')
|
|
rescue = tasks[identity['rescue_task']]
|
|
if rescue['target_entity_id'] != 'patient' or rescue['rescue_facility_id'] not in facilities:
|
|
raise ValueError('rescue patient/destination reference')
|
|
if initial and (rescue['state'] != 'MOVING' or rescue['assigned_staff_id'] != 'carrier' or staff['patient']['health'] != 0):
|
|
raise ValueError('rescue is not actually in flight')
|
|
inventories = indexed(r['inventory']['inventories'],'owner_inventory_id')
|
|
requests = indexed(r['logistics_requests'],'request_id')
|
|
reservations = indexed(r['inventory']['reservations'],'reservation_id')
|
|
if not identity['logistics_requests']: raise ValueError('missing original food request')
|
|
for rid in identity['logistics_requests']:
|
|
req = requests[rid]
|
|
if req['source_inventory_id'] not in inventories or req['destination_inventory_id'] not in inventories:
|
|
raise ValueError('orphan cargo endpoint')
|
|
if initial:
|
|
carrier = staff[req['assigned_carrier_id']]
|
|
cargo = inventories[carrier['carried_inventory_id']]
|
|
if not cargo['stacks'] or not req['reservation_ids'] or any(x not in reservations for x in req['reservation_ids']):
|
|
raise ValueError('lost cargo/reservation')
|
|
trips = indexed(r['elevators']['trips'],'request_id')
|
|
actors = set(guests) | set(staff)
|
|
for trip in trips.values():
|
|
actor = trip['actor_id']
|
|
if actor not in actors and actor.removeprefix('guest:').removeprefix('staff:') not in actors:
|
|
raise ValueError('orphan EV actor')
|
|
if initial:
|
|
if len(identity['ev_requests']) != 5 or not set(identity['ev_requests']) <= set(trips): raise ValueError('lost EV request')
|
|
if sum(len(c['boarded']) for c in r['elevators']['shafts']) != 4: raise ValueError('EV passengers/queue absent')
|
|
if 'p08.walker' not in guests or not r['movement_credits']: raise ValueError('mid-route guest/credit absent')
|
|
return r
|
|
|
|
def typed_differences(a, b, path=()):
|
|
if a == b: return []
|
|
if a.get('type') != b.get('type'): return [path]
|
|
if a['type'] == 'Dictionary':
|
|
aa = {str(decode(k)):(k,v) for k,v in a['entries']}; bb = {str(decode(k)):(k,v) for k,v in b['entries']}
|
|
if aa.keys() != bb.keys(): return [path]
|
|
differences = []
|
|
for k in aa:
|
|
if aa[k][0] != bb[k][0]: differences.append(path+(k,'@key-type'))
|
|
differences += typed_differences(aa[k][1],bb[k][1],path+(k,))
|
|
return differences
|
|
if a['type'] == 'Array' and a.get('element_type') == b.get('element_type') and a.get('element_class') == b.get('element_class'):
|
|
if len(a['items']) != len(b['items']): return [path]
|
|
return [p for i,(x,y) in enumerate(zip(a['items'],b['items'])) for p in typed_differences(x,y,path+(str(i),))]
|
|
return [path]
|
|
|
|
# Explicit geometry changes only. No actor, economy, request, event or route wildcard.
|
|
# This fixture deliberately places actors away from removed cells: NO route changes approved.
|
|
GEOMETRY = [
|
|
r'systems/runtime/world/occupancies/\d+/cells(?:/.*)?',
|
|
r'systems/runtime/world/dirty_cells(?:/.*)?',
|
|
r'systems/runtime/world/revisions/(world|occupancy)',
|
|
r'systems/runtime/construction/jobs/\d+/reserved_cells(?:/.*)?',
|
|
r'systems/runtime/construction/facilities/\d+/occupied_cells(?:/.*)?',
|
|
r'systems/runtime/facilities/instances/\d+/occupied_cells(?:/.*)?',
|
|
]
|
|
|
|
def check(project_root, evidence_dir):
|
|
root, evidence = Path(project_root).resolve(), Path(evidence_dir).resolve()
|
|
deficits = []
|
|
def need(ok, reason):
|
|
if not ok: deficits.append(reason)
|
|
def guarded(label, fn):
|
|
try: return fn()
|
|
except (KeyError,ValueError,TypeError,OSError,IndexError,AttributeError) as exc:
|
|
deficits.append(label+': '+str(exc)); return None
|
|
index = guarded('index',lambda:load(evidence/'index.json'))
|
|
if not isinstance(index,dict): return deficits or ['index is not an object']
|
|
def artifact(name):
|
|
if not isinstance(name,str) or Path(name).is_absolute() or '..' in Path(name).parts: raise ValueError('unsafe artifact path')
|
|
path = evidence/name
|
|
if not path.is_file() or any(p.is_symlink() for p in [path,*path.parents] if p != evidence.parent): raise ValueError('missing/aliased artifact '+name)
|
|
pin = index['artifacts'][name]
|
|
if pin != {'bytes':path.stat().st_size,'sha256':sha(path)}: raise ValueError('artifact hash '+name)
|
|
return path
|
|
def document(name): return load(artifact(name))
|
|
need(index.get('schema') == 'p08-evidence-v1','schema')
|
|
need(index.get('acceptance') == 'pending','integrity is not unit acceptance')
|
|
need(index.get('collection_error') is None,'collection failure')
|
|
def source_gate():
|
|
runner = module('p08_readonly_source',root/'tools/run_all_tests.py')
|
|
actual = {n:p['sha256'] for n,p in runner.source_refs(root).items()}
|
|
need(index['source_map'] == actual,'wrong/stale/incomplete final source map')
|
|
spec = load(root/'docs/spec/acceptance/P08-02.json')
|
|
need(index['spec_sha256'] == SPEC_SHA == hashlib.sha256((root/'docs/spec/acceptance/P08-02.json').read_text().strip().encode()).hexdigest(),'frozen spec SHA')
|
|
for name,pin in spec['current_source_pins'].items(): need(actual.get(name) == pin,'frozen production source '+name)
|
|
inputs = {str(p.relative_to(root)):sha(p) for p in (root/'docs/spec/inputs/p08-02').iterdir() if p.is_file()}
|
|
need(index['inputs'] == inputs,'frozen input map')
|
|
need(index['marker_contract_sha256'] == sha(root/CONTRACT),'marker contract source')
|
|
need(re.fullmatch('[0-9a-f]{40}',index['commit']) is not None,'submitted commit identity')
|
|
guarded('source',source_gate)
|
|
for name in index.get('artifacts',{}): guarded('artifact '+name,lambda name=name:artifact(name))
|
|
markers = module('p08_evidence_markers',root/'tools/strict_test_markers.py')
|
|
contracts = markers.load_contracts(root/CONTRACT)
|
|
pids = set()
|
|
def phase(name, role):
|
|
receipt = document(name); pid = receipt['actual_pid']
|
|
need(type(pid) is int and pid > 0 and pid not in pids,'distinct native PID '+name); pids.add(pid)
|
|
parent = str(Path(name).parent)
|
|
prefix = '' if parent == '.' else parent+'/'
|
|
folder = prefix+receipt['phase']+'/'
|
|
start,end,outer = [document(folder+n) for n in ('started.json','ended.json','outer-owned-finally.json')]
|
|
for row in (start,end):
|
|
need(row['actual_pid'] == pid and row['command'] == receipt['command'],'raw lifecycle PID/argv '+name)
|
|
need(end.get('exit_code') == 0 and end.get('engine_ended') is True,'ended exit '+name)
|
|
need(start.get('live_identity_exact') is True or start.get('tracked_child_ended_before_initial_probe') is True,'native Popen identity '+name)
|
|
need(outer['actual_captured_pids'] == [pid] and outer['all_captured_ended'] is True and not outer['outer_cleanup_errors'] and outer['foreign_process_or_group_signals'] == 0,'owned cleanup '+name)
|
|
native = receipt['native_process']
|
|
need(native['exit_code'] == 0 and native['helper_exit_code'] == 0 and native['timed_out'] is False and native['lifecycle_error'] is None,'native exit/errors '+name)
|
|
need(receipt['exit_code'] == 0 and receipt['engine_ended'] is True,'phase ended '+name)
|
|
if role:
|
|
test = 'tests/fixtures/p08-02/'+role+'.gd'
|
|
need(receipt['test'] == test and receipt['source_sha256'] == index['source_map'][test],'phase source '+name)
|
|
raw = artifact(prefix+receipt['log']).read_text()
|
|
parsed = markers.parse_raw(test,raw,{test:{'template':'P08 mixed '+role+': <checks> checks, <failures> failures'}},receipt['source_sha256'])
|
|
need(not parsed['errors'],'strict completion '+name+': '+str(parsed['errors']))
|
|
profile = receipt['actual_private_profile']['report']
|
|
need(profile['fresh_before_parent_initialize'] is True and profile['profile_writes_before_guard'] == 0 and profile['actual_user_data_dir'] == profile['expected_user_data_dir'],'guarded fresh profile '+name)
|
|
return receipt
|
|
def case_gate(case):
|
|
name = case['id']; prefix = case['directory']+'/'
|
|
need(name == case['operations']+'-'+case['kind'],'case identity '+name)
|
|
need(len(case['phases']) == 2,'writer and reader phases '+name)
|
|
receipts = [phase(n,r) for n,r in zip(case['phases'],('writer','reader'))]
|
|
def typed(name):
|
|
artifact(prefix+name+'.var')
|
|
return document(prefix+name+'.json')
|
|
identity = decode(typed('identity'))
|
|
original = typed('original'); after_live = typed('after-live')
|
|
need(original == after_live,'live partial batch '+name)
|
|
before_members,after_members = [decode(typed(n)) for n in ('before-members','after-members')]
|
|
for package in ('original','result','backup'):
|
|
for member in MEMBERS: artifact(prefix+package+'/formal/'+member)
|
|
for member in MEMBERS:
|
|
need(before_members['/'+member] == sha(artifact(prefix+'original/formal/'+member)),'original overwritten '+name+'/'+member)
|
|
need(after_members['/'+member] == sha(artifact(prefix+'result/formal/'+member)),'result member '+name+'/'+member)
|
|
need(after_members['.pre_migration.bak/'+member] == sha(artifact(prefix+'backup/formal/'+member)),'backup overwritten '+name+'/'+member)
|
|
refusal = case['kind'] == 'refusal'
|
|
if refusal: need(before_members == after_members,'refusal original/backup overwritten '+name)
|
|
source = semantics(decode(original),identity,True)
|
|
need(source['operations']['enabled'] == (case['operations']=='on'),'missing actual Operations mode '+name)
|
|
for role,receipt in zip(('writer','reader'),receipts):
|
|
proc = decode(typed(role+'-process'))
|
|
need(proc['pid'] == receipt['actual_pid'] and proc['failures'] == 0 and proc['operations'] == (case['operations']=='on') and proc['refusal'] == refusal,'fixture/native binding '+name+'/'+role)
|
|
checkpoint = typed('writer-checkpoint')
|
|
if refusal: need(original == checkpoint,'partial earlier candidate '+name)
|
|
else:
|
|
for path in typed_differences(original,checkpoint):
|
|
need(any(re.fullmatch(pattern,'/'.join(path)) for pattern in GEOMETRY),'unapproved typed migration delta '+name+'/'+ '/'.join(path))
|
|
migrated = decode(checkpoint)['systems']['runtime']
|
|
jobs = indexed(migrated['construction']['jobs'],'job_id')
|
|
need(all(len(jobs[j]['reserved_cells']) == 4 for j in identity['jobs']),'positive batch not applied '+name)
|
|
for label in LABELS:
|
|
writer,reader = typed('writer-'+label),typed('reader-'+label)
|
|
need(writer == reader,'cross-PID full typed state/events '+name+'/'+label)
|
|
need(sha(artifact(prefix+'writer-'+label+'.var')) == sha(artifact(prefix+'reader-'+label+'.var')),'cross-PID raw Variant '+name+'/'+label)
|
|
if label != 'events': semantics(decode(writer),identity)
|
|
final = decode(typed('writer-minute-160'))['systems']['runtime']
|
|
need(indexed(final['staff']['staff'],'staff_id')['patient']['health'] == 100,'patient lost/untreated '+name)
|
|
final_requests = indexed(final['logistics_requests'],'request_id')
|
|
need(all(final_requests[i]['state']=='COMPLETED' for i in identity['logistics_requests']),'original cargo completion '+name)
|
|
events = decode(typed('writer-events'))
|
|
for rid in identity['ev_requests']:
|
|
need(sum(e['event_type']=='ELEVATOR_ARRIVED' and e['payload'].get('request_id')==rid for e in events)==1,'EV original request exactly once '+name+'/'+rid)
|
|
cases = index.get('cases',[])
|
|
need([c.get('id') for c in cases] == list(CASES),'missing/duplicate ON/OFF positive/refusal case')
|
|
for case in cases: guarded('case '+str(case.get('id')),lambda case=case:case_gate(case))
|
|
for condition in CONDITIONS:
|
|
expected = [c for c in CASES if condition != 'unsolvable_preservation' or c.endswith('refusal')]
|
|
need(index.get('conditions',{}).get(condition) == expected,'condition mapping '+condition)
|
|
def os_gate():
|
|
proof = document(index['os_proof'])
|
|
need(proof['commit'] == index['commit'] and proof['source_map'] == index['source_map'],'OS current candidate')
|
|
need(proof['input_method'] == 'native_os' and proof['setup'] == 'controlled_old_save','OS input provenance/setup')
|
|
actions = proof['actions']
|
|
need([a['action'] for a in actions] == ['preview','cancel','confirm','changed_source_rejection'],'OS actions missing')
|
|
for action in actions:
|
|
raw = document(action['input_receipt'])
|
|
need(raw['method'] == 'native_os' and raw['event_type'] in ('mouse','keyboard') and type(raw['target_pid']) is int and raw['target_pid'] > 0 and raw['timestamp_ns'] > 0,'fake OS signal')
|
|
for key in ('screen_before','screen_after','model_before','model_after','formal_before','formal_after'): artifact(action[key])
|
|
if action['action'] in ('preview','cancel','changed_source_rejection'):
|
|
need(document(action['formal_before']) == document(action['formal_after']),'OS formal preservation '+action['action'])
|
|
guarded('OS proof',os_gate)
|
|
def regression_gate():
|
|
proof = document(index['regression']); run_input = document(proof['input']); ended = document(proof['ended'])
|
|
need(run_input['selected_source_map'] == index['source_map'],'final regression source map')
|
|
need(proof['commit'] == index['commit'],'final regression commit')
|
|
need(not ended['error'] and not ended['cleanup_errors'] and not ended['guard_errors'],'regression failure')
|
|
runner = module('p08_regression_roster',root/'tools/run_all_tests.py')
|
|
roster = runner.roster(root)
|
|
need(run_input['roster'] == roster,'full final roster')
|
|
rows = [x for x in ended['phases'] if x.get('test')]
|
|
need([x['test'] for x in rows] == roster,'full final regression receipt')
|
|
for row in rows:
|
|
raw = artifact(proof['directory']+'/'+row['log']).read_text()
|
|
need(not markers.parse_raw(row['test'],raw,contracts,index['source_map'][row['test']])['errors'],'regression strict '+row['test'])
|
|
need(row['exit_code'] == 0 and row['engine_ended'] is True,'regression native exit '+row['test'])
|
|
guarded('final regression',regression_gate)
|
|
return sorted(set(deficits))
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--verify-only',action='store_true',required=True)
|
|
parser.add_argument('--project-root',type=Path,default=HERE.parent)
|
|
args = parser.parse_args(argv)
|
|
build = os.environ.get('BUILD_OUTPUT_DIR')
|
|
deficits = ['BUILD_OUTPUT_DIR missing'] if not build else check(args.project_root,Path(build)/'p08-evidence')
|
|
print(json.dumps({'integrity':'deficient' if deficits else 'verified','deficits':deficits,
|
|
'acceptance':'pending','conditions':{c:'pending' for c in CONDITIONS}},ensure_ascii=False,indent=2))
|
|
return 1 if deficits else 0
|
|
|
|
if __name__ == '__main__': raise SystemExit(main())
|