package-live-S3-v2.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. """Package the unsaved S3 v2 display fix; never call business services."""
  2. from pathlib import Path
  3. import copy
  4. import hashlib
  5. import json
  6. import zipfile
  7. ROOT = Path(__file__).resolve().parents[1]
  8. DOCS = Path('F:/文档/工作坊当前资料')
  9. STAGE = ROOT / 'work/first-live-S3-v2-package'
  10. DELIVERY = DOCS / 'ksf-live-S3-v2-20261103.zip'
  11. RECEIPT = ROOT / 'work/first-live-S3-v2-package-receipt.json'
  12. def sha(data):
  13. return hashlib.sha256(data).hexdigest()
  14. def as_json(value):
  15. return (json.dumps(value, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  16. def write_new(path, data):
  17. with path.open('xb') as output:
  18. output.write(data)
  19. assert not STAGE.exists() and not DELIVERY.exists() and not RECEIPT.exists()
  20. v1_bytes = (ROOT / 'work/first-live-S3-template.json').read_bytes()
  21. v2_bytes = (ROOT / 'work/first-live-S3-v2-template.json').read_bytes()
  22. v1, v2 = json.loads(v1_bytes), json.loads(v2_bytes)
  23. assert sha(v1_bytes) == '987024e3a1d37e9ef17ee6bfdced24947e76d67c326027d6f92dddebb95d5478'
  24. assert sha(v2_bytes) == '41c32974519799103709fb124115ecd79a1ed2ef1b3b343c129e23cb2be9ca49'
  25. assert len(v2_bytes) == 21973 and v2['sessionId'] is None and v2['version'] == 2
  26. expected = copy.deepcopy(v1)
  27. expected['version'] = 2
  28. expected['report']['chapters'][2]['sections'].append({
  29. 'kind': 'table', 'title': '待补问题',
  30. 'data': {'cols': ['序号', '待核实问题'],
  31. 'rows': [[str(i + 1), q] for i, q in enumerate(v1['pendingQuestions'])]}
  32. })
  33. assert v2 == expected, 'Only version and pending-question display table may change'
  34. assert len(v2['rawText']) == 2554 and len(v2['pendingQuestions']) == 8
  35. assert len(v2['sourceIds']) == len(v2['report']['evidence']) == 4
  36. assert 'opportunity' not in v2 and 'projectId' not in v2
  37. preview = json.loads((ROOT / 'work/live-S3-v2-template-preview-20261103-02.json').read_bytes())
  38. assert preview['ok'] and preview['previewOnly'] and preview['templatePreviewVerified']
  39. assert preview['savedArtifactId'] is None and preview['businessWrites'] == 0
  40. assert preview['browserVerified'] is False
  41. assert len(preview['checks']) == 114 and all(x['passed'] for x in preview['checks'])
  42. assert preview['evidenceDrawersChecked'] == 8 and preview['errors'] == []
  43. preview_receipt = {key: preview[key] for key in [
  44. 'ok', 'operation', 'at', 'browserVerified', 'templatePreviewVerified', 'previewOnly',
  45. 'savedArtifactId', 'scope', 'readOnly', 'businessWrites', 'viewports',
  46. 'evidenceDrawersChecked', 'quoteComparison', 'checks', 'errors'
  47. ]}
  48. preview_receipt['templateSha256'] = sha(v2_bytes)
  49. preview_receipt['note'] = '未保存模板的隔离浏览器内存预览;不是第2版生产页面验收。原始截图与完整记录由Codex本地保留。'
  50. payload = {
  51. 'S3-save-v2.template.json': v2_bytes,
  52. '34-S3第1版独立验收与字段说明.md': (DOCS / '34-S3第1版独立验收与字段说明.md').read_bytes(),
  53. '36-发给王刚001-S3第2版保存.txt': (DOCS / '36-发给王刚001-S3第2版保存.txt').read_bytes(),
  54. 'REPORT-PREVIEW.txt': ('S3第2版展示修复预览(未保存)\n'
  55. '只补页面待补问题表;以下rawText与第1版完全相同。\n\n' + v2['rawText']).encode('utf-8'),
  56. 'TEMPLATE-PREVIEW-VERIFY.json': as_json(preview_receipt),
  57. }
  58. manifest = {
  59. 'package': 'first-live-S3-v2-20261103',
  60. 'purpose': 'Unsaved S3 v2 template: display all eight existing pending questions',
  61. 'sessionIdInTemplate': None, 'taskKey': v2['taskKey'], 'version': 2,
  62. 'stageCode': 'S3', 'artifactType': v2['artifactType'],
  63. 'priorArtifactId': 'oiNxX7o27j',
  64. 'priorTemplateSha256': sha(v1_bytes), 'templateSha256': sha(v2_bytes),
  65. 'changedPaths': ['version', 'report.chapters[2].sections[3] (append table)'],
  66. 'rawTextUnchanged': True, 'sourceIds': v2['sourceIds'],
  67. 'pendingQuestionCount': 8, 'pendingQuestionsInDisplayTable': 8,
  68. 'evidenceKeys': list(v2['report']['evidence']),
  69. 'opportunityObjectPresent': False, 'projectIdPresent': False,
  70. 'templatePreviewVerified': True, 'browserVerified': False, 'businessWrites': 0,
  71. 'files': {name: {'bytes': len(data), 'sha256': sha(data)} for name, data in payload.items()},
  72. }
  73. payload['MANIFEST.json'] = as_json(manifest)
  74. checksums = ''.join(sha(payload[name]) + ' ' + name + '\n' for name in sorted(payload))
  75. payload['FILES.sha256'] = checksums.encode('utf-8')
  76. assert len(payload) == 7
  77. STAGE.mkdir()
  78. for name, data in payload.items():
  79. write_new(STAGE / name, data)
  80. with zipfile.ZipFile(DELIVERY, 'x', compression=zipfile.ZIP_DEFLATED, compresslevel=9) as package:
  81. for name in sorted(payload):
  82. package.write(STAGE / name, arcname=name)
  83. with zipfile.ZipFile(DELIVERY) as package:
  84. assert sorted(package.namelist()) == sorted(payload)
  85. for name, data in payload.items():
  86. assert package.read(name) == data
  87. assert not name.startswith(('/', '\\')) and '..' not in Path(name).parts
  88. assert package.testzip() is None
  89. for line in checksums.splitlines():
  90. expected_sha, name = line.split(' ', 1)
  91. assert sha(package.read(name)) == expected_sha
  92. result = {
  93. 'ok': True, 'operation': 'package-unsaved-S3-v2-display-fix',
  94. 'packagePath': str(DELIVERY), 'packageBytes': DELIVERY.stat().st_size,
  95. 'packageSha256': sha(DELIVERY.read_bytes()), 'entries': len(payload), 'fileChecks': '6/6',
  96. 'templateBytes': len(v2_bytes), 'templateSha256': sha(v2_bytes),
  97. 'templatePreviewVerified': True, 'browserVerified': False,
  98. 'businessWrites': 0,
  99. }
  100. write_new(RECEIPT, as_json(result))
  101. print(json.dumps(result, ensure_ascii=True, indent=2))