rigortype 0.4.3 → 0.4.5

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.
Files changed (172) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/core_overlay/ostruct.rbs +40 -0
  4. data/docs/handbook/11-sig-gen.md +104 -15
  5. data/docs/manual/02-cli-reference.md +43 -12
  6. data/docs/manual/03-configuration.md +20 -7
  7. data/docs/manual/04-diagnostics.md +56 -3
  8. data/docs/manual/05-inspecting-types.md +2 -2
  9. data/docs/manual/06-baseline.md +1 -1
  10. data/docs/manual/07-plugins.md +3 -2
  11. data/docs/manual/08-skills.md +7 -0
  12. data/docs/manual/10-mcp-server.md +40 -4
  13. data/docs/manual/18-removing-dead-code.md +23 -2
  14. data/docs/manual/plugins/rigor-rails-routes.md +2 -2
  15. data/docs/manual/plugins/rigor-rspec.md +101 -1
  16. data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
  17. data/exe/rigor +12 -17
  18. data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
  19. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
  20. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
  21. data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
  22. data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
  23. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
  24. data/lib/rigor/analysis/check_rules.rb +274 -51
  25. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  26. data/lib/rigor/analysis/incremental.rb +61 -0
  27. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
  28. data/lib/rigor/analysis/incremental_session.rb +270 -28
  29. data/lib/rigor/analysis/path_expansion.rb +3 -1
  30. data/lib/rigor/analysis/reachability/graph.rb +111 -14
  31. data/lib/rigor/analysis/reachability/scan.rb +305 -23
  32. data/lib/rigor/analysis/rule_catalog.rb +22 -4
  33. data/lib/rigor/analysis/run_cache_key.rb +31 -7
  34. data/lib/rigor/analysis/run_cache_probe.rb +5 -3
  35. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
  36. data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
  37. data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
  38. data/lib/rigor/analysis/runner.rb +102 -72
  39. data/lib/rigor/analysis/severity_stamp.rb +45 -1
  40. data/lib/rigor/analysis/worker_session.rb +69 -2
  41. data/lib/rigor/cache/descriptor.rb +79 -12
  42. data/lib/rigor/cache/engine_source.rb +3 -2
  43. data/lib/rigor/cache/incremental_snapshot.rb +39 -5
  44. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  45. data/lib/rigor/cache/store.rb +4 -3
  46. data/lib/rigor/cli/command.rb +3 -1
  47. data/lib/rigor/cli/docs_command.rb +3 -2
  48. data/lib/rigor/cli/doctor_command.rb +1 -1
  49. data/lib/rigor/cli/explain_command.rb +3 -2
  50. data/lib/rigor/cli/mcp_command.rb +1 -1
  51. data/lib/rigor/cli/plugin_command.rb +2 -1
  52. data/lib/rigor/cli/plugins_command.rb +2 -1
  53. data/lib/rigor/cli/sig_gen_command.rb +32 -11
  54. data/lib/rigor/cli/skill_command.rb +2 -1
  55. data/lib/rigor/cli/skill_describe.rb +3 -2
  56. data/lib/rigor/cli/unused_command.rb +10 -7
  57. data/lib/rigor/cli.rb +3 -2
  58. data/lib/rigor/config_audit.rb +163 -22
  59. data/lib/rigor/configuration/dependencies.rb +19 -11
  60. data/lib/rigor/configuration/severity_profile.rb +20 -2
  61. data/lib/rigor/configuration.rb +29 -3
  62. data/lib/rigor/effects/signature_sources.rb +3 -1
  63. data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
  64. data/lib/rigor/environment/gated_signature_guard.rb +2 -1
  65. data/lib/rigor/environment/gated_signature_plan.rb +2 -1
  66. data/lib/rigor/environment/installed_gem_set.rb +2 -1
  67. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
  68. data/lib/rigor/environment/rbs_loader.rb +67 -11
  69. data/lib/rigor/inference/block_call_timing.rb +12 -1
  70. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  71. data/lib/rigor/inference/captured_locals.rb +67 -13
  72. data/lib/rigor/inference/clause_widening.rb +90 -0
  73. data/lib/rigor/inference/definer_resolution.rb +101 -25
  74. data/lib/rigor/inference/expression_typer.rb +94 -9
  75. data/lib/rigor/inference/guard_rebinding.rb +7 -2
  76. data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
  77. data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
  78. data/lib/rigor/inference/macro_block_self_type.rb +20 -6
  79. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
  80. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
  81. data/lib/rigor/inference/method_dispatcher.rb +67 -3
  82. data/lib/rigor/inference/module_function_state.rb +213 -51
  83. data/lib/rigor/inference/narrowing.rb +235 -75
  84. data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
  85. data/lib/rigor/inference/project_patched_methods.rb +168 -4
  86. data/lib/rigor/inference/project_patched_mixins.rb +162 -0
  87. data/lib/rigor/inference/project_patched_scanner.rb +51 -12
  88. data/lib/rigor/inference/project_patched_visibility.rb +214 -0
  89. data/lib/rigor/inference/project_seed_tables.rb +120 -0
  90. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
  91. data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
  92. data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
  93. data/lib/rigor/inference/repeated_or_writes.rb +414 -1
  94. data/lib/rigor/inference/return_barrier.rb +16 -0
  95. data/lib/rigor/inference/scope_indexer.rb +1061 -122
  96. data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
  97. data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
  98. data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
  99. data/lib/rigor/inference/statement_evaluator.rb +512 -150
  100. data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
  101. data/lib/rigor/lens/anchor.rb +3 -3
  102. data/lib/rigor/lens/contract_reader.rb +60 -2
  103. data/lib/rigor/lens/document.rb +4 -3
  104. data/lib/rigor/lens/file_reader.rb +452 -73
  105. data/lib/rigor/lens/members.rb +81 -17
  106. data/lib/rigor/lens/run.rb +73 -4
  107. data/lib/rigor/lens.rb +2 -1
  108. data/lib/rigor/mcp/server.rb +63 -22
  109. data/lib/rigor/plugin/base.rb +3 -2
  110. data/lib/rigor/plugin/box_probe.rb +154 -7
  111. data/lib/rigor/plugin/io_boundary.rb +42 -6
  112. data/lib/rigor/plugin/isolation.rb +4 -3
  113. data/lib/rigor/project_environment.rb +6 -3
  114. data/lib/rigor/protection/discovery_seed.rb +9 -36
  115. data/lib/rigor/protection/mutation_cache.rb +8 -2
  116. data/lib/rigor/rooted_glob.rb +27 -0
  117. data/lib/rigor/scope/discovery_index.rb +5 -0
  118. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
  119. data/lib/rigor/scope/resolution_chain.rb +40 -5
  120. data/lib/rigor/scope.rb +56 -8
  121. data/lib/rigor/sig_gen/alias_index.rb +2 -1
  122. data/lib/rigor/sig_gen/classification.rb +4 -1
  123. data/lib/rigor/sig_gen/generator.rb +167 -22
  124. data/lib/rigor/sig_gen/layout_index.rb +40 -1
  125. data/lib/rigor/sig_gen/method_candidate.rb +8 -0
  126. data/lib/rigor/sig_gen/observation_collector.rb +137 -11
  127. data/lib/rigor/sig_gen/path_mapper.rb +18 -2
  128. data/lib/rigor/sig_gen/renderer.rb +13 -155
  129. data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
  130. data/lib/rigor/sig_gen/write_reporter.rb +203 -0
  131. data/lib/rigor/sig_gen/write_result.rb +3 -1
  132. data/lib/rigor/sig_gen/writer.rb +50 -35
  133. data/lib/rigor/signature_path_audit.rb +4 -2
  134. data/lib/rigor/type/constant.rb +10 -1
  135. data/lib/rigor/version.rb +1 -1
  136. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  137. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
  138. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
  139. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
  141. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
  142. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
  143. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
  146. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
  147. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
  148. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  149. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  150. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  151. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
  152. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
  153. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
  154. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
  155. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
  156. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
  157. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
  158. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
  159. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
  160. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
  164. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
  166. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
  167. data/sig/rigor/plugin/io_boundary.rbs +5 -2
  168. data/sig/rigor/rooted_glob.rbs +7 -0
  169. data/sig/rigor/scope.rbs +4 -3
  170. data/skills/rigor-doctor/references/01-checks.md +23 -11
  171. data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
  172. metadata +16 -1
