oh-my-knowledge 0.43.0 → 0.44.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.
- package/dist/artifact-graph/eval.d.ts +16 -0
- package/dist/artifact-graph/eval.js +351 -0
- package/dist/doctor/index.js +2 -7
- package/dist/eval-core/artifact-file-names.d.ts +1 -1
- package/dist/eval-core/artifact-file-names.js +2 -3
- package/dist/eval-core/evaluation-reporting.d.ts +1 -1
- package/dist/eval-core/evaluation-reporting.js +31 -12
- package/dist/renderer/html-renderer.js +3 -2
- package/dist/renderer/skill-detail-renderer.js +832 -0
- package/dist/renderer/skill-list-renderer.js +2 -7
- package/dist/server/report-server.js +45 -11
- package/dist/server/skill-index.d.ts +6 -0
- package/dist/server/skill-index.js +384 -5
- package/dist/types/skill-index.d.ts +38 -0
- package/package.json +1 -1
|
@@ -397,6 +397,71 @@ 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-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 }
|
|
437
|
+
.sm-node--skill .sm-node-title { font-size:15px;line-height:1.2 }
|
|
438
|
+
.sm-node-meta { font-size:10.5px;color:#637083;line-height:1.35;word-break:break-word }
|
|
439
|
+
.sm-node--skill .sm-node-meta { font-size:11.5px }
|
|
440
|
+
.sm-node-kind { font-size:9.5px;text-transform:uppercase;letter-spacing:0;color:#8792a4;font-family:"SF Mono",Menlo,monospace;line-height:1 }
|
|
441
|
+
.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 }
|
|
442
|
+
.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 }
|
|
443
|
+
.sm-stage-card--ok { border-color:rgba(31,157,99,.38);background:rgba(31,157,99,.06) }
|
|
444
|
+
.sm-stage-card--warning { border-color:rgba(217,119,6,.38);background:rgba(217,119,6,.07) }
|
|
445
|
+
.sm-stage-card--failed { border-color:rgba(220,38,38,.38);background:rgba(220,38,38,.06) }
|
|
446
|
+
.sm-stage-card--pending { border-style:dashed;color:#637083;background:#f8f9fb }
|
|
447
|
+
.sm-stage-k { font-size:10px;text-transform:uppercase;letter-spacing:0;color:#7b8798;font-family:"SF Mono",Menlo,monospace;line-height:1 }
|
|
448
|
+
.sm-stage-v { font-size:13px;font-weight:700;color:#182033;line-height:1.35 }
|
|
449
|
+
.sm-stage-arrow { color:#b8c5d8;font-size:18px;line-height:1;text-align:center }
|
|
450
|
+
.sm-card { border:1px solid #e4e8f1;border-radius:8px;background:#fff;overflow:hidden }
|
|
451
|
+
.sm-card > summary { cursor:pointer;list-style:none;padding:10px 12px;font-size:12.5px;font-weight:600;color:#4f46e5;user-select:none }
|
|
452
|
+
.sm-card > summary::-webkit-details-marker { display:none }
|
|
453
|
+
.sm-card > summary::before { content:'▸ ';color:#94a3b8;font-size:10px }
|
|
454
|
+
.sm-card[open] > summary::before { content:'▾ ' }
|
|
455
|
+
.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 }
|
|
456
|
+
@media(max-width:900px){
|
|
457
|
+
.sm-toolbar { align-items:flex-start }
|
|
458
|
+
.sm-stage-rail { grid-template-columns:1fr;gap:6px }
|
|
459
|
+
.sm-stage-arrow { transform:rotate(90deg);font-size:16px }
|
|
460
|
+
.sm-canvas { height:620px;min-height:620px }
|
|
461
|
+
.sm-node { width:142px }
|
|
462
|
+
.sm-node--skill { width:168px }
|
|
463
|
+
}
|
|
464
|
+
|
|
400
465
|
/* doctor 规则展示(用在 doctor section 和 observe section) */
|
|
401
466
|
.sd-grid { display:flex;flex-direction:column;gap:4px }
|
|
402
467
|
.sd-dim { border-radius:6px;border:1px solid var(--border);overflow:hidden }
|
|
@@ -768,6 +833,322 @@ const TREND_INIT_SCRIPT = `
|
|
|
768
833
|
else init();
|
|
769
834
|
})();
|
|
770
835
|
</script>`;
|
|
836
|
+
const SKILL_MAP_INIT_SCRIPT = `
|
|
837
|
+
<script>
|
|
838
|
+
(function(){
|
|
839
|
+
function clamp(value, min, max){
|
|
840
|
+
return Math.max(min, Math.min(max, value));
|
|
841
|
+
}
|
|
842
|
+
function initSkillMap(map){
|
|
843
|
+
var viewport = map.querySelector('[data-sm-viewport]');
|
|
844
|
+
var stage = map.querySelector('[data-sm-graph-stage]');
|
|
845
|
+
var graph = map.querySelector('[data-sm-graph]');
|
|
846
|
+
var label = map.querySelector('[data-sm-zoom-label]');
|
|
847
|
+
if (!viewport || !stage || !graph) return;
|
|
848
|
+
var baseWidth = Number(map.getAttribute('data-sm-base-width')) || 1100;
|
|
849
|
+
var baseHeight = Number(map.getAttribute('data-sm-base-height')) || 560;
|
|
850
|
+
var rootX = Number(map.getAttribute('data-sm-root-x')) || 470;
|
|
851
|
+
var rootY = Number(map.getAttribute('data-sm-root-y')) || 260;
|
|
852
|
+
var zoom = 1;
|
|
853
|
+
var dragState = null;
|
|
854
|
+
var suppressMoreClickUntil = 0;
|
|
855
|
+
|
|
856
|
+
function edgePoint(fromX, fromY, toX, toY, box){
|
|
857
|
+
var dx = toX - fromX;
|
|
858
|
+
var dy = toY - fromY;
|
|
859
|
+
if (!dx && !dy) return { x: fromX, y: fromY };
|
|
860
|
+
var halfW = Math.max(1, (box.width || 154) / 2);
|
|
861
|
+
var halfH = Math.max(1, (box.height || 58) / 2);
|
|
862
|
+
var scaleX = dx === 0 ? Infinity : halfW / Math.abs(dx);
|
|
863
|
+
var scaleY = dy === 0 ? Infinity : halfH / Math.abs(dy);
|
|
864
|
+
var scale = Math.min(scaleX, scaleY);
|
|
865
|
+
return { x: fromX + dx * scale, y: fromY + dy * scale };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function nodeBox(node, fallbackWidth, fallbackHeight){
|
|
869
|
+
return {
|
|
870
|
+
width: node && node.offsetWidth ? node.offsetWidth : fallbackWidth,
|
|
871
|
+
height: node && node.offsetHeight ? node.offsetHeight : fallbackHeight
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function nodeCenter(node, fallbackX, fallbackY){
|
|
876
|
+
if (!node) return { x: fallbackX, y: fallbackY };
|
|
877
|
+
return {
|
|
878
|
+
x: Number(node.getAttribute('data-sm-x')) || parseFloat(node.style.left) || fallbackX,
|
|
879
|
+
y: Number(node.getAttribute('data-sm-y')) || parseFloat(node.style.top) || fallbackY
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function pathData(fromX, fromY, toX, toY, fromBox, toBox){
|
|
884
|
+
var start = edgePoint(fromX, fromY, toX, toY, fromBox);
|
|
885
|
+
var end = edgePoint(toX, toY, fromX, fromY, toBox);
|
|
886
|
+
var sx = start.x;
|
|
887
|
+
var sy = start.y;
|
|
888
|
+
var tx = end.x;
|
|
889
|
+
var ty = end.y;
|
|
890
|
+
var dx = Math.abs(tx - sx);
|
|
891
|
+
var c1x = sx + (tx >= sx ? dx * 0.45 : -dx * 0.45);
|
|
892
|
+
var c2x = tx - (tx >= sx ? dx * 0.45 : -dx * 0.45);
|
|
893
|
+
return 'M ' + sx + ' ' + sy + ' C ' + c1x + ' ' + sy + ', ' + c2x + ' ' + ty + ', ' + tx + ' ' + ty;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function setNodePosition(node, x, y){
|
|
897
|
+
node.setAttribute('data-sm-x', String(Math.round(x * 10) / 10));
|
|
898
|
+
node.setAttribute('data-sm-y', String(Math.round(y * 10) / 10));
|
|
899
|
+
node.style.left = x + 'px';
|
|
900
|
+
node.style.top = y + 'px';
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function updateEdgesForNode(nodeId, x, y){
|
|
904
|
+
map.querySelectorAll('[data-sm-edge-to]').forEach(function(edge){
|
|
905
|
+
if (edge.getAttribute('data-sm-edge-to') !== nodeId) return;
|
|
906
|
+
var fromNodeId = edge.getAttribute('data-sm-edge-from-node');
|
|
907
|
+
var fromNode = fromNodeId ? graph.querySelector('[data-sm-node-id="' + fromNodeId + '"]') : graph.querySelector('[data-sm-root]');
|
|
908
|
+
var toNode = graph.querySelector('[data-sm-node-id="' + nodeId + '"]');
|
|
909
|
+
var fromCenter = nodeCenter(fromNode, rootX, rootY);
|
|
910
|
+
edge.setAttribute('d', pathData(
|
|
911
|
+
fromCenter.x,
|
|
912
|
+
fromCenter.y,
|
|
913
|
+
x,
|
|
914
|
+
y,
|
|
915
|
+
nodeBox(fromNode, 184, 108),
|
|
916
|
+
nodeBox(toNode, 154, 58)
|
|
917
|
+
));
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function updateEdgesFromNode(nodeId, x, y){
|
|
922
|
+
map.querySelectorAll('[data-sm-edge-from-node]').forEach(function(edge){
|
|
923
|
+
if (edge.getAttribute('data-sm-edge-from-node') !== nodeId) return;
|
|
924
|
+
var toId = edge.getAttribute('data-sm-edge-to');
|
|
925
|
+
var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
|
|
926
|
+
if (!toNode) return;
|
|
927
|
+
var toCenter = nodeCenter(toNode, 0, 0);
|
|
928
|
+
edge.setAttribute('data-sm-edge-from-x', String(Math.round(x * 10) / 10));
|
|
929
|
+
edge.setAttribute('data-sm-edge-from-y', String(Math.round(y * 10) / 10));
|
|
930
|
+
edge.setAttribute('d', pathData(
|
|
931
|
+
x,
|
|
932
|
+
y,
|
|
933
|
+
toCenter.x,
|
|
934
|
+
toCenter.y,
|
|
935
|
+
nodeBox(graph.querySelector('[data-sm-node-id="' + nodeId + '"]'), 154, 58),
|
|
936
|
+
nodeBox(toNode, 154, 58)
|
|
937
|
+
));
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function refreshEdges(){
|
|
942
|
+
map.querySelectorAll('[data-sm-edge-to]').forEach(function(edge){
|
|
943
|
+
var toId = edge.getAttribute('data-sm-edge-to');
|
|
944
|
+
var toNode = toId ? graph.querySelector('[data-sm-node-id="' + toId + '"]') : null;
|
|
945
|
+
if (!toNode) return;
|
|
946
|
+
var toCenter = nodeCenter(toNode, 0, 0);
|
|
947
|
+
updateEdgesForNode(toId, toCenter.x, toCenter.y);
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function resetNodePositions(){
|
|
952
|
+
graph.querySelectorAll('[data-sm-draggable="1"]').forEach(function(node){
|
|
953
|
+
var nodeId = node.getAttribute('data-sm-node-id');
|
|
954
|
+
var originX = Number(node.getAttribute('data-sm-origin-x')) || Number(node.getAttribute('data-sm-x')) || parseFloat(node.style.left) || 0;
|
|
955
|
+
var originY = Number(node.getAttribute('data-sm-origin-y')) || Number(node.getAttribute('data-sm-y')) || parseFloat(node.style.top) || 0;
|
|
956
|
+
setNodePosition(node, originX, originY);
|
|
957
|
+
if (nodeId) updateEdgesForNode(nodeId, originX, originY);
|
|
958
|
+
if (nodeId) updateEdgesFromNode(nodeId, originX, originY);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function applyZoom(){
|
|
963
|
+
stage.style.width = Math.ceil(baseWidth * zoom) + 'px';
|
|
964
|
+
stage.style.height = Math.ceil(baseHeight * zoom) + 'px';
|
|
965
|
+
graph.style.transform = 'scale(' + zoom + ')';
|
|
966
|
+
if (label) label.textContent = Math.round(zoom * 100) + '%';
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function setZoom(nextZoom, anchorX, anchorY){
|
|
970
|
+
var oldZoom = zoom;
|
|
971
|
+
var x = anchorX == null ? viewport.clientWidth / 2 : anchorX;
|
|
972
|
+
var y = anchorY == null ? viewport.clientHeight / 2 : anchorY;
|
|
973
|
+
var graphX = (viewport.scrollLeft + x) / oldZoom;
|
|
974
|
+
var graphY = (viewport.scrollTop + y) / oldZoom;
|
|
975
|
+
zoom = clamp(nextZoom, 0.65, 1.8);
|
|
976
|
+
applyZoom();
|
|
977
|
+
viewport.scrollLeft = Math.max(0, graphX * zoom - x);
|
|
978
|
+
viewport.scrollTop = Math.max(0, graphY * zoom - y);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function centerRoot(){
|
|
982
|
+
var rootNode = graph.querySelector('[data-sm-root]');
|
|
983
|
+
var rootCenter = nodeCenter(rootNode, rootX, rootY);
|
|
984
|
+
var scaledRootX = rootCenter.x * zoom;
|
|
985
|
+
var scaledRootY = rootCenter.y * zoom;
|
|
986
|
+
viewport.scrollLeft = Math.max(0, scaledRootX - viewport.clientWidth / 2);
|
|
987
|
+
viewport.scrollTop = Math.max(0, scaledRootY - viewport.clientHeight / 2);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function layerEnabled(layer){
|
|
991
|
+
var input = map.querySelector('[data-sm-toggle-layer="' + layer + '"]');
|
|
992
|
+
return !input || input.checked;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function overflowGroupExpanded(group){
|
|
996
|
+
if (!group) return true;
|
|
997
|
+
var toggle = map.querySelector('[data-sm-more-toggle="' + group + '"]');
|
|
998
|
+
return !!toggle && toggle.getAttribute('aria-expanded') === 'true';
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function updateVisibility(){
|
|
1002
|
+
var showLeaves = true;
|
|
1003
|
+
var leafToggle = map.querySelector('[data-sm-toggle-leaves]');
|
|
1004
|
+
if (leafToggle) showLeaves = leafToggle.checked;
|
|
1005
|
+
var layers = {
|
|
1006
|
+
definition: layerEnabled('definition'),
|
|
1007
|
+
measurement: layerEnabled('measurement')
|
|
1008
|
+
};
|
|
1009
|
+
map.querySelectorAll('[data-sm-layer]').forEach(function(el){
|
|
1010
|
+
var layer = el.getAttribute('data-sm-layer');
|
|
1011
|
+
var isLeaf = el.getAttribute('data-sm-leaf') === '1';
|
|
1012
|
+
var overflowGroup = el.getAttribute('data-sm-overflow-group');
|
|
1013
|
+
var shouldHide = !layers[layer] || (isLeaf && !showLeaves) || !overflowGroupExpanded(overflowGroup);
|
|
1014
|
+
el.hidden = shouldHide;
|
|
1015
|
+
el.toggleAttribute('hidden', shouldHide);
|
|
1016
|
+
el.setAttribute('aria-hidden', shouldHide ? 'true' : 'false');
|
|
1017
|
+
el.style.display = shouldHide ? 'none' : '';
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function setMoreExpanded(toggle, expanded){
|
|
1022
|
+
toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
|
1023
|
+
var nextLabel = expanded
|
|
1024
|
+
? toggle.getAttribute('data-sm-expanded-label')
|
|
1025
|
+
: toggle.getAttribute('data-sm-collapsed-label');
|
|
1026
|
+
var title = toggle.querySelector('.sm-node-title');
|
|
1027
|
+
if (title && nextLabel) title.textContent = nextLabel;
|
|
1028
|
+
if (nextLabel) toggle.setAttribute('title', nextLabel);
|
|
1029
|
+
updateVisibility();
|
|
1030
|
+
refreshEdges();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function collapseMoreGroups(){
|
|
1034
|
+
map.querySelectorAll('[data-sm-more-toggle]').forEach(function(toggle){
|
|
1035
|
+
setMoreExpanded(toggle, false);
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
map.querySelectorAll('[data-sm-action]').forEach(function(button){
|
|
1040
|
+
button.addEventListener('click', function(){
|
|
1041
|
+
var action = button.getAttribute('data-sm-action');
|
|
1042
|
+
if (action === 'zoom-in') setZoom(zoom + 0.15);
|
|
1043
|
+
if (action === 'zoom-out') setZoom(zoom - 0.15);
|
|
1044
|
+
if (action === 'reset') {
|
|
1045
|
+
zoom = 1;
|
|
1046
|
+
collapseMoreGroups();
|
|
1047
|
+
resetNodePositions();
|
|
1048
|
+
applyZoom();
|
|
1049
|
+
centerRoot();
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
map.querySelectorAll('[data-sm-toggle-layer], [data-sm-toggle-leaves]').forEach(function(input){
|
|
1055
|
+
input.addEventListener('change', updateVisibility);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
graph.querySelectorAll('[data-sm-more-toggle]').forEach(function(toggle){
|
|
1059
|
+
toggle.addEventListener('click', function(ev){
|
|
1060
|
+
if (Date.now() < suppressMoreClickUntil) {
|
|
1061
|
+
ev.preventDefault();
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
ev.preventDefault();
|
|
1065
|
+
setMoreExpanded(toggle, toggle.getAttribute('aria-expanded') !== 'true');
|
|
1066
|
+
});
|
|
1067
|
+
toggle.addEventListener('keydown', function(ev){
|
|
1068
|
+
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
|
1069
|
+
ev.preventDefault();
|
|
1070
|
+
setMoreExpanded(toggle, toggle.getAttribute('aria-expanded') !== 'true');
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
graph.querySelectorAll('[data-sm-draggable="1"]').forEach(function(node){
|
|
1075
|
+
node.addEventListener('pointerdown', function(ev){
|
|
1076
|
+
if (ev.button != null && ev.button !== 0) return;
|
|
1077
|
+
if (node.hidden) return;
|
|
1078
|
+
var nodeId = node.getAttribute('data-sm-node-id');
|
|
1079
|
+
if (!nodeId) return;
|
|
1080
|
+
ev.preventDefault();
|
|
1081
|
+
dragState = {
|
|
1082
|
+
node: node,
|
|
1083
|
+
nodeId: nodeId,
|
|
1084
|
+
pointerId: ev.pointerId,
|
|
1085
|
+
startClientX: ev.clientX,
|
|
1086
|
+
startClientY: ev.clientY,
|
|
1087
|
+
startX: Number(node.getAttribute('data-sm-x')) || parseFloat(node.style.left) || 0,
|
|
1088
|
+
startY: Number(node.getAttribute('data-sm-y')) || parseFloat(node.style.top) || 0,
|
|
1089
|
+
moved: false
|
|
1090
|
+
};
|
|
1091
|
+
node.classList.add('is-dragging');
|
|
1092
|
+
node.setPointerCapture(ev.pointerId);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
node.addEventListener('pointermove', function(ev){
|
|
1096
|
+
if (!dragState || dragState.node !== node) return;
|
|
1097
|
+
ev.preventDefault();
|
|
1098
|
+
var deltaX = ev.clientX - dragState.startClientX;
|
|
1099
|
+
var deltaY = ev.clientY - dragState.startClientY;
|
|
1100
|
+
if (Math.sqrt(deltaX * deltaX + deltaY * deltaY) > 3) dragState.moved = true;
|
|
1101
|
+
var nextX = clamp(dragState.startX + deltaX / zoom, 52, baseWidth - 52);
|
|
1102
|
+
var nextY = clamp(dragState.startY + deltaY / zoom, 36, baseHeight - 36);
|
|
1103
|
+
setNodePosition(node, nextX, nextY);
|
|
1104
|
+
updateEdgesForNode(dragState.nodeId, nextX, nextY);
|
|
1105
|
+
updateEdgesFromNode(dragState.nodeId, nextX, nextY);
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
function finishDrag(ev){
|
|
1109
|
+
if (!dragState || dragState.node !== node) return;
|
|
1110
|
+
var wasMoved = dragState.moved;
|
|
1111
|
+
var isMoreToggle = node.hasAttribute('data-sm-more-toggle');
|
|
1112
|
+
node.classList.remove('is-dragging');
|
|
1113
|
+
if (node.hasPointerCapture && node.hasPointerCapture(dragState.pointerId)) {
|
|
1114
|
+
node.releasePointerCapture(dragState.pointerId);
|
|
1115
|
+
}
|
|
1116
|
+
dragState = null;
|
|
1117
|
+
if (!wasMoved && isMoreToggle) {
|
|
1118
|
+
suppressMoreClickUntil = Date.now() + 300;
|
|
1119
|
+
setMoreExpanded(node, node.getAttribute('aria-expanded') !== 'true');
|
|
1120
|
+
} else if (wasMoved && isMoreToggle) {
|
|
1121
|
+
suppressMoreClickUntil = Date.now() + 300;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
node.addEventListener('pointerup', finishDrag);
|
|
1126
|
+
node.addEventListener('pointercancel', finishDrag);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
viewport.addEventListener('wheel', function(ev){
|
|
1130
|
+
if (!ev.ctrlKey && !ev.metaKey) return;
|
|
1131
|
+
ev.preventDefault();
|
|
1132
|
+
var rect = viewport.getBoundingClientRect();
|
|
1133
|
+
var delta = ev.deltaY < 0 ? 0.12 : -0.12;
|
|
1134
|
+
setZoom(zoom + delta, ev.clientX - rect.left, ev.clientY - rect.top);
|
|
1135
|
+
}, { passive: false });
|
|
1136
|
+
|
|
1137
|
+
applyZoom();
|
|
1138
|
+
updateVisibility();
|
|
1139
|
+
requestAnimationFrame(function(){
|
|
1140
|
+
refreshEdges();
|
|
1141
|
+
centerRoot();
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function init(){
|
|
1146
|
+
document.querySelectorAll('[data-sm-map]').forEach(initSkillMap);
|
|
1147
|
+
}
|
|
1148
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
1149
|
+
else init();
|
|
1150
|
+
})();
|
|
1151
|
+
</script>`;
|
|
771
1152
|
/** doctor section:展示告警 / 失败规则;通过的规则折叠到底部 */
|
|
772
1153
|
function renderDoctorSection(snap, history, skillName, lang) {
|
|
773
1154
|
if (!snap) {
|
|
@@ -1106,6 +1487,455 @@ function renderObserveSection(snap, langQ, lang) {
|
|
|
1106
1487
|
</div>
|
|
1107
1488
|
</section>`;
|
|
1108
1489
|
}
|
|
1490
|
+
function skillMapBand(entry) {
|
|
1491
|
+
if (entry.doctor?.failCount || entry.eval?.failCount)
|
|
1492
|
+
return 'red';
|
|
1493
|
+
if (entry.doctor?.warnCount || (entry.eval?.compositeScore != null && entry.eval.compositeScore < 3.5))
|
|
1494
|
+
return 'yellow';
|
|
1495
|
+
if (entry.doctor || entry.eval || entry.observe)
|
|
1496
|
+
return 'green';
|
|
1497
|
+
return 'gray';
|
|
1498
|
+
}
|
|
1499
|
+
function stageText(entry, stage, lang) {
|
|
1500
|
+
const zh = lang === 'zh';
|
|
1501
|
+
if (stage === 'doctor') {
|
|
1502
|
+
if (!entry.doctor)
|
|
1503
|
+
return zh ? '未体检' : 'not checked';
|
|
1504
|
+
if (entry.doctor.failCount > 0)
|
|
1505
|
+
return zh ? '未通过' : 'failed';
|
|
1506
|
+
if (entry.doctor.warnCount > 0)
|
|
1507
|
+
return zh ? '有警告' : 'warnings';
|
|
1508
|
+
return zh ? '已通过' : 'passed';
|
|
1509
|
+
}
|
|
1510
|
+
if (stage === 'eval') {
|
|
1511
|
+
if (!entry.eval)
|
|
1512
|
+
return zh ? '未测量' : 'not measured';
|
|
1513
|
+
if (entry.eval.resultsStripped)
|
|
1514
|
+
return entry.eval.compositeScore != null ? `${entry.eval.compositeScore.toFixed(2)}/5` : (zh ? '已测量' : 'measured');
|
|
1515
|
+
return zh
|
|
1516
|
+
? `${entry.eval.passCount}/${entry.eval.totalSamples} 用例通过`
|
|
1517
|
+
: `${entry.eval.passCount}/${entry.eval.totalSamples} samples pass`;
|
|
1518
|
+
}
|
|
1519
|
+
if (!entry.observe)
|
|
1520
|
+
return zh ? '未观察' : 'not observed';
|
|
1521
|
+
return zh
|
|
1522
|
+
? `${entry.observe.segmentCount} 段,gap ${Math.round(entry.observe.gapRate * 100)}%`
|
|
1523
|
+
: `${entry.observe.segmentCount} segments, gap ${Math.round(entry.observe.gapRate * 100)}%`;
|
|
1524
|
+
}
|
|
1525
|
+
function mermaidCardLabel(label) {
|
|
1526
|
+
const entities = {
|
|
1527
|
+
'&': '&',
|
|
1528
|
+
'\\': '\',
|
|
1529
|
+
'"': '"',
|
|
1530
|
+
'[': '[',
|
|
1531
|
+
']': ']',
|
|
1532
|
+
'(': '(',
|
|
1533
|
+
')': ')',
|
|
1534
|
+
'{': '{',
|
|
1535
|
+
'}': '}',
|
|
1536
|
+
'|': '|',
|
|
1537
|
+
'#': '#',
|
|
1538
|
+
';': ';',
|
|
1539
|
+
'<': '<',
|
|
1540
|
+
'>': '>',
|
|
1541
|
+
};
|
|
1542
|
+
return label
|
|
1543
|
+
.replace(/\s+/g, ' ')
|
|
1544
|
+
.trim()
|
|
1545
|
+
.replace(/[&\\"[\](){}|#;<>]/g, (ch) => entities[ch] ?? ch);
|
|
1546
|
+
}
|
|
1547
|
+
function markdownInline(value) {
|
|
1548
|
+
if (!value)
|
|
1549
|
+
return '`unknown`';
|
|
1550
|
+
return `\`${value.replaceAll('`', '\\`')}\``;
|
|
1551
|
+
}
|
|
1552
|
+
function sourceCommandArg(value) {
|
|
1553
|
+
if (/^[A-Za-z0-9_./:@%+-]+$/.test(value))
|
|
1554
|
+
return value;
|
|
1555
|
+
return JSON.stringify(value);
|
|
1556
|
+
}
|
|
1557
|
+
const DEFINITION_PREVIEW_KINDS = new Set([
|
|
1558
|
+
'frontmatter',
|
|
1559
|
+
'reference',
|
|
1560
|
+
'script',
|
|
1561
|
+
'hard_rule',
|
|
1562
|
+
'workflow',
|
|
1563
|
+
'workflow_node',
|
|
1564
|
+
]);
|
|
1565
|
+
const MEASUREMENT_PREVIEW_KINDS = new Set([
|
|
1566
|
+
'sample',
|
|
1567
|
+
'assertion',
|
|
1568
|
+
'diagnostic',
|
|
1569
|
+
]);
|
|
1570
|
+
function nodeKindLabel(kind, lang) {
|
|
1571
|
+
const zh = lang === 'zh';
|
|
1572
|
+
const labels = {
|
|
1573
|
+
frontmatter: ['meta', 'meta'],
|
|
1574
|
+
reference: ['ref', 'ref'],
|
|
1575
|
+
script: ['script', 'script'],
|
|
1576
|
+
tool: ['tool', 'tool'],
|
|
1577
|
+
preflight: ['preflight', 'preflight'],
|
|
1578
|
+
hard_rule: ['rule', 'rule'],
|
|
1579
|
+
workflow: ['workflow', 'workflow'],
|
|
1580
|
+
workflow_node: ['step', 'step'],
|
|
1581
|
+
sample: ['sample', 'sample'],
|
|
1582
|
+
assertion: ['assert', 'assert'],
|
|
1583
|
+
diagnostic: ['diag', 'diag'],
|
|
1584
|
+
doctor_rule_result: ['doctor', 'doctor'],
|
|
1585
|
+
eval_result: ['eval', 'eval'],
|
|
1586
|
+
trace_session: ['observe', 'observe'],
|
|
1587
|
+
more: ['more', 'more'],
|
|
1588
|
+
};
|
|
1589
|
+
const pair = labels[kind] ?? [kind, kind];
|
|
1590
|
+
return zh ? pair[0] : pair[1];
|
|
1591
|
+
}
|
|
1592
|
+
function graphNodeDisplayLabel(node, lang) {
|
|
1593
|
+
const zh = lang === 'zh';
|
|
1594
|
+
switch (node.nodeKind) {
|
|
1595
|
+
case 'workflow':
|
|
1596
|
+
return `${zh ? 'workflow' : 'workflow'}: ${node.label}`;
|
|
1597
|
+
case 'hard_rule':
|
|
1598
|
+
return `${zh ? 'rule' : 'rule'}: ${node.label}`;
|
|
1599
|
+
case 'tool':
|
|
1600
|
+
return `${zh ? 'tool' : 'tool'}: ${node.label}`;
|
|
1601
|
+
case 'sample':
|
|
1602
|
+
return `${zh ? 'sample' : 'sample'}: ${node.label}`;
|
|
1603
|
+
case 'diagnostic':
|
|
1604
|
+
return node.label.startsWith('diagnostic') ? node.label : `${zh ? 'diagnostic' : 'diagnostic'}: ${node.label}`;
|
|
1605
|
+
default:
|
|
1606
|
+
return node.label;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function graphNodeMapLabel(node) {
|
|
1610
|
+
switch (node.nodeKind) {
|
|
1611
|
+
case 'workflow':
|
|
1612
|
+
case 'hard_rule':
|
|
1613
|
+
case 'tool':
|
|
1614
|
+
case 'sample':
|
|
1615
|
+
return node.label;
|
|
1616
|
+
case 'assertion':
|
|
1617
|
+
return node.label.replace(/^assertion:\s*/i, '');
|
|
1618
|
+
case 'diagnostic':
|
|
1619
|
+
return node.label.replace(/^diagnostic:\s*/i, '');
|
|
1620
|
+
default:
|
|
1621
|
+
return node.label;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
function previewNodes(nodes, allowedKinds, limit) {
|
|
1625
|
+
const filtered = (nodes ?? []).filter((node) => allowedKinds.has(node.nodeKind));
|
|
1626
|
+
return { visible: filtered.slice(0, limit), hiddenCount: Math.max(0, filtered.length - limit) };
|
|
1627
|
+
}
|
|
1628
|
+
const SKILL_MAP_WIDTH = 1100;
|
|
1629
|
+
const SKILL_MAP_HEIGHT = 560;
|
|
1630
|
+
const SKILL_MAP_ROOT_ID = 'skill-root';
|
|
1631
|
+
const SKILL_MAP_ROOT = { x: 470, y: 260 };
|
|
1632
|
+
const SKILL_MAP_ROOT_SIZE = { width: 172, height: 86 };
|
|
1633
|
+
const SKILL_MAP_NODE_SIZE = { width: 148, height: 54 };
|
|
1634
|
+
const SKILL_MAP_DEFINITION_COORDS = [
|
|
1635
|
+
{ x: 170, y: 112 },
|
|
1636
|
+
{ x: 140, y: 255 },
|
|
1637
|
+
{ x: 220, y: 420 },
|
|
1638
|
+
{ x: 392, y: 112 },
|
|
1639
|
+
{ x: 382, y: 425 },
|
|
1640
|
+
{ x: 312, y: 190 },
|
|
1641
|
+
{ x: 310, y: 328 },
|
|
1642
|
+
];
|
|
1643
|
+
const SKILL_MAP_MEASUREMENT_COORDS = [
|
|
1644
|
+
{ x: 720, y: 105 },
|
|
1645
|
+
{ x: 920, y: 165 },
|
|
1646
|
+
{ x: 750, y: 275 },
|
|
1647
|
+
{ x: 948, y: 318 },
|
|
1648
|
+
{ x: 770, y: 430 },
|
|
1649
|
+
{ x: 960, y: 430 },
|
|
1650
|
+
];
|
|
1651
|
+
const SKILL_MAP_NODE_MARGIN = { x: 92, y: 48 };
|
|
1652
|
+
const SKILL_MAP_STATUS_CLASSES = new Set(['ok', 'warning', 'failed', 'skipped', 'unknown', 'not_measured']);
|
|
1653
|
+
function graphNodeStatusClass(node) {
|
|
1654
|
+
return node.status && SKILL_MAP_STATUS_CLASSES.has(node.status) ? ` sm-node--${node.status}` : '';
|
|
1655
|
+
}
|
|
1656
|
+
function positionedNodeStyle(node) {
|
|
1657
|
+
return `left:${node.x}px;top:${node.y}px`;
|
|
1658
|
+
}
|
|
1659
|
+
function renderSkillMapNode(item, lang) {
|
|
1660
|
+
const title = graphNodeMapLabel(item.node);
|
|
1661
|
+
const fullTitle = graphNodeDisplayLabel(item.node, lang);
|
|
1662
|
+
const kindClass = item.node.nodeKind === 'more' ? ' sm-node--more' : '';
|
|
1663
|
+
const leaf = item.layer === 'definition' || item.layer === 'measurement' ? ' data-sm-leaf="1"' : '';
|
|
1664
|
+
const draggable = item.layer === 'definition' || item.layer === 'measurement' ? ' data-sm-draggable="1"' : '';
|
|
1665
|
+
const overflow = item.overflowGroup ? ` data-sm-overflow-group="${item.overflowGroup}" hidden aria-hidden="true"` : '';
|
|
1666
|
+
const moreToggle = item.moreToggle
|
|
1667
|
+
? ` 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"`
|
|
1668
|
+
: '';
|
|
1669
|
+
return `<div class="sm-node sm-node--${item.layer}${kindClass}${graphNodeStatusClass(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)}">
|
|
1670
|
+
<div class="sm-node-kind">${e(nodeKindLabel(item.node.nodeKind, lang))}</div>
|
|
1671
|
+
<div class="sm-node-title">${e(title)}</div>
|
|
1672
|
+
</div>`;
|
|
1673
|
+
}
|
|
1674
|
+
function edgePoint(from, to, box) {
|
|
1675
|
+
const dx = to.x - from.x;
|
|
1676
|
+
const dy = to.y - from.y;
|
|
1677
|
+
if (dx === 0 && dy === 0)
|
|
1678
|
+
return from;
|
|
1679
|
+
const scale = Math.min(dx === 0 ? Infinity : box.width / 2 / Math.abs(dx), dy === 0 ? Infinity : box.height / 2 / Math.abs(dy));
|
|
1680
|
+
return { x: from.x + dx * scale, y: from.y + dy * scale };
|
|
1681
|
+
}
|
|
1682
|
+
function skillMapPath(from, to, fromBox, toBox) {
|
|
1683
|
+
const start = edgePoint(from, to, fromBox);
|
|
1684
|
+
const end = edgePoint(to, from, toBox);
|
|
1685
|
+
const dx = Math.abs(end.x - start.x);
|
|
1686
|
+
const c1x = start.x + (end.x >= start.x ? dx * 0.45 : -dx * 0.45);
|
|
1687
|
+
const c2x = end.x - (end.x >= start.x ? dx * 0.45 : -dx * 0.45);
|
|
1688
|
+
return `M ${start.x} ${start.y} C ${c1x} ${start.y}, ${c2x} ${end.y}, ${end.x} ${end.y}`;
|
|
1689
|
+
}
|
|
1690
|
+
function renderSkillMapEdges(nodes, dimensions) {
|
|
1691
|
+
const rootEdges = nodes
|
|
1692
|
+
.map((node) => {
|
|
1693
|
+
const from = node.edgeFrom ?? SKILL_MAP_ROOT;
|
|
1694
|
+
const fromBox = node.edgeFrom ? SKILL_MAP_NODE_SIZE : SKILL_MAP_ROOT_SIZE;
|
|
1695
|
+
const overflow = node.overflowGroup ? ` data-sm-overflow-group="${node.overflowGroup}" hidden aria-hidden="true"` : '';
|
|
1696
|
+
const fromNodeId = node.overflowGroup ? `${node.overflowGroup}-more` : SKILL_MAP_ROOT_ID;
|
|
1697
|
+
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>`;
|
|
1698
|
+
})
|
|
1699
|
+
.join('');
|
|
1700
|
+
return `<svg class="sm-svg" viewBox="0 0 ${dimensions.width} ${dimensions.height}" aria-hidden="true">${rootEdges}</svg>`;
|
|
1701
|
+
}
|
|
1702
|
+
function moreNode(label) {
|
|
1703
|
+
return { nodeKind: 'more', label };
|
|
1704
|
+
}
|
|
1705
|
+
function overflowNodePosition(layer, index) {
|
|
1706
|
+
const columns = layer === 'definition' ? [112, 270, 428] : [720, 880, 1040];
|
|
1707
|
+
const startY = layer === 'definition' ? 500 : 560;
|
|
1708
|
+
return {
|
|
1709
|
+
x: columns[index % columns.length],
|
|
1710
|
+
y: startY + Math.floor(index / columns.length) * 98,
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
function skillMapDimensions(nodes) {
|
|
1714
|
+
return {
|
|
1715
|
+
width: Math.max(SKILL_MAP_WIDTH, ...nodes.map((node) => node.x + SKILL_MAP_NODE_MARGIN.x)),
|
|
1716
|
+
height: Math.max(SKILL_MAP_HEIGHT, ...nodes.map((node) => node.y + SKILL_MAP_NODE_MARGIN.y)),
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function renderSkillStageRail(entry, lang) {
|
|
1720
|
+
const doctorStatus = entry.doctor?.failCount ? 'failed' : entry.doctor?.warnCount ? 'warning' : entry.doctor ? 'ok' : 'pending';
|
|
1721
|
+
const evalStatus = entry.eval?.failCount ? 'failed' : entry.eval ? 'ok' : 'pending';
|
|
1722
|
+
const observeStatus = entry.observe ? (entry.observe.healthBand === 'red' ? 'failed' : entry.observe.healthBand === 'yellow' ? 'warning' : 'ok') : 'pending';
|
|
1723
|
+
const card = (key, value, status) => `<div class="sm-stage-card sm-stage-card--${status}">
|
|
1724
|
+
<div class="sm-stage-k">${e(key)}</div>
|
|
1725
|
+
<div class="sm-stage-v">${e(value)}</div>
|
|
1726
|
+
</div>`;
|
|
1727
|
+
return `<div class="sm-stage-rail" aria-label="${lang === 'zh' ? '三阶段状态' : 'Stage status'}">
|
|
1728
|
+
${card('doctor', stageText(entry, 'doctor', lang), doctorStatus)}
|
|
1729
|
+
<div class="sm-stage-arrow">→</div>
|
|
1730
|
+
${card('eval', stageText(entry, 'eval', lang), evalStatus)}
|
|
1731
|
+
<div class="sm-stage-arrow">→</div>
|
|
1732
|
+
${card('observe', stageText(entry, 'observe', lang), observeStatus)}
|
|
1733
|
+
</div>`;
|
|
1734
|
+
}
|
|
1735
|
+
function orderedPreviewNodes(nodes, allowedKinds, priority) {
|
|
1736
|
+
return (nodes ?? [])
|
|
1737
|
+
.map((node, index) => ({ node, index }))
|
|
1738
|
+
.filter(({ node }) => allowedKinds.has(node.nodeKind))
|
|
1739
|
+
.sort((a, b) => (priority[a.node.nodeKind] ?? 99) - (priority[b.node.nodeKind] ?? 99) || a.index - b.index)
|
|
1740
|
+
.map(({ node }) => node);
|
|
1741
|
+
}
|
|
1742
|
+
function renderSkillEvidenceMarkdown(entry, lang) {
|
|
1743
|
+
const zh = lang === 'zh';
|
|
1744
|
+
const graph = entry.graph;
|
|
1745
|
+
const doctor = graph?.doctor;
|
|
1746
|
+
const evalGraph = graph?.eval;
|
|
1747
|
+
const source = graph?.sourceLocator ?? entry.skillName;
|
|
1748
|
+
const sourceArg = sourceCommandArg(source);
|
|
1749
|
+
const doctorStatus = stageText(entry, 'doctor', lang);
|
|
1750
|
+
const evalStatus = stageText(entry, 'eval', lang);
|
|
1751
|
+
const observeStatus = stageText(entry, 'observe', lang);
|
|
1752
|
+
const stepSeparator = zh ? ':' : ':';
|
|
1753
|
+
const summary = zh
|
|
1754
|
+
? `这个 skill 当前处于「doctor ${doctorStatus},eval ${evalStatus},observe ${observeStatus}」状态。`
|
|
1755
|
+
: `Current state: doctor ${doctorStatus}, eval ${evalStatus}, observe ${observeStatus}.`;
|
|
1756
|
+
const nextSteps = [
|
|
1757
|
+
!entry.doctor ? `- ${zh ? '体检结构' : 'Check structure'}${stepSeparator} \`omk doctor ${sourceArg}\`` : '',
|
|
1758
|
+
!entry.eval ? `- ${zh ? '测量效果' : 'Measure impact'}${stepSeparator} \`omk eval --control baseline --treatment ${sourceArg}\`` : '',
|
|
1759
|
+
!entry.observe ? `- ${zh ? '接入观察' : 'Add observation'}${stepSeparator} \`omk observe ingest <trace-dir>\`` : '',
|
|
1760
|
+
].filter(Boolean);
|
|
1761
|
+
const definitionPreview = previewNodes(doctor?.definitionNodes, DEFINITION_PREVIEW_KINDS, 8);
|
|
1762
|
+
const measurementPreview = previewNodes(evalGraph?.measurementNodes, MEASUREMENT_PREVIEW_KINDS, 8);
|
|
1763
|
+
const previewMermaidLines = [
|
|
1764
|
+
...definitionPreview.visible.flatMap((node, index) => [
|
|
1765
|
+
` def_${index}["${mermaidCardLabel(graphNodeDisplayLabel(node, lang))}"]`,
|
|
1766
|
+
` skill --> def_${index}`,
|
|
1767
|
+
]),
|
|
1768
|
+
...measurementPreview.visible.flatMap((node, index) => [
|
|
1769
|
+
` measure_${index}["${mermaidCardLabel(graphNodeDisplayLabel(node, lang))}"]`,
|
|
1770
|
+
` skill --> measure_${index}`,
|
|
1771
|
+
]),
|
|
1772
|
+
];
|
|
1773
|
+
return [
|
|
1774
|
+
`## Skill Evidence Card:${entry.skillName}`,
|
|
1775
|
+
'',
|
|
1776
|
+
summary,
|
|
1777
|
+
'',
|
|
1778
|
+
'```mermaid',
|
|
1779
|
+
'flowchart LR',
|
|
1780
|
+
` skill["SKILL.md: ${mermaidCardLabel(entry.skillName)}"]`,
|
|
1781
|
+
` doctor["doctor: ${mermaidCardLabel(doctorStatus)}"]`,
|
|
1782
|
+
` eval["eval: ${mermaidCardLabel(evalStatus)}"]`,
|
|
1783
|
+
` observe["observe: ${mermaidCardLabel(observeStatus)}"]`,
|
|
1784
|
+
...previewMermaidLines,
|
|
1785
|
+
' doctor --> eval',
|
|
1786
|
+
' eval --> observe',
|
|
1787
|
+
'```',
|
|
1788
|
+
'',
|
|
1789
|
+
zh ? '### 三阶段状态' : '### Stage Status',
|
|
1790
|
+
'',
|
|
1791
|
+
`| ${zh ? '阶段' : 'Stage'} | ${zh ? '状态' : 'Status'} | ${zh ? '证据' : 'Evidence'} |`,
|
|
1792
|
+
'| --- | --- | --- |',
|
|
1793
|
+
`| doctor | ${doctorStatus} | ${doctor ? `${doctor.nodeCount} nodes / ${doctor.edgeCount} edges` : (zh ? '无 graph' : 'no graph')} |`,
|
|
1794
|
+
`| eval | ${evalStatus} | ${evalGraph ? `${zh ? 'variant 子图' : 'variant subgraph'}: ${evalGraph.nodeCount} nodes / ${evalGraph.edgeCount} edges` : (zh ? '无 graph' : 'no graph')} |`,
|
|
1795
|
+
`| observe | ${observeStatus} | ${zh ? '未接 production graph' : 'production graph not connected'} |`,
|
|
1796
|
+
'',
|
|
1797
|
+
zh ? '### 关键计数' : '### Key Counts',
|
|
1798
|
+
'',
|
|
1799
|
+
'| references | scripts | workflows | workflow nodes | samples | failed samples |',
|
|
1800
|
+
'| ---: | ---: | ---: | ---: | ---: | ---: |',
|
|
1801
|
+
`| ${doctor?.references ?? 0} | ${doctor?.scripts ?? 0} | ${doctor?.workflows ?? 0} | ${doctor?.workflowNodes ?? 0} | ${entry.eval?.totalSamples ?? 0} | ${entry.eval?.failCount ?? 0} |`,
|
|
1802
|
+
...(nextSteps.length > 0 ? ['', zh ? '### 下一步' : '### Next Steps', '', ...nextSteps] : []),
|
|
1803
|
+
'',
|
|
1804
|
+
zh ? '### 复现信息' : '### Reproduction',
|
|
1805
|
+
'',
|
|
1806
|
+
`- source:${markdownInline(source)}`,
|
|
1807
|
+
`- artifactHash:${markdownInline(graph?.artifactHash)}`,
|
|
1808
|
+
`- binding:${markdownInline(graph?.bindingStrength)}`,
|
|
1809
|
+
`- doctorGraphId:${markdownInline(doctor?.graphId)}`,
|
|
1810
|
+
`- evalGraphId:${markdownInline(evalGraph?.graphId)}`,
|
|
1811
|
+
'',
|
|
1812
|
+
].join('\n');
|
|
1813
|
+
}
|
|
1814
|
+
function renderSkillMapSection(entry, lang) {
|
|
1815
|
+
const graph = entry.graph;
|
|
1816
|
+
if (!graph)
|
|
1817
|
+
return '';
|
|
1818
|
+
const zh = lang === 'zh';
|
|
1819
|
+
const doctor = graph.doctor;
|
|
1820
|
+
const evalGraph = graph.eval;
|
|
1821
|
+
const band = skillMapBand(entry);
|
|
1822
|
+
const markdown = renderSkillEvidenceMarkdown(entry, lang);
|
|
1823
|
+
const definitionCandidates = orderedPreviewNodes(doctor?.definitionNodes, DEFINITION_PREVIEW_KINDS, {
|
|
1824
|
+
reference: 1,
|
|
1825
|
+
script: 2,
|
|
1826
|
+
workflow: 3,
|
|
1827
|
+
hard_rule: 4,
|
|
1828
|
+
frontmatter: 5,
|
|
1829
|
+
workflow_node: 6,
|
|
1830
|
+
});
|
|
1831
|
+
const measurementCandidates = orderedPreviewNodes(evalGraph?.measurementNodes, MEASUREMENT_PREVIEW_KINDS, {
|
|
1832
|
+
sample: 1,
|
|
1833
|
+
diagnostic: 2,
|
|
1834
|
+
assertion: 3,
|
|
1835
|
+
});
|
|
1836
|
+
const definitionLimit = SKILL_MAP_DEFINITION_COORDS.length - 1;
|
|
1837
|
+
const measurementLimit = SKILL_MAP_MEASUREMENT_COORDS.length - 1;
|
|
1838
|
+
const definitionVisible = definitionCandidates.slice(0, definitionLimit);
|
|
1839
|
+
const definitionOverflow = definitionCandidates.slice(definitionLimit);
|
|
1840
|
+
const measurementVisible = measurementCandidates.slice(0, measurementLimit);
|
|
1841
|
+
const measurementOverflow = measurementCandidates.slice(measurementLimit);
|
|
1842
|
+
const definitionMorePosition = SKILL_MAP_DEFINITION_COORDS[SKILL_MAP_DEFINITION_COORDS.length - 1];
|
|
1843
|
+
const measurementMorePosition = SKILL_MAP_MEASUREMENT_COORDS[SKILL_MAP_MEASUREMENT_COORDS.length - 1];
|
|
1844
|
+
const definitionNodes = [
|
|
1845
|
+
...definitionVisible.map((node, index) => ({
|
|
1846
|
+
id: `definition-${index}`,
|
|
1847
|
+
node,
|
|
1848
|
+
layer: 'definition',
|
|
1849
|
+
...SKILL_MAP_DEFINITION_COORDS[index],
|
|
1850
|
+
})),
|
|
1851
|
+
...(definitionOverflow.length > 0 ? [{
|
|
1852
|
+
id: 'definition-more',
|
|
1853
|
+
node: moreNode(`+ ${definitionOverflow.length} ${zh ? '更多结构节点' : 'more structure nodes'}`),
|
|
1854
|
+
layer: 'definition',
|
|
1855
|
+
moreToggle: {
|
|
1856
|
+
group: 'definition',
|
|
1857
|
+
collapsedLabel: `+ ${definitionOverflow.length} ${zh ? '更多结构节点' : 'more structure nodes'}`,
|
|
1858
|
+
expandedLabel: zh ? '收起结构节点' : 'collapse structure nodes',
|
|
1859
|
+
},
|
|
1860
|
+
...definitionMorePosition,
|
|
1861
|
+
}] : []),
|
|
1862
|
+
...definitionOverflow.map((node, index) => ({
|
|
1863
|
+
id: `definition-overflow-${index}`,
|
|
1864
|
+
node,
|
|
1865
|
+
layer: 'definition',
|
|
1866
|
+
edgeFrom: definitionMorePosition,
|
|
1867
|
+
overflowGroup: 'definition',
|
|
1868
|
+
...overflowNodePosition('definition', index),
|
|
1869
|
+
})),
|
|
1870
|
+
];
|
|
1871
|
+
const measurementNodes = [
|
|
1872
|
+
...measurementVisible.map((node, index) => ({
|
|
1873
|
+
id: `measurement-${index}`,
|
|
1874
|
+
node,
|
|
1875
|
+
layer: 'measurement',
|
|
1876
|
+
...SKILL_MAP_MEASUREMENT_COORDS[index],
|
|
1877
|
+
})),
|
|
1878
|
+
...(measurementOverflow.length > 0 ? [{
|
|
1879
|
+
id: 'measurement-more',
|
|
1880
|
+
node: moreNode(`+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`),
|
|
1881
|
+
layer: 'measurement',
|
|
1882
|
+
moreToggle: {
|
|
1883
|
+
group: 'measurement',
|
|
1884
|
+
collapsedLabel: `+ ${measurementOverflow.length} ${zh ? '更多测量节点' : 'more measurement nodes'}`,
|
|
1885
|
+
expandedLabel: zh ? '收起测量节点' : 'collapse measurement nodes',
|
|
1886
|
+
},
|
|
1887
|
+
...measurementMorePosition,
|
|
1888
|
+
}] : []),
|
|
1889
|
+
...measurementOverflow.map((node, index) => ({
|
|
1890
|
+
id: `measurement-overflow-${index}`,
|
|
1891
|
+
node,
|
|
1892
|
+
layer: 'measurement',
|
|
1893
|
+
edgeFrom: measurementMorePosition,
|
|
1894
|
+
overflowGroup: 'measurement',
|
|
1895
|
+
...overflowNodePosition('measurement', index),
|
|
1896
|
+
})),
|
|
1897
|
+
];
|
|
1898
|
+
const mapNodes = [...definitionNodes, ...measurementNodes];
|
|
1899
|
+
const mapDimensions = skillMapDimensions(mapNodes);
|
|
1900
|
+
return `<section id="section-skill-map" class="si-sect si-sect--${band} sm-sect">
|
|
1901
|
+
<div class="si-sect-h">
|
|
1902
|
+
<span class="si-sect-title">🗺 ${zh ? 'Skill Map' : 'Skill Map'}</span>
|
|
1903
|
+
<span class="si-sect-meta">${doctor ? `${doctor.nodeCount} definition nodes` : (zh ? 'definition 未生成' : 'definition missing')} · ${evalGraph ? `${evalGraph.nodeCount} variant subgraph nodes` : (zh ? 'measurement 未生成' : 'measurement missing')}</span>
|
|
1904
|
+
</div>
|
|
1905
|
+
<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}">
|
|
1906
|
+
${renderSkillStageRail(entry, lang)}
|
|
1907
|
+
<div class="sm-toolbar" aria-label="${zh ? 'Skill Map 控制' : 'Skill Map controls'}">
|
|
1908
|
+
<div class="sm-tools">
|
|
1909
|
+
<button class="sm-tool" type="button" data-sm-action="zoom-out" title="${zh ? '缩小' : 'Zoom out'}">−</button>
|
|
1910
|
+
<span class="sm-zoom-label" data-sm-zoom-label>100%</span>
|
|
1911
|
+
<button class="sm-tool" type="button" data-sm-action="zoom-in" title="${zh ? '放大' : 'Zoom in'}">+</button>
|
|
1912
|
+
<button class="sm-tool" type="button" data-sm-action="reset" title="${zh ? '重置视图' : 'Reset view'}">${zh ? '重置' : 'Reset'}</button>
|
|
1913
|
+
</div>
|
|
1914
|
+
<div class="sm-toggles">
|
|
1915
|
+
<label class="sm-toggle"><input type="checkbox" data-sm-toggle-layer="definition" checked> ${zh ? '定义' : 'Definition'}</label>
|
|
1916
|
+
<label class="sm-toggle"><input type="checkbox" data-sm-toggle-layer="measurement" checked> ${zh ? '测量' : 'Measurement'}</label>
|
|
1917
|
+
<label class="sm-toggle"><input type="checkbox" data-sm-toggle-leaves checked> ${zh ? '叶子节点' : 'Leaf nodes'}</label>
|
|
1918
|
+
</div>
|
|
1919
|
+
</div>
|
|
1920
|
+
<div class="sm-canvas" data-sm-viewport aria-label="Skill Map">
|
|
1921
|
+
<div class="sm-graph-stage" data-sm-graph-stage style="width:${mapDimensions.width}px;height:${mapDimensions.height}px">
|
|
1922
|
+
<div class="sm-graph" data-sm-graph style="width:${mapDimensions.width}px;height:${mapDimensions.height}px">
|
|
1923
|
+
${renderSkillMapEdges(mapNodes, mapDimensions)}
|
|
1924
|
+
<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)}">
|
|
1925
|
+
<div class="sm-node-title">SKILL.md</div>
|
|
1926
|
+
<div class="sm-node-meta">${e(entry.skillName)}</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
${mapNodes.map((node) => renderSkillMapNode(node, lang)).join('')}
|
|
1929
|
+
</div>
|
|
1930
|
+
</div>
|
|
1931
|
+
</div>
|
|
1932
|
+
<details class="sm-card">
|
|
1933
|
+
<summary>${zh ? '复制 Markdown Evidence Card' : 'Copy Markdown Evidence Card'}</summary>
|
|
1934
|
+
<textarea readonly>${e(markdown)}</textarea>
|
|
1935
|
+
</details>
|
|
1936
|
+
</div>
|
|
1937
|
+
</section>`;
|
|
1938
|
+
}
|
|
1109
1939
|
// ────────── 主入口 ──────────
|
|
1110
1940
|
export function renderSkillDetail(entry, evalReport, lang = DEFAULT_LANG, insights = []) {
|
|
1111
1941
|
const langQ = lang === DEFAULT_LANG ? '' : `?lang=${lang}`;
|
|
@@ -1116,11 +1946,13 @@ export function renderSkillDetail(entry, evalReport, lang = DEFAULT_LANG, insigh
|
|
|
1116
1946
|
<main>
|
|
1117
1947
|
<a class="si-back" href="/${langQ}">${lang === 'zh' ? '← 返回 Skill 列表' : '← Back to Skills'}</a>
|
|
1118
1948
|
${renderHero(entry, insights, lastTs, reportCount, lang)}
|
|
1949
|
+
${renderSkillMapSection(entry, lang)}
|
|
1119
1950
|
${renderDoctorSection(entry.doctor, entry.doctorHistory, entry.skillName, lang)}
|
|
1120
1951
|
${renderEvalSection(entry.eval, evalReport, entry.evalHistory, langQ, lang)}
|
|
1121
1952
|
${renderObserveSection(entry.observe, langQ, lang)}
|
|
1122
1953
|
</main>
|
|
1123
1954
|
<style>${SKILL_DETAIL_CSS}</style>
|
|
1124
1955
|
${TREND_INIT_SCRIPT}
|
|
1956
|
+
${SKILL_MAP_INIT_SCRIPT}
|
|
1125
1957
|
`, lang);
|
|
1126
1958
|
}
|