oh-my-knowledge 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +5 -3
  2. package/README.zh.md +5 -3
  3. package/dist/assets/agent-skills/omk/SKILL.md +4 -2
  4. package/dist/assets/agent-skills/omk/references/commands.md +86 -3
  5. package/dist/cli/commands/doctor.js +19 -4
  6. package/dist/cli/commands/evolve.d.ts +11 -0
  7. package/dist/cli/commands/evolve.js +105 -5
  8. package/dist/cli/commands/list.d.ts +5 -26
  9. package/dist/cli/commands/list.js +17 -142
  10. package/dist/cli/commands/promote.d.ts +24 -0
  11. package/dist/cli/commands/promote.js +158 -0
  12. package/dist/cli/commands/rollback.d.ts +20 -0
  13. package/dist/cli/commands/rollback.js +95 -0
  14. package/dist/cli/commands/sample.d.ts +1 -0
  15. package/dist/cli/commands/sample.js +10 -4
  16. package/dist/cli/lib/cell-format.d.ts +17 -0
  17. package/dist/cli/lib/cell-format.js +19 -0
  18. package/dist/cli/lib/cmd-flags.d.ts +1 -0
  19. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  20. package/dist/cli/lib/i18n-dict/common.js +8 -0
  21. package/dist/cli/lib/i18n-dict/list.d.ts +1 -1
  22. package/dist/cli/lib/i18n-dict/list.js +4 -0
  23. package/dist/cli/lib/i18n-dict/promote.d.ts +3 -0
  24. package/dist/cli/lib/i18n-dict/promote.js +54 -0
  25. package/dist/cli/lib/i18n-dict/rollback.d.ts +3 -0
  26. package/dist/cli/lib/i18n-dict/rollback.js +22 -0
  27. package/dist/cli/lib/i18n-dict.d.ts +3 -1
  28. package/dist/cli/lib/i18n-dict.js +4 -0
  29. package/dist/cli/lib/progress.d.ts +3 -0
  30. package/dist/cli/lib/progress.js +22 -0
  31. package/dist/cli/lib/source-probe.d.ts +15 -0
  32. package/dist/cli/lib/source-probe.js +129 -0
  33. package/dist/doctor/endpoint-rule.d.ts +60 -0
  34. package/dist/doctor/endpoint-rule.js +381 -0
  35. package/dist/doctor/health/load-custom-dimensions.d.ts +6 -0
  36. package/dist/doctor/health/load-custom-dimensions.js +43 -3
  37. package/dist/doctor/index.js +8 -1
  38. package/dist/executors/codex-cli.js +5 -1
  39. package/dist/managed/index.d.ts +1 -0
  40. package/dist/managed/index.js +1 -0
  41. package/dist/managed/list-view.d.ts +3 -0
  42. package/dist/managed/list-view.js +8 -4
  43. package/dist/managed/promote-gate.d.ts +40 -0
  44. package/dist/managed/promote-gate.js +37 -0
  45. package/dist/managed/store.d.ts +25 -2
  46. package/dist/managed/store.js +92 -3
  47. package/dist/renderer/doctor-detail-renderer.d.ts +9 -0
  48. package/dist/renderer/doctor-detail-renderer.js +114 -0
  49. package/dist/renderer/html-renderer.d.ts +3 -2
  50. package/dist/renderer/html-renderer.js +99 -99
  51. package/dist/renderer/icons.d.ts +29 -0
  52. package/dist/renderer/icons.js +66 -0
  53. package/dist/renderer/layout.js +81 -68
  54. package/dist/renderer/observation-inbox/styles.d.ts +1 -1
  55. package/dist/renderer/observation-inbox/styles.js +53 -53
  56. package/dist/renderer/report-shell.d.ts +77 -0
  57. package/dist/renderer/report-shell.js +223 -0
  58. package/dist/renderer/skill-detail-renderer.js +147 -352
  59. package/dist/renderer/skill-health-renderer.js +50 -73
  60. package/dist/renderer/skill-list-renderer.js +358 -306
  61. package/dist/renderer/summary.js +272 -162
  62. package/dist/renderer/test-view.d.ts +4 -3
  63. package/dist/renderer/test-view.js +386 -135
  64. package/dist/server/report-server.js +142 -28
  65. package/dist/types/doctor.d.ts +22 -0
  66. package/dist/types/managed.d.ts +17 -3
  67. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { e, fmtNum, fmtCost, fmtDuration, COLORS, t } from './layout.js';
2
+ import { icon as svgIcon } from './icons.js';
2
3
  import { generateAnalysisSummary } from '../analysis/report-diagnostics.js';
3
4
  import { pValueCategory } from '../eval-core/statistics.js';
4
5
  import { computeVerdict, ENSEMBLE_STRONG_PEARSON, ENSEMBLE_DISSENT_PEARSON } from '../eval-core/verdict.js';
