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
@@ -126,6 +126,29 @@ module Rigor
126
126
  [result, capture.snapshot]
127
127
  end
128
128
 
129
+ # Issue #992 — runs the block with its reads DETACHED from the current consumer (and from any enclosing
130
+ # capture) and returns `[block_result, read_set]`, so a caller that learns only afterwards whether its
131
+ # answer depended on those reads can {replay} them or drop them. `call.wrong-arity` reads a class's whole
132
+ # envelope bucket to decide a call that turns out to fit, and recording that as a file-level ancestry
133
+ # edge would make every correct call on a project class re-check on any edit to the class's files.
134
+ # Returns a nil read set when no recording is active.
135
+ def withhold
136
+ previous = Thread.current[KEY]
137
+ return [yield, nil] if previous.nil?
138
+
139
+ previous_captures = Thread.current[CAPTURE_KEY]
140
+ capture = Capture.new
141
+ Thread.current[KEY] = Accumulator.new(previous.consumer)
142
+ Thread.current[CAPTURE_KEY] = [capture]
143
+ begin
144
+ result = yield
145
+ ensure
146
+ Thread.current[KEY] = previous
147
+ Thread.current[CAPTURE_KEY] = previous_captures
148
+ end
149
+ [result, capture.snapshot]
150
+ end
151
+
129
152
  # ADR-84 WD2 — replays a {ReadSet} into the current consumer's accumulator as if each read happened
130
153
  # here, applying the same per-consumer self-read filter as {read_site}, and tees into every active
131
154
  # capture so an enclosing capture window stays transitive when a memo hit substitutes for a body walk.
@@ -21,8 +21,17 @@ module Rigor
21
21
  # #775 — a target is an immutable value keyed by its name, and the local target is built on every
22
22
  # binding (`Scope#with_local` invalidates through it) and every local-fact read: ~350k times on
23
23
  # the lib self-check for a few thousand distinct names. One shared instance per name.
24
+ #
25
+ # #1055 — the table is a class ivar holding an unshareable Hash, and a non-main Ractor may neither
26
+ # write one nor read one. A pool worker therefore pays the allocation instead of sharing the
27
+ # instance: the interning is a per-process ALLOCATION saving, never an identity contract (`Target`
28
+ # is a `Data` value and compares by value), so declining it changes nothing a caller can observe.
29
+ # The guard is a C-level predicate on the hot path, which is what keeps the sequential run's
30
+ # saving intact.
24
31
  def self.local(name)
25
32
  name = name.to_sym
33
+ return new(kind: :local, name: name) unless Ractor.main?
34
+
26
35
  @local_targets[name] ||= new(kind: :local, name: name)
27
36
  end
28
37
  @local_targets = {}
@@ -120,6 +120,32 @@ module Rigor
120
120
  appeared.freeze
121
121
  end
122
122
 
123
+ # Issue #1120 — the method names whose refinements moved in this edit: a `[refined class, method, refining
124
+ # module]` entry that appeared in, or vanished from, any file in `paths`. A consumer whose
125
+ # `call.undefined-method` answer consulted the refinement table recorded a `refinement:<method>` edge
126
+ # whichever way it answered, because the answer is a function of the whole project's refinements of that
127
+ # name. `paths` MUST include removed files, whose whole before-table vanished. `before` / `after` map a
128
+ # path to that file's `{refined class => {method => [modules]}}` table (nil when it refines nothing).
129
+ def changed_refinement_names(paths, before, after)
130
+ moved = Set.new
131
+ paths.each do |path|
132
+ entries_before = refinement_entries(before[path])
133
+ entries_after = refinement_entries(after[path])
134
+ (entries_before ^ entries_after).each { |(_refined, method_name, _module)| moved << method_name }
135
+ end
136
+ moved.freeze
137
+ end
138
+
139
+ def refinement_entries(table)
140
+ entries = Set.new
141
+ table&.each do |refined, methods|
142
+ methods.each do |method_name, modules|
143
+ modules.each { |refining| entries << [refined, method_name, refining] }
144
+ end
145
+ end
146
+ entries
147
+ end
148
+
123
149
  # Issue #639 — the SYMMETRIC reading of {appeared_classes}: the names whose declaration in a file
124
150
  # appeared OR VANISHED. Appearance alone answered the miss side, which was the only side recording a
