rubylens 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +67 -39
- data/assets/runtime/report.js +1607 -251
- data/assets/shells/report.html +9 -7
- data/assets/shells/showcase.html +1 -0
- data/assets/styles/report.css +51 -25
- data/assets/styles/showcase.css +3 -0
- data/lib/rubylens/art_model_builder.rb +62 -10
- data/lib/rubylens/artifact_marker.rb +17 -0
- data/lib/rubylens/atomic_output.rb +27 -0
- data/lib/rubylens/cli.rb +53 -36
- data/lib/rubylens/clip/chrome_page.rb +183 -0
- data/lib/rubylens/clip/deadline_io.rb +28 -0
- data/lib/rubylens/clip/renderer.rb +113 -0
- data/lib/rubylens/clip/toolchain.rb +82 -0
- data/lib/rubylens/clip/web_socket_channel.rb +116 -0
- data/lib/rubylens/clip_generator.rb +102 -0
- data/lib/rubylens/default_output.rb +24 -0
- data/lib/rubylens/generator.rb +9 -10
- data/lib/rubylens/git_repository.rb +13 -7
- data/lib/rubylens/index/manifest.rb +8 -7
- data/lib/rubylens/index/rspec_extractor.rb +12 -10
- data/lib/rubylens/index/rubydex_adapter.rb +290 -140
- data/lib/rubylens/index/source_path.rb +12 -11
- data/lib/rubylens/model/dependency_aggregation.rb +9 -7
- data/lib/rubylens/morphology_classifier.rb +52 -12
- data/lib/rubylens/report_writer.rb +5 -20
- data/lib/rubylens/showcase_generator.rb +5 -9
- data/lib/rubylens/showcase_model.rb +12 -14
- data/lib/rubylens/showcase_writer.rb +0 -6
- data/lib/rubylens/version.rb +1 -1
- data/lib/rubylens.rb +7 -0
- metadata +10 -1
data/assets/shells/report.html
CHANGED
|
@@ -22,20 +22,20 @@
|
|
|
22
22
|
</header>
|
|
23
23
|
<aside class="panel" id="panel" aria-label="Explore this codebase">
|
|
24
24
|
<div class="panel-header">
|
|
25
|
-
<div><
|
|
26
|
-
<div class="panel-header-actions"><button type="button" id="panel-toggle" aria-expanded="true" aria-controls="panel-body">
|
|
25
|
+
<div class="panel-title"><p class="panel-kicker">Stellar navigator</p><h2>Explorer</h2></div>
|
|
26
|
+
<div class="panel-header-actions"><button type="button" id="panel-toggle" aria-label="Collapse Explorer" aria-expanded="true" aria-controls="panel-body">Collapse</button></div>
|
|
27
27
|
</div>
|
|
28
28
|
<div id="panel-body">
|
|
29
29
|
<div class="explorer-search" id="explorer-search-region" role="search">
|
|
30
|
-
<label for="explorer-search">
|
|
31
|
-
<input type="search" id="explorer-search" placeholder="Class, module, or gem
|
|
32
|
-
<p class="search-status" id="search-status" role="status" aria-live="polite"
|
|
30
|
+
<label for="explorer-search">Find a star or system</label>
|
|
31
|
+
<input type="search" id="explorer-search" placeholder="Class, module, or gem" autocomplete="off" spellcheck="false" aria-controls="search-results" aria-describedby="search-status">
|
|
32
|
+
<p class="search-status" id="search-status" role="status" aria-live="polite"></p>
|
|
33
33
|
<div class="search-results" id="search-results" role="region" aria-label="Search results" hidden></div>
|
|
34
34
|
</div>
|
|
35
35
|
<div id="controls"></div>
|
|
36
|
-
<div class="foot">
|
|
36
|
+
<div class="foot">Keep private — includes codebase names and structure, but not source or file paths.</div></div>
|
|
37
37
|
</aside>
|
|
38
|
-
<div class="toolbar"><button type="button" id="motion" aria-label="Pause drift" aria-keyshortcuts="Space" aria-pressed="false" title="Pause drift (Space)">Pause drift</button><button type="button" id="reset-view" aria-label="Reset to default view" aria-keyshortcuts="0" title="Reset view (0)">Reset</button><button type="button" id="pan-mode" aria-label="Pan mode" aria-keyshortcuts="P" aria-pressed="false" title="Pan mode (P)">Pan</button><button type="button" id="zoom-out" aria-label="Zoom out" aria-keyshortcuts="-" title="Zoom out (−)">−</button><output class="zoom-level" id="zoom-level" aria-label="Zoom level">200%</output><button type="button" id="zoom-in" aria-label="Zoom in" aria-keyshortcuts="+" title="Zoom in (+)">+</button><button type="button" id="help-open" aria-label="Keyboard shortcuts" aria-keyshortcuts="?" aria-haspopup="dialog" title="Keyboard shortcuts (?)">?</button></div>
|
|
38
|
+
<div class="toolbar"><button type="button" id="motion" aria-label="Pause drift" aria-keyshortcuts="Space" aria-pressed="false" title="Pause drift (Space)">Pause drift</button><button type="button" id="reset-view" aria-label="Reset to default view" aria-keyshortcuts="0" title="Reset view (0)">Reset</button><button type="button" id="pan-mode" aria-label="Pan mode" aria-keyshortcuts="P" aria-pressed="false" title="Pan mode (P)">Pan</button><button type="button" id="zoom-out" aria-label="Zoom out" aria-keyshortcuts="-" title="Zoom out (−)">−</button><output class="zoom-level" id="zoom-level" aria-label="Zoom level">200%</output><button type="button" id="zoom-in" aria-label="Zoom in" aria-keyshortcuts="+" title="Zoom in (+)">+</button><button type="button" id="ui-toggle" aria-label="Hide interface" aria-keyshortcuts="H" aria-pressed="false" title="Hide interface (H)">Hide UI</button><button type="button" id="help-open" aria-label="Keyboard shortcuts" aria-keyshortcuts="?" aria-haspopup="dialog" title="Keyboard shortcuts (?)">?</button></div>
|
|
39
39
|
<div class="hint">Drag to orbit · scroll to zoom · press ? for all shortcuts</div>
|
|
40
40
|
<aside class="tooltip" id="tooltip" role="status" hidden><p class="tooltip-category" id="tooltip-category"></p><p class="tooltip-name" id="tooltip-name"></p><p class="tooltip-context" id="tooltip-context"></p><dl class="tooltip-metrics" id="tooltip-metrics"></dl></aside>
|
|
41
41
|
<div class="help-overlay" id="shortcuts-help" role="dialog" aria-modal="true" aria-label="Shortcuts and controls" hidden>
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
<div><dt>Shift-drag or right-drag</dt><dd>Move the view</dd></div>
|
|
50
50
|
<div><dt>Scroll or pinch</dt><dd>Zoom at the cursor</dd></div>
|
|
51
51
|
<div><dt>Click a star</dt><dd>Select and fly to it</dd></div>
|
|
52
|
+
<div><dt>Tap while UI is hidden</dt><dd>Show the interface</dd></div>
|
|
52
53
|
<div><dt>Double-click a gem cloud</dt><dd>Expand its stars</dd></div>
|
|
53
54
|
<div><dt>Double-click empty space</dt><dd>Zoom in · Shift zooms out</dd></div>
|
|
54
55
|
</dl>
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
<div><dt>Space</dt><dd>Pause or resume drift</dd></div>
|
|
64
65
|
<div><dt>Enter / F</dt><dd>Expand the selected gem cloud</dd></div>
|
|
65
66
|
<div><dt>/</dt><dd>Search</dd></div>
|
|
67
|
+
<div><dt>H</dt><dd>Hide or show the interface</dd></div>
|
|
66
68
|
<div><dt>Esc</dt><dd>Exit focus and fly back</dd></div>
|
|
67
69
|
<div><dt>?</dt><dd>Show or hide this list</dd></div>
|
|
68
70
|
</dl>
|
data/assets/shells/showcase.html
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<body data-rubylens-mode="showcase">
|
|
14
14
|
<main class="showcase-stage" id="showcase-stage">
|
|
15
15
|
<canvas id="cosmos" role="img" aria-label="Stellar artwork of a Ruby codebase."></canvas>
|
|
16
|
+
<canvas id="travel-cosmos" aria-hidden="true"></canvas>
|
|
16
17
|
<header class="masthead">
|
|
17
18
|
<p class="eyebrow">RubyLens · codebase galaxy</p>
|
|
18
19
|
<h1>Ruby project</h1>
|
data/assets/styles/report.css
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
canvas.is-star:not(.is-pan):not(.is-dragging-pan):not(:active) { cursor: pointer; }
|
|
9
9
|
canvas.is-pan:active, canvas.is-dragging-pan:active { cursor: grabbing; }
|
|
10
10
|
canvas:focus-visible { outline: 2px solid rgba(216, 200, 255, .85); outline-offset: -4px; }
|
|
11
|
+
body.is-ui-hidden .masthead,
|
|
12
|
+
body.is-ui-hidden .panel,
|
|
13
|
+
body.is-ui-hidden .toolbar,
|
|
14
|
+
body.is-ui-hidden .hint,
|
|
15
|
+
body.is-ui-hidden .tooltip,
|
|
16
|
+
body.is-ui-hidden .help-overlay { display: none; }
|
|
11
17
|
.masthead { position: fixed; top: 28px; left: 32px; z-index: 2; pointer-events: none; max-width: 760px; }
|
|
12
18
|
.eyebrow { margin: 0 0 10px; color: #b6a9c9; font-size: 11px; font-weight: 750; letter-spacing: .18em; text-transform: uppercase; }
|
|
13
19
|
h1 { margin: 0; font: 500 clamp(28px, 4vw, 54px)/.95 ui-serif, Georgia, serif; letter-spacing: -.045em; }
|
|
@@ -25,14 +31,15 @@
|
|
|
25
31
|
.warning-row { display: grid; gap: 2px; }
|
|
26
32
|
.warning-row strong { color: #f1e9dc; font-size: 11px; overflow-wrap: anywhere; }
|
|
27
33
|
.warning-row span, .warning-note { margin: 0; color: #958875; font-size: 9px; line-height: 1.45; }
|
|
28
|
-
.panel { position: fixed; z-index: 3; top: 20px; right: 20px; width: min(348px, calc(100vw - 40px)); max-height: calc(100vh - 40px); overflow: auto; border: 1px solid rgba(255,255,255,.1); border-radius: 18px; background: rgba(9, 9, 18, .
|
|
34
|
+
.panel { position: fixed; z-index: 3; top: 20px; right: 20px; width: min(348px, calc(100vw - 40px)); max-height: calc(100vh - 40px); overflow: auto; border: 1px solid rgba(255,255,255,.1); border-radius: 18px; background: rgba(9, 9, 18, .86); box-shadow: 0 24px 80px rgba(0,0,0,.42); backdrop-filter: blur(18px); }
|
|
29
35
|
.panel.is-collapsed { width: 190px; overflow: hidden; }
|
|
30
|
-
.panel-header { position: sticky; top: 0; z-index: 1; display: flex; justify-content: space-between; gap: 16px; align-items:
|
|
36
|
+
.panel-header { position: sticky; top: 0; z-index: 1; display: flex; justify-content: space-between; gap: 16px; align-items: center; padding: 16px 18px 15px; border-bottom: 1px solid rgba(255,255,255,.08); background: rgba(9, 9, 18, .94); }
|
|
31
37
|
.panel.is-collapsed .panel-header { align-items: center; padding-block: 13px; border-bottom: 0; }
|
|
32
|
-
.panel.is-collapsed .panel-
|
|
38
|
+
.panel.is-collapsed .panel-kicker { display: none; }
|
|
39
|
+
.panel-title { display: grid; gap: 3px; min-width: 0; }
|
|
40
|
+
.panel-kicker { margin: 0; color: #a99db8; font-size: 9px; font-weight: 750; letter-spacing: .14em; line-height: 1.2; text-transform: uppercase; }
|
|
33
41
|
.panel-header-actions { display: flex; flex: none; gap: 6px; }
|
|
34
|
-
.panel h2 { margin: 0; font-size:
|
|
35
|
-
.panel-header p { margin: 4px 0 0; color: #8d8798; font-size: 11px; line-height: 1.4; }
|
|
42
|
+
.panel h2 { margin: 0; color: #f3eef8; font-size: 15px; letter-spacing: -.02em; }
|
|
36
43
|
button { border: 1px solid rgba(255,255,255,.12); border-radius: 9px; padding: 7px 10px; background: rgba(255,255,255,.06); color: #eeeaf4; font: inherit; font-size: 11px; cursor: pointer; }
|
|
37
44
|
button:hover { background: rgba(255,255,255,.1); }
|
|
38
45
|
button:disabled { opacity: .45; cursor: not-allowed; }
|
|
@@ -42,40 +49,59 @@
|
|
|
42
49
|
summary::-webkit-details-marker { display: none; }
|
|
43
50
|
summary::after { content: "⌄"; margin-left: auto; color: #777080; font-size: 12px; transition: transform 160ms cubic-bezier(.16, 1, .3, 1); }
|
|
44
51
|
details[open] summary::after { transform: rotate(180deg); }
|
|
45
|
-
.
|
|
46
|
-
.
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
.section
|
|
52
|
+
.explorer-section { --accent: 216, 200, 255; }
|
|
53
|
+
.explorer-section.core { --accent: 232, 126, 158; }
|
|
54
|
+
.explorer-section.tests { --accent: 110, 192, 222; }
|
|
55
|
+
.explorer-section.dependencies { --accent: 218, 166, 87; }
|
|
56
|
+
.explorer-section > summary { gap: 10px; min-height: 58px; padding-block: 12px; }
|
|
57
|
+
.explorer-section > summary::before { content: ""; align-self: stretch; flex: 0 0 1px; width: 1px; border-radius: 1px; background: rgba(var(--accent), .48); transition: background-color 180ms cubic-bezier(.16, 1, .3, 1); }
|
|
58
|
+
.explorer-section[open] > summary { background: linear-gradient(90deg, rgba(var(--accent), .075), transparent 72%); }
|
|
59
|
+
.explorer-section[open] > summary::before { background: rgba(var(--accent), .92); }
|
|
60
|
+
.explorer-section > summary::after { margin-left: 0; }
|
|
61
|
+
.section-heading { display: grid; flex: 1 1 auto; gap: 3px; min-width: 0; }
|
|
50
62
|
.section-heading strong { color: #eee9f4; font-size: 12px; }
|
|
51
|
-
.section-heading small { color: #
|
|
63
|
+
.section-heading small { overflow: hidden; color: #958e9f; font-size: 9px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
64
|
+
.section-state { flex: none; color: #9992a2; font-size: 8px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
|
|
65
|
+
.section-state .focused, .section-state .hidden { display: none; }
|
|
66
|
+
.explorer-section:has(.section-actions button[aria-pressed="true"]) .section-state .visible { display: none; }
|
|
67
|
+
.explorer-section:has(.section-actions button[aria-pressed="true"]) .section-state .focused { display: inline; color: rgba(var(--accent), .95); }
|
|
68
|
+
.explorer-section:has(.visibility input:not(:checked)) .section-state .visible,
|
|
69
|
+
.explorer-section:has(.visibility input:not(:checked)) .section-state .focused { display: none; }
|
|
70
|
+
.explorer-section:has(.visibility input:not(:checked)) .section-state .hidden { display: inline; }
|
|
52
71
|
.section-note { margin: -4px 0 13px; color: #756e7e; font-size: 9px; line-height: 1.4; }
|
|
53
72
|
.section-body { padding: 0 18px 16px; }
|
|
54
|
-
.section-actions { display: flex; align-items: center; gap: 8px; margin-bottom:
|
|
73
|
+
.section-actions { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
55
74
|
.ruby-breakdown { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 18px; margin: 0 0 14px; padding: 10px 0; border-block: 1px solid rgba(255,255,255,.08); }
|
|
56
75
|
.ruby-breakdown div { min-width: 0; }
|
|
57
76
|
.ruby-breakdown dt, .ruby-breakdown dd { margin: 0; }
|
|
58
77
|
.ruby-breakdown dt { color: #837c8d; font-size: 9px; }
|
|
59
78
|
.ruby-breakdown dd { margin-top: 2px; color: #f3edf9; font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
|
60
|
-
.section-actions button[aria-pressed="true"] { border-color: rgba(
|
|
61
|
-
.visibility { display: inline-flex; align-items: center; gap:
|
|
62
|
-
.visibility input { width:
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
.
|
|
79
|
+
.section-actions button[aria-pressed="true"] { border-color: rgba(var(--accent), .4); background: rgba(var(--accent), .13); color: #fff; }
|
|
80
|
+
.visibility { position: relative; display: inline-flex; align-items: center; gap: 8px; margin-right: auto; color: #b8b1c0; font-size: 11px; cursor: pointer; }
|
|
81
|
+
.visibility input { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; }
|
|
82
|
+
.visibility-track { position: relative; width: 25px; height: 14px; border: 1px solid rgba(255,255,255,.2); border-radius: 999px; background: rgba(255,255,255,.07); transition: border-color 160ms ease, background-color 160ms ease; }
|
|
83
|
+
.visibility-track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 8px; height: 8px; border-radius: 50%; background: #8d8796; transition: transform 180ms cubic-bezier(.16, 1, .3, 1), background-color 160ms ease; }
|
|
84
|
+
.visibility input:checked + .visibility-track { border-color: rgba(var(--accent), .45); background: rgba(var(--accent), .14); }
|
|
85
|
+
.visibility input:checked + .visibility-track::after { background: rgb(var(--accent)); transform: translateX(11px); }
|
|
86
|
+
.visibility input:focus-visible + .visibility-track { outline: 2px solid rgba(216, 200, 255, .9); outline-offset: 2px; }
|
|
87
|
+
.fact-label { margin: 0 0 5px; color: #8d8697; font-size: 9px; font-weight: 650; }
|
|
88
|
+
.facts { border-block: 1px solid rgba(255,255,255,.075); }
|
|
89
|
+
.fact { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; width: 100%; border: 0; border-radius: 0; padding: 9px 7px; background: transparent; text-align: left; transition: background-color 160ms ease; }
|
|
90
|
+
.fact + .fact { border-top: 1px solid rgba(255,255,255,.065); }
|
|
91
|
+
.fact:hover { background: rgba(var(--accent), .065); }
|
|
67
92
|
.fact-title { display: flex; align-items: center; gap: 6px; min-width: 0; color: #a9a2b1; font-size: 10px; }
|
|
68
93
|
.fact-scope { flex: none; border: 1px solid rgba(255,255,255,.1); border-radius: 999px; padding: 2px 5px; color: #817a89; font-size: 8px; font-weight: 650; }
|
|
69
94
|
.fact-value { grid-row: 1 / span 2; grid-column: 2; align-self: center; color: #fffafd; font-size: 12px; font-weight: 750; font-variant-numeric: tabular-nums; }
|
|
70
95
|
.fact-name { overflow: hidden; color: #ece6f2; font-size: 11px; font-weight: 650; text-overflow: ellipsis; white-space: nowrap; }
|
|
71
|
-
.fact[aria-pressed="true"] {
|
|
72
|
-
.explorer-search { display: grid; gap: 7px; padding: 14px 18px; border-bottom: 1px solid rgba(255,255,255,.08); }
|
|
96
|
+
.fact[aria-pressed="true"] { background: rgba(var(--accent), .13); }
|
|
97
|
+
.explorer-search { display: grid; gap: 7px; padding: 14px 18px 15px; border-bottom: 1px solid rgba(255,255,255,.08); }
|
|
73
98
|
.explorer-search[hidden] { display: none; }
|
|
74
99
|
.explorer-search label { color: #b8b1c0; font-size: 10px; font-weight: 700; }
|
|
75
100
|
.explorer-search input { width: 100%; min-width: 0; border: 1px solid rgba(255,255,255,.13); border-radius: 9px; padding: 9px 10px; background: rgba(255,255,255,.055); color: #f4eff8; font: inherit; font-size: 12px; }
|
|
76
101
|
.explorer-search input::placeholder { color: #8f8998; opacity: 1; }
|
|
77
102
|
.explorer-search input:focus-visible { outline: 2px solid rgba(216, 200, 255, .9); outline-offset: 2px; }
|
|
78
|
-
.search-status {
|
|
103
|
+
.search-status { margin: 0; color: #9a93a3; font-size: 9px; line-height: 1.45; }
|
|
104
|
+
.search-status:empty { display: none; }
|
|
79
105
|
.search-results { display: grid; gap: 5px; }
|
|
80
106
|
.search-results[hidden] { display: none; }
|
|
81
107
|
.search-result { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 10px; width: 100%; padding: 9px 10px; text-align: left; }
|
|
@@ -119,9 +145,9 @@
|
|
|
119
145
|
.warning-details { max-height: clamp(48px, calc(54vh - 220px), 240px); }
|
|
120
146
|
.panel { top: auto; bottom: 14px; right: 14px; left: 14px; width: auto; max-height: 46vh; }
|
|
121
147
|
.panel.is-collapsed { right: 14px; left: auto; width: 190px; }
|
|
122
|
-
.toolbar { right: 14px; left: 14px; bottom: 88px; gap:
|
|
123
|
-
.toolbar button { min-height: 40px; padding-inline:
|
|
148
|
+
.toolbar { right: 14px; left: 14px; bottom: 88px; flex-wrap: wrap; gap: 4px; }
|
|
149
|
+
.toolbar button { min-height: 40px; padding-inline: 6px; white-space: nowrap; }
|
|
124
150
|
.panel:not(.is-collapsed) ~ .toolbar { bottom: calc(46vh + 28px); }
|
|
125
151
|
.hint { display: none; }
|
|
126
152
|
}
|
|
127
|
-
@media (prefers-reduced-motion: reduce) {
|
|
153
|
+
@media (prefers-reduced-motion: reduce) { summary::after { transition: none; } }
|
data/assets/styles/showcase.css
CHANGED
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
@media (prefers-reduced-motion: reduce) {
|
|
40
40
|
.cinema-annotation, .cinema-annotation::before, .cinema-annotation::after, .cinema-annotation-copy { transition: none; }
|
|
41
41
|
}
|
|
42
|
+
/* Clip export drives annotation opacity per frame from a synthetic clock;
|
|
43
|
+
wall-clock CSS fades would make captured frames nondeterministic. */
|
|
44
|
+
html[data-rubylens-clip] .cinema-annotation, html[data-rubylens-clip] .cinema-annotation::before, html[data-rubylens-clip] .cinema-annotation::after, html[data-rubylens-clip] .cinema-annotation-copy { transition: none; }
|
|
42
45
|
@media (max-width: 600px) {
|
|
43
46
|
.galaxy-summary { font-size: 24px; }
|
|
44
47
|
.showcase-status { max-width: 720px; font-size: 24px; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "dependency_warning"
|
|
4
|
+
require_relative "errors"
|
|
4
5
|
require_relative "morphology_classifier"
|
|
5
6
|
|
|
6
7
|
module RubyLens
|
|
@@ -15,6 +16,9 @@ module RubyLens
|
|
|
15
16
|
random = Random.new(@seed)
|
|
16
17
|
morphology = MorphologyClassifier.new(snapshot).call
|
|
17
18
|
namespace_order = (0...snapshot.fetch("namespaces").length).to_a.shuffle(random: random)
|
|
19
|
+
namespace_index = Array.new(namespace_order.length)
|
|
20
|
+
namespace_order.each_with_index { |old_index, new_index| namespace_index[old_index] = new_index }
|
|
21
|
+
constant_reference_rows = valid_constant_reference_rows(snapshot)
|
|
18
22
|
namespaces = namespace_order.map do |index|
|
|
19
23
|
row = snapshot.fetch("namespaces").fetch(index)
|
|
20
24
|
[random.rand(0..0xffff_ffff), *row]
|
|
@@ -26,7 +30,6 @@ module RubyLens
|
|
|
26
30
|
package_morphologies = []
|
|
27
31
|
packages = package_order.map do |old_index|
|
|
28
32
|
package = snapshot.fetch("packages").fetch(old_index)
|
|
29
|
-
declaration_count = package.fetch("declaration_count")
|
|
30
33
|
package_seed = random.rand(0..0xffff_ffff)
|
|
31
34
|
package_morphology = MorphologyClassifier.new(package:, phase_seed: package_seed).call
|
|
32
35
|
package_morphologies << [package_morphology.fetch("family"), *package_morphology.fetch("knobs")]
|
|
@@ -34,20 +37,37 @@ module RubyLens
|
|
|
34
37
|
package_seed,
|
|
35
38
|
package.fetch("role"),
|
|
36
39
|
package.fetch("location"),
|
|
37
|
-
|
|
40
|
+
package.fetch("declarations").length,
|
|
38
41
|
*package.fetch("ruby_counts"),
|
|
39
42
|
dependency_system_index.fetch(old_index, -1),
|
|
40
43
|
]
|
|
41
44
|
end
|
|
42
45
|
package_names = package_order.map { |old_index| snapshot.fetch("packages").fetch(old_index).fetch("name") }
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
dependency_offsets = []
|
|
47
|
+
dependency_count = 0
|
|
48
|
+
snapshot.fetch("packages").each do |package|
|
|
49
|
+
dependency_offsets << dependency_count
|
|
50
|
+
dependency_count += package.fetch("declarations").length
|
|
51
|
+
end
|
|
52
|
+
dependency_target_rows = {}.compare_by_identity
|
|
53
|
+
constant_reference_rows.each do |_referring_index, referenced_index|
|
|
54
|
+
dependency_ordinal = referenced_index - namespace_order.length
|
|
55
|
+
next if dependency_ordinal.negative? || dependency_ordinal >= dependency_count
|
|
56
|
+
|
|
57
|
+
next_package_index = dependency_offsets.bsearch_index { |offset| offset > dependency_ordinal }
|
|
58
|
+
old_package_index = (next_package_index || dependency_offsets.length) - 1
|
|
59
|
+
package = snapshot.fetch("packages").fetch(old_package_index)
|
|
60
|
+
local_index = dependency_ordinal - dependency_offsets.fetch(old_package_index)
|
|
61
|
+
dependency_target_rows[package.fetch("declarations").fetch(local_index)] = dependency_ordinal
|
|
45
62
|
end
|
|
46
63
|
dependencies = []
|
|
64
|
+
dependency_index = {}
|
|
47
65
|
package_order.each do |old_index|
|
|
48
66
|
package = snapshot.fetch("packages").fetch(old_index)
|
|
49
67
|
declarations = package.fetch("declarations").sort.shuffle(random: random)
|
|
50
68
|
declarations.each do |declaration|
|
|
69
|
+
original_ordinal = dependency_target_rows[declaration]
|
|
70
|
+
dependency_index[original_ordinal] = dependencies.length if original_ordinal
|
|
51
71
|
dependencies << [
|
|
52
72
|
random.rand(0..0xffff_ffff),
|
|
53
73
|
package_index.fetch(old_index),
|
|
@@ -55,21 +75,25 @@ module RubyLens
|
|
|
55
75
|
]
|
|
56
76
|
end
|
|
57
77
|
end
|
|
78
|
+
constant_reference_links = build_constant_reference_links(
|
|
79
|
+
constant_reference_rows,
|
|
80
|
+
namespace_index,
|
|
81
|
+
dependency_index,
|
|
82
|
+
)
|
|
58
83
|
{
|
|
59
|
-
"schema" => "rubylens.art.
|
|
84
|
+
"schema" => "rubylens.art.v13",
|
|
60
85
|
"projectName" => snapshot.fetch("project_name"),
|
|
61
|
-
"morphology" => morphology,
|
|
86
|
+
"morphology" => [morphology.fetch("family"), *morphology.fetch("knobs")],
|
|
62
87
|
"totals" => {
|
|
63
88
|
"namespaces" => namespaces.length,
|
|
64
89
|
"packages" => packages.length,
|
|
65
|
-
"dependencyStars" =>
|
|
66
|
-
"renderedDependencyStars" => dependencies.length,
|
|
90
|
+
"dependencyStars" => dependencies.length,
|
|
67
91
|
},
|
|
68
92
|
"domains" => signal_domains(namespaces, snapshot.fetch("dependency_signal_maxima")),
|
|
69
|
-
"componentCounts" => snapshot.fetch("components"),
|
|
70
93
|
"categoryStats" => snapshot.fetch("category_stats"),
|
|
71
94
|
"namespaceNames" => namespace_names,
|
|
72
95
|
"namespaces" => namespaces,
|
|
96
|
+
"constantReferenceLinks" => constant_reference_links,
|
|
73
97
|
"packageNames" => package_names,
|
|
74
98
|
"packages" => packages,
|
|
75
99
|
"packageMorphologies" => package_morphologies,
|
|
@@ -89,6 +113,34 @@ module RubyLens
|
|
|
89
113
|
|
|
90
114
|
private
|
|
91
115
|
|
|
116
|
+
def valid_constant_reference_rows(snapshot)
|
|
117
|
+
rows = snapshot.fetch("constant_reference_links", [])
|
|
118
|
+
return [] unless rows.is_a?(Array)
|
|
119
|
+
|
|
120
|
+
rows.select do |row|
|
|
121
|
+
row.is_a?(Array) && row.length == 2 && row.all?(Integer) && row.none?(&:negative?)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_constant_reference_links(rows, namespace_index, dependency_index)
|
|
126
|
+
snapshot_namespace_count = namespace_index.length
|
|
127
|
+
candidates = rows.filter_map do |row|
|
|
128
|
+
referring_index, referenced_index = row
|
|
129
|
+
remapped_referring_index = namespace_index[referring_index]
|
|
130
|
+
remapped_referenced_index = if referenced_index < snapshot_namespace_count
|
|
131
|
+
namespace_index[referenced_index]
|
|
132
|
+
else
|
|
133
|
+
dependency = dependency_index[referenced_index - snapshot_namespace_count]
|
|
134
|
+
snapshot_namespace_count + dependency if dependency
|
|
135
|
+
end
|
|
136
|
+
next if remapped_referring_index.nil? || remapped_referenced_index.nil?
|
|
137
|
+
|
|
138
|
+
[remapped_referring_index, remapped_referenced_index]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
candidates.shuffle(random: Random.new(@seed ^ 0xC057_A17E))
|
|
142
|
+
end
|
|
143
|
+
|
|
92
144
|
def build_dependency_systems(snapshot, package_index)
|
|
93
145
|
system_random = Random.new(@seed ^ 0xD3E5_157E)
|
|
94
146
|
package_system_index = {}
|
|
@@ -105,7 +157,7 @@ module RubyLens
|
|
|
105
157
|
end
|
|
106
158
|
|
|
107
159
|
def signal_domains(namespaces, dependency_maxima)
|
|
108
|
-
namespace_columns = [4, 5, 6, 7, 8
|
|
160
|
+
namespace_columns = [3, 4, 5, 6, 7, 8]
|
|
109
161
|
namespace_domains = namespace_columns.map { |column| maximum(namespaces, column) }
|
|
110
162
|
SIGNAL_FIELDS.each_with_index.to_h do |field, index|
|
|
111
163
|
[field, [namespace_domains[index], dependency_maxima[index]].max]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLens
|
|
4
|
+
# Recognizes RubyLens's own artifacts by scanning the head of a file for a
|
|
5
|
+
# format marker, so default output paths are only ever replaced in kind.
|
|
6
|
+
module ArtifactMarker
|
|
7
|
+
HEAD_BYTES = 2048
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def present?(path, marker, head_bytes: HEAD_BYTES)
|
|
12
|
+
File.file?(path) && File.open(path, "rb") { |file| file.read(head_bytes).to_s.include?(marker) }
|
|
13
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
14
|
+
false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module RubyLens
|
|
7
|
+
# Owner-only atomic replacement shared by every writer: outputs are staged in
|
|
8
|
+
# a 0600 temporary alongside the target and renamed into place, so a partial
|
|
9
|
+
# write never becomes visible and permissions never loosen mid-write.
|
|
10
|
+
module AtomicOutput
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def replace(output)
|
|
14
|
+
output = File.expand_path(output)
|
|
15
|
+
FileUtils.mkdir_p(File.dirname(output), mode: 0o700)
|
|
16
|
+
temporary = File.join(File.dirname(output), ".#{File.basename(output)}.#{SecureRandom.hex(6)}.tmp")
|
|
17
|
+
File.open(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600).close
|
|
18
|
+
yield temporary
|
|
19
|
+
File.chmod(0o600, temporary)
|
|
20
|
+
File.rename(temporary, output)
|
|
21
|
+
File.chmod(0o600, output)
|
|
22
|
+
output
|
|
23
|
+
ensure
|
|
24
|
+
FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/rubylens/cli.rb
CHANGED
|
@@ -6,6 +6,35 @@ require_relative "../rubylens"
|
|
|
6
6
|
|
|
7
7
|
module RubyLens
|
|
8
8
|
class CLI
|
|
9
|
+
# One entry per generating command: everything that differs between
|
|
10
|
+
# report, showcase, and clip lives here, not in per-command methods.
|
|
11
|
+
COMMANDS = {
|
|
12
|
+
"report" => {
|
|
13
|
+
default_name: Generator::DEFAULT_REPORT_NAME,
|
|
14
|
+
generator: :generate_report,
|
|
15
|
+
warning: "RubyLens reports contain private codebase structure. Keep the output local unless you intend to share it.",
|
|
16
|
+
details_option: false,
|
|
17
|
+
output_kind: "HTML",
|
|
18
|
+
report_progress: false,
|
|
19
|
+
},
|
|
20
|
+
"clip" => {
|
|
21
|
+
default_name: ClipGenerator::DEFAULT_CLIP_NAME,
|
|
22
|
+
generator: :generate_clip,
|
|
23
|
+
warning: "RubyLens clips show the same picture as showcases: the project name plus the galaxy's shape and scale; --details adds aggregate statistics and selected Ruby and dependency names. Share them intentionally.",
|
|
24
|
+
details_option: true,
|
|
25
|
+
output_kind: "MP4",
|
|
26
|
+
report_progress: true,
|
|
27
|
+
},
|
|
28
|
+
"showcase" => {
|
|
29
|
+
default_name: ShowcaseGenerator::DEFAULT_SHOWCASE_NAME,
|
|
30
|
+
generator: :generate_showcase,
|
|
31
|
+
warning: "RubyLens showcases disclose the project name and visual codebase structure; --details also includes aggregate statistics and selected Ruby and dependency names. Share them intentionally.",
|
|
32
|
+
details_option: true,
|
|
33
|
+
output_kind: "HTML",
|
|
34
|
+
report_progress: false,
|
|
35
|
+
},
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
9
38
|
def initialize(arguments)
|
|
10
39
|
@arguments = arguments.dup
|
|
11
40
|
end
|
|
@@ -15,8 +44,7 @@ module RubyLens
|
|
|
15
44
|
command = arguments.shift
|
|
16
45
|
return print_version if %w[-v --version version].include?(command)
|
|
17
46
|
return print_help(0) if command.nil? || %w[-h --help help].include?(command)
|
|
18
|
-
return
|
|
19
|
-
return generate_showcase(arguments) if command == "showcase"
|
|
47
|
+
return generate(command, arguments) if COMMANDS.key?(command)
|
|
20
48
|
|
|
21
49
|
$stderr.puts "Unknown command: #{command}"
|
|
22
50
|
print_help(2)
|
|
@@ -27,28 +55,8 @@ module RubyLens
|
|
|
27
55
|
|
|
28
56
|
private
|
|
29
57
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
arguments,
|
|
33
|
-
command: "report",
|
|
34
|
-
default_name: "rubylens-report.html",
|
|
35
|
-
generator: RubyLens.method(:generate_report),
|
|
36
|
-
warning: "RubyLens reports contain private codebase structure. Keep the output local unless you intend to share it.",
|
|
37
|
-
)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def generate_showcase(arguments)
|
|
41
|
-
generate_html(
|
|
42
|
-
arguments,
|
|
43
|
-
command: "showcase",
|
|
44
|
-
default_name: "rubylens-showcase.html",
|
|
45
|
-
generator: RubyLens.method(:generate_showcase),
|
|
46
|
-
warning: "RubyLens showcases disclose the project name and visual codebase structure; --details also includes aggregate statistics and selected Ruby and dependency names. Share them intentionally.",
|
|
47
|
-
details_option: true,
|
|
48
|
-
)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def generate_html(arguments, command:, default_name:, generator:, warning:, details_option: false)
|
|
58
|
+
def generate(command, arguments)
|
|
59
|
+
specification = COMMANDS.fetch(command)
|
|
52
60
|
options = {}
|
|
53
61
|
help = false
|
|
54
62
|
parser = OptionParser.new do |option|
|
|
@@ -56,13 +64,13 @@ module RubyLens
|
|
|
56
64
|
option.separator ""
|
|
57
65
|
option.separator "TARGET defaults to the current working directory."
|
|
58
66
|
option.separator ""
|
|
59
|
-
option.on("-o", "--output FILE", "Output
|
|
67
|
+
option.on("-o", "--output FILE", "Output #{specification.fetch(:output_kind)} (default: TARGET/#{specification.fetch(:default_name)})") do |value|
|
|
60
68
|
options[:output] = value
|
|
61
69
|
end
|
|
62
70
|
option.on("--lockfile FILE", "Gemfile.lock used to select exact dependency versions") do |value|
|
|
63
71
|
options[:lockfile] = value
|
|
64
72
|
end
|
|
65
|
-
if details_option
|
|
73
|
+
if specification.fetch(:details_option)
|
|
66
74
|
option.on("--details", "Include aggregate statistics and cinematic code/dependency labels") do
|
|
67
75
|
options[:details] = true
|
|
68
76
|
end
|
|
@@ -81,19 +89,27 @@ module RubyLens
|
|
|
81
89
|
raise OptionParser::InvalidArgument, "unexpected argument: #{arguments.first}" unless arguments.empty?
|
|
82
90
|
|
|
83
91
|
generator_options = { path: target, output: options[:output], lockfile: options[:lockfile] }
|
|
84
|
-
generator_options[:details] = options.fetch(:details, false) if details_option
|
|
85
|
-
|
|
92
|
+
generator_options[:details] = options.fetch(:details, false) if specification.fetch(:details_option)
|
|
93
|
+
generator_options[:progress] = progress_printer if specification.fetch(:report_progress)
|
|
94
|
+
result = RubyLens.public_send(specification.fetch(:generator), **generator_options)
|
|
86
95
|
print_result(result)
|
|
87
|
-
$stderr.puts warning
|
|
96
|
+
$stderr.puts specification.fetch(:warning)
|
|
88
97
|
0
|
|
89
98
|
end
|
|
90
99
|
|
|
100
|
+
def progress_printer
|
|
101
|
+
last_step = -1
|
|
102
|
+
lambda do |rendered, total|
|
|
103
|
+
step = rendered * 10 / total
|
|
104
|
+
next unless step > last_step
|
|
105
|
+
|
|
106
|
+
last_step = step
|
|
107
|
+
$stderr.puts "Rendering clip: #{rendered * 100 / total}% (#{rendered}/#{total} frames)"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
91
111
|
def print_result(result)
|
|
92
|
-
$stdout.puts JSON.generate(
|
|
93
|
-
output: result.output_path,
|
|
94
|
-
counts: result.counts,
|
|
95
|
-
warnings: result.warnings,
|
|
96
|
-
)
|
|
112
|
+
$stdout.puts JSON.generate(result.to_payload)
|
|
97
113
|
end
|
|
98
114
|
|
|
99
115
|
def print_version
|
|
@@ -110,10 +126,11 @@ module RubyLens
|
|
|
110
126
|
|
|
111
127
|
Commands:
|
|
112
128
|
report [OPTIONS] [TARGET] Generate a private, interactive galaxy report
|
|
113
|
-
|
|
129
|
+
clip [OPTIONS] [TARGET] Generate a shareable galaxy video (MP4, needs Chrome + ffmpeg)
|
|
130
|
+
showcase [OPTIONS] [TARGET] Generate a shareable, cinematic galaxy showcase page
|
|
114
131
|
version Print the RubyLens version
|
|
115
132
|
|
|
116
|
-
Run `rubylens report --help` or `rubylens showcase --help` for options.
|
|
133
|
+
Run `rubylens report --help`, `rubylens clip --help`, or `rubylens showcase --help` for options.
|
|
117
134
|
HELP
|
|
118
135
|
status
|
|
119
136
|
end
|