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
@@ -12,9 +12,6 @@
12
12
  * 隐藏 modal:每条 insight 一个 + 3 个阶段全量明细各一个
13
13
  */
14
14
  import { layout, e, DEFAULT_LANG } from './layout.js';
15
- const BAND_DOT = {
16
- green: '🟢', yellow: '🟡', red: '🔴', gray: '⚪',
17
- };
18
15
  // v6 之前的 insight × audience × 三视角 抽象在 v7 详情页砍掉了,只保留 sample 维度
19
16
  // (renderEvalSection 直接读 evalReport.results,不再走 detectInsights / audience 分组)。
20
17
  // 相关常量(SEVERITY_ICON / AUDIENCE_INFO_* / PATCH_TARGET_*)等以后做调试 UI
@@ -37,17 +34,6 @@ function relTime(ts, lang) {
37
34
  return '';
38
35
  }
39
36
  }
40
- function fmtDateShort(ts) {
41
- if (!ts)
42
- return '';
43
- try {
44
- const d = new Date(ts);
45
- return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`;
46
- }
47
- catch {
48
- return '';
49
- }
50
- }
51
37
  /** Underpowered observe (very few segments) must not paint a hard red/yellow band — the
52
38
  * analyzer's confidence guard means the band is indicative only, so Studio treats it as
53
39
  * a neutral (gray) signal with a caveat rather than a hard health verdict. */
@@ -123,122 +109,51 @@ export function assessHealth(entry, insights, lang) {
123
109
  return { grade: 'good', score, label: lang === 'zh' ? '良好' : 'Good', emoji: '🟢', color: 'green' };
124
110
  }
125
111
  // ────────── Hero:健康等级 + 名称 + 摘要 ──────────
126
- function renderHero(entry, insights, lastTs, reportCount, lang) {
112
+ function scoreGrade(score) {
113
+ if (score >= 80)
114
+ return 'A';
115
+ if (score >= 70)
116
+ return 'B';
117
+ if (score >= 60)
118
+ return 'C';
119
+ return 'D';
120
+ }
121
+ function scoreColor2(score) {
122
+ return score >= 100 ? '#1f9d63' : score >= 60 ? '#d97706' : '#dc2626';
123
+ }
124
+ function renderHero(entry, insights, lastTs, _reportCount, lang) {
127
125
  const h = assessHealth(entry, insights, lang);
128
- const scoreTip = lang === 'zh'
129
- ? '参考分:doctor 通过率、eval 综合分、observe 稳定度归一到 0-100 后取平均(warn/告警按 0.5 折算)。同一 skill 跨时间可比;不同 skill 之间因 sample 难度不同不严格可比'
130
- : 'Reference score: average of doctor pass-rate, eval composite, and observe stability normalized to 0-100. Comparable within a skill over time; not strictly comparable across skills (sample difficulty differs)';
131
- const scoreBlock = h.score != null
132
- ? `<span class="si-hero-score" title="${e(scoreTip)}"><span class="si-hero-score-num">${h.score}</span><span class="si-hero-score-unit">/100</span></span>`
133
- : `<span class="si-hero-score si-hero-score--empty" title="${e(scoreTip)}">—</span>`;
134
- return `<div class="si-hero">
135
- <div class="si-hero-grade si-hero-grade--${h.color}">
136
- <span class="si-hero-grade-emoji">${h.emoji}</span>
137
- <span class="si-hero-grade-label">${e(h.label)}</span>
138
- ${scoreBlock}
126
+ const zh = lang === 'zh';
127
+ const color = h.score != null ? scoreColor2(h.score) : '#9ca3af';
128
+ const grade = h.score != null ? scoreGrade(h.score) : '—';
129
+ const gradeLabel = h.score != null
130
+ ? (h.score >= 70 ? (zh ? '通过' : 'Pass') : h.score >= 50 ? (zh ? '待改进' : 'Fair') : (zh ? '不合格' : 'Fail'))
131
+ : '';
132
+ const size = 96, sw = 7, r = (size - sw) / 2, c = 2 * Math.PI * r;
133
+ const offset = c * (1 - (h.score ?? 0) / 100);
134
+ return `<div class="sd-hero">
135
+ <div class="sd-hero-left">
136
+ <h1 class="sd-hero-name">${e(entry.skillName)}</h1>
137
+ <div class="sd-hero-meta">${zh ? '最后更新' : 'Updated'} ${relTime(lastTs, lang)}</div>
139
138
  </div>
140
- <div class="si-hero-info">
141
- <div class="si-hero-name">${e(entry.skillName)}</div>
142
- <div class="si-hero-summary">${renderHealthSummary(entry, insights, lang)}</div>
139
+ <div class="sd-hero-score">
140
+ <div style="position:relative;width:${size}px;height:${size}px">
141
+ <svg width="${size}" height="${size}">
142
+ <circle cx="${size / 2}" cy="${size / 2}" r="${r}" fill="none" stroke="#edf0f7" stroke-width="${sw}"/>
143
+ ${h.score != null ? `<circle cx="${size / 2}" cy="${size / 2}" r="${r}" fill="none" stroke="${color}" stroke-width="${sw}"
144
+ stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${offset.toFixed(2)}"
145
+ stroke-linecap="round" transform="rotate(-90 ${size / 2} ${size / 2})" style="transition:stroke-dashoffset .6s"/>` : ''}
146
+ </svg>
147
+ <div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center">
148
+ <span style="font-size:28px;font-weight:800;line-height:1;font-variant-numeric:tabular-nums;color:${color}">${h.score ?? '—'}</span>
149
+ </div>
150
+ </div>
151
+ <div class="sd-hero-grade">
152
+ <div class="sd-hero-grade-label">${zh ? '综合健康分' : 'Health Score'}</div>
153
+ <div class="sd-hero-grade-value" style="color:${color}"><strong>${grade}</strong> <span>${gradeLabel}</span></div>
154
+ <div class="sd-hero-grade-hint">${zh ? '综合 = (健康体检 + 用例评测) / 2' : 'Score = (Doctor + Eval) / 2'}</div>
155
+ </div>
143
156
  </div>
144
- <div class="si-hero-meta">${reportCount}/3 · ${relTime(lastTs, lang)}</div>
145
- </div>`;
146
- }
147
- function renderHealthSummary(entry, insights, lang) {
148
- const issues = [];
149
- if (entry.doctor) {
150
- if (entry.doctor.failCount > 0)
151
- issues.push(lang === 'zh' ? `doctor ${entry.doctor.failCount} 项不通过` : `doctor ${entry.doctor.failCount} fail`);
152
- else if (entry.doctor.warnCount > 0)
153
- issues.push(lang === 'zh' ? `doctor ${entry.doctor.warnCount} 项告警` : `doctor ${entry.doctor.warnCount} warn`);
154
- }
155
- if (entry.eval) {
156
- const total = entry.eval.passCount + entry.eval.failCount;
157
- if (total > 0) {
158
- const pct = Math.round((entry.eval.passCount / total) * 100);
159
- if (entry.eval.failCount > 0)
160
- issues.push(lang === 'zh' ? `eval 通过率 ${pct}%(${entry.eval.failCount} 条挂)` : `eval ${pct}% pass (${entry.eval.failCount} fail)`);
161
- }
162
- }
163
- const obIssueBand = effectiveObserveBand(entry.observe);
164
- if (entry.observe && obIssueBand !== 'green' && obIssueBand !== 'gray') {
165
- issues.push(lang === 'zh' ? `observe ${BAND_DOT[obIssueBand]}` : `observe ${BAND_DOT[obIssueBand]}`);
166
- }
167
- if (entry.observe && entry.observe.confidence === 'underpowered') {
168
- // 低 N observe 既不算「全绿」也不下硬结论,跟列表口径一致标注仅供参考,避免摘要谎报三视角全绿。
169
- issues.push(lang === 'zh' ? 'observe 样本不足(仅供参考)' : 'observe low N (indicative)');
170
- }
171
- const high = insights.filter((i) => i.severity === 'high').length;
172
- const med = insights.filter((i) => i.severity === 'medium').length;
173
- if (high > 0 || med > 0) {
174
- const parts = [];
175
- if (high > 0)
176
- parts.push(lang === 'zh' ? `${high} 个高优` : `${high} high`);
177
- if (med > 0)
178
- parts.push(lang === 'zh' ? `${med} 个中优` : `${med} medium`);
179
- issues.push(lang === 'zh' ? `检出 ${insights.length} 个待优化(${parts.join(',')})` : `${insights.length} issue(s) (${parts.join(', ')})`);
180
- }
181
- if (issues.length === 0) {
182
- const ran = [entry.doctor, entry.eval, entry.observe].filter(Boolean).length;
183
- if (ran === 0)
184
- return lang === 'zh' ? '尚未运行任何检查' : 'No checks run yet';
185
- return lang === 'zh' ? '✅ 三视角全绿,无告警' : '✅ All three views green';
186
- }
187
- return issues.join(lang === 'zh' ? ',' : ', ');
188
- }
189
- function buildTrendData(entry) {
190
- const dateMap = new Map();
191
- const dateOf = (ts) => ts.slice(0, 10);
192
- const get = (date) => {
193
- if (!dateMap.has(date)) {
194
- dateMap.set(date, { x: date, doctorPct: null, evalPct: null, observePct: null, doctorReportId: null, evalReportId: null, observeAnalysisId: null });
195
- }
196
- return dateMap.get(date);
197
- };
198
- for (const h of entry.doctorHistory) {
199
- const total = h.passCount + h.warnCount + h.failCount;
200
- const d = get(dateOf(h.timestamp));
201
- d.doctorPct = total > 0 ? (h.passCount / total) * 100 : null;
202
- d.doctorReportId = h.reportId;
203
- }
204
- for (const h of entry.evalHistory) {
205
- const c = h.compositeScore;
206
- const d = get(dateOf(h.timestamp));
207
- d.evalPct = c != null ? (c / 5) * 100 : null;
208
- d.evalReportId = h.reportId;
209
- }
210
- for (const h of entry.observeHistory) {
211
- const d = get(dateOf(h.generatedAt));
212
- d.observePct = (1 - h.gapRate) * 100;
213
- d.observeAnalysisId = h.analysisId;
214
- }
215
- return Array.from(dateMap.values()).sort((a, b) => a.x.localeCompare(b.x));
216
- }
217
- function renderTrendChart(entry, langQ, lang) {
218
- const data = buildTrendData(entry);
219
- const labelDoctor = lang === 'zh' ? '🩺 健康度' : '🩺 Structure';
220
- const labelEval = lang === 'zh' ? '🧪 评测结果' : '🧪 Test score';
221
- const labelObserve = lang === 'zh' ? '👁 线上观测' : '👁 Live stability';
222
- if (data.length < 2) {
223
- return `<div class="si-trend-empty">${lang === 'zh' ? '📈 还没有足够的历史数据画趋势(至少 2 个时间点)' : '📈 Need at least 2 data points for trend'}</div>`;
224
- }
225
- const linksDoctor = data.map(() => null);
226
- const linksEval = data.map((d) => d.evalReportId ? `/reports/${d.evalReportId}${langQ}` : null);
227
- const linksObserve = data.map((d) => d.observeAnalysisId ? `/analyses/${d.observeAnalysisId}${langQ}` : null);
228
- const explainDoctor = lang === 'zh' ? 'omk doctor 通过率' : 'omk doctor pass-rate';
229
- const explainEval = lang === 'zh' ? 'omk eval 综合分(归一)' : 'omk eval composite (normalized)';
230
- const explainObserve = lang === 'zh' ? 'omk observe 真实使用稳定度' : 'omk observe production stability';
231
- const json = JSON.stringify({
232
- labels: data.map((d) => fmtDateShort(d.x)),
233
- datasets: [
234
- { label: labelDoctor, data: data.map((d) => d.doctorPct), borderColor: '#5e8252', backgroundColor: 'rgba(94,130,82,.1)', tension: 0.3, spanGaps: true, _hint: explainDoctor },
235
- { label: labelEval, data: data.map((d) => d.evalPct), borderColor: '#5a7a93', backgroundColor: 'rgba(90,122,147,.1)', tension: 0.3, spanGaps: true, _hint: explainEval },
236
- { label: labelObserve, data: data.map((d) => d.observePct), borderColor: '#b08030', backgroundColor: 'rgba(176,128,48,.1)', tension: 0.3, spanGaps: true, _hint: explainObserve },
237
- ],
238
- });
239
- const links = JSON.stringify([linksDoctor, linksEval, linksObserve]);
240
- return `<div class="si-trend-canvas-wrap">
241
- <canvas id="trend-chart" data-chart='${json.replace(/'/g, '&#39;')}' data-links='${links.replace(/'/g, '&#39;')}'></canvas>
242
157
  </div>`;
243
158
  }
244
159
  /** 给一个 insight,从 stageRefs.evalSampleIds + evalReport 拿完整的失败样本详情。
@@ -283,29 +198,21 @@ function renderSampleBody(s, lang) {
283
198
  * 点开看 prompt / 期望 / 实际 / per-sample 建议 */
284
199
  // ────────── CSS ──────────
285
200
  const SKILL_DETAIL_CSS = `
286
- .si-back { display:inline-block;margin-bottom:8px;color:var(--text-muted);font-size:13px;text-decoration:none }
201
+ .si-back { display:inline-block;margin-bottom:12px;color:var(--text-muted);font-size:13px;text-decoration:none }
287
202
  .si-back:hover { color:var(--text-primary) }
288
203
 
289
- /* Hero — grade 卡 + 信息块,grid 布局让 grade 卡固定宽度 */
290
- .si-hero { display:grid;grid-template-columns:auto 1fr auto;gap:18px;align-items:center;padding:14px 18px;background:var(--bg-surface);border-radius:8px;box-shadow:var(--shadow-sm);margin:8px 0 14px }
291
- @media(max-width:640px){ .si-hero { grid-template-columns:auto 1fr;gap:12px } .si-hero-meta { grid-column:1/-1;text-align:right } }
292
-
293
- /* 健康等级卡 — 左侧色卡,emoji + 等级 + 参考分纵向堆叠 */
294
- .si-hero-grade { display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;min-width:96px;padding:10px 14px;border-radius:8px;background:var(--bg-soft);border-left:4px solid var(--border) }
295
- .si-hero-grade--green { border-left-color:#5e8252;background:rgba(94,130,82,.08) }
296
- .si-hero-grade--yellow { border-left-color:#b08030;background:rgba(176,128,48,.08) }
297
- .si-hero-grade--red { border-left-color:#9c4a3f;background:rgba(156,74,63,.08) }
298
- .si-hero-grade--gray { border-left-color:var(--border);background:var(--bg-soft) }
299
- .si-hero-grade-emoji { font-size:20px;line-height:1 }
300
- .si-hero-grade-label { font-size:13px;font-weight:600;color:var(--text-primary);letter-spacing:0.02em }
301
- .si-hero-score { font-variant-numeric:tabular-nums;cursor:help;display:flex;align-items:baseline;gap:1px;margin-top:1px }
302
- .si-hero-score-num { font-size:22px;font-weight:700;line-height:1 }
303
- .si-hero-grade--green .si-hero-score-num { color:#5e8252 }
304
- .si-hero-grade--yellow .si-hero-score-num { color:#b08030 }
305
- .si-hero-grade--red .si-hero-score-num { color:#9c4a3f }
306
- .si-hero-grade--gray .si-hero-score-num { color:var(--text-muted) }
307
- .si-hero-score-unit { font-size:11px;color:var(--text-muted);font-weight:500 }
308
- .si-hero-score--empty { font-size:18px;color:var(--text-muted) }
204
+ /* Hero — skill 名 + 综合分圆环 + 等级 */
205
+ .sd-hero { display:flex;align-items:center;justify-content:space-between;gap:20px;padding:20px 24px;background:#fff;border:1px solid #e4e8f1;border-radius:12px;box-shadow:0 8px 24px rgba(31,41,55,4%);margin-bottom:16px }
206
+ @media(max-width:640px){ .sd-hero { flex-direction:column;text-align:center } }
207
+ .sd-hero-left { display:flex;flex-direction:column;gap:4px;min-width:0 }
208
+ .sd-hero-name { margin:0;font-size:18px;font-weight:700;color:#182033 }
209
+ .sd-hero-meta { font-size:12px;color:#637083 }
210
+ .sd-hero-score { display:flex;align-items:center;gap:16px }
211
+ .sd-hero-grade { display:flex;flex-direction:column;gap:2px }
212
+ .sd-hero-grade-label { font-size:12px;color:#637083;font-weight:500 }
213
+ .sd-hero-grade-value { display:flex;align-items:baseline;gap:6px;font-size:24px;font-weight:800;line-height:1 }
214
+ .sd-hero-grade-value span { font-size:12px;font-weight:500;color:#637083 }
215
+ .sd-hero-grade-hint { font-size:11px;color:#94a3b8 }
309
216
 
310
217
  .si-hero-info { display:flex;flex-direction:column;gap:4px;min-width:0 }
311
218
  .si-hero-name { font-size:20px;font-weight:600;color:var(--text-primary);line-height:1.3 }
@@ -321,7 +228,7 @@ const SKILL_DETAIL_CSS = `
321
228
 
322
229
  /* 空态 — 不喊"恭喜",用已有数据证明它健康 + 给可补充的下一步,有视觉重量 */
323
230
  .si-empty { display:flex;flex-direction:column;gap:14px;padding:6px 4px }
324
- .si-empty-h { display:flex;align-items:center;gap:10px;padding:10px 14px;background:rgba(94,130,82,.08);border-left:3px solid #5e8252;border-radius:6px }
231
+ .si-empty-h { display:flex;align-items:center;gap:10px;padding:10px 14px;background:rgba(31,157,99,.08);border-left:3px solid #1f9d63;border-radius:6px }
325
232
  .si-empty-emoji { font-size:22px;line-height:1 }
326
233
  .si-empty-title { font-size:14px;font-weight:600;color:var(--text-primary) }
327
234
  .si-empty-section-h { font-size:11.5px;font-weight:600;color:var(--text-muted);letter-spacing:0.04em;margin-bottom:6px;text-transform:uppercase }
@@ -329,7 +236,7 @@ const SKILL_DETAIL_CSS = `
329
236
  .si-empty-list li { display:grid;grid-template-columns:18px 1fr;gap:8px;align-items:start;font-size:13px;line-height:1.55;color:var(--text-primary) }
330
237
  .si-empty-list code { background:var(--bg-soft);padding:1px 6px;border-radius:3px;font-family:"SF Mono",Menlo,monospace;font-size:11.5px;color:var(--text-primary) }
331
238
  .si-empty-icon { font-weight:700;text-align:center }
332
- .si-empty-list--pass .si-empty-icon { color:#5e8252 }
239
+ .si-empty-list--pass .si-empty-icon { color:#1f9d63 }
333
240
  .si-empty-list--next .si-empty-icon { color:var(--accent) }
334
241
  .si-aud { margin-bottom:12px }
335
242
  .si-aud:last-child { margin-bottom:0 }
@@ -345,13 +252,13 @@ const SKILL_DETAIL_CSS = `
345
252
  .si-row:focus-visible { outline:2px solid var(--accent);outline-offset:1px }
346
253
  .si-row-num { font-size:11px;font-weight:700;color:var(--text-muted);font-variant-numeric:tabular-nums;background:var(--bg-soft);padding:1px 6px;border-radius:3px;min-width:28px;text-align:center }
347
254
  .si-row-sev { font-size:10.5px;font-weight:700;padding:1px 7px;border-radius:8px;letter-spacing:0.02em }
348
- .si-row-sev--high { background:rgba(156,74,63,.16);color:#9c4a3f }
349
- .si-row-sev--medium { background:rgba(176,128,48,.14);color:#b08030 }
350
- .si-row-sev--low { background:rgba(94,130,82,.16);color:#5e8252 }
255
+ .si-row-sev--high { background:rgba(220,38,38,.16);color:#dc2626 }
256
+ .si-row-sev--medium { background:rgba(217,119,6,.14);color:#d97706 }
257
+ .si-row-sev--low { background:rgba(31,157,99,.16);color:#1f9d63 }
351
258
  .si-row-title { font-size:13.5px;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap }
352
259
  .si-row-meta { font-size:11px;color:var(--text-muted);font-variant-numeric:tabular-nums }
353
260
  .si-row-arrow { color:var(--text-muted);font-size:14px;font-weight:300 }
354
- .si-row--high { border-left:3px solid #9c4a3f;padding-left:7px }
261
+ .si-row--high { border-left:3px solid #dc2626;padding-left:7px }
355
262
 
356
263
  /* 右栏 */
357
264
  .si-right { display:flex;flex-direction:column;gap:12px }
@@ -365,7 +272,7 @@ const SKILL_DETAIL_CSS = `
365
272
  .si-trend-hint { font-size:10.5px;color:var(--text-muted);text-align:center;margin-top:6px;font-style:italic }
366
273
  .si-trend-fallback { position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;background:var(--bg-soft);border-radius:6px;padding:14px;text-align:center }
367
274
  .si-trend-fallback-icon { font-size:24px;line-height:1 }
368
- .si-trend-fallback-msg { font-size:13px;font-weight:600;color:#9c4a3f }
275
+ .si-trend-fallback-msg { font-size:13px;font-weight:600;color:#dc2626 }
369
276
  .si-trend-fallback-hint { font-size:11px;color:var(--text-muted);max-width:80% }
370
277
 
371
278
  /* 三视角当前状态区:跟趋势图区域并列,加标题让用户一眼知道这是"当前快照速览" */
@@ -379,9 +286,9 @@ const SKILL_DETAIL_CSS = `
379
286
  .si-stagecard { all:unset;cursor:pointer;display:grid;grid-template-columns:32px 1fr auto auto;gap:10px;align-items:center;padding:10px 14px;background:var(--bg-surface);border-radius:7px;box-shadow:var(--shadow-sm);transition:transform .12s,box-shadow .12s;border-left:4px solid var(--border) }
380
287
  .si-stagecard:hover { transform:translateX(2px);box-shadow:var(--shadow-md) }
381
288
  .si-stagecard:focus-visible { outline:2px solid var(--accent);outline-offset:1px }
382
- .si-stagecard--green { border-left-color:#5e8252 }
383
- .si-stagecard--yellow { border-left-color:#b08030 }
384
- .si-stagecard--red { border-left-color:#9c4a3f }
289
+ .si-stagecard--green { border-left-color:#1f9d63 }
290
+ .si-stagecard--yellow { border-left-color:#d97706 }
291
+ .si-stagecard--red { border-left-color:#dc2626 }
385
292
  .si-stagecard--gray { border-left-color:var(--border) }
386
293
  .si-stagecard-icon { font-size:22px;line-height:1 }
387
294
  .si-stagecard-body { display:flex;flex-direction:column;gap:2px }
@@ -399,7 +306,7 @@ const SKILL_DETAIL_CSS = `
399
306
  /* 失败用例 section(sample 视角主信息)*/
400
307
  .si-failures { margin:14px 0;padding:12px 14px;background:var(--bg-soft);border-radius:6px }
401
308
  .si-failures-h { font-size:13px;font-weight:600;color:var(--text-primary);margin-bottom:10px }
402
- .si-failure-item { background:rgba(156,74,63,.04);border-radius:5px;margin-bottom:8px;border-left:3px solid #9c4a3f }
309
+ .si-failure-item { background:rgba(220,38,38,.04);border-radius:5px;margin-bottom:8px;border-left:3px solid #dc2626 }
403
310
  .si-failure-item:last-child { margin-bottom:0 }
404
311
  /* 两行布局:第一行 id + tags + 右对齐 trace;第二行 summary 独占 */
405
312
  .si-failure-head { display:flex;flex-direction:column;gap:6px;padding:10px 12px;cursor:default }
@@ -410,24 +317,24 @@ details.si-failure-item > summary.si-failure-head::-webkit-details-marker { disp
410
317
  details.si-failure-item[open] > summary > .si-failure-row1::before { content:'▾ ' }
411
318
  .si-failure-row2 { font-size:14px;color:var(--text-secondary);line-height:1.55;padding-left:16px;white-space:pre-wrap;word-break:break-word }
412
319
  .si-failure-spacer { flex:1 }
413
- .si-failure-id { font-size:12.5px;font-weight:600;color:#9c4a3f;background:var(--bg-soft);padding:2px 8px;border-radius:3px;font-family:"SF Mono",Menlo,monospace }
320
+ .si-failure-id { font-size:12.5px;font-weight:600;color:#dc2626;background:var(--bg-soft);padding:2px 8px;border-radius:3px;font-family:"SF Mono",Menlo,monospace }
414
321
  .si-failure-modes { display:flex;gap:4px;flex-wrap:wrap }
415
- .si-failure-mode { font-size:12px;color:#b08030;background:rgba(176,128,48,.14);padding:2px 8px;border-radius:8px;font-weight:500;white-space:nowrap }
322
+ .si-failure-mode { font-size:12px;color:#d97706;background:rgba(217,119,6,.14);padding:2px 8px;border-radius:8px;font-weight:500;white-space:nowrap }
416
323
  .si-failure-trace { font-size:12px;color:var(--accent);text-decoration:none;font-weight:500;white-space:nowrap }
417
324
  .si-failure-trace:hover { text-decoration:underline }
418
325
 
419
326
  /* 通过 / 诱错 sample 紧凑行 */
420
327
  .si-pass-item { display:flex;gap:10px;align-items:center;padding:5px 8px;border-radius:4px;font-size:14px;line-height:1.5 }
421
328
  .si-pass-item:hover { background:var(--bg-soft) }
422
- .si-pass-card { background:var(--bg-surface);border-radius:5px;margin-bottom:6px;border-left:3px solid #5e8252 }
329
+ .si-pass-card { background:var(--bg-surface);border-radius:5px;margin-bottom:6px;border-left:3px solid #1f9d63 }
423
330
  .si-pass-card > summary { list-style:none;cursor:pointer;user-select:none }
424
331
  .si-pass-card > summary::-webkit-details-marker { display:none }
425
332
  .si-pass-head { display:flex;flex-direction:column;gap:4px;padding:8px 12px }
426
333
  .si-pass-row1 { display:flex;align-items:center;gap:8px;flex-wrap:wrap }
427
334
  .si-pass-row1::before { content:'▸ ';color:var(--text-muted);font-size:11px;margin-right:-4px }
428
335
  details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
429
- .si-pass-id { font-size:12px;font-weight:600;color:#5e8252;background:var(--bg-soft);padding:2px 8px;border-radius:3px;font-family:"SF Mono",Menlo,monospace;flex-shrink:0 }
430
- .si-pass-score { font-size:12px;font-weight:600;color:#5e8252;background:rgba(94,130,82,.1);padding:1px 7px;border-radius:10px }
336
+ .si-pass-id { font-size:12px;font-weight:600;color:#1f9d63;background:var(--bg-soft);padding:2px 8px;border-radius:3px;font-family:"SF Mono",Menlo,monospace;flex-shrink:0 }
337
+ .si-pass-score { font-size:12px;font-weight:600;color:#1f9d63;background:rgba(31,157,99,.1);padding:1px 7px;border-radius:10px }
431
338
  .si-pass-prompt-preview { font-size:13px;color:var(--text-secondary);padding-left:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis }
432
339
  .si-pass-body { padding:0 12px 10px 28px;display:flex;flex-direction:column;gap:8px }
433
340
  .si-pass-section { }
@@ -447,9 +354,9 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
447
354
  .se-score-label { font-size:12px;color:var(--text-muted);margin-bottom:4px }
448
355
  .se-score-bar { height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:4px }
449
356
  .se-score-fill { height:100%;border-radius:2px }
450
- .se-score-fill--good { background:#5e8252 }
451
- .se-score-fill--mid { background:#b08030 }
452
- .se-score-fill--low { background:#9c4a3f }
357
+ .se-score-fill--good { background:#1f9d63 }
358
+ .se-score-fill--mid { background:#d97706 }
359
+ .se-score-fill--low { background:#dc2626 }
453
360
  .se-score-val { font-size:16px;font-weight:600;color:var(--text-primary) }
454
361
  .se-score-max { font-size:12px;font-weight:400;color:var(--text-muted) }
455
362
  .si-pass-preview { flex:1;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0 }
@@ -458,33 +365,21 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
458
365
  .si-failure-detail .si-illustration { border-left:none;background:transparent;padding:8px 0 0 0 }
459
366
  .si-failure-no-detail { padding:6px 0;font-size:13px;color:var(--text-muted);font-style:italic }
460
367
 
461
- /* v7:顶部全景区 — 左 3 个 donut ring + 右趋势图,两栏同高 */
462
- .si-overview { display:grid;grid-template-columns:3fr 2fr;gap:18px;margin:10px 0 22px;align-items:stretch }
463
- @media(max-width:880px){ .si-overview { grid-template-columns:1fr } }
464
- .si-overview-cards { background:var(--bg-surface);border-radius:10px;padding:18px 14px;box-shadow:var(--shadow-sm);display:flex;align-items:center;justify-content:space-around;gap:8px }
465
- .si-overview-trend { background:var(--bg-surface);border-radius:10px;padding:14px 16px;box-shadow:var(--shadow-sm);display:flex;flex-direction:column }
466
-
467
- /* 三视角 ring + label(锚定到下面对应 section);内容自身居中,被 cards 容器垂直拉伸时上下留白对称 */
468
- .si-vs { flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:8px 4px;text-decoration:none;color:var(--text-primary);border-radius:8px;transition:background .15s }
469
- .si-vs:hover { background:var(--bg-soft);text-decoration:none }
470
- .si-ring { display:block }
471
- .si-vs-h { display:flex;align-items:baseline;gap:4px;flex-wrap:wrap;justify-content:center;margin-top:4px }
472
- .si-vs-icon { font-size:15px;line-height:1 }
473
- .si-vs-label { font-size:13.5px;font-weight:600;color:var(--text-primary) }
474
- .si-vs-sublabel { font-size:11px;color:var(--text-muted) }
475
- .si-vs-stat { font-size:12.5px;color:var(--text-secondary);font-variant-numeric:tabular-nums;line-height:1.45 }
476
- .si-vs-spark { display:block;margin-top:2px }
477
- @media(max-width:560px){
478
- .si-overview-cards { flex-direction:column;align-items:stretch }
479
- .si-vs { flex-direction:row;justify-content:flex-start;text-align:left;gap:14px;align-items:center }
480
- .si-vs-h, .si-vs-stat, .si-vs-spark { margin:0 }
481
- }
368
+ /* 三栏摘要卡 v2 */
369
+ .si-scard-v2 { background:var(--bg-surface);border-radius:var(--radius);box-shadow:var(--shadow-sm);padding:16px 20px;display:flex;flex-direction:column;gap:8px;text-decoration:none;color:var(--text-primary);transition:box-shadow .15s,transform .12s }
370
+ .si-scard-v2:hover { box-shadow:var(--shadow-md);transform:translateY(-1px);text-decoration:none }
371
+ .si-scard-v2-h { display:flex;align-items:center;gap:8px }
372
+ .si-scard-v2-icon { font-size:16px }
373
+ .si-scard-v2-title { font-size:13px;font-weight:600;color:var(--text-secondary);flex:1 }
374
+ .si-scard-v2-score { font-size:22px;font-weight:700;font-variant-numeric:tabular-nums }
375
+ .si-scard-v2-stat { font-size:13px;color:var(--text-secondary);line-height:1.5;flex:1 }
376
+ .si-scard-v2-link { font-size:12px;color:var(--accent);font-weight:500;margin-top:auto;padding-top:8px;border-top:1px solid var(--border) }
482
377
 
483
378
  /* 三视角 section(下方主体) */
484
379
  .si-sect { background:var(--bg-surface);border-radius:8px;padding:14px 18px;box-shadow:var(--shadow-sm);margin-bottom:14px;border-left:4px solid var(--border);scroll-margin-top:16px }
485
- .si-sect--green { border-left-color:#5e8252 }
486
- .si-sect--yellow { border-left-color:#b08030 }
487
- .si-sect--red { border-left-color:#9c4a3f }
380
+ .si-sect--green { border-left-color:#1f9d63 }
381
+ .si-sect--yellow { border-left-color:#d97706 }
382
+ .si-sect--red { border-left-color:#dc2626 }
488
383
  .si-sect--gray { border-left-color:var(--border) }
489
384
  .si-sect-h { display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:10px;flex-wrap:wrap }
490
385
  .si-sect-title { font-size:15px;font-weight:600;color:var(--text-primary) }
@@ -492,7 +387,7 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
492
387
  .si-sect-body { display:flex;flex-direction:column;gap:6px }
493
388
  .si-sect-empty { font-size:13px;color:var(--text-muted);font-style:italic;padding:8px 0 }
494
389
  .si-sect-line { font-size:14px;color:var(--text-secondary);margin-bottom:4px }
495
- .si-sect-allpass { font-size:14px;color:#5e8252;padding:6px 10px;background:rgba(94,130,82,.07);border-radius:4px }
390
+ .si-sect-allpass { font-size:14px;color:#1f9d63;padding:6px 10px;background:rgba(31,157,99,.07);border-radius:4px }
496
391
  .si-sect-link { display:inline-block;margin-top:8px;font-size:12px;color:var(--accent);text-decoration:none;font-weight:500 }
497
392
  .si-sect-link:hover { text-decoration:underline }
498
393
  .si-sect-fold { margin-top:8px;border-top:1px dashed var(--border);padding-top:6px }
@@ -505,9 +400,9 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
505
400
  /* doctor 规则展示(用在 doctor section 和 observe section) */
506
401
  .sd-grid { display:flex;flex-direction:column;gap:4px }
507
402
  .sd-dim { border-radius:6px;border:1px solid var(--border);overflow:hidden }
508
- .sd-dim--err { border-color:rgba(156,74,63,.3) }
509
- .sd-dim--warn { border-color:rgba(176,128,48,.3) }
510
- .sd-dim--pass { border-color:rgba(94,130,82,.25);background:rgba(94,130,82,.04) }
403
+ .sd-dim--err { border-color:rgba(220,38,38,.3) }
404
+ .sd-dim--warn { border-color:rgba(217,119,6,.3) }
405
+ .sd-dim--pass { border-color:rgba(31,157,99,.25);background:rgba(31,157,99,.04) }
511
406
  .sd-dim--skip { border-color:var(--border);opacity:.6 }
512
407
  .sd-dim > summary { list-style:none;cursor:pointer }
513
408
  .sd-dim > summary::-webkit-details-marker { display:none }
@@ -516,14 +411,14 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
516
411
  .sd-dim-name { font-weight:600;color:var(--text-primary);flex:1;min-width:0 }
517
412
  .sd-dim-badges { display:flex;gap:6px;flex-shrink:0 }
518
413
  .sd-badge { font-size:12px;padding:1px 7px;border-radius:10px;font-weight:500 }
519
- .sd-badge--err { background:rgba(156,74,63,.1);color:#9c4a3f }
520
- .sd-badge--warn { background:rgba(176,128,48,.1);color:#b08030 }
414
+ .sd-badge--err { background:rgba(220,38,38,.1);color:#dc2626 }
415
+ .sd-badge--warn { background:rgba(217,119,6,.1);color:#d97706 }
521
416
  .sd-dim-body { padding:0 12px 10px;display:flex;flex-direction:column;gap:6px }
522
417
  .sd-item { padding:8px 10px;border-radius:4px;font-size:14px;line-height:1.6 }
523
- .sd-item--err { background:rgba(156,74,63,.05);border-left:2px solid #9c4a3f }
524
- .sd-item--warn { background:rgba(176,128,48,.05);border-left:2px solid #b08030 }
418
+ .sd-item--err { background:rgba(220,38,38,.05);border-left:2px solid #dc2626 }
419
+ .sd-item--warn { background:rgba(217,119,6,.05);border-left:2px solid #d97706 }
525
420
  .sd-item-desc { color:var(--text-primary);word-break:break-word }
526
- .sd-item-sug { margin-top:4px;font-size:13px;color:var(--text-secondary);padding:4px 8px;background:rgba(94,130,82,.05);border-radius:4px;word-break:break-word }
421
+ .sd-item-sug { margin-top:4px;font-size:13px;color:var(--text-secondary);padding:4px 8px;background:rgba(31,157,99,.05);border-radius:4px;word-break:break-word }
527
422
  .sd-warn-fold { margin-top:6px }
528
423
  .sd-warn-list { display:flex;flex-direction:column;gap:6px;margin-top:6px }
529
424
  .sd-warn-toggle { cursor:pointer;font-size:13px;color:var(--text-muted);user-select:none;list-style:none;padding:2px 0 }
@@ -542,12 +437,12 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
542
437
  .si-sb { display:flex;flex-direction:column;gap:10px;padding:10px 0 }
543
438
  .si-sb-block { padding:10px 12px;border-radius:5px;background:var(--bg-soft) }
544
439
  .si-sb-block--prompt { background:var(--bg-soft) }
545
- .si-sb-block--expected { background:rgba(94,130,82,.06) }
546
- .si-sb-block--actual { background:rgba(176,128,48,.07) }
440
+ .si-sb-block--expected { background:rgba(31,157,99,.06) }
441
+ .si-sb-block--actual { background:rgba(217,119,6,.07) }
547
442
  .si-sb-block--suggest { background:rgba(122,107,137,.07) }
548
443
  .si-sb-label { font-size:12px;font-weight:700;color:var(--text-secondary);letter-spacing:0.02em;margin-bottom:5px }
549
- .si-sb-block--expected .si-sb-label { color:#5e8252 }
550
- .si-sb-block--actual .si-sb-label { color:#b08030 }
444
+ .si-sb-block--expected .si-sb-label { color:#1f9d63 }
445
+ .si-sb-block--actual .si-sb-label { color:#d97706 }
551
446
  .si-sb-block--suggest .si-sb-label { color:#7a6b89 }
552
447
  .si-sb-text { font-size:13px;color:var(--text-primary);line-height:1.6;white-space:pre-wrap;word-break:break-word }
553
448
  .si-sb-block--prompt .si-sb-text { font-family:"SF Mono",Menlo,monospace;font-size:12.5px;max-height:200px;overflow-y:auto }
@@ -560,14 +455,14 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
560
455
  /* 单条 sample 的"期望 vs 实际 vs 卡在哪"diff 对照视图(已被 v6 sb 视图替代,保留 placeholder)*/
561
456
  .si-diff { display:flex;flex-direction:column;gap:8px;padding:10px 0 }
562
457
  .si-diff-row { display:grid;grid-template-columns:24px 50px 1fr;gap:10px;align-items:start;padding:8px 12px;border-radius:5px;line-height:1.55 }
563
- .si-diff-row--expected { background:rgba(94,130,82,.07);border-left:3px solid #5e8252 }
564
- .si-diff-row--actual { background:rgba(176,128,48,.08);border-left:3px solid #b08030 }
565
- .si-diff-row--failed { background:rgba(156,74,63,.07);border-left:3px solid #9c4a3f }
458
+ .si-diff-row--expected { background:rgba(31,157,99,.07);border-left:3px solid #1f9d63 }
459
+ .si-diff-row--actual { background:rgba(217,119,6,.08);border-left:3px solid #d97706 }
460
+ .si-diff-row--failed { background:rgba(220,38,38,.07);border-left:3px solid #dc2626 }
566
461
  .si-diff-icon { font-size:14px;line-height:1.5 }
567
462
  .si-diff-label { font-size:11.5px;font-weight:700;letter-spacing:0.04em;color:var(--text-secondary);padding-top:2px }
568
- .si-diff-row--expected .si-diff-label { color:#5e8252 }
569
- .si-diff-row--actual .si-diff-label { color:#b08030 }
570
- .si-diff-row--failed .si-diff-label { color:#9c4a3f }
463
+ .si-diff-row--expected .si-diff-label { color:#1f9d63 }
464
+ .si-diff-row--actual .si-diff-label { color:#d97706 }
465
+ .si-diff-row--failed .si-diff-label { color:#dc2626 }
571
466
  .si-diff-text { color:var(--text-primary);font-size:13px;word-break:break-word }
572
467
  .si-diff-text--mono { font-family:"SF Mono",Menlo,monospace;font-size:12px;background:var(--bg-surface);padding:2px 7px;border-radius:3px;width:fit-content }
573
468
  /* 原始 prompt/output 降级到二级折叠 */
@@ -578,13 +473,13 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
578
473
  .si-diff-raw-body { padding:6px 0 }
579
474
 
580
475
  /* 占位符标识 */
581
- .si-placeholder { background:rgba(176,128,48,.20);color:#7a5810;padding:1px 4px;border-radius:3px;font-weight:600;font-style:italic }
582
- .si-rec-patch-hint { font-size:11.5px;color:#7a5810;background:rgba(176,128,48,.10);padding:6px 10px;border-radius:4px;margin-bottom:6px;line-height:1.5 }
583
- .si-rec-patch-hint .si-placeholder { background:rgba(176,128,48,.30) }
476
+ .si-placeholder { background:rgba(217,119,6,.20);color:#7a5810;padding:1px 4px;border-radius:3px;font-weight:600;font-style:italic }
477
+ .si-rec-patch-hint { font-size:11.5px;color:#7a5810;background:rgba(217,119,6,.10);padding:6px 10px;border-radius:4px;margin-bottom:6px;line-height:1.5 }
478
+ .si-rec-patch-hint .si-placeholder { background:rgba(217,119,6,.30) }
584
479
 
585
480
  /* 第一条建议的"⭐ 推荐先做"标记 */
586
- .si-rec-item--primary { border-left-color:#5e8252;background:rgba(94,130,82,.06) }
587
- .si-rec-star { font-size:10.5px;font-weight:700;color:#5e8252;background:rgba(94,130,82,.16);padding:2px 8px;border-radius:9px;letter-spacing:0.02em;flex-shrink:0 }
481
+ .si-rec-item--primary { border-left-color:#1f9d63;background:rgba(31,157,99,.06) }
482
+ .si-rec-star { font-size:10.5px;font-weight:700;color:#1f9d63;background:rgba(31,157,99,.16);padding:2px 8px;border-radius:9px;letter-spacing:0.02em;flex-shrink:0 }
588
483
 
589
484
  /* 建议 + patch 直接铺(去掉 details 折叠)*/
590
485
  .si-recs { margin:14px 0 }
@@ -594,9 +489,9 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
594
489
  .si-rec-head { display:flex;align-items:flex-start;gap:8px;margin-bottom:8px }
595
490
  .si-rec-num { font-size:11px;font-weight:700;color:var(--text-muted);background:var(--bg-surface);padding:1px 7px;border-radius:9px;flex-shrink:0;margin-top:1px;font-variant-numeric:tabular-nums }
596
491
  .si-rec-pri { padding:1px 7px;border-radius:8px;font-size:10.5px;font-weight:600;flex-shrink:0;margin-top:1px }
597
- .si-rec-pri--high { background:rgba(156,74,63,.14);color:#9c4a3f }
598
- .si-rec-pri--medium { background:rgba(176,128,48,.12);color:#b08030 }
599
- .si-rec-pri--low { background:rgba(94,130,82,.14);color:#5e8252 }
492
+ .si-rec-pri--high { background:rgba(220,38,38,.14);color:#dc2626 }
493
+ .si-rec-pri--medium { background:rgba(217,119,6,.12);color:#d97706 }
494
+ .si-rec-pri--low { background:rgba(31,157,99,.14);color:#1f9d63 }
600
495
  .si-rec-action { flex:1;color:var(--text-primary);font-size:13px;line-height:1.55 }
601
496
  .si-rec-patch { margin-left:0 }
602
497
  .si-rec-patch-meta { display:flex;gap:8px;align-items:center;font-size:11px;color:var(--text-muted);margin-bottom:5px;flex-wrap:wrap }
@@ -604,7 +499,7 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
604
499
  .si-rec-patch-loc { font-family:"SF Mono",Menlo,monospace;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap }
605
500
  .si-rec-patch-copy { background:var(--accent);color:#fff;border:none;border-radius:4px;padding:3px 10px;font-size:11px;cursor:pointer;font-weight:500;transition:background .1s;flex-shrink:0 }
606
501
  .si-rec-patch-copy:hover { background:var(--accent-hover) }
607
- .si-rec-patch-copy.copied { background:#5e8252 }
502
+ .si-rec-patch-copy.copied { background:#1f9d63 }
608
503
  .si-rec-patch-snippet { background:var(--bg-surface);padding:10px 12px;border-radius:4px;border-left:2px solid var(--accent);font-size:11.5px;line-height:1.55;overflow-x:auto;margin:0 }
609
504
  .si-rec-patch-snippet code { font-family:"SF Mono",Menlo,monospace;color:var(--text-primary);white-space:pre;display:block }
610
505
 
@@ -628,9 +523,9 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
628
523
  .si-modal-empty { color:var(--text-muted);font-size:13px;padding:18px;text-align:center;font-style:italic }
629
524
 
630
525
  .si-sev-tag { padding:2px 9px;border-radius:11px;font-size:11px;font-weight:600 }
631
- .si-sev-tag--high { background:rgba(156,74,63,.14);color:#9c4a3f }
632
- .si-sev-tag--medium { background:rgba(176,128,48,.12);color:#b08030 }
633
- .si-sev-tag--low { background:rgba(94,130,82,.14);color:#5e8252 }
526
+ .si-sev-tag--high { background:rgba(220,38,38,.14);color:#dc2626 }
527
+ .si-sev-tag--medium { background:rgba(217,119,6,.12);color:#d97706 }
528
+ .si-sev-tag--low { background:rgba(31,157,99,.14);color:#1f9d63 }
634
529
 
635
530
  /* 证据 */
636
531
  .si-evidence { background:var(--bg-soft);padding:10px 12px;border-radius:6px;margin-bottom:12px }
@@ -638,7 +533,7 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
638
533
  .si-ev { padding:4px 0;font-size:13px;line-height:1.55 }
639
534
  .si-ev-line { display:grid;grid-template-columns:24px 100px 1fr;gap:10px;align-items:start }
640
535
  .si-ev-icon { text-align:center;font-weight:600 }
641
- .si-ev--flagged .si-ev-icon { color:#9c4a3f }
536
+ .si-ev--flagged .si-ev-icon { color:#dc2626 }
642
537
  .si-ev--blind .si-ev-icon { color:#7a6b89 }
643
538
  .si-ev--silent .si-ev-icon { color:#7a9270 }
644
539
  .si-ev--na .si-ev-icon { color:var(--text-muted) }
@@ -656,7 +551,7 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
656
551
  .si-ill-row { display:grid;grid-template-columns:80px 1fr;gap:8px;align-items:start;margin-bottom:3px }
657
552
  .si-ill-label { color:var(--text-muted);font-size:10.5px;font-weight:500 }
658
553
  .si-ill-text { color:var(--text-primary);font-size:11.5px;background:var(--bg-soft);padding:3px 6px;border-radius:3px;white-space:pre-wrap;word-break:break-word }
659
- .si-ill-text--mono { font-family:"SF Mono",Menlo,monospace;background:rgba(156,74,63,.10);color:#9c4a3f }
554
+ .si-ill-text--mono { font-family:"SF Mono",Menlo,monospace;background:rgba(220,38,38,.10);color:#dc2626 }
660
555
  .si-ill-tools { margin:0;padding-left:0;list-style:none;display:flex;flex-direction:column;gap:2px }
661
556
  .si-ill-tools li { font-family:"SF Mono",Menlo,monospace;font-size:10.5px;background:var(--bg-soft);padding:2px 5px;border-radius:3px }
662
557
 
@@ -665,9 +560,9 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
665
560
  .si-recs li { font-size:13px;line-height:1.55 }
666
561
  .si-rec-line { display:flex;gap:8px;align-items:flex-start }
667
562
  .si-rec-pri { padding:1px 7px;border-radius:8px;font-size:10.5px;font-weight:600;flex-shrink:0;margin-top:1px }
668
- .si-rec-pri--high { background:rgba(156,74,63,.14);color:#9c4a3f }
669
- .si-rec-pri--medium { background:rgba(176,128,48,.12);color:#b08030 }
670
- .si-rec-pri--low { background:rgba(94,130,82,.14);color:#5e8252 }
563
+ .si-rec-pri--high { background:rgba(220,38,38,.14);color:#dc2626 }
564
+ .si-rec-pri--medium { background:rgba(217,119,6,.12);color:#d97706 }
565
+ .si-rec-pri--low { background:rgba(31,157,99,.14);color:#1f9d63 }
671
566
  .si-rec-action { color:var(--text-primary);flex:1 }
672
567
  .si-recs-h { font-size:12px;font-weight:600;color:var(--text-secondary);margin-bottom:8px;margin-top:0 }
673
568
  .si-patch { margin-top:6px;margin-left:24px }
@@ -685,12 +580,12 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
685
580
  /* Modal 内的 doctor rule list */
686
581
  .si-rules { list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:4px }
687
582
  .si-rule { display:flex;gap:10px;font-size:13px;line-height:1.55;align-items:flex-start;padding:8px 10px;border-radius:4px }
688
- .si-rule--warn { background:rgba(176,128,48,.07) }
689
- .si-rule--fail { background:rgba(156,74,63,.08) }
583
+ .si-rule--warn { background:rgba(217,119,6,.07) }
584
+ .si-rule--fail { background:rgba(220,38,38,.08) }
690
585
  .si-rule-icon { flex-shrink:0;font-weight:700;width:16px;text-align:center;font-size:14px }
691
- .si-rule--pass .si-rule-icon { color:#5e8252 }
692
- .si-rule--warn .si-rule-icon { color:#b08030 }
693
- .si-rule--fail .si-rule-icon { color:#9c4a3f }
586
+ .si-rule--pass .si-rule-icon { color:#1f9d63 }
587
+ .si-rule--warn .si-rule-icon { color:#d97706 }
588
+ .si-rule--fail .si-rule-icon { color:#dc2626 }
694
589
  .si-rule-body { flex:1;min-width:0 }
695
590
  .si-rule-id { font-size:11px;color:var(--text-muted);background:var(--bg-soft);padding:1px 5px;border-radius:3px;margin-right:6px }
696
591
  .si-rule-msg { color:var(--text-primary) }
@@ -699,12 +594,12 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
699
594
  /* Sample list (eval modal — 全部用例清单)。复用 si-history-row 大部分样式,只重写 grid */
700
595
  .si-sample-list { max-height:340px;overflow-y:auto }
701
596
  .si-sample-row { grid-template-columns:18px 100px 56px 1fr auto !important;gap:8px !important }
702
- .si-sample-row--pass { background:rgba(94,130,82,.06) }
703
- .si-sample-row--fail { background:rgba(156,74,63,.06);border-left:2px solid #9c4a3f }
597
+ .si-sample-row--pass { background:rgba(31,157,99,.06) }
598
+ .si-sample-row--fail { background:rgba(220,38,38,.06);border-left:2px solid #dc2626 }
704
599
  .si-sample-row--tripwire { background:rgba(122,107,137,.06);border-left:2px solid #7a6b89 }
705
600
  .si-sample-icon { font-weight:700;text-align:center;font-size:13px }
706
- .si-sample-icon--pass { color:#5e8252 }
707
- .si-sample-icon--fail { color:#9c4a3f }
601
+ .si-sample-icon--pass { color:#1f9d63 }
602
+ .si-sample-icon--fail { color:#dc2626 }
708
603
  .si-sample-icon--tripwire { color:#7a6b89 }
709
604
  .si-sample-id { font-size:11px;color:var(--text-secondary);background:transparent;padding:0;font-family:"SF Mono",Menlo,monospace;font-weight:600 }
710
605
  .si-sample-score { font-variant-numeric:tabular-nums;font-size:12px;color:var(--text-secondary);font-weight:600;text-align:right }
@@ -731,9 +626,9 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
731
626
  .si-history-date { font-variant-numeric:tabular-nums;font-weight:600;color:var(--text-secondary) }
732
627
  .si-history-meta { color:var(--text-secondary);font-variant-numeric:tabular-nums }
733
628
  .si-history-status { font-size:10.5px;font-weight:700;padding:1px 7px;border-radius:8px;letter-spacing:0.02em;justify-self:end }
734
- .si-history-status--green { background:rgba(94,130,82,.18);color:#5e8252 }
735
- .si-history-status--yellow { background:rgba(176,128,48,.16);color:#b08030 }
736
- .si-history-status--red { background:rgba(156,74,63,.18);color:#9c4a3f }
629
+ .si-history-status--green { background:rgba(31,157,99,.18);color:#1f9d63 }
630
+ .si-history-status--yellow { background:rgba(217,119,6,.16);color:#d97706 }
631
+ .si-history-status--red { background:rgba(220,38,38,.18);color:#dc2626 }
737
632
  .si-history-arrow { color:var(--text-muted);font-size:14px;font-weight:300 }
738
633
  @media(max-width:640px){
739
634
  .si-history-row { grid-template-columns:56px 1fr auto;gap:6px }
@@ -753,19 +648,19 @@ details[open] > .sd-warn-toggle::before { content:'▾ ' }
753
648
  .si-layer-lbl { color:var(--text-secondary) }
754
649
  .si-layer-bar { background:var(--bg-surface);border-radius:4px;height:8px;overflow:hidden }
755
650
  .si-layer-fill { height:100%;border-radius:4px }
756
- .si-layer-fill--pass { background:#5e8252 }
757
- .si-layer-fill--warn { background:#b08030 }
758
- .si-layer-fill--fail { background:#9c4a3f }
651
+ .si-layer-fill--pass { background:#1f9d63 }
652
+ .si-layer-fill--warn { background:#d97706 }
653
+ .si-layer-fill--fail { background:#dc2626 }
759
654
  .si-layer-num { font-variant-numeric:tabular-nums;font-weight:600;text-align:right;font-size:12px }
760
- .si-layer-num--pass { color:#5e8252 }
761
- .si-layer-num--warn { color:#b08030 }
762
- .si-layer-num--fail { color:#9c4a3f }
655
+ .si-layer-num--pass { color:#1f9d63 }
656
+ .si-layer-num--warn { color:#d97706 }
657
+ .si-layer-num--fail { color:#dc2626 }
763
658
  .si-eval-link { display:inline-block;color:var(--accent);font-size:12.5px;text-decoration:none;font-weight:500 }
764
659
  .si-eval-link:hover { text-decoration:underline }
765
660
  .si-failed-list { margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:6px }
766
- .si-failed-list li { font-size:12.5px;line-height:1.55;display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;padding:8px 10px;border-radius:4px;background:rgba(156,74,63,.06);border-left:2px solid #9c4a3f }
767
- .si-fs-id { font-size:11px;background:var(--bg-surface);padding:1px 5px;border-radius:3px;color:#9c4a3f;font-weight:600 }
768
- .si-fs-mode { font-size:10.5px;color:#b08030;background:rgba(176,128,48,.14);padding:1px 6px;border-radius:8px;font-weight:500 }
661
+ .si-failed-list li { font-size:12.5px;line-height:1.55;display:flex;flex-wrap:wrap;gap:6px;align-items:baseline;padding:8px 10px;border-radius:4px;background:rgba(220,38,38,.06);border-left:2px solid #dc2626 }
662
+ .si-fs-id { font-size:11px;background:var(--bg-surface);padding:1px 5px;border-radius:3px;color:#dc2626;font-weight:600 }
663
+ .si-fs-mode { font-size:10.5px;color:#d97706;background:rgba(217,119,6,.14);padding:1px 6px;border-radius:8px;font-weight:500 }
769
664
  .si-fs-summary { color:var(--text-secondary);font-size:12px;flex-basis:100% }
770
665
 
771
666
  /* Eval history table */
@@ -873,98 +768,8 @@ const TREND_INIT_SCRIPT = `
873
768
  else init();
874
769
  })();
875
770
  </script>`;
876
- // ────────── v7:三视角顶层全景 + 各 section 直铺 ──────────
877
- /** 紧凑 inline sparkline(供顶部速览卡每张里用,显示该视角历史走向)。
878
- * values 是 0-100 归一化数组(空数组返回占位)。 */
879
- function renderInlineSpark(values, color) {
880
- if (!values || values.length === 0) {
881
- return `<svg class="si-vs-spark" width="80" height="22" viewBox="0 0 80 22" xmlns="http://www.w3.org/2000/svg"><text x="50%" y="55%" text-anchor="middle" fill="#a8a8a8" font-size="9">no data</text></svg>`;
882
- }
883
- const w = 80, h = 22, pad = 2;
884
- const min = Math.min(...values, 0);
885
- const max = Math.max(...values, 100);
886
- const range = (max - min) || 1;
887
- const dx = values.length > 1 ? (w - 2 * pad) / (values.length - 1) : 0;
888
- const pts = values.map((v, i) => {
889
- const x = pad + i * dx;
890
- const y = pad + (h - 2 * pad) * (1 - (v - min) / range);
891
- return `${x.toFixed(1)},${y.toFixed(1)}`;
892
- }).join(' ');
893
- return `<svg class="si-vs-spark" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg">
894
- <polyline points="${pts}" fill="none" stroke="${color}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
895
- </svg>`;
896
- }
897
- /** Donut ring SVG:中央显示百分比,弧色按 band。半径 38,周长 ~239。 */
898
- function renderDonutRing(pct, color, label) {
899
- const r = 38, c = 2 * Math.PI * r;
900
- const safePct = pct == null ? 0 : Math.max(0, Math.min(100, pct));
901
- const dashOffset = c * (1 - safePct / 100);
902
- const centerText = pct == null ? '—' : `${Math.round(safePct)}%`;
903
- return `<svg class="si-ring" width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="${e(label)}">
904
- <circle cx="50" cy="50" r="${r}" fill="none" stroke="var(--bg-soft)" stroke-width="8"/>
905
- ${pct != null ? `<circle cx="50" cy="50" r="${r}" fill="none" stroke="${color}" stroke-width="8"
906
- stroke-dasharray="${c.toFixed(2)}" stroke-dashoffset="${dashOffset.toFixed(2)}"
907
- stroke-linecap="round" transform="rotate(-90 50 50)"/>` : ''}
908
- <text x="50" y="56" text-anchor="middle" font-size="19" font-weight="700" fill="${pct == null ? 'var(--text-muted)' : color}" font-family="-apple-system,BlinkMacSystemFont,sans-serif">${centerText}</text>
909
- </svg>`;
910
- }
911
- /** 三视角顶部全景:3 个 Apple-watch 风 donut ring,每个 ring 下面带 emoji + label + 状态 + sparkline */
912
- function renderViewSummaryCards(entry, lang) {
913
- // doctor
914
- const docBand = entry.doctor
915
- ? (entry.doctor.status === 'fail' ? 'red' : entry.doctor.status === 'warn' ? 'yellow' : 'green') : 'gray';
916
- const docColor = docBand === 'green' ? '#5e8252' : docBand === 'yellow' ? '#b08030' : docBand === 'red' ? '#9c4a3f' : '#a8a8a8';
917
- const docPct = entry.doctor && (entry.doctor.passCount + entry.doctor.warnCount + entry.doctor.failCount) > 0
918
- ? (entry.doctor.passCount / (entry.doctor.passCount + entry.doctor.warnCount + entry.doctor.failCount)) * 100
919
- : null;
920
- const docPassRates = entry.doctorHistory.map((h) => {
921
- const tot = h.passCount + h.warnCount + h.failCount;
922
- return tot > 0 ? (h.passCount / tot) * 100 : 0;
923
- });
924
- const docStat = entry.doctor
925
- ? `${entry.doctor.passCount}✓ ${entry.doctor.warnCount}⚠ ${entry.doctor.failCount}✗`
926
- : (lang === 'zh' ? '未运行' : 'not run');
927
- // eval
928
- const evalBand = entry.eval
929
- ? (entry.eval.failCount === 0 ? 'green' : entry.eval.passCount === 0 ? 'red' : 'yellow') : 'gray';
930
- const evalColor = evalBand === 'green' ? '#5e8252' : evalBand === 'yellow' ? '#b08030' : evalBand === 'red' ? '#9c4a3f' : '#a8a8a8';
931
- const evalPct = entry.eval && entry.eval.compositeScore != null
932
- ? (entry.eval.compositeScore / 5) * 100
933
- : null;
934
- const evalCompositeRates = entry.evalHistory.map((h) => (h.compositeScore ?? 0) / 5 * 100);
935
- const evalStat = entry.eval && (entry.eval.passCount + entry.eval.failCount) > 0
936
- ? `${entry.eval.totalSamples} ${lang === 'zh' ? '用例' : 'samples'} · ${Math.round((entry.eval.passCount / (entry.eval.passCount + entry.eval.failCount)) * 100)}%${entry.eval.compositeScore != null ? ` · ${entry.eval.compositeScore.toFixed(2)}/5` : ''}`
937
- : (lang === 'zh' ? '未运行' : 'not run');
938
- // observe
939
- const obsBand = effectiveObserveBand(entry.observe);
940
- const obsColor = obsBand === 'green' ? '#5e8252' : obsBand === 'yellow' ? '#b08030' : obsBand === 'red' ? '#9c4a3f' : '#a8a8a8';
941
- const obsPct = entry.observe ? (1 - entry.observe.gapRate) * 100 : null;
942
- // underpowered 历史点不进趋势 spark:低 N 的 stability 仅供参考,不该当作可信趋势顶点(口径同参考分)。
943
- const obsStabilityRates = entry.observeHistory
944
- .filter((h) => h.confidence !== 'underpowered')
945
- .map((h) => (1 - h.gapRate) * 100);
946
- const obsStat = !entry.observe
947
- ? (lang === 'zh' ? '未运行' : 'not run')
948
- // 低 N 不报「X% 稳定」硬指标,只标段数 + 仅供参考,跟列表卡口径一致。
949
- : entry.observe.confidence === 'underpowered'
950
- ? `${entry.observe.segmentCount} ${lang === 'zh' ? '段 · 样本不足' : 'segs · low N'}`
951
- : `${entry.observe.segmentCount} ${lang === 'zh' ? '段' : 'segs'} · ${((1 - entry.observe.gapRate) * 100).toFixed(0)}% ${lang === 'zh' ? '稳定' : 'stable'}`;
952
- const node = (icon, label, sublabel, band, pct, color, stat, sparkRates, anchor) => `<a class="si-vs si-vs--${band}" href="#${anchor}">
953
- ${renderDonutRing(pct, color, `${label} ${sublabel}`)}
954
- <div class="si-vs-h">
955
- <span class="si-vs-icon">${icon}</span>
956
- <span class="si-vs-label">${e(label)}</span>
957
- <span class="si-vs-sublabel">${e(sublabel)}</span>
958
- </div>
959
- <div class="si-vs-stat">${e(stat)}</div>
960
- ${renderInlineSpark(sparkRates, color)}
961
- </a>`;
962
- return `${node('🩺', lang === 'zh' ? '健康度' : 'Structure', '(doctor)', docBand, docPct, docColor, docStat, docPassRates, 'section-doctor')}
963
- ${node('🧪', lang === 'zh' ? '评测结果' : 'Test score', '(eval)', evalBand, evalPct, evalColor, evalStat, evalCompositeRates, 'section-eval')}
964
- ${node('👁', lang === 'zh' ? '线上观测' : 'Live stability', '(observe)', obsBand, obsPct, obsColor, obsStat, obsStabilityRates, 'section-observe')}`;
965
- }
966
771
  /** doctor section:展示告警 / 失败规则;通过的规则折叠到底部 */
967
- function renderDoctorSection(snap, history, lang) {
772
+ function renderDoctorSection(snap, history, skillName, lang) {
968
773
  if (!snap) {
969
774
  return `<section id="section-doctor" class="si-sect si-sect--gray">
970
775
  <div class="si-sect-h">🩺 ${lang === 'zh' ? '健康度 (doctor)' : 'Structure (doctor)'}</div>
@@ -1038,6 +843,7 @@ function renderDoctorSection(snap, history, lang) {
1038
843
  </details>`
1039
844
  : ''}
1040
845
  </div>
846
+ <a class="si-sect-link" href="/doctors/${e(snap.reportId)}?skill=${encodeURIComponent(skillName)}${lang === DEFAULT_LANG ? '' : `&lang=${lang}`}">${lang === 'zh' ? '完整体检报告 →' : 'Full doctor report →'}</a>
1041
847
  ${renderDoctorHistory(history, lang)}
1042
848
  </section>`;
1043
849
  }
@@ -1299,20 +1105,9 @@ export function renderSkillDetail(entry, evalReport, lang = DEFAULT_LANG, insigh
1299
1105
  .filter((s) => Boolean(s)).sort().pop();
1300
1106
  return layout(entry.skillName, `
1301
1107
  <main>
1302
- <a class="si-back" href="/${langQ}">${lang === 'zh' ? '← 返回 Skills' : '← Back to Skills'}</a>
1108
+ <a class="si-back" href="/${langQ}">${lang === 'zh' ? '← 返回 Skill 列表' : '← Back to Skills'}</a>
1303
1109
  ${renderHero(entry, insights, lastTs, reportCount, lang)}
1304
-
1305
- <div class="si-overview">
1306
- <section class="si-overview-cards" aria-label="${lang === 'zh' ? '三视角速览' : 'View summary'}">
1307
- ${renderViewSummaryCards(entry, lang)}
1308
- </section>
1309
- <section class="si-overview-trend">
1310
- <div class="si-trend-h">📈 ${lang === 'zh' ? '健康趋势(0-100%)' : 'Trend (0-100%)'}</div>
1311
- ${renderTrendChart(entry, langQ, lang)}
1312
- </section>
1313
- </div>
1314
-
1315
- ${renderDoctorSection(entry.doctor, entry.doctorHistory, lang)}
1110
+ ${renderDoctorSection(entry.doctor, entry.doctorHistory, entry.skillName, lang)}
1316
1111
  ${renderEvalSection(entry.eval, evalReport, entry.evalHistory, langQ, lang)}
1317
1112
  ${renderObserveSection(entry.observe, langQ, lang)}
1318
1113
  </main>