@@ -4,6 +4,7 @@ require "digest"
4
4
  require "json"
5
5
  require_relative "../value_semantics"
6
6
  require_relative "file_digest"
7
+ require_relative "../rooted_glob"
7
8
 
8
9
  module Rigor
9
10
  module Cache
@@ -82,7 +83,18 @@ module Rigor
82
83
  # an unchanged file would fold as "no possible fact" where a cold walk records one.
83
84
  # v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
84
85
  # carries it empty, so an unchanged file's conditional visibility would fold as certain.
85
- SCHEMA_VERSION = 18
86
+ # v19: #1555 / #1962 — the run-result dependency descriptor now carries a `:kind` row per analysis root,
87
+ # `pre_eval:` entry and project signature root (configured, or the auto-detected `sig`), so one that
88
+ # appears, vanishes or changes kind invalidates the warm entry. A
89
+ # pre-19 entry carries none of them and would validate fresh across exactly those edits, so cached
90
+ # entries must read as misses once and rebuild carrying the rows.
91
+ # v20: #1977 — the `--incremental` run-result slot records a `:kind` row, not an existence row, per analysis
92
+ # root, `pre_eval:` entry and project signature root. A pre-20 slot's existence row validates fresh across a
93
+ # directory replaced by a file of the same name, so it must read as a miss once and rebuild.
94
+ # v21: #1984 — the visibility walk records each class's singleton slots (`[:singleton, name]`) in the seed
95
+ # bundles' `method_visibilities`. A pre-21 bundle carries none for an unchanged file, so cached bundles must read
96
+ # as misses once and rebuild carrying them.
97
+ SCHEMA_VERSION = 21
86
98
 
87
99
  # Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
88
100
  # struct so no caller can mutate after the entry is in a Descriptor.
@@ -96,7 +108,7 @@ module Rigor
96
108
  # It MUST NOT be placed in a descriptor used as a cache KEY (its value carries machine-local,
97
109
  # per-run-nondeterministic stat data); {RbsDescriptor.build}'s env-cache `files` therefore stay
98
110
  # `:digest`, while the {Runner} run-dependency descriptor and plugin {IoBoundary} reads use `:stat`.
99
- VALID_COMPARATORS = %i[digest stat mtime exists].freeze
111
+ VALID_COMPARATORS = %i[digest stat mtime kind exists].freeze
100
112
 
101
113
  # The two `:exists` values — `File.exist?`'s answer rendered with `to_s`, which is what
102
114
  # {Descriptor#file_entry_fresh?} compares the live answer against.
@@ -163,6 +175,32 @@ module Rigor
163
175
  new(path: path, comparator: :exists, value: PRESENT)
164
176
  end
165
177
 
