rigortype 0.3.8 → 0.4.0

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 (634) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/core_overlay/enumerable.rbs +51 -0
  5. data/data/core_overlay/enumerator.rbs +84 -0
  6. data/data/core_overlay/hash_rbs3.rbs +41 -0
  7. data/data/core_overlay/process.rbs +40 -0
  8. data/data/core_overlay/string_io.rbs +33 -0
  9. data/data/effects/core.yml +3 -3
  10. data/data/gem_overlay/activesupport/core_ext.rbs +236 -17
  11. data/docs/handbook/02-everyday-types.md +17 -14
  12. data/docs/handbook/03-narrowing.md +132 -14
  13. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  14. data/docs/handbook/05-methods-and-blocks.md +1 -1
  15. data/docs/handbook/06-classes.md +2 -2
  16. data/docs/handbook/07-rbs-and-extended.md +19 -1
  17. data/docs/handbook/10-sorbet.md +9 -10
  18. data/docs/handbook/11-sig-gen.md +525 -13
  19. data/docs/handbook/README.md +1 -1
  20. data/docs/handbook/appendix-elixir.md +2 -2
  21. data/docs/handbook/appendix-go.md +2 -2
  22. data/docs/handbook/appendix-java-csharp.md +2 -2
  23. data/docs/handbook/appendix-mypy.md +2 -2
  24. data/docs/handbook/appendix-phpstan.md +1 -1
  25. data/docs/handbook/appendix-rust.md +1 -1
  26. data/docs/handbook/appendix-type-theory.md +4 -4
  27. data/docs/handbook/appendix-typescript.md +1 -1
  28. data/docs/llms.txt +2 -0
  29. data/docs/manual/02-cli-reference.md +95 -6
  30. data/docs/manual/03-configuration.md +18 -0
  31. data/docs/manual/04-diagnostics.md +15 -0
  32. data/docs/manual/07-plugins.md +15 -5
  33. data/docs/manual/08-skills.md +23 -2
  34. data/docs/manual/10-mcp-server.md +3 -2
  35. data/docs/manual/11-ci.md +9 -0
  36. data/docs/manual/15-type-protection-coverage.md +8 -0
  37. data/docs/manual/16-rbs-extended-annotations.md +46 -8
  38. data/docs/manual/18-removing-dead-code.md +10 -8
  39. data/docs/manual/19-effect-labels.md +10 -0
  40. data/docs/manual/plugins/README.md +7 -0
  41. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  42. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  43. data/docs/manual/plugins/rigor-actionpack.md +261 -0
  44. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  45. data/docs/manual/plugins/rigor-activejob.md +7 -0
  46. data/docs/manual/plugins/rigor-activerecord.md +138 -4
  47. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  48. data/docs/manual/plugins/rigor-activesupport-core-ext.md +43 -7
  49. data/docs/manual/plugins/rigor-grape.md +106 -0
  50. data/docs/manual/plugins/rigor-graphql.md +23 -2
  51. data/docs/manual/plugins/rigor-pundit.md +8 -1
  52. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  53. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  54. data/docs/manual/plugins/rigor-rbs-inline.md +229 -1
  55. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  56. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  57. data/exe/rigor +19 -4
  58. data/lib/rigor/analysis/baseline.rb +3 -3
  59. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  60. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  61. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  62. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  63. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  64. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +6 -4
  65. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  66. data/lib/rigor/analysis/check_rules/rule_ids.rb +21 -6
  67. data/lib/rigor/analysis/check_rules/rule_walk.rb +41 -5
  68. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  69. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  70. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  71. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  72. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +38 -4
  73. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  74. data/lib/rigor/analysis/check_rules.rb +718 -79
  75. data/lib/rigor/analysis/crash_signature.rb +8 -24
  76. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  77. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  78. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  79. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  80. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  81. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  82. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  83. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  84. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  85. data/lib/rigor/analysis/fact_store.rb +15 -1
  86. data/lib/rigor/analysis/incremental.rb +42 -0
  87. data/lib/rigor/analysis/incremental_session.rb +117 -26
  88. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  89. data/lib/rigor/analysis/project_scan.rb +11 -1
  90. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  91. data/lib/rigor/analysis/reachability/plugin_roots.rb +5 -5
  92. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  93. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  94. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -2
  95. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  96. data/lib/rigor/analysis/result.rb +1 -3
  97. data/lib/rigor/analysis/rule_catalog.rb +131 -1
  98. data/lib/rigor/analysis/run_cache_key.rb +21 -12
  99. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  100. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  101. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  102. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +340 -33
  103. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  104. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +18 -14
  105. data/lib/rigor/analysis/runner/pool_coordinator.rb +357 -97
  106. data/lib/rigor/analysis/runner/project_pre_passes.rb +38 -13
  107. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  108. data/lib/rigor/analysis/runner.rb +407 -51
  109. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  110. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  111. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  112. data/lib/rigor/analysis/template_units.rb +399 -0
  113. data/lib/rigor/analysis/worker_session.rb +62 -20
  114. data/lib/rigor/bleeding_edge.rb +10 -24
  115. data/lib/rigor/builtins/hkt_builtins.rb +3 -2
  116. data/lib/rigor/builtins/imported_refinements.rb +159 -21
  117. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  118. data/lib/rigor/builtins/regex_refinement.rb +21 -13
  119. data/lib/rigor/builtins/static_return_refinements.rb +7 -6
  120. data/lib/rigor/cache/annotation_location.rb +2 -4
  121. data/lib/rigor/cache/descriptor.rb +63 -20
  122. data/lib/rigor/cache/engine_source.rb +33 -5
  123. data/lib/rigor/cache/file_digest.rb +9 -1
  124. data/lib/rigor/cache/incremental_snapshot.rb +72 -6
  125. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  126. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  127. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  128. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  129. data/lib/rigor/cache/rbs_descriptor.rb +112 -14
  130. data/lib/rigor/cache/rbs_environment.rb +8 -4
  131. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  132. data/lib/rigor/cache/store.rb +76 -32
  133. data/lib/rigor/ci_detector.rb +1 -0
  134. data/lib/rigor/cli/annotate_command.rb +5 -6
  135. data/lib/rigor/cli/check_command.rb +57 -10
  136. data/lib/rigor/cli/check_invocation.rb +6 -11
  137. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  138. data/lib/rigor/cli/coverage_command.rb +1 -1
  139. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  140. data/lib/rigor/cli/coverage_scan.rb +1 -5
  141. data/lib/rigor/cli/diff_command.rb +1 -1
  142. data/lib/rigor/cli/doc_links.rb +4 -4
  143. data/lib/rigor/cli/docs_command.rb +6 -6
  144. data/lib/rigor/cli/doctor_command.rb +85 -38
  145. data/lib/rigor/cli/effects_command.rb +2 -2
  146. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  147. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  148. data/lib/rigor/cli/explain_command.rb +34 -1
  149. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  150. data/lib/rigor/cli/lsp_command.rb +1 -1
  151. data/lib/rigor/cli/mcp_command.rb +1 -1
  152. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  153. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  154. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  155. data/lib/rigor/cli/plugin_command.rb +4 -4
  156. data/lib/rigor/cli/plugins_command.rb +2 -1
  157. data/lib/rigor/cli/prism_colorizer.rb +1 -2
  158. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  159. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  160. data/lib/rigor/cli/sig_gen_command.rb +212 -31
  161. data/lib/rigor/cli/skill_command.rb +2 -2
  162. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  163. data/lib/rigor/cli/skill_describe.rb +35 -35
  164. data/lib/rigor/cli/trace_command.rb +4 -4
  165. data/lib/rigor/cli/trace_renderer.rb +5 -6
  166. data/lib/rigor/cli/triage_command.rb +1 -1
  167. data/lib/rigor/cli/type_of_command.rb +29 -11
  168. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  169. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  170. data/lib/rigor/cli/type_scan_command.rb +4 -4
  171. data/lib/rigor/cli/unused_command.rb +11 -3
  172. data/lib/rigor/cli/upgrade_command.rb +1 -1
  173. data/lib/rigor/cli.rb +89 -9
  174. data/lib/rigor/config_audit.rb +30 -5
  175. data/lib/rigor/configuration/severity_profile.rb +25 -9
  176. data/lib/rigor/configuration.rb +109 -7
  177. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  178. data/lib/rigor/effects/attribution.rb +12 -5
  179. data/lib/rigor/effects/callee_rule.rb +368 -0
  180. data/lib/rigor/effects/catalog.rb +7 -4
  181. data/lib/rigor/effects/collector.rb +11 -5
  182. data/lib/rigor/effects/config_envelopes.rb +15 -10
  183. data/lib/rigor/effects/definition_context.rb +179 -0
  184. data/lib/rigor/effects/definition_lines.rb +25 -6
  185. data/lib/rigor/effects/effect_table.rb +11 -4
  186. data/lib/rigor/effects/entry_points.rb +0 -2
  187. data/lib/rigor/effects/envelope_check.rb +9 -9
  188. data/lib/rigor/effects/envelope_index.rb +20 -8
  189. data/lib/rigor/effects/file_collection.rb +59 -5
  190. data/lib/rigor/effects/framework_units.rb +5 -7
  191. data/lib/rigor/effects/identity.rb +20 -6
  192. data/lib/rigor/effects/inline_anchor.rb +7 -7
  193. data/lib/rigor/effects/label_intent.rb +3 -4
  194. data/lib/rigor/effects/liskov_check.rb +8 -8
  195. data/lib/rigor/effects/local_ownership.rb +38 -12
  196. data/lib/rigor/effects/method_key.rb +22 -1
  197. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  198. data/lib/rigor/effects/plugin_facts.rb +47 -37
  199. data/lib/rigor/effects/propagator.rb +297 -19
  200. data/lib/rigor/effects/registry.rb +1 -1
  201. data/lib/rigor/effects/scanner.rb +122 -73
  202. data/lib/rigor/effects/signature_sources.rb +4 -6
  203. data/lib/rigor/effects/snapshot.rb +12 -11
  204. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  205. data/lib/rigor/effects/summary.rb +27 -4
  206. data/lib/rigor/effects/unit_scan.rb +393 -38
  207. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  208. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  209. data/lib/rigor/effects/visibility.rb +101 -0
  210. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  211. data/lib/rigor/environment/class_registry.rb +3 -1
  212. data/lib/rigor/environment/failure_slot.rb +2 -2
  213. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  214. data/lib/rigor/environment/lockfile_resolver.rb +68 -4
  215. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  216. data/lib/rigor/environment/member_consistency.rb +298 -0
  217. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  218. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  219. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  220. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  221. data/lib/rigor/environment/rbs_loader.rb +674 -79
  222. data/lib/rigor/environment.rb +174 -24
  223. data/lib/rigor/flow_contribution/merger.rb +0 -2
  224. data/lib/rigor/flow_contribution.rb +11 -13
  225. data/lib/rigor/hashing/xxh3.rb +264 -0
  226. data/lib/rigor/inference/acceptance.rb +292 -17
  227. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  228. data/lib/rigor/inference/block_call_timing.rb +338 -0
  229. data/lib/rigor/inference/block_parameter_binder.rb +75 -30
  230. data/lib/rigor/inference/block_repetition.rb +71 -0
  231. data/lib/rigor/inference/body_fixpoint.rb +7 -6
  232. data/lib/rigor/inference/budget_trace.rb +3 -6
  233. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  234. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  235. data/lib/rigor/inference/captured_locals.rb +389 -18
  236. data/lib/rigor/inference/closure_escape_analyzer.rb +159 -17
  237. data/lib/rigor/inference/content_join.rb +200 -27
  238. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  239. data/lib/rigor/inference/def_return_typer.rb +11 -7
  240. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  241. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  242. data/lib/rigor/inference/element_read_widening.rb +180 -0
  243. data/lib/rigor/inference/error_info.rb +196 -0
  244. data/lib/rigor/inference/expression_typer.rb +2124 -468
  245. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  246. data/lib/rigor/inference/fork_map.rb +5 -7
  247. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  248. data/lib/rigor/inference/global_write_census.rb +239 -0
  249. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  250. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  251. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  252. data/lib/rigor/inference/hkt_registry.rb +4 -7
  253. data/lib/rigor/inference/index_write_widening.rb +16 -7
  254. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  255. data/lib/rigor/inference/jump_targets.rb +82 -0
  256. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  257. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  258. data/lib/rigor/inference/last_line.rb +340 -0
  259. data/lib/rigor/inference/last_status.rb +144 -0
  260. data/lib/rigor/inference/macro_block_self_type.rb +168 -21
  261. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  262. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  263. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  264. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  265. data/lib/rigor/inference/match_rebinding.rb +392 -0
  266. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  267. data/lib/rigor/inference/method_dispatcher/block_folding.rb +87 -33
  268. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  269. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  270. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  271. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  272. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  273. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  274. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  275. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  276. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  277. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  278. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  279. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  280. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +165 -111
  281. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  282. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  283. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  284. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +1046 -69
  285. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  286. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  287. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  288. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  289. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  290. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +129 -54
  291. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  292. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  293. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -6
  294. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  295. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  296. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  297. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  298. data/lib/rigor/inference/method_dispatcher.rb +225 -25
  299. data/lib/rigor/inference/method_parameter_binder.rb +12 -9
  300. data/lib/rigor/inference/multi_target_binder.rb +341 -55
  301. data/lib/rigor/inference/mutation_rejoin.rb +171 -0
  302. data/lib/rigor/inference/mutation_widening.rb +156 -85
  303. data/lib/rigor/inference/narrowing.rb +835 -162
  304. data/lib/rigor/inference/operand_effects.rb +167 -0
  305. data/lib/rigor/inference/operand_walk.rb +88 -0
  306. data/lib/rigor/inference/optimistic_origin.rb +153 -16
  307. data/lib/rigor/inference/origin_lookup.rb +2 -3
  308. data/lib/rigor/inference/parameter_inference_collector.rb +7 -7
  309. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  310. data/lib/rigor/inference/precision_scanner.rb +3 -4
  311. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  312. data/lib/rigor/inference/project_patched_methods.rb +9 -4
  313. data/lib/rigor/inference/project_patched_scanner.rb +10 -6
  314. data/lib/rigor/inference/protection_scanner.rb +1 -2
  315. data/lib/rigor/inference/range_constant.rb +57 -0
  316. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  317. data/lib/rigor/inference/receiver_alias.rb +93 -4
  318. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  319. data/lib/rigor/inference/refinement_mutation.rb +76 -0
  320. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  321. data/lib/rigor/inference/return_barrier.rb +54 -0
  322. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  323. data/lib/rigor/inference/scope_indexer.rb +5228 -627
  324. data/lib/rigor/inference/statement_evaluator.rb +3452 -595
  325. data/lib/rigor/inference/stored_block_call.rb +54 -0
  326. data/lib/rigor/inference/string_mutation.rb +97 -0
  327. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  328. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  329. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  330. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  331. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  332. data/lib/rigor/inference/version_guard.rb +28 -21
  333. data/lib/rigor/inference/void_origin.rb +3 -3
  334. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  335. data/lib/rigor/language_server/buffer_table.rb +4 -4
  336. data/lib/rigor/language_server/completion_provider.rb +1 -1
  337. data/lib/rigor/language_server/debouncer.rb +0 -1
  338. data/lib/rigor/language_server/diagnostic_publisher.rb +7 -6
  339. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  340. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  341. data/lib/rigor/language_server/hover_provider.rb +1 -1
  342. data/lib/rigor/language_server/hover_renderer.rb +4 -4
  343. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  344. data/lib/rigor/language_server/project_context.rb +7 -8
  345. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  346. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  347. data/lib/rigor/language_server/server.rb +9 -9
  348. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  349. data/lib/rigor/language_server/uri.rb +1 -1
  350. data/lib/rigor/mcp/server.rb +2 -1
  351. data/lib/rigor/plugin/base.rb +258 -26
  352. data/lib/rigor/plugin/box_probe.rb +91 -0
  353. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  354. data/lib/rigor/plugin/effect_attribution.rb +65 -11
  355. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  356. data/lib/rigor/plugin/fact_store.rb +6 -6
  357. data/lib/rigor/plugin/io_boundary.rb +93 -8
  358. data/lib/rigor/plugin/isolation.rb +29 -6
  359. data/lib/rigor/plugin/loader.rb +33 -13
  360. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  361. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  362. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  363. data/lib/rigor/plugin/manifest.rb +94 -11
  364. data/lib/rigor/plugin/registry.rb +76 -12
  365. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +21 -4
  366. data/lib/rigor/plugin/template_unit.rb +196 -0
  367. data/lib/rigor/plugin/trust_policy.rb +2 -4
  368. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  369. data/lib/rigor/plugin.rb +1 -0
  370. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  371. data/lib/rigor/project_environment.rb +138 -0
  372. data/lib/rigor/protection/analysis_guard.rb +93 -14
  373. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  374. data/lib/rigor/protection/dependency_closure.rb +5 -8
  375. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  376. data/lib/rigor/protection/discovery_seed.rb +14 -12
  377. data/lib/rigor/protection/kill_signature.rb +1 -4
  378. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  379. data/lib/rigor/protection/mutation_cache.rb +7 -11
  380. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  381. data/lib/rigor/protection/mutator.rb +2 -1
  382. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  383. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  384. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  385. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  386. data/lib/rigor/rbs_extended.rb +88 -21
  387. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  388. data/lib/rigor/reflection/constant_path.rb +139 -0
  389. data/lib/rigor/reflection.rb +93 -110
  390. data/lib/rigor/runtime/jit.rb +6 -8
  391. data/lib/rigor/scope/discovery_index.rb +165 -2
  392. data/lib/rigor/scope.rb +1041 -32
  393. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  394. data/lib/rigor/sig_gen/classification.rb +22 -5
  395. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  396. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  397. data/lib/rigor/sig_gen/generator.rb +673 -53
  398. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  399. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  400. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  401. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  402. data/lib/rigor/sig_gen/observation_collector.rb +18 -16
  403. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  404. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  405. data/lib/rigor/sig_gen/renderer.rb +128 -14
  406. data/lib/rigor/sig_gen/skip_reason_catalog.rb +154 -0
  407. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  408. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  409. data/lib/rigor/sig_gen/write_result.rb +19 -3
  410. data/lib/rigor/sig_gen/writer.rb +189 -41
  411. data/lib/rigor/sig_gen.rb +3 -0
  412. data/lib/rigor/signature_path_audit.rb +153 -6
  413. data/lib/rigor/source/literals.rb +0 -23
  414. data/lib/rigor/source/node_children.rb +0 -2
  415. data/lib/rigor/source/node_locator.rb +5 -11
  416. data/lib/rigor/source/node_walker.rb +0 -6
  417. data/lib/rigor/source/parameter_envelope.rb +72 -0
  418. data/lib/rigor/source.rb +1 -0
  419. data/lib/rigor/triage/catalogue.rb +1 -3
  420. data/lib/rigor/triage.rb +3 -5
  421. data/lib/rigor/type/accepts_result.rb +24 -6
  422. data/lib/rigor/type/combinator.rb +144 -5
  423. data/lib/rigor/type/data_class.rb +2 -2
  424. data/lib/rigor/type/data_instance.rb +4 -4
  425. data/lib/rigor/type/difference.rb +1 -0
  426. data/lib/rigor/type/float_range.rb +128 -0
  427. data/lib/rigor/type/hash_shape.rb +6 -6
  428. data/lib/rigor/type/integer_range.rb +13 -8
  429. data/lib/rigor/type/nominal.rb +8 -2
  430. data/lib/rigor/type/refined.rb +4 -3
  431. data/lib/rigor/type/struct_class.rb +3 -3
  432. data/lib/rigor/type/struct_instance.rb +4 -4
  433. data/lib/rigor/type.rb +1 -0
  434. data/lib/rigor/type_node/generic.rb +1 -1
  435. data/lib/rigor/type_node/range_literal.rb +25 -0
  436. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  437. data/lib/rigor/type_node.rb +1 -0
  438. data/lib/rigor/version.rb +1 -1
  439. data/lib/rigor.rb +1 -0
  440. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  441. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  442. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  443. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  444. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +10 -5
  445. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  446. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  447. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  448. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +14 -4
  449. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +32 -30
  450. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  451. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  452. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  453. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  454. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  455. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  456. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  457. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  458. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +264 -3
  459. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  460. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  461. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  462. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  463. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  464. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  465. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  466. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  467. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +2 -1
  468. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  469. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  470. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  471. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +10 -5
  472. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  473. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  474. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +383 -7
  475. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  476. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  477. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  478. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +322 -41
  479. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  480. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  481. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +26 -14
  482. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  483. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  484. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +290 -19
  485. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  486. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +3 -0
  487. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  488. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  489. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  490. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  491. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  492. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  493. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  494. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +2 -0
  495. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  496. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  497. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  498. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  499. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  500. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  501. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  502. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  503. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  504. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  505. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  506. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +78 -5
  507. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  508. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  509. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  510. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -3
  511. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  512. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  513. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  514. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  515. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  516. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +12 -7
  517. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  518. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  519. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  520. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +36 -37
  521. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  522. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -10
  523. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +5 -3
  524. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +3 -2
  525. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  526. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  527. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  528. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  529. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +10 -17
  530. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  531. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  532. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  533. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +324 -18
  534. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  535. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  536. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  537. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  538. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  539. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  540. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  541. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  542. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  543. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  544. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  545. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  546. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +2 -1
  547. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  548. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  549. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +10 -5
  550. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  551. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  552. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  553. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  554. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  555. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  556. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +3 -5
  557. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  558. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +51 -6
  559. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  560. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  561. data/sig/rigor/analysis/baseline.rbs +69 -7
  562. data/sig/rigor/analysis/fact_store.rbs +1 -1
  563. data/sig/rigor/analysis/project_scan.rbs +74 -0
  564. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  565. data/sig/rigor/cache.rbs +2 -3
  566. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  567. data/sig/rigor/effects/effect_table.rbs +60 -0
  568. data/sig/rigor/effects/envelope.rbs +97 -0
  569. data/sig/rigor/effects/envelope_index.rbs +33 -0
  570. data/sig/rigor/effects/file_collection.rbs +81 -0
  571. data/sig/rigor/effects/label.rbs +26 -0
  572. data/sig/rigor/effects/label_set.rbs +46 -0
  573. data/sig/rigor/effects/method_key.rbs +24 -0
  574. data/sig/rigor/effects/origin.rbs +50 -0
  575. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  576. data/sig/rigor/effects/registry.rbs +68 -0
  577. data/sig/rigor/effects/summary.rbs +50 -0
  578. data/sig/rigor/effects/taint_cause.rbs +12 -0
  579. data/sig/rigor/environment.rbs +17 -10
  580. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  581. data/sig/rigor/inference.rbs +9 -14
  582. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  583. data/sig/rigor/plugin/base.rbs +30 -7
  584. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  585. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  586. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  587. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  588. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  589. data/sig/rigor/plugin/loader.rbs +3 -3
  590. data/sig/rigor/plugin/manifest.rbs +51 -11
  591. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  592. data/sig/rigor/plugin/registry.rbs +62 -1
  593. data/sig/rigor/plugin.rbs +1 -1
  594. data/sig/rigor/rbs_extended.rbs +1 -1
  595. data/sig/rigor/reflection.rbs +9 -6
  596. data/sig/rigor/scope.rbs +148 -12
  597. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +21 -0
  598. data/sig/rigor/source.rbs +4 -4
  599. data/sig/rigor/testing.rbs +10 -4
  600. data/sig/rigor/type.rbs +33 -1
  601. data/sig/rigor.rbs +39 -20
  602. data/skills/rigor-ask/SKILL.md +16 -11
  603. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  604. data/skills/rigor-ci-setup/SKILL.md +17 -21
  605. data/skills/rigor-doctor/SKILL.md +24 -22
  606. data/skills/rigor-doctor/references/01-checks.md +97 -33
  607. data/skills/rigor-editor-setup/SKILL.md +6 -4
  608. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  609. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  610. data/skills/rigor-next-steps/SKILL.md +4 -2
  611. data/skills/rigor-plugin-author/SKILL.md +19 -23
  612. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  613. data/skills/rigor-plugin-author/references/02-walker-and-types.md +25 -27
  614. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  615. data/skills/rigor-plugin-review/SKILL.md +6 -4
  616. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  617. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  618. data/skills/rigor-project-init/SKILL.md +15 -10
  619. data/skills/rigor-project-init/references/01-detect.md +13 -9
  620. data/skills/rigor-project-init/references/02-configure.md +43 -14
  621. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  622. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  623. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  624. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  625. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  626. data/skills/rigor-type-oracle/SKILL.md +226 -0
  627. data/skills/rigor-type-oracle/references/01-oracle-commands.md +265 -0
  628. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  629. data/skills/rigor-type-oracle/references/03-gap-protocol.md +125 -0
  630. data/skills/rigor-unused-adjudicate/SKILL.md +12 -7
  631. data/skills/rigor-upgrade/SKILL.md +13 -8
  632. metadata +133 -3
  633. data/lib/rigor/cli/probe_environment.rb +0 -85
  634. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -43,7 +43,6 @@ module Rigor
