rigortype 0.3.7 → 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 (450) 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 +96 -15
  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 +39 -4
  22. data/docs/manual/03-configuration.md +11 -0
  23. data/docs/manual/04-diagnostics.md +12 -1
  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 +69 -13
  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 +100 -29
  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 +316 -40
  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 +430 -77
  76. data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
  77. data/lib/rigor/analysis/runner/run_snapshots.rb +13 -3
  78. data/lib/rigor/analysis/runner.rb +200 -43
  79. data/lib/rigor/analysis/worker_session.rb +36 -12
  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 +166 -30
  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 +70 -0
  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_environment_marshal_patch.rb +39 -0
  97. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  98. data/lib/rigor/cache/store.rb +85 -33
  99. data/lib/rigor/cli/annotate_command.rb +5 -6
  100. data/lib/rigor/cli/check_command.rb +57 -10
  101. data/lib/rigor/cli/check_invocation.rb +6 -11
  102. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  103. data/lib/rigor/cli/coverage_command.rb +1 -1
  104. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  105. data/lib/rigor/cli/coverage_scan.rb +1 -5
  106. data/lib/rigor/cli/diff_command.rb +1 -1
  107. data/lib/rigor/cli/doc_links.rb +3 -3
  108. data/lib/rigor/cli/docs_command.rb +2 -2
  109. data/lib/rigor/cli/doctor_command.rb +85 -38
  110. data/lib/rigor/cli/effects_command.rb +2 -2
  111. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  112. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  113. data/lib/rigor/cli/explain_command.rb +34 -1
  114. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  115. data/lib/rigor/cli/lsp_command.rb +1 -1
  116. data/lib/rigor/cli/mcp_command.rb +1 -1
  117. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  118. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  119. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  120. data/lib/rigor/cli/plugin_command.rb +1 -1
  121. data/lib/rigor/cli/plugins_command.rb +2 -1
  122. data/lib/rigor/cli/prism_colorizer.rb +2 -2
  123. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  124. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  125. data/lib/rigor/cli/sig_gen_command.rb +31 -2
  126. data/lib/rigor/cli/skill_command.rb +1 -1
  127. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  128. data/lib/rigor/cli/skill_describe.rb +36 -35
  129. data/lib/rigor/cli/trace_command.rb +4 -4
  130. data/lib/rigor/cli/trace_renderer.rb +5 -6
  131. data/lib/rigor/cli/triage_command.rb +1 -1
  132. data/lib/rigor/cli/type_of_command.rb +10 -6
  133. data/lib/rigor/cli/type_scan_command.rb +4 -4
  134. data/lib/rigor/cli/unused_command.rb +11 -3
  135. data/lib/rigor/cli/upgrade_command.rb +1 -1
  136. data/lib/rigor/cli.rb +68 -6
  137. data/lib/rigor/config_audit.rb +30 -5
  138. data/lib/rigor/configuration/severity_profile.rb +6 -6
  139. data/lib/rigor/configuration.rb +43 -4
  140. data/lib/rigor/effects/attribution.rb +1 -3
  141. data/lib/rigor/effects/config_envelopes.rb +6 -8
  142. data/lib/rigor/effects/definition_lines.rb +25 -6
  143. data/lib/rigor/effects/effect_table.rb +0 -1
  144. data/lib/rigor/effects/entry_points.rb +0 -2
  145. data/lib/rigor/effects/envelope_check.rb +8 -8
  146. data/lib/rigor/effects/envelope_index.rb +5 -8
  147. data/lib/rigor/effects/framework_units.rb +4 -6
  148. data/lib/rigor/effects/identity.rb +4 -6
  149. data/lib/rigor/effects/inline_anchor.rb +7 -7
  150. data/lib/rigor/effects/label_intent.rb +3 -4
  151. data/lib/rigor/effects/liskov_check.rb +8 -8
  152. data/lib/rigor/effects/method_key.rb +1 -1
  153. data/lib/rigor/effects/plugin_facts.rb +4 -6
  154. data/lib/rigor/effects/propagator.rb +2 -3
  155. data/lib/rigor/effects/scanner.rb +1 -1
  156. data/lib/rigor/effects/signature_sources.rb +3 -5
  157. data/lib/rigor/effects/snapshot.rb +10 -10
  158. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  159. data/lib/rigor/effects/unit_scan.rb +10 -10
  160. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  161. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  162. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  163. data/lib/rigor/environment/class_registry.rb +3 -1
  164. data/lib/rigor/environment/failure_slot.rb +28 -0
  165. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  166. data/lib/rigor/environment/lockfile_resolver.rb +51 -4
  167. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  168. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  169. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  170. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  171. data/lib/rigor/environment/rbs_loader.rb +613 -113
  172. data/lib/rigor/environment.rb +189 -27
  173. data/lib/rigor/flow_contribution/merger.rb +0 -2
  174. data/lib/rigor/flow_contribution.rb +11 -13
  175. data/lib/rigor/inference/acceptance.rb +153 -8
  176. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  177. data/lib/rigor/inference/body_fixpoint.rb +5 -5
  178. data/lib/rigor/inference/budget_trace.rb +1 -5
  179. data/lib/rigor/inference/captured_locals.rb +2 -3
  180. data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
  181. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  182. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  183. data/lib/rigor/inference/element_read_widening.rb +168 -0
  184. data/lib/rigor/inference/expression_typer.rb +763 -143
  185. data/lib/rigor/inference/fork_map.rb +5 -7
  186. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  187. data/lib/rigor/inference/hkt_registry.rb +19 -10
  188. data/lib/rigor/inference/hkt_sugar_translator.rb +15 -21
  189. data/lib/rigor/inference/index_write_widening.rb +1 -5
  190. data/lib/rigor/inference/macro_block_self_type.rb +1 -4
  191. data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
  192. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  193. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  194. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  195. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -2
  197. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  198. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  199. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  200. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  201. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +61 -47
  202. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  203. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
  204. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  205. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  206. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  208. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
  209. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  210. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  211. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -5
  212. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  213. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  214. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  215. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  216. data/lib/rigor/inference/method_dispatcher.rb +12 -13
  217. data/lib/rigor/inference/method_parameter_binder.rb +4 -6
  218. data/lib/rigor/inference/multi_target_binder.rb +1 -3
  219. data/lib/rigor/inference/mutation_rejoin.rb +168 -0
  220. data/lib/rigor/inference/mutation_widening.rb +100 -51
  221. data/lib/rigor/inference/narrowing.rb +299 -46
  222. data/lib/rigor/inference/optimistic_origin.rb +1 -7
  223. data/lib/rigor/inference/origin_lookup.rb +2 -3
  224. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  225. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  226. data/lib/rigor/inference/precision_scanner.rb +3 -4
  227. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  228. data/lib/rigor/inference/project_patched_scanner.rb +3 -3
  229. data/lib/rigor/inference/protection_scanner.rb +1 -2
  230. data/lib/rigor/inference/range_constant.rb +57 -0
  231. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  232. data/lib/rigor/inference/receiver_alias.rb +3 -3
  233. data/lib/rigor/inference/refinement_mutation.rb +72 -0
  234. data/lib/rigor/inference/scope_indexer.rb +688 -151
  235. data/lib/rigor/inference/statement_evaluator.rb +221 -50
  236. data/lib/rigor/inference/string_mutation.rb +60 -0
  237. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  238. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  239. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  240. data/lib/rigor/inference/version_guard.rb +28 -21
  241. data/lib/rigor/inference/void_origin.rb +3 -3
  242. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  243. data/lib/rigor/language_server/buffer_table.rb +4 -4
  244. data/lib/rigor/language_server/completion_provider.rb +1 -1
  245. data/lib/rigor/language_server/debouncer.rb +1 -1
  246. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  247. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  248. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  249. data/lib/rigor/language_server/hover_provider.rb +1 -1
  250. data/lib/rigor/language_server/hover_renderer.rb +2 -2
  251. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  252. data/lib/rigor/language_server/project_context.rb +2 -5
  253. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  254. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  255. data/lib/rigor/language_server/server.rb +9 -9
  256. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  257. data/lib/rigor/language_server/uri.rb +1 -1
  258. data/lib/rigor/plugin/base.rb +90 -21
  259. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  260. data/lib/rigor/plugin/effect_attribution.rb +7 -7
  261. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  262. data/lib/rigor/plugin/fact_store.rb +6 -6
  263. data/lib/rigor/plugin/io_boundary.rb +93 -8
  264. data/lib/rigor/plugin/isolation.rb +29 -6
  265. data/lib/rigor/plugin/loader.rb +31 -12
  266. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  267. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  268. data/lib/rigor/plugin/manifest.rb +23 -1
  269. data/lib/rigor/plugin/registry.rb +67 -14
  270. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
  271. data/lib/rigor/plugin/trust_policy.rb +2 -4
  272. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  273. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  274. data/lib/rigor/project_environment.rb +138 -0
  275. data/lib/rigor/protection/analysis_guard.rb +93 -14
  276. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  277. data/lib/rigor/protection/dependency_closure.rb +5 -8
  278. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  279. data/lib/rigor/protection/discovery_seed.rb +11 -11
  280. data/lib/rigor/protection/kill_signature.rb +2 -4
  281. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  282. data/lib/rigor/protection/mutation_cache.rb +6 -9
  283. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  284. data/lib/rigor/protection/mutator.rb +2 -1
  285. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  286. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  287. data/lib/rigor/rbs_extended/envelope_scanner.rb +12 -13
  288. data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
  289. data/lib/rigor/rbs_extended/reporter.rb +122 -16
  290. data/lib/rigor/rbs_extended.rb +67 -22
  291. data/lib/rigor/reflection/constant_path.rb +126 -0
  292. data/lib/rigor/reflection.rb +26 -10
  293. data/lib/rigor/runtime/jit.rb +6 -8
  294. data/lib/rigor/scope/discovery_index.rb +30 -0
  295. data/lib/rigor/scope.rb +203 -4
  296. data/lib/rigor/sig_gen/generator.rb +181 -102
  297. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  298. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  299. data/lib/rigor/sig_gen/observation_collector.rb +17 -15
  300. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  301. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  302. data/lib/rigor/sig_gen/renderer.rb +17 -11
  303. data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -0
  304. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  305. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  306. data/lib/rigor/sig_gen/writer.rb +23 -6
  307. data/lib/rigor/signature_path_audit.rb +153 -6
  308. data/lib/rigor/source/literals.rb +0 -23
  309. data/lib/rigor/source/node_children.rb +0 -2
  310. data/lib/rigor/source/node_locator.rb +5 -11
  311. data/lib/rigor/source/node_walker.rb +2 -5
  312. data/lib/rigor/triage/catalogue.rb +1 -3
  313. data/lib/rigor/triage.rb +3 -5
  314. data/lib/rigor/type/accepts_result.rb +24 -6
  315. data/lib/rigor/type/combinator.rb +68 -5
  316. data/lib/rigor/type/data_class.rb +2 -2
  317. data/lib/rigor/type/data_instance.rb +4 -4
  318. data/lib/rigor/type/float_range.rb +128 -0
  319. data/lib/rigor/type/hash_shape.rb +5 -5
  320. data/lib/rigor/type/integer_range.rb +13 -8
  321. data/lib/rigor/type/nominal.rb +8 -2
  322. data/lib/rigor/type/refined.rb +3 -3
  323. data/lib/rigor/type/struct_class.rb +3 -3
  324. data/lib/rigor/type/struct_instance.rb +4 -4
  325. data/lib/rigor/type.rb +1 -0
  326. data/lib/rigor/type_node/generic.rb +1 -1
  327. data/lib/rigor/type_node/range_literal.rb +25 -0
  328. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  329. data/lib/rigor/type_node.rb +1 -0
  330. data/lib/rigor/version.rb +1 -1
  331. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  332. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  333. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  334. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -0
  335. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  336. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  337. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  338. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +1 -0
  339. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -30
  340. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  341. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  342. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +28 -2
  343. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  344. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  345. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  346. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  347. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  348. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  349. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +1 -0
  350. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
  351. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  352. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  353. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +42 -7
  354. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  355. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
  356. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  357. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +119 -14
  358. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  359. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -0
  360. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  361. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  362. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  363. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  364. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  365. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  366. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  367. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  368. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  369. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  370. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  371. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  372. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  373. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  374. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  375. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
  376. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  377. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  378. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +0 -3
  379. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  380. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  381. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  382. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  383. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  384. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +1 -0
  385. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  386. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  387. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  388. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -0
  389. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  390. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -10
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -2
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  395. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  396. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  397. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -0
  398. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  399. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  400. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +128 -3
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  403. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  404. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  405. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  406. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  407. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  408. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  409. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  410. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  411. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -1
  414. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  416. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +1 -0
  417. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  423. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +4 -5
  424. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  425. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
  426. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  427. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  428. data/sig/rigor/cache.rbs +2 -3
  429. data/sig/rigor/environment.rbs +11 -0
  430. data/sig/rigor/inference.rbs +3 -10
  431. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  432. data/sig/rigor/plugin/manifest.rbs +2 -1
  433. data/sig/rigor/reflection.rbs +3 -1
  434. data/sig/rigor/scope.rbs +24 -3
  435. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
  436. data/sig/rigor/type.rbs +27 -1
  437. data/sig/rigor.rbs +1 -0
  438. data/skills/rigor-ask/SKILL.md +8 -6
  439. data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
  440. data/skills/rigor-project-init/SKILL.md +6 -3
  441. data/skills/rigor-project-init/references/02-configure.md +10 -6
  442. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  443. data/skills/rigor-type-oracle/SKILL.md +228 -0
  444. data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
  445. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  446. data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
  447. data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
  448. metadata +28 -3
  449. data/lib/rigor/cli/probe_environment.rb +0 -85
  450. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -38,7 +38,14 @@ module Rigor
