oh-my-knowledge 0.35.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 (90) 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 +121 -2
  5. package/dist/authoring/evolver.js +1 -1
  6. package/dist/cli/commands/doctor.js +21 -5
  7. package/dist/cli/commands/eval/index.js +7 -6
  8. package/dist/cli/commands/evolve.d.ts +11 -0
  9. package/dist/cli/commands/evolve.js +105 -5
  10. package/dist/cli/commands/list.d.ts +25 -0
  11. package/dist/cli/commands/list.js +118 -0
  12. package/dist/cli/commands/promote.d.ts +24 -0
  13. package/dist/cli/commands/promote.js +158 -0
  14. package/dist/cli/commands/rollback.d.ts +20 -0
  15. package/dist/cli/commands/rollback.js +95 -0
  16. package/dist/cli/commands/sample.d.ts +1 -0
  17. package/dist/cli/commands/sample.js +10 -4
  18. package/dist/cli/lib/cell-format.d.ts +17 -0
  19. package/dist/cli/lib/cell-format.js +19 -0
  20. package/dist/cli/lib/cmd-flags.d.ts +1 -0
  21. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  22. package/dist/cli/lib/i18n-dict/common.js +8 -0
  23. package/dist/cli/lib/i18n-dict/list.d.ts +3 -0
  24. package/dist/cli/lib/i18n-dict/list.js +36 -0
  25. package/dist/cli/lib/i18n-dict/promote.d.ts +3 -0
  26. package/dist/cli/lib/i18n-dict/promote.js +54 -0
  27. package/dist/cli/lib/i18n-dict/rollback.d.ts +3 -0
  28. package/dist/cli/lib/i18n-dict/rollback.js +22 -0
  29. package/dist/cli/lib/i18n-dict.d.ts +4 -1
  30. package/dist/cli/lib/i18n-dict.js +6 -0
  31. package/dist/cli/lib/progress.d.ts +3 -0
  32. package/dist/cli/lib/progress.js +22 -0
  33. package/dist/cli/lib/run-tally.js +1 -1
  34. package/dist/cli/lib/shared.js +1 -1
  35. package/dist/cli/lib/source-probe.d.ts +15 -0
  36. package/dist/cli/lib/source-probe.js +129 -0
  37. package/dist/doctor/endpoint-rule.d.ts +60 -0
  38. package/dist/doctor/endpoint-rule.js +381 -0
  39. package/dist/doctor/health/load-custom-dimensions.d.ts +6 -0
  40. package/dist/doctor/health/load-custom-dimensions.js +43 -3
  41. package/dist/doctor/index.js +9 -2
  42. package/dist/eval-core/evaluation-reporting.d.ts +1 -0
  43. package/dist/eval-core/evaluation-reporting.js +6 -4
  44. package/dist/eval-workflows/batch-evaluation-workflow.js +3 -2
  45. package/dist/eval-workflows/run-evaluation.js +2 -2
  46. package/dist/executors/codex-cli.js +5 -1
  47. package/dist/inputs/skill-loader.js +6 -3
  48. package/dist/managed/index.d.ts +2 -0
  49. package/dist/managed/index.js +2 -0
  50. package/dist/managed/list-view.d.ts +58 -0
  51. package/dist/managed/list-view.js +70 -0
  52. package/dist/managed/promote-gate.d.ts +40 -0
  53. package/dist/managed/promote-gate.js +37 -0
  54. package/dist/managed/store.d.ts +25 -2
  55. package/dist/managed/store.js +137 -11
  56. package/dist/observability/experience.d.ts +2 -0
  57. package/dist/observability/experience.js +31 -2
  58. package/dist/observability/inbox.js +31 -5
  59. package/dist/observability/review-state.js +22 -11
  60. package/dist/observability/soft-standards/llm-extractor.js +3 -3
  61. package/dist/observability/soft-standards/skill-standards-store.d.ts +1 -0
  62. package/dist/observability/soft-standards/skill-standards-store.js +77 -14
  63. package/dist/observability/soft-standards/types.d.ts +2 -2
  64. package/dist/renderer/doctor-detail-renderer.d.ts +9 -0
  65. package/dist/renderer/doctor-detail-renderer.js +114 -0
  66. package/dist/renderer/html-renderer.d.ts +3 -2
  67. package/dist/renderer/html-renderer.js +102 -102
  68. package/dist/renderer/icons.d.ts +29 -0
  69. package/dist/renderer/icons.js +66 -0
  70. package/dist/renderer/layout.js +81 -68
  71. package/dist/renderer/observation-inbox/styles.d.ts +1 -1
  72. package/dist/renderer/observation-inbox/styles.js +53 -53
  73. package/dist/renderer/report-shell.d.ts +77 -0
  74. package/dist/renderer/report-shell.js +223 -0
  75. package/dist/renderer/skill-detail-renderer.js +147 -352
  76. package/dist/renderer/skill-health-renderer.js +50 -73
  77. package/dist/renderer/skill-list-renderer.js +358 -306
  78. package/dist/renderer/summary.js +272 -162
  79. package/dist/renderer/test-view.d.ts +4 -3
  80. package/dist/renderer/test-view.js +386 -135
  81. package/dist/server/report-server.js +142 -28
  82. package/dist/server/report-store.d.ts +1 -1
  83. package/dist/server/report-store.js +16 -15
  84. package/dist/server/skill-index.js +5 -4
  85. package/dist/types/doctor.d.ts +24 -2
  86. package/dist/types/doctor.js +1 -1
  87. package/dist/types/managed.d.ts +17 -3
  88. package/dist/types/observability.d.ts +6 -6
  89. package/dist/types/report.d.ts +9 -4
  90. package/package.json +1 -1
@@ -13,7 +13,8 @@
13
13
  *
14
14
  * 旧 report 缺 sampleSnapshots 时降级显示("无设计快照,只有执行结果")。
15
15
  */
16
- import { e } from './layout.js';
16
+ import { e, t } from './layout.js';
17
+ import { icon } from './icons.js';
17
18
  // ──────────────────────────────────────────────────────────────────
18
19
  // Assertion → Turn 绑定:扫执行轨迹找断言匹配的 turn 编号
19
20
  // ──────────────────────────────────────────────────────────────────
@@ -531,30 +532,134 @@ function rootCauseLabel(c, lang) {
531
532
  };
532
533
  return (lang === 'zh' ? zh : en)[c] ?? c;
533
534
  }
