#!/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
PREV=Path('/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-onboarding-package-origin-successor-st_01a01262')
HERMES=Path('/home/cube/projects/richard/hermes-agent')
PROFILE=Path('/home/cube/.cache/task26-strict-successor-1786976146/src-p')
PREV_CANDIDATE='bea97f51a31fdd92d4b44976614ef2a1cbcbfa82e35eb2cc12e9c7bd947fb483'
PREV_PRODUCT='c6ec9cec649fe632442e5aa5324f56ec8d854c972091aecb3929db52f6c02c3c'
EXPECTED=['checkin_cli/nutrition_onboarding.py','checkin_cli/nutrition_onboarding_models.py','tests/test_nutrition_onboarding.py','tests/test_nutrition_onboarding_models.py']
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 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 wheel_parity(wheel:Path,source:Path)->dict[str,Any]:
 compared=0; missing=[]; mismatch=[]
 with zipfile.ZipFile(wheel) as z:
  for member in z.namelist():
   if '.dist-info/' in member or '.data/' in member or member.endswith('/'):continue
   p=source/member
   if not p.is_file():missing.append(member);continue
   compared+=1
   if hashlib.sha256(z.read(member)).hexdigest()!=sha(p):mismatch.append(member)
 return {'compared':compared,'unmapped':missing,'mismatches':mismatch,'status':'PASS' if not missing and not mismatch else 'FAIL'}
def main()->None:
 previous=json.loads((PREV/'product-inventory.json').read_text())
 old={(r['root'],r['path']):r for r in previous}
 rows=inventory(HERMES,'hermes')+inventory(PROFILE,'profile')
 now={(r['root'],r['path']):r for r in rows}
 changed=[{'root':k[0],'path':k[1],'before_sha256':old[k]['sha256'],'after_sha256':now[k]['sha256']} for k in sorted(old.keys()&now.keys()) if old[k]['sha256']!=now[k]['sha256']]
 added=[{'root':k[0],'path':k[1]} for k in sorted(now.keys()-old.keys())]
 removed=[{'root':k[0],'path':k[1]} for k in sorted(old.keys()-now.keys())]
 if [(r['root'],r['path']) for r in changed] != [('profile',p) for p in EXPECTED] or added or removed:raise RuntimeError(f'unexpected source delta changed={changed} added={added} removed={removed}')
 hw=ROOT/'artifacts/hermes_agent-0.17.0-py3-none-any.whl'; pw=ROOT/'artifacts/physique_checkin_cli-0.1.0-py3-none-any.whl'
 if sha(hw)!='c092432b2ed9af300342c5d402781cd16d45e21b0f239a047936d9106d675228':raise RuntimeError('Hermes wheel drift')
 profile_parity=wheel_parity(pw,PROFILE)
 if profile_parity['status']!='PASS':raise RuntimeError(f'profile parity failed {profile_parity}')
 inv_digest=hashlib.sha256(canon(rows)).hexdigest(); delta_digest=hashlib.sha256(canon(changed)).hexdigest()
 product_bindings={'inventory_digest':inv_digest,'hermes_wheel_sha256':sha(hw),'profile_wheel_sha256':sha(pw),'predecessor_product':PREV_PRODUCT,'source_delta_digest':delta_digest}
 product=hashlib.sha256(canon(product_bindings)).hexdigest()
 names=('profile-focused51.log','profile-full705.log','profile-ruff.log','profile-basedpyright.log','profile-compileall.log','profile-wheel-build.log','hermes-profile-compatibility.log','full-gateway.log','installed-hermes-wheel-tests.log')
 receipts={n:sha(ROOT/'receipts'/n) for n in names}
 candidate_bindings={'product_digest':product,'predecessor_candidate':PREV_CANDIDATE,'predecessor_product':PREV_PRODUCT,'receipts':receipts,'profile_source_changed':True,'zero_unindexed_source':not added and not removed}
 candidate=hashlib.sha256(canon(candidate_bindings)).hexdigest()
 (ROOT/'product-inventory.json').write_bytes(canon(rows)); (ROOT/'source-delta.json').write_bytes(canon({'changed':changed,'added':added,'removed':removed,'status':'PASS_EXACT_FOUR'}))
 manifest={'schema':'task26-sex-basis-normalization-successor-v1','status':'SEALED_VERIFIED','successor_candidate_digest':candidate,'successor_product_digest':product,'lineage':{'candidate':PREV_CANDIDATE,'product':PREV_PRODUCT},'inventory_count':len(rows),'inventory_digest':inv_digest,'source_delta':changed,'zero_unindexed_source':True,'wheels':{'hermes':sha(hw),'profile':sha(pw)},'hermes_wheel_preserved_exact':True,'profile_source_wheel_parity':profile_parity,'tests':{'profile_focused':'51 passed','profile_full':'705 passed','compatibility':'296 passed','installed_hermes':'55 passed','full_gateway':'8191 passed, 59 skipped','ruff':'PASS','basedpyright':'0 errors, 0 warnings','compileall':'PASS'},'bindings':candidate_bindings}
 (ROOT/'candidate-manifest.json').write_bytes(canon(manifest)); (ROOT/'SEAL.json').write_bytes(canon({'schema':'task26-successor-seal-v1','status':'SEALED_VERIFIED','candidate':candidate,'product':product,'manifest_sha256':sha(ROOT/'candidate-manifest.json'),'inventory_sha256':sha(ROOT/'product-inventory.json'),'source_delta_sha256':sha(ROOT/'source-delta.json'),'wheels':manifest['wheels']}))
 print(json.dumps({'candidate':candidate,'product':product,'inventory':inv_digest,'count':len(rows),**manifest['wheels']},sort_keys=True))
if __name__=='__main__':main()
