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_typedisplay 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.
42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/rigor/triage.rb', line 42 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
70 71 72 73 74 |
# File 'lib/rigor/triage.rb', line 70 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
123 124 125 126 127 128 |
# File 'lib/rigor/triage.rb', line 123 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.
81 82 83 84 85 86 |
# File 'lib/rigor/triage.rb', line 81 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
60 61 62 63 64 65 66 67 68 |
# File 'lib/rigor/triage.rb', line 60 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
130 131 132 133 134 135 136 |
# File 'lib/rigor/triage.rb', line 130 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(C) and a bare C fold
to C; a generic C[...] keeps the Array[String] 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.
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/rigor/triage.rb', line 94 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
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/rigor/triage.rb', line 138 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.
56 57 58 |
# File 'lib/rigor/triage.rb', line 56 def rule_key(diagnostic) diagnostic.qualified_rule || UNCATEGORISED end |
.selector_for(receiver, method, group) ⇒ Object
114 115 116 117 118 119 120 121 |
# File 'lib/rigor/triage.rb', line 114 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 |