38
38
  # `PAYLOAD_ABI_VERSION` already rebuilds across a RELEASE, so the exposure is a same-version tree; this
39
39
  # closes that window too, because "the same project reports differently depending on how you ran it" is
40
40
  # the defect the diagnostic exists to end and a stale blob reintroduces it.
41
- FORMAT_VERSION = 3
41
+ #
42
+ # v4 (issue #799): `RBS::AST::Annotation` now carries its own `marshal_dump` / `marshal_load`, which
43
+ # keep the annotation's POSITION as well as its file, so `rbs_extended.unsatisfied-conformance` points
44
+ # at the `%a{rigor:v1:conforms-to …}` line warm as well as cold. A pre-change blob loads perfectly well
45
+ # — Marshal encodes an ivar dump and a `marshal_dump` payload differently, and only the latter reaches
46
+ # `marshal_load` — which is exactly the problem: without a bump it would keep reporting the row at
47
+ # `1:1` forever, and `--verify-incremental` would keep failing on that project.
48
+ FORMAT_VERSION = 4
42
49
 
43
50
  # Payload ABI version. Store values are mostly Marshal blobs of Rigor/RBS objects, so a Rigor release
44
51
  # upgrade is an ABI boundary even when the byte layout and descriptor schema are unchanged. Folding the
