lumitrace 0.6.2 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4de22b014b5ce2ce0e63b230487c6888147ee99459a2ef832ccae42a9b6004c6
4
- data.tar.gz: 78cb61f36f9764daa1898eae3897090a92c2d44dd422a31589db983b7a951ec7
3
+ metadata.gz: 95fd4185e9243312420705cc551f30f16ce4399e19c79165683bf97dbf6da70b
4
+ data.tar.gz: '0998518cd551365fd8c146855fa8a767316665ba1f2a4bdd6db3e83680cc17a9'
5
5
  SHA512:
6
- metadata.gz: 1ac281aaf18f6f0382af924d9d2ef850ae27353e10255114e90cce2121bcdaa5572d077c3edb3b8dde465bb8a820804aa039abab4af0420bbcbc8381ffca112d
7
- data.tar.gz: 6df68f6e6c1769616c584278fdca6be2c736c8c5a99ddab37b5a86c42b66d056edeb51b041a9e0c53b11db324fa219e72a5c1bfe6c968ee4dfe6869f350f1219
6
+ metadata.gz: 7e2adb4bd754617497dc8d8b7925d44356aa010a7ddf0942bc2f3631d83fc7aba6468fcd008e01742aaf83b26ebe2b1809f244eefd4b728b0321e9fe11b71221
7
+ data.tar.gz: 82ba698e166b72ff0d47d0f630a76348641189bfccc4393ecea714fb974b24fb0921666784c34a9627ad143aae0443a1e0823eaa1c51f30e82a2af99cf8419d3
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Lumitrace instruments Ruby source code at load time, records expression results, and renders an HTML view that overlays recorded values on your code. It is designed for quick, local “what happened here?” inspection during test runs or scripts.
4
4
 
5
+ [HTML view example](./docs/lumitrace_html_example.png)
6
+
5
7
  ## Useful links
6
8
 