125
151
  # `class:` edge; a bare reference that RESOLVED now records one too, and what invalidates it is the
@@ -162,7 +162,11 @@ module Rigor
162
162
  runner = build_runner(record_dependencies: true)
163
163
  diagnostics = run_runner(runner).diagnostics
164
164
  @last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
165
- @analyzed = runner.analyzed_files
165
+ # #392 — the template units are deliberately NOT in the analysed set. `Runner#target_files`
166
+ # re-analyses every unit on every run, so a unit must never be reused from the per-file cache and
167
+ # must never read as a file that VANISHED from the project on the next recheck (which is what
168
+ # `previous - current` would say, `current_files` being a `.rb` expansion).
169
+ @analyzed = runner.analyzed_files - runner.template_unit_paths
166
170
  @seed_bundles = runner.seed_bundles # ADR-85 WD2 — the freshly built bundle set for the next run.
167
171
  absorb_dependency_graph(runner)
168
172
  @return_summaries = runner.return_summaries # ADR-89 WD2 — the full-run behavioural surface.
@@ -283,12 +287,24 @@ module Rigor
283
287
  symbol_pairs = behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) | param_pairs
284
288
  base = dependents_base(unstable, symbol_pairs)
285
289
  closure = base | changed.to_set | added.to_set |
286
- negative_affected(scan, removed, new_fps, new_class_decls, new_constant_decls)
290
+ negative_affected(scan, removed, new_fps, new_class_decls, new_constant_decls, summary)
287
291
  closure = param_seed_closure(closure, param_files)
288
292
  removed.each { |path| closure |= @dependents[path] || Set.new }
289
293
  closure.freeze
290
294
  end
291
295
 
296
+ # Issue #1367 — every file whose last result holds a `global.*` diagnostic re-checks on any recheck. What
297
+ # exempts such a write is a fact of the whole program — an alias, or a `write` / `to_str` / `to_int` / hatch
298
+ # defined in any spelling on any receiver, a rewritten ancestor, a mixin — which no name-keyed edge can
299
+ # follow, so a stale report would outlive the edit that exempts it. Such files are rare, so this costs little.
300
+ # A file the census silenced is not re-checked when the exemption goes away: that stale answer misses a
301
+ # report, it never makes one.
302
+ def global_write_reporters
303
+ @cache.each_with_object(Set.new) do |(path, diagnostics), reporters|
304
+ reporters << path if diagnostics.any? { |diagnostic| diagnostic.rule.to_s.start_with?("global.") }
305
+ end
306
+ end
307
+
292
308
  # ADR-67 WD6c lift — the seed-invalidated callees' own contribution to the closure: the files
293
309
  # themselves, plus — on a pre-slice-4 snapshot with no symbol edges, where the pairs' symbol fan-out
294
310
  # found nothing — their file-level dependents (wider, always sound).
@@ -828,7 +844,9 @@ module Rigor
828
844
  # a prior missed lookup, and (issue #639) because a class DECLARATION appeared or vanished. Maps each
829
845
  # appeared `"ClassName#method"` to the negative-dependency key it would satisfy (`toplevel:foo` for a
830
846
  # top-level def, `method:C#m` otherwise), then unions the recorded negative-dependents of those keys.
831
- def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls)
847
+ # Issue #1120 — plus the consumers of any refinement name the edit moved ({#refinement_affected}). Issue #1367 —
848
+ # and every file whose last result holds a `global.*` diagnostic ({#global_write_reporters}).
849
+ def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls, summary = nil)
832
850
  appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
833
851
  # Issue #639 — a class DISAPPEARING satisfies the `class:` kind too, now that a resolved bare
834
852
  # reference records the edge and not only a failed one; removed files join the diff for it.
@@ -844,7 +862,21 @@ module Rigor
844
862
  keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
845
863
  keys.concat(moved_classes.map { |klass| "class:#{klass.split('::').last}" })
846
864
  keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