178
+ # The four `:kind` values, in the order {Analysis::Runner}'s path expansion asks: a directory, a regular
179
+ # file, something else that exists (a FIFO, a socket), nothing (a missing path or a dangling symlink).
180
+ # Each predicate follows symlinks, as the expansion's do.
181
+ KIND_DIRECTORY = "directory"
182
+ KIND_FILE = "file"
183
+ KIND_OTHER = "other"
184
+ KIND_ABSENT = "absent"
185
+
186
+ # #1962 — the kind row: fresh while `path` is still the same kind of thing, stale once it appears,
187
+ # vanishes, or a directory and a file trade places. It is the row for an analysis root, whose
188
+ # diagnostics (`no such file or directory`, `not a Ruby file`) answer to exactly that and to nothing
189
+ # else about the path, and for a `pre_eval:` entry and a signature root: an existence row would read a
190
+ # directory replaced by a file of the same name as fresh (ADR-45 WD1b's bound), and what the path holds
191
+ # has rows of its own (the analysed-file set the key carries, a signature root's `.rbs` rows).
192
+ def self.kind(path:)
193
+ new(path: path, comparator: :kind, value: kind_of(path))
194
+ end
195
+
196
+ def self.kind_of(path)
197
+ if File.directory?(path) then KIND_DIRECTORY
198
+ elsif File.file?(path) then KIND_FILE
199
+ elsif File.exist?(path) then KIND_OTHER
200
+ else KIND_ABSENT
201
+ end
202
+ end
203
+
166
204
  # @return whether this row records the path as absent (see {.absent}).
167
205
  def absent?
168
206
  comparator == :exists && value == ABSENT
@@ -294,7 +332,12 @@ module Rigor
294
332
  # tuple moves, so a `git checkout`, a `bundle install` or a CI cache restored over a fresh checkout
295
333
  # keeps the file rows fresh while it would move every glob signature, turning every such run into a
296
334
  # full re-analysis. `:names` also reads no stat at all, so it costs a `Dir.glob` and nothing else.
297
- VALID_MODES = %i[stat names].freeze
335
+ #
336
+ # `:kinds` (#1959) is `:names` plus each match's `File.lstat` file type: the set moves it, and so does a
337
+ # path that keeps its name and changes kind (a file `gen` replaced by a directory `gen/`, a symlinked
338
+ # directory replaced by a real one). It reads one `lstat` per match and no content, so a `touch` or an
339
+ # edit leaves it fresh.
340
+ VALID_MODES = %i[stat names kinds].freeze
298
341
 
299
342
  attr_reader :root, :pattern, :value
300
343
 
@@ -326,13 +369,15 @@ module Rigor
326
369
  # The aggregate signature the entry's `value` carries: SHA-256 over the sorted per-file rows. In the
327
370
  # default (`:stat`) mode a row is the file's `(path, size, mtime_ns, ctime_ns, inode)` tuple — NO
328
371
  # content read; in strict mode a row is `(path, content-sha256)`, restoring the pre-ADR-87 authority.
329
- # In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature.
372
+ # In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature; in
373
+ # `:kinds` mode it is the path and its {.lstat_kind}.
330
374
  # Per-file stat failures (a file vanishing between the glob and the stat) drop the row — same race
331
375
  # posture as {Descriptor#file_entry_fresh?}. `Dir.glob` returns sorted entries by default so the row
332
376
  # order — and therefore the signature — is stable.
333
377
  def self.signature_for(root:, pattern:, mode: :stat)
334
- paths = Dir.glob(File.join(root, pattern))
378
+ paths = RootedGlob.glob(root, pattern)
335
379
  return Digest::SHA256.hexdigest(paths.map { |path| "#{path}#{ROW_SEPARATOR}" }.join) if mode == :names
380
+ return kinds_signature(paths) if mode == :kinds
336
381
 
337
382
  strict = FileDigest.strict_validation?
338
383
  rows = paths.filter_map do |path|
@@ -351,10 +396,29 @@ module Rigor
351
396
  Digest::SHA256.hexdigest(rows.join)
352
397
  end
353
398
 
399
+ # #1959 — what `path` itself is, a symlink not followed: `:file`, `:directory`, `:symlink` or `:other`, and
400
+ # `:absent` for a path that vanished between the glob and the `lstat`.
401
+ def self.lstat_kind(path)
402
+ case File.lstat(path).ftype
403
+ when "file" then :file
404
+ when "directory" then :directory
405
+ when "link" then :symlink
406
+ else :other
407
+ end
408
+ rescue SystemCallError
409
+ :absent
410
+ end
411
+
412
+ def self.kinds_signature(paths)
413
+ rows = paths.map { |path| "#{path}#{FIELD_SEPARATOR}#{lstat_kind(path)}#{ROW_SEPARATOR}" }
414
+ Digest::SHA256.hexdigest(rows.join)
415
+ end
416
+ private_class_method :kinds_signature
417
+
354
418
  # ADR-87 WD2 — fresh iff re-globbing + re-stat-ing reproduces the recorded signature. Zero file-content
355
- # bytes are read in the default mode, and no stat at all in `:names`. Any failure reads as stale
356
- # (recompute), never a crash. A stored signature recorded under the opposite validation mode simply
357
- # mismatches and recomputes.
419
+ # bytes are read in the default mode, no stat at all in `:names`, and one `lstat` per match in `:kinds`.
420
+ # Any failure reads as stale (recompute), never a crash. A stored signature recorded under the opposite
421
+ # validation mode simply mismatches and recomputes.
358
422
  def self.fresh?(entry)
