rigortype 0.2.0 → 0.2.2
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/README.md +82 -20
- data/data/core_overlay/numeric.rbs +33 -0
- data/data/core_overlay/pathname.rbs +25 -0
- data/data/core_overlay/string_scanner.rbs +28 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +473 -0
- data/data/vendored_gem_sigs/ast/ast.rbs +130 -0
- data/data/vendored_gem_sigs/bcrypt/bcrypt.rbs +47 -0
- data/data/vendored_gem_sigs/bundler/bundler.rbs +238 -0
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +34 -0
- data/data/vendored_gem_sigs/did_you_mean/did_you_mean_extras.rbs +34 -0
- data/data/vendored_gem_sigs/idn-ruby/idn.rbs +54 -0
- data/data/vendored_gem_sigs/mysql2/client.rbs +55 -0
- data/data/vendored_gem_sigs/mysql2/error.rbs +5 -0
- data/data/vendored_gem_sigs/mysql2/result.rbs +31 -0
- data/data/vendored_gem_sigs/mysql2/statement.rbs +5 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri.rbs +2332 -0
- data/data/vendored_gem_sigs/nokogiri/nokogiri_html5.rbs +47 -0
- data/data/vendored_gem_sigs/pg/pg.rbs +212 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +44 -0
- data/data/vendored_gem_sigs/redis/errors.rbs +50 -0
- data/data/vendored_gem_sigs/redis/future.rbs +5 -0
- data/data/vendored_gem_sigs/redis/redis.rbs +348 -0
- data/data/vendored_gem_sigs/redis/redis_extras.rbs +130 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +226 -0
- data/docs/handbook/01-getting-started.md +311 -0
- data/docs/handbook/02-everyday-types.md +337 -0
- data/docs/handbook/03-narrowing.md +359 -0
- data/docs/handbook/04-tuples-and-shapes.md +321 -0
- data/docs/handbook/05-methods-and-blocks.md +339 -0
- data/docs/handbook/06-classes.md +305 -0
- data/docs/handbook/07-rbs-and-extended.md +427 -0
- data/docs/handbook/08-understanding-errors.md +373 -0
- data/docs/handbook/09-plugins.md +241 -0
- data/docs/handbook/10-sorbet.md +347 -0
- data/docs/handbook/11-sig-gen.md +312 -0
- data/docs/handbook/12-lightweight-hkt.md +333 -0
- data/docs/handbook/README.md +275 -0
- data/docs/handbook/appendix-elixir.md +370 -0
- data/docs/handbook/appendix-go.md +399 -0
- data/docs/handbook/appendix-java-csharp.md +470 -0
- data/docs/handbook/appendix-liskov.md +580 -0
- data/docs/handbook/appendix-mypy.md +370 -0
- data/docs/handbook/appendix-phpstan.md +338 -0
- data/docs/handbook/appendix-protocols-and-structural-typing.md +292 -0
- data/docs/handbook/appendix-rust.md +446 -0
- data/docs/handbook/appendix-steep.md +336 -0
- data/docs/handbook/appendix-type-theory.md +1662 -0
- data/docs/handbook/appendix-typeprof.md +416 -0
- data/docs/handbook/appendix-typescript.md +332 -0
- data/docs/install.md +189 -0
- data/docs/llms.txt +72 -0
- data/docs/manual/01-installation.md +342 -0
- data/docs/manual/02-cli-reference.md +557 -0
- data/docs/manual/03-configuration.md +152 -0
- data/docs/manual/04-diagnostics.md +206 -0
- data/docs/manual/05-inspecting-types.md +109 -0
- data/docs/manual/06-baseline.md +104 -0
- data/docs/manual/07-plugins.md +92 -0
- data/docs/manual/08-skills.md +143 -0
- data/docs/manual/09-editor-integration.md +245 -0
- data/docs/manual/10-mcp-server.md +532 -0
- data/docs/manual/11-ci.md +274 -0
- data/docs/manual/12-caching.md +116 -0
- data/docs/manual/13-troubleshooting.md +120 -0
- data/docs/manual/14-rails-quickstart.md +332 -0
- data/docs/manual/15-type-protection-coverage.md +204 -0
- data/docs/manual/16-rbs-extended-annotations.md +190 -0
- data/docs/manual/17-driving-improvement.md +160 -0
- data/docs/manual/README.md +87 -0
- data/docs/manual/ci-templates/README.md +58 -0
- data/docs/manual/plugins/README.md +86 -0
- data/docs/manual/plugins/rigor-actioncable.md +78 -0
- data/docs/manual/plugins/rigor-actionmailer.md +74 -0
- data/docs/manual/plugins/rigor-actionpack.md +80 -0
- data/docs/manual/plugins/rigor-activejob.md +58 -0
- data/docs/manual/plugins/rigor-activerecord.md +102 -0
- data/docs/manual/plugins/rigor-activestorage.md +74 -0
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +86 -0
- data/docs/manual/plugins/rigor-devise.md +70 -0
- data/docs/manual/plugins/rigor-dry-schema.md +56 -0
- data/docs/manual/plugins/rigor-dry-struct.md +60 -0
- data/docs/manual/plugins/rigor-dry-types.md +59 -0
- data/docs/manual/plugins/rigor-dry-validation.md +62 -0
- data/docs/manual/plugins/rigor-factorybot.md +76 -0
- data/docs/manual/plugins/rigor-graphql.md +89 -0
- data/docs/manual/plugins/rigor-hanami.md +83 -0
- data/docs/manual/plugins/rigor-mangrove.md +73 -0
- data/docs/manual/plugins/rigor-minitest.md +86 -0
- data/docs/manual/plugins/rigor-pundit.md +72 -0
- data/docs/manual/plugins/rigor-rails-i18n.md +92 -0
- data/docs/manual/plugins/rigor-rails-routes.md +94 -0
- data/docs/manual/plugins/rigor-rails.md +44 -0
- data/docs/manual/plugins/rigor-rbs-inline.md +83 -0
- data/docs/manual/plugins/rigor-rspec-rails.md +72 -0
- data/docs/manual/plugins/rigor-rspec.md +86 -0
- data/docs/manual/plugins/rigor-shoulda-matchers.md +78 -0
- data/docs/manual/plugins/rigor-sidekiq.md +78 -0
- data/docs/manual/plugins/rigor-sinatra.md +61 -0
- data/docs/manual/plugins/rigor-sorbet.md +63 -0
- data/docs/manual/plugins/rigor-statesman.md +75 -0
- data/docs/manual/plugins/rigor-typescript-utility-types.md +71 -0
- data/exe/rigor +1 -1
- data/lib/rigor/analysis/incremental_session.rb +4 -2
- data/lib/rigor/analysis/run_stats.rb +13 -1
- data/lib/rigor/analysis/runner.rb +54 -12
- data/lib/rigor/cli/check_command.rb +26 -3
- data/lib/rigor/cli/coverage_command.rb +67 -92
- data/lib/rigor/cli/coverage_mutation.rb +149 -0
- data/lib/rigor/cli/docs_command.rb +248 -0
- data/lib/rigor/cli/fused_protection_renderer.rb +67 -0
- data/lib/rigor/cli/fused_protection_report.rb +76 -0
- data/lib/rigor/cli/skill_command.rb +103 -41
- data/lib/rigor/cli/skill_describe.rb +346 -0
- data/lib/rigor/cli.rb +25 -3
- data/lib/rigor/config_audit.rb +152 -0
- data/lib/rigor/configuration.rb +12 -0
- data/lib/rigor/environment/rbs_loader.rb +27 -0
- data/lib/rigor/environment.rb +49 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +140 -38
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +37 -6
- data/lib/rigor/inference/scope_indexer.rb +87 -89
- data/lib/rigor/inference/statement_evaluator.rb +27 -0
- data/lib/rigor/plugin/isolation.rb +5 -5
- data/lib/rigor/plugin/loader.rb +4 -2
- data/lib/rigor/protection/diagnostic_oracle.rb +51 -0
- data/lib/rigor/protection/mutation_scanner.rb +98 -38
- data/lib/rigor/protection/mutator.rb +21 -0
- data/lib/rigor/protection/test_suite_oracle.rb +68 -0
- data/lib/rigor/signature_path_audit.rb +92 -0
- data/lib/rigor/version.rb +1 -1
- data/skills/rigor-ask/SKILL.md +172 -0
- data/skills/rigor-doctor/SKILL.md +87 -0
- data/skills/rigor-editor-setup/SKILL.md +114 -0
- data/skills/rigor-mcp-setup/SKILL.md +117 -0
- data/skills/rigor-monkeypatch-resolve/SKILL.md +79 -0
- data/skills/rigor-next-steps/SKILL.md +113 -0
- data/skills/rigor-plugin-tune/SKILL.md +79 -0
- data/skills/rigor-protection-uplift/SKILL.md +133 -0
- data/skills/rigor-rbs-setup/SKILL.md +128 -0
- data/skills/rigor-upgrade/SKILL.md +79 -0
- metadata +120 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
-
require_relative "../analysis/runner"
|
|
6
5
|
require_relative "mutator"
|
|
6
|
+
require_relative "diagnostic_oracle"
|
|
7
7
|
|
|
8
8
|
module Rigor
|
|
9
9
|
module Protection
|
|
@@ -15,17 +15,16 @@ module Rigor
|
|
|
15
15
|
# Mechanism (the ADR-62 warm loop, narrowed to per-file measurement):
|
|
16
16
|
# generate the type-visible mutations ({Mutator}), keep only those whose
|
|
17
17
|
# receiver Rigor holds a concrete type for (the type-aware filter — the
|
|
18
|
-
# FP-safe meaning-maker; an unresolved receiver is kept), then for each
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# FP-safe meaning-maker; an unresolved receiver is kept), then for each ask
|
|
19
|
+
# the **kill oracle** whether the mutant is caught. The oracle is the ADR-69
|
|
20
|
+
# seam: {#scan_file} uses the {DiagnosticOracle} (a *new Rigor diagnostic* =
|
|
21
|
+
# a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the
|
|
22
|
+
# type-survivors (ADR-70 — the dynamic protection axis).
|
|
22
23
|
#
|
|
23
24
|
# The expensive builds (RBS environment + the whole-project pre-pass scan)
|
|
24
|
-
# are paid ONCE by the caller and threaded
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# Passing `prebuilt:` disables the run-result cache (whose key digests the
|
|
28
|
-
# *disk* file), so a mutant is never served a stale clean hit.
|
|
25
|
+
# are paid ONCE by the caller and threaded into the {DiagnosticOracle}; each
|
|
26
|
+
# mutant reuses them through `Runner.new(prebuilt:)#run_source` (in-memory
|
|
27
|
+
# overlay, no disk write).
|
|
29
28
|
class MutationScanner
|
|
30
29
|
# A surviving mutation site — a breakage Rigor did not catch.
|
|
31
30
|
SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
|
|
@@ -39,18 +38,46 @@ module Rigor
|
|
|
39
38
|
def ratio = total.zero? ? 1.0 : killed.to_f / total
|
|
40
39
|
end
|
|
41
40
|
|
|
41
|
+
# ADR-70 — one type-survivor classified by the dynamic (test) axis.
|
|
42
|
+
# `protection` is `:test` (a test caught it) or `:none` (unprotected — the
|
|
43
|
+
# "add a type OR a test here" sites).
|
|
44
|
+
FusedSite = Data.define(:line, :receiver, :method_name, :operator, :protection)
|
|
45
|
+
|
|
46
|
+
# ADR-70 — the per-file fused classification. The gradual short-circuit
|
|
47
|
+
# collapses the conceptual "doubly-protected" bucket into `type_killed`:
|
|
48
|
+
# a mutant the type checker already kills never reaches the suite, because
|
|
49
|
+
# the static net already suffices and re-running the suite to learn a test
|
|
50
|
+
# *would also* catch it is wasted work. So the observed buckets are three.
|
|
51
|
+
FusedFileResult = Data.define(:path, :type_killed, :test_killed, :sites) do
|
|
52
|
+
# The unprotected sites (neither a type nor a test caught the breakage).
|
|
53
|
+
def unprotected = sites.size
|
|
54
|
+
def total = type_killed + test_killed + unprotected
|
|
55
|
+
|
|
56
|
+
# Fused protected ratio — caught by *either* axis.
|
|
57
|
+
def ratio = total.zero? ? 1.0 : (type_killed + test_killed).to_f / total
|
|
58
|
+
end
|
|
59
|
+
|
|
42
60
|
# @param configuration [Rigor::Configuration]
|
|
43
61
|
# @param environment [Rigor::Environment] pre-built once by the caller
|
|
44
62
|
# @param project_scan [Rigor::Analysis::ProjectScan] pre-built once
|
|
45
63
|
# @param limit [Integer, nil] optional per-file mutation cap (sampled with
|
|
46
64
|
# `seed`); nil analyses every type-relevant mutation (deterministic).
|
|
47
65
|
# @param seed [Integer] RNG seed for the optional sample.
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
# @param oracle [#baseline, #killed?, nil] the kill oracle (ADR-69 Seam 1);
|
|
67
|
+
# defaults to the {DiagnosticOracle} (the ADR-62/63 behaviour).
|
|
68
|
+
# @param site_selector [:biteable, :all] which sites to mutate (ADR-69
|
|
69
|
+
# Seam 2). `:biteable` (default) keeps only concrete-type sites Rigor can
|
|
70
|
+
# bite; `:all` also mutates Dynamic-receiver dispatch sites — use only
|
|
71
|
+
# with a {TestSuiteOracle} (the fused overlay), never the diagnostic path.
|
|
72
|
+
def initialize(configuration:, environment:, project_scan:, limit: nil, seed: 1, oracle: nil,
|
|
73
|
+
site_selector: :biteable)
|
|
50
74
|
@environment = environment
|
|
51
|
-
@project_scan = project_scan
|
|
52
75
|
@limit = limit
|
|
53
76
|
@seed = seed
|
|
77
|
+
@site_selector = site_selector
|
|
78
|
+
@oracle = oracle || DiagnosticOracle.new(
|
|
79
|
+
configuration: configuration, environment: environment, project_scan: project_scan
|
|
80
|
+
)
|
|
54
81
|
end
|
|
55
82
|
|
|
56
83
|
# @param path [String] the file to measure (used as the in-memory bind path)
|
|
@@ -58,21 +85,13 @@ module Rigor
|
|
|
58
85
|
# @return [FileResult]
|
|
59
86
|
def scan_file(path, source: nil)
|
|
60
87
|
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
61
|
-
|
|
62
|
-
kept, = mutator.filter_by_type(mutator.mutations, environment: @environment, path: path)
|
|
63
|
-
kept = sample(kept)
|
|
88
|
+
kept = kept_mutations(source, path)
|
|
64
89
|
return FileResult.new(path: path, killed: 0, survived: 0, sites: []) if kept.empty?
|
|
65
90
|
|
|
66
|
-
baseline =
|
|
67
|
-
measure(source, path, kept, baseline)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def measure(source, path, mutations, baseline)
|
|
91
|
+
baseline = @oracle.baseline(source: source, path: path)
|
|
73
92
|
killed = 0
|
|
74
93
|
sites = []
|
|
75
|
-
|
|
94
|
+
kept.each do |mut|
|
|
76
95
|
case classify(source, path, mut, baseline)
|
|
77
96
|
when :killed then killed += 1
|
|
78
97
|
when :survived then sites << surviving_site(mut)
|
|
@@ -82,39 +101,80 @@ module Rigor
|
|
|
82
101
|
FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
|
|
83
102
|
end
|
|
84
103
|
|
|
104
|
+
# ADR-70 — the fused static∪dynamic measurement. Runs the type pass
|
|
105
|
+
# (the {DiagnosticOracle}); for every mutant the type checker did **not**
|
|
106
|
+
# kill, asks `test_oracle` whether the project's test suite catches it.
|
|
107
|
+
# The expensive suite run is paid only for type-survivors (the gradual
|
|
108
|
+
# short-circuit), so the cost is proportional to the protection hole.
|
|
109
|
+
# @param test_oracle [TestSuiteOracle]
|
|
110
|
+
# @return [FusedFileResult]
|
|
111
|
+
def scan_file_fused(path, test_oracle:, source: nil)
|
|
112
|
+
source ||= File.read(path, encoding: Encoding::UTF_8)
|
|
113
|
+
kept = kept_mutations(source, path)
|
|
114
|
+
return FusedFileResult.new(path: path, type_killed: 0, test_killed: 0, sites: []) if kept.empty?
|
|
115
|
+
|
|
116
|
+
baseline = @oracle.baseline(source: source, path: path)
|
|
117
|
+
type_killed = 0
|
|
118
|
+
test_killed = 0
|
|
119
|
+
sites = []
|
|
120
|
+
kept.each do |mut|
|
|
121
|
+
case classify(source, path, mut, baseline)
|
|
122
|
+
when :killed then type_killed += 1
|
|
123
|
+
when :survived
|
|
124
|
+
if test_oracle.killed?(path: path, original: source, mutant_source: mut.apply(source))
|
|
125
|
+
test_killed += 1
|
|
126
|
+
else
|
|
127
|
+
sites << fused_site(mut, :none)
|
|
128
|
+
end
|
|
129
|
+
# :invalid — a parse-broken mutant; not a measurement, skip it.
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
FusedFileResult.new(path: path, type_killed: type_killed, test_killed: test_killed, sites: sites)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
# The mutations to measure: the biteable filter (concrete-type sites only;
|
|
138
|
+
# the FP-safe default — an unresolved receiver is kept) or, under the
|
|
139
|
+
# `:all` selector (ADR-69 Seam 2), every dispatch site including Dynamic
|
|
140
|
+
# receivers. Optionally sampled.
|
|
141
|
+
def kept_mutations(source, path)
|
|
142
|
+
mutator = Mutator.new(source)
|
|
143
|
+
muts = mutator.mutations
|
|
144
|
+
kept =
|
|
145
|
+
if @site_selector == :all
|
|
146
|
+
mutator.dispatch_site_mutations(muts, environment: @environment, path: path)
|
|
147
|
+
else
|
|
148
|
+
mutator.filter_by_type(muts, environment: @environment, path: path).first
|
|
149
|
+
end
|
|
150
|
+
sample(kept)
|
|
151
|
+
end
|
|
152
|
+
|
|
85
153
|
def classify(source, path, mut, baseline)
|
|
86
154
|
mutant_source = mut.apply(source)
|
|
87
155
|
return :invalid unless Prism.parse(mutant_source).success?
|
|
88
156
|
|
|
89
|
-
|
|
90
|
-
new_diagnostics.empty? ? :survived : :killed
|
|
157
|
+
@oracle.killed?(mutant_source: mutant_source, path: path, baseline: baseline) ? :killed : :survived
|
|
91
158
|
rescue StandardError
|
|
92
159
|
# A harness-level failure on one mutant must not abort the file.
|
|
93
160
|
:invalid
|
|
94
161
|
end
|
|
95
162
|
|
|
96
|
-
# cache_store: nil + prebuilt: scan ⇒ the run cache is bypassed and the
|
|
97
|
-
# mutant is always re-analysed against the in-memory bytes.
|
|
98
|
-
def analyse(source, path)
|
|
99
|
-
Rigor::Analysis::Runner.new(
|
|
100
|
-
configuration: @configuration, environment: @environment, prebuilt: @project_scan,
|
|
101
|
-
cache_store: nil, collect_stats: false
|
|
102
|
-
).run_source(source: source, path: path).diagnostics
|
|
103
|
-
end
|
|
104
|
-
|
|
105
163
|
def sample(mutations)
|
|
106
164
|
return mutations unless @limit
|
|
107
165
|
|
|
108
166
|
mutations.sample(@limit, random: Random.new(@seed))
|
|
109
167
|
end
|
|
110
168
|
|
|
111
|
-
def signatures(diagnostics) = diagnostics.to_set { |d| sig(d) }
|
|
112
|
-
def sig(diagnostic) = [diagnostic.rule, diagnostic.path, diagnostic.line, diagnostic.column, diagnostic.message]
|
|
113
|
-
|
|
114
169
|
def surviving_site(mut)
|
|
115
170
|
SurvivingSite.new(line: mut.line, receiver: mut.anchor_type,
|
|
116
171
|
method_name: mut.method_name, operator: mut.operator.to_s)
|
|
117
172
|
end
|
|
173
|
+
|
|
174
|
+
def fused_site(mut, protection)
|
|
175
|
+
FusedSite.new(line: mut.line, receiver: mut.anchor_type, method_name: mut.method_name,
|
|
176
|
+
operator: mut.operator.to_s, protection: protection)
|
|
177
|
+
end
|
|
118
178
|
end
|
|
119
179
|
end
|
|
120
180
|
end
|
|
@@ -98,6 +98,27 @@ module Rigor
|
|
|
98
98
|
[kept, mutations.size - kept.size]
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# ADR-69 Seam 2 (AllSites) — keep every *dispatch-site* mutation (a method
|
|
102
|
+
# call or a call-argument literal), Dynamic receiver included, annotating
|
|
103
|
+
# the anchor type where Rigor holds one. Drops only non-dispatch literals
|
|
104
|
+
# (a literal outside any call — no receiver contract to violate). The
|
|
105
|
+
# biteable {#filter_by_type} hides exactly the Dynamic sites a test-suite
|
|
106
|
+
# consumer most wants to probe: where Rigor cannot bite, a test is the only
|
|
107
|
+
# protection. Use only with a {TestSuiteOracle} — at a Dynamic site the
|
|
108
|
+
# type pass can never kill, so without the test axis these are all noise.
|
|
109
|
+
def dispatch_site_mutations(mutations, environment:, path:)
|
|
110
|
+
base = Rigor::Scope.empty(environment: environment, source_path: path)
|
|
111
|
+
index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
|
|
112
|
+
cache = {}
|
|
113
|
+
mutations.select do |mut|
|
|
114
|
+
next false if mut.method_name.nil?
|
|
115
|
+
|
|
116
|
+
_keep, type = anchor_decision(mut.anchor, index, cache)
|
|
117
|
+
mut.anchor_type = type
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
101
122
|
private
|
|
102
123
|
|
|
103
124
|
def walk(node, &blk)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Protection
|
|
5
|
+
# ADR-70 — the **test-suite** kill oracle, the dynamic sibling of
|
|
6
|
+
# {DiagnosticOracle} on the ADR-69 seam. A mutant is killed iff applying its
|
|
7
|
+
# bytes to the file under test turns the project's test suite **red**. This is
|
|
8
|
+
# the dynamic half of the fused static∪dynamic protection map: a `Dynamic`
|
|
9
|
+
# site Rigor cannot bite (a type survivor) may still be fully guarded by a
|
|
10
|
+
# test.
|
|
11
|
+
#
|
|
12
|
+
# The suite command is the **runner hook** (`--test-command`, e.g.
|
|
13
|
+
# `bundle exec rake`). The runner is injectable so the decision logic is
|
|
14
|
+
# unit-testable without shelling out; the default shells out and reads the
|
|
15
|
+
# process exit status (0 = green / passed).
|
|
16
|
+
#
|
|
17
|
+
# I/O policy: {#killed?} writes the mutant to disk, runs the suite, and
|
|
18
|
+
# **always restores** the original bytes in an `ensure` — a normal exception
|
|
19
|
+
# never leaves a mutant on disk. (A hard interrupt mid-suite is the standard
|
|
20
|
+
# mutation-testing hazard the `ensure` cannot cover; callers running this in
|
|
21
|
+
# CI accept that, as `mutant` / Stryker do.)
|
|
22
|
+
class TestSuiteOracle
|
|
23
|
+
# @param command [Array<String>] the test command (the runner hook)
|
|
24
|
+
# @param runner [#call, nil] `runner.call(command) -> true iff the suite
|
|
25
|
+
# passed`. Defaults to shelling out via `system`.
|
|
26
|
+
def initialize(command:, runner: nil)
|
|
27
|
+
@command = command
|
|
28
|
+
@runner = runner || method(:shell_run)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The baseline: the suite must pass on clean code, else "a mutant survived"
|
|
32
|
+
# is meaningless (every mutant would look killed, or none would). Run once
|
|
33
|
+
# before measuring.
|
|
34
|
+
def green?
|
|
35
|
+
@runner.call(@command)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Killed iff the mutant turns the suite red. Restores `original` afterward.
|
|
39
|
+
# @param path [String] the file to (temporarily) overwrite with the mutant
|
|
40
|
+
# @param original [String] the clean bytes to restore
|
|
41
|
+
# @param mutant_source [String] the mutated bytes to test against
|
|
42
|
+
def killed?(path:, original:, mutant_source:)
|
|
43
|
+
File.write(path, mutant_source)
|
|
44
|
+
!@runner.call(@command)
|
|
45
|
+
ensure
|
|
46
|
+
File.write(path, original)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Run the suite with Bundler's environment stripped, so a `bundle exec`
|
|
52
|
+
# test command resolves the **target** project's Gemfile — not whatever
|
|
53
|
+
# bundle Rigor itself was launched under. Running Rigor via `bundle exec`
|
|
54
|
+
# leaks `RUBYOPT=-rbundler/setup` + `GEM_HOME` / `BUNDLE_*` into a plain
|
|
55
|
+
# `system` subprocess, which then resolves the target's Gemfile against
|
|
56
|
+
# Rigor's gems and fails — so a green suite looks red and the run aborts.
|
|
57
|
+
# `with_unbundled_env` restores the pre-bundler env (a bare `env -u
|
|
58
|
+
# BUNDLE_GEMFILE` is not enough — the `BUNDLER_ORIG_*` preservers defeat
|
|
59
|
+
# it). Found validating ADR-70 on real projects (2026-06-17).
|
|
60
|
+
def shell_run(command)
|
|
61
|
+
run = -> { system(*command, out: File::NULL, err: File::NULL) }
|
|
62
|
+
return run.call unless defined?(Bundler) && Bundler.respond_to?(:with_unbundled_env)
|
|
63
|
+
|
|
64
|
+
Bundler.with_unbundled_env(&run)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# Classifies each configured `signature_paths:` entry by what it
|
|
5
|
+
# actually contributes to the RBS environment, so a caller can warn
|
|
6
|
+
# when a configured path resolves to nothing.
|
|
7
|
+
#
|
|
8
|
+
# The failure this guards against is silent. {Environment::RbsLoader}
|
|
9
|
+
# `add`s a `signature_paths:` entry only when `path.directory?`, and
|
|
10
|
+
# only the `.rbs` files under it carry signatures — so a typo'd or
|
|
11
|
+
# moved path (or a directory holding no `.rbs`) loads zero signatures
|
|
12
|
+
# with no trace on stderr or in the run summary. The downstream symptom
|
|
13
|
+
# is the most authoritative diagnostics: every call into the extensions
|
|
14
|
+
# the missing RBS was meant to describe fires `call.undefined-method` at
|
|
15
|
+
# `evidence_tier: high`. A one-character path typo can manufacture
|
|
16
|
+
# hundreds of plausible-looking false positives; surfacing the empty
|
|
17
|
+
# entry makes the real cause visible.
|
|
18
|
+
#
|
|
19
|
+
# The audit deliberately mirrors the loader's own acceptance test
|
|
20
|
+
# (`path.directory?` + a recursive `**/*.rbs` glob) so a `:ok` verdict
|
|
21
|
+
# means the loader did load from it and a warning means it did not.
|
|
22
|
+
module SignaturePathAudit
|
|
23
|
+
# One configured `signature_paths:` entry's resolution status.
|
|
24
|
+
#
|
|
25
|
+
# `status` is one of:
|
|
26
|
+
# - `:ok` — a directory containing at least one `.rbs`.
|
|
27
|
+
# - `:missing` — the path does not exist.
|
|
28
|
+
# - `:not_directory` — the path exists but is not a directory (the
|
|
29
|
+
# loader only `add`s directories, so a `.rbs` file passed directly
|
|
30
|
+
# is silently ignored).
|
|
31
|
+
# - `:empty` — a directory with no `.rbs` file (recursive).
|
|
32
|
+
Entry = Data.define(:path, :status, :rbs_file_count) do
|
|
33
|
+
def ok?
|
|
34
|
+
status == :ok
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def warning?
|
|
38
|
+
!ok?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# One-line, human-facing reason. The wording matches the loader's
|
|
42
|
+
# actual behaviour ("loaded nothing from it") rather than the
|
|
43
|
+
# filesystem error, so the message points at the consequence.
|
|
44
|
+
def message
|
|
45
|
+
case status
|
|
46
|
+
when :missing
|
|
47
|
+
"signature_paths: #{path.inspect} does not exist (no signatures loaded from it)"
|
|
48
|
+
when :not_directory
|
|
49
|
+
"signature_paths: #{path.inspect} is not a directory (no signatures loaded from it)"
|
|
50
|
+
when :empty
|
|
51
|
+
"signature_paths: #{path.inspect} matched 0 signature files"
|
|
52
|
+
else
|
|
53
|
+
"signature_paths: #{path.inspect} loaded #{rbs_file_count} signature file(s)"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{ "path" => path, "status" => status.to_s, "rbs_file_count" => rbs_file_count, "message" => message }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Audits each configured entry. `signature_paths` is the
|
|
63
|
+
# {Configuration#signature_paths} array (absolute paths, already
|
|
64
|
+
# resolved against the config file's directory). Pass `nil` — the
|
|
65
|
+
# unset default, where Rigor auto-detects `<root>/sig` — to get an
|
|
66
|
+
# empty result: an absent auto-detected `sig/` is a normal setup, not
|
|
67
|
+
# a misconfiguration, so it is never audited.
|
|
68
|
+
#
|
|
69
|
+
# @param signature_paths [Array<String, Pathname>, nil]
|
|
70
|
+
# @return [Array<Entry>]
|
|
71
|
+
def self.audit(signature_paths)
|
|
72
|
+
Array(signature_paths).map { |path| classify(path.to_s) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The subset of {audit} that resolved to nothing — the entries worth
|
|
76
|
+
# warning about.
|
|
77
|
+
#
|
|
78
|
+
# @param signature_paths [Array<String, Pathname>, nil]
|
|
79
|
+
# @return [Array<Entry>]
|
|
80
|
+
def self.warnings(signature_paths)
|
|
81
|
+
audit(signature_paths).select(&:warning?)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.classify(path)
|
|
85
|
+
return Entry.new(path: path, status: :missing, rbs_file_count: 0) unless File.exist?(path)
|
|
86
|
+
return Entry.new(path: path, status: :not_directory, rbs_file_count: 0) unless File.directory?(path)
|
|
87
|
+
|
|
88
|
+
count = Dir.glob(File.join(path, "**", "*.rbs")).size
|
|
89
|
+
Entry.new(path: path, status: count.zero? ? :empty : :ok, rbs_file_count: count)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
data/lib/rigor/version.rb
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-ask
|
|
3
|
+
description: |
|
|
4
|
+
Rigor is a niche, fast-moving Ruby type checker; its rules, flags, and type behaviour are version-specific, so what you "remember" about it is likely wrong or stale — do NOT answer from memory or guess. For ANY question about Rigor, use this skill and investigate procedurally: run `rigor docs` (handbook + manual, bundled OFFLINE and version-matched), `rigor explain` for a diagnostic id, and for the user's own code `rigor check` / `annotate` / `type-of`, then answer only from what you read. Covers: why a line is flagged or if it's a false positive; the type model (narrowing, refinements, `Dynamic`, RBS); config keys, flags, baselines; comparisons to Sorbet, Steep, mypy, PHPStan; whether it handles Rails, RSpec, or a gem; writing an RBS signature; "what is Rigor / why use it / is it right for us?". Trigger on any Rigor mention seeking understanding — even casual, comparative, or grumbling. Skip only when Rigor isn't mentioned, or it's purely "set it up / fix / reduce it for me" (→ rigor-next-steps).
|
|
5
|
+
license: MPL-2.0
|
|
6
|
+
metadata:
|
|
7
|
+
version: 0.2.0
|
|
8
|
+
homepage: https://github.com/rigortype/rigor
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Ask Rigor anything
|
|
12
|
+
|
|
13
|
+
Someone has a question about Rigor. It might be about a diagnostic, the
|
|
14
|
+
type model, a flag, how Rigor stacks up against another type checker,
|
|
15
|
+
whether Rigor can handle their framework, how to type a method — or just
|
|
16
|
+
"what is this, and should I use it?" Whatever it is, **answer from the
|
|
17
|
+
source, not from memory.**
|
|
18
|
+
|
|
19
|
+
Two things make that easy, and you have both offline:
|
|
20
|
+
|
|
21
|
+
- **Rigor's own docs ship inside the gem.** `rigor docs` serves the full
|
|
22
|
+
handbook and manual, always matching the user's installed version, no
|
|
23
|
+
network. This is the authoritative copy: a rule's exact firing
|
|
24
|
+
condition, a flag's spelling, a config default — all drift release to
|
|
25
|
+
release, and `rigor docs` is the copy that shipped with *this* install,
|
|
26
|
+
so an answer drawn from it cannot disagree with the binary they run.
|
|
27
|
+
- **Rigor can read the user's actual code.** When the question is about
|
|
28
|
+
*their* program — "why is this flagged?", "what type does Rigor see
|
|
29
|
+
here?", "how well-typed is my project?" — `rigor check` / `annotate` /
|
|
30
|
+
`type-of` / `triage` / `coverage` answer from what Rigor inferred. A
|
|
31
|
+
concrete inferred type beats any abstract explanation.
|
|
32
|
+
|
|
33
|
+
This is the user's shortcut: they only ever need to remember two skills —
|
|
34
|
+
**`rigor-next-steps`** ("what should we do next?") and **`rigor-ask`**
|
|
35
|
+
("answer this about Rigor"). They ask in plain language; *you* turn it
|
|
36
|
+
into the right lookup or analysis so they never have to remember the
|
|
37
|
+
command.
|
|
38
|
+
|
|
39
|
+
## The toolbox
|
|
40
|
+
|
|
41
|
+
Everything here is read-only and needs no network.
|
|
42
|
+
|
|
43
|
+
### Reading the docs
|
|
44
|
+
|
|
45
|
+
| Command | Use |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `rigor docs` | The offline doc index (`llms.txt`) — the map. Start here when you don't know which page. |
|
|
48
|
+
| `rigor docs --list [manual\|handbook]` | List every bundled page with its path (optionally one category). |
|
|
49
|
+
| `rigor docs <name>` | Print a page. `<name>` is a category-qualified path (`handbook/03-narrowing`), a prefixed basename (`03-narrowing`), or a unique short name (`narrowing`). Pages that exist in **both** trees (e.g. `plugins`) must be qualified — `manual/07-plugins` vs `handbook/09-plugins`. |
|
|
50
|
+
| `rigor explain <rule>` | The catalogue entry for a diagnostic id (`rigor explain call.undefined-method`) — what it means, why it fires, how to address it. |
|
|
51
|
+
|
|
52
|
+
### Grounding the answer in the user's code
|
|
53
|
+
|
|
54
|
+
| Command | Use |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `rigor check <path>` | Run the analysis. Scope it to a file or directory for a quick answer — don't analyse the whole project just to settle one question. `--format json` exposes structured fields (`receiver_type`, `method_name`, `evidence_tier`, …). |
|
|
57
|
+
| `rigor annotate <file>` | Reprint the file with the inferred type of each line in the margin — *what Rigor actually sees*. |
|
|
58
|
+
| `rigor type-of <file>:<line>:<col>` | The inferred type at one position. |
|
|
59
|
+
| `rigor triage` | Cluster the project's diagnostics by rule / receiver / method — for "what's the shape of my errors?". |
|
|
60
|
+
| `rigor coverage [--protection]` | Type / type-protection coverage — for "how well-typed is this?" and "where are the holes?". |
|
|
61
|
+
| `rigor plugins` | Which plugins are installed and enabled *here* — the honest answer to "does Rigor support <gem/framework>?". |
|
|
62
|
+
| `rigor sig-gen <path>` | Generate RBS for code — for "how do I type this?". Offer it and show the result; this project prefers sig-gen over hand-written RBS. |
|
|
63
|
+
|
|
64
|
+
## Where the answer lives
|
|
65
|
+
|
|
66
|
+
Classify the question, then go to the page(s) — and, for anything about
|
|
67
|
+
*their* code, the command(s) — that own it. When unsure where a page is,
|
|
68
|
+
`rigor docs` (the index) or `rigor docs --list handbook` routes you.
|
|
69
|
+
|
|
70
|
+
| The question is about… | Go to |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| **A specific diagnostic** — "why is this flagged?", "what does this error mean?", "is this a false positive?" | `rigor explain <rule>`, then `rigor docs diagnostics`. If it's *their* code, also `rigor annotate <file>` / `rigor type-of` to see the inferred types the rule fired on. |
|
|
73
|
+
| **The type model / a concept** — narrowing, refinements, tuple & hash shapes, `Dynamic`, RBS interop, lightweight HKT | The handbook: `rigor docs --list handbook`, then the chapter — `handbook/03-narrowing`, `04-tuples-and-shapes`, `07-rbs-and-extended`, `12-lightweight-hkt`, … |
|
|
74
|
+
| **Operating Rigor** — a config key, CLI flag, baseline, plugins, CI, caching | The manual: `rigor docs configuration`, `cli-reference`, `baseline`, `manual/07-plugins`, `ci`, `caching`, `troubleshooting`. |
|
|
75
|
+
| **How Rigor compares to another tool** — Sorbet, Steep, RBS, TypeScript, mypy, PHPStan, TypeProf, Go, Rust, Java/C# | The chapter/appendix written for exactly that: `handbook/10-sorbet`, `appendix-steep`, `appendix-typescript`, `appendix-mypy`, `appendix-phpstan`, `appendix-typeprof`, `appendix-rust`, `appendix-go`, `appendix-java-csharp` (`rigor docs --list handbook` shows them all). |
|
|
76
|
+
| **Whether Rigor can do X** — generics, Rails, RSpec, a specific gem, concurrency | The handbook for the language feature; for framework/gem support, **`rigor plugins`** (what's actually available in *this* install) plus the per-plugin page `rigor docs rigor-<gem>` (e.g. `rigor docs rigor-sidekiq`) and the catalogue `rigor docs --list manual`. |
|
|
77
|
+
| **Writing a type / RBS** — "how do I type this?", an annotation, a signature | Handbook `07-rbs-and-extended` + `11-sig-gen`; manual `rbs-extended-annotations`. Then offer `rigor sig-gen <path>` to generate it (preferred over hand-RBS) and show the output. |
|
|
78
|
+
| **What Rigor is / why use it / is it right for me** | Handbook `01-getting-started` for the pitch, `handbook/02-everyday-types` for a quick mental model of the type zoo. Ground "is it right for *my* project" in a scoped `rigor check` / `rigor coverage` so they see Rigor on their real code. |
|
|
79
|
+
|
|
80
|
+
## Answer from the page, name the page
|
|
81
|
+
|
|
82
|
+
Quote or paraphrase the relevant passage and **say which page you drew
|
|
83
|
+
from** (e.g. "per `rigor docs handbook/03-narrowing` …"), so the user can
|
|
84
|
+
re-read it with the same command. Prefer the doc's own wording over a
|
|
85
|
+
remembered approximation. When you ran a command against their code, show
|
|
86
|
+
the relevant line of output — a concrete inferred type is more convincing
|
|
87
|
+
than prose, and it proves the answer rather than asserting it.
|
|
88
|
+
|
|
89
|
+
## When the question is really "do X for me"
|
|
90
|
+
|
|
91
|
+
Some questions are a task in disguise: *"how do I get Rigor into CI?"*,
|
|
92
|
+
*"how do I shrink this baseline?"*, *"how do I set Rigor up here?"* The
|
|
93
|
+
useful reply is **short**: orient the user — what the thing is, the one
|
|
94
|
+
decision that actually matters, the rough shape of it — then **hand the
|
|
95
|
+
doing to the skill built for it.** Resist pasting the full procedure
|
|
96
|
+
inline (the entire CI workflow YAML, the whole baseline-reduction loop):
|
|
97
|
+
that skill owns the steps, keeps them correct, and updates as the tool
|
|
98
|
+
moves, so duplicating them here only bloats the answer and drifts out of
|
|
99
|
+
date. The line is *explaining* the thing (yours) versus *wiring it in*
|
|
100
|
+
(the setup skill's).
|
|
101
|
+
|
|
102
|
+
A good hand-off is two or three sentences of orientation plus the pointer:
|
|
103
|
+
|
|
104
|
+
- setup / "what next?" → **`rigor-next-steps`** (it probes the project and routes)
|
|
105
|
+
- CI → **`rigor-ci-setup`** · editor → **`rigor-editor-setup`** · MCP agent → **`rigor-mcp-setup`**
|
|
106
|
+
- baseline reduction → **`rigor-baseline-reduce`** · coverage holes → **`rigor-protection-uplift`**
|
|
107
|
+
- a missing gem/DSL → **`rigor-plugin-author`** · monkey-patch clusters → **`rigor-monkeypatch-resolve`**
|
|
108
|
+
|
|
109
|
+
When in doubt, give less and point — it respects the user's "two skills
|
|
110
|
+
to remember" promise and keeps each answer to the part only `rigor-ask`
|
|
111
|
+
can give.
|
|
112
|
+
|
|
113
|
+
## If the docs don't cover it
|
|
114
|
+
|
|
115
|
+
The bundled set is the **drive-Rigor** corpus (manual + handbook). The
|
|
116
|
+
normative **type specification**, the internal spec, and the ADRs are
|
|
117
|
+
contributor-facing and stay web-only — they are *not* in `rigor docs`; if
|
|
118
|
+
a question genuinely needs them, say so and point at
|
|
119
|
+
<https://rigor.typedduck.fail/llms.txt> rather than guessing.
|
|
120
|
+
|
|
121
|
+
But before you defer, remember Rigor installs from RubyGems **with its
|
|
122
|
+
full source** — the per-plugin pages under the gem's `docs/manual/plugins/`,
|
|
123
|
+
the analyzer and plugin code under `lib/`. For a detail no doc page spells
|
|
124
|
+
out (a plugin's exact rule, a default baked into the code), reading the
|
|
125
|
+
bundled file directly is a perfectly good way to ground the answer, and
|
|
126
|
+
beats a guess. The rule that never bends: **never invent a flag, rule id,
|
|
127
|
+
config key, behaviour, or command output** — read the page, read the
|
|
128
|
+
source, or run the command, and quote only output you actually saw. A
|
|
129
|
+
confident wrong answer about a type checker is worse than "let me check."
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
**A diagnostic on their code** — *"Why is Rigor flagging `s.lenght`?"*
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
rigor explain call.undefined-method # what the rule means and why it fires
|
|
137
|
+
rigor annotate demo.rb # the inferred type of `s` on that line
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Answer from both: Rigor inferred a concrete `String` receiver for `s`,
|
|
141
|
+
and `String` has no `lenght` (a typo for `length`) — grounded in what
|
|
142
|
+
`annotate` showed, not in a guess.
|
|
143
|
+
|
|
144
|
+
**A comparison** — *"How is Rigor different from Sorbet?"*
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
rigor docs handbook/10-sorbet # the chapter written for this
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Answer from the chapter's framing (RBS-superset, gradual `Dynamic`,
|
|
151
|
+
inference-first) rather than a remembered summary, and name it so they
|
|
152
|
+
can read on.
|
|
153
|
+
|
|
154
|
+
**A capability** — *"Does Rigor understand our Sidekiq workers?"*
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
rigor plugins # is rigor-sidekiq enabled in THIS project?
|
|
158
|
+
rigor docs rigor-sidekiq # the per-plugin page: what it teaches Rigor
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Answer from what's actually installed, plus — if useful — a scoped
|
|
162
|
+
`rigor check app/workers` so they see Rigor on their real workers.
|
|
163
|
+
|
|
164
|
+
**Authoring** — *"How do I type this method?"*
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
rigor sig-gen path/to/file.rb # generate the RBS, show it
|
|
168
|
+
rigor docs handbook/07-rbs-and-extended
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Generate it, show the signature, and explain it from the handbook —
|
|
172
|
+
preferring sig-gen's output over hand-written RBS.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-doctor
|
|
3
|
+
description: |
|
|
4
|
+
Validate that a project's Rigor setup is actually healthy — config parses with no silently-inert values, every configured plugin loads, the baseline is not stale, and the bundled paths resolve — by running Rigor's existing validators and interpreting them. Triggers: "is my Rigor setup correct?", "check my rigor config", "rigor diagnostics look wrong / suspicious", "validate rigor setup", "why is rigor reporting nothing / everything?". NOT for first-time setup (use rigor-project-init) or for working real diagnostics down (use rigor-baseline-reduce).
|
|
5
|
+
license: MPL-2.0
|
|
6
|
+
metadata:
|
|
7
|
+
version: 0.1.0
|
|
8
|
+
homepage: https://github.com/rigortype/rigor
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Rigor Doctor
|
|
12
|
+
|
|
13
|
+
`rigor skill describe` reports what *exists* (presence checks). This skill
|
|
14
|
+
goes a level deeper: it *runs* Rigor's validators to confirm the setup is
|
|
15
|
+
actually working — the difference between "a `.rigor.yml` is present" and
|
|
16
|
+
"it parses, loads its plugins, and analyses the right files." Use it when
|
|
17
|
+
the diagnostics look wrong (suspiciously zero, or suspiciously many) or
|
|
18
|
+
after editing the config.
|
|
19
|
+
|
|
20
|
+
It needs **no special command** — it orchestrates checks Rigor already
|
|
21
|
+
ships and interprets the results.
|
|
22
|
+
|
|
23
|
+
## Checks
|
|
24
|
+
|
|
25
|
+
### 1. Config resolves with nothing silently inert
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
rigor check --format json # read the `config_warnings` array
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`rigor check` audits the config and emits `config_warnings` for the typo
|
|
32
|
+
class whose only symptom is a confusing downstream error: a
|
|
33
|
+
`signature_paths:` that is missing / not a directory / holds no `.rbs`
|
|
34
|
+
(which would turn every covered call into a false `call.undefined-method`
|
|
35
|
+
at `evidence_tier: high`), a `libraries:` name RBS does not recognise, a
|
|
36
|
+
`disable:` / `severity_overrides:` id naming no real rule, or a missing
|
|
37
|
+
`bundler` / `rbs_collection` path. **Each warning here is a real
|
|
38
|
+
misconfiguration — fix it.** None appearing is the healthy state.
|
|
39
|
+
|
|
40
|
+
### 2. Every configured plugin loads
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
rigor plugins --strict
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Reports the activation status of each plugin in `plugins:`; `--strict`
|
|
47
|
+
exits non-zero on any failure. A failure is usually a misspelled id or a
|
|
48
|
+
plugin whose `signature_paths:` did not resolve. Fix it, or the plugin's
|
|
49
|
+
type knowledge is silently absent.
|
|
50
|
+
|
|
51
|
+
### 3. The baseline is not stale (if one exists)
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
rigor baseline drift
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Shows whether the live diagnostics have drifted from `.rigor-baseline.yml`
|
|
58
|
+
— entries the baseline ignores that no longer occur (safe to prune) and
|
|
59
|
+
new diagnostics outside the envelope. A large drift means the baseline
|
|
60
|
+
needs regenerating (often after an upgrade — see `rigor-upgrade`).
|
|
61
|
+
|
|
62
|
+
### 4. The analysis is actually seeing your code
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
rigor check --format json # check the "Ruby source files" count
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If the file count is `0` or far below your project size, `paths:` /
|
|
69
|
+
`exclude:` are mis-scoped, or the command is running from the wrong
|
|
70
|
+
directory. The analysis is only as good as the files it reads.
|
|
71
|
+
|
|
72
|
+
## Interpreting the result
|
|
73
|
+
|
|
74
|
+
- **All clean** → the setup is healthy; any diagnostics are about the
|
|
75
|
+
code, not the configuration. Move on to `rigor-baseline-reduce` or
|
|
76
|
+
`rigor-protection-uplift`.
|
|
77
|
+
- **A `config_warning` or a plugin failure** → that is the real problem;
|
|
78
|
+
fixing it usually resolves a whole cluster of confusing downstream
|
|
79
|
+
diagnostics at once.
|
|
80
|
+
|
|
81
|
+
For deeper symptoms (hover shows `untyped` everywhere, completion empty,
|
|
82
|
+
LSP silent) see the manual's troubleshooting:
|
|
83
|
+
<https://github.com/rigortype/rigor/blob/master/docs/manual/13-troubleshooting.md>
|
|
84
|
+
|
|
85
|
+
## Next step
|
|
86
|
+
|
|
87
|
+
Re-run `rigor skill describe` for the recommended next move.
|