"""Build local benchmark deliverables from the reviewed evidence ledger."""
from pathlib import Path
import json, html, hashlib, zipfile
import xlsxwriter
P=Path(__file__).resolve().parent
D=json.loads((P/'benchmark-data.json').read_text())
OUT=P/'deliverables';OUT.mkdir(exist_ok=True);(OUT/'.gitignore').write_text('*\n!.gitignore\n')
V=D['vendors'];C=D['criteria'];A={(a['vendor'],a['criterion']):a for a in D['assessments']};S={s['id']:s for s in D['sources']}
E=html.escape
COL={'H':'#24775D','P':'#B97A23','R':'#667DA3','U':'#A5ADB3'}
LIGHT={'H':'#E5F1EB','P':'#FAEFD9','R':'#EAF0FA','U':'#EEF0F1'}
def counts(v,domain=''):
 a=[x for x in D['assessments'] if x['vendor']==v and x['criterion'].startswith(domain)]
 return {s:sum(x['status']==s for x in a) for s in D['statuses']}
def weighted(v,w):
 if v=='zk':return None
 return sum(w[i]*counts(v,dom['id'])['H']/8 for i,dom in enumerate(D['domains']))/sum(w)
# Workbook: formulas retain cached values for readers without an Excel calculation engine.
wb=xlsxwriter.Workbook(OUT/'五厂商量化对比与实测台账.xlsx')
base={'font_name':'Microsoft YaHei','font_size':12,'valign':'top','text_wrap':True}
fmt=wb.add_format(base);head=wb.add_format({**base,'bold':True,'bg_color':'#253D39','font_color':'white'});inp=wb.add_format({**base,'font_color':'#185BA5','bg_color':'#FFF6DF'});pct=wb.add_format({**base,'num_format':'0.0%'});num=wb.add_format({**base,'num_format':'0.0'});link=wb.add_format({**base,'font_color':'#236CAA','underline':1})
def sheet(name,headers,widths):
 s=wb.add_worksheet(name);s.freeze_panes(1,0);s.set_row(0,32)
 for i,h in enumerate(headers):s.write(0,i,h,head);s.set_column(i,i,widths[i],fmt)
 return s
s=sheet('阅读说明',['项目','说明'],[28,105])
notes=[('版本',D['date']+'｜内部评审版'),('指标口径','24项统一指标；H明确声明/P部分声明/R海外邻近/U未获证据。U不等于不支持。'),('数据对象','厂商产品组合；不代表同一SKU默认集成。默认四家国内公开声明同口径，熵基国内证据不足不排名。'),('覆盖公式','明确声明覆盖率=H数量/24；分系统=H数量/8；加权覆盖=SUM(系统权重×系统H数/8)/SUM(权重)。'),('深度评分','尚无统一实测分。0=实测不支持；1=基本动作通过；2=正常流程通过；3=异常恢复通过；4=跨系统及性能验收通过。空白=未测，禁止写0替代。'),('172项映射','只提供康比特内部功能与对比维度的导航；父指标H不得向下继承为全部子功能支持。'),('场景','案例/方案均为官网声明；六场景权重是D级假设，不是市场事实或采购推荐。'),('实测填写','实测量化表的输入蓝字黄底。阈值由具体项目确认；空白表示未测，公式保持空白。'),('报价','三年总成本只在硬件、软件、接口、实施、年费全部输入后计算；无报价不得估填。'),('范围',D['source_policy'])]
for r,row in enumerate(notes,1):s.write_row(r,0,row);s.set_row(r,52)
s=sheet('指标定义',['编号','系统','指标','明确声明最低条件','统一演示/POC动作'],[10,25,25,75,85])
for r,c in enumerate(C,1):s.write_row(r,0,[c['id'],next(x['name'] for x in D['domains'] if x['id']==c['domain']),c['name'],c['rule'],c['test']]);s.set_row(r,50)
s.autofilter(0,0,len(C),4)
s=sheet('证据矩阵',['厂商','指标ID','指标','状态','判断摘要','来源ID','证据等级','同口径可比较','深度分0至4（待测）','深度证据/版本'],[18,10,26,16,80,20,13,18,20,55])
for r,a in enumerate(D['assessments'],1):
 c=next(x for x in C if x['id']==a['criterion']);v=next(x for x in V if x['id']==a['vendor']);s.write_row(r,0,[v['name'],c['id'],c['name'],a['status'],a['note'],' '.join(a['sources']),a['evidence_level'] or '—','是' if a['comparable'] else '否']);s.write_blank(r,8,None,inp);s.write_blank(r,9,None,inp);s.set_row(r,45)
 s.write_comment(r,3,'H/P/R/U为资料判断，不是能力深度或质量分。来源：'+ '; '.join(S[x]['url'] for x in a['sources']))
