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
@@ -48,6 +48,7 @@ require_relative "runner/run_snapshots"
48
48
  require_relative "runner/project_pre_passes"
49
49
  require_relative "runner/pool_coordinator"
50
50
  require_relative "runner/diagnostic_aggregator"
51
+ require_relative "template_units"
51
52
  require_relative "runner/effect_envelope_pass"
52
53
  require_relative "runner/effect_annotation_residual_pass"
53
54
  require_relative "runner/buffer_pool_dispatcher"
@@ -107,6 +108,32 @@ module Rigor
107
108
  @effect_table || Effects::EffectTable.empty
108
109
  end
109
110
 
111
+ # ADR-103 WD9 (#391) — the envelope index this run resolved, for `rigor sig-gen`'s annotation
112
+ # emission to consult. Public for the same reason {#effect_table} is: a consumer outside the
113
+ # diagnostic stream needs what the run already computed, and rebuilding it from configuration
114
+ # alone would read a narrower set of strata — the accepted signatures and the rbs-inline virtual
115
+ # RBS both come from the built environment.
116
+ #
117
+ # Built on demand when the run never analysed a file. A WARM whole-run cache hit serves the
118
+ # propagated table from cache, so {#analyze_with_effects} — the only caller of
119
+ # {#effect_envelope_index} — never runs and the ivar stays nil. Returning an empty index there
120
+ # would silently drop sig-gen's "the author already declared a bound" gate on exactly the runs a
121
+ # user makes most often: the same `sig-gen` invocation would withhold on its first run and emit on
122
+ # its second. `@run_environment` is resolved before the cache is consulted, so the warm build reads
123
+ # the same strata the cold one does.
124
+ #
125
+ # `@run_environment` is nil only when the run resolved no environment at all — a run over zero
126
+ # project files, or one that never reached {#compute_run_diagnostics}. The build then falls back to
127
+ # the configuration-only strata (`sig/` annotations, class-level ones and `effects.envelopes:`,
128
+ # without the accepted signatures or the rbs-inline virtual RBS), which is the narrower answer but
129
+ # not a wrong one: a run with no environment analysed no file, so there is no candidate for the
130
+ # missing strata to have bounded.
131
+ def effect_envelopes
132
+ return @effect_envelope_index if @effect_envelope_index
133
+
134
+ effect_envelope_index(@run_environment)
135
+ end
136
+
110
137
  # ADR-103 WD2 / WD6 / WD10 / #387 — the loaded plugins' effect contributions, compiled once per
111
138
  # process. Memoised on first use rather than built in the constructor for two reasons: the plugin
112
139
  # registry is adopted after construction (`apply_prebuilt` / the plugin-load pre-pass), and the
@@ -314,6 +341,10 @@ module Rigor
314
341
  # check reads.
315
342
  @no_tolerated_effects = no_tolerated_effects
316
343
  @file_effects = {}
344
+ # #392 — the run's template units, synthesised once per run after the plugins load (see
345
+ # {#template_units}). nil until then; a project whose plugins declare no `template_globs:` resolves
346
+ # to the shared empty index and pays one `Array#empty?` for the whole run.
347
+ @template_units = nil
317
348
  @effect_table = nil
318
349
  @effects_served_from_cache = false
319
350
  # #482 — set only by a run served from the summary entry: the two derived tables it carries, and
@@ -378,8 +409,16 @@ module Rigor
378
409
  # Issue #682 — the `Module.nesting` each class / module declaration HEADER is written in.
379
410
  @project_discovered_header_nestings = {}.freeze
380
411
  @project_discovered_includes = {}.freeze
412
+ # Issue #1123 — the instance-side prepend table (`prepend M`, and `Recv.prepend(M)`), in
413
+ # instance-ancestor SEARCH order.
414
+ @project_discovered_prepends = {}.freeze
381
415
  # Issue #898 — the singleton-side twin of the include table (`extend M` / `extend self`).
382
416
  @project_discovered_extends = {}.freeze
417
+ @project_discovered_deferred_ranges = {}.freeze
418
+ # Issue #1120 — `{refined class => {method => [refining modules]}}`, every `refine X do … end` body.
419
+ @project_discovered_refinements = {}.freeze
420
+ # Issue #1367 — the project's `Inference::GlobalWriteCensus`, which the `global.*` write rules read.
421
+ @project_discovered_global_write_census = Set.new.freeze
383
422
  @project_discovered_class_sources = {}.freeze
