package-industry-policy.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. """Freeze r9 from sealed r8 plus explicit policy application and source verification modules."""
  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-r8/package'
  13. RELEASE = 'ksf-industry-monitor-20261107-r9'
  14. SOURCES = ['src/pages/view-industry.js', 'src/pages/industry-research-views.js', 'src/pages/industry-policy-view.js',
  15. 'src/services/industry-service.js', 'server/voc-library/industry_monitor.py', 'server/voc-library/industry_research.py',
  16. 'server/voc-library/industry_policy.py', 'server/voc-library/monitoring_policy.py', 'server/voc-library/test_industry_policy.py',
  17. 'scripts/register-industry-policy.py', 'scripts/verify-industry-monitor.py', 'scripts/verify-industry-mainline.py',
  18. 'scripts/verify-industry-policy-release.py', 'scripts/verify-industry-policy-runtime.py']
  19. DOCUMENTS = ['agent-queries.md', 'deployment.md', '主线待决策清单.md', 'r9本地实现与未完成条件.md']
  20. def policy_view_from_baseline():
  21. """Keep concurrent innovation edits out of this independently tested policy release."""
  22. text = (BASE/'payload/src/pages/view-industry.js').read_text('utf-8')
  23. changes = [
  24. ("import { reviewsView, policiesView } from './industry-research-views.js';", "import { reviewsView, policiesView } from './industry-research-views.js';\nimport { POLICY_APPLICATION_CSS } from './industry-policy-view.js';"),
  25. (" for (const [route, key] of Object.entries({ policyPage: 'page', policySegment: 'segment' }))", " for (const [route, key] of Object.entries({ policyApplication: 'application', policyAsOf: 'asOf', policyMg: 'perServingMg', policyGrams: 'servingGrams', policyReference: 'referenceMgPer100g' })) if (this.routeQuery[route]) request.policyQuery[key] = this.routeQuery[route];\n for (const [route, key] of Object.entries({ policyPage: 'page', policySegment: 'segment' }))"),
  26. ("q: '', policyDoc: '', policyStatus: '', policyPage: '', policySegment: ''", "q: '', policyDoc: '', policyStatus: '', policyPage: '', policySegment: '', policyApplication: '', policyAsOf: '', policyMg: '', policyGrams: '', policyReference: ''"),
  27. ('[VIEW_CSS, INNOVATION_CSS, DETAIL_CSS, css`', '[VIEW_CSS, INNOVATION_CSS, DETAIL_CSS, POLICY_APPLICATION_CSS, css`'),
  28. ]
  29. for old, new in changes:
  30. if text.count(old) != 1:raise ValueError('R8_POLICY_PATCH_BASELINE_CHANGED')
  31. text = text.replace(old, new)
  32. return text.encode('utf-8')
  33. def sha(raw):return hashlib.sha256(raw).hexdigest()
  34. def save(path, data):path.write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n','utf-8')
  35. def build(out):
  36. if out.exists():raise ValueError('RELEASE_DIRECTORY_MUST_BE_NEW')
  37. for line in (BASE/'FILES.sha256').read_text('utf-8').splitlines():
  38. digest,name=line.split(' ',1)
  39. if sha((BASE/name).read_bytes())!=digest:raise ValueError('FROZEN_R8_CHANGED')
  40. captured={name:(ROOT/name).read_bytes() for name in SOURCES}
  41. workspace_view = captured['src/pages/view-industry.js']
  42. captured['src/pages/view-industry.js'] = policy_view_from_baseline()
  43. shutil.copytree(BASE,out)
  44. for name,raw in captured.items():
  45. target=out/('payload/'+name if name.startswith('src/') else name)
  46. target.parent.mkdir(exist_ok=True,parents=True);target.write_bytes(raw)
  47. if any((ROOT/name).read_bytes()!=(workspace_view if name=='src/pages/view-industry.js' else raw) for name,raw in captured.items()):raise ValueError('SOURCE_CHANGED_DURING_FREEZE')
  48. save(out/'evidence/r9-source-capture.json',{'release':RELEASE,'sourceFilesSha256':{name:sha(raw) for name,raw in captured.items()},'viewIndustrySource':'sealed r8 plus four explicit policy changes; concurrent workspace innovation edits excluded'})
  49. auxiliary=ROOT/'work/industry-monitoring-20261107-r1/monitoring-registry'
  50. with closing(sqlite3.connect((auxiliary/'monitoring.sqlite').resolve().as_uri()+'?mode=ro',uri=True)) as source:
  51. with closing(sqlite3.connect(out/'data/monitoring-registry/monitoring.sqlite')) as destination:
  52. source.backup(destination)
  53. destination.execute("UPDATE pipeline_workers SET state='ARCHIVED_COPY'")
  54. destination.execute("UPDATE dispatch_audits SET state='ARCHIVED_COPY'");destination.commit()
  55. for file in (auxiliary/'assets').iterdir():
  56. target=out/'data/monitoring-registry/assets'/file.name
  57. if not target.exists():shutil.copyfile(file,target)
  58. release=json.loads((out/'RELEASE.json').read_bytes())
  59. release.update(release=RELEASE,createdAt=datetime.now(timezone.utc).isoformat(),builtFromFrozenRelease='ksf-industry-monitor-20261107-r8',
  60. policyApplicationWorkflows=8,officialPolicyMetadataVerified=9,officialPolicyPdfShaMatches=9,
  61. officialPolicyFetchFailures=16,overallComplete=False,productionDeployed=False,paidCalls=0)
  62. release['staticFiles'].append('src/pages/industry-policy-view.js')
  63. release['backendFiles'].extend(['server/voc-library/industry_policy.py','server/voc-library/monitoring_policy.py'])
  64. save(out/'RELEASE.json',release)
  65. mpath=out/'deploy/industry-ui-files.json';manifest=json.loads(mpath.read_bytes());manifest['release']=RELEASE
  66. manifest['files'].append({'path':'src/pages/industry-policy-view.js','allowMissing':True,'priorNormalizedSha256':[],'observedProductionSha256':None})
  67. for entry in manifest['files']:
  68. old=BASE/'payload'/entry['path']
  69. if old.exists():
  70. digest=sha(old.read_bytes().replace(b'\r\n',b'\n'))
  71. if digest not in entry['priorNormalizedSha256']:entry['priorNormalizedSha256'].append(digest)
  72. entry['sha256']=sha((out/'payload'/entry['path']).read_bytes().replace(b'\r\n',b'\n'))
  73. save(mpath,manifest)
  74. publisher=out/'scripts/publish-industry-ui.mjs';text=publisher.read_text('utf-8').replace('ksf-industry-monitor-20261107-r8',RELEASE)
  75. before=next(line for line in text.splitlines() if line.startswith('export const FILES='))
  76. publisher.write_text(text.replace(before,'export const FILES=Object.freeze('+json.dumps(release['staticFiles'])+');'),'utf-8')
  77. for file in (out/'deploy').iterdir():
  78. if file.suffix=='.example':file.write_text(file.read_text('utf-8').replace('ksf-industry-monitor-20261107-r8',RELEASE),'utf-8')
  79. evidence=ROOT/'work/industry-policy-verification-20261107-r1'
  80. target=out/'evidence/r9-policy-sources';target.mkdir()
  81. for file in evidence.iterdir():
  82. if file.is_file() and file.suffix in ('.json','.html','.png') and file.name not in ('browser-0.json','browser-1.json','application-response.json','preview-response.html'):
  83. shutil.copyfile(file,target/file.name)
  84. (out/'docs/README.md').write_text('''# 方便面行业 VOC 后台 r9 本地验收版
  85. 在冻结 r8 上补齐 8 类政策/膳食条款应用流程、官方状态核验附账和营养算术换算。14 个前端模块、15 个后端文件。
  86. 9 份标准官方元数据与 PDF 已核,PDF SHA 全部与研究归档一致;另 16 份官方页面本次访问未核成,不据此判文件失效。原研究库声明与新核验记录分开。PDF/HTML 原件保存在辅助资产区,研究库不改写。
  87. 按用户最新要求先保留本机,全部条件完成前不请求转发。历史 r8 文档保留为来源,旧模块数量与路径不作为 r9 执行指令。原生运行及浏览器结果见 evidence/implementation-receipt-v9.json;本地通过不等于生产已上线。
  88. 付费保持停止,未自动安装上游定时任务。全量可访问评价、真实全榜及重复观察、持续采集接入、详情文字覆盖和生产主机验收仍分别待证,不写为全部完成。A/B/C 与核心库合并规则保持原状。
  89. ''','utf-8')
  90. for name in DOCUMENTS:
  91. shutil.copyfile(ROOT/'docs/industry-monitoring-20261107'/name, out/'docs'/name)
  92. print(json.dumps({'ok':True,'candidate':str(out),'staticFiles':len(release['staticFiles']),'backendFiles':len(release['backendFiles']),'sealed':False}))
  93. def seal(out):
  94. receipt=json.loads((out/'evidence/implementation-receipt-v9.json').read_bytes())
  95. if not receipt['accepted'] or not receipt['packageVerificationPassed']:raise ValueError('ACCEPTANCE_REQUIRED')
  96. for name,digest in receipt['testedFilesSha256'].items():
  97. if sha((out/name).read_bytes())!=digest:raise ValueError('TESTED_FILE_CHANGED')
  98. archive=out.parent/(RELEASE+'.zip')
  99. if archive.exists():raise ValueError('ARCHIVE_EXISTS')
  100. files=sorted(file for file in out.rglob('*') if file.is_file() and file!=out/'FILES.sha256')
  101. (out/'FILES.sha256').write_text(''.join(sha(file.read_bytes())+' '+file.relative_to(out).as_posix()+'\n' for file in files),'utf-8')
  102. with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED) as z:
  103. for file in sorted(out.rglob('*')):
  104. if file.is_file():z.write(file,file.relative_to(out).as_posix())
  105. print(json.dumps({'ok':True,'archive':str(archive),'bytes':archive.stat().st_size,'sha256':sha(archive.read_bytes()),'productionDeployed':False,'paidCalls':0}))
  106. if __name__=='__main__':
  107. parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--out',type=Path,required=True);parser.add_argument('--seal',action='store_true')
  108. args=parser.parse_args();(seal if args.seal else build)(args.out.resolve())