package-board-research-release.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. """Freeze the latest approved library and industry UI with read-only industry runtime."""
  2. import argparse
  3. import ast
  4. from concurrent.futures import ThreadPoolExecutor
  5. from datetime import datetime, timezone
  6. import difflib
  7. import hashlib
  8. import json
  9. from pathlib import Path
  10. import re
  11. import shutil
  12. from urllib.error import HTTPError
  13. from urllib.request import Request, urlopen
  14. import zipfile
  15. ROOT = Path(__file__).resolve().parents[1]
  16. RELEASE = 'ksf-board-research-20261108-r13'
  17. OUT = ROOT / 'artifacts' / RELEASE
  18. PACKAGE = OUT / 'package'
  19. WORK = ROOT / 'work' / RELEASE
  20. PRIOR = ROOT / 'artifacts/ksf-industry-monitor-20261107-r11/package'
  21. def sha(raw):
  22. return hashlib.sha256(raw).hexdigest()
  23. def save(path, data):
  24. path.parent.mkdir(parents=True, exist_ok=True)
  25. path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  26. def dependencies():
  27. selected, required = set(), set()
  28. queue = ['src/layout/kf-app.js', 'src/layout/kf-nav.js', 'src/utils/router.js',
  29. 'src/pages/view-library.js', 'src/pages/view-industry.js']
  30. while queue:
  31. name = queue.pop()
  32. if name in selected:
  33. continue
  34. selected.add(name)
  35. for rel in re.findall(r'(?:from\s*|import\s*)[\'\"]([.][^\'\"]+)[\'\"]', (ROOT / name).read_text(encoding='utf-8')):
  36. target = (ROOT / name).parent.joinpath(rel).resolve().relative_to(ROOT).as_posix()
  37. if any(part in target for part in ['/industry-', '/policy-reviewed-', '/library-', '/library.']):
  38. queue.append(target)
  39. else:
  40. required.add(target)
  41. return selected, required - selected
  42. def capture(name):
  43. try:
  44. with urlopen(Request('https://kangshifu.fmode.cn/' + name, headers={'Cache-Control': 'no-cache'}), timeout=35) as response:
  45. raw = response.read(3_000_001)
  46. content_type = response.headers.get('Content-Type', '')
  47. assert len(raw) < 3_000_001, name
  48. exists = name == 'index.html' or 'javascript' in content_type or 'text/css' in content_type
  49. if not exists:
  50. assert b'<html' in raw[:500].lower() or b'<!doctype' in raw[:500].lower(), name
  51. if exists:
  52. target = WORK / 'public-baseline' / name
  53. target.parent.mkdir(parents=True, exist_ok=True)
  54. target.write_bytes(raw)
  55. return dict(path=name, exists=exists, sha256=sha(raw) if exists else None,
  56. normalizedSha256=sha(raw.replace(b'\r\n', b'\n')) if exists else None)
  57. except HTTPError as error:
  58. assert error.code == 404, (name, error.code)
  59. return dict(path=name, exists=False, sha256=None, normalizedSha256=None)
  60. def build():
  61. assert not PACKAGE.exists(), 'NEW_RELEASE_REQUIRED'
  62. static, required = dependencies()
  63. with ThreadPoolExecutor(max_workers=8) as pool:
  64. baseline = list(pool.map(capture, sorted(static | required | {'index.html'})))
  65. observed = {row['path']: row for row in baseline}
  66. assert all(observed[name]['exists'] for name in required), 'MISSING_PUBLIC_DEPENDENCY'
  67. assert observed['index.html']['sha256'] == 'dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3'
  68. captured = {}
  69. def copy(name, dest=None, normalize=False):
  70. raw = (ROOT / name).read_bytes()
  71. captured[name] = sha(raw)
  72. output = PACKAGE / (dest or name)
  73. output.parent.mkdir(parents=True, exist_ok=True)
  74. output.write_bytes(raw.replace(b'\r\n', b'\n') if normalize else raw)
  75. files, patch = [], []
  76. for name in sorted(static):
  77. copy(name, 'payload/' + name, True)
  78. raw = (PACKAGE / 'payload' / name).read_bytes()
  79. old = observed[name]
  80. files.append(dict(path=name, sha256=sha(raw), allowMissing=not old['exists'],
  81. priorNormalizedSha256=[old['normalizedSha256']] if old['exists'] else [],
  82. observedProductionSha256=old['sha256']))
  83. before = (WORK / 'public-baseline' / name).read_text(encoding='utf-8') if old['exists'] else ''
  84. patch.extend(difflib.unified_diff(before.splitlines(True), raw.decode().splitlines(True), fromfile='public/' + name, tofile='r13/' + name))
  85. backend = json.loads((PRIOR / 'RELEASE.json').read_bytes())['backendFiles'] + [
  86. 'server/voc-library/industry_workshop.py', 'server/voc-library/industry_decisions.py', 'server/voc-library/policy_usage_review.py']
  87. for name in backend:
  88. copy(name)
  89. for path in (ROOT / 'server/voc-library').glob('test_*.py'):
  90. if path.name.startswith('test_industry_') or path.name == 'test_policy_usage_review.py':
  91. copy(path.relative_to(ROOT).as_posix())
  92. node_tests = [p for p in (ROOT / 'test').glob('*.test.js') if (p.name.startswith(('industry-', 'library')) or p.name == 'router.test.js')
  93. and p.name not in ('library-comparison.test.js', 'industry-collection-guard.test.js')]
  94. for path in node_tests:
  95. name = path.relative_to(ROOT).as_posix()
  96. copy(name)
  97. dest = PACKAGE / name
  98. dest.write_text(dest.read_text(encoding='utf-8').replace('../src/', '../payload/src/'), encoding='utf-8')
  99. # Library API tests import this existing unchanged transport; it is test support only.
  100. copy('server/library-api.mjs', 'test-support/library-api.mjs')
  101. p = PACKAGE / 'test/library.test.js'
  102. p.write_text(p.read_text(encoding='utf-8').replace('../server/library-api.mjs', '../test-support/library-api.mjs'), encoding='utf-8')
  103. public_config = (WORK / 'public-baseline/src/app.config.js').read_bytes()
  104. (PACKAGE / 'payload/src/app.config.js').write_bytes(public_config)
  105. copy('scripts/verify-industry-existing-data.py')
  106. copy('scripts/query-industry-policy-usage.py')
  107. copy('scripts/package-board-research-release.py')
  108. for name in ['19-已核政策原文与用法查询.md', '20-上方范围与产品人群筛选扩展.md', 'agent-queries.md']:
  109. copy('docs/industry-monitoring-20261107/' + name, 'docs/' + name)
  110. policy = ROOT / 'artifacts/ksf-industry-policy-usage-20261108-r1/package'
  111. shutil.copytree(policy / 'sources', PACKAGE / 'data/policy-review/sources')
  112. shutil.copyfile(policy / 'policy-usage.json', PACKAGE / 'data/policy-review/policy-usage.json')
  113. assert sha((PACKAGE / 'data/policy-review/policy-usage.json').read_bytes()) == 'd22574f1ce28e1487e0be2abb78c47b32d22de801dc6e54ae9a8e3b3d31e777f'
  114. publisher = (PRIOR / 'scripts/publish-industry-ui.mjs').read_text(encoding='utf-8')
  115. old = next(line for line in publisher.splitlines() if line.startswith('export const FILES='))
  116. publisher = publisher.replace(old, 'export const FILES=Object.freeze(' + json.dumps(sorted(static)) + ');').replace('ksf-industry-monitor-20261107-r11', RELEASE)
  117. (PACKAGE / 'scripts/publish-industry-ui.mjs').write_text(publisher, encoding='utf-8')
  118. save(PACKAGE / 'deploy/industry-ui-files.json', dict(version='ksf.industry-ui.publish.v1', release=RELEASE, homepageSha256=observed['index.html']['sha256'], files=files))
  119. save(PACKAGE / 'evidence/production-baseline.json', dict(observedAt=datetime.now(timezone.utc).isoformat(), files=baseline))
  120. (PACKAGE / 'evidence/ui.patch').write_text(''.join(patch), encoding='utf-8')
  121. save(PACKAGE / 'package.json', dict(type='module', private=True))
  122. save(PACKAGE / 'RELEASE.json', dict(release=RELEASE, createdAt=datetime.now(timezone.utc).isoformat(),
  123. scope='Latest approved library and innovation analysis UI, read-only industry service and reviewed policies',
  124. staticFiles=sorted(static), backendFiles=backend, sourceFilesSha256=captured,
  125. generation='82b104fc024b49a75d2f5376bf9778ced6f63a9433b5fc5a6daa3c458d05d5ea',
  126. policyReviewSha256='d22574f1ce28e1487e0be2abb78c47b32d22de801dc6e54ae9a8e3b3d31e777f',
  127. databaseIncluded=False, existingVocCoreModulesReplaced=0, credentialsIncluded=False,
  128. productionDeployed=False, nativeProductionRuntimeVerified=False, paidCalls=0))
  129. for name, digest in captured.items():
  130. assert sha((ROOT / name).read_bytes()) == digest, 'SOURCE_CHANGED_DURING_FREEZE:' + name
  131. for path in (PACKAGE / 'server').rglob('*.py'):
  132. ast.parse(path.read_text(encoding='utf-8-sig'), feature_version=(3, 9))
  133. print(json.dumps(dict(ok=True, package=str(PACKAGE), staticFiles=len(static), backendFiles=len(backend), sealed=False)))
  134. def seal():
  135. receipt = json.loads((PACKAGE / 'evidence/acceptance.json').read_bytes())
  136. assert receipt['ok'] and receipt['publisher']['ok'] and receipt['browser']['ok']
  137. for name, digest in receipt['testedFilesSha256'].items():
  138. assert sha((PACKAGE / name).read_bytes()) == digest, 'TESTED_FILE_CHANGED:' + name
  139. assert not list(PACKAGE.rglob('*.sqlite')) and not list(PACKAGE.rglob('*.db'))
  140. assert not list(PACKAGE.rglob('snapshot.json'))
  141. files = sorted(p for p in PACKAGE.rglob('*') if p.is_file() and p.name != 'FILES.sha256')
  142. (PACKAGE / 'FILES.sha256').write_text(''.join(sha(p.read_bytes()) + ' ' + p.relative_to(PACKAGE).as_posix() + '\n' for p in files), encoding='utf-8')
  143. archive = OUT / (RELEASE + '.zip')
  144. assert not archive.exists(), 'SEALED_ARCHIVE_ALREADY_EXISTS'
  145. with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as bundle:
  146. for path in sorted(PACKAGE.rglob('*')):
  147. if path.is_file():
  148. bundle.write(path, path.relative_to(PACKAGE).as_posix())
  149. with zipfile.ZipFile(archive) as bundle:
  150. assert bundle.testzip() is None
  151. for line in bundle.read('FILES.sha256').decode().splitlines():
  152. digest, name = line.split(' ', 1)
  153. assert sha(bundle.read(name)) == digest
  154. save(OUT / 'delivery.json', dict(ok=True, archive=str(archive), bytes=archive.stat().st_size, sha256=sha(archive.read_bytes()), manifestEntries=len(files), productionDeployed=False, paidCalls=0))
  155. print((OUT / 'delivery.json').read_text(encoding='utf-8'))
  156. if __name__ == '__main__':
  157. parser = argparse.ArgumentParser(description=__doc__)
  158. parser.add_argument('--seal', action='store_true')
  159. (seal if parser.parse_args().seal else build)()