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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../check_rules/rule_ids"
3
4
  require_relative "../crash_signature"
4
5
  require_relative "../diagnostic"
5
6
  require_relative "../severity_stamp"
@@ -39,7 +40,9 @@ module Rigor
39
40
  # (`[error_class, first_error_line, conflicting_buffer_names]`) or nil when the env built.
40
41
  # @param definition_build_failures_snapshot — issue #696 — reader returning the per-class
41
42
  # `RBS::DefinitionBuilder` failures the run observed, as `[class_name, error_class, member,
42
- # conflicting_buffer_names]` tuples. Empty for a healthy sig set.
43
+ # conflicting_buffer_names, unresolved_type_name_detail]` tuples (issue #997 added the fifth
44
+ # element — `[token, location, refinement_hint]`, `nil` unless `error_class` is
45
+ # `RBS::NoTypeFoundError`). Empty for a healthy sig set.
43
46
  # @param hkt_scan_failure_snapshot — issue #784 — reader returning the `[error_class_name,
44
47
  # first_message_line, raw_frame_or_nil, stage]` tuple whichever stage of the HKT-registry build
45
48
  # raised, or nil when both built (or were never demanded). `stage` is `:scan` (the RBS `type`-alias
@@ -51,7 +54,8 @@ module Rigor
51
54
  pre_eval_diagnostics_from_scanner:, synthesized_namespaces_snapshot:,
52
55
  quarantined_signatures_snapshot:, env_build_failure_snapshot:,
53
56
  definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
54
- conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] })
57
+ conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] },
58
+ pooled_run_disclosures: -> { [] })
55
59
  @configuration = configuration
56
60
  @rbs_extended_reporter = rbs_extended_reporter
57
61
  @boundary_cross_reporter = boundary_cross_reporter
@@ -68,6 +72,9 @@ module Rigor
68
72
  @definition_build_failures_snapshot_reader = definition_build_failures_snapshot
69
73
  @hkt_scan_failure_snapshot_reader = hkt_scan_failure_snapshot
70
74
  @conformance_results_snapshot_reader = conformance_results_snapshot
75
+ # Issue #1051 — reader for the pool coordinator's de-duplicated run-scoped disclosure table.
76
+ # Empty on a sequential run, where `plugin_registry`'s own instances are the ones that registered.
77
+ @pooled_run_disclosures_reader = pooled_run_disclosures
71
78
  end
72
79
 
73
80
  # Pre-file diagnostic streams that fire once per run rather than per analyzed file: plugin load /
@@ -441,8 +448,9 @@ module Rigor
441
448
  # "Explicit conformance directive"). A class that declares `conforms-to _Interface` but is missing
442
449
  # a required interface method surfaces as `rbs_extended.unsatisfied-conformance`; an unresolvable
443
450
  # interface name surfaces as `dynamic.rbs-extended.unresolved` `:info` (the same fail-soft channel
444
- # the other directive parsers use). Empty for a project with no directive, a well-formed
445
- # conformance, or a non-sequential pool run (the snapshot mirrors `synthesized_namespaces`).
451
+ # the other directive parsers use). Empty for a project with no directive or a well-formed
452
+ # conformance; every analysis path, pooled or degraded, takes the snapshot `synthesized_namespaces`
453
+ # comes from.
446
454
  def conforms_to_diagnostics
447
455
  results = conformance_results_snapshot
448
456
  return [] if results.nil? || results.empty?
@@ -585,22 +593,50 @@ module Rigor
585
593
  end
586
594
 
587
595
  # The closing advice follows the FIRST failure's error class: `GenericParameterMismatchError` is two
588
- # declarations of one CLASS at different generic arity (#610), and "remove the duplicate member"
589
- # sends its reader after a member that does not exist.
596
+ # declarations of one CLASS at different generic arity (#610); `NoTypeFoundError` (issue #997) is
597
+ # ONE declaration naming a type that does not exist, which "remove the duplicate declaration" sends
598
+ # its reader after a duplicate that is not there — the fifth tuple element
599
+ # ({RbsLoader#unresolved_type_name_detail}) is non-nil for exactly that case. Every other error class
600
+ # (the two duplicated-definition errors, `RecursiveAncestorError`, …) keeps the duplicate-declaration
601
+ # wording, which is accurate for them.
590
602
  def definition_build_advice(failure)
591
- _, error_class, = failure
603
+ _, error_class, _member, _buffers, unresolved = failure
592
604
  if error_class.to_s.end_with?("GenericParameterMismatchError")
593
605
  "Two signature sources declare the class with a different number of type parameters; make " \
594
606
  "the declarations agree (`rbs validate`) to restore type coverage."
607
+ elsif unresolved
608
+ unresolved_type_name_advice(unresolved)
595
609
  else
596
610
  "Two signature sources declare the same member; remove the duplicate declaration " \
597
611
  "(`rbs validate`) to restore type coverage."