384
423
  # Issue #644 — the cross-file VALUE-constant publication table (`{qualified name => Type::Constant}`,
385
424
  # literal writes only) and its per-name write attribution. The first seeds `in_source_constants` on
@@ -390,8 +429,12 @@ module Rigor
390
429
  # ...and the raw per-(name, path) publication census the incremental producer diffs.
391
430
  @project_constant_writes = {}.freeze
392
431
  @published_constant_name_set = nil
432
+ @constant_writer_index = nil
433
+ @constant_shadower_index = nil
393
434
  @project_discovered_method_visibilities = {}.freeze
394
435
  @project_discovered_methods = {}.freeze
436
+ # Issue #992 — the cross-file parameter-envelope table `call.wrong-arity` reads for an undeclared `def`.
437
+ @project_discovered_parameter_envelopes = {}.freeze
395
438
  @project_data_member_layouts = {}.freeze
396
439
  @project_struct_member_layouts = {}.freeze
397
440
  # Issue #684 — set per run by {#project_discovery_expansion}; nil on every path that does not widen.
@@ -453,6 +496,12 @@ module Rigor
453
496
  @snapshots.reset_for_run
454
497
  # Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
455
498
  @project_discovery_done = false
499
+ # #392 — re-derive the template units each run: their source files can change between two runs of
500
+ # one long-lived Runner (the LSP's), and a unit is never served from a cache of its own. Since
501
+ # #1038 "re-derive" is not "recompile": a `prebuilt:` runner carries the snapshot's warm index into
502
+ # {#template_units}, which still re-expands every claim and revalidates every template against the
503
+ # filesystem, and only compiles what actually moved.
504
+ @template_units = nil
456
505
  # Per-run reset of the environment the cacheable path resolves, reused by the envelope pass so a
457
506
  # run never builds two.
458
507
  @run_environment = nil
@@ -746,7 +795,10 @@ module Rigor
746
795
  @run_served_from_cache = false
747
796
  return assemble_run_diagnostics(expansion) unless run_result_cacheable?
748
797
 
749
- environment = @pool_coordinator.resolve_sequential_environment(source_files: target_files(expansion))
798
+ # #392 — `source_files:` is what a plugin's `source_rbs_synthesizer` is offered at env-build time,
799
+ # and a template unit is not Ruby the synthesiser can read (rbs-inline would be handed `.rbx`
800
+ # bytes). The `.rb` expansion is the right set here; the units join the ANALYSED set only.
801
+ environment = @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files))
750
802
  # Lazy-files descriptor: the cache KEY reads only `gems` + `configs`; the RBS signature-tree `files`
751
803
  # are digested solely by `run_dependency_descriptor` on a MISS, so a warm HIT never walks the tree.
752
804
  rbs_descriptor = if environment&.rbs_loader
@@ -998,7 +1050,14 @@ module Rigor
998
1050
  def close_effect_graph
999
1051
  return unless @record_effects
1000
1052
 
1001
- @effect_table = Effects::Propagator.propagate(effect_collection, discharge: effect_discharge)
1053
+ # `declined_unit_keys:` — #1065. The templates a plugin claimed and produced no unit for; a render
1054
+ # edge's format fallback must not resolve past one of them, because the file exists and the
1055
+ # framework runs it, so the other format's effects are not what that render produces.
1056
+ @effect_table = Effects::Propagator.propagate(
1057
+ effect_collection,
1058
+ discharge: effect_discharge,
1059
+ declined_unit_keys: template_units.declined_unit_keys
1060
+ )
1002
1061
  rescue StandardError
1003
1062
  @effect_table = Effects::EffectTable.empty
1004
1063
  end
@@ -1029,6 +1088,11 @@ module Rigor
1029
1088
  # untouched so its lazy build timing is unchanged.
1030
1089
  environment = seed_pre_eval_constants(expansion, environment)
1031
1090
  diagnostics = @diagnostic_aggregator.pre_file_diagnostics(expansion)
1091
+ # #392 — a template transform that raised, a template that could not be read, or a unit naming a
1092
+ # file it was not offered. Reported through the same `:plugin_loader` `runtime-error` envelope a
1093
+ # raise from `#diagnostics_for_file` uses (ADR-2 § "Plugin Trust and I/O Policy"), rather than
1094
+ # dropped in silence: a plugin author whose view stopped being analysed has nothing else to read.
1095
+ diagnostics += template_unit_failure_diagnostics
1032
1096
  # ADR-46 — record which project files this run actually analyzed (the `analyze_only` subset, or
