rigortype 0.2.7 → 0.2.8

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 (414) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/docs/manual/04-diagnostics.md +7 -4
  4. data/docs/manual/06-baseline.md +5 -2
  5. data/docs/manual/15-type-protection-coverage.md +6 -4
  6. data/lib/rigor/analysis/baseline.rb +56 -96
  7. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  8. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  9. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  10. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  11. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  12. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  13. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  14. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  15. data/lib/rigor/analysis/check_rules.rb +174 -274
  16. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  17. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  18. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  19. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  20. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  21. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  22. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  23. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  24. data/lib/rigor/analysis/diagnostic.rb +48 -75
  25. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  26. data/lib/rigor/analysis/fact_store.rb +12 -22
  27. data/lib/rigor/analysis/incremental.rb +47 -63
  28. data/lib/rigor/analysis/incremental_session.rb +74 -100
  29. data/lib/rigor/analysis/project_scan.rb +12 -23
  30. data/lib/rigor/analysis/result.rb +3 -5
  31. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  32. data/lib/rigor/analysis/run_stats.rb +38 -61
  33. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  34. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  35. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  36. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  37. data/lib/rigor/analysis/runner.rb +220 -333
  38. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  39. data/lib/rigor/analysis/worker_session.rb +64 -102
  40. data/lib/rigor/ast/type_node.rb +6 -8
  41. data/lib/rigor/ast.rb +6 -10
  42. data/lib/rigor/bleeding_edge.rb +27 -35
  43. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  44. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  45. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  46. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  47. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  48. data/lib/rigor/cache/descriptor.rb +58 -94
  49. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  50. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  51. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  52. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  53. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  54. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  55. data/lib/rigor/cache/rbs_environment.rb +11 -19
  56. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  57. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  58. data/lib/rigor/cache/store.rb +96 -151
  59. data/lib/rigor/cli/annotate_command.rb +51 -88
  60. data/lib/rigor/cli/baseline_command.rb +10 -17
  61. data/lib/rigor/cli/check_command.rb +121 -199
  62. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  63. data/lib/rigor/cli/ci_detector.rb +9 -14
  64. data/lib/rigor/cli/command.rb +8 -14
  65. data/lib/rigor/cli/coverage_command.rb +19 -26
  66. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  67. data/lib/rigor/cli/coverage_report.rb +0 -2
  68. data/lib/rigor/cli/coverage_scan.rb +7 -11
  69. data/lib/rigor/cli/diagnostic_formats.rb +43 -62
  70. data/lib/rigor/cli/diff_command.rb +10 -20
  71. data/lib/rigor/cli/docs_command.rb +26 -40
  72. data/lib/rigor/cli/doctor_command.rb +8 -9
  73. data/lib/rigor/cli/explain_command.rb +4 -7
  74. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  75. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  76. data/lib/rigor/cli/lsp_command.rb +11 -19
  77. data/lib/rigor/cli/mcp_command.rb +4 -6
  78. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  79. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  80. data/lib/rigor/cli/options.rb +11 -19
  81. data/lib/rigor/cli/plugin_command.rb +18 -30
  82. data/lib/rigor/cli/plugins_command.rb +29 -51
  83. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  84. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  85. data/lib/rigor/cli/protection_renderer.rb +5 -7
  86. data/lib/rigor/cli/protection_report.rb +27 -17
  87. data/lib/rigor/cli/renderable.rb +4 -6
  88. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  89. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  90. data/lib/rigor/cli/skill_command.rb +30 -47
  91. data/lib/rigor/cli/skill_describe.rb +40 -64
  92. data/lib/rigor/cli/trace_command.rb +9 -16
  93. data/lib/rigor/cli/trace_renderer.rb +35 -51
  94. data/lib/rigor/cli/triage_command.rb +6 -10
  95. data/lib/rigor/cli/triage_renderer.rb +1 -2
  96. data/lib/rigor/cli/type_of_command.rb +14 -23
  97. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  98. data/lib/rigor/cli/type_scan_command.rb +6 -10
  99. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  100. data/lib/rigor/cli/type_scan_report.rb +2 -3
  101. data/lib/rigor/cli/upgrade_command.rb +2 -3
  102. data/lib/rigor/cli.rb +16 -28
  103. data/lib/rigor/config_audit.rb +35 -44
  104. data/lib/rigor/configuration/dependencies.rb +41 -70
  105. data/lib/rigor/configuration/severity_profile.rb +28 -42
  106. data/lib/rigor/configuration.rb +150 -241
  107. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  108. data/lib/rigor/environment/class_registry.rb +19 -26
  109. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  110. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  111. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  112. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  113. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  114. data/lib/rigor/environment/rbs_loader.rb +420 -518
  115. data/lib/rigor/environment/reflection.rb +28 -53
  116. data/lib/rigor/environment/reporters.rb +13 -24
  117. data/lib/rigor/environment.rb +176 -291
  118. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  119. data/lib/rigor/flow_contribution/element.rb +7 -12
  120. data/lib/rigor/flow_contribution/fact.rb +33 -57
  121. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  122. data/lib/rigor/flow_contribution/merger.rb +32 -47
  123. data/lib/rigor/flow_contribution.rb +37 -55
  124. data/lib/rigor/inference/acceptance.rb +133 -219
  125. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  126. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  127. data/lib/rigor/inference/budget_trace.rb +43 -63
  128. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  129. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  130. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  131. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  132. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  133. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  134. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  135. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  136. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  137. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  138. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  139. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  140. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  141. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  142. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  143. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  144. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  145. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  146. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  147. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  148. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  149. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  150. data/lib/rigor/inference/def_return_typer.rb +8 -14
  151. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  152. data/lib/rigor/inference/expression_typer.rb +774 -1152
  153. data/lib/rigor/inference/fallback.rb +8 -12
  154. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  155. data/lib/rigor/inference/flow_tracer.rb +25 -36
  156. data/lib/rigor/inference/hkt_body.rb +45 -68
  157. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  158. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  159. data/lib/rigor/inference/hkt_registry.rb +50 -76
  160. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  161. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  162. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  163. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  164. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  165. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  166. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  167. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  168. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  169. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  170. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  171. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  172. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  173. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  174. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  175. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  176. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  177. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  178. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  179. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  180. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  181. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  182. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  183. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  184. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  185. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  186. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  187. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  188. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  189. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  190. data/lib/rigor/inference/mutation_widening.rb +115 -158
  191. data/lib/rigor/inference/narrowing.rb +509 -727
  192. data/lib/rigor/inference/origin_lookup.rb +38 -0
  193. data/lib/rigor/inference/parameter_inference_collector.rb +100 -100
  194. data/lib/rigor/inference/precision_scanner.rb +26 -35
  195. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  196. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  197. data/lib/rigor/inference/protection_scanner.rb +16 -18
  198. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  199. data/lib/rigor/inference/scope_indexer.rb +473 -817
  200. data/lib/rigor/inference/statement_evaluator.rb +706 -1111
  201. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  202. data/lib/rigor/inference/synthetic_method.rb +9 -16
  203. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  204. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  205. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  206. data/lib/rigor/language_server/buffer_table.rb +11 -18
  207. data/lib/rigor/language_server/completion_provider.rb +69 -116
  208. data/lib/rigor/language_server/debouncer.rb +16 -25
  209. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  210. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  211. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  212. data/lib/rigor/language_server/hover_provider.rb +8 -11
  213. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  214. data/lib/rigor/language_server/loop.rb +14 -19
  215. data/lib/rigor/language_server/project_context.rb +46 -74
  216. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  217. data/lib/rigor/language_server/server.rb +55 -83
  218. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  219. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  220. data/lib/rigor/language_server/uri.rb +8 -13
  221. data/lib/rigor/language_server.rb +4 -6
  222. data/lib/rigor/mcp/loop.rb +2 -3
  223. data/lib/rigor/mcp/server.rb +4 -7
  224. data/lib/rigor/mcp.rb +3 -6
  225. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  226. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  227. data/lib/rigor/plugin/base.rb +335 -518
  228. data/lib/rigor/plugin/blueprint.rb +14 -23
  229. data/lib/rigor/plugin/box.rb +18 -29
  230. data/lib/rigor/plugin/fact_store.rb +16 -26
  231. data/lib/rigor/plugin/inflector.rb +37 -53
  232. data/lib/rigor/plugin/io_boundary.rb +33 -56
  233. data/lib/rigor/plugin/isolation.rb +42 -55
  234. data/lib/rigor/plugin/load_error.rb +10 -15
  235. data/lib/rigor/plugin/loader.rb +30 -49
  236. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  237. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  238. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  239. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  240. data/lib/rigor/plugin/macro.rb +10 -15
  241. data/lib/rigor/plugin/manifest.rb +85 -144
  242. data/lib/rigor/plugin/node_context.rb +14 -22
  243. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  244. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  245. data/lib/rigor/plugin/registry.rb +132 -205
  246. data/lib/rigor/plugin/services.rb +21 -33
  247. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  248. data/lib/rigor/plugin/trust_policy.rb +24 -38
  249. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  250. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  251. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  252. data/lib/rigor/protection/mutator.rb +50 -70
  253. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  254. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  255. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  256. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  257. data/lib/rigor/rbs_extended.rb +107 -197
  258. data/lib/rigor/reflection.rb +68 -86
  259. data/lib/rigor/scope/discovery_index.rb +14 -19
  260. data/lib/rigor/scope.rb +255 -310
  261. data/lib/rigor/sig_gen/classification.rb +6 -10
  262. data/lib/rigor/sig_gen/generator.rb +197 -323
  263. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  264. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  265. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  266. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  267. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  268. data/lib/rigor/sig_gen/renderer.rb +7 -13
  269. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  270. data/lib/rigor/sig_gen/write_result.rb +8 -16
  271. data/lib/rigor/sig_gen/writer.rb +95 -174
  272. data/lib/rigor/sig_gen.rb +3 -6
  273. data/lib/rigor/signature_path_audit.rb +24 -30
  274. data/lib/rigor/source/constant_path.rb +10 -14
  275. data/lib/rigor/source/literals.rb +31 -45
  276. data/lib/rigor/source/node_locator.rb +9 -11
  277. data/lib/rigor/source/node_walker.rb +9 -13
  278. data/lib/rigor/source.rb +3 -4
  279. data/lib/rigor/testing.rb +16 -20
  280. data/lib/rigor/triage/catalogue.rb +38 -62
  281. data/lib/rigor/triage.rb +31 -52
  282. data/lib/rigor/trinary.rb +9 -13
  283. data/lib/rigor/type/acceptance_router.rb +4 -6
  284. data/lib/rigor/type/accepts_result.rb +10 -14
  285. data/lib/rigor/type/app.rb +19 -27
  286. data/lib/rigor/type/bot.rb +4 -6
  287. data/lib/rigor/type/bound_method.rb +10 -15
  288. data/lib/rigor/type/combinator.rb +165 -257
  289. data/lib/rigor/type/constant.rb +23 -34
  290. data/lib/rigor/type/data_class.rb +10 -15
  291. data/lib/rigor/type/data_instance.rb +14 -20
  292. data/lib/rigor/type/difference.rb +21 -32
  293. data/lib/rigor/type/dynamic.rb +3 -5
  294. data/lib/rigor/type/hash_shape.rb +32 -18
  295. data/lib/rigor/type/integer_range.rb +11 -16
  296. data/lib/rigor/type/intersection.rb +27 -42
  297. data/lib/rigor/type/nominal.rb +10 -15
  298. data/lib/rigor/type/plain_lattice.rb +9 -13
  299. data/lib/rigor/type/refined.rb +67 -114
  300. data/lib/rigor/type/singleton.rb +4 -6
  301. data/lib/rigor/type/struct_class.rb +11 -16
  302. data/lib/rigor/type/struct_instance.rb +15 -21
  303. data/lib/rigor/type/tuple.rb +14 -19
  304. data/lib/rigor/type/union.rb +30 -42
  305. data/lib/rigor/type_node/generic.rb +14 -26
  306. data/lib/rigor/type_node/identifier.rb +12 -19
  307. data/lib/rigor/type_node.rb +3 -12
  308. data/lib/rigor/value_semantics.rb +16 -21
  309. data/lib/rigor/version.rb +1 -1
  310. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  311. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  312. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  313. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  314. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  315. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  316. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  317. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  318. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +118 -176
  319. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  320. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  321. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +71 -108
  322. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  323. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  324. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  325. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  326. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +30 -47
  327. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +79 -129
  328. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +36 -57
  329. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  330. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  331. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +148 -235
  332. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  333. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  334. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  335. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  336. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  337. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  338. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  339. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  340. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  341. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  342. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  343. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  344. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  345. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  346. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  347. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  348. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  349. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  350. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  351. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  352. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  353. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  354. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  355. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  356. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  357. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  358. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  359. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  360. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  361. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  362. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  363. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  364. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  365. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  366. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  367. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  368. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  369. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  370. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  371. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  372. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  373. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  374. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  375. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  376. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  377. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  378. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +42 -74
  379. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +280 -497
  380. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +54 -89
  381. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  382. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  383. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  384. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  385. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  386. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  387. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  388. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  389. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  390. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  391. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  392. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  393. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  394. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  395. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  396. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  397. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  398. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  399. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  400. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  401. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  402. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  403. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  404. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  405. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  406. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  407. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  408. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  409. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  410. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  411. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  412. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  413. data/sig/rigor/scope.rbs +6 -0
  414. metadata +2 -1
