#!/usr/bin/env python3
import hashlib,json,re,sys
from pathlib import Path
PROJECT='gacmai_20260717_104203'
def fail(es):print(json.dumps({'status':'failed','errors':es},ensure_ascii=False,indent=2));raise SystemExit(1)
def main(story_path,outline_path):
 sp,op=Path(story_path),Path(outline_path);s=json.loads(sp.read_text());o=json.loads(op.read_text());es=[]
 if s.get('project_id')!=PROJECT:es.append('project mismatch')
 if s.get('outline_sha256')!=hashlib.sha256(op.read_bytes()).hexdigest():es.append('outline checksum mismatch')
 parts={x.get('part'):'\n'.join(x.get('paragraphs',[])) for x in s.get('parts',[])}
 if set(parts)!=set(range(1,13)):es.append('parts mismatch')
 ledgers=[]
 for src in s.get('sources',[]):
  bp=Path(src['path'])
  if not bp.exists():es.append(f'missing source snapshot {bp}');continue
  ledgers.append(json.loads(bp.read_text()).get('evidence_ledger',{}))
 merged={}
 for ledger in ledgers:
  for eid,value in ledger.items():merged.setdefault(eid,[]).append(value)
 expected=[f'E{i:02d}' for i in range(1,10)]
 if any(eid not in merged for eid in expected):es.append('one or more E01-E09 absent from batch evidence ledgers')
 evidence=[];alltext='\n'.join(parts.values()).lower()
 for item in o.get('evidence',[]):
  eid=item.get('evidence_id');aliases=[str(x).lower() for x in item.get('aliases',[]) if str(x).strip()];matched=[x for x in aliases if x in alltext]
  if eid not in expected:es.append(f'bad evidence id {eid}')
  if item.get('lawful_authority') not in {f'A{i:02d}' for i in range(1,7)}:es.append(f'bad lawful authority {eid}')
  if not all(item.get(k) for k in ('setup','preservation','payoff','cannot_support')):es.append(f'incomplete evidence contract {eid}')
  if not matched:es.append(f'no narrative alias match {eid}')
  evidence.append({'evidence_id':eid,'lawful_authority':item.get('lawful_authority'),'matched_aliases':matched,'ledger_entries':merged.get(eid,[]),'cannot_support':item.get('cannot_support')})
 auth=o.get('authorities',[]);authority_contract=[]
 if [x.get('authority_id') for x in auth]!=[f'A{i:02d}' for i in range(1,7)]:es.append('authority contract mismatch')
 for x in auth:
  if not x.get('scope') or not x.get('cannot_do'):es.append(f'incomplete authority {x.get("authority_id")}')
  authority_contract.append({'authority_id':x.get('authority_id'),'name':x.get('name'),'scope':x.get('scope'),'cannot_do':x.get('cannot_do')})
 patterns=[r'khẳng định chắc chắn[^.!?]{0,120}(?:thủ phạm|cố ý)',r'chứng minh duy nhất[^.!?]{0,120}(?:thủ phạm|người làm)',r'không thể là ai khác']
 hits=[]
 for pat in patterns:
  for m in re.finditer(pat,alltext,re.I):hits.append({'pattern':pat,'excerpt':alltext[max(0,m.start()-80):m.end()+120]})
 if hits:es.append(f'direct overclaim patterns: {len(hits)}')
 out={'status':'passed' if not es else 'failed','story_sha256':hashlib.sha256(sp.read_bytes()).hexdigest(),'outline_sha256':hashlib.sha256(op.read_bytes()).hexdigest(),'evidence':evidence,'authority_contract':authority_contract,'authority_semantic_review':'required_in_independent_full_read_audit','direct_overclaim_hits':hits,'errors':es};print(json.dumps(out,ensure_ascii=False,indent=2));raise SystemExit(1 if es else 0)
if __name__=='__main__':
 if len(sys.argv)!=3:fail(['usage: auditor STORY OUTLINE'])
 main(sys.argv[1],sys.argv[2])
