import json,hashlib
from design import *
g,t=load_source();g=g[g.eligible&g.period.ne('other')].copy()
records=[]
for i,a in enumerate(g.itertuples(),1):
 records.append(dict(appid=a.appid,**classify(a.short_description,a.description)))
 if i%10000==0:print('Classified',i,flush=True)
g=g.merge(pd.DataFrame(records),on='appid',validate='one_to_one')
hostcols={}
for rank in [5,10,20]:
 for label,tags in HOSTS.items():hostcols[f'host{rank}_{label}']=g.appid.isin(t.loc[t.tag.isin(tags)&t['rank'].le(rank),'appid'])
intro=(g.short_description.fillna('')+' '+g.description.fillna('').map(lambda s:' '.join(s.split()[:350])))
for label,pattern in HOST_WORDS.items():hostcols['text_'+label]=intro.str.contains(pattern,case=False,regex=True,na=False)
for label,tags in [('core',CORE),('rogue',ROGUE)]:hostcols[label]=g.appid.isin(t.loc[t.tag.isin(tags)&t['rank'].le(10),'appid'])
g=pd.concat([g,pd.DataFrame(hostcols,index=g.index)],axis=1)
g.to_parquet(R/'classified.parquet',index=False)
spec=dict(snapshot='2026-09-05',network='Offline; no review texts used.',population='Paid, dated released records, outside prior explicit-content and central-idler/incremental/FMV/VN exclusions. No manual quality filter.',
 hosts=HOSTS,host_rank=10,rank_sensitivity=[5,20],host_text_patterns=HOST_WORDS,host_text='Main: tag plus explicit activity language in short pitch/first 350 full-description words. Tag-only retained as sensitivity; not an exhaustive genre census.',patterns=PATTERNS,
 measurement='Strict explicit advertised builds/decks/synergies/loadouts/class combinations; extended includes functional customization. Neither verifies heavy/deep gameplay. No detected promise is not proven absence.',
 outcomes=[10,50,100,200,1000],sentiment='Not used for ranking or conclusions.',
 periods=['2019_2022','2023_2025','2026_JanAug'],
 comparison='Within host; review-count rates, date/current-price descriptive standardization, text-length sensitivity, familiar-build/roguelike overlap split, normalized creator-name removals.',
 sparse_lead_rule='In 2023-2025: host >=100 games, strict build share <=15%, >=10 strict matches, >=5 with 100 reviews across >=5 developer names; within-host observed/expected >=1.5. Report every passing or failing candidate and older/newer counts. This is an exploratory screen, not an underserved-market estimate.',
 inference='No sales, development duration, causal genre effects, verified demand or equal-age launch histories are inferred. Current dates/tags/prices/descriptions and surviving catalog scope limit interpretation.',
 classifier_hash=hashlib.sha256((R/'design.py').read_bytes()).hexdigest())
(R/'specification.json').write_text(json.dumps(spec,indent=2))
# Numerical review outcomes and prices withheld from audit packets.
audit=[];used=set();recent=g[g.period.isin(['2023_2025','2026_JanAug'])]
for host in AUDIT_HOSTS:
 x=recent[recent[f'host10_{host}']]
 for label,mask,n in [('strict',x.strict,3),('extended_only',x.extended&~x.strict,3),('no_signal',~x.extended,2)]:
  z=x[mask&~x.appid.isin(used)]
  for a in z.sample(min(n,len(z)),random_state=906813).itertuples():
   used.add(a.appid);audit.append(dict(appid=a.appid,name=a.name,host=host,stratum=label,short=a.short_description,description=a.description,evidence=json.loads(a.evidence)))
if not (R/'audit_blind.json').exists():(R/'audit_blind.json').write_text(json.dumps(audit,indent=2,ensure_ascii=False))
print('Prepared',len(g),'games;',len(audit),'unique audit cases.')
print('Strict',g.strict.sum(),'extended',g.extended.sum(),'additional strict',int((g.strict&~g.old_strict).sum()),flush=True)
