package-workshop-report-update.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. """Build the r7 instructions update against the reconstructed, sealed r6 baseline."""
  2. from pathlib import Path
  3. import argparse
  4. import hashlib
  5. import importlib.util
  6. import json
  7. import re
  8. import zipfile
  9. ROOT = Path(__file__).resolve().parents[1]
  10. OUT = ROOT / 'docs/workshop-skill-release-20261109-r7'
  11. NAME = 'ksf-workshop-skill-update-20261109-r7'
  12. spec = importlib.util.spec_from_file_location('r6_packager', ROOT / 'scripts/package-workshop-intake-update.py')
  13. previous = importlib.util.module_from_spec(spec)
  14. spec.loader.exec_module(previous)
  15. ARCHIVES = previous.ARCHIVES + [
  16. ('r6', 'docs/workshop-skill-release-20261109-r6/ksf-workshop-skill-update-20261109-r6.zip',
  17. 'c329432b9773cfb0a8e5d4a3e5c162b3778eab28d576b79875da6854f33c4dc5'),
  18. ]
  19. sha = lambda b: hashlib.sha256(b).hexdigest()
  20. encode = lambda v: (json.dumps(v, ensure_ascii=False, indent=2) + '\n').encode('utf-8')
  21. def stage():
  22. bodies = {}
  23. for version, relative, digest in ARCHIVES:
  24. archive = ROOT / relative
  25. assert sha(archive.read_bytes()) == digest, version
  26. with zipfile.ZipFile(archive) as z:
  27. assert z.testzip() is None
  28. meta = json.loads(z.read('release.json'))
  29. prefix = 'skill/' if version == 'r2' else 'payload/'
  30. bodies = {n: z.read(prefix + n) if prefix + n in z.namelist() else bodies[n]
  31. for n in meta['candidate']}
  32. assert all(sha(bodies[n]) == h for n, h in meta['candidate'].items()), version
  33. baseline = meta['candidate']
  34. assert len(baseline) == 42
  35. assert baseline['SKILL.md'] == '3cb64f381937411b0a2b853772bb19d37f44528a2902a89ef475b707472d3473'
  36. for n, raw in bodies.items():
  37. p = OUT / 'rehearsal-baseline-r6' / n
  38. p.parent.mkdir(parents=True, exist_ok=True)
  39. p.write_bytes(raw)
  40. skill = ROOT / 'skills/ksf-workshop-runtime'
  41. candidate = {}
  42. for line in (skill / 'FILES.sha256').read_text(encoding='utf-8').splitlines():
  43. digest, n = line.split(' ', 1)
  44. assert re.fullmatch(r'[a-f0-9]{64}', digest)
  45. assert re.fullmatch(r'[A-Za-z0-9_./-]+', n) and '..' not in Path(n).parts
  46. assert not Path(n).is_absolute() and n not in candidate
  47. assert sha((skill / n).read_bytes()) == digest, n
  48. candidate[n] = digest
  49. candidate['FILES.sha256'] = sha((skill / 'FILES.sha256').read_bytes())
  50. assert len(candidate) == 43
  51. assert candidate['SKILL.md'] == '15b0bd8be52bd40dbcb040ff2d532e8d637c1ee09305c19f07b3e1c28efaf2e5'
  52. assert candidate['FILES.sha256'] == '48686b72c78fee7bb5bfd3c2589744d41a14070cd7875cedae58c9d3441601b1'
  53. delta = json.loads((OUT / 'planned-paths.json').read_bytes())
  54. assert len(delta) == len(set(delta)) == 23
  55. assert not set(baseline) - set(candidate)
  56. assert {n for n in candidate if baseline.get(n) != candidate[n]} == set(delta)
  57. assert set(candidate) - set(baseline) == {'references/workshop-report-design.md'}
  58. runtime = [n for n in baseline if n == 'runtime-lock.json' or n.startswith('scripts/')]
  59. assert len(runtime) == 7 and all(candidate[n] == baseline[n] for n in runtime)
  60. links = 0
  61. markdown = []
  62. for n in candidate:
  63. if not n.endswith('.md'):
  64. continue
  65. p = skill / n
  66. text = p.read_text(encoding='utf-8')
  67. assert not re.search('豆包|付费|收费|零费用|免费|计费', text), n
  68. markdown.append(text)
  69. for target in re.findall(r'\]\(([^)]+)\)', text):
  70. if '://' in target or target.startswith('#'):
  71. continue
  72. resolved = (p.parent / target.split('#', 1)[0]).resolve()
  73. assert resolved.is_relative_to(skill.resolve()) and resolved.is_file(), (n, target)
  74. links += 1
  75. stages = ['D1-01', 'D1-02', 'D1-03', 'D1-04', 'D1-N'] + [f'D2-{i:02}' for i in range(1, 9)]
  76. for n in ['references/workshop-intake.md', 'references/workshop-course-flow.md']:
  77. text = (skill / n).read_text(encoding='utf-8')
  78. assert all(s in text for s in stages), n
  79. refs = [n for n in candidate if n.startswith('references/')]
  80. assert len(refs) == 34 and sum(n.endswith('.md') for n in refs) == 32
  81. plan = [{'path': n, 'action': 'update' if n in baseline else 'create',
  82. 'beforeSha256': baseline.get(n), 'afterSha256': candidate[n]} for n in delta]
  83. release = {'schema': 'ksf.workshop-research-update.v1', 'releaseId': NAME,
  84. 'baselines': {'r6': baseline}, 'candidate': candidate,
  85. 'baselineArchiveSha256': {'r6': ARCHIVES[-1][2]},
  86. 'researchEntryNames': meta['researchEntryNames'], 'researchRuntimePins': meta['researchRuntimePins'],
  87. 'plans': {'r6': plan}, 'runtimeReplacementAllowed': False,
  88. 'businessWritesDuringUpgrade': False, 'serviceRestartAllowed': False, 'credentialsIncluded': False}
  89. integrity = {'ok': True, 'skillSha256': candidate['SKILL.md'], 'manifestSha256': candidate['FILES.sha256'],
  90. 'manifestEntries': 42, 'publicFiles': 43, 'relativeLinks': links,
  91. 'references': {'all': 34, 'md': 32, 'json': 2},
  92. 'runtimeFilesUnchanged': 7, 'courseModuleIdsPresent': stages,
  93. 'baseline': 'complete sealed r6', 'changedFiles': 23, 'updates': 22, 'creates': 1}
  94. sources = [
  95. 'docs/workshop-report-design-20261109/reference-review.md',
  96. 'docs/workshop-report-design-20261109/local-acceptance.json',
  97. 'docs/workshop-report-design-20261109/browser-checks.json',
  98. 'docs/workshop-report-design-20261109/拌面研究报告形式样稿.html',
  99. 'docs/workshop-live-case-r5-20261109/rounds/R06/local-skill-review/receipt.json',
  100. 'docs/workshop-live-case-r5-20261109/rounds/R07/local-skill-review/receipt.json',
  101. ]
  102. design = json.loads((ROOT / sources[1]).read_bytes())
  103. browser = json.loads((ROOT / sources[2]).read_bytes())
  104. assert design['ok'] and browser['ok'] and browser['renderChecks'] == 6
  105. assert sha((ROOT / sources[3]).read_bytes()) == design['sampleSha256']
  106. assert sha((ROOT / sources[2]).read_bytes()) == design['browserSha256']
  107. latest = json.loads((ROOT / sources[-1]).read_bytes())
  108. assert latest['ok'] and latest['manifestSha256'] == candidate['FILES.sha256']
  109. review = json.loads((OUT / 'content-review.json').read_bytes())
  110. assert review['ok'] and review['skillSha256'] == candidate['SKILL.md']
  111. assert review['manifestSha256'] == candidate['FILES.sha256']
  112. local = {'schema': 'ksf.report-update-local-review.v1', 'ok': True,
  113. 'skillSha256': candidate['SKILL.md'], 'manifestSha256': candidate['FILES.sha256'],
  114. 'evidence': [{'path': p, 'sha256': sha((ROOT / p).read_bytes())} for p in sources],
  115. 'reportExampleBrowserChecks': 6, 'reportExampleViewports': browser['viewports'],
  116. 'contentReview': review,
  117. 'scope': 'local instruction integrity, manual reasoning review and report example presentation; not live agent acceptance',
  118. 'remoteInstalled': False, 'newResearchExecuted': False, 'employeeUsefulnessValidated': False}
  119. files = {'payload/' + n: (skill / n).read_bytes() for n in delta}
  120. files.update({'release.json': encode(release),
  121. 'update-skill.mjs': (ROOT / 'scripts/update-workshop-report.mjs').read_bytes(),
  122. 'run-update.py': (ROOT / 'scripts/run-workshop-report-update.py').read_bytes(),
  123. 'README.md': (OUT / 'README.md').read_bytes(), 'CHANGES.md': (OUT / 'CHANGES.md').read_bytes(),
  124. 'skill-integrity.json': encode(integrity), 'local-acceptance.json': encode(local)})
  125. validation = OUT / 'installer-validation.json'
  126. if validation.exists():
  127. value = json.loads(validation.read_bytes())
  128. assert value['ok'] and value['updaterSha256'] == sha(files['update-skill.mjs'])
  129. assert value['runnerSha256'] == sha(files['run-update.py'])
  130. files[validation.name] = validation.read_bytes()
  131. files['FILES.sha256'] = ''.join(sha(raw) + ' ' + n + '\n' for n, raw in sorted(files.items())).encode()
  132. for n, raw in files.items():
  133. assert not any(t in n.lower() for t in ['credential-binding', 'private.pem', 'envelope', 'channel-config'])
  134. p = OUT / 'package' / n
  135. p.parent.mkdir(parents=True, exist_ok=True)
  136. p.write_bytes(raw)
  137. (OUT / 'skill-integrity.json').write_bytes(encode(integrity))
  138. (OUT / 'local-acceptance.json').write_bytes(encode(local))
  139. return files, release
  140. def main():
  141. parser = argparse.ArgumentParser()
  142. parser.add_argument('--pack', action='store_true')
  143. args = parser.parse_args()
  144. files, release = stage()
  145. if not args.pack:
  146. print(json.dumps({'ok': True, 'stagedFiles': len(files), 'baseline': 'r6', 'changes': 23}))
  147. return
  148. assert 'installer-validation.json' in files, 'Installer validation required before packaging'
  149. target = OUT / (NAME + '.zip')
  150. assert not target.exists(), 'Never overwrite a released ZIP'
  151. with zipfile.ZipFile(target, 'x', zipfile.ZIP_DEFLATED) as z:
  152. for n, raw in sorted(files.items()):
  153. info = zipfile.ZipInfo(n, (2026, 11, 9, 0, 0, 0))
  154. info.external_attr = 0o100600 << 16
  155. z.writestr(info, raw, compress_type=zipfile.ZIP_DEFLATED)
  156. with zipfile.ZipFile(target) as z:
  157. assert z.testzip() is None and set(z.namelist()) == set(files)
  158. assert all(z.read(n) == raw for n, raw in files.items())
  159. receipt = {'ok': True, 'releaseId': NAME, 'filename': target.name, 'bytes': target.stat().st_size,
  160. 'sha256': sha(target.read_bytes()), 'packageEntries': len(files),
  161. 'outerManifestEntries': len(files) - 1, 'payloadFiles': 23, 'targetPublicFiles': 43,
  162. 'plans': {'r6': {'updates': 22, 'creates': 1}},
  163. 'candidateSkillSha256': release['candidate']['SKILL.md'],
  164. 'candidateManifestSha256': release['candidate']['FILES.sha256'],
  165. 'runtimeFilesChanged': 0, 'credentialsIncluded': False,
  166. 'remoteInstalled': False, 'productionDatabaseWrites': 0}
  167. (OUT / 'package-receipt.json').write_bytes(encode(receipt))
  168. print(json.dumps(receipt, ensure_ascii=False, indent=2))
  169. if __name__ == '__main__':
  170. main()