847
- Incremental.negative_closure(keys, @negative_dependents)
865
+ Incremental.negative_closure(keys, @negative_dependents) | refinement_affected(changed + removed, summary) |
866
+ global_write_reporters
867
+ end
868
+
869
+ # Issue #1120 — the consumers whose `call.undefined-method` answer read the refinement table for a method
870
+ # name whose refinements moved in this edit. The before-state is each file's seed bundle (the per-file
871
+ # table the last run recorded), the after-state the scan's per-file tables. A refine body is not a class's
872
+ # method, so neither the symbol fingerprints nor the appeared-symbol diff above sees it.
873
+ def refinement_affected(paths, summary)
874
+ after = (summary && summary[:refinements]) || {}
875
+ before = paths.to_h { |path| [path, @seed_bundles.dig(path, :refinements)] }
876
+ names = Incremental.changed_refinement_names(paths, before, after)
877
+ return Set.new if names.empty?
878
+
879
+ Incremental.negative_closure(names.map { |name| "refinement:#{name}" }, @negative_dependents)
848
880
  end
849
881
 
850
882
  # The qualified class/module names declared in the pre-parsed `index` (shared with
@@ -16,13 +16,23 @@ module Rigor
16
16
  # are NOT visible until the owner invalidates the scan — typically via `workspace/didChangeWatchedFiles`.
17
17
  # This is the same trade-off the LSP made when slice 7 cached only the `Environment`; extending the cache
18
18
  # to the pre-pass outputs preserves the contract.
19
+ #
20
+ # `template_units` (#1038) is the one slot a consumer does not adopt verbatim. The rest of the snapshot
21
+ # is reused as it stands until the owner invalidates it; the template index is handed to
22
+ # `TemplateUnits.collect(previous:)`, which re-expands the claimed globs and revalidates each template
23
+ # against the filesystem, so a template edited, added or deleted on disk is seen on the next publish
24
+ # even though the snapshot itself did not move. What the carry saves is the plugin transform — for ERB
25
+ # (#393) an Erubi compile of every view in the project, otherwise paid per keystroke. The index is
26
+ # always built WITHOUT a buffer binding, so the shared snapshot never holds a unit compiled from an
27
+ # editor's in-flight bytes.
19
28
  ProjectScan = Data.define(
20
29
  :plugin_registry,
21
30
  :dependency_source_index,
22
31
  :synthetic_method_index,
23
32
  :project_patched_methods,
24
33
  :plugin_prepare_diagnostics,
25
- :pre_eval_diagnostics
34
+ :pre_eval_diagnostics,
35
+ :template_units
26
36
  )
27
37
  end
28
38
  end
@@ -83,7 +83,6 @@ module Rigor
83
83
  # Loads the project's configured plugins, runs every `#prepare`, and returns the union of every
84
84
  # published `:reachability_roots` and `:reachability_references` fact.
85
85
  #
86
- # @param configuration — the loaded project configuration.
87
86
  # @param plugin_requirer — how a plugin gem is brought into the process, called with the gem name. The
88
87
  # same seam `Analysis::Runner` exposes, so a spec can register a plugin class without publishing a
89
88
  # gem.
@@ -36,6 +36,7 @@ module Rigor
36
36
  File.expand_path("scan.rb", __dir__),
37
37
  File.expand_path(__FILE__)
38
38
  ].freeze
39
+ Ractor.make_shareable(SOURCE_FILES)
39
40
 
40
41
  # A file modified within this window of the recording instant is computed but never
41
42
  # recorded — the FileDigest racy-write guard, one tier simpler because a refused signature
@@ -656,6 +656,90 @@ module Rigor
656
656
  since: "0.1.2"
657
657
  ),
658
658
 
