check-workshop-quality-r13.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """Content traceability and real isolated installer verification. No production mutations."""
  2. import hashlib
  3. import importlib.util
  4. import json
  5. import re
  6. import shutil
  7. import tempfile
  8. import zipfile
  9. from unittest.mock import patch
  10. from html.parser import HTMLParser
  11. from pathlib import Path
  12. from urllib.parse import urlsplit,unquote
  13. ROOT=Path(__file__).resolve().parents[1]
  14. OUT=ROOT/'docs/workshop-quality-r13-20261110'
  15. BASE=ROOT/'docs/workshop-validated-final-20261110'
  16. sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
  17. class Links(HTMLParser):
  18. def __init__(self):super().__init__();self.links=[];self.ids=set()
  19. def handle_starttag(self,tag,attrs):
  20. d=dict(attrs)
  21. if 'id' in d:self.ids.add(d['id'])
  22. if tag=='a' and 'href' in d:self.links.append(d['href'])
  23. def run():
  24. receipts=sorted((OUT/'data').glob('*.request.json'))
  25. assert len(receipts)==37,len(receipts)
  26. gen=json.loads((OUT/'data/version.json').read_bytes())['generation']
  27. for p in receipts:
  28. r=json.loads(p.read_bytes());assert r['readOnly'] is True and r['databaseWrites']==0 and r['ok']
  29. assert r['generation']==gen
  30. assert sha(p.with_name(p.name.replace('.request.json','.json')))==r['responseSha256']
  31. d=json.loads((OUT/'证据与编码-r13.json').read_bytes())
  32. old=json.loads((ROOT/'docs/workshop-self-run-20261110/证据与编码.json').read_bytes())
  33. assert d['oldReviews']==old['reviews'] and d['oldProducts']==old['products'] and d['social']==old['social']
  34. keys=set()
  35. for r in d['addedReviews']:
  36. raw=json.loads((OUT/r['responseFile']).read_bytes())['record']
  37. assert (r['recordId'],r['quote'],r['productId'],r['purchasedSku'])==(raw['id'],raw['content'],raw['product_id'],raw['purchased_sku_text'])
  38. assert r['recordId'] not in {x['recordId'] for x in old['reviews']}
  39. keys.add(r['recordId'])
  40. assert len(keys)==18
  41. usable=[r for r in d['addedReviews'] if r['status']=='usable-with-scope']
  42. assert len(usable)==14 and len({r['quote'].strip() for r in usable})==14
  43. assert [r['key'] for r in d['addedReviews'] if r['status']=='pending']==['R06','R08','R10']
  44. assert [r['key'] for r in d['addedReviews'] if r['status']=='excluded']==['R17']
  45. for s in d['statistics']:
  46. assert s['denominatorMembers']==[r['key'] for r in usable]
  47. assert s['members']==[r['key'] for r in usable if s['theme'] in r['tags']]
  48. assert s['N']==14 and s['n']==len(s['members']) and s['percent']==round(100*s['n']/14,1)
  49. assert s['sensitivityN']==12 and s['sensitivityn']==len(set(s['members'])-{'R07','R12'})
  50. assert [(s['n'],s['N']) for s in d['statistics']]==[(7,14),(2,14),(3,14),(1,14)]
  51. assert '吃腻' in next(r['quote'] for r in d['addedReviews'] if r['key']=='R13')
  52. assert json.loads((OUT/'data/flavour-fatigue.json').read_bytes())['totalMatched']==0
  53. products={r['key']:r for r in d['addedProducts']}
  54. assert list(products)==['P08','P09','P10','P11']
  55. for r in products.values():
  56. raw=json.loads((OUT/r['responseFile']).read_bytes())['record']
  57. assert (r['recordId'],r['title'])==(raw['id'],raw['title'])
  58. for key,day,minor,count,value in [('P08','2026-11-06',4090,6,6.82),('P09','2026-11-06',5390,12,4.49),('P11','2026-11-06',5770,12,4.81),('P11','2026-11-03',5340,12,4.45)]:
  59. assert any(p['source_day']==day and p['amount_minor']==minor for p in products[key]['priceRows'])
  60. assert round(minor/100/count,2)==value
  61. pkg=OUT/'installation';meta=json.loads((pkg/'release.json').read_bytes())
  62. files=meta['workshop']['files'];assert len(files)==45
  63. skill=pkg/'skills/ksf-workshop-runtime'
  64. assert (skill/'references/workshop-report-design.md').read_text('utf8').count('## 分类型学习与内容验收')==1
  65. for n,h in files.items():assert sha(skill/n)==h and sha(ROOT/'skills/ksf-workshop-runtime'/n)==h,n
  66. protected=['runtime-lock.json']+['scripts/'+n for n in ['ksf.mjs','ksf-secure.mjs','ksf-workshop.mjs','ksf-identity.mjs','ksf-identity-bridge.mjs','describe-business-schema.mjs']]
  67. for n in protected:assert sha(skill/n)==sha(BASE/'installation/skills/ksf-workshop-runtime'/n),n
  68. for folder in [pkg,skill]:
  69. for line in (folder/'FILES.sha256').read_text('utf8').splitlines():
  70. h,n=line.split(' ',1);assert sha(folder/n)==h,n
  71. with zipfile.ZipFile(OUT/'康师傅工作坊Skill-r13本地修订.zip') as z:
  72. assert {n for n in z.namelist()}=={p.relative_to(pkg).as_posix() for p in pkg.rglob('*') if p.is_file()}
  73. assert all(z.read(n)==(pkg/n).read_bytes() for n in z.namelist())
  74. references=0
  75. for p in skill.rglob('*.md'):
  76. for _,link in re.findall(r'\[([^\]]+)\]\(([^)]+)\)',p.read_text('utf8')):
  77. if '://' in link or link.startswith('#'):continue
  78. assert (p.parent/link.split('#')[0]).is_file(),(p,link)
  79. references+=1
  80. spec=importlib.util.spec_from_file_location('isolated_r13_installer',pkg/'install.py')
  81. installer=importlib.util.module_from_spec(spec);spec.loader.exec_module(installer)
  82. fixtures=Path(tempfile.mkdtemp(prefix='r13-check-',dir=ROOT/'work/workshop-report-quality-20261110'))
  83. payload={'ksf-workshop-runtime':{n:(skill/n).read_bytes() for n in files}}
  84. target=fixtures/'exact/ksf-workshop-runtime'
  85. shutil.copytree(BASE/'installation/skills/ksf-workshop-runtime',target)
  86. for name in ['credential-binding.json','local-note.txt']:(target/name).write_text('synthetic-preservation-fixture\n',encoding='utf8')
  87. preserved={n:sha(target/n) for n in ['credential-binding.json','local-note.txt']}
  88. actions=installer.plan(target.parent,payload,meta['upgradeFrom']);assert actions[0]['action']=='upgrade'
  89. installer.install_files(target.parent,payload,actions,fixtures/'backup')
  90. installer.verify_component(target,payload['ksf-workshop-runtime'])
  91. assert all(sha(target/n)==h for n,h in preserved.items())
  92. assert installer.plan(target.parent,payload,meta['upgradeFrom'])[0]['action']=='verify-only'
  93. unknown=fixtures/'unknown/ksf-workshop-runtime';shutil.copytree(BASE/'installation/skills/ksf-workshop-runtime',unknown)
  94. (unknown/'references/workshop-feedback.md').write_text('unknown local edits',encoding='utf8')
  95. before={p.relative_to(unknown).as_posix():sha(p) for p in unknown.rglob('*') if p.is_file()}
  96. try:installer.plan(unknown.parent,payload,meta['upgradeFrom']);raise AssertionError('unknown baseline accepted')
  97. except installer.InstallError as e:assert str(e)=='UPGRADE_BASELINE_CHANGED'
  98. assert before=={p.relative_to(unknown).as_posix():sha(p) for p in unknown.rglob('*') if p.is_file()}
  99. assert installer.plan(fixtures/'new',payload,meta['upgradeFrom'])[0]['action']=='create'
  100. collision=fixtures/'collision/ksf-workshop-runtime';shutil.copytree(BASE/'installation/skills/ksf-workshop-runtime',collision)
  101. addition='references/workshop-deliverable-contracts.md'
  102. (collision/addition).write_text('unknown local reference',encoding='utf8')
  103. try:installer.plan(collision.parent,payload,meta['upgradeFrom']);raise AssertionError('new-file collision accepted')
  104. except installer.InstallError as e:assert str(e)=='UPGRADE_NEW_FILE_COLLISION'
  105. rollback=fixtures/'rollback/ksf-workshop-runtime';shutil.copytree(BASE/'installation/skills/ksf-workshop-runtime',rollback)
  106. before={p.relative_to(rollback).as_posix():sha(p) for p in rollback.rglob('*') if p.is_file()}
  107. actions=installer.plan(rollback.parent,payload,meta['upgradeFrom'])
  108. with patch.object(installer,'verify_component',side_effect=installer.InstallError('INJECTED_POST_UPGRADE_FAILURE')):
  109. try:installer.install_files(rollback.parent,payload,actions,fixtures/'rollback-backup');raise AssertionError('failure not injected')
  110. except installer.InstallError as e:assert str(e)=='INJECTED_POST_UPGRADE_FAILURE'
  111. assert before=={p.relative_to(rollback).as_posix():sha(p) for p in rollback.rglob('*') if p.is_file()}
  112. model=json.loads((OUT/'商业回推模型.json').read_bytes())
  113. assert all(x['value'] is None for x in model['fields'].values()) and not model['empiricalCompanyData']
  114. original=ROOT/'docs/workshop-final-local-20261110/康师傅工作坊操作手册-本地定稿.html'
  115. assert sha(original)=='8c8942c3d1331693570928b8f54300f13f635387cfa3dcf1422bde3cec1740ab'
  116. # All links and cross-document evidence anchors must resolve, including old citations.
  117. checked=0;cache={}
  118. for p in OUT.rglob('*.html'):
  119. parser=Links();parser.feed(p.read_text('utf8'))
  120. for href in parser.links:
  121. link=urlsplit(href)
  122. if link.scheme or link.netloc:continue
  123. dest=(p.parent/unquote(link.path)).resolve() if link.path else p.resolve()
  124. assert dest.is_file(),(p,href)
  125. dynamic_manual=dest.name.startswith('康师傅工作坊操作手册') or dest==OUT/'manual/index.html'
  126. if link.fragment and dest.suffix=='.html' and not dynamic_manual:
  127. if dest not in cache:
  128. scan=Links();scan.feed(dest.read_text('utf8'));cache[dest]=scan.ids
  129. assert unquote(link.fragment) in cache[dest],(p,href)
  130. checked+=1
  131. result={'ok':True,'actualReadOnlyRequests':len(receipts),'generation':gen,'productionWrites':0,
  132. 'old49Preserved':True,'new18BodiesExact':True,'denominatorsRecomputed':len(d['statistics']),
  133. 'newProductIndexVerified':4,'skillPublicFiles':len(files),'protected7Unchanged':True,
  134. 'skillReferenceLinks':references,'strictR12Upgrade':'actual isolated plan/apply/verify',
  135. 'unknownBaselineRejectedWithoutMutation':True,'localSyntheticFilesPreserved':True,
  136. 'newReferenceCollisionRejected':True,'injectedUpgradeFailureRolledBack':True,
  137. 'htmlLinksAndAnchors':checked,'empiricalCompanyModelInputs':False,
  138. 'independentModelReview':False,'realEmployeeTrial':False,'completeCourseEmpiricalAcceptance':False}
  139. (OUT/'内容与安装核验.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n',encoding='utf8')
  140. print(json.dumps(result,ensure_ascii=False))
  141. if __name__=='__main__':run()