rigortype 0.3.9 → 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 (363) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/10-sorbet.md +9 -10
  14. data/docs/handbook/11-sig-gen.md +454 -13
  15. data/docs/manual/02-cli-reference.md +63 -2
  16. data/docs/manual/03-configuration.md +7 -0
  17. data/docs/manual/04-diagnostics.md +4 -0
  18. data/docs/manual/07-plugins.md +1 -1
  19. data/docs/manual/10-mcp-server.md +3 -2
  20. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  21. data/docs/manual/19-effect-labels.md +10 -0
  22. data/docs/manual/plugins/README.md +7 -0
  23. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  24. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  25. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  26. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  27. data/docs/manual/plugins/rigor-activejob.md +7 -0
  28. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  29. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  30. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  31. data/docs/manual/plugins/rigor-grape.md +106 -0
  32. data/docs/manual/plugins/rigor-graphql.md +23 -2
  33. data/docs/manual/plugins/rigor-pundit.md +8 -1
  34. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  35. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  36. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  37. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  38. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  39. data/exe/rigor +19 -4
  40. data/lib/rigor/analysis/baseline.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  42. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  43. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  46. data/lib/rigor/analysis/check_rules/source_arity.rb +323 -0
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  49. data/lib/rigor/analysis/check_rules.rb +520 -49
  50. data/lib/rigor/analysis/dependency_recorder.rb +23 -0
  51. data/lib/rigor/analysis/fact_store.rb +9 -0
  52. data/lib/rigor/analysis/incremental.rb +26 -0
  53. data/lib/rigor/analysis/incremental_session.rb +36 -4
  54. data/lib/rigor/analysis/project_scan.rb +11 -1
  55. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  56. data/lib/rigor/analysis/reachability/scan_cache.rb +1 -0
  57. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  58. data/lib/rigor/analysis/run_cache_key.rb +20 -11
  59. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +167 -11
  60. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  61. data/lib/rigor/analysis/runner/pool_coordinator.rb +107 -15
  62. data/lib/rigor/analysis/runner/project_pre_passes.rb +26 -7
  63. data/lib/rigor/analysis/runner.rb +278 -20
  64. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  65. data/lib/rigor/analysis/template_unit_paths.rb +91 -0
  66. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  67. data/lib/rigor/analysis/template_units.rb +399 -0
  68. data/lib/rigor/analysis/worker_session.rb +53 -10
  69. data/lib/rigor/bleeding_edge.rb +0 -2
  70. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  71. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  72. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  73. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  74. data/lib/rigor/cache/descriptor.rb +6 -1
  75. data/lib/rigor/cache/engine_source.rb +29 -1
  76. data/lib/rigor/cache/incremental_snapshot.rb +33 -1
  77. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  78. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  79. data/lib/rigor/cache/store.rb +0 -1
  80. data/lib/rigor/ci_detector.rb +1 -0
  81. data/lib/rigor/cli/doc_links.rb +1 -1
  82. data/lib/rigor/cli/docs_command.rb +4 -4
  83. data/lib/rigor/cli/plugin_command.rb +3 -3
  84. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  85. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  86. data/lib/rigor/cli/skill_command.rb +1 -1
  87. data/lib/rigor/cli/skill_describe.rb +0 -1
  88. data/lib/rigor/cli/type_of_command.rb +19 -5
  89. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  90. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  91. data/lib/rigor/cli.rb +21 -3
  92. data/lib/rigor/configuration/severity_profile.rb +19 -3
  93. data/lib/rigor/configuration.rb +66 -3
  94. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  95. data/lib/rigor/effects/attribution.rb +11 -2
  96. data/lib/rigor/effects/callee_rule.rb +368 -0
  97. data/lib/rigor/effects/catalog.rb +7 -4
  98. data/lib/rigor/effects/collector.rb +11 -5
  99. data/lib/rigor/effects/config_envelopes.rb +9 -2
  100. data/lib/rigor/effects/definition_context.rb +179 -0
  101. data/lib/rigor/effects/effect_table.rb +11 -3
  102. data/lib/rigor/effects/envelope_check.rb +1 -1
  103. data/lib/rigor/effects/envelope_index.rb +15 -0
  104. data/lib/rigor/effects/file_collection.rb +59 -5
  105. data/lib/rigor/effects/framework_units.rb +1 -1
  106. data/lib/rigor/effects/identity.rb +16 -0
  107. data/lib/rigor/effects/local_ownership.rb +38 -12
  108. data/lib/rigor/effects/method_key.rb +21 -0
  109. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  110. data/lib/rigor/effects/plugin_facts.rb +43 -31
  111. data/lib/rigor/effects/propagator.rb +295 -16
  112. data/lib/rigor/effects/registry.rb +1 -1
  113. data/lib/rigor/effects/scanner.rb +121 -72
  114. data/lib/rigor/effects/signature_sources.rb +1 -1
  115. data/lib/rigor/effects/snapshot.rb +2 -1
  116. data/lib/rigor/effects/summary.rb +27 -4
  117. data/lib/rigor/effects/unit_scan.rb +385 -30
  118. data/lib/rigor/effects/visibility.rb +101 -0
  119. data/lib/rigor/environment/lockfile_resolver.rb +17 -0
  120. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  121. data/lib/rigor/environment/member_consistency.rb +298 -0
  122. data/lib/rigor/environment/rbs_loader.rb +399 -133
  123. data/lib/rigor/environment.rb +103 -38
  124. data/lib/rigor/hashing/xxh3.rb +264 -0
  125. data/lib/rigor/inference/acceptance.rb +139 -9
  126. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  127. data/lib/rigor/inference/block_call_timing.rb +338 -0
  128. data/lib/rigor/inference/block_parameter_binder.rb +73 -27
  129. data/lib/rigor/inference/block_repetition.rb +71 -0
  130. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  131. data/lib/rigor/inference/budget_trace.rb +2 -1
  132. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  133. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  134. data/lib/rigor/inference/captured_locals.rb +387 -15
  135. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  136. data/lib/rigor/inference/content_join.rb +200 -27
  137. data/lib/rigor/inference/def_return_typer.rb +11 -7
  138. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  139. data/lib/rigor/inference/element_read_widening.rb +22 -10
  140. data/lib/rigor/inference/error_info.rb +196 -0
  141. data/lib/rigor/inference/expression_typer.rb +1532 -496
  142. data/lib/rigor/inference/external_ancestor_resolution.rb +267 -0
  143. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  144. data/lib/rigor/inference/global_write_census.rb +239 -0
  145. data/lib/rigor/inference/guard_rebinding.rb +447 -0
  146. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  147. data/lib/rigor/inference/index_write_widening.rb +16 -3
  148. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  149. data/lib/rigor/inference/jump_targets.rb +82 -0
  150. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  151. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  152. data/lib/rigor/inference/last_line.rb +340 -0
  153. data/lib/rigor/inference/last_status.rb +144 -0
  154. data/lib/rigor/inference/macro_block_self_type.rb +167 -17
  155. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  156. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  157. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  158. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  159. data/lib/rigor/inference/match_rebinding.rb +392 -0
  160. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  161. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  162. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  163. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  164. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  165. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  166. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  167. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  168. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  169. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +881 -38
  170. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  171. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  172. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  173. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  174. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  175. data/lib/rigor/inference/multi_target_binder.rb +340 -52
  176. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  177. data/lib/rigor/inference/mutation_widening.rb +70 -48
  178. data/lib/rigor/inference/narrowing.rb +540 -120
  179. data/lib/rigor/inference/operand_effects.rb +167 -0
  180. data/lib/rigor/inference/operand_walk.rb +88 -0
  181. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  182. data/lib/rigor/inference/parameter_inference_collector.rb +3 -2
  183. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  184. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  185. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  186. data/lib/rigor/inference/receiver_alias.rb +90 -1
  187. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  188. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  189. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  190. data/lib/rigor/inference/return_barrier.rb +54 -0
  191. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  192. data/lib/rigor/inference/scope_indexer.rb +4615 -551
  193. data/lib/rigor/inference/statement_evaluator.rb +3176 -490
  194. data/lib/rigor/inference/stored_block_call.rb +54 -0
  195. data/lib/rigor/inference/string_mutation.rb +44 -7
  196. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  197. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  198. data/lib/rigor/language_server/debouncer.rb +0 -1
  199. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  200. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  201. data/lib/rigor/language_server/project_context.rb +5 -3
  202. data/lib/rigor/mcp/server.rb +2 -1
  203. data/lib/rigor/plugin/base.rb +168 -5
  204. data/lib/rigor/plugin/box_probe.rb +91 -0
  205. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  206. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  207. data/lib/rigor/plugin/loader.rb +2 -1
  208. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  209. data/lib/rigor/plugin/manifest.rb +71 -10
  210. data/lib/rigor/plugin/registry.rb +35 -1
  211. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  212. data/lib/rigor/plugin/template_unit.rb +196 -0
  213. data/lib/rigor/plugin.rb +1 -0
  214. data/lib/rigor/protection/discovery_seed.rb +3 -1
  215. data/lib/rigor/protection/kill_signature.rb +0 -1
  216. data/lib/rigor/protection/mutation_cache.rb +1 -2
  217. data/lib/rigor/rbs_extended.rb +27 -0
  218. data/lib/rigor/reflection/constant_ancestors.rb +97 -0
  219. data/lib/rigor/reflection/constant_path.rb +19 -6
  220. data/lib/rigor/reflection.rb +72 -105
  221. data/lib/rigor/scope/discovery_index.rb +135 -2
  222. data/lib/rigor/scope.rb +859 -49
  223. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  224. data/lib/rigor/sig_gen/classification.rb +22 -5
  225. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  226. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  227. data/lib/rigor/sig_gen/generator.rb +558 -31
  228. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  229. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  230. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  231. data/lib/rigor/sig_gen/renderer.rb +124 -9
  232. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  233. data/lib/rigor/sig_gen/write_result.rb +19 -3
  234. data/lib/rigor/sig_gen/writer.rb +166 -35
  235. data/lib/rigor/sig_gen.rb +3 -0
  236. data/lib/rigor/signature_path_audit.rb +1 -1
  237. data/lib/rigor/source/node_walker.rb +0 -3
  238. data/lib/rigor/source/parameter_envelope.rb +72 -0
  239. data/lib/rigor/source.rb +1 -0
  240. data/lib/rigor/type/combinator.rb +88 -12
  241. data/lib/rigor/type/difference.rb +1 -0
  242. data/lib/rigor/type/hash_shape.rb +1 -1
  243. data/lib/rigor/type/refined.rb +1 -0
  244. data/lib/rigor/version.rb +1 -1
  245. data/lib/rigor.rb +1 -0
  246. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  247. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  248. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  249. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  250. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  251. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  252. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  253. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  254. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  255. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  256. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  257. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  258. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  259. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  260. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  261. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  262. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  263. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  264. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  265. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  266. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  267. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  268. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  269. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  270. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  271. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  272. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  273. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  274. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  275. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  276. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  277. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  278. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  279. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  280. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  281. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  282. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  283. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  284. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  285. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  286. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  287. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  288. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  289. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  290. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  291. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  292. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  293. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  294. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  295. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  296. data/sig/rigor/analysis/baseline.rbs +69 -7
  297. data/sig/rigor/analysis/fact_store.rbs +1 -1
  298. data/sig/rigor/analysis/project_scan.rbs +74 -0
  299. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  300. data/sig/rigor/effects/effect_table.rbs +60 -0
  301. data/sig/rigor/effects/envelope.rbs +97 -0
  302. data/sig/rigor/effects/envelope_index.rbs +33 -0
  303. data/sig/rigor/effects/file_collection.rbs +81 -0
  304. data/sig/rigor/effects/label.rbs +26 -0
  305. data/sig/rigor/effects/label_set.rbs +46 -0
  306. data/sig/rigor/effects/method_key.rbs +24 -0
  307. data/sig/rigor/effects/origin.rbs +50 -0
  308. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  309. data/sig/rigor/effects/registry.rbs +68 -0
  310. data/sig/rigor/effects/summary.rbs +50 -0
  311. data/sig/rigor/effects/taint_cause.rbs +12 -0
  312. data/sig/rigor/environment.rbs +12 -10
  313. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  314. data/sig/rigor/inference.rbs +6 -4
  315. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  316. data/sig/rigor/plugin/base.rbs +30 -7
  317. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  318. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  319. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  320. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  321. data/sig/rigor/plugin/io_boundary.rbs +1 -1
  322. data/sig/rigor/plugin/loader.rbs +3 -3
  323. data/sig/rigor/plugin/manifest.rbs +50 -11
  324. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  325. data/sig/rigor/plugin/registry.rbs +62 -1
  326. data/sig/rigor/plugin.rbs +1 -1
  327. data/sig/rigor/rbs_extended.rbs +1 -1
  328. data/sig/rigor/reflection.rbs +7 -6
  329. data/sig/rigor/scope.rbs +135 -20
  330. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  331. data/sig/rigor/source.rbs +4 -4
  332. data/sig/rigor/testing.rbs +10 -4
  333. data/sig/rigor/type.rbs +6 -0
  334. data/sig/rigor.rbs +38 -20
  335. data/skills/rigor-ask/SKILL.md +8 -5
  336. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  337. data/skills/rigor-ci-setup/SKILL.md +17 -21
  338. data/skills/rigor-doctor/SKILL.md +24 -22
  339. data/skills/rigor-doctor/references/01-checks.md +97 -33
  340. data/skills/rigor-editor-setup/SKILL.md +6 -4
  341. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  342. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  343. data/skills/rigor-next-steps/SKILL.md +4 -2
  344. data/skills/rigor-plugin-author/SKILL.md +19 -23
  345. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  346. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  347. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  348. data/skills/rigor-plugin-review/SKILL.md +6 -4
  349. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  350. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  351. data/skills/rigor-project-init/SKILL.md +9 -7
  352. data/skills/rigor-project-init/references/01-detect.md +13 -9
  353. data/skills/rigor-project-init/references/02-configure.md +33 -8
  354. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  355. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  356. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  357. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  358. data/skills/rigor-type-oracle/SKILL.md +4 -6
  359. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  360. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  361. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  362. data/skills/rigor-upgrade/SKILL.md +13 -8
  363. metadata +108 -1
