package-industry-mainline.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. """Build one frozen r8 containing innovation, detail evidence, pipeline and request guard."""
  2. import argparse
  3. from datetime import datetime, timezone
  4. import difflib
  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-r7/package'
  13. WORK = ROOT / 'work/industry-monitoring-20261107-r1'
  14. RELEASE = 'ksf-industry-monitor-20261107-r8'
  15. SOURCES = [
  16. 'src/pages/view-industry.js', 'src/pages/industry-innovation-view.js', 'src/pages/industry-detail-view.js',
  17. 'src/core/industry-brief.js', 'src/core/industry-materials.js', 'src/services/industry-service.js',
  18. 'server/voc-library/industry_monitor.py', 'server/voc-library/industry_innovation.py',
  19. 'server/voc-library/monitoring_details.py', 'server/voc-library/test_industry_innovation.py',
  20. 'server/voc-library/test_monitoring_details.py', 'test/industry-brief.test.js',
  21. 'scripts/verify-industry-install.py', 'scripts/prepare-industry-handoff.py',
  22. 'scripts/verify-industry-monitor.py', 'scripts/verify-industry-mainline.py',
  23. 'scripts/verify-industry-mainline-release.py',
  24. ]
  25. def sha(raw):
  26. return hashlib.sha256(raw).hexdigest()
  27. def save_json(path, value):
  28. path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', 'utf-8')
  29. def build(out):
  30. if out.exists():
  31. raise ValueError('RELEASE_DIRECTORY_MUST_BE_NEW')
  32. for line in (BASE / 'FILES.sha256').read_text('utf-8').splitlines():
  33. digest, name = line.split(' ', 1)
  34. if sha((BASE / name).read_bytes()) != digest:
  35. raise ValueError('FROZEN_R7_CHANGED')
  36. # Capture only the named modules; shared navigation and core VOC remain frozen r7.
  37. captured = {name: (ROOT / name).read_bytes() for name in SOURCES}
  38. shutil.copytree(BASE, out)
  39. patch = []
  40. for name, raw in captured.items():
  41. target_name = ('payload/' + name) if name.startswith('src/') else name
  42. target = out / target_name
  43. target.parent.mkdir(parents=True, exist_ok=True)
  44. before = target.read_text('utf-8') if target.is_file() else ''
  45. after = raw.decode('utf-8')
  46. target.write_bytes(raw)
  47. patch.extend(difflib.unified_diff(before.splitlines(True), after.splitlines(True),
  48. fromfile='r7/' + target_name, tofile='r8/' + target_name))
  49. # Test imports follow the deployable payload layout inside a release archive.
  50. test_path = out / 'test/industry-brief.test.js'
  51. test_path.write_text(test_path.read_text('utf-8').replace('../src/', '../payload/src/'), 'utf-8')
  52. if any((ROOT / name).read_bytes() != raw for name, raw in captured.items()):
  53. raise ValueError('SOURCE_CHANGED_DURING_FREEZE')
  54. save_json(out / 'evidence/r8-source-capture.json', {'release': RELEASE,
  55. 'sourceFilesSha256': {name: sha(raw) for name, raw in captured.items()},
  56. 'builtFromFrozenRelease': 'ksf-industry-monitor-20261107-r7'})
  57. (out / 'evidence/r8-integration.patch').write_text(''.join(patch), 'utf-8')
  58. handoff = ROOT / 'work/industry-mainline-handoff-20261107-r1'
  59. shutil.copytree(handoff, out / 'handoff')
  60. for name in ('detail-endpoint-proof-receipt.json',):
  61. shutil.copyfile(WORK / name, out / 'evidence' / name)
  62. for name in ('发给VOC采集方-只读核查与报价.md', '主线待决策清单.md', '运维001-r8上线执行单.md', 'r8交付与待接通事项.md'):
  63. shutil.copyfile(ROOT / 'docs/industry-monitoring-20261107' / name, out / 'docs' / name)
  64. # A fresh auxiliary copy carries existing history plus the appended endpoint provenance.
  65. auxiliary = WORK / 'monitoring-registry'
  66. with sqlite3.connect((auxiliary / 'monitoring.sqlite').resolve().as_uri() + '?mode=ro', uri=True) as c:
  67. with sqlite3.connect(out / 'data/monitoring-registry/monitoring.sqlite') as dest:
  68. c.backup(dest)
  69. dest.execute("UPDATE pipeline_workers SET state='ARCHIVED_COPY'")
  70. dest.execute("UPDATE dispatch_audits SET state='ARCHIVED_COPY'")
  71. dest.commit()
  72. for source in (auxiliary / 'assets').iterdir():
  73. target = out / 'data/monitoring-registry/assets' / source.name
  74. if not target.exists():
  75. shutil.copyfile(source, target)
  76. if list(out.rglob('detail-originals')):
  77. raise ValueError('PRIVATE_ORIGINALS_MUST_NOT_BE_INCLUDED')
  78. release = json.loads((out / 'RELEASE.json').read_bytes())
  79. release.update(release=RELEASE, createdAt=datetime.now(timezone.utc).isoformat(),
  80. builtFromFrozenRelease='ksf-industry-monitor-20261107-r7',
  81. innovationWorkspaceImplemented=True, agentCopyMaterialsImplemented=True,
  82. unifiedMainlinePackage=True, overallComplete=False,
  83. nativePython39Verified=False, productionDeployed=False, paidCalls=0)
  84. for name in ('src/pages/industry-innovation-view.js', 'src/core/industry-brief.js', 'src/core/industry-materials.js'):
  85. if name not in release['staticFiles']:
  86. release['staticFiles'].append(name)
  87. if 'server/voc-library/industry_innovation.py' not in release['backendFiles']:
  88. release['backendFiles'].append('server/voc-library/industry_innovation.py')
  89. save_json(out / 'RELEASE.json', release)
  90. path = out / 'deploy/industry-ui-files.json'
  91. manifest = json.loads(path.read_bytes())
  92. manifest['release'] = RELEASE
  93. for name in release['staticFiles']:
  94. if not any(e['path'] == name for e in manifest['files']):
  95. manifest['files'].append({'path': name, 'allowMissing': True,
  96. 'priorNormalizedSha256': [], 'observedProductionSha256': None})
  97. for entry in manifest['files']:
  98. previous = BASE / 'payload' / entry['path']
  99. if previous.exists():
  100. old_sha = sha(previous.read_bytes().replace(b'\r\n', b'\n'))
  101. if old_sha not in entry['priorNormalizedSha256']:
  102. entry['priorNormalizedSha256'].append(old_sha)
  103. entry['sha256'] = sha((out / 'payload' / entry['path']).read_bytes().replace(b'\r\n', b'\n'))
  104. save_json(path, manifest)
  105. publisher = out / 'scripts/publish-industry-ui.mjs'
  106. script = publisher.read_text('utf-8').replace('ksf-industry-monitor-20261107-r7', RELEASE)
  107. old = next(line for line in script.splitlines() if line.startswith('export const FILES='))
  108. script = script.replace(old, 'export const FILES=Object.freeze(' + json.dumps(release['staticFiles']) + ');')
  109. publisher.write_text(script, 'utf-8')
  110. for path in (out / 'deploy').iterdir():
  111. if path.suffix == '.example':
  112. path.write_text(path.read_text('utf-8').replace('ksf-industry-monitor-20261107-r7', RELEASE), 'utf-8')
  113. (out / 'docs/README.md').write_text('''# 方便面行业VOC后台统一r8交付
  114. 本包整合创新分析工作区、员工复制给Agent的分析资料、详情原文证据、评价/政策/榜单查询、分页接收与停费核对。已实现与本地可验证的功能统一交付;真实全量采集、上游持续调度、商品全榜和生产发布仍按回执分别验收。
  115. 请先读[运维001-r8上线执行单.md](运维001-r8上线执行单.md)。采集恢复前只读核查见[发给VOC采集方-只读核查与报价.md](发给VOC采集方-只读核查与报价.md),精确范围在handoff/collector-scope.json。
  116. 历史r4-r7文档和回执保留作来源,旧文件数与版本路径不作为r8执行指令。r8实际前端13项,后端13项,以RELEASE.json和deploy清单为准。不存在的接口不包装成已可调用;本轮无新增收费,无生产写入。包内原始研究库、凭据与私有完整详情原件均不包含。辅助SQLite副本不得覆盖现有工作台账。
  117. 一次分析固定当前研究代际。默认主店京东1000084104;新旧快照不混用,不重跑A/B/C,不修改正常合并规则。完整性只对证实的接口可访问范围成立,不把首页样本、正文可读或详情徽标说成全量或合格分析样本。
  118. ''', 'utf-8')
  119. print(json.dumps({'ok': True, 'candidate': str(out), 'staticFiles': len(release['staticFiles']),
  120. 'backendFiles': len(release['backendFiles']), 'sealed': False, 'paidCalls': 0}))
  121. def seal(out):
  122. receipt = json.loads((out / 'evidence/implementation-receipt-v8.json').read_bytes())
  123. if not receipt.get('accepted') or not receipt.get('packageVerificationPassed'):
  124. raise ValueError('CANDIDATE_ACCEPTANCE_REQUIRED')
  125. for name, digest in receipt['testedFilesSha256'].items():
  126. if sha((out / name).read_bytes()) != digest:
  127. raise ValueError('TESTED_SOURCE_CHANGED')
  128. archive = out.parent / (RELEASE + '.zip')
  129. if archive.exists():
  130. raise ValueError('SEALED_ARCHIVE_ALREADY_EXISTS')
  131. # Preserve nested historical manifests; only the root manifest is rebuilt.
  132. files = sorted(p for p in out.rglob('*') if p.is_file() and p != out / 'FILES.sha256')
  133. (out / 'FILES.sha256').write_text(''.join(sha(p.read_bytes()) + ' ' + p.relative_to(out).as_posix() + '\n' for p in files), 'utf-8')
  134. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z:
  135. for path in sorted(out.rglob('*')):
  136. if path.is_file():
  137. z.write(path, path.relative_to(out).as_posix())
  138. print(json.dumps({'ok': True, 'archive': str(archive), 'bytes': archive.stat().st_size,
  139. 'sha256': sha(archive.read_bytes()), 'paidCalls': 0, 'productionDeployed': False}))
  140. if __name__ == '__main__':
  141. p = argparse.ArgumentParser(description=__doc__)
  142. p.add_argument('--out', type=Path, required=True)
  143. p.add_argument('--seal', action='store_true')
  144. args = p.parse_args()
  145. (seal if args.seal else build)(args.out.resolve())