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
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "blueprint"
4
+ # Issue #806 — {Registry#guarded_manifest} constructs one. `Rigor::Plugin::LoadError` MUST be defined by
5
+ # then: an unresolved bare `LoadError` in this lexical scope silently falls back to Ruby's own ::LoadError.
6
+ require_relative "load_error"
4
7
 
5
8
  module Rigor
6
9
  module Plugin
@@ -217,23 +220,26 @@ module Rigor
217
220
  class Registry
218
221
  attr_reader :plugins, :load_errors, :blueprints, :contribution_index, :resolved_gem_paths
219
222
 
220
- # @param plugins [Array<Rigor::Plugin::Base>] instantiated plugin instances in deterministic order.
221
- # @param load_errors [Array<Rigor::Plugin::LoadError>] failures surfaced during loading. Each error is
223
+ # @param plugins instantiated plugin instances in deterministic order.
224
+ # @param load_errors failures surfaced during loading. Each error is
222
225
  # also turned into a diagnostic by the runner.
223
- # @param blueprints [Array<Rigor::Plugin::Blueprint>] frozen, Ractor-shareable replay descriptors
226
+ # @param blueprints frozen, Ractor-shareable replay descriptors
224
227
  # aligned 1:1 with `plugins`. The loader fills this in; callers that construct Registry manually MAY
225
228
  # pass `[]` and accept that {.materialize} cannot replay the set.
226
- # @param resolved_gem_paths [Hash{String=>String,nil}] #194 slice 1 — `gem name => resolved file path`
229
+ # @param resolved_gem_paths #194 slice 1 — `gem name => resolved file path`
227
230
  # for each successfully required plugin gem, so `rigor plugins` can print where a loaded (and
228
231
  # possibly frozen) plugin actually loaded from. Defaults to empty; a worker registry built by
229
232
  # {.materialize} carries none (the provenance surface runs only on the coordinator).
230
233
  def initialize(plugins: [], load_errors: [], blueprints: [], resolved_gem_paths: {})
231
234
  @plugins = plugins.dup.freeze
232
- @load_errors = load_errors.dup.freeze
233
235
  @blueprints = blueprints.dup.freeze
234
236
  @resolved_gem_paths = resolved_gem_paths.dup.freeze
235
237
  @contribution_index = ContributionIndex.new(@plugins)
236
- compile_aggregates
238
+ # Issue #806 — every manifest the construction-time aggregates need is read ONCE here, behind a
239
+ # per-plugin rescue, and a plugin that raises joins `load_errors` instead of taking the run down.
240
+ manifest_errors = []
241
+ compile_aggregates(@plugins.map { |plugin| guarded_manifest(plugin, manifest_errors) })
242
+ @load_errors = (load_errors + manifest_errors).freeze
237
243
  # ADR-52 WD4 — the single engine-owned node-rule walk, compiled once per run from the node-rule
238
244
  # plugin subset (registry order). The runner reuses it for every file; it builds fresh per-file
239
245
  # state internally, so it is safe to freeze and share.
@@ -306,9 +312,14 @@ module Rigor
306
312
  # in plugin registration order. `Environment#build_name_scope` builds a `TypeNode::ResolverChain` from
307
313
  # this list (environment.rb). The first non-nil `#resolve(node, scope)` return wins per ADR-13 WD3 /
308
314
  # WD5 — registration order is the user's lever.
309
- def type_node_resolvers
310
- plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
311
- end
315
+ #
316
+ # Issue #806 compiled at construction alongside its `compile_aggregates` siblings, and for a second
317
+ # reason beyond theirs: `Environment#build_name_scope` demands it during Environment CONSTRUCTION, so
318
+ # the previous per-call `plugin.manifest` read had no rescue above it anywhere — a plugin raising here
319
+ # aborted every `Environment.for_project`, i.e. the whole run, before any seam could record it. The
320
+ # guard has to sit at registry construction because that is the last point at which a failure can still
321
+ # join `load_errors` (the registry freezes immediately after).
322
+ attr_reader :type_node_resolvers
312
323
 