@@ -3,6 +3,7 @@
3
3
  require "digest"
4
4
 
5
5
  require_relative "../version"
6
+ require_relative "descriptor"
6
7
 
7
8
  module Rigor
8
9
  module Cache
@@ -63,6 +64,9 @@ module Rigor
63
64
  # to whatever else happened to be there, which is exactly the weakening this module forbids.
64
65
  REQUIRED_DIRECTORY = "lib"
65
66
 
67
+ # The config-row key every computed-value slot carries the identity under.
68
+ CONFIG_KEY = "engine-source"
69
+
66
70
  module_function
67
71
 
68
72
  # @return the gem root — the directory holding `lib/`, three levels above this file.
@@ -100,10 +104,34 @@ module Rigor
100
104
  # exists at all. The same reading covers the long-running `rigor lsp` process.
101
105
  #
102
106
  # {Unavailable} propagates and is deliberately NOT memoised: the ivar is only assigned on success.
107
+ #
108
+ # The memo is FROZEN because a Ractor-pool worker reads it (#1055): the auto-wired ADR-93
109
+ # `rigor-rbs-inline` synthesizer reaches `.key_config_entries` from inside
110
+ # `Environment.for_project`, which is inside `WorkerSession#initialize`, which is inside the worker's
111
+ # own Ractor — and a non-main Ractor may read a class/module ivar only when its value is shareable.
112
+ # An unfrozen digest String therefore killed every worker in its constructor on any run with the
113
+ # synthesizer on, which is every real CLI run (the spec suite pins it off). The worker never WRITES
114
+ # it: `PoolCoordinator#analyze_files_in_pool` pre-warms this on the main Ractor first, which is where
115
+ # the argument above already says the value belongs.
103
116
  def process_identity
