package-workshop-quality-update.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """Stage/publish r5 with exact r2/r3/r4 baseline reconstruction; never overwrite a ZIP."""
  2. from pathlib import Path
  3. import argparse
  4. import hashlib
  5. import json
  6. import re
  7. import zipfile
  8. ROOT = Path(__file__).resolve().parents[1]
  9. OUT = ROOT / 'docs/workshop-skill-release-20261109-r5'
  10. NAME = 'ksf-workshop-skill-update-20261109-r5'
  11. sha = lambda b: hashlib.sha256(b).hexdigest()
  12. encode = lambda v: (json.dumps(v, ensure_ascii=False, indent=2) + '\n').encode()
  13. ARCHIVES = {
  14. 'r2': ('docs/workshop-skill-release-20261108-r2/ksf-workshop-unified-20261108-r2.zip', '65660a366589d1b7a6c93b333e9ab1017a487e4b3f5c7399f225c38aedb9e844'),
  15. 'r3': ('docs/workshop-skill-release-20261109-r3/ksf-workshop-skill-update-20261109-r3.zip', '83c44c8a2f16c682faa5f91f41f513c7d638839cedf43516f96a6cb9bec14828'),
  16. 'r4': ('docs/workshop-skill-release-20261109-r4/ksf-workshop-skill-update-20261109-r4.zip', 'c9b60ce30fb5d3bed2b2a73ac7bc33f6c6b639dc100afd7cef4fe82a7e988308'),
  17. }
  18. def stage():
  19. OUT.mkdir(exist_ok=True)
  20. baselines, bodies = {}, {}
  21. previous = {}
  22. for version, (relative, digest) in ARCHIVES.items():
  23. source = ROOT / relative
  24. assert sha(source.read_bytes()) == digest
  25. with zipfile.ZipFile(source) as z:
  26. assert z.testzip() is None
  27. meta = json.loads(z.read('release.json'))
  28. baselines[version] = meta['candidate']
  29. prefix = 'skill/' if version == 'r2' else 'payload/'
  30. current = {n: z.read(prefix + n) if prefix + n in z.namelist() else previous[n] for n in meta['candidate']}
  31. for n, h in baselines[version].items():
  32. assert sha(current[n]) == h, (version, n)
  33. p = OUT / ('rehearsal-baseline-' + version) / n
  34. p.parent.mkdir(parents=True, exist_ok=True)
  35. p.write_bytes(current[n])
  36. bodies[version] = current
  37. previous = current
  38. if version == 'r2':
  39. policy = meta['legacyManifestPolicy']
  40. if version == 'r4':
  41. expected = json.loads(z.read('expected-agent-read.json'))
  42. skill = ROOT / 'skills/ksf-workshop-runtime'
  43. candidate = {}
  44. for line in (skill / 'FILES.sha256').read_text(encoding='utf-8').splitlines():
  45. h, n = line.split(' ', 1)
  46. assert '..' not in Path(n).parts and not Path(n).is_absolute() and n not in candidate
  47. assert sha((skill / n).read_bytes()) == h
  48. candidate[n] = h
  49. assert len(candidate) == 40
  50. candidate['FILES.sha256'] = sha((skill / 'FILES.sha256').read_bytes())
  51. assert candidate['SKILL.md'] == '16b428f4f4fa0819a764505269116f3f91e503342d9813d3855bc5c2cf80feb6'
  52. assert candidate['FILES.sha256'] == '21ae0a245de46e9acbf97ed31718aeb1cdb4ea8ff1ca38259a2b3a9b0c9795e1'
  53. deltas = {v: {n: h for n, h in candidate.items() if base.get(n) != h} for v, base in baselines.items()}
  54. for v, counts in {'r2': (17, 3), 'r3': (16, 2), 'r4': (9, 1)}.items():
  55. assert len(deltas[v]) == counts[0] and len(set(candidate) - set(baselines[v])) == counts[1]
  56. assert not set(baselines[v]) - set(candidate)
  57. assert all(candidate[n] == h for n, h in baselines[v].items() if n.startswith('scripts/') or n == 'runtime-lock.json')
  58. expected['skillFingerprint'] = candidate['SKILL.md']
  59. release = {
  60. 'schema': 'ksf.workshop-research-update.v1', 'releaseId': NAME,
  61. 'baselines': baselines, 'candidate': candidate,
  62. 'baselineArchiveSha256': {v: x[1] for v, x in ARCHIVES.items()},
  63. 'researchEntryNames': [n for n in policy['entryNames'] if n != 'SKILL.md'],
  64. 'researchRuntimePins': policy['verifiedRuntimeEntries'],
  65. 'plans': {v: [{'path': n, 'action': 'update' if n in baselines[v] else 'create', 'beforeSha256': baselines[v].get(n), 'afterSha256': h} for n, h in delta.items()] for v, delta in deltas.items()},
  66. 'runtimeReplacementAllowed': False, 'businessWritesDuringUpgrade': False,
  67. 'serviceRestartAllowed': False, 'credentialsIncluded': False,
  68. }
  69. links = 0
  70. for n in candidate:
  71. if not n.endswith('.md'):
  72. continue
  73. p = skill / n
  74. for target in re.findall(r'\]\(([^)]+)\)', p.read_text(encoding='utf-8')):
  75. if '://' in target or target.startswith('#'):
  76. continue
  77. resolved = (p.parent / target.split('#', 1)[0]).resolve()
  78. assert resolved.is_relative_to(skill.resolve()) and resolved.is_file(), (n, target)
  79. links += 1
  80. refs = [n for n in candidate if n.startswith('references/')]
  81. integrity = {'ok': True, 'skillSha256': candidate['SKILL.md'], 'manifestSha256': candidate['FILES.sha256'],
  82. 'manifestEntries': 40, 'publicFiles': 41, 'relativeLinks': links,
  83. 'references': {'all': len(refs), 'md': sum(n.endswith('.md') for n in refs), 'json': sum(n.endswith('.json') for n in refs)},
  84. 'runtimeFilesUnchanged': 7, 'courseModulesPreserved': 13}
  85. assert integrity['references'] == {'all': 32, 'md': 30, 'json': 2}
  86. (OUT / 'skill-integrity.json').write_bytes(encode(integrity))
  87. local = json.loads((ROOT / 'docs/workshop-decision-quality-20261109/local-revision.json').read_bytes())
  88. assert local['skillSha256'] == candidate['SKILL.md'] and local['manifestSha256'] == candidate['FILES.sha256']
  89. local_receipt = {'schema': 'ksf.quality-revision-local-acceptance.v1', 'ok': True,
  90. 'skillSha256': candidate['SKILL.md'], 'manifestSha256': candidate['FILES.sha256'],
  91. 'sourceReceiptSha256': sha(encode(local)), 'manualSyntheticProbes': 5,
  92. 'method': local['manualContentReview'], 'sourceAndArithmeticChecks': local['sourceAndArithmeticChecks'],
  93. 'previewRenderChecks': 30, 'screenshotsManuallyReviewed': True,
  94. 'researchModulesPreserved': 13, 'runtimeFilesUnchanged': 7,
  95. 'actualDifferentModelReview': False, 'realVOCInvocation': False,
  96. 'remoteInstalled': False, 'productionDatabaseWrites': 0, 'paidCalls': 0}
  97. (OUT / 'local-acceptance.json').write_bytes(encode(local_receipt))
  98. files = {'payload/' + n: (skill / n).read_bytes() for n in deltas['r2']}
  99. files.update({'release.json': encode(release), 'expected-agent-read.json': encode(expected),
  100. 'update-skill.mjs': (ROOT / 'scripts/update-workshop-quality.mjs').read_bytes(),
  101. 'run-update.py': (ROOT / 'scripts/run-workshop-quality-update.py').read_bytes(),
  102. 'probe-agent-library-read.py': (ROOT / 'scripts/probe-agent-library-read.py').read_bytes(),
  103. 'README.md': (OUT / 'README.md').read_bytes(), 'CHANGES.md': (OUT / 'CHANGES.md').read_bytes(),
  104. 'skill-integrity.json': encode(integrity), 'local-acceptance.json': encode(local_receipt)})
  105. validation = OUT / 'installer-validation.json'
  106. if validation.exists():
  107. v = json.loads(validation.read_bytes())
  108. assert v['ok'] and v['updaterSha256'] == sha(files['update-skill.mjs']) and v['runnerSha256'] == sha(files['run-update.py'])
  109. files[validation.name] = validation.read_bytes()
  110. files['FILES.sha256'] = ''.join(sha(raw) + ' ' + n + '\n' for n, raw in sorted(files.items())).encode()
  111. for n, raw in files.items():
  112. assert not any(t in n.lower() for t in ('credential-binding', 'private.pem', 'envelope', 'channel-config'))
  113. p = OUT / 'package' / n
  114. p.parent.mkdir(parents=True, exist_ok=True)
  115. p.write_bytes(raw)
  116. return files, release
  117. def main():
  118. parser = argparse.ArgumentParser()
  119. parser.add_argument('--pack', action='store_true')
  120. args = parser.parse_args()
  121. files, release = stage()
  122. if not args.pack:
  123. print(json.dumps({'ok': True, 'staged': len(files), 'plans': {v: len(p) for v, p in release['plans'].items()}}))
  124. return
  125. assert 'installer-validation.json' in files
  126. destination = OUT / (NAME + '.zip')
  127. assert not destination.exists(), 'Choose another revision; never overwrite a released ZIP'
  128. with zipfile.ZipFile(destination, 'x', zipfile.ZIP_DEFLATED) as z:
  129. for n, raw in sorted(files.items()):
  130. info = zipfile.ZipInfo(n, (2026, 11, 9, 0, 0, 0))
  131. info.external_attr = 0o100600 << 16
  132. z.writestr(info, raw, compress_type=zipfile.ZIP_DEFLATED)
  133. with zipfile.ZipFile(destination) as z:
  134. assert z.testzip() is None and set(z.namelist()) == set(files)
  135. for n, raw in files.items():
  136. assert z.read(n) == raw
  137. receipt = {'ok': True, 'releaseId': NAME, 'filename': destination.name,
  138. 'bytes': destination.stat().st_size, 'sha256': sha(destination.read_bytes()),
  139. 'packageEntries': len(files), 'outerManifestEntries': len(files) - 1,
  140. 'payloadFiles': len([n for n in files if n.startswith('payload/')]), 'targetPublicFiles': 41,
  141. 'plans': {v: {a + 's': sum(p['action'] == a for p in plans) for a in ['update', 'create']} for v, plans in release['plans'].items()},
  142. 'candidateSkillSha256': release['candidate']['SKILL.md'], 'candidateManifestSha256': release['candidate']['FILES.sha256'],
  143. 'runtimeFilesChanged': 0, 'credentialsIncluded': False, 'remoteInstalled': False, 'paidCalls': 0}
  144. (OUT / 'package-receipt.json').write_bytes(encode(receipt))
  145. print(json.dumps(receipt, ensure_ascii=False, indent=2))
  146. if __name__ == '__main__':
  147. main()