rigortype 0.3.8 → 0.3.9

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 (447) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
  5. data/docs/handbook/02-everyday-types.md +17 -14
  6. data/docs/handbook/03-narrowing.md +37 -4
  7. data/docs/handbook/05-methods-and-blocks.md +1 -1
  8. data/docs/handbook/06-classes.md +2 -2
  9. data/docs/handbook/07-rbs-and-extended.md +14 -1
  10. data/docs/handbook/11-sig-gen.md +72 -1
  11. data/docs/handbook/README.md +1 -1
  12. data/docs/handbook/appendix-elixir.md +2 -2
  13. data/docs/handbook/appendix-go.md +2 -2
  14. data/docs/handbook/appendix-java-csharp.md +2 -2
  15. data/docs/handbook/appendix-mypy.md +2 -2
  16. data/docs/handbook/appendix-phpstan.md +1 -1
  17. data/docs/handbook/appendix-rust.md +1 -1
  18. data/docs/handbook/appendix-type-theory.md +4 -4
  19. data/docs/handbook/appendix-typescript.md +1 -1
  20. data/docs/llms.txt +2 -0
  21. data/docs/manual/02-cli-reference.md +32 -4
  22. data/docs/manual/03-configuration.md +11 -0
  23. data/docs/manual/04-diagnostics.md +11 -0
  24. data/docs/manual/07-plugins.md +15 -5
  25. data/docs/manual/08-skills.md +23 -2
  26. data/docs/manual/11-ci.md +9 -0
  27. data/docs/manual/15-type-protection-coverage.md +8 -0
  28. data/docs/manual/16-rbs-extended-annotations.md +6 -1
  29. data/docs/manual/18-removing-dead-code.md +10 -8
  30. data/docs/manual/plugins/rigor-actionpack.md +18 -0
  31. data/docs/manual/plugins/rigor-activerecord.md +28 -0
  32. data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
  33. data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
  34. data/lib/rigor/analysis/baseline.rb +2 -2
  35. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  36. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  37. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  38. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  40. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
  42. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  43. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  44. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  46. data/lib/rigor/analysis/check_rules.rb +200 -32
  47. data/lib/rigor/analysis/crash_signature.rb +8 -24
  48. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  52. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  53. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  54. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  55. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  56. data/lib/rigor/analysis/fact_store.rb +6 -1
  57. data/lib/rigor/analysis/incremental.rb +16 -0
  58. data/lib/rigor/analysis/incremental_session.rb +81 -22
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  60. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  61. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -5
  62. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  63. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -2
  65. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  66. data/lib/rigor/analysis/result.rb +1 -3
  67. data/lib/rigor/analysis/rule_catalog.rb +4 -1
  68. data/lib/rigor/analysis/run_cache_key.rb +1 -1
  69. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  70. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  71. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  72. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +177 -26
  73. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  74. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
  75. data/lib/rigor/analysis/runner/pool_coordinator.rb +256 -88
  76. data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
  77. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  78. data/lib/rigor/analysis/runner.rb +133 -35
  79. data/lib/rigor/analysis/worker_session.rb +9 -10
  80. data/lib/rigor/bleeding_edge.rb +12 -24
  81. data/lib/rigor/builtins/hkt_builtins.rb +2 -2
  82. data/lib/rigor/builtins/imported_refinements.rb +155 -21
  83. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  84. data/lib/rigor/builtins/regex_refinement.rb +4 -4
  85. data/lib/rigor/builtins/static_return_refinements.rb +5 -6
  86. data/lib/rigor/cache/annotation_location.rb +2 -4
  87. data/lib/rigor/cache/descriptor.rb +58 -20
  88. data/lib/rigor/cache/engine_source.rb +4 -4
  89. data/lib/rigor/cache/file_digest.rb +9 -1
  90. data/lib/rigor/cache/incremental_snapshot.rb +40 -6
  91. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  92. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  93. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  94. data/lib/rigor/cache/rbs_descriptor.rb +79 -11
  95. data/lib/rigor/cache/rbs_environment.rb +8 -4
  96. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  97. data/lib/rigor/cache/store.rb +77 -32
  98. data/lib/rigor/cli/annotate_command.rb +5 -6
  99. data/lib/rigor/cli/check_command.rb +57 -10
  100. data/lib/rigor/cli/check_invocation.rb +6 -11
  101. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  102. data/lib/rigor/cli/coverage_command.rb +1 -1
  103. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  104. data/lib/rigor/cli/coverage_scan.rb +1 -5
  105. data/lib/rigor/cli/diff_command.rb +1 -1
  106. data/lib/rigor/cli/doc_links.rb +3 -3
  107. data/lib/rigor/cli/docs_command.rb +2 -2
  108. data/lib/rigor/cli/doctor_command.rb +85 -38
  109. data/lib/rigor/cli/effects_command.rb +2 -2
  110. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  111. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  112. data/lib/rigor/cli/explain_command.rb +34 -1
  113. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  114. data/lib/rigor/cli/lsp_command.rb +1 -1
  115. data/lib/rigor/cli/mcp_command.rb +1 -1
  116. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  117. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  118. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  119. data/lib/rigor/cli/plugin_command.rb +1 -1
  120. data/lib/rigor/cli/plugins_command.rb +2 -1
  121. data/lib/rigor/cli/prism_colorizer.rb +2 -2
  122. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  123. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  124. data/lib/rigor/cli/sig_gen_command.rb +2 -2
  125. data/lib/rigor/cli/skill_command.rb +1 -1
  126. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  127. data/lib/rigor/cli/skill_describe.rb +36 -35
  128. data/lib/rigor/cli/trace_command.rb +4 -4
  129. data/lib/rigor/cli/trace_renderer.rb +5 -6
  130. data/lib/rigor/cli/triage_command.rb +1 -1
  131. data/lib/rigor/cli/type_of_command.rb +10 -6
  132. data/lib/rigor/cli/type_scan_command.rb +4 -4
  133. data/lib/rigor/cli/unused_command.rb +11 -3
  134. data/lib/rigor/cli/upgrade_command.rb +1 -1
  135. data/lib/rigor/cli.rb +68 -6
  136. data/lib/rigor/config_audit.rb +30 -5
  137. data/lib/rigor/configuration/severity_profile.rb +6 -6
  138. data/lib/rigor/configuration.rb +43 -4
  139. data/lib/rigor/effects/attribution.rb +1 -3
  140. data/lib/rigor/effects/config_envelopes.rb +6 -8
  141. data/lib/rigor/effects/definition_lines.rb +25 -6
  142. data/lib/rigor/effects/effect_table.rb +0 -1
  143. data/lib/rigor/effects/entry_points.rb +0 -2
  144. data/lib/rigor/effects/envelope_check.rb +8 -8
  145. data/lib/rigor/effects/envelope_index.rb +5 -8
  146. data/lib/rigor/effects/framework_units.rb +4 -6
  147. data/lib/rigor/effects/identity.rb +4 -6
  148. data/lib/rigor/effects/inline_anchor.rb +7 -7
  149. data/lib/rigor/effects/label_intent.rb +3 -4
  150. data/lib/rigor/effects/liskov_check.rb +8 -8
  151. data/lib/rigor/effects/method_key.rb +1 -1
  152. data/lib/rigor/effects/plugin_facts.rb +4 -6
  153. data/lib/rigor/effects/propagator.rb +2 -3
  154. data/lib/rigor/effects/scanner.rb +1 -1
  155. data/lib/rigor/effects/signature_sources.rb +3 -5
  156. data/lib/rigor/effects/snapshot.rb +10 -10
  157. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  158. data/lib/rigor/effects/unit_scan.rb +10 -10
  159. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  160. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  161. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  162. data/lib/rigor/environment/class_registry.rb +3 -1
  163. data/lib/rigor/environment/failure_slot.rb +2 -2
  164. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  165. data/lib/rigor/environment/lockfile_resolver.rb +51 -4
  166. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  167. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  168. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  169. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  170. data/lib/rigor/environment/rbs_loader.rb +388 -59
  171. data/lib/rigor/environment.rb +105 -20
  172. data/lib/rigor/flow_contribution/merger.rb +0 -2
  173. data/lib/rigor/flow_contribution.rb +11 -13
  174. data/lib/rigor/inference/acceptance.rb +153 -8
  175. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  176. data/lib/rigor/inference/body_fixpoint.rb +5 -5
  177. data/lib/rigor/inference/budget_trace.rb +1 -5
  178. data/lib/rigor/inference/captured_locals.rb +2 -3
  179. data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
  180. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  181. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  182. data/lib/rigor/inference/element_read_widening.rb +168 -0
  183. data/lib/rigor/inference/expression_typer.rb +763 -143
  184. data/lib/rigor/inference/fork_map.rb +5 -7
  185. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  186. data/lib/rigor/inference/hkt_registry.rb +4 -7
  187. data/lib/rigor/inference/index_write_widening.rb +1 -5
  188. data/lib/rigor/inference/macro_block_self_type.rb +1 -4
  189. data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
  190. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  191. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  192. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  193. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  194. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -2
  195. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  197. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  198. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  199. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +61 -47
  200. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  201. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
  202. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  203. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  204. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  206. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  208. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  209. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -5
  210. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  211. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  212. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  213. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  214. data/lib/rigor/inference/method_dispatcher.rb +12 -13
  215. data/lib/rigor/inference/method_parameter_binder.rb +4 -6
  216. data/lib/rigor/inference/multi_target_binder.rb +1 -3
  217. data/lib/rigor/inference/mutation_rejoin.rb +168 -0
  218. data/lib/rigor/inference/mutation_widening.rb +100 -51
  219. data/lib/rigor/inference/narrowing.rb +299 -46
  220. data/lib/rigor/inference/optimistic_origin.rb +1 -7
  221. data/lib/rigor/inference/origin_lookup.rb +2 -3
  222. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  223. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  224. data/lib/rigor/inference/precision_scanner.rb +3 -4
  225. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  226. data/lib/rigor/inference/project_patched_scanner.rb +3 -3
  227. data/lib/rigor/inference/protection_scanner.rb +1 -2
  228. data/lib/rigor/inference/range_constant.rb +57 -0
  229. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  230. data/lib/rigor/inference/receiver_alias.rb +3 -3
  231. data/lib/rigor/inference/refinement_mutation.rb +72 -0
  232. data/lib/rigor/inference/scope_indexer.rb +688 -151
  233. data/lib/rigor/inference/statement_evaluator.rb +221 -50
  234. data/lib/rigor/inference/string_mutation.rb +60 -0
  235. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  236. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  237. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  238. data/lib/rigor/inference/version_guard.rb +28 -21
  239. data/lib/rigor/inference/void_origin.rb +3 -3
  240. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  241. data/lib/rigor/language_server/buffer_table.rb +4 -4
  242. data/lib/rigor/language_server/completion_provider.rb +1 -1
  243. data/lib/rigor/language_server/debouncer.rb +1 -1
  244. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  245. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  246. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  247. data/lib/rigor/language_server/hover_provider.rb +1 -1
  248. data/lib/rigor/language_server/hover_renderer.rb +2 -2
  249. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  250. data/lib/rigor/language_server/project_context.rb +2 -5
  251. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  252. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  253. data/lib/rigor/language_server/server.rb +9 -9
  254. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  255. data/lib/rigor/language_server/uri.rb +1 -1
  256. data/lib/rigor/plugin/base.rb +90 -21
  257. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  258. data/lib/rigor/plugin/effect_attribution.rb +7 -7
  259. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  260. data/lib/rigor/plugin/fact_store.rb +6 -6
  261. data/lib/rigor/plugin/io_boundary.rb +93 -8
  262. data/lib/rigor/plugin/isolation.rb +29 -6
  263. data/lib/rigor/plugin/loader.rb +31 -12
  264. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  265. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  266. data/lib/rigor/plugin/manifest.rb +23 -1
  267. data/lib/rigor/plugin/registry.rb +42 -12
  268. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
  269. data/lib/rigor/plugin/trust_policy.rb +2 -4
  270. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  271. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  272. data/lib/rigor/project_environment.rb +138 -0
  273. data/lib/rigor/protection/analysis_guard.rb +93 -14
  274. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  275. data/lib/rigor/protection/dependency_closure.rb +5 -8
  276. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  277. data/lib/rigor/protection/discovery_seed.rb +11 -11
  278. data/lib/rigor/protection/kill_signature.rb +2 -4
  279. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  280. data/lib/rigor/protection/mutation_cache.rb +6 -9
  281. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  282. data/lib/rigor/protection/mutator.rb +2 -1
  283. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  284. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  287. data/lib/rigor/rbs_extended.rb +61 -21
  288. data/lib/rigor/reflection/constant_path.rb +126 -0
  289. data/lib/rigor/reflection.rb +26 -10
  290. data/lib/rigor/runtime/jit.rb +6 -8
  291. data/lib/rigor/scope/discovery_index.rb +30 -0
  292. data/lib/rigor/scope.rb +203 -4
  293. data/lib/rigor/sig_gen/generator.rb +115 -22
  294. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  295. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  296. data/lib/rigor/sig_gen/observation_collector.rb +17 -15
  297. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  298. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  299. data/lib/rigor/sig_gen/renderer.rb +4 -5
  300. data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -0
  301. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  302. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  303. data/lib/rigor/sig_gen/writer.rb +23 -6
  304. data/lib/rigor/signature_path_audit.rb +153 -6
  305. data/lib/rigor/source/literals.rb +0 -23
  306. data/lib/rigor/source/node_children.rb +0 -2
  307. data/lib/rigor/source/node_locator.rb +5 -11
  308. data/lib/rigor/source/node_walker.rb +2 -5
  309. data/lib/rigor/triage/catalogue.rb +1 -3
  310. data/lib/rigor/triage.rb +3 -5
  311. data/lib/rigor/type/accepts_result.rb +24 -6
  312. data/lib/rigor/type/combinator.rb +68 -5
  313. data/lib/rigor/type/data_class.rb +2 -2
  314. data/lib/rigor/type/data_instance.rb +4 -4
  315. data/lib/rigor/type/float_range.rb +128 -0
  316. data/lib/rigor/type/hash_shape.rb +5 -5
  317. data/lib/rigor/type/integer_range.rb +13 -8
  318. data/lib/rigor/type/nominal.rb +8 -2
  319. data/lib/rigor/type/refined.rb +3 -3
  320. data/lib/rigor/type/struct_class.rb +3 -3
  321. data/lib/rigor/type/struct_instance.rb +4 -4
  322. data/lib/rigor/type.rb +1 -0
  323. data/lib/rigor/type_node/generic.rb +1 -1
  324. data/lib/rigor/type_node/range_literal.rb +25 -0
  325. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  326. data/lib/rigor/type_node.rb +1 -0
  327. data/lib/rigor/version.rb +1 -1
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  329. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  330. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  331. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -0
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  333. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  334. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  335. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +1 -0
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -30
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  338. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  339. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +28 -2
  340. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  341. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  342. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  343. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  344. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  345. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  346. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +1 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
  348. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  349. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  350. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +42 -7
  351. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +119 -14
  355. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  356. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -0
  357. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  358. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  359. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  360. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  361. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  362. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  363. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  364. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  365. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  366. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  367. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  368. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  369. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  370. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  371. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  372. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
  373. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  374. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  375. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +0 -3
  376. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  377. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  378. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  379. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  380. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  381. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +1 -0
  382. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  383. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  384. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  385. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -0
  386. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  387. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -10
  388. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
  389. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -2
  390. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -0
  395. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  396. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  397. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +128 -3
  398. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  399. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  400. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  403. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  404. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  405. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  406. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  407. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  408. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  409. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  410. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -1
  411. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +1 -0
  414. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  415. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +4 -5
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
  423. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  424. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  425. data/sig/rigor/cache.rbs +2 -3
  426. data/sig/rigor/environment.rbs +5 -0
  427. data/sig/rigor/inference.rbs +3 -10
  428. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  429. data/sig/rigor/plugin/manifest.rbs +2 -1
  430. data/sig/rigor/reflection.rbs +3 -1
  431. data/sig/rigor/scope.rbs +24 -3
  432. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
  433. data/sig/rigor/type.rbs +27 -1
  434. data/sig/rigor.rbs +1 -0
  435. data/skills/rigor-ask/SKILL.md +8 -6
  436. data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
  437. data/skills/rigor-project-init/SKILL.md +6 -3
  438. data/skills/rigor-project-init/references/02-configure.md +10 -6
  439. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  440. data/skills/rigor-type-oracle/SKILL.md +228 -0
  441. data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
  442. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  443. data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
  444. data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
  445. metadata +26 -3
  446. data/lib/rigor/cli/probe_environment.rb +0 -85
  447. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -32,14 +32,21 @@ module Rigor
