oh-my-knowledge 0.45.0 → 0.46.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 (35) hide show
  1. package/README.md +1 -1
  2. package/README.zh.md +1 -1
  3. package/dist/assets/agent-skills/omk/SKILL.md +3 -3
  4. package/dist/assets/agent-skills/omk/references/commands.md +13 -6
  5. package/dist/cli/commands/doctor.d.ts +3 -2
  6. package/dist/cli/commands/doctor.js +66 -63
  7. package/dist/cli/lib/cmd-flags.d.ts +3 -2
  8. package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
  9. package/dist/cli/lib/i18n-dict/common.js +0 -4
  10. package/dist/doctor/endpoint-rule.js +1 -1
  11. package/dist/doctor/health/composer.js +162 -50
  12. package/dist/doctor/health/consensus.d.ts +43 -0
  13. package/dist/doctor/health/consensus.js +224 -0
  14. package/dist/doctor/health/dimension-spec.d.ts +6 -0
  15. package/dist/doctor/health/register.d.ts +0 -1
  16. package/dist/doctor/health/register.js +0 -1
  17. package/dist/doctor/index.js +3 -0
  18. package/dist/doctor/messages.d.ts +1 -1
  19. package/dist/doctor/messages.js +8 -0
  20. package/dist/doctor/rules.d.ts +4 -2
  21. package/dist/doctor/rules.js +4 -2
  22. package/dist/executors/claude-cli.js +1 -1
  23. package/dist/observability/skill-chain-advisories.js +2 -2
  24. package/dist/renderer/doctor-detail-renderer.js +30 -2
  25. package/dist/renderer/report-shell.d.ts +1 -1
  26. package/dist/renderer/report-shell.js +5 -0
  27. package/dist/renderer/skill-detail-renderer.js +844 -100
  28. package/dist/server/skill-index.js +37 -1
  29. package/dist/shared/llm-prompts/registry.js +2 -1
  30. package/dist/shared/llm-prompts/skill-health-merge.d.ts +22 -0
  31. package/dist/shared/llm-prompts/skill-health-merge.js +79 -0
  32. package/dist/shared/llm-prompts/skill-health.js +1 -1
  33. package/dist/types/doctor.d.ts +32 -8
  34. package/dist/types/skill-index.d.ts +10 -0
  35. package/package.json +4 -3
@@ -400,11 +400,21 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
400
400
  /* Skill Map:以 SKILL.md 为根的轻量知识图,不暴露完整 graph JSON 拓扑 */