598
612
  end
599
613
  end
600
614
 
601
- # `[class_name, error_class, member, buffers]`. The member is nil for the error classes that carry no
602
- # name at all (`RecursiveAncestorError`), and the clause then names the error class alone rather than
603
- # inventing a member.
615
+ # Issue #997 — `unresolved` is {RbsLoader#unresolved_type_name_detail}'s `[token, location,
616
+ # refinement_hint]`. `refinement_hint` turns the message into a fix: the token is not a typo to
617
+ # chase down but the start of a Rigor refinement name that is not valid in an RBS type position, and
618
+ # the reader is told the spelling that IS valid today. Without a hint, the token is named with its
619
+ # position and the reader is left with the same "not a duplicate" correction plus a typo/missing-load
620
+ # check — still narrower than the duplicate-declaration wording it replaces.
621
+ def unresolved_type_name_advice(unresolved)
622
+ token, location, refinement_hint = unresolved
623
+ position = location ? " at `#{location}`" : ""
624
+ if refinement_hint
625
+ "`#{token}`#{position} does not name a type RBS can resolve; it is the start of the Rigor " \
626
+ "refinement `#{refinement_hint}`, which is not valid in an RBS type position. Write it as " \
627
+ "an `%a{rigor:v1:param: name is #{refinement_hint}}` (or `return:` / `assert`) annotation " \
628
+ "alongside the plain RBS type instead — see docs/manual/16-rbs-extended-annotations.md — " \
629
+ "to restore type coverage."
630
+ else
631
+ "One signature source names `#{token}`#{position}, which is not a type RBS can resolve; fix " \
632
+ "the typo, or if it names a class, confirm it is loaded (`rbs validate`) to restore type " \
633
+ "coverage."
634
+ end
635
+ end
636
+
637
+ # `[class_name, error_class, member, buffers, unresolved_type_name_detail]`. The member is nil for
638
+ # the error classes that carry no name at all (`RecursiveAncestorError`), and the clause then names
639
+ # the error class alone rather than inventing a member.
604
640
  def first_failure_clause(failure)
605
641
  _, error_class, member, = failure
606
642
  return " First failure: #{error_class}." if member.nil? || member.empty?
@@ -849,10 +885,29 @@ module Rigor
849
885
  return [] if @source_rbs_synthesis_reporter.empty?
850
886
 
851
887
  @source_rbs_synthesis_reporter.entries.map do |entry|
852
- entry.kind == :not_honoured ? not_honoured_diagnostic(entry) : synthesis_failed_diagnostic(entry)
888
+ case entry.kind
889
+ when :not_honoured then not_honoured_diagnostic(entry)
890
+ when :contradiction then contradicting_signature_diagnostic(entry)
891
+ else synthesis_failed_diagnostic(entry)
892
+ end
853
893
  end
854
894
  end
855
895
 
896
+ # ADR-112 WD5 / issue #1075 — a member whose two declarations contradict, or whose refinement is
897
+ # outside its own declared type. An error in every profile: the false-positive discipline is carried
898
+ # by the rule's narrow definition of a contradiction, never by the severity (ADR-112 WD5). The message
899
+ # is composed where the record is read ({Environment.record_member_consistency}), since only there
900
+ # are both sources' paths at hand.
901
+ def contradicting_signature_diagnostic(entry)
902
+ Diagnostic.new(
903
+ path: entry.path, line: entry.line || 1, column: 1,
904
+ message: entry.message,
905
+ severity: :error,
906
+ rule: CheckRules::RULE_CONTRADICTING_SIGNATURE,
907
+ source_family: :builtin
908
+ )
909
+ end
910
+
856
911
  def synthesis_failed_diagnostic(entry)