s.data_validation(1,3,120,3,{'validate':'list','source':['H','P','R','U']});s.data_validation(1,8,120,8,{'validate':'integer','criteria':'between','minimum':0,'maximum':4,'ignore_blank':True});s.autofilter(0,0,120,9)
for k,color in COL.items():s.conditional_format(1,3,120,3,{'type':'text','criteria':'containing','value':k,'format':wb.add_format({**base,'bg_color':LIGHT[k],'font_color':color})})
s=sheet('量化汇总',['厂商','H明确','P部分','R海外','U未知','明确覆盖率','营养H/8','采购H/8','食安H/8','说明'],[18,12,12,12,12,18,16,16,16,70])
for r,v in enumerate(V,1):
 s.write(r,0,v['name']);co=counts(v['id'])
 for col,k in enumerate(['H','P','R','U'],1):s.write_formula(r,col,f'=COUNTIFS(证据矩阵!$A$2:$A$121,A{r+1},证据矩阵!$D$2:$D$121,"{k}")',None,co[k])
 if v['id']=='zk':s.write(r,5,'不计算');[s.write(r,col,'不计算') for col in [6,7,8]]
 else:
  s.write_formula(r,5,f'=B{r+1}/COUNTA(指标定义!$A$2:$A$25)',pct,co['H']/24)
  for col,dom in enumerate(['N','P','S'],6):s.write_formula(r,col,f'=COUNTIFS(证据矩阵!$A$2:$A$121,A{r+1},证据矩阵!$B$2:$B$121,"{dom}*",证据矩阵!$D$2:$D$121,"H")/COUNTIF(指标定义!$A$2:$A$25,"{dom}*")',pct,counts(v['id'],dom)['H']/8)
 s.write(r,9,v['scope']+'；资料覆盖不等于产品实力');s.set_row(r,50)
chart=wb.add_chart({'type':'bar','subtype':'stacked'})
for col,k in enumerate(['H','P','R','U'],1):chart.add_series({'name':['量化汇总',0,col],'categories':['量化汇总',1,0,5,0],'values':['量化汇总',1,col,5,col],'fill':{'color':COL[k]},'border':{'none':True}})
chart.set_title({'name':'24项指标的证据状态（不表示实力排名）'});chart.set_x_axis({'max':24,'major_unit':4});chart.set_size({'width':950,'height':350});s.insert_chart('A9',chart)
s=sheet('场景权重',['场景','营养结算权重','采购权重','食安权重','依据/边界'],[20,20,18,18,90])
for r,prof in enumerate(D['profiles'],1):s.write(r,0,prof['name']);[s.write(r,j+1,x,inp) for j,x in enumerate(prof['weights'])];s.write(r,4,prof['reason']);s.set_row(r,45)
s.data_validation(1,1,7,3,{'validate':'decimal','criteria':'>=','value':0})
s=sheet('场景加权覆盖',['场景']+[v['name'] for v in V]+['指标性质'],[20]+[18]*5+[60])
for r,prof in enumerate(D['profiles'],1):
 s.write(r,0,prof['name'])
 for j,v in enumerate(V,1):
  if v['id']=='zk':s.write(r,j,'不计算');continue
  vr=j+1;rr=r+1
  f=f'=IF(SUM(场景权重!B{rr}:D{rr})=0,"",SUMPRODUCT(场景权重!B{rr}:D{rr},量化汇总!G{vr}:I{vr})/SUM(场景权重!B{rr}:D{rr}))'
  s.write_formula(r,j,f,pct,weighted(v['id'],prof['weights']))
 s.write(r,6,'假设权重下的公开声明覆盖；不是场景成熟度')
s=sheet('六场景证据',['厂商','场景','状态','来源ID','说明'],[18,20,20,18,90])
for r,a in enumerate(D['scenes'],1):s.write_row(r,0,[next(v['name'] for v in V if v['id']==a['vendor']),a['scene'],a['status'],' '.join(a['sources']),a['note']]);s.set_row(r,40)
s=sheet('商务交付比较',['维度','比较边界']+[v['name'] for v in V],[25,60]+[55]*5)
for r,c in enumerate(D['commercial'],1):
 s.write_row(r,0,[c['name'],c['criterion']]+[x[0]+' '+' '.join(x[1]) for x in c['cells']]);s.set_row(r,75)
s=sheet('172功能映射' ,['功能ID','对应指标','系统','入口','模块','功能','描述','角色','价值'],[10,12,25,23,24,35,85,40,45])
for r,f in enumerate(D['functions'],1):s.write_row(r,0,[f['id'],f['criterion'],f['system'],f['entry'],f['module'],f['function'],f['description'],f['role'],f['value']]);s.set_row(r,65)
s.autofilter(0,0,172,8)
s=sheet('来源账本',['ID','厂商','标题','URL','访问日','原文日期','等级','读取方式','支持范围','不证明'],[10,18,42,75,16,35,10,35,75,80])
for r,src in enumerate(D['sources'],1):
 s.write_row(r,0,[src['id'],next(v['name'] for v in V if v['id']==src['vendor']),src['title'],src['url'],src['access'],src['publication_date'],src['level'],src['read_status'],src['fact'],src['not_proven']]);s.write_url(r,3,src['url'],link);s.set_row(r,58)