@@ -2,27 +2,19 @@
2
2
 
3
3
  module Rigor
4
4
  module Plugin
5
- # ADR-32 WD6 — per-run accumulator for failures encountered by
6
- # a plugin's `Manifest#source_rbs_synthesizer` callable. The
7
- # synthesizer returns `[:error, message]` on parse failure
8
- # (per its contract); `Environment.for_project` routes the
9
- # tuple through `#record` here. `Analysis::Runner` queries
10
- # `#entries` after analysis and emits one
11
- # `source-rbs-synthesis-failed` `:info` diagnostic per
12
- # entry so the user sees which files contributed nothing
13
- # and why.
5
+ # ADR-32 WD6 — per-run accumulator for failures encountered by a plugin's
6
+ # `Manifest#source_rbs_synthesizer` callable. The synthesizer returns `[:error, message]` on parse failure
7
+ # (per its contract); `Environment.for_project` routes the tuple through `#record` here.
8
+ # `Analysis::Runner` queries `#entries` after analysis and emits one `source-rbs-synthesis-failed` `:info`
9
+ # diagnostic per entry so the user sees which files contributed nothing and why.
14
10
  #
15
- # Empty by default. The Runner only emits diagnostics when
16
- # at least one entry is recorded — projects without
17
- # synthesizer-emitting plugins pay zero cost.
11
+ # Empty by default. The Runner only emits diagnostics when at least one entry is recorded — projects
12
+ # without synthesizer-emitting plugins pay zero cost.
18
13
  #
