verify-handoff-merge.py 5.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. """Exercise the real handoff against a local baseline; publish only to a NEW test registry."""
  2. import argparse
  3. import json
  4. from pathlib import Path
  5. import sys
  6. ROOT=Path(__file__).resolve().parents[1]
  7. sys.path.insert(0,str(ROOT/'server/voc-library'))
  8. import retrieval
  9. import schema_catalog
  10. import update_library as u
  11. import voc_db as v
  12. def verify(baseline,incoming,out):
  13. v.require(not out.exists() and not out.is_symlink(),'NEW_TEST_DIRECTORY_REQUIRED')
  14. fingerprints={str(p):u.digest(p) for p in (baseline,incoming)}
  15. out.mkdir(parents=True,mode=0o700)
  16. plan=u.merge_plan(baseline,incoming)
  17. registry=out/'isolated-registry'
  18. first=u.publish(baseline,registry,True,label='仅本地验收基线')
  19. actual=u.publish(incoming,registry,True,label='仅本地真实样本合并',expected_generation=first['generation'])
  20. current=v.registry_database(registry)
  21. checks={'planMatchesApply':plan['delta']==actual['delta'] and plan['plannedGeneration']==actual['generation'],
  22. 'inputsUnchanged':all(u.digest(p)==h for p,h in fingerprints.items())}
  23. with v.readonly(baseline) as old,v.readonly(incoming) as inc,v.readonly(current) as new:
  24. checks['foreignKeysValid']=not bool(new.execute('PRAGMA foreign_key_check').fetchall())
  25. checks['schemaComplete']=schema_catalog.catalog(new)['complete']
  26. lost=[];preserved=0
  27. for table in ('products','reviews'):
  28. for row in inc.execute('SELECT * FROM '+table):
  29. prior=old.execute('SELECT * FROM '+table+' WHERE id=?',(row['id'],)).fetchone()
  30. if not prior:continue
  31. selected=new.execute('SELECT * FROM '+table+' WHERE id=?',(row['id'],)).fetchone()
  32. policy,reason=u.choose_record(table,dict(prior),dict(row))
  33. if policy=='kept_existing':
  34. preserved+=1
  35. if dict(selected)!=dict(prior):lost.append({'table':table,'id':row['id']})
  36. item=v.get_record(new,table,row['id'])['record']
  37. checks['historyReadable']=bool(item.get('mergeHistory')) and checks.get('historyReadable',True)
  38. for decision in item['mergeHistory']:
  39. for field in ('prior_source_record_id','incoming_source_record_id','selected_source_record_id'):
  40. v.get_source(new,decision[field])
  41. checks['retainedRowsMatchOriginalBytes']=not bool(lost)
  42. stats={}
  43. for label,c in [('before',old),('after',new)]:
  44. stats[label]={'counts':v.table_counts(c)}
  45. for quality in ('analysis','audit'):
  46. result=retrieval.search(c,{'entities':['reviews'],'quality':quality,'limit':1})
  47. stats[label][quality+'Reviews']=result['totalMatched']
  48. for entity in ('products','reviews'):
  49. r=retrieval.search(c,{'entities':[entity],'filters':{'platform':'jd','brand':'ksf','category':'banmian'},'quality':'analysis','limit':1})
  50. stats[label]['strictJdKsfBanmian'+entity.title()]=r['totalMatched']
  51. checks['existingAnalysisCoveragePreserved']=stats['before']['analysisReviews']==stats['after']['analysisReviews']
  52. checks['strictReviewBaselinePreserved']=stats['before']['strictJdKsfBanmianReviews']==stats['after']['strictJdKsfBanmianReviews']
  53. counts=[dict(r) for r in new.execute('SELECT decision,count(*) AS count FROM merge_decisions GROUP BY decision')]
  54. conflicts=[dict(r) for r in new.execute("SELECT entity,record_id,reason_codes_json FROM merge_decisions WHERE reason_codes_json LIKE '%IDENTITY_OR_FACT_CONFLICT%' ORDER BY entity,record_id")]
  55. for r in conflicts:r['reasons']=json.loads(r.pop('reason_codes_json'))
  56. for source in new.execute("SELECT id FROM source_records WHERE id IN (SELECT incoming_source_record_id FROM merge_decisions)"):
  57. citation=v.get_source(new,source['id'])['citation']
  58. v.require(bool(citation['archiveSha256']) and all(x['resolution']=='matched' for x in citation['upstreamSources']),'SOURCE_READBACK_FAILED')
  59. pointer=(registry/'current.json').read_bytes()
  60. replay=u.publish(incoming,registry,True)
  61. checks['replayDoesNotReimport']=not replay['applied'] and pointer==(registry/'current.json').read_bytes()
  62. checks['finalInputsUnchanged']=all(u.digest(p)==h for p,h in fingerprints.items())
  63. v.require(all(checks.values()),'REAL_HANDOFF_MERGE_CHECK_FAILED:'+str(checks))
  64. report={'ok':True,'scope':'local-preview-and-isolated-registry-only','productionImported':False,
  65. 'networkRequests':0,'collectionCostMinor':0,'baseline':{'path':str(baseline),'sha256':fingerprints[str(baseline)]},
  66. 'incoming':{'path':str(incoming),'sha256':fingerprints[str(incoming)]},'mergePolicy':u.MERGE_POLICY,
  67. 'checks':checks,'statistics':stats,'preservedExistingRows':preserved,'decisions':counts,
  68. 'conflictsForReview':conflicts,'plan':plan,'isolatedPublication':actual,'replay':replay}
  69. with (out/'verification.json').open('x',encoding='utf-8') as f:json.dump(report,f,ensure_ascii=False,indent=2);f.write('\n')
  70. return report
  71. if __name__=='__main__':
  72. p=argparse.ArgumentParser(description=__doc__);p.add_argument('--baseline-db',type=Path,required=True)
  73. p.add_argument('--incoming',type=Path,required=True);p.add_argument('--out',type=Path,required=True);a=p.parse_args()
  74. report=verify(a.baseline_db.absolute(),a.incoming.absolute(),a.out.absolute())
  75. print(json.dumps({k:report[k] for k in ('ok','checks','statistics','decisions','mergePolicy','productionImported')},ensure_ascii=False,indent=2))