@@ -58,8 +65,8 @@ module Rigor
58
65
 
59
66
  VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
60
67
 
61
- # @param root [String] cache root directory.
62
- # @param read_only [Boolean] when true, every disk-side side-effect is suppressed: `fetch_or_compute`
68
+ # @param root — cache root directory.
69
+ # @param read_only — when true, every disk-side side-effect is suppressed: `fetch_or_compute`
63
70
  # still reads existing entries (hits, gated on a current `schema_version.txt` marker — see
64
71
  # {#ensure_schema_version!}) and still runs the producer block on miss, but it does NOT write the
65
72
  # produced value to disk, does NOT update the marker, and does NOT touch the on-disk root directory.
@@ -100,7 +107,7 @@ module Rigor
100
107
 
101
108
  attr_reader :root
102
109
 
103
- # @return [Boolean] whether this Store suppresses disk writes (`schema_version.txt`, entry creation).
110
+ # @return whether this Store suppresses disk writes (`schema_version.txt`, entry creation).
104
111
  # Reads are unaffected.
105
112
  def read_only?
106
113
  @read_only
@@ -111,7 +118,7 @@ module Rigor
111
118
  # specific instance rather than the on-disk cache state. Disk-level state is reported separately by
112
119
  # {.disk_inventory}.
113
120
  #
