import json,hashlib,ast
import numpy as np
from design import *
g=pd.read_parquet(R/'classified.parquet').set_index('appid',drop=False)
o=pd.read_csv(R/'host_results.csv');m=pd.read_csv(R/'matched_games.csv');r=pd.read_csv(R/'matched_results.csv')
checks={}
def check(k,b):checks[k]=bool(b);assert b,k
check('75860_unique',len(g)==g.index.nunique()==75860)
check('eligible_only',g.eligible.all() and g.period.ne('other').all())
check('strict_implies_extended',(~g.strict|g.extended).all())
check('direct_implies_strict',(~g.direct|g.strict).all())
check('classifier_hash',hashlib.sha256((R/'design.py').read_bytes()).hexdigest()==json.loads((R/'specification.json').read_text())['classifier_hash'])
for a in o.itertuples():
 p=g[g.period.eq(a.period)&g['host10_'+a.host]&g['text_'+a.host]];b=p[p.strict];c=p[~p.strict]
 check('host_counts_'+a.period+'_'+a.host,len(p)==a.n and len(b)==a.b_n and len(c)==a.c_n and b.reviews.ge(100).sum()==a.b_ge100 and c.reviews.ge(100).sum()==a.c_ge100)
check('unique_matched_case',not m.duplicated(['period','host','variant','appid']).any())
check('three_to_five_peers',m.peer_n.between(3,5).all())
e=m[['period','host','variant','appid','peer_ids']].copy();e['peer_appid']=e.peer_ids.str.split('|');e=e.explode('peer_appid');e['peer_appid']=e.peer_appid.astype(int)
check('unique_peers_per_case',not e.reset_index().duplicated(['index','peer_appid']).any())
check('all_peers_nonbuild',not bool(e.peer_appid.map(g.strict).any()))
check('all_focal_build',m.appid.map(g.strict).all())
check('same_period',e.period.eq(e.peer_appid.map(g.period)).all())
check('different_developers',e.appid.map(g.developer_id).ne(e.peer_appid.map(g.developer_id)).all())
check('same_price_band',e.appid.map(g.price_bucket).fillna('missing').eq(e.peer_appid.map(g.price_bucket).fillna('missing')).all())
gap=(e.appid.map(g.first_date)-e.peer_appid.map(g.first_date)).dt.total_seconds().abs()/86400
check('date_windows',gap.le(np.where(e.period.eq('2026_JanAug'),30,90)+1e-7).all())
for host in HOSTS:
 z=e[e.host.eq(host)];check('peer_host_'+host,z.peer_appid.map(g['host10_'+host]&g['text_'+host]).all())
for q in [50,100,200,1000]:
 computed=e.peer_appid.map(g.reviews).ge(q).groupby(level=0).mean()
 check('peer_expected_'+str(q),np.allclose(computed.reindex(m.index),m['expected'+str(q)]))
for a in r.itertuples():
 z=m[m.period.eq(a.period)&m.host.eq(a.host)&m.variant.eq(a.variant)]
 check('aggregate_'+a.period+'_'+a.host+'_'+a.variant,len(z)==a.supported_n and z.reviews.ge(100).sum()==a.observed100 and abs(z.expected100.sum()-a.expected100)<1e-8)
alt=pd.read_csv(R/'match_sensitivity.csv')
z=alt[alt.definition.eq('strict')&alt.k.eq(5)&((alt.period.eq('2023_2025')&alt.window.eq(90))|(alt.period.eq('2026_JanAug')&alt.window.eq(30)))]
z=z.merge(r[r.variant.eq('all')],on=['period','host'],suffixes=('_alt','_base'))
check('independent_match_reproduction',z.supported_n_alt.eq(z.supported_n_base).all() and z.observed100_alt.eq(z.observed100_base).all() and np.allclose(z.expected100_alt,z.expected100_base))
screen=pd.read_csv(R/'final_sparse_screen.csv')
criteria=screen.n.ge(100)&screen.share.le(.15)&screen.b_n.ge(10)&screen.b_ge100.ge(5)&screen.responding_developers.ge(5)&screen.support.ge(.8)&screen.matched_ratio100.ge(1.5)&screen.observed100.ge(5)
check('final_screen_rule',criteria.eq(screen.final_pass).all())
check('five_final_passes',screen.final_pass.sum()==5)
audit=json.loads((R/'audit_manifest.json').read_text())
check('96_audit_cases',len(json.loads((R/'audit_blind.json').read_text()))==96)
check('audit_frozen',hashlib.sha256((R/'audit_blind.json').read_bytes()).hexdigest()==audit['packet_hash'])
check('audit_labels_frozen',hashlib.sha256((R/'audit_labels.csv').read_bytes()).hexdigest()==audit['labels_hash'])
sample=g.sample(300,random_state=906813)
check('300_classifier_reproduced',all((classify(a.short_description,a.description)['strict']==a.strict and classify(a.short_description,a.description)['extended']==a.extended) for a in sample.itertuples()))
check('network_paused',json.loads((ROOT/'network_policy.json').read_text())['network_paused'])
check('no_media_downloader',not (ROOT/'images.py').exists())
check('figures_present',all((R/f'{n}.{e}').stat().st_size>1000 for n in ['prevalence','matched_comparison'] for e in ['png','svg']))
for s in R.glob('*.py'):ast.parse(s.read_text())
check('scripts_parse',True)
source_hashes={}
for name in ['analysis-2026-09-05/games.parquet','analysis-2026-09-05/tags.parquet','build-expression-2026-09-05/classification.parquet']:
 h=hashlib.sha256()
 with (ROOT/name).open('rb') as f:
  for b in iter(lambda:f.read(1024*1024),b''):h.update(b)
 source_hashes[name]=h.hexdigest()
result=dict(passed=sum(checks.values()),checks=checks,source_hashes=source_hashes,script_hashes={s.name:hashlib.sha256(s.read_bytes()).hexdigest() for s in R.glob('*.py')},
 scope='Mechanical consistency and reproducibility; not verified gameplay depth or causal validation.')
(R/'validation.json').write_text(json.dumps(result,indent=2))
print('Passed',len(checks),'checks; matched records',len(m),'peer links',len(e))