104
117
  return @process_identity if defined?(@process_identity)
105
118
 
106
- @process_identity = identity
119
+ @process_identity = identity.freeze
120
+ end
121
+
122
+ # The `engine-source` key row for a cache whose value this engine computed, built from
123
+ # {.process_identity}: empty for a version-pinned tree, so a released gem's keys stay byte-identical.
124
+ #
125
+ # Issue #1009 — a slot keyed only on its inputs and a plugin's manifest version outlives the engine
126
+ # that filled it. The per-file synthesizer slot and every plugin producer were keyed that way, so after
127
+ # an engine or bundled-plugin edit the run-result key moved and the run re-analysed, but through the
128
+ # previous build's synthesized RBS and producer values. {Unavailable} propagates, so a caller that
129
+ # cannot identify the engine bypasses its cache instead of falling back to the weaker key.
130
+ def key_config_entries
131
+ identity = process_identity
132
+ return [] if identity.nil?
133
+
134
+ [Descriptor::ConfigEntry.new(key: CONFIG_KEY, value_hash: Digest::SHA256.hexdigest(identity))]
107
135
  end
108
136
 
109
137
  # Discards the {.process_identity} memo; production code MUST NOT call this — a run that recomputed
@@ -106,7 +106,39 @@ module Rigor
106
106
  # blob has no such section and would deserialise it as nil, which reads as "nothing cached" and is
107
107
  # therefore not misread — but a snapshot written by an engine that did not yet REPLAY them was also
108
108
  # written by one whose narrowed runs under-reported, so the gate rejects it and the next run is cold.