114
- # @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
121
+ # @return `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
115
122
  def stats
116
123
  @monitor.synchronize do
117
124
  per_producer = @by_producer.transform_values { |counts| counts.dup.freeze }.freeze
@@ -123,7 +130,7 @@ module Rigor
123
130
  # `rigor check --cache-stats` to surface cache size and per-producer entry counts without depending on
124
131
  # in-process counters (which only reflect the current run).
125
132
  #
126
- # @return [Hash] `{ root:, schema_version:, total_entries:, total_bytes:, producers: [{ id:, entries:,
133
+ # @return `{ root:, schema_version:, total_entries:, total_bytes:, producers: [{ id:, entries:,
127
134
  # bytes: }, ...] }`. When the root does not exist or has no schema-version marker, `schema_version` is
128
135
  # nil and the producer list is empty.
129
136
  #
@@ -169,21 +176,21 @@ module Rigor
169
176
  end
170
177
  private_class_method :collect_producers
171
178
 
172
- # @param producer_id [String] stable cache namespace; only `[a-z][a-z0-9._-]*` is accepted.
173
- # @param generation_cap [Integer, Symbol] how many generations of this producer survive a compaction
179
+ # @param producer_id — stable cache namespace; only `[a-z][a-z0-9._-]*` is accepted.
180
+ # @param generation_cap — how many generations of this producer survive a compaction
174
181
  # pass — a positive `Integer` for a whole-project producer (one live entry, older ones unreachable),