359
423
  signature_for(root: entry.root, pattern: entry.pattern, mode: entry.mode) == entry.value
360
424
  rescue StandardError
@@ -413,10 +477,11 @@ module Rigor
413
477
 
414
478
  # File-comparator strictness ordering. `:stat` is strictest (it carries the content digest AND a stat
415
479
  # tuple, so it validates content-authoritatively while short-circuiting on an unmoved stat); `:digest` is
416
- # deterministic across machines; `:mtime` is cheaper but local; `:exists` is the weakest signal. Ranking
417
- # `:stat` and `:digest` apart guarantees a path contributed under both never raises a value {Conflict}
418
- # (their `value` strings differ by construction) — the stricter one wins and its value is used.
419
- COMPARATOR_STRICTNESS = { stat: 3, digest: 2, mtime: 1, exists: 0 }.freeze
480
+ # deterministic across machines; `:mtime` is cheaper but local; `:kind` answers what the path is and
481
+ # `:exists` only whether it is there, the weakest signal. Ranking `:stat` and `:digest` apart guarantees a
482
+ # path contributed under both never raises a value {Conflict} (their `value` strings differ by
483
+ # construction) — the stricter one wins and its value is used.
484
+ COMPARATOR_STRICTNESS = { stat: 4, digest: 3, mtime: 2, kind: 1, exists: 0 }.freeze
420
485
  private_constant :COMPARATOR_STRICTNESS
421
486
 
422
487
  # Composes any number of descriptors into a single descriptor whose slots are the union of the inputs'
@@ -505,6 +570,8 @@ module Rigor
505
570
  FileDigest.stat_fresh?(entry.path, entry.value)
506
571
  when :mtime
507
572
  File.exist?(entry.path) && File.mtime(entry.path).to_i.to_s == entry.value
573
+ when :kind
574
+ FileEntry.kind_of(entry.path) == entry.value
508
575
  when :exists
509
576
  File.exist?(entry.path).to_s == entry.value
510
577
  else
@@ -4,6 +4,7 @@ require "digest"
4
4
 
5
5
  require_relative "../version"
6
6
  require_relative "descriptor"
7
+ require_relative "../rooted_glob"
7
8
 
8
9
  module Rigor
9
10
  module Cache
@@ -195,8 +196,8 @@ module Rigor
195
196
  # Sorted, de-duplicated absolute paths of every engine `.rb` file and {DATA_GLOBS} data file, across
196
197
  # the directories that exist. `plugins/*/sig/**/*.rbs` overlaps nothing else but is de-duplicated anyway.
197
198
  def source_files(root)
198
- sources = SOURCE_DIRECTORIES.flat_map { |relative| Dir.glob(File.join(root, relative, "**", "*.rb")) }
199
- data = DATA_GLOBS.flat_map { |pattern| Dir.glob(File.join(root, pattern)) }
199
+ sources = SOURCE_DIRECTORIES.flat_map { |relative| RootedGlob.glob(root, File.join(relative, "**", "*.rb")) }
200
+ data = DATA_GLOBS.flat_map { |pattern| RootedGlob.glob(root, pattern) }
200
201
  (sources + data).uniq.sort
201
202
  end
202
203
  end
@@ -10,6 +10,7 @@ require_relative "../analysis/shadow_harness"
10
10
  require_relative "../analysis/run_cache_key"
11
11
  require_relative "../environment"
12
12
  require_relative "../analysis/path_expansion"
13
+ require_relative "../rooted_glob"
13
14
 
14
15
  module Rigor
15
16
  module Cache
@@ -169,7 +170,19 @@ module Rigor
169
170
  # file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
170
171
  # 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
171
172
  # carries empty.
172
- SCHEMA = 37
173
+ # 38: issue #1898 — the payload gains `effect_plugin_digest`, the plugin effect-fact digest the collections were
174
+ # stamped under, which a recheck now checks before its served collections reach the envelope pass. A pre-38
175
+ # blob carries none, so the gate rejects it and the next run is cold.
176
+ # 39: issue #1984 — the visibility walk records the singleton slots (`[:singleton, name]`) and contests the ones
177
+ # two files disagree on, which a pre-39 bundle carries none of, so a warm run would answer a singleton call a
178
+ # cold run reports.
179
+ # 40: issue #1553 — the payload gains `plugin_reads`, the plugin reads each analysed file's last analysis made,
180
+ # which a recheck validates to put a reader of a changed input into the closure. A pre-40 blob carries none,
181
+ # so its readers would be served stale; the gate rejects it and the next run is cold.
182
+ # 41: issue #1583 — the payload gains `discovery_digests`, the packed digest of each file a run over path
183
+ # arguments discovered without analysing, which a recheck compares to re-baseline when one moved. A pre-41
184
+ # blob carries none; the gate rejects it and the next run is cold.
185
+ SCHEMA = 41
173
186
 
174
187
  # The persisted per-file state.
175
188
  # `cache` maps an analyzed file to its diagnostics.
@@ -224,11 +237,26 @@ module Rigor
224
237
  # Issues #796 / #794 — the run-level rows a narrowed run replays instead of re-deriving; see
225
238
  # docs/internal-spec/cache.md § "Run-level rows the snapshot carries" for why exactly these two are
226
239
  # fresh under the fingerprint and why no other `.rigor.yml`-level row may join them.