109
- SCHEMA = 22
109
+ # 23: issue #992 gives each seed bundle a `parameter_envelopes` table — the joined positional envelope
110
+ # of every method a file records, plus its module / dynamic-surface marks — which `call.wrong-arity`
111
+ # reads for a method no signature declares. A pre-23 bundle folds as "no envelopes", which only ever
112
+ # withholds a check, but a warm run that withholds where a cold run fires is still the
113
+ # `--verify-incremental` divergence 14 was bumped for.
114
+ # 24: issue #1097 gives each seed bundle a `deferred_ranges` table — the per-file def / block /
115
+ # lambda body ranges `Scope#*_def_shadows_call?` orders a project-defined `sig` override
116
+ # against. A pre-24 bundle would fold as "no ranges", which the predicate reads as "cannot
117
+ # order → shadow" — a warm run declining the binding where a cold run binds.
118
+ # 25: issue #1097 extends each `deferred_ranges` row to `[start, end, name, kind, owner]` —
119
+ # owner-scoped ordering. A 24 bundle folds as "owner = nil", which the predicate reads as
120
+ # "cannot order", under-shadowing `module_function` defs whose only table evidence is the row
121
+ # itself.
122
+ # 26: issue #1123 gives each seed bundle a `prepends` table — the instance-ancestor ORDER of a
123
+ # class's `prepend`ed modules, which `Scope#user_def_through_ancestors` searches ahead of the
124
+ # class's own `def`s. A pre-26 bundle would fold as "prepends nothing": the warm run would answer
125
+ # the pre-fix MRO where a cold run answers the prepended module, exactly the `--verify-incremental`
126
+ # divergence 14 was bumped for (and a silently WRONG dispatch answer, not a withheld check).
127
+ # 27: issue #1173 re-MEANs each seed bundle's `includes` VALUES without changing their shape: the
128
+ # lists now store instance-ancestor SEARCH order (nearest first, prepends ahead of includes)
129
+ # instead of call order — the same flip 17 made for def-row `nesting`. A pre-27 blob deserialises
130
+ # cleanly and would serve the old call order on a warm run while a cold run searches nearer-first:
131
+ # `include A; include B` would resolve A's methods where a cold run resolves B's — the
132
+ # `--verify-incremental` divergence 14 was bumped for.
133
+ # 28: issue #1120 gives each seed bundle a `refinements` table — the `def`s of every `refine X do … end`
134
+ # body, which `call.undefined-method` reads where a `using` of the refining module is in effect. A
135
+ # pre-28 bundle would fold as "refines nothing", and a warm run would report a refined call a cold run
136
+ # declines.
137
+ # 29: issue #1367 gives each seed bundle a `global_write_census` — the globals its `alias` statements name and
138
+ # the `write` / `to_str` / `to_int` / hatch definitions and top-level mixins it holds, which the `global.*`
139
+ # write rules decline on project-wide. A pre-29 bundle would fold as "none of those", and a warm run would
140
+ # report a write a cold run exempts.
141
+ SCHEMA = 29
110
142
 
111
143
  # The persisted per-file state.
112
144
  # `cache` maps an analyzed file to its diagnostics.
@@ -29,6 +29,10 @@ module Rigor
29
29
  # files, same libraries), so the loader memoises one build per process instead of re-digesting every
30
30
  # .rbs file once per producer.
31
31
  descriptor = loader.rbs_cache_descriptor
32
+ # Issue #1014 — nil is an engine the descriptor could not identify ({EngineSource::Unavailable}).
33
+ # Compute uncached rather than keying the value without the engine that produced it.
34
+ return compute(loader) if descriptor.nil?
35
+
32
36
  store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor,
33
37
  generation_cap: generation_cap) do
34
38
  compute(loader)
@@ -3,20 +3,47 @@
3
3
  require "digest"
4
4
 
5
5
  require_relative "descriptor"
6
+ require_relative "engine_source"
6
7
  require_relative "file_digest"
7
8
 
8
9
  module Rigor
9
10
  module Cache
10
11
  # Shared descriptor builder for cache producers that depend on the RBS environment (constant table,
11
- # known-class set, future Marshal-clean reflection artefacts). Every consumer attaches the same three
12
- # slots, so factoring the construction here keeps the producers small and ensures invalidation behaves
13
- # identically across them.
12
+ # known-class set, future Marshal-clean reflection artefacts). Every consumer attaches the same slots —
13
+ # the `rbs` gem row, a digest row per signature file, and the `rbs.libraries` / `rbs.virtual_rbs` /
14
+ # `rbs.deferred_signature_paths` / `engine-source` config rows — so factoring the construction here keeps
15
+ # the producers small and ensures invalidation behaves identically across them.
14
16
  module RbsDescriptor
17
+ # The key every `rbs.*` producer shares ({RbsCacheProducer#fetch}): `rbs.environment`,
18
+ # `rbs.constant_type_table`, `rbs.class_ancestor_table`, `rbs.known_class_names`,
19
+ # `rbs.class_type_param_names`.
20
+ #
21
+ # Issue #1014 — the engine-source row is here, not in {.config_entries}, because these are
22
+ # computed-value slots: what they hold is not the RBS the descriptor digests but what Rigor's own code
23
+ # made of it. `rbs.constant_type_table` stores `Inference::RbsTypeTranslator.translate` output and
24
+ # `rbs.class_ancestor_table` the chains `RbsClassAncestorTable.compute` walks out of
25
+ # `instance_definition`, so an engine edit to either moved the value while the key stood still: the
26
+ # run-result key moved (#285) and the run re-analysed, then read the previous build's table. Both were
27
+ # reproduced cross-build on a fixture — a true positive dropped, and a `flow.unreachable-clause` false
28
+ # positive served — before this row was added.
29
+ #
30
+ # {.config_entries} deliberately does NOT get it: that half is what the ADR-45 run cache key reads
31
+ # ({.build_run}), and `RunCacheKey` already contributes the identical row from
32
+ # `EngineSource.key_config_entries` itself. Adding it there would duplicate the row in the run key and
33
+ # would have to be reconstructible by the boot-slimming probe, which builds `config_entries` without a
34
+ # loader.
35
+ #
36
+ # @raise EngineSource::Unavailable — an engine that cannot be identified must not be keyed by its
37
+ # inputs alone; {RbsCacheProducer.fetch} turns this into an uncached compute.
15
38
  def self.build(loader)
39
+ # FIRST, before {.file_entries} digests the whole signature tree: the {EngineSource::Unavailable}
40
+ # path discards this descriptor entirely, and a SHA-256 of every `.rbs` under every signature root is
41
+ # the expensive way to reach that conclusion.
42
+ engine = EngineSource.key_config_entries
16
43
  Descriptor.new(
17
44
  gems: [rbs_gem_entry],
18
45
  files: file_entries(loader),
19
- configs: config_entries(loader) + env_only_config_entries(loader)
46
+ configs: config_entries(loader) + env_only_config_entries(loader) + engine
20
47
  )
21
48
  end
22
49
 
@@ -25,8 +52,10 @@ module Rigor
25
52
  # MISS, by the dependency descriptor ({Runner#run_dependency_descriptor}). So a warm HIT never digests
26
53
  # the (large, vendored) RBS tree. {RunDescriptor} is NOT a {Descriptor} — it is never composed, hashed,