175
182
  # or {UNBOUNDED_GENERATIONS} for a producer with many simultaneously-live entries. REQUIRED, and
176
183
  # sourced from the producer's own declaration (`RbsCacheProducer.generation_cap`,
177
184
  # `RunCacheKey::GENERATION_CAP`, `Plugin::Base.producer generation_cap:`) rather than invented at the
178
185
  # call site. See {#evict!}.
179
- # @param params [Hash] producer inputs; mixed into the cache key via {Descriptor#cache_key_for}.
180
- # @param descriptor [Rigor::Cache::Descriptor] the invalidation descriptor for the value being cached.
181
- # @param serialize [#call, nil] optional callable that turns the producer's return value into a binary
186
+ # @param params — producer inputs; mixed into the cache key via {Descriptor#cache_key_for}.
187
+ # @param descriptor — the invalidation descriptor for the value being cached.
188
+ # @param serialize — optional callable that turns the producer's return value into a binary
182
189
  # `String`. Defaults to `Marshal.dump(value).b`. Producers whose return values are not
183
190
  # `Marshal`-clean (RBS-native objects with `RBS::Location` members, raw `IO`, …) MUST provide a
184
191
  # serialiser. The pair `(serialize, deserialize)` MUST round-trip — a producer that reads with one
185
192
  # strategy and writes with another corrupts its own cache slice.
186
- # @param deserialize [#call, nil] optional callable that turns bytes back into the producer's value.
193
+ # @param deserialize — optional callable that turns bytes back into the producer's value.
187
194
  # Defaults to `Marshal.load`. Any exception (`StandardError`) raised by the deserialiser is treated as
188
195
  # a cache miss — the entry is considered corrupt, the producer block reruns, and the next write
189
196
  # overwrites it. This is consistent with the fault-tolerance contract for the default `Marshal.load`
@@ -387,10 +394,22 @@ module Rigor
387
394
 
388
395
  # Reads and validates one entry file. Any failure (missing, short, bad magic, bad version, bad
389
396
  # checksum, unmarshal-able) returns nil so the caller treats it as a cache miss.
397
+ #
398
+ # The `ENOENT` rescue covers the gap between the existence check and the open: a concurrent process
399
+ # compacting or clearing the root can unlink the file in between, and a file that is gone reads as the
400
+ # same miss the existence check above already reports for one that was never there (issue #807). It is
401
+ # deliberately narrow — every other `SystemCallError` (a permission error, an I/O error) still raises,
402
+ # so a genuinely broken filesystem stays visible instead of degrading into an endless recompute. A
403
+ # present-but-corrupt file is not touched by this either: it still goes through the envelope and
404
+ # payload checks below.
390
405
  def read_entry(path, deserialize: nil)