857
912
  Diagnostic.new(
858
913
  path: entry.path, line: 1, column: 1,
@@ -938,6 +993,103 @@ module Rigor
938
993
  plugin_registry.plugins.filter_map { |plugin| plugin_trust_refusal_diagnostic(plugin) }
939
994
  end
940
995
 
996
+ # Issue #1051 — the run-scoped plugin disclosures ({Plugin::Base#disclose_once}), the run's
997
+ # project-global notices about its own inputs. Two sources feed one table:
998
+ #
999
+ # - the coordinator-side registry, which is where a SEQUENTIAL run's `#prepare` and per-file hooks
1000
+ # ran; and
1001
+ # - `pooled_run_disclosures`, the pool coordinator's already-de-duplicated collection from the
1002
+ # pre-fork parent session and every worker payload.
1003
+ #
1004
+ # Both are keyed `[plugin id, key]` and the first registration of a pair wins, so a run emits one
1005
+ # row per pair no matter how many plugin instances (workers) reached it — the #1051 bug, where a
1006
+ # per-instance `@emitted` flag produced one row per WORKER.
1007
+ #
1008
+ # The emitted order is `(registry position, key)`, NOT registration order: a worker's slice
1009
+ # determines which instance registers a key first, so registration order is a function of
1010
+ # `--workers N` and would make a pooled run's stream differ from a sequential one's even with the
1011
+ # duplicates gone. Registry position is the plugin load order (topological by `consumes:`), which
1012
+ # both modes share.
1013
+ #
1014
+ # Position is `.rigor.yml:1:1` for every row; see {Plugin::Base#disclose_once} for why that and not
1015
+ # the first analysed file. A positioned batch ({Plugin::Base#emit_once}) shares the table but not
1016
+ # this stream — see {#plugin_run_emission_diagnostics}.
1017
+ def plugin_run_disclosure_diagnostics
1018
+ ordered_run_disclosure_records
1019
+ .reject { |record| record.key?(:diagnostics) }
1020
+ .map { |record| run_disclosure_diagnostic(record) }
1021
+ end
1022
+
1023
+ # Issue #1060 — the positioned batches ({Plugin::Base#emit_once}) from the same table, de-duplicated
1024
+ # by the same `[plugin id, key]` identity over the same sources, so the first registration of a key
1025
+ # wins whole and a batch is never merged row by row across workers. Unlike a disclosure, every row
1026
+ # keeps the position the plugin gave it; the engine only stamps `source_family: "plugin.<id>"`, the
1027
+ # same stamp a `#diagnostics_for_file` row gets, so a baseline keyed to the row's own file and
1028
+ # qualified rule matches it before and after a plugin moves onto this channel.
1029
+ #
1030
+ # Order is `(registry position, key)` and then the batch's own row order — the same
1031
+ # `--workers`-independent order the disclosure stream uses. The runner appends this stream right
1032
+ # after the per-file stream rather than into the run-level block: these rows are about files, and
1033
+ # the files they name are usually not among the analysed targets (a view template), so there is no
1034
+ # file slot to splice them into.
1035
+ def plugin_run_emission_diagnostics
1036
+ ordered_run_disclosure_records
1037
+ .select { |record| record.key?(:diagnostics) }
1038
+ .flat_map { |record| record[:diagnostics].map { |row| run_emission_diagnostic(row, record[:plugin_id]) } }
1039
+ end
1040
+
1041
+ def ordered_run_disclosure_records
1042
+ registry = plugin_registry
1043
+ return [] if registry.empty?
1044
+
1045
+ positions = {}
1046
+ registry.plugins.each_with_index { |plugin, index| positions[disclosure_plugin_id(plugin)] ||= index }
1047
+ collect_run_disclosure_records(registry)
1048
+ .sort_by { |record| [positions.fetch(record[:plugin_id], positions.size), record[:key].to_s] }
1049
+ end
1050
+
1051
+ def collect_run_disclosure_records(registry)
1052
+ records = {}
1053
+ Array(pooled_run_disclosures).each do |record|
1054
+ records[[record[:plugin_id], record[:key]]] ||= record
1055
+ end
1056
+ registry.plugins.each do |plugin|
1057
+ plugin_id = disclosure_plugin_id(plugin)
1058
+ Array(plugin.run_disclosure_records).each do |record|
1059
+ records[[plugin_id, record[:key]]] ||= record.merge(plugin_id: plugin_id)
1060
+ end
1061
+ rescue StandardError
1062
+ next
1063
+ end
1064
+ records.values
1065
+ end
1066
+
1067
+ def run_disclosure_diagnostic(record)
1068
+ Diagnostic.new(
1069
+ path: ".rigor.yml", line: 1, column: 1,
1070
+ message: record.fetch(:message),
1071
+ severity: record.fetch(:severity),
1072
+ rule: record.fetch(:rule),
1073
+ source_family: "plugin.#{record[:plugin_id]}"
1074
+ )
1075
+ end
1076
+
1077
+ def run_emission_diagnostic(row, plugin_id)
1078
+ Diagnostic.new(
1079
+ path: row.path, line: row.line, column: row.column,
1080
+ message: row.message,
1081
+ severity: row.severity,
1082
+ rule: row.rule,
1083
+ source_family: "plugin.#{plugin_id}"
1084
+ )
1085
+ end
1086
+
1087
+ def disclosure_plugin_id(plugin)
1088
+ plugin.manifest.id
1089
+ rescue StandardError
1090
+ plugin.class.to_s
1091
+ end
1092
+
941
1093
  def plugin_trust_refusal_diagnostic(plugin)
942
1094
  summary = plugin.io_boundary.refusal_summary
943
1095
  return nil if summary.nil?
@@ -1024,6 +1176,10 @@ module Rigor
1024
1176
  @cached_plugin_prepare_diagnostics_reader.call
1025
1177
  end
1026
1178
 
1179
+ def pooled_run_disclosures
1180
+ @pooled_run_disclosures_reader.call
1181
+ end
1182
+
1027
1183
  def pre_eval_diagnostics_from_scanner
1028
1184
  @pre_eval_diagnostics_from_scanner_reader.call
1029
1185
  end
@@ -250,9 +250,7 @@ module Rigor
250
250
  )