27
54
  # or `==`'d, only its four readers are consulted — so deferring `files` costs no soundness, and `gems`
28
- # / `configs` match {.build}'s shared slots (the key is unchanged; the one slot the env key adds on
29
- # top, {.env_only_config_entries}, is never read here).
55
+ # / `configs` match {.build}'s shared slots (the key is unchanged; the slots the env key adds on top,
56
+ # {.env_only_config_entries} and the #1014 `engine-source` row, are never read here — `RunCacheKey`
57
+ # contributes the identical engine-source row itself, from the same
58
+ # `EngineSource.key_config_entries`).
30
59
  def self.build_run(loader)
31
60
  RunDescriptor.new(loader: loader, gems: [rbs_gem_entry], configs: config_entries(loader))
32
61
  end
@@ -112,7 +141,8 @@ module Rigor
112
141
  # overlays. Rigor's own `data/` trees ({RbsLoader.vendored_gem_sig_paths}, `.core_overlay_sig_paths`,
113
142
  # `.capability_role_sig_paths`) are engine-owned: a file appears under one only when the engine tree is
114
143
  # edited, which normally arrives with the `lib/` change that reads it and so re-keys every
115
- # computed-value slot through {EngineSource}. They get file rows and no glob — a glob there would cost
144
+ # computed-value slot through {EngineSource} — including, since #1014, this descriptor itself, which
145
+ # until then carried no such row. They get file rows and no glob — a glob there would cost
116
146
  # every project a stat walk of the vendored gem-signature tree per warm run to catch an
117
147
  # engine-development edit.
118
148
  def self.glob_entries(loader)
@@ -65,7 +65,6 @@ module Rigor
65
65
 
66
66
  VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
67
67
 
68
- # @param root — cache root directory.
69
68
  # @param read_only — when true, every disk-side side-effect is suppressed: `fetch_or_compute`
70
69
  # still reads existing entries (hits, gated on a current `schema_version.txt` marker — see
71
70
  # {#ensure_schema_version!}) and still runs the producer block on miss, but it does NOT write the
@@ -61,6 +61,7 @@ module Rigor
61
61
  { id: "ci", name: "CI", format: nil, tier: :reviewdog,
62
62
  var: "CI", match: :truthy }
63
63
  ].freeze
64
+ Ractor.make_shareable(PROVIDERS)
64
65
 
65
66
  module_function
66
67
 
@@ -31,7 +31,7 @@ module Rigor
31
31
  private_constant :MARKDOWN_LINK
32
32
 
33
33
  # Where the gem's `docs/` tree sits, so a rendered path can be made repository-relative.
34
- GEM_ROOT = File.expand_path("../../..", __dir__)
34
+ GEM_ROOT = File.expand_path("../../..", __dir__).freeze
35
35
  private_constant :GEM_ROOT
36
36
 
37
37
  module_function
@@ -47,10 +47,10 @@ module Rigor
47
47
  USAGE
48
48
 
49
49
  # The bundled docs live at `<gem_root>/docs/`. From `lib/rigor/cli/docs_command.rb` that is three directories up.
50
- DOCS_ROOT = File.expand_path("../../../docs", __dir__)
51
- MANUAL_ROOT = File.join(DOCS_ROOT, "manual")
52
- HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook")
53
- LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt")
50
+ DOCS_ROOT = File.expand_path("../../../docs", __dir__).freeze
51
+ MANUAL_ROOT = File.join(DOCS_ROOT, "manual").freeze
52
+ HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook").freeze
53
+ LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt").freeze
54
54
 
55
55
  # @return CLI exit status.
56
56
  def run
@@ -60,9 +60,9 @@ module Rigor
60
60
 
61
61
  # The bundled plugins/examples/source live at `<gem_root>/...`. From `lib/rigor/cli/plugin_command.rb` the gem
62
62
  # root is three directories up (matching SkillCommand::SKILLS_ROOT).
63
- GEM_ROOT = File.expand_path("../../..", __dir__)
64
- PLUGINS_ROOT = File.join(GEM_ROOT, "plugins")
65
- EXAMPLES_ROOT = File.join(GEM_ROOT, "examples")
63
+ GEM_ROOT = File.expand_path("../../..", __dir__).freeze
64
+ PLUGINS_ROOT = File.join(GEM_ROOT, "plugins").freeze
65
+ EXAMPLES_ROOT = File.join(GEM_ROOT, "examples").freeze
66
66
 
67
67
  # @return CLI exit status.
68
68
  def run
@@ -35,7 +35,6 @@ module Rigor
35
35
 
36
36
  VARIABLE_TOKENS = %i[INSTANCE_VARIABLE CLASS_VARIABLE GLOBAL_VARIABLE].freeze
37
37
 
38
- # @param source — Ruby source.
39
38
  # @return the source with ANSI colour escapes, or
40
39
  # the input unchanged when lexing surfaces an error.
41
40
  def colorize(source)
@@ -3,6 +3,8 @@
3
3
  require "optionparser"
4
4
 
5
5
  require_relative "../configuration"
6
+ require_relative "../effects/config_envelopes"
7
+ require_relative "../effects/registry"
6
8
  require_relative "options"
7
9
  require_relative "../sig_gen"
8
10
  require_relative "command"
@@ -22,21 +24,27 @@ module Rigor
22
24
  # AND the candidate is a `tighter-return`.
23
25
  #
24
26
  # Parameter policy defaults to `untyped`. `--params=observed` (slice 3) opts in to caller-side observation
25
- # harvesting: the `ObservationCollector` walks `--observe=PATH...` (default `spec/` when no flag is given AND a
26
- # `spec/` directory exists), unions per-position arg types, and the generator emits the union per ADR-5 clause 2.
27
+ # harvesting: the `ObservationCollector` walks `--observe=PATH...` (default: the project's test roots,
28
+ # {Configuration#resolved_test_paths}), unions per-position arg types, and the generator emits the union per ADR-5
29
+ # clause 2.
27
30
  # `--params=observed-strict` stays reserved-but-inert until the capability-role catalog ships (rejected with a usage
28
31
  # error so the surface stays stable).