313
324
  # ADR-20 slice 6 — aggregate every loaded plugin's manifest-declared HKT registrations + definitions
314
325
  # into a single `Inference::HktRegistry` overlay that `Environment#hkt_registry` merges on top of the
@@ -419,10 +430,14 @@ module Rigor
419
430
  # compiled once at construction (the registry is frozen, so the flat_map-on-every-call versions
420
431
  # re-derived an invariant). `@contracts_by_path` is a mutable per-path memo inside the frozen registry
421
432
  # — safe because the contract set and the glob semantics are fixed for the lifetime of the run.
422
- def compile_aggregates
423
- @additional_initializers = @plugins.flat_map { |p| safe_manifest(p)&.additional_initializers || [] }.freeze
424
- @open_receivers = @plugins.flat_map { |p| (safe_manifest(p)&.open_receivers || []).map(&:to_s) }.uniq.freeze
433
+ #
434
+ # @param manifests one entry per plugin, positionally aligned with `@plugins`;
435
+ # nil where {#guarded_manifest} caught a raising manifest read.
436
+ def compile_aggregates(manifests)
437
+ @additional_initializers = manifests.flat_map { |m| m&.additional_initializers || [] }.freeze
438
+ @open_receivers = manifests.flat_map { |m| (m&.open_receivers || []).map(&:to_s) }.uniq.freeze
425
439
  @open_receivers_set = @open_receivers.to_set.freeze
440
+ @type_node_resolvers = manifests.flat_map { |m| m&.type_node_resolvers || [] }.freeze
426
441
  @protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
427
442
  @contracts_by_path = {}
428
443
  @effect_memo = {}
@@ -466,6 +481,21 @@ module Rigor
466
481
  nil
467
482
  end
468
483
 
484
+ # Issue #806 — {#safe_manifest} with a receipt. The construction-time aggregates degraded silently
485
+ # when a manifest read raised, so a plugin that contributed nothing looked identical to a plugin that
486
+ # was never asked; the load-error channel is where "a plugin could not be loaded" already lives, and
487
+ # `Analysis::Runner` turns each entry into one `plugin_loader.load-error` row naming the plugin. The
488
+ # plugin ref is the CLASS, not `manifest.id`: the manifest is the surface that just failed.
489
+ def guarded_manifest(plugin, errors)
490
+ plugin.manifest
491
+ rescue StandardError => e
492
+ errors << LoadError.new(
493
+ "plugin #{plugin.class} raised while reading its manifest: #{e.class}: #{e.message}",
494
+ plugin_ref: plugin.class.to_s, cause: e
495
+ )
496
+ nil
497
+ end
498
+
469
499
  def safe_protocol_contracts(plugin)
470
500
  plugin.protocol_contracts || []
471
501
  rescue StandardError
@@ -25,15 +25,26 @@ module Rigor
25
25
 
26
26
  def initialize
27
27
  @entries = []
28
+ @seen = {}
28
29
  end
29
30
 
31
+ # Identical entries are collapsed. Each becomes one `:info` row, and two rows saying the same thing
32
+ # about the same file carry nothing the first does not: the synthesizer's own per-file notices are
33
+ # already `uniq`, but a worker pool re-runs the whole env build in EVERY worker over the same project
34
+ # file list and then drains each worker's stream into this one reporter, so without the dedup a
35
+ # `--workers N` run printed N copies of a row a `--workers=0` run printed once. The same collapse the
36
+ # {RbsExtended::Reporter} drain documents, for the same reason.
30
37
  def record(plugin_id:, path:, message:, kind: :failed)