32
32
  # The class-length budget is relaxed (as on `Rigor::CLI` itself) because `check` aggregates several independent
33
33
  # concerns that are clearer read together than split across micro-classes.
34
34
  class CheckCommand < Command # rubocop:disable Metrics/ClassLength
35
- # @return [Integer] CLI exit status.
35
+ # Issue #812 `--fail-on=SEVERITY` raises the exit-status bar above the default (`:error`-only).
36
+ # Orders the three severities a diagnostic can carry once it survives {SeverityStamp} (`:off` rows are
37
+ # dropped upstream and never reach a `Result`, so they are not a member here).
38
+ FAIL_ON_RANK = { info: 0, warning: 1, error: 2 }.freeze
39
+
40
+ # @return CLI exit status.
36
41
  #
37
42
  # Deferred YJIT enablement (Runtime::Jit) is armed by `CLI#dispatch` for every command, this
38
43
  # one included, before any analysis work runs.
39
- def run # rubocop:disable Metrics/AbcSize
44
+ def run # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
40
45
  # ADR-87 WD4 — parse options + resolve config WITHOUT the inference engine, so the run-cache hit probe
41
46
  # can run first. The heavy engine (`load_check_dependencies`) loads only on a miss / non-cacheable run.
42
47
  options = parse_check_options