251
251
  end
252
252
 
253
- def table_class_names
254
- @effect_table.keys.filter_map { |key| Effects::MethodKey.owner(key) }.uniq
255
- end
253
+ def table_class_names = @effect_table.keys.filter_map { |k| Effects::MethodKey.envelope_owner(k) }.uniq
256
254
 
257
255
  def judge(scan)
258
256
  Effects::EnvelopeCheck.run(
@@ -301,7 +299,7 @@ module Rigor
301
299
 
302
300
  def build_diagnostic(finding)
303
301
  Diagnostic.new(
304
- path: finding.path || ".rigor.yml",
302
+ path: finding.path || unit_source_path(finding.key) || ".rigor.yml",
305
303
  line: finding.line,
306
304
  column: 1,
307
305
  message: message_for(finding),
@@ -312,6 +310,13 @@ module Rigor
312
310
  )
313
311
  end
314
312
 
313
+ # #393 — a template unit has no `def` for {Effects::EnvelopeCheck::Positions} to find, so a view
314
+ # finding would land on `.rigor.yml` — the config file, not the template the reviewer has to open.
315
+ # `Runner#effect_sources` already records the file every unit key came from; the line stays at the
316
+ # finding's own (1 for a whole-file unit), because the bound is exceeded by the unit, not by one
317
+ # line of it, and the explanation chain names the origin site.
318
+ def unit_source_path(key) = Effects::MethodKey.template_unit?(key) ? @unit_sources[key.to_s]&.first : nil
319
+
315
320
  def message_for(finding)
316
321
  EnvelopeMessages.exceeded(finding)
317
322
  end
@@ -10,6 +10,7 @@ require_relative "../run_stats"
10
10
  require_relative "../../effects/signature_sources"
11
11
  require_relative "../../rbs_extended/conformance_checker"
12
12
  require_relative "../../runtime/jit"
13
+ require_relative "../../cache/engine_source"
13
14
 
14
15
  module Rigor
15
16
  module Analysis
@@ -44,7 +45,7 @@ module Rigor
44
45
  snapshots:, plugin_registry:, dependency_source_index:,
45
46
  synthetic_method_index:, project_patched_methods:,
46
47
  analyze_file:, project_scope_seed: -> { {} }, record_dependencies: false,
47
- restored_run_level_rows: nil)
48
+ restored_run_level_rows: nil, template_units: -> { TemplateUnits.empty })
48
49
  @configuration = configuration
49
50
  @cache_store = cache_store
50
51
  @explain = explain
@@ -59,6 +60,11 @@ module Rigor
59
60
  # `#collected_effects`, so a pooled run's effect graph is the sequential run's graph.
60
61
  @record_effects = configuration.effects_enabled?
61
62
  @collected_effects = {}
63
+ # Issue #1051 — run-scoped plugin disclosures ({Plugin::Base#disclose_once}), keyed
64
+ # `[plugin id, key]` so the FIRST worker to register a pair wins and every later copy is dropped.
65
+ # The de-duplication has to happen here, on the parent, because each fork worker owns its own
66
+ # plugin instances and therefore its own `@run_disclosures` table.
67
+ @collected_run_disclosures = {}
62
68
  @collect_stats = collect_stats
63
69
  @buffer = buffer
64
70
  @environment_override = environment_override
@@ -71,6 +77,10 @@ module Rigor
71
77
  @synthetic_method_index_reader = synthetic_method_index
72
78
  @project_patched_methods_reader = project_patched_methods
73
79
  @project_scope_seed_reader = project_scope_seed
80
+ # #392 — read through a proc, like every other runner-owned table here: the units are synthesised
81
+ # on the parent during the run and handed to the ONE pre-fork {WorkerSession}, so every child
82
+ # inherits the same compiled bytes.
83
+ @template_units_reader = template_units
74
84
  @analyze_file = analyze_file
75
85
  # Issues #796 / #794 — the previous full run's run-level rows, handed down by
76
86
  # {Analysis::IncrementalSession} on a NARROWED run only (a recheck's closure, a
@@ -88,6 +98,25 @@ module Rigor
88
98
  # these with its own sequential records before the post-pool fixpoint.
89
99
  attr_reader :collected_effects
90
100
 
101
+ # Issue #1051 — the run-scoped disclosures a pool run collected, de-duplicated by `[plugin id, key]`
102
+ # and in first-registration order. Empty on a sequential run: there the runner's own plugin
103
+ # registry is the one that registered them, and {Runner} reads it directly.
104
+ def collected_run_disclosures
105
+ @collected_run_disclosures.values
106
+ end
107
+
108
+ # Issue #1051 — folds one session's / one worker payload's disclosures in, keeping the first
109
+ # registration of each `[plugin id, key]` pair. Nothing here inspects the message: two workers that
110
+ # disclose the same key with differently interpolated text still collapse to one row, which is the
111
+ # point — the key is the identity a plugin author controls, and an author who interpolates
112
+ # worker-varying text into a message has authored one disclosure, not N.
113
+ def merge_run_disclosures(records)
114
+ Array(records).each do |record|
115
+ identity = [record[:plugin_id], record[:key]].freeze
116
+ @collected_run_disclosures[identity] ||= record
117
+ end
118
+ end
119
+
91
120
  # ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