19
- # Thread-/Ractor-safety: this reporter is per-`WorkerSession`
20
- # in pool mode, so concurrent writes from one Ractor's
21
- # `collect_virtual_rbs` loop are serialised by the worker
22
- # body itself. The sequential path shares a single reporter
23
- # across the run; entries are appended one at a time during
24
- # env build (before any per-file analysis runs), so no
25
- # locking is needed.
14
+ # Thread-/Ractor-safety: this reporter is per-`WorkerSession` in pool mode, so concurrent writes from one
15
+ # Ractor's `collect_virtual_rbs` loop are serialised by the worker body itself. The sequential path shares a
16
+ # single reporter across the run; entries are appended one at a time during env build (before any per-file
17
+ # analysis runs), so no locking is needed.
26
18
  class SourceRbsSynthesisReporter
27
19
  Entry = Data.define(:plugin_id, :path, :message)
28
20
 
@@ -2,42 +2,30 @@
2
2
 
3
3
  module Rigor
4
4
  module Plugin
5
- # Declarative trust / I/O policy for the active plugin set.
6
- # Pinned by [ADR-2 § "Plugin Trust and I/O Policy"](../../../docs/adr/2-extension-api.md):
7
- # plugins are *trusted Ruby gems selected by the user, their
8
- # Gemfile, or project configuration*; this class is the
9
- # programmatic surface that documents that trust and lets the
10
- # analyzer enforce read scope + network disablement at the
5
+ # Declarative trust / I/O policy for the active plugin set. Pinned by
6
+ # [ADR-2 § "Plugin Trust and I/O Policy"](../../../docs/adr/2-extension-api.md): plugins are *trusted Ruby
7
+ # gems selected by the user, their Gemfile, or project configuration*; this class is the programmatic
8
+ # surface that documents that trust and lets the analyzer enforce read scope + network disablement at the
11
9
  # documented edges.
12
10
  #
13
- # The policy is **not a sandbox.** A plugin that uses raw
14
- # `File.read` or `Net::HTTP` bypasses the policy ADR-2
15
- # explicitly chooses documentation over forced isolation. The
16
- # contract is: when plugins go through {Rigor::Plugin::IoBoundary}
17
- # (the analyzer-side helper service slice 2 introduces), the
18
- # boundary checks against this policy and feeds compliant reads
19
- # into the cache descriptor for invalidation. Slices 3-6 wire
20
- # plugin contributions through the boundary so the policy is
21
- # the actual mechanism, not just paperwork.
11
+ # The policy is **not a sandbox.** A plugin that uses raw `File.read` or `Net::HTTP` bypasses the policy —
12
+ # ADR-2 explicitly chooses documentation over forced isolation. The contract is: when plugins go through
13
+ # {Rigor::Plugin::IoBoundary} (the analyzer-side helper service slice 2 introduces), the boundary checks
14
+ # against this policy and feeds compliant reads into the cache descriptor for invalidation. Slices 3-6 wire
15
+ # plugin contributions through the boundary so the policy is the actual mechanism, not just paperwork.
22
16
  #
23
17
  # ## Fields
24
18
  #
25
- # - `trusted_gems`: gem names the user has authorised. Derived
26
- # from the `plugins:` section of `.rigor.yml` plus any gems
27
- # they reach transitively. Used today for documentation and
28
- # future trust diagnostics.
29
- # - `allowed_read_roots`: absolute paths plugin code may read
30
- # from through the {IoBoundary}. The default set covers the
31
- # project root, the project's `signature_paths`, the active
32
- # `Gemfile.lock`, and each trusted gem's
33
- # `Gem::Specification#full_gem_path`. The user extends this
34
- # with `.rigor.yml`'s `plugins_io.allowed_paths:`.
35
- # - `network_policy`: one of {VALID_NETWORK_POLICIES}.
36
- # `:disabled` (default) makes {IoBoundary#open_url} always
37
- # raise. `:allowlist` (v0.1.2) consults `allowed_url_hosts`
38
- # on every fetch — the hostname must be on the list and
39
- # the URL scheme MUST be `https`. The list of allowed hosts
40
- # is exact-match (no wildcards in v0.1.2).
19
+ # - `trusted_gems`: gem names the user has authorised. Derived from the `plugins:` section of `.rigor.yml`
20
+ # plus any gems they reach transitively. Used today for documentation and future trust diagnostics.
21
+ # - `allowed_read_roots`: absolute paths plugin code may read from through the {IoBoundary}. The default
22
+ # set covers the project root, the project's `signature_paths`, the active `Gemfile.lock`, and each
23
+ # trusted gem's `Gem::Specification#full_gem_path`. The user extends this with `.rigor.yml`'s
24
+ # `plugins_io.allowed_paths:`.
25
+ # - `network_policy`: one of {VALID_NETWORK_POLICIES}. `:disabled` (default) makes
26
+ # {IoBoundary#open_url} always raise. `:allowlist` (v0.1.2) consults `allowed_url_hosts` on every fetch —
27
+ # the hostname must be on the list and the URL scheme MUST be `https`. The list of allowed hosts is
28
+ # exact-match (no wildcards in v0.1.2).
41
29
  class TrustPolicy