48
+ return CLI::EXIT_USAGE unless valid_fail_on_option?(options)
49
+
43
50
  buffer = Options.resolve_buffer_binding(options, err: @err)
44
51
  return CLI::EXIT_USAGE if buffer == :usage_error
45
52
 
@@ -68,7 +75,8 @@ module Rigor
68
75
  result = apply_baseline_filter(raw_result, configuration, options)
69
76
 
70
77
  coverage = compute_coverage(runner, configuration, options)
71
- write_result(result, options.fetch(:format), coverage: coverage, config_warnings: config_warnings)
78
+ write_result(result, options.fetch(:format), coverage: coverage, config_warnings: config_warnings,
79
+ fail_on: options.fetch(:fail_on))
72
80
  emit_ci_detected_output(result, options)
73
81
  write_run_stats(result.stats) if result.stats
74
82
  write_trace_appendices
@@ -77,11 +85,34 @@ module Rigor
77
85
 
78
86
  exit_code = result.success? ? 0 : 1
79
87
  exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
88
+ exit_code = 1 if fail_on_violation?(result.diagnostics, options)
80
89
  exit_code
81
90
  end
82
91
 
83
92
  private
84
93
 
94
+ # True when `diagnostics` (already baseline-filtered and severity-resolved — the same list
95
+ # `Result#success?` reads) carries a row at or above the `--fail-on` threshold. With the default
96
+ # `:error` threshold this is exactly `!diagnostics.any?(&:error?)`, i.e. a no-op over the existing exit
97
+ # code — `--fail-on` only ever makes a run *stricter*, never masks an `:error` the old exit code caught.
98
+ def fail_on_violation?(diagnostics, options)
99
+ threshold = FAIL_ON_RANK.fetch(options.fetch(:fail_on))
100
+ diagnostics.any? { |diagnostic| FAIL_ON_RANK.fetch(diagnostic.severity, FAIL_ON_RANK[:error]) >= threshold }
101
+ end
102
+
103
+ # True when `options[:fail_on]` (an arbitrary user-typed Symbol at this point — parsing only lower-cases
104
+ # and interns it, see `parse_check_options`) is one of {FAIL_ON_RANK}'s keys; writes a usage note to
105
+ # `@err` and returns false otherwise. Validated as a plain post-parse check rather than raised from
106
+ # inside the `opts.on` block: OptionParser rescues a `ParseError` raised from a switch's own block and
107
+ # rebuilds it as a generic `"invalid argument: --fail-on=<value>"`, discarding any custom message — this
108
+ # way the usage note actually names the accepted values.
109
+ def valid_fail_on_option?(options)
110
+ return true if FAIL_ON_RANK.key?(options.fetch(:fail_on))
111
+
112
+ @err.puts("rigor: invalid --fail-on value: #{options.fetch(:fail_on)} (expected error, warning, or info)")
113
+ false
114
+ end
115
+
85
116
  # ADR-87 WD4 — attempt the boot-slimming run-cache hit. Returns the cached {Analysis::Result} (severity