534
- /** 把 design / trace / assertions 三段子内容包成 <details>(默认折叠),供 pass row 展开 / fail card 折叠后查看用。 */
535
- function renderSubSections(d, lang) {
536
- const { snapshot, variant } = d;
535
+ /** 用例设计 fold(prompt/rubric/期望断言/mocks)— 来自 snapshot,与 variant 无关。
536
+ * 多 variant 模式下只渲染一次(共享)。 */
537
+ function renderDesignFold(snapshot, lang) {
538
+ return `<details class="tv-section"><summary>${lang === 'zh' ? '用例设计' : 'Test design'}</summary><div class="tv-section-body">${renderDesignSection(snapshot, lang)}</div></details>`;
539
+ }
540
+ /** 执行轨迹 + 断言结果 fold(per-variant)。两段绑在一起,因为断言的 turn 绑定靠同一份轨迹解析。 */
541
+ function renderTraceAndAssertionsFolds(variant, snapshot, lang) {
537
542
  const toolCalls = variant.toolCalls ?? [];
538
543
  const turns = variant.turns;
539
544
  const summary = renderAssertionsSummary(variant.assertions?.details, snapshot?.assertions, toolCalls, turns, lang);
540
545
  const matchedByTurn = buildMatchedMocksByTurn(summary.matchedMocks, snapshot?.assertions, toolCalls, turns);
541
- const designHtml = renderDesignSection(snapshot, lang);
542
546
  const traceHtml = renderExecutionTrace(variant, summary.detailsByTurn, matchedByTurn, lang);
543
547
  const totalAsserts = variant.assertions?.total ?? 0;
544
548
  const passedAsserts = variant.assertions?.passed ?? 0;
545
549
  return `
546
- <details class="tv-section"><summary>${lang === 'zh' ? '用例设计' : 'Test design'}</summary><div class="tv-section-body">${designHtml}</div></details>
547
550
  <details class="tv-section"><summary>${lang === 'zh' ? '执行轨迹' : 'Execution trace'}<span class="tv-section-stat">(${turns?.length ?? 0} ${lang === 'zh' ? '步' : 'steps'} · ${toolCalls.length} ${lang === 'zh' ? '工具调用' : 'tool calls'})</span></summary><div class="tv-section-body">${traceHtml}</div></details>
548
551
  <details class="tv-section"><summary>${lang === 'zh' ? '断言结果' : 'Assertion results'}<span class="tv-section-stat">(${passedAsserts}/${totalAsserts})</span></summary><div class="tv-section-body">${summary.html}</div></details>
549
552
  `;
550
553
  }
554
+ /** 把 design / trace / assertions 三段子内容包成 <details>(默认折叠)。单 variant 用。 */
555
+ function renderSubSections(d, lang) {
556
+ return renderDesignFold(d.snapshot, lang) + renderTraceAndAssertionsFolds(d.variant, d.snapshot, lang);
557
+ }
551
558
  function buildCardData(sample_id, snapshot, variant) {
552
559
  const rawVerdict = variant.ok === false ? 'fail' : sampleVerdict(variant.assertions?.details);
553
560
  const isTripwire = rawVerdict === 'fail' && isTripwireSample(snapshot, variant);
554
561
  return { sample_id, snapshot, variant, cardKind: isTripwire ? 'tripwire' : rawVerdict };
555
562
  }
556
- /** Header 公共部分:状态点 + sample_id + 标题 + 耗时。
557
- * 评分(compositeScore)和成本($cost)挪到详情区,列表只保留状态识别需要的最小信息。 */
563
+ // ──────────────────────────────────────────────────────────────────
564
+ // 融合视角:评分内容(原"评测视角"逐用例)直接长进单测卡片
565
+ // - 头部:综合分 badge(一眼看分高低)
566
+ // - 体内:三层分布条(事实/行为/评委)+ 断言计数 + 评委评价文本
567
+ // 这样一条用例 = 既是功能测试(pass/fail/诊断)也是评分卡,不再渲染两遍。
568
+ // ──────────────────────────────────────────────────────────────────
569
+ /** 1-5 分 → 色档 class 后缀(>=4 通过 / >=3 警示 / 其余失败)。 */
570
+ function scoreBand(score) {
571
+ return score >= 4 ? 'pass' : score >= 3 ? 'warn' : 'fail';
572
+ }
573
+ /** 卡头综合分 badge:`4.09/5`,按色档着色。无分(纯断言用例 / executor 错误)返回空串。 */
574
+ function renderCompositeBadge(variant) {
575
+ const score = variant.compositeScore ?? variant.llmScore;
576
+ if (typeof score !== 'number')
577
+ return '';
578
+ return `<span class="tv-composite tv-composite--${scoreBand(score)}">${score.toFixed(2)}<span class="tv-composite-unit">/5</span></span>`;
579
+ }
580
+ /** 一个分数 chip:`[icon] 事实 4.0`。primary(综合分)加粗。 */
581
+ function scoreChip(label, score, primary = false) {
582
+ return `<span class="tv-score-item${primary ? ' tv-score-item--primary' : ''}">${label} <b class="tv-score-v tv-score-v--${scoreBand(score)}">${score.toFixed(2)}</b></span>`;
583
+ }
584
+ /** 体内分布条:综合分 + 三层(事实/行为/评委)+ 断言计数。无任何分数返回空串。 */
585
+ function renderScoreStrip(variant, lang) {
586
+ const items = [];
587
+ const composite = variant.compositeScore ?? variant.llmScore;
588
+ if (typeof composite === 'number')
589
+ items.push(scoreChip(lang === 'zh' ? '综合' : 'Composite', composite, true));
590
+ const ls = variant.layeredScores;
591
+ // 三层图标沿用六维共享映射(fact/behavior/judge);chip 是 baseline 对齐的 inline-flex,svg 单独居中。
592
+ const chipIcon = (name) => icon(name, { size: 13, style: 'align-self:center' });
593
+ if (ls?.factScore != null)
594
+ items.push(scoreChip(`${chipIcon('fact')} ${lang === 'zh' ? '事实' : 'Fact'}`, ls.factScore));
595
+ if (ls?.behaviorScore != null)
596
+ items.push(scoreChip(`${chipIcon('behavior')} ${lang === 'zh' ? '行为' : 'Behavior'}`, ls.behaviorScore));
597
+ if (ls?.judgeScore != null)
598
+ items.push(scoreChip(`${chipIcon('judge')} ${lang === 'zh' ? '评委' : 'Judge'}`, ls.judgeScore));
599
+ if (variant.assertions && variant.assertions.total > 0) {
600
+ const a = variant.assertions;
601
+ const allPass = a.passed === a.total;
602
+ items.push(`<span class="tv-score-item">${lang === 'zh' ? '断言' : 'Assertions'} <b class="tv-score-v tv-score-v--${allPass ? 'pass' : 'fail'}">${a.passed}/${a.total}</b></span>`);
603
+ }
604
+ if (items.length === 0)
605
+ return '';
606
+ return `<div class="tv-scores">${items.join('')}</div>`;
607
+ }
608
+ /** 多评委 ensemble 分项(≥2 评委时):每个评委的分 + 推理,折叠展示。一致性指标 MAD/Pearson 收底。 */
609
+ function renderEnsembleBlock(ensemble, agreement, lang) {
610
+ if (!ensemble || ensemble.length < 2)
611
+ return '';
612
+ const rows = ensemble.map((j) => {
613
+ const stddev = j.scoreStddev != null && j.scoreStddev > 0 ? ` <span class="tv-judge-stab">±${j.scoreStddev}</span>` : '';
614
+ const fail = j.judgeFailureCount && j.judgeFailureCount > 0
615
+ ? ` <span class="tv-judge-stab tv-judge-stab--fail">${j.judgeFailureCount}/${j.scoreSamples?.length || '?'} fail</span>`
616
+ : '';
617
+ const reasoning = j.reasoning
618
+ ? `<details class="tv-collapsible"><summary>${t('judgeReasoning', lang)} ${t('judgeReasoningExpand', lang)}</summary><pre class="tv-pre">${e(j.reasoning)}</pre></details>`
619
+ : '';
620
+ return `<div class="tv-ensemble-row"><b>${e(j.judge)}</b>: ${j.score}${stddev}${fail}${reasoning}</div>`;
621
+ }).join('');
622
+ const agreementLine = agreement && agreement.pairCount > 0
623
+ ? `<div class="tv-ensemble-agree" title="${t('madDesc', lang)}">MAD ${agreement.meanAbsDiff}${agreement.pearson != null ? ` · Pearson ${agreement.pearson}` : ''}</div>`
624
+ : '';
625
+ return `<details class="tv-collapsible tv-ensemble"><summary>${t('ensembleHeader', lang)} (${ensemble.length})</summary>${rows}${agreementLine}</details>`;
626
+ }
627
+ /** 评委评价:打分理由(llmReason)+ CoT 折叠 + 自一致性(stddev/失败数)+ 多评委 ensemble 分项。
628
+ * 无任何 judge 信息(--no-judge / 纯断言)返回空串。与 diagnostic 互补:judge 答"打几分 + 为什么"。 */
629
+ function renderJudgeBlock(variant, lang) {
630
+ const hasReason = !!variant.llmReason;
631
+ const hasEnsemble = !!variant.llmEnsemble && variant.llmEnsemble.length >= 2;
632
+ const hasStddev = !!variant.llmScoreSamples && variant.llmScoreSamples.length > 1 && variant.llmScoreStddev != null;
633
+ if (!hasReason && !hasEnsemble && !hasStddev)
634
+ return '';
635
+ // Judge 自一致性:同一份输出多次评分的标准差 + 失败数(--judge-repeat ≥ 2 时)。
636
+ let stabTag = '';
637
+ if (hasStddev) {
638
+ stabTag += `<span class="tv-judge-stab" title="${t('judgeStddevDesc', lang)}">±${variant.llmScoreStddev}</span>`;
639
+ if (variant.llmScoreFailures && variant.llmScoreFailures > 0) {
640
+ stabTag += `<span class="tv-judge-stab tv-judge-stab--fail" title="${t('judgeFailuresDesc', lang)}">${variant.llmScoreFailures}/${variant.llmScoreSamples.length} fail</span>`;
641
+ }
642
+ }
643
+ const reasonHtml = hasReason
644
+ ? (() => {
645
+ const reason = variant.llmReason.length > 280 ? variant.llmReason.slice(0, 280) + '…' : variant.llmReason;
646
+ const cot = variant.llmReasoning
647
+ ? `<details class="tv-collapsible"><summary>${lang === 'zh' ? '展开完整 CoT' : 'show full CoT'}</summary><pre class="tv-pre">${e(variant.llmReasoning)}</pre></details>`
648
+ : '';
649
+ return `<div class="tv-judge-text">${e(reason)}</div>${cot}`;
650
+ })()
651
+ : '';
652
+ return `<div class="tv-judge">
653
+ <div class="tv-judge-h">${icon('judge', { size: 14 })}${lang === 'zh' ? '评委评价' : 'Judge'}${stabTag}</div>
654
+ ${reasonHtml}
655
+ ${renderEnsembleBlock(variant.llmEnsemble, variant.llmAgreement, lang)}
656
+ </div>`;
657
+ }
658
+ /** 评分区 = 分布条 + 评委评价,放在卡片 body 顶部(诊断之前)。 */
659
+ function renderScoringPart(variant, lang) {
660
+ return `${renderScoreStrip(variant, lang)}${renderJudgeBlock(variant, lang)}`;
661
+ }
662
+ /** Header 公共部分:状态点 + sample_id + 标题 + 综合分 badge + 耗时。 */
558
663
  function renderCardHeader(d, lang) {
559
664
  const { sample_id, snapshot, variant, cardKind } = d;
560
665
  const dur = variant.timing?.execMs ?? variant.durationMs;
@@ -566,14 +671,15 @@ function renderCardHeader(d, lang) {
566
671
  <span class="tv-dot tv-dot--${cardKind}"></span>
567
672
  <code class="tv-id">${e(sample_id)}</code>
568
673
  <span class="tv-title">${e(titleSnippet)}${snapshot?.prompt && snapshot.prompt.length > 80 ? '…' : ''}</span>
674
+ ${renderCompositeBadge(variant)}
569
675
  <span class="tv-meta">${durTag}</span>
570
676
  `;
571
677
  }
572
- /** Pass 用例:单行折叠,点击展开看完整内容(design/trace/assertions)。 */
678
+ /** Pass 用例:单行折叠,点击展开看分布条 + 评委评价 + design/trace/assertions。 */
573
679
  function renderPassRow(d, lang) {
574
680
  return `<details class="tv-row tv-row--pass">
575
681
  <summary class="tv-row-h">${renderCardHeader(d, lang)}</summary>
576
- <div class="tv-row-body">${renderSubSections(d, lang)}</div>
682
+ <div class="tv-row-body">${renderScoringPart(d.variant, lang)}${renderSubSections(d, lang)}</div>
577
683
  </details>`;
578
684
  }
579
685
  /** Fail / tripwire 卡片:默认展开,顶部诊断区,sub-section 折叠。 */
@@ -597,14 +703,143 @@ function renderFailCard(d, lang) {
597
703
  ${diagSummaryPreview}
598
704
  <div class="tv-card-body">
599
705
  ${tripwireBanner}
706
+ ${renderScoreStrip(d.variant, lang)}
600
707
  ${diagnosticHtml}
708
+ ${renderJudgeBlock(d.variant, lang)}
601
709
  ${renderSubSections(d, lang)}
602
710
  </div>
603
711
  </details>`;
604
712
  }
605
713
  // ──────────────────────────────────────────────────────────────────
714
+ // 多 variant:每条用例详展「主轮(最新)」+ 各轮分数轨迹条 + 其他轮折叠
715
+ // ──────────────────────────────────────────────────────────────────
716
+ /** 该 sample 的主轮 = variants 顺序里最后一个有结果的 variant(通常是最新/treatment)。 */
717
+ function pickPrimaryVariant(variantResults, variants) {
718
+ for (let i = variants.length - 1; i >= 0; i--) {
719
+ if (variantResults[variants[i]])
720
+ return variants[i];
721
+ }
722
+ return null;
723
+ }
724
+ /** 各轮分数轨迹条:`round-0 3.86 › round-1 1.71 › round-2 1.59 [最新]`,按分着色,主轮高亮。 */
725
+ function renderRoundStrip(variants, variantResults, primaryName, lang) {
726
+ const chips = variants.map((name) => {
727
+ const v = variantResults[name];
728
+ const isPrimary = name === primaryName;
729
+ if (!v) {
730
+ return `<span class="tv-round tv-round--missing">${e(name)} —</span>`;
731
+ }
732
+ const score = v.compositeScore ?? v.llmScore;
733
+ const hasScore = typeof score === 'number';
734
+ const band = hasScore ? scoreBand(score) : 'fail';
735
+ const kind = v.ok === false ? 'fail' : sampleVerdict(v.assertions?.details);
736
+ const icon = kind === 'pass' ? '✓' : '✗';
737
+ const curTag = isPrimary ? `<span class="tv-round-cur">${lang === 'zh' ? '最新' : 'latest'}</span>` : '';
738
+ return `<span class="tv-round tv-round--${band}${isPrimary ? ' tv-round--current' : ''}"><span class="tv-round-icon tv-round-icon--${kind === 'pass' ? 'pass' : 'fail'}">${icon}</span>${e(name)} <b>${hasScore ? score.toFixed(2) : '—'}</b>${curTag}</span>`;
739
+ }).join('<span class="tv-round-arrow">·</span>');
740
+ return `<div class="tv-rounds">${chips}</div>`;
741
+ }
742
+ /** 单条用例的多 variant 卡片:主轮详情 + 轨迹条 + 共享设计 + 其他轮折叠。 */
743
+ function renderMultiSampleCard(sample_id, snapshot, variantResults, variants, primaryName, lang) {
744
+ const primary = variantResults[primaryName];
745
+ const d = buildCardData(sample_id, snapshot, primary);
746
+ const verdict = d.cardKind === 'pass' ? 'pass' : 'fail';
747
+ const tripwireBanner = d.cardKind === 'tripwire'
748
+ ? `<div class="tv-tripwire-banner">
749
+ <div class="tv-tripwire-banner-title">${lang === 'zh' ? '◆ 这是诱错样本' : '◆ This is a tripwire sample'}</div>
750
+ <div class="tv-tripwire-banner-body">${lang === 'zh'
751
+ ? '用户 prompt 故意诱导 LLM 走错。LLM 失败 = 已记录的 LLM 边界数据点,不是要修的 bug。'
752
+ : 'The user prompt intentionally misdirects the LLM. A failure here is a recorded LLM-susceptibility data point — not a bug to fix.'}</div>
753
+ </div>`
754
+ : '';
755
+ // 其他轮(非主轮)折叠:每轮分数条 + 诊断 + 评委 + 轨迹/断言。evolve 多轮也不塞爆。
756
+ const otherRounds = variants.filter((n) => n !== primaryName && variantResults[n]);
757
+ const otherRoundsHtml = otherRounds.map((name) => {
758
+ const v = variantResults[name];
759
+ const rd = buildCardData(sample_id, snapshot, v);
760
+ const rv = rd.cardKind === 'pass' ? 'pass' : 'fail';
761
+ const score = v.compositeScore ?? v.llmScore;
762
+ const scoreText = typeof score === 'number' ? score.toFixed(2) : '—';
763
+ const icon = rd.cardKind === 'pass' ? '✓' : rd.cardKind === 'tripwire' ? '◆' : '✗';
764
+ return `<div class="tv-round-detail">
765
+ <div class="tv-round-detail-h"><span class="tv-round-detail-icon tv-round-detail-icon--${rd.cardKind}">${icon}</span> ${e(name)} <span class="tv-round-detail-score">${lang === 'zh' ? '综合' : 'composite'} ${scoreText}</span></div>
766
+ ${renderScoreStrip(v, lang)}
767
+ ${renderDiagnosticSection(v, rv, lang)}
768
+ ${renderJudgeBlock(v, lang)}
769
+ ${renderTraceAndAssertionsFolds(v, snapshot, lang)}
770
+ </div>`;
771
+ }).join('');
772
+ const otherRoundsFold = otherRoundsHtml
773
+ ? `<details class="tv-section"><summary>${lang === 'zh' ? '其他轮详情' : 'Other rounds'}<span class="tv-section-stat">(${otherRounds.length})</span></summary><div class="tv-section-body">${otherRoundsHtml}</div></details>`
774
+ : '';
775
+ const diagSummaryPreview = primary.diagnostic?.ok && primary.diagnostic.summary
776
+ ? `<div class="tv-card-preview">${e(primary.diagnostic.summary.slice(0, 120))}${primary.diagnostic.summary.length > 120 ? '…' : ''}</div>`
777
+ : '';
778
+ return `<details class="tv-card tv-card--${d.cardKind}">
779
+ <summary class="tv-card-h">${renderCardHeader(d, lang)}</summary>
780
+ ${diagSummaryPreview}
781
+ <div class="tv-card-body">
782
+ ${renderRoundStrip(variants, variantResults, primaryName, lang)}
783
+ ${tripwireBanner}
784
+ ${renderScoreStrip(primary, lang)}
785
+ ${renderDiagnosticSection(primary, verdict, lang)}
786
+ ${renderJudgeBlock(primary, lang)}
787
+ ${renderDesignFold(snapshot, lang)}
788
+ ${renderTraceAndAssertionsFolds(primary, snapshot, lang)}
789
+ ${otherRoundsFold}
790
+ </div>
791
+ </details>`;
792
+ }
793
+ // ──────────────────────────────────────────────────────────────────
606
794
  // 主入口
607
795
  // ──────────────────────────────────────────────────────────────────
796
+ /** 单 variant 的逐用例 body:统计条 + 三组(失败/诱错/通过)。 */
797
+ function renderSingleVariantBody(report, variant, lang) {
798
+ const results = report.results || [];
799
+ const snapshots = report.sampleSnapshots ?? {};
800
+ const passData = [];
801
+ const failData = [];
802
+ const tripwireData = [];
803
+ for (const r of results) {
804
+ const v = r.variants[variant];
805
+ if (!v)
806
+ continue;
807
+ const d = buildCardData(r.sample_id, snapshots[r.sample_id], v);
808
+ if (d.cardKind === 'pass')
809
+ passData.push(d);
810
+ else if (d.cardKind === 'tripwire')
811
+ tripwireData.push(d);
812
+ else
813
+ failData.push(d);
814
+ }
815
+ const passSection = passData.length > 0
816
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '通过用例' : 'Passed'}</div>
817
+ <div class="tv-rows">${passData.map((d) => renderPassRow(d, lang)).join('')}</div>`
818
+ : '';
819
+ const failSection = failData.length > 0
820
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '失败用例' : 'Failed'}</div>
821
+ ${failData.map((d) => renderFailCard(d, lang)).join('')}`
822
+ : '';
823
+ const tripwireSection = tripwireData.length > 0
824
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '诱错样本' : 'Tripwire samples'}</div>
825
+ ${tripwireData.map((d) => renderFailCard(d, lang)).join('')}`
826
+ : '';
827
+ const stats = computeVariantStats(report, variant);
828
+ return `${renderStatsBar(stats, lang)}${failSection}${tripwireSection}${passSection}`;
829
+ }
830
+ /** 统计条:通过 / 失败 / 诱错 / 平均分。可加 prefix(多 variant 标主轮名)。 */
831
+ function renderStatsBar(stats, lang, prefix = '') {
832
+ const tripwireStat = stats.tripwire > 0
833
+ ? `<span class="tv-stat tv-stat--tripwire"><span class="tv-stat-num">${stats.tripwire}</span>${lang === 'zh' ? '诱错触发' : 'tripwire'}</span>`
834
+ : '';
835
+ return `<div class="tv-variant-stats">
836
+ ${prefix}
837
+ <span class="tv-stat tv-stat--pass"><span class="tv-stat-num">${stats.passed}</span>${lang === 'zh' ? '通过' : 'passed'}</span>
838
+ <span class="tv-stat tv-stat--fail"><span class="tv-stat-num">${stats.failed}</span>${lang === 'zh' ? '失败' : 'failed'}</span>
839
+ ${tripwireStat}
840
+ <span class="tv-stat"><span class="tv-stat-num">${stats.avgScore.toFixed(1)}</span>${lang === 'zh' ? '平均分' : 'avg / 5'}</span>
841
+ </div>`;
842
+ }
608
843
  export function renderTestView(report, lang) {
609
844
  const variants = report.meta.variants || [];
610
845
  const results = report.results || [];
@@ -614,59 +849,46 @@ export function renderTestView(report, lang) {
614
849
  }
615
850
  const hasSnapshots = Object.keys(snapshots).length > 0;
616
851
  const fallbackBanner = hasSnapshots ? '' : `<div class="tv-warn">${lang === 'zh' ? '⚠ 该 report 缺 sampleSnapshots(早期版本生成),只能展示执行结果,无设计信息' : '⚠ Report has no sampleSnapshots (older format); only execution results shown'}</div>`;
617
- const variantSections = variants.map((variant, idx) => {
618
- // 按 verdict 分三组:pass(单行) / fail(完整卡片) / tripwire(诱错卡片)。
619
- const passData = [];
620
- const failData = [];
621
- const tripwireData = [];
622
- for (const r of results) {
623
- const v = r.variants[variant];
624
- if (!v)
625
- continue;
626
- const d = buildCardData(r.sample_id, snapshots[r.sample_id], v);
627
- if (d.cardKind === 'pass')
628
- passData.push(d);
629
- else if (d.cardKind === 'tripwire')
630
- tripwireData.push(d);
631
- else
632
- failData.push(d);
633
- }
634
- const passSection = passData.length > 0
635
- ? `<div class="tv-group-sep">${lang === 'zh' ? '通过用例' : 'Passed'}</div>
636
- <div class="tv-rows">${passData.map((d) => renderPassRow(d, lang)).join('')}</div>`
637
- : '';
638
- const failSection = failData.length > 0
639
- ? `<div class="tv-group-sep">${lang === 'zh' ? '失败用例' : 'Failed'}</div>
640
- ${failData.map((d) => renderFailCard(d, lang)).join('')}`
641
- : '';
642
- const tripwireSection = tripwireData.length > 0
643
- ? `<div class="tv-group-sep">${lang === 'zh' ? '诱错样本' : 'Tripwire samples'}</div>
644
- ${tripwireData.map((d) => renderFailCard(d, lang)).join('')}`
645
- : '';
646
- const variantStats = computeVariantStats(report, variant);
647
- const tripwireStat = variantStats.tripwire > 0
648
- ? `<span class="tv-stat tv-stat--tripwire"><span class="tv-stat-num">${variantStats.tripwire}</span>${lang === 'zh' ? '诱错触发' : 'tripwire'}</span>`
649
- : '';
650
- return `<div class="tv-variant-panel" data-variant="${e(variant)}" ${idx === 0 ? '' : 'hidden'}>
651
- <div class="tv-variant-stats">
652
- <span class="tv-stat tv-stat--pass"><span class="tv-stat-num">${variantStats.passed}</span>${lang === 'zh' ? '通过' : 'passed'}</span>
653
- <span class="tv-stat tv-stat--fail"><span class="tv-stat-num">${variantStats.failed}</span>${lang === 'zh' ? '失败' : 'failed'}</span>
654
- ${tripwireStat}
655
- <span class="tv-stat"><span class="tv-stat-num">${variantStats.avgScore.toFixed(1)}</span>${lang === 'zh' ? '平均分' : 'avg / 5'}</span>
656
- </div>
657
- ${failSection}
658
- ${tripwireSection}
659
- ${passSection}
660
- </div>`;
661
- }).join('');
662
- // 只 1 个 variant 时不显示切换器(SOLO 模式没对照,显示 tab 反而困惑)。
663
- const variantTabsHtml = variants.length > 1
664
- ? `<div class="tv-variant-tabs">${variants.map((variant, idx) => `<button type="button" class="tv-vtab ${idx === 0 ? 'tv-vtab--active' : ''}" data-target="${e(variant)}" onclick="omkSwitchVariant(this)">${e(variant)}</button>`).join('')}</div>`
852
+ // 单 variant:无对照,直接分组列用例。
853
+ if (variants.length === 1) {
854
+ return `<div class="tv-root">${fallbackBanner}${renderSingleVariantBody(report, variants[0], lang)}</div>`;
855
+ }
856
+ // 多 variant:主轮(最新)领头,各轮分数轨迹条 + 其他轮折叠,不再用 variant tabs。
857
+ const primaryName = variants[variants.length - 1];
858
+ const failCards = [];
859
+ const tripwireCards = [];
860
+ const passCards = [];
861
+ for (const r of results) {
862
+ const samplePrimary = pickPrimaryVariant(r.variants, variants) ?? primaryName;
863
+ const primary = r.variants[samplePrimary];
864
+ if (!primary)
865
+ continue;
866
+ const d = buildCardData(r.sample_id, snapshots[r.sample_id], primary);
867
+ const card = renderMultiSampleCard(r.sample_id, snapshots[r.sample_id], r.variants, variants, samplePrimary, lang);
868
+ if (d.cardKind === 'pass')
869
+ passCards.push(card);
870
+ else if (d.cardKind === 'tripwire')
871
+ tripwireCards.push(card);
872
+ else
873
+ failCards.push(card);
874
+ }
875
+ const failSection = failCards.length > 0
876
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '失败用例' : 'Failed'}</div>${failCards.join('')}`
877
+ : '';
878
+ const tripwireSection = tripwireCards.length > 0
879
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '诱错样本' : 'Tripwire samples'}</div>${tripwireCards.join('')}`
880
+ : '';
881
+ const passSection = passCards.length > 0
882
+ ? `<div class="tv-group-sep">${lang === 'zh' ? '通过用例' : 'Passed'}</div>${passCards.join('')}`
665
883
  : '';
666
- // 整个 test view 包在 tv-root 里,scoped 浅色岛(覆盖外层深色 layout)。
884
+ const stats = computeVariantStats(report, primaryName);
885
+ const primaryNote = `<span class="tv-primary-note">${lang === 'zh' ? `主轮 ${e(primaryName)}` : `Primary ${e(primaryName)}`}</span>`;
667
886
  return `<div class="tv-root">${fallbackBanner}
