rigortype 0.1.12 → 0.1.13
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/lib/rigor/analysis/check_rules.rb +96 -3
- data/lib/rigor/cli/skill_command.rb +170 -0
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +3 -0
- data/lib/rigor/scope.rb +14 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/scope.rbs +1 -0
- data/skills/rigor-baseline-reduce/SKILL.md +100 -0
- data/skills/rigor-baseline-reduce/references/01-classify.md +107 -0
- data/skills/rigor-baseline-reduce/references/02-fix-or-suppress.md +133 -0
- data/skills/rigor-plugin-author/SKILL.md +95 -0
- data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +195 -0
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +155 -0
- data/skills/rigor-plugin-author/references/03-test-and-ship.md +163 -0
- data/skills/rigor-project-init/SKILL.md +129 -0
- data/skills/rigor-project-init/references/01-detect.md +101 -0
- data/skills/rigor-project-init/references/02-configure.md +185 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +168 -0
- data/skills/rigor-project-init/references/04-sig-uplift.md +171 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f7d66cf2c7a6c883fbbab59a805b1d1bca62867022e42d9b68d0b788525e831
|
|
4
|
+
data.tar.gz: c327399cf8c239da46f383de404cd15843d8ce220b5538f1905d72f4e082a59f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a69935a6c878eba22f3050041d044855b9f98eefa8dc3ab55bd33ca2ecce12efc5abffb2dd0fbc57edfd7736fe5ee4eb985694e4fbcddbc63f2e231bf48483a4
|
|
7
|
+
data.tar.gz: 58839858c8a5adcf8db74ef9d8c5ed2999c4459854a85c330b107ce3a933d08cb024cacc6c1303c638ab12ae1f5bc6559c14922ac13d17f41a5609631220fc28
|
|
@@ -57,6 +57,7 @@ module Rigor
|
|
|
57
57
|
# system; new rules MUST register here so user configuration
|
|
58
58
|
# can refer to them.
|
|
59
59
|
RULE_UNDEFINED_METHOD = "call.undefined-method"
|
|
60
|
+
RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
|
|
60
61
|
RULE_WRONG_ARITY = "call.wrong-arity"
|
|
61
62
|
RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
|
|
62
63
|
RULE_NIL_RECEIVER = "call.possible-nil-receiver"
|
|
@@ -72,6 +73,7 @@ module Rigor
|
|
|
72
73
|
|
|
73
74
|
ALL_RULES = [
|
|
74
75
|
RULE_UNDEFINED_METHOD,
|
|
76
|
+
RULE_UNRESOLVED_TOPLEVEL,
|
|
75
77
|
RULE_WRONG_ARITY,
|
|
76
78
|
RULE_ARGUMENT_TYPE,
|
|
77
79
|
RULE_NIL_RECEIVER,
|
|
@@ -162,6 +164,7 @@ module Rigor
|
|
|
162
164
|
def call_node_diagnostics(path, node, scope_index)
|
|
163
165
|
[
|
|
164
166
|
undefined_method_diagnostic(path, node, scope_index),
|
|
167
|
+
unresolved_toplevel_diagnostic(path, node, scope_index),
|
|
165
168
|
wrong_arity_diagnostic(path, node, scope_index),
|
|
166
169
|
argument_type_diagnostic(path, node, scope_index),
|
|
167
170
|
nil_receiver_diagnostic(path, node, scope_index),
|
|
@@ -365,10 +368,14 @@ module Rigor
|
|
|
365
368
|
return nil if open_receiver?(class_name, scope)
|
|
366
369
|
|
|
367
370
|
# Slice 7 phase 12 — suppress when the user has
|
|
368
|
-
# declared the method in source (
|
|
369
|
-
# `
|
|
371
|
+
# declared the method in source (`def` /
|
|
372
|
+
# `define_method`) OR in a `pre_eval:` monkey-patch
|
|
373
|
+
# file (ADR-17). Both paths are project-side method
|
|
374
|
+
# contributions the dispatcher already resolved; the
|
|
375
|
+
# rule must not surface a false `undefined-method`
|
|
376
|
+
# for them.
|
|
370
377
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
371
|
-
return nil if
|
|
378
|
+
return nil if source_declared_method?(scope, class_name, call_node.name, kind)
|
|
372
379
|
|
|
373
380
|
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
374
381
|
|
|
@@ -404,6 +411,92 @@ module Rigor
|
|
|
404
411
|
scope.environment.rbs_module?(receiver_type.class_name)
|
|
405
412
|
end
|
|
406
413
|
|
|
414
|
+
# Combined suppression probe for `undefined-method` /
|
|
415
|
+
# `unresolved-toplevel`. Returns true when the method is
|
|
416
|
+
# declared by any project-side contributor the dispatcher
|
|
417
|
+
# already resolves: an in-source `def` / `define_method`
|
|
418
|
+
# (`scope.discovered_method?`) OR an ADR-17 `pre_eval:`
|
|
419
|
+
# monkey-patch (`Environment#project_patched_methods`).
|
|
420
|
+
# Both paths sit at the same dispatcher precedence; the
|
|
421
|
+
# check must hold them together so neither rule fires a
|
|
422
|
+
# false positive.
|
|
423
|
+
def source_declared_method?(scope, class_name, method_name, kind)
|
|
424
|
+
return true if scope.discovered_method?(class_name, method_name, kind)
|
|
425
|
+
|
|
426
|
+
project_patched_method?(scope, class_name, method_name, kind)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# ADR-17 § "Inference contract" — consults
|
|
430
|
+
# `Environment#project_patched_methods` so a `def` declared
|
|
431
|
+
# in a `pre_eval:` file suppresses the diagnostic at the
|
|
432
|
+
# same dispatcher precedence the registry holds for type
|
|
433
|
+
# inference (between plugins and dependency-source).
|
|
434
|
+
# Returns false when the environment carries no registry
|
|
435
|
+
# (legacy path) or the lookup misses.
|
|
436
|
+
def project_patched_method?(scope, class_name, method_name, kind)
|
|
437
|
+
environment = scope.environment
|
|
438
|
+
registry = environment&.project_patched_methods
|
|
439
|
+
return false if registry.nil? || registry.empty?
|
|
440
|
+
|
|
441
|
+
!registry.lookup(class_name: class_name.to_s, method_name: method_name.to_sym, kind: kind).nil?
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# ADR-34 — `call.unresolved-toplevel`. Fires on an
|
|
445
|
+
# implicit-self call (no explicit receiver) at toplevel
|
|
446
|
+
# scope (`scope.toplevel?`, i.e. outside any class /
|
|
447
|
+
# module body) whose name does not resolve against:
|
|
448
|
+
#
|
|
449
|
+
# 1. A same-file toplevel `def` via
|
|
450
|
+
# {Scope#top_level_def_for}.
|
|
451
|
+
# 2. The ADR-17 `ProjectPatchedMethods` registry under
|
|
452
|
+
# `(Object, name, :instance)` — projects declare
|
|
453
|
+
# their toplevel-injecting monkey-patches in
|
|
454
|
+
# `.rigor.yml`'s `pre_eval:` array as the canonical
|
|
455
|
+
# opt-out per ADR-34 WD2.
|
|
456
|
+
# 3. The standard `Kernel` / `Object` private-method
|
|
457
|
+
# surface (`puts`, `p`, `require`, `loop`, `raise`,
|
|
458
|
+
# …) drawn from the loaded RBS environment.
|
|
459
|
+
#
|
|
460
|
+
# The rule deliberately does NOT generalise to
|
|
461
|
+
# implicit-self calls inside `def` / `class` / `module`
|
|
462
|
+
# bodies — ADR-24 WD3's lenient-on-unresolved default
|
|
463
|
+
# stays in force there. ADR-24 WD4's gated class-body
|
|
464
|
+
# diagnostic is a separate decision this ADR does not
|
|
465
|
+
# open.
|
|
466
|
+
#
|
|
467
|
+
# Authored severity is `:warning`; the severity profile
|
|
468
|
+
# remaps it (`strict` → `:error`, `balanced` →
|
|
469
|
+
# `:warning`, `lenient` → `:off` / suppressed).
|
|
470
|
+
def unresolved_toplevel_diagnostic(path, call_node, scope_index)
|
|
471
|
+
return nil unless call_node.receiver.nil?
|
|
472
|
+
|
|
473
|
+
scope = scope_index[call_node]
|
|
474
|
+
return nil if scope.nil?
|
|
475
|
+
return nil unless scope.toplevel?
|
|
476
|
+
|
|
477
|
+
name = call_node.name
|
|
478
|
+
return nil if scope.top_level_def_for(name)
|
|
479
|
+
return nil if source_declared_method?(scope, "Object", name, :instance)
|
|
480
|
+
return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
|
|
481
|
+
|
|
482
|
+
build_unresolved_toplevel_diagnostic(path, call_node)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def build_unresolved_toplevel_diagnostic(path, call_node)
|
|
486
|
+
location = call_node.message_loc || call_node.location
|
|
487
|
+
Diagnostic.new(
|
|
488
|
+
path: path,
|
|
489
|
+
line: location.start_line,
|
|
490
|
+
column: location.start_column + 1,
|
|
491
|
+
message: "unresolved toplevel call to `#{call_node.name}`. " \
|
|
492
|
+
"If a project file defines `#{call_node.name}` via a toplevel " \
|
|
493
|
+
"`def` or a monkey-patch on Object/Kernel, list that file in " \
|
|
494
|
+
"`.rigor.yml`'s `pre_eval:` (ADR-17) so the analyzer sees it.",
|
|
495
|
+
severity: :warning,
|
|
496
|
+
rule: RULE_UNRESOLVED_TOPLEVEL
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
|
|
407
500
|
# Returns a qualified class name for the in-scope check.
|
|
408
501
|
# Nominal / Singleton carry a single-class identity
|
|
409
502
|
# directly. Constant projects to its value's class.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class CLI
|
|
7
|
+
# `rigor skill` — discover and print the SKILL.md files
|
|
8
|
+
# bundled with the `rigortype` gem.
|
|
9
|
+
#
|
|
10
|
+
# Rigor ships a small set of Agent Skills under `skills/` that
|
|
11
|
+
# walk an AI coding agent through onboarding (`rigor-project-init`),
|
|
12
|
+
# baseline reduction (`rigor-baseline-reduce`), and authoring a
|
|
13
|
+
# plugin (`rigor-plugin-author`). When Rigor is installed via
|
|
14
|
+
# `mise` / `gem install` / etc. the SKILL files live inside the
|
|
15
|
+
# gem checkout — the project being analysed has no copy, so an
|
|
16
|
+
# AI agent has no a priori way to find them.
|
|
17
|
+
#
|
|
18
|
+
# This command exposes the bundled skills via three subcommands:
|
|
19
|
+
#
|
|
20
|
+
# - `rigor skill list` — table of name + absolute path.
|
|
21
|
+
# - `rigor skill print <name>` — short header (paths + how to use)
|
|
22
|
+
# followed by the SKILL.md body. This
|
|
23
|
+
# is the form AI agents should call;
|
|
24
|
+
# the inline body plus the header's
|
|
25
|
+
# absolute paths together let the
|
|
26
|
+
# agent act with or without a file
|
|
27
|
+
# reading tool.
|
|
28
|
+
# - `rigor skill path <name>` — one-line absolute path, suitable
|
|
29
|
+
# as input to a Read tool.
|
|
30
|
+
#
|
|
31
|
+
# `rigor skill` with no subcommand is an alias for `list`.
|
|
32
|
+
class SkillCommand
|
|
33
|
+
USAGE = <<~USAGE
|
|
34
|
+
Usage: rigor skill <subcommand> [args]
|
|
35
|
+
|
|
36
|
+
Subcommands:
|
|
37
|
+
list List bundled skills (default when no subcommand given)
|
|
38
|
+
print <name> Print the SKILL.md body for <name> to stdout, with a header
|
|
39
|
+
path <name> Print the absolute path of the SKILL.md file for <name>
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
rigor skill list
|
|
43
|
+
rigor skill print rigor-project-init
|
|
44
|
+
rigor skill path rigor-baseline-reduce
|
|
45
|
+
USAGE
|
|
46
|
+
|
|
47
|
+
# The bundled skills live at `<gem_root>/skills/`. From
|
|
48
|
+
# `lib/rigor/cli/skill_command.rb` that is three directories up.
|
|
49
|
+
SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
|
|
50
|
+
|
|
51
|
+
def initialize(argv:, out: $stdout, err: $stderr)
|
|
52
|
+
@argv = argv
|
|
53
|
+
@out = out
|
|
54
|
+
@err = err
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @return [Integer] CLI exit status.
|
|
58
|
+
def run
|
|
59
|
+
subcommand = @argv.shift || "list"
|
|
60
|
+
|
|
61
|
+
case subcommand
|
|
62
|
+
when "list" then run_list
|
|
63
|
+
when "print" then run_print
|
|
64
|
+
when "path" then run_path
|
|
65
|
+
when "-h", "--help", "help"
|
|
66
|
+
print_usage(@out)
|
|
67
|
+
0
|
|
68
|
+
else
|
|
69
|
+
@err.puts("Unknown subcommand: #{subcommand}")
|
|
70
|
+
print_usage(@err)
|
|
71
|
+
Rigor::CLI::EXIT_USAGE
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def run_list
|
|
78
|
+
skills = discover_skills
|
|
79
|
+
if skills.empty?
|
|
80
|
+
@err.puts("No bundled skills found under #{SKILLS_ROOT}")
|
|
81
|
+
return 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
width = skills.map { |s| s.fetch(:name).length }.max
|
|
85
|
+
skills.each do |skill|
|
|
86
|
+
@out.puts(format("%-#{width}s %s", skill.fetch(:name), skill.fetch(:path)))
|
|
87
|
+
end
|
|
88
|
+
0
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_print
|
|
92
|
+
name = @argv.shift
|
|
93
|
+
return usage_error("`print` requires a skill name") if name.nil?
|
|
94
|
+
|
|
95
|
+
skill = find_skill(name)
|
|
96
|
+
return name_error(name) if skill.nil?
|
|
97
|
+
|
|
98
|
+
@out.puts(render_print_header(skill))
|
|
99
|
+
@out.puts
|
|
100
|
+
@out.write(File.read(skill.fetch(:path)))
|
|
101
|
+
0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def run_path
|
|
105
|
+
name = @argv.shift
|
|
106
|
+
return usage_error("`path` requires a skill name") if name.nil?
|
|
107
|
+
|
|
108
|
+
skill = find_skill(name)
|
|
109
|
+
return name_error(name) if skill.nil?
|
|
110
|
+
|
|
111
|
+
@out.puts(skill.fetch(:path))
|
|
112
|
+
0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The header that precedes the SKILL.md body when an agent
|
|
116
|
+
# runs `rigor skill print <name>`. Kept as `# `-prefixed
|
|
117
|
+
# comment lines so the combined output remains parseable as
|
|
118
|
+
# markdown — anything below `---` (the SKILL frontmatter
|
|
119
|
+
# marker) is unchanged.
|
|
120
|
+
def render_print_header(skill)
|
|
121
|
+
references_dir = File.join(File.dirname(skill.fetch(:path)), "references")
|
|
122
|
+
ref_line = if File.directory?(references_dir)
|
|
123
|
+
"# References: #{references_dir}/ (read referenced `references/NN-*.md` files from here)"
|
|
124
|
+
else
|
|
125
|
+
"# References: (none)"
|
|
126
|
+
end
|
|
127
|
+
<<~HEADER.chomp
|
|
128
|
+
# Rigor skill: #{skill.fetch(:name)}
|
|
129
|
+
# Source: #{skill.fetch(:path)}
|
|
130
|
+
#{ref_line}
|
|
131
|
+
#
|
|
132
|
+
# The body below is the canonical SKILL definition shipped with
|
|
133
|
+
# rigortype #{Rigor::VERSION}. Follow its instructions.
|
|
134
|
+
HEADER
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def discover_skills
|
|
138
|
+
return [] unless File.directory?(SKILLS_ROOT)
|
|
139
|
+
|
|
140
|
+
Dir.children(SKILLS_ROOT).sort.filter_map do |name|
|
|
141
|
+
skill_md = File.join(SKILLS_ROOT, name, "SKILL.md")
|
|
142
|
+
next unless File.file?(skill_md)
|
|
143
|
+
|
|
144
|
+
{ name: name, path: skill_md }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def find_skill(name)
|
|
149
|
+
discover_skills.find { |s| s.fetch(:name) == name }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def name_error(name)
|
|
153
|
+
@err.puts("Unknown skill: #{name}")
|
|
154
|
+
@err.puts("Available skills:")
|
|
155
|
+
discover_skills.each { |s| @err.puts(" #{s.fetch(:name)}") }
|
|
156
|
+
1
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def usage_error(message)
|
|
160
|
+
@err.puts(message)
|
|
161
|
+
print_usage(@err)
|
|
162
|
+
Rigor::CLI::EXIT_USAGE
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def print_usage(io)
|
|
166
|
+
io.puts(USAGE)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/lib/rigor/cli.rb
CHANGED
|
@@ -33,7 +33,8 @@ module Rigor
|
|
|
33
33
|
"triage" => :run_triage,
|
|
34
34
|
"coverage" => :run_coverage,
|
|
35
35
|
"plugins" => :run_plugins,
|
|
36
|
-
"playground" => :run_playground
|
|
36
|
+
"playground" => :run_playground,
|
|
37
|
+
"skill" => :run_skill
|
|
37
38
|
}.freeze
|
|
38
39
|
|
|
39
40
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -635,6 +636,12 @@ module Rigor
|
|
|
635
636
|
Rigor::CLI::PlaygroundCommand.new(@argv[1..], @out, @err).run
|
|
636
637
|
end
|
|
637
638
|
|
|
639
|
+
def run_skill
|
|
640
|
+
require_relative "cli/skill_command"
|
|
641
|
+
|
|
642
|
+
CLI::SkillCommand.new(argv: @argv, out: @out, err: @err).run
|
|
643
|
+
end
|
|
644
|
+
|
|
638
645
|
def write_result(result, format)
|
|
639
646
|
case format
|
|
640
647
|
when "json"
|
|
@@ -682,6 +689,7 @@ module Rigor
|
|
|
682
689
|
coverage Report type-precision coverage (precise vs Dynamic ratio)
|
|
683
690
|
plugins Report activation status of every configured plugin
|
|
684
691
|
playground Start the browser playground (requires rigor-playground gem)
|
|
692
|
+
skill List or print bundled Agent Skills (rigor-project-init, ...)
|
|
685
693
|
version Print the Rigor version
|
|
686
694
|
help Print this help
|
|
687
695
|
HELP
|
|
@@ -39,6 +39,7 @@ module Rigor
|
|
|
39
39
|
PROFILES = {
|
|
40
40
|
lenient: {
|
|
41
41
|
"call.undefined-method" => :error,
|
|
42
|
+
"call.unresolved-toplevel" => :off,
|
|
42
43
|
"call.wrong-arity" => :error,
|
|
43
44
|
"call.argument-type-mismatch" => :warning,
|
|
44
45
|
"call.possible-nil-receiver" => :warning,
|
|
@@ -54,6 +55,7 @@ module Rigor
|
|
|
54
55
|
}.freeze,
|
|
55
56
|
balanced: {
|
|
56
57
|
"call.undefined-method" => :error,
|
|
58
|
+
"call.unresolved-toplevel" => :warning,
|
|
57
59
|
"call.wrong-arity" => :error,
|
|
58
60
|
"call.argument-type-mismatch" => :error,
|
|
59
61
|
"call.possible-nil-receiver" => :error,
|
|
@@ -69,6 +71,7 @@ module Rigor
|
|
|
69
71
|
}.freeze,
|
|
70
72
|
strict: {
|
|
71
73
|
"call.undefined-method" => :error,
|
|
74
|
+
"call.unresolved-toplevel" => :error,
|
|
72
75
|
"call.wrong-arity" => :error,
|
|
73
76
|
"call.argument-type-mismatch" => :error,
|
|
74
77
|
"call.possible-nil-receiver" => :error,
|
data/lib/rigor/scope.rb
CHANGED
|
@@ -309,6 +309,20 @@ module Rigor
|
|
|
309
309
|
table[method_name.to_sym] == kind
|
|
310
310
|
end
|
|
311
311
|
|
|
312
|
+
# ADR-34 § "Decision" — predicate identifying a toplevel-shaped
|
|
313
|
+
# scope (no enclosing `class` / `module` body). True at the top
|
|
314
|
+
# of a file AND inside a top-level `def` body (since toplevel
|
|
315
|
+
# defs leave `self_type` nil per the existing scope-construction
|
|
316
|
+
# contract, mirroring how ADR-24's `adoptable_self_call_result?`
|
|
317
|
+
# also keys on `self_type.nil?` for the same context). Used by
|
|
318
|
+
# `CheckRules#unresolved_toplevel_diagnostic` to gate the
|
|
319
|
+
# `call.unresolved-toplevel` rule so it fires only outside
|
|
320
|
+
# class / module bodies, where Rails-DSL metaprogramming
|
|
321
|
+
# leniency (ADR-24 WD3 → WD4) does not apply.
|
|
322
|
+
def toplevel?
|
|
323
|
+
@self_type.nil?
|
|
324
|
+
end
|
|
325
|
+
|
|
312
326
|
def with_discovered_methods(table)
|
|
313
327
|
rebuild(discovered_methods: table)
|
|
314
328
|
end
|
data/lib/rigor/version.rb
CHANGED
data/sig/rigor/scope.rbs
CHANGED
|
@@ -58,6 +58,7 @@ module Rigor
|
|
|
58
58
|
def with_discovered_def_nodes: (Hash[String, Hash[Symbol, untyped]] table) -> Scope
|
|
59
59
|
def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
|
|
60
60
|
def top_level_def_for: (String | Symbol method_name) -> untyped?
|
|
61
|
+
def toplevel?: () -> bool
|
|
61
62
|
def with_discovered_method_visibilities: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
|
|
62
63
|
def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
|
|
63
64
|
def superclass_of: (String | Symbol class_name) -> String?
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rigor-baseline-reduce
|
|
3
|
+
description: |
|
|
4
|
+
Work a Rigor project's `.rigor-baseline.yml` down rule by rule: prioritise with `rigor triage`, sample call sites, classify each as real bug / stylistic-safe / false positive, then fix, `# rigor:disable`, or open a Rigor issue — and regenerate the baseline. Triggers: "reduce the rigor baseline", "fix some baseline diagnostics", "what rigor issue should I fix next?". NOT for first-time setup (use rigor-project-init) or authoring a plugin (use rigor-plugin-author).
|
|
5
|
+
license: MPL-2.0
|
|
6
|
+
metadata:
|
|
7
|
+
version: 0.1.0
|
|
8
|
+
homepage: https://github.com/rigortype/rigor
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Rigor Baseline Reduce
|
|
12
|
+
|
|
13
|
+
Opportunistic, session-bounded quality improvement for a project
|
|
14
|
+
that already has a `.rigor-baseline.yml`. Each session picks a rule,
|
|
15
|
+
walks its sites, and lands a mix of fixes, intentional suppressions,
|
|
16
|
+
and issue reports — then refreshes the baseline so the gains stick.
|
|
17
|
+
|
|
18
|
+
This skill is for **users improving their own project**. It uses the
|
|
19
|
+
published `rigor` executable on `PATH` and references only public CLI
|
|
20
|
+
flags and config keys.
|
|
21
|
+
|
|
22
|
+
## Phase 0 — When to use this skill
|
|
23
|
+
|
|
24
|
+
Trigger when the user says "reduce the rigor baseline", "fix some
|
|
25
|
+
baseline diagnostics", "what should I fix next in rigor?", or asks to
|
|
26
|
+
work down the diagnostics a previous onboarding parenthesised.
|
|
27
|
+
|
|
28
|
+
**Precondition: the project is in acknowledge mode.** This skill
|
|
29
|
+
operates on a `.rigor-baseline.yml` that `.rigor.yml` /
|
|
30
|
+
`.rigor.dist.yml` declares via `baseline:`. A strict-mode project
|
|
31
|
+
(`severity_profile: strict`, no baseline) has nothing for this skill
|
|
32
|
+
to reduce — its diagnostics are already all live; fix them as
|
|
33
|
+
ordinary `rigor check` output. If no baseline exists, the user wants
|
|
34
|
+
`rigor-project-init`, not this skill.
|
|
35
|
+
|
|
36
|
+
Do NOT trigger for:
|
|
37
|
+
|
|
38
|
+
- **First-time onboarding** — no `.rigor.yml` yet → `rigor-project-init`.
|
|
39
|
+
- **Writing a plugin** for the project's DSL → `rigor-plugin-author`.
|
|
40
|
+
|
|
41
|
+
## What baseline reduction is
|
|
42
|
+
|
|
43
|
+
`.rigor-baseline.yml` records `(file, rule, count)` buckets — the
|
|
44
|
+
diagnostics that existed when the project adopted Rigor. They are
|
|
45
|
+
suppressed while their count holds. Reduction means: go through them
|
|
46
|
+
deliberately, decide what each one really is, and shrink the file.
|
|
47
|
+
Three outcomes per site:
|
|
48
|
+
|
|
49
|
+
- **Real bug** — Rigor caught a genuine defect. Fix the code.
|
|
50
|
+
- **Stylistic / safe** — the static reading is worst-case-sound but
|
|
51
|
+
the site is fine in practice (an idiom the analyzer's narrowing
|
|
52
|
+
doesn't follow, a slot the project always initialises). Mark it
|
|
53
|
+
`# rigor:disable <rule>` with intent.
|
|
54
|
+
- **False positive** — Rigor is wrong; the rule should narrow
|
|
55
|
+
further. Leave it baselined and open a Rigor issue.
|
|
56
|
+
|
|
57
|
+
Shrinking the baseline is progress whichever outcome applies — the
|
|
58
|
+
file gets smaller, and "reduce the baseline by N this sprint" is a
|
|
59
|
+
trackable goal.
|
|
60
|
+
|
|
61
|
+
## Phase outline
|
|
62
|
+
|
|
63
|
+
| Phase | What | Reference |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| 1 | Prioritise — `rigor triage --format json` + `rigor baseline dump` pick the rule to work. | [`references/01-classify.md`](references/01-classify.md) |
|
|
66
|
+
| 2 | Per rule: sample 3–5 sites, classify each (real bug / stylistic / FP). | [`references/01-classify.md`](references/01-classify.md) |
|
|
67
|
+
| 3 | Act — fix, `# rigor:disable`, or open a Rigor issue. | [`references/02-fix-or-suppress.md`](references/02-fix-or-suppress.md) |
|
|
68
|
+
| 4 | Refresh — `rigor baseline drift`, then `rigor baseline regenerate`. | [`references/02-fix-or-suppress.md`](references/02-fix-or-suppress.md) |
|
|
69
|
+
|
|
70
|
+
Phases 2–4 repeat per rule until a stop condition (below) fires.
|
|
71
|
+
|
|
72
|
+
## Reading order — modules
|
|
73
|
+
|
|
74
|
+
| Module | Read | Covers |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| 1 | [`references/01-classify.md`](references/01-classify.md) | **Phases 1–2.** Priority order from the `rigor triage` hints + ascending bucket count. The sample-and-classify protocol; the three-way real-bug / stylistic / FP triage and how to tell them apart. |
|
|
77
|
+
| 2 | [`references/02-fix-or-suppress.md`](references/02-fix-or-suppress.md) | **Phases 3–4.** Acting on each classification — fix patterns, `# rigor:disable` placement (per-line vs per-file), FP escalation as a Rigor GitHub issue. Refreshing the baseline with `drift` + `regenerate`. |
|
|
78
|
+
|
|
79
|
+
## Stop conditions
|
|
80
|
+
|
|
81
|
+
End the session — do not grind indefinitely — when any of these hit:
|
|
82
|
+
|
|
83
|
+
- The user signals halt.
|
|
84
|
+
- The next rule's site count exceeds the session budget (default:
|
|
85
|
+
~20 sites). A 200-site rule is systemic; escalate it as a decision
|
|
86
|
+
(below), do not walk it site by site.
|
|
87
|
+
- A wall-time budget is reached (default: ~60 minutes).
|
|
88
|
+
|
|
89
|
+
Always finish with Phase 4 (refresh the baseline) before stopping, so
|
|
90
|
+
the landed work is recorded.
|
|
91
|
+
|
|
92
|
+
## Decision points to escalate to the user
|
|
93
|
+
|
|
94
|
+
- "This rule has 200 sites across 14 files — it looks systemic. A
|
|
95
|
+
plugin or an engine fix would clear them in bulk; want me to
|
|
96
|
+
investigate that instead of walking sites one by one?"
|
|
97
|
+
- "This file's diagnostics would be cleaner under one per-file
|
|
98
|
+
`# rigor:disable-file <rule>` than 30 per-line comments — switch?"
|
|
99
|
+
- "The diagnostic wording changed between Rigor versions and the
|
|
100
|
+
baseline no longer matches cleanly — regenerate now?"
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# 01 — Prioritise and classify
|
|
2
|
+
|
|
3
|
+
Covers **Phase 1** (pick the rule) and **Phase 2** (sample and
|
|
4
|
+
classify its sites).
|
|
5
|
+
|
|
6
|
+
## Phase 1 — Pick the rule to work
|
|
7
|
+
|
|
8
|
+
Do not walk the baseline top to bottom. Two commands decide the
|
|
9
|
+
order; run both.
|
|
10
|
+
|
|
11
|
+
### `rigor triage --format json` — the priority signal
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
rigor triage --format json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`rigor triage` runs the analysis and returns a structured summary —
|
|
18
|
+
a rule `distribution`, file `hotspots`, and a `hints` catalogue. It
|
|
19
|
+
is the deterministic diagnosis layer; use it rather than counting the
|
|
20
|
+
raw `rigor check` stream by hand. The `hints` array is the priority
|
|
21
|
+
signal — each hint has an `id`:
|
|
22
|
+
|
|
23
|
+
| Hint `id` | What it means for reduction | Priority |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `genuine-bugs` | Low-count, scattered rules — the localised bugs Rigor caught. | **Work these first.** Highest value per fix. |
|
|
26
|
+
| `systemic-file-cluster` | One file × one rule, large count. | One fix may clear many — high leverage. Or escalate as systemic. |
|
|
27
|
+
| `activesupport-core-ext`, `gem-without-rbs` | Config gaps, not code bugs. | **Not reduction work** — these are `rigor-project-init` territory (wire the RBS bundle). Flag to the user; do not walk site by site. |
|
|
28
|
+
| `project-monkey-patch` | A DSL / monkey-patch Rigor can't see. | Escalate — a `pre_eval:` entry or a plugin clears the whole cluster. |
|
|
29
|
+
| `activerecord-relation-misinference` | Likely an engine gap. | Treat sites as candidate false positives (Phase 2). |
|
|
30
|
+
|
|
31
|
+
### `rigor baseline dump --format json` — the bucket list
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
rigor baseline dump --format json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This is the authoritative list of `(file, rule, count)` buckets in
|
|
38
|
+
`.rigor-baseline.yml`. Within the priority tier the triage hints set,
|
|
39
|
+
**sort rules by ascending total count** — the smallest rules first.
|
|
40
|
+
A rule with 3 sites is either a quick win or a contained pattern;
|
|
41
|
+
either way it is finishable in one session, and finishing a rule is
|
|
42
|
+
more motivating than half-clearing a 200-site one.
|
|
43
|
+
|
|
44
|
+
So the working order is: rules flagged `genuine-bugs` first, then the
|
|
45
|
+
rest by ascending count, with config-gap hints handed back to the
|
|
46
|
+
user instead of walked.
|
|
47
|
+
|
|
48
|
+
## Phase 2 — Sample and classify
|
|
49
|
+
|
|
50
|
+
Pick the chosen rule. Surface its actual diagnostics — run
|
|
51
|
+
`rigor check` scoped to the affected files so the user sees real
|
|
52
|
+
messages, not baseline rows:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
rigor check app/models/account.rb app/services/feed_service.rb
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
(The baseline still suppresses these in a full run; naming the files
|
|
59
|
+
and reading the stream shows them. `--no-baseline` also works if you
|
|
60
|
+
want the whole project's live stream.)
|
|
61
|
+
|
|
62
|
+
From the rule's sites, **sample 3–5 distinct ones** — different
|
|
63
|
+
files, different shapes, not five copies of the same line. For each
|
|
64
|
+
sampled site, read the code and classify it. Ask the user to confirm
|
|
65
|
+
when the call is not clear-cut.
|
|
66
|
+
|
|
67
|
+
### The three classifications
|
|
68
|
+
|
|
69
|
+
**Real bug** — Rigor found a genuine defect.
|
|
70
|
+
|
|
71
|
+
- A `possible-nil-receiver` where the value genuinely can be `nil` on
|
|
72
|
+
some path and the code would crash.
|
|
73
|
+
- An `undefined-method` that is a real typo or a removed method.
|
|
74
|
+
- An argument-type mismatch that would raise or misbehave.
|
|
75
|
+
- Tell: trace the value's origin and you find a path that actually
|
|
76
|
+
produces the bad case.
|
|
77
|
+
|
|
78
|
+
**Stylistic / safe** — the static reading is worst-case-sound, but
|
|
79
|
+
the site is fine in practice.
|
|
80
|
+
|
|
81
|
+
- `T | nil` where the slot is always initialised before this point
|
|
82
|
+
by code Rigor's narrowing doesn't follow (a memoised getter, a
|
|
83
|
+
framework lifecycle guarantee).
|
|
84
|
+
- A dynamic `send` over a known-finite, known-safe tag set.
|
|
85
|
+
- An idiom repeated across dozens of files — at that scale the
|
|
86
|
+
pattern *is* the project's style.
|
|
87
|
+
- Tell: you can articulate *why* the bad case never reaches this
|
|
88
|
+
line, and that reason is a real invariant, not a hope.
|
|
89
|
+
|
|
90
|
+
**False positive** — Rigor is simply wrong; a correct analyzer would
|
|
91
|
+
not flag this site.
|
|
92
|
+
|
|
93
|
+
- The narrowing should have followed an `&.` chain or an early
|
|
94
|
+
`return`/`raise` guard and didn't.
|
|
95
|
+
- The receiver type is misinferred (e.g. an ActiveRecord relation
|
|
96
|
+
typed as `Array`).
|
|
97
|
+
- Tell: the code is correct *and* a reasonable type checker should
|
|
98
|
+
see that it is correct — the gap is in Rigor, not the code.
|
|
99
|
+
|
|
100
|
+
The distinction between "stylistic / safe" and "false positive"
|
|
101
|
+
matters: stylistic-safe sites get a `# rigor:disable` (the code stays
|
|
102
|
+
as is, the suppression is intentional); false positives get a Rigor
|
|
103
|
+
issue (the analyzer should improve). Both stay out of the count, but
|
|
104
|
+
only one of them is feedback Rigor's maintainers can act on.
|
|
105
|
+
|
|
106
|
+
Carry the per-site classifications into Phase 3
|
|
107
|
+
([`02-fix-or-suppress.md`](02-fix-or-suppress.md)).
|