92
121
  # silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
93
122
  # cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
@@ -474,6 +503,20 @@ module Rigor
474
503
  # (shareable) registry into the class-ivar cache before any worker reads.
475
504
  Environment::ClassRegistry.default
476
505
 
506
+ # #1055 — the same treatment for the engine-source digest. Every worker's `Environment.for_project`
507
+ # reaches `Cache::EngineSource.key_config_entries` whenever a source-RBS synthesizer is wired (the
508
+ # ADR-93 `rigor-rbs-inline` auto-wire, on by default and off throughout the spec suite), and the
509
+ # memo behind it is a 90 ms tree walk that cannot be eager-loaded at require time. Warming it here
510
+ # is what makes the worker's access a READ of a frozen String rather than a forbidden class-ivar
511
+ # write. `Unavailable` is left to the worker: it raises rather than assigning, so it costs the pool
512
+ # no more than it costs a sequential run, and swallowing it here would hide a tree the engine
513
+ # cannot identify.
514
+ begin
515
+ Cache::EngineSource.process_identity
516
+ rescue Cache::EngineSource::Unavailable
517
+ nil
518
+ end
519
+
477
520
  # ADR-15 Phase 4b.x — pre-warm the RBS cache so workers serve every reflection query from the
478
521
  # Marshal blob on disk. Without this, the first cache MISS inside a worker falls through to
479
522
  # `RBS::EnvironmentLoader.new`, which reads a chain of non-`Ractor.shareable?` RubyGems / RBS
@@ -507,7 +550,18 @@ module Rigor
507
550
  # matching explicit call for the HKT-scan outcome: every worker already demands it from its OWN
508
551
  # environment in `#drain_reporters`, and the scan has one outcome whoever demands it, so a
509
552
  # worker's report already says what the coordinator's own demand would.
510
- warm_env = prewarm_rbs_cache_for_pool(source_files: source_files)
553
+ # #1064 — the lockfile is resolved HERE, once, and travels to every worker as frozen data the way
554
+ # `source_files` does: Bundler's parser reads RubyGems module state a non-main Ractor may not touch,
555
+ # so a worker resolving its own raised `Ractor::IsolationError`, printed a "malformed Gemfile.lock"
556
+ # warning per worker, and analysed with the project's gem signatures silently missing.
557
+ locked_gems = Ractor.make_shareable(
558
+ Environment::LockfileResolver.locked_gems(
559
+ lockfile_path: @configuration.bundler_lockfile,
560
+ project_root: Dir.pwd,
561
+ auto_detect: @configuration.bundler_auto_detect
562
+ )
563
+ )
564
+ warm_env = prewarm_rbs_cache_for_pool(source_files: source_files, locked_gems: locked_gems)
511
565
  snapshot_project_signature_state(warm_env)
512
566
  snapshot_effect_annotation_carrier(warm_env&.rbs_loader)
513
567
  record_definition_build_failures(warm_env&.rbs_loader&.definition_build_failures)
@@ -523,14 +577,15 @@ module Rigor
523
577
  shareable_source_files = source_files.map { |path| path.to_s.dup.freeze }.freeze
524
578
 
525
579
  pool = Array.new(@workers) do
526
- Ractor.new(configuration, cache_root, blueprints, explain, shareable_source_files) do |configuration, cache_root, blueprints, explain, shareable_source_files| # rubocop:disable Layout/LineLength
580
+ Ractor.new(configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems) do |configuration, cache_root, blueprints, explain, shareable_source_files, locked_gems| # rubocop:disable Layout/LineLength
527
581
  cache_store = cache_root ? Rigor::Cache::Store.new(root: cache_root) : nil
528
582
  session = Rigor::Analysis::WorkerSession.new(
529
583
  configuration: configuration,
530
584
  cache_store: cache_store,
531
585
  plugin_blueprints: blueprints,
532
586
  explain: explain,
533
- source_files: shareable_source_files
587
+ source_files: shareable_source_files,
588
+ locked_gems: locked_gems
534
589
  )
535
590
  main = Ractor.main
536
591
  main.send([:prepare, session.prepare_diagnostics])
@@ -542,7 +597,10 @@ module Rigor
542
597
  main.send([:file, msg, session.analyze(msg)])
543
598
  end
544
599
 