391
406
  return nil unless File.file?(path)
392
407
 
393
- bytes = File.binread(path)
408
+ begin
409
+ bytes = File.binread(path)
410
+ rescue Errno::ENOENT
411
+ return nil
412
+ end
394
413
  return nil unless envelope_valid?(bytes)
395
414
 
396
415
  body = bytes.byteslice(HEADER.bytesize, bytes.bytesize - HEADER.bytesize - 32)
@@ -479,22 +498,34 @@ module Rigor
479
498
  def atomically_replace(path, body)
480
499
  File.open(path, File::RDWR | File::CREAT, 0o644) do |lock_fd|
481
500
  lock_fd.flock(File::LOCK_EX)
482
- tmp = "#{path}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
483
- begin
484
- File.open(tmp, "wb") do |f|
485
- f.write(body)
486
- f.fsync
487
- end
488
- File.rename(tmp, path)
489
- ensure
490
- # A failed write/rename must not leak its temp file — rely on the 1-hour `cleanup_stale_temp_files`
491
- # sweep only as a backstop for crashes that skip this ensure entirely.
492
- unlink_entry(tmp) if File.exist?(tmp)
493
- end
501
+ publish_by_rename(path, body)
494
502
  fsync_directory(File.dirname(path))
495
503
  end
496
504
  end
497
505
 
506
+ # The rename-into-place half of {#atomically_replace}, without the destination lock: writes `body` to a
507
+ # sibling temp file, fsyncs it, and renames it over `path`. POSIX makes the rename atomic within one
508
+ # filesystem, so an unlocked concurrent reader sees either the previous contents in full or the new
509
+ # ones in full — never a truncated or half-written file.
510
+ #
511
+ # {#repair_writable_marker!} uses this half alone. It needs the atomicity (its reader is another
512
+ # process's constructor, which does not lock) but not the ordering a lock would buy: every writer of
513
+ # `schema_version.txt` writes the same value, so there is no last-writer-wins question to settle.
514
+ def publish_by_rename(path, body)
515
+ tmp = "#{path}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
516
+ begin
517
+ File.open(tmp, "wb") do |f|
518
+ f.write(body)
519
+ f.fsync
520
+ end
521
+ File.rename(tmp, path)
522
+ ensure
523
+ # A failed write/rename must not leak its temp file — rely on the 1-hour `cleanup_stale_temp_files`
524
+ # sweep only as a backstop for crashes that skip this ensure entirely.
525
+ unlink_entry(tmp) if File.exist?(tmp)
526
+ end
527
+ end
528
+
498
529
  # Checks (and, for a writable store, repairs) the `schema_version.txt` marker, and reports whether the
499
530
  # disk tier is usable for the rest of this Store's lifetime. The result is memoized in `@disk_available`
500
531
  # — one check per Store is enough; a benign double-check under a thread race would just repeat
@@ -508,8 +539,6 @@ module Rigor
508
539
  # (permission, disk full, deleted root) also reports unavailable, degrading this Store instance to
509
540
  # in-memory-memo-only for the rest of its lifetime — the producer block still runs, its result still
510
541
  # lands in `@memo`, and disk reads/writes are simply skipped.
511
- #
512
- # @return [Boolean]
513
542
  def ensure_schema_version!
514
543
  return @disk_available unless @disk_available.nil?
515
544
 
@@ -525,25 +554,48 @@ module Rigor
525
554
  false
526
555
  end
527
556
 
