package-product-completion.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. """Package verified sources, staging databases and an offline preparation tool."""
  2. import hashlib
  3. import json
  4. from pathlib import Path
  5. import shutil
  6. import subprocess
  7. import sys
  8. import zipfile
  9. ROOT = Path(__file__).resolve().parents[1]
  10. WORK = ROOT / 'work/product-completion-20261108-r1'
  11. DOCS = ROOT / 'docs/product-completion-20261108'
  12. OUT = ROOT / 'artifacts/ksf-product-completion-20261108-r1'
  13. def sha(path):
  14. h = hashlib.sha256()
  15. with path.open('rb') as stream:
  16. for block in iter(lambda: stream.read(1024 * 1024), b''):
  17. h.update(block)
  18. return h.hexdigest()
  19. def read(path):
  20. return json.loads(path.read_bytes())
  21. def copy(source, destination):
  22. destination.parent.mkdir(parents=True, exist_ok=True)
  23. shutil.copyfile(source, destination)
  24. def main():
  25. assert not OUT.exists(), 'NEW_DELIVERY_DIRECTORY_REQUIRED'
  26. OUT.mkdir(parents=True)
  27. package = OUT / 'package'
  28. checked = read(DOCS / 'staging-readback.json')
  29. assert checked['ok'] and checked['productionImported'] is False
  30. plan = read(WORK / 'batch-plan.json')
  31. release = {'schema': 'ksf.product-completion-delivery.v1', 'baselineGeneration': plan['generation'],
  32. 'productionImported': False, 'productionPublishedByThisMachine': False,
  33. 'reason': 'CHANNEL_NOT_CONFIGURED', 'stagingVerified': True,
  34. 'paidCalls': 0, 'productionWrites': 0, 'newSpecRecords': 648, 'newReviewRecords': 21,
  35. 'contextTitleSpecsAlreadyExisting': 650,
  36. 'baselineCounts': read(DOCS / 'baseline.json')['counts'],
  37. 'expectedAfterCounts': {'products': 4674, 'reviews': 3475, 'prices': 4172, 'specs': 5422},
  38. 'productsWithReviewsBeforeAndExpectedAfter': 166,
  39. 'fullReviewCoverageClaimed': False,
  40. 'sourceAttributeSpecProductCoverageBefore': 2,
  41. 'sourceAttributeSpecProductCoverageExpectedAfter': 650,
  42. 'batches': checked['batches'],
  43. 'verifiedSpecRecordIds': checked['verifiedSpecRecordIds'],
  44. 'verifiedReviewRecords': checked['verifiedReviewRecords']}
  45. for b, source_part in zip(release['batches'], plan['batches']):
  46. b['expectedProductionAddedSpecs'] = source_part['specs']
  47. b['expectedProductionAddedReviews'] = source_part.get('reviews', 0)
  48. b['expectedProductionAddedProducts'] = 0
  49. b['expectedProductionAddedPrices'] = 0
  50. directory = WORK / source_part['directory']
  51. for name in ['batch.json', 'staging.sqlite', 'staging.source.zip', 'preflight.stdout.json',
  52. 'preflight.stderr.txt', 'preflight.exit.json', 'staging-build.stdout.json',
  53. 'staging-build.stderr.txt', 'staging-build.exit.json', 'readback.json']:
  54. copy(directory / name, package / 'batches' / b['batch'] / name)
  55. for name in ['baseline.json', 'public-probe.json', 'public-probe-corrected.json', 'review-recovery-audit.json',
  56. 'spec-recovery-plan.json', 'generated-title-spec-duplicates.json', 'staging-readback.json']:
  57. copy(DOCS / name, package / 'audit' / name)
  58. copy(WORK / 'batch-plan.json', package / 'audit/batch-plan.json')
  59. for path in sorted((WORK / 'current-spec-details').glob('*.json')):
  60. copy(path, package / 'audit/current-spec-details' / path.name)
  61. for name in ['channel-dry.stdout.json', 'channel-dry.stderr.json', 'channel-dry.exit.json']:
  62. copy(WORK / name, package / 'audit' / name)
  63. copy(DOCS / '入库执行单.md', package / '入库执行单.md')
  64. copy(ROOT / 'scripts/prepare-product-completion.py', package / 'tools/prepare-product-completion.py')
  65. runtime = ['agent_batch.py', 'structured_batch.py', 'intake_contract.py', 'voc_db.py', 'query_voc.py',
  66. 'retrieval.py', 'schema.sql', 'taxonomy.json', 'schema_catalog.py', 'product_insights.py',
  67. 'update_library.py', 'library_channel.py', 'review_correction.py']
  68. for name in runtime:
  69. copy(ROOT / 'server/voc-library' / name, package / 'tools/voc-library' / name)
  70. test = subprocess.run([sys.executable, '-X', 'utf8', '-B', '-m', 'unittest', 'discover', '-s',
  71. str(ROOT / 'scripts'), '-p', 'test_product_completion.py', '-v'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  72. assert test.returncode == 0
  73. (package / 'audit/extraction-tests.stdout.txt').write_bytes(test.stdout)
  74. (package / 'audit/extraction-tests.stderr.txt').write_bytes(test.stderr)
  75. (package / 'release.json').write_text(json.dumps(release, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  76. files = sorted(p for p in package.rglob('*') if p.is_file())
  77. (package / 'FILES.sha256').write_text(''.join(sha(p) + ' ' + p.relative_to(package).as_posix() + '\n' for p in files), encoding='utf-8')
  78. archive = OUT / 'ksf-product-completion-20261108-r1.zip'
  79. with zipfile.ZipFile(archive, 'x') as z:
  80. for p in sorted(package.rglob('*')):
  81. if not p.is_file():
  82. continue
  83. rel = p.relative_to(package).as_posix()
  84. info = zipfile.ZipInfo(rel, (2026, 11, 8, 0, 0, 0))
  85. info.external_attr = 0o100600 << 16
  86. z.writestr(info, p.read_bytes(), compress_type=zipfile.ZIP_STORED if p.suffix == '.zip' else zipfile.ZIP_DEFLATED)
  87. # Reopen the delivered bytes and verify every file against the manifest.
  88. with zipfile.ZipFile(archive) as z:
  89. manifest = z.read('FILES.sha256').decode('utf8')
  90. assert len(z.namelist()) == len(set(z.namelist())) == len(files) + 1
  91. for line in manifest.splitlines():
  92. hashed, rel = line.split(' ', 1)
  93. assert hashlib.sha256(z.read(rel)).hexdigest() == hashed
  94. receipt = {'ok': True, 'archive': str(archive), 'bytes': archive.stat().st_size, 'sha256': sha(archive),
  95. 'filesVerified': len(files), 'stagingVerified': True, 'productionImported': False,
  96. 'specsPrepared': 648, 'reviewsPrepared': 21, 'paidCalls': 0, 'productionWrites': 0}
  97. (OUT / 'delivery.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  98. print(json.dumps(receipt, ensure_ascii=False))
  99. if __name__ == '__main__':
  100. main()