42
30
  VALID_NETWORK_POLICIES = %i[disabled allowlist].freeze
43
31
 
@@ -58,10 +46,9 @@ module Rigor
58
46
  end
59
47
 
60
48
  # @param path [String]
61
- # @return [Boolean] true when the absolute path falls inside
62
- # any allowed read root. Symlinks are resolved through
63
- # `File.expand_path` only (no `realpath`); plugins with
64
- # adversarial intent are out of scope per ADR-2.
49
+ # @return [Boolean] true when the absolute path falls inside any allowed read root. Symlinks are
50
+ # resolved through `File.expand_path` only (no `realpath`); plugins with adversarial intent are out of
51
+ # scope per ADR-2.
65
52
  def allow_read?(path)
66
53
  absolute = File.expand_path(path.to_s)
67
54
  @allowed_read_roots.any? { |root| inside?(absolute, root) }
@@ -72,9 +59,8 @@ module Rigor
72
59
  end
73
60
 
74
61
  # @param url [String, URI]
75
- # @return [Boolean] true when the URL scheme is `https` and
76
- # the parsed hostname is in `allowed_url_hosts`. Always
77
- # `false` while `network_policy` is `:disabled`.
62
+ # @return [Boolean] true when the URL scheme is `https` and the parsed hostname is in
63
+ # `allowed_url_hosts`. Always `false` while `network_policy` is `:disabled`.
78
64
  def allow_url?(url)
79
65
  return false if @network_policy == :disabled
80
66
  return false if @allowed_url_hosts.empty?
@@ -2,18 +2,14 @@
2
2
 
3
3
  module Rigor
4
4
  module Plugin
5
- # Plugin-supplied resolver for custom named / generic type
6
- # vocabulary in RBS::Extended payloads. ADR-13 § "Decision".
5
+ # Plugin-supplied resolver for custom named / generic type vocabulary in RBS::Extended payloads. ADR-13 §
6
+ # "Decision".
7
7
  #
8
- # Subclasses override {#resolve} to return a
9
- # {Rigor::Type::Base} when the node matches the vocabulary
10
- # the resolver covers, or `nil` to fall through to the next
11
- # resolver in the chain (and finally to the built-in / RBS
12
- # fallback). The base implementation returns `nil` so an
13
- # unimplemented subclass is a safe no-op.
8
+ # Subclasses override {#resolve} to return a {Rigor::Type::Base} when the node matches the vocabulary the
9
+ # resolver covers, or `nil` to fall through to the next resolver in the chain (and finally to the built-in /
10
+ # RBS fallback). The base implementation returns `nil` so an unimplemented subclass is a safe no-op.
14
11
  #
15
- # Resolvers are registered through their plugin's manifest
16
- # under the `type_node_resolvers:` slot:
12
+ # Resolvers are registered through their plugin's manifest under the `type_node_resolvers:` slot:
17
13
  #
18
14
  # class RigorTypescriptUtilityTypes < Rigor::Plugin::Base
19
15
  # manifest(
@@ -24,24 +20,19 @@ module Rigor
24
20
  # )
25
21
  # end
26
22
  #
27
- # ADR-13 — base class, manifest hook, and registry aggregation
28
- # for plugin-contributed type-node resolvers. Resolvers declared
29
- # via `manifest(type_node_resolvers:)` run for every real
30
- # `%a{rigor:v1:...}` payload through `TypeNode::ResolverChain`
31
- # (built by `Environment#build_name_scope` from
23
+ # ADR-13 — base class, manifest hook, and registry aggregation for plugin-contributed type-node resolvers.
24
+ # Resolvers declared via `manifest(type_node_resolvers:)` run for every real `%a{rigor:v1:...}` payload
25
+ # through `TypeNode::ResolverChain` (built by `Environment#build_name_scope` from
32
26
  # `Plugin::Registry#type_node_resolvers`).
33
27
  #
34
- # Resolvers SHOULD be stateless and re-entrant; the registry
35
- # builds the chain once per `Analysis::Runner.run` and may
36
- # consult any resolver multiple times for the same node.
28
+ # Resolvers SHOULD be stateless and re-entrant; the registry builds the chain once per
29
+ # `Analysis::Runner.run` and may consult any resolver multiple times for the same node.
37
30
  class TypeNodeResolver
38
- # @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic]
39
- # the parser-emitted node the chain is asking about.
40
- # @param scope [Rigor::TypeNode::NameScope] companion
41
- # value object (slice 3); slice 2 invocations MAY pass
31
+ # @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic] the parser-emitted node the chain is
32
+ # asking about.
33
+ # @param scope [Rigor::TypeNode::NameScope] companion value object (slice 3); slice 2 invocations MAY pass
42
34
  # `nil` because the chain doesn't exist yet.
43
- # @return [Rigor::Type::Base, nil] resolved type, or `nil`
44
- # to fall through.
35
+ # @return [Rigor::Type::Base, nil] resolved type, or `nil` to fall through.
45
36
  def resolve(node, scope) # rubocop:disable Lint/UnusedMethodArgument
46
37
  nil
47
38
  end
@@ -4,18 +4,16 @@ require_relative "../analysis/runner"
4
4
 
5
5
  module Rigor
6
6
  module Protection
7
- # ADR-69 Seam 1 — the **kill oracle** Rigor's analyzer-teeth measurement uses:
8
- # a mutant is killed iff re-analysing it introduces a diagnostic absent from
9
- # the clean baseline. This is exactly the behaviour ADR-62/63 shipped, lifted
10
- # out of {MutationScanner} so a {TestSuiteOracle} (ADR-70)which kills by
11
- # *running tests* rather than by re-analysis — can sit beside it without the
12
- # scanner baking in either assumption.
7
+ # ADR-69 Seam 1 — the **kill oracle** Rigor's analyzer-teeth measurement uses: a mutant is killed iff
8
+ # re-analysing it introduces a diagnostic absent from the clean baseline. This is exactly the behaviour
9
+ # ADR-62/63 shipped, lifted out of {MutationScanner} so a {TestSuiteOracle} (ADR-70) which kills by
10
+ # *running tests* rather than by re-analysiscan sit beside it without the scanner baking in either
11
+ # assumption.
13
12
  #