240
+ # Issue #1583 — `discovery_digests` maps each file a run over path arguments discovered but did not analyse
241
+ # (#684's widening to the configured `paths:`) to its ADR-87 packed stat-digest entry; `{}` on a run over
242
+ # the configured paths. A recheck that finds one moved, or the set changed, re-baselines.
243
+ # Issue #1898 — `effect_plugin_digest` is {Rigor::Effects::PluginFacts#digest} at the run that collected
244
+ # `effect_collections` (nil when collection was off). A plugin stamps its attributions and edges into a file's
245
+ # collection, and `effects_identity` is plugin-blind (it is compared before any plugin loads), so the session
246
+ # compares this one after the recheck has loaded them.
227
247
  Payload = Data.define(:cache, :sources, :digests, :analyzed,
228
248
  :symbol_sources, :ancestry_sources, :symbol_fingerprints,
229
249
  :missing, :class_decls, :constant_decls, :seed_bundles, :plugin_fact_digest,
230
250
  :return_summaries, :param_table,
231
- :effect_collections, :effects_identity, :run_level_rows)
251
+ :effect_collections, :effects_identity, :run_level_rows,
252
+ :effect_plugin_digest, :plugin_reads, :discovery_digests) do
253
+ # `effect_plugin_digest` defaults to nil so a payload built without it (an older caller, a spec fixture)
254
+ # reads as "unknown", which the session treats as moved. `plugin_reads` defaults to no reads, and
255
+ # `discovery_digests` to no discovery-only files.
256
+ def initialize(effect_plugin_digest: nil, plugin_reads: {}, discovery_digests: {}, **rest)
257
+ super
258
+ end
259
+ end
232
260
 
233
261
  # Issues #796 / #794 — the two run-level rows the snapshot carries. `definition_build_failures` is the
234
262
  # reported per-class detail list ({Environment::RbsLoader#definition_build_failures}); `hkt_scan_failure`
@@ -353,7 +381,7 @@ module Rigor
353
381
  # checkouts.
354
382
  def self.digest_signature_paths(signature_paths)
355
383
  globbed = Array(signature_paths).flat_map do |entry|
356
- File.directory?(entry) ? Dir.glob(File.join(entry, "**", "*.rbs")) : [entry]
384
+ File.directory?(entry) ? RootedGlob.glob(entry, "**/*.rbs") : [entry]
357
385
  end
358
386
  files = globbed.select { |path| File.file?(path) }.sort
359
387
  digest = Digest::SHA256.new
@@ -434,7 +462,10 @@ module Rigor
434
462
  param_table: data[:param_table] || {},
435
463
  effect_collections: data[:effect_collections] || {},
436
464
  effects_identity: data[:effects_identity],
437
- run_level_rows: data[:run_level_rows]
465
+ run_level_rows: data[:run_level_rows],
466
+ effect_plugin_digest: data[:effect_plugin_digest],
467
+ plugin_reads: data[:plugin_reads] || {},
468
+ discovery_digests: data.fetch(:discovery_digests, {})
438
469
  )
439
470
  end
440
471
  private :payload_from
@@ -459,7 +490,10 @@ module Rigor
459
490
  param_table: payload.param_table,
460
491
  effect_collections: payload.effect_collections,
461
492
  effects_identity: payload.effects_identity,
462
- run_level_rows: payload.run_level_rows
493
+ run_level_rows: payload.run_level_rows,
494
+ effect_plugin_digest: payload.effect_plugin_digest,
495
+ plugin_reads: payload.plugin_reads,
496
+ discovery_digests: payload.discovery_digests
463
497
  )
464
498
  # #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
465
499
  # on the edit path; see {Store::DEFLATE_LEVEL}.
@@ -5,6 +5,7 @@ require "digest"
5
5
  require_relative "descriptor"
6
6
  require_relative "engine_source"
7
7
  require_relative "file_digest"
8
+ require_relative "../rooted_glob"
8
9
 
9
10
  module Rigor
10
11
  module Cache
@@ -110,7 +111,7 @@ module Rigor
110
111
  roots.flat_map do |root|
111
112
  next [] unless root.directory?
112
113
 
113
- Dir.glob(root.join("**", "*.rbs")).map do |path|
114
+ RootedGlob.glob(root, "**/*.rbs").map do |path|
114
115
  digest = FileDigest.hexdigest(path)
115
116
  if comparator == :stat
116
117
  Descriptor::FileEntry.stat(path: path, digest: digest)
@@ -9,6 +9,7 @@ require "zlib"
9
9
 
10
10
  require_relative "../version"
11
11
  require_relative "descriptor"
12
+ require_relative "../rooted_glob"
12
13
 
13
14
  module Rigor
14
15
  module Cache
@@ -176,7 +177,7 @@ module Rigor
176
177
  subdir = File.join(root, child)
177
178
  next nil unless File.directory?(subdir)
178
179
 
179
- entries = Dir.glob(File.join(subdir, "**", "*.entry"))
180
+ entries = RootedGlob.glob(subdir, "**/*.entry")
180
181
  next nil if entries.empty?
181
182
 
182
183
  { id: child, entries: entries.size, bytes: entries.sum { |e| File.size(e) } }
@@ -709,7 +710,7 @@ module Rigor
709
710
 
710
711
  def cleanup_stale_temp_files
711
712
  cutoff = Time.now - STALE_TEMP_FILE_AGE_SECONDS
712
- Dir.glob(File.join(@root, "**", "*.tmp.*")).each do |path|
713
+ RootedGlob.glob(@root, "**/*.tmp.*").each do |path|
713
714
  next unless File.file?(path)
714
715
  next if File.mtime(path) > cutoff
715
716
 
@@ -771,7 +772,7 @@ module Rigor
771
772
  # Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
772
773
  # cache root, skipping unreadable entries.
773
774
  def collect_entry_stats