557
+ # The marker is PUBLISHED BY RENAME rather than written in place. Its reader is another process's
558
+ # constructor, which takes no lock, so an `O_TRUNC`-then-write would expose a window in which the file
559
+ # exists and is empty; a reader landing there concludes the marker disagrees and clears the whole cache
560
+ # root — under, among others, a sibling that is between its own `File.file?` and `File.binread`, which
561
+ # is the `Errno::ENOENT` of issue #807. Two `rigor check` processes over one fresh `.rigor/cache` are
562
+ # enough to hit it.
563
+ #
564
+ # An EMPTY marker is treated as an ABSENT one — rewritten, never cleared on. Emptiness carries no
565
+ # information: it is what a torn write by an older Rigor looks like, what a crash between the create
566
+ # and the write leaves, and what this method's own clear-then-rewrite passes through. Answering an
567
+ # ambiguous signal by destroying a cache root is the wrong trade, and declining grants the entries
568
+ # below no trust they did not already have — a marker that is simply MISSING over existing entries is
569
+ # already kept and served (see the spec's writable-store rules), and `clear_cache_root!` removes the
570
+ # marker before rewriting it, so it produces that state itself.
528
571
  def repair_writable_marker!
529
572
  FileUtils.mkdir_p(@root)
530
573
  marker = File.join(@root, "schema_version.txt")
531
574
  current = self.class.schema_marker_value
532
575
 
533
- if File.file?(marker)
534
- on_disk = File.read(marker).strip
535
- return true if on_disk == current
576
+ on_disk = read_marker(marker)
577
+ return true if on_disk == current
536
578
 
537
- clear_cache_root!
538
- end
579
+ clear_cache_root! unless on_disk.nil? || on_disk.empty?
539
580
 
540
581
  FileUtils.mkdir_p(@root)
541
- File.write(marker, "#{current}\n")
582
+ publish_by_rename(marker, "#{current}\n")
583
+ fsync_directory(@root)
542
584
  true
543
585
  rescue StandardError
544
586
  false
545
587
  end
546
588
 
589
+ # @return the marker's content, or nil when there is none to read — including the case of a concurrent
590
+ # {#clear_cache_root!} removing it between the check and the open.
591
+ def read_marker(path)
592
+ return nil unless File.file?(path)
593
+
594
+ File.read(path).strip
595
+ rescue Errno::ENOENT
596
+ nil
597
+ end
598
+
547
599
  def clear_cache_root!
548
600
  Dir.children(@root).each do |entry|
549
601
  FileUtils.rm_rf(File.join(@root, entry))
@@ -15,7 +15,7 @@ require_relative "../inference/scope_indexer"
15
15
  require_relative "../inference/statement_evaluator"
16
16
  require_relative "prism_colorizer"
17
17
  require_relative "command"
18
- require_relative "probe_environment"
18
+ require_relative "../project_environment"
19
19
 
20
20
  module Rigor
21
21
  class CLI
@@ -43,7 +43,7 @@ module Rigor
43
43
  # paging stays off because the CLI may itself sit in a pipeline.
44
44
  BAT_ARGS = %w[--language=ruby --style=plain --paging=never --color=always].freeze
45
45
 
46
- # @return [Integer] CLI exit status.
46
+ # @return CLI exit status.
47
47
  def run
48
48
  options = parse_options
49
49
  file = @argv.shift
@@ -132,10 +132,10 @@ module Rigor
132
132
 
133
133
  # The plugin-aware environment for the annotated file, so a `#=> <type>` matches what `rigor check`
134
134
  # infers on that line — including types synthesized from the file's own inline RBS annotations (the file
135
- # is threaded as the synthesizer's `source_files:`; see {ProbeEnvironment}).
135
+ # is threaded as the synthesizer's `source_files:`; see {ProjectEnvironment}).
136
136
  def base_scope(configuration, file)
137
137
  Scope.empty(
138
- environment: ProbeEnvironment.build(configuration: configuration, source_files: [file])
138
+ environment: ProjectEnvironment.build(configuration: configuration, source_files: [file])
139
139
  )
140
140
  end
141
141
 
@@ -220,8 +220,7 @@ module Rigor
220
220
  @scope_index = scope_index
221
221
  end
222
222
 
223
- # @param program [Prism::ProgramNode]
224
- # @return [Hash{Integer => Rigor::Type}] 1-indexed line => type.
223
+ # @return 1-indexed line => type.
225
224
  def collect(program)
226
225
  by_line = {}
227
226
  each_statement(program) do |statement|
@@ -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