545
- main.send([:done, session.drain_reporters])
600
+ # Issue #1051 — sent with `:done`, not with `:prepare`: this backend builds no parent session,
601
+ # so the worker's own table is the only place a disclosure exists, and it is complete only
602
+ # once the worker has finished its files.
603
+ main.send([:done, session.drain_reporters, session.drain_run_disclosures])
546
604
  end
547
605
  end
548
606
 
@@ -576,7 +634,8 @@ module Rigor
576
634
  when :file
577
635
  results_by_path[message[1]] = message[2]
578
636
  when :done
579
- merge_worker_reporters(message.last)
637
+ merge_worker_reporters(message[1])
638
+ merge_run_disclosures(message[2])
580
639
  end
581
640
  end
582
641
 
@@ -604,6 +663,18 @@ module Rigor
604
663
  # definition-build failures the re-analysis demanded (#696) and the HKT-scan outcome, demanded
605
664
  # once more by the run itself (#784 — a rescued scan failure otherwise vanished with the worker).
606
665
  # The fork backend needs none of this: it re-analyses on the parent {WorkerSession} and drains it.
666
+ #
667
+ # Issue #1051, known exception: run-scoped disclosures a plugin registered from `#prepare` are NOT
668
+ # recovered here — nor, since #1060, a positioned `#emit_once` batch registered there, which rides
669
+ # the same table. There is no session to drain — this backend builds none on the coordinator — and
670
+ # `#prepare` deliberately does not run on the coordinator-side registry under pool mode, so a
671
+ # prepare-time disclosure lives only inside the Ractor that died with it. A disclosure registered
672
+ # from `#diagnostics_for_file` IS recovered, because the re-analysis below runs the coordinator-side
673
+ # plugin instances that {DiagnosticAggregator#plugin_run_disclosure_diagnostics} reads directly.
674
+ # Left as is: recovering the rest means running `#prepare` on the coordinator registry, which would
675
+ # re-publish every cross-plugin fact for a degrade, and this backend is off by default. Since #1055
676
+ # the path is also rare rather than universal — it used to be taken on every run, because every
677
+ # worker died in its constructor on a class-ivar memo.
607
678
  def reanalyze_degraded_in_process(degraded, results_by_path, source_files:)
608
679
  return if degraded.empty?
609
680
 
@@ -641,7 +712,8 @@ module Rigor
641
712
  project_patched_methods: project_patched_methods,
642
713
  project_scope_seed: project_scope_seed,
643
714
  source_files: source_files,
644
- record_dependencies: @record_dependencies
715
+ record_dependencies: @record_dependencies,
716
+ template_units: template_units
645
717
  )
646
718
  # Force the full RBS load on the parent so children copy-on-write inherit a warm Environment
647
719
  # rather than each rebuilding it after the fork.
@@ -662,6 +734,14 @@ module Rigor
662
734
  # same project).
663
735
  snapshot_project_signature_state(session.environment)
664
736
  snapshot_effect_annotation_carrier(session.environment.rbs_loader)
737
+ # Issue #1051 — taken BEFORE the fork, for the same reason and with the same effect as the two
738
+ # snapshots above: `#prepare` ran on the parent when the session was built, so every disclosure a
739
+ # plugin registers there (the normal case — a disclosure is a fact about the project's inputs,
740
+ # known before a file is read) is already in this table, in the same order the sequential path
741
+ # would produce. Each child inherits the identical table and re-offers it in its payload, where
742
+ # `merge_run_disclosures` drops it as a duplicate; only a disclosure a worker registers while
743
+ # analysing its own slice adds anything.
744
+ merge_run_disclosures(session.drain_run_disclosures)
665
745
  snapshot_fork_pool_stats(session) if @collect_stats
666
746
 
667
747
  worker_count = [@workers, files.size].min
@@ -680,6 +760,8 @@ module Rigor
680
760
  unless degraded.empty?
681
761
  degraded.each { |path| results_by_path[path] = session.analyze(path) }
682
762
  merge_worker_reporters(session.drain_reporters)
763
+ # The degraded slice ran on the parent session, so a disclosure it registered lives there.
764
+ merge_run_disclosures(session.drain_run_disclosures)
683
765
  # The degraded slice was re-analysed on the parent session, so its dependency records live there,
684
766
  # not in a child payload — fold them in alongside the successful children's.
685
767
  @collected_dependencies.merge!(session.drain_dependencies) if @record_dependencies
@@ -704,7 +786,8 @@ module Rigor
704
786
  Runtime::Jit.rearm_after_fork
705
787
  results = slice.to_h { |path| [path, session.analyze(path)] }
706
788
  payload = { results: results, reporters: session.drain_reporters,
707
- dependencies: session.drain_dependencies }
789
+ dependencies: session.drain_dependencies,
790
+ disclosures: session.drain_run_disclosures }
708
791
  # ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
709
792
  # marshals exactly the payload it marshalled before the feature existed.
710
793
  payload[:effects] = session.drain_effects if @record_effects