14
- # The expensive builds (RBS environment + the whole-project pre-pass scan) are
15
- # paid once by the caller and threaded in; each mutant reuses them through
16
- # `Runner.new(prebuilt:)#run_source` (in-memory overlay, no disk write).
17
- # Passing `prebuilt:` disables the run-result cache (whose key digests the
18
- # *disk* file), so a mutant is never served a stale clean hit.
13
+ # The expensive builds (RBS environment + the whole-project pre-pass scan) are paid once by the caller and
14
+ # threaded in; each mutant reuses them through `Runner.new(prebuilt:)#run_source` (in-memory overlay, no
15
+ # disk write). Passing `prebuilt:` disables the run-result cache (whose key digests the *disk* file), so a
16
+ # mutant is never served a stale clean hit.
19
17
  class DiagnosticOracle
20
18
  def initialize(configuration:, environment:, project_scan:)
21
19
  @configuration = configuration
@@ -23,8 +21,8 @@ module Rigor
23
21
  @project_scan = project_scan
24
22
  end
25
23
 
26
- # The clean per-file baseline: the diagnostic signatures a mutant must add
27
- # to count as killed. Computed once per file by the caller.
24
+ # The clean per-file baseline: the diagnostic signatures a mutant must add to count as killed. Computed
25
+ # once per file by the caller.
28
26
  def baseline(source:, path:)
29
27
  analyse(source, path).to_set { |d| sig(d) }
30
28
  end
@@ -7,24 +7,20 @@ require_relative "diagnostic_oracle"
7
7
 
8
8
  module Rigor
9
9
  module Protection
10
- # ADR-63 Tier 2 — the mutation *effectiveness* tier (the truth tier behind
11
- # Tier 1's static {Inference::ProtectionScanner} proxy). For one file it
12
- # answers the question Tier 1 only bounds: when a type-visible bug is
13
- # introduced at a dispatch site, does Rigor actually catch it?
10
+ # ADR-63 Tier 2 — the mutation *effectiveness* tier (the truth tier behind Tier 1's static
11
+ # {Inference::ProtectionScanner} proxy). For one file it answers the question Tier 1 only bounds: when a
12
+ # type-visible bug is introduced at a dispatch site, does Rigor actually catch it?
14
13
  #
15
- # Mechanism (the ADR-62 warm loop, narrowed to per-file measurement):
16
- # generate the type-visible mutations ({Mutator}), keep only those whose
17
- # receiver Rigor holds a concrete type for (the type-aware filter the
18
- # FP-safe meaning-maker; an unresolved receiver is kept), then for each ask
19
- # the **kill oracle** whether the mutant is caught. The oracle is the ADR-69
20
- # seam: {#scan_file} uses the {DiagnosticOracle} (a *new Rigor diagnostic* =
21
- # a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the
22
- # type-survivors (ADR-70 — the dynamic protection axis).
14
+ # Mechanism (the ADR-62 warm loop, narrowed to per-file measurement): generate the type-visible mutations
15
+ # ({Mutator}), keep only those whose receiver Rigor holds a concrete type for (the type-aware filter — the
16
+ # FP-safe meaning-maker; an unresolved receiver is kept), then for each ask the **kill oracle** whether the
17
+ # mutant is caught. The oracle is the ADR-69 seam: {#scan_file} uses the {DiagnosticOracle} (a *new Rigor
18
+ # diagnostic* = a kill); {#scan_file_fused} additionally consults a {TestSuiteOracle} on the type-survivors
19
+ # (ADR-70 the dynamic protection axis).
23
20
  #
24
- # The expensive builds (RBS environment + the whole-project pre-pass scan)
25
- # are paid ONCE by the caller and threaded into the {DiagnosticOracle}; each
26
- # mutant reuses them through `Runner.new(prebuilt:)#run_source` (in-memory
27
- # overlay, no disk write).
21
+ # The expensive builds (RBS environment + the whole-project pre-pass scan) are paid ONCE by the caller and
22
+ # threaded into the {DiagnosticOracle}; each mutant reuses them through `Runner.new(prebuilt:)#run_source`
23
+ # (in-memory overlay, no disk write).
28
24
  class MutationScanner
29
25
  # A surviving mutation site — a breakage Rigor did not catch.
30
26
  SurvivingSite = Data.define(:line, :receiver, :method_name, :operator)
@@ -33,21 +29,19 @@ module Rigor
33
29
  # Mutations actually analysed (parse-invalid mutants are not counted).
34
30
  def total = killed + survived
35
31
 
36
- # Effectiveness ratio; a file with no type-relevant mutation is
37
- # vacuously fully effective (no breakage was available to miss).
32
+ # Effectiveness ratio; a file with no type-relevant mutation is vacuously fully effective (no breakage
33
+ # was available to miss).
38
34
  def ratio = total.zero? ? 1.0 : killed.to_f / total
39
35
  end
40
36
 
41
- # ADR-70 — one type-survivor classified by the dynamic (test) axis.
42
- # `protection` is `:test` (a test caught it) or `:none` (unprotected — the
43
- # "add a type OR a test here" sites).
37
+ # ADR-70 — one type-survivor classified by the dynamic (test) axis. `protection` is `:test` (a test caught
38
+ # it) or `:none` (unprotected — the "add a type OR a test here" sites).
44
39
  FusedSite = Data.define(:line, :receiver, :method_name, :operator, :protection)
45
40
 
46
- # ADR-70 — the per-file fused classification. The gradual short-circuit
47
- # collapses the conceptual "doubly-protected" bucket into `type_killed`:
48
- # a mutant the type checker already kills never reaches the suite, because
49
- # the static net already suffices and re-running the suite to learn a test
50
- # *would also* catch it is wasted work. So the observed buckets are three.
41
+ # ADR-70 — the per-file fused classification. The gradual short-circuit collapses the conceptual
42
+ # "doubly-protected" bucket into `type_killed`: a mutant the type checker already kills never reaches the
43
+ # suite, because the static net already suffices and re-running the suite to learn a test *would also*
44
+ # catch it is wasted work. So the observed buckets are three.
51
45
  FusedFileResult = Data.define(:path, :type_killed, :test_killed, :sites) do
52
46
  # The unprotected sites (neither a type nor a test caught the breakage).
53
47
  def unprotected = sites.size
@@ -101,11 +95,10 @@ module Rigor
101
95
  FileResult.new(path: path, killed: killed, survived: sites.size, sites: sites)
102
96
  end
103
97
 