s.autofilter(0,0,len(S),9)
s=sheet('实测量化',['厂商','样本场景/版本/配置','总订单数','计价正确订单','正确率','结算P95秒','真阳性告警','误报告警','漏报告警','告警精确率','告警召回率','监测小时','误报每小时','用例总数','闭环通过用例','闭环通过率','硬件元','软件元','接口元','实施元','年服务费元','年数','总成本元','证据链接/样本说明'],[18,50]+[18]*21+[65])
for r,v in enumerate(V,1):
 rr=r+1;s.write(r,0,v['name'])
 for j in range(1,24):s.write_blank(r,j,None,inp)
 s.write(r,21,3,inp)
 for col,fo,ff in [(4,f'IF(OR(COUNT(C{rr}:D{rr})<2,C{rr}=0),"",D{rr}/C{rr})',pct),(9,f'IF(OR(COUNT(G{rr}:H{rr})<2,SUM(G{rr}:H{rr})=0),"",G{rr}/SUM(G{rr}:H{rr}))',pct),(10,f'IF(OR(COUNT(G{rr},I{rr})<2,SUM(G{rr},I{rr})=0),"",G{rr}/SUM(G{rr},I{rr}))',pct),(12,f'IF(OR(COUNT(H{rr},L{rr})<2,L{rr}=0),"",H{rr}/L{rr})',num),(15,f'IF(OR(COUNT(N{rr}:O{rr})<2,N{rr}=0),"",O{rr}/N{rr})',pct),(22,f'IF(COUNT(Q{rr}:V{rr})<6,"",SUM(Q{rr}:T{rr})+U{rr}*V{rr})',num)]:s.write_formula(r,col,'='+fo,ff,'')
 s.set_row(r,45)
s.write(8,0,'实验规则');s.merge_range(8,1,8,23,'统一菜单、光照、餐盘、设备与网络；原始耗时记录计算P95。比率均保留分子分母。验证成本统一含税口径、授权数量、接口范围和三年运维，不含未列明的其他费用。',fmt)
wb.close()
# High-resolution vector charts.
def text(x,y,t,size=24,color='#223B36',weight='400'):
 return f'<text x="{x}" y="{y}" font-size="{size}" fill="{color}" font-weight="{weight}">{E(str(t))}</text>'