86
117
  # profile applied, no stats — matching a cache-served `Runner#run`) on a hit, or nil to fall through to
87
118
  # the full engine path. Only an ordinary sequential check whose result IS cache-served the same way is
@@ -129,11 +160,13 @@ module Rigor
129
160
  # gate reads the same diagnostics the full path would.
130
161
  def finalize_cache_hit(raw_result, configuration, options, config_warnings)
131
162
  result = apply_baseline_filter(raw_result, configuration, options)
132
- write_result(result, options.fetch(:format), config_warnings: config_warnings)
163
+ write_result(result, options.fetch(:format), config_warnings: config_warnings,
164
+ fail_on: options.fetch(:fail_on))
133
165
  emit_ci_detected_output(result, options)
134
166
 
135
167
  exit_code = result.success? ? 0 : 1
136
168
  exit_code = 1 if baseline_strict_violation?(raw_result.diagnostics, configuration, options)
169
+ exit_code = 1 if fail_on_violation?(result.diagnostics, options)
137
170
  exit_code
138
171
  end
139
172
 
@@ -233,8 +266,8 @@ module Rigor
233
266
 
234
267
  result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration,
235
268
  options)
236
- write_result(result, options.fetch(:format))
237
- result.success? ? 0 : 1
269
+ write_result(result, options.fetch(:format), fail_on: options.fetch(:fail_on))
270
+ !result.success? || fail_on_violation?(result.diagnostics, options) ? 1 : 0
238
271
  end
239
272
 
240
273
  # Editor mode option B (#146) — `--incremental` plus an editor buffer. The whole project is in scope with
@@ -260,8 +293,8 @@ module Rigor
260
293
  filtered = apply_baseline_filter(
261
294
  Analysis::Result.new(diagnostics: result.diagnostics, stats: nil), configuration, options
262
295
  )
263
- write_result(filtered, options.fetch(:format))
264
- filtered.success? ? 0 : 1
296
+ write_result(filtered, options.fetch(:format), fail_on: options.fetch(:fail_on))
297
+ !filtered.success? || fail_on_violation?(filtered.diagnostics, options) ? 1 : 0
265
298
  end
266
299
 
267
300
  # ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
@@ -439,6 +472,10 @@ module Rigor
439
472
  baseline: :unset,
440
473
  # ADR-22 slice 5 — `--baseline-strict` CI gate: fail the run on any baseline drift, in either direction.
441
474
  baseline_strict: false,
475
+ # Issue #812 — `--fail-on=SEVERITY` raises the exit-status bar: the run exits non-zero when a
476
+ # diagnostic AT OR ABOVE this severity survives baseline filtering and severity resolution, not only
477
+ # on `:error` (today's default and behaviour). See `FAIL_ON_RANK`.
478
+ fail_on: :error,
442
479
  # ADR-32 WD10 carry-over — `--treat-all-as-inline-rbs` forces the `rigor-rbs-inline` plugin into the loaded
443
480
  # plugin set with `require_magic_comment: false` so a single ad-hoc `rigor check` invocation treats every
444
481
  # analysed file as inline-RBS without the user editing `.rigor.yml`. Intended for single-file / ad-hoc CI use;
@@ -508,6 +545,12 @@ module Rigor
508
545
  "fail the run on any baseline drift (CI gate)") do
509
546
  options[:baseline_strict] = true
510
547
  end
548
+ opts.on("--fail-on=SEVERITY",
549
+ "exit non-zero on a diagnostic at or above SEVERITY: error (default), warning, or info") do |value|
550
+ # Validated post-parse (`valid_fail_on_option?`, called from `run`) rather than here — see its
551
+ # comment for why an unrecognised value is not raised from inside this block.
552
+ options[:fail_on] = value.to_s.downcase.to_sym
553
+ end
511
554
  opts.on("--treat-all-as-inline-rbs",
512
555
  "force-load rigor-rbs-inline with require_magic_comment: false") do
513
556
  options[:treat_all_as_inline_rbs] = true
@@ -661,7 +704,6 @@ module Rigor
661
704
  @err.puts(" body evals: #{counts[bt::MEMO_BODY_EVALS]}")
662
705
  @err.puts(" non-stored: on-stack #{counts[bt::MEMO_REFUSE_ON_STACK]} " \
663
706
  "unroll-in-flight #{counts[bt::MEMO_REFUSE_UNROLL]} " \
664
- "consult-tainted #{counts[bt::MEMO_REFUSE_CONSULT_TAINTED]} " \
665
707
  "transient-tainted #{counts[bt::MEMO_REFUSE_TRANSIENT]}")
666
708
  write_memo_signature_table
667
709
  end
@@ -841,10 +883,15 @@ module Rigor
841
883
  format == "text" ? @out : @err
842
884
  end
843
885
 
844
- def write_result(result, format, coverage: nil, config_warnings: [])
886
+ def write_result(result, format, coverage: nil, config_warnings: [], fail_on: :error)
845
887
  case format
846
888
  when "json"
847
889
  payload = enrich_json(result.to_h)
