finalize-library-cleanup.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. """Reconcile both sealed publications against the original inventory."""
  2. from collections import Counter
  3. from datetime import datetime,timezone
  4. import hashlib
  5. import json
  6. from pathlib import Path
  7. ROOT=Path(__file__).resolve().parents[1];WORK=ROOT/'work/library-cleanup-20261111'
  8. def load(p):return json.loads(p.read_bytes())
  9. def unwrap(v):return v['body']['result']
  10. before=unwrap(load(WORK/'inventory-result.json'))
  11. after=unwrap(load(WORK/'round-02/inventory-after-result.json'))
  12. plans=[load(d/'sealed-plan.json')['plan'] for d in [WORK,WORK/'round-02']]
  13. checks=[load(d/'verification-result.json') for d in [WORK,WORK/'round-02']]
  14. browser=load(WORK/'browser-final/report.json')
  15. assert all(c['ok'] for c in checks) and browser['ok']
  16. assert checks[-1]['generation']==after['generation']
  17. removals=[r for p in plans for r in p['removals']]
  18. assert len({(r['entity'],r['id']) for r in removals})==len(removals)
  19. def typed(row,entity,manifest):
  20. fields=next(t['columns'] for t in manifest['tables'] if t['name']==('social_posts' if entity=='social' else entity))
  21. result={k:v for k,v in row.items() if k not in ('_ksf_source_rowid','payload_sha256','source_payload')}
  22. for f in fields:
  23. if result.get(f['name']) is not None and 'INT' in f['type'].upper():result[f['name']]=int(result[f['name']])
  24. return result
  25. retained={}
  26. for entity in ['products','social']:
  27. removed={r['id'] for r in removals if r['entity']==entity}
  28. repairs={r['id']:r['changes'] for p in plans for r in p['repairs'] if r['entity']==entity and r['id'] not in removed}
  29. actual={r['id']:typed(r,entity,after['manifest']) for r in after[entity]}
  30. for raw in before[entity]:
  31. if raw['id'] in removed:assert raw['id'] not in actual;continue
  32. expected=typed(raw,entity,before['manifest']);expected.update(repairs.get(raw['id'],{}))
  33. assert expected==actual[raw['id']],raw['id']
  34. assert len(actual)==len(before[entity])-len(removed)
  35. retained[entity]=len(actual)
  36. social_removed={r['id'] for r in removals if r['entity']=='social'}
  37. retained_repairs={r['id'] for p in plans for r in p['repairs']}-social_removed
  38. for table in ['assets','source_records','source_links','source_origins','collection_requests','prices','policy_documents','policy_clauses','ranking_signals']:
  39. a=next(t for t in after['manifest']['tables'] if t['name']==table)
  40. b=next(t for t in before['manifest']['tables'] if t['name']==table)
  41. assert a==b,table
  42. receipt={'schema':'ksf.library-cleanup.final.v1','ok':True,'checkedAt':datetime.now(timezone.utc).isoformat(),
  43. 'baseGeneration':before['generation'],'generation':after['generation'],
  44. 'scannedProducts':len(before['products']),'scannedSocial':len(before['social']),
  45. 'removedByEntity':dict(Counter(r['entity'] for r in removals)),
  46. 'removedByReason':dict(Counter(r['reason'] for r in removals)),
  47. 'retainedFulltextCorrections':len(retained_repairs),'correctionOperations':sum(len(p['repairs']) for p in plans),
  48. 'correctedThenRemoved':sum(len(p['repairs']) for p in plans)-len(retained_repairs),
  49. 'protectedArticles':63,'retainedRecordsVerified':retained,'allRowsVerified':True,'browserVerified':True,'browserScenes':len(browser['scenes']),
  50. 'removedDependentReviews':150,'removedDependentSpecs':101,'sourceRecordsPreserved':20581,
  51. 'historicalGenerationsPreserved':True,'publicCounts':checks[-1]['counts'],
  52. 'limits':['本轮清理的是来源证据可确认的无关或无内容记录;未把访问失败、缺价格/图片/评论等同于无意义。',
  53. '有具体产品/配方/工艺参考价值但尚未取得正文的资料仍以仅标题线索保留,不能作为正文证据。']}
  54. (WORK/'final-receipt.json').write_text(json.dumps(receipt,ensure_ascii=False,indent=2),encoding='utf8')
  55. lines=['# 行业资料库清理验收','',f"核验时间:{receipt['checkedAt']}",'',
  56. '| 项目 | 清理前 | 清理后 | 本轮移除 |','|---|---:|---:|---:|',
  57. '| 商品与竞品 | 4,674 | 4,580 | 94 |','| 社媒内容 | 3,877 | 3,795 | 82 |',
  58. '| 关联电商评价 | 6,772 | 6,622 | 150 |','| 关联规格 | 5,737 | 5,636 | 101 |','',
  59. '商品清理:93条独立饮料、1条邮费链接。社媒清理:游戏/影视/歌曲/手工/明星片段及无有效内容的泛标题或挑战编号。',
  60. '当前保留资料中,1,258条正文标记已纠正为“仅标题线索”,不再把占位符、重复标题、纯链接和标签当正文。',
  61. '共执行1,272次正文标记纠正,其中14条在第二次复核后确认无意义并移除,以上统计不重复相加。','',
  62. '用户举例 bilibili:BV11ENX6BE6p 已移除;公开接口查询为0条,详情显示“未找到这条资料”。',
  63. '全量保留商品和社媒逐字段核对通过;两轮发布均全表SHA回读通过,关联外键无孤儿;63篇专项新资料完整保留。',
  64. '原始来源20,581条、来源文件7,450件、来源关联32,737条及历史代际保留;价格、政策和榜单原行未变。',
  65. '真实线上浏览器6场景通过:商品目录、移动社媒目录、删除社媒详情、删除商品详情、正文标记纠正、专项文章原文。未捕获异常0,横向溢出0。','',
  66. f"当前代际:`{after['generation']}`",'',*['- '+s for s in receipt['limits']]]
  67. (WORK/'清理结果.md').write_text('\n'.join(lines)+'\n',encoding='utf8')
  68. print(json.dumps({k:receipt[k] for k in ['ok','generation','removedByEntity','retainedFulltextCorrections','retainedRecordsVerified','browserScenes']},ensure_ascii=False))