1033
1097
  # all of them). The incremental orchestrator serves every analyzed-but-not-affected file from the
1034
1098
  # per-file cache, so it needs the full analyzed set to subtract the affected closure from.
@@ -1045,6 +1109,11 @@ module Rigor
1045
1109
  # override re-stamps it; and a pool backend folds `.rigor.yml`-positioned prepare / pool-degraded rows
1046
1110
  # into the same return, which the slice drops. The run's own stream is stamped once, at the end.
1047
1111
  diagnostics += analyze_targets(targets, environment: environment, project_files: expansion.fetch(:files))
1112
+ # Issue #1060 — the positioned batches plugins registered through `Plugin::Base#emit_once`, read once
1113
+ # every worker payload has been merged. Kept beside the per-file stream (their rows name files and
1114
+ # lines) but outside `#per_file_diagnostics`, which `analyze_targets` has already sliced, so no one
1115
+ # file's incremental cache entry carries a project-wide batch.
1116
+ diagnostics += @diagnostic_aggregator.plugin_run_emission_diagnostics
1048
1117
  # ADR-103 WD12 — the effect fixpoint, in the post-pool aggregation slot beside the conformance
1049
1118
  # results. Graph-only over a finite lattice, so it is a plain worklist to a true fixpoint; it
1050
1119
  # contributes NO diagnostics and its result leaves through `#effect_table`, never through the
@@ -1053,6 +1122,18 @@ module Rigor
1053
1122
  diagnostics + post_analysis_diagnostic_streams
1054
1123
  end
1055
1124
 
1125
+ def template_unit_failure_diagnostics
1126
+ template_units.failures.map do |failure|
1127
+ Diagnostic.new(
1128
+ path: failure.path, line: 1, column: 1,
1129
+ message: "plugin #{failure.plugin_id.inspect} produced no template unit for " \
1130
+ "#{failure.path}: #{failure.message}",
1131
+ severity: :error, rule: "runtime-error", source_family: :plugin_loader
1132
+ )
1133
+ end
1134
+ end
1135
+ private :template_unit_failure_diagnostics
1136
+
1056
1137
  # The rbs-coverage build-failure ladder plus every remaining post-analysis stream
1057
1138
  # `#assemble_run_diagnostics` appends, in their fixed contract order (see the comments at each call
1058
1139
  # below and at the streams' own definitions). Split out of `#assemble_run_diagnostics` so that
@@ -1077,6 +1158,11 @@ module Rigor
1077
1158
  diagnostics += @diagnostic_aggregator.rbs_extended_reporter_diagnostics
1078
1159
  diagnostics += @diagnostic_aggregator.boundary_cross_diagnostics
1079
1160
  diagnostics += @diagnostic_aggregator.source_rbs_synthesis_diagnostics
1161
+ # Issue #1051 — read after `analyze_targets` for the same reason the refusal stream below is: a
1162
+ # plugin may register a disclosure from `#prepare` (the usual case) or while analysing a file, and
1163
+ # both have to be in before the table is harvested. Ordered before the refusals so the run-level
1164
+ # plugin rows read plugin-authored notice first, engine-authored envelope second.
1165
+ diagnostics += @diagnostic_aggregator.plugin_run_disclosure_diagnostics
1080
1166
  # Issue #959 — read LAST (after `analyze_targets`, which already ran before this method is called),
1081
1167
  # so every per-file plugin call has had its chance to record a refusal on its (per-plugin-instance
1082
1168
  # memoised) `IoBoundary`, alongside the prepare-time refusals the same boundary instance may
@@ -1091,7 +1177,9 @@ module Rigor
1091
1177
  # re-stamps), sliced to the rows positioned at the targets. Returns the RAW return for the run's stream,
1092
1178
  # which is stamped once, at the end of `#run_analysis`.
1093
1179
  def analyze_targets(targets, environment:, project_files:)
1094
- raw = @pool_coordinator.analyze_files(targets, environment: environment, project_files: project_files)
1180
+ raw = template_units.remap(
1181
+ @pool_coordinator.analyze_files(targets, environment: environment, project_files: project_files)
1182
+ )
1095
1183
  analysed = targets.to_set
1096
1184
  @per_file_diagnostics = @diagnostic_aggregator.apply_severity_profile(raw)