104
- # ADR-70 — the fused static∪dynamic measurement. Runs the type pass
105
- # (the {DiagnosticOracle}); for every mutant the type checker did **not**
106
- # kill, asks `test_oracle` whether the project's test suite catches it.
107
- # The expensive suite run is paid only for type-survivors (the gradual
108
- # short-circuit), so the cost is proportional to the protection hole.
98
+ # ADR-70 — the fused static∪dynamic measurement. Runs the type pass (the {DiagnosticOracle}); for every
99
+ # mutant the type checker did **not** kill, asks `test_oracle` whether the project's test suite catches it.
100
+ # The expensive suite run is paid only for type-survivors (the gradual short-circuit), so the cost is
101
+ # proportional to the protection hole.
109
102
  # @param test_oracle [TestSuiteOracle]
110
103
  # @return [FusedFileResult]
111
104
  def scan_file_fused(path, test_oracle:, source: nil)
@@ -134,10 +127,9 @@ module Rigor
134
127
 
135
128
  private
136
129
 
137
- # The mutations to measure: the biteable filter (concrete-type sites only;
138
- # the FP-safe default — an unresolved receiver is kept) or, under the
139
- # `:all` selector (ADR-69 Seam 2), every dispatch site including Dynamic
140
- # receivers. Optionally sampled.
130
+ # The mutations to measure: the biteable filter (concrete-type sites only; the FP-safe default — an
131
+ # unresolved receiver is kept) or, under the `:all` selector (ADR-69 Seam 2), every dispatch site including
132
+ # Dynamic receivers. Optionally sampled.
141
133
  def kept_mutations(source, path)
142
134
  mutator = Mutator.new(source)
143
135
  muts = mutator.mutations
@@ -6,19 +6,15 @@ require_relative "../scope"
6
6
  require_relative "../inference/scope_indexer"
7
7
 
8
8
  module Rigor
9
- # ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing
10
- # harness (`tool/mutation/`, ADR-62). Only the *per-file effectiveness
11
- # measurement* lives here the type-visible {Mutator} and the warm-loop
12
- # {MutationScanner} kill-rate measurement. The dev sweep / fuzz / survivor
13
- # clustering stay off the frozen surface in `tool/mutation/mutate.rb`
14
- # (which now reuses this {Mutator} so there is one source of truth).
9
+ # ADR-63 Tier 2 — the productized subset of the dev-only mutation-testing harness (`tool/mutation/`, ADR-62).
10
+ # Only the *per-file effectiveness measurement* lives here — the type-visible {Mutator} and the warm-loop
11
+ # {MutationScanner} kill-rate measurement. The dev sweep / fuzz / survivor clustering stay off the frozen
12
+ # surface in `tool/mutation/mutate.rb` (which now reuses this {Mutator} so there is one source of truth).
15
13
  module Protection