774
- Dir.glob(File.join(@root, "**", "*.entry")).filter_map do |path|
775
+ RootedGlob.glob(@root, "**/*.entry").filter_map do |path|
775
776
  stat = File.stat(path)
776
777
  producer = producer_id_for_entry(path)
777
778
  next nil if producer.nil?
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../rooted_glob"
4
+
3
5
  module Rigor
4
6
  class CLI
5
7
  # Base class for the `rigor <subcommand>` command objects.
@@ -26,7 +28,7 @@ module Rigor
26
28
  paths = []
27
29
  args.each do |arg|
28
30
  if File.directory?(arg)
29
- paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
31
+ paths.concat(RootedGlob.glob(arg, "**/*.rb"))
30
32
  elsif File.file?(arg)
31
33
  paths << arg
32
34
  else
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "command"
4
4
  require_relative "doc_links"
5
+ require_relative "../rooted_glob"
5
6
 
6
7
  module Rigor
7
8
  class CLI
@@ -186,8 +187,8 @@ module Rigor
186
187
  # `install.md` sits at the docs root (category `guide`); the rest are manual / handbook chapters.
187
188
  def discover_docs
188
189
  paths = [File.join(DOCS_ROOT, "install.md")]
189
- paths += Dir.glob(File.join(MANUAL_ROOT, "**", "*.md")) # Dir.glob is already sorted
190
- paths += Dir.glob(File.join(HANDBOOK_ROOT, "**", "*.md"))
190
+ paths += RootedGlob.glob(MANUAL_ROOT, "**/*.md") # Dir.glob is already sorted
191
+ paths += RootedGlob.glob(HANDBOOK_ROOT, "**/*.md")
191
192
  paths.select { |path| File.file?(path) }.map { |path| doc_entry(path) }
192
193
  end
193
194
 
@@ -364,7 +364,7 @@ module Rigor
364
364
  status: :warn,
365
365
  message: "#{gap.locked_gems.join(', ')} #{gap.locked_gems.one? ? 'is a dependency' : 'are dependencies'} " \
366
366
  "of this project and #{gap.plugin_gem} models it, but it is not enabled",
367
- hint: "Add `#{gap.plugin_gem}` to `.rigor.yml` `plugins:`, or leave it out deliberately."
367
+ hint: "Add `#{gap.plugin_gem}` to `.rigor.yml` `plugins:`, or decline it with an `enabled: false` entry."
368
368
  }
369
369
  end
370
370
 
@@ -10,7 +10,7 @@ require_relative "command"
10
10
  module Rigor
11
11
  class CLI
12
12
  # Executes `rigor explain <rule>`. Prints the catalog entry for one canonical rule id, a legacy alias, or a family
13
- # wildcard (`call`, `flow`, `assert`, `dump`, `def`).
13
+ # wildcard (`call`, `flow`, `def`, `assert`, `dump`, `global`, `suppression`, `static`, `effect`).
14
14
  #
15
15
  # Without arguments lists every rule's id and one-line summary.
16
16
  #
@@ -74,7 +74,8 @@ module Rigor
74
74
  end
75
75
  @out.puts("")
76
76
  @out.puts("Run `rigor explain <rule>` for the full description.")
77
- @out.puts("Family wildcards (`call`, `flow`, `assert`, `dump`, `def`) print every rule under that prefix.")
77
+ @out.puts("Family wildcards (#{Analysis::CheckRules::RULE_FAMILIES.map { |f| "`#{f}`" }.join(', ')}) " \
78
+ "print every rule under that prefix.")
78
79
  @out.puts("")
79
80
  @out.puts("`rigor sig-gen` skip reasons:")
80
81
  @out.puts("")
@@ -12,7 +12,7 @@ module Rigor
12
12
  # Starts a long-running MCP (Model Context Protocol) server over stdio. The server exposes Rigor's analysis tools as
13
13
  # MCP tool calls over a newline-delimited JSON-RPC 2.0 stream. See ADR-33.
14
14
  #
15
- # Slice 1 ships the stdio transport with seven read-only tools: rigor_check, rigor_type_of, rigor_triage,
15
+ # The stdio transport serves eight read-only tools: rigor_check, rigor_type_of, rigor_triage, rigor_lens,
16
16
  # rigor_annotate, rigor_sig_gen, rigor_explain, rigor_coverage.
17
17
  class McpCommand < Command
18
18
  USAGE = "Usage: rigor mcp [options]"
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "command"
4
+ require_relative "../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  class CLI
@@ -204,7 +205,7 @@ module Rigor
204
205
  candidate = File.join(dir, "lib", "#{plugin.fetch(:name)}.rb")
205
206
  return candidate if File.file?(candidate)
206
207
 
207
- Dir.glob(File.join(dir, "lib", "*.rb")).min
208
+ RootedGlob.glob(dir, "lib/*.rb").min
208
209
  end
209
210
 
210
211
  def docker_note
@@ -13,6 +13,7 @@ require_relative "../reflection"
13
13
  require_relative "../type/combinator"
14
14
  require_relative "plugins_renderer"
15
15
  require_relative "command"
16
+ require_relative "../rooted_glob"
16
17
 
17
18
  module Rigor
18
19
  class CLI
@@ -297,7 +298,7 @@ module Rigor
297
298
  def rbs_file_count(dir)
298
299
  return 0 unless File.directory?(dir)
299
300
 
300
- Dir.glob(File.join(dir, "**", "*.rbs")).size
301
+ RootedGlob.glob(dir, "**/*.rbs").size
301
302
  end
302
303
 