890
+ # Issue #812 — the exit code can be stricter than `payload["success"]` (which stays the `:error`-only
891
+ # reading `Result#success?` always gave, per the other consumers already reading it); a JSON
892
+ # consumer that wants to know WHY a run exited non-zero reads this alongside it rather than
893
+ # re-deriving `--fail-on`'s value from the invocation.
894
+ payload["fail_on"] = fail_on.to_s
848
895
  payload["coverage"] = coverage_payload(coverage) if coverage
849
896
  payload["config_warnings"] = config_warnings.map(&:to_h) unless config_warnings.empty?
850
897
  @out.puts(JSON.pretty_generate(payload))
@@ -22,7 +22,7 @@ module Rigor
22
22
  # The outcome of {.attempt} — the best-effort variant. Exactly one of the two is non-nil, so a caller can never
23
23
  # read "the check could not run" as "the check ran clean": `result` is nil precisely when `error` is set.
24
24
  Outcome = Data.define(:result, :error) do
25
- # @return [Boolean] true when an analysis actually completed.
25
+ # @return true when an analysis actually completed.
26
26
  def ran?
27
27
  !result.nil?
28
28
  end
@@ -42,12 +42,9 @@ module Rigor
42
42
 
43
43
  # Runs the analysis the way `rigor check` does and returns the runner + its raw result.
44
44
  #
45
- # @param configuration [Rigor::Configuration]
46
- # @param options [Hash] at least `:no_cache`, `:explain`, `:stats`, `:workers` (see {CheckRunnerFactory.build}).
47
- # @param paths [Array<String>, nil] analysed paths; nil falls back to the configuration's `paths:`.
48
- # @param buffer [Rigor::Analysis::BufferBinding, nil]
49
- # @param cache_root [String, nil] nil falls back to the configuration's cache path.
50
- # @return [Invocation]
45
+ # @param options — at least `:no_cache`, `:explain`, `:stats`, `:workers` (see {CheckRunnerFactory.build}).
46
+ # @param paths analysed paths; nil falls back to the configuration's `paths:`.
47
+ # @param cache_root nil falls back to the configuration's cache path.
51
48
  def run(configuration:, options:, paths: nil, buffer: nil, cache_root: nil)
52
49
  require_relative "check_runner_factory"
53
50
  runner = CheckRunnerFactory.build(
@@ -68,10 +65,8 @@ module Rigor
68
65
  # routing hint is outside the false-positive envelope, but a crash in it is not — `describe` must stay a command
69
66
  # an agent can run freely.
70
67
  #
71
- # @param config_path [String, nil] path to the config file; nil uses {Configuration.discover}.
72
- # @param options [Hash] runner options (defaults to {DEEP_OPTIONS}).
73
- # @param paths [Array<String>, nil]
74
- # @return [Outcome]
68
+ # @param config_path path to the config file; nil uses {Configuration.discover}.
69
+ # @param options runner options (defaults to {DEEP_OPTIONS}).
75
70
  def attempt(config_path: nil, options: DEEP_OPTIONS, paths: nil)
76
71
  require_relative "../configuration"
77
72
  configuration = Configuration.load(config_path)
@@ -14,12 +14,8 @@ module Rigor
14
14
 
15
15
  # Builds a runner matching the configuration/plugin/cache resolution that `rigor check` itself uses.
16
16
  #
17
- # @param configuration [Rigor::Configuration]
18
- # @param options [Hash] parsed CLI options (must contain at least
17
+ # @param options — parsed CLI options (must contain at least
19
18
  # `:no_cache`, `:explain`, `:stats`, `:workers`)
20
- # @param buffer [Rigor::Analysis::BufferBinding, nil]
21
- # @param cache_root [String]
22
- # @return [Rigor::Analysis::Runner]
23
19
  def build(configuration:, options:, buffer:, cache_root:)
24
20
  cache_store = if options.fetch(:no_cache)
25
21
  nil
@@ -53,7 +53,7 @@ module Rigor
53
53
  # `--test-command`.
54
54
  DEFAULT_TEST_COMMAND = %w[bundle exec rake].freeze
55
55
 
56
- # @return [Integer] CLI exit status.
56
+ # @return CLI exit status.
57
57
  #
58
58
  # Deferred YJIT enablement (Runtime::Jit) is armed by `CLI#dispatch` for every command, this
59
59
  # one included: a scan long enough to amortize JIT compile cost enables mid-flight; a short one
@@ -48,8 +48,6 @@ module Rigor
48
48
  # Both measurement-integrity warnings, and the escalation rule between them, live in
49
49
  # {MeasurementIntegrityWarning}. Only the floor stays here: it is this command's policy, and
50
50
  # `coverage_command_spec` reads it off this module.
51
- #
52
- # @param report [MutationProtectionReport, FusedProtectionReport]
53
51
  def warn_harness_errors(report)
54
52
  MeasurementIntegrityWarning.emit(report, err: @err, floor: HARNESS_ERROR_WARN_FLOOR)
55
53
  end
@@ -113,10 +111,21 @@ module Rigor
113
111
  cache_store: context.cache_store, workers: workers
114
112
  ),
115
113
  seed_bundles: Protection::DiscoverySeed.bundles(paths: paths),
116
- discovery_seed: seed
114
+ discovery_seed: seed,
115
+ rebuild_session: -> { rebuilt_session(context) }
117
116
  )
118
117
  end
119
118
 
119
+ # Issue #790 — how the oracle replaces an Environment a mutant degraded. {LanguageServer::ProjectContext}
120
+ # already has the operation: `invalidate!` drops the cached environment AND the project scan together,
121
+ # which is the pair the oracle measures over, and the next reader rebuilds both. Under
122
+ # {MutationForkScan} this runs in a forked child against its copy-on-write context, so one file's
123
+ # defect never costs the parent or its siblings a rebuild.
124
+ def rebuilt_session(context)
125
+ context.invalidate!
126
+ [context.environment, context.project_scan]
127
+ end
128
+
120
129
  # The scope {Protection::Mutator} judges a mutation site's receiver against, derived from the same seed
121
130
  # the oracle gets. nil (the gate off, or an empty seed) keeps the bare `Scope.empty` per file.
122
131
  def mutation_base_scope(seed, environment)
@@ -21,9 +21,7 @@ module Rigor
21
21
  module CoverageScan
22
22
  module_function
23
23
 
24
- # @param files [Array<String>] explicit `.rb` file paths to scan.
25
- # @param configuration [Rigor::Configuration]
26
- # @return [Rigor::CLI::CoverageReport]
24
+ # @param files explicit `.rb` file paths to scan.
27
25
  #
28
26
  # Issue #513 — the environment is PLUGIN-AWARE (`LanguageServer::ProjectContext`, the same builder