43
43
  @base_classes = base_classes.to_set { |name| strip_root(name.to_s) }
44
44
  end
45
45
 
46
- # @return [ChannelIndex]
47
46
  def discover
48
47
  entries = []
49
48
  ruby_files_under(@search_paths).each do |path|
@@ -39,8 +39,6 @@ module Rigor
39
39
  # `"::ChatChannel"`; see {ChannelDiscoverer}), while a QUERY may legitimately arrive rooted:
40
40
  # `::ChatChannel.broadcast_to(...)` renders its receiver as `"::ChatChannel"`. The root marker is
41
41
  # dropped here, once, so no caller needs a `find(name) || find("::#{name}")` retry (#621).
42
- #
43
- # @return [Entry, nil]
44
42
  def find(class_name)
45
43
  @by_name[strip_leading_namespace(class_name.to_s)]
46
44
  end
@@ -29,6 +29,7 @@ module Rigor
29
29
 
30
30
  # The `_later` twins enqueue an ActiveJob that broadcasts, so they carry the enqueue meaning too.
31
31
  TURBO_LATER_SELECTORS = TURBO_SELECTORS.map { |name| "#{name.delete_suffix('_to')}_later_to" }.freeze
32
+ Ractor.make_shareable(TURBO_LATER_SELECTORS)
32
33
 