29
- class SigGenCommand < Command
32
+ #
33
+ # `--check` (ADR-112 WD4) is the CI freshness gate: it runs the `--write` merge with every other flag as given,
34
+ # writes nothing, prints what would change, and exits 1 when anything would.
35
+ class SigGenCommand < Command # rubocop:disable Metrics/ClassLength
30
36
  USAGE = "Usage: rigor sig-gen [options] [paths]"
31
37
 
32
- VALID_MODES = %w[print diff write].freeze
38
+ VALID_MODES = %w[print diff write check].freeze
33
39
  VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
34
40
  VALID_FORMATS = %w[text json].freeze
35
41
 
36
- # The skip reasons {#report_skipped} counts. The two left out each have a detailed report of their own
37
- # ({#report_unrenderable}, {#report_unresolvable_superclasses}), so a method never shows up in two tallies.
42
+ # The skip reasons {#report_skipped} counts. The four left out each have a report of their own
43
+ # ({#report_unrenderable}, {#report_unresolvable_superclasses}, {#report_inline_declared}, and the refusal
44
+ # lines `--write` / `--check` print for `inline_differs`), so a method never shows up in two tallies.
38
45
  SUMMARISED_SKIP_REASONS = (SigGen::Classification::SKIP_DIAGNOSTIC_IDS.keys -
39
- %i[unrenderable_rbs unresolvable_superclass]).freeze
46
+ %i[unrenderable_rbs unresolvable_superclass inline_declared
47
+ inline_differs]).freeze
40
48
  private_constant :SUMMARISED_SKIP_REASONS
41
49
 
42
50
  # @return CLI exit status.
@@ -50,17 +58,22 @@ module Rigor
50
58
  observations = collect_observations(configuration, options)
51
59
  generator = SigGen::Generator.new(configuration: configuration, paths: paths,
52
60
  observations: observations,
53
- include_private: options.fetch(:include_private))
61
+ include_private: options.fetch(:include_private),
62
+ effect_annotator: effect_annotator(configuration, paths, options),
63
+ overwrite: options.fetch(:overwrite))
54
64
  candidates = generator.run
55
65
  mode = options.fetch(:mode).to_sym
56
66
 
57
- status = if mode == :write
58
- dispatch_write(candidates, configuration, options)
67
+ status = case mode
68
+ when :write then dispatch_write(candidates, configuration, options)
69
+ when :check then dispatch_check(candidates, configuration, options)
59
70
  else
60
71
  dispatch_print_or_diff(candidates, mode, options)
61
72
  0
62
73
  end
63
74
  report_skipped(candidates, options)
75
+ report_inline_declared(candidates, options)
76
+ report_withheld_annotations(candidates, options)
64
77
  report_unrenderable(generator.unrenderable)
65
78
  report_unresolvable_superclasses(generator.unresolvable_superclasses)
66
79
  status
@@ -68,6 +81,74 @@ module Rigor
68
81
 
69
82
  private
70
83
 
84
+ # ADR-103 WD9 — the effect table sig-gen writes annotations from, or `nil`.
85
+ #
86
+ # The gate is the project's own `effects:` opt-in and nothing else. An annotation is read back as an
87
+ # enforced envelope, so turning emission on from a sig-gen flag alone would let one command commit a
88
+ # project to a contract `rigor check` was never asked to keep; and a project that has not opted in
89
+ # pays neither the analysis this needs nor a single changed byte of output.
90
+ #
91
+ # `--effect-envelopes` is the second, narrower switch: `%a{pure}` is the ecosystem's existing purity
92
+ # spelling and round-trips through Steep as well as Rigor, while `%a{rigor:v1:effect …}` is Rigor's
93
+ # own and belongs in a project's `sig/` only when the author asked for it by name.
94
+ def effect_annotator(configuration, paths, options)
95
+ unless configuration.effects_enabled?
96
+ if options.fetch(:effect_envelopes)
97
+ @err.puts("rigor sig-gen: --effect-envelopes needs the `effects:` opt-in in .rigor.yml; " \
98
+ "no effect annotation was emitted.")
99
+ end
100
+ return nil
101
+ end
102
+
103
+ require_relative "check_runner_factory"
104
+ # Through the same factory `rigor check` and `rigor doctor` use, so the LRU cap, the worker
105
+ # resolution and the tolerated-effects switch cannot drift from the command whose cache this run
106
+ # shares. `workers: 0` because a collecting run is pinned to the sequential path anyway, and
107
+ # `--no-cache` mirrors `rigor check`'s flag of the same name.
108
+ runner = CheckRunnerFactory.build(
109
+ configuration: configuration,
110
+ options: { no_cache: options.fetch(:no_cache), explain: false, stats: false, workers: 0 },
111
+ buffer: nil, cache_root: configuration.cache_path
112
+ )
113
+ runner.run((configuration.paths + paths).uniq)
114
+ SigGen::EffectAnnotation::Annotator.new(
115
+ table: runner.effect_table, envelopes: options.fetch(:effect_envelopes),
116
+ envelope_index: runner.effect_envelopes, config_envelopes: config_envelopes(configuration)
117
+ )
118
+ end
119
+
120
+ # The project's `effects.envelopes:` entries, for gate 0's `match:` half. Built with a plain
121
+ # registry rather than the run's: gate 0 asks only WHETHER an entry selects this class, never what
122
+ # it bounds, so the vocabulary a plugin would add cannot change the answer.
123
+ def config_envelopes(configuration)
124
+ Effects::ConfigEnvelopes.build(
125
+ entries: configuration.effects_envelopes,
126
+ registry: Effects::Registry.for_configuration(configuration)
127
+ )
128
+ rescue StandardError
129
+ []
130
+ end
131
+
132
+ # The withheld half of the emission, counted the way {#report_skipped} counts a skip: a method that
133
+ # did NOT get `%a{pure}` is the interesting case for a reader who expected one, and silence would
134
+ # read as "sig-gen does not do this" rather than "this method did not earn it".
135
+ def report_withheld_annotations(candidates, options)
136
+ return unless options.fetch(:format) == "text"
137
+
138
+ counts = candidates.each_with_object(Hash.new(0)) do |candidate, acc|
139
+ reason = candidate.effect_reason
140
+ acc[reason] += 1 if reason && reason != :emitted
141
+ end
142
+ return if counts.empty?
143
+
144
+ breakdown = counts.map { |reason, n| "#{SigGen::EffectAnnotation::DIAGNOSTIC_IDS.fetch(reason)}: #{n}" }
145
+ @err.puts(
146
+ "rigor sig-gen: withheld an effect annotation from #{counts.values.sum} method(s) " \
147
+ "(#{breakdown.join(', ')}). An annotation is enforced once written, so it is emitted only " \
148
+ "from an exhaustive, undischarged summary."
149
+ )
150
+ end
151
+
71
152
  # Issue #778 — one stderr line per run saying how many methods the generator declined and why, so a