29
27
  # `--protection` uses), not the bare RBS environment this path carried until 2026-09-01. Without the
@@ -57,8 +55,6 @@ module Rigor
57
55
  # the precision lens must mirror the walk it describes (`check` leaves the table empty unless
58
56
  # `parameter_inference:` is on, ADR-67 WD6a), while `--protection` seeds it unconditionally by ADR-67's own
59
57
  # wiring — it only ever reclassifies sites it already counted.
60
- #
61
- # @return [Rigor::Scope]
62
58
  def discovery_seeded_scope(files:, configuration:, environment:, parameter_inference:, workers: nil)
63
59
  base = Scope.empty(environment: environment)
64
60
  seed = Protection::DiscoverySeed.discovery_tables(files).dup
@@ -26,7 +26,7 @@ module Rigor
26
26
  class DiffCommand < Command
27
27
  USAGE = "Usage: rigor diff [options] <baseline.json> [paths...]"
28
28
 
29
- # @return [Integer] CLI exit status.
29
+ # @return CLI exit status.
30
30
  def run
31
31
  options = parse_options
32
32
 
@@ -36,9 +36,9 @@ module Rigor
36
36
 
37
37
  module_function
38
38
 
39
- # @param body [String] the page as it is stored.
40
- # @param from [String] its absolute path, so relative targets resolve.
41
- # @return [String] the page with every relative link turned into a `rigor docs` key.
39
+ # @param body the page as it is stored.
40
+ # @param from its absolute path, so relative targets resolve.
41
+ # @return the page with every relative link turned into a `rigor docs` key.
42
42
  def rewrite(body, from:)
43
43
  dir = File.dirname(from)
44
44
  body.gsub(MARKDOWN_LINK) do
@@ -52,7 +52,7 @@ module Rigor
52
52
  HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook")
53
53
  LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt")
54
54
 
55
- # @return [Integer] CLI exit status.
55
+ # @return CLI exit status.
56
56
  def run
57
57
  case @argv.first
58
58
  when nil
@@ -136,7 +136,7 @@ module Rigor
136
136
  # Resolve a query to a single doc. Exact relative-path and prefixed-basename aliases are unique; a short
137
137
  # (prefix-stripped) name is accepted only when one category owns it.
138
138
  #
139
- # @return [Hash, Integer] the doc entry, or an error exit status
139
+ # @return the doc entry, or an error exit status
140
140
  # after the error has been written to `@err`.
141
141
  def resolve_doc(query)
142
142
  # A key printed by a rendered page may carry the section it pointed at; resolve the page and say
@@ -7,9 +7,11 @@ require_relative "../configuration"
7
7
  require_relative "../config_audit"
8
8
  require_relative "../analysis/baseline"
9
9
  require_relative "../analysis/result"
10
+ require_relative "../environment/bundle_sig_discovery"
10
11
  require_relative "../plugin"
11
12
  require_relative "../plugin/loader"
12
13
  require_relative "../plugin/services"
14
+ require_relative "../plugin_gap_advisory"
13
15
  require_relative "../reflection"
14
16
  require_relative "../type/combinator"
15
17
  require_relative "check_invocation"
@@ -33,17 +35,12 @@ module Rigor
33
35
  CHECK_RBS = "rbs_environment"
34
36
  CHECK_PLUGINS = "plugins"
35
37
  CHECK_BASELINE = "baseline"
36
- CHECK_RAILS = "rails_plugins"
38
+ CHECK_PLUGIN_GAP = "plugin_gap"
37
39
  CHECK_GEMFILE = "gemfile_install"
38
40
  CHECK_PLUGIN_SKEW = "plugin_skew"
41
+ CHECK_BUNDLE_LAYOUT = "bundle_layout"
39
42
 
40
- RAILS_LOCK_MARKERS = %w[railties actionpack activerecord actioncable].freeze
41
- RAILS_PLUGIN_MARKERS = %w[
42
- rigor-activerecord rigor-actionpack rigor-actionmailer rigor-activejob
43
- rigor-rails-routes rigor-rails-i18n rigor-actioncable rigor-activestorage rigor-rails
44
- ].freeze
45
-
46
- # @return [Integer] CLI exit status.
43
+ # @return CLI exit status.
47
44
  def run
48
45
  options = parse_options
49
46
  configuration = Configuration.load(options.fetch(:config))
@@ -70,8 +67,8 @@ module Rigor
70
67
  # 5. Baseline drift check.
71
68
  findings.concat(check_baseline(configuration, result))
72
69
 
73
- # 6. Rails locked but no Rails plugins.
74
- findings.concat(check_rails_plugins(configuration))
70
+ # 6. Gems locked that a bundled plugin models, with that plugin not enabled (ADR-96 WD2).
71
+ findings.concat(check_plugin_gap(configuration))
75
72
 
76
73
  # 7. Rigor itself resolved as a project dependency.
77
74
  findings.concat(check_gemfile_install)
@@ -79,6 +76,9 @@ module Rigor
79
76
  # 8. A bundled plugin loaded from a different rigortype installation than the engine (#194 slice 3).
80
77
  findings.concat(check_plugin_skew(registry))
81
78
 
79
+ # 9. The project's gems are installed somewhere Rigor cannot see (ADR-25 carry-over).
80
+ findings.concat(check_bundle_layout(configuration))
81
+
82
82
  report(findings, options.fetch(:format))
83
83
  findings.any? { |f| f[:status] == :fail } ? 1 : 0
84
84
  rescue OptionParser::InvalidArgument => e
@@ -249,6 +249,46 @@ module Rigor
249
249
  File.expand_path(path)
250
250
  end
251
251
 