659
+ CheckRules::RULE_GLOBAL_WRITE_TYPE_MISMATCH => Entry.new(
660
+ id: CheckRules::RULE_GLOBAL_WRITE_TYPE_MISMATCH,
661
+ summary: "A special global is assigned a literal its setter rejects (TypeError at runtime).",
662
+ fires_when: [
663
+ "The write is `$g = literal` to a special whose setter checks the value: `$/` / `$-0`, `$,` and " \
664
+ "`$\\` (a String or nil), `$;` / `$-F` (a String, a Regexp, nil, or an object with `to_str`), `$~` " \
665
+ "(a MatchData or nil), `$0` / `$PROGRAM_NAME` (a String or an object with `to_str`), `$.` (an " \
666
+ "Integer, a Float, or an object with `to_int`), `$-i` (a String, nil, false, or an object with " \
667
+ "`to_str`), and `$stdout` / `$>` / `$stderr` (an object that responds to `write`, ADR-117 WD2).",
668
+ "The value is a literal node — an Integer, Float, Rational or imaginary literal, a String, Symbol " \
669
+ "or Regexp literal (interpolated or not), an Array or Hash literal, or `nil` / `true` / `false`, " \
670
+ "parenthesised or not — whose class the setter does not take: `$/ = 1`, `$/ = /x/`, `$~ = \"x\"`, " \
671
+ "`$0 = nil`, `$. = \"3\"`, `$stdout = 1`.",
672
+ "Where the setter also converts (`to_str`, `to_int`) or asks for `write`, the literal's object " \
673
+ "cannot answer that method: RBS gives its class neither the method nor a `method_missing` / " \
674
+ "`respond_to_missing?` / `respond_to?` of its own, and the program defines none of them anywhere.",
675
+ "The envelope is the interpreter's setter, not the global's RBS declaration: `$/ = /x/` fires " \
676
+ "although `$;` takes a Regexp."
677
+ ],
678
+ does_not_fire_when: [
679
+ "The special's setter accepts every value: `$stdin` (never checked, ADR-117 WD2), `$_`, `$VERBOSE`, " \
680
+ "`$DEBUG`, `$=`; nor `$@`, whose setter depends on whether `$!` is set.",
681
+ "The value is not a literal — a variable, a method call, a constant, a conditional — whatever its " \
682
+ "inferred type: a class RBS declares without `write` or `to_str` does not prove the object lacks it.",
683
+ "RBS gives the literal's class the method or a hatch of its own (a `sig/` reopening or `include` " \
684
+ "counts).",
685
+ "The program — any project or `pre_eval:` file — defines the method or a `method_missing` / " \
686
+ "`respond_to_missing?` / `respond_to?` anywhere, in any spelling (`def`, `define_method`, " \
687
+ "`define_singleton_method`, `alias`, `alias_method`, `attr_*`, a delegation macro, a string `eval`, " \
688
+ "the same through `send`) and on any receiver, or defines a method whose name no literal spells (a " \
689
+ "computed `define_method` or `send` name, a string `eval` whose code is not a plain literal, a name " \
690
+ "whose bytes are not valid in its encoding). " \
691
+ "This is blanket: no census of where a definition lands can be complete, so the literal of every " \
692
+ "class declines, whatever the definition's owner.",
693
+ "An ancestor's surface is rewritten from outside (`Integer.include(M)`, `Object.include(M)`, a string " \
694
+ "`class_eval`), or a top-level `include` / `prepend` / `extend`, or an ancestor's `include`, names a " \
695
+ "module RBS does not rule out.",
696
+ "For `$stdout` / `$>` / `$stderr`, a refinement that may add `write`, to any class, and a `using` in " \
697
+ "effect at the write. Which class it refines is not followed. The `to_str` / `to_int` conversions, and a " \
698
+ "refined `respond_to_missing?` / `respond_to?`, ignore refinements, so those still fire.",
699
+ "Any project or `pre_eval:` file aliases the special (`alias $stdout $out`, on either side).",
700
+ "The write is `$g op= v`, `$g ||= v`, `$g &&= v`, a multiple-assignment target, a `for` index, or a " \
701
+ "`rescue => $g` reference: their value is not type-checked.",
702
+ "The write sits in the dead arm of a decidable version guard. Other code that never runs (`if " \
703
+ "false`, after `return`) is not recognised, and a write there still fires."
704
+ ],
705
+ suppression: "`# rigor:disable global.write-type-mismatch` on the write.",
706
+ severity_authored: :error,
707
+ severity_by_profile: { lenient: :warning, balanced: :error, strict: :error },
708
+ # A literal's class is exact, the setter's envelope is the running interpreter's, and every way a program can
709
+ # give the object the method the setter asks for declines; the write then raises every time it runs.
710
+ evidence_tier: :high,
711
+ since: "0.4.0"
712
+ ),
713
+
714
+ CheckRules::RULE_GLOBAL_READONLY_WRITE => Entry.new(
715
+ id: CheckRules::RULE_GLOBAL_READONLY_WRITE,
716
+ summary: "A read-only special global is written (NameError at runtime).",
717
+ fires_when: [
718
+ "The write is `$g = value`, `$g op= value`, or a target of a multiple assignment (`$g, x = ...`, " \
719
+ "`*$g`) — forms that always write — to a special Ruby defines read-only: `$!`, `$$`, `$?`, `$<`, " \
720
+ "`$FILENAME`, `$*`, `$:` / `$LOAD_PATH` / `$-I`, `$\"` / `$LOADED_FEATURES`, `$-W`, `$-p`, `$-l`, " \
721
+ "`$-a`.",
722
+ "Whatever the value: the setter raises `NameError` (`$! is a read-only variable`) before it " \
723
+ "looks at it."
724
+ ],
725
+ does_not_fire_when: [
726
+ "The write is `$g ||= v` or `$g &&= v`, which writes only when the current value is falsy " \
727
+ "(truthy): `$LOAD_PATH ||= []` never writes.",
728
+ "The write is a `for` index or a `rescue => $g` reference.",
729
+ "Any project or `pre_eval:` file aliases the special (`alias $! $err`, on either side), which can " \
730
+ "make the name another variable.",
731
+ "Mutating the value is not a write: `$LOAD_PATH << dir` and `$LOADED_FEATURES.delete(f)` stay silent.",
732
+ "The write sits in the dead arm of a decidable version guard. Other code that never runs is not " \
733
+ "recognised, and a write there still fires."
734
+ ],
735
+ suppression: "`# rigor:disable global.readonly-write` on the write.",
736
+ severity_authored: :error,
737
+ severity_by_profile: { lenient: :error, balanced: :error, strict: :error },
738
+ # Syntactic: the name alone decides it, and the write raises every time it runs.
739
+ evidence_tier: :high,
740
+ since: "0.4.0"
741
+ ),
742
+
659
743
  CheckRules::RULE_SUPPRESSION_UNKNOWN_RULE => Entry.new(
660
744
  id: CheckRules::RULE_SUPPRESSION_UNKNOWN_RULE,
661
745
  summary: "A `# rigor:disable[-file]` comment names a rule that does not exist.",
@@ -896,6 +980,48 @@ module Rigor
896
980
  since: "0.3.4"
897
981
  ),