31
- @entries << Entry.new(
38
+ entry = Entry.new(
32
39
  plugin_id: plugin_id.to_s.dup.freeze,
33
40
  path: path.to_s.dup.freeze,
34
41
  message: message.to_s.dup.freeze,
35
42
  kind: kind
36
43
  )
44
+ return nil if @seen.key?(entry)
45
+
46
+ @seen[entry] = true
47
+ @entries << entry
37
48
  nil
38
49
  end
39
50
 
@@ -45,8 +45,7 @@ module Rigor
45
45
  freeze
46
46
  end
47
47
 
48
- # @param path [String]
49
- # @return [Boolean] true when the absolute path falls inside any allowed read root. Symlinks are
48
+ # @return true when the absolute path falls inside any allowed read root. Symlinks are
50
49
  # resolved through `File.expand_path` only (no `realpath`); plugins with adversarial intent are out of
51
50
  # scope per ADR-2.
52
51
  def allow_read?(path)
@@ -58,8 +57,7 @@ module Rigor
58
57
  @network_policy != :disabled
59
58
  end
60
59
 
61
- # @param url [String, URI]
62
- # @return [Boolean] true when the URL scheme is `https` and the parsed hostname is in
60
+ # @return true when the URL scheme is `https` and the parsed hostname is in
63
61
  # `allowed_url_hosts`. Always `false` while `network_policy` is `:disabled`.
64
62
  def allow_url?(url)
65
63
  return false if @network_policy == :disabled
@@ -28,11 +28,11 @@ module Rigor
28
28
  # Resolvers SHOULD be stateless and re-entrant; the registry builds the chain once per
29
29
  # `Analysis::Runner.run` and may consult any resolver multiple times for the same node.
30
30
  class TypeNodeResolver
31
- # @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic] the parser-emitted node the chain is
31
+ # @param node the parser-emitted node the chain is
32
32
  # asking about.
33
- # @param scope [Rigor::TypeNode::NameScope] companion value object (slice 3); slice 2 invocations MAY pass
33
+ # @param scope companion value object (slice 3); slice 2 invocations MAY pass
34
34
  # `nil` because the chain doesn't exist yet.
35
- # @return [Rigor::Type::Base, nil] resolved type, or `nil` to fall through.
35
+ # @return resolved type, or `nil` to fall through.
36
36
  def resolve(node, scope) # rubocop:disable Lint/UnusedMethodArgument
37
37
  nil
38
38
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "plugin/bundled_catalog"
4
+ require_relative "environment/lockfile_resolver"
5
+
6
+ module Rigor
7
+ # ADR-96 WD2 — the plugin-gap advisory: a gem the project itself depends on that a bundled plugin declares
8
+ # in `target_gems:`, with that plugin absent from `plugins:`.
9
+ #
10
+ # One home for a fact that ADR-96 found in four hand-maintained copies, none of which was the plugin.
11
+ # `rigor doctor` and `rigor skill describe`'s project probe both route through here, so the two can no
12
+ # longer disagree — they were byte-identical Rails tables by luck, not by construction.
13
+ #
14
+ # The advisory is deliberately advice, not activation: naming a plugin never loads it (ADR-96 Criterion 2),
15
+ # and declining a plugin is a legitimate choice, which is why a gap is a warning and never a failure.
16
+ module PluginGapAdvisory
17
+ # One unenabled plugin and the depended-on gems that make it relevant.
18
+ Gap = Data.define(:plugin_gem, :plugin_id, :locked_gems)
19
+
20
+ # A gem whose whole point is to pull its constituents in. A Rails application's `Gemfile` says `rails`
21
+ # and never `activerecord`, so a direct-dependency match alone would silence the entire Rails family for
22
+ # the projects the advisory exists for. The table is explicit rather than derived: expanding whatever a
23
+ # gem happens to resolve to is the transitive read this advisory is here to avoid, and the set of gems
24
+ # that are genuinely umbrellas is small and known.
25
+ UMBRELLA_GEMS = {
26
+ "rails" => %w[
27
+ actioncable actionmailer actionpack actiontext actionview activejob activemodel
28
+ activerecord activestorage activesupport railties
29
+ ].freeze
30
+ }.freeze
31
+
32
+ class << self
33
+ # @param project_root — the directory whose `Gemfile.lock` and `plugins:` list are read.
34
+ # @param plugins — the configuration's raw `plugins:` entries (String or Hash form).
35
+ # @return the gaps, sorted by plugin gem name; empty when the project declares no dependency.
36
+ def gaps(project_root:, plugins:)
37
+ depended = depended_gems(project_root)
38
+ return [] if depended.empty?
39
+
40
+ listed = listed_plugin_names(plugins)
41
+ Plugin::BundledCatalog.entries.filter_map do |entry|
42
+ next if listed?(entry, listed)
43
+
44
+ matched = entry.target_gems.select { |gem| depended.include?(gem) }
45
+ next if matched.empty?
46
+
47
+ Gap.new(plugin_gem: entry.gem_name, plugin_id: entry.plugin_id, locked_gems: matched.freeze)
48
+ end.sort_by(&:plugin_gem)
49
+ end
50
+
51
+ # Whether the project enables no bundled plugin at all for the gems it depends on — ADR-96's preserved
52
+ # `:fail` case, generalised off the Rails-only table it replaces. A project that enables *some* of the
53
+ # matching plugins has made choices; one that enables none of them is unconfigured for its own stack.
54
+ def unconfigured?(project_root:, plugins:)
55
+ found = gaps(project_root: project_root, plugins: plugins)
56
+ return false if found.empty?
57
+
58
+ listed = listed_plugin_names(plugins)
59
+ Plugin::BundledCatalog.entries.none? { |entry| listed?(entry, listed) }
60
+ end
61
+
62
+ private
63
+
64
+ # The gems the project chose, which is the `DEPENDENCIES` section and never the resolved graph. A
65
+ # transitive `minitest` / `i18n` / `ffi` is somebody else's dependency, and advising on it fires on
66
+ # correct configuration — including, through {unconfigured?}, as a hard failure.
67
+ def depended_gems(project_root)
68
+ direct = Environment::LockfileResolver.direct_dependency_names(
69
+ lockfile_path: nil, project_root: project_root
70
+ )
71
+ return direct if direct.empty?
72
+
73
+ direct + direct.flat_map { |gem| UMBRELLA_GEMS.fetch(gem, []) }
74
+ end
75
+
76
+ # Both spellings a `plugins:` entry may use for the same plugin — the gem name and the manifest id.
77
+ # An `enabled: false` entry still counts as naming it: the user has demonstrably found the plugin, so
78
+ # advising them to add what is already there would be worse than staying quiet.
79
+ def listed_plugin_names(plugins)
80
+ Array(plugins).flat_map do |raw|
81
+ case raw
82
+ when String then [raw]
83
+ when Hash
84
+ keyed = raw.to_h { |key, value| [key.to_s, value] }
85
+ [keyed["gem"], keyed["id"]].compact.map(&:to_s)
86
+ else []
87
+ end
88
+ end.to_set
89
+ end
90
+
91
+ def listed?(entry, listed)
92
+ listed.include?(entry.gem_name) || listed.include?(entry.plugin_id)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "environment"
4
+ require_relative "plugin"
5
+ require_relative "plugin/loader"
6
+ require_relative "plugin/services"
7
+ require_relative "reflection"
8
+ require_relative "type/combinator"
9
+
10
+ module Rigor
11
+ # Shared construction of the project environment for every command that types against the project WITHOUT
12
+ # driving a full {Analysis::Runner}: the single-shot probes (`type-of`, `type-scan`, `trace`, `annotate`) and
13
+ # the `sig-gen` generator / observation collector. Each of these answers "what does the engine see here?" for
14
+ # a hand-picked file, position or class, so the type universe they read MUST match the one `rigor check`
15
+ # analyses against — otherwise the command reports, or declines to emit, against a universe the real run never
16
+ # had.
17
+ #
18
+ # Two gaps this closes, both the same shape:
19
+ #
20
+ # - The probes historically built their environment with `Environment.for_project(libraries:,
21
+ # signature_paths:)` only — no plugin registry, no `source_files:`. The `source_rbs_synthesizer` plugin hook
22
+ # (ADR-32 / ADR-93's auto-wired `rigor-rbs-inline`) therefore never ran there, so a class whose only
23
+ # signature comes from an inline `#: () -> void` annotation typed as `Dynamic[top]` under a probe while
24
+ # `check` resolved it through the synthesized RBS. That divergence misattributed a dispatch tier during the
25
+ # #162 transitive-void design pass (see the 2026-07-19 addendum in
26
+ # docs/adr/100-static-diagnostic-family-and-void-origins.md).
27
+ # - Neither the probes nor `sig-gen` passed the dependency-discovery axes — the `rbs collection` lockfile, the
28
+ # bundle's per-gem `sig/`, the Gemfile.lock gating them — that `check` reads straight off the same
29
+ # configuration. On a Rails project with `rbs collection install` run, `sig-gen` therefore could not see
30
+ # `ActiveRecord::Base` at all, and its superclass-without-RBS skip guard declined to emit every model
31
+ # (issue #821). The advice the guard prints — install the collection — had no effect, because the command
32
+ # never looked at it.
33
+ #
34
+ # {.dependency_discovery_options} is the single spelling of those axes; `Runner`'s build path
35
+ # (`Analysis::Runner::PoolCoordinator#build_runner_environment`), the worker session and the LSP project
36
+ # context read it too, so a new axis cannot be added to `check` and silently missed here.
37
+ #
38
+ # A non-`check` command legitimately simplifies in two places: no synthesis-failure reporter (there is no
39
+ # diagnostic pipeline) and no synthesis cache store (a single-position probe recomputes cheaply). Plugin
40
+ # loading itself matches check — same `.rigor.yml` `plugins:` config, same ADR-93 auto-wire (already applied
41
+ # by `Configuration.load`), same `enabled:` / `require_magic_comment` semantics via `Plugin::Loader.load`.
42
+ #
43
+ # Fail-soft is the invariant: a project with no plugins, an unresolvable plugin gem, or any error during the
44
+ # plugin-aware build degrades — never crashes. `Plugin::Loader` already isolates per-entry load failures onto
45
+ # the registry; the `rescue` here is the belt-and-suspenders around anything the loader or the plugin-aware
46
+ # env build itself might raise.
47
+ module ProjectEnvironment
48
+ module_function
49
+
50
+ # Builds the plugin-aware, dependency-aware {Rigor::Environment} a non-`check` command works against.
51
+ #
52
+ # @param configuration — the loaded project configuration (already carries the
53
+ # ADR-93 auto-wired `rigor-rbs-inline` entry when the library is resolvable).
54
+ # @param source_files — the file(s) the command inspects. Threaded so each loaded plugin's
55
+ # `source_rbs_synthesizer` runs over them at env-build time; an empty list contributes no synthesized RBS.
56
+ def build(configuration:, source_files:)
57
+ Environment.for_project(
58
+ libraries: configuration.libraries,
59
+ signature_paths: configuration.signature_paths,
60
+ plugin_registry: load_plugin_registry(configuration),
61
+ source_files: source_files,
62
+ **dependency_discovery_options(configuration)
63
+ )
64
+ rescue StandardError
65
+ bare(configuration)
66
+ end
67
+
68
+ # The dependency-discovery axes `check` reads off the configuration, as a keyword hash. Extracted so the
69
+ # list lives in one place: every command that types against the project's real dependencies passes exactly
70
+ # these, and adding an axis to `Environment.for_project` means adding it here once.
71
+ def dependency_discovery_options(configuration)
72
+ {
73
+ bundler_bundle_path: configuration.bundler_bundle_path,
74
+ bundler_auto_detect: configuration.bundler_auto_detect,
75
+ bundler_lockfile: configuration.bundler_lockfile,
76
+ rbs_collection_lockfile: configuration.rbs_collection_lockfile,
77
+ rbs_collection_auto_detect: configuration.rbs_collection_auto_detect
78
+ }
79
+ end
80
+
81
+ # Loads the project's configured plugins the same way the plugin-inspection commands (`rigor plugins`,
82
+ # `rigor doctor`) do: a `Plugin::Services` with no cache store (these commands recompute) driving
83
+ # `Plugin::Loader.load`. Returns `nil` — so `Environment.for_project` skips the plugin tier entirely — when
84
+ # the project declares no plugins.
85
+ def load_plugin_registry(configuration)
86
+ return nil if configuration.plugins.empty?
87
+
88
+ services = Plugin::Services.new(
89
+ reflection: Reflection,
90
+ type: Type::Combinator,
91
+ configuration: configuration,
92
+ cache_store: nil
93
+ )
94
+ registry = Plugin::Loader.load(configuration: configuration, services: services)
95
+ run_plugin_prepare(registry)
96
+ registry
97
+ end
98
+
99
+ # `Analysis::WorkerSession#run_plugin_prepare` runs every plugin's `#prepare(services)` before the real
100
+ # `check` pipeline reads `services.fact_store` — an ADR-9 fact published there (#921's `:factory_index`,
101
+ # `rigor-activerecord`'s `:model_index`, …) is otherwise invisible. The probe commands built on
102
+ # {ProjectEnvironment} share the same plugin registry and the same `read_fact` call sites, so they need
103
+ # the same step; skipping it made every cross-plugin fact silently read nil under `type-of` / `annotate`
104
+ # / `trace` / `sig-gen` even once a plugin published it correctly. Per-plugin `rescue` keeps one plugin's
105
+ # `#prepare` failure from costing the rest — the probes have no diagnostic pipeline to report it through,
106
+ # so it is swallowed the same way the rest of this module fails soft.
107
+ def run_plugin_prepare(registry)
108
+ return if registry.nil? || registry.empty?
109
+
110
+ registry.plugins.each do |plugin|
111
+ plugin.prepare(plugin.services)
112
+ rescue StandardError
113
+ next
114
+ end
115
+ end
116
+
117
+ # The first fail-soft floor: no plugin tier and no synthesized RBS, but still the project's own dependency
118
+ # sources — dropping those is what issue #821 was, so a plugin-loading failure must not cost them.
119
+ def bare(configuration)
120
+ Environment.for_project(
121
+ libraries: configuration.libraries,
122
+ signature_paths: configuration.signature_paths,
123
+ **dependency_discovery_options(configuration)
124
+ )
125
+ rescue StandardError
126
+ minimal(configuration)
127
+ end
128
+
129
+ # The last floor: RBS core plus the project's own signature paths, which cannot fail on anything outside
130
+ # the project itself.
131
+ def minimal(configuration)
132
+ Environment.for_project(
133
+ libraries: configuration.libraries,
134
+ signature_paths: configuration.signature_paths
135
+ )
136
+ end
137
+ end
138
+ end
@@ -10,7 +10,24 @@ module Rigor
10
10
  # A named class, not a bare RuntimeError: {MutationScanner#classify} already folds a per-mutant
11
11
  # `StandardError` into the `:harness_error` bucket, and the scanner's file-level rescue must catch THIS
12
12
  # and nothing else, so the two failure modes stay distinguishable at every site that handles either.
13
- class AnalyzerCrashed < StandardError; end
13
+ #
14
+ # Issue #790 — it says WHICH of the two refusals below fired, because they want different recovery. A
15
+ # crashed check rule left the Environment as it found it; an analyzer defect (the #784 HKT registry
16
+ # build) recorded itself ON that Environment and is memoised there, so the caller that owns the
17
+ # Environment has to replace it before the next mutant is scored. {#analyzer_defect?} is how a caller
18
+ # tells the two apart without re-deriving the classification from the message it was handed.
19
+ class AnalyzerCrashed < StandardError
20
+ def initialize(message = nil, analyzer_defect: false)
21
+ super(message)
22
+ @analyzer_defect = analyzer_defect
23
+ end
24
+
25
+ # True when the refusal was an analyzer defect rather than a crashed check rule — the shape whose
26
+ # cause outlives the run that observed it.
27
+ def analyzer_defect?
28
+ @analyzer_defect
29
+ end
30
+ end
14
31
 
15
32
  # The kill oracles' guard against judging a mutant by a run that never happened.
16
33
  #
@@ -27,34 +44,96 @@ module Rigor
27
44
  # let {MutationScanner} put the mutant in the `harness_errors` bucket (#264), which is already excluded
28
45
  # from `killed + survived` and already surfaced by the CLI. A crash then READS as a crash.
29
46
  #
30
- # Only {Analysis::Result#crashed?} is armed — the check-rule rescue, the one shape that REPLACES a
31
- # file's whole diagnostic list. Refusing more than that is the same error pointed the other way, and it
47
+ # Two shapes are armed, and only two.
48
+ #
49
+ # {Analysis::Result#crashed?} — the check-rule rescue, the one shape that REPLACES a file's whole
50
+ # diagnostic list. Refusing more than the shapes below is the same error pointed the other way, and it
32
51
  # was measured: on a project whose plugin `prepare` raises, refusing the `:plugin` row took a file from
33
52
  # `killed=1 survived=6` to `killed=0 survived=0 harness_errors=7` — the builtin rules had run, their
34
53
  # diagnostics were all present, and a real measurement was thrown away. `invoke_plugin_prepare` appends
35
54
  # its row to every sequential run, so arming it would have refused every run for the life of the
36
- # process. `:rbs_build` is excluded for the same reason plus one more: the site filter that admits
37
- # mutations already drops receivers whose type did not resolve, so a class whose definition failed to
38
- # build contributes no measured sites to begin with.
55
+ # process. The user-caused `:rbs_build` rungs are excluded for the same reason plus one more: the site
56
+ # filter that admits mutations already drops receivers whose type did not resolve, so a class whose
57
+ # definition failed to build contributes no measured sites to begin with.
58
+ #
59
+ # {Analysis::CrashSignature.analyzer_defect?} (issue #790) — the one `:rbs_build` rung whose cause is
60
+ # Rigor rather than the user's `sig/`: post-#788 a mutant that makes the implicit HKT scan raise no
61
+ # longer produces a per-file `internal analyzer error` but ONE readable `rbs.coverage.hkt-scan-failed`
62
+ # row, so `Result#crashed?` stays false and the mutant was scored as a measurement over a silently
63
+ # degraded universe — issue #776 was exactly such a mutant. The run is readable (every rule fired), so
64
+ # it stays out of `#crashed?`, which answers the user-facing "may I read these diagnostics?"; what this
65
+ # guard asks is the narrower "is this a valid measurement OF RIGOR?", and it is not.
66
+ #
67
+ # The refusal is asked off {Analysis::CrashSignature} rather than off a second string match here, so a
68
+ # reworded row or a fourth rung added to `ANALYZER_DEFECT_RULES` cannot disarm one copy and leave the
69
+ # others armed (#696).
70
+ #
71
+ # The defect half is asked TWICE, of two surfaces, because each covers the other's blind spot. The row
72
+ # is severity-stamped like every other diagnostic, so `severity_overrides:` resolving `rbs` (or the
73
+ # exact rule id) to `off` drops it from the run entirely — and a harness that exists to find analyzer
74
+ # defects must not be the one thing a project's config can hide one from. {Rigor::Environment} records
75
+ # the failure BEFORE any severity resolution, so `#hkt_scan_failure` still answers. Conversely that slot
76
+ # only ever describes THIS process's Environment: under the fork pool the defect happens in a worker's
77
+ # own Environment and reaches the parent as the row alone.
39
78
  module AnalysisGuard
40
79
  module_function
41
80
 
42
- # Takes the whole {Analysis::Result} rather than its diagnostics so the question is asked through
43
- # {Analysis::Result#crashed?} — the same predicate the spec-side guard reads, off the same
81
+ # Takes the whole {Analysis::Result} rather than its diagnostics so the crash question is asked
82
+ # through {Analysis::Result#crashed?} — the same predicate the spec-side guard reads, off the same
44
83
  # {Analysis::CrashSignature} table. Hands back the diagnostics, which is all a kill comparison wants.
45
84
  #
46
- # @param result [Rigor::Analysis::Result] one analysis run.
47
- # @param context [String] which oracle call produced it, so the raise points at the right seam.
48
- # @return [Array<Rigor::Analysis::Diagnostic>] the run's diagnostics, when the run was healthy.
49
- # @raise [AnalyzerCrashed]
50
- def checked(result, context:)
51
- return result.diagnostics unless result.crashed?
85
+ # @param result one analysis run.
86
+ # @param context which oracle call produced it, so the raise points at the right seam.
87
+ # @param environment the Environment the run was made over, read for the pre-severity record of a
88
+ # shared-build failure. Optional: a caller that has none loses only the severity-override case.
89
+ # @return the run's diagnostics, when the run was healthy.
90
+ # @raise AnalyzerCrashed
91
+ def checked(result, context:, environment: nil)
92
+ refuse_crash(result, context) if result.crashed?
93
+ defect = result.diagnostics.find { |diagnostic| Analysis::CrashSignature.analyzer_defect?(diagnostic) }
94
+ refuse_defect(context, Analysis::CrashSignature.describe(defect)) if defect
52
95
 
96
+ failure = shared_build_failure(environment)
97
+ refuse_defect(context, failure) if failure
98
+
99
+ result.diagnostics
100
+ end
101
+
102
+ # The pre-severity record, described the way {Analysis::CrashSignature.describe} describes a row, so
103
+ # the two refusals read alike whichever surface saw the defect.
104
+ def shared_build_failure(environment)
105
+ return nil unless environment.respond_to?(:hkt_scan_failure)
106
+
107
+ failure = environment.hkt_scan_failure
108
+ return nil if failure.nil?
109
+
110
+ error_class, message, frame, stage = failure
111
+ "#{stage} HKT registry build raised #{error_class}: #{message}#{" at #{frame}" if frame}"
112
+ end
113
+ private_class_method :shared_build_failure
114
+
115
+ def refuse_crash(result, context)
53
116
  raise AnalyzerCrashed,
54
117
  "#{context}: the analyzer crashed, so this run's diagnostics say nothing about the code " \
55
118
  "(#{Analysis::CrashSignature.describe(result.crash_diagnostics.first)}). A kill comparison " \
56
119
  "against it would score the mutant a survivor it was never measured against. See issue #686."
57
120
  end
121
+ private_class_method :refuse_crash
122
+
123
+ # The run is readable, so the message says what is wrong with it instead: the universe it was produced
124
+ # over is missing the type constructors the RBS `type` aliases and the loaded plugins would have
125
+ # contributed, and the BASELINE this mutant is compared against carries the identical degradation — so
126
+ # the set difference is empty for a reason that has nothing to do with the mutation.
127
+ def refuse_defect(context, description)
128
+ refusal = AnalyzerCrashed.new(
129
+ "#{context}: the analyzer degraded its own type universe, so this run is readable but is not a " \
130
+ "measurement of Rigor (#{description}). Scoring a mutant against it reports the analyzer's own " \
131
+ "defect as the mutant's survival. See issue #790.",
132
+ analyzer_defect: true
133
+ )
134
+ raise refusal
135
+ end
136
+ private_class_method :refuse_defect
58
137
  end
59
138
  end
60
139
  end