7
9
  - [runv/](https://ko1.github.io/lumitrace/runv/): Lumitrace demonstration Ruby playground with inlined tracing
@@ -50,7 +52,7 @@ Write JSON output explicitly:
50
52
 
51
53
  ```bash
52
54
  lumitrace -j path/to/entry.rb
53
- lumitrace --json=out/lumitrace_recorded.json path/to/entry.rb
55
+ lumitrace --json out/lumitrace_recorded.json path/to/entry.rb
54
56
  ```
55
57
 
56
58
  Restrict to specific line ranges:
@@ -94,10 +96,10 @@ require "lumitrace/enable"
94
96
 
95
97
  ## Output
96
98
 
97
- - Text: printed by default; use `--text=PATH` to write to a file.
98
- - HTML: `lumitrace_recorded.html` by default, or `--html=PATH`.
99
+ - Text: printed by default; use `--text PATH` to write to a file.
100
+ - HTML: `lumitrace_recorded.html` by default, or `--html PATH`.
99
101
  - JSON: written only when `--json` (CLI) or `LUMITRACE_JSON` (library/CLI) is provided. Default filename is `lumitrace_recorded.json`.
100
- - JSON collection mode: `--collect-mode=last|types|history` (default `last`).
102
+ - JSON collection mode: `--collect-mode last|types|history` (default `last`).
101
103
  - Fork/exec: merged by default. Child processes write fragments under `LUMITRACE_RESULTS_DIR`.
102
104
 
103
105
  JSON event entries always include `types` (type-name => count).
@@ -151,4 +153,4 @@ Run the CLI locally:
151
153
 
152
154
  ```bash
153
155
  lumitrace path/to/entry.rb
154
- ```
156
+ ```
data/docs/ai-help.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  AI agents: run `lumitrace help --format json` to get structured help.
6
6
 
7
- - Version: 0.6.1
7
+ - Version: 0.7.0
8
8
  - Help version: 1
9
9
  - Primary JSON entrypoint: `lumitrace help --format json`
10
10
  - Schema JSON entrypoint: `lumitrace schema --format json`
@@ -20,6 +20,7 @@ AI agents: run `lumitrace help --format json` to get structured help.
20
20
  - Then switch to `--collect-mode last` for final value inspection on suspicious lines.
21
21
  - Use `--collect-mode history --max-samples N` only when value transitions matter.
22
22
  - Combine `--range` or `--git-diff` to keep outputs small and focused.
23
+ - -j, -h, -t are flags that enable output with default paths. Use --json PATH, --html PATH, --text PATH to specify output paths.
23
24
 
24
25
  ## Commands
25
26
  - `lumitrace [options] script.rb [ruby_opt]`
@@ -39,9 +40,9 @@ AI agents: run `lumitrace help --format json` to get structured help.
39
40
  ## Key Options
40
41
  - `--collect-mode` (default="last"; values=last,types,history)
41
42
  - `--max-samples` (default=3; Used by history mode.)
42
- - `-j, --json[=PATH]` (Emit JSON output.)
43
- - `-h, --html[=PATH]` (Emit HTML output.)
44
- - `-t, --text[=PATH]` (Emit text output.)
43
+ - `-j` / `--json PATH` (Emit JSON output. -j uses default path; --json PATH writes to PATH.)
44
+ - `-h` / `--html PATH` (Emit HTML output. -h uses default path; --html PATH writes to PATH.)
45
+ - `-t` / `--text PATH` (Emit text output. -t uses default path; --text PATH writes to PATH.)
45
46
  - `-g, --git-diff[=MODE]` (Restrict instrumentation to diff hunks.)
46
47
  - `--range SPEC` (repeatable=true; Restrict instrumentation to file ranges.)
47
48
  - `--git-diff-context N` (Expand diff hunks by +/-N lines.)
@@ -54,5 +55,7 @@ AI agents: run `lumitrace help --format json` to get structured help.
54
55
  - `lumitrace --collect-mode history --max-samples 5 -j app.rb`
55
56
  - `lumitrace --collect-mode types -h -j app.rb`
56
57
  - `lumitrace --collect-mode last -j exec bin/rails test`
58
+ - `lumitrace --json output.json exec bin/rails test`
59
+ - `lumitrace -j --html report.html app.rb`
57
60
  - `lumitrace help --format json`
58
61
  - `lumitrace schema --format json`
data/docs/ai-schema.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Lumitrace JSON Schema
4
4
 
5
- - Version: 0.6.1
5
+ - Version: 0.7.0
6
6
  - Schema version: 1
7
7
  - Top level: object
8
8
  - `version` (integer) - Schema version number.
Binary file
data/docs/spec.md CHANGED
@@ -269,9 +269,9 @@ lumitrace [options] exec CMD [args...]
269
269
  ```
270
270
 
271
271
  - Text is rendered by default (from in-memory events; no JSON file is required).
272
- - `-t` enables text output to stdout. `--text=PATH` writes to a file.
273
- - `-h` enables HTML output (default path). `--html=PATH` writes to a file.
274
- - `-j` enables JSON output (default path). `--json=PATH` writes to a file.
272
+ - `-t` enables text output to stdout. `--text PATH` writes to a file.
273
+ - `-h` enables HTML output (default path). `--html PATH` writes to a file.
274
+ - `-j` enables JSON output (default path). `--json PATH` writes to a file.
275
275
  - `-g` enables git diff with `working` mode. `--git-diff=MODE` selects `staged|base:REV|range:SPEC`.
276
276
  - `--max-samples` sets max samples per expression in `collect_mode=history`.
277
277
  - `--collect-mode` sets value collection mode (`last|types|history`).
data/docs/tutorial.ja.md CHANGED
@@ -325,7 +325,7 @@ lumitrace -t path/to/entry.rb
325
325
  CI アーティファクトなどに残したいときに便利です。
326
326
 
327
327
  ```bash
328
- lumitrace --text=/tmp/lumi.txt path/to/entry.rb
328
+ lumitrace --text /tmp/lumi.txt path/to/entry.rb
329
329
  ```
330
330
 
331
331
 
@@ -342,7 +342,7 @@ lumitrace -t -h path/to/entry.rb
342
342
  テストなど別コマンドをラップして計測します。
343
343
 
344
344
  ```bash
345
- lumitrace --html=sample/lumitrace_rake.html exec rake
345
+ lumitrace --html sample/lumitrace_rake.html exec rake
346
346
  ```
347
347
 
348
348
  HTML 出力:
@@ -423,7 +423,7 @@ require "lumitrace"
423
423
  `LUMITRACE_ENABLE` に CLI 互換のオプションを渡すこともできます:
424
424
 
425
425
  ```ruby
426
- ENV["LUMITRACE_ENABLE"] = "-t --html=/tmp/lumi.html -j"
426
+ ENV["LUMITRACE_ENABLE"] = "-t --html /tmp/lumi.html -j"
427
427
  require "lumitrace"
428
428
  ```
429
429
 
data/docs/tutorial.md CHANGED
@@ -325,7 +325,7 @@ lumitrace -t path/to/entry.rb
325
325
  Send text output to a file when you want to archive results or attach them to CI artifacts.
326
326
 
327
327
  ```bash
328
- lumitrace --text=/tmp/lumi.txt path/to/entry.rb
328
+ lumitrace --text /tmp/lumi.txt path/to/entry.rb
329
329
  ```
330
330
 
331
331
 
@@ -342,7 +342,7 @@ lumitrace -t -h path/to/entry.rb
342
342
  Wrap another command (like tests) so Lumitrace instruments what that command runs.
343
343
 
344
344
  ```bash
345
- lumitrace --html=sample/lumitrace_rake.html exec rake
345
+ lumitrace --html sample/lumitrace_rake.html exec rake
346
346
  ```
347
347
 
348
348
  HTML output:
@@ -422,7 +422,7 @@ require "lumitrace"
422
422
  You can also pass CLI-style options via `LUMITRACE_ENABLE`:
423
423
 
424
424
  ```ruby
425
- ENV["LUMITRACE_ENABLE"] = "-t --html=/tmp/lumi.html -j"
425
+ ENV["LUMITRACE_ENABLE"] = "-t --html /tmp/lumi.html -j"
426
426
  require "lumitrace"
427
427
  ```
428
428
 
data/exe/lumitrace CHANGED
@@ -58,6 +58,17 @@ if args.empty?
58
58
  exit 1
59
59
  end
60
60
 
61
+ # Auto-correct: `lumitrace [options] ruby ...` -> exec mode
62
+ # If the first remaining arg is not `exec` and not a `.rb` file,
63
+ # check whether it looks like an external command.
64
+ if args[0] != "exec" && !args[0].end_with?(".rb")
65
+ cmd = args[0]
66
+ if system("command -v #{cmd} > /dev/null 2>&1")
67
+ warn "lumitrace: auto-inserting 'exec' before '#{cmd}' (use 'lumitrace exec #{cmd} ...' to be explicit)"
68
+ args.unshift("exec")
69
+ end
70
+ end
71
+
61
72
  if opts[:git_diff_mode] == "exec" && args.first != "exec"
62
73
  args.unshift("exec")
63
74
  opts[:git_diff_mode] = "working"
@@ -60,7 +60,9 @@ module GenerateResultedHtml
60
60
  meta = {
61
61
  mode: mode_info[:mode],
62
62
  mode_text: mode_info[:text],
63
- max_samples: mode_info[:max_samples]
63
+ max_samples: mode_info[:max_samples],
64
+ ruby_version: RUBY_DESCRIPTION,
65
+ lumitrace_version: defined?(Lumitrace::VERSION) ? Lumitrace::VERSION : nil
64
66
  }
65
67
  meta[:command] = command_text if command_text && !command_text.to_s.empty?
66
68
  {
@@ -202,6 +204,25 @@ module GenerateResultedHtml
202
204
  .marker:focus-within .tooltip,
203
205
  .marker .tooltip:hover { display: block; }
204
206
  .noscript { color: #666; }
207
+ .tree-overview-btn { font-weight: bold; margin-bottom: 8px; }
208
+ .overview-section { background: #fffdf7; border: 1px solid #e5dfd0; border-radius: 8px; padding: 20px; }
209
+ .overview-heading { font-size: 14px; color: #333; margin: 20px 0 8px; border-bottom: 1px solid #e5dfd0; padding-bottom: 4px; }
210
+ .overview-heading:first-child { margin-top: 0; }
211
+ .overview-dl { display: grid; grid-template-columns: auto 1fr; gap: 4px 16px; margin: 0; font-size: 13px; }
212
+ .overview-dl dt { color: #666; font-weight: bold; }
213
+ .overview-dl dd { margin: 0; word-break: break-all; }
214
+ .overview-table { border-collapse: collapse; width: 100%; font-size: 13px; margin: 4px 0; }
215
+ .overview-table th, .overview-table td { padding: 4px 10px; text-align: left; border-bottom: 1px solid #e5dfd0; }
216
+ .overview-table th { color: #555; font-weight: bold; font-size: 12px; }
217
+ .overview-table th.sortable { cursor: pointer; user-select: none; }
218
+ .overview-table th.sortable:hover { color: #2f6f8e; }
219
+ .overview-table th[data-sort="asc"]::after { content: " \\25B2"; font-size: 10px; }
220
+ .overview-table th[data-sort="desc"]::after { content: " \\25BC"; font-size: 10px; }
221
+ .overview-table td { color: #1f1f1f; }
222
+ .overview-summary-table { max-width: 400px; }
223
+ .overview-multitype-table td { font-size: 12px; }
224
+ .overview-file-link { color: #2f6f8e; text-decoration: none; }
225
+ .overview-file-link:hover { text-decoration: underline; }
205
226
  @media (max-width: 900px) {
206
227
  body { padding: 16px; }
207
228
  .report-layout { grid-template-columns: 1fr; }
@@ -638,6 +638,279 @@
638
638
  }
639
639
  }
640
640
 
641
+ function computeExpressionCoverage(trace) {
642
+ const seen = new Set();
643
+ let total = 0;
644
+ let executed = 0;
645
+ for (const event of trace || []) {
646
+ if (!event || event.kind === "arg") continue;
647
+ const loc = event.location;
648
+ if (!Array.isArray(loc) || loc.length < 4) continue;
649
+ const key = `${loc[SL]}:${loc[SC]}:${loc[EL]}:${loc[EC]}`;
650
+ if (seen.has(key)) continue;
651
+ seen.add(key);
652
+ total += 1;
653
+ if (Number(event.total) > 0) executed += 1;
654
+ }
655
+ return { total, executed };
656
+ }
657
+
658
+ function computeOverviewData(payload) {
659
+ const files = payload.files || [];
660
+ const meta = payload.meta || {};
661
+
662
+ let totalExprs = 0, executedExprs = 0;
663
+ let totalLines = 0, executedLines = 0;
664
+ const multiTypeExprs = [];
665
+ const perFile = [];
666
+
667
+ for (let i = 0; i < files.length; i++) {
668
+ const file = files[i];
669
+ const trace = Array.isArray(file.trace) ? file.trace : [];
670
+ const display = fileDisplayPath(file, i);
671
+
672
+ const exprCov = computeExpressionCoverage(trace);
673
+ const lineCov = expressionLineCoverageForTrace(trace);
674
+
675
+ totalExprs += exprCov.total;
676
+ executedExprs += exprCov.executed;
677
+ totalLines += lineCov.expected;
678
+ executedLines += lineCov.executed;
679
+
680
+ const seen = new Set();
681
+ for (const event of trace) {
682
+ if (!event) continue;
683
+ const loc = event.location;
684
+ if (!Array.isArray(loc) || loc.length < 4) continue;
685
+ const key = `${loc[SL]}:${loc[SC]}:${loc[EL]}:${loc[EC]}`;
686
+ if (seen.has(key)) continue;
687
+ seen.add(key);
688
+ const types = normalizeTypeCounts(event.types);
689
+ const typeKeys = Object.keys(types);
690
+ if (typeKeys.length > 1) {
691
+ multiTypeExprs.push({
692
+ file: display,
693
+ fileKey: fileUrlKey(file, i),
694
+ line: Number(loc[SL]),
695
+ types: types
696
+ });
697
+ }
698
+ }
699
+
700
+ perFile.push({
701
+ display,
702
+ key: fileUrlKey(file, i),
703
+ exprTotal: exprCov.total,
704
+ exprExecuted: exprCov.executed,
705
+ lineTotal: lineCov.expected,
706
+ lineExecuted: lineCov.executed
707
+ });
708
+ }
709
+
710
+ return {
711
+ command: meta.command || null,
712
+ mode: meta.mode_text || null,
713
+ rubyVersion: meta.ruby_version || null,
714
+ lumitraceVersion: meta.lumitrace_version || null,
715
+ totalExprs, executedExprs,
716
+ totalLines, executedLines,
717
+ perFile,
718
+ multiTypeExprs
719
+ };
720
+ }
721
+
722
+ function pct(n, d) {
723
+ if (d === 0) return "—";
724
+ return (n / d * 100).toFixed(1) + "%";
725
+ }
726
+
727
+ function renderOverviewSection(data, onSelectFile) {
728
+ const section = document.createElement("section");
729
+ section.className = "overview-section";
730
+
731
+ // Context
732
+ const ctxH = document.createElement("h2");
733
+ ctxH.className = "overview-heading";
734
+ ctxH.textContent = "Execution Context";
735
+ section.appendChild(ctxH);
736
+
737
+ const ctxDl = document.createElement("dl");
738
+ ctxDl.className = "overview-dl";
739
+ const ctxItems = [];
740
+ if (data.command) ctxItems.push(["Command", data.command]);
741
+ if (data.mode) ctxItems.push(["Collect Mode", data.mode]);
742
+ if (data.rubyVersion) ctxItems.push(["Ruby", data.rubyVersion]);
743
+ if (data.lumitraceVersion) ctxItems.push(["Lumitrace", data.lumitraceVersion]);
744
+ for (const [label, value] of ctxItems) {
745
+ const dt = document.createElement("dt");
746
+ dt.textContent = label;
747
+ ctxDl.appendChild(dt);
748
+ const dd = document.createElement("dd");
749
+ dd.textContent = value;
750
+ ctxDl.appendChild(dd);
751
+ }
752
+ section.appendChild(ctxDl);
753
+
754
+ // Coverage summary
755
+ const covH = document.createElement("h2");
756
+ covH.className = "overview-heading";
757
+ covH.textContent = "Coverage Summary";
758
+ section.appendChild(covH);
759
+
760
+ const covTable = document.createElement("table");
761
+ covTable.className = "overview-table overview-summary-table";
762
+ covTable.innerHTML = `<thead><tr><th></th><th>Executed</th><th>Total</th><th>Coverage</th></tr></thead>` +
763
+ `<tbody>` +
764
+ `<tr><td>Expressions</td><td>${data.executedExprs}</td><td>${data.totalExprs}</td><td>${pct(data.executedExprs, data.totalExprs)}</td></tr>` +
765
+ `<tr><td>Lines</td><td>${data.executedLines}</td><td>${data.totalLines}</td><td>${pct(data.executedLines, data.totalLines)}</td></tr>` +
766
+ `</tbody>`;
767
+ section.appendChild(covTable);
768
+
769
+ // Per-file coverage table
770
+ if (data.perFile.length > 0) {
771
+ const fileH = document.createElement("h2");
772
+ fileH.className = "overview-heading";
773
+ fileH.textContent = `File Coverage (${data.perFile.length} files)`;
774
+ section.appendChild(fileH);
775
+
776
+ const fileTable = document.createElement("table");
777
+ fileTable.className = "overview-table overview-file-table";
778
+ const thead = document.createElement("thead");
779
+ const headerRow = document.createElement("tr");
780
+
781
+ const columns = [
782
+ { key: "display", label: "File", type: "string" },
783
+ { key: "exprPct", label: "Expr %", type: "number" },
784
+ { key: "exprExecuted", label: "Expr Exec", type: "number" },
785
+ { key: "exprTotal", label: "Expr Total", type: "number" },
786
+ { key: "linePct", label: "Line %", type: "number" },
787
+ { key: "lineExecuted", label: "Line Exec", type: "number" },
788
+ { key: "lineTotal", label: "Line Total", type: "number" }
789
+ ];
790
+
791
+ let sortCol = "exprPct";
792
+ let sortAsc = true;
793
+
794
+ const rows = data.perFile.map((f) => ({
795
+ ...f,
796
+ exprPct: f.exprTotal > 0 ? f.exprExecuted / f.exprTotal * 100 : -1,
797
+ linePct: f.lineTotal > 0 ? f.lineExecuted / f.lineTotal * 100 : -1
798
+ }));
799
+
800
+ function sortRows() {
801
+ const col = columns.find((c) => c.key === sortCol);
802
+ if (!col) return;
803
+ rows.sort((a, b) => {
804
+ let av = a[sortCol], bv = b[sortCol];
805
+ if (col.type === "string") {
806
+ av = String(av || "");
807
+ bv = String(bv || "");
808
+ return sortAsc ? av.localeCompare(bv) : bv.localeCompare(av);
809
+ }
810
+ return sortAsc ? av - bv : bv - av;
811
+ });
812
+ }
813
+
814
+ function renderFileTableBody() {
815
+ sortRows();
816
+ const oldTbody = fileTable.querySelector("tbody");
817
+ if (oldTbody) oldTbody.remove();
818
+ const tbody = document.createElement("tbody");
819
+ for (const row of rows) {
820
+ const tr = document.createElement("tr");
821
+ const tdFile = document.createElement("td");
822
+ const link = document.createElement("a");
823
+ link.href = "#";
824
+ link.className = "overview-file-link";
825
+ link.textContent = row.display;
826
+ link.addEventListener("click", (e) => { e.preventDefault(); onSelectFile(row.key); });
827
+ tdFile.appendChild(link);
828
+ tr.appendChild(tdFile);
829
+ tr.appendChild(cellTd(row.exprPct >= 0 ? row.exprPct.toFixed(1) + "%" : "—"));
830
+ tr.appendChild(cellTd(row.exprExecuted));
831
+ tr.appendChild(cellTd(row.exprTotal));
832
+ tr.appendChild(cellTd(row.linePct >= 0 ? row.linePct.toFixed(1) + "%" : "—"));
833
+ tr.appendChild(cellTd(row.lineExecuted));
834
+ tr.appendChild(cellTd(row.lineTotal));
835
+ tbody.appendChild(tr);
836
+ }
837
+ fileTable.appendChild(tbody);
838
+
839
+ // update header sort indicators
840
+ headerRow.querySelectorAll("th").forEach((th) => {
841
+ const col = th.dataset.col;
842
+ if (col === sortCol) {
843
+ th.dataset.sort = sortAsc ? "asc" : "desc";
844
+ } else {
845
+ delete th.dataset.sort;
846
+ }
847
+ });
848
+ }
849
+
850
+ function cellTd(val) {
851
+ const td = document.createElement("td");
852
+ td.textContent = String(val);
853
+ return td;
854
+ }
855
+
856
+ for (const col of columns) {
857
+ const th = document.createElement("th");
858
+ th.textContent = col.label;
859
+ th.dataset.col = col.key;
860
+ th.className = "sortable";
861
+ th.addEventListener("click", () => {
862
+ if (sortCol === col.key) {
863
+ sortAsc = !sortAsc;
864
+ } else {
865
+ sortCol = col.key;
866
+ sortAsc = col.type === "string";
867
+ }
868
+ renderFileTableBody();
869
+ });
870
+ headerRow.appendChild(th);
871
+ }
872
+ thead.appendChild(headerRow);
873
+ fileTable.appendChild(thead);
874
+ renderFileTableBody();
875
+ section.appendChild(fileTable);
876
+ }
877
+
878
+ // Multi-type expressions
879
+ if (data.multiTypeExprs.length > 0) {
880
+ const mtH = document.createElement("h2");
881
+ mtH.className = "overview-heading";
882
+ mtH.textContent = `Multi-type Expressions (${data.multiTypeExprs.length})`;
883
+ section.appendChild(mtH);
884
+
885
+ const mtTable = document.createElement("table");
886
+ mtTable.className = "overview-table overview-multitype-table";
887
+ mtTable.innerHTML = `<thead><tr><th>File</th><th>Line</th><th>Types</th></tr></thead>`;
888
+ const tbody = document.createElement("tbody");
889
+ for (const entry of data.multiTypeExprs) {
890
+ const tr = document.createElement("tr");
891
+ const tdFile = document.createElement("td");
892
+ const link = document.createElement("a");
893
+ link.href = "#";
894
+ link.className = "overview-file-link";
895
+ link.textContent = entry.file;
896
+ link.addEventListener("click", (e) => { e.preventDefault(); onSelectFile(entry.fileKey, entry.line); });
897
+ tdFile.appendChild(link);
898
+ tr.appendChild(tdFile);
899
+ const tdLine = document.createElement("td");
900
+ tdLine.textContent = String(entry.line);
901
+ tr.appendChild(tdLine);
902
+ const tdTypes = document.createElement("td");
903
+ tdTypes.textContent = Object.entries(entry.types).sort(([, a], [, b]) => b - a).map(([t, c]) => `${t}(${c})`).join(", ");
904
+ tr.appendChild(tdTypes);
905
+ tbody.appendChild(tr);
906
+ }
907
+ mtTable.appendChild(tbody);
908
+ section.appendChild(mtTable);
909
+ }
910
+
911
+ return section;
912
+ }
913
+
641
914
  function render(payload) {
642
915
  app.textContent = "";
643
916
 
@@ -702,20 +975,45 @@
702
975
  viewer.className = "report-viewer";
703
976
  main.appendChild(viewer);
704
977
 
978
+ const OVERVIEW_KEY = "__overview__";
979
+ const overviewData = computeOverviewData(payload);
980
+
705
981
  const fileKeyToEntry = new Map();
706
982
  files.forEach((file, idx) => fileKeyToEntry.set(fileUrlKey(file, idx), { file, idx }));
707
983
 
708
- let selectedKey = preferredInitialFileKey(files);
709
- let selectedLine = preferredInitialLineNumber();
984
+ let selectedKey = files.length > 1 ? OVERVIEW_KEY : preferredInitialFileKey(files);
985
+ let selectedLine = files.length > 1 ? null : preferredInitialLineNumber();
710
986
 
711
987
  function renderTree() {
712
988
  treeMount.textContent = "";
989
+
990
+ // Overview button
991
+ if (files.length > 1) {
992
+ const overviewBtn = document.createElement("button");
993
+ overviewBtn.type = "button";
994
+ overviewBtn.className = "tree-file-btn tree-overview-btn";
995
+ if (selectedKey === OVERVIEW_KEY) {
996
+ overviewBtn.classList.add("active");
997
+ overviewBtn.setAttribute("aria-current", "page");
998
+ }
999
+ overviewBtn.textContent = "Overview";
1000
+ overviewBtn.addEventListener("click", () => selectOverview(true));
1001
+ treeMount.appendChild(overviewBtn);
1002
+ }
1003
+
713
1004
  const treeRoot = buildFileTree(files);
714
1005
  treeMount.appendChild(renderFileTreeNode(treeRoot, selectedKey, (key) => selectFile(key, true, null), 0));
715
1006
  }
716
1007
 
717
- function renderSelectedFile() {
1008
+ function renderSelectedView() {
718
1009
  viewer.textContent = "";
1010
+
1011
+ if (selectedKey === OVERVIEW_KEY) {
1012
+ currentPath.textContent = "Overview";
1013
+ viewer.appendChild(renderOverviewSection(overviewData, (key, line) => selectFile(key, true, line || null)));
1014
+ return;
1015
+ }
1016
+
719
1017
  const entry = fileKeyToEntry.get(selectedKey);
720
1018
  if (!entry) return;
721
1019
 
@@ -733,10 +1031,18 @@
733
1031
  function selectLine(lineno, updateHash) {
734
1032
  const n = Number(lineno);
735
1033
  selectedLine = Number.isFinite(n) && n > 0 ? n : null;
736
- renderSelectedFile();
1034
+ renderSelectedView();
737
1035
  if (updateHash) setLocationHashSelection(selectedKey, selectedLine);
738
1036
  }
739
1037
 
1038
+ function selectOverview(updateHash) {
1039
+ selectedKey = OVERVIEW_KEY;
1040
+ selectedLine = null;
1041
+ renderTree();
1042
+ renderSelectedView();
1043
+ if (updateHash) setLocationHashSelection("overview", null);
1044
+ }
1045
+
740
1046
  function selectFile(key, updateHash, nextLine) {
741
1047
  if (!fileKeyToEntry.has(key)) return;
742
1048
  if (arguments.length >= 3) {
@@ -746,23 +1052,31 @@
746
1052
  }
747
1053
  selectedKey = key;
748
1054
  renderTree();
749
- renderSelectedFile();
1055
+ renderSelectedView();
750
1056
  if (updateHash) setLocationHashSelection(key, selectedLine);
751
1057
  }
752
1058
 
753
1059
  window.addEventListener("hashchange", () => {
754
1060
  const state = parseHashStateFromLocation();
1061
+ if (state.fileKey === "overview") {
1062
+ if (selectedKey !== OVERVIEW_KEY) selectOverview(false);
1063
+ return;
1064
+ }
755
1065
  if (state.fileKey && state.fileKey !== selectedKey && fileKeyToEntry.has(state.fileKey)) {
756
1066
  selectFile(state.fileKey, false, state.line);
757
1067
  return;
758
1068
  }
759
1069
  if (state.fileKey === selectedKey || (!state.fileKey && selectedKey)) {
760
1070
  selectedLine = state.line;
761
- renderSelectedFile();
1071
+ renderSelectedView();
762
1072
  }
763
1073
  });
764
1074
 
765
- selectFile(selectedKey, true);
1075
+ if (selectedKey === OVERVIEW_KEY) {
1076
+ selectOverview(true);
1077
+ } else {
1078
+ selectFile(selectedKey, true);
1079
+ }
766
1080
  }
767
1081
 
768
1082
  try {
@@ -23,7 +23,8 @@ module Lumitrace
23
23
  "First run with `--collect-mode types` to get a compact shape of runtime behavior.",
24
24
  "Then switch to `--collect-mode last` for final value inspection on suspicious lines.",
25
25
  "Use `--collect-mode history --max-samples N` only when value transitions matter.",
26
- "Combine `--range` or `--git-diff` to keep outputs small and focused."
26
+ "Combine `--range` or `--git-diff` to keep outputs small and focused.",
27
+ "-j, -h, -t are flags that enable output with default paths. Use --json PATH, --html PATH, --text PATH to specify output paths."
27
28
  ],
28
29
  commands: [
29
30
  {
@@ -56,9 +57,9 @@ module Lumitrace
56
57
  key_options: [
57
58
  { name: "--collect-mode", values: COLLECT_MODES, default: "last" },
58
59
  { name: "--max-samples", type: "Integer", default: 3, note: "Used by history mode." },
59
- { name: "-j, --json[=PATH]", type: "bool|string", note: "Emit JSON output." },
60
- { name: "-h, --html[=PATH]", type: "bool|string", note: "Emit HTML output." },
61
- { name: "-t, --text[=PATH]", type: "bool|string", note: "Emit text output." },
60
+ { name: "-j / --json PATH", type: "bool|string", note: "Emit JSON output. -j uses default path; --json PATH writes to PATH." },
61
+ { name: "-h / --html PATH", type: "bool|string", note: "Emit HTML output. -h uses default path; --html PATH writes to PATH." },
62
+ { name: "-t / --text PATH", type: "bool|string", note: "Emit text output. -t uses default path; --text PATH writes to PATH." },
62
63
  { name: "-g, --git-diff[=MODE]", type: "string", note: "Restrict instrumentation to diff hunks." },
63
64
  { name: "--range SPEC", type: "string", repeatable: true, note: "Restrict instrumentation to file ranges." },
64
65
  { name: "--git-diff-context N", type: "Integer", note: "Expand diff hunks by +/-N lines." },
@@ -76,6 +77,8 @@ module Lumitrace
76
77
  "lumitrace --collect-mode history --max-samples 5 -j app.rb",
77
78
  "lumitrace --collect-mode types -h -j app.rb",
78
79
  "lumitrace --collect-mode last -j exec bin/rails test",
80
+ "lumitrace --json output.json exec bin/rails test",
81
+ "lumitrace -j --html report.html app.rb",
79
82
  "lumitrace help --format json",
80
83
  "lumitrace schema --format json"
81
84
  ]
@@ -94,7 +94,7 @@ if defined?(RubyVM::InstructionSequence)
94
94
  return recordrequire_orig_translate(iseq) if respond_to?(:recordrequire_orig_translate) && !RecordRequire.enabled?
95
95
  path = iseq.path
96
96
  abs = File.expand_path(path)
97
- if RecordRequire.in_root?(abs) && !RecordRequire.excluded?(abs) && !RecordRequire.already_processed?(abs) &&
97
+ if RecordRequire.in_root?(abs) && File.file?(abs) && !RecordRequire.excluded?(abs) && !RecordRequire.already_processed?(abs) &&
98
98
  (iseq.label == "<main>" || iseq.label == "<top (required)>")
99
99
  if RecordRequire.ranges_filtering? && !RecordRequire.listed_file?(abs)
100
100
  return recordrequire_orig_translate(iseq) if respond_to?(:recordrequire_orig_translate)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lumitrace
4
- VERSION = "0.6.2"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/lumitrace.rb CHANGED
@@ -290,9 +290,12 @@ module Lumitrace
290
290
  o.banner = banner if banner
291
291
  o.separator ""
292
292
  o.separator "Options:"
293
- o.on("-t", "--text[=PATH]", "Text output (stdout or PATH)") { |v| opts[:text] = v.nil? || v.empty? ? true : v }
294
- o.on("-h", "--html[=PATH]", "HTML output (default file or PATH)") { |v| opts[:html] = v.nil? || v.empty? ? true : v }
295
- o.on("-j", "--json[=PATH]", "JSON output (default file or PATH)") { |v| opts[:json] = v.nil? || v.empty? ? true : v }
293
+ o.on("-t", "Enable text output (stdout or default path)") { opts[:text] = true }
294
+ o.on("--text PATH", "Text output to PATH") { |v| opts[:text] = v }
295
+ o.on("-h", "Enable HTML output (default path)") { opts[:html] = true }
296
+ o.on("--html PATH", "HTML output to PATH") { |v| opts[:html] = v }
297
+ o.on("-j", "Enable JSON output (default path)") { opts[:json] = true }
298
+ o.on("--json PATH", "JSON output to PATH") { |v| opts[:json] = v }
296
299
  o.on("-g", "--git-diff[=MODE]", "Diff ranges (working, staged, base:REV, range:SPEC)") { |v| opts[:git_diff_mode] = v.nil? || v.empty? ? "working" : v }
297
300
  o.on("--max-samples N", Integer, "Max samples per expression") { |v| opts[:max_samples] = v }
298
301
  o.on("--collect-mode MODE", "Collect mode: last, types, history") { |v| opts[:collect_mode] = v }
@@ -27,7 +27,7 @@ jobs:
27
27
  - name: Run tests with Lumitrace
28
28
  env:
29
29
  LUMITRACE_GIT_DIFF: range:origin/${{ github.base_ref }}...HEAD
30
- run: bundle exec lumitrace -v -t --html=lumitrace_recorded.html exec rake test
30
+ run: bundle exec lumitrace -v -t --html lumitrace_recorded.html exec rake test
31
31
 
32
32
  - name: Prepare Pages content
33
33
  run: |
@@ -42,7 +42,7 @@ steps:
42
42
  - name: Run tests with Lumitrace
43
43
  env:
44
44
  LUMITRACE_GIT_DIFF: range:origin/${{ github.base_ref }}...HEAD
45
- run: bundle exec lumitrace -v -t --html=lumitrace_recorded.html exec rake test
45
+ run: bundle exec lumitrace -v -t --html lumitrace_recorded.html exec rake test
46
46
 
47
47
  - name: Prepare Pages content
48
48
  run: |
@@ -128,7 +128,7 @@ class LumiTraceTest < Minitest::Test
128
128
  end
129
129
 
130
130
  def test_parse_cli_options_basic
131
- argv = ["-t", "--html=/tmp/out.html", "-j", "--max-samples", "7", "--collect-mode", "history", "--root", "/tmp/root",
131
+ argv = ["-t", "--html", "/tmp/out.html", "-j", "--max-samples", "7", "--collect-mode", "history", "--root", "/tmp/root",
132
132
  "--range", "a.rb:1-3,5-6", "--verbose", "file.rb"]
133
133
  opts, args, _parser = Lumitrace.parse_cli_options(argv, allow_help: true)
134
134
 
@@ -144,7 +144,7 @@ class LumiTraceTest < Minitest::Test
144
144
  end
145
145
 
146
146
  def test_parse_enable_args_cli_string
147
- opts = Lumitrace.parse_enable_args("--text=/tmp/out.txt -h --json=/tmp/out.json --max-samples 5 --collect-mode types --root /tmp/root")
147
+ opts = Lumitrace.parse_enable_args("--text /tmp/out.txt -h --json /tmp/out.json --max-samples 5 --collect-mode types --root /tmp/root")
148
148
 
149
149
  assert_equal "/tmp/out.txt", opts[:text]
150
150
  assert_equal true, opts[:html]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lumitrace
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Koichi Sasada
@@ -23,6 +23,7 @@ files:
23
23
  - bench/bench_sample.rb
24
24
  - docs/ai-help.md
25
25
  - docs/ai-schema.md
26
+ - docs/lumitrace_html_example.png
26
27
  - docs/spec.md
27
28
  - docs/supported_syntax.md
28
29
  - docs/tutorial.ja.md