package-industry-publications.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. """Freeze r11 on sealed r10 with publication identity checks and a concrete ops handoff."""
  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-r10/package'
  13. WORK=ROOT/'work/industry-mainline-finish-20261107-r1'
  14. RELEASE='ksf-industry-monitor-20261107-r11'
  15. SOURCES=['server/voc-library/policy_publications.py','server/voc-library/test_policy_publications.py',
  16. 'server/voc-library/monitoring_policy.py','server/voc-library/industry_policy.py',
  17. 'src/pages/industry-policy-view.js','scripts/register-industry-policy.py',
  18. 'scripts/verify-industry-monitor.py','scripts/verify-industry-publications-release.py',
  19. 'scripts/package-industry-publications.py']
  20. DOCS=['deployment.md','agent-queries.md','主线待决策清单.md','发给VOC采集方-只读核查与报价.md',
  21. '交给运维001-主线接通执行单.md','r11正式环境核通与政策发布核验.md']
  22. def sha(raw):return hashlib.sha256(raw).hexdigest()
  23. def save(path,value):
  24. path.parent.mkdir(parents=True,exist_ok=True)
  25. path.write_text(json.dumps(value,ensure_ascii=False,indent=2)+'\n','utf-8')
  26. def build(out):
  27. if out.exists():raise ValueError('RELEASE_DIRECTORY_MUST_BE_NEW')
  28. for line in (BASE/'FILES.sha256').read_text('utf-8').splitlines():
  29. digest,name=line.split(' ',1)
  30. if sha((BASE/name).read_bytes())!=digest:raise ValueError('FROZEN_R10_CHANGED')
  31. captured={name:(ROOT/name).read_bytes() for name in SOURCES}
  32. shutil.copytree(BASE,out)
  33. for name,raw in captured.items():
  34. target=out/('payload/'+name if name.startswith('src/') else name)
  35. target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(raw)
  36. if any((ROOT/name).read_bytes()!=raw for name,raw in captured.items()):raise ValueError('SOURCE_CHANGED_DURING_FREEZE')
  37. save(out/'evidence/r11-source-capture.json',{'release':RELEASE,'sourceFilesSha256':{n:sha(v) for n,v in captured.items()},
  38. 'viewIndustrySource':'sealed r10; concurrent workspace innovation edits excluded'})
  39. store=ROOT/'work/industry-monitoring-20261107-r1/monitoring-registry'
  40. with closing(sqlite3.connect((store/'monitoring.sqlite').resolve().as_uri()+'?mode=ro',uri=True)) as source:
  41. with closing(sqlite3.connect(out/'data/monitoring-registry/monitoring.sqlite')) as target:
  42. source.backup(target);target.execute("UPDATE pipeline_workers SET state='ARCHIVED_COPY'")
  43. target.execute("UPDATE dispatch_audits SET state='ARCHIVED_COPY'");target.commit()
  44. for file in (store/'assets').iterdir():
  45. if sha(file.read_bytes())!=file.stem:raise ValueError('AUXILIARY_ASSET_CHANGED')
  46. target=out/'data/monitoring-registry/assets'/file.name
  47. if not target.exists():shutil.copyfile(file,target)
  48. for name in ['current-production-probe.json','configured-existing-data-counts.json','official-probe.json']:
  49. shutil.copyfile(WORK/name,out/'evidence'/('r11-'+name))
  50. for name in DOCS:shutil.copyfile(ROOT/'docs/industry-monitoring-20261107'/name,out/'docs'/name)
  51. release=json.loads((out/'RELEASE.json').read_bytes())
  52. release.update(release=RELEASE,createdAt=datetime.now(timezone.utc).isoformat(),
  53. builtFromFrozenRelease='ksf-industry-monitor-20261107-r10',policyPublicationIdentityVerified=4,
  54. archivedPublicationObservedDate='2026-11-04',nativePython39Verified=False,
  55. productionApplicationConnectionVerified=True,productionIndustryEndpointStatus=404,
  56. productionDeploymentExecutor='运维001',overallComplete=False,productionDeployed=False)
  57. release['backendFiles'].append('server/voc-library/policy_publications.py');save(out/'RELEASE.json',release)
  58. path=out/'deploy/industry-ui-files.json';manifest=json.loads(path.read_bytes());manifest['release']=RELEASE
  59. for entry in manifest['files']:
  60. previous=sha((BASE/'payload'/entry['path']).read_bytes().replace(b'\r\n',b'\n'))
  61. if previous not in entry['priorNormalizedSha256']:entry['priorNormalizedSha256'].append(previous)
  62. entry['sha256']=sha((out/'payload'/entry['path']).read_bytes().replace(b'\r\n',b'\n'))
  63. save(path,manifest)
  64. publisher=out/'scripts/publish-industry-ui.mjs'
  65. publisher.write_text(publisher.read_text('utf-8').replace('ksf-industry-monitor-20261107-r10',RELEASE),'utf-8')
  66. for file in (out/'deploy').iterdir():
  67. if file.suffix=='.example':file.write_text(file.read_text('utf-8').replace('ksf-industry-monitor-20261107-r10',RELEASE),'utf-8')
  68. (out/'docs/README.md').write_text('''# 方便面行业VOC r11 本地验收与运维接通版
  69. 以封存r10为基础,新增公告/膳食指导/问答发布原文核验,14个前端模块、17个独立后端文件。4份历史发布原文已按研究库SHA核实,9份标准状态另列;不把历史发布证明写成当前有效性。
  70. 正式应用API已独立核通,研究接口正常;行业接口当前404。用户指定部署执行者为运维001。请先读《交给运维001-主线接通执行单.md》并按deployment.md受控执行,旧版本文档/回执仅作来源,不重复发布。当前验收见implementation-receipt-v11.json。
  71. 本包不含应用配置、密钥、私有详情原件或活动研究库。主线全量数据、榜单、持续监听及生产上线仍分别待验,overallComplete=false。收费保持停止;原¥46.20已扣、¥3.80保底保留。封存r5至r10不变。
  72. ''','utf-8')
  73. print(json.dumps({'ok':True,'candidate':str(out),'staticFiles':14,'backendFiles':17,'sealed':False}))
  74. def seal(out):
  75. receipt=json.loads((out/'evidence/implementation-receipt-v11.json').read_bytes())
  76. if not receipt['accepted']:raise ValueError('ACCEPTANCE_REQUIRED')
  77. for name,digest in receipt['testedFilesSha256'].items():
  78. if sha((out/name).read_bytes())!=digest:raise ValueError('TESTED_FILE_CHANGED')
  79. archive=out.parent/(RELEASE+'.zip')
  80. if archive.exists():raise ValueError('ARCHIVE_EXISTS')
  81. files=sorted(f for f in out.rglob('*') if f.is_file() and f!=out/'FILES.sha256')
  82. (out/'FILES.sha256').write_text(''.join(sha(f.read_bytes())+' '+f.relative_to(out).as_posix()+'\n' for f in files),'utf-8')
  83. with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED) as z:
  84. for f in sorted(out.rglob('*')):
  85. if f.is_file():z.write(f,f.relative_to(out).as_posix())
  86. print(json.dumps({'ok':True,'archive':str(archive),'bytes':archive.stat().st_size,'sha256':sha(archive.read_bytes()),'productionDeployed':False,'paidCalls':0}))
  87. if __name__=='__main__':
  88. p=argparse.ArgumentParser(description=__doc__);p.add_argument('--out',type=Path,required=True);p.add_argument('--seal',action='store_true')
  89. a=p.parse_args();(seal if a.seal else build)(a.out.resolve())