303
304
  def entry_gem_name(entry)
@@ -55,7 +55,8 @@ module Rigor
55
55
  configuration = Configuration.load(options.fetch(:config))
56
56
  paths = @argv.empty? ? configuration.paths : @argv
57
57
 
58
- observations = collect_observations(configuration, options)
58
+ observation_roots = observation_roots(configuration, options)
59
+ observations = collect_observations(configuration, options, observation_roots)
59
60
  generator = SigGen::Generator.new(configuration: configuration, paths: paths,
60
61
  observations: observations,
61
62
  include_private: options.fetch(:include_private),
@@ -68,7 +69,7 @@ module Rigor
68
69
  when :write then dispatch_write(candidates, configuration, options)
69
70
  when :check then dispatch_check(candidates, configuration, options)
70
71
  else
71
- dispatch_print_or_diff(candidates, mode, options)
72
+ dispatch_print_or_diff(candidates, mode, options, observation_roots)
72
73
  0
73
74
  end
74
75
  report_skipped(candidates, options)
@@ -219,12 +220,13 @@ module Rigor
219
220
  unresolvable.sort.each { |class_name, superclass| @err.puts(" #{class_name} < #{superclass}") }
220
221
  end
221
222
 
222
- def dispatch_print_or_diff(candidates, mode, options)
223
+ def dispatch_print_or_diff(candidates, mode, options, observation_roots)
223
224
  SigGen::Renderer.new(out: @out).render(
224
225
  candidates: candidates,
225
226
  mode: mode,
226
227
  format: options.fetch(:format),
227
- selection: options.fetch(:selection)
228
+ selection: options.fetch(:selection),
229
+ observation: observation_report(observation_roots, options)
228
230
  )
229
231
  end
230
232
 
@@ -289,11 +291,9 @@ module Rigor
289
291
  # specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
290
292
  # root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
291
293
  # naming the reason.
292
- def collect_observations(configuration, options)
294
+ def collect_observations(configuration, options, observe_paths)
293
295
  return {} if options.fetch(:params) != "observed"
294
296
 
295
- observe_paths = options.fetch(:observe)
296
- observe_paths = configuration.resolved_test_paths if observe_paths.empty?
297
297
  if observe_paths.empty?
298
298
  warn_no_test_roots(configuration)
299
299
  else
@@ -302,15 +302,36 @@ module Rigor
302
302
  SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
303
303
  end
304
304
 
305
- # A declared root that does not exist is read as empty, so name it: `sig-gen` is the only command that reads
306
- # the test roots, and without this the run looks exactly like one whose tests pass no typed arguments.
305
+ # The roots `--params=observed` reads: `--observe=PATH...` when given, else the project's test roots.
306
+ def observation_roots(configuration, options)
307
+ return [] if options.fetch(:params) != "observed"
308
+
309
+ observe_paths = options.fetch(:observe)
310
+ observe_paths.empty? ? configuration.resolved_test_paths : observe_paths
311
+ end
312
+
313
+ # #1398 — the same fact the stderr notes carry, as data for `--format=json` (and the MCP tool, whose
314
+ # stderr nobody reads). Absent under `--params=untyped`, where no root is read. Paths are spelled as
315
+ # {#observation_roots} yields them: `--observe=PATH` as typed, `test_paths:` as the configuration
316
+ # resolved them, an auto-detected `spec/` / `test/` absolute.
317
+ def observation_report(observe_paths, options)
318
+ return nil if options.fetch(:params) != "observed"
319
+
320
+ present, missing = observe_paths.uniq.partition { |path| SigGen::ObservationCollector.readable_root?(path) }
321
+ { paths: present, missing: missing }
322
+ end
323
+
324
+ # A declared root that does not exist (or is not a directory or `.rb` file) is read as empty, so name it:
325
+ # `sig-gen` is the only command that reads the test roots, and without this the run looks exactly like one
326
+ # whose tests pass no typed arguments.
307
327
  def warn_missing_test_roots(observe_paths)
308
- missing = observe_paths.reject { |path| File.exist?(path) }
328
+ missing = observe_paths.reject { |path| SigGen::ObservationCollector.readable_root?(path) }
309
329
  return if missing.empty?
310
330
 
311
331
  consequence = missing.size == observe_paths.size ? "; every parameter stays untyped" : ""
312
332
  @err.puts("rigor sig-gen: no call sites are observed from #{missing.map(&:inspect).join(', ')}, " \
313
- "which does not exist#{consequence}. Check `test_paths:` or --observe=PATH.")
333
+ "which does not exist or is not a directory or `.rb` file#{consequence}. " \
334
+ "Check `test_paths:` or --observe=PATH.")
314
335
  end
315
336
 
316
337
  def warn_no_test_roots(configuration)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "command"
4
+ require_relative "../rooted_glob"
4
5
 
5
6
  module Rigor
6
7
  class CLI
@@ -227,7 +228,7 @@ module Rigor
227
228
  return [] unless File.directory?(dir)
228
229
 
229
230
  # `Dir.glob` returns lexicographically sorted paths (Ruby 3.0+), so the `NN-` prefixes already drive read order.
230
- Dir.glob(File.join(dir, "*.md"))
231
+ RootedGlob.glob(dir, "*.md")
231
232
  end
232
233
 
233
234
  def name_error(name)
@@ -3,6 +3,7 @@
3
3
  require "yaml"
4
4
 
5
5
  require_relative "../plugin_gap_advisory"
6
+ require_relative "../rooted_glob"
6
7
 
7
8
  module Rigor
8
9
  class CLI
