rigortype 0.2.2 → 0.2.4
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/docs/manual/02-cli-reference.md +12 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/lib/rigor/analysis/diagnostic.rb +3 -0
- data/lib/rigor/cli/skill_describe.rb +8 -3
- data/lib/rigor/cli/triage_command.rb +8 -2
- data/lib/rigor/cli/triage_renderer.rb +4 -0
- data/lib/rigor/environment/rbs_loader.rb +54 -9
- data/lib/rigor/triage/catalogue.rb +16 -1
- data/lib/rigor/triage.rb +30 -7
- data/lib/rigor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 749e0e15c9b85dcec97c9afc1b107d5ebc1a1a685cc19aa39d316a9826992d4a
|
|
4
|
+
data.tar.gz: 45bcee26284594e35b51a050ce8ac234935bee954063b50fdfe1b0c6a9bcddea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fbf022b6e92a96e1c095cb3d887936161e836201662f051c60de5d0d78494f263228a28df509c3b937768735b5ce18162ee28657b2aaf1cd7a8b2d4851338a53
|
|
7
|
+
data.tar.gz: a49e89a07950f54fcf8f38e328b8fdb658aaf0802154df457c7b9b1370f35279c1d6d5fe3acf6a45c9e49ff2f7c09ff9f209b1f7dab68d20b283c9baa4b2760b
|
|
@@ -232,6 +232,18 @@ rigor triage [paths]
|
|
|
232
232
|
`--selectors-only`, and `--no-hints` select which sections print.
|
|
233
233
|
`triage` is advisory and always exits `0` — it never gates a build.
|
|
234
234
|
|
|
235
|
+
By default the distribution, selectors, and hotspots sections count
|
|
236
|
+
only the **actionable** diagnostics (`error` + `warning`). `info`
|
|
237
|
+
diagnostics are excluded from these volume views — on a Rails project
|
|
238
|
+
they are dominated by plugin *recognition trace* (`Account.find
|
|
239
|
+
resolves to Account`, `users_path → GET /users`), positive "Rigor
|
|
240
|
+
resolved this" records that would otherwise bury the genuine signal
|
|
241
|
+
and skew the hotspot ranking toward the files with the most *working*
|
|
242
|
+
code. The summary line still reports the full `info` count, and
|
|
243
|
+
heuristic hints still see every diagnostic (so the `gem-without-rbs`
|
|
244
|
+
notice survives). Pass `--include-info` to route `info` into the
|
|
245
|
+
volume views as well.
|
|
246
|
+
|
|
235
247
|
The **`selectors`** section is the by-(class, method) axis: it
|
|
236
248
|
aggregates the structured `receiver_type` / `method_name` fields the
|
|
237
249
|
diagnostics carry into `{receiver, method, count, files, rules}`
|
|
@@ -295,10 +295,17 @@ the most common error clusters.
|
|
|
295
295
|
"hints": [
|
|
296
296
|
{ "id": "activesupport-core-ext", "confidence": "likely",
|
|
297
297
|
"diagnostic_count": 365, "summary": "...", "action": "..." }
|
|
298
|
-
]
|
|
298
|
+
],
|
|
299
|
+
"include_info": false
|
|
299
300
|
}
|
|
300
301
|
```
|
|
301
302
|
|
|
303
|
+
By default the `distribution` / `selectors` / `hotspots` arrays count
|
|
304
|
+
only the actionable diagnostics (`error` + `warning`); `info` (mostly
|
|
305
|
+
plugin recognition trace) is excluded, so those counts do not sum to
|
|
306
|
+
`summary.total` and `include_info` is `false`. `summary.info` still
|
|
307
|
+
reports the full info count, and the hints still see every diagnostic.
|
|
308
|
+
|
|
302
309
|
The `selectors` array is the by-(class, method) axis: each row is a
|
|
303
310
|
dispatch target with its `count`, distinct-`files` spread, and
|
|
304
311
|
per-`rule` breakdown, keyed on a normalised receiver class — so an
|
|
@@ -47,6 +47,9 @@ module Rigor
|
|
|
47
47
|
def initialize(path:, line:, column:, message:, severity: :error, rule: nil, # rubocop:disable Metrics/ParameterLists
|
|
48
48
|
source_family: DEFAULT_SOURCE_FAMILY,
|
|
49
49
|
receiver_type: nil, method_name: nil, project_definition_site: nil)
|
|
50
|
+
raise ArgumentError, "line must be >= 1, got #{line}" if line < 1
|
|
51
|
+
raise ArgumentError, "column must be >= 1, got #{column}" if column < 1
|
|
52
|
+
|
|
50
53
|
@path = path
|
|
51
54
|
@line = line
|
|
52
55
|
@column = column
|
|
@@ -208,8 +208,14 @@ module Rigor
|
|
|
208
208
|
["rigor-rbs-setup", "your gems ship no community RBS yet — install it so Rigor stops typing them as Dynamic."]
|
|
209
209
|
elsif state.fetch(:ci) != :wired
|
|
210
210
|
["rigor-ci-setup", "Rigor is configured but not wired into CI — lock in the regression guard."]
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
# A present baseline is deliberately NOT a recommendation trigger.
|
|
212
|
+
# A baseline is a healthy, finished onboarding state, not a problem to
|
|
213
|
+
# work off; pushing every baselined project to "reduce it" turns a
|
|
214
|
+
# working build into a chore and tempts scattering `# rigor:disable`
|
|
215
|
+
# through the code to make a number go down — means over ends.
|
|
216
|
+
# `rigor-baseline-reduce` stays in the catalogue for the intermediate
|
|
217
|
+
# user who *chooses* to invest in it; the headline routes to genuinely
|
|
218
|
+
# additive steps instead.
|
|
213
219
|
elsif state.fetch(:editor) == :unwired
|
|
214
220
|
["rigor-editor-setup",
|
|
215
221
|
"you have an editor config but no Rigor LSP — wire `rigor lsp` for live diagnostics and hover types."]
|
|
@@ -297,7 +303,6 @@ module Rigor
|
|
|
297
303
|
The recommendation above is from a presence-only probe — it does not run
|
|
298
304
|
`rigor check`. If you have run (or now run) `rigor check`, let its findings
|
|
299
305
|
refine the choice:
|
|
300
|
-
- errors present and no baseline yet → rigor-baseline-reduce
|
|
301
306
|
- a `call.unresolved-toplevel` / `call.undefined-method` cluster on the
|
|
302
307
|
project's own monkey-patches → rigor-monkeypatch-resolve
|
|
303
308
|
- framework calls (ActiveRecord, routes, i18n …) typing as Dynamic with no
|
|
@@ -31,7 +31,8 @@ module Rigor
|
|
|
31
31
|
diagnostics = analyze(configuration)
|
|
32
32
|
|
|
33
33
|
report = Triage.analyze(diagnostics, top: options.fetch(:top),
|
|
34
|
-
hints: options.fetch(:sections).include?(:hints)
|
|
34
|
+
hints: options.fetch(:sections).include?(:hints),
|
|
35
|
+
include_info: options.fetch(:include_info))
|
|
35
36
|
renderer = TriageRenderer.new(report, sections: options.fetch(:sections))
|
|
36
37
|
@out.puts(options.fetch(:format) == "json" ? renderer.json : renderer.text)
|
|
37
38
|
0
|
|
@@ -40,12 +41,17 @@ module Rigor
|
|
|
40
41
|
private
|
|
41
42
|
|
|
42
43
|
def parse_options
|
|
43
|
-
options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS }
|
|
44
|
+
options = { config: nil, format: "text", top: 10, sections: DEFAULT_SECTIONS, include_info: false }
|
|
44
45
|
OptionParser.new do |opts|
|
|
45
46
|
opts.banner = USAGE
|
|
46
47
|
Options.add_config(opts, options)
|
|
47
48
|
opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
|
|
48
49
|
opts.on("--top=N", Integer, "Hotspot-file count (default 10)") { |v| options[:top] = v }
|
|
50
|
+
opts.on("--include-info",
|
|
51
|
+
"Route info diagnostics into distribution / selectors / hotspots " \
|
|
52
|
+
"(excluded by default — mostly plugin recognition trace)") do
|
|
53
|
+
options[:include_info] = true
|
|
54
|
+
end
|
|
49
55
|
opts.on("--hints-only", "Print only the heuristic-hints section") { options[:sections] = %i[hints] }
|
|
50
56
|
opts.on("--no-hints", "Print distribution + selectors + hotspots only") do
|
|
51
57
|
options[:sections] = %i[distribution selectors hotspots]
|
|
@@ -37,6 +37,10 @@ module Rigor
|
|
|
37
37
|
max = @report.distribution.map(&:count).max || 1
|
|
38
38
|
lines = ["Diagnostic distribution — #{s.total} total " \
|
|
39
39
|
"(#{s.error} error / #{s.warning} warning#{" / #{s.info} info" if s.info.positive?})"]
|
|
40
|
+
if !@report.include_info && s.info.positive?
|
|
41
|
+
lines << " #{s.info} info diagnostic(s) hidden below " \
|
|
42
|
+
"(mostly plugin recognition trace) — pass --include-info to route them"
|
|
43
|
+
end
|
|
40
44
|
@report.distribution.each do |row|
|
|
41
45
|
lines << format(" %<rule>-32s %<count>5d %<bar>s",
|
|
42
46
|
rule: row.rule, count: row.count, bar: bar(row.count, max))
|
|
@@ -170,7 +170,7 @@ module Rigor
|
|
|
170
170
|
source = missing.map { |name| "module #{name}\nend\n" }.join
|
|
171
171
|
buffer = ::RBS::Buffer.new(name: SYNTHETIC_NAMESPACE_BUFFER, content: source)
|
|
172
172
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
173
|
-
env
|
|
173
|
+
add_parsed_decls(env, buffer, directives, decls)
|
|
174
174
|
rescue ::RBS::BaseError
|
|
175
175
|
# Fail-soft: synthesis is an opportunistic uplift, never a
|
|
176
176
|
# hard requirement. A parse failure here just leaves the env
|
|
@@ -232,11 +232,52 @@ module Rigor
|
|
|
232
232
|
missing.uniq
|
|
233
233
|
end
|
|
234
234
|
|
|
235
|
+
# Normalises a `class_decls` entry's representative declaration
|
|
236
|
+
# across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
|
|
237
|
+
# RBS 4.x exposes it as `entry.primary_decl` (the AST declaration
|
|
238
|
+
# directly); RBS 3.x exposes `entry.primary` (a wrapper whose
|
|
239
|
+
# `#decl` is the AST declaration). Returns the AST declaration, or
|
|
240
|
+
# nil when neither accessor is present. Without this guard,
|
|
241
|
+
# `class_decl_paths` crashed under RBS 3.x with
|
|
242
|
+
# `undefined method 'primary_decl'`.
|
|
243
|
+
def primary_decl_for(entry)
|
|
244
|
+
if entry.respond_to?(:primary_decl)
|
|
245
|
+
entry.primary_decl
|
|
246
|
+
elsif entry.respond_to?(:primary)
|
|
247
|
+
primary = entry.primary
|
|
248
|
+
primary.respond_to?(:decl) ? primary.decl : primary
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Appends freshly-parsed declarations to an `RBS::Environment`
|
|
253
|
+
# across the gemspec's supported RBS range (`rbs >= 3.0, < 5.0`).
|
|
254
|
+
# RBS 4.x wraps the declarations in an `RBS::Source::RBS` and
|
|
255
|
+
# takes them through `env.add_source`; RBS 3.x has neither
|
|
256
|
+
# `RBS::Source` nor `add_source` and instead registers them with
|
|
257
|
+
# `env.add_signature(buffer:, directives:, decls:)` (a bare
|
|
258
|
+
# `env << decl` is NOT enough — it skips the `signatures` table
|
|
259
|
+
# that `resolve_type_names` rebuilds from, so the synthesized
|
|
260
|
+
# declarations silently vanish on resolve). Without this guard
|
|
261
|
+
# the synthesis paths (`synthesize_missing_namespaces`,
|
|
262
|
+
# `append_stub_declarations`, `add_virtual_rbs`) crashed under
|
|
263
|
+
# RBS 3.x with `uninitialized constant RBS::Source`.
|
|
264
|
+
def add_parsed_decls(env, buffer, directives, decls)
|
|
265
|
+
decls ||= []
|
|
266
|
+
directives ||= []
|
|
267
|
+
if env.respond_to?(:add_source)
|
|
268
|
+
env.add_source(::RBS::Source::RBS.new(buffer, directives, decls))
|
|
269
|
+
elsif env.respond_to?(:add_signature)
|
|
270
|
+
env.add_signature(buffer: buffer, directives: directives, decls: decls)
|
|
271
|
+
else
|
|
272
|
+
decls.each { |decl| env << decl }
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
235
276
|
# True when a `class_decls` entry was declared in one of the
|
|
236
277
|
# project's own signature files (by declaration location), so
|
|
237
278
|
# the sweep skips the bundled stdlib / vendored universe.
|
|
238
279
|
def project_entry?(entry, project_files)
|
|
239
|
-
decl = entry
|
|
280
|
+
decl = primary_decl_for(entry)
|
|
240
281
|
location = decl&.location
|
|
241
282
|
buffer_name = location&.buffer&.name
|
|
242
283
|
return false unless buffer_name
|
|
@@ -262,7 +303,7 @@ module Rigor
|
|
|
262
303
|
end.join
|
|
263
304
|
buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source)
|
|
264
305
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
265
|
-
base_env
|
|
306
|
+
add_parsed_decls(base_env, buffer, directives, decls)
|
|
266
307
|
rescue ::RBS::BaseError
|
|
267
308
|
nil
|
|
268
309
|
end
|
|
@@ -284,8 +325,7 @@ module Rigor
|
|
|
284
325
|
|
|
285
326
|
buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
|
|
286
327
|
_, directives, decls = ::RBS::Parser.parse_signature(buffer)
|
|
287
|
-
|
|
288
|
-
env.add_source(source)
|
|
328
|
+
add_parsed_decls(env, buffer, directives, decls)
|
|
289
329
|
rescue ::RBS::BaseError
|
|
290
330
|
# WD6 fail-soft: a single broken virtual RBS contribution
|
|
291
331
|
# does not pull the whole env down. The plugin layer
|
|
@@ -589,7 +629,7 @@ module Rigor
|
|
|
589
629
|
|
|
590
630
|
result = {}
|
|
591
631
|
env.class_decls.each do |rbs_name, entry|
|
|
592
|
-
decl = entry
|
|
632
|
+
decl = self.class.primary_decl_for(entry)
|
|
593
633
|
next if decl.nil?
|
|
594
634
|
|
|
595
635
|
location = decl.location
|
|
@@ -911,12 +951,17 @@ module Rigor
|
|
|
911
951
|
end
|
|
912
952
|
|
|
913
953
|
# Collects the AST declaration nodes behind a `class_decls`
|
|
914
|
-
# entry
|
|
915
|
-
#
|
|
916
|
-
#
|
|
954
|
+
# entry across the supported RBS range (`rbs >= 3.0, < 5.0`).
|
|
955
|
+
# RBS 4's `ModuleEntry` / `ClassEntry` expose `each_decl` yielding
|
|
956
|
+
# bare AST declarations; RBS 3.x exposes `decls`, an array of
|
|
957
|
+
# `MultiEntry::D` wrappers whose `#decl` is the AST declaration.
|
|
958
|
+
# The single-`decl` shape is handled defensively so the loader
|
|
959
|
+
# survives an rbs-gem minor bump.
|
|
917
960
|
def entry_declarations(entry)
|
|
918
961
|
if entry.respond_to?(:each_decl)
|
|
919
962
|
[].tap { |acc| entry.each_decl { |decl| acc << decl } }
|
|
963
|
+
elsif entry.respond_to?(:decls)
|
|
964
|
+
entry.decls.map { |d| d.respond_to?(:decl) ? d.decl : d }
|
|
920
965
|
elsif entry.respond_to?(:decl)
|
|
921
966
|
[entry.decl]
|
|
922
967
|
else
|
|
@@ -87,12 +87,27 @@ module Rigor
|
|
|
87
87
|
GENUINE_BUG_MAX_COUNT = 5 # rule total ≤ N → "likely genuine bug"
|
|
88
88
|
private_constant :SYSTEMIC_THRESHOLD, :MONKEY_PATCH_MIN_FILES, :GENUINE_BUG_MAX_COUNT
|
|
89
89
|
|
|
90
|
+
# WD6 (ADR-23): H5 (systemic cluster) and H6 (genuine bugs) are
|
|
91
|
+
# the only count-based, severity-agnostic recognisers, so they
|
|
92
|
+
# alone risk reading plugin recognition trace (`:info`
|
|
93
|
+
# `*.model-call` / `*.helper`, …) as a "systemic cluster" or a
|
|
94
|
+
# "genuine bug" — frightening working code. They are guarded
|
|
95
|
+
# against `:info` unless `include_info` restores the pre-v0.2.3
|
|
96
|
+
# behaviour. Every other recogniser keys on an error/warning rule
|
|
97
|
+
# (H1/H2/H2K/H4/H7) or intentionally reads an info notice
|
|
98
|
+
# (H3 — the `gem-without-rbs` `rbs.coverage.missing-gem`), so the
|
|
99
|
+
# full pool is correct for them.
|
|
100
|
+
INFO_GUARDED = %i[h5_systemic_cluster h6_genuine_bugs].freeze
|
|
101
|
+
private_constant :INFO_GUARDED
|
|
102
|
+
|
|
90
103
|
# @param diagnostics [Array<Analysis::Diagnostic>]
|
|
104
|
+
# @param include_info [Boolean] let H5/H6 see `:info` diagnostics
|
|
91
105
|
# @return [Array<Hint>]
|
|
92
|
-
def recognise(diagnostics)
|
|
106
|
+
def recognise(diagnostics, include_info: false)
|
|
93
107
|
claimed = {}.compare_by_identity
|
|
94
108
|
recognisers.filter_map do |recogniser|
|
|
95
109
|
pool = diagnostics.reject { |d| claimed[d] }
|
|
110
|
+
pool = pool.reject { |d| d.severity == :info } if INFO_GUARDED.include?(recogniser) && !include_info
|
|
96
111
|
hint, matched = send(recogniser, pool)
|
|
97
112
|
next unless hint
|
|
98
113
|
|
data/lib/rigor/triage.rb
CHANGED
|
@@ -26,21 +26,40 @@ module Rigor
|
|
|
26
26
|
# receiver); `files` is the distinct-file count (a systemic vs.
|
|
27
27
|
# localised signal); `rules` is the per-rule breakdown.
|
|
28
28
|
Selector = Data.define(:receiver, :method_name, :count, :files, :rules)
|
|
29
|
-
Report = Data.define(:summary, :distribution, :selectors, :hotspots, :hints)
|
|
29
|
+
Report = Data.define(:summary, :distribution, :selectors, :hotspots, :hints, :include_info)
|
|
30
30
|
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
|
+
# WD6 (ADR-23): the volume views — distribution / selectors /
|
|
34
|
+
# hotspots — route only the *actionable* diagnostics (error +
|
|
35
|
+
# warning) by default. Plugin-emitted `:info` diagnostics are
|
|
36
|
+
# overwhelmingly recognition trace (`plugin.activerecord.model-call`,
|
|
37
|
+
# `plugin.rails-routes.helper`, …) — positive "Rigor resolved this
|
|
38
|
+
# call" records, not problems — and on a real Rails app they swamp
|
|
39
|
+
# the genuine error/warning signal (the field trip: 257 of 267
|
|
40
|
+
# diagnostics were such trace) and invert the hotspot ranking
|
|
41
|
+
# towards the files with the *most working* code. The summary still
|
|
42
|
+
# reports the full info count, and `include_info: true` (the
|
|
43
|
+
# `--include-info` flag) restores the pre-v0.2.3 behaviour. Hints
|
|
44
|
+
# always see the full stream so the `gem-without-rbs` notice (an
|
|
45
|
+
# info-severity `rbs.coverage.missing-gem`) survives; the
|
|
46
|
+
# count-based H5/H6 recognisers guard against info themselves so
|
|
47
|
+
# recognition trace never reads as a bug.
|
|
48
|
+
#
|
|
33
49
|
# @param diagnostics [Array<Analysis::Diagnostic>]
|
|
34
50
|
# @param top [Integer] hotspot-file cap
|
|
35
51
|
# @param hints [Boolean] run the heuristic catalogue
|
|
52
|
+
# @param include_info [Boolean] route info into the volume views
|
|
36
53
|
# @return [Report]
|
|
37
|
-
def analyze(diagnostics, top: 10, hints: true)
|
|
54
|
+
def analyze(diagnostics, top: 10, hints: true, include_info: false)
|
|
55
|
+
routed = include_info ? diagnostics : diagnostics.reject { |d| d.severity == :info }
|
|
38
56
|
Report.new(
|
|
39
57
|
summary: build_summary(diagnostics),
|
|
40
|
-
distribution: build_distribution(
|
|
41
|
-
selectors: build_selectors(
|
|
42
|
-
hotspots: build_hotspots(
|
|
43
|
-
hints: hints ? Catalogue.recognise(diagnostics) : []
|
|
58
|
+
distribution: build_distribution(routed),
|
|
59
|
+
selectors: build_selectors(routed),
|
|
60
|
+
hotspots: build_hotspots(routed, top),
|
|
61
|
+
hints: hints ? Catalogue.recognise(diagnostics, include_info: include_info) : [],
|
|
62
|
+
include_info: include_info
|
|
44
63
|
)
|
|
45
64
|
end
|
|
46
65
|
|
|
@@ -150,7 +169,11 @@ module Rigor
|
|
|
150
169
|
"hotspots" => report.hotspots.map do |h|
|
|
151
170
|
{ "file" => h.file, "count" => h.count, "by_rule" => h.by_rule }
|
|
152
171
|
end,
|
|
153
|
-
"hints" => report.hints.map(&:to_h)
|
|
172
|
+
"hints" => report.hints.map(&:to_h),
|
|
173
|
+
# WD6: false means distribution / selectors / hotspots above
|
|
174
|
+
# exclude `:info` (their counts will not sum to summary.total);
|
|
175
|
+
# the summary's `info` field still reports the full count.
|
|
176
|
+
"include_info" => report.include_info
|
|
154
177
|
}
|
|
155
178
|
end
|
|
156
179
|
end
|
data/lib/rigor/version.rb
CHANGED