@@ -736,6 +819,7 @@ module Rigor
736
819
  if payload
737
820
  results_by_path.merge!(payload.fetch(:results))
738
821
  merge_worker_reporters(payload.fetch(:reporters))
822
+ merge_run_disclosures(payload.fetch(:disclosures, []))
739
823
  @collected_dependencies.merge!(payload.fetch(:dependencies, {})) if @record_dependencies
740
824
  @collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
741
825
  else
@@ -798,13 +882,14 @@ module Rigor
798
882
  # index, the synthetic-method / project-patched indexes) is withheld for the same reason: nothing
799
883
  # here dispatches.
800
884
  # @param source_files — the WHOLE project's file list, never the analyzed subset.
801
- def prewarm_rbs_cache_for_pool(source_files:)
885
+ def prewarm_rbs_cache_for_pool(source_files:, locked_gems: nil)
802
886
  warm_env = Environment.for_project(
803
887
  libraries: @configuration.libraries,
804
888
  signature_paths: @configuration.signature_paths,
805
889
  cache_store: @cache_store,
806
890
  plugin_registry: plugin_registry,
807
891
  source_files: source_files,
892
+ locked_gems: locked_gems,
808
893
  **ProjectEnvironment.dependency_discovery_options(@configuration)
809
894
  )
810
895
  warm_env.rbs_loader&.prewarm
@@ -820,6 +905,11 @@ module Rigor
820
905
  # i.e. with no synthesized RBS at all, even on a full run.
821
906
  def analyze_files_sequentially_fallback(files, reason:, source_files: files)
822
907
  environment = build_runner_environment(source_files: source_files)
908
+ # Same set, same ORDER as `#analyze_files_sequentially`. The signature state used to be hand-copied
909
+ # here slot by slot, which dropped `synthesized-namespace` and every conformance row. The conformance
910
+ # scan inside it demands the definition of every `rigor:v1:conforms-to` class, so it has to run
911
+ # BEFORE the definition-build failures are read below, or a failure only that scan reaches is lost.
912
+ snapshot_project_signature_state(environment)
823
913
  snapshot_effect_annotation_carrier(environment.rbs_loader)
824
914
  diagnostics = files.flat_map { |path| @analyze_file.call(path, environment) }
825
915
  loader = environment.rbs_loader
@@ -834,10 +924,6 @@ module Rigor
834
924
  record_hkt_scan_failure(hkt_scan_outcome(environment))
835
925
  @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
836
926
  @snapshots.signature_paths = loader&.signature_paths || [].freeze
