"""Synthetic provenance/version impact fixture. No network or factual inference.""" import copy,hashlib,json,platform from datetime import datetime,timezone sources={'S1':{'url':'https://example.test/tool','excerpt':'테스트 도구는 텍스트 파일을 읽는다. 이미지 인식은 시험하지 않았다.','version':'v1'},'S2':{'url':'https://example.test/download','excerpt':'이 예제는 무료로 내려받을 수 있다. 재배포 권한은 별도다.','version':'v2'}} rows=[ {'id':'C1','claim':'텍스트 파일을 읽는다','source':'S1','source_version':'v1','quote':'텍스트 파일을 읽는다','locator':'문장1','checked_at':'2026-10-15T10:00:00+09:00','assessment':'supports','scope':'합성 문서 v1'}, {'id':'C2','claim':'모든 이미지도 정확하게 읽는다','source':'S1','source_version':'v1','quote':'이미지 인식은 시험하지 않았다','locator':'문장2','checked_at':'2026-10-15T10:00:00+09:00','assessment':'unsupported','scope':'합성 문서 v1'}, {'id':'C3','claim':'무료이고 재배포도 자유다','source':'S2','source_version':'v2','quote':'재배포 권한은 별도다','locator':'문장2','checked_at':'2026-10-15T10:00:00+09:00','assessment':'partial','scope':'합성 문서 v2'}, {'id':'C4','claim':'처리 시간이 절반이다','source':'','source_version':'','quote':'','locator':'','checked_at':'','assessment':'unknown','scope':''}, {'id':'C5','claim':'이 예제는 무료로 내려받을 수 있다','source':'S2','source_version':'v2','quote':'이 예제는 무료로 내려받을 수 있다','locator':'문장1','checked_at':'2026-10-15T10:00:00+09:00','assessment':'supports','scope':'합성 문서 v2'}] occurrences=[{'claim':'C1','location':'body:2'},{'claim':'C5','location':'title'},{'claim':'C5','location':'summary'},{'claim':'C5','location':'body:5'},{'claim':'C3','location':'draft-note:1'}] def audit(row,current): missing=[k for k in ['source','source_version','quote','locator','checked_at','scope'] if not row.get(k)] errors=[];source=current.get(row.get('source')) if row.get('source') and source is None:errors.append('unknown-source') if source: if row.get('source_version')!=source['version']:errors.append('stale-source-version') if row.get('quote') and row['quote'] not in source['excerpt']:errors.append('quote-not-in-recorded-source') if row.get('checked_at'): try: stamp=datetime.fromisoformat(row['checked_at']) if stamp.tzinfo is None or stamp.utcoffset() is None:errors.append('missing-time-offset') except ValueError:errors.append('invalid-check-time') return {'id':row['id'],'missing':missing,'errors':errors,'manualAssessment':row['assessment'],'publicationAction':'retain-narrow' if row['assessment']=='supports' and not missing and not errors else 'hold'} def unique_rows(items): if len({x['id'] for x in items})!=len(items):raise ValueError('duplicate claim ID') def sha(text):return hashlib.sha256(text.encode('utf-8')).hexdigest() def impact(before,after): changed=[];url_only=[] for sid,source in before.items(): newer=after.get(sid) if newer is None or sha(source['excerpt'])!=sha(newer['excerpt']):changed.append(sid) elif source['url']!=newer['url']:url_only.append(sid) affected=sorted(r['id'] for r in rows if r['source'] in changed) return {'changedSources':changed,'urlOnlySources':url_only,'affectedClaims':affected,'affectedOccurrences':[o for o in occurrences if o['claim'] in affected],'action':'re-read affected claims; no automatic factual rewrite'} unique_rows(rows);initial=[audit(r,sources) for r in rows] updated=copy.deepcopy(sources);updated['S2']['version']='v3';updated['S2']['excerpt']='이 예제는 7일 체험 기간에만 무료로 내려받을 수 있다. 재배포 권한은 별도다.' changed=impact(sources,updated);reaudit=[audit(r,updated)for r in rows] moved=copy.deepcopy(sources);moved['S1']['url']='https://docs.example.test/tool';url_only=impact(sources,moved) negative=[] for name,changes in [('unknown-source',{'source':'S404'}),('wrong-quote',{'quote':'이미지 전체를 완벽하게 읽는다'}),('wrong-version',{'source_version':'v99'}),('no-offset',{'checked_at':'2026-10-15T10:00:00'})]: row=dict(rows[0],**changes);negative.append({'case':name,'row':row,'result':audit(row,sources)}) blocked=False try:unique_rows(rows+[copy.deepcopy(rows[0])]) except ValueError:blocked=True assert [r['publicationAction']for r in initial]==['retain-narrow','hold','hold','hold','retain-narrow'] assert changed['affectedClaims']==['C3','C5'] and len(changed['affectedOccurrences'])==4 assert reaudit[4]['publicationAction']=='hold' and 'stale-source-version'in reaudit[4]['errors'] assert url_only['changedSources']==[] and url_only['urlOnlySources']==['S1'] assert all(x['result']['publicationAction']=='hold'for x in negative) and blocked result={'syntheticSources':sources,'rows':rows,'occurrences':occurrences,'results':initial,'updatedSources':updated,'contentRevisionImpact':changed,'reAuditAgainstUpdatedVersion':reaudit,'sameTextNewUrlImpact':url_only,'negativeCases':negative,'duplicateClaimIdBlocked':blocked,'automaticTruthAssessment':False,'fixtureCreatedOn':'2026-10-15','executedAt':datetime.now(timezone.utc).isoformat(),'runtime':platform.python_version(),'synthetic':True} print(json.dumps(result,ensure_ascii=False,indent=2))