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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aff81474cc15f0e2c89a02c0097822b6daaf3cfea0e3df6c446388a3c7332169
4
- data.tar.gz: f6067ff21b7aa44825633342c01452212021b8efb137464b85c8f084ebc49fe3
3
+ metadata.gz: 29883d384c097a6052f9f7bbb5e34dd190d588dc8e3df3068f82c11a00d95a18
4
+ data.tar.gz: 6312cebd9d94311407d1da27bc6f9f061ace3086f38e288a104c8ac388246696
5
5
  SHA512:
6
- metadata.gz: 3db3478d3f26333df8a8e85fdf8ed28280efef21ad888356c2f44c79d8b4097ef65ba0abfde29ce94e5e3491d1c22236d39d7f551b5546eb973ff1374809c007
7
- data.tar.gz: 90410f2fdddc51c375dcb2e9bbe0a171604578758d4ab056cb64a269e7b4c845e1761df9476356423afaf13f8732c523c0badd17ca027968aaac68fa62320fdc
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
- html = Kaskd::Lens::HtmlReport.new(result).render
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
  &#8505; Info
122
122
  </div>
123
- #{@focused ? '<div class="tab tab-tests" data-tab="tests">&#128221; 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">&#9654;</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">&#9906; Dependencies</button><button class="view-btn" id="btn-view-tests">&#128203; 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 renderTestsTab(tests) {
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
- '</div>';
1364
+ '</div>';
1340
1365
  }).join('') +
1341
- '</div>';
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
  })();
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kaskd
4
4
  module Lens
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.3"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kaskd-lens
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - nildiert