@@ -78,7 +79,7 @@ module Rigor
78
79
  vscode = File.join(@root, ".vscode")
79
80
  return :none unless File.directory?(vscode)
80
81
 
81
- files = Dir.glob(File.join(vscode, "*.json"))
82
+ files = RootedGlob.glob(vscode, "*.json")
82
83
  return :unwired if files.empty?
83
84
 
84
85
  files.any? { |path| file_mentions_rigor?(path) } ? :wired : :unwired
@@ -103,7 +104,7 @@ module Rigor
103
104
  end
104
105
 
105
106
  def ci_config_files
106
- files = Dir.glob(File.join(@root, ".github", "workflows", "*.{yml,yaml}"))
107
+ files = RootedGlob.glob(@root, ".github/workflows/*.{yml,yaml}")
107
108
  gitlab = File.join(@root, ".gitlab-ci.yml")
108
109
  files << gitlab if File.file?(gitlab)
109
110
  files
@@ -15,6 +15,7 @@ require_relative "../cache/store"
15
15
  require_relative "options"
16
16
  require_relative "command"
17
17
  require_relative "../project_environment"
18
+ require_relative "../rooted_glob"
18
19
 
19
20
  module Rigor
20
21
  class CLI
@@ -51,7 +52,7 @@ module Rigor
51
52
  paths = @argv.empty? ? configuration.paths : @argv
52
53
  establish_root(configuration)
53
54
  scan_cache = open_scan_cache(configuration)
54
- declarations, references, dynamic_uses, shadows = scan(paths, configuration, scan_cache)
55
+ declarations, references, dynamic_uses, shadows, ancestries = scan(paths, configuration, scan_cache)
55
56
  references.concat(signature_references(configuration))
56
57
  dynamic_uses.concat(template_mentions(declarations, scan_cache))
57
58
 
@@ -60,7 +61,7 @@ module Rigor
60
61
  references.concat(plugin_references(contribution.references))
61
62
  graph = build_graph(configuration, options, contribution,
62
63
  declarations: declarations, references: references, dynamic_uses: dynamic_uses,
63
- shadows: shadows)
64
+ shadows: shadows, ancestries: ancestries)
64
65
  emit(graph.report, options, supply: root_supply(contribution.roots, declarations))
65
66
  scan_cache.save
66
67
  0
@@ -109,9 +110,9 @@ module Rigor
109
110
  cwd == @root || inside?(cwd, @root) ? [@root] : [@root, cwd]
110
111
  end
111
112
 
112
- def build_graph(configuration, options, contribution, declarations:, references:, dynamic_uses:, shadows:)
113
+ def build_graph(configuration, options, contribution, declarations:, **scanned)
113
114
  Analysis::Reachability::Graph.new(
114
- declarations: declarations, references: references, dynamic_uses: dynamic_uses, shadows: shadows,
115
+ declarations: declarations, **scanned,
115
116
  root_fqns: root_fqns(declarations, options.fetch(:entry_points)) + contribution.roots,
116
117
  foreign: foreign_predicate(configuration)
117
118
  )
@@ -160,16 +161,18 @@ module Rigor
160
161
  # is the one the report prints; any other spelling's declarations are shadows, as a wider-corpus
161
162
  # file's are (#1732).
162
163
  def scan(paths, configuration, cache)
163
- out = { declarations: [], references: [], dynamic_uses: [], shadows: [] }
164
+ out = { declarations: [], references: [], dynamic_uses: [], shadows: [], ancestries: [] }
164
165
  file_spellings(paths, configuration).sort.each do |real, list|
165
166
  primary, analysed = list.find { |_, declared| declared } || list.first
166
167
  result = cache.serve(:scan, primary) { read_and_scan(primary, real, configuration) }
167
168
  next if result.nil?
168
169
 
169
170
  out[analysed ? :declarations : :shadows].concat(result.declarations)
171
+ # An ancestry edge is a fact about the program, not about the file's role, so one spelling carries it.
172
+ out[:ancestries].concat(result.ancestries)
170
173
  list.map(&:first).each { |file| add_spelling(out, result, file, real, primary) }
171
174
  end
172
- out.values_at(:declarations, :references, :dynamic_uses, :shadows)
175
+ out.values_at(:declarations, :references, :dynamic_uses, :shadows, :ancestries)
173
176
  end
174
177
 
175
178
  def add_spelling(out, result, file, real, primary)
@@ -289,7 +292,7 @@ module Rigor
289
292
  Array(configuration.signature_paths).flat_map do |dir|
290
293
  next [] unless File.directory?(dir)
291
294
 
292
- Dir.glob(File.join(dir, "**/*.rbs")).flat_map { |file| Analysis::Reachability::SignatureScan.call(file) }
295
+ RootedGlob.glob(dir, "**/*.rbs").flat_map { |file| Analysis::Reachability::SignatureScan.call(file) }
293
296
  end
294
297
  end
295
298
 
data/lib/rigor/cli.rb CHANGED
@@ -250,8 +250,9 @@ module Rigor
250
250
  # ships (run `rigor explain <rule>` for what
251
251
  # each one catches):
252
252
  #{rule_ids_comment}
253
- # A bare family token (`call`, `flow`,
254
- # `assert`, `dump`, `def`) wildcards every
253
+ # A bare family token (`call`, `flow`, `def`,
254
+ # `assert`, `dump`, `global`, `suppression`,
255
+ # `static`, `effect`, `pre-eval`) wildcards every
255
256
  # rule under that prefix. Legacy unprefixed
256
257
  # names (`undefined-method`, …) still
257
258
  # resolve. In-source