668
- ${variantTabsHtml}
669
- ${variantSections}</div>`;
887
+ <div class="tv-multi-hint">${lang === 'zh' ? '以主轮(最新)状态分组;展开看各轮分数轨迹与逐轮对比' : 'Grouped by primary (latest) status; expand for per-round score trajectory and comparison'}</div>
888
+ ${renderStatsBar(stats, lang, primaryNote)}
889
+ ${failSection}
890
+ ${tripwireSection}
891
+ ${passSection}</div>`;
670
892
  }
671
893
  function computeVariantStats(report, variant) {
672
894
  const snapshots = report.sampleSnapshots ?? {};
@@ -712,61 +934,78 @@ function computeVariantStats(report, variant) {
712
934
  * - **default-collapsed**:通过的用例只显一行;失败/诱错展开诊断,其余 section 折叠。
713
935
  */
714
936
  export const TEST_VIEW_CSS = `
715
- /* tv-root: 浅色岛,scoped 覆盖外层深色 layout */
937
+ /* tv-root: 白底卡片(与 hero / 结论 / fold 同一视觉语言),内部逐用例是 hairline 分隔的列表项。
938
+ 仍保留一套 scoped 变量给富内容(诊断 / 轨迹)用,色调对齐外层冷灰页面。 */
716
939
  .tv-root {
717
- --tv-bg:#fbfaf7; --tv-bg-card:#ffffff; --tv-bg-soft:rgba(58,58,58,.025);
718
- --tv-text:#3a3a3a; --tv-text-2:#7a7a7a; --tv-text-3:#a8a8a8;
719
- --tv-divider:#e8e3da;
720
- /* 加大 pass / fail / tripwire 的色相距离 + 暗度差,避免远看雷同。
721
- pass: 鼠尾草绿(暗一档,靠近林地);fail: 砖红土陶(暗一档,远离 pink);tripwire: 紫罗兰(独立色相)。 */
722
- --tv-pass:#5e8252; --tv-fail:#9c4a3f; --tv-tripwire:#7a6b89;
723
- --tv-pass-soft:rgba(94,130,82,.10); --tv-fail-soft:rgba(156,74,63,.10); --tv-tripwire-soft:rgba(122,107,137,.10);
724
- --tv-accent:#5a7a93; --tv-warn:#b08030;
725
- --tv-shadow:0 1px 3px rgba(58,58,58,.06);
726
- --tv-shadow-hover:0 2px 8px rgba(58,58,58,.10);
727
- background:var(--tv-bg); color:var(--tv-text);
728
- padding:32px 0; margin:-16px 0 0; border-radius:6px;
940
+ --tv-bg:#ffffff; --tv-bg-card:#ffffff; --tv-bg-soft:rgba(99,112,131,.05);
941
+ --tv-text:#2c333f; --tv-text-2:#637083; --tv-text-3:#9ca3af;
942
+ --tv-divider:#edf0f5;
943
+ /* pass / fail / tripwire 用形状 + 色相区分(色盲友好) */
944
+ --tv-pass:#1f9d63; --tv-fail:#dc2626; --tv-tripwire:#7c3aed;
945
+ --tv-pass-soft:rgba(31,157,99,.10); --tv-fail-soft:rgba(220,38,38,.10); --tv-tripwire-soft:rgba(124,58,237,.10);
946
+ --tv-accent:#4f46e5; --tv-warn:#d97706;
947
+ --tv-shadow:0 1px 3px rgba(0,0,0,.06);
948
+ --tv-shadow-hover:0 2px 8px rgba(0,0,0,.10);
949
+ background:#fff; color:var(--tv-text);
950
+ border:1px solid #e4e8f1; border-radius:12px; box-shadow:0 8px 24px rgba(31,41,55,.04);
951
+ padding:8px 16px 12px; margin:0;
729
952
  font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei","Segoe UI",sans-serif;
730
953
  font-size:15px; line-height:1.75;
731
954
  }
732
- /* Variant tabs — 显式 reset 浏览器默认 button 样式(border-radius / outline / appearance)
733
- 防止 layout 那边的全局 button 规则给底部加了奇怪圆角和深色背景。 */
734
- .tv-root .tv-variant-tabs { display:flex;gap:0;margin:0 0 24px;border-bottom:1px solid var(--tv-divider) }
735
- .tv-root .tv-vtab { padding:12px 20px;background:transparent;border:none;border-radius:0;cursor:pointer;color:var(--tv-text-2);border-bottom:2px solid transparent;font-size:14px;font-family:inherit;font-weight:500;outline:none;appearance:none;-webkit-appearance:none;margin-bottom:-1px }
736
- .tv-root .tv-vtab:hover { color:var(--tv-text) }
737
- .tv-root .tv-vtab--active { color:var(--tv-text);border-bottom-color:var(--tv-accent);font-weight:600 }
738
- .tv-root .tv-vtab:focus-visible { outline:2px solid var(--tv-accent);outline-offset:-4px;border-radius:4px }
739
- /* tv-variant-panel: 不打横向 padding,内容直接对齐 .tv-root 边界,跟外层 omk-view-panel 的 h1/subtitle 同基线。 */
955
+ /* 多 variant 提示 + 主轮标签 */
956
+ .tv-root .tv-multi-hint { font-size:13px;color:var(--tv-text-2);margin:0 0 16px;line-height:1.6 }
957
+ .tv-root .tv-primary-note { font-size:13px;color:var(--tv-text-3);font-family:"SF Mono",Menlo,monospace;margin-right:6px }
958
+ /* 各轮分数轨迹条 — round-0 3.86 › round-1 1.71 › round-2 1.59[最新],按分着色,主轮加框 */
959
+ .tv-root .tv-rounds { display:flex;flex-wrap:wrap;align-items:center;gap:6px 4px;margin:0 0 18px }
960
+ .tv-root .tv-round { display:inline-flex;align-items:center;gap:5px;font-size:13px;padding:5px 10px;border-radius:14px;background:var(--tv-bg-soft);color:var(--tv-text-2);border:1px solid transparent }
961
+ .tv-root .tv-round b { font-variant-numeric:tabular-nums;font-family:"SF Mono",Menlo,monospace;font-weight:700 }
962
+ .tv-root .tv-round--pass b { color:var(--tv-pass) }
963
+ .tv-root .tv-round--warn b { color:var(--tv-warn) }
964
+ .tv-root .tv-round--fail b { color:var(--tv-fail) }
965
+ .tv-root .tv-round--current { border-color:var(--tv-accent);background:rgba(79,70,229,.06);color:var(--tv-text);font-weight:600 }
966
+ .tv-root .tv-round--missing { color:var(--tv-text-3);font-style:italic }
967
+ .tv-root .tv-round-icon { font-size:11px;font-weight:700 }
968
+ .tv-root .tv-round-icon--pass { color:var(--tv-pass) }
969
+ .tv-root .tv-round-icon--fail { color:var(--tv-fail) }
970
+ .tv-root .tv-round-cur { font-size:11px;color:var(--tv-accent);font-weight:600;margin-left:2px }
971
+ .tv-root .tv-round-arrow { color:var(--tv-text-3);font-size:13px;margin:0 1px }
972
+ /* 其他轮详情块(折叠内) */
973
+ .tv-root .tv-round-detail { padding:14px 0;border-top:1px dashed var(--tv-divider) }
974
+ .tv-root .tv-round-detail:first-child { border-top:none;padding-top:4px }
975
+ .tv-root .tv-round-detail-h { font-size:14px;font-weight:600;color:var(--tv-text);margin-bottom:10px;display:flex;align-items:center;gap:8px }
976
+ .tv-root .tv-round-detail-icon { font-size:13px;font-weight:700 }
977
+ .tv-root .tv-round-detail-icon--pass { color:var(--tv-pass) }
978
+ .tv-root .tv-round-detail-icon--fail { color:var(--tv-fail) }
979
+ .tv-root .tv-round-detail-icon--tripwire { color:var(--tv-tripwire) }
980
+ .tv-root .tv-round-detail-score { font-family:"SF Mono",Menlo,monospace;font-weight:400;color:var(--tv-text-2);font-size:13px }
740
981
  /* Stats — 更扁平的数字+标签布局 */
741
- .tv-root .tv-variant-stats { display:flex;gap:32px;margin:0 0 24px;font-size:14px;color:var(--tv-text-2);align-items:baseline }
982
+ .tv-root .tv-variant-stats { display:flex;gap:32px;margin:6px 0 10px;padding:0 14px;font-size:14px;color:var(--tv-text-2);align-items:baseline }
742
983
  .tv-root .tv-stat { display:flex;align-items:baseline;gap:6px }
743
984
  .tv-root .tv-stat-num { color:var(--tv-text);font-weight:600;font-size:18px;font-variant-numeric:tabular-nums }
744
985
  .tv-root .tv-stat--pass .tv-stat-num { color:var(--tv-pass) }
745
986
  .tv-root .tv-stat--fail .tv-stat-num { color:var(--tv-fail) }
746
987
  .tv-root .tv-stat--tripwire .tv-stat-num { color:var(--tv-tripwire) }
747
- /* Group separator */
748
- .tv-root .tv-group-sep { display:flex;align-items:center;gap:14px;margin:32px 0 16px;color:var(--tv-text-3);font-size:12px;text-transform:uppercase;letter-spacing:0.08em;font-weight:500 }
988
+ /* Group separator — 卡片内分组小标题 */
989
+ .tv-root .tv-group-sep { display:flex;align-items:center;gap:12px;margin:18px 0 4px;padding:0 14px;color:var(--tv-text-3);font-size:11.5px;text-transform:uppercase;letter-spacing:0.08em;font-weight:600 }
749
990
  .tv-root .tv-group-sep::after { content:'';flex:1;height:1px;background:var(--tv-divider) }
750
- /* Pass rows — one-line list,左边一道淡 sage 色条做"通过"暗示 */
751
- .tv-root .tv-rows { display:flex;flex-direction:column;gap:6px;margin:0 }
752
- .tv-root .tv-row { background:var(--tv-bg-card);border-radius:6px;box-shadow:var(--tv-shadow);transition:box-shadow .15s;border-left:3px solid var(--tv-pass) }
753
- .tv-root .tv-row:hover { box-shadow:var(--tv-shadow-hover) }
754
- .tv-root .tv-row[open] { box-shadow:var(--tv-shadow-hover) }
755
- .tv-root .tv-row-h { display:flex;align-items:center;gap:14px;padding:14px 20px;cursor:pointer;list-style:none;font-size:15px }
756
- .tv-root .tv-row-h::-webkit-details-marker { display:none }
757
- .tv-root .tv-row-h::after { content:'›';margin-left:8px;color:var(--tv-text-3);font-size:18px;transition:transform .15s }
758
- .tv-root .tv-row[open] .tv-row-h::after { transform:rotate(90deg) }
759
- /* Cards (fail / tripwire) — 默认折叠,跟 pass row 一致;头部下面加一行 preview 让折叠态也有信息密度 */
760
- .tv-root .tv-card { background:var(--tv-bg-card);border-radius:8px;box-shadow:var(--tv-shadow);margin:0 0 12px;transition:box-shadow .15s }
761
- .tv-root .tv-card:hover { box-shadow:var(--tv-shadow-hover) }
762
- .tv-root .tv-card--fail { border-left:3px solid var(--tv-fail) }
763
- .tv-root .tv-card--tripwire { border-left:3px solid var(--tv-tripwire) }
764
- .tv-root .tv-card-h { display:flex;align-items:center;gap:14px;padding:16px 22px;cursor:pointer;list-style:none;font-size:15px }
765
- .tv-root .tv-card-h::-webkit-details-marker { display:none }
766
- .tv-root .tv-card-h::after { content:'›';margin-left:8px;color:var(--tv-text-3);font-size:18px;transition:transform .15s }
767
- .tv-root .tv-card[open] .tv-card-h::after { transform:rotate(90deg) }
991
+ /* 列表项 — pass row 与 fail/tripwire card 统一成 hairline 分隔的行,无阴影/无左色条;
992
+ 状态靠左侧状态点,hover/展开时整行淡底高亮。 */
993
+ .tv-root .tv-rows { display:flex;flex-direction:column;margin:0 }
994
+ .tv-root .tv-row, .tv-root .tv-card { background:transparent;border:none;border-bottom:1px solid var(--tv-divider);border-radius:8px;box-shadow:none;margin:0;transition:background .12s }
995
+ .tv-root .tv-row:hover, .tv-root .tv-card:hover { background:var(--tv-bg-soft) }
996
+ .tv-root .tv-row[open], .tv-root .tv-card[open] { background:var(--tv-bg-soft);border-bottom-color:transparent }
997
+ .tv-root .tv-rows .tv-row:last-child, .tv-root .tv-card:last-child { border-bottom:none }
998
+ /* 聚焦:展开某条用例时,其余收起的行淡化,突出当前;hover 被淡化的行可临时恢复。 */
999
+ .tv-root:has(.tv-card[open], .tv-row[open]) .tv-card:not([open]),
1000
+ .tv-root:has(.tv-card[open], .tv-row[open]) .tv-row:not([open]) { opacity:.42;transition:opacity .15s }
1001
+ .tv-root:has(.tv-card[open], .tv-row[open]) .tv-card:not([open]):hover,
1002
+ .tv-root:has(.tv-card[open], .tv-row[open]) .tv-row:not([open]):hover { opacity:1 }
1003
+ .tv-root .tv-row-h, .tv-root .tv-card-h { display:flex;align-items:center;gap:13px;padding:14px;cursor:pointer;list-style:none;font-size:14.5px }
1004
+ .tv-root .tv-row-h::-webkit-details-marker, .tv-root .tv-card-h::-webkit-details-marker { display:none }
1005
+ .tv-root .tv-row-h::after, .tv-root .tv-card-h::after { content:'›';margin-left:4px;color:var(--tv-text-3);font-size:17px;transition:transform .15s }
1006
+ .tv-root .tv-row[open] .tv-row-h::after, .tv-root .tv-card[open] .tv-card-h::after { transform:rotate(90deg) }
768
1007
  .tv-root .tv-card[open] .tv-card-h { border-bottom:1px solid var(--tv-divider) }
769
- .tv-root .tv-card-preview { padding:0 22px 14px;color:var(--tv-text-2);font-size:14px;line-height:1.7;border-bottom:1px solid var(--tv-divider) }
1008
+ .tv-root .tv-card-preview { padding:0 14px 13px;color:var(--tv-text-2);font-size:13.5px;line-height:1.6 }
770
1009
  .tv-root .tv-card[open] .tv-card-preview { display:none }
771
1010
  /* Status dots — 用形状 + 色 + 字符的三重区分,避免色盲 / 远看雷同。
772
1011
  pass: 实心圆 sage;fail: 红 ✗ 字符(更显眼);tripwire: 紫色 ◆ 菱形。 */
@@ -777,16 +1016,41 @@ export const TEST_VIEW_CSS = `
777
1016
  .tv-root .tv-id { font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;font-size:13px;color:var(--tv-text-3) }
778
1017
  .tv-root .tv-title { flex:1;font-size:15px;color:var(--tv-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap }
779
1018
  .tv-root .tv-meta { font-size:13px;color:var(--tv-text-3);font-variant-numeric:tabular-nums;font-family:"SF Mono","JetBrains Mono",Menlo,monospace }
1019
+ /* Composite badge in header — 综合分,卡头右侧紧贴耗时,一眼看分高低 */
1020
+ .tv-root .tv-composite { flex-shrink:0;font-size:16px;font-weight:700;font-variant-numeric:tabular-nums;font-family:"SF Mono","JetBrains Mono",Menlo,monospace;letter-spacing:-0.02em }
1021
+ .tv-root .tv-composite--pass { color:var(--tv-pass) }
1022
+ .tv-root .tv-composite--warn { color:var(--tv-warn) }
1023
+ .tv-root .tv-composite--fail { color:var(--tv-fail) }
1024
+ .tv-root .tv-composite-unit { font-size:11px;color:var(--tv-text-3);font-weight:400;margin-left:1px }
780
1025
  /* Body padding */
781
- .tv-root .tv-card-body, .tv-root .tv-row-body { padding:22px }
1026
+ .tv-root .tv-card-body, .tv-root .tv-row-body { padding:4px 14px 20px }
1027
+ /* Score strip — body 顶部分布条:综合 + 三层 + 断言,浅底 chip 行 */
1028
+ .tv-root .tv-scores { display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 20px;margin:0 0 18px;padding:12px 16px;background:var(--tv-bg-soft);border-radius:6px;font-size:13.5px }
1029
+ .tv-root .tv-score-item { color:var(--tv-text-2);display:inline-flex;align-items:baseline;gap:5px }
1030
+ .tv-root .tv-score-item--primary { color:var(--tv-text);font-weight:600 }
1031
+ .tv-root .tv-score-v { font-weight:700;font-variant-numeric:tabular-nums;font-family:"SF Mono","JetBrains Mono",Menlo,monospace }
1032
+ .tv-root .tv-score-v--pass { color:var(--tv-pass) }
1033
+ .tv-root .tv-score-v--warn { color:var(--tv-warn) }
1034
+ .tv-root .tv-score-v--fail { color:var(--tv-fail) }
1035
+ /* Judge 评价文本块 — 与 diagnostic 并列,judge 答"打几分 + 为什么" */
1036
+ .tv-root .tv-judge { margin:0 0 18px }
1037
+ .tv-root .tv-judge-h { font-size:13px;font-weight:600;color:var(--tv-text-2);margin-bottom:6px;letter-spacing:0.02em;display:flex;align-items:center;gap:8px;flex-wrap:wrap }
1038
+ .tv-root .tv-judge-text { font-size:14px;line-height:1.75;color:var(--tv-text);white-space:pre-wrap;word-break:break-word }
1039
+ .tv-root .tv-judge-stab { font-size:12px;color:var(--tv-text-3);font-weight:500;font-family:"SF Mono",Menlo,monospace;background:var(--tv-bg-soft);padding:1px 6px;border-radius:3px }
1040
+ .tv-root .tv-judge-stab--fail { color:var(--tv-fail);background:var(--tv-fail-soft) }
1041
+ /* 多评委 ensemble 分项 */
1042
+ .tv-root .tv-ensemble { margin-top:10px }
1043
+ .tv-root .tv-ensemble-row { font-size:13.5px;margin-top:6px;color:var(--tv-text-2) }
1044
+ .tv-root .tv-ensemble-row b { color:var(--tv-text);font-weight:600 }
1045
+ .tv-root .tv-ensemble-agree { font-size:12px;color:var(--tv-text-3);margin-top:8px }
782
1046
  /* Tripwire banner — 卡片顶部的紫色解释带 */
783
- .tv-root .tv-tripwire-banner { margin:-22px -22px 20px;padding:18px 22px;background:rgba(139,124,147,.08);border-radius:8px 8px 0 0;border-bottom:1px solid var(--tv-divider) }
1047
+ .tv-root .tv-tripwire-banner { margin:0 0 18px;padding:14px 16px;background:var(--tv-tripwire-soft);border-radius:8px }
784
1048
  .tv-root .tv-tripwire-banner-title { font-size:14px;font-weight:600;color:var(--tv-tripwire);margin-bottom:6px;letter-spacing:0.02em }
785
1049
  .tv-root .tv-tripwire-banner-body { font-size:14px;color:var(--tv-text-2);line-height:1.7 }
786
1050
  /* Diagnosis — 一段白话 + 单一蓝色建议块 + 折叠细节 */
787
1051
  .tv-root .tv-diag { margin:4px 0 16px }
788
1052
  .tv-root .tv-diag-text { font-size:15px;line-height:1.75;color:var(--tv-text);margin:0 0 16px }
789
- .tv-root .tv-suggestion { background:rgba(90,122,147,.06);border-left:3px solid var(--tv-accent);padding:14px 18px;margin:12px 0;border-radius:0 4px 4px 0 }
1053
+ .tv-root .tv-suggestion { background:rgba(79,70,229,.06);border-left:3px solid var(--tv-accent);padding:14px 18px;margin:12px 0;border-radius:0 4px 4px 0 }
790
1054
  .tv-root .tv-suggestion-label { display:inline-block;color:var(--tv-accent);font-weight:600;font-size:13px;margin-right:8px;letter-spacing:0.02em }
791
1055
  .tv-root .tv-suggestion-body { margin:6px 0 0;font-size:14.5px;line-height:1.7;color:var(--tv-text) }
792
1056
  .tv-root .tv-diag-details { margin-top:14px }
@@ -800,18 +1064,18 @@ export const TEST_VIEW_CSS = `
800
1064
  /* RootCause inline pill */
801
1065
  .tv-root .tv-diag-rc-pill { font-size:11px;padding:2px 8px;border-radius:10px;background:var(--tv-bg-soft);color:var(--tv-text-2);margin-right:6px;display:inline-block }
802
1066
  .tv-root .tv-diag-rc--tripwire_intentional { background:rgba(139,124,147,.12);color:var(--tv-tripwire) }
803
- .tv-root .tv-diag-rc--sample_design { background:rgba(90,122,147,.10);color:var(--tv-accent) }
804
- .tv-root .tv-diag-rc--skill_doc_unclear, .tv-diag-rc--skill_doc_missing, .tv-root .tv-diag-rc--llm_misread { background:rgba(176,128,48,.10);color:var(--tv-warn) }
1067
+ .tv-root .tv-diag-rc--sample_design { background:rgba(79,70,229,.10);color:var(--tv-accent) }
1068
+ .tv-root .tv-diag-rc--skill_doc_unclear, .tv-diag-rc--skill_doc_missing, .tv-root .tv-diag-rc--llm_misread { background:rgba(217,119,6,.10);color:var(--tv-warn) }
805
1069
  /* Failure-mode chips — 中文标签,显著展示在 summary 下 */
806
1070
  .tv-root .tv-diag-modes { display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:8px 0 14px }
807
1071
  .tv-root .tv-diag-modes-label { font-size:12px;color:var(--tv-text-2);margin-right:4px }
808
1072
  .tv-root .tv-diag-mode-pill { font-size:12px;padding:3px 10px;border-radius:11px;background:var(--tv-bg-soft);color:var(--tv-text);font-weight:500 }
809
- .tv-root .tv-diag-mode--workflow-skip { background:rgba(176,128,48,.12);color:var(--tv-warn) }
810
- .tv-root .tv-diag-mode--hardcoded { background:rgba(176,128,48,.12);color:var(--tv-warn) }
1073
+ .tv-root .tv-diag-mode--workflow-skip { background:rgba(217,119,6,.12);color:var(--tv-warn) }
1074
+ .tv-root .tv-diag-mode--hardcoded { background:rgba(217,119,6,.12);color:var(--tv-warn) }
811
1075
  .tv-root .tv-diag-mode--hallucination { background:rgba(180,75,75,.14);color:#b44b4b }
812
- .tv-root .tv-diag-mode--tool-misuse { background:rgba(176,128,48,.12);color:var(--tv-warn) }
813
- .tv-root .tv-diag-mode--env-blocked { background:rgba(90,122,147,.12);color:var(--tv-accent) }
814
- .tv-root .tv-diag-mode--misread { background:rgba(176,128,48,.12);color:var(--tv-warn) }
1076
+ .tv-root .tv-diag-mode--tool-misuse { background:rgba(217,119,6,.12);color:var(--tv-warn) }
1077
+ .tv-root .tv-diag-mode--env-blocked { background:rgba(79,70,229,.12);color:var(--tv-accent) }
1078
+ .tv-root .tv-diag-mode--misread { background:rgba(217,119,6,.12);color:var(--tv-warn) }
815
1079
  .tv-root .tv-diag-mode--other { background:var(--tv-bg-soft);color:var(--tv-text-2) }
816
1080
  /* Workflow checklist — 默认展开,放在 suggestion 之上,直观展示 LLM 走对哪几步 */
817
1081
  .tv-root .tv-diag-workflow { margin:14px 0;padding:12px 14px;background:var(--tv-bg-soft);border-radius:6px }
@@ -858,14 +1122,14 @@ export const TEST_VIEW_CSS = `
858
1122
  .tv-root .tv-turn-dur { color:var(--tv-text-3);font-weight:400;font-size:12px;margin-left:6px }
859
1123
  .tv-root .tv-thinking { margin:6px 0;font-size:13px }
860
1124
  .tv-root .tv-thinking summary { cursor:pointer;color:var(--tv-text-3);font-style:italic }
861
- .tv-root .tv-thinking-body { padding:10px 14px;background:rgba(58,58,58,.04);border-radius:4px;color:var(--tv-text-2);white-space:pre-wrap;font-size:13px;line-height:1.7;margin-top:6px }
1125
+ .tv-root .tv-thinking-body { padding:10px 14px;background:rgba(0,0,0,.04);border-radius:4px;color:var(--tv-text-2);white-space:pre-wrap;font-size:13px;line-height:1.7;margin-top:6px }
862
1126
  /* Tool calls */
863
- .tv-root .tv-call { margin:6px 0;padding:10px 14px;background:rgba(58,58,58,.03);border-radius:4px;font-size:13px }
1127
+ .tv-root .tv-call { margin:6px 0;padding:10px 14px;background:rgba(0,0,0,.03);border-radius:4px;font-size:13px }
864
1128
  .tv-root .tv-call--err { border-left:2px solid var(--tv-fail) }
865
1129
  .tv-root .tv-call-h { font-weight:500;color:var(--tv-text);margin-bottom:4px;font-size:14px }
866
1130
  .tv-root .tv-call details { margin:4px 0;font-size:12px }
867
1131
  .tv-root .tv-call summary { cursor:pointer;color:var(--tv-text-3);padding:2px 0 }
868
- .tv-root .tv-pre { font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;font-size:13px;background:rgba(58,58,58,.04);padding:10px 14px;border-radius:4px;overflow:auto;max-height:320px;white-space:pre-wrap;margin:4px 0;color:var(--tv-text-2);line-height:1.55 }
1132
+ .tv-root .tv-pre { font-family:"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;font-size:13px;background:rgba(0,0,0,.04);padding:10px 14px;border-radius:4px;overflow:auto;max-height:320px;white-space:pre-wrap;margin:4px 0;color:var(--tv-text-2);line-height:1.55 }
869
1133
  .tv-root .tv-mock-badge { font-size:12px;color:var(--tv-pass);background:rgba(125,155,118,.12);padding:2px 8px;border-radius:10px;margin-left:8px }
870
1134
  .tv-root .tv-trace-assert { font-size:13px;font-family:"SF Mono",Menlo,monospace;padding:3px 0;margin-top:6px;line-height:1.5 }
871
1135
  .tv-root .tv-trace-assert.tv-assert--fail { color:var(--tv-fail) }
@@ -877,28 +1141,15 @@ export const TEST_VIEW_CSS = `
877
1141
  .tv-root .tv-assert { font-size:14px;font-family:"SF Mono",Menlo,monospace;padding:5px 0;line-height:1.6 }
878
1142
  .tv-root .tv-assert--pass { color:var(--tv-text-2) }
879
1143
  .tv-root .tv-assert--fail { color:var(--tv-fail) }
880
- .tv-root .tv-assert-value { background:rgba(58,58,58,.04);padding:2px 6px;border-radius:3px;margin:0 2px }
1144
+ .tv-root .tv-assert-value { background:rgba(0,0,0,.04);padding:2px 6px;border-radius:3px;margin:0 2px }
881
1145
  .tv-root .tv-turn-tag { font-size:12px;color:var(--tv-text-3);margin-left:8px;font-style:italic;font-family:-apple-system,sans-serif }
882
1146
  .tv-root .tv-turn-tag--ok { color:var(--tv-pass) }
883
1147
  .tv-root .tv-turn-tag--miss { color:var(--tv-fail) }
884
1148
  .tv-root .tv-assert-msg { font-size:13px;color:var(--tv-text-3);margin-left:20px;margin-top:3px }
885
1149
  /* Empty/warn states */
886
1150
  .tv-root .tv-empty, .tv-root .tv-empty-page { color:var(--tv-text-3);font-style:italic;padding:14px 0;font-size:14px }
887
- .tv-root .tv-warn { color:var(--tv-warn);background:rgba(176,128,48,.08);padding:14px 18px;border-radius:4px;margin:8px 24px;font-size:14px;line-height:1.7 }
888
- `;
889
- /** 单测视角配套 JS:variant 切换 + view tab 切换。挂在 layout 末尾。 */
890
- export const TEST_VIEW_JS = `
891
- function omkSwitchVariant(btn) {
892
- var target = btn.getAttribute('data-target');
893
- var tabs = btn.parentElement.querySelectorAll('.tv-vtab');
894
- tabs.forEach(function(t){ t.classList.toggle('tv-vtab--active', t === btn); });
895
- var panels = btn.parentElement.parentElement.querySelectorAll('.tv-variant-panel');
896
- panels.forEach(function(p){ p.hidden = (p.getAttribute('data-variant') !== target); });
897
- }
898
- function omkSwitchView(name) {
899
- var tabs = document.querySelectorAll('.omk-view-tab');
900
- tabs.forEach(function(t){ t.classList.toggle('omk-view-tab--active', t.getAttribute('data-view') === name); });
901
- var panels = document.querySelectorAll('.omk-view-panel');
902
- panels.forEach(function(p){ p.hidden = (p.getAttribute('data-view') !== name); });
903
- }
1151
+ .tv-root .tv-warn { color:var(--tv-warn);background:rgba(217,119,6,.08);padding:14px 18px;border-radius:4px;margin:8px 24px;font-size:14px;line-height:1.7 }
904
1152
  `;
1153
+ /** 融合单页全部用原生 <details> 折叠,逐用例与各轮对比都不再需要 JS 切换。
1154
+ * 保留导出(html-renderer 仍引用),内容置空。 */
1155
+ export const TEST_VIEW_JS = '';