1097
1185
  .select { |diagnostic| analysed.include?(diagnostic.path) }
@@ -1199,7 +1287,8 @@ module Rigor
1199
1287
  # reconstructs `rbs.libraries` from config) can never drift out of key agreement.
1200
1288
  RunCacheKey.descriptor(
1201
1289
  configuration: @configuration, files: expansion.fetch(:files),
1202
- explain: @explain, rbs_config_entries: rbs_descriptor.configs
1290
+ explain: @explain, rbs_config_entries: rbs_descriptor.configs,
1291
+ template_units_digest: template_units.digest
1203
1292
  )
1204
1293
  end
1205
1294
 
@@ -1216,11 +1305,11 @@ module Rigor
1216
1305
 
1217
1306
  def build_run_dependency_descriptor(expansion, rbs_descriptor)
1218
1307
  entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
1219
- pre_eval_file_entries + rbs_descriptor.files
1308
+ pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files
1220
1309
  # #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
1221
1310
  # `.rbs` files that existed while the run read them, so without these a signature file written after
1222
1311
  # the run left the slot validating fresh and the warm run answered without it.
1223
- globs = rbs_descriptor.globs.dup
1312
+ globs = rbs_descriptor.globs + template_unit_glob_entries
1224
1313
  @plugin_registry.plugins.each do |plugin|
1225
1314
  # Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
1226
1315
  # are frozen after the run, and a plugin that never built a boundary read no files through it,
@@ -1268,6 +1357,30 @@ module Rigor
1268
1357
  # expansion — so without this entry, editing one would leave the run-result cache serving diagnostics
1269
1358
  # computed against the previous version. The common case (a `lib/core_ext/` file that is also under
1270
1359
  # `paths:`) contributes a duplicate entry, which the descriptor's per-path validation absorbs.
1360
+ # #392 — the TEMPLATE files behind the run's units. The key slot carries the compiled Ruby's digest
1361
+ # (`template_units_digest`), which is what decides whether a cached answer describes this source; these
1362
+ # entries are the validation half, so a template edited between two runs re-globs and re-digests here
1363
+ # exactly as an analysed `.rb` file does. Both are needed: a transform that compiles two different
1364
+ # templates to the same Ruby must still not resurrect the wrong PATH in a diagnostic.
1365
+ # #392 — one `:names` row per claimed `template_globs:` pattern, whether or not it matched. The file
1366
+ # rows below cover edits to templates that EXIST; only a glob row notices one appearing or vanishing,
1367
+ # which is what let a project's first template stay invisible to every warm run.
1368
+ def template_unit_glob_entries
1369
+ template_units.glob_entries
1370
+ end
1371
+
1372
+ # Every template the run READ, successes and failures alike: a `plugin_loader` row produced by a
1373
+ # template that would not compile must not outlive the edit that fixes it, and a buffer-bound path is
1374
+ # digested at the bytes the run actually read.
1375
+ def template_unit_file_entries
1376
+ template_units.source_paths.filter_map do |path|
1377
+ physical = template_units.physical_path(path, @buffer)
1378
+ next unless File.file?(physical)
1379
+
1380
+ Cache::Descriptor::FileEntry.stat(path: physical, digest: Cache::FileDigest.hexdigest(physical))
1381
+ end
1382
+ end
1383
+
1271
1384
  def pre_eval_file_entries
1272
1385
  @configuration.pre_eval.filter_map do |path|
1273
1386
  physical = @buffer ? @buffer.resolve(path) : path
@@ -1352,19 +1465,39 @@ module Rigor
1352
1465
  @project_discovered_singleton_def_sources = discovery.discovered_singleton_def_sources
1353
1466
  @project_discovered_superclasses = discovery.discovered_superclasses
1354
1467
  @project_discovered_header_nestings = discovery.discovered_header_nestings
1355
- @project_discovered_includes = discovery.discovered_includes
1356
- @project_discovered_extends = discovery.discovered_extends
1468
+ apply_discovery_mixin_tables(discovery)
1469
+ @project_discovered_deferred_ranges = discovery.discovered_deferred_ranges
1470
+ apply_discovery_call_surface_tables(discovery)
1357
1471
  @project_discovered_class_sources = discovery.discovered_class_sources
1358
1472
  @project_constant_values = discovery.constant_values
1359
1473
  @published_constant_name_set = nil
