Module: Rigor::Triage
- Defined in:
- lib/rigor/triage.rb,
lib/rigor/triage/hint.rb,
lib/rigor/triage/catalogue.rb
Overview
ADR-23 — diagnostic triage. Aggregates a ‘rigor check` diagnostic stream into the data behind the `rigor triage` report: a rule-ID distribution, per-file hotspots, and the heuristic hint catalogue (Catalogue).
Pure over the diagnostic stream — no second analysis pass, no analyzer internals. ‘Triage.analyze` is the single entry point; rendering is CLI::TriageRenderer’s job.
Defined Under Namespace
Modules: Catalogue Classes: Hint, Hotspot, Report, RuleCount, Selector, Summary
Constant Summary collapse
- UNCATEGORISED =
"(uncategorised)"
Class Method Summary collapse
-
.analyze(diagnostics, top: 10, hints: true, include_info: false) ⇒ Report
WD6 (ADR-23): the volume views — distribution / selectors / hotspots — route only the actionable diagnostics (error + warning) by default.
- .build_distribution(diagnostics) ⇒ Object
- .build_hotspots(diagnostics, top) ⇒ Object
-
.build_selectors(diagnostics) ⇒ Object
The class/method aggregation axis (ADR-23 follow-up).
- .build_summary(diagnostics) ⇒ Object
- .hotspot_for(path, group) ⇒ Object
-
.normalize_receiver(token) ⇒ Object
Folds a receiver token — a ‘Diagnostic#receiver_type` display string or a message-parsed token — to the class the diagnostics should bucket under, so the selector axis does not fragment one method across every distinct literal receiver.
- .report_to_h(report) ⇒ Object
-
.rule_key(diagnostic) ⇒ Object
Diagnostics without a ‘rule` (parse errors, internal-analyzer errors) bucket under a single sentinel rather than vanishing.
- .selector_for(receiver, method, group) ⇒ Object
Class Method Details
.analyze(diagnostics, top: 10, hints: true, include_info: false) ⇒ Report
WD6 (ADR-23): the volume views — distribution / selectors / hotspots — route only the actionable diagnostics (error + warning) by default. Plugin-emitted ‘:info` diagnostics are overwhelmingly recognition trace (`plugin.activerecord.model-call`, `plugin.rails-routes.helper`, …) — positive “Rigor resolved this call” records, not problems — and on a real Rails app they swamp the genuine error/warning signal (the field trip: 257 of 267 diagnostics were such trace) and invert the hotspot ranking towards the files with the *most working* code. The summary still reports the full info count, and `include_info: true` (the `–include-info` flag) restores the pre-v0.2.3 behaviour. Hints always see the full stream so the `gem-without-rbs` notice (an info-severity `rbs.coverage.missing-gem`) survives; the count-based H5/H6 recognisers guard against info themselves so recognition trace never reads as a bug.
54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/rigor/triage.rb', line 54 def analyze(diagnostics, top: 10, hints: true, include_info: false) routed = include_info ? diagnostics : diagnostics.reject { |d| d.severity == :info } Report.new( summary: build_summary(diagnostics), distribution: build_distribution(routed), selectors: build_selectors(routed), hotspots: build_hotspots(routed, top), hints: hints ? Catalogue.recognise(diagnostics, include_info: include_info) : [], include_info: include_info ) end |
.build_distribution(diagnostics) ⇒ Object
82 83 84 85 86 |
# File 'lib/rigor/triage.rb', line 82 def build_distribution(diagnostics) diagnostics.group_by { |d| rule_key(d) } .map { |rule, group| RuleCount.new(rule: rule, count: group.size) } .sort_by { |row| [-row.count, row.rule] } end |
.build_hotspots(diagnostics, top) ⇒ Object
143 144 145 146 147 148 |
# File 'lib/rigor/triage.rb', line 143 def build_hotspots(diagnostics, top) diagnostics.group_by(&:path) .map { |path, group| hotspot_for(path, group) } .sort_by { |spot| [-spot.count, spot.file] } .first(top) end |
.build_selectors(diagnostics) ⇒ Object
The class/method aggregation axis (ADR-23 follow-up). Groups every diagnostic that carries a ‘method_name` by its `(receiver_type, method_name)` pair so a consumer can answer “which method / class concentrates the diagnostics?” with a `jq` query over the JSON instead of parsing message text. Method-only diagnostics (nil `receiver_type`) keep a null `receiver` and still group by method. The full list is returned uncapped — the JSON is the agent-facing surface; the text renderer caps its own rows.
97 98 99 100 101 102 |
# File 'lib/rigor/triage.rb', line 97 def build_selectors(diagnostics) diagnostics.select(&:method_name) .group_by { |d| [normalize_receiver(d.receiver_type) || d.receiver_type, d.method_name.to_s] } .map { |(receiver, method), group| selector_for(receiver, method, group) } .sort_by { |s| [-s.count, s.receiver.to_s, s.method_name] } end |
.build_summary(diagnostics) ⇒ Object
72 73 74 75 76 77 78 79 80 |
# File 'lib/rigor/triage.rb', line 72 def build_summary(diagnostics) by_severity = diagnostics.group_by(&:severity).transform_values(&:size) Summary.new( total: diagnostics.size, error: by_severity.fetch(:error, 0), warning: by_severity.fetch(:warning, 0), info: by_severity.fetch(:info, 0) ) end |
.hotspot_for(path, group) ⇒ Object
150 151 152 153 154 155 156 |
# File 'lib/rigor/triage.rb', line 150 def hotspot_for(path, group) by_rule = group.group_by { |d| rule_key(d) } .transform_values(&:size) .sort_by { |rule, count| [-count, rule] } .to_h Hotspot.new(file: path, count: group.size, by_rule: by_rule) end |
.normalize_receiver(token) ⇒ Object
Folds a receiver token — a ‘Diagnostic#receiver_type` display string or a message-parsed token — to the class the diagnostics should bucket under, so the selector axis does not fragment one method across every distinct literal receiver. String / integer / float / symbol literals collapse to their class; `singleton©` and a bare `C` fold to `C`; a generic `C` keeps the `Array` element form (the AR-relation heuristic keys on it). Returns nil for a token it cannot reduce to a class (a union display, an inferred shape) — the caller keeps the raw string then, never losing the row. Shared with Catalogue.
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rigor/triage.rb', line 114 def normalize_receiver(token) return nil if token.nil? t = token.to_s.strip return "Integer" if t.match?(/\A-?\d+\z/) return "Float" if t.match?(/\A-?\d+\.\d+\z/) return "String" if t.start_with?('"', "'") return "Symbol" if t.start_with?(":") singleton = t[/\Asingleton\(([\w:]+)\)\z/, 1] return singleton if singleton return t if t.start_with?("Array[") nominal = t[/\A([\w:]+)\[/, 1] return nominal if nominal return t if t.match?(/\A[\w:]+\z/) nil end |
.report_to_h(report) ⇒ Object
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/rigor/triage.rb', line 158 def report_to_h(report) { "summary" => { "total" => report.summary.total, "error" => report.summary.error, "warning" => report.summary.warning, "info" => report.summary.info }, "distribution" => report.distribution.map { |r| { "rule" => r.rule, "count" => r.count } }, "selectors" => report.selectors.map do |s| { "receiver" => s.receiver, "method" => s.method_name, "count" => s.count, "files" => s.files, "rules" => s.rules } end, "hotspots" => report.hotspots.map do |h| { "file" => h.file, "count" => h.count, "by_rule" => h.by_rule } end, "hints" => report.hints.map(&:to_h), # WD6: false means distribution / selectors / hotspots above # exclude `:info` (their counts will not sum to summary.total); # the summary's `info` field still reports the full count. "include_info" => report.include_info } end |
.rule_key(diagnostic) ⇒ Object
Diagnostics without a ‘rule` (parse errors, internal-analyzer errors) bucket under a single sentinel rather than vanishing.
68 69 70 |
# File 'lib/rigor/triage.rb', line 68 def rule_key(diagnostic) diagnostic.qualified_rule || UNCATEGORISED end |
.selector_for(receiver, method, group) ⇒ Object
134 135 136 137 138 139 140 141 |
# File 'lib/rigor/triage.rb', line 134 def selector_for(receiver, method, group) rules = group.group_by { |d| rule_key(d) } .transform_values(&:size) .sort_by { |rule, count| [-count, rule] } .to_h Selector.new(receiver: receiver, method_name: method, count: group.size, files: group.map(&:path).uniq.size, rules: rules) end |