| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- """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()
|