oh-my-knowledge 0.48.0 → 0.49.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 (243) hide show
  1. package/README.md +50 -18
  2. package/README.zh.md +55 -23
  3. package/dist/analysis/coverage-analyzer.d.ts +1 -0
  4. package/dist/analysis/coverage-analyzer.js +125 -62
  5. package/dist/analysis/failure-clusterer.js +2 -1
  6. package/dist/analysis/gap-analyzer.d.ts +2 -2
  7. package/dist/analysis/gap-analyzer.js +13 -3
  8. package/dist/analysis/hedging-classifier.d.ts +2 -2
  9. package/dist/analysis/hedging-classifier.js +3 -4
  10. package/dist/analysis/report-diagnostics.js +9 -7
  11. package/dist/analysis/sample-diagnostics.js +6 -6
  12. package/dist/artifact-graph/doctor.js +15 -7
  13. package/dist/assets/agent-skills/omk/SKILL.md +27 -7
  14. package/dist/assets/agent-skills/omk/references/commands.md +18 -17
  15. package/dist/authoring/evolver.d.ts +10 -6
  16. package/dist/authoring/evolver.js +496 -83
  17. package/dist/authoring/generator.d.ts +3 -3
  18. package/dist/authoring/generator.js +5 -10
  19. package/dist/authoring/sample-fixer.d.ts +8 -6
  20. package/dist/authoring/sample-fixer.js +76 -5
  21. package/dist/cli/commands/doctor.js +31 -14
  22. package/dist/cli/commands/eval/index.d.ts +3 -0
  23. package/dist/cli/commands/eval/index.js +163 -17
  24. package/dist/cli/commands/evolve.d.ts +4 -4
  25. package/dist/cli/commands/evolve.js +27 -13
  26. package/dist/cli/commands/init.js +16 -3
  27. package/dist/cli/commands/observe/inbox.js +28 -21
  28. package/dist/cli/commands/observe/index.js +20 -11
  29. package/dist/cli/commands/observe/ingest.d.ts +3 -0
  30. package/dist/cli/commands/observe/ingest.js +30 -2
  31. package/dist/cli/commands/sample.d.ts +6 -3
  32. package/dist/cli/commands/sample.js +72 -68
  33. package/dist/cli/lib/codex-model-hint.d.ts +9 -0
  34. package/dist/cli/lib/codex-model-hint.js +45 -0
  35. package/dist/cli/lib/generation-failure-hint.d.ts +2 -0
  36. package/dist/cli/lib/generation-failure-hint.js +61 -0
  37. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  38. package/dist/cli/lib/i18n-dict/common.js +4 -0
  39. package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
  40. package/dist/cli/lib/i18n-dict/gen.js +38 -6
  41. package/dist/cli/lib/i18n-dict/help.js +6 -6
  42. package/dist/cli/lib/i18n-dict/init.d.ts +1 -1
  43. package/dist/cli/lib/i18n-dict/init.js +13 -9
  44. package/dist/cli/lib/i18n-dict/run.d.ts +1 -1
  45. package/dist/cli/lib/i18n-dict/run.js +34 -2
  46. package/dist/cli/lib/llm-failure-classifier.d.ts +2 -0
  47. package/dist/cli/lib/llm-failure-classifier.js +8 -0
  48. package/dist/cli/lib/parse-run-config.d.ts +6 -5
  49. package/dist/cli/lib/parse-run-config.js +16 -9
  50. package/dist/cli/lib/runtime-defaults.d.ts +21 -0
  51. package/dist/cli/lib/runtime-defaults.js +79 -0
  52. package/dist/diagnosis/observe-mapper.js +14 -15
  53. package/dist/diagnosis/observe-producer.js +3 -1
  54. package/dist/diagnosis/studio-projection.js +14 -7
  55. package/dist/diagnosis/types.d.ts +2 -0
  56. package/dist/diagnosis/types.js +12 -0
  57. package/dist/doctor/endpoint-rule.js +2 -1
  58. package/dist/eval-core/artifact-file-names.js +18 -1
  59. package/dist/eval-core/artifact-index.d.ts +7 -11
  60. package/dist/eval-core/artifact-index.js +139 -80
  61. package/dist/eval-core/cache.d.ts +12 -3
  62. package/dist/eval-core/cache.js +89 -29
  63. package/dist/eval-core/comparability.js +10 -6
  64. package/dist/eval-core/evaluation-execution.d.ts +2 -1
  65. package/dist/eval-core/evaluation-execution.js +122 -37
  66. package/dist/eval-core/evaluation-job.d.ts +4 -1
  67. package/dist/eval-core/evaluation-job.js +4 -1
  68. package/dist/eval-core/evaluation-reporting.d.ts +15 -13
  69. package/dist/eval-core/evaluation-reporting.js +54 -52
  70. package/dist/eval-core/execution-strategy.d.ts +2 -0
  71. package/dist/eval-core/execution-strategy.js +11 -9
  72. package/dist/eval-core/fact-checker.js +15 -7
  73. package/dist/eval-core/holdout.js +3 -2
  74. package/dist/eval-core/judge-independence.d.ts +2 -2
  75. package/dist/eval-core/mock-hook.cjs +23 -6
  76. package/dist/eval-core/mocks-runtime.js +30 -8
  77. package/dist/eval-core/report-document.d.ts +12 -0
  78. package/dist/eval-core/report-document.js +1151 -0
  79. package/dist/eval-core/report-extensions.d.ts +4 -0
  80. package/dist/eval-core/report-extensions.js +500 -0
  81. package/dist/eval-core/report-file-migration.js +7 -2
  82. package/dist/eval-core/resume-compatibility.d.ts +31 -0
  83. package/dist/eval-core/resume-compatibility.js +141 -0
  84. package/dist/eval-core/sample-fingerprint.d.ts +12 -0
  85. package/dist/eval-core/sample-fingerprint.js +193 -0
  86. package/dist/eval-core/schema.js +86 -31
  87. package/dist/eval-core/verdict.d.ts +8 -4
  88. package/dist/eval-core/verdict.js +24 -10
  89. package/dist/eval-workflows/batch-evaluation-workflow.d.ts +2 -1
  90. package/dist/eval-workflows/batch-evaluation-workflow.js +25 -12
  91. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.d.ts +10 -5
  92. package/dist/eval-workflows/evaluation-pipeline/preflight-warnings.js +58 -21
  93. package/dist/eval-workflows/evaluation-pipeline/report-finalize.js +3 -1
  94. package/dist/eval-workflows/evaluation-pipeline/run-state.d.ts +4 -1
  95. package/dist/eval-workflows/evaluation-pipeline/run-state.js +4 -1
  96. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.d.ts +6 -5
  97. package/dist/eval-workflows/evaluation-pipeline/test-set-hash.js +17 -10
  98. package/dist/eval-workflows/evaluation-pipeline.js +12 -7
  99. package/dist/eval-workflows/run-evaluation.d.ts +9 -7
  100. package/dist/eval-workflows/run-evaluation.js +79 -51
  101. package/dist/executors/anthropic-api.js +65 -9
  102. package/dist/executors/claude-cli.js +16 -79
  103. package/dist/executors/claude-protocol.d.ts +28 -0
  104. package/dist/executors/claude-protocol.js +180 -0
  105. package/dist/executors/claude-sdk-trace.js +56 -28
  106. package/dist/executors/claude-sdk.d.ts +1 -0
  107. package/dist/executors/claude-sdk.js +39 -93
  108. package/dist/executors/codex-cli-trace.js +166 -31
  109. package/dist/executors/codex-cli.d.ts +6 -8
  110. package/dist/executors/codex-cli.js +49 -151
  111. package/dist/executors/codex-protocol.d.ts +24 -0
  112. package/dist/executors/codex-protocol.js +234 -0
  113. package/dist/executors/codex-sdk.js +68 -120
  114. package/dist/executors/gemini.js +88 -13
  115. package/dist/executors/index.d.ts +2 -3
  116. package/dist/executors/index.js +5 -3
  117. package/dist/executors/openai-api.js +70 -9
  118. package/dist/executors/runtime-fingerprint.js +88 -11
  119. package/dist/executors/script-command.d.ts +8 -0
  120. package/dist/executors/script-command.js +87 -0
  121. package/dist/executors/script.js +202 -29
  122. package/dist/executors/shared.d.ts +35 -3
  123. package/dist/executors/shared.js +113 -15
  124. package/dist/grading/assertions.d.ts +1 -1
  125. package/dist/grading/assertions.js +19 -9
  126. package/dist/grading/diagnostic.d.ts +9 -2
  127. package/dist/grading/diagnostic.js +25 -2
  128. package/dist/grading/index.js +10 -4
  129. package/dist/grading/judge.js +19 -6
  130. package/dist/grading/layered-scores.d.ts +2 -3
  131. package/dist/grading/layered-scores.js +2 -3
  132. package/dist/inputs/load-samples.d.ts +1 -2
  133. package/dist/inputs/load-samples.js +23 -1
  134. package/dist/inputs/mcp-resolver.js +6 -3
  135. package/dist/inputs/sample-document.d.ts +11 -0
  136. package/dist/inputs/sample-document.js +96 -0
  137. package/dist/managed/evidence.d.ts +1 -0
  138. package/dist/managed/evidence.js +1 -1
  139. package/dist/managed/store.js +200 -91
  140. package/dist/observability/codex-trace-adapter.d.ts +5 -0
  141. package/dist/observability/codex-trace-adapter.js +850 -0
  142. package/dist/observability/experience.d.ts +32 -6
  143. package/dist/observability/experience.js +2695 -459
  144. package/dist/observability/feedback-matchers.js +16 -1
  145. package/dist/observability/inbox-view-model.d.ts +1 -1
  146. package/dist/observability/inbox-view-model.js +19 -14
  147. package/dist/observability/inbox.d.ts +7 -1
  148. package/dist/observability/inbox.js +632 -124
  149. package/dist/observability/problem-patterns.js +2 -0
  150. package/dist/observability/review-state.d.ts +6 -0
  151. package/dist/observability/review-state.js +235 -63
  152. package/dist/observability/skill-chain-advisories.js +1 -1
  153. package/dist/observability/skill-chain.js +17 -4
  154. package/dist/observability/skill-health-analyzer.d.ts +32 -7
  155. package/dist/observability/skill-health-analyzer.js +194 -121
  156. package/dist/observability/skill-health-report.d.ts +10 -0
  157. package/dist/observability/skill-health-report.js +620 -0
  158. package/dist/observability/soft-standards/constants.d.ts +0 -1
  159. package/dist/observability/soft-standards/constants.js +0 -1
  160. package/dist/observability/soft-standards/index.d.ts +1 -1
  161. package/dist/observability/soft-standards/index.js +1 -1
  162. package/dist/observability/soft-standards/llm-extractor.js +8 -10
  163. package/dist/observability/soft-standards/skill-standards-store.d.ts +2 -1
  164. package/dist/observability/soft-standards/skill-standards-store.js +59 -18
  165. package/dist/observability/soft-standards/types.d.ts +2 -2
  166. package/dist/observability/trace-adapter.d.ts +12 -7
  167. package/dist/observability/trace-adapter.js +11 -9
  168. package/dist/observability/trace-attribution.d.ts +13 -5
  169. package/dist/observability/trace-attribution.js +315 -21
  170. package/dist/observability/trace-ingestion.d.ts +9 -0
  171. package/dist/observability/trace-ingestion.js +80 -0
  172. package/dist/observability/trace-ir.d.ts +113 -0
  173. package/dist/observability/trace-ir.js +87 -0
  174. package/dist/observability/trace-segmenter.d.ts +19 -6
  175. package/dist/observability/trace-segmenter.js +377 -196
  176. package/dist/observability/trace-session-index.d.ts +19 -0
  177. package/dist/observability/trace-session-index.js +68 -0
  178. package/dist/observability/trace-source.d.ts +12 -4
  179. package/dist/observability/trace-source.js +939 -215
  180. package/dist/renderer/html-renderer.js +37 -6
  181. package/dist/renderer/icons.js +3 -0
  182. package/dist/renderer/observation-inbox-renderer.js +208 -90
  183. package/dist/renderer/skill-detail-renderer.js +452 -109
  184. package/dist/renderer/skill-health-renderer.js +69 -12
  185. package/dist/renderer/summary.js +28 -7
  186. package/dist/renderer/table.js +21 -4
  187. package/dist/renderer/test-view.d.ts +1 -0
  188. package/dist/renderer/test-view.js +44 -9
  189. package/dist/server/indexed-report-store.js +14 -18
  190. package/dist/server/job-store.js +64 -26
  191. package/dist/server/report-server.js +190 -78
  192. package/dist/server/report-store.js +57 -80
  193. package/dist/server/skill-index.js +143 -49
  194. package/dist/server/skill-insights.js +44 -5
  195. package/dist/shared/artifact-graph.d.ts +3 -0
  196. package/dist/shared/artifact-graph.js +224 -0
  197. package/dist/shared/assertion-types.d.ts +8 -0
  198. package/dist/shared/assertion-types.js +46 -0
  199. package/dist/shared/atomic-json.d.ts +8 -0
  200. package/dist/shared/atomic-json.js +33 -0
  201. package/dist/shared/diagnosis-schema.d.ts +9 -0
  202. package/dist/shared/diagnosis-schema.js +181 -0
  203. package/dist/shared/doctor-report.d.ts +3 -0
  204. package/dist/shared/doctor-report.js +103 -0
  205. package/dist/shared/evaluation-job.d.ts +6 -0
  206. package/dist/shared/evaluation-job.js +217 -0
  207. package/dist/shared/executor-result.d.ts +17 -0
  208. package/dist/shared/executor-result.js +221 -0
  209. package/dist/shared/file-lock.d.ts +12 -0
  210. package/dist/shared/file-lock.js +129 -0
  211. package/dist/shared/json-value.d.ts +5 -0
  212. package/dist/shared/json-value.js +36 -0
  213. package/dist/shared/keyed-mutex.d.ts +7 -0
  214. package/dist/shared/keyed-mutex.js +24 -0
  215. package/dist/shared/record-count.d.ts +8 -0
  216. package/dist/shared/record-count.js +43 -0
  217. package/dist/shared/sample-contract.d.ts +3 -0
  218. package/dist/shared/sample-contract.js +332 -0
  219. package/dist/shared/timestamp.d.ts +6 -0
  220. package/dist/shared/timestamp.js +64 -0
  221. package/dist/shared/token-usage.d.ts +19 -0
  222. package/dist/shared/token-usage.js +50 -0
  223. package/dist/shared/tool-call-status.d.ts +8 -0
  224. package/dist/shared/tool-call-status.js +28 -0
  225. package/dist/shared/tool-identity.d.ts +21 -0
  226. package/dist/shared/tool-identity.js +84 -0
  227. package/dist/shared/tool-search.js +73 -16
  228. package/dist/shared/trace-projection.d.ts +5 -0
  229. package/dist/shared/trace-projection.js +20 -0
  230. package/dist/shared/trace-source-kind.d.ts +3 -0
  231. package/dist/shared/trace-source-kind.js +12 -0
  232. package/dist/types/diagnosis.d.ts +2 -0
  233. package/dist/types/eval.d.ts +4 -0
  234. package/dist/types/executor.d.ts +32 -5
  235. package/dist/types/index.d.ts +1 -0
  236. package/dist/types/index.js +1 -0
  237. package/dist/types/judge.d.ts +2 -0
  238. package/dist/types/observability.d.ts +116 -9
  239. package/dist/types/report.d.ts +58 -6
  240. package/dist/types/skill-index.d.ts +7 -0
  241. package/dist/types/trace.d.ts +2 -0
  242. package/dist/types/trace.js +1 -0
  243. package/package.json +9 -5
