package-unified-database.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. """Freeze an explicit runtime whitelist; exclude databases, secrets and business binaries."""
  2. import argparse
  3. import ast
  4. import hashlib
  5. import json
  6. from pathlib import Path
  7. import zipfile
  8. ROOT = Path(__file__).resolve().parents[1]
  9. NAME = 'ksf-unified-database-migration-20261109-r3'
  10. MODULES = '''agent_batch board_library industry_decisions industry_innovation industry_monitor
  11. industry_policy industry_research industry_workshop intake_contract library_audit library_channel
  12. library_receiver monitoring_details monitoring_dispatch monitoring_evidence monitoring_inbox
  13. monitoring_ledger monitoring_pipeline monitoring_policy monitoring_store monitoring_tasks
  14. policy_publications policy_usage_review product_insights query_voc ranking_discovery retrieval
  15. review_correction schema_catalog structured_batch unified_store unified_worker update_library voc_db'''.split()
  16. COMMANDS = '''migrate-unified-database verify-unified-parity install-unified-skills
  17. manage-industry-pipeline manage-industry-tasks manage-industry-dispatch register-industry-evidence
  18. register-industry-details register-industry-policy register-industry-ranking-identities
  19. check-monitoring-evidence compare-industry-rankings discover-industry-rankings
  20. prepare-industry-inbox export-industry-snapshot'''.split()
  21. def sha(raw):
  22. return hashlib.sha256(raw).hexdigest()
  23. def main():
  24. p = argparse.ArgumentParser(description=__doc__)
  25. p.add_argument('--out', required=True, type=Path, help='New delivery directory')
  26. a = p.parse_args()
  27. assert not a.out.exists(), 'NEW_DELIVERY_DIRECTORY_REQUIRED'
  28. evidence = ROOT / 'work/single-database-20261109'
  29. for name in ('unified-test-result.json', 'regression-result.json', 'http-test-result.json'):
  30. assert json.loads((evidence / name).read_text('utf-8'))['success'], name
  31. assert json.loads((evidence / 'cli-full/04-parity.json').read_text('utf-8'))['ok']
  32. assert json.loads((evidence / 'component-installer-result-r2.json').read_text('utf-8'))['success']
  33. assert json.loads((evidence / 'r3/validation.json').read_text('utf-8'))['success']
  34. files = {}
  35. def copy(source, target=None):
  36. files[target or source] = (ROOT / source).read_bytes()
  37. for n in MODULES:
  38. copy('server/voc-library/' + n + '.py')
  39. for n in ('schema.sql', 'taxonomy.json'):
  40. copy('server/voc-library/' + n)
  41. for n in ('library-api', 'industry-api', 'industry-service', 'unified-worker'):
  42. copy('server/' + n + '.mjs')
  43. for n in COMMANDS:
  44. copy('scripts/' + n + '.py')
  45. copy('docs/unified-database-migration-20261109/执行单.md', '执行单.md')
  46. copy('docs/unified-database-migration-20261109/本地验收.md', '本地验收.md')
  47. copy('docs/unified-database-migration-20261109/unified-db.example.json', 'deploy/unified-db.example.json')
  48. copy('docs/unified-database-migration-20261109/requirements-unified.txt', 'requirements-unified.txt')
  49. protected = []
  50. for skill in ('ksf-voc-library', 'ksf-workshop-runtime'):
  51. base = ROOT / 'skills' / skill
  52. component_manifest = {}
  53. for line in (base / 'FILES.sha256').read_text('utf-8').splitlines():
  54. digest, name = line.split(' ', 1)
  55. assert sha((base / name).read_bytes()) == digest, (skill, name)
  56. if skill == 'ksf-voc-library':
  57. if name.startswith('scripts/'):
  58. copy('skills/' + skill + '/' + name, 'components/research-runtime/' + name)
  59. component_manifest[name] = digest
  60. continue
  61. key = 'skills/' + skill + '/' + name
  62. if skill == 'ksf-workshop-runtime' and (name.startswith('scripts/') or name == 'runtime-lock.json'):
  63. protected.append({'path': name, 'sha256': digest})
  64. continue
  65. copy(key)
  66. if skill == 'ksf-voc-library':
  67. files['components/research-runtime/FILES.sha256'] = ''.join(h + ' ' + n + '\n' for n, h in sorted(component_manifest.items())).encode('utf-8')
  68. else:
  69. copy('skills/' + skill + '/FILES.sha256')
  70. assert len(protected) == 7
  71. checks = ['unified-test-result.json', 'regression-result.json', 'http-test-result.json',
  72. 'workshop-test-result.json', 'skill-static-verification-r2.json', 'component-installer-result-r2.json',
  73. 'cli-full/01-plan/migration-receipt.json', 'cli-full/02-import/migration-receipt.json',
  74. 'cli-full/03-verify/migration-receipt.json', 'cli-full/04-parity.json', 'runtime-measurement.json']
  75. for name in checks:
  76. copy('work/single-database-20261109/' + name, 'evidence/history-r2/' + name)
  77. for name in ('validation.json', 'parity.json', 'plan-final/migration-receipt.json', 'apply-final/migration-receipt.json', 'verify-final/migration-receipt.json', 'store-tests.log', 'installer-probe-tests.log', 'http-node-tests.log', 'rehearsal-corrections.json', 'receiver-regression.log', 'receiver-offline-service-tests.log', 'skill-static-verification.json'):
  78. copy('work/single-database-20261109/r3/' + name, 'evidence/r3/' + name)
  79. copy('docs/unified-database-migration-20261109/operations-readonly-identity-confirmed-r3.json', 'evidence/r3/operations-readonly-identity-confirmed.json')
  80. # Provenance hashes do not substitute for the separately shipped operational checks.
  81. for name, body in files.items():
  82. assert not name.endswith(('.sqlite', '.zip', '.env', '.pem', '.key'))
  83. if name.endswith('.py'):
  84. ast.parse(body.decode('utf-8-sig'), filename=name, feature_version=(3, 9))
  85. release = {'release': NAME, 'schema': 'ksf.unified-db.v1', 'database': 'FKieRbIoGt_master', 'role': 'FKieRbIoGt_admin',
  86. 'appId': 'FKieRbIoGt_main', 'supersedes': 'ksf-unified-database-migration-20261109-r2', 'strictTargetGuardRetained': True,
  87. 'workshopSkillVersion': 'r9', 'productionDeployed': False, 'sourceDataIncluded': False,
  88. 'connectionCredentialsIncluded': False, 'businessBinariesIncluded': False,
  89. 'protectedInstalledBusinessFiles': protected, 'payloadFiles': len(files),
  90. 'workshopSkillEntrypoints': ['ksf-workshop-runtime'],
  91. 'researchRuntimeComponent': 'components/research-runtime',
  92. 'legacyResearchSkillEntryDistributed': False,
  93. 'localValidationScope': 'isolated PostgreSQL; older local library; production acceptance pending'}
  94. entries = [n for n in files if n.endswith('/SKILL.md')]
  95. assert entries == ['skills/ksf-workshop-runtime/SKILL.md'], entries
  96. files['release.json'] = (json.dumps(release, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  97. files['FILES.sha256'] = ''.join(sha(body) + ' ' + name + '\n' for name, body in sorted(files.items())).encode('utf-8')
  98. a.out.mkdir(parents=True)
  99. package = a.out / 'package'
  100. for name, raw in files.items():
  101. path = package / name;path.parent.mkdir(parents=True, exist_ok=True);path.write_bytes(raw)
  102. archive = a.out / (NAME + '.zip')
  103. with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as z:
  104. for name, raw in sorted(files.items()):
  105. z.writestr(name, raw)
  106. with zipfile.ZipFile(archive) as z:
  107. assert z.testzip() is None
  108. assert set(z.namelist()) == set(files)
  109. for line in z.read('FILES.sha256').decode('utf-8').splitlines():
  110. digest, name = line.split(' ', 1)
  111. assert sha(z.read(name)) == digest
  112. receipt = {**release, 'zip': str(archive.absolute()), 'sha256': sha(archive.read_bytes()),
  113. 'bytes': archive.stat().st_size, 'entriesVerified': len(files)}
  114. (a.out / 'delivery-receipt.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  115. print(json.dumps(receipt, ensure_ascii=False))
  116. if __name__ == '__main__':
  117. main()