build_quality_preview.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. """Build a local business-output preview and verify evidence/arithmetic, not insight quality."""
  2. from pathlib import Path
  3. from decimal import Decimal
  4. from collections import Counter
  5. from datetime import datetime
  6. import hashlib
  7. import html
  8. import json
  9. import re
  10. import unicodedata
  11. WORK = Path(__file__).resolve().parent
  12. ROOT = WORK.parents[1]
  13. SKILL = ROOT / 'skills/ksf-workshop-runtime'
  14. load = lambda p: json.loads(p.read_text(encoding='utf-8'))
  15. sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
  16. inputs = load(WORK / 'probe-inputs.json')
  17. outputs = load(WORK / 'probe-outputs.json')
  18. raw = load((WORK / inputs['rawConsumerAndCostFixture']).resolve())
  19. samples = {s['id']: s for s in outputs['samples']}
  20. checks = []
  21. def check(name, passed, actual=None):
  22. checks.append({'name': name, 'passed': bool(passed), 'actual': actual})
  23. assert passed, (name, actual)
  24. market = inputs['marketFixture']
  25. prior, current = market['periods']
  26. check('同一固定面板', set(prior['panelIds']) == set(current['panelIds']))
  27. facts = samples['trend']['facts']
  28. prior_n, current_n, denominator = len(prior['adjustableClaimIds']), len(current['adjustableClaimIds']), len(current['panelIds'])
  29. check('趋势分子分母来自原始列表', (facts['priorClaimCount'], facts['currentClaimCount'], facts['panelSize']) == (prior_n, current_n, denominator))
  30. check('占比与百分点分别计算', facts['priorShare'] == prior_n / denominator and facts['currentShare'] == current_n / denominator and Decimal(str(facts['changePercentagePoints'])) == (Decimal(current_n) - Decimal(prior_n)) / Decimal(denominator) * 100)
  31. check('同源转载未计作独立资料', facts['independentNewsOrigins'] == len({n['original'] for n in market['news']}))
  32. check('目录范围不同且没有需求增长数', market['broadSearch'][0]['catalogueScope'] != market['broadSearch'][1]['catalogueScope'] and market['sales'] is None and not facts['demandGrowthKnown'])
  33. reviews = {r['id']: r for r in raw['reviews']}
  34. seen = set()
  35. duplicates = []
  36. for r in raw['reviews']:
  37. if r['upstreamId'] in seen:
  38. duplicates.append(r['id'])
  39. seen.add(r['upstreamId'])
  40. cf = samples['consumer']['facts']
  41. check('原始记录规模与重复取得', cf['rawReviewCount'] == len(reviews) and cf['repeatedAcquisitionIds'] == duplicates)
  42. check('占位记录依据原文', [reviews[x]['text'] for x in cf['placeholderIds']] == ['此用户没有填写评价'])
  43. check('物流用途排除保留原件', cf['logisticsOnlyIds'] == ['E10'] and reviews['E10']['text'] == '发来外箱压了,面没破,味道还没吃。')
  44. excluded = set(duplicates + cf['placeholderIds'] + cf['logisticsOnlyIds'])
  45. check('口味/使用集合没有混入重复占位物流', set(cf['tasteUsageIds']) == set(reviews) - excluded and len(cf['tasteUsageIds']) == len(set(cf['tasteUsageIds'])), len(cf['tasteUsageIds']))
  46. check('关键原句逐字对应', samples['consumer']['rows'][0][0] == 'E02:' + reviews['E02']['text'] and samples['consumer']['rows'][1][0] == 'E03:' + reviews['E03']['text'])
  47. check('相反取舍索引回查', cf['counterexampleIds'] == ['E05', 'E06'] and all(x in cf['tasteUsageIds'] for x in cf['counterexampleIds']))
  48. conditions = raw['scenarioConditions']
  49. dec = lambda n: Decimal(str(n))
  50. revenue, base, margin = map(dec, (conditions['netRevenuePerServing'], conditions['baseUnitCost'], conditions['minimumGrossMargin']))
  51. ceiling = revenue * (1 - margin)
  52. rf = samples['rd']['facts']
  53. check('成本上限与增量空间', dec(rf['costCeiling']) == ceiling and dec(rf['incrementalHeadroom']) == ceiling - base, {'ceiling': str(ceiling), 'headroom': str(ceiling - base)})
  54. estimates = {v['change']: dec(v['extraCost']) for v in conditions['incrementalCostEstimates']}
  55. for option in rf['options']:
  56. cost = base + estimates[option['change']]
  57. gross = (revenue - cost) / revenue
  58. check('情景成本与毛利:' + option['change'], dec(option['cost']) == cost and abs(dec(option['grossMargin']) - gross) < Decimal('0.000000000001'))
  59. rev = samples['revision']['facts']
  60. check('修订保留经营与设备条件', dec(rev['netRevenue']) == revenue and dec(rev['minimumGrossMargin']) == margin and dec(rev['costCeiling']) == ceiling and rev['noodleEquipmentChangeAllowed'] == conditions['noodleEquipmentChangeAllowed'])
  61. check('修订没有假称新路线已核价或实测', rev['noNewSachet'] and not rev['newRecipeCostVerified'] and not rev['physicalTestPerformed'])
  62. before = load(WORK / 'before-fingerprints.json')
  63. runtime = {n: h for n, h in before.items() if n.startswith('scripts/') or n == 'runtime-lock.json'}
  64. check('七个运行/锁定文件未变', len(runtime) == 7 and all(sha(SKILL / n) == h for n, h in runtime.items()))
  65. old_course = WORK / 'before/references/workshop-course-flow.md'
  66. new_course = SKILL / 'references/workshop-course-flow.md'
  67. check('课表映射文件原字节未变', sha(old_course) == sha(new_course))
  68. check('13个研究环节顺序保留', re.findall(r'^\| (D[12]-(?:\d\d|N)) \| \d\d \|', new_course.read_text(encoding='utf-8'), re.M) == ['D1-01','D1-02','D1-03','D1-04','D1-N','D2-01','D2-02','D2-03','D2-04','D2-05','D2-06','D2-07','D2-08'])
  69. # Check real relative resources. Fragment anchors are checked for the newly introduced reference.
  70. links = []
  71. for p in SKILL.rglob('*.md'):
  72. for target in re.findall(r'(?<!!)\[[^\]]*\]\(([^)]+)\)', p.read_text(encoding='utf-8')):
  73. if re.match(r'^[a-zA-Z][a-zA-Z0-9+.-]*:', target):
  74. continue
  75. path, _, fragment = target.partition('#')
  76. resolved = (p.parent / path).resolve() if path else p
  77. check('资源可达:' + p.relative_to(SKILL).as_posix() + ' → ' + target, resolved.is_file())
  78. if resolved.name == 'workshop-innovation-reasoning.md' and fragment:
  79. def anchor(s):
  80. return ''.join(c for c in s.strip().lower().replace(' ', '-') if c in '-_' or not unicodedata.category(c).startswith(('P', 'S')))
  81. headings = re.findall(r'^#+ (.+)$', resolved.read_text(encoding='utf-8'), re.M)
  82. check('新分析细则锚点:' + fragment, fragment in {anchor(x) for x in headings})
  83. links.append(target)
  84. tracked = sorted(p.relative_to(SKILL).as_posix() for p in SKILL.rglob('*') if p.is_file() and p.name != 'FILES.sha256')
  85. (SKILL / 'FILES.sha256').write_text(''.join(sha(SKILL / n) + ' ' + n + '\n' for n in tracked), encoding='utf-8')
  86. current_hashes = {p.relative_to(SKILL).as_posix(): sha(p) for p in SKILL.rglob('*') if p.is_file()}
  87. changed = [n for n in current_hashes if before.get(n) != current_hashes[n]]
  88. receipt = {
  89. 'schema': 'ksf.decision-quality-evidence-checks.v1',
  90. 'checkedAt': datetime.now().astimezone().isoformat(),
  91. 'scope': 'Deterministic source/arithmetic/link/integrity checks for five manually authored local synthetic probes; not a semantic quality score or independent Agent evaluation',
  92. 'allChecksPassed': all(c['passed'] for c in checks),
  93. 'checks': checks,
  94. 'checksCount': len(checks),
  95. 'relativeLinks': len(links),
  96. 'manifestEntries': len(tracked),
  97. 'skillSha256': current_hashes['SKILL.md'],
  98. 'manifestSha256': current_hashes['FILES.sha256'],
  99. 'changedFiles': sorted(changed),
  100. 'newFiles': sorted(set(current_hashes) - set(before)),
  101. 'runtimeFilesUnchanged': 7,
  102. 'researchModulesPreserved': 13,
  103. 'manualProbes': len(outputs['samples']),
  104. 'actualDifferentModelReview': False,
  105. 'independentAgentExecution': False,
  106. 'realVOCInvocation': False,
  107. 'productionDatabaseWrites': 0,
  108. 'paidCalls': 0,
  109. 'remoteInstalled': False,
  110. 'forwardRequired': False
  111. }
  112. (WORK / 'evidence-checks.json').write_text(json.dumps(receipt, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  113. data = json.dumps(outputs, ensure_ascii=False).replace('<', '\\u003c')
  114. template = '''<!doctype html>
  115. <html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
  116. <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:">
  117. <title>工作坊 · 分析与成果样张</title>
  118. <style>
  119. :root{color-scheme:light;--ink:#16302c;--muted:#546965;--line:#d8e4dd;--accent:#176b55;--wash:#f2f6f2}
  120. *{box-sizing:border-box}body{margin:0;background:var(--wash);color:var(--ink);font:16px/1.85 'Microsoft YaHei','PingFang SC',system-ui,sans-serif}button{font:inherit;cursor:pointer}button:focus-visible{outline:3px solid #f2b44c;outline-offset:3px}
  121. .wrap{max-width:1200px;margin:auto;padding:38px 30px 64px}.eyebrow{font-size:14px;letter-spacing:.08em;color:var(--accent);font-weight:700}h1{font-size:34px;line-height:1.4;margin:10px 0 15px}h2{font-size:27px;line-height:1.55;margin:0 0 18px}h3{font-size:18px;margin:0 0 10px}.intro{max-width:800px;margin:0;color:var(--muted)}.notice{padding:12px 18px;background:#fff5dd;border:1px solid #e8d9aa;border-radius:10px;margin:22px 0;font-size:15px;color:#695323}
  122. nav{display:flex;gap:9px;flex-wrap:wrap;margin:22px 0}nav button,.mode button{border:1px solid var(--line);border-radius:8px;background:white;padding:9px 16px;color:var(--ink)}nav button[aria-current=true],.mode button[aria-pressed=true]{background:var(--accent);border-color:var(--accent);color:white}.layout{display:grid;grid-template-columns:minmax(0,1fr) 260px;gap:24px;align-items:start}article{min-width:0;background:white;border:1px solid var(--line);border-radius:16px;padding:30px}.kicker{font-size:15px;color:var(--accent);margin-bottom:10px;font-weight:700}.mode{display:flex;gap:8px;margin:0 0 22px}.mode button{padding:7px 13px;font-size:15px}.answer{font-size:19px;line-height:1.9;font-weight:600;margin:0 0 18px}.meaning{margin:0 0 22px}.next{border-left:4px solid var(--accent);background:#f0f7f2;padding:15px 20px;margin-top:24px}.next p{margin:0}.aside-card{border:1px solid var(--line);border-radius:12px;padding:20px;background:#e8efe8}.aside-card p{margin:0 0 16px}.aside-card p:last-child{margin-bottom:0}.aside-card strong{display:block}.evidence{border-top:1px solid var(--line);padding-top:18px;margin-top:25px;color:var(--muted);font-size:15px}.table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:9px}table{border-collapse:collapse;width:100%;min-width:660px;table-layout:fixed}th,td{padding:14px 16px;border-bottom:1px solid var(--line);vertical-align:top;text-align:left;font-size:15px;overflow-wrap:anywhere}th{background:#ecf3ed;color:#235442;font-weight:700}tr:last-child td{border:0}td:first-child{font-weight:600}.view-note{font-size:15px;color:var(--muted);margin-bottom:14px}footer{margin-top:25px;font-size:15px;color:var(--muted)}[hidden]{display:none!important}
  123. @media(max-width:800px){.layout{grid-template-columns:minmax(0,1fr)}.wrap{padding:25px 20px 45px}.aside-card{display:none}h1{font-size:28px}article{padding:24px}}
  124. @media(max-width:480px){.wrap{padding:20px 14px 40px}h1{font-size:25px}h2{font-size:23px}.answer{font-size:17px}.notice{padding:10px 13px}nav{gap:7px}nav button{padding:7px 11px;font-size:15px}article{padding:20px 17px;border-radius:11px}.next{padding:13px 15px}.mode{gap:7px}th,td{padding:12px}}
  125. </style>
  126. <div class="wrap"><div class="eyebrow">产品创新工作坊 · 本地质量走查</div><h1>让分析进入产品讨论</h1><p class="intro">查看同一组构造素材怎样形成趋势判断、消费者洞察、研发比较,以及用户提出修改后怎样接续。业务回复便于快速讨论,评审底表保留具体对照。</p>
  127. <div class="notice">构造素材示例 · 全部产品、原话与经营数字均为本地测试数据。未进行真实消费者研究、试制、业务保存或发布。</div>
  128. <nav aria-label="样张类型" id="tabs"></nav><div class="layout"><article id="content"></article><aside class="aside-card"><h3>怎样判断它有用</h3><p><strong>先看认识</strong>有没有回答当前问题,说明它为什么影响产品选择。</p><p><strong>再看取舍</strong>能否比较现有办法与改动,保留成本、操作与体验代价。</p><p><strong>最后看判别</strong>下一项证据会让我们继续、修改还是停止。</p><p>样张由主助手人工走查,不代表独立模型或真实业务验收。</p></aside></div><footer>本页只展示业务内容。完整素材、算术检查和本地规则修改记录保留在本地档案;未生成新的部署包。</footer></div>
  129. <script>
  130. const DATA=__DATA__;let selected=0;let mode='reply';const names=['趋势扫描','消费者洞察','研发对照','修改反馈','简短回答'];
  131. const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
  132. function render(){const s=DATA.samples[selected];document.querySelector('#tabs').innerHTML=DATA.samples.map((v,i)=>`<button data-select="${i}" aria-current="${i===selected}">${esc(names[i])}</button>`).join('');
  133. let table=s.columns.length?`<div class="view-note">横向比较具体选项;窄屏可在表格内滑动,字号保持可读。</div><div class="table-wrap"><table><thead><tr>${s.columns.map(c=>`<th>${esc(c)}</th>`).join('')}</tr></thead><tbody>${s.rows.map(r=>`<tr>${r.map(c=>`<td>${esc(c)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`:'<p>这个问题只需简短判断,不额外套用报告或图表。</p>';
  134. document.querySelector('#content').innerHTML=`<div class="kicker">${esc(s.kicker)} · 构造样张</div><h2>${esc(s.title)}</h2><div class="mode"><button data-mode="reply" aria-pressed="${mode==='reply'}">业务回复</button><button data-mode="report" aria-pressed="${mode==='report'}">评审底表</button></div><p class="answer">${esc(s.answer)}</p><p class="meaning">${esc(s.meaning)}</p><section id="report" ${mode==='report'?'':'hidden'}>${table}</section>${s.next?`<div class="next"><h3>建议接续</h3><p>${esc(s.next)}</p></div>`:''}<div class="evidence"><strong>依据与适用范围</strong><br>${esc(s.evidence)}</div>`;
  135. document.querySelectorAll('[data-select]').forEach(b=>b.onclick=()=>{selected=Number(b.dataset.select);render()});document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;render()});}
  136. render();
  137. </script></html>'''
  138. (WORK / '分析与成果样张.html').write_text(template.replace('__DATA__', data), encoding='utf-8')
  139. print(json.dumps({k: receipt[k] for k in ['allChecksPassed','checksCount','relativeLinks','manifestEntries','changedFiles','newFiles','skillSha256','runtimeFilesUnchanged','researchModulesPreserved']}, ensure_ascii=False, indent=2))