1474
+ @constant_writer_index = nil
1475
+ @constant_shadower_index = nil
1360
1476
  @project_constant_sources = discovery.constant_sources
1361
1477
  @project_constant_writes = discovery.constant_writes
1362
1478
  @project_discovered_method_visibilities = discovery.discovered_method_visibilities
1363
1479
  @project_discovered_methods = discovery.discovered_methods
1480
+ @project_discovered_parameter_envelopes = discovery.discovered_parameter_envelopes
1364
1481
  @project_data_member_layouts = discovery.data_member_layouts
1365
1482
  @project_struct_member_layouts = discovery.struct_member_layouts
1366
1483
  end
1367
1484
 
1485
+ # The tables only check rules read beyond dispatch — the issue #1120 refinements and the issue #1367
1486
+ # global-write census. Extracted to keep {#apply_discovery_result} under its ABC budget.
1487
+ def apply_discovery_call_surface_tables(discovery)
1488
+ @project_discovered_refinements = discovery.discovered_refinements
1489
+ @project_discovered_global_write_census = discovery.discovered_global_write_census
1490
+ end
1491
+
1492
+ # The three mixin tables the discovery pass carries — the ADR-24 `include` map, its issue #1123
1493
+ # `prepend` twin, and the #898 `extend` table. Extracted to keep {#apply_discovery_result} under its
1494
+ # ABC budget.
1495
+ def apply_discovery_mixin_tables(discovery)
1496
+ @project_discovered_includes = discovery.discovered_includes
1497
+ @project_discovered_prepends = discovery.discovered_prepends
1498
+ @project_discovered_extends = discovery.discovered_extends
1499
+ end
1500
+
1368
1501
  # Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
1369
1502
  # Memoised on `@project_discovery_done` (reset at the start of `#run`). No-op under `@prebuilt` — the
1370
1503
  # LSP path deliberately seeds an empty project scope from a snapshot that carries no discovery tables,
@@ -1498,6 +1631,25 @@ module Rigor
1498
1631
  # The buffer's logical path is added to the file list even if it's not under `paths:` — per design §
1499
1632
  # "Failure envelope": "--instead-of=Y with Y not under any paths: directory → treated as a valid
1500
1633
  # logical identity for the buffer".
1634
+ # #392 — the run's template units. Built once per run, on the parent, AFTER the plugin pre-pass has
1635
+ # loaded the registry; a run whose plugins declare no `template_globs:` never globs and never calls a
1636
+ # plugin.
1637
+ #
1638
+ # #1038 — a runner built with `prebuilt:` hands the snapshot's warm index to `collect(previous:)`, so
1639
+ # a per-buffer publish against a long-lived {LanguageServer::ProjectContext} re-runs the transform
1640
+ # only for a template that actually moved on disk (and for the buffer's own, always). The globs are
1641
+ # still re-expanded and every surviving template is still revalidated, so an added, deleted or edited
1642
+ # template is seen on the next publish without the owner invalidating anything. A runner with no
1643
+ # `prebuilt:` — every CLI run — passes no `previous:` and builds the index exactly as before.
1644
+ def template_units
1645
+ @template_units ||= if @plugin_registry.nil?
1646
+ TemplateUnits.empty
1647
+ else
1648
+ TemplateUnits.collect(registry: @plugin_registry, buffer: @buffer,
1649
+ previous: @prebuilt&.template_units)
1650
+ end
1651
+ end
1652
+
1501
1653
  def target_files(expansion)
1502
1654
  files = expansion.fetch(:files)
1503
1655
  # ADR-46 slice 2 — restrict the analyzed set to the affected closure while the pre-pass (run
@@ -1508,14 +1660,41 @@ module Rigor
1508
1660
  # the same allowance option A makes below.
1509
1661
  files = files.select { |path| @analyze_only.include?(path) }
1510
1662
  files |= [@buffer.logical_path] if @buffer && @analyze_only.include?(@buffer.logical_path)
1511
- return files
1663
+ return template_unit_targets(files)
1512
1664
  end
1513
- return files if @buffer.nil?
1665
+ return template_unit_targets(files) if @buffer.nil?
1514
1666
 
1515
- # Editor mode option A — no closure, so the buffer's single logical path IS the analyzed set.
1667
+ # Editor mode option A — no closure, so the buffer's single logical path IS the analyzed set. The
1668
+ # template units deliberately do NOT join it: a per-buffer publish answers about the buffer the
1669
+ # editor is showing, and appending every view would publish diagnostics for files the editor did
1670
+ # not ask about (and re-parse them on every keystroke).
1516
1671
  [@buffer.logical_path]