16
- # One concrete edit: replace source bytes [start, stop) with `replacement`.
17
- # `anchor` is the Prism node whose inferred type decides type-relevance
18
- # the call receiver whose contract the mutation could violate, or nil when
19
- # there is no concrete receiver (implicit-self call, literal outside a call).
20
- # `anchor_type` (the rendered receiver type) and `method_name` are filled in
21
- # for reporting a surviving site; both may stay nil.
14
+ # One concrete edit: replace source bytes [start, stop) with `replacement`. `anchor` is the Prism node whose
15
+ # inferred type decides type-relevance — the call receiver whose contract the mutation could violate, or nil
16
+ # when there is no concrete receiver (implicit-self call, literal outside a call). `anchor_type` (the
17
+ # rendered receiver type) and `method_name` are filled in for reporting a surviving site; both may stay nil.
22
18
  Mutation = Struct.new(
23
19
  :operator, :expected_rule, :start, :stop, :replacement, :line, :label, :anchor,
24
20
  :anchor_type, :method_name,
@@ -31,35 +27,29 @@ module Rigor
31
27
  end
32
28
  end
33
29
 
34
- # Generates type-visible mutations of a Ruby source string by walking the
35
- # Prism AST and recording byte-range splices (no unparser needed — Prism
36
- # hands us exact offsets, and the analyzer re-parses the spliced source).
30
+ # Generates type-visible mutations of a Ruby source string by walking the Prism AST and recording byte-range
31
+ # splices (no unparser needed — Prism hands us exact offsets, and the analyzer re-parses the spliced source).
37
32
  #
38
- # A mutation is "type-visible" when it should trip a diagnostic rule *if*
39
- # Rigor holds a type at the site: a call-argument literal dropped to `nil`
40
- # or type-swapped (→ `call.argument-type-mismatch`), or a call site renamed
41
- # to a missing method (→ `call.undefined-method`). Only call sites and
42
- # bodies are mutated, never `def` signatures, so a reused project scan stays
43
- # valid.
33
+ # A mutation is "type-visible" when it should trip a diagnostic rule *if* Rigor holds a type at the site: a
34
+ # call-argument literal dropped to `nil` or type-swapped (→ `call.argument-type-mismatch`), or a call site
35
+ # renamed to a missing method (→ `call.undefined-method`). Only call sites and bodies are mutated, never
36
+ # `def` signatures, so a reused project scan stays valid.
44
37
  class Mutator
45
38
  IDENT = /\A[a-z_][A-Za-z0-9_]*\z/
46
39
  QUOTES = ['"', "'"].freeze
47
- # Mutating an argument to a universal-equality method is always an
48
- # equivalent mutant: Ruby's `==` / `<=>` family returns false / nil on a
49
- # type mismatch rather than raising, so the engine exempts them
40
+ # Mutating an argument to a universal-equality method is always an equivalent mutant: Ruby's `==` / `<=>`
41
+ # family returns false / nil on a type mismatch rather than raising, so the engine exempts them
50
42
  # (`UNIVERSAL_EQUALITY_METHODS`). Skip them to keep survivors meaningful.
51
43
  UNIVERSAL_EQUALITY = %w[== != eql? equal? <=>].freeze
52
44
 
53
- # Every operator the mutator knows. Each maps to the diagnostic rule
54
- # family it is *engineered* to trip when the mutated value/call sits in a
55
- # context where Rigor has type knowledge.
45
+ # Every operator the mutator knows. Each maps to the diagnostic rule family it is *engineered* to trip when
46
+ # the mutated value/call sits in a context where Rigor has type knowledge.
56
47
  ALL_OPERATORS = %i[nil_inject type_swap undefined_method arity_extra].freeze
57
48
 
58
- # The default set. `arity_extra` is excluded: most Ruby methods accept an
59
- # extra argument (splat / optional), so appending one is usually an
60
- # equivalent mutant it contributes almost only noise. Re-enable it
61
- # explicitly via `operators:` to measure arity teeth. (A signature-arity
62
- # guard would make it default-worthy — a follow-up.)
49
+ # The default set. `arity_extra` is excluded: most Ruby methods accept an extra argument (splat / optional),
50
+ # so appending one is usually an equivalent mutant — it contributes almost only noise. Re-enable it
51
+ # explicitly via `operators:` to measure arity teeth. (A signature-arity guard would make it
52
+ # default-worthy a follow-up.)
63
53
  OPERATORS = %i[nil_inject type_swap undefined_method].freeze
64
54
 
65
55
  def initialize(source, operators: OPERATORS)
@@ -78,14 +68,12 @@ module Rigor
78
68
  out
79
69
  end
80
70
 
81
- # Phase 1.5 — keep only mutations whose anchor types to a concrete,
82
- # non-Dynamic type, i.e. a site where Rigor actually holds a contract the
83
- # mutation could violate. Drops implicit-self calls and literals outside a
84
- # typed call (no contract guaranteed survival noise). FP-safe
85
- # direction: an unresolved/probe-failed type KEEPS the mutation, so the
86
- # filter never hides a kill it is unsure about — it only removes
87
- # provably-Dynamic sites. Returns [kept, dropped_count]. Builds the scope
88
- # index from THIS mutator's parse so anchor node identity matches the keys.
71
+ # Phase 1.5 — keep only mutations whose anchor types to a concrete, non-Dynamic type, i.e. a site where
72
+ # Rigor actually holds a contract the mutation could violate. Drops implicit-self calls and literals
73
+ # outside a typed call (no contract guaranteed survival → noise). FP-safe direction: an
74
+ # unresolved/probe-failed type KEEPS the mutation, so the filter never hides a kill it is unsure about — it
75
+ # only removes provably-Dynamic sites. Returns [kept, dropped_count]. Builds the scope index from THIS
76
+ # mutator's parse so anchor node identity matches the keys.
89
77
  def filter_by_type(mutations, environment:, path:)
90
78
  base = Rigor::Scope.empty(environment: environment, source_path: path)
91
79
  index = Rigor::Inference::ScopeIndexer.index(@parse.value, default_scope: base)
@@ -98,13 +86,11 @@ module Rigor
98
86
  [kept, mutations.size - kept.size]
99
87
  end
100
88
 
101
- # ADR-69 Seam 2 (AllSites) — keep every *dispatch-site* mutation (a method
102
- # call or a call-argument literal), Dynamic receiver included, annotating
103
- # the anchor type where Rigor holds one. Drops only non-dispatch literals
104
- # (a literal outside any call no receiver contract to violate). The
105
- # biteable {#filter_by_type} hides exactly the Dynamic sites a test-suite
106
- # consumer most wants to probe: where Rigor cannot bite, a test is the only
107
- # protection. Use only with a {TestSuiteOracle} — at a Dynamic site the
89
+ # ADR-69 Seam 2 (AllSites) — keep every *dispatch-site* mutation (a method call or a call-argument
90
+ # literal), Dynamic receiver included, annotating the anchor type where Rigor holds one. Drops only
91
+ # non-dispatch literals (a literal outside any call no receiver contract to violate). The biteable
92
+ # {#filter_by_type} hides exactly the Dynamic sites a test-suite consumer most wants to probe: where Rigor
93
+ # cannot bite, a test is the only protection. Use only with a {TestSuiteOracle} at a Dynamic site the
108
94
  # type pass can never kill, so without the test axis these are all noise.
109
95
  def dispatch_site_mutations(mutations, environment:, path:)
110
96
  base = Rigor::Scope.empty(environment: environment, source_path: path)
@@ -139,10 +125,9 @@ module Rigor
139
125
  end
140
126
  end
141
127
 
142
- # Record, for each literal that is a direct call argument, the receiver of
143
- # the enclosing call the anchor whose param contract a literal mutation
144
- # could violate. Literals elsewhere get a nil anchor (filtered out under
145
- # the type filter).
128
+ # Record, for each literal that is a direct call argument, the receiver of the enclosing call — the anchor
129
+ # whose param contract a literal mutation could violate. Literals elsewhere get a nil anchor (filtered out
130
+ # under the type filter).
146
131
  def index_literal_anchors(node)
147
132
  return if node.nil?
148
133
 
@@ -158,9 +143,8 @@ module Rigor
158
143
  node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode) || node.is_a?(Prism::StringNode)
159
144
  end
160
145
 
161
- # Returns [keep?, rendered_type]. Keep when `anchor` is a site where Rigor
162
- # holds a concrete (non-Dynamic/Top) type. FP-safe: an unresolved or
163
- # probe-failed type keeps the mutation (with a nil rendered type).
146
+ # Returns [keep?, rendered_type]. Keep when `anchor` is a site where Rigor holds a concrete (non-Dynamic/Top)
147
+ # type. FP-safe: an unresolved or probe-failed type keeps the mutation (with a nil rendered type).
164
148
  def anchor_decision(anchor, index, cache)
165
149
  return [false, nil] if anchor.nil?
166
150
  return cache[anchor] if cache.key?(anchor)
@@ -181,11 +165,10 @@ module Rigor
181
165
  [true, nil] # never let a probe failure hide a candidate
182
166
  end
183
167
 
184
- # A receiver type Rigor cannot bite on, so a mutation anchored to it would
185
- # survive as noise: `Dynamic` / `Top` / `bot`, or a union with any such arm
186
- # (gradually valid `Array | Dynamic[top]`.whatever never fires). A union
187
- # of fully-concrete arms (`String | Symbol`) stays concrete — it now has
188
- # undefined-method teeth.
168
+ # A receiver type Rigor cannot bite on, so a mutation anchored to it would survive as noise: `Dynamic` /
169
+ # `Top` / `bot`, or a union with any such arm (gradually valid — `Array | Dynamic[top]`.whatever never
170
+ # fires). A union of fully-concrete arms (`String | Symbol`) stays concrete it now has undefined-method
171
+ # teeth.
189
172
  def non_concrete_type?(type)
190
173
  return true if type.is_a?(Rigor::Type::Dynamic) || type.is_a?(Rigor::Type::Top) ||
191
174
  type.is_a?(Rigor::Type::Bot)
@@ -200,9 +183,8 @@ module Rigor
200
183
  type.class.name
201
184
  end
202
185
 
203
- # Mutate a literal: drop it to nil (possible-nil channel) and swap its
204
- # type (type-mismatch channel). String literals are only touched when the
205
- # node is a real quoted string, so we never corrupt `%w[...]` words.
186
+ # Mutate a literal: drop it to nil (possible-nil channel) and swap its type (type-mismatch channel). String
187
+ # literals are only touched when the node is a real quoted string, so we never corrupt `%w[...]` words.
206
188
  def literal_mutations(node, out, numeric:)
