kaskd-lens 0.1.1 → 0.1.3
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.
- checksums.yaml +4 -4
- data/README.md +48 -1
- data/lib/kaskd/lens/html_report.rb +196 -11
- data/lib/kaskd/lens/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29883d384c097a6052f9f7bbb5e34dd190d588dc8e3df3068f82c11a00d95a18
|
|
4
|
+
data.tar.gz: 6312cebd9d94311407d1da27bc6f9f061ace3086f38e288a104c8ac388246696
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b60ef49e1cd1ad18f3be61f36cf1fda64b3688b389ca43f8e8bfcb602d7023ed7aad37e94a9380faee73fc723e6ebb205373c2fc416aff5be94e186c104f665e
|
|
7
|
+
data.tar.gz: 225e5254fda8afd4ee2ad71106324d38e451898ff119b67e88770443fb31605fa17c662c9518c6f25f1312346c3f9d162cfb41b7708f4f74209e9c3677a254e2
|
data/README.md
CHANGED
|
@@ -15,6 +15,7 @@ Generates a self-contained HTML page that visualizes the service dependency grap
|
|
|
15
15
|
- **Export JSON** dependency report
|
|
16
16
|
- **Navigation history** (back/forward)
|
|
17
17
|
- **No external dependencies** — vis-network.js is vendored and inlined
|
|
18
|
+
- **Focused mode** — pre-select a specific service and view its blast radius + affected test files in a dedicated Tests tab
|
|
18
19
|
|
|
19
20
|
## Installation
|
|
20
21
|
|
|
@@ -51,14 +52,60 @@ Kaskd::Lens.serve
|
|
|
51
52
|
Kaskd::Lens.serve(root: "/path/to/project", port: 8080)
|
|
52
53
|
```
|
|
53
54
|
|
|
55
|
+
### Focused mode — inspect a specific service
|
|
56
|
+
|
|
57
|
+
Opens a report pre-selected on one service, with its full blast radius highlighted and a **Tests** tab listing every test file that exercises it or any service it affects.
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Generate focused report and open in browser
|
|
61
|
+
Kaskd::Lens.open_focus(class_name: "Payments::ProcessRefund")
|
|
62
|
+
|
|
63
|
+
# Custom root / output path / BFS depth
|
|
64
|
+
Kaskd::Lens.open_focus(
|
|
65
|
+
class_name: "Payments::ProcessRefund",
|
|
66
|
+
root: "/path/to/project",
|
|
67
|
+
output: "/tmp/focus-process-refund.html",
|
|
68
|
+
max_depth: 4
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Just generate the file without opening it
|
|
72
|
+
path = Kaskd::Lens.focus(class_name: "Payments::ProcessRefund")
|
|
73
|
+
# => "/path/to/project/kaskd-focus-payments-processrefund.html"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The focused report adds:
|
|
77
|
+
|
|
78
|
+
- Header shows `Focus: Payments::ProcessRefund`
|
|
79
|
+
- Green **tests** badge with the count of affected test files
|
|
80
|
+
- **Tests** tab in the detail overlay, grouped by pack
|
|
81
|
+
|
|
82
|
+
Inside a Rails app:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bundle exec rails runner \
|
|
86
|
+
"require 'kaskd-lens'; Kaskd::Lens.open_focus(class_name: 'Payments::ProcessRefund', root: Rails.root.to_s)"
|
|
87
|
+
```
|
|
88
|
+
|
|
54
89
|
### Programmatic usage
|
|
55
90
|
|
|
56
91
|
```ruby
|
|
57
92
|
require 'kaskd-lens'
|
|
58
93
|
|
|
59
94
|
result = Kaskd.analyze(root: "/path/to/project")
|
|
60
|
-
|
|
95
|
+
|
|
96
|
+
# Standard report
|
|
97
|
+
html = Kaskd::Lens::HtmlReport.new(result).render
|
|
61
98
|
File.write("my-report.html", html)
|
|
99
|
+
|
|
100
|
+
# Focused report (requires kaskd BlastRadius + TestFinder)
|
|
101
|
+
blast = Kaskd::BlastRadius.new(result[:services]).compute("Payments::ProcessRefund", max_depth: 6)
|
|
102
|
+
tests = Kaskd::TestFinder.new(root: "/path/to/project").find_for(
|
|
103
|
+
blast[:affected].map { |a| a[:class_name] } + ["Payments::ProcessRefund"],
|
|
104
|
+
result[:services]
|
|
105
|
+
)
|
|
106
|
+
focused = { class_name: "Payments::ProcessRefund", blast_radius: blast, tests: tests[:test_files] }
|
|
107
|
+
html = Kaskd::Lens::HtmlReport.new(result, focused: focused).render
|
|
108
|
+
File.write("focused.html", html)
|
|
62
109
|
```
|
|
63
110
|
|
|
64
111
|
## License
|
|
@@ -120,12 +120,10 @@ module Kaskd
|
|
|
120
120
|
<div class="tab" data-tab="info">
|
|
121
121
|
ℹ Info
|
|
122
122
|
</div>
|
|
123
|
-
#{@focused ? '<div class="tab tab-tests" data-tab="tests">📝 Tests <span class="tab-count" id="tc-tests">0</span></div>' : ""}
|
|
124
123
|
</div>
|
|
125
124
|
<div class="tab-body active" id="tb-affected"></div>
|
|
126
125
|
<div class="tab-body" id="tb-deps"></div>
|
|
127
126
|
<div class="tab-body" id="tb-info"></div>
|
|
128
|
-
#{@focused ? '<div class="tab-body" id="tb-tests"></div>' : ""}
|
|
129
127
|
</div>
|
|
130
128
|
<button id="btn-toggle-detail" title="Toggle detail panel"><span class="arrow">▶</span></button>
|
|
131
129
|
|
|
@@ -153,7 +151,10 @@ module Kaskd
|
|
|
153
151
|
|
|
154
152
|
<!-- Graph -->
|
|
155
153
|
<div class="graph-area">
|
|
154
|
+
#{@focused ? '<div id="view-switch"><button class="view-btn active" id="btn-view-graph">⚲ Dependencies</button><button class="view-btn" id="btn-view-tests">📋 Tests</button></div>' : ""}
|
|
155
|
+
|
|
156
156
|
<div id="vis-graph"></div>
|
|
157
|
+
<div id="vis-tests-graph" style="display:none;width:100%;height:100%;"></div>
|
|
157
158
|
|
|
158
159
|
<div id="graph-empty">
|
|
159
160
|
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" stroke="#4a6080" stroke-width="1.5">
|
|
@@ -845,6 +846,35 @@ module Kaskd
|
|
|
845
846
|
border-color: #2da44e;
|
|
846
847
|
color: #1a7f37;
|
|
847
848
|
}
|
|
849
|
+
/* ── View switch (Graph / Tests) ─────────────────────────── */
|
|
850
|
+
#view-switch {
|
|
851
|
+
position: absolute;
|
|
852
|
+
top: 12px;
|
|
853
|
+
left: 50%;
|
|
854
|
+
transform: translateX(-50%);
|
|
855
|
+
z-index: 10;
|
|
856
|
+
display: flex;
|
|
857
|
+
background: var(--bg-card);
|
|
858
|
+
border: 1px solid var(--border);
|
|
859
|
+
border-radius: 8px;
|
|
860
|
+
overflow: hidden;
|
|
861
|
+
}
|
|
862
|
+
.view-btn {
|
|
863
|
+
background: none;
|
|
864
|
+
border: none;
|
|
865
|
+
color: var(--text-muted);
|
|
866
|
+
padding: 5px 14px;
|
|
867
|
+
font-size: 11px;
|
|
868
|
+
font-weight: 600;
|
|
869
|
+
cursor: pointer;
|
|
870
|
+
transition: background .15s, color .15s;
|
|
871
|
+
letter-spacing: .03em;
|
|
872
|
+
}
|
|
873
|
+
.view-btn:hover { background: var(--bg-hover); color: var(--text-base); }
|
|
874
|
+
.view-btn.active {
|
|
875
|
+
background: var(--accent);
|
|
876
|
+
color: #ffffff;
|
|
877
|
+
}
|
|
848
878
|
.test-pack-group { margin-bottom: 14px; }
|
|
849
879
|
.test-pack-label {
|
|
850
880
|
font-size: 10px;
|
|
@@ -1076,9 +1106,6 @@ module Kaskd
|
|
|
1076
1106
|
renderAffectedTab(affectedDist, affectedPrev, name);
|
|
1077
1107
|
renderDepsTab(depsDist, depsPrev, name);
|
|
1078
1108
|
renderInfoTab(svc);
|
|
1079
|
-
if (FOCUSED && name === FOCUSED.class_name) {
|
|
1080
|
-
renderTestsTab(FOCUSED.tests);
|
|
1081
|
-
}
|
|
1082
1109
|
showDetailOverlay(svc.class_name);
|
|
1083
1110
|
}
|
|
1084
1111
|
|
|
@@ -1307,11 +1334,9 @@ module Kaskd
|
|
|
1307
1334
|
</div>`;
|
|
1308
1335
|
}
|
|
1309
1336
|
|
|
1310
|
-
function
|
|
1311
|
-
var el = document.getElementById('tb-tests');
|
|
1337
|
+
function renderTestsListTab(tests) {
|
|
1338
|
+
var el = document.getElementById('tb-tests-list');
|
|
1312
1339
|
if (!el) return;
|
|
1313
|
-
var countEl = document.getElementById('tc-tests');
|
|
1314
|
-
if (countEl) countEl.textContent = tests.length;
|
|
1315
1340
|
if (!tests.length) {
|
|
1316
1341
|
el.innerHTML = '<div class="desc-info">No test files found.</div>';
|
|
1317
1342
|
return;
|
|
@@ -1336,12 +1361,166 @@ module Kaskd
|
|
|
1336
1361
|
'<div class="test-path">' + esc(t.path) + '</div>' +
|
|
1337
1362
|
(t.class_name ? '<div class="test-class">' + esc(t.class_name) + '</div>' : '') +
|
|
1338
1363
|
'</div>' +
|
|
1339
|
-
|
|
1364
|
+
'</div>';
|
|
1340
1365
|
}).join('') +
|
|
1341
|
-
|
|
1366
|
+
'</div>';
|
|
1342
1367
|
}).join('');
|
|
1343
1368
|
}
|
|
1344
1369
|
|
|
1370
|
+
// ── Pack color palette for test graph nodes ─────────────────────────────────
|
|
1371
|
+
var PACK_COLORS = [
|
|
1372
|
+
'#1f6feb','#2ea043','#a371f7','#f0883e','#58a6ff',
|
|
1373
|
+
'#3fb950','#d2a8ff','#ffa657','#79c0ff','#56d364',
|
|
1374
|
+
];
|
|
1375
|
+
var packColorMap = {};
|
|
1376
|
+
var packColorIdx = 0;
|
|
1377
|
+
function packColor(pack) {
|
|
1378
|
+
if (!packColorMap[pack]) {
|
|
1379
|
+
packColorMap[pack] = PACK_COLORS[packColorIdx % PACK_COLORS.length];
|
|
1380
|
+
packColorIdx++;
|
|
1381
|
+
}
|
|
1382
|
+
return packColorMap[pack];
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
var testsNetwork = null;
|
|
1386
|
+
var testsBuilt = false;
|
|
1387
|
+
|
|
1388
|
+
function buildTestsGraph(focused) {
|
|
1389
|
+
if (testsBuilt) return;
|
|
1390
|
+
testsBuilt = true;
|
|
1391
|
+
|
|
1392
|
+
var tests = focused.tests;
|
|
1393
|
+
var container = document.getElementById('vis-tests-graph');
|
|
1394
|
+
if (!container) return;
|
|
1395
|
+
|
|
1396
|
+
if (!tests.length) {
|
|
1397
|
+
container.innerHTML = '<div class="desc-info" style="padding:24px;text-align:center;color:var(--text-dim)">No test files found.</div>';
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
var nodes = new vis.DataSet();
|
|
1402
|
+
var edges = new vis.DataSet();
|
|
1403
|
+
|
|
1404
|
+
// Central service node
|
|
1405
|
+
nodes.add({
|
|
1406
|
+
id: '__svc__',
|
|
1407
|
+
label: nodeLabel(focused.class_name),
|
|
1408
|
+
title: focused.class_name,
|
|
1409
|
+
shape: 'ellipse',
|
|
1410
|
+
size: 28,
|
|
1411
|
+
color: { background: '#f0f6fc', border: '#cdd9e5',
|
|
1412
|
+
highlight: { background: '#ffffff', border: '#e6edf3' } },
|
|
1413
|
+
font: { color: '#0d1117', size: 12, bold: true,
|
|
1414
|
+
face: 'SF Mono, Fira Code, monospace' },
|
|
1415
|
+
borderWidth: 2,
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
tests.forEach(function(t, i) {
|
|
1419
|
+
var m = t.path.match(/^packs\/([^\/]+)\//);
|
|
1420
|
+
var pack = m ? m[1] : 'app';
|
|
1421
|
+
var color = packColor(pack);
|
|
1422
|
+
var parts = t.path.split('/');
|
|
1423
|
+
var fileName = parts[parts.length - 1];
|
|
1424
|
+
var shortClass = t.class_name ? t.class_name.split('::').pop() : '';
|
|
1425
|
+
var label = fileName + (shortClass ? '\n' + shortClass : '');
|
|
1426
|
+
|
|
1427
|
+
nodes.add({
|
|
1428
|
+
id: 'test_' + i,
|
|
1429
|
+
label: label,
|
|
1430
|
+
title: t.path + (t.class_name ? '\n' + t.class_name : ''),
|
|
1431
|
+
shape: 'box',
|
|
1432
|
+
color: {
|
|
1433
|
+
background: color + '22',
|
|
1434
|
+
border: color,
|
|
1435
|
+
highlight: { background: color + '55', border: color },
|
|
1436
|
+
},
|
|
1437
|
+
font: { color: '#e6edf3', size: 10,
|
|
1438
|
+
face: 'SF Mono, Fira Code, monospace' },
|
|
1439
|
+
borderWidth: 1,
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
edges.add({
|
|
1443
|
+
from: '__svc__',
|
|
1444
|
+
to: 'test_' + i,
|
|
1445
|
+
arrows: 'to',
|
|
1446
|
+
color: { color: color, opacity: 0.5 },
|
|
1447
|
+
width: 1,
|
|
1448
|
+
title: t.path,
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
if (testsNetwork) { testsNetwork.destroy(); testsNetwork = null; }
|
|
1453
|
+
|
|
1454
|
+
testsNetwork = new vis.Network(container, { nodes: nodes, edges: edges }, {
|
|
1455
|
+
layout: { randomSeed: 42 },
|
|
1456
|
+
physics: {
|
|
1457
|
+
enabled: true,
|
|
1458
|
+
repulsion: {
|
|
1459
|
+
centralGravity: 0.3,
|
|
1460
|
+
springLength: 130,
|
|
1461
|
+
springConstant: 0.04,
|
|
1462
|
+
nodeDistance: 170,
|
|
1463
|
+
damping: 0.09,
|
|
1464
|
+
},
|
|
1465
|
+
solver: 'repulsion',
|
|
1466
|
+
stabilization: { iterations: 250, fit: true },
|
|
1467
|
+
},
|
|
1468
|
+
interaction: { hover: true, tooltipDelay: 150, zoomView: true },
|
|
1469
|
+
edges: { smooth: { type: 'continuous', roundness: 0.3 } },
|
|
1470
|
+
nodes: { shadow: { enabled: true, size: 4, color: 'rgba(0,0,0,0.35)' } },
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
testsNetwork.once('stabilizationIterationsDone', function() {
|
|
1474
|
+
testsNetwork.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// ── View switch (Dependencies <-> Tests) ───────────────────────────────────
|
|
1479
|
+
var currentView = 'graph'; // 'graph' | 'tests'
|
|
1480
|
+
|
|
1481
|
+
function switchView(view) {
|
|
1482
|
+
if (view === currentView) return;
|
|
1483
|
+
currentView = view;
|
|
1484
|
+
|
|
1485
|
+
var visGraph = document.getElementById('vis-graph');
|
|
1486
|
+
var visTests = document.getElementById('vis-tests-graph');
|
|
1487
|
+
var btnGraph = document.getElementById('btn-view-graph');
|
|
1488
|
+
var btnTests = document.getElementById('btn-view-tests');
|
|
1489
|
+
var depthCtrl = document.getElementById('depth-ctrl');
|
|
1490
|
+
var copyBtn = document.getElementById('btn-copy-diagram');
|
|
1491
|
+
var legendEl = document.getElementById('legend');
|
|
1492
|
+
|
|
1493
|
+
if (view === 'tests') {
|
|
1494
|
+
if (visGraph) visGraph.style.display = 'none';
|
|
1495
|
+
if (visTests) visTests.style.display = 'block';
|
|
1496
|
+
if (depthCtrl) depthCtrl.classList.remove('visible');
|
|
1497
|
+
if (copyBtn) copyBtn.classList.remove('visible');
|
|
1498
|
+
if (legendEl) legendEl.classList.remove('visible');
|
|
1499
|
+
if (btnGraph) btnGraph.classList.remove('active');
|
|
1500
|
+
if (btnTests) btnTests.classList.add('active');
|
|
1501
|
+
if (FOCUSED) {
|
|
1502
|
+
buildTestsGraph(FOCUSED);
|
|
1503
|
+
setTimeout(function() {
|
|
1504
|
+
if (testsNetwork) {
|
|
1505
|
+
testsNetwork.redraw();
|
|
1506
|
+
testsNetwork.fit({ animation: { duration: 200, easingFunction: 'easeInOutQuad' } });
|
|
1507
|
+
}
|
|
1508
|
+
}, 30);
|
|
1509
|
+
}
|
|
1510
|
+
} else {
|
|
1511
|
+
if (visGraph) visGraph.style.display = 'block';
|
|
1512
|
+
if (visTests) visTests.style.display = 'none';
|
|
1513
|
+
if (btnGraph) btnGraph.classList.add('active');
|
|
1514
|
+
if (btnTests) btnTests.classList.remove('active');
|
|
1515
|
+
// Restore depth/copy/legend visibility if a service is selected
|
|
1516
|
+
if (selected) {
|
|
1517
|
+
if (depthCtrl) depthCtrl.classList.add('visible');
|
|
1518
|
+
if (copyBtn) copyBtn.classList.add('visible');
|
|
1519
|
+
if (legendEl) legendEl.classList.add('visible');
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1345
1524
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
1346
1525
|
function groupByDepth(distMap) {
|
|
1347
1526
|
const g = {};
|
|
@@ -1624,6 +1803,12 @@ module Kaskd
|
|
|
1624
1803
|
// Export
|
|
1625
1804
|
document.getElementById('btn-export').addEventListener('click', exportJSON);
|
|
1626
1805
|
|
|
1806
|
+
// View switch
|
|
1807
|
+
var btnViewGraph = document.getElementById('btn-view-graph');
|
|
1808
|
+
var btnViewTests = document.getElementById('btn-view-tests');
|
|
1809
|
+
if (btnViewGraph) btnViewGraph.addEventListener('click', function() { switchView('graph'); });
|
|
1810
|
+
if (btnViewTests) btnViewTests.addEventListener('click', function() { switchView('tests'); });
|
|
1811
|
+
|
|
1627
1812
|
// ── Init ───────────────────────────────────────────────────────────────────
|
|
1628
1813
|
loadData();
|
|
1629
1814
|
})();
|
data/lib/kaskd/lens/version.rb
CHANGED