72
153
  # method missing from the output is never a silent absence. Text mode only: under `--format=json` every
73
154
  # skipped row is already in the payload with its `skip_reason`, and stderr stays clean for the consumer.
@@ -90,6 +171,18 @@ module Rigor
90
171
  )
91
172
  end
92
173
 
174
+ # ADR-112 WD4 — `sig_gen.inline_declared: skip` leaving methods out is the project's own choice, not a
175
+ # method sig-gen "could not type or would not overwrite", so it is counted on a line of its own.
176
+ def report_inline_declared(candidates, options)
177
+ return unless options.fetch(:format) == "text"
178
+
179
+ count = candidates.count { |c| c.skip_reason == :inline_declared }
180
+ return if count.zero?
181
+
182
+ @err.puts("rigor sig-gen: left #{count} method(s) declared inline out of sig/, as " \
183
+ "`sig_gen.inline_declared: skip` asks (sig.skipped.inline-declared).")
184
+ end
185
+
93
186
  # A method whose rendered RBS does not parse is a Rigor rendering defect, not a fact about the user's
94
187
  # code — the generator skipped it (so the rest of the signatures are still usable and still valid), but
95
188
  # staying silent would leave the user with a quietly incomplete `sig/` and us with an unreported bug.
@@ -137,30 +230,99 @@ module Rigor
137
230
 
138
231
  # @return exit status — non-zero when a file the user asked to write could not be written.
139
232
  def dispatch_write(candidates, configuration, options)
140
- layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
141
- path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
142
- writer = SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite))
143
-
144
- results = writer.write_all(candidates)
233
+ results = build_writer(configuration, options).write_all(candidates)
145
234
 
146
- SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format))
147
- # A refused write (an assembled file that does not parse, or an existing target that is not valid
148
- # UTF-8) means the user asked for a write and did not get one, so the command must not report
149
- # success — a green `sig-gen --write` in CI would otherwise mean nothing.
235
+ refused = refused_candidates(candidates)
236
+ SigGen::Renderer.new(out: @out).render_write(results: results, format: options.fetch(:format), refused: refused)
237
+ SigGen::Renderer.refusal_lines(refused).each { |line| @err.puts(line) }
238
+ # A refused write (an assembled file that does not parse, an existing target that is not valid UTF-8,
239
+ # or a method whose inline and `sig/` declarations disagree without `--overwrite`) means the user asked for a
240
+ # write and did not get one, so the command must not report success — a green `sig-gen --write` in CI
241
+ # would otherwise mean nothing.
150
242
  refusals = %i[skipped_invalid_rbs skipped_invalid_encoding]
151
- results.any? { |result| refusals.include?(result.action) } ? 1 : 0
243
+ results.any? { |result| refusals.include?(result.action) } || !refused.empty? ? 1 : 0
244
+ end
245
+
246
+ # ADR-112 WD4 — the methods whose inline and `sig/` declarations disagree, refused because the run did not
247
+ # pass `--overwrite`. Unlike every other skip, `sig/` contradicts the source while one stands.
248
+ def refused_candidates(candidates)
249
+ candidates.select { |candidate| candidate.skip_reason == :inline_differs }
250
+ end
251
+
252
+ # ADR-112 WD4 — the same writer as {#dispatch_write}, flags included, in dry-run mode. Defined by what
253
+ # `--write` would do rather than by whether `--diff` prints anything: a tighter return against an existing
254
+ # declaration is a proposal `--write` declines without `--overwrite`, and a gate that counted it could
255
+ # never pass on a project that reviewed it and said no. With `--overwrite` it counts, because `--write
256
+ # --overwrite` would apply it.
257
+ #
258
+ # @return 1 when `sig/` is out of date (or a write would be refused), 0 when it is current.
259
+ def dispatch_check(candidates, configuration, options)
260
+ results = build_writer(configuration, options, dry_run: true).write_all(candidates)
261
+ refused = refused_candidates(candidates)
262
+ SigGen::Renderer.new(out: @out).render_check(results: results, format: options.fetch(:format),
263
+ refused: refused)
264
+ stale = SigGen::Renderer.out_of_date(results)
265
+ return 0 if stale.empty? && refused.empty?
266
+
267
+ report_check_failure(stale, refused) if options.fetch(:format) == "text"
268
+ 1
269
+ end
270
+
271
+ def report_check_failure(stale, refused)
272
+ unless stale.empty?
273
+ @err.puts("rigor sig-gen --check: #{stale.size} signature file(s) out of date; " \
274
+ "run `rigor sig-gen --write` with the same options to update them.")
275
+ end
276
+ return if refused.empty?
277
+
278
+ @err.puts("rigor sig-gen --check: #{refused.size} method(s) whose inline declaration and sig/ copy " \
279
+ "disagree; make them agree, or pass --overwrite to replace the sig/ member.")
280
+ end
281
+
282
+ def build_writer(configuration, options, dry_run: false)
283
+ layout_index = SigGen::LayoutIndex.new(signature_paths: configuration.signature_paths)
284
+ path_mapper = SigGen::PathMapper.new(configuration: configuration, layout_index: layout_index)
285
+ SigGen::Writer.new(path_mapper: path_mapper, overwrite: options.fetch(:overwrite), dry_run: dry_run)
152
286
  end
153
287
 
154
288
  # Slice 3 — collect call-site argument observations when `--params=observed` is set. When `--observe=PATH` is not
155
- # specified, default to `spec/` (skipped silently when the directory is absent).
289
+ # specified, observe the project's test roots: `test_paths:`, or whichever of `spec/` and `test/` exist. With no
290
+ # root at all the run still succeeds, but says so — otherwise every parameter stays `untyped` with nothing
291
+ # naming the reason.
156
292
  def collect_observations(configuration, options)