837
- @snapshots.signature_standdowns = plugin_signature_paths? ? signature_standdowns_for(loader) : []
838
- @snapshots.quarantined_signatures =
839
- project_signature_paths? ? (loader&.quarantined_signatures || []) : []
840
- @snapshots.env_build_failure = project_signature_paths? ? loader&.env_build_failure : nil
841
927
  diagnostics.unshift(
842
928
  Diagnostic.new(
843
929
  path: ".rigor.yml", line: 1, column: 1,
@@ -864,10 +950,12 @@ module Rigor
864
950
  # `:failed`, so under `--workers N` a WD12 "parsed but not honoured" row came back as
865
951
  # `source-rbs-synthesis-failed` — a different id, and a message telling the user the file
866
952
  # contributed nothing when all but one of its annotations bound. Every worker reads the same
867
- # sources and so hands over the same entries; the reporter's own dedup collapses them.
953
+ # sources and so hands over the same entries; the reporter's own dedup collapses them. `line:` rides
954
+ # along for the same reason (#1075): a consistency error is positioned at its `.rbs` member.
868
955
  Array(drained[:source_rbs_synthesis]).each do |entry|
869
956
  @source_rbs_synthesis_reporter.record(
870
- plugin_id: entry.plugin_id, path: entry.path, message: entry.message, kind: entry.kind
957
+ plugin_id: entry.plugin_id, path: entry.path, message: entry.message, kind: entry.kind,
958
+ line: entry.line
871
959
  )
872
960
  end
873
961
  # Issue #696. Fetched with a default so an older drain stays compatible, exactly as the line above.
@@ -1022,6 +1110,10 @@ module Rigor
1022
1110
  @project_patched_methods_reader.call
1023
1111
  end
1024
1112
 
1113
+ def template_units
1114
+ @template_units_reader.call
1115
+ end
1116
+
1025
1117
  def project_scope_seed
1026
1118
  @project_scope_seed_reader.call
1027
1119
  end
@@ -8,6 +8,8 @@ require_relative "../../inference/synthetic_method_scanner"
8
8
  require_relative "../../inference/project_patched_scanner"
9
9
  require_relative "../dependency_source_inference"
10
10
  require_relative "../project_scan"
11
+ require_relative "../template_units"
12
+ require_relative "../template_unit_collector"
11
13
 
12
14
  module Rigor
13
15
  module Analysis
@@ -53,10 +55,10 @@ module Rigor
53
55
  Discovery = Data.define(
54
56
  :discovered_classes, :discovered_def_nodes, :discovered_def_nestings,
55
57
  :discovered_singleton_def_nodes, :discovered_def_sources, :discovered_singleton_def_sources,
56
- :discovered_superclasses, :discovered_header_nestings, :discovered_includes,
58
+ :discovered_superclasses, :discovered_header_nestings, :discovered_includes, :discovered_prepends,
57
59
  :discovered_extends, :discovered_class_sources, :constant_values, :constant_sources, :constant_writes,
58
- :discovered_method_visibilities, :discovered_methods, :data_member_layouts,
59
- :struct_member_layouts
60
+ :discovered_method_visibilities, :discovered_methods, :discovered_parameter_envelopes, :data_member_layouts,
61
+ :struct_member_layouts, :discovered_deferred_ranges, :discovered_refinements, :discovered_global_write_census
60
62
  )
61
63
 
62
64
  # Internal: drives every EAGER project-wide pre-pass — the ones whose products feed the RBS
@@ -164,24 +166,38 @@ module Rigor
164
166
  # the cref Ruby resolves it in rather than by peeling the subclass's own qualified name.
165
167
  discovered_header_nestings: def_index.fetch(:header_nestings),
166
168
  discovered_includes: def_index.fetch(:includes),
169
+ # Issue #1123 — the instance-side prepend table, kept beside the include one so
170
+ # `Scope#user_def_through_ancestors` can search a prepended module ahead of the class's own
171
+ # `def`s from a sibling file the way it does within the declaring one.
172
+ discovered_prepends: def_index.fetch(:prepends),
167
173
  # Issue #898 — the singleton-side mixin table, kept beside the instance-side one so a class
168
174
  # object's `extend`s reach `Narrowing` from a sibling file the way its `include`s already do.
169
175
  discovered_extends: def_index.fetch(:extends),
170
176
  discovered_class_sources: def_index.fetch(:class_sources),
171
177
  # Issue #644 — the cross-file value-constant publication table and its write attribution.
172
- constant_values: def_index.fetch(:constant_values),
173
- constant_sources: def_index.fetch(:constant_sources),
178
+ constant_values: def_index.fetch(:constant_values), constant_sources: def_index.fetch(:constant_sources),
174
179
  constant_writes: def_index.fetch(:constant_writes),
175
180
  discovered_method_visibilities: def_index.fetch(:method_visibilities),
176
181
  discovered_methods: def_index.fetch(:methods),
182
+ discovered_parameter_envelopes: def_index.fetch(:parameter_envelopes),
177
183
  data_member_layouts: def_index.fetch(:data_member_layouts),
178
- struct_member_layouts: def_index.fetch(:struct_member_layouts)
184
+ struct_member_layouts: def_index.fetch(:struct_member_layouts),
185
+ # Issue #1097 — per-file def / block / lambda body ranges, the execution-timing table the
186
+ # `*_def_shadows_call?` predicates order `sig`-shadowing defs against.
187
+ discovered_deferred_ranges: def_index.fetch(:deferred_ranges),
188
+ discovered_refinements: def_index.fetch(:refinements), # Issue #1120
189
+ discovered_global_write_census: def_index.fetch(:global_write_census) # Issue #1367
179
190
  )
180
191
  end
181
192
 
182
193
  # Builds the LSP-facing {ProjectScan} snapshot from a fresh pre-pass run. The runner adopts `result`
183
194
  # onto its ivars first so the same registry object that ran `#prepare` (and so the populated
184
195
  # `services.fact_store`) is the one the snapshot carries.
196
+ #
197
+ # Since #1038 the snapshot also carries the template-unit index, so a long-lived owner's per-buffer
198
+ # publishes carry it instead of re-running every plugin transform per keystroke. It is built through
199
+ # {TemplateUnitCollector.collect_for_scan}, which owns both of that slot's rules — no buffer binding,
200
+ # and an ADR-87 run scope around the packs a later run revalidates against.
185
201
  def build_project_scan(result)
186
202
  ProjectScan.new(
187
203
  plugin_registry: result.plugin_registry,
@@ -189,7 +205,10 @@ module Rigor
189
205
  synthetic_method_index: result.synthetic_method_index,
190
206
  project_patched_methods: result.project_patched_methods,
191
207
  plugin_prepare_diagnostics: result.cached_plugin_prepare_diagnostics.dup.freeze,
192
- pre_eval_diagnostics: result.pre_eval_diagnostics_from_scanner.dup.freeze
208
+ pre_eval_diagnostics: result.pre_eval_diagnostics_from_scanner.dup.freeze,
209
+ template_units: TemplateUnitCollector.collect_for_scan(
210
+ registry: result.plugin_registry, strict: @configuration.cache_validation_strict?
211
+ )
193
212
  )
194
213
  end
195
214