finalize_local_revision.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. """Validate and record this local report-design revision, preserving sealed releases."""
  2. from pathlib import Path
  3. from urllib.parse import unquote
  4. import difflib
  5. import hashlib
  6. import json
  7. import re
  8. OUT = Path(__file__).resolve().parent
  9. ROOT = OUT.parents[1]
  10. SKILL = ROOT / 'skills/ksf-workshop-runtime'
  11. CHANGED = {
  12. 'SKILL.md',
  13. 'references/workshop-feedback.md',
  14. 'references/workshop-delivery.md',
  15. 'references/workshop-innovation-reasoning.md',
  16. 'references/workshop-verification.md',
  17. }
  18. ADDED = {'references/workshop-report-design.md'}
  19. def sha(path):
  20. return hashlib.sha256(path.read_bytes()).hexdigest()
  21. def main():
  22. baseline = json.loads((OUT / 'before-fingerprints.json').read_text(encoding='utf-8'))
  23. original_manifest = dict(line.split(' ', 1)[::-1] for line in
  24. (OUT / 'before/FILES.sha256').read_text(encoding='utf-8').splitlines())
  25. assert baseline == original_manifest and len(baseline) == 41
  26. files = {p.relative_to(SKILL).as_posix() for p in SKILL.rglob('*') if p.is_file()}
  27. expected = set(baseline) | ADDED | {'FILES.sha256'}
  28. assert files == expected and len(files) == 43, files ^ expected
  29. changed = {name for name, digest in baseline.items() if sha(SKILL / name) != digest}
  30. assert changed == CHANGED, changed ^ CHANGED
  31. runtime = [name for name in baseline if name.startswith('scripts/') or name == 'runtime-lock.json']
  32. assert len(runtime) == 7
  33. assert all(sha(SKILL / name) == baseline[name] for name in runtime)
  34. for name in ('references/workshop-course-flow.md', 'references/workshop-intake.md',
  35. 'references/workshop-commercial-feasibility.md'):
  36. assert sha(SKILL / name) == baseline[name], name
  37. intake = (SKILL / 'references/workshop-intake.md').read_text(encoding='utf-8')
  38. assert len(re.findall(r'^\| D[12]-(?:\d+|N) ', intake, re.M)) == 13
  39. refs = [p for p in (SKILL / 'references').rglob('*') if p.is_file()]
  40. assert len(refs) == 34
  41. assert sum(p.suffix == '.md' for p in refs) == 32
  42. assert sum(p.suffix == '.json' for p in refs) == 2
  43. links = 0
  44. for path in SKILL.rglob('*.md'):
  45. content = path.read_text(encoding='utf-8')
  46. assert not re.search(r'豆包|doubao', content, re.I), path
  47. assert not re.search(r'付费|收费|计费|停费|paidCalls|零费用', content), path
  48. for target in re.findall(r'\[[^\]]*\]\(([^)\s]+)\)', content):
  49. if target.startswith(('https:', 'http:', '#')):
  50. continue
  51. target_path = unquote(target.split('#', 1)[0])
  52. if target_path:
  53. assert (path.parent / target_path).is_file(), (path, target)
  54. links += 1
  55. sealed = ROOT / 'docs/workshop-skill-release-20261109-r6/ksf-workshop-skill-update-20261109-r6.zip'
  56. sealed_hash = sha(sealed)
  57. assert sealed_hash == 'c329432b9773cfb0a8e5d4a3e5c162b3778eab28d576b79875da6854f33c4dc5'
  58. # The manifest excludes itself, retaining the previous public paths and adding one guide.
  59. manifest_names = sorted(set(baseline) | ADDED)
  60. manifest = ''.join(sha(SKILL / name) + ' ' + name + '\n' for name in manifest_names)
  61. (SKILL / 'FILES.sha256').write_text(manifest, encoding='utf-8', newline='\n')
  62. written = dict(line.split(' ', 1)[::-1] for line in
  63. (SKILL / 'FILES.sha256').read_text(encoding='utf-8').splitlines())
  64. assert len(written) == 42 and set(written) | {'FILES.sha256'} == files
  65. assert all(sha(SKILL / name) == digest for name, digest in written.items())
  66. changes = []
  67. for name in sorted(CHANGED | ADDED | {'FILES.sha256'}):
  68. before = OUT / 'before' / name
  69. if name not in ADDED:
  70. assert before.is_file(), name
  71. if name != 'FILES.sha256':
  72. assert sha(before) == baseline[name], name
  73. old = before.read_text(encoding='utf-8') if before.is_file() else ''
  74. new = (SKILL / name).read_text(encoding='utf-8')
  75. target = OUT / 'diff' / (name + '.diff')
  76. target.parent.mkdir(parents=True, exist_ok=True)
  77. target.write_text(''.join(difflib.unified_diff(
  78. old.splitlines(True), new.splitlines(True),
  79. fromfile='before-report-design/' + name if before.is_file() else '/dev/null',
  80. tofile='local-report-design/' + name)), encoding='utf-8', newline='\n')
  81. changes.append({'path': name, 'action': 'add' if name in ADDED else 'update',
  82. 'beforeSha256': sha(before) if before.is_file() else None,
  83. 'afterSha256': sha(SKILL / name)})
  84. report = {
  85. 'ok': True,
  86. 'kind': 'local-instruction-and-report-design-draft',
  87. 'comparisonBaseline': 'local draft immediately before report-design changes; not remote r6',
  88. 'changes': changes,
  89. 'skillSha256': sha(SKILL / 'SKILL.md'),
  90. 'manifestSha256': sha(SKILL / 'FILES.sha256'),
  91. 'publicFilesVerified': len(files), 'manifestEntriesVerified': len(written),
  92. 'relativeFileLinksVerified': links, 'referenceFilesVerified': len(refs),
  93. 'intakeStagesPreserved': 13, 'courseFlowAndIntakeUnchanged': True,
  94. 'runtimeFilesVerifiedUnchanged': len(runtime),
  95. 'businessSaveContractChanged': False,
  96. 'platformBindingRemoved': True,
  97. 'sealedR6PackageModified': False, 'sealedR6Sha256': sealed_hash,
  98. 'remoteInstalled': False, 'researchExecuted': False,
  99. 'employeeUsefulnessValidated': False,
  100. }
  101. (OUT / 'change-receipt.json').write_text(
  102. json.dumps(report, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  103. print(json.dumps({k: v for k, v in report.items() if k != 'changes'}, ensure_ascii=False, indent=2))
  104. if __name__ == '__main__':
  105. main()