898
982
 
983
+ CheckRules::RULE_CONTRADICTING_SIGNATURE => Entry.new(
984
+ id: CheckRules::RULE_CONTRADICTING_SIGNATURE,
985
+ summary: "Two declarations of one method provably contradict: no value or call satisfies both.",
986
+ fires_when: [
987
+ "A method is declared both in the project's `sig/` and by an inline `# @rbs` / `#:` " \
988
+ "annotation, and the return or a parameter every call must pass holds two proven-disjoint " \
989
+ "types — distinct literals, a literal outside such a class, or two classes written absolutely " \
990
+ "whose RBS comes from Ruby core or the stdlib, neither an RBS ancestor of the other " \
991
+ "(`::String` against `::Integer`) — or the two keyword-free declarations accept " \
992
+ "positional counts that cannot meet, or one requires a keyword the other cannot take in any " \
993
+ "form. Several overloads are paired by correspondence, never by order. Positioned at the " \
994
+ "`.rbs` member; Rigor reads that declaration.",
995
+ "A member-level `%a{rigor:v1:return: …}` or `%a{rigor:v1:param: …}` refinement is proven " \
996
+ "disjoint from the type its own member declares, in every overload it applies to " \
997
+ "(`rigor:v1:return: positive-int` on `-> String`)."
998
+ ],
999
+ does_not_fire_when: [
1000
+ "One declaration refines the other (`String` against `non-empty-string`, `Symbol` against " \
1001
+ "`:asc | :desc`, `bool` against `TrueClass`): the two merge to the more precise one, silently.",
1002
+ "Either side is `untyped`, `void` or `top` in that position — consistent with everything.",
1003
+ "Rigor cannot prove the two contradict: a module or interface on either side, a class the " \
1004
+ "project declares (in `sig/`, inline or only in Ruby), a gem's class, any relative class name, " \
1005
+ "two element types of one generic class, a type alias, `self`, a type variable, a proc, an " \
1006
+ "optional or rest position, any block position, overloads that do not pair one to one, or " \
1007
+ "differently shaped parameter lists that still overlap. The " \
1008
+ "inline signature is then dropped and reported as `source-rbs-annotation-not-honoured` (info).",
1009
+ "A refinement is written on one overload rather than on the member: call sites do not honour it.",
1010
+ "Two `.rbs` files declare the same method (that fails the class's definition build, " \
1011
+ "`rbs.coverage.definition-build-failed`), or a project `.rbs` collides with bundled RBS " \
1012
+ "(the file is quarantined, `rbs.coverage.quarantined-signature`)."
1013
+ ],
1014
+ suppression: "`disable: [\"rbs.contradicting-signature\"]` in `.rigor.yml`, or a baseline entry. A " \
1015
+ "`# rigor:disable` comment does not reach a row positioned in a `.rbs` file. The real " \
1016
+ "fix is to regenerate the stale signature or correct the annotation.",
1017
+ severity_authored: :error,
1018
+ severity_by_profile: { lenient: :error, balanced: :error, strict: :error },
1019
+ # Both sides are authored declarations, and a firing needs a proof that no value or call satisfies
1020
+ # both — the ADR-35 both-sides-authored construction, with every doubt routed to the `:info` row.
1021
+ evidence_tier: :high,
1022
+ since: "0.4.0"
1023
+ ),
1024
+
899
1025
  CheckRules::RULE_SUPPRESSION_UNKNOWN_MARKER => Entry.new(
900
1026
  id: CheckRules::RULE_SUPPRESSION_UNKNOWN_MARKER,
901
1027
  summary: "A comment uses a suppression marker Rigor does not recognise " \
@@ -925,6 +1051,7 @@ module Rigor
925
1051
  since: "0.3.0"
926
1052
  )
