seal-workshop-r12-delivery.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """Seal the local reviewable result after all real checks finish. No publication."""
  2. import hashlib
  3. import importlib.util
  4. import json
  5. import re
  6. import zipfile
  7. from html.parser import HTMLParser
  8. from pathlib import Path
  9. ROOT=Path(__file__).resolve().parents[1]
  10. OUT=ROOT/'docs/workshop-validated-final-20261110'
  11. BASE=ROOT/'docs/workshop-self-run-20261110'
  12. sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
  13. dump=lambda p,x:p.write_text(json.dumps(x,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
  14. spec=importlib.util.spec_from_file_location('installer',OUT/'installation/install.py')
  15. installer=importlib.util.module_from_spec(spec);spec.loader.exec_module(installer)
  16. meta=installer.verify_package(OUT/'installation')
  17. for n,h in meta['workshop']['files'].items():
  18. assert sha(OUT/'installation/skills/ksf-workshop-runtime'/n)==h
  19. assert sha(ROOT/'work/workshop-r12-rerun-20261110/isolated-skills/ksf-workshop-runtime'/n)==h
  20. for n in [n for n in meta['workshop']['files'] if n.startswith('scripts/') or n=='runtime-lock.json']:
  21. assert sha(OUT/'installation/skills/ksf-workshop-runtime'/n)==sha(ROOT/'work/workshop-self-run-20261110/isolated-skills/ksf-workshop-runtime'/n)
  22. checks=json.loads((OUT/'内容与安装核验.json').read_bytes())
  23. browser=json.loads((OUT/'浏览器核验.json').read_bytes())
  24. assert checks['ok'] and browser['ok']
  25. before=json.loads((BASE/'对话与产出.json').read_bytes())
  26. after=json.loads((OUT/'rerun/对话与产出.json').read_bytes())
  27. assert len(before['steps'])==len(after['steps'])==15
  28. original=ROOT/'docs/workshop-final-local-20261110/康师傅工作坊操作手册-本地定稿.html'
  29. assert sha(original)=='8c8942c3d1331693570928b8f54300f13f635387cfa3dcf1422bde3cec1740ab'
  30. comparison=[]
  31. previous=ROOT/'work/workshop-self-run-20261110/baseline-source-skill'
  32. for n in meta['workshop']['files']:
  33. if sha(previous/n)!=meta['workshop']['files'][n]:comparison.append({'path':n,'beforeSha256':sha(previous/n),'afterSha256':meta['workshop']['files'][n]})
  34. dump(OUT/'定稿修改文件.json',comparison)
  35. nav=[('market-analysis','市场报告','tasks'),('social-needs','社媒场景报告','voc'),('evidence-pool','用户与竞品证言池','voc'),('opportunity-cards','三源研究卡','opportunities'),('opportunity-validation','产品论证机会清单','feasibility'),('buying-points','场景买点方案','value'),('product-proposal','产品概念提案','proposal')]
  36. dump(OUT/'七类成果导航.json',[{'chapterId':key,'name':name,'url':'rerun/index.html#s-'+step,'version':'local-v1','savedToBusiness':False} for key,name,step in nav])
  37. first_chars=sum(len(s['prompt']) for s in before['steps']);second_chars=sum(len(s['prompt']) for s in after['steps'])
  38. dump(OUT/'核验结果.json',{
  39. 'ok':True,'version':'r12-local-final','scope':'local paired content freeze + two one-assistant self-roleplays',
  40. 'before':{'manualContent':'r11-local','installedSkill':'r10','steps':15,'promptCharacters':first_chars},
  41. 'after':{'manualContent':'r12-local-final','installedSkill':'r12-local-final','steps':15,'promptCharacters':second_chars},
  42. 'promptReductionCharacters':first_chars-second_chars,
  43. 'promptReductionPercent':round((first_chars-second_chars)/first_chars*100,2),
  44. 'decisiveBusinessQuestions':{'beforeBatches':4,'beforeItems':6,'afterBatches':3,'afterItems':5,'basis':'authored roleplay; not real employee efficiency measurement'},
  45. 'dataChecks':checks,'browserChecks':browser,
  46. 'originalManualSha256':sha(original),'r12ManualSha256':sha(OUT/'康师傅工作坊操作手册-r12本地定稿.html'),
  47. 'r12SkillArchiveSha256':sha(OUT/'康师傅工作坊Skill-r12本地定稿.zip'),
  48. 'skillCreatorQuickValidation':'Skill is valid!',
  49. 'contentReview':'same assistant; not blind or independent evaluation',
  50. 'contentReviewFinding':'clearer version binding, question reuse, maturity labels, scenario separation and executable observation tool; unchanged evidence-based product verdict',
  51. 'notVerified':['native host skill discovery','real employee/Doubao use','real consumer or engineering tests','production saves or group board attribution','enterprise account integration'],
  52. 'productionWrites':0,'published':False})
  53. for n in ['核验结果.json','浏览器核验.json','定稿清单.json','七类成果导航.json','定稿修改文件.json']:
  54. assert (OUT/n).is_file()
  55. # Check local links in every rendered page, including report source footnotes.
  56. links_checked=0
  57. class Links(HTMLParser):
  58. def __init__(self):super().__init__();self.links=[]
  59. def handle_starttag(self,tag,attrs):
  60. if tag=='a':
  61. href=dict(attrs).get('href')
  62. if href:self.links.append(href)
  63. for p in OUT.rglob('*.html'):
  64. if p.name=='使用的原手册.html':continue # preserve the user's exact archived original
  65. parser=Links();parser.feed(p.read_text('utf-8'))
  66. for href in parser.links:
  67. if not href or href.startswith('#') or re.match(r'^[a-z]+:',href,re.I):continue
  68. name=href.split('#',1)[0]
  69. if name=='本地定稿与两轮记录.zip':continue
  70. assert (p.parent/name).is_file(),(p,href)
  71. links_checked+=1
  72. archive=OUT/'本地定稿与两轮记录.zip'
  73. manifest=OUT/'FILES.sha256'
  74. excluded={archive,manifest,OUT/'整包.sha256'}
  75. files=[p for p in sorted(OUT.rglob('*')) if p.is_file() and p not in excluded and '__pycache__' not in p.parts]
  76. assert not any(p.name in ['credential-binding.json','.env','.pgpass'] or 'node_modules' in p.parts for p in files)
  77. manifest.write_text(''.join(sha(p)+' '+p.relative_to(OUT).as_posix()+'\n' for p in files),encoding='utf-8')
  78. with zipfile.ZipFile(archive,'w',zipfile.ZIP_DEFLATED) as z:
  79. for p in files+[manifest]:z.write(p,p.relative_to(OUT).as_posix())
  80. with zipfile.ZipFile(archive) as z:
  81. assert z.testzip() is None
  82. for line in z.read('FILES.sha256').decode('utf-8').splitlines():
  83. h,name=line.split(' ',1);assert hashlib.sha256(z.read(name)).hexdigest()==h
  84. (OUT/'整包.sha256').write_text(sha(archive)+' '+archive.name+'\n',encoding='utf-8')
  85. print(json.dumps({'ok':True,'localLinksChecked':links_checked,'archiveFiles':len(files)+1,'zipBytes':archive.stat().st_size,'zipSha256':sha(archive),'skillChangedPublicFiles':len(comparison),'productionWrites':0,'published':False},ensure_ascii=False))