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
Class Method Details
.analyze(diagnostics, top: 10, hints: true) ⇒ Report
37
38
39
40
41
42
43
44
45
|
# File 'lib/rigor/triage.rb', line 37
def analyze(diagnostics, top: 10, hints: true)
Report.new(
summary: build_summary(diagnostics),
distribution: build_distribution(diagnostics),
selectors: build_selectors(diagnostics),
hotspots: build_hotspots(diagnostics, top),
hints: hints ? Catalogue.recognise(diagnostics) : []
)
end
|
.build_distribution(diagnostics) ⇒ Object
63
64
65
66
67
|
# File 'lib/rigor/triage.rb', line 63
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
124
125
126
127
128
129
|
# File 'lib/rigor/triage.rb', line 124
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.
78
79
80
81
82
83
|
# File 'lib/rigor/triage.rb', line 78
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
53
54
55
56
57
58
59
60
61
|
# File 'lib/rigor/triage.rb', line 53
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
131
132
133
134
135
136
137
|
# File 'lib/rigor/triage.rb', line 131
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.
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
# File 'lib/rigor/triage.rb', line 95
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
# File 'lib/rigor/triage.rb', line 139
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)
}
end
|
.rule_key(diagnostic) ⇒ Object
Diagnostics without a ‘rule` (parse errors, internal-analyzer errors) bucket under a single sentinel rather than vanishing.
49
50
51
|
# File 'lib/rigor/triage.rb', line 49
def rule_key(diagnostic)
diagnostic.qualified_rule || UNCATEGORISED
end
|
.selector_for(receiver, method, group) ⇒ Object
115
116
117
118
119
120
121
122
|
# File 'lib/rigor/triage.rb', line 115
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
|