927
1053
  }.freeze
1054
+ Ractor.make_shareable(ENTRIES)
928
1055
 
929
1056
  module_function
930
1057
 
@@ -82,15 +82,23 @@ module Rigor
82
82
 
83
83
  # @param rbs_config_entries — the RBS-derived config slots
84
84
  # (`rbs.libraries` [+ `rbs.virtual_rbs`]). nil on any failure so a malformed key disables the cache.
85
- def descriptor(configuration:, files:, explain:, rbs_config_entries:)
85
+ # @param template_units_digest — #392 — the run's template-unit identity
86
+ # ({Analysis::TemplateUnits#digest}: every unit's source bytes + transform id + synthesis version).
87
+ # nil for a project whose plugins declare no `template_globs:`, which is every project today, and a
88
+ # nil contributes NO slot — so no existing key moves. A project that DOES have units produces a key
89
+ # the ADR-87 boot-slim probe (which loads no plugin and so passes nil) cannot reconstruct: the probe
90
+ # simply misses and the full path takes over, the same forgone-fast-lane trade the `rbs.virtual_rbs`
91
+ # slot already makes, and never a wrong hit.
92
+ def descriptor(configuration:, files:, explain:, rbs_config_entries:, template_units_digest: nil)
86
93
  Cache::Descriptor.new(
87
94
  gems: [Cache::RbsDescriptor.rbs_gem_entry],
88
- configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) + [
89
- config_entry("configuration", Marshal.dump(configuration.to_h)),
90
- config_entry("engine",
91
- "#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{explain}"),
92
- config_entry("paths", files.sort.join("\n"))
93
- ]
95
+ configs: rbs_config_entries + engine_source_entries + lockfile_entries(configuration) +
96
+ template_unit_entries(template_units_digest) + [
97
+ config_entry("configuration", Marshal.dump(configuration.to_h)),
98
+ config_entry("engine",
99
+ "#{Rigor::VERSION}:#{Cache::Descriptor::SCHEMA_VERSION}:#{explain}"),
100
+ config_entry("paths", files.sort.join("\n"))
101
+ ]
94
102
  )
95
103
  rescue StandardError
96
104
  nil
@@ -104,11 +112,12 @@ module Rigor
104
112
  #
105
113
  # {Cache::EngineSource::Unavailable} is left to propagate into `descriptor`'s rescue, which disables
106
114
  # the cache for the run: an engine we cannot identify must not be keyed by its version alone.
107
- def engine_source_entries
108
- identity = Cache::EngineSource.process_identity
109
- return [] if identity.nil?
115
+ def template_unit_entries(digest)
116
+ digest.nil? ? [] : [config_entry("template-units", digest)]
117
+ end
110
118
 
111
- [config_entry("engine-source", identity)]
119
+ def engine_source_entries
120
+ Cache::EngineSource.key_config_entries
112
121
  end
113
122
 
114
123
  # Issue #564 — the two dependency lockfiles, BY CONTENT. Both are inputs to the run's diagnostics and