package-industry-recovery.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """Freeze historical recovery additions on sealed r9; never touch a deployed service."""
  2. import argparse
  3. from contextlib import closing
  4. from datetime import datetime, timezone
  5. import hashlib
  6. import json
  7. from pathlib import Path
  8. import shutil
  9. import sqlite3
  10. import zipfile
  11. ROOT = Path(__file__).resolve().parents[1]
  12. BASE = ROOT / 'artifacts/ksf-industry-monitor-20261107-r9/package'
  13. WORK = ROOT / 'work/industry-mainline-continuation-20261107-r1'
  14. RELEASE = 'ksf-industry-monitor-20261107-r10'
  15. SOURCES = ['src/pages/industry-pipeline-view.js', 'server/voc-library/monitoring_ledger.py',
  16. 'server/voc-library/monitoring_dispatch.py', 'server/voc-library/test_monitoring_ledger.py',
  17. 'scripts/manage-industry-dispatch.py', 'scripts/prepare-industry-handoff.py',
  18. 'scripts/audit-industry-archived-details.py', 'scripts/reuse-industry-archived-details.py',
  19. 'scripts/verify-industry-monitor.py', 'scripts/verify-industry-recovery-release.py',
  20. 'scripts/verify-industry-recovered-data.py', 'scripts/package-industry-recovery.py']
  21. DOCUMENTS = ['agent-queries.md', 'deployment.md', 'request-guard.md', '主线待决策清单.md',
  22. 'r10本地实现与未完成条件.md']
  23. def sha(raw): return hashlib.sha256(raw).hexdigest()
  24. def save(path, value):
  25. path.parent.mkdir(parents=True, exist_ok=True)
  26. path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', 'utf-8')
  27. def build(out):
  28. if out.exists(): raise ValueError('RELEASE_DIRECTORY_MUST_BE_NEW')
  29. for line in (BASE/'FILES.sha256').read_text('utf-8').splitlines():
  30. digest, name = line.split(' ', 1)
  31. if sha((BASE/name).read_bytes()) != digest: raise ValueError('FROZEN_R9_CHANGED')
  32. captured = {name:(ROOT/name).read_bytes() for name in SOURCES}
  33. shutil.copytree(BASE, out)
  34. for name, raw in captured.items():
  35. target = out/('payload/'+name if name.startswith('src/') else name)
  36. target.parent.mkdir(parents=True, exist_ok=True); target.write_bytes(raw)
  37. if any((ROOT/name).read_bytes() != raw for name, raw in captured.items()):
  38. raise ValueError('SOURCE_CHANGED_DURING_FREEZE')
  39. save(out/'evidence/r10-source-capture.json', {'release':RELEASE,
  40. 'sourceFilesSha256':{name:sha(raw) for name, raw in captured.items()},
  41. 'viewIndustrySource':'sealed r9; concurrent workspace innovation edits excluded'})
  42. store = ROOT/'work/industry-monitoring-20261107-r1/monitoring-registry'
  43. with closing(sqlite3.connect((store/'monitoring.sqlite').resolve().as_uri()+'?mode=ro', uri=True)) as source:
  44. with closing(sqlite3.connect(out/'data/monitoring-registry/monitoring.sqlite')) as destination:
  45. source.backup(destination)
  46. destination.execute("UPDATE pipeline_workers SET state='ARCHIVED_COPY'")
  47. destination.execute("UPDATE dispatch_audits SET state='ARCHIVED_COPY'"); destination.commit()
  48. for file in (store/'assets').iterdir():
  49. if sha(file.read_bytes()) != file.stem: raise ValueError('AUXILIARY_ASSET_CHANGED')
  50. target = out/'data/monitoring-registry/assets'/file.name
  51. if not target.exists(): shutil.copyfile(file, target)
  52. # Replace only the bounded scope; preserve all sealed source evidence and receipts.
  53. for file in (WORK/'handoff').iterdir():
  54. if file.is_file(): shutil.copyfile(file, out/'handoff'/file.name)
  55. target = out/'evidence/r10-recovery'; target.mkdir()
  56. for name in ['ledger-import.json', 'detail-reuse-receipt.json', 'detail-reuse-replay.json', 'connector-capabilities.json']:
  57. shutil.copyfile(WORK/name, target/name)
  58. inventory = json.loads((WORK/'archived-details/inventory.json').read_bytes())
  59. save(target/'archive-inventory-summary.json', {k:v for k,v in inventory.items()
  60. if k not in ('details','unboundDetailCandidates','rankingEndpointCandidates')})
  61. save(target/'public-access-status.json', {'rankingUrlSource':'verified historical JD detail rankInfo h5JumpUrl',
  62. 'ordinaryFreshBrowserTitle':'京东登录注册','loginRequired':True,'loginAttempted':False,
  63. 'challengeHandling':False,'completeTop100Or400':0,'newOfficialMetadataVerified':0,
  64. 'paidCalls':0,'privateBrowserDiagnosticsIncluded':False})
  65. release = json.loads((out/'RELEASE.json').read_bytes())
  66. release.update(release=RELEASE,createdAt=datetime.now(timezone.utc).isoformat(),
  67. builtFromFrozenRelease='ksf-industry-monitor-20261107-r9',
  68. settledCloseoutIdentifiedRows=154,parameterBoundSettledRows=139,
  69. unboundSettledParameterRows=15,serverLedgerRefetched=False,
  70. detailObservations=52,productsWithDetailText=13,primaryDetailsWithResponse=41,
  71. primaryDetailsWithText=12,nativePython39Verified=False,
  72. overallComplete=False,productionDeployed=False,paidCalls=0)
  73. release['backendFiles'].append('server/voc-library/monitoring_ledger.py')
  74. save(out/'RELEASE.json',release)
  75. mpath = out/'deploy/industry-ui-files.json'; manifest = json.loads(mpath.read_bytes()); manifest['release']=RELEASE
  76. for entry in manifest['files']:
  77. old = sha((BASE/'payload'/entry['path']).read_bytes().replace(b'\r\n',b'\n'))
  78. if old not in entry['priorNormalizedSha256']: entry['priorNormalizedSha256'].append(old)
  79. entry['sha256']=sha((out/'payload'/entry['path']).read_bytes().replace(b'\r\n',b'\n'))
  80. save(mpath,manifest)
  81. publisher=out/'scripts/publish-industry-ui.mjs'
  82. publisher.write_text(publisher.read_text('utf-8').replace('ksf-industry-monitor-20261107-r9',RELEASE),'utf-8')
  83. for file in (out/'deploy').iterdir():
  84. if file.suffix=='.example':
  85. file.write_text(file.read_text('utf-8').replace('ksf-industry-monitor-20261107-r9',RELEASE),'utf-8')
  86. for name in DOCUMENTS: shutil.copyfile(ROOT/'docs/industry-monitoring-20261107'/name,out/'docs'/name)
  87. (out/'docs/README.md').write_text('''# 方便面行业 VOC 后台 r10 本地验收版
  88. 当前版本基于封存r9,补齐154行附件账本身份对齐、139行商品页防重核对,以及历史详情响应复用。14个前端模块、16个独立后端文件。
  89. 主店165件中41件有详情响应、12件有商品文字卖点;全库52/13。历史观察不冒充新采集,宣称不冒充实证。政策8类用法和创新工作区沿用已验版本。当前细节见r10本地实现与未完成条件.md。
  90. 本包没有活动研究库、私有详情原件、完整账本ZIP、登录页诊断或凭据。历史文档/回执仅保留出处,当前执行入口和数量以本页、RELEASE与implementation-receipt-v10.json为准。
  91. 当前未部署生产、未恢复付费采集、未转发。全量当前目录/评价、完整Top100/400与跨期监听、16份官方状态及生产接通仍有真实外部缺口,overallComplete=false。封存r5至r9不变。
  92. ''','utf-8')
  93. print(json.dumps({'ok':True,'candidate':str(out),'staticFiles':14,'backendFiles':16,'sealed':False}))
  94. def seal(out):
  95. receipt=json.loads((out/'evidence/implementation-receipt-v10.json').read_bytes())
  96. if not receipt['accepted'] or not receipt['packageVerificationPassed']: raise ValueError('ACCEPTANCE_REQUIRED')
  97. for name, digest in receipt['testedFilesSha256'].items():
  98. if sha((out/name).read_bytes())!=digest: raise ValueError('TESTED_FILE_CHANGED')
  99. archive=out.parent/(RELEASE+'.zip')
  100. if archive.exists(): raise ValueError('ARCHIVE_EXISTS')
  101. files=sorted(f for f in out.rglob('*') if f.is_file() and f!=out/'FILES.sha256')
  102. (out/'FILES.sha256').write_text(''.join(sha(f.read_bytes())+' '+f.relative_to(out).as_posix()+'\n' for f in files),'utf-8')
  103. with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED) as z:
  104. for f in sorted(out.rglob('*')):
  105. if f.is_file(): z.write(f,f.relative_to(out).as_posix())
  106. print(json.dumps({'ok':True,'archive':str(archive),'bytes':archive.stat().st_size,
  107. 'sha256':sha(archive.read_bytes()),'productionDeployed':False,'paidCalls':0}))
  108. if __name__=='__main__':
  109. p=argparse.ArgumentParser(description=__doc__);p.add_argument('--out',type=Path,required=True);p.add_argument('--seal',action='store_true')
  110. a=p.parse_args();(seal if a.seal else build)(a.out.resolve())