252
+ # ADR-25 carry-over — the plain `bundle install` layout. {Environment::BundleSigDiscovery} detects a
253
+ # bundle root only from paths the PROJECT records: `.bundle/config`'s `BUNDLE_PATH`, an in-tree
254
+ # `vendor/bundle`, or the user-global Bundler config. The default layout records none of those — the
255
+ # gems live in the active Ruby's `GEM_HOME`, which is a property of the project's toolchain, and
256
+ # ADR-27 keeps Rigor from running that toolchain to ask. So detection is not a bounded probe Rigor can
257
+ # add; what it CAN do is stop the state being silent. Without this the user sees gem-shipped `sig/`
258
+ # simply not taking effect, with nothing anywhere naming the reason.
259
+ #
260
+ # Warn, never fail: the layout is the Ruby default and is perfectly correct — only ~3% of gems ship
261
+ # `sig/` at all, so most projects lose nothing by it.
262
+ def check_bundle_layout(configuration)
263
+ return [] unless File.file?(File.join(Dir.pwd, "Gemfile.lock"))
264
+ return [] if resolved_bundle_root(configuration)
265
+
266
+ [
267
+ {
268
+ check: CHECK_BUNDLE_LAYOUT,
269
+ status: :warn,
270
+ message: "Gems are installed in the active Ruby's default gem home, which Rigor cannot locate — " \
271
+ "no gem-shipped `sig/` is being discovered",
272
+ hint: "Rigor reads your project as data and never runs its toolchain, so it can only find a " \
273
+ "bundle whose location the project records (`.bundle/config`'s `BUNDLE_PATH`, an in-tree " \
274
+ "`vendor/bundle`, or your global Bundler config). Point it at the install root with " \
275
+ "`bundler.bundle_path:` in `.rigor.yml`, run `bundle config set --local path vendor/bundle` " \
276
+ "and reinstall, or supply signatures with `rbs collection install` instead."
277
+ }
278
+ ]
279
+ end
280
+
281
+ def resolved_bundle_root(configuration)
282
+ Environment::BundleSigDiscovery.resolve_bundle_path(
283
+ bundle_path: configuration.bundler_bundle_path,
284
+ project_root: Dir.pwd,
285
+ auto_detect: configuration.bundler_auto_detect
286
+ )
287
+ rescue StandardError
288
+ # A doctor check must never be the thing that breaks the command it is diagnosing.
289
+ nil
290
+ end
291
+
252
292
  def check_baseline(configuration, result)
253
293
  path = configuration.baseline_path
254
294
  return [] if path.nil? || !File.exist?(path)
@@ -287,38 +327,45 @@ module Rigor
287
327
  parts.join(", ")
288
328
  end
289
329
 
290
- def check_rails_plugins(configuration)
291
- return [] unless rails_unconfigured?(configuration)
292
-
293
- [
294
- {
295
- check: CHECK_RAILS,
296
- status: :fail,
297
- message: "Rails detected but no Rails plugins enabled",
298
- hint: "Add Rails plugins (e.g. rigor-activerecord, rigor-actionpack) to " \
299
- "`.rigor.yml` `plugins:` so framework calls resolve."
300
- }
301
- ]
330
+ # ADR-96 WD2 — the plugin-gap advisory, read off each bundled plugin's `target_gems:` rather than the
331
+ # Rails-only tables it replaces. Per plugin, so a Rails app that enabled only `rigor-activerecord` now
332
+ # hears about the other seven; `:warn`, because declining a plugin is a legitimate choice and must not
333
+ # fail the command forever. The one `:fail` is today's behaviour generalised: the project depends on
334
+ # gems Rigor models and enables none of the plugins that model them, which is genuinely unconfigured.
335
+ # Both branches read the lockfile's `DEPENDENCIES` section, never its resolved graph: a transitive
336
+ # `minitest` or `i18n` rides along in nearly every Rails lock, and failing `doctor` on one would fail a
337
+ # correctly configured project.
338
+ def check_plugin_gap(configuration)
339
+ return [] if Configuration.discover.nil?
340
+
341
+ gaps = PluginGapAdvisory.gaps(project_root: Dir.pwd, plugins: configuration.plugins)
342
+ return [] if gaps.empty?
343
+
344
+ if PluginGapAdvisory.unconfigured?(project_root: Dir.pwd, plugins: configuration.plugins)
345
+ [unconfigured_plugin_finding(gaps)]
346
+ else
347
+ gaps.map { |gap| plugin_gap_finding(gap) }
348
+ end
302
349
  end
303
350
 
304
- # True when Rails is in Gemfile.lock but the config enables no Rails plugin — the same probe {ProjectStateProbe}
305
- # uses.
306
- def rails_unconfigured?(_configuration)
307
- config_path = Configuration.discover
308
- return false if config_path.nil?
309
- return false unless File.file?(config_path)
310
-
311
- lock = File.join(Dir.pwd, "Gemfile.lock")
312
- return false unless File.file?(lock) && file_mentions_any?(lock, RAILS_LOCK_MARKERS)
313
-
314
- !file_mentions_any?(config_path, RAILS_PLUGIN_MARKERS)
351
+ def unconfigured_plugin_finding(gaps)
352
+ {
353
+ check: CHECK_PLUGIN_GAP,
354
+ status: :fail,
355
+ message: "#{gaps.size} bundled plugin(s) model gems this project depends on and none is enabled " \
356
+ "(#{gaps.map(&:plugin_gem).join(', ')})",
357
+ hint: "Add the plugins for your stack to `.rigor.yml` `plugins:` so framework calls resolve."
358
+ }
315
359
  end
316
360
 
317
- def file_mentions_any?(path, markers)
318
- content = File.read(path)
319
- markers.any? { |marker| content.include?(marker) }
320
- rescue StandardError
321
- false
361
+ def plugin_gap_finding(gap)
362
+ {
363
+ check: CHECK_PLUGIN_GAP,
364
+ status: :warn,
365
+ message: "#{gap.locked_gems.join(', ')} #{gap.locked_gems.one? ? 'is a dependency' : 'are dependencies'} " \
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."
368
+ }
322
369
  end
323
370
 
324
371
  # Rigor is a tool, not a library (ADR-27): resolving it as one of the project's own dependencies pins the
@@ -41,7 +41,7 @@ module Rigor
41
41
  class EffectsCommand < Command
42
42
  USAGE = "Usage: rigor effects [options] [paths]"
43
43
 
44
- # @return [Integer] CLI exit status: 0 on success, 1 on `check` drift, 64 on a usage error.
44
+ # @return CLI exit status: 0 on success, 1 on `check` drift, 64 on a usage error.
45
45
  def run
46
46
  verb = @argv.first
47
47
  return run_verb(verb) if EffectsSnapshotCommand::VERBS.include?(verb)
@@ -231,7 +231,7 @@ module Rigor
231
231
  # project does — still analyses it. Inside a project the argument is already under `paths:` and the
232
232
  # union is the configured set unchanged.
233
233
  #
234
- # @return [Array(Rigor::Effects::EffectTable, Hash{String=>Array<String>})] the table, and which
234
+ # @return the table, and which
235
235
  # file each unit was defined in — the map {EffectsReport} needs to answer a path argument.
236
236
  def analyze(configuration, scope)
237
237
  require_relative "../analysis/runner"
@@ -50,12 +50,12 @@ module Rigor
50
50
  REGENERATION_CLOSING_LINE = "Run `rigor effects update` to regenerate the record under the current " \
51
51
  "rules."
52
52
 
