oh-my-knowledge 0.43.0 → 0.45.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.
@@ -397,6 +397,73 @@ details.si-pass-card[open] > summary > .si-pass-row1::before { content:'▾ ' }
397
397
  .si-sect-fold-body { padding-top:6px;display:flex;flex-direction:column;gap:5px }
398
398
  .si-sect-skipped { font-size:11px;color:var(--text-muted);font-style:italic;margin-top:6px }
399
399
 
400
+ /* Skill Map:以 SKILL.md 为根的轻量知识图,不暴露完整 graph JSON 拓扑 */
401
+ .sm-sect { border-left-color:#4f46e5 }
402
+ .sm-body { display:flex;flex-direction:column;gap:10px }
403
+ .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
+ .sm-tools,.sm-toggles { display:flex;align-items:center;gap:6px;flex-wrap:wrap }
405
+ .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
+ .sm-tool:hover { background:#f7f8ff;border-color:#c7d2fe }
407
+ .sm-zoom-label { min-width:44px;text-align:center;font-size:11.5px;color:#637083;font-variant-numeric:tabular-nums }
408
+ .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
+ .sm-toggle input { margin:0;accent-color:#4f46e5 }
410
+ .sm-canvas { position:relative;height:min(68vh,620px);min-height:520px;padding:0;background:
411
+ linear-gradient(90deg,rgba(31,157,99,.045) 0%,rgba(31,157,99,.026) 39%,transparent 39%,transparent 58%,rgba(79,70,229,.026) 58%,rgba(79,70,229,.045) 100%),
412
+ linear-gradient(rgba(148,163,184,.075) 1px,transparent 1px),
413
+ linear-gradient(90deg,rgba(148,163,184,.06) 1px,transparent 1px),
414
+ 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 }
415
+ .sm-graph-stage { position:relative;width:1100px;height:560px }
416
+ .sm-graph { position:absolute;left:0;top:0;width:1100px;height:560px;transform-origin:0 0;transition:transform .12s ease }
417
+ .sm-graph [hidden] { display:none !important }
418
+ .sm-svg { position:absolute;inset:0;width:100%;height:100%;pointer-events:none }
419
+ .sm-edge { fill:none;stroke:#d8e0ec;stroke-width:1.45;stroke-linecap:round;opacity:.86;vector-effect:non-scaling-stroke }
420
+ .sm-edge--definition { stroke:#a9cfc4 }
421
+ .sm-edge--measurement { stroke:#bbc7ff }
422
+ .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
+ .sm-node[data-sm-draggable="1"] { cursor:grab;touch-action:none;user-select:none }
424
+ .sm-node[data-sm-draggable="1"].is-dragging { cursor:grabbing;z-index:3;box-shadow:0 16px 34px rgba(31,41,55,14%) }
425
+ .sm-node[data-sm-more-toggle] { cursor:pointer;user-select:none }
426
+ .sm-node[data-sm-draggable="1"][data-sm-more-toggle] { cursor:grab }
427
+ .sm-node[data-sm-draggable="1"][data-sm-more-toggle].is-dragging { cursor:grabbing }
428
+ .sm-node[data-sm-more-toggle]:hover,.sm-node[data-sm-more-toggle]:focus-visible { border-color:#9fbfb5;background:#f1fbf7;outline:none }
429
+ .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) }
430
+ .sm-node--definition { border-color:#b7d8cf;background:#f4fbf8 }
431
+ .sm-node--measurement { border-color:#c7d2fe;background:#f7f8ff }
432
+ .sm-node--ok { border-color:rgba(31,157,99,.38);background:rgba(31,157,99,.06) }
433
+ .sm-node--warning { border-color:rgba(217,119,6,.38);background:rgba(217,119,6,.07) }
434
+ .sm-node--failed { border-color:rgba(220,38,38,.38);background:rgba(220,38,38,.06) }
435
+ .sm-node--pending,.sm-node--more { border-style:dashed;background:rgba(248,249,251,.94);color:#637083 }
436
+ .sm-node--coverage-declared { border-color:rgba(31,157,99,.42);box-shadow:0 10px 24px rgba(31,157,99,.08) }
437
+ .sm-node--coverage-undeclared { border-style:dashed;background:rgba(248,249,251,.94);color:#637083 }
438
+ .sm-node-title { font-size:12.2px;font-weight:700;color:#182033;line-height:1.28;word-break:break-word;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden }
439
+ .sm-node--skill .sm-node-title { font-size:15px;line-height:1.2 }
440
+ .sm-node-meta { font-size:10.5px;color:#637083;line-height:1.35;word-break:break-word }
441
+ .sm-node--skill .sm-node-meta { font-size:11.5px }
442
+ .sm-node-kind { font-size:9.5px;text-transform:uppercase;letter-spacing:0;color:#8792a4;font-family:"SF Mono",Menlo,monospace;line-height:1 }
443
+ .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
+ .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
+ .sm-stage-card--ok { border-color:rgba(31,157,99,.38);background:rgba(31,157,99,.06) }
446
+ .sm-stage-card--warning { border-color:rgba(217,119,6,.38);background:rgba(217,119,6,.07) }
447
+ .sm-stage-card--failed { border-color:rgba(220,38,38,.38);background:rgba(220,38,38,.06) }
448
+ .sm-stage-card--pending { border-style:dashed;color:#637083;background:#f8f9fb }
449
+ .sm-stage-k { font-size:10px;text-transform:uppercase;letter-spacing:0;color:#7b8798;font-family:"SF Mono",Menlo,monospace;line-height:1 }
450
+ .sm-stage-v { font-size:13px;font-weight:700;color:#182033;line-height:1.35 }
451
+ .sm-stage-arrow { color:#b8c5d8;font-size:18px;line-height:1;text-align:center }
452
+ .sm-card { border:1px solid #e4e8f1;border-radius:8px;background:#fff;overflow:hidden }
453
+ .sm-card > summary { cursor:pointer;list-style:none;padding:10px 12px;font-size:12.5px;font-weight:600;color:#4f46e5;user-select:none }
454
+ .sm-card > summary::-webkit-details-marker { display:none }
455
+ .sm-card > summary::before { content:'▸ ';color:#94a3b8;font-size:10px }
456
+ .sm-card[open] > summary::before { content:'▾ ' }
457
+ .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
+ @media(max-width:900px){
459
+ .sm-toolbar { align-items:flex-start }
460
+ .sm-stage-rail { grid-template-columns:1fr;gap:6px }
461
+ .sm-stage-arrow { transform:rotate(90deg);font-size:16px }
462
+ .sm-canvas { height:620px;min-height:620px }
463
+ .sm-node { width:142px }
464
+ .sm-node--skill { width:168px }
465
+ }
466
+
400
467
  /* doctor 规则展示(用在 doctor section 和 observe section) */
401
468
  .sd-grid { display:flex;flex-direction:column;gap:4px }
402
469
  .sd-dim { border-radius:6px;border:1px solid var(--border);overflow:hidden }
@@ -768,6 +835,336 @@ const TREND_INIT_SCRIPT = `
768
835
  else init();
769
836
  })();
770
837
  </script>`;
838
+ const SKILL_MAP_INIT_SCRIPT = `
839
+ <script>
840
+ (function(){
841
+ function clamp(value, min, max){
842
+ return Math.max(min, Math.min(max, value));
843
+ }
844
+ function initSkillMap(map){
845
+ var viewport = map.querySelector('[data-sm-viewport]');
846
+ var stage = map.querySelector('[data-sm-graph-stage]');
847
+ var graph = map.querySelector('[data-sm-graph]');
848
+ var label = map.querySelector('[data-sm-zoom-label]');
849
+ if (!viewport || !stage || !graph) return;
850
+ var baseWidth = Number(map.getAttribute('data-sm-base-width')) || 1100;
851
+ var baseHeight = Number(map.getAttribute('data-sm-base-height')) || 560;
852
+ var rootX = Number(map.getAttribute('data-sm-root-x')) || 470;
853
+ var rootY = Number(map.getAttribute('data-sm-root-y')) || 260;
854
+ var zoom = 1;
855
+ var dragState = null;
856
+ var suppressMoreClickUntil = 0;
857
+
858
+ function edgePoint(fromX, fromY, toX, toY, box){
859
+ var dx = toX - fromX;
860
+ var dy = toY - fromY;
861
+ if (!dx && !dy) return { x: fromX, y: fromY };
862
+ var halfW = Math.max(1, (box.width || 154) / 2);
863
+ var halfH = Math.max(1, (box.height || 58) / 2);
864
+ var scaleX = dx === 0 ? Infinity : halfW / Math.abs(dx);
865
+ var scaleY = dy === 0 ? Infinity : halfH / Math.abs(dy);
866
+ var scale = Math.min(scaleX, scaleY);
867
+ return { x: fromX + dx * scale, y: fromY + dy * scale };
868
+ }
869
+
870
+ function nodeBox(node, fallbackWidth, fallbackHeight){
871
+ return {
872
+ width: node && node.offsetWidth ? node.offsetWidth : fallbackWidth,
873
+ height: node && node.offsetHeight ? node.offsetHeight : fallbackHeight
874
+ };
875
+ }
876
+
877
+ function nodeCenter(node, fallbackX, fallbackY){
878
+ if (!node) return { x: fallbackX, y: fallbackY };
879
+ return {
880
+ x: Number(node.getAttribute('data-sm-x')) || parseFloat(node.style.left) || fallbackX,
881
+ y: Number(node.getAttribute('data-sm-y')) || parseFloat(node.style.top) || fallbackY
882
+ };
883
+ }
884
+
885
+ function pathData(fromX, fromY, toX, toY, fromBox, toBox){
886
+ var start = edgePoint(fromX, fromY, toX, toY, fromBox);
887
+ var end = edgePoint(toX, toY, fromX, fromY, toBox);
888
+ var sx = start.x;
889
+ var sy = start.y;
890
+ var tx = end.x;
891
+ var ty = end.y;
892
+ var dx = Math.abs(tx - sx);
893
+ var c1x = sx + (tx >= sx ? dx * 0.45 : -dx * 0.45);
894
+ var c2x = tx - (tx >= sx ? dx * 0.45 : -dx * 0.45);
895
+ return 'M ' + sx + ' ' + sy + ' C ' + c1x + ' ' + sy + ', ' + c2x + ' ' + ty + ', ' + tx + ' ' + ty;
896
+ }
897
+
898
+ function setNodePosition(node, x, y){
899
+ node.setAttribute('data-sm-x', String(Math.round(x * 10) / 10));
900
+ node.setAttribute('data-sm-y', String(Math.round(y * 10) / 10));
901
+ node.style.left = x + 'px';
902
+ node.style.top = y + 'px';
903
+ }
904
+
905
+ function updateEdgesForNode(nodeId, x, y){
906
+ map.querySelectorAll('[data-sm-edge-to]').forEach(function(edge){
907
+ if (edge.getAttribute('data-sm-edge-to') !== nodeId) return;
908
+ var fromNodeId = edge.getAttribute('data-sm-edge-from-node');
909
+ var fromNode = fromNodeId ? graph.querySelector('[data-sm-node-id="' + fromNodeId + '"]') : graph.querySelector('[data-sm-root]');
910
+ var toNode = graph.querySelector('[data-sm-node-id="' + nodeId + '"]');
911
+ var fromCenter = nodeCenter(fromNode, rootX, rootY);
912
+ edge.setAttribute('d', pathData(
913
+ fromCenter.x,
914
+ fromCenter.y,
915
+ x,
916
+ y,
917
+ nodeBox(fromNode, 184, 108),
918
+ nodeBox(toNode, 154, 58)
919
+ ));
920
+ });
921
+ }
922
+
923
+ function updateEdgesFromNode(nodeId, x, y){
924
+ map.querySelectorAll('[data-sm-edge-from-node]').forEach(function(edge){
925
+ if (edge.getAttribute('data-sm-edge-from-node') !== nodeId) return;
926
+ var toId = edge.getAttribute('data-sm-edge-to');
927
+ var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
928
+ if (!toNode) return;
929
+ var toCenter = nodeCenter(toNode, 0, 0);
930
+ edge.setAttribute('data-sm-edge-from-x', String(Math.round(x * 10) / 10));
931
+ edge.setAttribute('data-sm-edge-from-y', String(Math.round(y * 10) / 10));
932
+ edge.setAttribute('d', pathData(
933
+ x,
934
+ y,
935
+ toCenter.x,
936
+ toCenter.y,
937
+ nodeBox(graph.querySelector('[data-sm-node-id="' + nodeId + '"]'), 154, 58),
938
+ nodeBox(toNode, 154, 58)
939
+ ));
940
+ });
941
+ }
942
+
943
+ function refreshEdges(){
944
+ map.querySelectorAll('[data-sm-edge-to]').forEach(function(edge){
945
+ var toId = edge.getAttribute('data-sm-edge-to');
946
+ var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
947
+ if (!toNode) return;
948
+ var toCenter = nodeCenter(toNode, 0, 0);
949
+ updateEdgesForNode(toId, toCenter.x, toCenter.y);
950
+ });
951
+ }
952
+
953
+ function resetNodePositions(){
954
+ graph.querySelectorAll('[data-sm-draggable="1"]').forEach(function(node){
955
+ 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);
961
+ });
962
+ }
963
+
964
+ function applyZoom(){
965
+ stage.style.width = Math.ceil(baseWidth * zoom) + 'px';
966
+ stage.style.height = Math.ceil(baseHeight * zoom) + 'px';
967
+ graph.style.transform = 'scale(' + zoom + ')';
968
+ if (label) label.textContent = Math.round(zoom * 100) + '%';
969
+ }
970
+
971
+ function setZoom(nextZoom, anchorX, anchorY){
972
+ var oldZoom = zoom;
973
+ var x = anchorX == null ? viewport.clientWidth / 2 : anchorX;
974
+ var y = anchorY == null ? viewport.clientHeight / 2 : anchorY;
975
+ var graphX = (viewport.scrollLeft + x) / oldZoom;
976
+ var graphY = (viewport.scrollTop + y) / oldZoom;
977
+ zoom = clamp(nextZoom, 0.65, 1.8);
978
+ applyZoom();
979
+ viewport.scrollLeft = Math.max(0, graphX * zoom - x);
980
+ viewport.scrollTop = Math.max(0, graphY * zoom - y);
981
+ }
982
+
983
+ function centerRoot(){
984
+ var rootNode = graph.querySelector('[data-sm-root]');
985
+ var rootCenter = nodeCenter(rootNode, rootX, rootY);
986
+ var scaledRootX = rootCenter.x * zoom;
987
+ var scaledRootY = rootCenter.y * zoom;
988
+ viewport.scrollLeft = Math.max(0, scaledRootX - viewport.clientWidth / 2);
989
+ viewport.scrollTop = Math.max(0, scaledRootY - viewport.clientHeight / 2);
990
+ }
991
+
992
+ function layerEnabled(layer){
993
+ var input = map.querySelector('[data-sm-toggle-layer="' + layer + '"]');
994
+ return !input || input.checked;
995
+ }
996
+
997
+ function overflowGroupExpanded(group){
998
+ if (!group) return true;
999
+ var toggle = map.querySelector('[data-sm-more-toggle="' + group + '"]');
1000
+ return !!toggle && toggle.getAttribute('aria-expanded') === 'true';
1001
+ }
1002
+
1003
+ function updateVisibility(){
1004
+ var showLeaves = true;
1005
+ var leafToggle = map.querySelector('[data-sm-toggle-leaves]');
1006
+ if (leafToggle) showLeaves = leafToggle.checked;
1007
+ var layers = {
1008
+ definition: layerEnabled('definition'),
1009
+ measurement: layerEnabled('measurement')
1010
+ };
1011
+ map.querySelectorAll('[data-sm-layer]').forEach(function(el){
1012
+ var layer = el.getAttribute('data-sm-layer');
1013
+ var isLeaf = el.getAttribute('data-sm-leaf') === '1';
1014
+ var overflowGroup = el.getAttribute('data-sm-overflow-group');
1015
+ var shouldHide = !layers[layer] || (isLeaf && !showLeaves) || !overflowGroupExpanded(overflowGroup);
1016
+ el.setAttribute('data-sm-filter-hidden', shouldHide ? '1' : '0');
1017
+ el.hidden = shouldHide;
1018
+ el.toggleAttribute('hidden', shouldHide);
1019
+ el.setAttribute('aria-hidden', shouldHide ? 'true' : 'false');
1020
+ el.style.display = shouldHide ? 'none' : '';
1021
+ });
1022
+ map.querySelectorAll('[data-sm-edge-to]').forEach(function(edge){
1023
+ var toId = edge.getAttribute('data-sm-edge-to');
1024
+ var fromId = edge.getAttribute('data-sm-edge-from-node');
1025
+ var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
1026
+ var fromNode = fromId ? graph.querySelector('[data-sm-node-id="' + fromId + '"]') : null;
1027
+ var filterHidden = edge.getAttribute('data-sm-filter-hidden') === '1';
1028
+ var endpointHidden = (toNode && toNode.hidden) || (fromNode && fromNode.hidden);
1029
+ var shouldHideEdge = filterHidden || endpointHidden;
1030
+ edge.hidden = shouldHideEdge;
1031
+ edge.toggleAttribute('hidden', shouldHideEdge);
1032
+ edge.setAttribute('aria-hidden', shouldHideEdge ? 'true' : 'false');
1033
+ edge.style.display = shouldHideEdge ? 'none' : '';
1034
+ });
1035
+ }
1036
+
1037
+ function setMoreExpanded(toggle, expanded){
1038
+ toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1039
+ var nextLabel = expanded
1040
+ ? toggle.getAttribute('data-sm-expanded-label')
1041
+ : toggle.getAttribute('data-sm-collapsed-label');
1042
+ var title = toggle.querySelector('.sm-node-title');
1043
+ if (title && nextLabel) title.textContent = nextLabel;
1044
+ if (nextLabel) toggle.setAttribute('title', nextLabel);
1045
+ updateVisibility();
1046
+ refreshEdges();
1047
+ }
1048
+
1049
+ function collapseMoreGroups(){
1050
+ map.querySelectorAll('[data-sm-more-toggle]').forEach(function(toggle){
1051
+ setMoreExpanded(toggle, false);
1052
+ });
1053
+ }
1054
+
1055
+ map.querySelectorAll('[data-sm-action]').forEach(function(button){
1056
+ button.addEventListener('click', function(){
1057
+ var action = button.getAttribute('data-sm-action');
1058
+ if (action === 'zoom-in') setZoom(zoom + 0.15);
1059
+ if (action === 'zoom-out') setZoom(zoom - 0.15);
1060
+ if (action === 'reset') {
1061
+ zoom = 1;
1062
+ collapseMoreGroups();
1063
+ resetNodePositions();
1064
+ applyZoom();
1065
+ centerRoot();
1066
+ }
1067
+ });
1068
+ });
1069
+
1070
+ map.querySelectorAll('[data-sm-toggle-layer], [data-sm-toggle-leaves]').forEach(function(input){
1071
+ input.addEventListener('change', updateVisibility);
1072
+ });
1073
+
1074
+ graph.querySelectorAll('[data-sm-more-toggle]').forEach(function(toggle){
1075
+ toggle.addEventListener('click', function(ev){
1076
+ if (Date.now() < suppressMoreClickUntil) {
1077
+ ev.preventDefault();
1078
+ return;
1079
+ }
1080
+ ev.preventDefault();
1081
+ setMoreExpanded(toggle, toggle.getAttribute('aria-expanded') !== 'true');
1082
+ });
1083
+ toggle.addEventListener('keydown', function(ev){
1084
+ if (ev.key !== 'Enter' && ev.key !== ' ') return;
1085
+ ev.preventDefault();
1086
+ setMoreExpanded(toggle, toggle.getAttribute('aria-expanded') !== 'true');
1087
+ });
1088
+ });
1089
+
1090
+ graph.querySelectorAll('[data-sm-draggable="1"]').forEach(function(node){
1091
+ node.addEventListener('pointerdown', function(ev){
1092
+ if (ev.button != null && ev.button !== 0) return;
1093
+ if (node.hidden) return;
1094
+ var nodeId = node.getAttribute('data-sm-node-id');
1095
+ if (!nodeId) return;
1096
+ ev.preventDefault();
1097
+ dragState = {
1098
+ node: node,
1099
+ nodeId: nodeId,
1100
+ pointerId: ev.pointerId,
1101
+ startClientX: ev.clientX,
1102
+ startClientY: ev.clientY,
1103
+ startX: Number(node.getAttribute('data-sm-x')) || parseFloat(node.style.left) || 0,
1104
+ startY: Number(node.getAttribute('data-sm-y')) || parseFloat(node.style.top) || 0,
1105
+ moved: false
1106
+ };
1107
+ node.classList.add('is-dragging');
1108
+ node.setPointerCapture(ev.pointerId);
1109
+ });
1110
+
1111
+ node.addEventListener('pointermove', function(ev){
1112
+ if (!dragState || dragState.node !== node) return;
1113
+ ev.preventDefault();
1114
+ var deltaX = ev.clientX - dragState.startClientX;
1115
+ var deltaY = ev.clientY - dragState.startClientY;
1116
+ if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 3) dragState.moved = true;
1117
+ var nextX = clamp(dragState.startX + deltaX / zoom, 52, baseWidth - 52);
1118
+ var nextY = clamp(dragState.startY + deltaY / zoom, 36, baseHeight - 36);
1119
+ setNodePosition(node, nextX, nextY);
1120
+ updateEdgesForNode(dragState.nodeId, nextX, nextY);
1121
+ updateEdgesFromNode(dragState.nodeId, nextX, nextY);
1122
+ });
1123
+
1124
+ function finishDrag(ev){
1125
+ if (!dragState || dragState.node !== node) return;
1126
+ var wasMoved = dragState.moved;
1127
+ var isMoreToggle = node.hasAttribute('data-sm-more-toggle');
1128
+ node.classList.remove('is-dragging');
1129
+ if (node.hasPointerCapture && node.hasPointerCapture(dragState.pointerId)) {
1130
+ node.releasePointerCapture(dragState.pointerId);
1131
+ }
1132
+ dragState = null;
1133
+ if (!wasMoved && isMoreToggle) {
1134
+ suppressMoreClickUntil = Date.now() + 300;
1135
+ setMoreExpanded(node, node.getAttribute('aria-expanded') !== 'true');
1136
+ } else if (wasMoved && isMoreToggle) {
1137
+ suppressMoreClickUntil = Date.now() + 300;
1138
+ }
1139
+ }
1140
+
1141
+ node.addEventListener('pointerup', finishDrag);
1142
+ node.addEventListener('pointercancel', finishDrag);
1143
+ });
1144
+
1145
+ viewport.addEventListener('wheel', function(ev){
1146
+ if (!ev.ctrlKey && !ev.metaKey) return;
1147
+ ev.preventDefault();
1148
+ var rect = viewport.getBoundingClientRect();
1149
+ var delta = ev.deltaY < 0 ? 0.12 : -0.12;
1150
+ setZoom(zoom + delta, ev.clientX - rect.left, ev.clientY - rect.top);
1151
+ }, { passive: false });
1152
+
1153
+ applyZoom();
1154
+ updateVisibility();
1155
+ requestAnimationFrame(function(){
1156
+ refreshEdges();
1157
+ centerRoot();
1158
+ });
1159
+ }
1160
+
1161
+ function init(){
1162
+ document.querySelectorAll('[data-sm-map]').forEach(initSkillMap);
1163
+ }
1164
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1165
+ else init();
1166
+ })();
1167
+ </script>`;
771
1168
  /** doctor section:展示告警 / 失败规则;通过的规则折叠到底部 */
772
1169
  function renderDoctorSection(snap, history, skillName, lang) {
773
1170
  if (!snap) {
@@ -1106,6 +1503,509 @@ function renderObserveSection(snap, langQ, lang) {
1106
1503
  </div>
1107
1504
  </section>`;
1108
1505
  }
1506
+ function skillMapBand(entry) {
1507
+ if (entry.doctor?.failCount || entry.eval?.failCount)
1508
+ return 'red';
1509
+ if (entry.doctor?.warnCount || (entry.eval?.compositeScore != null && entry.eval.compositeScore < 3.5))
1510
+ return 'yellow';
1511
+ if (entry.doctor || entry.eval || entry.observe)
1512
+ return 'green';
1513
+ return 'gray';
1514
+ }
1515
+ function stageText(entry, stage, lang) {
1516
+ const zh = lang === 'zh';
1517
+ if (stage === 'doctor') {
1518
+ if (!entry.doctor)
1519
+ return zh ? '未体检' : 'not checked';
1520
+ if (entry.doctor.failCount > 0)
1521
+ return zh ? '未通过' : 'failed';
1522
+ if (entry.doctor.warnCount > 0)
1523
+ return zh ? '有警告' : 'warnings';
1524
+ return zh ? '已通过' : 'passed';
1525
+ }
1526
+ if (stage === 'eval') {
1527
+ if (!entry.eval)
1528
+ return zh ? '未测量' : 'not measured';
1529
+ if (entry.eval.resultsStripped)
1530
+ return entry.eval.compositeScore != null ? `${entry.eval.compositeScore.toFixed(2)}/5` : (zh ? '已测量' : 'measured');
1531
+ return zh
1532
+ ? `${entry.eval.passCount}/${entry.eval.totalSamples} 用例通过`
1533
+ : `${entry.eval.passCount}/${entry.eval.totalSamples} samples pass`;
1534
+ }
1535
+ if (!entry.observe)
1536
+ return zh ? '未观察' : 'not observed';
1537
+ return zh
1538
+ ? `${entry.observe.segmentCount} 段,gap ${Math.round(entry.observe.gapRate * 100)}%`
1539
+ : `${entry.observe.segmentCount} segments, gap ${Math.round(entry.observe.gapRate * 100)}%`;
1540
+ }
1541
+ function mermaidCardLabel(label) {
1542
+ const entities = {
1543
+ '&': '&amp;',
1544
+ '\\': '&#92;',
1545
+ '"': '&quot;',
1546
+ '[': '&#91;',
1547
+ ']': '&#93;',
1548
+ '(': '&#40;',
1549
+ ')': '&#41;',
1550
+ '{': '&#123;',
1551
+ '}': '&#125;',
1552
+ '|': '&#124;',
1553
+ '#': '&#35;',
1554
+ ';': '&#59;',
1555
+ '<': '&lt;',
1556
+ '>': '&gt;',
1557
+ };
1558
+ return label
1559
+ .replace(/\s+/g, ' ')
1560
+ .trim()
1561
+ .replace(/[&\\"[\](){}|#;<>]/g, (ch) => entities[ch] ?? ch);
1562
+ }
1563
+ function markdownInline(value) {
1564
+ if (!value)
1565
+ return '`unknown`';
1566
+ return `\`${value.replaceAll('`', '\\`')}\``;
1567
+ }
1568
+ function sourceCommandArg(value) {
1569
+ if (/^[A-Za-z0-9_./:@%+-]+$/.test(value))
1570
+ return value;
1571
+ return JSON.stringify(value);
1572
+ }
1573
+ const DEFINITION_PREVIEW_KINDS = new Set([
1574
+ 'frontmatter',
1575
+ 'reference',
1576
+ 'script',
1577
+ 'hard_rule',
1578
+ 'workflow',
1579
+ 'workflow_node',
1580
+ ]);
1581
+ const MEASUREMENT_PREVIEW_KINDS = new Set([
1582
+ 'sample',
1583
+ 'assertion',
1584
+ 'diagnostic',
1585
+ ]);
1586
+ function nodeKindLabel(kind, lang) {
1587
+ const zh = lang === 'zh';
1588
+ const labels = {
1589
+ frontmatter: ['meta', 'meta'],
1590
+ reference: ['ref', 'ref'],
1591
+ script: ['script', 'script'],
1592
+ tool: ['tool', 'tool'],
1593
+ preflight: ['preflight', 'preflight'],
1594
+ hard_rule: ['rule', 'rule'],
1595
+ workflow: ['workflow', 'workflow'],
1596
+ workflow_node: ['step', 'step'],
1597
+ sample: ['sample', 'sample'],
1598
+ assertion: ['assert', 'assert'],
1599
+ diagnostic: ['diag', 'diag'],
1600
+ doctor_rule_result: ['doctor', 'doctor'],
1601
+ eval_result: ['eval', 'eval'],
1602
+ trace_session: ['observe', 'observe'],
1603
+ more: ['more', 'more'],
1604
+ };
1605
+ const pair = labels[kind] ?? [kind, kind];
1606
+ return zh ? pair[0] : pair[1];
1607
+ }
1608
+ function graphNodeDisplayLabel(node, lang) {
1609
+ const zh = lang === 'zh';
1610
+ switch (node.nodeKind) {
1611
+ case 'workflow':
1612
+ return `${zh ? 'workflow' : 'workflow'}: ${node.label}`;
1613
+ case 'hard_rule':
1614
+ return `${zh ? 'rule' : 'rule'}: ${node.label}`;
1615
+ case 'tool':
1616
+ return `${zh ? 'tool' : 'tool'}: ${node.label}`;
1617
+ case 'sample':
1618
+ return `${zh ? 'sample' : 'sample'}: ${node.label}`;
1619
+ case 'diagnostic':
1620
+ return node.label.startsWith('diagnostic') ? node.label : `${zh ? 'diagnostic' : 'diagnostic'}: ${node.label}`;
1621
+ default:
1622
+ return node.label;
1623
+ }
1624
+ }
1625
+ function graphNodeMapLabel(node) {
1626
+ switch (node.nodeKind) {
1627
+ case 'workflow':
1628
+ case 'hard_rule':
1629
+ case 'tool':
1630
+ case 'sample':
1631
+ return node.label;
1632
+ case 'assertion':
1633
+ return node.label.replace(/^assertion:\s*/i, '');
1634
+ case 'diagnostic':
1635
+ return node.label.replace(/^diagnostic:\s*/i, '');
1636
+ default:
1637
+ return node.label;
1638
+ }
1639
+ }
1640
+ function previewNodes(nodes, allowedKinds, limit) {
1641
+ const filtered = (nodes ?? []).filter((node) => allowedKinds.has(node.nodeKind));
1642
+ return { visible: filtered.slice(0, limit), hiddenCount: Math.max(0, filtered.length - limit) };
1643
+ }
1644
+ const SKILL_MAP_WIDTH = 1100;
1645
+ const SKILL_MAP_HEIGHT = 560;
1646
+ const SKILL_MAP_ROOT_ID = 'skill-root';
1647
+ const SKILL_MAP_ROOT = { x: 470, y: 260 };
1648
+ const SKILL_MAP_ROOT_SIZE = { width: 172, height: 86 };
1649
+ 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 },
1658
+ ];
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 },
1666
+ ];
1667
+ const SKILL_MAP_NODE_MARGIN = { x: 92, y: 48 };
1668
+ const SKILL_MAP_STATUS_CLASSES = new Set(['ok', 'warning', 'failed', 'skipped', 'unknown', 'not_measured']);
1669
+ function graphNodeStatusClass(node) {
1670
+ return node.status && SKILL_MAP_STATUS_CLASSES.has(node.status) ? ` sm-node--${node.status}` : '';
1671
+ }
1672
+ function graphNodeCoverageClass(node) {
1673
+ if (node.coverage === 'declared')
1674
+ return ' sm-node--coverage-declared';
1675
+ if (node.coverage === 'undeclared')
1676
+ return ' sm-node--coverage-undeclared';
1677
+ return '';
1678
+ }
1679
+ function graphNodeCoverageLabel(node, lang) {
1680
+ if (node.coverage === 'declared')
1681
+ return lang === 'zh' ? '已由评测用例声明' : 'declared by eval samples';
1682
+ if (node.coverage === 'undeclared')
1683
+ return lang === 'zh' ? '尚未由评测用例声明' : 'not declared by eval samples';
1684
+ return '';
1685
+ }
1686
+ function positionedNodeStyle(node) {
1687
+ return `left:${node.x}px;top:${node.y}px`;
1688
+ }
1689
+ function renderSkillMapNode(item, lang) {
1690
+ const title = graphNodeMapLabel(item.node);
1691
+ 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"' : '';
1695
+ const draggable = item.layer === 'definition' || item.layer === 'measurement' ? ' data-sm-draggable="1"' : '';
1696
+ const overflow = item.overflowGroup ? ` data-sm-overflow-group="${item.overflowGroup}" hidden aria-hidden="true"` : '';
1697
+ const moreToggle = item.moreToggle
1698
+ ? ` 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>
1702
+ <div class="sm-node-title">${e(title)}</div>
1703
+ </div>`;
1704
+ }
1705
+ function edgePoint(from, to, box) {
1706
+ const dx = to.x - from.x;
1707
+ const dy = to.y - from.y;
1708
+ if (dx === 0 && dy === 0)
1709
+ return from;
1710
+ const scale = Math.min(dx === 0 ? Infinity : box.width / 2 / Math.abs(dx), dy === 0 ? Infinity : box.height / 2 / Math.abs(dy));
1711
+ return { x: from.x + dx * scale, y: from.y + dy * scale };
1712
+ }
1713
+ function skillMapPath(from, to, fromBox, toBox) {
1714
+ const start = edgePoint(from, to, fromBox);
1715
+ const end = edgePoint(to, from, toBox);
1716
+ const dx = Math.abs(end.x - start.x);
1717
+ const c1x = start.x + (end.x >= start.x ? dx * 0.45 : -dx * 0.45);
1718
+ const c2x = end.x - (end.x >= start.x ? dx * 0.45 : -dx * 0.45);
1719
+ return `M ${start.x} ${start.y} C ${c1x} ${start.y}, ${c2x} ${end.y}, ${end.x} ${end.y}`;
1720
+ }
1721
+ function renderSkillMapEdges(nodes, dimensions) {
1722
+ const rootEdges = nodes
1723
+ .map((node) => {
1724
+ const from = node.edgeFrom ?? SKILL_MAP_ROOT;
1725
+ const fromBox = node.edgeFrom ? SKILL_MAP_NODE_SIZE : SKILL_MAP_ROOT_SIZE;
1726
+ 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>`;
1729
+ })
1730
+ .join('');
1731
+ return `<svg class="sm-svg" viewBox="0 0 ${dimensions.width} ${dimensions.height}" aria-hidden="true">${rootEdges}</svg>`;
1732
+ }
1733
+ function moreNode(label) {
1734
+ return { nodeKind: 'more', label };
1735
+ }
1736
+ function overflowNodePosition(layer, index) {
1737
+ const columns = layer === 'definition' ? [112, 270, 428] : [720, 880, 1040];
1738
+ const startY = layer === 'definition' ? 500 : 560;
1739
+ return {
1740
+ x: columns[index % columns.length],
1741
+ y: startY + Math.floor(index / columns.length) * 98,
1742
+ };
1743
+ }
1744
+ function skillMapDimensions(nodes) {
1745
+ 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)),
1748
+ };
1749
+ }
1750
+ function renderSkillStageRail(entry, lang) {
1751
+ const doctorStatus = entry.doctor?.failCount ? 'failed' : entry.doctor?.warnCount ? 'warning' : entry.doctor ? 'ok' : 'pending';
1752
+ const evalStatus = entry.eval?.failCount ? 'failed' : entry.eval ? 'ok' : 'pending';
1753
+ const observeStatus = entry.observe ? (entry.observe.healthBand === 'red' ? 'failed' : entry.observe.healthBand === 'yellow' ? 'warning' : 'ok') : 'pending';
1754
+ const card = (key, value, status) => `<div class="sm-stage-card sm-stage-card--${status}">
1755
+ <div class="sm-stage-k">${e(key)}</div>
1756
+ <div class="sm-stage-v">${e(value)}</div>
1757
+ </div>`;
1758
+ return `<div class="sm-stage-rail" aria-label="${lang === 'zh' ? '三阶段状态' : 'Stage status'}">
1759
+ ${card('doctor', stageText(entry, 'doctor', lang), doctorStatus)}
1760
+ <div class="sm-stage-arrow">→</div>
1761
+ ${card('eval', stageText(entry, 'eval', lang), evalStatus)}
1762
+ <div class="sm-stage-arrow">→</div>
1763
+ ${card('observe', stageText(entry, 'observe', lang), observeStatus)}
1764
+ </div>`;
1765
+ }
1766
+ function orderedPreviewNodes(nodes, allowedKinds, priority) {
1767
+ return (nodes ?? [])
1768
+ .map((node, index) => ({ node, index }))
1769
+ .filter(({ node }) => allowedKinds.has(node.nodeKind))
1770
+ .sort((a, b) => (priority[a.node.nodeKind] ?? 99) - (priority[b.node.nodeKind] ?? 99) || a.index - b.index)
1771
+ .map(({ node }) => node);
1772
+ }
1773
+ function applyDefinitionCoverageDeclarations(nodes, declaredStableKeys) {
1774
+ if (!nodes)
1775
+ return undefined;
1776
+ if (!declaredStableKeys || declaredStableKeys.length === 0)
1777
+ return nodes;
1778
+ const declared = new Set(declaredStableKeys);
1779
+ return nodes.map((node) => {
1780
+ if (!node.stableKey || !DEFINITION_PREVIEW_KINDS.has(node.nodeKind))
1781
+ return node;
1782
+ return { ...node, coverage: declared.has(node.stableKey) ? 'declared' : 'undeclared' };
1783
+ });
1784
+ }
1785
+ function skillMapCoverageDeclarationSummary(doctor, evalGraph, lang) {
1786
+ const declaredKeys = new Set(evalGraph?.declaredCoverageStableKeys ?? []);
1787
+ if (!doctor || declaredKeys.size === 0)
1788
+ return null;
1789
+ const coverable = doctor.definitionNodes.filter((node) => node.stableKey && DEFINITION_PREVIEW_KINDS.has(node.nodeKind));
1790
+ if (coverable.length === 0)
1791
+ return null;
1792
+ const declaredCount = coverable.filter((node) => node.stableKey && declaredKeys.has(node.stableKey)).length;
1793
+ const samples = evalGraph?.samples ?? 0;
1794
+ return lang === 'zh'
1795
+ ? `${samples} 条评测用例声明了 ${declaredCount}/${coverable.length} 个结构节点`
1796
+ : `${samples} eval samples declared ${declaredCount}/${coverable.length} structure nodes`;
1797
+ }
1798
+ function skillMapCoverageDeclarationHint(lang) {
1799
+ return lang === 'zh'
1800
+ ? '这些关系来自 sample.covers,表示评测用例显式标注的主要触达节点;尚未声明只表示还没有被评测用例显式标注,不应直接视为测试缺口。'
1801
+ : 'These relationships come from sample.covers and show the main nodes explicitly annotated by eval samples; not declared only means not explicitly annotated yet, not necessarily a test gap.';
1802
+ }
1803
+ function renderSkillEvidenceMarkdown(entry, lang) {
1804
+ const zh = lang === 'zh';
1805
+ const graph = entry.graph;
1806
+ const doctor = graph?.doctor;
1807
+ const evalGraph = graph?.eval;
1808
+ const source = graph?.sourceLocator ?? entry.skillName;
1809
+ const sourceArg = sourceCommandArg(source);
1810
+ const doctorStatus = stageText(entry, 'doctor', lang);
1811
+ const evalStatus = stageText(entry, 'eval', lang);
1812
+ const observeStatus = stageText(entry, 'observe', lang);
1813
+ const stepSeparator = zh ? ':' : ':';
1814
+ const summary = zh
1815
+ ? `这个 skill 当前处于「doctor ${doctorStatus},eval ${evalStatus},observe ${observeStatus}」状态。`
1816
+ : `Current state: doctor ${doctorStatus}, eval ${evalStatus}, observe ${observeStatus}.`;
1817
+ const nextSteps = [
1818
+ !entry.doctor ? `- ${zh ? '体检结构' : 'Check structure'}${stepSeparator} \`omk doctor ${sourceArg}\`` : '',
1819
+ !entry.eval ? `- ${zh ? '测量效果' : 'Measure impact'}${stepSeparator} \`omk eval --control baseline --treatment ${sourceArg}\`` : '',
1820
+ !entry.observe ? `- ${zh ? '接入观察' : 'Add observation'}${stepSeparator} \`omk observe ingest <trace-dir>\`` : '',
1821
+ ].filter(Boolean);
1822
+ const coverageSummary = skillMapCoverageDeclarationSummary(doctor, evalGraph, lang);
1823
+ const definitionPreview = previewNodes(applyDefinitionCoverageDeclarations(doctor?.definitionNodes, evalGraph?.declaredCoverageStableKeys), DEFINITION_PREVIEW_KINDS, 8);
1824
+ const measurementPreview = previewNodes(evalGraph?.measurementNodes, MEASUREMENT_PREVIEW_KINDS, 8);
1825
+ const previewMermaidLines = [
1826
+ ...definitionPreview.visible.flatMap((node, index) => [
1827
+ ` def_${index}["${mermaidCardLabel(graphNodeDisplayLabel(node, lang))}"]`,
1828
+ ` skill --> def_${index}`,
1829
+ ]),
1830
+ ...measurementPreview.visible.flatMap((node, index) => [
1831
+ ` measure_${index}["${mermaidCardLabel(graphNodeDisplayLabel(node, lang))}"]`,
1832
+ ` skill --> measure_${index}`,
1833
+ ]),
1834
+ ];
1835
+ return [
1836
+ `## ${zh ? '知识图谱摘要' : 'Skill Map Summary'}${zh ? ':' : ': '}${entry.skillName}`,
1837
+ '',
1838
+ summary,
1839
+ '',
1840
+ '```mermaid',
1841
+ 'flowchart LR',
1842
+ ` skill["SKILL.md: ${mermaidCardLabel(entry.skillName)}"]`,
1843
+ ` doctor["doctor: ${mermaidCardLabel(doctorStatus)}"]`,
1844
+ ` eval["eval: ${mermaidCardLabel(evalStatus)}"]`,
1845
+ ` observe["observe: ${mermaidCardLabel(observeStatus)}"]`,
1846
+ ...previewMermaidLines,
1847
+ ' doctor --> eval',
1848
+ ' eval --> observe',
1849
+ '```',
1850
+ '',
1851
+ zh ? '### 三阶段状态' : '### Stage Status',
1852
+ '',
1853
+ `| ${zh ? '阶段' : 'Stage'} | ${zh ? '状态' : 'Status'} | ${zh ? '证据' : 'Evidence'} |`,
1854
+ '| --- | --- | --- |',
1855
+ `| doctor | ${doctorStatus} | ${doctor ? `${doctor.nodeCount} nodes / ${doctor.edgeCount} edges` : (zh ? '无 graph' : 'no graph')} |`,
1856
+ `| eval | ${evalStatus} | ${evalGraph ? `${zh ? 'variant 子图' : 'variant subgraph'}: ${evalGraph.nodeCount} nodes / ${evalGraph.edgeCount} edges` : (zh ? '无 graph' : 'no graph')} |`,
1857
+ `| observe | ${observeStatus} | ${zh ? '未接 production graph' : 'production graph not connected'} |`,
1858
+ ...(coverageSummary ? ['', zh ? '### 声明锚点' : '### Declared Anchors', '', coverageSummary, '', skillMapCoverageDeclarationHint(lang)] : []),
1859
+ '',
1860
+ zh ? '### 关键计数' : '### Key Counts',
1861
+ '',
1862
+ '| references | scripts | workflows | workflow nodes | samples | failed samples |',
1863
+ '| ---: | ---: | ---: | ---: | ---: | ---: |',
1864
+ `| ${doctor?.references ?? 0} | ${doctor?.scripts ?? 0} | ${doctor?.workflows ?? 0} | ${doctor?.workflowNodes ?? 0} | ${entry.eval?.totalSamples ?? 0} | ${entry.eval?.failCount ?? 0} |`,
1865
+ ...(nextSteps.length > 0 ? ['', zh ? '### 下一步' : '### Next Steps', '', ...nextSteps] : []),
1866
+ '',
1867
+ zh ? '### 复现信息' : '### Reproduction',
1868
+ '',
1869
+ `- source:${markdownInline(source)}`,
1870
+ `- artifactHash:${markdownInline(graph?.artifactHash)}`,
1871
+ `- binding:${markdownInline(graph?.bindingStrength)}`,
1872
+ `- doctorGraphId:${markdownInline(doctor?.graphId)}`,
1873
+ `- evalGraphId:${markdownInline(evalGraph?.graphId)}`,
1874
+ '',
1875
+ ].join('\n');
1876
+ }
1877
+ function renderSkillMapSection(entry, lang) {
1878
+ const graph = entry.graph;
1879
+ if (!graph)
1880
+ return '';
1881
+ const zh = lang === 'zh';
1882
+ const doctor = graph.doctor;
1883
+ const evalGraph = graph.eval;
1884
+ const band = skillMapBand(entry);
1885
+ const markdown = renderSkillEvidenceMarkdown(entry, lang);
1886
+ const definitionNodesWithCoverageDeclarations = applyDefinitionCoverageDeclarations(doctor?.definitionNodes, evalGraph?.declaredCoverageStableKeys);
1887
+ const coverageSummary = skillMapCoverageDeclarationSummary(doctor, evalGraph, lang);
1888
+ const definitionCandidates = orderedPreviewNodes(definitionNodesWithCoverageDeclarations, DEFINITION_PREVIEW_KINDS, {
1889
+ reference: 1,
1890
+ script: 2,
1891
+ workflow: 3,
1892
+ hard_rule: 4,
1893
+ frontmatter: 5,
1894
+ workflow_node: 6,
1895
+ });
1896
+ const measurementCandidates = orderedPreviewNodes(evalGraph?.measurementNodes, MEASUREMENT_PREVIEW_KINDS, {
1897
+ sample: 1,
1898
+ diagnostic: 2,
1899
+ 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);
1905
+ const measurementVisible = measurementCandidates.slice(0, measurementLimit);
1906
+ 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
+ ];
1936
+ const measurementNodes = [
1937
+ ...measurementVisible.map((node, index) => ({
1938
+ id: `measurement-${index}`,
1939
+ node,
1940
+ layer: 'measurement',
1941
+ ...SKILL_MAP_MEASUREMENT_COORDS[index],
1942
+ })),
1943
+ ...(measurementOverflow.length > 0 ? [{
1944
+ id: 'measurement-more',
1945
+ node: moreNode(`+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`),
1946
+ layer: 'measurement',
1947
+ moreToggle: {
1948
+ group: 'measurement',
1949
+ collapsedLabel: `+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`,
1950
+ expandedLabel: zh ? '收起测量节点' : 'collapse measurement nodes',
1951
+ },
1952
+ ...measurementMorePosition,
1953
+ }] : []),
1954
+ ...measurementOverflow.map((node, index) => ({
1955
+ id: `measurement-overflow-${index}`,
1956
+ node,
1957
+ layer: 'measurement',
1958
+ edgeFrom: measurementMorePosition,
1959
+ overflowGroup: 'measurement',
1960
+ ...overflowNodePosition('measurement', index),
1961
+ })),
1962
+ ];
1963
+ const mapNodes = [...definitionNodes, ...measurementNodes];
1964
+ 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(' · ');
1970
+ return `<section id="section-skill-map" class="si-sect si-sect--${band} sm-sect">
1971
+ <div class="si-sect-h">
1972
+ <span class="si-sect-title">🗺 ${zh ? 'Skill Map' : 'Skill Map'}</span>
1973
+ <span class="si-sect-meta">${e(topMeta || (zh ? '图谱数据未生成' : 'graph data missing'))}</span>
1974
+ </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}">
1976
+ ${renderSkillStageRail(entry, lang)}
1977
+ <div class="sm-toolbar" aria-label="${zh ? 'Skill Map 控制' : 'Skill Map controls'}">
1978
+ <div class="sm-tools">
1979
+ <button class="sm-tool" type="button" data-sm-action="zoom-out" title="${zh ? '缩小' : 'Zoom out'}">−</button>
1980
+ <span class="sm-zoom-label" data-sm-zoom-label>100%</span>
1981
+ <button class="sm-tool" type="button" data-sm-action="zoom-in" title="${zh ? '放大' : 'Zoom in'}">+</button>
1982
+ <button class="sm-tool" type="button" data-sm-action="reset" title="${zh ? '重置视图' : 'Reset view'}">${zh ? '重置' : 'Reset'}</button>
1983
+ </div>
1984
+ <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>
1987
+ <label class="sm-toggle"><input type="checkbox" data-sm-toggle-leaves checked> ${zh ? '叶子节点' : 'Leaf nodes'}</label>
1988
+ </div>
1989
+ </div>
1990
+ <div class="sm-canvas" data-sm-viewport aria-label="Skill Map">
1991
+ <div class="sm-graph-stage" data-sm-graph-stage style="width:${mapDimensions.width}px;height:${mapDimensions.height}px">
1992
+ <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)}">
1995
+ <div class="sm-node-title">SKILL.md</div>
1996
+ <div class="sm-node-meta">${e(entry.skillName)}</div>
1997
+ </div>
1998
+ ${mapNodes.map((node) => renderSkillMapNode(node, lang)).join('')}
1999
+ </div>
2000
+ </div>
2001
+ </div>
2002
+ <details class="sm-card">
2003
+ <summary>${zh ? '复制图谱摘要' : 'Copy Skill Map Summary'}</summary>
2004
+ <textarea readonly>${e(markdown)}</textarea>
2005
+ </details>
2006
+ </div>
2007
+ </section>`;
2008
+ }
1109
2009
  // ────────── 主入口 ──────────
1110
2010
  export function renderSkillDetail(entry, evalReport, lang = DEFAULT_LANG, insights = []) {
1111
2011
  const langQ = lang === DEFAULT_LANG ? '' : `?lang=${lang}`;
@@ -1116,11 +2016,13 @@ export function renderSkillDetail(entry, evalReport, lang = DEFAULT_LANG, insigh
1116
2016
  <main>
1117
2017
  <a class="si-back" href="/${langQ}">${lang === 'zh' ? '← 返回 Skill 列表' : '← Back to Skills'}</a>
1118
2018
  ${renderHero(entry, insights, lastTs, reportCount, lang)}
2019
+ ${renderSkillMapSection(entry, lang)}
1119
2020
  ${renderDoctorSection(entry.doctor, entry.doctorHistory, entry.skillName, lang)}
1120
2021
  ${renderEvalSection(entry.eval, evalReport, entry.evalHistory, langQ, lang)}
1121
2022
  ${renderObserveSection(entry.observe, langQ, lang)}
1122
2023
  </main>
1123
2024
  <style>${SKILL_DETAIL_CSS}</style>
1124
2025
  ${TREND_INIT_SCRIPT}
2026
+ ${SKILL_MAP_INIT_SCRIPT}
1125
2027
  `, lang);
1126
2028
  }