build-workshop-quality-r13.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. """Build paired local report-quality revision. No deployment or host installation."""
  2. import csv
  3. import hashlib
  4. import html
  5. import importlib.util
  6. import json
  7. import re
  8. import shutil
  9. import zipfile
  10. from pathlib import Path
  11. from workshop_quality_r13_content import VERSION, REFERENCES, STAGES, RETRIEVAL_RULES
  12. from workshop_quality_r13_examples import EXAMPLES
  13. from workshop_quality_r13_model import build_model
  14. from update_workshop_manual_install_r13 import configure_website_entry, configure_evidence_feedback
  15. ROOT = Path(__file__).resolve().parents[1]
  16. OUT = ROOT/'docs/workshop-quality-r13-20261110'
  17. PRIOR = ROOT/'docs/workshop-validated-final-20261110'
  18. WORK = ROOT/'work/workshop-report-quality-20261110'
  19. SKILL = ROOT/'skills/ksf-workshop-runtime'
  20. sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
  21. dump = lambda p,x:p.write_text(json.dumps(x,ensure_ascii=False,indent=2)+'\n',encoding='utf8')
  22. spec=importlib.util.spec_from_file_location('old_renderer',ROOT/'scripts/build-workshop-self-run.py')
  23. renderer=importlib.util.module_from_spec(spec);spec.loader.exec_module(renderer)
  24. md=renderer.markdown
  25. def cited_markdown(text, base=''):
  26. """Make evidence identifiers usable without rewriting original quotations."""
  27. rendered=md(text)
  28. def link(match):
  29. code=match.group(0)
  30. new=code.startswith('R') or (code.startswith('P') and int(code[1:])>=8)
  31. target=base+'证据底表.html#'+code if new else base+'../workshop-validated-final-20261110/rerun/证据索引.html#'+code
  32. return '<a href="'+target+'">'+code+'</a>'
  33. # Do not process HTML attributes, existing links, source quotations or code.
  34. parts=re.split(r'(<(?:a|pre|blockquote)\b[^>]*>.*?</(?:a|pre|blockquote)>|<[^>]+>)',rendered,flags=re.S)
  35. return ''.join(part if part.startswith('<') else re.sub(r'(?<![A-Za-z0-9])(?:E\d{2}|P\d{2}|R\d{2}|S\d{2})(?![A-Za-z0-9])',link,part) for part in parts)
  36. CSS='''*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;color:#26334b;background:#f4f6fa;font:15px/1.85 "Segoe UI","Microsoft YaHei",sans-serif}a{color:#285bb1;text-underline-offset:4px}a:hover{color:#994c25}header{padding:22px max(24px,calc((100vw - 1300px)/2));background:#172d51;color:#fff}header a{color:#d5e5ff;margin-right:20px}header small{letter-spacing:.12em;color:#abc6ed}h1{font-size:29px;line-height:1.5;margin:7px 0}h2{font-size:23px;line-height:1.5;margin:12px 0}h3{font-size:18px;margin:28px 0 8px}h4{font-size:16px}p{margin:10px 0}main{max-width:1320px;margin:24px auto;padding:0 24px;display:grid;grid-template-columns:222px minmax(0,1fr);gap:22px}nav{position:sticky;top:16px;align-self:start;max-height:94vh;overflow:auto}nav a{display:block;padding:7px 10px;font-size:13px;text-decoration:none;border-left:2px solid transparent}nav a:hover{background:#e5ecf6;border-color:#376bbd}nav b{display:block;padding:6px 10px;font-size:12px;color:#6a7990}article,section.panel{min-width:0;background:#fff;padding:26px 30px;border:1px solid #dfe5ef;border-radius:9px;margin:0 0 20px;scroll-margin-top:16px}.reading{min-width:0}.kicker{color:#7c572d;font-size:12px;letter-spacing:.08em}.lead{padding:14px 18px;background:#edf3fc;border-left:3px solid #3769aa;font-size:17px}.muted{font-size:13px;color:#637188}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.card{display:block;padding:16px;border:1px solid #dfe5ef;border-radius:7px;background:#fff;text-decoration:none;color:#26334b}.card:hover{background:#f1f6ff}.card small{display:block;color:#6c7d94}.card strong{display:block;font-size:17px;line-height:1.6}.table-wrap{overflow-x:auto;max-width:100%;margin:15px 0;border:1px solid #dfe5ef;border-radius:6px}table{width:100%;border-collapse:collapse;min-width:650px;font-size:13px;line-height:1.8}th,td{padding:10px 12px;text-align:left;vertical-align:top;border-bottom:1px solid #e6ebf3;overflow-wrap:anywhere}th{background:#eef3fa;color:#234777}tr:last-child td{border-bottom:0}pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:17px;background:#f2f5fa;border-radius:6px;font:13px/1.8 Consolas,"Microsoft YaHei",monospace}blockquote{margin:14px 0;border-left:3px solid #4b75af;padding:12px 18px;background:#f4f7fc;white-space:pre-line}code{overflow-wrap:anywhere;font-size:13px}ul,ol{padding-left:24px}li{margin:5px 0}footer{padding:20px;text-align:center;color:#718098;font-size:12px}button{font:inherit;cursor:pointer}h2,h3{overflow-wrap:anywhere}.bars{display:grid;gap:14px}.barrow{display:grid;grid-template-columns:160px minmax(0,1fr) 85px;align-items:center;gap:10px;font-size:13px}.track{height:16px;background:#e9eef7;border-radius:3px;overflow:hidden}.fill{height:100%;background:#3d6eb7}.barrow:nth-child(2) .fill{background:#b97a45}.barrow:nth-child(4) .fill{background:#a55252}.switch{padding:7px 10px;background:#fff;border:1px solid #ccd7e7;border-radius:4px;color:#254e8b}label{font-size:13px}input[type=number]{width:100%;padding:9px;border:1px solid #c7d2e1;border-radius:4px}.calcgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}output{display:block;padding:14px;background:#edf3fc;margin-top:14px}figure{margin:20px 0}figcaption{font-size:12px;color:#65758c;margin-top:8px}@media(max-width:760px){main{display:block;margin:12px auto;padding:0 12px}nav{position:static;max-height:none;display:flex;overflow:auto;gap:6px;margin-bottom:12px}nav b{display:none}nav a{flex:0 0 auto;font-size:12px;padding:7px}header{padding:16px}h1{font-size:24px}article,section.panel{padding:18px 16px}.grid{grid-template-columns:1fr}.barrow{grid-template-columns:115px minmax(0,1fr) 65px;font-size:12px}.calcgrid{grid-template-columns:1fr}table{min-width:650px}}'''
  37. def shell(title,body,nav='',base=''):
  38. return f'''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>{html.escape(title)}</title><style>{CSS}</style><header><small>康师傅 · 产品创新工作坊 / r13 本地修订</small><h1>{html.escape(title)}</h1><a href="{base}index.html">交付首页</a><a href="{base}康师傅工作坊操作手册-r13本地修订.html">操作手册</a><a href="{base}案例报告合集.html">报告案例</a></header><main><nav>{nav}</nav><div class="reading">{body}</div></main><footer>本地内容修订与研究样板 · 不是真实员工对话或完整课程实测 · 未发布线上、未写生产业务</footer></html>'''
  39. def research_ledger():
  40. old=json.loads((ROOT/'docs/workshop-self-run-20261110/证据与编码.json').read_bytes())
  41. added=[]
  42. pending={6:'汤头措辞与拌面用途歧义',8:'汤底措辞与拌面用途歧义',10:'3种不同12味表述不清'}
  43. themes={'novelty':['R01','R04','R07','R12','R13','R15','R16'],
  44. 'preference_difference':['R03','R05'], 'preparation_positive':['R04','R12','R13'],
  45. 'soup_misidentification':['R11']}
  46. for i in range(1,19):
  47. p=WORK/'data'/f'new-review-{i:02}.json';r=json.loads(p.read_bytes())['record'];key=f'R{i:02}'
  48. added.append(dict(key=key,recordId=r['id'],productId=r['product_id'],quote=r['content'],
  49. purchasedSku=r['purchased_sku_text'],dateRaw=r['review_date_raw'],
  50. status='pending' if i in pending else 'excluded' if i==17 else 'usable-with-scope',
  51. note=pending.get(i,'语义紊乱' if i==17 else {7:'原文末尾截断;仅用可读部分',9:'泛评,不支持产品机制',12:'推广风格;不当正式产品规格',16:'未食期待,不当食后满意'}.get(i,'限定用于原句表达,未验证真人消费')),
  52. source=r['source'],tags=[t for t,ks in themes.items() if key in ks],
  53. responseFile='data/'+p.name))
  54. usable=[r for r in added if r['status']=='usable-with-scope'];den=[r['key'] for r in usable]
  55. assert len(den)==14
  56. rows=[]
  57. labels=['尝新/轮换动机或期待','明确拒绝某味/偏回经典','准备便利正向','明确误当汤面自述']
  58. for (t,ks),label in zip(themes.items(),labels):
  59. assert set(ks)<=set(den)
  60. sens=[k for k in den if k not in ['R07','R12']]
  61. rows.append(dict(theme=t,label=label,n=len(ks),N=len(den),percent=round(len(ks)/len(den)*100,1),
  62. members=ks,denominatorMembers=den,unit='评价记录;非独立消费者/需求发生率',
  63. sensitivityExcluded=['R07','R12'],sensitivityN=len(sens),sensitivityn=len(set(ks)&set(sens))))
  64. products=[]
  65. for key,n in [('P08',2),('P09',1),('P10',4),('P11',3)]:
  66. p=WORK/'data'/f'new-product-{n}.json';r=json.loads(p.read_bytes())['record']
  67. pricefile=WORK/'data'/f'new-{n}-prices.json'
  68. prices=json.loads(pricefile.read_bytes()) if pricefile.exists() else None
  69. products.append(dict(key=key,recordId=r['id'],title=r['title'],url=r['url'],specs=r.get('specs',[]),
  70. source=r.get('source'),sourceRecordId=r.get('source_record_id'),sourceTime=r.get('source_time_raw'),
  71. responseFile='data/'+p.name,priceResponseFile='data/'+pricefile.name if prices else None,
  72. priceRows=prices.get('records',[]) if prices else [],qualification='规格为标题候选;非实物标签核验,价格非成交/企业收入'))
  73. d=dict(schema='ksf.workshop-quality-evidence.v1',generation=json.loads((WORK/'data/version.json').read_bytes())['generation'],
  74. oldReviews=old['reviews'],addedReviews=added,oldProducts=old['products'],social=old['social'],statistics=rows,
  75. addedProducts=products,
  76. scope=dict(totalRawReviews=67,oldRaw=49,newRaw=18,newUsable=14,newPending=3,newExcluded=1,
  77. allUsefulWithDifferentScopes=59,independentCodingVerified=False,sourceArchivesRehashed=False),
  78. warning='真实库记录/原文回读;不是新采集消费事件,真实性/独立消费者未验证;作者概念不作事实')
  79. dump(OUT/'证据与编码-r13.json',d)
  80. for filename,rs in [('新增评价逐条编码.csv',added),('提及率复算.csv',rows)]:
  81. keys=['key','recordId','productId','quote','purchasedSku','dateRaw','status','note','tags'] if rs==added else list(rows[0])
  82. with (OUT/filename).open('w',encoding='utf-8-sig',newline='') as f:
  83. w=csv.DictWriter(f,fieldnames=keys,extrasaction='ignore');w.writeheader()
  84. for r in rs:w.writerow({k:json.dumps(r[k],ensure_ascii=False) if isinstance(r[k],list) else r[k] for k in keys})
  85. return d
  86. def charts(stats):
  87. # Widths are exact fractions of N, not animated or invented KPIs.
  88. rows=''.join(f'<div class="barrow" data-theme="{r["theme"]}" data-n="{r["n"]}" data-N="{r["N"]}"><span>{r["label"]}</span><div class="track"><div class="fill" style="width:{r["n"]/r["N"]*100:.4f}%"></div></div><b>{r["n"]}/{r["N"]} · {r["percent"]}%</b></div>' for r in stats)
  89. return '<figure><h3>P08的主题表达:以14条限定记录为分母</h3><div class="bars">'+rows+'</div><figcaption>主题重叠,不求和;这是本批记录提及,非需求发生率/购买转化。R16是未食期待;完整成员与敏感性见下表和编码CSV。</figcaption></figure>'
  90. def skill_and_manual():
  91. entry=(SKILL/'SKILL.md').read_text('utf8')
  92. entry=entry.replace('version: r12-local-final','version: '+VERSION)
  93. entry=entry.replace('提示词资产由教学材料承载,不放入Skill。','提示词资产是教学成果,Agent应协助形成可复制的本组任务文本;课题原文随成果保存,不固化成本Skill的案例答案。')
  94. anchor='## 按课表组织数据分析与报告'
  95. quality='''按当前环节先读 [选材与分类型产出标准](references/workshop-deliverable-contracts.md)。先将业务问题落实为主张、分析单位、所需字段、支持与反证,选择适合的数据,再执行对应报告类型。关键词命中只做发现,比例须另建合格分母;按商品ID回读并复核零命中。技术检查通过、资料诚实披露和产出内容合格分别判断,不用同一段缺口说明代替各节实质分析。\n\n'''
  96. if quality not in entry:entry=entry.replace(anchor,quality+anchor)
  97. (SKILL/'SKILL.md').write_text(entry,encoding='utf8')
  98. ref='# 选材与分类型产出标准\n\n用于课表13类产出。手册负责发起当前任务,本Skill负责选材、分析、反证和成报。不存在固定案例答案,也不要求学员反复填研究表。\n'+RETRIEVAL_RULES
  99. for s in STAGES:
  100. ref+=f'\n## {s["code"]} {s["title"]}\n\n要回答:{s["question"]}\n\n选材:{s["retrieval"]}\n\n分析:{s["task"]}\n\n应交付:'+ ';'.join(s['deliverables'])+'。\n\n验收:'+';'.join(s['checks'])+f'\n\n不合格示例:{s["bad"]}\n'
  101. ref+='\n## 参考学习的采用范围\n\n'
  102. for r in REFERENCES:
  103. ref+=f'- [{r["title"]}]({r["url"]})({r["kind"]}):{r["learn"]} 采用:{r["apply"]} 不移植:{r["reject"]}\n'
  104. (SKILL/'references/workshop-deliverable-contracts.md').write_text(ref,encoding='utf8')
  105. # Establish the new rule in the existing routes, without changing runtime interfaces.
  106. for name,paragraph in {
  107. 'workshop-course-flow.md':'每节开工前按[选材与分类型产出标准](workshop-deliverable-contracts.md)确定分析单位和材料用途,按本节产出类型分析。验收实际判断、比较和可复用结果,不以章节齐全、链接/哈希通过或诚实列缺口代替内容合格。提示词资产包属于D1-02教学成果,Agent须协助形成可复制任务资产;Skill保留方法与验收标准,不固化本题答案。',
  108. 'workshop-feedback.md':'修改报告时先检查选材是否支持当前判断,再检查分析是否回答本节问题。按[分类型产出标准](workshop-deliverable-contracts.md)分别交图谱、研究报告、聚类/痛点库、机会卡、优先级、回推与提案;同一观察任务换标题不算多个机会。不要求所有探索候选等到真实成本齐全才初排,也不强行给S级。',
  109. }.items():
  110. p=SKILL/'references'/name;t=p.read_text('utf8')
  111. if paragraph not in t:t+='\n## r13选材与内容验收\n\n'+paragraph+'\n'
  112. p.write_text(t,encoding='utf8')
  113. p=SKILL/'references/workshop-report-design.md';t=p.read_text('utf8')
  114. cuts=[t.index(marker) for marker in ['## 设计参考的使用范围','## 分类型学习与内容验收'] if marker in t]
  115. if cuts:t=t[:min(cuts)]
  116. t+='''## 分类型学习与内容验收
  117. 按[选材与分类型产出标准](workshop-deliverable-contracts.md)选用实际已读的行业研究、消费者调查、场景报告、竞品感官研究公开说明、技术经济分析与价值设计方法。报告案例、方法指南和未读完整数据的公开摘要分别标明,不统称已验证优质报告。每类借鉴一个有用途的做法,并说明本题如何实现。
  118. 图表先有适合本主张的数据和明确单位。图谱应显示层级/关系与对象,矩阵应解释供需错位,机会卡须有具体产品机制和代价,商业回推须拆实际变化,提案须有可评审产品设定。通用框架、过程聊天、引用列表或美观页面不能替代这些内容。
  119. 旧VOC参考中,茶决决可借鉴满意/未满足与多路径机会,但含SAMPLE和综合画像;香薰部分评分/增长无可回查底表,不采用其数值与雷达;COMPEX报告有分析对象品牌错配,不能作为品牌结论范本。引用报告自身也要核资格,不能因为样式漂亮就整体照搬。
  120. '''
  121. p.write_text(t,encoding='utf8')
  122. data=json.loads((PRIOR/'manual-source.json').read_bytes())
  123. data['release'].update(version=VERSION,contentVersion=VERSION,installVersion=VERSION,
  124. guide=(OUT/'INSTALL-本地修订.md').as_uri(),deploymentStatus='local-only')
  125. data['common']='''使用康师傅产品创新工作坊Skill,作为食品品类研究负责人沿本组同一课题推进当前环节。先恢复已知目标、真实约束、前序成果和选择;只问会改变路线且必须由业务方回答的1—3项,不重复问已有信息。
  126. 先将本节问题落实为主张、分析单位与所需数据。按对应细则查我方、竞品、用户及反证,核商品/规格/时间/来源;能获取的事实、关联、补查和计算由你完成。关键词命中只用于发现,原文按对象回读,零命中先复核;比例从声明的合格集合计算。
  127. 交付本节实际报告正文,先给发现及产品含义,再用图谱、比较或原话解释依据、反例与取舍。区别已核事实、研究推断、产品原型设定和真实验证;不得用缺口清单、过程聊天或“继续观察”替代成品。缺实测不编结果,但仍要提出具体可比较方案。末尾核产出标准,更新简短接续摘要与原件/成果位置;沿真实能力保存回读,未接通则本地交付,不编编号。本轮停在当前环节。'''
  128. by={s['id']:s for s in data['steps']}
  129. for s in STAGES:
  130. target=by[s['id']];target.update(question=s['question'],task=s['task'],retrieval=s['retrieval'],deliverables=s['deliverables'],checks=s['checks'],courseCode=s['code'],example='reports/'+s['id']+'.html',references=s['refs'])
  131. target['inputs']=target.get('inputs','本组研究简报及上一节实际成果;先回读原文与版本。')
  132. target['skillReference']='references/workshop-deliverable-contracts.md + '+target.get('skillReference','references/workshop-course-flow.md')
  133. target['prompt']=None
  134. # Attendees use the centrally configured website agent.
  135. configure_website_entry(data)
  136. by['topic']['prompt']='''使用康师傅产品创新工作坊Skill,帮助本组形成一个可研究的产品决策问题。先读我已提供的目标、材料和已有选择;若仍不明确,只问最影响路线的1—3项,例如想作哪项产品决定、在什么使用情境、必须保留或避开什么。能查的事实由你查,未知成本与产线不阻断早期研究。
  137. 形成简短研究简报:决策问题、研究对象/排除、目标任务与现有替代、真实限制及来源、已有证据、关键未知。然后给本组对应的选材路线,说明首节需要什么数据与为什么。不同小组依各自任务和真实选择形成方向,不随机分题,不把示例拌面方案当本组答案,不替我选择。沿本简报接续后续环节,已答内容不再问。'''
  138. configure_evidence_feedback(data)
  139. dump(OUT/'manual-source.json',data)
  140. prompts=[]
  141. for s in data['steps']:
  142. if s.get('kind') == 'introduction':
  143. prompts.append({**s, 'fullPrompt': ''})
  144. continue
  145. text=s.get('prompt') or f"{data['common']}\n\n本节问题:{s['question']}\n复用输入:{s['inputs']}\n按需细则:{s['skillReference']}\n\n选材要求:{s['retrieval']}\n\n本节任务:{s['task']}\n\n交付:\n"+'\n'.join('• '+x for x in s['deliverables'])
  146. prompts.append({**s,'fullPrompt':text})
  147. dump(OUT/'prompts-and-outputs.json',dict(version=VERSION,steps=prompts))
  148. manual=OUT/'manual';manual.mkdir(exist_ok=True)
  149. (manual/'content.js').write_text('window.WORKSHOP_MANUAL_DATA = '+json.dumps(data,ensure_ascii=False,indent=2)+';\n',encoding='utf8')
  150. for name in ['index.html','manual.css','manual.js']:
  151. if not (manual/name).exists():
  152. shutil.copyfile(PRIOR/'manual'/name,manual/name)
  153. p=manual/'index.html';h=p.read_text('utf8').replace('r12 定稿','r13 修订').replace('r12 本地定稿 · Skill与手册配套 · 企业账套待后续接入','r13 本地修订 · 按报告类型选材与分析 · 企业账套后续接入');p.write_text(h,encoding='utf8')
  154. p=manual/'manual.js';js=p.read_text('utf8').replace('r12-local-final','r13-local-quality')
  155. js=js.replace('本节要回答:','本节问题:').replace('先复用:','复用输入:').replace('执行细则:','按需细则:')
  156. js=js.replace('\\n\\n本节任务:\\n${step.task}','\\n\\n选材要求:${step.retrieval}\\n\\n本节任务:${step.task}').replace('本节应交付:','交付:').replace('\\n\\n交付:\\n','\\n\\n交付:\\n')
  157. # Make the DOM prompt exactly match the archived text, without maintaining two generators.
  158. prompt_map={s['id']:s['fullPrompt'] for s in prompts}
  159. start=js.index(' const promptMap =') if ' const promptMap =' in js else js.index(' const fullPrompt =')
  160. end=js.index('\n',js.index(' const fullPrompt =',start))
  161. js=js[:start]+' const promptMap = '+json.dumps(prompt_map,ensure_ascii=False)+';\n const fullPrompt = step => promptMap[step.id];'+js[end:]
  162. js=js.replace('<p class="action-copy">${escape(step.action)}</p>','')
  163. js=js.replace('${version}\n <div class="prompt-box">','${version}${step.example ? `<p class="example-link"><a href="../${escape(step.example)}" target="_blank">看本节产出样板 ↗</a> · <a href="../参考学习与类型对应.html" target="_blank">参考报告与采用方法 ↗</a></p>` : ""}\n <div class="prompt-box">')
  164. p.write_text(js,encoding='utf8')
  165. # The standalone file lives one directory above manual/.
  166. standalone_js=js.replace('href="../${escape(step.example)}"','href="${escape(step.example)}"').replace('href="../参考学习与类型对应.html"','href="参考学习与类型对应.html"')
  167. css=(manual/'manual.css').read_text('utf8')+'\n.example-link{font-size:12px;margin:9px 0 14px}.chapter-body{padding-top:14px}.prompt-text{font-size:14px;line-height:1.8}.outputs-box{margin-top:0}\n'
  168. (manual/'manual.css').write_text(css,encoding='utf8')
  169. safe=lambda s:s.replace('</script','<\\/script')
  170. standalone=h.replace('<link rel="stylesheet" href="manual.css">','<style>'+css+'</style>').replace('<script src="content.js" defer></script>','').replace('<script src="manual.js" defer></script>','')
  171. standalone=standalone.replace('</body>','<script>'+safe((manual/'content.js').read_text('utf8'))+'</script><script>'+safe(standalone_js)+'</script></body>')
  172. (OUT/'康师傅工作坊操作手册-r13本地修订.html').write_text(standalone,encoding='utf8')
  173. def package():
  174. baseline=PRIOR/'installation/skills/ksf-workshop-runtime'
  175. protected=['runtime-lock.json']+['scripts/'+s for s in ['ksf.mjs','ksf-secure.mjs','ksf-workshop.mjs','ksf-identity.mjs','ksf-identity-bridge.mjs','describe-business-schema.mjs']]
  176. for n in protected:assert sha(SKILL/n)==sha(baseline/n),n
  177. oldnames=sorted(json.loads((PRIOR/'installation/release.json').read_bytes())['workshop']['files'])
  178. names=sorted(set(oldnames+['references/workshop-deliverable-contracts.md']))
  179. (SKILL/'FILES.sha256').write_text(''.join(sha(SKILL/n)+' '+n+'\n' for n in names if n!='FILES.sha256'),encoding='utf8')
  180. pkg=OUT/'installation';pkg.mkdir(exist_ok=True)
  181. for n in names:
  182. target=pkg/'skills/ksf-workshop-runtime'/n;target.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(SKILL/n,target)
  183. shutil.copyfile(ROOT/'scripts/workshop-quality-install.py',pkg/'install.py')
  184. shutil.copyfile(PRIOR/'installation/check-voc.cjs',pkg/'check-voc.cjs')
  185. meta=json.loads((PRIOR/'installation/release.json').read_bytes())
  186. meta.update(releaseId='ksf-workshop-report-quality-20261110-r13',localOnly=True)
  187. meta['workshop'].update(version=VERSION,files={n:sha(SKILL/n) for n in names})
  188. meta['upgradeFrom']={'r12-local-exact':{n:sha(baseline/n) for n in oldnames}}
  189. dump(pkg/'release.json',meta)
  190. (pkg/'FILES.sha256').write_text(''.join(sha(p)+' '+p.relative_to(pkg).as_posix()+'\n' for p in sorted(pkg.rglob('*')) if p.is_file() and p!=pkg/'FILES.sha256'),encoding='utf8')
  191. with zipfile.ZipFile(OUT/'康师傅工作坊Skill-r13本地修订.zip','w',zipfile.ZIP_DEFLATED) as z:
  192. for p in sorted(pkg.rglob('*')):
  193. if p.is_file():z.write(p,p.relative_to(pkg).as_posix())
  194. (OUT/'INSTALL-本地修订.md').write_text(f'''# {VERSION} 本地资源说明
  195. 固定本地资源:{pkg.as_posix()}。仅本地修订,未发布线上;不能把旧线上r10下载地址称r13。
  196. 先核FILES.sha256与release.json,再执行包内install.py只读plan:
  197. ```text
  198. python -B install.py --skills-dir <宿主实际支持的用户技能根> --out <本地回执目录>
  199. ```
  200. 仅新目录create/已有同版verify-only/完整匹配release.json的r12-local-exact可正常安装或备份升级;r7–r11、未知改动或缺文件停止,不强制覆盖。保留本机credential-binding.json等身份、授权与连接和其他技能。VOC仍沿官方0.3.42固定清单,未在本次修改或重新安装;原研究目录仅为运行兼容路径,不恢复其SKILL.md。
  201. 本次只改研究选材、分析与产出规则:公开文件{len(names)};六个业务脚本与runtime-lock.json对r12逐字节不变。宿主实际加载、真实工具/资料/保存能力须在目标环境分别核验,不能以文件可读替代。企业账套以后接入,本稿不创建或假定已有账套。
  202. ''',encoding='utf8')
  203. return dict(version=VERSION,publicFiles=len(names),protected=[dict(file=n,sha256=sha(SKILL/n),unchanged=True) for n in protected],
  204. sourceSkill=(SKILL.as_posix()),zipSha256=sha(OUT/'康师傅工作坊Skill-r13本地修订.zip'),upgradeFrom='exact r12-local baseline only',productionWrites=0)
  205. def documents(ledger):
  206. reportdir=OUT/'reports';reportdir.mkdir(exist_ok=True)
  207. nav=''.join(f'<a href="#{s["id"]}">{s["code"]} {s["output"]}</a>' for s in STAGES)
  208. blocks=[]
  209. for s in STAGES:
  210. text=EXAMPLES[s['id']]
  211. (reportdir/(s['id']+'.md')).write_text(text,encoding='utf8')
  212. figure=charts(ledger['statistics']) if s['id']=='matrix' else ''
  213. model='<p><a href="../商业回推演算器.html">打开可重算模型与敏感性演算 ↗</a></p>' if s['id']=='feasibility' else ''
  214. body=f'<article><div class="kicker">{s["code"]} · {s["output"]}</div>'+cited_markdown(text,'../')+figure+model+'<p class="muted"><a href="../证据底表.html">原句、统计与实际请求</a> · <a href="../参考学习与类型对应.html">采用的报告方法</a> · <a href="../康师傅工作坊操作手册-r13本地修订.html#'+s['id']+'">对应任务提示词</a></p></article>'
  215. side=''.join(f'<a href="{x["id"]}.html">{x["code"]} {x["title"]}</a>' for x in STAGES)
  216. (reportdir/(s['id']+'.html')).write_text(shell(s['output']+'|本地报告样板',body,side,'../'),encoding='utf8')
  217. blocks.append(f'<article id="{s["id"]}"><div class="kicker">{s["code"]} · {s["output"]}</div>'+cited_markdown(text)+figure+model.replace('../商业','商业')+'</article>')
  218. (OUT/'案例报告合集.html').write_text(shell('宿舍常备拌面|13类产出报告样板','<section class="panel"><p class="lead">真实资料支撑的探索性研究;新增18条定向原文回读,补齐风味选择分析,明确既有供给与候选差异。</p><p>产品规格/组合为研究者原型设定;无双模型、KANO、真实试吃、工厂成本或小组批准。各篇保留未完成部分,不标课程全链验收通过。</p><a href="证据底表.html">回查依据与复算</a></section>'+''.join(blocks),nav),encoding='utf8')
  219. # Honest, substantive audit of all 13 screenshot deliverables.
  220. oldfiles=['02-品类全景.md','03-验证规则.md','04-竞品对照.md','05-研究任务与市场报告.md','06-调研与预聚类.md','07-调研验收.md','08-需求与证言池.md','09-供需矩阵与线索.md','10-三源研究卡与V1.md','11-证据与优先级.md','12-商业生产与V2.md','13-价值主张与买点.md','14-概念提案与V3.md']
  221. audit='''# 对照课表截图的旧稿复核
  222. 总体:上一版不宜作为合格课程案例。安装、文件哈希、链接、复制和移动排版检查只证明技术行为,未证明研究深度或业务内容。部分原句/分母核查可以保留,但不足以让后续概念自动成立。此次按13类核心产出重新验收,不靠七类导航或15份文件数给合格。
  223. | 环节/截图要求 | 旧稿状态 | 实际不足 | 本次改法 |
  224. |---|---|---|---|
  225. '''
  226. for s,n in zip(STAGES,oldfiles):audit+=f'| [{s["code"]} {s["output"]}](../workshop-validated-final-20261110/rerun/{n}) | {s["old"]} | {s["audit"]} | 先用适合的数据:{s["retrieval"]} [新样板](reports/{s["id"]}.html) |\n'
  227. audit+='''
  228. ## 内容验收与研究验证的分别结论
  229. r13已重写选材规则、提示词及13类报告正文,并实际补查我方系列、组合与P08原文。报告深度和产品具体程度有可见改动,但不因本轮修订就称全课程达标:行业测量/趋势、首提/考虑/转化、真实双模型、KANO、目标用户观察/感官、成本/工艺和真实评审仍未完成。技术检查另见核验回执。
  230. ## 不能再用的通过口径
  231. - 有图谱标题不等于分类和关系成立;有3张卡不等于3种产品价值机制。
  232. - 写明“待核”不等于已经完成应做的研究;能取得的关键原文仍须实际补查。
  233. - 所有商业输入未知不妨碍合理探索初排,但不能将探索初排报成商业批准。
  234. - 同一助手自演多角色、自己看自己的结论不构成独立双模型或真实员工测试。
  235. '''
  236. (OUT/'旧稿逐项复核.md').write_text(audit,encoding='utf8')
  237. (OUT/'旧稿逐项复核.html').write_text(shell('原案例为何不合格|逐项复核','<article>'+md(audit)+'</article>','<a href="index.html">返回首页</a>'),encoding='utf8')
  238. learning='''# 按产出类型学习报告,而非只借一套页面样式
  239. 报告成品与方法指南分开。本次已访问并实际读取下列注明的部分;没有把网络报告本身一概认定为完全合格,也未把海外行业数字放进康师傅结论。全文/局部阅读、公开摘要与无法取得全文分别注明。各类型至少有对应参考方法和本地重做的报告样板,不将一个综合VOC报告套满全程。
  240. | 课程产出类型 | 参考 | 要学的方法 | 本地落实 |
  241. |---|---|---|---|
  242. '''
  243. for s in STAGES:learning+=f'| {s["code"]} {s["output"]} | '+ '、'.join(s['refs'])+' | '+s['question']+f' 用匹配分析单位的数据回答,按{ s["output"] }组织成品。 | [对应样板](reports/{s["id"]}.html);手册和Skill的本节选材/任务/验收已重写 |\n'
  244. for r in REFERENCES:
  245. learning+=f'\n## {r["id"]} {r["title"]}\n\n类型:{r["kind"]}\n\n[原始公开来源]({r["url"]})\n\n阅读范围:{r["read"]}\n\n采用:{r["learn"]}\n\n用于:{r["apply"]}\n\n不照搬:{r["reject"]}\n'
  246. learning+='''
  247. ## 筛除与未成功获取
  248. 香薰竞品网页的市场增长/评分缺回查底表,不采用;COMPEX报告发现品牌/ASIN错配,不作为品牌结论范本。网页上的营销承诺、动画KPI、综合画像和点赞数不拿来填消费者证据。GOV.UK/ProductTalk请求超时、部分猜测地址404、NECTAR一页PDF连接失败,均未列作已读全文。康师傅IR验证页与WINA维护页不作为行业数据。完整请求回执在本地work目录,公开来源可从本页链接复核。
  249. '''
  250. (OUT/'参考学习与类型对应.md').write_text(learning,encoding='utf8')
  251. (OUT/'参考学习与类型对应.html').write_text(shell('参考学习|13类产出如何分别改进','<article>'+md(learning)+'</article>',''.join(f'<a href="#r{n}">R{n}</a>' for n in [])),encoding='utf8')
  252. (OUT/'选材与分析规则.md').write_text('# 如何更精准找到适合分析的数据\n'+RETRIEVAL_RULES,encoding='utf8')
  253. (OUT/'选材与分析规则.html').write_text(shell('从业务问题到适用证据','<article>'+md('# 如何更精准找到适合分析的数据\n'+RETRIEVAL_RULES)+'</article>'),encoding='utf8')
  254. evidence='# 原句、底表与实际执行\n\n原E/P/S引用沿旧稿原件索引;R01–R18为本轮按P08商品ID新增回读的真实记录。这里没有真实员工对话、独立第二模型输出或工厂报价。\n\n[旧E/P/S完整原话与来源](../workshop-validated-final-20261110/rerun/证据索引.html) · [新旧编码JSON](证据与编码-r13.json) · [新增18条CSV](新增评价逐条编码.csv) · [提及率复算](提及率复算.csv)\n'
  255. for r in ledger['addedProducts']:
  256. evidence+=f'\n## {r["key"]}|新增商品定位\n\n{r["title"]}\n\n完整商品键:`{r["recordId"]}`;源记录:`{r["sourceRecordId"]}`;源时间:{r["sourceTime"]}。{r["qualification"]}。[本次商品原文]({r["responseFile"]}) · [商品来源]({r["url"]})\n'
  257. if r['priceResponseFile']:
  258. evidence+=f'\n[该商品的价格原始记录]({r["priceResponseFile"]});逐日期列示,金额原单位为分:\n\n```json\n'+json.dumps(r['priceRows'],ensure_ascii=False,indent=2)+'\n```\n'
  259. for r in ledger['addedReviews']:
  260. evidence+=f'\n## {r["key"]}|{r["status"]}\n\n> {r["quote"]}\n\n{r["note"]}\n\n商品:{r["productId"]};购买规格:{r["purchasedSku"]};原日期:{r["dateRaw"]}。完整记录:`{r["recordId"]}`。源记录:`{r["source"]["id"]}`;{r["source"]["asset_path"]}第{r["source"]["line_number"]}行。[本次原文响应]({r["responseFile"]})\n'
  261. evidence+='\n## 查询回执\n\n'
  262. for p in sorted((OUT/'data').glob('*.request.json')):
  263. d=json.loads(p.read_bytes());evidence+=f'- [{p.stem}](data/{p.name}):ok={d["ok"]},readOnly={d["readOnly"]},databaseWrites={d["databaseWrites"]}。\n'
  264. (OUT/'证据底表.md').write_text(evidence,encoding='utf8')
  265. rendered=md(evidence)
  266. rendered=re.sub(r'<(h[1-6])>(R\d{2})',r'<\1 id="\2">\2',rendered)
  267. (OUT/'证据底表.html').write_text(shell('证据底表|原句、成员与实际查询','<article>'+rendered+charts(ledger['statistics'])+'</article>','<a href="案例报告合集.html">返回报告</a>'),encoding='utf8')
  268. # Comparison reports focus on actual changes rather than word count or file counts.
  269. change='''# 本次修订改变了什么
  270. | 旧版 | 新版 | 实际依据 |
  271. |---|---|---|
  272. | 各节多次复述有限材料和下一步观察 | 每节确定主张/单位/来源,再分别作图谱、研究报告、聚类、卡片、回推与提案 | 13份重写正文与配套提示词 |
  273. | 围绕便利、投料、结构三条近似任务 | 风味分层、首次识别、组合/补货三种价值机制 | 实际补查我方系列,P08新增18条原文回读 |
  274. | 我方只比较一个混合箱 | 纳入经典单味、探索三味与小组合等反证 | 品牌×品类139命中读首50,关键SKU精核 |
  275. | 关键词查不到便容易停在缺口 | 查同义与按商品ID完整回读 | “吃腻”零命中但R13原文存在,已记录漏项 |
  276. | 三项商业等级全部待评 | A/A/B探索初排,无S,理由与升降级可讨论 | 原有Skill定性规则+新增支持/反证,不编成功率 |
  277. | 财务主要是通用毛利公式 | 针对配比、印刷、履约拆成本及新增/替代贡献临界 | 模型可复算;真实成本仍缺,不报盈利 |
  278. | 最终只建议观察现品 | 有具体风味、配比、基准、使用/购买路径与反证的概念提案 | 原型设定明确,不冒充已量产或已测试 |
  279. 保留:原始49条评价、旧稿对话与历史文件不覆盖,业务脚本/身份/连接保持。新增内容只在本地;不称真实小组或豆包端到端测试已通过。
  280. '''
  281. (OUT/'修订对照.md').write_text(change,encoding='utf8')
  282. cards=''.join(f'<a class="card" href="reports/{s["id"]}.html"><small>{s["code"]} · {s["title"]}</small><strong>{s["output"]}</strong><span>{html.escape(s["question"])}</span></a>' for s in STAGES)
  283. homepage='''<section class="panel"><div class="kicker">先看产出内容</div><h2>按13类成果重做选材、分析与报告</h2><p class="lead">原案例不合格:重要类型缺少实质内容,候选过窄,提案退成了观察计划。本版补查供给与原文,用不同报告类型回答不同问题。</p><p>本地探索案例:宿舍常备拌面的风味选择与准备体验。原49条评价+新增18条定向回读,含相反偏好和误认自述;新概念是研究者设定。企业账套、真实员工测试和线上发布未实施。</p><div class="grid"><a class="card" href="康师傅工作坊操作手册-r13本地修订.html"><small>15节 · 提示词默认展开</small><strong>新版操作手册 ↗</strong>每节有选材要求、产出标准与样板</a><a class="card" href="reports/proposal.html"><small>先看最终报告是否有用</small><strong>产品概念提案 ↗</strong>具体设定、对照、代价和判别</a><a class="card" href="旧稿逐项复核.html"><strong>旧稿逐项复核 ↗</strong>对照截图要求,说明不合格处</a><a class="card" href="参考学习与类型对应.html"><strong>优质做法与采用范围 ↗</strong>行业/消费者/感官/技术经济报告分别学习</a></div></section><section class="panel"><h2>13类产出样板</h2><p class="muted">各篇是独立报告。市场测量、趋势、双模型、KANO、真实实验与成本缺项在对应章节单列,未标全课程已验收。</p><div class="grid">'''+cards+'''</div></section><section class="panel"><h2>规则、底表与Skill</h2><div class="grid"><a class="card" href="选材与分析规则.html"><strong>选材与分析规则</strong>主张→单位→来源→支持/反证</a><a class="card" href="证据底表.html"><strong>原句、统计与执行依据</strong>成员、分母、资格与真实请求</a><a class="card" href="康师傅工作坊Skill-r13本地修订.zip"><strong>r13本地Skill包</strong>仅完整r12基线支持升级;不自动安装</a><a class="card" href="installation/skills/ksf-workshop-runtime/SKILL.md"><strong>查看Skill入口</strong>配套新选材与分类型合同</a></div></section>'''
  284. (OUT/'index.html').write_text(shell('工作坊产出质量修订|r13本地版',homepage,'<a href="旧稿逐项复核.html">旧稿复核</a><a href="参考学习与类型对应.html">参考学习</a><a href="案例报告合集.html">案例合集</a><a href="选材与分析规则.html">选材规则</a><a href="证据底表.html">依据与复算</a>'),encoding='utf8')
  285. (OUT/'README.md').write_text('# r13本地内容修订\n\n从index.html查看。旧r12文件和对话保留,未覆盖。主交付是配套Skill/提示词/手册、旧稿逐项复核、分类参考学习、13类实质报告样板与证据编码。技术验收和研究结论边界分开;本次不标全课表实测通过。\n',encoding='utf8')
  286. def main():
  287. OUT.mkdir(exist_ok=True)
  288. data=OUT/'data';data.mkdir(exist_ok=True)
  289. for p in sorted((WORK/'data').glob('*.json')):shutil.copyfile(p,data/p.name)
  290. ledger=research_ledger()
  291. skill_and_manual()
  292. meta=package()
  293. documents(ledger)
  294. build_model(OUT,shell)
  295. dump(OUT/'本地发行清单.json',{**meta,'manualSha256':sha(OUT/'康师傅工作坊操作手册-r13本地修订.html'),
  296. 'referenceSources':REFERENCES,'courseReportTypes':len(STAGES),'steps':15,'contentStatus':'revised exploratory reports; full empirical course acceptance not claimed',
  297. 'oldArtifactPreserved':True})
  298. print(json.dumps(dict(ok=True,version=VERSION,root=str(OUT),publicFiles=meta['publicFiles']),ensure_ascii=False))
  299. if __name__=='__main__':main()