53
- # @param sources [Hash{String=>Array<String>}] `Runner#effect_sources` — where each unit is defined.
53
+ # @param sources `Runner#effect_sources` — where each unit is defined.
54
54
  # A drift row names `file:line` so a reviewer does not have to search for the method (#435). The
55
55
  # file rides the cached summary entry and is free; the line is resolved by parsing the row's own
56
56
  # file ({Effects::DefinitionLines}), which is why a fresh report — no rows — still parses nothing
57
57
  # and the whole-project parse ADR-104 removed from this command stays removed.
58
- # @param lines [Effects::DefinitionLines] seam for the specs; the default parses on demand.
58
+ # @param lines seam for the specs; the default parses on demand.
59
59
  def initialize(out:, path:, sources: nil, lines: Effects::DefinitionLines.new)
60
60
  @out = out
61
61
  @path = path
@@ -121,7 +121,7 @@ module Rigor
121
121
  # method that no longer exists was not defined by it. Such a row renders exactly as it did before
122
122
  # this suffix existed.
123
123
  #
124
- # @return [Array<Array(String, Integer)>] `[relative path, line]` per file, the line nil when that
124
+ # @return `[relative path, line]` per file, the line nil when that
125
125
  # file spells the key with no `def` — an accessor, or a definition the nesting cannot name.
126
126
  def sources_of(symbol)
127
127
  Array(@sources[symbol]).map do |path|
@@ -51,7 +51,7 @@ module Rigor
51
51
  @verb = verb
52
52
  end
53
53
 
54
- # @return [Integer] 0 fresh / written, 1 on drift under `check`, 64 on a usage error.
54
+ # @return 0 fresh / written, 1 on drift under `check`, 64 on a usage error.
55
55
  def run
56
56
  options = parse_options
57
57
  return usage_error("unsupported format: #{options.fetch(:format)}") unless
@@ -4,6 +4,7 @@ require "json"
4
4
  require "optionparser"
5
5
 
6
6
  require_relative "../analysis/rule_catalog"
7
+ require_relative "../sig_gen/skip_reason_catalog"
7
8
  require_relative "command"
8
9
 
9
10
  module Rigor
@@ -18,7 +19,7 @@ module Rigor
18
19
  class ExplainCommand < Command
19
20
  USAGE = "Usage: rigor explain [options] [<rule>]"
20
21
 
21
- # @return [Integer] CLI exit status.
22
+ # @return CLI exit status.
22
23
  def run
23
24
  options = parse_options
24
25
 
@@ -28,6 +29,12 @@ module Rigor
28
29
  end
29
30
 
30
31
  token = @argv.shift
32
+ skip_reason = SigGen::SkipReasonCatalog.resolve(token)
33
+ if skip_reason
34
+ render_skip_reason(skip_reason, options.fetch(:format))
35
+ return 0
36
+ end
37
+
31
38
  entries = Analysis::RuleCatalog.resolve(token)
32
39
  if entries.empty?
33
40
  @err.puts("Unknown rule: #{token}")
@@ -68,6 +75,32 @@ module Rigor
68
75
  @out.puts("")
69
76
  @out.puts("Run `rigor explain <rule>` for the full description.")
70
77
  @out.puts("Family wildcards (`call`, `flow`, `assert`, `dump`, `def`) print every rule under that prefix.")
78
+ @out.puts("")
79
+ @out.puts("`rigor sig-gen` skip reasons:")
80
+ @out.puts("")
81
+ SigGen::SkipReasonCatalog.all.each do |entry|
82
+ @out.puts(" #{entry.id.ljust(33)} #{entry.summary}")
83
+ end
84
+ end
85
+
86
+ def render_skip_reason(entry, format)
87
+ if format == "json"
88
+ @out.puts(JSON.pretty_generate(entry.to_h))
89
+ return
90
+ end
91
+
92
+ @out.puts(entry.id)
93
+ @out.puts("=" * entry.id.length)
94
+ @out.puts("")
95
+ @out.puts(entry.summary)
96
+ @out.puts("")
97
+ @out.puts("A `rigor sig-gen` skip reason, not a diagnostic rule: it reports a signature that was NOT")
98
+ @out.puts("written, so it has no severity and nothing to suppress.")
99
+ @out.puts("")
100
+ @out.puts(entry.explanation)
101
+ @out.puts("")
102
+ @out.puts("What to do:")
103
+ @out.puts(" #{entry.next_step}")
71
104
  end
72
105
 
73
106
  def render_entries(entries, format)
@@ -12,7 +12,13 @@ module Rigor
12
12
  # Framing (ADR-63 / ADR-62 Criterion A, extended): the payload is the **attribution** — which protection axis is
13
13
  # missing — never raw survival. An unprotected site is "add protection here", never "your code is broken".
14
14
  # `harness_errors` (#264) — see {Rigor::CLI::FileEffectiveness}; defaults to 0 for the same reason.
15
- FusedFileProtection = Data.define(:path, :type_killed, :test_killed, :unprotected, :ratio, :harness_errors) do
15
+ # A `class ... < Data.define(...)` body, not the `Data.define(...) do ... end` shorthand: Steep
16
+ # resolves a `def` inside the shorthand block against the *lexically enclosing* class (here,
17
+ # `Rigor::CLI`, which declares its own differently-shaped `initialize`) rather than the anonymous
18
+ # Data subclass being defined — a `sig/` declaration for `FusedFileProtection` cannot fix this,
19
+ # since the misattribution happens before any signature is consulted. The explicit `class` keyword
20
+ # is a real namespace boundary Steep's block walker respects; behaviour is identical either way.
21
+ class FusedFileProtection < Data.define(:path, :type_killed, :test_killed, :unprotected, :ratio, :harness_errors)
16
22
  def initialize(path:, type_killed:, test_killed:, unprotected:, ratio:, harness_errors: 0)
17
23
  super
18
24
  end
@@ -15,7 +15,7 @@ module Rigor
15
15
  class LspCommand < Command
16
16
  USAGE = "Usage: rigor lsp [options]"
17
17
 
18
- # @return [Integer] CLI exit status.
18
+ # @return CLI exit status.
19
19
  def run
20
20
  options = parse_options
21
21
  return CLI::EXIT_USAGE if options == :usage_error
@@ -17,7 +17,7 @@ module Rigor
17
17
  class McpCommand < Command
18
18
  USAGE = "Usage: rigor mcp [options]"
19
19
 
20
- # @return [Integer] CLI exit status.
20
+ # @return CLI exit status.
21
21
  def run
22
22
  options = parse_options
23
23
  return CLI::EXIT_USAGE if options == :usage_error