def rect(x,y,w,h,fill,rx=8):return f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{fill}"/>'
def svg(title,sub,w=1680,h=1800):return [f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" viewBox="0 0 {w} {h}" font-family="Microsoft YaHei, PingFang SC, sans-serif">',rect(0,0,w,h,'#FAFAF7',0),rect(64,65,8,58,'#E65C1E',0),text(92,95,title,40,weight='700'),text(92,139,sub,21,color='#5D6B66')]
g=svg('五厂商产品能力 · 公开证据矩阵','2026.09.23  /  24项统一指标 × 5家厂商  /  官网声明，尚未统一实测',h=1900)
for j,(k,label) in enumerate(D['statuses'].items()):g += [rect(80+j*365,180,28,28,COL[k],5),text(120+j*365,202,k+' '+label,22)]
g += [text(80,250,'每格标记表示证据状态；不是功能强弱或成熟度。H也不代表同一SKU默认包含。',22)]
xs=[535,754,973,1192,1411]
for x,v in zip(xs,V):g += [text(x,310,v['name'],25,weight='700')]
y=355
for dom in D['domains']:
 g += [rect(75,y-28,1530,42,'#E8ECE8',4),text(94,y,dom['name']+' · 8项',23,weight='700')];y+=52
 for c in [c for c in C if c['domain']==dom['id']]:
  g.append(text(92,y+27,c['id']+'  '+c['name'],22))
  for x,v in zip(xs,V):
   a=A[v['id'],c['id']];g += [rect(x-20,y,188,40,LIGHT[a['status']],5),text(x+47,y+28,a['status'],24,COL[a['status']],'700')]
  y+=48
 y+=38
for v,x in zip(V,xs):
 co=counts(v['id']);g.append(text(x-12,1792,f"H {co['H']} · P {co['P']} · U {co['U']}" if v['id']!='zk' else '国内同口径：不足',20))
g += [text(80,1857,'阅读要点：多数产品已覆盖三大系统；真正的差异，需要下钻到业务深度、统一实测和交付证据。',22),'</svg>']
(OUT/'01-五厂商证据矩阵.svg').write_text(''.join(g))
g=svg('三大系统 · 声明覆盖与信息缺口','固定分母：每个系统8项。绿色=明确声明；黄色=部分；灰色=未获。',h=1280)
for j,dom in enumerate(D['domains']):
 x=80+j*535;g.append(text(x,235,dom['name'],27,weight='700'))
 for i,v in enumerate(V):
  y=295+i*137;g.append(text(x,y,v['name'],22));co=counts(v['id'],dom['id'])
  if v['id']=='zk':g += [rect(x,y+20,450,28,'#EAF0FA'),text(x,y+74,'海外线索，国内不计百分比',19,color='#667DA3')];continue
  st=x
  for k in ['H','P','U']:
   width=450*co[k]/8
   if width:g.append(rect(st,y+20,width,28,COL[k],0));st+=width
  g.append(text(x,y+75,f"明确 {co['H']}/8 ({co['H']/8:.1%}) · 部分 {co['P']} · 未获 {co['U']}",20))
g += [rect(80,1050,1510,140,'#E8ECE8'),text(110,1095,'这些百分比衡量本轮材料是否充分回答统一指标。',27,weight='700'),text(110,1140,'康比特内部172项功能与官网披露粒度不同；不能用本图断言其功能少于竞品。',24),text(80,1240,'完整来源、每格判定与下层功能，见交互看板及Excel台账。访问日期：2026-09-23。',21),'</svg>']
(OUT/'02-三系统量化覆盖.svg').write_text(''.join(g))
g=svg('从“有功能”到“做得好” · 下一层实测','本轮尚未实测；以下是五家共用的验证口径，实际数值在Excel输入后自动计算。',h=1120)
measures=[('交易正确率','计价正确订单 ÷ 总订单','统一菜单、餐具、重量与支付方式；保留样本'),('结算耗时 P95','95%的订单耗时不超过该值','按同一流程起止点记录；不要混用识别与支付耗时'),('食安精确率 / 召回率','TP/(TP+FP)  /  TP/(TP+FN)','统一事件集；另记录每小时误报，不能只报准确率'),('业务闭环通过率','通过用例数 ÷ 统一用例总数','覆盖正常、分支、权限、异常、重试和恢复'),('三年总成本','硬件+软件+接口+实施+3×年服务费','同规模、同授权与含税口径；缺报价保持空白'),('场景交付成熟度','现场验收 / 使用记录 / 持续效果','医院患者餐与养老护理单独验收，不套用员工餐')]
for i,(n,f,cond) in enumerate(measures):
 y=215+i*130;g += [rect(80,y-32,1520,112,'#FFFFFF'),text(110,y+10,n,26,weight='700'),text(560,y+8,f,24),text(560,y+48,cond,21,color='#5D6B66')]
g += [text(80,1060,'深度等级：未测留空；0不支持 / 1基本动作 / 2正常闭环 / 3异常恢复 / 4跨系统与性能验收。',22),'</svg>']
(OUT/'03-统一实测量化框架.svg').write_text(''.join(g))
# HTML derives from pakco webpage skeleton; embeds local token styles, no network dependencies.
template=Path('/Users/jack/code/099-github/pakco-html/templates/webpage.html').read_text()
css='''
:root{--bg:#fafaf7;--surface:#fff;--text-1:#223b36;--text-2:#66726e;--accent:#e65c1e;--border:#dfe5de;--good:#24775d;--warn:#b97a23;--blue:#667da3;--radius:14px;--font-sans:"Microsoft YaHei","PingFang SC",sans-serif}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text-1);font:15px/1.7 var(--font-sans)}.page{max-width:1420px;margin:auto;padding:45px 48px}a{color:var(--good);text-underline-offset:4px}h1{font-size:clamp(30px,4vw,49px);line-height:1.2;margin:16px 0 20px}h2{font-size:27px;line-height:1.35;margin:0 0 14px}h3{font-size:19px;margin:0 0 12px}.kicker{font-size:12px;letter-spacing:.13em;color:var(--accent);font-weight:700}.lede{font-size:19px;max-width:970px;color:var(--text-2)}.section-break{height:1px;background:var(--border);margin:36px 0}section{scroll-margin-top:25px}.grid{display:grid;gap:18px}.g3{grid-template-columns:repeat(3,minmax(0,1fr))}.g2{grid-template-columns:repeat(2,minmax(0,1fr))}.card{background:var(--surface);padding:23px;border:1px solid var(--border);border-radius:var(--radius)}.big{font-size:40px;font-weight:700;line-height:1.25}.dim{color:var(--text-2)}.note{padding:15px 18px;background:#edf1eb;border-left:4px solid var(--good);margin:18px 0}.toolbar{display:flex;flex-wrap:wrap;gap:15px;align-items:end;margin:20px 0}label{display:block;font-size:13px}select,input,button{font:inherit;border:1px solid var(--border);border-radius:7px;background:white;color:var(--text-1);padding:8px 11px}input{max-width:100%}button{cursor:pointer}button:hover,button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.pills{display:flex;flex-wrap:wrap;gap:9px}.pill{padding:5px 12px;border-radius:99px;border:1px solid var(--border);font-size:13px}nav{display:flex;gap:20px;flex-wrap:wrap;margin-top:25px}.scroll{overflow:auto;border:1px solid var(--border);border-radius:12px;background:white}table{border-collapse:collapse;width:100%;min-width:920px}td,th{padding:11px 13px;border-bottom:1px solid var(--border);text-align:left}thead{background:#eef1ec}tbody th{font-size:14px;font-weight:500;min-width:225px}td{text-align:center}.cell{width:100%;min-width:94px;padding:7px;font-size:13px;border:0;font-weight:600}.H{color:#1f684f;background:#e5f1eb}.P{color:#81500e;background:#faefd9}.R{color:#465e83;background:#eaf0fa}.U{color:#616a70;background:#eef0f1}.domainrow th{background:#e8ece8;font-weight:700}.brow{margin:18px 0}.bhead{display:flex;justify-content:space-between;gap:12px}.bar{height:14px;display:flex;background:#eef0f1;margin:8px 0;border-radius:4px;overflow:hidden}.bar span{display:block}.small{font-size:12px}.source-list{display:grid;grid-template-columns:1fr 1fr;gap:12px}.source{padding:17px;background:white;border:1px solid var(--border);border-radius:10px;overflow-wrap:anywhere}.source a{font-size:14px}details{background:white;border:1px solid var(--border);padding:14px;border-radius:9px;margin:10px 0}summary{cursor:pointer;font-weight:600}.func{padding:12px 0;border-bottom:1px solid var(--border)}.func p{margin:4px 0}.footer{padding:25px 0;color:var(--text-2);font-size:12px}dialog{width:min(760px,95vw);max-height:85vh;overflow:auto;border:1px solid var(--border);border-radius:15px;padding:28px;color:var(--text-1)}dialog::backdrop{background:#16312888}.close{float:right}dialog h3{margin-top:18px}.legend span{margin-right:18px}.legend b{padding:3px 9px;border-radius:4px}.chart-card{min-width:0}.weights{display:flex;gap:12px;flex-wrap:wrap}.weights input{width:84px}.full{grid-column:1/-1}.threshold{font-size:12px;color:var(--text-2)}@media(max-width:700px){.page{padding:25px 19px}.g3,.g2,.source-list{grid-template-columns:1fr}.lede{font-size:16px}.card{padding:18px}h2{font-size:24px}.toolbar>*{max-width:100%}.scroll{max-width:100%}}@media print{.toolbar,nav,.close{display:none}.page{padding:0}.card{break-inside:avoid}a{color:inherit}.source-list{display:block}}
'''
body='''<div class="page"><section class="hero"><p class="kicker">CPT / COMPETITIVE EVIDENCE / 2026.09.23</p><h1>把产品架构，变成<br>可追溯的竞品对比</h1><p class="lede">康比特 × 熵基科技 × 雄伟科技 × 戈子科技 × 满客宝。先看公开能力声明覆盖，再用同一套演示与实测判断业务深度。</p><div class="pills"><span class="pill">内部评审版</span><span class="pill">公开声明 · C级证据</span><span class="pill">尚未统一POC</span></div><nav><a href="#matrix">能力矩阵</a><a href="#coverage">量化覆盖</a><a href="#scenes">六场景</a><a href="#functions">172项下钻</a><a href="#measure">实测口径</a><a href="#sources">来源账本</a></nav></section>
<div class="section-break"></div><section><div class="grid g3"><div class="card"><div class="big">24 × 5</div><p>统一指标与逐格证据判断</p></div><div class="card"><div class="big">172</div><p>原架构功能全部保留并映射</p></div><div class="card"><div class="big" id="sourceCount"></div><p>本轮实际读取的官方页面来源</p></div></div><div class="note"><b>当前能确认：</b>雄伟、戈子、满客宝均有营养、采购、食安能力声明。“三大系统齐全”本身不足以证明差异化。<br><b>需要验证：</b>营养服务效果、异常流程、跨系统一致性、现场性能与三年成本。</div></section>
<div class="section-break"></div><section id="matrix"><p class="kicker">01 / CAPABILITY EVIDENCE</p><h2>产品能力证据矩阵</h2><p class="dim">点击任意格查看判定条件、证据来源和演示动作；手机可左右滑动表格。公开声明为产品组合口径，不表示某一SKU默认包含。</p><div class="toolbar"><label>系统<select id="domain"><option value="">全部系统</option><option value="N">营养结算</option><option value="P">智慧采购</option><option value="S">智慧食安</option></select></label><label>搜索指标<input id="search" placeholder="例如：营养、盘点、复核"></label><label><input type="checkbox" id="gaps"> 只看存在信息缺口的指标</label><button id="reset">重置</button></div><p class="legend"><span><b class="H">H</b> 明确声明</span><span><b class="P">P</b> 部分声明</span><span><b class="R">R</b> 海外邻近</span><span><b class="U">U</b> 未获证据</span></p><div class="scroll"><table id="heatmap"></table></div><p class="small dim">U不是不支持。熵基国内官方材料不足；海外线索单列，不参与国内覆盖百分比。所有厂商均未获得本轮统一实测分。</p></section>
<div class="section-break"></div><section id="coverage"><p class="kicker">02 / QUANTIFIED DISCLOSURE</p><h2>分系统看覆盖，也看缺口</h2><p class="dim">每系统8项，固定分母。百分比只表示明确声明项占比，不表示产品强弱。</p><div class="grid g3" id="domainCharts"></div><div class="card" style="margin-top:20px"><h3>切换场景，观察关注权重变化</h3><div class="toolbar"><label>场景预设<select id="profile"></select></label><div class="weights"><label>营养结算<input type="number" id="w0" min="0" value="1"></label><label>智慧采购<input type="number" id="w1" min="0" value="1"></label><label>智慧食安<input type="number" id="w2" min="0" value="1"></label></div></div><p id="profileReason" class="dim small"></p><div id="weighted"></div><p class="small dim">加权声明覆盖 = Σ(系统权重 × 系统H数/8) ÷ 权重合计。权重为D级分析假设，可修改；结果不是场景成熟度或采购推荐分。</p></div></section>
<div class="section-break"></div><section id="scenes"><p class="kicker">03 / SCENARIO EVIDENCE</p><h2>六场景，区分案例与方案</h2><p class="dim">“案例声明”指官网项目展示；“场景方案”指适用范围说明。均未取得本轮客户验收。医院仅按通用医院场景归类；患者餐、营养科、住养护理另设门槛。</p><div class="scroll"><table id="sceneTable"></table></div><div class="grid g2" style="margin-top:18px"><div class="card"><h3>医院的额外门槛</h3><p>HIS接口版本、医嘱变更、禁忌检查、营养科审批、床位迁转、送餐签收及权限审计，逐项演示。</p></div><div class="card"><h3>养老机构的额外门槛</h3><p>代理操作、质构餐、过敏禁忌、护理反馈、实际摄入与取餐量区分。社区助餐案例不能直接替代。</p></div></div></section>
<div class="section-break"></div><section id="functions"><p class="kicker">04 / ARCHITECTURE DRILL-DOWN</p><h2>保留原来的172项功能，逐层下钻</h2><p class="dim">这里是康比特内部资料映射，供确定演示脚本与补证任务使用。上层指标H，不代表竞品实现本栏每一个子功能。</p><label>选择对比指标<select id="functionSelect"></select></label><div id="functionList"></div></section>
<div class="section-break"></div><section id="measure"><p class="kicker">05 / COMMON TEST PROTOCOL</p><h2>真正比较实力，要补这组量化数据</h2><div class="grid g2" id="measureCards"></div><div class="note">能力深度保持独立：未测留空；0=实测不支持，1=基本动作，2=正常闭环，3=异常恢复，4=跨系统与性能验收。官网不能直接换算为深度分。</div><p><a href="deliverables/五厂商量化对比与实测台账.xlsx">下载可编辑Excel：指标、证据、场景权重、172功能与实测公式</a></p></section>
<div class="section-break"></div><section id="commercial"><h2>部署、接口、服务与成本也要放在一起比较</h2><p class="dim">以下为公开声明与资料缺口，不进入24项功能覆盖分母。点击来源核对具体产品范围。</p><div class="scroll"><table id="commercialTable"></table></div></section><div class="section-break"></div><section><p class="kicker">06 / WHAT TO DO NEXT</p><h2>把差异化主张变成可验证动作</h2><div class="grid g3"><div class="card"><h3>营养闭环</h3><p>用同样三组画像演示建议、实际取餐、报告、营养师复核。康比特的专业服务需要可交付的人员、频次和报告样例支撑。</p></div><div class="card"><h3>跨系统闭环</h3><p>挑一笔采购，从询价到验收、退货、库存、成本、食安追溯。用统一异常清单比较，避免只看菜单数量。</p></div><div class="card"><h3>交付与成本</h3><p>同一规模和设备配置询价，逐项列模块、接口、实施与运维。现场验收证据优先于官网效果百分比。</p></div></div><details><summary>资料边界、冲突与缺口</summary><ul id="limitations"></ul><p>本轮刷新公开页面及既有内部功能快照；本地竞品目录发现98份候选文件，戈子食安目录另1份表格，仅作后续候选，未批量深读。企业微信在线文档未在本轮重新获取，未声称已全量刷新。</p></details></section>
<div class="section-break"></div><section id="sources"><p class="kicker">07 / SOURCE LEDGER</p><h2>每项判断都有来源入口</h2><p class="dim">访问日期2026-09-23；未标日期的页面不推断其发布日期。直接页面与完整搜索索引正文使用同一内容判定，读取方式单列。</p><div class="source-list" id="sourceList"></div></section><div class="footer">康比特数字业务 · 产品对比内部评审材料 / 24项指标、120格判断、172功能映射 / 本地静态可视化</div></div><dialog id="detail"><button class="close" onclick="document.getElementById('detail').close()">关闭</button><div id="detailBody"></div></dialog>'''
js='''
const D=DATA_JSON,A=Object.fromEntries(D.assessments.map(x=>[x.vendor+'-'+x.criterion,x])),S=Object.fromEntries(D.sources.map(x=>[x.id,x]));
const $=x=>document.getElementById(x),esc=x=>String(x).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function count(v,d){let a=D.assessments.filter(x=>x.vendor===v&&x.criterion.startsWith(d||''));return Object.fromEntries(Object.keys(D.statuses).map(s=>[s,a.filter(x=>x.status===s).length]));}
function sourceLinks(ids){return ids.length?ids.map(id=>`<p><a href="${S[id].url}" target="_blank" rel="noopener">${id} · ${esc(S[id].title)}</a><br><span class="small dim">C级 · ${esc(S[id].read_status)} · 访问 ${S[id].access}</span></p>`).join(''):'<p>本轮未获同口径证据，需进一步索取产品资料或演示。</p>';}
function show(v,c){let a=A[v+'-'+c],crit=D.criteria.find(x=>x.id===c),vendor=D.vendors.find(x=>x.id===v);$('detailBody').innerHTML=`<p class="kicker">${vendor.name} / ${c}</p><h2>${crit.name}</h2><p class="pill ${a.status}">${D.statuses[a.status]}</p><h3>同一判定条件</h3><p>${crit.rule}</p><h3>本格判断</h3><p>${a.note}</p><h3>证据</h3>${sourceLinks(a.sources)}<h3>需要演示验证</h3><p>${crit.test}</p><p class="small dim">深度尚未实测；声明范围需按实际SKU与项目确认。</p>`;$('detail').showModal();}
function renderMatrix(){const d=$('domain').value,q=$('search').value.trim(),g=$('gaps').checked;let h='<thead><tr><th>统一指标</th>'+D.vendors.map(v=>'<th>'+v.name+'</th>').join('')+'</tr></thead><tbody>';let last='';for(const c of D.criteria.filter(c=>(!d||c.domain===d)&&(!q||(c.id+c.name+c.rule).includes(q))&&(!g||D.vendors.some(v=>v.id!=='zk'&&A[v.id+'-'+c.id].status!=='H')))){if(last!==c.domain){h+=`<tr class="domainrow"><th colspan="6">${D.domains.find(x=>x.id===c.domain).name}</th></tr>`;last=c.domain;}h+=`<tr data-criterion="${c.id}"><th>${c.id} · ${c.name}</th>`+D.vendors.map(v=>{let a=A[v.id+'-'+c.id];return `<td><button aria-label="${v.name} ${c.name} ${D.statuses[a.status]}" class="cell ${a.status}" data-vendor="${v.id}" data-criterion="${c.id}">${a.status} ${D.statuses[a.status]}</button></td>`;}).join('')+'</tr>';}h+='</tbody>';$('heatmap').innerHTML=h;$('heatmap').querySelectorAll('button').forEach(b=>b.onclick=()=>show(b.dataset.vendor,b.dataset.criterion));}
function domainCharts(){$('domainCharts').innerHTML=D.domains.map(d=>`<div class="card chart-card"><h3>${d.name}</h3>`+D.vendors.map(v=>{let c=count(v.id,d.id);if(v.id==='zk')return '<div class="brow"><b>熵基科技</b><p class="small dim">海外线索 · 国内百分比不计算</p></div>';return `<div class="brow"><div class="bhead"><span>${v.name}</span><b>${c.H}/8 · ${(c.H/8*100).toFixed(1)}%</b></div><div class="bar">`+['H','P','U'].map(k=>`<span style="width:${c[k]/8*100}%;background:${{H:'#24775d',P:'#b97a23',U:'#a5adb3'}[k]}"></span>`).join('')+`</div><div class="small dim">明确 ${c.H} / 部分 ${c.P} / 未获 ${c.U}</div></div>`;}).join('')+'</div>').join('');}
function weighted(){const w=[0,1,2].map(i=>Math.max(0,Number($('w'+i).value)||0)),sum=w.reduce((a,b)=>a+b,0);$('weighted').innerHTML=D.vendors.map(v=>{if(v.id==='zk')return '<p class="small dim">熵基科技：国内同口径证据不足，不计算加权覆盖。</p>';const value=sum?D.domains.reduce((n,d,i)=>n+w[i]*count(v.id,d.id).H/8,0)/sum:null;return `<div class="brow" data-weighted="${v.id}"><div class="bhead"><span>${v.name}</span><b>${value===null?'权重合计为0':(value*100).toFixed(1)+'%'}</b></div><div class="bar"><span style="width:${(value||0)*100}%;background:var(--good)"></span></div></div>`;}).join('');}
function profile(){const p=D.profiles[+$('profile').value];p.weights.forEach((v,i)=>$('w'+i).value=v);$('profileReason').textContent=p.reason;weighted();}
function functions(){const k=$('functionSelect').value,fs=D.functions.filter(f=>f.criterion===k);$('functionList').innerHTML=`<p class="dim">${k} 对应 ${fs.length} 项内部功能，映射按主要业务归属；不代表指标覆盖率。</p>`+fs.map(f=>`<details><summary>${f.id} · ${f.function}</summary><p class="small dim">${f.system} / ${f.entry} / ${f.module}</p><p>${f.description}</p><p>主要角色：${f.role}</p><p>价值：${f.value}</p></details>`).join('');}
$('sourceCount').textContent=D.sources.length;$('profile').innerHTML=D.profiles.map((p,i)=>`<option value="${i}">${p.name}</option>`).join('');$('functionSelect').innerHTML=D.criteria.map(c=>`<option value="${c.id}">${c.id} · ${c.name}</option>`).join('');
$('sourceList').innerHTML=D.sources.map(s=>`<div class="source"><b>${s.id} · ${D.vendors.find(v=>v.id===s.vendor).name}</b><p><a href="${s.url}" target="_blank" rel="noopener">${s.title}</a></p><p class="small">${s.fact}</p><p class="small dim">C级 / ${s.read_status}<br>访问：${s.access} / 发布：${s.publication_date}</p></div>`).join('');
$('sceneTable').innerHTML='<thead><tr><th>场景</th>'+D.vendors.map(v=>'<th>'+v.name+'</th>').join('')+'</tr></thead><tbody>'+['政府机关','企业','高校','K12','医院','养老机构'].map(n=>'<tr><th>'+n+'</th>'+D.vendors.map(v=>{let a=D.scenes.find(x=>x.vendor===v.id&&x.scene===n);return `<td><b>${a.status}</b><p class="small dim">${a.note}</p>${a.sources.map(id=>`<a href="${S[id].url}" target="_blank" rel="noopener">${id}</a>`).join(' ')}</td>`;}).join('')+'</tr>').join('')+'</tbody>';
$('commercialTable').innerHTML='<thead><tr><th>维度与统一边界</th>'+D.vendors.map(v=>'<th>'+v.name+'</th>').join('')+'</tr></thead><tbody>'+D.commercial.map(c=>'<tr><th>'+c.name+'<p class="small dim">'+c.criterion+'</p></th>'+c.cells.map(cell=>'<td>'+cell[0]+'<p>'+cell[1].map(id=>`<a href="${S[id].url}" target="_blank" rel="noopener">${id}</a>`).join(' ')+'</p></td>').join('')+'</tr>').join('')+'</tbody>';
const ms=MEASURES_JSON;$('measureCards').innerHTML=ms.map(m=>`<div class="card"><h3>${m[0]}</h3><p><b>${m[1]}</b></p><p class="dim">${m[2]}</p><span class="pill">当前：待统一实测 / 无数值</span></div>`).join('');$('limitations').innerHTML=D.limitations.map(x=>'<li>'+x+'</li>').join('');
['domain','search','gaps'].forEach(x=>$(x).addEventListener(x==='search'?'input':'change',renderMatrix));$('reset').onclick=()=>{$('domain').value='';$('search').value='';$('gaps').checked=false;renderMatrix();};$('profile').onchange=profile;[0,1,2].forEach(i=>$('w'+i).oninput=weighted);$('functionSelect').onchange=functions;renderMatrix();domainCharts();profile();functions();
'''.replace('DATA_JSON',json.dumps(D,ensure_ascii=False).replace('</','<\\/')).replace('MEASURES_JSON',json.dumps(measures,ensure_ascii=False))
page=template[:template.index('<head>')]+'<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>智慧食堂五厂商竞品对比</title><style>'+css+'</style></head><body class="webpage">'+body+'<script>'+js+'</script></body></html>'
(P/'竞品对比可视化.html').write_text(page)
(P/'summary.json').write_text(json.dumps({'date':D['date'],'vendors':{v['id']:counts(v['id']) for v in V},'functions':len(D['functions']),'sources':len(S)},ensure_ascii=False,indent=2))
print(json.dumps({'html_bytes':len(page.encode()),'sources':len(S),'workbook':str(OUT/'五厂商量化对比与实测台账.xlsx')},ensure_ascii=False))
