#!/usr/bin/env python3
from __future__ import annotations
import hashlib,json,os,stat,zipfile
from pathlib import Path
from typing import Any
ROOT=Path(__file__).resolve().parent
PROJECT=ROOT.parents[3]
HERMES=Path('/home/cube/projects/richard/hermes-agent')
PROFILE=Path('/home/cube/.hermes/profiles/dualcoachtest/workspace/checkin_cli')
OLD=PROJECT/'.omo/evidence/task26/task26-strict-final-candidate-st_01a00f35'
EXCLUDED={'.git','.venv','venv','.venv-task26-preterminal-retained','node_modules','.pytest-cache','.pytest_cache','.ruff_cache','__pycache__','build','dist','.gjc','.plans','.task26-evidence','.task26-owner-v1-repair-v4','.task26-owner-v1-repair-v5','.task26-owner-v1-repair-v6','.task26-owner-v1-snapshots','hermes_agent.egg-info','physique_checkin_cli.egg-info'}
def sha(p:Path)->str:return hashlib.sha256(p.read_bytes()).hexdigest()
def canon(v:Any)->bytes:return (json.dumps(v,sort_keys=True,separators=(',',':'))+'\n').encode()
def inventory(base:Path,label:str)->list[dict[str,Any]]:
 rows=[]
 for cur,dirs,files in os.walk(base,followlinks=False):
  dirs[:]=sorted(d for d in dirs if d not in EXCLUDED and not d.endswith('.egg-info'))
  for name in sorted(files):
   p=Path(cur)/name; rel=p.relative_to(base).as_posix()
   if any(x in EXCLUDED or x.endswith('.pyc') for x in Path(rel).parts):continue
   st=p.lstat()
   if not stat.S_ISREG(st.st_mode):raise RuntimeError(f'unsupported product input: {p}')
   rows.append({'root':label,'path':rel,'size':st.st_size,'sha256':sha(p),'mode':f'{stat.S_IMODE(st.st_mode):04o}'})
 return rows
def parity(wheel:Path,prefixes:dict[str,Path])->dict[str,Any]:
 n=0;bad=[]
 with zipfile.ZipFile(wheel) as z:
  for member in z.namelist():
   if not member.endswith(('.py','.json')):continue
   for prefix,source in prefixes.items():
    if member.startswith(prefix):
     p=source/member.removeprefix(prefix)
     if p.is_file():
      n+=1
      if hashlib.sha256(z.read(member)).hexdigest()!=sha(p):bad.append(member)
     break
 return {'compared':n,'mismatches':bad,'status':'PASS' if not bad else 'FAIL'}
def main()->int:
 hw=next((ROOT/'artifacts').glob('hermes*.whl'));pw=next((ROOT/'artifacts').glob('physique*.whl'))
 rows=inventory(HERMES,'hermes')+inventory(PROFILE,'profile')
 inv=hashlib.sha256(canon(rows)).hexdigest()
 predecessors={
  'strict_final_candidate_573d19e':{'path':str(OLD/'SEAL.json'),'sha256':'76df4f322d316a8ee1ca332df7fdd2a17d2315ae3cee9c968d253b38bad6388c'},
  'failed_launch_v3_clean':{'path':str(PROJECT/'.omo/evidence/task26/task26-strict-launch-controller-st_01a00f53/run-20260817-strict-a-v3/RUN-SEAL.json'),'sha256':'227de3df3854d8a2867a50697c21b405539d13edc1f27931024f97e1109154ec'},
  'failed_launch_v4_clean':{'path':str(PROJECT/'.omo/evidence/task26/task26-strict-launch-controller-v4-st_01a00f53/run-20260817-strict-a-v4/RUN-SEAL-v4.json'),'sha256':'8f9111b2ac21ac1ae1fb3f0af50bd1ff9b522e3db14cf1cf886b13cb2ab9f4b0'},
  'failed_launch_v5_clean':{'path':str(PROJECT/'.omo/evidence/task26/task26-strict-launch-controller-v5-st_01a00f53/run-20260817-strict-a-v5/RUN-SEAL-v5.json'),'sha256':'ba8226e9e7cc036560070d43e2d90a4c7bd56345fc0dc2148ea1a69ba986b1f9'},
 }
 for x in predecessors.values():
  if sha(Path(x['path']))!=x['sha256']:raise RuntimeError('predecessor drift')
 authorities={'config':sha(Path('/home/cube/.hermes/profiles/dualcoachtest/config.yaml')),'provider':sha(Path('/home/cube/.hermes/profiles/dualcoachtest/data/dualcoach-provider-auth/index.json'))}
 core=hashlib.sha256(canon({'inventory':inv,'authorities':authorities,'predecessors':predecessors})).hexdigest()
 bindings={'core':core,'inventory':inv,'hermes_wheel':sha(hw),'profile_wheel':sha(pw),'predecessors':predecessors,'read_only_api':sha(HERMES/'gateway/config.py'),'membership':sha(HERMES/'gateway/platforms/telegram_staff_membership_gate.py'),'capability':sha(HERMES/'gateway/platforms/dualcoach_tasks21_25_controller.py'),'retention':'f02a9671422b2d49aace2037dfbcfff461249ac79d4999d46bc09ddce638367d','capture':'fd973db6926fe12a87913814e9a15e1f396574e47d153e7221dd2efa72184484'}
 full=hashlib.sha256(canon(bindings)).hexdigest()
 par={'hermes':parity(hw,{'gateway/':HERMES/'gateway','plugins/':HERMES/'plugins','hermes_cli/':HERMES/'hermes_cli'}),'profile':parity(pw,{'checkin_cli/':PROFILE/'checkin_cli'})}
 if any(x['status']!='PASS' for x in par.values()):raise RuntimeError('parity failed')
 manifest={'schema':'task26-strict-final-successor-v1','status':'ASSEMBLED_OFFLINE','full_candidate_digest':full,'core_candidate_digest':core,'inventory_digest':inv,'inventory_count':len(rows),'hermes_wheel_sha256':sha(hw),'profile_wheel_sha256':sha(pw),'predecessors':predecessors,'bindings':bindings,'source_wheel_parity':par,'source_changes':['gateway/config.py','gateway/__init__.py','gateway/platforms/dualcoach_admin.py','tests/gateway/test_read_only_preflight_config.py'],'preflight_api':'gateway.config.load_gateway_preflight_inputs','authority_tree_mutations':0,'network_actions':0,'service_actions':0,'customer_actions':0,'live_mutations':0}
 (ROOT/'product-inventory.json').write_bytes(canon(rows));(ROOT/'candidate-manifest.json').write_bytes(canon(manifest))
 print(json.dumps({'full':full,'core':core,'inventory':inv,'hermes':sha(hw),'profile':sha(pw),'count':len(rows)},sort_keys=True));return 0
if __name__=='__main__':raise SystemExit(main())