1517
1672
  end
1518
1673
 
1674
+ # #392 — the template units join the analysed set AFTER any narrowing, so an `--incremental` recheck
1675
+ # re-analyses every unit even when its closure is one `.rb` file. Nothing in the ADR-46 dependency
1676
+ # graph names a synthesised file, so there is no edge that could put a unit in a closure — narrowing
1677
+ # first and appending second is what makes "always re-analysed" true rather than a comment. The cost
1678
+ # is one parse per template on the warm path; the alternative silently drops every template
1679
+ # diagnostic from the second run onwards.
1680
+ #
1681
+ # Appended rather than merged in sorted order so the `.rb` expansion's own order — and every output
1682
+ # that follows it — is byte-identical for a project with no units.
1683
+ def template_unit_targets(files)
1684
+ return files if template_units.empty?
1685
+
1686
+ files + (template_units.paths - files)
1687
+ end
1688
+ private :template_unit_targets
1689
+
1690
+ # #392 — the unit paths this run synthesised. Read by {IncrementalSession}, which keeps them OUT of
1691
+ # its analysed-file set: a unit is never served from the per-file cache and never counts as a file
1692
+ # that was removed from the project, because `#target_files` re-analyses it every run.
1693
+ def template_unit_paths
1694
+ template_units.paths
1695
+ end
1696
+ public :template_unit_paths
1697
+
1519
1698
  # Editor mode (`buffer:` non-nil) auto-flips the cache store to `read_only: true` so multiple
1520
1699
  # debounced editor invocations against the same project don't churn the on-disk cache or race on
1521
1700
  # schema-version writes. Cache reads continue unchanged; misses still run the producer block but the
@@ -1552,6 +1731,7 @@ module Rigor
1552
1731
  synthetic_method_index: -> { @synthetic_method_index },
1553
1732
  project_patched_methods: -> { @project_patched_methods },
1554
1733
  project_scope_seed: -> { project_scope_seed_tables },
1734
+ template_units: -> { template_units },
1555
1735
  analyze_file: ->(path, environment) { analyze_file(path, environment) },
1556
1736
  restored_run_level_rows: @restored_run_level_rows
1557
1737
  )
@@ -1571,7 +1751,8 @@ module Rigor
1571
1751
  env_build_failure_snapshot: -> { @snapshots.env_build_failure },
1572
1752
  definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
1573
1753
  hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
1574
- conformance_results_snapshot: -> { @snapshots.conformance_results }
1754
+ conformance_results_snapshot: -> { @snapshots.conformance_results },
1755
+ pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures }
1575
1756
  )
1576
1757
  end
1577
1758
 