207
189
  return if !numeric && !QUOTES.include?(node.opening_loc&.slice)
208
190
 
@@ -222,12 +204,10 @@ module Rigor
222
204
  extend_arity(node, out)
223
205
  end
224
206
 
225
- # Rename the *call site* (not the def) to a method that cannot exist, so a
226
- # typed receiver trips call.undefined-method. We leave `def` signatures
227
- # untouched on purpose: the prebuilt ProjectScan still carries the file's
228
- # original declarations, so mutating only bodies/call-sites keeps it valid.
229
- # Anchor is the explicit receiver (nil ⇒ implicit self ⇒ filtered out, as
230
- # call.self-undefined-method ships `:off`).
207
+ # Rename the *call site* (not the def) to a method that cannot exist, so a typed receiver trips
208
+ # call.undefined-method. We leave `def` signatures untouched on purpose: the prebuilt ProjectScan still
209
+ # carries the file's original declarations, so mutating only bodies/call-sites keeps it valid. Anchor is
210
+ # the explicit receiver (nil implicit self ⇒ filtered out, as call.self-undefined-method ships `:off`).
231
211
  def rename_call(node, out)
232
212
  name = node.name.to_s
233
213
  mloc = node.message_loc
@@ -237,8 +217,8 @@ module Rigor
237
217
  "#{name}__rigor_absent", mloc.start_line, "call ##{name} → missing method", node.receiver, name)
238
218
  end
239
219
 
240
- # Append a trailing argument inside explicit `(...)` parens to trip an
241
- # arity diagnostic against a known fixed-arity signature.
220
+ # Append a trailing argument inside explicit `(...)` parens to trip an arity diagnostic against a known
221
+ # fixed-arity signature.
242
222
  def extend_arity(node, out)
243
223
  open = node.opening_loc
244
224
  close = node.closing_loc
@@ -2,23 +2,19 @@
2
2
 
3
3
  module Rigor
4
4
  module Protection
5
- # ADR-70 — the **test-suite** kill oracle, the dynamic sibling of
6
- # {DiagnosticOracle} on the ADR-69 seam. A mutant is killed iff applying its
7
- # bytes to the file under test turns the project's test suite **red**. This is
8
- # the dynamic half of the fused static∪dynamic protection map: a `Dynamic`
9
- # site Rigor cannot bite (a type survivor) may still be fully guarded by a
10
- # test.
5
+ # ADR-70 — the **test-suite** kill oracle, the dynamic sibling of {DiagnosticOracle} on the ADR-69 seam. A
6
+ # mutant is killed iff applying its bytes to the file under test turns the project's test suite **red**. This
7
+ # is the dynamic half of the fused static∪dynamic protection map: a `Dynamic` site Rigor cannot bite (a type
8
+ # survivor) may still be fully guarded by a test.
11
9
  #
12
- # The suite command is the **runner hook** (`--test-command`, e.g.
13
- # `bundle exec rake`). The runner is injectable so the decision logic is
14
- # unit-testable without shelling out; the default shells out and reads the
15
- # process exit status (0 = green / passed).
10
+ # The suite command is the **runner hook** (`--test-command`, e.g. `bundle exec rake`). The runner is
11
+ # injectable so the decision logic is unit-testable without shelling out; the default shells out and reads
12
+ # the process exit status (0 = green / passed).
16
13
  #
17
- # I/O policy: {#killed?} writes the mutant to disk, runs the suite, and
18
- # **always restores** the original bytes in an `ensure` — a normal exception
19
- # never leaves a mutant on disk. (A hard interrupt mid-suite is the standard
20
- # mutation-testing hazard the `ensure` cannot cover; callers running this in
21
- # CI accept that, as `mutant` / Stryker do.)
14
+ # I/O policy: {#killed?} writes the mutant to disk, runs the suite, and **always restores** the original
15
+ # bytes in an `ensure` — a normal exception never leaves a mutant on disk. (A hard interrupt mid-suite is the
16
+ # standard mutation-testing hazard the `ensure` cannot cover; callers running this in CI accept that, as
17
+ # `mutant` / Stryker do.)
22
18
  class TestSuiteOracle
23
19
  # @param command [Array<String>] the test command (the runner hook)
24
20
  # @param runner [#call, nil] `runner.call(command) -> true iff the suite
@@ -28,9 +24,8 @@ module Rigor
28
24
  @runner = runner || method(:shell_run)
29
25
  end
30
26
 
31
- # The baseline: the suite must pass on clean code, else "a mutant survived"
32
- # is meaningless (every mutant would look killed, or none would). Run once
33
- # before measuring.
27
+ # The baseline: the suite must pass on clean code, else "a mutant survived" is meaningless (every mutant
28
+ # would look killed, or none would). Run once before measuring.
34
29
  def green?
35
30
  @runner.call(@command)
36
31
  end
@@ -48,15 +43,13 @@ module Rigor
48
43
 
49
44
  private
50
45
 
51
- # Run the suite with Bundler's environment stripped, so a `bundle exec`
52
- # test command resolves the **target** project's Gemfile — not whatever
53
- # bundle Rigor itself was launched under. Running Rigor via `bundle exec`
54
- # leaks `RUBYOPT=-rbundler/setup` + `GEM_HOME` / `BUNDLE_*` into a plain
55
- # `system` subprocess, which then resolves the target's Gemfile against
56
- # Rigor's gems and fails so a green suite looks red and the run aborts.
57
- # `with_unbundled_env` restores the pre-bundler env (a bare `env -u
58
- # BUNDLE_GEMFILE` is not enough — the `BUNDLER_ORIG_*` preservers defeat
59
- # it). Found validating ADR-70 on real projects (2026-06-17).
46
+ # Run the suite with Bundler's environment stripped, so a `bundle exec` test command resolves the
47
+ # **target** project's Gemfile — not whatever bundle Rigor itself was launched under. Running Rigor via
48
+ # `bundle exec` leaks `RUBYOPT=-rbundler/setup` + `GEM_HOME` / `BUNDLE_*` into a plain `system` subprocess,
49
+ # which then resolves the target's Gemfile against Rigor's gems and fails — so a green suite looks red and
50
+ # the run aborts. `with_unbundled_env` restores the pre-bundler env (a bare `env -u BUNDLE_GEMFILE` is not
51
+ # enough the `BUNDLER_ORIG_*` preservers defeat it). Found validating ADR-70 on real projects
52
+ # (2026-06-17).
60
53
  def shell_run(command)
61
54
  run = -> { system(*command, out: File::NULL, err: File::NULL) }
62
55
  return run.call unless defined?(Bundler) && Bundler.respond_to?(:with_unbundled_env)