@@ -12,6 +12,7 @@
12
12
  * 隐藏 modal:每条 insight 一个 + 3 个阶段全量明细各一个
13
13
  */
14
14
  import { layout, e, DEFAULT_LANG } from './layout.js';
15
+ import { icon } from './icons.js';
15
16
  // v6 之前的 insight × audience × 三视角 抽象在 v7 详情页砍掉了,只保留 sample 维度
16
17
  // (renderEvalSection 直接读 evalReport.results,不再走 detectInsights / audience 分组)。
17
18
  // 相关常量(SEVERITY_ICON / AUDIENCE_INFO_* / PATCH_TARGET_*)等以后做调试 UI
@@ -40,7 +41,14 @@ function relTime(ts, lang) {
40
41
  function effectiveObserveBand(observe) {
41
42
  if (!observe)
42
43
  return 'gray';
43
- return observe.confidence === 'underpowered' ? 'gray' : observe.healthBand;
44
+ if (observe.confidence === 'underpowered')
45
+ return 'gray';
46
+ if (observe.healthBand === 'green'
47
+ && (observe.toolCallCount ?? 0) > 0
48
+ && Math.max(0, (observe.toolResolvedCount ?? observe.toolCallCount ?? 0)
49
+ - (observe.toolCancelledCount ?? 0)) < 5)
50
+ return 'gray';
51
+ return observe.healthBand;
44
52
  }
45
53
  export function assessHealth(entry, insights, lang) {
46
54
  // underpowered observe 的色带仅供参考,不能算作一个「可信维度」:否则只有 observe、且段数过少时,
@@ -408,8 +416,10 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
408
416
  .sm-evidence-v { font-size:12.5px;color:#182033;font-weight:700;line-height:1.35;word-break:break-word }
409
417
  .sm-toolbar { display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;padding:8px 10px;border:1px solid #e4e8f1;border-radius:8px;background:#fff }
410
418
  .sm-tools,.sm-toggles { display:flex;align-items:center;gap:6px;flex-wrap:wrap }
411
- .sm-tool { min-width:30px;height:28px;padding:0 9px;border:1px solid #dbe2ee;border-radius:6px;background:#fff;color:#384255;font-size:13px;font-weight:700;cursor:pointer }
419
+ .sm-tool { min-width:30px;height:28px;padding:0 9px;border:1px solid #dbe2ee;border-radius:6px;background:#fff;color:#384255;font-size:13px;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:5px }
412
420
  .sm-tool:hover { background:#f7f8ff;border-color:#c7d2fe }
421
+ .sm-tool svg { flex:none }
422
+ .sm-tool--icon { width:30px;padding:0 }
413
423
  .sm-zoom-label { min-width:44px;text-align:center;font-size:11.5px;color:#637083;font-variant-numeric:tabular-nums }
414
424
  .sm-view-switch { display:inline-flex;align-items:center;gap:3px;padding:3px;border:1px solid #dbe2ee;border-radius:8px;background:#f8fafc }
415
425
  .sm-view { height:26px;padding:0 10px;border:0;border-radius:6px;background:transparent;color:#637083;font-size:11.5px;font-weight:700;cursor:pointer }
@@ -429,7 +439,7 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
429
439
  linear-gradient(#fff,#fbfcff);background-size:auto,28px 28px,28px 28px,auto;background-position:0 0 }
430
440
  .sm-body[data-sm-view="boundary"] [data-sm-layer="measurement"],
431
441
  .sm-body[data-sm-view="boundary"] [data-sm-evidence-edge="1"] { display:none !important }
432
- .sm-graph-stage { position:relative;width:1100px;height:560px }
442
+ .sm-graph-stage { position:relative;width:1100px;height:560px;margin:0 auto }
433
443
  .sm-graph { position:absolute;left:0;top:0;width:1100px;height:560px;transform-origin:0 0;transition:transform .12s ease }
434
444
  .sm-graph [hidden] { display:none !important }
435
445
  .sm-svg { position:absolute;inset:0;width:100%;height:100%;pointer-events:none }
@@ -438,9 +448,18 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
438
448
  .sm-edge--measurement { stroke:#bbc7ff }
439
449
  .sm-edge--coverage { stroke:#f59e0b;stroke-width:2;stroke-dasharray:5 5;opacity:.9 }
440
450
  .sm-edge--coverage-failed { stroke:#dc2626;stroke-width:2.2;opacity:.92 }
451
+ .sm-body[data-sm-view="evidence"] .sm-node.is-muted { opacity:.2;filter:saturate(.45) }
452
+ .sm-body[data-sm-view="evidence"] .sm-edge.is-muted { opacity:.12 }
453
+ .sm-body[data-sm-view="evidence"] .sm-edge.is-related { opacity:1;stroke-width:2.15 }
441
454
  .sm-zone-label { position:absolute;top:16px;z-index:1;font-size:10.5px;font-weight:800;letter-spacing:0;text-transform:uppercase;color:#8792a4;font-family:"SF Mono",Menlo,monospace;background:rgba(255,255,255,.82);border:1px solid #e4e8f1;border-radius:12px;padding:4px 9px;pointer-events:none }
442
455
  .sm-zone-label--definition { left:36px;color:#2f7d68;border-color:rgba(47,125,104,.22);background:rgba(244,251,248,.86) }
443
456
  .sm-zone-label--measurement { right:36px;color:#4f46e5;border-color:rgba(79,70,229,.22);background:rgba(247,248,255,.86) }
457
+ .sm-flow-label { position:absolute;top:18px;z-index:1;padding:3px 8px;border-radius:10px;background:rgba(255,255,255,.84);border:1px solid #e4e8f1;color:#7b8798;font-size:10px;font-weight:700;line-height:1;pointer-events:none }
458
+ .sm-flow-label--samples { left:326px }
459
+ .sm-flow-label--results { left:621px }
460
+ .sm-flow-label--targets { left:886px;color:#2f7d68;border-color:rgba(47,125,104,.22) }
461
+ .sm-body[data-sm-view="boundary"] [data-sm-evidence-only] { display:none !important }
462
+ .sm-body[data-sm-view="evidence"] .sm-zone-label { display:none !important }
444
463
  .sm-node { position:absolute;z-index:1;box-sizing:border-box;transform:translate(-50%,-50%);width:148px;min-height:54px;padding:8px 10px;border:1px solid #d8deea;border-radius:7px;background:rgba(255,255,255,.96);box-shadow:0 8px 20px rgba(31,41,55,6%);display:flex;flex-direction:column;justify-content:center;gap:4px;text-align:center;backdrop-filter:blur(4px) }
445
464
  .sm-node[data-sm-draggable="1"] { cursor:grab;touch-action:none;user-select:none }
446
465
  .sm-node[data-sm-draggable="1"].is-dragging { cursor:grabbing;z-index:3;box-shadow:0 16px 34px rgba(31,41,55,14%) }
@@ -904,6 +923,8 @@ const SKILL_MAP_INIT_SCRIPT = `
904
923
  var rootX = Number(map.getAttribute('data-sm-root-x')) || 470;
905
924
  var rootY = Number(map.getAttribute('data-sm-root-y')) || 260;
906
925
  var zoom = 1;
926
+ var activeWidth = baseWidth;
927
+ var activeHeight = baseHeight;
907
928
  var dragState = null;
908
929
  var suppressMoreClickUntil = 0;
909
930
  var selectedNodeId = '';
@@ -949,6 +970,24 @@ const SKILL_MAP_INIT_SCRIPT = `
949
970
  return 'M ' + sx + ' ' + sy + ' C ' + c1x + ' ' + sy + ', ' + c2x + ' ' + ty + ', ' + tx + ' ' + ty;
950
971
  }
951
972
 
973
+ function coveragePathData(fromX, fromY, toX, toY, fromBox, toBox){
974
+ var start = { x: fromX + fromBox.width / 2, y: fromY };
975
+ var end = { x: toX - toBox.width / 2, y: toY };
976
+ var routeY = fromY <= 285 ? Math.max(42, fromY - 92) : Math.min(baseHeight - 42, fromY + 92);
977
+ var leaveX = fromX + 112;
978
+ var approachX = Math.max(leaveX + 80, toX - 105);
979
+ return 'M ' + start.x + ' ' + start.y
980
+ + ' C ' + leaveX + ' ' + start.y + ', ' + leaveX + ' ' + routeY + ', ' + (leaveX + 48) + ' ' + routeY
981
+ + ' L ' + approachX + ' ' + routeY
982
+ + ' C ' + (toX - 70) + ' ' + routeY + ', ' + (toX - 70) + ' ' + end.y + ', ' + end.x + ' ' + end.y;
983
+ }
984
+
985
+ function edgePathData(edge, fromX, fromY, toX, toY, fromBox, toBox){
986
+ return edge.getAttribute('data-sm-evidence-edge') === '1'
987
+ ? coveragePathData(fromX, fromY, toX, toY, fromBox, toBox)
988
+ : pathData(fromX, fromY, toX, toY, fromBox, toBox);
989
+ }
990
+
952
991
  function setNodePosition(node, x, y){
953
992
  node.setAttribute('data-sm-x', String(Math.round(x * 10) / 10));
954
993
  node.setAttribute('data-sm-y', String(Math.round(y * 10) / 10));
@@ -967,6 +1006,89 @@ const SKILL_MAP_INIT_SCRIPT = `
967
1006
  };
968
1007
  }
969
1008
 
1009
+ function isVisibleNode(node){
1010
+ return !!node && !node.hidden && node.style.display !== 'none';
1011
+ }
1012
+
1013
+ function nodeLayoutPriority(node){
1014
+ if (node.hasAttribute('data-sm-root')) return 0;
1015
+ if (node.classList.contains('is-selected')) return 1;
1016
+ if (node.classList.contains('sm-node--group')) return 2;
1017
+ if (node.hasAttribute('data-sm-more-toggle')) return 3;
1018
+ return 4;
1019
+ }
1020
+
1021
+ function nodesOverlapAt(node, x, y, other){
1022
+ if (!isVisibleNode(other) || node === other) return false;
1023
+ var box = nodeBox(node, 154, 58);
1024
+ var otherBox = nodeBox(other, 154, 58);
1025
+ var otherCenter = nodeCenter(other, 0, 0);
1026
+ var horizontalGap = 18;
1027
+ var verticalGap = 14;
1028
+ return Math.abs(x - otherCenter.x) < (box.width + otherBox.width) / 2 + horizontalGap
1029
+ && Math.abs(y - otherCenter.y) < (box.height + otherBox.height) / 2 + verticalGap;
1030
+ }
1031
+
1032
+ function positionHasCollision(node, x, y, candidates){
1033
+ var others = candidates || Array.prototype.slice.call(graph.querySelectorAll('.sm-node'));
1034
+ return others.some(function(other){ return nodesOverlapAt(node, x, y, other); });
1035
+ }
1036
+
1037
+ function nearestOpenPosition(node, desiredX, desiredY, placed){
1038
+ var box = nodeBox(node, 154, 58);
1039
+ var minX = box.width / 2 + 24;
1040
+ var minY = box.height / 2 + 24;
1041
+ var startX = Math.max(minX, desiredX);
1042
+ var startY = Math.max(minY, desiredY);
1043
+ var rootNode = graph.querySelector('[data-sm-root]');
1044
+ var rootCenter = nodeCenter(rootNode, rootX, rootY);
1045
+ var outwardAngle = Math.atan2(startY - rootCenter.y, startX - rootCenter.x);
1046
+ if (!Number.isFinite(outwardAngle)) outwardAngle = 0;
1047
+
1048
+ for (var radius = 0; radius <= 1800; radius += 18) {
1049
+ var pointCount = radius === 0 ? 1 : Math.max(12, Math.ceil(Math.PI * 2 * radius / 28));
1050
+ for (var index = 0; index < pointCount; index += 1) {
1051
+ var offsetIndex = index === 0 ? 0 : Math.ceil(index / 2) * (index % 2 ? 1 : -1);
1052
+ var angle = outwardAngle + offsetIndex * (Math.PI * 2 / pointCount);
1053
+ var x = Math.max(minX, startX + Math.cos(angle) * radius);
1054
+ var y = Math.max(minY, startY + Math.sin(angle) * radius);
1055
+ if (!positionHasCollision(node, x, y, placed)) return { x: x, y: y };
1056
+ }
1057
+ }
1058
+
1059
+ var fallbackX = startX;
1060
+ var fallbackY = Math.max(
1061
+ minY,
1062
+ placed.reduce(function(maxY, other){
1063
+ var otherCenter = nodeCenter(other, 0, 0);
1064
+ var otherBox = nodeBox(other, 154, 58);
1065
+ return Math.max(maxY, otherCenter.y + otherBox.height / 2);
1066
+ }, baseHeight) + box.height / 2 + 28
1067
+ );
1068
+ while (positionHasCollision(node, fallbackX, fallbackY, placed)) {
1069
+ fallbackY += box.height + 28;
1070
+ }
1071
+ return { x: fallbackX, y: fallbackY };
1072
+ }
1073
+
1074
+ function layoutVisibleNodes(resetToPreferred){
1075
+ var nodes = Array.prototype.slice.call(graph.querySelectorAll('.sm-node'))
1076
+ .filter(isVisibleNode)
1077
+ .sort(function(a, b){
1078
+ return nodeLayoutPriority(a) - nodeLayoutPriority(b);
1079
+ });
1080
+ var placed = [];
1081
+ nodes.forEach(function(node){
1082
+ var desired = resetToPreferred
1083
+ ? nodeViewPosition(node, currentView)
1084
+ : nodeCenter(node, rootX, rootY);
1085
+ var open = nearestOpenPosition(node, desired.x, desired.y, placed);
1086
+ setNodePosition(node, open.x, open.y);
1087
+ placed.push(node);
1088
+ });
1089
+ refreshEdges();
1090
+ }
1091
+
970
1092
  function hideFilteredNode(node){
971
1093
  node.hidden = true;
972
1094
  node.toggleAttribute('hidden', true);
@@ -1009,7 +1131,7 @@ const SKILL_MAP_INIT_SCRIPT = `
1009
1131
  return node.getAttribute(attr) || '';
1010
1132
  }
1011
1133
 
1012
- function setSelectedNode(node){
1134
+ function setSelectedNode(node, refit){
1013
1135
  if (!node || node.hidden) return;
1014
1136
  var nodeId = node.getAttribute('data-sm-node-id') || '';
1015
1137
  selectedNodeId = nodeId;
@@ -1019,27 +1141,35 @@ const SKILL_MAP_INIT_SCRIPT = `
1019
1141
  });
1020
1142
  node.classList.add('is-selected');
1021
1143
  node.setAttribute('aria-pressed', 'true');
1022
- if (!detail) return;
1023
- var title = detailText(node, 'data-sm-detail-title', node.textContent || '');
1024
- var subtitle = detailText(node, 'data-sm-detail-subtitle', '');
1025
- var kind = detailText(node, 'data-sm-detail-kind', '');
1026
- var kindEl = detail.querySelector('[data-sm-detail-kind-label]');
1027
- var titleEl = detail.querySelector('[data-sm-detail-title]');
1028
- var subtitleEl = detail.querySelector('[data-sm-detail-subtitle]');
1029
- if (kindEl) kindEl.textContent = kind;
1030
- if (titleEl) titleEl.textContent = title;
1031
- if (subtitleEl) {
1032
- subtitleEl.textContent = subtitle;
1033
- subtitleEl.hidden = !subtitle;
1034
- subtitleEl.toggleAttribute('hidden', !subtitle);
1144
+ if (detail) {
1145
+ var title = detailText(node, 'data-sm-detail-title', node.textContent || '');
1146
+ var subtitle = detailText(node, 'data-sm-detail-subtitle', '');
1147
+ var kind = detailText(node, 'data-sm-detail-kind', '');
1148
+ var kindEl = detail.querySelector('[data-sm-detail-kind-label]');
1149
+ var titleEl = detail.querySelector('[data-sm-detail-title]');
1150
+ var subtitleEl = detail.querySelector('[data-sm-detail-subtitle]');
1151
+ if (kindEl) kindEl.textContent = kind;
1152
+ if (titleEl) titleEl.textContent = title;
1153
+ if (subtitleEl) {
1154
+ subtitleEl.textContent = subtitle;
1155
+ subtitleEl.hidden = !subtitle;
1156
+ subtitleEl.toggleAttribute('hidden', !subtitle);
1157
+ }
1158
+ setDetailValue('scope', detailText(node, 'data-sm-detail-scope', ''), true);
1159
+ setDetailValue('source', detailText(node, 'data-sm-detail-source', ''), false);
1160
+ setDetailValue('coverage', detailText(node, 'data-sm-detail-coverage', ''), false);
1161
+ setDetailValue('status', selectedMoreStatus(node) || detailText(node, 'data-sm-detail-status', ''), false);
1162
+ setDetailValue('evidence', detailText(node, 'data-sm-detail-evidence', ''), false);
1163
+ setDetailValue('action', detailText(node, 'data-sm-detail-action', ''), false);
1035
1164
  }
1036
- setDetailValue('scope', detailText(node, 'data-sm-detail-scope', ''), true);
1037
- setDetailValue('source', detailText(node, 'data-sm-detail-source', ''), false);
1038
- setDetailValue('coverage', detailText(node, 'data-sm-detail-coverage', ''), false);
1039
- setDetailValue('status', selectedMoreStatus(node) || detailText(node, 'data-sm-detail-status', ''), false);
1040
- setDetailValue('evidence', detailText(node, 'data-sm-detail-evidence', ''), false);
1041
- setDetailValue('action', detailText(node, 'data-sm-detail-action', ''), false);
1042
1165
  updateEvidenceFocus();
1166
+ if (refit) {
1167
+ requestAnimationFrame(function(){
1168
+ layoutVisibleNodes(false);
1169
+ fitToViewport();
1170
+ centerRoot();
1171
+ });
1172
+ }
1043
1173
  }
1044
1174
 
1045
1175
  function updateEdgesForNode(nodeId, x, y){
@@ -1049,7 +1179,8 @@ const SKILL_MAP_INIT_SCRIPT = `
1049
1179
  var fromNode = fromNodeId ? graph.querySelector('[data-sm-node-id="' + fromNodeId + '"]') : graph.querySelector('[data-sm-root]');
1050
1180
  var toNode = graph.querySelector('[data-sm-node-id="' + nodeId + '"]');
1051
1181
  var fromCenter = nodeCenter(fromNode, rootX, rootY);
1052
- edge.setAttribute('d', pathData(
1182
+ edge.setAttribute('d', edgePathData(
1183
+ edge,
1053
1184
  fromCenter.x,
1054
1185
  fromCenter.y,
1055
1186
  x,
@@ -1069,7 +1200,8 @@ const SKILL_MAP_INIT_SCRIPT = `
1069
1200
  var toCenter = nodeCenter(toNode, 0, 0);
1070
1201
  edge.setAttribute('data-sm-edge-from-x', String(Math.round(x * 10) / 10));
1071
1202
  edge.setAttribute('data-sm-edge-from-y', String(Math.round(y * 10) / 10));
1072
- edge.setAttribute('d', pathData(
1203
+ edge.setAttribute('d', edgePathData(
1204
+ edge,
1073
1205
  x,
1074
1206
  y,
1075
1207
  toCenter.x,
@@ -1092,6 +1224,7 @@ const SKILL_MAP_INIT_SCRIPT = `
1092
1224
 
1093
1225
  function resetNodePositions(){
1094
1226
  applyViewPositions(currentView);
1227
+ layoutVisibleNodes(false);
1095
1228
  }
1096
1229
 
1097
1230
  function applyViewPositions(view){
@@ -1105,8 +1238,12 @@ const SKILL_MAP_INIT_SCRIPT = `
1105
1238
  }
1106
1239
 
1107
1240
  function applyZoom(){
1108
- stage.style.width = Math.ceil(baseWidth * zoom) + 'px';
1109
- stage.style.height = Math.ceil(baseHeight * zoom) + 'px';
1241
+ var svg = graph.querySelector('.sm-svg');
1242
+ if (svg) svg.setAttribute('viewBox', '0 0 ' + activeWidth + ' ' + activeHeight);
1243
+ graph.style.width = activeWidth + 'px';
1244
+ graph.style.height = activeHeight + 'px';
1245
+ stage.style.width = Math.ceil(activeWidth * zoom) + 'px';
1246
+ stage.style.height = Math.ceil(activeHeight * zoom) + 'px';
1110
1247
  graph.style.transform = 'scale(' + zoom + ')';
1111
1248
  if (label) label.textContent = Math.round(zoom * 100) + '%';
1112
1249
  }
@@ -1123,6 +1260,29 @@ const SKILL_MAP_INIT_SCRIPT = `
1123
1260
  viewport.scrollTop = Math.max(0, graphY * zoom - y);
1124
1261
  }
1125
1262
 
1263
+ function fitToViewport(){
1264
+ var maxX = 0;
1265
+ var maxY = 0;
1266
+ graph.querySelectorAll('.sm-node').forEach(function(node){
1267
+ if (node.hidden || node.style.display === 'none') return;
1268
+ var center = nodeCenter(node, 0, 0);
1269
+ var box = nodeBox(node, 154, 58);
1270
+ maxX = Math.max(maxX, center.x + box.width / 2 + 24);
1271
+ maxY = Math.max(maxY, center.y + box.height / 2 + 24);
1272
+ });
1273
+ graph.querySelectorAll('[data-sm-evidence-only]').forEach(function(labelNode){
1274
+ if (labelNode.hidden || labelNode.style.display === 'none') return;
1275
+ maxX = Math.max(maxX, labelNode.offsetLeft + labelNode.offsetWidth + 24);
1276
+ maxY = Math.max(maxY, labelNode.offsetTop + labelNode.offsetHeight + 24);
1277
+ });
1278
+ activeWidth = Math.max(560, maxX);
1279
+ activeHeight = Math.max(420, maxY);
1280
+ var availableWidth = Math.max(1, viewport.clientWidth - 24);
1281
+ var availableHeight = Math.max(1, viewport.clientHeight - 24);
1282
+ zoom = clamp(Math.min(availableWidth / activeWidth, availableHeight / activeHeight, 1), 0.65, 1);
1283
+ applyZoom();
1284
+ }
1285
+
1126
1286
  function centerRoot(){
1127
1287
  var rootNode = graph.querySelector('[data-sm-root]');
1128
1288
  var rootCenter = nodeCenter(rootNode, rootX, rootY);
@@ -1153,15 +1313,52 @@ const SKILL_MAP_INIT_SCRIPT = `
1153
1313
  node.removeAttribute('data-sm-evidence-revealed');
1154
1314
  if (node.getAttribute('data-sm-layer') === 'definition' && currentView === 'evidence') hideFilteredNode(node);
1155
1315
  });
1156
- graph.querySelectorAll('.sm-node.is-related').forEach(function(node){
1316
+ graph.querySelectorAll('.sm-node.is-related,.sm-node.is-muted').forEach(function(node){
1157
1317
  node.classList.remove('is-related');
1318
+ node.classList.remove('is-muted');
1319
+ });
1320
+ map.querySelectorAll('.sm-edge.is-related,.sm-edge.is-muted').forEach(function(edge){
1321
+ edge.classList.remove('is-related');
1322
+ edge.classList.remove('is-muted');
1158
1323
  });
1324
+ var selectedNode = selectedNodeId
1325
+ ? graph.querySelector('[data-sm-node-id="' + selectedNodeId + '"]')
1326
+ : null;
1327
+ var focusedChainId = selectedNode
1328
+ ? (selectedNode.getAttribute('data-sm-chain') || selectedNode.getAttribute('data-sm-parent-sample') || '')
1329
+ : '';
1330
+ if (!focusedChainId && selectedNodeId && currentView === 'evidence') {
1331
+ var selectedCoverageEdge = map.querySelector('[data-sm-evidence-edge="1"][data-sm-edge-to="' + selectedNodeId + '"]');
1332
+ if (selectedCoverageEdge) focusedChainId = selectedCoverageEdge.getAttribute('data-sm-edge-from-node') || '';
1333
+ }
1334
+ var relatedLabel = graph.querySelector('[data-sm-related-label]');
1335
+ if (relatedLabel) {
1336
+ var showRelatedLabel = currentView === 'evidence' && !!focusedChainId;
1337
+ relatedLabel.hidden = !showRelatedLabel;
1338
+ relatedLabel.toggleAttribute('hidden', !showRelatedLabel);
1339
+ relatedLabel.setAttribute('aria-hidden', showRelatedLabel ? 'false' : 'true');
1340
+ relatedLabel.style.display = showRelatedLabel ? '' : 'none';
1341
+ }
1342
+ if (currentView === 'evidence' && focusedChainId) {
1343
+ graph.querySelectorAll('[data-sm-layer="measurement"][data-sm-chain]').forEach(function(node){
1344
+ var related = node.getAttribute('data-sm-chain') === focusedChainId;
1345
+ node.classList.toggle('is-muted', !related);
1346
+ if (related && node.getAttribute('data-sm-node-id') !== selectedNodeId) node.classList.add('is-related');
1347
+ });
1348
+ map.querySelectorAll('.sm-edge--measurement[data-sm-chain]').forEach(function(edge){
1349
+ var related = edge.getAttribute('data-sm-chain') === focusedChainId;
1350
+ edge.classList.toggle('is-muted', !related);
1351
+ edge.classList.toggle('is-related', related);
1352
+ });
1353
+ }
1159
1354
  map.querySelectorAll('[data-sm-evidence-edge="1"]').forEach(function(edge){
1160
1355
  var toId = edge.getAttribute('data-sm-edge-to');
1161
1356
  var fromId = edge.getAttribute('data-sm-edge-from-node');
1162
1357
  var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
1163
1358
  var fromNode = fromId ? graph.querySelector('[data-sm-node-id="' + fromId + '"]') : null;
1164
- var focused = currentView === 'evidence' && !!selectedNodeId && (toId === selectedNodeId || fromId === selectedNodeId);
1359
+ var focused = currentView === 'evidence'
1360
+ && !!selectedNodeId
1361
+ && (toId === selectedNodeId || fromId === selectedNodeId || fromId === focusedChainId);
1165
1362
  if (focused) {
1166
1363
  if (toNode) revealNode(toNode, true);
1167
1364
  if (fromNode) revealNode(fromNode, true);
@@ -1173,10 +1370,12 @@ const SKILL_MAP_INIT_SCRIPT = `
1173
1370
  edge.setAttribute('aria-hidden', shouldHide ? 'true' : 'false');
1174
1371
  edge.style.display = shouldHide ? 'none' : '';
1175
1372
  if (!shouldHide) {
1373
+ edge.classList.add('is-related');
1176
1374
  if (toNode && toId !== selectedNodeId) toNode.classList.add('is-related');
1177
1375
  if (fromNode && fromId !== selectedNodeId) fromNode.classList.add('is-related');
1178
1376
  }
1179
1377
  });
1378
+ layoutVisibleNodes(false);
1180
1379
  }
1181
1380
 
1182
1381
  function updateVisibility(){
@@ -1228,6 +1427,7 @@ const SKILL_MAP_INIT_SCRIPT = `
1228
1427
  updateViewButtons();
1229
1428
  updateVisibility();
1230
1429
  refreshEdges();
1430
+ fitToViewport();
1231
1431
  centerRoot();
1232
1432
  }
1233
1433
 
@@ -1241,6 +1441,8 @@ const SKILL_MAP_INIT_SCRIPT = `
1241
1441
  updateVisibility();
1242
1442
  refreshEdges();
1243
1443
  if (selectedNodeId && toggle.getAttribute('data-sm-node-id') === selectedNodeId) setSelectedNode(toggle);
1444
+ fitToViewport();
1445
+ centerRoot();
1244
1446
  }
1245
1447
 
1246
1448
  function collapseMoreGroups(){
@@ -1255,10 +1457,9 @@ const SKILL_MAP_INIT_SCRIPT = `
1255
1457
  if (action === 'zoom-in') setZoom(zoom + 0.15);
1256
1458
  if (action === 'zoom-out') setZoom(zoom - 0.15);
1257
1459
  if (action === 'reset') {
1258
- zoom = 1;
1259
1460
  collapseMoreGroups();
1260
1461
  resetNodePositions();
1261
- applyZoom();
1462
+ fitToViewport();
1262
1463
  centerRoot();
1263
1464
  }
1264
1465
  });
@@ -1271,7 +1472,11 @@ const SKILL_MAP_INIT_SCRIPT = `
1271
1472
  });
1272
1473
 
1273
1474
  map.querySelectorAll('[data-sm-toggle-leaves]').forEach(function(input){
1274
- input.addEventListener('change', updateVisibility);
1475
+ input.addEventListener('change', function(){
1476
+ updateVisibility();
1477
+ fitToViewport();
1478
+ centerRoot();
1479
+ });
1275
1480
  });
1276
1481
 
1277
1482
  graph.querySelectorAll('[data-sm-more-toggle]').forEach(function(toggle){
@@ -1320,8 +1525,12 @@ const SKILL_MAP_INIT_SCRIPT = `
1320
1525
  var deltaX = ev.clientX - dragState.startClientX;
1321
1526
  var deltaY = ev.clientY - dragState.startClientY;
1322
1527
  if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 3) dragState.moved = true;
1323
- var nextX = clamp(dragState.startX + deltaX / zoom, 52, baseWidth - 52);
1324
- var nextY = clamp(dragState.startY + deltaY / zoom, 36, baseHeight - 36);
1528
+ var dragBox = nodeBox(node, 154, 58);
1529
+ var horizontalMargin = dragBox.width / 2 + 24;
1530
+ var verticalMargin = dragBox.height / 2 + 24;
1531
+ var nextX = clamp(dragState.startX + deltaX / zoom, horizontalMargin, Math.max(baseWidth, activeWidth) - horizontalMargin);
1532
+ var nextY = clamp(dragState.startY + deltaY / zoom, verticalMargin, Math.max(baseHeight, activeHeight) - verticalMargin);
1533
+ if (positionHasCollision(node, nextX, nextY)) return;
1325
1534
  setNodePosition(node, nextX, nextY);
1326
1535
  updateEdgesForNode(dragState.nodeId, nextX, nextY);
1327
1536
  updateEdgesFromNode(dragState.nodeId, nextX, nextY);
@@ -1351,13 +1560,13 @@ const SKILL_MAP_INIT_SCRIPT = `
1351
1560
  graph.querySelectorAll('[data-sm-node-id]').forEach(function(node){
1352
1561
  node.addEventListener('click', function(){
1353
1562
  if (node.hasAttribute('data-sm-more-toggle') && Date.now() < suppressMoreClickUntil) return;
1354
- setSelectedNode(node);
1563
+ setSelectedNode(node, true);
1355
1564
  });
1356
1565
  node.addEventListener('keydown', function(ev){
1357
1566
  if (ev.key !== 'Enter' && ev.key !== ' ') return;
1358
1567
  if (node.hasAttribute('data-sm-more-toggle')) return;
1359
1568
  ev.preventDefault();
1360
- setSelectedNode(node);
1569
+ setSelectedNode(node, true);
1361
1570
  });
1362
1571
  });
1363
1572
 
@@ -1370,12 +1579,12 @@ const SKILL_MAP_INIT_SCRIPT = `
1370
1579
  }, { passive: false });
1371
1580
 
1372
1581
  updateViewButtons();
1373
- applyZoom();
1374
1582
  updateVisibility();
1375
1583
  var initialRoot = graph.querySelector('[data-sm-root]');
1376
1584
  if (initialRoot) setSelectedNode(initialRoot);
1377
1585
  requestAnimationFrame(function(){
1378
1586
  refreshEdges();
1587
+ fitToViewport();
1379
1588
  centerRoot();
1380
1589
  });
1381
1590
  }
@@ -1689,6 +1898,25 @@ function renderObserveSection(snap, langQ, lang) {
1689
1898
  }
1690
1899
  const gapPct = (snap.gapRate * 100).toFixed(0);
1691
1900
  const failPct = (snap.failureRate * 100).toFixed(1);
1901
+ const hasToolCallCount = snap.toolCallCount !== undefined;
1902
+ const toolCallCount = snap.toolCallCount ?? 0;
1903
+ const noToolCalls = hasToolCallCount && toolCallCount === 0;
1904
+ const toolResolvedCount = snap.toolResolvedCount
1905
+ ?? (hasToolCallCount ? toolCallCount : undefined);
1906
+ const toolCancelledCount = snap.toolCancelledCount ?? 0;
1907
+ const toolComparableCount = toolResolvedCount === undefined
1908
+ ? undefined
1909
+ : Math.max(0, toolResolvedCount - toolCancelledCount);
1910
+ const toolOutcomeUnknown = snap.stability === 'unknown'
1911
+ || (toolComparableCount === 0 && toolCallCount > 0);
1912
+ const toolOutcomeUnderpowered = toolComparableCount !== undefined
1913
+ && toolComparableCount > 0
1914
+ && toolComparableCount < 5;
1915
+ const toolOutcomeEvidence = hasToolCallCount && toolCallCount > 0
1916
+ ? (lang === 'zh'
1917
+ ? `${toolComparableCount}/${toolCallCount} 个结果可比较${toolCancelledCount > 0 ? `,${toolCancelledCount} 个取消` : ''}`
1918
+ : `${toolComparableCount}/${toolCallCount} outcomes comparable${toolCancelledCount > 0 ? `, ${toolCancelledCount} cancelled` : ''}`)
1919
+ : '';
1692
1920
  const alerts = [];
1693
1921
  if (snap.gapRate >= 0.2) {
1694
1922
  alerts.push(`<div class="si-rule si-rule--warn">
@@ -1699,25 +1927,50 @@ function renderObserveSection(snap, langQ, lang) {
1699
1927
  </div>
1700
1928
  </div>`);
1701
1929
  }
1702
- if (snap.failureRate >= 0.2) {
1930
+ if (toolOutcomeUnknown) {
1931
+ alerts.push(`<div class="si-rule si-rule--warn">
1932
+ <div class="si-rule-head">
1933
+ <span class="si-rule-icon">⚠</span>
1934
+ <code class="si-rule-id">${toolCancelledCount > 0
1935
+ ? (lang === 'zh' ? '工具调用取消' : 'tool calls cancelled')
1936
+ : (lang === 'zh' ? '工具结果状态未知' : 'tool outcomes unavailable')}</code>
1937
+ <span class="si-rule-msg">${toolCancelledCount > 0
1938
+ ? (lang === 'zh' ? '工具调用被取消,没有可比较的成功 / 失败结果' : 'tool calls were cancelled, leaving no comparable success/failure outcomes')
1939
+ : (lang === 'zh' ? '运行时未提供可判定的工具结果,失败率不可测' : 'runtime did not expose resolvable tool outcomes, so failure rate is unavailable')}</span>
1940
+ </div>
1941
+ </div>`);
1942
+ }
1943
+ else if (snap.failureRate >= 0.2) {
1703
1944
  alerts.push(`<div class="si-rule si-rule--warn">
1704
1945
  <div class="si-rule-head">
1705
1946
  <span class="si-rule-icon">⚠</span>
1706
1947
  <code class="si-rule-id">${lang === 'zh' ? '工具失败率' : 'tool fail rate'}</code>
1707
- <span class="si-rule-msg">${failPct}% ${lang === 'zh' ? '工具调用失败 — 可能环境问题或 skill 让 LLM 走错路径' : 'tool calls failing'}</span>
1948
+ <span class="si-rule-msg">${failPct}% ${lang === 'zh' ? '工具调用失败,可能是环境问题或 skill 让 LLM 走错路径' : 'tool calls failing'}${toolOutcomeUnderpowered
1949
+ ? (lang === 'zh' ? `;仅 ${toolOutcomeEvidence},结论置信度低` : `; only ${toolOutcomeEvidence}, low confidence`)
1950
+ : ''}</span>
1708
1951
  </div>
1709
1952
  </div>`);
1710
1953
  }
1711
1954
  const body = alerts.length > 0
1712
1955
  ? alerts.join('')
1713
1956
  : `<div class="si-sect-allpass">✓ ${lang === 'zh' ? '生产观测健康' : 'production observation healthy'}</div>`;
1714
- const obsCaveat = snap.confidence === 'underpowered'
1715
- ? (lang === 'zh' ? ' · ⚠ 样本不足,色带仅供参考' : ' · ⚠ low N, band indicative')
1716
- : '';
1957
+ const obsCaveat = [
1958
+ snap.confidence === 'underpowered'
1959
+ ? (lang === 'zh' ? '样本不足,色带仅供参考' : 'low N, band indicative')
1960
+ : '',
1961
+ toolOutcomeUnderpowered
1962
+ ? (lang === 'zh' ? `${toolOutcomeEvidence},工具失败率置信度低` : `${toolOutcomeEvidence}, low-confidence failure rate`)
1963
+ : '',
1964
+ ].filter(Boolean).map((value) => ` · ⚠ ${value}`).join('');
1965
+ const toolFailureMeta = toolOutcomeUnknown
1966
+ ? (lang === 'zh' ? '工具失败率不可测' : 'tool fail unavailable')
1967
+ : noToolCalls
1968
+ ? (lang === 'zh' ? '无工具调用' : 'no tool calls')
1969
+ : `${lang === 'zh' ? '工具失败率' : 'tool fail'} ${failPct}%${toolOutcomeEvidence ? `(${toolOutcomeEvidence})` : ''}`;
1717
1970
  return `<section id="section-observe" class="si-sect si-sect--${effectiveObserveBand(snap)}">
1718
1971
  <div class="si-sect-h">
1719
1972
  <span class="si-sect-title">👁 ${lang === 'zh' ? '线上观测 (observe)' : 'Live stability (observe)'}</span>
1720
- <span class="si-sect-meta">${snap.segmentCount} ${lang === 'zh' ? '段' : 'segs'} · gap ${gapPct}% · ${lang === 'zh' ? '工具失败率' : 'tool fail'} ${failPct}% · ${relTime(snap.generatedAt, lang)}${obsCaveat}</span>
1973
+ <span class="si-sect-meta">${snap.segmentCount} ${lang === 'zh' ? '段' : 'segs'} · gap ${gapPct}% · ${toolFailureMeta} · ${relTime(snap.generatedAt, lang)}${obsCaveat}</span>
1721
1974
  </div>
1722
1975
  <div class="si-sect-body">
1723
1976
  ${body}
@@ -1726,11 +1979,14 @@ function renderObserveSection(snap, langQ, lang) {
1726
1979
  </section>`;
1727
1980
  }
1728
1981
  function skillMapBand(entry) {
1729
- if (entry.doctor?.failCount || entry.eval?.failCount)
1982
+ const observeBand = effectiveObserveBand(entry.observe);
1983
+ if (entry.doctor?.failCount || entry.eval?.failCount || observeBand === 'red')
1730
1984
  return 'red';
1731
- if (entry.doctor?.warnCount || (entry.eval?.compositeScore != null && entry.eval.compositeScore < 3.5))
1985
+ if (entry.doctor?.warnCount
1986
+ || (entry.eval?.compositeScore != null && entry.eval.compositeScore < 3.5)
1987
+ || observeBand === 'yellow')
1732
1988
  return 'yellow';
1733
- if (entry.doctor || entry.eval || entry.observe)
1989
+ if (entry.doctor || entry.eval || observeBand === 'green')
1734
1990
  return 'green';
1735
1991
  return 'gray';
1736
1992
  }
@@ -1868,7 +2124,7 @@ const SKILL_MAP_WIDTH = 1100;
1868
2124
  const SKILL_MAP_HEIGHT = 560;
1869
2125
  const SKILL_MAP_ROOT_ID = 'skill-root';
1870
2126
  const SKILL_MAP_BOUNDARY_ROOT = { x: 560, y: 285 };
1871
- const SKILL_MAP_EVIDENCE_ROOT = { x: 470, y: 260 };
2127
+ const SKILL_MAP_EVIDENCE_ROOT_X = 125;
1872
2128
  const SKILL_MAP_ROOT_SIZE = { width: 172, height: 86 };
1873
2129
  const SKILL_MAP_NODE_SIZE = { width: 148, height: 54 };
1874
2130
  const SKILL_MAP_DEFINITION_GROUPS = [
@@ -1932,22 +2188,21 @@ const SKILL_MAP_BOUNDARY_ASSET_COORDS = [
1932
2188
  { x: 560, y: 510 },
1933
2189
  ];
1934
2190
  const SKILL_MAP_EVIDENCE_DEFINITION_COORDS = [
1935
- { x: 145, y: 145 },
1936
- { x: 145, y: 285 },
1937
- { x: 145, y: 425 },
1938
- { x: 955, y: 145 },
1939
- { x: 955, y: 285 },
1940
- { x: 955, y: 425 },
1941
- { x: 560, y: 485 },
1942
- ];
1943
- const SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS = [
1944
- { x: 320, y: 140 },
1945
- { x: 560, y: 115 },
1946
- { x: 800, y: 150 },
1947
- { x: 345, y: 360 },
1948
- { x: 775, y: 365 },
1949
- { x: 560, y: 455 },
2191
+ { x: 900, y: 80 },
2192
+ { x: 900, y: 180 },
2193
+ { x: 900, y: 280 },
2194
+ { x: 900, y: 380 },
2195
+ { x: 900, y: 480 },
2196
+ { x: 1065, y: 130 },
2197
+ { x: 1065, y: 230 },
2198
+ { x: 1065, y: 330 },
2199
+ { x: 1065, y: 430 },
1950
2200
  ];
2201
+ const SKILL_MAP_EVIDENCE_SAMPLE_X = 370;
2202
+ const SKILL_MAP_EVIDENCE_RESULT_X = 670;
2203
+ const SKILL_MAP_EVIDENCE_ROW_GAP = 140;
2204
+ const SKILL_MAP_EVIDENCE_CHILD_GAP = 66;
2205
+ const SKILL_MAP_EVIDENCE_START_Y = 100;
1951
2206
  const SKILL_MAP_NODE_MARGIN = { x: 92, y: 48 };
1952
2207
  const SKILL_MAP_STATUS_CLASSES = new Set(['ok', 'warning', 'failed', 'skipped', 'unknown', 'not_measured']);
1953
2208
  function graphNodeStatusClass(node) {
@@ -2248,7 +2503,9 @@ function renderSkillMapNode(item, lang) {
2248
2503
  const moreToggle = item.moreToggle
2249
2504
  ? ` data-sm-more-toggle="${item.moreToggle.group}" data-sm-collapsed-label="${e(item.moreToggle.collapsedLabel)}" data-sm-expanded-label="${e(item.moreToggle.expandedLabel)}" role="button" tabindex="0" aria-expanded="false"`
2250
2505
  : ' role="button" tabindex="0"';
2251
- return `<div class="sm-node sm-node--${item.layer}${kindClass}${graphNodeStatusClass(item.node)}${graphNodeCoverageClass(item.node)}" data-sm-node-id="${e(item.id)}" data-sm-layer="${item.layer}" data-sm-x="${item.x}" data-sm-y="${item.y}" data-sm-origin-x="${item.x}" data-sm-origin-y="${item.y}"${renderSkillMapViewPositionAttrs(item)}${leaf}${draggable}${overflow}${moreToggle}${renderSkillMapDetailAttrs(item, lang)} style="${positionedNodeStyle(item)}" title="${e(fullTitle)}" aria-label="${e(fullTitle)}">
2506
+ const chain = item.chainId ? ` data-sm-chain="${e(item.chainId)}"` : '';
2507
+ const parentSample = item.parentSampleId ? ` data-sm-parent-sample="${e(item.parentSampleId)}"` : '';
2508
+ return `<div class="sm-node sm-node--${item.layer}${kindClass}${graphNodeStatusClass(item.node)}${graphNodeCoverageClass(item.node)}" data-sm-node-id="${e(item.id)}" data-sm-layer="${item.layer}" data-sm-x="${item.x}" data-sm-y="${item.y}" data-sm-origin-x="${item.x}" data-sm-origin-y="${item.y}"${renderSkillMapViewPositionAttrs(item)}${chain}${parentSample}${leaf}${draggable}${overflow}${moreToggle}${renderSkillMapDetailAttrs(item, lang)} style="${positionedNodeStyle(item)}" title="${e(fullTitle)}" aria-label="${e(fullTitle)}">
2252
2509
  ${toggleIcon}
2253
2510
  ${kindLabel ? `<div class="sm-node-kind">${e(kindLabel)}</div>` : ''}
2254
2511
  <div class="sm-node-title">${e(title)}</div>
@@ -2271,6 +2528,16 @@ function skillMapPath(from, to, fromBox, toBox) {
2271
2528
  const c2x = end.x - (end.x >= start.x ? dx * 0.45 : -dx * 0.45);
2272
2529
  return `M ${start.x} ${start.y} C ${c1x} ${start.y}, ${c2x} ${end.y}, ${end.x} ${end.y}`;
2273
2530
  }
2531
+ function skillMapCoveragePath(from, to, graphHeight) {
2532
+ const start = { x: from.x + SKILL_MAP_NODE_SIZE.width / 2, y: from.y };
2533
+ const end = { x: to.x - SKILL_MAP_NODE_SIZE.width / 2, y: to.y };
2534
+ const routeY = from.y <= SKILL_MAP_BOUNDARY_ROOT.y
2535
+ ? Math.max(42, from.y - 92)
2536
+ : Math.min(graphHeight - 42, from.y + 92);
2537
+ const leaveX = from.x + 112;
2538
+ const approachX = Math.max(leaveX + 80, to.x - 105);
2539
+ return `M ${start.x} ${start.y} C ${leaveX} ${start.y}, ${leaveX} ${routeY}, ${leaveX + 48} ${routeY} L ${approachX} ${routeY} C ${to.x - 70} ${routeY}, ${to.x - 70} ${end.y}, ${end.x} ${end.y}`;
2540
+ }
2274
2541
  function renderSkillMapEdges(nodes, dimensions, evidenceLinks) {
2275
2542
  const rootEdges = nodes
2276
2543
  .map((node) => {
@@ -2279,11 +2546,12 @@ function renderSkillMapEdges(nodes, dimensions, evidenceLinks) {
2279
2546
  const overflow = node.overflowGroup ? ` data-sm-overflow-group="${node.overflowGroup}" hidden aria-hidden="true"` : '';
2280
2547
  const fromNodeId = node.edgeFromId ?? (node.overflowGroup ? `${node.overflowGroup}-more` : SKILL_MAP_ROOT_ID);
2281
2548
  const leaf = node.isLeaf === false ? '' : ' data-sm-leaf="1"';
2282
- return `<path class="sm-edge sm-edge--${node.layer}" data-sm-layer="${node.layer}"${leaf} data-sm-edge-to="${e(node.id)}" data-sm-edge-from-node="${e(fromNodeId)}" data-sm-edge-from-x="${from.x}" data-sm-edge-from-y="${from.y}"${overflow} d="${skillMapPath(from, node, fromBox, SKILL_MAP_NODE_SIZE)}"></path>`;
2549
+ const chain = node.chainId ? ` data-sm-chain="${e(node.chainId)}"` : '';
2550
+ return `<path class="sm-edge sm-edge--${node.layer}" data-sm-layer="${node.layer}"${chain}${leaf} data-sm-edge-to="${e(node.id)}" data-sm-edge-from-node="${e(fromNodeId)}" data-sm-edge-from-x="${from.x}" data-sm-edge-from-y="${from.y}"${overflow} d="${skillMapPath(from, node, fromBox, SKILL_MAP_NODE_SIZE)}"></path>`;
2283
2551
  })
2284
2552
  .join('');
2285
2553
  const evidenceEdges = evidenceLinks
2286
- .map((link, index) => `<path class="sm-edge sm-edge--coverage${link.failed ? ' sm-edge--coverage-failed' : ''}" data-sm-layer="measurement" data-sm-leaf="1" data-sm-evidence-edge="1" data-sm-edge-to="${e(link.toId)}" data-sm-edge-from-node="${e(link.fromId)}" data-sm-edge-from-x="${link.from.x}" data-sm-edge-from-y="${link.from.y}" d="${skillMapPath(link.from, link.to, SKILL_MAP_NODE_SIZE, SKILL_MAP_NODE_SIZE)}" data-sm-edge-index="${index}" hidden aria-hidden="true"></path>`)
2554
+ .map((link, index) => `<path class="sm-edge sm-edge--coverage${link.failed ? ' sm-edge--coverage-failed' : ''}" data-sm-layer="measurement" data-sm-leaf="1" data-sm-evidence-edge="1" data-sm-edge-to="${e(link.toId)}" data-sm-edge-from-node="${e(link.fromId)}" data-sm-edge-from-x="${link.from.x}" data-sm-edge-from-y="${link.from.y}" d="${skillMapCoveragePath(link.from, link.to, dimensions.height)}" data-sm-edge-index="${index}" hidden aria-hidden="true"></path>`)
2287
2555
  .join('');
2288
2556
  return `<svg class="sm-svg" viewBox="0 0 ${dimensions.width} ${dimensions.height}" aria-hidden="true">${rootEdges}${evidenceEdges}</svg>`;
2289
2557
  }
@@ -2305,16 +2573,21 @@ function skillMapEvidenceLinks(nodes, evalGraph) {
2305
2573
  if (!evalGraph?.declaredCoverageEdges.length)
2306
2574
  return [];
2307
2575
  const targetByStableKey = new Map();
2576
+ const sampleByStableKey = new Map();
2308
2577
  const sampleByLabel = new Map();
2309
2578
  for (const node of nodes) {
2310
2579
  if (node.node.stableKey)
2311
2580
  targetByStableKey.set(node.node.stableKey, node);
2312
- if (node.node.nodeKind === 'sample')
2581
+ if (node.node.nodeKind === 'sample') {
2582
+ if (node.node.stableKey)
2583
+ sampleByStableKey.set(node.node.stableKey, node);
2313
2584
  sampleByLabel.set(node.node.label, node);
2585
+ }
2314
2586
  }
2315
2587
  return evalGraph.declaredCoverageEdges
2316
2588
  .map((edge) => {
2317
- const from = sampleByLabel.get(edge.sampleLabel);
2589
+ const from = (edge.sampleStableKey ? sampleByStableKey.get(edge.sampleStableKey) : undefined)
2590
+ ?? sampleByLabel.get(edge.sampleLabel);
2318
2591
  const to = targetByStableKey.get(edge.targetStableKey);
2319
2592
  if (!from || !to)
2320
2593
  return null;
@@ -2482,7 +2755,8 @@ function skillMapDimensions(nodes) {
2482
2755
  function renderSkillStageRail(entry, lang) {
2483
2756
  const doctorStatus = entry.doctor?.failCount ? 'failed' : entry.doctor?.warnCount ? 'warning' : entry.doctor ? 'ok' : 'pending';
2484
2757
  const evalStatus = entry.eval?.failCount ? 'failed' : entry.eval ? 'ok' : 'pending';
2485
- const observeStatus = entry.observe ? (entry.observe.healthBand === 'red' ? 'failed' : entry.observe.healthBand === 'yellow' ? 'warning' : 'ok') : 'pending';
2758
+ const observeBand = effectiveObserveBand(entry.observe);
2759
+ const observeStatus = observeBand === 'red' ? 'failed' : observeBand === 'yellow' ? 'warning' : observeBand === 'green' ? 'ok' : 'pending';
2486
2760
  const card = (key, value, status) => `<div class="sm-stage-card sm-stage-card--${status}">
2487
2761
  <div class="sm-stage-k">${e(key)}</div>
2488
2762
  <div class="sm-stage-v">${e(value)}</div>
@@ -2529,6 +2803,107 @@ function measurementStatusPriority(node) {
2529
2803
  return 2;
2530
2804
  return 3;
2531
2805
  }
2806
+ function buildMeasurementMapNodes(candidates, lang) {
2807
+ const samples = candidates.filter((node) => node.nodeKind === 'sample').slice(0, 5);
2808
+ const sampleStableKeys = new Set(samples.map((node) => node.stableKey).filter((value) => Boolean(value)));
2809
+ const childrenBySample = new Map();
2810
+ const overflow = candidates.filter((node) => node.nodeKind === 'sample').slice(5);
2811
+ for (const node of candidates.filter((candidate) => candidate.nodeKind !== 'sample')) {
2812
+ const parent = node.parentSampleStableKey;
2813
+ if (!parent || !sampleStableKeys.has(parent)) {
2814
+ overflow.push(node);
2815
+ continue;
2816
+ }
2817
+ const children = childrenBySample.get(parent) ?? [];
2818
+ if (children.length < 3)
2819
+ children.push(node);
2820
+ else
2821
+ overflow.push(node);
2822
+ childrenBySample.set(parent, children);
2823
+ }
2824
+ const nodes = [];
2825
+ let cursorY = SKILL_MAP_EVIDENCE_START_Y;
2826
+ for (const [sampleIndex, sample] of samples.entries()) {
2827
+ const sampleId = `measurement-sample-${sampleIndex}`;
2828
+ const children = sample.stableKey ? childrenBySample.get(sample.stableKey) ?? [] : [];
2829
+ const childSpan = Math.max(SKILL_MAP_EVIDENCE_ROW_GAP, (children.length - 1) * SKILL_MAP_EVIDENCE_CHILD_GAP + 84);
2830
+ const sampleY = cursorY + childSpan / 2;
2831
+ const samplePosition = { x: SKILL_MAP_EVIDENCE_SAMPLE_X, y: sampleY };
2832
+ nodes.push({
2833
+ id: sampleId,
2834
+ node: sample,
2835
+ layer: 'measurement',
2836
+ x: samplePosition.x,
2837
+ y: samplePosition.y,
2838
+ boundaryPosition: samplePosition,
2839
+ evidencePosition: samplePosition,
2840
+ chainId: sampleId,
2841
+ });
2842
+ children.forEach((child, childIndex) => {
2843
+ const childY = sampleY + (childIndex - (children.length - 1) / 2) * SKILL_MAP_EVIDENCE_CHILD_GAP;
2844
+ const childPosition = { x: SKILL_MAP_EVIDENCE_RESULT_X, y: childY };
2845
+ nodes.push({
2846
+ id: `${sampleId}-evidence-${childIndex}`,
2847
+ node: child,
2848
+ layer: 'measurement',
2849
+ x: childPosition.x,
2850
+ y: childPosition.y,
2851
+ boundaryPosition: childPosition,
2852
+ evidencePosition: childPosition,
2853
+ edgeFrom: samplePosition,
2854
+ edgeFromId: sampleId,
2855
+ chainId: sampleId,
2856
+ parentSampleId: sampleId,
2857
+ });
2858
+ });
2859
+ cursorY += childSpan;
2860
+ }
2861
+ const samplePositions = nodes.filter((node) => node.node.nodeKind === 'sample').map((node) => node.y);
2862
+ const rootY = samplePositions.length > 0
2863
+ ? ((samplePositions[0] ?? SKILL_MAP_BOUNDARY_ROOT.y) + (samplePositions[samplePositions.length - 1] ?? SKILL_MAP_BOUNDARY_ROOT.y)) / 2
2864
+ : SKILL_MAP_BOUNDARY_ROOT.y;
2865
+ const rootPosition = {
2866
+ x: SKILL_MAP_EVIDENCE_ROOT_X,
2867
+ y: Number.isFinite(rootY) ? rootY : SKILL_MAP_BOUNDARY_ROOT.y,
2868
+ };
2869
+ if (overflow.length > 0) {
2870
+ const morePosition = {
2871
+ x: SKILL_MAP_EVIDENCE_SAMPLE_X,
2872
+ y: Math.max(cursorY + 18, rootPosition.y + SKILL_MAP_EVIDENCE_ROW_GAP),
2873
+ };
2874
+ nodes.push({
2875
+ id: 'measurement-more',
2876
+ node: moreNode(`+ ${overflow.length} ${lang === 'zh' ? '更多证据节点' : 'more evidence nodes'}`),
2877
+ layer: 'measurement',
2878
+ x: morePosition.x,
2879
+ y: morePosition.y,
2880
+ boundaryPosition: morePosition,
2881
+ evidencePosition: morePosition,
2882
+ isLeaf: false,
2883
+ moreToggle: {
2884
+ group: 'measurement',
2885
+ collapsedLabel: `+ ${overflow.length} ${lang === 'zh' ? '更多证据节点' : 'more evidence nodes'}`,
2886
+ expandedLabel: lang === 'zh' ? '收起证据节点' : 'collapse evidence nodes',
2887
+ },
2888
+ });
2889
+ overflow.forEach((node, index) => {
2890
+ const position = overflowNodePosition('measurement', index, 'evidence');
2891
+ nodes.push({
2892
+ id: `measurement-overflow-${index}`,
2893
+ node,
2894
+ layer: 'measurement',
2895
+ x: position.x,
2896
+ y: position.y,
2897
+ boundaryPosition: position,
2898
+ evidencePosition: position,
2899
+ edgeFrom: morePosition,
2900
+ edgeFromId: 'measurement-more',
2901
+ overflowGroup: 'measurement',
2902
+ });
2903
+ });
2904
+ }
2905
+ return { nodes, rootPosition };
2906
+ }
2532
2907
  function applyDefinitionCoverageDeclarations(nodes, declaredStableKeys, coveredByStableKey) {
2533
2908
  if (!nodes)
2534
2909
  return undefined;
@@ -2663,44 +3038,9 @@ function renderSkillMapSection(entry, lang) {
2663
3038
  diagnostic: 2,
2664
3039
  assertion: 3,
2665
3040
  }).sort((a, b) => measurementStatusPriority(a) - measurementStatusPriority(b));
2666
- const measurementLimit = SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS.length - 1;
2667
- const measurementVisible = measurementCandidates.slice(0, measurementLimit);
2668
- const measurementOverflow = measurementCandidates.slice(measurementLimit);
2669
- const measurementMorePosition = SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS.length - 1];
2670
3041
  const definitionNodes = buildDefinitionMapNodes(definitionCandidates, lang);
2671
- const measurementNodes = [
2672
- ...measurementVisible.map((node, index) => ({
2673
- id: `measurement-${index}`,
2674
- node,
2675
- layer: 'measurement',
2676
- ...SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[index],
2677
- boundaryPosition: SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[index],
2678
- evidencePosition: SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[index],
2679
- })),
2680
- ...(measurementOverflow.length > 0 ? [{
2681
- id: 'measurement-more',
2682
- node: moreNode(`+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`),
2683
- layer: 'measurement',
2684
- boundaryPosition: measurementMorePosition,
2685
- evidencePosition: measurementMorePosition,
2686
- moreToggle: {
2687
- group: 'measurement',
2688
- collapsedLabel: `+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`,
2689
- expandedLabel: zh ? '收起测量节点' : 'collapse measurement nodes',
2690
- },
2691
- ...measurementMorePosition,
2692
- }] : []),
2693
- ...measurementOverflow.map((node, index) => ({
2694
- id: `measurement-overflow-${index}`,
2695
- node,
2696
- layer: 'measurement',
2697
- edgeFrom: measurementMorePosition,
2698
- overflowGroup: 'measurement',
2699
- ...overflowNodePosition('measurement', index, 'evidence'),
2700
- boundaryPosition: overflowNodePosition('measurement', index, 'evidence'),
2701
- evidencePosition: overflowNodePosition('measurement', index, 'evidence'),
2702
- })),
2703
- ];
3042
+ const measurementLayout = buildMeasurementMapNodes(measurementCandidates, lang);
3043
+ const measurementNodes = measurementLayout.nodes;
2704
3044
  const mapNodes = [...definitionNodes, ...measurementNodes];
2705
3045
  const evidenceLinks = skillMapEvidenceLinks(mapNodes, evalGraph);
2706
3046
  const mapDimensions = skillMapDimensions(mapNodes);
@@ -2715,10 +3055,10 @@ function renderSkillMapSection(entry, lang) {
2715
3055
  ${renderSkillMapEvidenceStrip(entry, doctor, evalGraph, lang)}
2716
3056
  <div class="sm-toolbar" aria-label="${zh ? 'Skill Map 控制' : 'Skill Map controls'}">
2717
3057
  <div class="sm-tools">
2718
- <button class="sm-tool" type="button" data-sm-action="zoom-out" title="${zh ? '缩小' : 'Zoom out'}">−</button>
3058
+ <button class="sm-tool sm-tool--icon" type="button" data-sm-action="zoom-out" title="${zh ? '缩小' : 'Zoom out'}" aria-label="${zh ? '缩小' : 'Zoom out'}">${icon('zoom-out', { size: 15 })}</button>
2719
3059
  <span class="sm-zoom-label" data-sm-zoom-label>100%</span>
2720
- <button class="sm-tool" type="button" data-sm-action="zoom-in" title="${zh ? '放大' : 'Zoom in'}">+</button>
2721
- <button class="sm-tool" type="button" data-sm-action="reset" title="${zh ? '重置视图' : 'Reset view'}">${zh ? '重置' : 'Reset'}</button>
3060
+ <button class="sm-tool sm-tool--icon" type="button" data-sm-action="zoom-in" title="${zh ? '放大' : 'Zoom in'}" aria-label="${zh ? '放大' : 'Zoom in'}">${icon('zoom-in', { size: 15 })}</button>
3061
+ <button class="sm-tool" type="button" data-sm-action="reset" title="${zh ? '适配画布并重置节点' : 'Fit canvas and reset nodes'}">${icon('maximize-2', { size: 14 })}${zh ? '适配' : 'Fit'}</button>
2722
3062
  </div>
2723
3063
  <div class="sm-toggles">
2724
3064
  <div class="sm-view-switch" role="group" aria-label="${zh ? '图谱视图' : 'Map view'}">
@@ -2733,8 +3073,11 @@ function renderSkillMapSection(entry, lang) {
2733
3073
  <div class="sm-graph" data-sm-graph style="width:${mapDimensions.width}px;height:${mapDimensions.height}px">
2734
3074
  <div class="sm-zone-label sm-zone-label--definition" data-sm-layer="definition">${zh ? '知识边界' : 'Knowledge'}</div>
2735
3075
  <div class="sm-zone-label sm-zone-label--measurement" data-sm-layer="measurement">${zh ? '评测证据' : 'Evidence'}</div>
3076
+ <div class="sm-flow-label sm-flow-label--samples" data-sm-layer="measurement" data-sm-evidence-only>${zh ? '评测用例' : 'Eval samples'}</div>
3077
+ <div class="sm-flow-label sm-flow-label--results" data-sm-layer="measurement" data-sm-evidence-only>${zh ? '断言与诊断' : 'Checks & diagnostics'}</div>
3078
+ <div class="sm-flow-label sm-flow-label--targets" data-sm-layer="measurement" data-sm-evidence-only data-sm-related-label hidden aria-hidden="true">${zh ? '关联知识' : 'Related knowledge'}</div>
2736
3079
  ${renderSkillMapEdges(mapNodes, mapDimensions, evidenceLinks)}
2737
- <div class="sm-node sm-node--skill" data-sm-root data-sm-node-id="${SKILL_MAP_ROOT_ID}" data-sm-x="${SKILL_MAP_BOUNDARY_ROOT.x}" data-sm-y="${SKILL_MAP_BOUNDARY_ROOT.y}" data-sm-origin-x="${SKILL_MAP_BOUNDARY_ROOT.x}" data-sm-origin-y="${SKILL_MAP_BOUNDARY_ROOT.y}" data-sm-boundary-x="${SKILL_MAP_BOUNDARY_ROOT.x}" data-sm-boundary-y="${SKILL_MAP_BOUNDARY_ROOT.y}" data-sm-evidence-x="${SKILL_MAP_EVIDENCE_ROOT.x}" data-sm-evidence-y="${SKILL_MAP_EVIDENCE_ROOT.y}" data-sm-draggable="1"${renderSkillMapRootDetailAttrs(entry, coverageSummary, lang)} role="button" tabindex="0" aria-label="SKILL.md ${e(entry.skillName)}" style="${positionedNodeStyle(SKILL_MAP_BOUNDARY_ROOT)}">
3080
+ <div class="sm-node sm-node--skill" data-sm-root data-sm-node-id="${SKILL_MAP_ROOT_ID}" data-sm-x="${SKILL_MAP_BOUNDARY_ROOT.x}" data-sm-y="${SKILL_MAP_BOUNDARY_ROOT.y}" data-sm-origin-x="${SKILL_MAP_BOUNDARY_ROOT.x}" data-sm-origin-y="${SKILL_MAP_BOUNDARY_ROOT.y}" data-sm-boundary-x="${SKILL_MAP_BOUNDARY_ROOT.x}" data-sm-boundary-y="${SKILL_MAP_BOUNDARY_ROOT.y}" data-sm-evidence-x="${measurementLayout.rootPosition.x}" data-sm-evidence-y="${measurementLayout.rootPosition.y}" data-sm-draggable="1"${renderSkillMapRootDetailAttrs(entry, coverageSummary, lang)} role="button" tabindex="0" aria-label="SKILL.md ${e(entry.skillName)}" style="${positionedNodeStyle(SKILL_MAP_BOUNDARY_ROOT)}">
2738
3081
  <div class="sm-node-title">SKILL.md</div>
2739
3082
  <div class="sm-node-meta">${e(entry.skillName)}</div>
2740
3083
  </div>