401
401
  .sm-sect { border-left-color:#4f46e5 }
402
402
  .sm-body { display:flex;flex-direction:column;gap:10px }
403
+ .sm-evidence-strip { display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px }
404
+ .sm-evidence-item { min-height:44px;padding:8px 10px;border:1px solid #e4e8f1;border-radius:8px;background:#fff;display:flex;flex-direction:column;justify-content:center;gap:2px }
405
+ .sm-evidence-item--risk { border-color:rgba(220,38,38,.34);background:rgba(220,38,38,.055) }
406
+ .sm-evidence-item--action { border-color:rgba(245,158,11,.42);background:rgba(245,158,11,.075) }
407
+ .sm-evidence-k { font-size:10px;color:#8792a4;font-weight:700;line-height:1;text-transform:uppercase;font-family:"SF Mono",Menlo,monospace }
408
+ .sm-evidence-v { font-size:12.5px;color:#182033;font-weight:700;line-height:1.35;word-break:break-word }
403
409
  .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 }
404
410
  .sm-tools,.sm-toggles { display:flex;align-items:center;gap:6px;flex-wrap:wrap }
405
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 }
406
412
  .sm-tool:hover { background:#f7f8ff;border-color:#c7d2fe }
407
413
  .sm-zoom-label { min-width:44px;text-align:center;font-size:11.5px;color:#637083;font-variant-numeric:tabular-nums }
414
+ .sm-view-switch { display:inline-flex;align-items:center;gap:3px;padding:3px;border:1px solid #dbe2ee;border-radius:8px;background:#f8fafc }
415
+ .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 }
416
+ .sm-view:hover { color:#384255;background:#fff }
417
+ .sm-view.is-active { color:#182033;background:#fff;box-shadow:0 1px 4px rgba(31,41,55,.08) }
408
418
  .sm-toggle { display:inline-flex;align-items:center;gap:5px;height:28px;padding:0 8px;border:1px solid #dbe2ee;border-radius:14px;background:#fff;color:#384255;font-size:11.5px;cursor:pointer;user-select:none }
409
419
  .sm-toggle input { margin:0;accent-color:#4f46e5 }
410
420
  .sm-canvas { position:relative;height:min(68vh,620px);min-height:520px;padding:0;background:
@@ -412,6 +422,13 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
412
422
  linear-gradient(rgba(148,163,184,.075) 1px,transparent 1px),
413
423
  linear-gradient(90deg,rgba(148,163,184,.06) 1px,transparent 1px),
414
424
  linear-gradient(#fff,#fbfcff);background-size:auto,28px 28px,28px 28px,auto;background-position:0 0;border:1px solid #e4e8f1;border-radius:8px;overflow:auto;overscroll-behavior:contain }
425
+ .sm-body[data-sm-view="boundary"] .sm-canvas { background:
426
+ linear-gradient(90deg,rgba(31,157,99,.052) 0%,rgba(31,157,99,.025) 42%,transparent 42%,transparent 100%),
427
+ linear-gradient(rgba(148,163,184,.07) 1px,transparent 1px),
428
+ linear-gradient(90deg,rgba(148,163,184,.055) 1px,transparent 1px),
429
+ linear-gradient(#fff,#fbfcff);background-size:auto,28px 28px,28px 28px,auto;background-position:0 0 }
430
+ .sm-body[data-sm-view="boundary"] [data-sm-layer="measurement"],
431
+ .sm-body[data-sm-view="boundary"] [data-sm-evidence-edge="1"] { display:none !important }
415
432
  .sm-graph-stage { position:relative;width:1100px;height:560px }
416
433
  .sm-graph { position:absolute;left:0;top:0;width:1100px;height:560px;transform-origin:0 0;transition:transform .12s ease }
417
434
  .sm-graph [hidden] { display:none !important }
@@ -419,6 +436,11 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
419
436
  .sm-edge { fill:none;stroke:#d8e0ec;stroke-width:1.45;stroke-linecap:round;opacity:.86;vector-effect:non-scaling-stroke }
420
437
  .sm-edge--definition { stroke:#a9cfc4 }
421
438
  .sm-edge--measurement { stroke:#bbc7ff }
439
+ .sm-edge--coverage { stroke:#f59e0b;stroke-width:2;stroke-dasharray:5 5;opacity:.9 }
440
+ .sm-edge--coverage-failed { stroke:#dc2626;stroke-width:2.2;opacity:.92 }
441
+ .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
+ .sm-zone-label--definition { left:36px;color:#2f7d68;border-color:rgba(47,125,104,.22);background:rgba(244,251,248,.86) }
443
+ .sm-zone-label--measurement { right:36px;color:#4f46e5;border-color:rgba(79,70,229,.22);background:rgba(247,248,255,.86) }
422
444
  .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) }
423
445
  .sm-node[data-sm-draggable="1"] { cursor:grab;touch-action:none;user-select:none }
424
446
  .sm-node[data-sm-draggable="1"].is-dragging { cursor:grabbing;z-index:3;box-shadow:0 16px 34px rgba(31,41,55,14%) }
@@ -426,7 +448,11 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
426
448
  .sm-node[data-sm-draggable="1"][data-sm-more-toggle] { cursor:grab }
427
449
  .sm-node[data-sm-draggable="1"][data-sm-more-toggle].is-dragging { cursor:grabbing }
428
450
  .sm-node[data-sm-more-toggle]:hover,.sm-node[data-sm-more-toggle]:focus-visible { border-color:#9fbfb5;background:#f1fbf7;outline:none }
451
+ .sm-node:hover,.sm-node:focus-visible,.sm-node.is-selected { border-color:#4f46e5;outline:3px solid rgba(79,70,229,.12);outline-offset:2px }
452
+ .sm-node.is-selected { z-index:2;box-shadow:0 14px 30px rgba(79,70,229,13%) }
453
+ .sm-node.is-related { z-index:2;outline:3px solid rgba(245,158,11,.14);outline-offset:2px;box-shadow:0 12px 26px rgba(245,158,11,.12) }
429
454
  .sm-node--skill { width:172px;min-height:86px;border-color:#4f46e5;background:linear-gradient(180deg,#fff 0%,#f7f8ff 100%);box-shadow:0 16px 34px rgba(79,70,229,14%);outline:4px solid rgba(79,70,229,.07) }
455
+ .sm-node--skill.is-selected { outline-color:rgba(79,70,229,.15) }
430
456
  .sm-node--definition { border-color:#b7d8cf;background:#f4fbf8 }
431
457
  .sm-node--measurement { border-color:#c7d2fe;background:#f7f8ff }
432
458
  .sm-node--ok { border-color:rgba(31,157,99,.38);background:rgba(31,157,99,.06) }
@@ -440,6 +466,18 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
440
466
  .sm-node-meta { font-size:10.5px;color:#637083;line-height:1.35;word-break:break-word }
441
467
  .sm-node--skill .sm-node-meta { font-size:11.5px }
442
468
  .sm-node-kind { font-size:9.5px;text-transform:uppercase;letter-spacing:0;color:#8792a4;font-family:"SF Mono",Menlo,monospace;line-height:1 }
469
+ .sm-node--group { width:164px;min-height:66px;border-color:rgba(47,125,104,.42);background:linear-gradient(180deg,#fff 0%,#f4fbf8 100%);box-shadow:0 12px 28px rgba(47,125,104,.1) }
470
+ .sm-node--group .sm-node-title { font-size:13px }
471
+ .sm-node-toggle-icon { position:absolute;right:7px;top:7px;width:17px;height:17px;border:1px solid rgba(47,125,104,.24);border-radius:50%;background:rgba(255,255,255,.72);box-shadow:0 4px 10px rgba(47,125,104,.06);color:#2f7d68;display:flex;align-items:center;justify-content:center;transition:background .14s ease,border-color .14s ease,box-shadow .14s ease }
472
+ .sm-node-toggle-icon::before { content:'';width:5px;height:5px;border-right:1.8px solid currentColor;border-bottom:1.8px solid currentColor;transform:translateY(-1px) rotate(45deg);transition:transform .14s ease }
473
+ .sm-node[data-sm-more-toggle]:hover .sm-node-toggle-icon,.sm-node[data-sm-more-toggle]:focus-visible .sm-node-toggle-icon { background:#fff;border-color:rgba(47,125,104,.42);box-shadow:0 5px 12px rgba(47,125,104,.11) }
474
+ .sm-node[data-sm-more-toggle][aria-expanded="true"] .sm-node-toggle-icon { background:rgba(47,125,104,.1);border-color:rgba(47,125,104,.34);box-shadow:0 4px 10px rgba(47,125,104,.08) }
475
+ .sm-node[data-sm-more-toggle][aria-expanded="true"] .sm-node-toggle-icon::before { transform:translateY(1px) rotate(225deg) }
476
+ .sm-node-signal { align-self:center;max-width:100%;padding:1px 6px;border-radius:9px;font-size:9.5px;font-weight:700;line-height:1.25;color:#637083;background:#eef2f7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis }
477
+ .sm-node-signal--failed { color:#dc2626;background:rgba(220,38,38,.12) }
478
+ .sm-node-signal--warning { color:#d97706;background:rgba(217,119,6,.13) }
479
+ .sm-node-signal--declared { color:#1f9d63;background:rgba(31,157,99,.13) }
480
+ .sm-node-signal--undeclared { color:#637083;background:#eef2f7;border:1px dashed #cbd5e1 }
443
481
  .sm-stage-rail { display:grid;grid-template-columns:1fr auto 1fr auto 1fr;align-items:center;gap:8px;padding:10px 12px;border:1px solid #e4e8f1;border-radius:8px;background:#fff }
444
482
  .sm-stage-card { min-height:50px;padding:9px 11px;border:1px solid #dbe2ee;border-radius:8px;background:#fbfcff;text-align:center;display:flex;flex-direction:column;justify-content:center;gap:3px }
445
483
  .sm-stage-card--ok { border-color:rgba(31,157,99,.38);background:rgba(31,157,99,.06) }
@@ -449,6 +487,16 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
449
487
  .sm-stage-k { font-size:10px;text-transform:uppercase;letter-spacing:0;color:#7b8798;font-family:"SF Mono",Menlo,monospace;line-height:1 }
450
488
  .sm-stage-v { font-size:13px;font-weight:700;color:#182033;line-height:1.35 }
451
489
  .sm-stage-arrow { color:#b8c5d8;font-size:18px;line-height:1;text-align:center }
490
+ .sm-detail { display:grid;grid-template-columns:minmax(180px,1.1fr) minmax(260px,2fr);gap:12px;align-items:start;padding:12px;border:1px solid #e4e8f1;border-radius:8px;background:linear-gradient(180deg,#fff 0%,#fbfcff 100%) }
491
+ .sm-detail-head { min-width:0 }
492
+ .sm-detail-kind { display:inline-flex;align-items:center;min-height:20px;padding:0 7px;border-radius:10px;background:#eef2ff;color:#4f46e5;font-size:10.5px;font-weight:700;line-height:1 }
493
+ .sm-detail-title { margin-top:8px;font-size:15px;font-weight:700;line-height:1.35;color:#182033;word-break:break-word }
494
+ .sm-detail-subtitle { margin-top:3px;font-size:12px;line-height:1.45;color:#637083;word-break:break-word }
495
+ .sm-detail-list { margin:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px }
496
+ .sm-detail-row { min-width:0;padding:8px 10px;border:1px solid #eef2f7;border-radius:7px;background:#fff }
497
+ .sm-detail-row[hidden] { display:none !important }
498
+ .sm-detail-row dt { margin:0 0 3px;font-size:10.5px;color:#8792a4;font-weight:600;line-height:1.2 }
499
+ .sm-detail-row dd { margin:0;font-size:12px;color:#384255;line-height:1.45;word-break:break-word }
452
500
  .sm-card { border:1px solid #e4e8f1;border-radius:8px;background:#fff;overflow:hidden }
453
501
  .sm-card > summary { cursor:pointer;list-style:none;padding:10px 12px;font-size:12.5px;font-weight:600;color:#4f46e5;user-select:none }
454
502
  .sm-card > summary::-webkit-details-marker { display:none }
@@ -457,9 +505,12 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
457
505
  .sm-card textarea { width:100%;min-height:260px;border:0;border-top:1px solid #e4e8f1;background:#fbfcff;padding:12px;font-family:"SF Mono",Menlo,monospace;font-size:11.5px;line-height:1.55;color:#182033;resize:vertical;box-sizing:border-box }
458
506
  @media(max-width:900px){
459
507
  .sm-toolbar { align-items:flex-start }
508
+ .sm-evidence-strip { grid-template-columns:1fr }
460
509
  .sm-stage-rail { grid-template-columns:1fr;gap:6px }
461
510
  .sm-stage-arrow { transform:rotate(90deg);font-size:16px }
462
511
  .sm-canvas { height:620px;min-height:620px }
512
+ .sm-detail { grid-template-columns:1fr }
513
+ .sm-detail-list { grid-template-columns:1fr }
463
514
  .sm-node { width:142px }
464
515
  .sm-node--skill { width:168px }
465
516
  }
@@ -846,6 +897,7 @@ const SKILL_MAP_INIT_SCRIPT = `
846
897
  var stage = map.querySelector('[data-sm-graph-stage]');
847
898
  var graph = map.querySelector('[data-sm-graph]');
848
899
  var label = map.querySelector('[data-sm-zoom-label]');
900
+ var detail = map.querySelector('[data-sm-detail]');
849
901
  if (!viewport || !stage || !graph) return;
850
902
  var baseWidth = Number(map.getAttribute('data-sm-base-width')) || 1100;
851
903
  var baseHeight = Number(map.getAttribute('data-sm-base-height')) || 560;
@@ -854,6 +906,8 @@ const SKILL_MAP_INIT_SCRIPT = `
854
906
  var zoom = 1;
855
907
  var dragState = null;
856
908
  var suppressMoreClickUntil = 0;
909
+ var selectedNodeId = '';
910
+ var currentView = map.getAttribute('data-sm-view') || 'boundary';
857
911
 
858
912
  function edgePoint(fromX, fromY, toX, toY, box){
859
913
  var dx = toX - fromX;
@@ -902,6 +956,92 @@ const SKILL_MAP_INIT_SCRIPT = `
902
956
  node.style.top = y + 'px';
903
957
  }
904
958
 
959
+ function nodeViewPosition(node, view){
960
+ var prefix = view === 'evidence' ? 'data-sm-evidence' : 'data-sm-boundary';
961
+ var x = Number(node.getAttribute(prefix + '-x'));
962
+ var y = Number(node.getAttribute(prefix + '-y'));
963
+ if (Number.isFinite(x) && Number.isFinite(y)) return { x: x, y: y };
964
+ return {
965
+ x: Number(node.getAttribute('data-sm-origin-x')) || Number(node.getAttribute('data-sm-x')) || parseFloat(node.style.left) || 0,
966
+ y: Number(node.getAttribute('data-sm-origin-y')) || Number(node.getAttribute('data-sm-y')) || parseFloat(node.style.top) || 0
967
+ };
968
+ }
969
+
970
+ function hideFilteredNode(node){
971
+ node.hidden = true;
972
+ node.toggleAttribute('hidden', true);
973
+ node.setAttribute('aria-hidden', 'true');
974
+ node.style.display = 'none';
975
+ }
976
+
977
+ function revealNode(node, transient){
978
+ node.hidden = false;
979
+ node.toggleAttribute('hidden', false);
980
+ node.setAttribute('aria-hidden', 'false');
981
+ node.style.display = '';
982
+ node.setAttribute('data-sm-filter-hidden', '0');
983
+ if (transient) node.setAttribute('data-sm-evidence-revealed', '1');
984
+ }
985
+
986
+ function detailText(node, attr, fallback){
987
+ var value = node ? node.getAttribute(attr) : '';
988
+ return value && value.trim() ? value : (fallback || '');
989
+ }
990
+
991
+ function setDetailValue(key, value, required){
992
+ if (!detail) return;
993
+ var row = detail.querySelector('[data-sm-detail-row="' + key + '"]');
994
+ var valueEl = detail.querySelector('[data-sm-detail-value="' + key + '"]');
995
+ var hasValue = !!(value && value.trim());
996
+ if (valueEl) valueEl.textContent = hasValue ? value : '—';
997
+ if (row) {
998
+ var hidden = !hasValue && !required;
999
+ row.hidden = hidden;
1000
+ row.toggleAttribute('hidden', hidden);
1001
+ }
1002
+ }
1003
+
1004
+ function selectedMoreStatus(node){
1005
+ if (!node || !node.hasAttribute('data-sm-more-toggle')) return '';
1006
+ var attr = node.getAttribute('aria-expanded') === 'true'
1007
+ ? 'data-sm-detail-expanded-status'
1008
+ : 'data-sm-detail-collapsed-status';
1009
+ return node.getAttribute(attr) || '';
1010
+ }
1011
+
1012
+ function setSelectedNode(node){
1013
+ if (!node || node.hidden) return;
1014
+ var nodeId = node.getAttribute('data-sm-node-id') || '';
1015
+ selectedNodeId = nodeId;
1016
+ graph.querySelectorAll('.sm-node.is-selected').forEach(function(item){
1017
+ item.classList.remove('is-selected');
1018
+ item.setAttribute('aria-pressed', 'false');
1019
+ });
1020
+ node.classList.add('is-selected');
1021
+ 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);
1035
+ }
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
+ updateEvidenceFocus();
1043
+ }
1044
+
905
1045
  function updateEdgesForNode(nodeId, x, y){
906
1046
  map.querySelectorAll('[data-sm-edge-to]').forEach(function(edge){
907
1047
  if (edge.getAttribute('data-sm-edge-to') !== nodeId) return;
@@ -951,13 +1091,16 @@ const SKILL_MAP_INIT_SCRIPT = `
951
1091
  }
952
1092
 
953
1093
  function resetNodePositions(){
1094
+ applyViewPositions(currentView);
1095
+ }
1096
+
1097
+ function applyViewPositions(view){
954
1098
  graph.querySelectorAll('[data-sm-draggable="1"]').forEach(function(node){
955
1099
  var nodeId = node.getAttribute('data-sm-node-id');
956
- var originX = Number(node.getAttribute('data-sm-origin-x')) || Number(node.getAttribute('data-sm-x')) || parseFloat(node.style.left) || 0;
957
- var originY = Number(node.getAttribute('data-sm-origin-y')) || Number(node.getAttribute('data-sm-y')) || parseFloat(node.style.top) || 0;
958
- setNodePosition(node, originX, originY);
959
- if (nodeId) updateEdgesForNode(nodeId, originX, originY);
960
- if (nodeId) updateEdgesFromNode(nodeId, originX, originY);
1100
+ var next = nodeViewPosition(node, view);
1101
+ setNodePosition(node, next.x, next.y);
1102
+ if (nodeId) updateEdgesForNode(nodeId, next.x, next.y);
1103
+ if (nodeId) updateEdgesFromNode(nodeId, next.x, next.y);
961
1104
  });
962
1105
  }
963
1106
 
@@ -989,24 +1132,60 @@ const SKILL_MAP_INIT_SCRIPT = `
989
1132
  viewport.scrollTop = Math.max(0, scaledRootY - viewport.clientHeight / 2);
990
1133
  }
991
1134
 
992
- function layerEnabled(layer){
993
- var input = map.querySelector('[data-sm-toggle-layer="' + layer + '"]');
994
- return !input || input.checked;
995
- }
996
-
997
1135
  function overflowGroupExpanded(group){
998
1136
  if (!group) return true;
999
1137
  var toggle = map.querySelector('[data-sm-more-toggle="' + group + '"]');
1000
1138
  return !!toggle && toggle.getAttribute('aria-expanded') === 'true';
1001
1139
  }
1002
1140
 
1141
+ function updateViewButtons(){
1142
+ map.setAttribute('data-sm-view', currentView);
1143
+ map.querySelectorAll('[data-sm-view-button]').forEach(function(button){
1144
+ var active = button.getAttribute('data-sm-view-button') === currentView;
1145
+ button.classList.toggle('is-active', active);
1146
+ button.setAttribute('aria-pressed', active ? 'true' : 'false');
1147
+ });
1148
+ }
1149
+
1150
+ function updateEvidenceFocus(){
1151
+ if (!graph) return;
1152
+ graph.querySelectorAll('[data-sm-evidence-revealed="1"]').forEach(function(node){
1153
+ node.removeAttribute('data-sm-evidence-revealed');
1154
+ if (node.getAttribute('data-sm-layer') === 'definition' && currentView === 'evidence') hideFilteredNode(node);
1155
+ });
1156
+ graph.querySelectorAll('.sm-node.is-related').forEach(function(node){
1157
+ node.classList.remove('is-related');
1158
+ });
1159
+ map.querySelectorAll('[data-sm-evidence-edge="1"]').forEach(function(edge){
1160
+ var toId = edge.getAttribute('data-sm-edge-to');
1161
+ var fromId = edge.getAttribute('data-sm-edge-from-node');
1162
+ var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
1163
+ var fromNode = fromId ? graph.querySelector('[data-sm-node-id="' + fromId + '"]') : null;
1164
+ var focused = currentView === 'evidence' && !!selectedNodeId && (toId === selectedNodeId || fromId === selectedNodeId);
1165
+ if (focused) {
1166
+ if (toNode) revealNode(toNode, true);
1167
+ if (fromNode) revealNode(fromNode, true);
1168
+ }
1169
+ var endpointHidden = (toNode && toNode.hidden) || (fromNode && fromNode.hidden);
1170
+ var shouldHide = !focused || endpointHidden;
1171
+ edge.hidden = shouldHide;
1172
+ edge.toggleAttribute('hidden', shouldHide);
1173
+ edge.setAttribute('aria-hidden', shouldHide ? 'true' : 'false');
1174
+ edge.style.display = shouldHide ? 'none' : '';
1175
+ if (!shouldHide) {
1176
+ if (toNode && toId !== selectedNodeId) toNode.classList.add('is-related');
1177
+ if (fromNode && fromId !== selectedNodeId) fromNode.classList.add('is-related');
1178
+ }
1179
+ });
1180
+ }
1181
+
1003
1182
  function updateVisibility(){
1004
1183
  var showLeaves = true;
1005
1184
  var leafToggle = map.querySelector('[data-sm-toggle-leaves]');
1006
1185
  if (leafToggle) showLeaves = leafToggle.checked;
1007
1186
  var layers = {
1008
- definition: layerEnabled('definition'),
1009
- measurement: layerEnabled('measurement')
1187
+ definition: currentView === 'boundary',
1188
+ measurement: currentView === 'evidence'
1010
1189
  };
1011
1190
  map.querySelectorAll('[data-sm-layer]').forEach(function(el){
1012
1191
  var layer = el.getAttribute('data-sm-layer');
@@ -1020,6 +1199,7 @@ const SKILL_MAP_INIT_SCRIPT = `
1020
1199
  el.style.display = shouldHide ? 'none' : '';
1021
1200
  });
1022
1201
  map.querySelectorAll('[data-sm-edge-to]').forEach(function(edge){
1202
+ if (edge.getAttribute('data-sm-evidence-edge') === '1') return;
1023
1203
  var toId = edge.getAttribute('data-sm-edge-to');
1024
1204
  var fromId = edge.getAttribute('data-sm-edge-from-node');
1025
1205
  var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
@@ -1032,6 +1212,23 @@ const SKILL_MAP_INIT_SCRIPT = `
1032
1212
  edge.setAttribute('aria-hidden', shouldHideEdge ? 'true' : 'false');
1033
1213
  edge.style.display = shouldHideEdge ? 'none' : '';
1034
1214
  });
1215
+ if (selectedNodeId) {
1216
+ var selectedNode = graph.querySelector('[data-sm-node-id="' + selectedNodeId + '"]');
1217
+ if (selectedNode && selectedNode.hidden) {
1218
+ var rootNode = graph.querySelector('[data-sm-root]');
1219
+ if (rootNode) setSelectedNode(rootNode);
1220
+ }
1221
+ }
1222
+ updateEvidenceFocus();
1223
+ }
1224
+
1225
+ function setView(view){
1226
+ currentView = view === 'evidence' ? 'evidence' : 'boundary';
1227
+ applyViewPositions(currentView);
1228
+ updateViewButtons();
1229
+ updateVisibility();
1230
+ refreshEdges();
1231
+ centerRoot();
1035
1232
  }
1036
1233
 
1037
1234
  function setMoreExpanded(toggle, expanded){
@@ -1039,11 +1236,11 @@ const SKILL_MAP_INIT_SCRIPT = `
1039
1236
  var nextLabel = expanded
1040
1237
  ? toggle.getAttribute('data-sm-expanded-label')
1041
1238
  : toggle.getAttribute('data-sm-collapsed-label');
1042
- var title = toggle.querySelector('.sm-node-title');
1043
- if (title && nextLabel) title.textContent = nextLabel;
1044
1239
  if (nextLabel) toggle.setAttribute('title', nextLabel);
1240
+ if (nextLabel) toggle.setAttribute('aria-label', nextLabel);
1045
1241
  updateVisibility();
1046
1242
  refreshEdges();
1243
+ if (selectedNodeId && toggle.getAttribute('data-sm-node-id') === selectedNodeId) setSelectedNode(toggle);
1047
1244
  }
1048
1245
 
1049
1246
  function collapseMoreGroups(){
@@ -1067,7 +1264,13 @@ const SKILL_MAP_INIT_SCRIPT = `
1067
1264
  });
1068
1265
  });
1069
1266
 
1070
- map.querySelectorAll('[data-sm-toggle-layer], [data-sm-toggle-leaves]').forEach(function(input){
1267
+ map.querySelectorAll('[data-sm-view-button]').forEach(function(button){
1268
+ button.addEventListener('click', function(){
1269
+ setView(button.getAttribute('data-sm-view-button') || 'boundary');
1270
+ });
1271
+ });
1272
+
1273
+ map.querySelectorAll('[data-sm-toggle-leaves]').forEach(function(input){
1071
1274
  input.addEventListener('change', updateVisibility);
1072
1275
  });
1073
1276
 
@@ -1078,11 +1281,13 @@ const SKILL_MAP_INIT_SCRIPT = `
1078
1281
  return;
1079
1282
  }
1080
1283
  ev.preventDefault();
1284
+ setSelectedNode(toggle);
1081
1285
  setMoreExpanded(toggle, toggle.getAttribute('aria-expanded') !== 'true');
1082
1286
  });
1083
1287
  toggle.addEventListener('keydown', function(ev){
1084
1288
  if (ev.key !== 'Enter' && ev.key !== ' ') return;
1085
1289
  ev.preventDefault();
1290
+ setSelectedNode(toggle);
1086
1291
  setMoreExpanded(toggle, toggle.getAttribute('aria-expanded') !== 'true');
1087
1292
  });
1088
1293
  });
@@ -1094,6 +1299,7 @@ const SKILL_MAP_INIT_SCRIPT = `
1094
1299
  var nodeId = node.getAttribute('data-sm-node-id');
1095
1300
  if (!nodeId) return;
1096
1301
  ev.preventDefault();
1302
+ setSelectedNode(node);
1097
1303
  dragState = {
1098
1304
  node: node,
1099
1305
  nodeId: nodeId,
@@ -1142,6 +1348,19 @@ const SKILL_MAP_INIT_SCRIPT = `
1142
1348
  node.addEventListener('pointercancel', finishDrag);
1143
1349
  });
1144
1350
 
1351
+ graph.querySelectorAll('[data-sm-node-id]').forEach(function(node){
1352
+ node.addEventListener('click', function(){
1353
+ if (node.hasAttribute('data-sm-more-toggle') && Date.now() < suppressMoreClickUntil) return;
1354
+ setSelectedNode(node);
1355
+ });
1356
+ node.addEventListener('keydown', function(ev){
1357
+ if (ev.key !== 'Enter' && ev.key !== ' ') return;
1358
+ if (node.hasAttribute('data-sm-more-toggle')) return;
1359
+ ev.preventDefault();
1360
+ setSelectedNode(node);
1361
+ });
1362
+ });
1363
+
1145
1364
  viewport.addEventListener('wheel', function(ev){
1146
1365
  if (!ev.ctrlKey && !ev.metaKey) return;
1147
1366
  ev.preventDefault();
@@ -1150,8 +1369,11 @@ const SKILL_MAP_INIT_SCRIPT = `
1150
1369
  setZoom(zoom + delta, ev.clientX - rect.left, ev.clientY - rect.top);
1151
1370
  }, { passive: false });
1152
1371
 
1372
+ updateViewButtons();
1153
1373
  applyZoom();
1154
1374
  updateVisibility();
1375
+ var initialRoot = graph.querySelector('[data-sm-root]');
1376
+ if (initialRoot) setSelectedNode(initialRoot);
1155
1377
  requestAnimationFrame(function(){
1156
1378
  refreshEdges();
1157
1379
  centerRoot();
@@ -1601,6 +1823,7 @@ function nodeKindLabel(kind, lang) {
1601
1823
  eval_result: ['eval', 'eval'],
1602
1824
  trace_session: ['observe', 'observe'],
1603
1825
  more: ['more', 'more'],
1826
+ knowledge_group: ['group', 'group'],
1604
1827
  };
1605
1828
  const pair = labels[kind] ?? [kind, kind];
1606
1829
  return zh ? pair[0] : pair[1];
@@ -1644,25 +1867,86 @@ function previewNodes(nodes, allowedKinds, limit) {
1644
1867
  const SKILL_MAP_WIDTH = 1100;
1645
1868
  const SKILL_MAP_HEIGHT = 560;
1646
1869
  const SKILL_MAP_ROOT_ID = 'skill-root';
1647
- const SKILL_MAP_ROOT = { x: 470, y: 260 };
1870
+ const SKILL_MAP_BOUNDARY_ROOT = { x: 560, y: 285 };
1871
+ const SKILL_MAP_EVIDENCE_ROOT = { x: 470, y: 260 };
1648
1872
  const SKILL_MAP_ROOT_SIZE = { width: 172, height: 86 };
1649
1873
  const SKILL_MAP_NODE_SIZE = { width: 148, height: 54 };
1650
- const SKILL_MAP_DEFINITION_COORDS = [
1651
- { x: 170, y: 112 },
1652
- { x: 140, y: 255 },
1653
- { x: 220, y: 420 },
1654
- { x: 392, y: 112 },
1655
- { x: 382, y: 425 },
1656
- { x: 312, y: 190 },
1657
- { x: 310, y: 328 },
1874
+ const SKILL_MAP_DEFINITION_GROUPS = [
1875
+ {
1876
+ key: 'references',
1877
+ labelZh: '引用材料',
1878
+ labelEn: 'References',
1879
+ descriptionZh: 'Skill 显式依赖的参考文档和知识来源。',
1880
+ descriptionEn: 'Reference docs and knowledge sources the Skill depends on.',
1881
+ nodeKinds: ['reference'],
1882
+ boundaryPosition: { x: 270, y: 225 },
1883
+ evidencePosition: { x: 140, y: 145 },
1884
+ childBoundaryPositions: [
1885
+ { x: 135, y: 125 },
1886
+ { x: 130, y: 325 },
1887
+ { x: 270, y: 435 },
1888
+ { x: 410, y: 125 },
1889
+ { x: 405, y: 325 },
1890
+ ],
1891
+ },
1892
+ {
1893
+ key: 'workflow',
1894
+ labelZh: '执行流程',
1895
+ labelEn: 'Workflow',
1896
+ descriptionZh: 'Skill 期望代理遵循的流程与步骤。',
1897
+ descriptionEn: 'Workflows and steps the agent is expected to follow.',
1898
+ nodeKinds: ['workflow', 'workflow_node'],
1899
+ boundaryPosition: { x: 850, y: 225 },
1900
+ evidencePosition: { x: 960, y: 145 },
1901
+ childBoundaryPositions: [
1902
+ { x: 990, y: 125 },
1903
+ { x: 990, y: 325 },
1904
+ { x: 850, y: 435 },
1905
+ { x: 710, y: 125 },
1906
+ { x: 715, y: 325 },
1907
+ ],
1908
+ },
1909
+ {
1910
+ key: 'rules',
1911
+ labelZh: '约束规则',
1912
+ labelEn: 'Rules',
1913
+ descriptionZh: 'Skill 中不可违反的硬规则和输出约束。',
1914
+ descriptionEn: 'Hard rules and output constraints that must not be violated.',
1915
+ nodeKinds: ['hard_rule'],
1916
+ boundaryPosition: { x: 560, y: 105 },
1917
+ evidencePosition: { x: 140, y: 285 },
1918
+ childBoundaryPositions: [
1919
+ { x: 360, y: 80 },
1920
+ { x: 760, y: 80 },
1921
+ { x: 560, y: 30 },
1922
+ { x: 360, y: 160 },
1923
+ { x: 760, y: 160 },
1924
+ ],
1925
+ },
1926
+ ];
1927
+ const SKILL_MAP_BOUNDARY_ASSET_COORDS = [
1928
+ { x: 380, y: 455 },
1929
+ { x: 740, y: 455 },
1930
+ { x: 205, y: 455 },
1931
+ { x: 915, y: 455 },
1932
+ { x: 560, y: 510 },
1933
+ ];
1934
+ 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 },
1658
1942
  ];
1659
- const SKILL_MAP_MEASUREMENT_COORDS = [
1660
- { x: 720, y: 105 },
1661
- { x: 920, y: 165 },
1662
- { x: 750, y: 275 },
1663
- { x: 948, y: 318 },
1664
- { x: 770, y: 430 },
1665
- { x: 960, y: 430 },
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 },
1666
1950
  ];
1667
1951
  const SKILL_MAP_NODE_MARGIN = { x: 92, y: 48 };
1668
1952
  const SKILL_MAP_STATUS_CLASSES = new Set(['ok', 'warning', 'failed', 'skipped', 'unknown', 'not_measured']);
@@ -1677,29 +1961,298 @@ function graphNodeCoverageClass(node) {
1677
1961
  return '';
1678
1962
  }
1679
1963
  function graphNodeCoverageLabel(node, lang) {
1964
+ const groupSummary = skillMapGroupSummary(node);
1965
+ if (groupSummary) {
1966
+ return lang === 'zh'
1967
+ ? `${groupSummary.declared}/${groupSummary.total} 个节点已被评测用例声明`
1968
+ : `${groupSummary.declared}/${groupSummary.total} nodes declared by eval samples`;
1969
+ }
1680
1970
  if (node.coverage === 'declared')
1681
1971
  return lang === 'zh' ? '已由评测用例声明' : 'declared by eval samples';
1682
1972
  if (node.coverage === 'undeclared')
1683
1973
  return lang === 'zh' ? '尚未由评测用例声明' : 'not declared by eval samples';
1684
1974
  return '';
1685
1975
  }
1976
+ function graphNodeEvidenceLabel(node, lang) {
1977
+ const zh = lang === 'zh';
1978
+ const groupSummary = skillMapGroupSummary(node);
1979
+ if (groupSummary) {
1980
+ return zh
1981
+ ? `包含 ${groupSummary.total} 个结构节点,其中 ${groupSummary.declared} 个已有评测用例声明。`
1982
+ : `Contains ${groupSummary.total} structure nodes; ${groupSummary.declared} are declared by eval samples.`;
1983
+ }
1984
+ if (node.coveredBySamples?.length) {
1985
+ const samples = node.coveredBySamples.slice(0, 3).join(zh ? '、' : ', ');
1986
+ const more = node.coveredBySamples.length > 3 ? (zh ? ` 等 ${node.coveredBySamples.length} 条用例` : ` and ${node.coveredBySamples.length} samples`) : '';
1987
+ return zh ? `由 ${samples}${more} 声明覆盖` : `Declared by ${samples}${more}`;
1988
+ }
1989
+ if (node.coverage === 'undeclared')
1990
+ return zh ? '暂无评测用例显式声明覆盖' : 'No eval sample explicitly declares this node yet';
1991
+ if (node.status === 'failed')
1992
+ return zh ? '当前测量证据失败' : 'Current measurement evidence failed';
1993
+ if (node.status === 'warning')
1994
+ return zh ? '当前测量证据有告警' : 'Current measurement evidence has warnings';
1995
+ if (node.nodeKind === 'sample')
1996
+ return zh ? '这条用例用于测量当前 Skill' : 'This sample measures the current Skill';
1997
+ if (node.nodeKind === 'assertion')
1998
+ return zh ? '断言用于把用例输出转成可比较证据' : 'Assertion turns sample output into comparable evidence';
1999
+ return '';
2000
+ }
2001
+ function graphNodeActionLabel(node, lang) {
2002
+ const zh = lang === 'zh';
2003
+ if (node.nodeKind === 'knowledge_group') {
2004
+ return zh
2005
+ ? '点击分组展开叶子节点;优先检查未声明或失败证据集中的节点。'
2006
+ : 'Open the group to inspect leaves; prioritize undeclared nodes or nodes linked to failures.';
2007
+ }
2008
+ if (node.status === 'failed' && node.nodeKind === 'sample') {
2009
+ return zh ? '优先查看这条失败用例的输出,确认是 Skill 缺知识、流程缺口还是断言过严。' : 'Inspect this failed sample first and decide whether the Skill, workflow, or assertion needs work.';
2010
+ }
2011
+ if (node.status === 'failed' && node.nodeKind === 'assertion') {
2012
+ return zh ? '回到对应用例,检查失败断言期望是否与 Skill 边界一致。' : 'Go back to the sample and verify whether the failed assertion matches the Skill boundary.';
2013
+ }
2014
+ if (node.coverage === 'undeclared') {
2015
+ return zh ? '如果这是关键知识边界,给代表性评测用例补 `sample.covers`;否则可保持未声明。' : 'If this is a key boundary, add sample.covers to a representative eval sample; otherwise leave it undeclared.';
2016
+ }
2017
+ if (node.coveredBySamples?.length) {
2018
+ return zh ? '优先看覆盖它的用例是否失败;失败集中时,回到这条知识边界修 Skill。' : 'Check whether samples covering this node fail; clustered failures point back to this Skill boundary.';
2019
+ }
2020
+ return '';
2021
+ }
2022
+ function graphNodeDetailKindLabel(nodeKind, lang) {
2023
+ const zh = lang === 'zh';
2024
+ const labels = {
2025
+ skill: ['Skill 根节点', 'Skill root'],
2026
+ skill_file: ['Skill 文件', 'Skill file'],
2027
+ frontmatter: ['Frontmatter', 'Frontmatter'],
2028
+ reference: ['引用文档', 'Reference'],
2029
+ script: ['脚本', 'Script'],
2030
+ tool: ['工具', 'Tool'],
2031
+ hard_rule: ['硬规则', 'Hard rule'],
2032
+ workflow: ['流程', 'Workflow'],
2033
+ workflow_node: ['流程节点', 'Workflow node'],
2034
+ sample: ['评测用例', 'Eval sample'],
2035
+ assertion: ['断言', 'Assertion'],
2036
+ eval_result: ['评测结果', 'Eval result'],
2037
+ diagnostic: ['诊断', 'Diagnostic'],
2038
+ more: ['折叠分组', 'Collapsed group'],
2039
+ knowledge_group: ['知识分组', 'Knowledge group'],
2040
+ };
2041
+ const pair = labels[nodeKind] ?? [nodeKind, nodeKind];
2042
+ return zh ? pair[0] : pair[1];
2043
+ }
2044
+ function graphNodeStatusLabel(status, lang) {
2045
+ if (!status)
2046
+ return '';
2047
+ const zh = lang === 'zh';
2048
+ const labels = {
2049
+ ok: ['正常', 'ok'],
2050
+ warning: ['告警', 'warning'],
2051
+ failed: ['失败', 'failed'],
2052
+ skipped: ['已跳过', 'skipped'],
2053
+ unknown: ['未知', 'unknown'],
2054
+ not_measured: ['未测量', 'not measured'],
2055
+ };
2056
+ const pair = labels[status];
2057
+ if (!pair)
2058
+ return '';
2059
+ return zh ? pair[0] : pair[1];
2060
+ }
2061
+ function skillMapLayerLabel(layer, lang) {
2062
+ if (layer === 'definition')
2063
+ return lang === 'zh' ? '结构定义' : 'Definition';
2064
+ return lang === 'zh' ? '评测测量' : 'Measurement';
2065
+ }
2066
+ function skillMapGroupSummary(node) {
2067
+ return node.groupSummary;
2068
+ }
2069
+ function skillMapGroupDescription(node) {
2070
+ return node.groupDescription ?? '';
2071
+ }
2072
+ function graphNodeDetailSource(node, lang) {
2073
+ const zh = lang === 'zh';
2074
+ switch (node.nodeKind) {
2075
+ case 'knowledge_group':
2076
+ return skillMapGroupDescription(node);
2077
+ case 'reference':
2078
+ case 'script':
2079
+ return node.label;
2080
+ case 'workflow':
2081
+ return `${zh ? 'workflow id' : 'workflow id'}: ${node.label}`;
2082
+ case 'workflow_node':
2083
+ return `${zh ? 'workflow node' : 'workflow node'}: ${node.label}`;
2084
+ case 'hard_rule':
2085
+ return `${zh ? 'rule id' : 'rule id'}: ${node.label}`;
2086
+ case 'sample':
2087
+ return `${zh ? 'sample_id' : 'sample_id'}: ${node.label}`;
2088
+ case 'assertion':
2089
+ case 'diagnostic':
2090
+ case 'eval_result':
2091
+ case 'tool':
2092
+ case 'frontmatter':
2093
+ return node.label;
2094
+ default:
2095
+ return '';
2096
+ }
2097
+ }
2098
+ function attr(name, value) {
2099
+ return value ? ` ${name}="${e(value)}"` : '';
2100
+ }
2101
+ function renderSkillMapDetailAttrs(item, lang) {
2102
+ const coverageLabel = graphNodeCoverageLabel(item.node, lang);
2103
+ const isMore = item.node.nodeKind === 'more';
2104
+ const isGroup = item.node.nodeKind === 'knowledge_group';
2105
+ const groupSummary = skillMapGroupSummary(item.node);
2106
+ const scope = skillMapLayerLabel(item.layer, lang);
2107
+ const detailTitle = item.presentation?.detailTitle ?? item.presentation?.title;
2108
+ const status = isMore
2109
+ ? (lang === 'zh' ? '已收起' : 'collapsed')
2110
+ : isGroup && groupSummary
2111
+ ? (lang === 'zh' ? `${groupSummary.total} 个节点` : `${groupSummary.total} nodes`)
2112
+ : graphNodeStatusLabel(item.node.status, lang);
2113
+ return [
2114
+ attr('data-sm-detail-kind', graphNodeDetailKindLabel(item.node.nodeKind, lang)),
2115
+ attr('data-sm-detail-title', isMore ? item.node.label : detailTitle ?? graphNodeDisplayLabel(item.node, lang)),
2116
+ attr('data-sm-detail-subtitle', isMore ? scope : graphNodeDetailSource(item.node, lang)),
2117
+ attr('data-sm-detail-scope', scope),
2118
+ attr('data-sm-detail-source', graphNodeDetailSource(item.node, lang)),
2119
+ attr('data-sm-detail-coverage', coverageLabel),
2120
+ attr('data-sm-detail-status', status),
2121
+ attr('data-sm-detail-evidence', graphNodeEvidenceLabel(item.node, lang)),
2122
+ attr('data-sm-detail-action', graphNodeActionLabel(item.node, lang)),
2123
+ ...(item.moreToggle ? [
2124
+ attr('data-sm-detail-collapsed-status', lang === 'zh' ? '已收起' : 'collapsed'),
2125
+ attr('data-sm-detail-expanded-status', lang === 'zh' ? '已展开' : 'expanded'),
2126
+ ] : []),
2127
+ ].join('');
2128
+ }
2129
+ function graphNodeSignalLabel(node, lang) {
2130
+ const zh = lang === 'zh';
2131
+ const groupSummary = skillMapGroupSummary(node);
2132
+ if (groupSummary)
2133
+ return zh ? `${groupSummary.declared}/${groupSummary.total} 已声明` : `${groupSummary.declared}/${groupSummary.total} declared`;
2134
+ if (node.status === 'failed')
2135
+ return zh ? '失败证据' : 'failed';
2136
+ if (node.status === 'warning')
2137
+ return zh ? '告警' : 'warning';
2138
+ if (node.coverage === 'declared')
2139
+ return zh ? '已声明覆盖' : 'declared';
2140
+ if (node.coverage === 'undeclared')
2141
+ return zh ? '未声明' : 'undeclared';
2142
+ return '';
2143
+ }
2144
+ function graphNodeSignalClass(node) {
2145
+ const groupSummary = skillMapGroupSummary(node);
2146
+ if (groupSummary)
2147
+ return groupSummary.declared === groupSummary.total ? ' sm-node-signal--declared' : ' sm-node-signal--undeclared';
2148
+ if (node.status === 'failed')
2149
+ return ' sm-node-signal--failed';
2150
+ if (node.status === 'warning')
2151
+ return ' sm-node-signal--warning';
2152
+ if (node.coverage === 'declared')
2153
+ return ' sm-node-signal--declared';
2154
+ if (node.coverage === 'undeclared')
2155
+ return ' sm-node-signal--undeclared';
2156
+ return '';
2157
+ }
2158
+ function skillMapRootDetail(entry, coverageSummary, lang) {
2159
+ const zh = lang === 'zh';
2160
+ const failedSamples = entry.eval?.failCount ?? 0;
2161
+ return {
2162
+ kindLabel: graphNodeDetailKindLabel('skill', lang),
2163
+ title: 'SKILL.md',
2164
+ subtitle: entry.skillName,
2165
+ scope: zh ? '中心节点' : 'Root',
2166
+ source: 'SKILL.md',
2167
+ coverage: coverageSummary ?? '',
2168
+ status: [
2169
+ `doctor ${stageText(entry, 'doctor', lang)}`,
2170
+ `eval ${stageText(entry, 'eval', lang)}`,
2171
+ `observe ${stageText(entry, 'observe', lang)}`,
2172
+ ].join(' · '),
2173
+ evidence: coverageSummary ?? (zh ? '尚未形成声明锚点' : 'No declared anchors yet'),
2174
+ action: failedSamples > 0
2175
+ ? (zh ? `先处理 ${failedSamples} 条失败评测用例,再回看其覆盖的知识边界。` : `Start with ${failedSamples} failed eval samples, then inspect the covered Skill boundaries.`)
2176
+ : (zh ? '继续补充关键场景的 sample.covers,让图谱从结构图变成证据图。' : 'Keep adding sample.covers for key scenarios so the map becomes evidence-oriented.'),
2177
+ };
2178
+ }
2179
+ function renderSkillMapRootDetailAttrs(entry, coverageSummary, lang) {
2180
+ const detail = skillMapRootDetail(entry, coverageSummary, lang);
2181
+ return [
2182
+ attr('data-sm-detail-kind', detail.kindLabel),
2183
+ attr('data-sm-detail-title', detail.title),
2184
+ attr('data-sm-detail-subtitle', detail.subtitle),
2185
+ attr('data-sm-detail-scope', detail.scope),
2186
+ attr('data-sm-detail-source', detail.source),
2187
+ attr('data-sm-detail-coverage', detail.coverage),
2188
+ attr('data-sm-detail-status', detail.status),
2189
+ attr('data-sm-detail-evidence', detail.evidence),
2190
+ attr('data-sm-detail-action', detail.action),
2191
+ ].join('');
2192
+ }
2193
+ function renderSkillMapInspector(entry, coverageSummary, lang) {
2194
+ const zh = lang === 'zh';
2195
+ const detail = skillMapRootDetail(entry, coverageSummary, lang);
2196
+ const row = (key, label, value, required = false) => {
2197
+ const hidden = !value && !required ? ' hidden' : '';
2198
+ return `<div class="sm-detail-row" data-sm-detail-row="${e(key)}"${hidden}>
2199
+ <dt>${e(label)}</dt>
2200
+ <dd data-sm-detail-value="${e(key)}">${e(value || '—')}</dd>
2201
+ </div>`;
2202
+ };
2203
+ return `<div class="sm-detail" data-sm-detail>
2204
+ <div class="sm-detail-head">
2205
+ <div class="sm-detail-kind" data-sm-detail-kind-label>${e(detail.kindLabel)}</div>
2206
+ <div class="sm-detail-title" data-sm-detail-title>${e(detail.title)}</div>
2207
+ <div class="sm-detail-subtitle" data-sm-detail-subtitle>${e(detail.subtitle)}</div>
2208
+ </div>
2209
+ <dl class="sm-detail-list">
2210
+ ${row('scope', zh ? '范围' : 'Scope', detail.scope, true)}
2211
+ ${row('source', zh ? '来源' : 'Source', detail.source)}
2212
+ ${row('coverage', zh ? '声明' : 'Declaration', detail.coverage)}
2213
+ ${row('status', zh ? '状态' : 'Status', detail.status)}
2214
+ ${row('evidence', zh ? '证据' : 'Evidence', detail.evidence)}
2215
+ ${row('action', zh ? '下一步' : 'Next', detail.action)}
2216
+ </dl>
2217
+ </div>`;
2218
+ }
1686
2219
  function positionedNodeStyle(node) {
1687
2220
  return `left:${node.x}px;top:${node.y}px`;
1688
2221
  }
2222
+ function renderSkillMapViewPositionAttrs(item) {
2223
+ const attrs = [];
2224
+ if (item.boundaryPosition) {
2225
+ attrs.push(` data-sm-boundary-x="${item.boundaryPosition.x}" data-sm-boundary-y="${item.boundaryPosition.y}"`);
2226
+ }
2227
+ if (item.evidencePosition) {
2228
+ attrs.push(` data-sm-evidence-x="${item.evidencePosition.x}" data-sm-evidence-y="${item.evidencePosition.y}"`);
2229
+ }
2230
+ return attrs.join('');
2231
+ }
1689
2232
  function renderSkillMapNode(item, lang) {
1690
- const title = graphNodeMapLabel(item.node);
2233
+ const title = item.presentation?.title ?? graphNodeMapLabel(item.node);
1691
2234
  const coverageLabel = graphNodeCoverageLabel(item.node, lang);
1692
- const fullTitle = [graphNodeDisplayLabel(item.node, lang), coverageLabel].filter(Boolean).join(' · ');
1693
- const kindClass = item.node.nodeKind === 'more' ? ' sm-node--more' : '';
1694
- const leaf = item.layer === 'definition' || item.layer === 'measurement' ? ' data-sm-leaf="1"' : '';
2235
+ const fullTitle = [
2236
+ item.presentation?.title,
2237
+ graphNodeDisplayLabel(item.node, lang),
2238
+ coverageLabel,
2239
+ ].filter((value, index, values) => Boolean(value) && values.indexOf(value) === index).join(' · ');
2240
+ const signal = graphNodeSignalLabel(item.node, lang);
2241
+ const kindLabel = item.presentation?.hideKind || item.node.nodeKind === 'knowledge_group' ? '' : nodeKindLabel(item.node.nodeKind, lang);
2242
+ const kindClass = item.node.nodeKind === 'more' ? ' sm-node--more' : item.node.nodeKind === 'knowledge_group' ? ' sm-node--group' : '';
2243
+ const isLeaf = item.isLeaf !== false;
2244
+ const leaf = isLeaf ? ' data-sm-leaf="1"' : '';
1695
2245
  const draggable = item.layer === 'definition' || item.layer === 'measurement' ? ' data-sm-draggable="1"' : '';
1696
2246
  const overflow = item.overflowGroup ? ` data-sm-overflow-group="${item.overflowGroup}" hidden aria-hidden="true"` : '';
2247
+ const toggleIcon = item.moreToggle ? '<span class="sm-node-toggle-icon" aria-hidden="true"></span>' : '';
1697
2248
  const moreToggle = item.moreToggle
1698
2249
  ? ` 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"`
1699
- : '';
1700
- 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}"${leaf}${draggable}${overflow}${moreToggle} style="${positionedNodeStyle(item)}" title="${e(fullTitle)}">
1701
- <div class="sm-node-kind">${e(nodeKindLabel(item.node.nodeKind, lang))}</div>
2250
+ : ' 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)}">
2252
+ ${toggleIcon}
2253
+ ${kindLabel ? `<div class="sm-node-kind">${e(kindLabel)}</div>` : ''}
1702
2254
  <div class="sm-node-title">${e(title)}</div>
2255
+ ${signal ? `<div class="sm-node-signal${graphNodeSignalClass(item.node)}">${e(signal)}</div>` : ''}
1703
2256
  </div>`;
1704
2257
  }
1705
2258
  function edgePoint(from, to, box) {
@@ -1718,33 +2271,212 @@ function skillMapPath(from, to, fromBox, toBox) {
1718
2271
  const c2x = end.x - (end.x >= start.x ? dx * 0.45 : -dx * 0.45);
1719
2272
  return `M ${start.x} ${start.y} C ${c1x} ${start.y}, ${c2x} ${end.y}, ${end.x} ${end.y}`;
1720
2273
  }
1721
- function renderSkillMapEdges(nodes, dimensions) {
2274
+ function renderSkillMapEdges(nodes, dimensions, evidenceLinks) {
1722
2275
  const rootEdges = nodes
1723
2276
  .map((node) => {
1724
- const from = node.edgeFrom ?? SKILL_MAP_ROOT;
2277
+ const from = node.edgeFrom ?? SKILL_MAP_BOUNDARY_ROOT;
1725
2278
  const fromBox = node.edgeFrom ? SKILL_MAP_NODE_SIZE : SKILL_MAP_ROOT_SIZE;
1726
2279
  const overflow = node.overflowGroup ? ` data-sm-overflow-group="${node.overflowGroup}" hidden aria-hidden="true"` : '';
1727
- const fromNodeId = node.overflowGroup ? `${node.overflowGroup}-more` : SKILL_MAP_ROOT_ID;
1728
- return `<path class="sm-edge sm-edge--${node.layer}" data-sm-layer="${node.layer}" data-sm-leaf="1" 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>`;
2280
+ const fromNodeId = node.edgeFromId ?? (node.overflowGroup ? `${node.overflowGroup}-more` : SKILL_MAP_ROOT_ID);
2281
+ 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>`;
1729
2283
  })
1730
2284
  .join('');
1731
- return `<svg class="sm-svg" viewBox="0 0 ${dimensions.width} ${dimensions.height}" aria-hidden="true">${rootEdges}</svg>`;
2285
+ 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>`)
2287
+ .join('');
2288
+ return `<svg class="sm-svg" viewBox="0 0 ${dimensions.width} ${dimensions.height}" aria-hidden="true">${rootEdges}${evidenceEdges}</svg>`;
2289
+ }
2290
+ function coverageSamplesByStableKey(evalGraph, lang) {
2291
+ const zh = lang === 'zh';
2292
+ const out = new Map();
2293
+ for (const edge of evalGraph?.declaredCoverageEdges ?? []) {
2294
+ const samples = out.get(edge.targetStableKey) ?? [];
2295
+ const sampleLabel = edge.sampleStatus === 'failed'
2296
+ ? (zh ? `${edge.sampleLabel}(失败)` : `${edge.sampleLabel} (failed)`)
2297
+ : edge.sampleLabel;
2298
+ if (!samples.includes(sampleLabel))
2299
+ samples.push(sampleLabel);
2300
+ out.set(edge.targetStableKey, samples);
2301
+ }
2302
+ return out;
2303
+ }
2304
+ function skillMapEvidenceLinks(nodes, evalGraph) {
2305
+ if (!evalGraph?.declaredCoverageEdges.length)
2306
+ return [];
2307
+ const targetByStableKey = new Map();
2308
+ const sampleByLabel = new Map();
2309
+ for (const node of nodes) {
2310
+ if (node.node.stableKey)
2311
+ targetByStableKey.set(node.node.stableKey, node);
2312
+ if (node.node.nodeKind === 'sample')
2313
+ sampleByLabel.set(node.node.label, node);
2314
+ }
2315
+ return evalGraph.declaredCoverageEdges
2316
+ .map((edge) => {
2317
+ const from = sampleByLabel.get(edge.sampleLabel);
2318
+ const to = targetByStableKey.get(edge.targetStableKey);
2319
+ if (!from || !to)
2320
+ return null;
2321
+ return {
2322
+ fromId: from.id,
2323
+ toId: to.id,
2324
+ from: { x: from.x, y: from.y },
2325
+ to: { x: to.x, y: to.y },
2326
+ ...(edge.sampleStatus === 'failed' ? { failed: true } : {}),
2327
+ };
2328
+ })
2329
+ .filter((link) => Boolean(link));
1732
2330
  }
1733
2331
  function moreNode(label) {
1734
2332
  return { nodeKind: 'more', label };
1735
2333
  }
1736
- function overflowNodePosition(layer, index) {
1737
- const columns = layer === 'definition' ? [112, 270, 428] : [720, 880, 1040];
1738
- const startY = layer === 'definition' ? 500 : 560;
2334
+ function overflowNodePosition(layer, index, view) {
2335
+ const columns = layer === 'definition'
2336
+ ? view === 'boundary' ? [205, 382, 560, 738, 915] : [120, 290, 810, 980]
2337
+ : [250, 430, 610, 790, 970];
2338
+ const startY = layer === 'definition'
2339
+ ? view === 'boundary' ? 570 : 535
2340
+ : 555;
1739
2341
  return {
1740
2342
  x: columns[index % columns.length],
1741
2343
  y: startY + Math.floor(index / columns.length) * 98,
1742
2344
  };
1743
2345
  }
2346
+ function definitionGroupSpecForNode(node) {
2347
+ return SKILL_MAP_DEFINITION_GROUPS.find((group) => group.nodeKinds.includes(node.nodeKind));
2348
+ }
2349
+ function definitionGroupLabel(group, lang) {
2350
+ return lang === 'zh' ? group.labelZh : group.labelEn;
2351
+ }
2352
+ function definitionGroupDescription(group, lang) {
2353
+ return lang === 'zh' ? group.descriptionZh : group.descriptionEn;
2354
+ }
2355
+ function knowledgeGroupNode(group, children, lang) {
2356
+ const declared = children.filter((node) => node.coverage === 'declared').length;
2357
+ return {
2358
+ nodeKind: 'knowledge_group',
2359
+ label: definitionGroupLabel(group, lang),
2360
+ groupDescription: definitionGroupDescription(group, lang),
2361
+ groupSummary: {
2362
+ total: children.length,
2363
+ declared,
2364
+ },
2365
+ };
2366
+ }
2367
+ function definitionGroupChildPosition(group, index) {
2368
+ const position = group.childBoundaryPositions[index];
2369
+ if (position)
2370
+ return position;
2371
+ const columns = group.key === 'references'
2372
+ ? [120, 290, 460]
2373
+ : group.key === 'workflow'
2374
+ ? [660, 830, 1000]
2375
+ : [250, 405, 560, 715, 870];
2376
+ const startY = group.key === 'rules' ? 190 : 500;
2377
+ return {
2378
+ x: columns[index % columns.length],
2379
+ y: startY + Math.floor(index / columns.length) * 92,
2380
+ };
2381
+ }
2382
+ function standaloneAssetPosition(index) {
2383
+ const position = SKILL_MAP_BOUNDARY_ASSET_COORDS[index];
2384
+ if (position)
2385
+ return position;
2386
+ const columns = [205, 380, 560, 740, 915];
2387
+ return {
2388
+ x: columns[index % columns.length],
2389
+ y: 610 + Math.floor(index / columns.length) * 92,
2390
+ };
2391
+ }
2392
+ function standaloneAssetPresentation(node, lang) {
2393
+ const zh = lang === 'zh';
2394
+ if (node.nodeKind === 'script') {
2395
+ return {
2396
+ title: zh ? '执行脚本' : 'Script',
2397
+ detailTitle: zh ? '执行脚本' : 'Script',
2398
+ hideKind: true,
2399
+ };
2400
+ }
2401
+ if (node.nodeKind === 'frontmatter') {
2402
+ return {
2403
+ title: zh ? '元信息' : 'Metadata',
2404
+ detailTitle: zh ? '元信息' : 'Metadata',
2405
+ hideKind: true,
2406
+ };
2407
+ }
2408
+ return undefined;
2409
+ }
2410
+ function buildDefinitionMapNodes(candidates, lang) {
2411
+ let evidenceIndex = 0;
2412
+ const groupNodes = SKILL_MAP_DEFINITION_GROUPS.flatMap((group) => {
2413
+ const children = candidates.filter((node) => definitionGroupSpecForNode(node)?.key === group.key);
2414
+ if (children.length === 0)
2415
+ return [];
2416
+ const groupId = `definition-group-${group.key}`;
2417
+ const label = definitionGroupLabel(group, lang);
2418
+ const groupNode = {
2419
+ id: groupId,
2420
+ node: knowledgeGroupNode(group, children, lang),
2421
+ layer: 'definition',
2422
+ x: group.boundaryPosition.x,
2423
+ y: group.boundaryPosition.y,
2424
+ boundaryPosition: group.boundaryPosition,
2425
+ evidencePosition: group.evidencePosition,
2426
+ isLeaf: false,
2427
+ moreToggle: {
2428
+ group: groupId,
2429
+ collapsedLabel: lang === 'zh' ? `展开${label}` : `Expand ${label}`,
2430
+ expandedLabel: lang === 'zh' ? `收起${label}` : `Collapse ${label}`,
2431
+ },
2432
+ };
2433
+ const childNodes = children.map((node, index) => {
2434
+ const boundaryPosition = definitionGroupChildPosition(group, index);
2435
+ const evidencePosition = SKILL_MAP_EVIDENCE_DEFINITION_COORDS[evidenceIndex] ?? overflowNodePosition('definition', evidenceIndex - SKILL_MAP_EVIDENCE_DEFINITION_COORDS.length, 'evidence');
2436
+ evidenceIndex += 1;
2437
+ return {
2438
+ id: `${groupId}-node-${index}`,
2439
+ node,
2440
+ layer: 'definition',
2441
+ edgeFrom: group.boundaryPosition,
2442
+ edgeFromId: groupId,
2443
+ overflowGroup: groupId,
2444
+ x: boundaryPosition.x,
2445
+ y: boundaryPosition.y,
2446
+ boundaryPosition,
2447
+ evidencePosition,
2448
+ };
2449
+ });
2450
+ return [groupNode, ...childNodes];
2451
+ });
2452
+ const standaloneAssets = candidates
2453
+ .filter((node) => node.nodeKind === 'script' || node.nodeKind === 'frontmatter')
2454
+ .map((node, index) => {
2455
+ const boundaryPosition = standaloneAssetPosition(index);
2456
+ const evidencePosition = SKILL_MAP_EVIDENCE_DEFINITION_COORDS[evidenceIndex] ?? overflowNodePosition('definition', evidenceIndex - SKILL_MAP_EVIDENCE_DEFINITION_COORDS.length, 'evidence');
2457
+ evidenceIndex += 1;
2458
+ return {
2459
+ id: `definition-asset-${index}`,
2460
+ node,
2461
+ layer: 'definition',
2462
+ x: boundaryPosition.x,
2463
+ y: boundaryPosition.y,
2464
+ boundaryPosition,
2465
+ evidencePosition,
2466
+ presentation: standaloneAssetPresentation(node, lang),
2467
+ };
2468
+ });
2469
+ return [...groupNodes, ...standaloneAssets];
2470
+ }
1744
2471
  function skillMapDimensions(nodes) {
2472
+ const allPositions = nodes.flatMap((node) => [
2473
+ { x: node.x, y: node.y },
2474
+ ...(node.boundaryPosition ? [node.boundaryPosition] : []),
2475
+ ...(node.evidencePosition ? [node.evidencePosition] : []),
2476
+ ]);
1745
2477
  return {
1746
- width: Math.max(SKILL_MAP_WIDTH, ...nodes.map((node) => node.x + SKILL_MAP_NODE_MARGIN.x)),
1747
- height: Math.max(SKILL_MAP_HEIGHT, ...nodes.map((node) => node.y + SKILL_MAP_NODE_MARGIN.y)),
2478
+ width: Math.max(SKILL_MAP_WIDTH, ...allPositions.map((node) => node.x + SKILL_MAP_NODE_MARGIN.x)),
2479
+ height: Math.max(SKILL_MAP_HEIGHT, ...allPositions.map((node) => node.y + SKILL_MAP_NODE_MARGIN.y)),
1748
2480
  };
1749
2481
  }
1750
2482
  function renderSkillStageRail(entry, lang) {
@@ -1763,6 +2495,24 @@ function renderSkillStageRail(entry, lang) {
1763
2495
  ${card('observe', stageText(entry, 'observe', lang), observeStatus)}
1764
2496
  </div>`;
1765
2497
  }
2498
+ function renderSkillMapEvidenceStrip(entry, doctor, evalGraph, lang) {
2499
+ const zh = lang === 'zh';
2500
+ const failedSamples = entry.eval?.failCount ?? 0;
2501
+ const totalSamples = entry.eval?.totalSamples ?? evalGraph?.samples ?? 0;
2502
+ const declaredKeys = new Set(evalGraph?.declaredCoverageStableKeys ?? []);
2503
+ const coverable = doctor?.definitionNodes.filter((node) => node.stableKey && DEFINITION_PREVIEW_KINDS.has(node.nodeKind)) ?? [];
2504
+ const declaredCount = coverable.filter((node) => node.stableKey && declaredKeys.has(node.stableKey)).length;
2505
+ const undeclaredCount = coverable.filter((node) => node.stableKey && !declaredKeys.has(node.stableKey)).length;
2506
+ const item = (key, value, tone = '') => `<div class="sm-evidence-item${tone ? ` sm-evidence-item--${tone}` : ''}">
2507
+ <div class="sm-evidence-k">${e(key)}</div>
2508
+ <div class="sm-evidence-v">${e(value)}</div>
2509
+ </div>`;
2510
+ return `<div class="sm-evidence-strip" aria-label="${zh ? '图谱诊断摘要' : 'Map diagnosis summary'}">
2511
+ ${item(zh ? '边界覆盖' : 'Boundary coverage', coverable.length > 0 ? `${declaredCount}/${coverable.length} ${zh ? '个结构节点' : 'structure nodes'}` : (zh ? '暂无结构节点' : 'no structure nodes'))}
2512
+ ${item(zh ? '未声明' : 'Undeclared', coverable.length > 0 ? `${undeclaredCount} ${zh ? '个结构节点' : 'structure nodes'}` : (zh ? '暂无' : 'none'), undeclaredCount > 0 ? 'action' : '')}
2513
+ ${item(zh ? '失败证据' : 'Failed evidence', totalSamples > 0 ? `${failedSamples}/${totalSamples} ${zh ? '条评测用例' : 'eval samples'}` : (zh ? '暂无评测用例' : 'no eval samples'), failedSamples > 0 ? 'risk' : '')}
2514
+ </div>`;
2515
+ }
1766
2516
  function orderedPreviewNodes(nodes, allowedKinds, priority) {
1767
2517
  return (nodes ?? [])
1768
2518
  .map((node, index) => ({ node, index }))
@@ -1770,16 +2520,30 @@ function orderedPreviewNodes(nodes, allowedKinds, priority) {
1770
2520
  .sort((a, b) => (priority[a.node.nodeKind] ?? 99) - (priority[b.node.nodeKind] ?? 99) || a.index - b.index)
1771
2521
  .map(({ node }) => node);
1772
2522
  }
1773
- function applyDefinitionCoverageDeclarations(nodes, declaredStableKeys) {
2523
+ function measurementStatusPriority(node) {
2524
+ if (node.status === 'failed')
2525
+ return 0;
2526
+ if (node.status === 'warning')
2527
+ return 1;
2528
+ if (node.nodeKind === 'sample')
2529
+ return 2;
2530
+ return 3;
2531
+ }
2532
+ function applyDefinitionCoverageDeclarations(nodes, declaredStableKeys, coveredByStableKey) {
1774
2533
  if (!nodes)
1775
2534
  return undefined;
1776
- if (!declaredStableKeys || declaredStableKeys.length === 0)
2535
+ if ((!declaredStableKeys || declaredStableKeys.length === 0) && (!coveredByStableKey || coveredByStableKey.size === 0))
1777
2536
  return nodes;
1778
2537
  const declared = new Set(declaredStableKeys);
1779
2538
  return nodes.map((node) => {
1780
2539
  if (!node.stableKey || !DEFINITION_PREVIEW_KINDS.has(node.nodeKind))
1781
2540
  return node;
1782
- return { ...node, coverage: declared.has(node.stableKey) ? 'declared' : 'undeclared' };
2541
+ const coveredBySamples = coveredByStableKey?.get(node.stableKey) ?? [];
2542
+ return {
2543
+ ...node,
2544
+ coverage: declared.has(node.stableKey) ? 'declared' : 'undeclared',
2545
+ ...(coveredBySamples.length > 0 ? { coveredBySamples } : {}),
2546
+ };
1783
2547
  });
1784
2548
  }
1785
2549
  function skillMapCoverageDeclarationSummary(doctor, evalGraph, lang) {
@@ -1820,7 +2584,7 @@ function renderSkillEvidenceMarkdown(entry, lang) {
1820
2584
  !entry.observe ? `- ${zh ? '接入观察' : 'Add observation'}${stepSeparator} \`omk observe ingest <trace-dir>\`` : '',
1821
2585
  ].filter(Boolean);
1822
2586
  const coverageSummary = skillMapCoverageDeclarationSummary(doctor, evalGraph, lang);
1823
- const definitionPreview = previewNodes(applyDefinitionCoverageDeclarations(doctor?.definitionNodes, evalGraph?.declaredCoverageStableKeys), DEFINITION_PREVIEW_KINDS, 8);
2587
+ const definitionPreview = previewNodes(applyDefinitionCoverageDeclarations(doctor?.definitionNodes, evalGraph?.declaredCoverageStableKeys, coverageSamplesByStableKey(evalGraph, lang)), DEFINITION_PREVIEW_KINDS, 8);
1824
2588
  const measurementPreview = previewNodes(evalGraph?.measurementNodes, MEASUREMENT_PREVIEW_KINDS, 8);
1825
2589
  const previewMermaidLines = [
1826
2590
  ...definitionPreview.visible.flatMap((node, index) => [
@@ -1883,7 +2647,8 @@ function renderSkillMapSection(entry, lang) {
1883
2647
  const evalGraph = graph.eval;
1884
2648
  const band = skillMapBand(entry);
1885
2649
  const markdown = renderSkillEvidenceMarkdown(entry, lang);
1886
- const definitionNodesWithCoverageDeclarations = applyDefinitionCoverageDeclarations(doctor?.definitionNodes, evalGraph?.declaredCoverageStableKeys);
2650
+ const coverageSamples = coverageSamplesByStableKey(evalGraph, lang);
2651
+ const definitionNodesWithCoverageDeclarations = applyDefinitionCoverageDeclarations(doctor?.definitionNodes, evalGraph?.declaredCoverageStableKeys, coverageSamples);
1887
2652
  const coverageSummary = skillMapCoverageDeclarationSummary(doctor, evalGraph, lang);
1888
2653
  const definitionCandidates = orderedPreviewNodes(definitionNodesWithCoverageDeclarations, DEFINITION_PREVIEW_KINDS, {
1889
2654
  reference: 1,
@@ -1897,53 +2662,27 @@ function renderSkillMapSection(entry, lang) {
1897
2662
  sample: 1,
1898
2663
  diagnostic: 2,
1899
2664
  assertion: 3,
1900
- });
1901
- const definitionLimit = SKILL_MAP_DEFINITION_COORDS.length - 1;
1902
- const measurementLimit = SKILL_MAP_MEASUREMENT_COORDS.length - 1;
1903
- const definitionVisible = definitionCandidates.slice(0, definitionLimit);
1904
- const definitionOverflow = definitionCandidates.slice(definitionLimit);
2665
+ }).sort((a, b) => measurementStatusPriority(a) - measurementStatusPriority(b));
2666
+ const measurementLimit = SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS.length - 1;
1905
2667
  const measurementVisible = measurementCandidates.slice(0, measurementLimit);
1906
2668
  const measurementOverflow = measurementCandidates.slice(measurementLimit);
1907
- const definitionMorePosition = SKILL_MAP_DEFINITION_COORDS[SKILL_MAP_DEFINITION_COORDS.length - 1];
1908
- const measurementMorePosition = SKILL_MAP_MEASUREMENT_COORDS[SKILL_MAP_MEASUREMENT_COORDS.length - 1];
1909
- const definitionNodes = [
1910
- ...definitionVisible.map((node, index) => ({
1911
- id: `definition-${index}`,
1912
- node,
1913
- layer: 'definition',
1914
- ...SKILL_MAP_DEFINITION_COORDS[index],
1915
- })),
1916
- ...(definitionOverflow.length > 0 ? [{
1917
- id: 'definition-more',
1918
- node: moreNode(`+ ${definitionOverflow.length} ${zh ? '更多结构节点' : 'more structure nodes'}`),
1919
- layer: 'definition',
1920
- moreToggle: {
1921
- group: 'definition',
1922
- collapsedLabel: `+ ${definitionOverflow.length} ${zh ? '更多结构节点' : 'more structure nodes'}`,
1923
- expandedLabel: zh ? '收起结构节点' : 'collapse structure nodes',
1924
- },
1925
- ...definitionMorePosition,
1926
- }] : []),
1927
- ...definitionOverflow.map((node, index) => ({
1928
- id: `definition-overflow-${index}`,
1929
- node,
1930
- layer: 'definition',
1931
- edgeFrom: definitionMorePosition,
1932
- overflowGroup: 'definition',
1933
- ...overflowNodePosition('definition', index),
1934
- })),
1935
- ];
2669
+ const measurementMorePosition = SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS.length - 1];
2670
+ const definitionNodes = buildDefinitionMapNodes(definitionCandidates, lang);
1936
2671
  const measurementNodes = [
1937
2672
  ...measurementVisible.map((node, index) => ({
1938
2673
  id: `measurement-${index}`,
1939
2674
  node,
1940
2675
  layer: 'measurement',
1941
- ...SKILL_MAP_MEASUREMENT_COORDS[index],
2676
+ ...SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[index],
2677
+ boundaryPosition: SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[index],
2678
+ evidencePosition: SKILL_MAP_EVIDENCE_MEASUREMENT_COORDS[index],
1942
2679
  })),
1943
2680
  ...(measurementOverflow.length > 0 ? [{
1944
2681
  id: 'measurement-more',
1945
2682
  node: moreNode(`+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`),
1946
2683
  layer: 'measurement',
2684
+ boundaryPosition: measurementMorePosition,
2685
+ evidencePosition: measurementMorePosition,
1947
2686
  moreToggle: {
1948
2687
  group: 'measurement',
1949
2688
  collapsedLabel: `+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`,
@@ -1957,23 +2696,23 @@ function renderSkillMapSection(entry, lang) {
1957
2696
  layer: 'measurement',
1958
2697
  edgeFrom: measurementMorePosition,
1959
2698
  overflowGroup: 'measurement',
1960
- ...overflowNodePosition('measurement', index),
2699
+ ...overflowNodePosition('measurement', index, 'evidence'),
2700
+ boundaryPosition: overflowNodePosition('measurement', index, 'evidence'),
2701
+ evidencePosition: overflowNodePosition('measurement', index, 'evidence'),
1961
2702
  })),
1962
2703
  ];
1963
2704
  const mapNodes = [...definitionNodes, ...measurementNodes];
2705
+ const evidenceLinks = skillMapEvidenceLinks(mapNodes, evalGraph);
1964
2706
  const mapDimensions = skillMapDimensions(mapNodes);
1965
- const topMeta = [
1966
- coverageSummary,
1967
- !coverageSummary && doctor ? `${doctor.nodeCount} ${zh ? '结构节点' : 'definition nodes'}` : '',
1968
- !coverageSummary && evalGraph ? `${evalGraph.samples} ${zh ? '用例' : 'samples'}` : '',
1969
- ].filter(Boolean).join(' · ');
2707
+ const topMeta = '';
1970
2708
  return `<section id="section-skill-map" class="si-sect si-sect--${band} sm-sect">
1971
2709
  <div class="si-sect-h">
1972
2710
  <span class="si-sect-title">🗺 ${zh ? 'Skill Map' : 'Skill Map'}</span>
1973
- <span class="si-sect-meta">${e(topMeta || (zh ? '图谱数据未生成' : 'graph data missing'))}</span>
2711
+ ${topMeta ? `<span class="si-sect-meta">${e(topMeta)}</span>` : ''}
1974
2712
  </div>
1975
- <div class="sm-body" data-sm-map data-sm-base-width="${mapDimensions.width}" data-sm-base-height="${mapDimensions.height}" data-sm-root-x="${SKILL_MAP_ROOT.x}" data-sm-root-y="${SKILL_MAP_ROOT.y}">
2713
+ <div class="sm-body" data-sm-map data-sm-view="boundary" data-sm-base-width="${mapDimensions.width}" data-sm-base-height="${mapDimensions.height}" data-sm-root-x="${SKILL_MAP_BOUNDARY_ROOT.x}" data-sm-root-y="${SKILL_MAP_BOUNDARY_ROOT.y}">
1976
2714
  ${renderSkillStageRail(entry, lang)}
2715
+ ${renderSkillMapEvidenceStrip(entry, doctor, evalGraph, lang)}
1977
2716
  <div class="sm-toolbar" aria-label="${zh ? 'Skill Map 控制' : 'Skill Map controls'}">
1978
2717
  <div class="sm-tools">
1979
2718
  <button class="sm-tool" type="button" data-sm-action="zoom-out" title="${zh ? '缩小' : 'Zoom out'}">−</button>
@@ -1982,16 +2721,20 @@ function renderSkillMapSection(entry, lang) {
1982
2721
  <button class="sm-tool" type="button" data-sm-action="reset" title="${zh ? '重置视图' : 'Reset view'}">${zh ? '重置' : 'Reset'}</button>
1983
2722
  </div>
1984
2723
  <div class="sm-toggles">
1985
- <label class="sm-toggle"><input type="checkbox" data-sm-toggle-layer="definition" checked> ${zh ? '定义' : 'Definition'}</label>
1986
- <label class="sm-toggle"><input type="checkbox" data-sm-toggle-layer="measurement" checked> ${zh ? '测量' : 'Measurement'}</label>
2724
+ <div class="sm-view-switch" role="group" aria-label="${zh ? '图谱视图' : 'Map view'}">
2725
+ <button class="sm-view is-active" type="button" data-sm-view-button="boundary" aria-pressed="true">${zh ? '边界图' : 'Boundary'}</button>
2726
+ <button class="sm-view" type="button" data-sm-view-button="evidence" aria-pressed="false">${zh ? '证据视图' : 'Evidence'}</button>
2727
+ </div>
1987
2728
  <label class="sm-toggle"><input type="checkbox" data-sm-toggle-leaves checked> ${zh ? '叶子节点' : 'Leaf nodes'}</label>
1988
2729
  </div>
1989
2730
  </div>
1990
2731
  <div class="sm-canvas" data-sm-viewport aria-label="Skill Map">
1991
2732
  <div class="sm-graph-stage" data-sm-graph-stage style="width:${mapDimensions.width}px;height:${mapDimensions.height}px">
1992
2733
  <div class="sm-graph" data-sm-graph style="width:${mapDimensions.width}px;height:${mapDimensions.height}px">
1993
- ${renderSkillMapEdges(mapNodes, mapDimensions)}
1994
- <div class="sm-node sm-node--skill" data-sm-root data-sm-node-id="${SKILL_MAP_ROOT_ID}" data-sm-x="${SKILL_MAP_ROOT.x}" data-sm-y="${SKILL_MAP_ROOT.y}" data-sm-origin-x="${SKILL_MAP_ROOT.x}" data-sm-origin-y="${SKILL_MAP_ROOT.y}" data-sm-draggable="1" style="${positionedNodeStyle(SKILL_MAP_ROOT)}">
2734
+ <div class="sm-zone-label sm-zone-label--definition" data-sm-layer="definition">${zh ? '知识边界' : 'Knowledge'}</div>
2735
+ <div class="sm-zone-label sm-zone-label--measurement" data-sm-layer="measurement">${zh ? '评测证据' : 'Evidence'}</div>
2736
+ ${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)}">
1995
2738
  <div class="sm-node-title">SKILL.md</div>
1996
2739
  <div class="sm-node-meta">${e(entry.skillName)}</div>
1997
2740
  </div>
@@ -1999,6 +2742,7 @@ function renderSkillMapSection(entry, lang) {
1999
2742
  </div>
2000
2743
  </div>
2001
2744
  </div>
2745
+ ${renderSkillMapInspector(entry, coverageSummary, lang)}
2002
2746
  <details class="sm-card">
2003
2747
  <summary>${zh ? '复制图谱摘要' : 'Copy Skill Map Summary'}</summary>
2004
2748
  <textarea readonly>${e(markdown)}</textarea>