33
34
  LATER = (BROADCASTING + ["rails.activejob.enqueue", "job.enqueue"]).freeze
34
35
 
@@ -65,6 +65,7 @@ module Rigor
65
65
  class Actioncable < Rigor::Plugin::Base
66
66
  manifest(
67
67
  id: "actioncable",
68
+ target_gems: ["actioncable"],
68
69
  # Bumped 2026-09-02 (#621) — channel keys and base-class names are de-rooted at the producer and a
69
70
  # reopened class's actions and streams are UNIONed rather than clobbered, so a cached 0.1.0 index
70
71
  # can be missing a whole channel (`< ::ApplicationCable::Channel`) or half its stream names.
@@ -122,12 +123,12 @@ module Rigor
122
123
  @protocol_contracts || manifest.protocol_contracts
123
124
  end
124
125
 
125
- # File-level only: the load-error emission. Per-call broadcast validation runs over the
126
+ # File-level only: the load-error disclosure. Per-call broadcast validation runs over the
126
127
  # engine-owned walk via the node_rule below (ADR-37). The channel index is lazily loaded +
127
128
  # memoised by `producer_value`, shared by both surfaces.
128
129
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
129
130
  index = producer_value(:channel_index)
130
- return [load_error_diagnostic(path)] if index.nil? && producer_error(:channel_index)
131
+ disclose_load_error if index.nil? && producer_error(:channel_index)
131
132
 
132
133
  []
133
134
  end
@@ -150,10 +151,14 @@ module Rigor
150
151
  "#{base}/**/*.rb"
151
152
  end
152
153
 
153
- def load_error_diagnostic(path)
154
+ # Issue #1056 — "the channel index did not load" is a fact about the run's INPUTS, not about the file being
155
+ # analysed, so it is registered rather than returned: see {Plugin::Base#disclose_once} for the
156
+ # channel, the `(plugin id, key)` de-duplication and why the engine positions it at `.rigor.yml:1:1`.
157
+ # Returned from the per-file hook it carried no once-guard at all and repeated on every file.
158
+ def disclose_load_error
154
159
  error = producer_error(:channel_index)
155
- Rigor::Analysis::Diagnostic.new(
156
- path: path, line: 1, column: 1,
160
+ disclose_once(
161
+ :channel_index_load_failed,
157
162
  message: "rigor-actioncable: failed to discover channels: #{error.class}: #{error.message}",
158
163
  severity: :warning,
159
164
  rule: "load-error"
@@ -42,10 +42,6 @@ module Rigor
42
42
 
43
43
  # The mailer-call violations for a single call node (0..2), or `[]` when it is not a
44
44
  # `<Mailer>.action(...)` call on a known mailer. ADR-37: the engine owns the walk.
45
- #
46
- # @param call_node [Prism::Node]
47
- # @param mailer_index [MailerIndex]
48
- # @return [Array<Violation>]
49
45
  def violations_for(call_node:, mailer_index:)
50
46
  return [] unless call_node.is_a?(Prism::CallNode) && action_call_candidate?(call_node)
51
47
 
@@ -38,10 +38,9 @@ module Rigor
38
38
  VIEW_FORMATS = %w[html text].freeze
39
39
  VIEW_EXTENSIONS = %w[erb haml slim].freeze
40
40
 
41
- # @param io_boundary [Rigor::Plugin::IoBoundary]
42
- # @param search_paths [Array<String>] absolute or project-relative paths to scan for mailers.
43
- # @param base_classes [Array<String>] direct superclasses that mark a class as a mailer.
44
- # @param views_root [String] absolute or project-relative path to the views directory (typically
41
+ # @param search_paths — absolute or project-relative paths to scan for mailers.
42
+ # @param base_classes — direct superclasses that mark a class as a mailer.
43
+ # @param views_root — absolute or project-relative path to the views directory (typically
45
44
  # `app/views`).
46
45
  def initialize(io_boundary:, search_paths:, base_classes:, views_root: DEFAULT_VIEWS_ROOT)
47
46
  @io_boundary = io_boundary
@@ -53,7 +52,6 @@ module Rigor
53
52
  @views_root = views_root
54
53
  end
55
54
 
56
- # @return [MailerIndex]
57
55
  def discover
58
56
  # Two-pass: first collect every module's defs (for the include-following step), then build
59
57
  # per-class entries that pull in actions from include'd modules. GitLab's `Notify` mailer
@@ -58,8 +58,6 @@ module Rigor
58
58
  # `"::UserMailer"`; see {MailerDiscoverer}), while a QUERY may legitimately arrive rooted:
59
59
  # `::UserMailer.welcome(u)` renders its receiver as `"::UserMailer"`. The root marker is dropped
60
60
  # here, once, so no caller needs a `find(name) || find("::#{name}")` retry (#621).
61
- #
62
- # @return [ClassEntry, nil]
63
61
  def find(class_name)
64
62
  @by_name[strip_leading_namespace(class_name.to_s)]
65
63
  end
@@ -68,9 +66,8 @@ module Rigor
68
66
  @by_name.key?(strip_leading_namespace(class_name.to_s))
69
67
  end
70
68
 
71
- # @param file_path [String] absolute path of a mailer file (canonicalised — see plugin entry's
69
+ # @param file_path — absolute path of a mailer file (canonicalised — see plugin entry's
72
70
  # `harvest`)
73
- # @return [ClassEntry, nil]
74
71
  def find_by_file(file_path)
75
72
  @entries.find { |entry| entry.file_path == file_path }
76
73
  end
@@ -43,6 +43,7 @@ module Rigor
43
43
  class Actionmailer < Rigor::Plugin::Base
44
44
  manifest(
45
45
  id: "actionmailer",
46
+ target_gems: ["actionmailer"],
46
47
  # Bumped 2026-05-28 — extended RESERVED_CLASS_METHODS to include `respond_to?` / `public_send` /
47
48
  # `send` / `__send__` / `method` and friends so dynamic-dispatch idioms
48
49
  # (`Mailer.respond_to?(action)` / `Mailer.public_send(action)`) stop firing `unknown-action`
@@ -92,7 +93,10 @@ module Rigor
92
93
  # loaded + memoised, shared.
93
94
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
94
95
  index = producer_value(:mailer_index)
95
- return [load_error_diagnostic(path)] if index.nil? && producer_error(:mailer_index)
96
+ if index.nil? && producer_error(:mailer_index)
97
+ disclose_load_error
98
+ return []
99
+ end
96
100
  return [] if index.nil? || index.empty?
97
101
 
98
102
  missing_view_diagnostics(path, index)
@@ -135,10 +139,16 @@ module Rigor
135
139
  File.expand_path(path)
136
140
  end
137
141
 
138
- def load_error_diagnostic(path)
142
+ # Issue #1056 — "the mailer index did not load" is a fact about the run's INPUTS, not about the file being
143
+ # analysed, so it is registered rather than returned: see {Plugin::Base#disclose_once} for the
144
+ # channel, the `(plugin id, key)` de-duplication and why the engine positions it at `.rigor.yml:1:1`.
145
+ # Returned from the per-file hook it carried no once-guard at all and repeated on every file.
146
+ # The missing-view rows this hook still returns are NOT disclosures: they name the mailer's own
147
+ # source file.
148
+ def disclose_load_error
139
149
  error = producer_error(:mailer_index)
140
- Rigor::Analysis::Diagnostic.new(
141
- path: path, line: 1, column: 1,
150
+ disclose_once(
151
+ :mailer_index_load_failed,
142
152
  message: "rigor-actionmailer: failed to discover mailers: #{error.class}: #{error.message}",
143
153
  severity: :warning,
144
154
  rule: "load-error"
@@ -81,10 +81,8 @@ module Rigor
81
81
  # duplicates on Mastodon and ~119 on Redmine, exactly stacked with the rails-routes diagnostics.
82
82
  # Returns `[]` for unknown / arity-mismatch shapes.
83
83
  #
84
- # @param call_node [Prism::Node]
85
- # @param helper_table [Hash{String => Hash}] each entry carries `name`, `arity`,
84
+ # @param helper_table — each entry carries `name`, `arity`,
86
85
  # `acceptable_arities`, `path`, `http_method`, `action`.
87
- # @return [Array<Violation>]
88
86
  def helper_violations_for(call_node:, helper_table:)
89
87
  return [] unless implicit_helper_call?(call_node)
90
88
 
@@ -108,10 +106,7 @@ module Rigor
108
106
  # each filter name reference is validated against it. Calls outside a known controller
109
107
  # contribute nothing.
110
108
  #
111
- # @param call_node [Prism::Node]
112
- # @param ancestors [Array<Prism::Node>] the lexical ancestor chain
113
- # @param controller_index [ControllerIndex]
114
- # @return [Array<Violation>]
109
+ # @param ancestors — the lexical ancestor chain
115
110
  def filter_violations_for(call_node:, ancestors:, controller_index:)
116
111
  return [] unless filter_call?(call_node)
117
112
 
@@ -144,10 +139,6 @@ module Rigor
144
139
  # AR model's column list (looked up via the model_index fact published by `rigor-activerecord`).
145
140
  # Calls whose `:require` argument is a non-literal Symbol are passed through; namespaced models
146
141
  # (`params.require(:admin_user)` → `Admin::User`) are deferred.
147
- #
148
- # @param call_node [Prism::Node]
149
- # @param model_index [Hash{String => Hash}]
150
- # @return [Array<Violation>]
151
142
  def permit_violations_for(call_node:, model_index:)
152
143
  return [] unless permit_chain?(call_node)
153
144
 
@@ -179,13 +170,12 @@ module Rigor
179
170
  # method that doesn't call `render`) is also skipped — Phase 3 validates explicit renders only,
180
171
  # since the implicit path would false-positive on `redirect_to` / `head` / early returns.
181
172
  #
182
- # @param call_node [Prism::Node]
183
- # @param ancestors [Array<Prism::Node>] the lexical ancestor chain
184
- # @param path [String] file being analysed
185
- # @param view_search_roots [Array<String>]
186
- # @param controller_index [ControllerIndex, nil]
187
- # @return [Array<Violation>]
188
- def render_violations_for(call_node:, ancestors:, path:, view_search_roots:, controller_index: nil)
173
+ # @param ancestors — the lexical ancestor chain
174
+ # @param io_boundary — the plugin's {Rigor::Plugin::IoBoundary}; #629 — every filesystem
175
+ # question this phase asks goes through it, so "no such template" is a recorded dependency and
176
+ # the run-result cache invalidates once the template appears
177
+ def render_violations_for(call_node:, ancestors:, path:, view_search_roots:, io_boundary:,
178
+ controller_index: nil)
189
179
  return [] unless render_call?(call_node)
190
180
 
191
181
  class_name = enclosing_controller_name(ancestors)
@@ -217,12 +207,12 @@ module Rigor
217
207
  # `app/views/admin/settings/` holds about/, appearance/, … but no top-level templates). Skip
218
208
  # render checks for these — the diagnostic would be a false positive against intentional Rails
219
209
  # abstract-base layouts.
220
- return [] if abstract_base_controller?(class_name, controller_path, view_search_roots)
210
+ return [] if abstract_base_controller?(class_name, controller_path, view_search_roots, io_boundary)
221
211
 
222
212
  target = render_target_for(call_node, controller_path)
223
213
  return [] if target.nil?
224
214
 
225
- violation = render_violation(call_node, target, view_search_roots)
215
+ violation = render_violation(call_node, target, view_search_roots, io_boundary)
226
216
  violation ? [violation] : []
227
217
  end
228
218
 
@@ -406,15 +396,16 @@ module Rigor
406
396
  # controllers.
407
397
  # Deliberately NOT triggered by "no view directory at all" because that fires the diagnostic we
408
398
  # DO want for genuinely-missing views (the typo / forgot-to-create case).
409
- def abstract_base_controller?(class_name, controller_path, view_search_roots)
399
+ def abstract_base_controller?(class_name, controller_path, view_search_roots, io_boundary)
410
400
  return true if class_name.end_with?("BaseController")
411
401
 
412
402
  view_search_roots.any? do |root|
413
403
  dir = File.join(root, controller_path)
414
- next false unless File.directory?(dir)
415
-
416
- entries = Dir.children(dir)
417
- entries.any? && entries.all? { |e| File.directory?(File.join(dir, e)) }
404
+ # #629 — the listing row this records is the same one {#locate_template} needs, and the
405
+ # boundary deduplicates it per directory, so the whole render phase costs one glob
406
+ # validation per consulted view directory on a warm run.
407
+ entries = io_boundary.list_directory(dir)
408
+ entries.any? && entries.all? { |e| File.directory?(e) }
418
409
  end
419
410
  end
420
411
 
@@ -473,9 +464,9 @@ module Rigor
473
464
  Rigor::Plugin::Inflector.underscore(stripped)
474
465
  end
475
466
 
476
- def render_violation(call_node, target, view_search_roots)
467
+ def render_violation(call_node, target, view_search_roots, io_boundary)
477
468
  kind, relative = target
478
- existing = locate_template(relative, view_search_roots)
469
+ existing = locate_template(relative, view_search_roots, io_boundary)
479
470
  if existing
480
471
  render_target_violation(call_node, kind, relative, existing)
481
472
  else
@@ -483,11 +474,22 @@ module Rigor
483
474
  end
484
475
  end
485
476
 
486
- def locate_template(relative, view_search_roots)
477
+ # #629 — resolved from the DIRECTORY LISTING, not from a probe per candidate path. The old shape
478
+ # asked `File.file?` up to `extensions × roots` times per `render` call and recorded nothing, so a
479
+ # cached run kept reporting `plugin.actionpack.missing-template` after the template was written.
480
+ # One {Rigor::Plugin::IoBoundary#list_directory} row per `app/views/<controller>` directory carries
481
+ # the dependency instead: any file added or removed there invalidates the run, and the warm-run
482
+ # validation count is the number of view directories the run consulted rather than the number of
483
+ # candidate paths it tried.
484
+ def locate_template(relative, view_search_roots, io_boundary)
485
+ basename = File.basename(relative)
487
486
  view_search_roots.each do |root|
487
+ # Names, not paths: the boundary answers in absolute paths while `root` may be the relative
488
+ # `app/views` the user configured, and the reported candidate keeps the configured spelling.
489
+ listing = io_boundary.list_directory(File.join(root, File.dirname(relative)))
490
+ names = listing.map { |entry| File.basename(entry) }
488
491
  RENDER_TEMPLATE_EXTENSIONS.each do |ext|
489
- candidate = File.join(root, "#{relative}#{ext}")
490
- return candidate if File.file?(candidate)
492
+ return File.join(root, "#{relative}#{ext}") if names.include?("#{basename}#{ext}")
491
493
  end
492
494
  end
493
495
  nil
@@ -42,7 +42,6 @@ module Rigor
42
42
  @search_paths = search_paths
43
43
  end
44
44
 
45
- # @return [ControllerIndex]
46
45
  def discover
47
46
  entries = {}
48
47
  ruby_files_under(@search_paths).each do |path|
@@ -42,8 +42,6 @@ module Rigor
42
42
  # `"Admin::AccountsController"` — never `"::AccountsController"`; see {ControllerDiscoverer}),
43
43
  # while a QUERY may legitimately arrive rooted. The root marker is dropped at every lookup entry
44
44
  # point, once, so no caller needs a `find(name) || find("::#{name}")` retry (#621).
45
- #
46
- # @return [Entry, nil]
47
45
  def find(class_name)
48
46
  @entries[strip_leading_namespace(class_name)]
49
47
  end
@@ -84,7 +82,7 @@ module Rigor
84
82
  methods.uniq.freeze
85
83
  end
86
84
 
87
- # @return [Boolean] true when the class has at least one include OR parent class we couldn't
85
+ # @return true when the class has at least one include OR parent class we couldn't
88
86
  # resolve in the index (typically a gem-shipped concern such as Devise's
89
87
  # `Devise::Controllers::Helpers`, or a gem-shipped parent controller such as
90
88
  # `Devise::ConfirmationsController` or `Doorkeeper::AuthorizedApplicationsController`). Phase
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rigor/source/node_children"
4
+
5
+ require "prism"
6
+
7
+ module Rigor
8
+ module Plugin
9
+ class Actionpack < Rigor::Plugin::Base
10
+ # The syntactic controller walk two parent-side indexes share: {ViewAssigns}, which reads what a
11
+ # controller action ASSIGNED, and {RenderLocals} (#1047), which reads what a render site PASSED.
12
+ #
13
+ # Both run on the parent before any analysis — `#template_units_for_file` is called before the
14
+ # engine has indexed anything — so neither may ask the typer a question, and both need the same four
15
+ # answers off a Prism tree: which class declarations in this file are controllers, what their
16
+ # qualified names are, which view directory each one owns, and which `def`s sit directly in the body.
17
+ # Keeping one copy is what stops the two indexes from disagreeing about, say, whether
18
+ # `module Admin; class UsersController` is `admin/users` — a disagreement that would be silent, since
19
+ # each side would simply find nothing.
20
+ #
21
+ # It deliberately stops at the shapes {ControllerDiscoverer} already recognises. Anything a
22
+ # controller does at runtime — `define_method`, a concern that adds actions on include — is not read
23
+ # here and is not read there either.
24
+ module ControllerScan
25
+ module_function
26
+
27
+ # Yields `[class_node, enclosing_namespace_segments]` for every `ClassNode` in the tree, with the
28
+ # enclosing `module` chain accumulated. Mirrors {ControllerDiscoverer#walk_declarations}'s
29
+ # qualification reduced to the two shapes that can carry an action: `class Admin::UsersController`
30
+ # and `module Admin; class UsersController`.
31
+ def each_controller(node, namespace, &)
32
+ return unless node.is_a?(Prism::Node)
33
+
34
+ if node.is_a?(Prism::ClassNode) || node.is_a?(Prism::ModuleNode)
35
+ yield node, namespace if node.is_a?(Prism::ClassNode)
36
+ inner = namespace + constant_segments(node.constant_path)
37
+ each_controller(node.body, inner, &) if node.body
38
+ return
39
+ end
40
+
41
+ node.rigor_each_child { |child| each_controller(child, namespace, &) }
42
+ end
43
+
44
+ def constant_segments(path)
45
+ case path
46
+ when Prism::ConstantReadNode then [path.name.to_s]
47
+ when Prism::ConstantPathNode then constant_segments(path.parent) + [path.name.to_s]
48
+ else []
49
+ end
50
+ end
51
+
52
+ # `["Admin", "UsersController"]` → `"admin/users"`. `ApplicationController` and the other abstract
53
+ # bases are not excluded: an action they define really is inherited, and a template under their own
54
+ # path simply never exists. nil for a class whose name is not a controller's.
55
+ def controller_path(segments)
56
+ return nil unless segments.last&.end_with?("Controller")
57
+
58
+ parts = segments.map { |segment| underscore(segment) }
59
+ parts[-1] = parts[-1].sub(/_controller\z/, "")
60
+ return nil if parts[-1].empty?
61
+
62
+ parts.join("/")
63
+ end
64
+
65
+ # The ASCII-only inflection this needs: a controller constant is a CamelCase identifier, and
66
+ # nothing here has to invert an irregular plural (`ActiveSupport::Inflector` is the analysed
67
+ # project's, not Rigor's).
68
+ def underscore(segment)
69
+ segment.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
70
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
71
+ .downcase
72
+ end
73
+
74
+ # `{ :show => body_node }` for every `def` directly in the class body.
75
+ def method_bodies(node)
76
+ body = node.body
77
+ return {} if body.nil?
78
+
79
+ body.child_nodes.compact.filter_map do |child|
80
+ next nil unless child.is_a?(Prism::DefNode) && child.receiver.nil?
81
+
82
+ [child.name, child.body]
83
+ end.to_h
84
+ end
85
+
86
+ # Every implicit-self `render` call node under `node`, at any depth.
87
+ def each_render(node, &)
88
+ return unless node.is_a?(Prism::Node)
89
+
90
+ yield node if node.is_a?(Prism::CallNode) && node.receiver.nil? && node.name == :render
91
+ node.rigor_each_child { |child| each_render(child, &) }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -31,6 +31,11 @@ module Rigor
31
31
  module Effects
32
32
  CONTROLLER = "ActionController::Base"
33
33
 
34
+ # The view context a template unit's body runs under — the `self_type:` the plugin declares on
35
+ # every {Rigor::Plugin::TemplateUnit}. A `render` inside a template is an implicit-self call on
36
+ # it, which is what lets one row colour the template → partial edge (#1048).
37
+ VIEW = "ActionView::Base"
38
+
34
39
  RESPONSE = ["mutate.self", "rails.response.write"].freeze
35
40
  SESSION_WRITE = ["mutate", "rails.session.write"].freeze
36
41
  SESSION_READ = ["io", "rails.session.read"].freeze
@@ -46,6 +51,26 @@ module Rigor
46
51
  # the one genuinely misleading row in the whole Rails layer.
47
52
  RENDERERS = %w[render render_to_string render_to_body].freeze
48
53
 
54
+ # Rails' own name for the implicit render — what `ActionController::Base` calls when an action
55
+ # returns without having answered. The project never writes it, which is exactly the point: the
56
+ # row exists so the engine's unit-level {Rigor::Effects::CalleeRule} has a declaration to read,
57
+ # and it carries no labels because an action that implicitly renders has already been coloured by
58
+ # nothing at all and must not be coloured by a guess (see `why:`).
59
+ IMPLICIT_RENDER = "default_render"
60
+
61
+ # Action View's own format lookup order for a template-side render, as DATA the engine's
62
+ # `rails_render_partial` rule copies onto the edge (#1065). While a `.js.erb` template renders, the
63
+ # lookup context holds `[:js, :html]` — `LookupContext#formats=` appends `:html` to a lone `:js` —
64
+ # so `render "watchers"` runs `_watchers.js.erb` where it exists and `_watchers.html.erb`
65
+ # otherwise. The propagator takes the first key a unit answers.
66
+ #
67
+ # `js` is the only entry, because it is the only fallback Action View itself hard-codes. What a
68
+ # `.json`, `.xml` or `.turbo_stream` template falls back to is whatever the REQUEST's formats
69
+ # happened to be (`prepend_formats` puts the template's format in front of them) — an `Accept`
70
+ # header the source never states, Turbo's included — so those keep their taint rather than guess.
71
+ FORMAT_FALLBACKS = { "js" => ["html"] }.freeze
72
+ Ractor.make_shareable(FORMAT_FALLBACKS)
73
+
49
74
  # The cookie jars a Rails app writes through.
50
75
  COOKIE_JARS = ["self.cookies", "self.cookies.signed", "self.cookies.encrypted",
51
76
  "self.cookies.permanent"].freeze
@@ -59,35 +84,67 @@ module Rigor
59
84
  def response_rows
60
85
  RESPONSE_WRITERS.map do |selector|
61
86
  EffectAttribution.new(
62
- receiver: CONTROLLER, method: selector, labels: RESPONSE, discharge: true,
87
+ receiver: CONTROLLER, method: selector, labels: RESPONSE, discharge: true, responds: true,
63
88
  why: "sets the response on the controller instance — Rack writes the socket later, outside " \
64
89
  "any project method, so this is `mutate.self` and deliberately not `io`"
65
90
  )
66
- end + render_rows
91
+ end + render_rows + [implicit_render_row]
67
92
  end
68
93
 
94
+ # `render` and its two `*_to_string` twins. Each carries a `callee:` so the render site edges to
95
+ # the template's own effect unit (#1048), and each keeps the `template-not-analysed` taint — which
96
+ # now rides the edge: a render whose template produced a unit discharges it, and one whose
97
+ # template the plugin declined (a layout, #1047) or whose target is computed keeps it.
98
+ #
99
+ # Only `render` sets `responds:`. `render_to_string` builds a string and leaves the response
100
+ # unanswered, so an action that calls it and returns still takes Rails' implicit render.
69
101
  def render_rows
70
102
  RENDERERS.map do |selector|
71
103
  EffectAttribution.new(
72
104
  receiver: CONTROLLER, method: selector, labels: RESPONSE, discharge: true,
73
- taint: "template-not-analysed",
105
+ taint: "template-not-analysed", callee: "rails_render", responds: selector == "render",
74
106
  why: "sets the response body from a template. The controller half is fully stated; the " \
75
- "template's own effects are unknown until views become effect units, and the taint " \
76
- "is how the summary says so rather than reading exhaustive"
107
+ "template's own effects reach it through the `rails_render` callee edge, and the " \
108
+ "taint survives on the edge for a target the rule cannot resolve"
77
109
  )
78
- end
110
+ end + [
111
+ EffectAttribution.new(
112
+ receiver: VIEW, method: :render, labels: [], discharge: true,
113
+ taint: "template-not-analysed", callee: "rails_render_partial",
114
+ callee_fallbacks: FORMAT_FALLBACKS,
115
+ why: "a `render` inside a template runs another template. The edge is the whole " \
116
+ "contribution — what a partial render DOES is what the partial does, and the row " \
117
+ "reaches it now — while a target the rule cannot resolve keeps the taint. In a view " \
118
+ "a bare argument names a PARTIAL and `layout:` names one too, which is why this is a " \
119
+ "separate rule from the controller's"
120
+ )
121
+ ]
122
+ end
123
+
124
+ # The implicit render, as an EDGE and nothing else. An action that never answered still renders
125
+ # `<controller>/<action>`, so its summary must include that template's effects — but the fact is
126
+ # the absence of a call, so there is no site to colour, and inventing a `rails.response.write`
127
+ # here would put it on every private helper a controller defines as well.
128
+ def implicit_render_row
129
+ EffectAttribution.new(
130
+ receiver: CONTROLLER, method: IMPLICIT_RENDER, labels: [], discharge: true,
131
+ callee: "rails_implicit_render",
132
+ why: "Rails renders `<controller>/<action>` for an action that answered nothing. The edge " \
133
+ "is the whole contribution: no label, because an implicit render is observed from a " \
134
+ "body that made no call, and a label read off an absence would colour every helper"
135
+ )
79
136
  end
80
137
 
81
138
  # `send_file` streams from disk; `send_data` does not.
82
139
  def file_rows
83
140
  [
84
141
  EffectAttribution.new(
85
- receiver: CONTROLLER, method: :send_data, labels: RESPONSE, discharge: true,
142
+ receiver: CONTROLLER, method: :send_data, labels: RESPONSE, discharge: true, responds: true,
86
143
  why: "sets the response body from an in-memory string"
87
144
  ),
88
145
  EffectAttribution.new(
89
146
  receiver: CONTROLLER, method: :send_file, labels: RESPONSE + ["io.fs.read"], discharge: true,
90
- why: "sets the response AND reads the named file off disk"
147
+ responds: true, why: "sets the response AND reads the named file off disk"
91
148
  )
92
149
  ]
93
150
  end