@@ -363,111 +364,162 @@ export function renderSaturationCurve(saturation, variants, lang) {
363
364
  </svg>
364
365
  ${noteHtml}`;
365
366
  }
367
+ const clamp01 = (x) => Math.max(0, Math.min(1, x));
368
+ /**
369
+ * 六维雷达图(SVG)。把六维归一化到 0-1「越外越好」画多边形,直观看一个 variant
370
+ * 的强弱形状 / 多 variant 的形状差异。精确数值由下方数值表兜底(omk 受众要精确数字)。
371
+ *
372
+ * 归一化口径:事实/行为/评委 = 分÷5(绝对);成本/效率按绝对软参考($1/用例、120s/次
373
+ * 记为 0,越省越外);稳定性 = 1 − CV/0.3。null(未测量/单组无对照)画空心点、中性半径。
374
+ */
375
+ /** 六维 i18n key → SVG 图标名(替代内嵌 emoji)。 */
376
+ const DIM_ICON = {
377
+ dimFact: 'fact', dimBehavior: 'behavior', dimJudge: 'judge',
378
+ dimCost: 'cost', dimEfficiency: 'efficiency', dimStability: 'stability', dimQuality: 'quality',
379
+ };
380
+ /** 取维度标签并去掉历史内嵌 emoji 前缀(i18n 串形如「📋 事实」,图标改由 SVG 渲染)。 */
381
+ function dimText(key, lang) {
382
+ return t(key, lang).replace(/^\S+\s+/, '');
383
+ }
384
+ // 雷达图例点击切换:全局只挂一次 document 委托,toggle 对应 .rad-series 的显隐。
385
+ const RADAR_TOGGLE_SCRIPT = `<script>
386
+ if(!window.__radarToggle){window.__radarToggle=1;document.addEventListener('click',function(ev){var b=ev.target.closest&&ev.target.closest('.rad-leg');if(!b)return;var w=b.closest('.sm-radar');if(!w)return;var g=w.querySelector('.rad-series[data-idx="'+b.getAttribute('data-idx')+'"]');var off=b.classList.toggle('off');b.setAttribute('aria-pressed',String(!off));if(g)g.style.display=off?'none':'';});}
387
+ </script>`;
388
+ function renderRadarChart(entries, dims, lang) {
389
+ const W = 300, H = 250, cx = 150, cy = 118, R = 82;
390
+ const n = dims.length;
391
+ const ang = (i) => (-90 + (360 / n) * i) * Math.PI / 180;
392
+ const pt = (g, i) => [cx + R * g * Math.cos(ang(i)), cy + R * g * Math.sin(ang(i))];
393
+ const fmtPt = (g, i) => pt(g, i).map((x) => x.toFixed(1)).join(',');
394
+ const rings = [0.25, 0.5, 0.75, 1].map((lv) => `<polygon points="${dims.map((_, i) => fmtPt(lv, i)).join(' ')}" class="rad-grid"/>`).join('');
395
+ const spokes = dims.map((_, i) => { const [x, y] = pt(1, i); return `<line x1="${cx}" y1="${cy}" x2="${x.toFixed(1)}" y2="${y.toFixed(1)}" class="rad-spoke"/>`; }).join('');
396
+ const labels = dims.map((d, i) => {
397
+ const [x, y] = pt(1.22, i);
398
+ const anchor = x < cx - 4 ? 'end' : x > cx + 4 ? 'start' : 'middle';
399
+ return `<text x="${x.toFixed(1)}" y="${y.toFixed(1)}" text-anchor="${anchor}" dominant-baseline="middle" class="rad-label">${e(d.label)}</text>`;
400
+ }).join('');
401
+ const polys = entries.map((en, idx) => {
402
+ const ptsStr = dims.map((d, i) => fmtPt(en.g[d.key] == null ? 0.5 : Math.max(0.04, en.g[d.key]), i)).join(' ');
403
+ const dots = dims.map((d, i) => {
404
+ const g = en.g[d.key];
405
+ const [x, y] = pt(g == null ? 0.5 : Math.max(0.04, g), i);
406
+ const na = g == null;
407
+ return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3" class="rad-dot${na ? ' rad-dot--na' : ''}"${na ? '' : ` style="fill:${en.color}"`}><title>${e(en.name)} · ${e(d.label)}${na ? (lang === 'zh' ? ' 未测量/无对照' : ' n/a') : ''}</title></circle>`;
408
+ }).join('');
409
+ return `<g class="rad-series" data-idx="${idx}"><polygon points="${ptsStr}" class="rad-area" style="fill:${en.color};stroke:${en.color}"/>${dots}</g>`;
410
+ }).join('');
411
+ const svg = `<svg viewBox="0 0 ${W} ${H}" class="rad-svg" role="img" aria-label="${lang === 'zh' ? '六维雷达图' : 'Six-dimension radar'}">${rings}${spokes}${polys}${labels}</svg>`;
412
+ // 多 variant 时给一排可点击图例,点击切换该 variant 多边形显隐(单独看 / 对比子集)。
413
+ const legend = entries.length > 1
414
+ ? `<div class="rad-legend">${entries.map((en, idx) => `<button type="button" class="rad-leg" data-idx="${idx}" aria-pressed="true" style="--c:${en.color}"><span class="rad-leg-dot"></span>${e(en.name)}</button>`).join('')}${RADAR_TOGGLE_SCRIPT}</div>`
415
+ : '';
416
+ return `<div class="sm-radar">${svg}${legend}</div>`;
417
+ }
366
418
  export function renderSummaryCards(variants, summary, lang, variance) {
367
- // 六维对比表格:事实 / 行为 / LLM 评价 / 成本 / 效率 / 稳定性。
368
- // 前三列(事实/行为/LLM 评价)是 task 级原始指标的 variant 聚合;成本/效率同。
369
- // 稳定性是 variant 级散度度量(需 --repeat ≥ 2)。
370
- //
371
- // composite 合成分 (= (fact + behavior + judge) / 3) 从 v0.16 起不再在此表主视觉呈现,
372
- // 仅保留在 report JSON 数据层 + Variance & Significance 表顶层 flat 字段(legacy)。
373
- // 综合分放在第一列, 紧贴 实验分组. 它是 fact/behavior/judge 三层的等权均值,
374
- // 在 omk 内部承担「跨 run 排序 / bootstrap CI / verdict 比较信号」三个角色 ——
375
- // 这里展示 + 表头 ? button 弹 modal 公开计算方式 + 局限, 让用户知情消费。
376
- // 其余六列保持 layer-first (fact / behavior / judge / cost / efficiency / stability)。
419
+ // 六维:事实 / 行为 / LLM 评价 / 成本 / 效率 / 稳定性。雷达图看形状,数值表给精确值。
420
+ // 综合分 (= (fact + behavior + judge) / 3) 并入数值表首列(大字号 + delta),不再单独一行一卡。
421
+ const isMulti = variants.length > 1;
422
+ const baselineComposite = summary[variants[0]]?.avgCompositeScore;
423
+ const scoringModalId = 'guide-scoring';
377
424
  const headerCols = [
378
- { key: 'dimFact', label: t('dimFact', lang) },
379
- { key: 'dimBehavior', label: t('dimBehavior', lang) },
380
- { key: 'dimJudge', label: t('dimJudge', lang) },
381
- { key: 'dimCost', label: t('dimCost', lang) },
382
- { key: 'dimEfficiency', label: t('dimEfficiency', lang) },
383
- { key: 'dimStability', label: t('dimStability', lang) },
425
+ { key: 'dimFact', label: dimText('dimFact', lang) },
426
+ { key: 'dimBehavior', label: dimText('dimBehavior', lang) },
427
+ { key: 'dimJudge', label: dimText('dimJudge', lang) },
428
+ { key: 'dimCost', label: dimText('dimCost', lang) },
429
+ { key: 'dimEfficiency', label: dimText('dimEfficiency', lang) },
430
+ { key: 'dimStability', label: dimText('dimStability', lang) },
384
431
  ];
385
- const scoringModalId = 'guide-scoring';
386
- // v0.30 — 拆 hero(每变体综合分一行)+ heatmap 表(去 composite,加色块)。
387
- // composite 从表里挪出来当 hero,大字号 + delta 跟 baseline 对比;
388
- // 维度细节继续用表,但每格按 score 浅色填充,便于扫读弱维度。
389
- const thead = `<tr><th data-i18n="variants">${t('variants', lang)}</th>${headerCols.map((c) => `<th data-i18n="${c.key}">${c.label}</th>`).join('')}</tr>`;
390
- // 渲染单层分数 cell(事实/行为/LLM 评价通用)— 加 heatmap 浅色背景
391
- function renderLayerCell(varianceMean, summaryValue, detailHtml = '') {
432
+ const compositeHint = `<button type="button" class="hint-btn" onclick="openModal('${scoringModalId}')" aria-label="${e(lang === 'zh' ? '综合分怎么算的?' : 'How is composite computed?')}" aria-haspopup="dialog">?</button>`;
433
+ const thead = `<tr><th data-i18n="variants">${t('variants', lang)}</th><th>${lang === 'zh' ? '综合' : 'Composite'} ${compositeHint}</th>${headerCols.map((c) => `<th data-i18n="${c.key}">${svgIcon(DIM_ICON[c.key], { size: 13, cls: 'sm-th-ico' })}${c.label}</th>`).join('')}</tr>`;
434
+ // 质量层(事实/行为/评委)cell — 1-5 分。只给数值上色(绿/黄/红),不再填单元格底色(底色块突兀;
435
+ // 维度强弱形状已由左侧雷达图传达,表格只需精确数值)。
436
+ const layerDim = (label, varianceMean, summaryValue, detailLines = [], iconName) => {
392
437
  const v = varianceMean ?? summaryValue;
393
- const hasValue = typeof v === 'number' && v > 0;
394
- const heatClass = hasValue
395
- ? (v >= 4 ? 'sm-heat-pass' : v >= 3 ? 'sm-heat-warn' : 'sm-heat-fail')
396
- : '';
397
- const color = hasValue
398
- ? (v >= 4 ? 'var(--green)' : v >= 3 ? 'var(--yellow)' : 'var(--red)')
399
- : 'var(--text-muted)';
400
- const display = hasValue ? v.toFixed(2) : '—';
401
- return `<td class="summary-cell ${heatClass}"><div class="summary-value summary-value-primary" style="color:${color}">${display}</div>${detailHtml}</td>`;
438
+ const has = typeof v === 'number' && v > 0;
439
+ return {
440
+ label,
441
+ value: has ? v.toFixed(2) : '—',
442
+ valueColor: has ? (v >= 4 ? 'var(--green)' : v >= 3 ? 'var(--yellow)' : 'var(--red)') : 'var(--text-muted)',
443
+ bg: '',
444
+ primary: true,
445
+ detailLines,
446
+ icon: iconName,
447
+ };
448
+ };
449
+ // 注意:detailLines 是「已转义/可信 HTML」契约 —— 既有纯文本(数字 metric)也有原始
450
+ // HTML(如 stabDetails 的 <span>),dimCell/dimMetric 都不再转义。caller push 的内容
451
+ // 里凡含 user 字符串(skill 名 / 用例文本)必须先过 e()。当前全是数字,安全。
452
+ const dimCell = (d) => {
453
+ const cls = d.primary ? 'summary-value summary-value-primary' : 'summary-value';
454
+ const detail = d.detailLines.map((l) => `<div class="summary-detail">${l}</div>`).join('');
455
+ return `<td class="summary-cell"${d.bg ? ` style="background:${d.bg}"` : ''}${d.title ? ` title="${e(d.title)}"` : ''}><div class="${cls}" style="color:${d.valueColor}">${d.value}</div>${detail}</td>`;
456
+ };
457
+ const dimMetric = (d) => {
458
+ const detail = d.detailLines.length > 0 ? `<span class="sm-metric-detail">${d.detailLines.join(' · ')}</span>` : '';
459
+ const ico = d.icon ? svgIcon(d.icon, { size: 14, cls: 'sm-mico' }) : '';
460
+ return `<div class="sm-metric"${d.title ? ` title="${e(d.title)}"` : ''}><span class="sm-metric-label">${ico}${d.label}</span><span class="sm-metric-val" style="color:${d.valueColor}">${d.value}</span>${detail}</div>`;
461
+ };
462
+ // 综合分 cell(表格首列,大字号 + delta vs baseline)。
463
+ function renderCompositeCell(composite, i) {
464
+ const has = typeof composite === 'number' && composite > 0;
465
+ const color = has ? (composite >= 4 ? 'var(--green)' : composite >= 3 ? 'var(--yellow)' : 'var(--red)') : 'var(--text-muted)';
466
+ let delta = '';
467
+ if (isMulti && i > 0 && has && typeof baselineComposite === 'number' && baselineComposite > 0) {
468
+ const d = composite - baselineComposite;
469
+ if (Math.abs(d) >= 0.01) {
470
+ delta = `<div class="sm-delta" style="color:${d > 0 ? 'var(--green)' : 'var(--red)'}">${d > 0 ? '+' : ''}${d.toFixed(2)} ${d > 0 ? '↑' : '↓'}</div>`;
471
+ }
472
+ }
473
+ return `<td class="summary-cell sm-composite-cell"><div class="summary-value summary-value-primary" style="color:${color}">${has ? composite.toFixed(2) : '—'}</div>${delta}</td>`;
402
474
  }
403
- const rows = variants.map((v, i) => {
475
+ const avgLabel = lang === 'zh' ? '次' : 'req';
476
+ const costUnreportedTooltip = lang === 'zh'
477
+ ? 'executor 不报 USD 成本(如 codex CLI),无法估算'
478
+ : 'executor does not report USD cost (e.g. codex CLI); not measurable';
479
+ let anyStabUnmeasured = false;
480
+ const perVariant = variants.map((v, i) => {
404
481
  const s = summary[v] || {};
405
482
  const vd = variance?.perVariant[v];
406
483
  const color = COLORS[i % COLORS.length];
407
- // 事实层 cell(含事实验证率 detail,如果有)
484
+ const factVal = vd?.byLayer?.fact?.mean ?? s.avgFactScore;
485
+ const behaviorVal = vd?.byLayer?.behavior?.mean ?? s.avgBehaviorScore;
486
+ const judgeVal = vd?.byLayer?.judge?.mean ?? s.avgJudgeScore;
408
487
  const factDetailParts = [];
409
- if (s.avgFactVerifiedRate != null) {
410
- const pct = Math.round(s.avgFactVerifiedRate * 100);
411
- factDetailParts.push(`${lang === 'zh' ? '验证率' : 'Verified'} ${pct}%`);
412
- }
413
- const factDetail = factDetailParts.length > 0 ? `<div class="summary-detail">${factDetailParts.join(' · ')}</div>` : '';
414
- const factCell = renderLayerCell(vd?.byLayer?.fact?.mean, s.avgFactScore, factDetail);
415
- // 行为层 cell(暂无 detail,后续可按工具调用成功率之类扩展)
416
- const behaviorCell = renderLayerCell(vd?.byLayer?.behavior?.mean, s.avgBehaviorScore);
417
- // LLM 评价层 cell
418
- const judgeCell = renderLayerCell(vd?.byLayer?.judge?.mean, s.avgJudgeScore);
419
- // Cost — only show execution cost (judge cost is tool overhead, not skill cost)
420
- // execCostReported === false 时(如 codex executor)显示「—」并 tooltip 解释,
421
- // 跟"真的花了 $0(全 cached / 短 prompt)"区分开。
488
+ if (s.avgFactVerifiedRate != null)
489
+ factDetailParts.push(`${lang === 'zh' ? '验证率' : 'Verified'} ${Math.round(s.avgFactVerifiedRate * 100)}%`);
490
+ // Cost — 只算 execution cost(judge cost 是工具开销,不算 skill 成本)。
422
491
  const execCost = s.totalExecCostUSD || 0;
423
492
  const costReported = s.execCostReported !== false;
424
493
  const hasCost = execCost > 0 || (s.avgTotalTokens || 0) > 0;
425
- const costUnreportedTooltip = lang === 'zh'
426
- ? 'executor 不报 USD 成本(如 codex CLI),无法估算'
427
- : 'executor does not report USD cost (e.g. codex CLI); not measurable';
428
- const costCell = hasCost
429
- ? (costReported
430
- ? `<td class="summary-cell"><div class="summary-value">${fmtCost(execCost)}</div><div class="summary-detail">${fmtNum(s.avgTotalTokens)} tokens/${t('tokPerReq', lang).replace('tokens/', '')}</div></td>`
431
- : `<td class="summary-cell" title="${e(costUnreportedTooltip)}"><div class="summary-value" style="color:var(--text-muted)">${fmtCost(0, false)}</div><div class="summary-detail">${fmtNum(s.avgTotalTokens)} tokens/${t('tokPerReq', lang).replace('tokens/', '')}</div></td>`)
432
- : `<td class="summary-cell"><span style="color:var(--text-muted)">N/A</span></td>`;
433
- // Efficiency
434
- const effDetails = [];
494
+ const costPerSample = (s.totalSamples || 0) > 0 ? execCost / s.totalSamples : execCost;
495
+ const tokenDetail = `${fmtNum(s.avgTotalTokens)} tokens/${t('tokPerReq', lang).replace('tokens/', '')}`;
496
+ const costDim = hasCost
497
+ ? { label: dimText('dimCost', lang), value: costReported ? fmtCost(execCost) : fmtCost(0, false), valueColor: costReported ? 'var(--text-primary)' : 'var(--text-muted)', primary: false, bg: '', detailLines: [tokenDetail], title: costReported ? undefined : costUnreportedTooltip, icon: 'cost' }
498
+ : { label: dimText('dimCost', lang), value: 'N/A', valueColor: 'var(--text-muted)', primary: false, bg: '', detailLines: [], icon: 'cost' };
499
+ // Efficiency — 主值是每次耗时;轮次/工具压成一条紧凑副行(去掉「总计」那条,留白更清爽)。
500
+ const effParts = [];
435
501
  const displayTurns = s.avgFullNumTurns ?? s.avgNumTurns;
436
502
  if ((displayTurns || 0) > 0)
437
- effDetails.push(`${displayTurns} ${t('turnsPerReq', lang)}`);
503
+ effParts.push(`${displayTurns} ${t('turnsPerReq', lang)}`);
438
504
  if (s.avgToolCalls != null && s.avgToolCalls > 0) {
439
- const srPct = s.toolSuccessRate != null ? ` (${(s.toolSuccessRate * 100).toFixed(0)}% OK)` : '';
440
- effDetails.push(`${s.avgToolCalls} tools/req${srPct}`);
505
+ const srPct = s.toolSuccessRate != null ? ` ${(s.toolSuccessRate * 100).toFixed(0)}%` : '';
506
+ effParts.push(`${s.avgToolCalls} ${lang === 'zh' ? '工具' : 'tools'}${srPct}`);
441
507
  }
442
- const totalDurationMs = (s.avgDurationMs || 0) * (s.successCount || 0);
443
- if (totalDurationMs > 0)
444
- effDetails.push(`${lang === 'zh' ? '总计' : 'total'} ${fmtDuration(totalDurationMs)}`);
445
- const effDetail = effDetails.length > 0 ? `<div class="summary-detail">${effDetails.join(' · ')}</div>` : '';
446
- const avgLabel = lang === 'zh' ? '次' : 'req';
447
- const effCell = `<td class="summary-cell"><div class="summary-value">${fmtDuration(s.avgDurationMs)}<span class="summary-unit">/${avgLabel}</span></div>${effDetail}</td>`;
448
- // Stability — 多次运行分数一致性(test-retest reliability),统计学定义的稳定性。
449
- // 主视觉:白话定性词 + ±σ 直观量级,让读者一眼判断。
450
- // 副区:CV (变异系数 = σ / mean) 分两行展示 + 95% CI(置信区间)。
451
- // 无 --repeat(variance 缺失) 时主值显示 "—" + 明示需多跑,不虚报 100%——
452
- // 符合 omk 叙事底线"诚实交代测不到什么"。
453
- //
454
- // 跨用例 min~max range 不是稳定性(反映用例难度差异,非 variant 波动),已从此列移除。
455
- // 成功率 ≠ 稳定性(执行完成率,不是分数一致性),降级到 < 100% 时的副区 alert。
508
+ const durSec = (s.avgDurationMs || 0) / 1000;
509
+ // 副行拆成多条堆叠(表格窄列里逐条独占一行,避免「轮/工具」挤在一行从中间难看换行);
510
+ // solo 的 dimMetric 仍用 ' · ' 合并成一行(够宽)。
511
+ const effDim = { label: dimText('dimEfficiency', lang), value: `${fmtDuration(s.avgDurationMs)}<span class="summary-unit">/${avgLabel}</span>`, valueColor: 'var(--text-primary)', primary: false, bg: '', detailLines: effParts, icon: 'efficiency' };
512
+ // Stability — 多次运行分数一致性(test-retest reliability)。无 --repeat 时显「未测量」不虚报。
456
513
  const total = s.totalSamples || 0;
457
- const successCount = s.successCount || 0;
458
514
  const errorCount = s.errorCount || 0;
459
- const successRate = total > 0 ? Number((successCount / total * 100).toFixed(1)) : 0;
515
+ const successRate = total > 0 ? Number((s.successCount || 0) / total * 100) : 0;
516
+ const cv = (vd && typeof vd.stddev === 'number' && typeof vd.mean === 'number' && vd.mean >= 0.5)
517
+ ? Math.abs(vd.stddev / vd.mean) : null;
460
518
  const stabDetails = [];
461
519
  let stabValue;
462
520
  let stabColor;
463
- // CV = σ / mean,当 mean 过小(接近 0)时 CV 发散,数值无参考价值——1-5 分数量纲下
464
- // mean < 0.5 已属全灭场景,直接降级显示"—"。负 mean(理论上不会出现)也走降级。
465
- if (vd && typeof vd.stddev === 'number' && typeof vd.mean === 'number' && vd.mean >= 0.5) {
466
- const cv = Math.abs(vd.stddev / vd.mean);
521
+ if (cv != null) {
467
522
  const cvPct = cv * 100;
468
- const sigma = vd.stddev;
469
- // 主值用白话定性 + ±σ 直观量级,让非统计背景读者一眼判断。
470
- // CV 和 CI 下沉到副区,给懂的人细看。阈值面向 1-5 分数量纲的经验值。
471
523
  let label;
472
524
  if (cvPct < 5) {
473
525
  label = lang === 'zh' ? '稳定' : 'Stable';
@@ -481,72 +533,84 @@ export function renderSummaryCards(variants, summary, lang, variance) {
481
533
  label = lang === 'zh' ? '波动大' : 'Variable';
482
534
  stabColor = 'var(--red)';
483
535
  }
484
- stabValue = `${label} · ±${fmtNum(sigma, 2)}`;
485
- const ciLo = fmtNum(vd.lower, 2);
486
- const ciHi = fmtNum(vd.upper, 2);
487
- stabDetails.push(`CV ${cvPct.toFixed(1)}% · 95% CI [${ciLo}, ${ciHi}]`);
536
+ stabValue = `${label} · ±${fmtNum(vd.stddev, 2)}`;
537
+ stabDetails.push(`CV ${cvPct.toFixed(1)}% · 95% CI [${fmtNum(vd.lower, 2)}, ${fmtNum(vd.upper, 2)}]`);
488
538
  }
489
539
  else {
490
- // No cross-run data → make the gap loud, not silent. 之前用灰色 "—" 让单轮
491
- // 报告读者误以为"无显示 = 没问题",实际是 omk 测不到这个维度。改红 +
492
- // "未测量" 字样让缺失可见 — 鼓励用户加 --repeat ≥ 2 而不是默默 ship。
493
- stabValue = lang === 'zh' ? '⚠ 未测量' : '⚠ Not measured';
494
- stabColor = 'var(--red)';
495
- stabDetails.push(`<span style="color:var(--red)">${lang === 'zh' ? '单轮评测,加 --repeat ≥ 2 才能测 CV' : 'single-run; needs --repeat ≥ 2 to measure CV'}</span>`);
540
+ // 未测量:仅一个安静的词,不在每行重复整句红色告警(那是右侧最大的视觉噪声)。
541
+ // 「需 --repeat ≥ 2」的提示压成表格下方一条脚注,只出现一次。
542
+ anyStabUnmeasured = true;
543
+ stabValue = lang === 'zh' ? '未测量' : 'n/a';
544
+ stabColor = 'var(--text-muted)';
496
545
  }
497
- // Execution-completion alerts:success rate < 100% 时降级到此处,避免和"稳定性"语义混淆。
498
546
  if (errorCount > 0) {
499
- stabDetails.unshift(`<span style="color:var(--red)">${successRate}% ${lang === 'zh' ? '完成率' : 'completed'} · ${errorCount} ${t('errors', lang)}</span>`);
547
+ stabDetails.unshift(`<span style="color:var(--red)">${successRate.toFixed(1)}% ${lang === 'zh' ? '完成率' : 'completed'} · ${errorCount} ${t('errors', lang)}</span>`);
500
548
  }
501
- // 正常情况副区只一行:`CV X.X% · 95% CI [...]`。
502
- // 有 alert(成功率 < 100%) 时把 alert 放第一行、CV+CI 放第二行,让异常信息第一眼看到。
503
- const stabDetail = stabDetails.length > 0
504
- ? stabDetails.map((d) => `<div class="summary-detail">${d}</div>`).join('')
505
- : '';
506
- const stabCell = `<td class="summary-cell"><div class="summary-value" style="color:${stabColor}">${stabValue}</div>${stabDetail}</td>`;
507
- return `<tr><td style="border-left:3px solid ${color};padding-left:12px"><strong>${e(v)}</strong></td>${factCell}${behaviorCell}${judgeCell}${costCell}${effCell}${stabCell}</tr>`;
508
- }).join('');
509
- // ──────────── Hero 行:每 variant 一行,综合分大字号 + delta vs baseline ────────────
510
- // 每变体一行,左 variant 名 + 颜色条;中 大字号 composite;右 delta(vs variants[0])
511
- const baselineComposite = summary[variants[0]]?.avgCompositeScore;
512
- const heroRows = variants.map((v, i) => {
513
- const s = summary[v] || {};
514
- const composite = s.avgCompositeScore;
515
- const compositeHasValue = typeof composite === 'number' && composite > 0;
516
- const compositeColor = compositeHasValue
517
- ? (composite >= 4 ? 'var(--green)' : composite >= 3 ? 'var(--yellow)' : 'var(--red)')
518
- : 'var(--text-muted)';
519
- const compositeDisplay = compositeHasValue ? composite.toFixed(2) : '—';
520
- const color = COLORS[i % COLORS.length];
521
- let deltaHtml = '';
522
- if (i > 0 && compositeHasValue && typeof baselineComposite === 'number' && baselineComposite > 0) {
523
- const diff = composite - baselineComposite;
524
- if (Math.abs(diff) >= 0.01) {
525
- const sign = diff > 0 ? '+' : '';
526
- const dColor = diff > 0 ? 'var(--green)' : 'var(--red)';
527
- const arrow = diff > 0 ? '↑' : '↓';
528
- deltaHtml = `<span class="sm-hero-delta" style="color:${dColor}">${sign}${diff.toFixed(2)} ${arrow}</span>`;
529
- }
530
- }
531
- return `<div class="sm-hero" style="border-left:3px solid ${color}">
532
- <div class="sm-hero-name">${e(v)}</div>
533
- <div class="sm-hero-score" style="color:${compositeColor}">${compositeDisplay}<span class="sm-hero-unit">/ 5</span></div>
534
- ${deltaHtml || '<span class="sm-hero-delta-placeholder"></span>'}
535
- </div>`;
536
- }).join('');
537
- const heroBlock = `<div class="sm-hero-list">
538
- <div class="sm-hero-h">
539
- ${lang === 'zh' ? '综合分' : 'Composite'}
540
- <button type="button" class="hint-btn" onclick="openModal('${scoringModalId}')" aria-label="${e(lang === 'zh' ? '综合分怎么算的?' : 'How is composite computed?')}" aria-haspopup="dialog">?</button>
541
- </div>
542
- ${heroRows}
543
- </div>`;
549
+ const stabDim = { label: dimText('dimStability', lang), value: stabValue, valueColor: stabColor, primary: false, bg: '', detailLines: stabDetails, icon: 'stability' };
550
+ const dims = [
551
+ layerDim(dimText('dimFact', lang), vd?.byLayer?.fact?.mean, s.avgFactScore, factDetailParts, 'fact'),
552
+ layerDim(dimText('dimBehavior', lang), vd?.byLayer?.behavior?.mean, s.avgBehaviorScore, [], 'behavior'),
553
+ layerDim(dimText('dimJudge', lang), vd?.byLayer?.judge?.mean, s.avgJudgeScore, [], 'judge'),
554
+ costDim,
555
+ effDim,
556
+ stabDim,
557
+ ];
558
+ return {
559
+ name: v,
560
+ color,
561
+ composite: s.avgCompositeScore,
562
+ dims,
563
+ g: {
564
+ fact: typeof factVal === 'number' && factVal > 0 ? clamp01(factVal / 5) : null,
565
+ behavior: typeof behaviorVal === 'number' && behaviorVal > 0 ? clamp01(behaviorVal / 5) : null,
566
+ judge: typeof judgeVal === 'number' && judgeVal > 0 ? clamp01(judgeVal / 5) : null,
567
+ // 成本/效率:绝对软参考,越省越外。$1/用例、120s/次 记为 0。
568
+ cost: hasCost && costReported ? clamp01(1 - costPerSample / 1.0) : null,
569
+ efficiency: durSec > 0 ? clamp01(1 - durSec / 120) : null,
570
+ stability: cv != null ? clamp01(1 - cv / 0.30) : null,
571
+ },
572
+ };
573
+ });
574
+ // 表格行(多 variant):行首颜色点对应雷达多边形,行即图例。
575
+ const rows = perVariant.map((pv, i) => `<tr><td class="sm-name-cell"><span class="sm-row-dot" style="background:${pv.color}"></span><strong>${e(pv.name)}</strong></td>${renderCompositeCell(pv.composite, i)}${pv.dims.map(dimCell).join('')}</tr>`).join('');
576
+ const stabFootnote = anyStabUnmeasured
577
+ ? `<p class="sm-foot">${svgIcon('stability', { size: 13, cls: 'sm-foot-ico' })}${lang === 'zh' ? '稳定性需 <code>--repeat ≥ 2</code> 才能测(本次为单轮评测)' : 'Stability needs <code>--repeat ≥ 2</code> (single-run here)'}</p>`
578
+ : '';
579
+ // 指标行列表(单 variant):综合分 + 六维竖排,免去单行表格的空旷/表头冗余。
580
+ const compositeMetric = (() => {
581
+ const c = perVariant[0]?.composite;
582
+ const has = typeof c === 'number' && c > 0;
583
+ const color = has ? (c >= 4 ? 'var(--green)' : c >= 3 ? 'var(--yellow)' : 'var(--red)') : 'var(--text-muted)';
584
+ return `<div class="sm-metric sm-metric--composite"><span class="sm-metric-label">${lang === 'zh' ? '综合' : 'Composite'} ${compositeHint}</span><span class="sm-metric-val" style="color:${color}">${has ? c.toFixed(2) : '—'}<small class="sm-metric-unit"> / 5</small></span></div>`;
585
+ })();
586
+ // 六维分两列(列优先填充:左列 = 质量三层 事实/行为/评委,右列 = 资源三项 成本/效率/稳定),
587
+ // 综合分整行领头。单 variant 时撑满横向空间,不再细长一条。
588
+ const metricList = `<div class="sm-metrics">${compositeMetric}<div class="sm-metric-grid">${(perVariant[0]?.dims ?? []).map(dimMetric).join('')}</div></div>`;
589
+ const radarDims = [
590
+ { key: 'fact', label: lang === 'zh' ? '事实' : 'Fact' },
591
+ { key: 'behavior', label: lang === 'zh' ? '行为' : 'Behavior' },
592
+ { key: 'judge', label: lang === 'zh' ? '评委' : 'Judge' },
593
+ { key: 'cost', label: lang === 'zh' ? '成本' : 'Cost' },
594
+ { key: 'efficiency', label: lang === 'zh' ? '效率' : 'Eff' },
595
+ { key: 'stability', label: lang === 'zh' ? '稳定' : 'Stab' },
596
+ ];
597
+ const radar = renderRadarChart(perVariant.map((pv) => ({ name: pv.name, color: pv.color, g: pv.g })), radarDims, lang);
544
598
  const guideModalId = 'guide-six-dims';
545
- const guideTitle = lang === 'zh' ? '如何阅读六维对比?' : 'How to read this six-dimension comparison?';
599
+ // 单 variant 没有对照,标题用「六维评分」;多 variant 才是「六维对比」。
600
+ const sectionTitle = lang === 'zh' ? (isMulti ? '六维对比' : '六维评分') : (isMulti ? 'Six-Dimension Comparison' : 'Six-Dimension Scores');
601
+ const guideTitle = lang === 'zh' ? `如何阅读${sectionTitle}?` : `How to read these six dimensions?`;
546
602
  const guideIntro = lang === 'zh'
547
- ? '每行是一个实验分组(Variant),六列分别衡量不同维度:'
548
- : 'Each row is an experiment variant. Six columns measure independent dimensions:';
549
- const icon = (emoji) => `<span aria-hidden="true">${emoji}</span>`;
603
+ ? '雷达图把六维归一化(越外越好)看强弱形状,下方表格给精确数值。各维含义:'
604
+ : 'The radar normalizes the six dimensions (outer = better) to show shape; the table below gives exact values. The dimensions:';
605
+ const radarNote = lang === 'zh'
606
+ ? '雷达归一化口径:事实/行为/评委 = 分÷5;成本、效率按绝对参考折算(约 $1/用例、120s/次 记为最低,越省越靠外);稳定性 = 1 − CV/0.3。空心点 = 未测量或单组无对照。精确数值一律以下方表格为准。'
607
+ : 'Radar normalization: Fact/Behavior/Judge = score÷5; Cost/Efficiency use absolute references (~$1/sample, 120s/req map to 0 — cheaper/faster reaches outward); Stability = 1 − CV/0.3. Hollow dots = not measured or single-variant (no comparison). The table below is the source of truth for exact values.';
608
+ // 六维说明弹框的行首图标:把历史 emoji 映射到统一 SVG 图标(未命中则保留原 emoji 兜底)。
609
+ const EMOJI_ICON = { '📋': 'fact', '🛠️': 'behavior', '💬': 'judge', '💰': 'cost', '⚡': 'efficiency', '🛡️': 'stability' };
610
+ const icon = (emoji) => {
611
+ const name = EMOJI_ICON[emoji];
612
+ return name ? svgIcon(name, { size: 14, style: 'color:var(--text-secondary);vertical-align:-2px;margin-right:4px' }) : `<span aria-hidden="true">${emoji}</span>`;
613
+ };
550
614
  // 维度分隔加粗(border-top 2px),让六维的视觉边界更明显。sub 缩进从 28 收到 22。
551
615
  const dim = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-primary);font-weight:600"';
552
616
  const dimDesc = 'style="padding:12px 0 4px;border-top:2px solid var(--border);color:var(--text-secondary)"';
@@ -579,7 +643,7 @@ export function renderSummaryCards(variants, summary, lang, variance) {
579
643
  `;
580
644
  const scoringModalHtml = renderScoringModal(scoringModalId, lang);
581
645
  return `
582
- <h2 style="display:flex;align-items:center;gap:4px">${lang === 'zh' ? '六维对比' : 'Six-Dimension Comparison'} <button type="button" class="hint-btn" onclick="openModal('${guideModalId}')" aria-label="${e(guideTitle)}" aria-haspopup="dialog">?</button></h2>
646
+ <h2 style="display:flex;align-items:center;gap:4px">${sectionTitle} <button type="button" class="hint-btn" onclick="openModal('${guideModalId}')" aria-label="${e(guideTitle)}" aria-haspopup="dialog">?</button></h2>
583
647
  <div id="${guideModalId}" class="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="${guideModalId}-title" onclick="if(event.target===this)closeModal('${guideModalId}')">
584
648
  <div class="modal-content">
585
649
  <div class="modal-header">
@@ -588,30 +652,76 @@ export function renderSummaryCards(variants, summary, lang, variance) {
588
652
  </div>
589
653
  <p style="font-size:13px;color:var(--text-secondary);margin:4px 0 16px">${e(guideIntro)}</p>
590
654
  <table class="modal-table"><tbody>${guideRows}</tbody></table>
655
+ <p style="font-size:12px;color:var(--text-muted);margin:14px 0 0;line-height:1.6;border-top:1px solid var(--border);padding-top:12px">${e(radarNote)}</p>
591
656
  </div>
592
657
  </div>
593
658
  ${scoringModalHtml}
594
- ${heroBlock}
595
- <div class="table-wrap">
596
- <table class="summary-table">
597
- <thead>${thead}</thead>
598
- <tbody>${rows}</tbody>
599
- </table>
659
+ <div class="sm-vis${isMulti ? '' : ' sm-vis--solo'}">
660
+ ${radar}
661
+ ${isMulti
662
+ ? `<div class="table-wrap"><table class="summary-table"><thead>${thead}</thead><tbody>${rows}</tbody></table></div>`
663
+ : metricList}
600
664
  </div>
665
+ ${stabFootnote}
601
666
  <style>
602
- /* 六维对比 — Hero 区(每 variant 综合分 + delta) */
603
- .sm-hero-list { display:flex;flex-direction:column;gap:8px;margin:14px 0 18px }
604
- .sm-hero-h { font-size:13px;color:var(--text-muted);margin-bottom:4px;letter-spacing:0.02em }
605
- .sm-hero { display:flex;align-items:baseline;gap:16px;padding:14px 18px;background:var(--bg-surface);border-radius:var(--radius);box-shadow:var(--shadow-sm) }
606
- .sm-hero-name { flex:1;font-size:14.5px;font-weight:500;color:var(--text-primary) }
607
- .sm-hero-score { font-size:28px;font-weight:600;font-variant-numeric:tabular-nums;line-height:1 }
608
- .sm-hero-unit { font-size:13px;color:var(--text-muted);margin-left:4px;font-weight:400 }
609
- .sm-hero-delta { font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;font-family:"SF Mono",Menlo,monospace;min-width:60px;text-align:right }
610
- .sm-hero-delta-placeholder { display:inline-block;min-width:60px }
611
- /* 六维 heatmap — 每格按分数浅色填充,便于远看识别弱维度 */
612
- .sm-heat-pass { background:linear-gradient(0deg,var(--green-bg),var(--green-bg)) }
613
- .sm-heat-warn { background:linear-gradient(0deg,var(--yellow-bg),var(--yellow-bg)) }
614
- .sm-heat-fail { background:linear-gradient(0deg,var(--red-bg),var(--red-bg)) }
667
+ /* 对比表减负:行距更松、副行更淡更小、名字列左对齐去掉左色条(行首色点已是图例) */
668
+ .summary-table td { padding:13px 14px }
669
+ .summary-table .sm-name-cell { text-align:left;white-space:nowrap;font-size:14px;max-width:200px;overflow:hidden;text-overflow:ellipsis }
670
+ .summary-table .summary-detail { font-size:11px;color:var(--text-faint);margin-top:4px;line-height:1.4;white-space:nowrap }
671
+ .summary-table .summary-detail + .summary-detail { margin-top:2px }
672
+ .summary-table .summary-value { font-weight:600 }
673
+ .sm-foot { font-size:11.5px;color:var(--text-muted);margin:10px 2px 0;line-height:1.5 }
674
+ .sm-foot code { font-size:11px }
675
+ /* 六维区 — 雷达图 + 数值并排一行(宽屏);窄屏自动换行堆叠。单 variant 用指标列表,多 variant 用对比表。 */
676
+ .sm-vis { display:flex;gap:28px;align-items:center;margin:16px 0 18px }
677
+ .sm-vis .sm-radar { flex:0 0 auto;margin:0;padding:0 }
678
+ .sm-vis .table-wrap { flex:1 1 auto;min-width:0;margin:0 }
679
+ .sm-vis .sm-metrics { flex:0 1 560px;min-width:0 }
680
+ /* 单 variant:雷达放大些(只有一个多边形,大点更好看)+ 居中,避免右侧大片留白。 */
681
+ .sm-vis--solo { justify-content:center;gap:44px }
682
+ .sm-vis--solo .rad-svg { width:360px;height:300px }
683
+ .sm-vis--solo .sm-metrics { flex:0 1 680px }
684
+ @media (max-width:1080px) { .sm-vis { flex-direction:column;align-items:stretch } .sm-vis .sm-metrics { flex:1 1 auto } .sm-vis--solo { justify-content:flex-start } .sm-vis--solo .rad-svg { width:300px;height:250px } }
685
+ /* 指标列表(单 variant)— 综合分整行领头 + 六维分两列(左质量三层 / 右资源三项) */
686
+ .sm-metrics { display:flex;flex-direction:column }
687
+ .sm-metric { display:flex;align-items:baseline;gap:12px;padding:9px 4px }
688
+ .sm-metric--composite { padding:4px 4px 12px;margin-bottom:6px;border-bottom:2px solid var(--border) }
689
+ .sm-metric-label { display:inline-flex;align-items:center;gap:6px;flex:0 0 auto;min-width:92px;color:var(--text-secondary);font-size:13.5px }
690
+ .sm-mico { color:var(--text-muted);flex-shrink:0 }
691
+ .sm-th-ico { color:var(--text-muted);vertical-align:-2px;margin-right:5px }
692
+ .sm-foot-ico { color:var(--text-muted);vertical-align:-2px;margin-right:5px }
693
+ .sm-metric--composite .sm-metric-label { color:var(--text-primary);font-weight:600;font-size:14px }
694
+ .sm-metric-val { flex:0 0 auto;min-width:60px;font-weight:700;font-variant-numeric:tabular-nums;font-family:"SF Mono",Menlo,monospace;font-size:15px }
695
+ .sm-metric--composite .sm-metric-val { font-size:30px;line-height:1 }
696
+ .sm-metric-unit { font-size:13px;color:var(--text-muted);font-weight:400 }
697
+ .sm-metric-detail { flex:1 1 auto;min-width:0;color:var(--text-muted);font-size:12px;line-height:1.5 }
698
+ /* 六维两列网格:列优先填充(左列前 3 = 质量,右列后 3 = 资源),列间一条竖分隔 */
699
+ .sm-metric-grid { display:grid;grid-template-columns:1fr 1fr;grid-template-rows:repeat(3,auto);grid-auto-flow:column }
700
+ .sm-metric-grid .sm-metric { border-bottom:1px solid var(--border) }
701
+ .sm-metric-grid .sm-metric:nth-child(3), .sm-metric-grid .sm-metric:nth-child(6) { border-bottom:none }
702
+ .sm-metric-grid .sm-metric:nth-child(n+4) { padding-left:24px;border-left:1px solid var(--border) }
703
+ @media (max-width:600px) { .sm-metric-grid { grid-template-columns:1fr;grid-auto-flow:row } .sm-metric-grid .sm-metric:nth-child(n+4) { padding-left:4px;border-left:none } .sm-metric-grid .sm-metric:nth-child(3) { border-bottom:1px solid var(--border) } .sm-metric-grid .sm-metric:last-child { border-bottom:none } }
704
+ /* 六维雷达图 — 归一化「越外越好」多边形 */
705
+ .sm-radar { padding:4px }
706
+ .rad-svg { width:300px;height:250px;overflow:visible }
707
+ .rad-grid { fill:none;stroke:var(--border);stroke-width:1 }
708
+ .rad-spoke { stroke:var(--border);stroke-width:1 }
709
+ .rad-label { font-size:11px;fill:var(--text-secondary);font-weight:500 }
710
+ .rad-area { fill-opacity:0.13;stroke-width:2;stroke-linejoin:round }
711
+ .rad-dot { stroke:#fff;stroke-width:1 }
712
+ .rad-dot--na { fill:var(--bg-surface);stroke:var(--text-faint);stroke-width:1.5 }
713
+ .rad-series { transition:opacity .12s }
714
+ .rad-legend { display:flex;flex-wrap:wrap;gap:6px 8px;justify-content:center;margin-top:10px }
715
+ .rad-leg { display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);background:none;border:1px solid var(--border);border-radius:999px;padding:3px 11px;cursor:pointer;transition:.15s }
716
+ .rad-leg-dot { width:9px;height:9px;border-radius:50%;background:var(--c);flex:none }
717
+ .rad-leg:hover { border-color:var(--c);color:var(--text-primary) }
718
+ .rad-leg.off { opacity:.45;text-decoration:line-through }
719
+ .rad-leg.off .rad-leg-dot { background:var(--text-faint) }
720
+ /* 数值表 — 综合分首列大字号;行首颜色点对应雷达多边形;hover 不盖热力底色 */
721
+ .sm-row-dot { display:inline-block;width:9px;height:9px;border-radius:3px;margin-right:7px;vertical-align:middle }
722
+ .sm-composite-cell .summary-value-primary { font-size:1.35rem }
723
+ .sm-delta { font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;margin-top:2px }
724
+ .summary-table tr:hover td { background:transparent }
615
725
  </style>`;
616
726
  }
617
727
  /**