"""Seal reviewed HTML originals, precise excerpts and government documents.""" from datetime import datetime,timezone import hashlib import importlib.util import json from pathlib import Path import sqlite3 import sys from lxml import html ROOT=Path(__file__).resolve().parents[1] WORK=ROOT/(sys.argv[1] if len(sys.argv)>1 else 'work/five-topic-research-20261111') BATCH=ROOT/'work/database-enrichment-20261110/batches'/(sys.argv[2] if len(sys.argv)>2 else 'five-topics-multisource-20261111-01') BATCH_ID=sys.argv[3] if len(sys.argv)>3 else 'ksf-five-topics-multisource-20261111-r1' sys.path.insert(0,str(ROOT/'server/voc-library')) import structured_batch import library_channel def sha(raw):return hashlib.sha256(raw).hexdigest() def save(file,value):file.write_text(json.dumps(value,ensure_ascii=False,indent=2),encoding='utf8') def main(): review=json.loads((WORK/'reviewed-selection.json').read_bytes()) dedup=json.loads((WORK/'dedup-result.json').read_bytes());assert dedup['ok'] selected=dedup['selected'];assert selected and not BATCH.exists() spec=importlib.util.spec_from_file_location('replay_source',ROOT/'scripts/collect-five-topic-public.py') base=importlib.util.module_from_spec(spec);spec.loader.exec_module(base) (BATCH/'raw/original').mkdir(parents=True) batch={'schema':'ksf.voc-agent-batch.v2','batchId':BATCH_ID, 'producer':'Codex reviewed multi-source topic collection with explicit source boundaries', 'sources':[],'attachments':[],'social':[],'documents':[],'clauses':[]} expectations={name:[] for name in ['social','documents','clauses']} source_metadata={} def attach(name,raw): (BATCH/name).write_bytes(raw);batch['attachments'].append({'path':name,'sha256':sha(raw)}) for i,r in enumerate(selected): original=(WORK/r['collectionDirectory']/r['httpReceipt']['responseFile']).read_bytes() assert sha(original)==r['httpReceipt']['sha256'] tree=html.fromstring(original.decode(r['sourceEncoding']));nodes=tree.xpath(r['bodyExtractionSelector']);assert len(nodes)==1 body=base.paragraphs(nodes[0]) if 'paragraphIndexes' in r: lines=body.splitlines();body='\n'.join(lines[n] for n in r['paragraphIndexes']) assert body==r['body'] and sha(body.encode())==r['bodySha256'],'SOURCE_REPLAY_FAILED' assert all(quote in body for t in r['topics'] for quote in t['passages']) stem=str(i+1).zfill(2)+'-'+r['id'].replace(':','-') op='raw/original/'+stem+'.html';attach(op,original) hp='raw/original/'+stem+'.http.json';attach(hp,json.dumps(r['httpReceipt'],ensure_ascii=False,indent=2).encode()) if r['entity']=='social': record={'postId':r['postId'],'title':r['title'],'content':body,'url':r['url'], 'publishedAt':r['publishedAtRaw'],'contentScope':r['contentScope']} else: tp='raw/original/'+stem+'.txt';attach(tp,body.encode()) record={'documentId':r['id'],'title':r['title'],'sourceUrl':r['url'], 'declaredStatus':r['declaredStatus'],'statusAsOf':datetime.now(timezone.utc).date().isoformat(), 'issuedDate':r['publishedAtRaw'],'fullText':body,'expectedSha256':sha(body.encode()),'textFile':tp, 'coverage':json.dumps({'sourceType':r['sourceType'],'topics':r['topics'],'dateEvidence':r['dateEvidence'], 'limits':r['limits']+review['limits']},ensure_ascii=False)} adapter={'kind':'explicit_html_extraction_adapter_not_upstream_json','record':record, 'original':{'path':op,'sha256':sha(original),'sourceUrl':r['httpReceipt']['requestedUrl'], 'resolvedUrl':r['httpReceipt']['finalUrl'],'encoding':r['sourceEncoding'],'selector':r['bodyExtractionSelector'], 'paragraphIndexes':r.get('paragraphIndexes'),'collectedAt':r['httpReceipt']['collectedAt']}, 'researchTopics':r['topics'],'sourceType':r['sourceType'],'reviewNote':r['reviewNote'], 'limits':r['limits']+review['limits']} if r['entity']=='documents': adapter['clauses']=[{'documentId':r['id'],'clause':c['name'],'excerpt':c['excerpt'], 'excerptMethod':'HTML正文逐字摘录;XPath '+r['bodyExtractionSelector']+';非PDF逐字核验', 'applicationNote':c['application']} for c in r['clauses']] ap='raw/adapter-'+stem+'.json';save(BATCH/ap,adapter);sid='source-'+stem batch['sources'].append({'id':sid,'path':ap,'sha256':sha((BATCH/ap).read_bytes()), 'platform':r['platform'],'url':r['url'],'collectedAt':r['httpReceipt']['collectedAt']}) batch[r['entity']].append({'sourceId':sid,'pointer':'/record','fields':{k:'/'+k for k in record}}) for j,c in enumerate(adapter.get('clauses',[])): batch['clauses'].append({'sourceId':sid,'pointer':'/clauses/'+str(j),'fields':{k:'/'+k for k in c}}) source_metadata[ap]={'adapterPath':ap,'adapterSha256':sha((BATCH/ap).read_bytes()),'originalPath':op,'originalSha256':sha(original), 'topics':[t['topic'] for t in r['topics']],'topicEvidence':r['topics'],'sourceType':r['sourceType'],'reviewNote':r['reviewNote']} save(BATCH/'batch.json',batch) dry=structured_batch.build(BATCH/'batch.json',BATCH/'staging.sqlite',False);save(BATCH/'validation.json',dry) assert dry['ok'] and dry['networkRequests']==0 and dry['databaseWrites']==0 built=structured_batch.build(BATCH/'batch.json',BATCH/'staging.sqlite',True) with sqlite3.connect(BATCH/'staging.sqlite') as c: c.row_factory=sqlite3.Row for entity,table in [('social','social_posts'),('documents','policy_documents'),('clauses','policy_clauses')]: for record in c.execute('SELECT * FROM '+table): actual=dict(record) payload=json.loads(c.execute('SELECT payload_json FROM source_records WHERE id=?',(actual['source_record_id'],)).fetchone()[0]) ap=payload['source_file'];meta=source_metadata[ap] assert entity!='documents' or actual['attachment_hash_state']=='matched' assert entity!='social' or actual['fulltext_available']==1 expectations[entity].append({'entity':entity,'id':actual['id'],'record':actual,**meta}) transport,fingerprint=library_channel.pack(BATCH/'batch.json');(BATCH/'transport.zip').write_bytes(transport) receipt={'kind':'five_topic_reviewed_multisource','batchId':batch['batchId'],'archiveSha256':fingerprint,'archiveBytes':len(transport), 'accepted':{k:len(v) for k,v in expectations.items()},'products':[],'sourceTopics':dedup['countsByTopic'],'validation':built, 'createdAt':datetime.now(timezone.utc).isoformat(),'productionImported':False} save(BATCH/'receipt.json',receipt);save(BATCH/'expected-records.json',expectations) save(BATCH/'reviewed-source-index.json',selected) print(json.dumps({'ok':True,'batch':BATCH.name,'accepted':receipt['accepted'],'archiveBytes':len(transport)},ensure_ascii=False)) if __name__=='__main__':main()