157
293
  return {} if options.fetch(:params) != "observed"
158
294
 
159
295
  observe_paths = options.fetch(:observe)
160
- observe_paths = ["spec"] if observe_paths.empty? && File.directory?("spec")
296
+ observe_paths = configuration.resolved_test_paths if observe_paths.empty?
297
+ if observe_paths.empty?
298
+ warn_no_test_roots(configuration)
299
+ else
300
+ warn_missing_test_roots(observe_paths)
301
+ end
161
302
  SigGen::ObservationCollector.new(configuration: configuration, paths: observe_paths).collect
162
303
  end
163
304
 
305
+ # A declared root that does not exist is read as empty, so name it: `sig-gen` is the only command that reads
306
+ # the test roots, and without this the run looks exactly like one whose tests pass no typed arguments.
307
+ def warn_missing_test_roots(observe_paths)
308
+ missing = observe_paths.reject { |path| File.exist?(path) }
309
+ return if missing.empty?
310
+
311
+ consequence = missing.size == observe_paths.size ? "; every parameter stays untyped" : ""
312
+ @err.puts("rigor sig-gen: no call sites are observed from #{missing.map(&:inspect).join(', ')}, " \
313
+ "which does not exist#{consequence}. Check `test_paths:` or --observe=PATH.")
314
+ end
315
+
316
+ def warn_no_test_roots(configuration)
317
+ reason = if configuration.test_paths.nil?
318
+ "no spec/ or test/ directory was found and `test_paths:` is unset"
319
+ else
320
+ "`test_paths:` is empty"
321
+ end
322
+ @err.puts("rigor sig-gen: --params=observed has no test roots to observe (#{reason}); every parameter " \
323
+ "stays untyped. Declare `test_paths:` in the configuration or pass --observe=PATH.")
324
+ end
325
+
164
326
  def parse_options
165
327
  options = {
166
328
  mode: "print",
@@ -170,6 +332,8 @@ module Rigor
170
332
  overwrite: false,
171
333
  observe: [],
172
334
  include_private: false,
335
+ effect_envelopes: false,
336
+ no_cache: false,
173
337
  config: nil
174
338
  }
175
339
  build_option_parser(options).parse!(@argv)
@@ -181,23 +345,33 @@ module Rigor
181
345
  nil
182
346
  end
183
347
 
184
- def build_option_parser(options) # rubocop:disable Metrics/AbcSize
348
+ def build_option_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
185
349
  OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
186
350
  opts.banner = USAGE
187
- opts.on("--print", "Write RBS skeletons to stdout (default)") { options[:mode] = "print" }
188
- opts.on("--diff", "Write a unified diff against existing RBS") { options[:mode] = "diff" }
189
- opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { options[:mode] = "write" }
190
- opts.on("--overwrite", "Allow tighter-return updates to replace user-authored RBS") do
351
+ opts.on("--print", "Write RBS skeletons to stdout (default)") { select_mode(options, "print") }
352
+ opts.on("--diff", "Write a unified diff against existing RBS") { select_mode(options, "diff") }
353
+ opts.on("--write", "Write generated RBS to sig/<path>.rbs files") { select_mode(options, "write") }
354
+ opts.on("--check", "Exit 1 when --write would change sig/; write nothing") { select_mode(options, "check") }
355
+ opts.on("--overwrite", "Allow tighter-return updates, and inline declarations that disagree with sig/, " \
356
+ "to replace user-authored RBS") do
191
357
  options[:overwrite] = true
192
358
  end
193
359
  opts.on("--include-private", "Emit private / protected instance methods (default: public only)") do
194
360
  options[:include_private] = true
195
361
  end
362
+ opts.on("--effect-envelopes", "Also emit %a{rigor:v1:effect ...} for effectful methods " \
363
+ "(requires the effects: opt-in)") do
364
+ options[:effect_envelopes] = true
365
+ end
366
+ opts.on("--no-cache", "Do not read or write the analysis cache (effect collection only)") do
367
+ options[:no_cache] = true
368
+ end
196
369
  opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
197
370
  opts.on("--params=POLICY", "Parameter policy: untyped (default), observed, observed-strict") do |value|
198
371
  options[:params] = value
199
372
  end
200
- opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable)") do |value|
373
+ opts.on("--observe=PATH", "Directory / file to scan for call-site observations (repeatable; " \
374
+ "default: the configured test_paths)") do |value|
201
375
  options[:observe] << value
202
376
  end
203
377
  opts.on("--new-files", "Emit only new-file classifications") do
@@ -213,12 +387,19 @@ module Rigor
213
387
  end
214
388
  end
215
389
 
390
+ # A second, different mode flag is a usage error rather than last-one-wins: `--check --write` must not
391
+ # quietly write in a CI job that meant to gate, nor `--write --check` quietly not.
392
+ def select_mode(options, mode)
393
+ options[:mode] = options[:mode_given] && options[:mode] != mode ? "conflict" : mode
394
+ options[:mode_given] = true
395
+ end
396
+
216
397
  def validation_error(options)
217
398
  mode = options.fetch(:mode)
218
399
  format = options.fetch(:format)
219
400
  params = options.fetch(:params)
220
401
 
221
- return "--print, --diff, and --write are mutually exclusive flags; pick one" unless VALID_MODES.include?(mode)
402
+ return "--print, --diff, --write, and --check are mutually exclusive flags; pick one" if mode == "conflict"
222
403
  return "unsupported --format=#{format}" unless VALID_FORMATS.include?(format)
223
404
  return "unsupported --params=#{params}" unless VALID_PARAM_POLICIES.include?(params)
224
405
  if params == "observed-strict"
@@ -74,7 +74,7 @@ module Rigor
74
74
 
75
75
  # The bundled skills live at `<gem_root>/skills/`. From `lib/rigor/cli/skill_command.rb` that is three directories
76
76
  # up.
77
- SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
77
+ SKILLS_ROOT = File.expand_path("../../../skills", __dir__).freeze
78
78
 
79
79
  # @return CLI exit status.
80
80
  def run
@@ -148,7 +148,6 @@ module Rigor
148
148
  @deep = deep
149
149
  end
150
150
 
151
- # @return the full describe report.
152
151
  def render
153
152
  catalog = catalog_skills
154
153
  state = ProjectStateProbe.new(@root).to_h