@@ -1598,7 +1779,9 @@ module Rigor
1598
1779
  RunStats.new(
1599
1780
  wall_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - wall_started_at,
1600
1781
  peak_rss_bytes: RunStats.peak_rss_bytes,
1601
- target_files: expansion.fetch(:files).size,
1782
+ # #392 — the template units count too: they are files this run parsed and typed, and a stats line
1783
+ # that omitted them would under-report exactly the work a views-heavy project added.
1784
+ target_files: expansion.fetch(:files).size + template_units.paths.size,
1602
1785
  rbs_classes_total: snapshot.size,
1603
1786
  rbs_classes_project_sig: project_sig,
1604
1787
  rbs_classes_bundled: bundled,
@@ -1755,6 +1938,12 @@ module Rigor
1755
1938
  # so Prism's location data carries the LOGICAL path. Non-binding paths go through the cheaper
1756
1939
  # `Prism.parse_file` codepath unchanged.
1757
1940
  def parse_source(path)
1941
+ entry = template_units[path]
1942
+ if entry
1943
+ return Prism.parse(entry.source, filepath: path, version: @configuration.target_ruby,
1944
+ scopes: template_units.parse_scopes(path))
1945
+ end
1946
+
1758
1947
  if @in_memory_sources&.key?(path)
1759
1948
  return Prism.parse(@in_memory_sources[path], filepath: path, version: @configuration.target_ruby)
1760
1949
  end
@@ -1807,25 +1996,53 @@ module Rigor
1807
1996
  tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
1808
1997
  end
1809
1998
  tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
1999
+ unless @project_discovered_deferred_ranges.empty?
2000
+ tables[:discovered_deferred_ranges] = @project_discovered_deferred_ranges
2001
+ end
2002
+ seed_call_surface_tables(tables)
1810
2003
  seed_opt_in_pre_pass_tables(tables)
1811
2004
  seed_member_layout_tables(tables)
1812
2005
  seed_dependency_attribution_tables(tables)
1813
2006
  tables
1814
2007
  end
1815
2008
 
1816
- # The two mixin tables: the ADR-24 instance-side `include` / `prepend` map and its #898 singleton-side
1817
- # `extend` twin. Paired here because they are read as one picture of a class's ancestry, and split out
1818
- # of {#project_scope_seed_tables} to keep it under the complexity budget.
2009
+ # The tables only check rules read: the issue #992 parameter envelopes (`call.wrong-arity`), the issue #1120
2010
+ # refinements (`call.undefined-method`), and the issue #1367 global-write census (the `global.*` write
2011
+ # rules). Split out of {#project_scope_seed_tables} to keep it under the complexity budget.
2012
+ def seed_call_surface_tables(tables)
2013
+ unless @project_discovered_parameter_envelopes.empty?
2014
+ tables[:discovered_parameter_envelopes] = @project_discovered_parameter_envelopes
2015
+ end
2016
+ tables[:discovered_refinements] = @project_discovered_refinements unless @project_discovered_refinements.empty?
2017
+ # Issue #1367 — read by the `global.*` write rules.
2018
+ return if @project_discovered_global_write_census.empty?
2019
+
2020
+ tables[:discovered_global_write_census] = @project_discovered_global_write_census
2021
+ end
2022
+
2023
+ # The three mixin tables: the ADR-24 instance-side `include` map, its issue #1123 `prepend` twin
2024
+ # (which carries the instance-ancestor order the include table's set-shaped value cannot), and the
2025
+ # #898 singleton-side `extend` table. Grouped here because they are read as one picture of a class's
2026
+ # ancestry, and split out of {#project_scope_seed_tables} to keep it under the complexity budget.
1819
2027
  def seed_mixin_tables(tables)
1820
2028
  tables[:discovered_includes] = @project_discovered_includes unless @project_discovered_includes.empty?
2029
+ tables[:discovered_prepends] = @project_discovered_prepends unless @project_discovered_prepends.empty?
1821
2030
  tables[:discovered_extends] = @project_discovered_extends unless @project_discovered_extends.empty?
1822
2031
  end
1823
2032
 
1824
2033
  # ADR-46 slice 1 / issue #644 — the two SOURCE-ATTRIBUTION tables, read only by the recording accessors
1825
2034
  # (`Scope#record_class_dependency` for the class-declaration map, `Scope#record_constant_dependency`
1826
- # for the constant-write map). A normal run carries neither. Extracted to keep
2035
+ # for the constant-write map). A normal run carries neither of the two. Extracted to keep
1827
2036
  # {#project_scope_seed_tables} under the complexity budget.
2037
+ #
2038
+ # Issue #617 — the census's binding writes, grouped by last segment, ride every run instead: a constant
2039
+ # compound write whose plain read resolves nothing reads its binding off them, a TYPE rather than an edge.
2040
+ # Issue #1290's shadowing writes ride beside them for the lexical ladder, for the same reason.
1828
2041
  def seed_dependency_attribution_tables(tables)
2042
+ writers = constant_writer_index
2043
+ tables[:constant_writers] = writers unless writers.empty?
2044
+ shadowers = constant_shadower_index
2045
+ tables[:constant_shadowers] = shadowers unless shadowers.empty?
1829
2046
  return unless @record_dependencies
1830
2047
 
1831
2048
  tables[:discovered_class_sources] = @project_discovered_class_sources unless
@@ -1860,6 +2077,44 @@ module Rigor
1860
2077
  tables[:published_constant_names] = names unless names.empty?
1861
2078
  end
1862
2079
 
2080
+ # Issue #617 — the censused names that bind, grouped by last segment (`Scope#bound_constant_names`). A
2081
+ # name some write other than a memo `||=` touches always binds. A memo-only name binds once memos of its
2082
+ # segment span two files: either may load first and set the constant the other then reads, so only a
2083
+ # memo no other file shares keeps the idiom's reading. Files are counted expanded, because discovery can
2084
+ # walk one file under two spellings (`lib/b.rb`, `./lib/b.rb`).
2085
+ def constant_writer_index
2086
+ @constant_writer_index ||= begin
2087
+ memo_files = memo_files_by_segment
2088
+ @project_constant_writes.each_with_object({}) do |(name, by_path), index|
2089
+ segment = name.split("::").last
2090
+ next if memo_only?(by_path) && memo_files.fetch(segment).size < 2
2091
+
2092
+ (index[segment] ||= []) << name
2093
+ end.each_value(&:freeze).freeze
2094
+ end
2095
+ end
2096
+
2097
+ # Issue #1290 — the censused names some write other than a memo `||=` assigns, grouped by last segment
2098
+ # (`Scope#shadowing_constant_names`): the ones that exist whether or not an outer constant of the name
2099
+ # does, and so stop the lexical ladder above it. A memo-only name is left out however many files memoize
2100
+ # it, unlike in {#constant_writer_index}: `X ||= v` resolves `X` through the very lookup whose lower rung
2101
+ # the stop would replace, so wherever that rung answers, the memo finds it and never creates the name.
2102
+ def constant_shadower_index
2103
+ @constant_shadower_index ||= @project_constant_writes.each_with_object({}) do |(name, by_path), index|
2104
+ (index[name.split("::").last] ||= []) << name unless memo_only?(by_path)
2105
+ end.each_value(&:freeze).freeze
2106
+ end
2107
+
2108
+ def memo_files_by_segment
2109
+ @project_constant_writes.each_with_object({}) do |(name, by_path), files|
2110
+ set = (files[name.split("::").last] ||= Set.new)
2111
+ by_path.each { |path, descriptor| set << File.expand_path(path) if memo_descriptor?(descriptor) }
2112
+ end
2113
+ end
2114
+
2115
+ def memo_only?(by_path) = by_path.each_value.all? { |descriptor| memo_descriptor?(descriptor) }
2116
+ def memo_descriptor?(descriptor) = descriptor == Inference::ScopeIndexer::CONSTANT_MEMO
2117
+
1863
2118
  # Issue #644 — the LAST SEGMENTS of the published table, the run-wide half of
1864
2119
  # `Scope#published_constant?`. Seeded on EVERY run (unlike `constant_sources`, which only a recording
1865
2120
  # run needs) because {CheckRules::PublishedConstantGuard} asks the question on every `if` / `unless`.
@@ -1963,10 +2218,11 @@ module Rigor
1963
2218
  def analyze_with_effects(path, environment)
1964
2219
  return analyze_file_body(path, environment) unless @record_effects
1965
2220
 
2221
+ entry = template_units[path]
1966
2222
  diagnostics = nil
1967
2223
  collection = Effects::Collector.collect_for(
1968
2224
  path, attribution: @effect_attribution, envelopes: effect_envelope_index(environment),
1969
- plugin_facts: effect_plugin_facts
2225
+ plugin_facts: effect_plugin_facts, unit_key: entry&.unit_key, unit_owner: entry&.self_type
1970
2226
  ) do
1971
2227
  diagnostics = analyze_file_body(path, environment)
1972
2228
  end
@@ -2005,7 +2261,9 @@ module Rigor
2005
2261
  # is a per-FILE site, so one `Thread.current` read is already nothing. The per-dispatch site in
2006
2262
  # `ExpressionTyper#call_type_for` is the one that pays for the integer fast path.
2007
2263
  Effects::Collector.record_root(parse_result.value)
2008
- scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
2264
+ scope = template_units.seed(
2265
+ seed_project_scope(Scope.empty(environment: environment, source_path: path)), path
2266
+ )
2009
2267
  # ADR-24 slice 4a/4 — record unresolved implicit-self calls during the typing pass ONLY (not
2010
2268
  # CheckRules, whose own `type_of` queries would otherwise re-trigger the choke-point).
2011
2269
  # `self_call_misses` feeds the `call.self-undefined-method` collector; the recorder is inert unless
@@ -2045,7 +2303,7 @@ module Rigor
2045
2303
  # output and the plugin diagnostics are then built from that single walk's results.
2046
2304
  def rule_and_plugin_diagnostics(path, parse_result, scope, index, self_call_misses)
2047
2305
  root = parse_result.value
2048
- node_collectors = CheckRules.build_node_collectors(path, index)
2306
+ node_collectors = CheckRules.build_node_collectors(path, index, root)
2049
2307
  node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
2050
2308
  diagnostics = CheckRules.diagnose(
2051
2309
  path: path,