rigortype 0.3.9 → 0.4.1

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 (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  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/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -2,7 +2,10 @@
2
2
 
3
3
  require "digest"
4
4
  require_relative "incremental"
5
+ require_relative "incremental_run_slot"
6
+ require_relative "incremental_run_slot/write_guard"
5
7
  require_relative "plugin_fact_fingerprint"
8
+ require_relative "source_rbs_gate"
6
9
  require_relative "../cache/file_digest"
7
10
  require_relative "../cache/incremental_snapshot"
8
11
  require_relative "../effects/file_collection"
@@ -22,10 +25,11 @@ module Rigor
22
25
  # only `ΔF ∪ dependents[ΔF]`, and serves every other analyzed file from the cache — the body tier.
23
26
  #
24
27
  # The invariant the verify harness (and the spec) assert: `#recheck`'s merged diagnostics are
25
- # byte-identical (as a sorted set) to a full `--no-cache` re-analysis of the edited tree. This is the
26
- # `--verify-incremental` acceptance gate, here without disk persistence or CLI wiring (the cache is
27
- # in-process). It models the body tier only: an edit that adds / removes / moves a *file* is outside
28
- # the analyzed set it maintains and falls to a fresh {#baseline} (the structural tier is a later slice).
28
+ # byte-identical to a full `--no-cache` re-analysis of the edited tree, in the same order (#1524; the gate
29
+ # itself compares sorted sets). This is the `--verify-incremental` acceptance gate, here without disk
30
+ # persistence or CLI wiring (the cache is in-process). It models the body tier only: an edit that adds /
31
+ # removes / moves a *file* is outside the analyzed set it maintains and falls to a fresh {#baseline} (the
32
+ # structural tier is a later slice).
29
33
  # The class-length budget is relaxed: this is one cohesive orchestrator of the incremental state
30
34
  # (per-file diagnostics cache, the file-level / symbol-level / negative dependency graphs, and the ADR-85
31
35
  # seed bundles), clearer read together than split across micro-classes that would all share the same ivars.
@@ -159,11 +163,18 @@ module Rigor
159
163
  # Full baseline analysis with recording. Returns the run's diagnostics; populates the in-process cache
160
164
  # + dependency state.
161
165
  def baseline
166
+ @slot_carried_from = nil # ADR-45 WD2 — see {#load_snapshot}
162
167
  runner = build_runner(record_dependencies: true)
163
168
  diagnostics = run_runner(runner).diagnostics
164
169
  @last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
165
- @analyzed = runner.analyzed_files
166
- @seed_bundles = runner.seed_bundles # ADR-85 WD2 — the freshly built bundle set for the next run.
170
+ # #392 — the template units are deliberately NOT in the analysed set. `Runner#target_files`
171
+ # re-analyses every unit on every run, so a unit must never be reused from the per-file cache and
172
+ # must never read as a file that VANISHED from the project on the next recheck (which is what
173
+ # `previous - current` would say, `current_files` being a `.rb` expansion).
174
+ @analyzed = runner.analyzed_files - runner.template_unit_paths
175
+ # ADR-85 WD2 — the freshly built bundle set for the next run, each stamped with its file's source-RBS
176
+ # digest (issue #1536).
177
+ @seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: true)
167
178
  absorb_dependency_graph(runner)
168
179
  @return_summaries = runner.return_summaries # ADR-89 WD2 — the full-run behavioural surface.
169
180
  # ADR-67 WD6c lift — the seed table the runner's own pre-pass computed ({} when the gate is off).
@@ -174,6 +185,7 @@ module Rigor
174
185
  @effects_identity = current_effects_identity
175
186
  @cache = per_file(runner.per_file_diagnostics)
176
187
  @digests = @analyzed.to_h { |path| [path, pack_digest(path)] }
188
+ forget_unbound_digests
177
189
  @run_level_rows = runner.run_level_rows
178
190
  diagnostics
179
191
  end
@@ -195,15 +207,16 @@ module Rigor
195
207
  param_files, param_pairs = param_seed_invalidation(fresh_params)
196
208
  affected = affected_closure(changed, added, removed, param_files, param_pairs)
197
209
  analyze_set = affected & current
210
+ reused = (current & previous) - affected.to_a
198
211
  # The freshly collected table is handed to the runner so the run seeds from the SAME table the diff
199
- # was decided on (and the collector runs once per recheck, not twice).
212
+ # was decided on (and the collector runs once per recheck, not twice). The reused files' cached rows
213
+ # go in too, so the runner lists them in the full run's file order (#1524).
200
214
  runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
201
215
  param_inferred_types: fresh_params,
202
- restored_run_level_rows: @run_level_rows)
203
- fresh = run_runner(runner).diagnostics
216
+ restored_run_level_rows: @run_level_rows,
217
+ served_per_file_diagnostics: @cache.slice(*reused))
218
+ merged = run_runner(runner).diagnostics
204
219
  @last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
205
- reused = (current & previous) - affected.to_a
206
- merged = fresh + reused.flat_map { |path| @cache[path] || [] }
207
220
  absorb(runner, current, analyze_set, removed)
208
221
  @param_table = fresh_params
209
222
  Recheck.new(diagnostics: merged, changed: changed.to_set, added: added.to_set,
@@ -231,6 +244,7 @@ module Rigor
231
244
  # stale. A full baseline is the sound answer for `--incremental`, but in editor mode it is also the
232
245
  # latency this mode exists to avoid, so decline and let the caller drop to single-file scope.
233
246
  break nil unless @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
247
+ break nil if source_rbs_unverified?(result)
234
248
 
235
249
  result
236
250
  end
@@ -249,7 +263,20 @@ module Rigor
249
263
  # return exactly the way a body edit does, so it reuses the same audited dependents machinery. The
250
264
  # pairs join AFTER the ADR-89 WD2 behavioural-stability pruning: that gate re-evaluates returns under
251
265
  # the snapshot's OLD seeds, which is the wrong oracle for a pair whose seeds are the thing that moved.
266
+ #
267
+ # Issue #1536 (ADR-89 WD1 amendment) — every gate in {#gated_closure} reads the RUBY side of a file. What a
268
+ # source-RBS synthesizer derives from its comments reaches the environment as a `virtual:` buffer, and a
269
+ # reader of that RBS records no edge to the file it came from (an `attr_reader :name #: String` consumer
270
+ # records none at all), so no dependent set can bound who read it. An edit that moved it re-analyses the
271
+ # whole project — what a `sig/` edit already costs through the snapshot fingerprint.
252
272
  def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
273
+ return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
274
+
275
+ gated_closure(changed, added, removed, param_files, param_pairs)
276
+ end
277
+
278
+ # {#affected_closure} for an edit whose synthesized RBS is byte-identical in every file it touched.
279
+ def gated_closure(changed, added, removed, param_files, param_pairs)
253
280
  scan = changed + added
254
281
  # Parse the changed / added set ONCE for the per-symbol fingerprints, the class declarations, AND the
255
282
  # ADR-89 WD1 declaration signatures. They were separate `discovered_def_index_for_paths` passes over
@@ -283,12 +310,24 @@ module Rigor
283
310
  symbol_pairs = behaviourally_unstable_pairs(changed_pairs, unstable, scan_index) | param_pairs
284
311
  base = dependents_base(unstable, symbol_pairs)
285
312
  closure = base | changed.to_set | added.to_set |
286
- negative_affected(scan, removed, new_fps, new_class_decls, new_constant_decls)
313
+ negative_affected(scan, removed, new_fps, new_class_decls, new_constant_decls, summary)
287
314
  closure = param_seed_closure(closure, param_files)
288
315
  removed.each { |path| closure |= @dependents[path] || Set.new }
289
316
  closure.freeze
290
317
  end
291
318
 
319
+ # Issue #1367 — every file whose last result holds a `global.*` diagnostic re-checks on any recheck. What
320
+ # exempts such a write is a fact of the whole program — an alias, or a `write` / `to_str` / `to_int` / hatch
321
+ # defined in any spelling on any receiver, a rewritten ancestor, a mixin — which no name-keyed edge can
322
+ # follow, so a stale report would outlive the edit that exempts it. Such files are rare, so this costs little.
323
+ # A file the census silenced is not re-checked when the exemption goes away: that stale answer misses a
324
+ # report, it never makes one.
325
+ def global_write_reporters
326
+ @cache.each_with_object(Set.new) do |(path, diagnostics), reporters|
327
+ reporters << path if diagnostics.any? { |diagnostic| diagnostic.rule.to_s.start_with?("global.") }
328
+ end
329
+ end
330
+
292
331
  # ADR-67 WD6c lift — the seed-invalidated callees' own contribution to the closure: the files
293
332
  # themselves, plus — on a pre-slice-4 snapshot with no symbol edges, where the pairs' symbol fan-out
294
333
  # found nothing — their file-level dependents (wider, always sound).
@@ -380,13 +419,11 @@ module Rigor
380
419
  # dependents skippable) when its current {ScopeIndexer.declaration_signature} matches the one stored in
381
420
  # the snapshot's seed bundle — i.e. its edit changed no method signature, visibility, ancestry, member
382
421
  # layout, def line, or method existence, so every cross-file DECLARATION fact its dependents consume is
383
- # unchanged. Falls back to treating EVERY changed file as unstable (today's full closure) when a
384
- # comment-ingesting plugin is loaded — such a plugin reads the very comments the signature ignores, so a
385
- # comment edit it treats as a no-op could change a cross-file type. Sorbet sigs / dry-types includes are
386
- # CODE, captured by ADR-88's plugin-fact fingerprint (WD3), so only comment-as-input plugins escape here.
422
+ # unchanged. The signature reads code, not comments; the other half of stability — that no source-RBS
423
+ # synthesizer's output for the file moved — is settled before this runs ({SourceRbsGate#moved?}), so every
424
+ # file reaching here has a byte-identical synthesized contribution. Sorbet sigs / dry-types includes are
425
+ # CODE, captured by ADR-88's plugin-fact fingerprint (WD3).
387
426
  def declaration_unstable(changed, declaration_signatures)
388
- return changed if comment_ingesting_plugin_loaded?
389
-
390
427
  changed.reject { |path| declaration_stable?(path, declaration_signatures[path]) }
391
428
  end
392
429
 
@@ -399,14 +436,11 @@ module Rigor
399
436
  bundle[:declaration_signature] == current_declaration_signature
400
437
  end
401
438
 
402
- def comment_ingesting_plugin_loaded?
403
- # Mirrors the plugin loader's gem-name resolution (`ProjectPrePasses#trusted_gem_name`): a String
404
- # entry IS the gem name; a Hash entry names it under `"gem"` (or the manifest `"id"`).
405
- @configuration.plugins.any? do |entry|
406
- name = entry.is_a?(Hash) ? (entry["gem"] || entry["id"]) : entry
407
- COMMENT_INGESTING_PLUGIN_IDS.include?(name.to_s)
408
- end
439
+ # Every file this recheck can analyse: the removed ones drop out at `affected & current`.
440
+ def whole_project_closure(changed, added)
441
+ (@analyzed.to_set | changed | added).freeze
409
442
  end
443
+ private :gated_closure, :whole_project_closure
410
444
 
411
445
  # The current project file set (cheap directory expansion, no analysis), used to detect files added /
412
446
  # removed since the last run. Also where an editor buffer's binding is re-spelled onto the set, since
@@ -425,13 +459,13 @@ module Rigor
425
459
  # mutating session state. Returns the merged diagnostics.
426
460
  def reanalyze_subset(subset)
427
461
  affected = subset.to_set
462
+ reused = @analyzed - affected.to_a
428
463
  # ADR-67 WD6c lift — seed the subset run from the baseline's own table so the verification engine
429
464
  # exercises the exact seeds the served cache entries were computed under (and skips a re-collect).
430
465
  runner = build_runner(analyze_only: affected, param_inferred_types: @param_table,
431
- restored_run_level_rows: @run_level_rows)
432
- fresh = run_runner(runner).diagnostics
433
- reused = @analyzed - affected.to_a
434
- fresh + reused.flat_map { |path| @cache[path] || [] }
466
+ restored_run_level_rows: @run_level_rows,
467
+ served_per_file_diagnostics: @cache.slice(*reused))
468
+ run_runner(runner).diagnostics
435
469
  end
436
470
 
437
471
  # Cross-process incremental run (the `--incremental` flag's engine). With a disk `snapshot` whose
@@ -452,7 +486,7 @@ module Rigor
452
486
  # shares one digest memo across change detection and the baseline/absorb re-pack. The inner
453
487
  # `Runner#run` nests its own `with_run` for the analysis descriptors; nesting is safe (each restores).
454
488
  Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
455
- restored = fingerprint && snapshot.load(fingerprint: fingerprint)
489
+ restored = load_snapshot(snapshot, fingerprint)
456
490
  # ADR-88 WD1 — the plugin fact-surface fingerprint gates snapshot reuse the same way the global
457
491
  # fingerprint gates the load: a plugin sig/catalog edit outside `signature_paths:` (a Sorbet `.rbi`)
458
492
  # changes the types unchanged call sites resolve, without moving any analyzed file, so the global
@@ -473,7 +507,7 @@ module Rigor
473
507
  result = recheck
474
508
  adopt_plugin_fact_fingerprint
475
509
  fact_reusable = @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
476
- if fact_reusable && effects_reusable
510
+ if fact_reusable && effects_reusable && !source_rbs_unverified?(result)
477
511
  diagnostics = result.diagnostics
478
512
  warm = true
479
513
  # ADR-87 WD3 — a warm recheck that changed nothing leaves the session state byte-equivalent to the
@@ -481,9 +515,10 @@ module Rigor
481
515
  # A cold baseline always persists — there was no valid snapshot to reuse.
482
516
  skip_save = result.no_change?
483
517
  else
484
- # The fact surface moved (a plugin sig/catalog edit), a plugin is opaque, or the effects
485
- # identity moved: the cached-served files the recheck merged may be stale (or, for effects, a
486
- # partial collection cannot be closed), so re-analyze the whole tree. The current fact-surface
518
+ # The fact surface moved (a plugin sig/catalog edit), a plugin is opaque, the effects identity
519
+ # moved, or a synthesizer the source-RBS gate could not see was loaded (#1536): the cached-served
520
+ # files the recheck merged may be stale (or, for effects, a partial collection cannot be closed),
521
+ # so re-analyze the whole tree. The current fact-surface
487
522
  # digest (from the recheck runner) is unchanged by the re-analysis, so it is kept for the save.
488
523
  # Only a genuine fact-surface reason sets the reporting flag the CLI banner reads.
489
524
  @fact_surface_invalidated = true unless fact_reusable
@@ -497,7 +532,7 @@ module Rigor
497
532
  warm = false
498
533
  skip_save = false
499
534
  end
500
- snapshot.save(fingerprint: fingerprint, payload: to_payload) if persist && fingerprint && !skip_save
535
+ persist_run(snapshot, fingerprint, diagnostics, skip_save: skip_save) if persist && fingerprint
501
536
  [diagnostics, warm]
502
537
  end
503
538
  end
@@ -522,11 +557,41 @@ module Rigor
522
557
 
523
558
  # ADR-88 WD1 — capture this invocation's fact-surface fingerprint (from the last analysis runner) onto the
524
559
  # reporting ivars + the `@plugin_fact_reusable` decision object.
560
+ #
561
+ # Issue #1536 — the same prepared registry settles whether the synthesizers the session digested with
562
+ # are the ones the run's environment was built from ({SourceRbsGate#verify}). A plugin that builds its
563
+ # synthesizer in `#prepare` leaves the gate untrusted, and every stored digest is stamped unknown before
564
+ # the snapshot is saved.
525
565
  def adopt_plugin_fact_fingerprint
526
- fact = compute_plugin_fact_fingerprint
566
+ registry = prepared_plugin_registry
567
+ fact = compute_plugin_fact_fingerprint(registry)
527
568
  @plugin_fact_digest = fact.digest
528
569
  @opaque_plugin_ids = fact.opaque_plugin_ids
529
570
  @plugin_fact_reusable = fact
571
+ @seed_bundles = source_rbs_gate.verify(registry, @seed_bundles)
572
+ end
573
+
574
+ # Issue #1536 — a recheck of an edit whose closure left some file out may have under-read a synthesizer
575
+ # the gate could not see. {#run_incremental} re-analyses the project and {#run_buffer_recheck} declines,
576
+ # the way each treats a moved plugin fact surface. A run that changed nothing is exempt: under an
577
+ # untrusted gate every earlier edit already re-analysed the whole project, so the snapshot it serves is
578
+ # whole, and a null run stays one.
579
+ def source_rbs_unverified?(result)
580
+ source_rbs_gate.untrusted? && !result.no_change? && !result.reused.empty?
581
+ end
582
+
583
+ # Issue #1536 — a file the gate could not stamp because it was saved after the closure was decided (its
584
+ # readers' cached answers predate that save) must read as changed on the next run, where its unknown
585
+ # stamp sends the edit to the whole project. Recording its post-save content digest would hide it.
586
+ def forget_unbound_digests
587
+ source_rbs_gate.unbound.each { |path| @digests.delete(path) }
588
+ end
589
+
590
+ # Issue #1536 — decides whether an edit moved synthesized RBS, and stamps each seed bundle's digest. One
591
+ # per session: its untrusted state must outlive every run the session makes.
592
+ def source_rbs_gate
593
+ @source_rbs_gate ||= SourceRbsGate.new(configuration: @configuration, cache_store: @cache_store,
594
+ plugin_requirer: @plugin_requirer)
530
595
  end
531
596
 
532
597
  # ADR-88 WD1 — the plugin fact-surface fingerprint for this invocation. For a SEQUENTIAL run it is read
@@ -535,17 +600,195 @@ module Rigor
535
600
  # published facts — there it falls back to the always-sequential probe. Both paths compute the identical
536
601
  # digest for a given fact surface, so the reuse decision is worker-count-independent (the parity spec
537
602
  # asserts this).
538
- def compute_plugin_fact_fingerprint
603
+ def compute_plugin_fact_fingerprint(registry = prepared_plugin_registry)
604
+ PluginFactFingerprint.from_registry(registry)
605
+ end
606
+
607
+ # The registry the run's environment was built from, after `#prepare`: the analysis runner's own on a
608
+ # sequential run, the always-sequential probe's on a pooled one (nil when the probe fails).
609
+ def prepared_plugin_registry
539
610
  registry = @last_runner&.plugin_registry
540
- if @workers.zero? && registry && !registry.empty?
541
- PluginFactFingerprint.from_registry(registry)
542
- else
543
- PluginFactFingerprint.compute(
544
- configuration: @configuration, cache_store: @cache_store, plugin_requirer: @plugin_requirer
545
- )
611
+ return registry if @workers.zero? && registry && !registry.empty?
612
+
613
+ PluginFactFingerprint.prepared_registry(
614
+ configuration: @configuration, cache_store: @cache_store, plugin_requirer: @plugin_requirer
615
+ )
616
+ end
617
+
618
+ # Loads the snapshot, noting (ADR-45 WD2) which write of the file it read: the identity is kept only when
619
+ # the file was the same before and after the read, since a rewrite in between leaves no telling which one
620
+ # the payload came from.
621
+ def load_snapshot(snapshot, fingerprint)
622
+ @slot_guard = start_slot_guard(fingerprint)
623
+ before = snapshot_identity(snapshot)
624
+ restored = fingerprint && snapshot.load(fingerprint: fingerprint)
625
+ @restored_snapshot_identity = restored && before == snapshot_identity(snapshot) ? before : nil
626
+ # The path set the previous run-result slot is keyed by, for a recheck to carry its chain from; a
627
+ # {#baseline} clears it, since a full run starts a chain of its own.
628
+ @slot_carried_from = restored&.analyzed
629
+ restored
630
+ end
631
+
632
+ # ADR-87 WD3 — a warm recheck that changed nothing skips the snapshot rewrite: the file it restored is still
633
+ # the one on disk, which is what the run-result slot records as its snapshot.
634
+ def persist_run(snapshot, fingerprint, diagnostics, skip_save:)
635
+ written = if skip_save
636
+ @restored_snapshot_identity
637
+ elsif snapshot.save(fingerprint: fingerprint, payload: to_payload)
638
+ snapshot_identity(snapshot)
639
+ end
640
+ write_run_slot(diagnostics, written_identity: written)
641
+ end
642
+
643
+ # ADR-45 WD2 (#1507) — records this run's answer as the run-result slot a later `rigor check --incremental`
644
+ # serves without loading the engine ({IncrementalRunSlot}). The slot's dependency descriptor must validate
645
+ # what the answer was computed from, and after a narrowed recheck most of the answer was computed by earlier
646
+ # runs, so it is assembled from three sources:
647
+ #
648
+ # - the analysed files, at the digest this session holds for each: the bytes its cached or fresh rows were
649
+ # computed from, never a re-digest taken after the fact;
650
+ # - what this run read and re-derives every run ({Runner#incremental_slot_rows}' `observed` and `derived`
651
+ # rows);
652
+ # - the CHAIN: the rows the last full run recorded for inputs a recheck does not re-derive
653
+ # ({Runner#baseline_dependency_rows}), and the plugin reads credited to each file's analysis. A full run
654
+ # starts a chain; a recheck takes the previous slot's, replaces the reads of every file it re-analysed with
655
+ # this run's, and drops the removed files'.
656
+ #
657
+ # A recheck writes only when the previous slot exists and was written against the very snapshot file it
658
+ # restored: otherwise some run in between — a pool run, `--no-cache`, a write that failed — analysed files
659
+ # without recording their reads, and the chain cannot vouch for them. It then writes nothing, and the next
660
+ # full run starts a fresh chain. Declining to write is always safe; it only leaves the next null run on the
661
+ # full path.
662
+ #
663
+ # Every row not recorded as a plugin read it must still describe the tree the run read
664
+ # ({IncrementalRunSlot::WriteGuard}): the analysed files' (whose session digest a full run takes after its
665
+ # analysis), the `derived` rows, and the chain's baseline, taken now or carried ({#guarded_rows}).
666
+ #
667
+ # @param written_identity — the snapshot file as this run leaves it, recorded for the next writer's check.
668
+ def write_run_slot(diagnostics, written_identity:)
669
+ return unless run_slot_writable?
670
+
671
+ roots = @paths || @configuration.paths
672
+ rows = @last_runner.incremental_slot_rows(roots: roots)
673
+ chain = rows && (@slot_carried_from ? carry_chain(rows, roots) : start_chain(rows))
674
+ analysed = chain && analysed_file_rows
675
+ return if analysed.nil?
676
+
677
+ baseline, pinned, reads = chain
678
+ return unless @slot_guard&.admits?(guarded_rows(analysed, rows.derived, baseline), pinned: pinned)
679
+
680
+ # A save the run read after the mark's tick must not hide behind an unmoved stat tuple.
681
+ reads = reads.transform_values { |descriptor| @slot_guard.mark_racy(descriptor) }
682
+
683
+ entry = IncrementalRunSlot::Entry.new(
684
+ diagnostics: diagnostics, roots: IncrementalRunSlot.as_written(roots),
685
+ baseline: baseline, pinned: pinned, reads: reads, snapshot: written_identity
686
+ )
687
+ dependencies = @slot_guard.mark_racy(run_slot_dependencies(analysed, rows, [baseline, pinned], reads.values))
688
+ wrote = IncrementalRunSlot.write(
689
+ store: @cache_store, configuration: @configuration, entry: entry, dependencies: dependencies,
690
+ target: IncrementalRunSlot::Target.new(files: @analyzed, roots: roots)
691
+ )
692
+ discard_previous_slot(roots) if wrote
693
+ rescue StandardError
694
+ nil
695
+ end
696
+
697
+ # The previous slot is keyed by the previous path set; once this run's stands, nothing asks for it again.
698
+ def discard_previous_slot(roots)
699
+ return if @slot_carried_from.nil? || @slot_carried_from.sort == @analyzed.sort
700
+
701
+ IncrementalRunSlot.discard(
702
+ store: @cache_store, configuration: @configuration,
703
+ target: IncrementalRunSlot::Target.new(files: @slot_carried_from, roots: roots)
704
+ )
705
+ end
706
+
707
+ # ADR-45 WD2 — the mark {IncrementalRunSlot::WriteGuard#admits?} checks the slot's rows against, taken before
708
+ # the run reads anything. nil (no slot this run) for a store no slot is written to, or no fingerprint.
709
+ def start_slot_guard(fingerprint)
710
+ return nil if fingerprint.nil? || @cache_store.nil? || @cache_store.read_only? || !@buffer.nil?
711
+ # Known before the run: none of these can be written afterwards, so the stamp and the recomputed
712
+ # fingerprint would be spent for nothing.
713
+ return nil unless @workers.zero? && !@configuration.effects_enabled?
714
+
715
+ IncrementalRunSlot::WriteGuard.start(
716
+ configuration: @configuration, roots: @paths || @configuration.paths, cache_root: @cache_store.root,
717
+ fingerprint: fingerprint
718
+ )
719
+ end
720
+
721
+ # The runs whose answer the slot may hold: a writable store, no editor buffer (whose bytes exist only in
722
+ # the editor), a sequential run (a pool worker's plugin reads never reach this process), effect collection
723
+ # off ({IncrementalRunSlot.serve} declines it), and no opaque plugin — the next full-path run would then be
724
+ # cold, not the warm run the probe stands in for.
725
+ def run_slot_writable?
726
+ !@cache_store.nil? && !@cache_store.read_only? && @buffer.nil? && @workers.zero? &&
727
+ !@configuration.effects_enabled? && @opaque_plugin_ids.empty? && !@last_runner.nil?
728
+ end
729
+
730
+ def start_chain(rows)
731
+ baseline = @last_runner.baseline_dependency_rows(files: @analyzed)
732
+ baseline && [baseline.owned, baseline.pinned, rows.by_file.slice(*@analyzed)]
733
+ end
734
+
735
+ def carry_chain(rows, roots)
736
+ return nil if @restored_snapshot_identity.nil?
737
+
738
+ previous = IncrementalRunSlot.previous_entry(
739
+ store: @cache_store, configuration: @configuration,
740
+ target: IncrementalRunSlot::Target.new(files: @slot_carried_from, roots: roots)
741
+ )
742
+ return nil if previous.nil? || previous.snapshot != @restored_snapshot_identity
743
+
744
+ served = previous.reads.except(*@last_runner.analyzed_files)
745
+ [previous.baseline, previous.pinned, served.merge(rows.by_file).slice(*@analyzed)]
746
+ end
747
+
748
+ # One `:stat` row per analysed file, from the entry this session recorded for it (`#pack_digest`), re-packed
749
+ # against the file's current stat when the bytes still hash to the recorded digest but the tuple moved (a
750
+ # `touch`, a checkout): the snapshot keeps the old tuple, and a row carrying it would cost every later probe
751
+ # a re-hash of the file. nil when any file has no usable entry (it could not be read): no row could say what
752
+ # its answer was computed from.
753
+ def analysed_file_rows
754
+ @analyzed.map do |path|
755
+ packed = Cache::FileDigest.refresh_stat(path, @digests[path])
756
+ return nil if packed.nil?
757
+
758
+ Cache::Descriptor::FileEntry.new(path: path, comparator: :stat, value: packed)
546
759
  end
547
760
  end
548
761
 
762
+ # @param chained — the chain's baseline descriptors, owned and pinned
763
+ def run_slot_dependencies(analysed, rows, chained, reads)
764
+ carried = chained + reads
765
+ Cache::Descriptor.new(
766
+ files: analysed + rows.derived.files + rows.observed.files + carried.flat_map(&:files),
767
+ globs: (rows.derived.globs + rows.observed.globs + carried.flat_map(&:globs)).uniq
768
+ )
769
+ end
770
+
771
+ # What the write guard asks about besides the pinned baseline: every row the key does not pin that was not
772
+ # recorded as a plugin read it. A recheck's carried baseline is among them. The run that took it vouched for
773
+ # the tree it saw, but this run reads those inputs again (a recheck with a non-empty closure rebuilds its
774
+ # environment from the signature tree), and a save it read that is reverted before the run ends leaves every
775
+ # carried row fresh while the answer came from the saved bytes.
776
+ def guarded_rows(analysed, derived, baseline)
777
+ Cache::Descriptor.new(files: analysed + derived.files + baseline.files, globs: derived.globs + baseline.globs)
778
+ end
779
+
780
+ # The snapshot file's `(size, mtime_ns, ctime_ns, inode)`: which write of it a run restored, or left behind.
781
+ # A rewrite renames a new file into place, so any write moves it. nil when there is no file to stat.
782
+ def snapshot_identity(snapshot)
783
+ path = snapshot.respond_to?(:path) ? snapshot.path : nil
784
+ return nil if path.nil?
785
+
786
+ stat = File.stat(path)
787
+ [stat.size, Cache::FileDigest.ns_of(stat.mtime), Cache::FileDigest.ns_of(stat.ctime), stat.ino]
788
+ rescue SystemCallError
789
+ nil
790
+ end
791
+
549
792
  # Adopt a persisted snapshot's per-file state as this session's baseline (the warm-start path).
550
793
  def restore(payload)
551
794
  @analyzed = payload.analyzed
@@ -707,13 +950,16 @@ module Rigor
707
950
  removed.each { |path| forget(path) }
708
951
  @analyzed = current
709
952
  # ADR-85 WD2 — the recheck's discovery folded the restored bundles and refreshed them (changed files
710
- # re-walked, removed files dropped, added files built), so adopt the runner's current set wholesale.
711
- @seed_bundles = runner.seed_bundles
953
+ # re-walked, removed files dropped, added files built), so adopt the runner's current set wholesale,
954
+ # stamping the re-walked ones with their source-RBS digest (issue #1536).
955
+ whole_project = current.all? { |path| analyze_set.include?(path) }
956
+ @seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: whole_project)
712
957
  fresh_by_file = per_file(runner.per_file_diagnostics)
713
958
  analyze_set.each do |path|
714
959
  @cache[path] = fresh_by_file[path] || []
715
960
  @digests[path] = pack_digest(path)
716
961
  end
962
+ forget_unbound_digests
717
963
  absorb_dependency_graph(runner)
718
964
  # Issues #796 / #794 — the recheck's own snapshots already carry the replayed rows folded together
719
965
  # with anything its closure demanded for itself, so this is the same union the next run replays.
@@ -828,7 +1074,9 @@ module Rigor
828
1074
  # a prior missed lookup, and (issue #639) because a class DECLARATION appeared or vanished. Maps each
829
1075
  # appeared `"ClassName#method"` to the negative-dependency key it would satisfy (`toplevel:foo` for a
830
1076
  # top-level def, `method:C#m` otherwise), then unions the recorded negative-dependents of those keys.
831
- def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls)
1077
+ # Issue #1120 — plus the consumers of any refinement name the edit moved ({#refinement_affected}). Issue #1367 —
1078
+ # and every file whose last result holds a `global.*` diagnostic ({#global_write_reporters}).
1079
+ def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls, summary = nil)
832
1080
  appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
833
1081
  # Issue #639 — a class DISAPPEARING satisfies the `class:` kind too, now that a resolved bare
834
1082
  # reference records the edge and not only a failed one; removed files join the diff for it.
@@ -844,7 +1092,21 @@ module Rigor
844
1092
  keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
845
1093
  keys.concat(moved_classes.map { |klass| "class:#{klass.split('::').last}" })
846
1094
  keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
847
- Incremental.negative_closure(keys, @negative_dependents)
1095
+ Incremental.negative_closure(keys, @negative_dependents) | refinement_affected(changed + removed, summary) |
1096
+ global_write_reporters
1097
+ end
1098
+
1099
+ # Issue #1120 — the consumers whose `call.undefined-method` answer read the refinement table for a method
1100
+ # name whose refinements moved in this edit. The before-state is each file's seed bundle (the per-file
1101
+ # table the last run recorded), the after-state the scan's per-file tables. A refine body is not a class's
1102
+ # method, so neither the symbol fingerprints nor the appeared-symbol diff above sees it.
1103
+ def refinement_affected(paths, summary)
1104
+ after = (summary && summary[:refinements]) || {}
1105
+ before = paths.to_h { |path| [path, @seed_bundles.dig(path, :refinements)] }
1106
+ names = Incremental.changed_refinement_names(paths, before, after)
1107
+ return Set.new if names.empty?
1108
+
1109
+ Incremental.negative_closure(names.map { |name| "refinement:#{name}" }, @negative_dependents)
848
1110
  end
849
1111
 
850
1112
  # The qualified class/module names declared in the pre-parsed `index` (shared with
@@ -985,12 +1247,6 @@ module Rigor
985
1247
  TOP_LEVEL_KEY = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
986
1248
  private_constant :TOP_LEVEL_KEY
987
1249
 
988
- # B1 — plugin require-names that ingest COMMENT content as semantic input (inline-RBS reads `# @rbs` /
989
- # `#:` annotations). B1's code fingerprint ignores comments, so a project configuring one of these opts
990
- # OUT of the bundle-equality skip (a comment edit could change a cross-file type it contributes).
991
- COMMENT_INGESTING_PLUGIN_IDS = %w[rigor-rbs-inline].freeze
992
- private_constant :COMMENT_INGESTING_PLUGIN_IDS
993
-
994
1250
  def negative_key_for(symbol)
995
1251
  class_name, method = symbol.split("#", 2)
996
1252
  class_name == TOP_LEVEL_KEY ? "toplevel:#{method}" : "method:#{symbol}"
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "digest"
4
4
  require_relative "runner"
5
+ require_relative "../cache/value_digest"
5
6
 
6
7
  module Rigor
7
8
  module Analysis
@@ -98,9 +99,9 @@ module Rigor
98
99
  def digest_registry(registry)
99
100
  return Result.new(digest: nil, opaque_plugin_ids: [].freeze) if registry.nil? || registry.empty?
100
101
 
101
- facts_by_plugin = facts_by_plugin(registry.plugins.first&.services&.fact_store)
102
- parts = fact_parts(facts_by_plugin)
103
102
  opaque = []
103
+ facts_by_plugin = facts_by_plugin(registry, opaque)
104
+ parts = fact_parts(facts_by_plugin)
104
105
  registry.plugins.each { |plugin| collect_plugin_parts(plugin, facts_by_plugin, parts, opaque) }
105
106
  # Sort the parts so the digest is independent of plugin registration / iteration order.
106
107
  Result.new(
@@ -129,16 +130,25 @@ module Rigor
129
130
 
130
131
  # `{ plugin_id => ["name=digest", ...] }` for every ADR-9 published fact (channel a). A plugin that
131
132
  # published nothing does not appear.
132
- def facts_by_plugin(fact_store)
133
+ #
134
+ # A fact the fingerprint cannot digest is an input it cannot see, so its publisher is opaque, whether or
135
+ # not the publisher contributes a type itself: other plugins read its facts. A fact store that cannot be
136
+ # read at all makes every plugin opaque. Both force the full run; dropping the facts instead would let a
137
+ # plugin with another surface reuse a snapshot its facts no longer match.
138
+ def facts_by_plugin(registry, opaque)
133
139
  result = Hash.new { |hash, key| hash[key] = [] }
134
- return result if fact_store.nil?
135
-
136
- fact_store.each_fact do |fact|
137
- result[fact.plugin_id.to_s] << "#{fact.name}=#{digest_value(fact.value)}"
140
+ fact_store = registry.plugins.first&.services&.fact_store
141
+ fact_store&.each_fact do |fact|
142
+ plugin_id = fact.plugin_id.to_s
143
+ begin
144
+ result[plugin_id] << "#{fact.name}=#{digest_value(fact.value)}"
145
+ rescue StandardError
146
+ opaque << plugin_id
147
+ end
138
148
  end
139
149
  result
140
150
  rescue StandardError
141
- # An unreadable fact store contributes nothing; opacity for contributing plugins is decided below.
151
+ registry.plugins.each { |plugin| opaque << safe_id(plugin) }
142
152
  Hash.new { |hash, key| hash[key] = [] }
143
153
  end
144
154
 
@@ -175,12 +185,13 @@ module Rigor
175
185
  true
176
186
  end
177
187
 
178
- # A stable content digest of an arbitrary fact / producer value. Producer values are Marshal-clean by
179
- # contract (the ADR-45 disk cache serialises them the same way), so this is the same round-trip the cache
180
- # already relies on. A Marshal failure raises to the caller, which marks the plugin opaque (never a
181
- # silent wrong-value digest).
188
+ # A digest of a fact / producer / hook value that depends on the value alone (issue #1574). A producer's
189
+ # value is computed on one run and served from its ADR-45 cache entry on the next; the two are equal but
190
+ # do not share the same objects, so a digest of their `Marshal.dump` bytes differed and invalidated the
191
+ # snapshot on every such switch. {Cache::ValueDigest} reads no identity. A value it cannot digest raises
192
+ # to the caller, which marks the plugin opaque (never a silent wrong-value digest).
182
193
  def digest_value(value)
183
- Digest::SHA256.hexdigest(Marshal.dump(value))
194
+ Cache::ValueDigest.hexdigest(value)
184
195
  end
185
196
 
186
197
  def contributes_types?(plugin)
@@ -16,13 +16,23 @@ module Rigor
16
16
  # are NOT visible until the owner invalidates the scan — typically via `workspace/didChangeWatchedFiles`.
17
17
  # This is the same trade-off the LSP made when slice 7 cached only the `Environment`; extending the cache
18
18
  # to the pre-pass outputs preserves the contract.
19
+ #
20
+ # `template_units` (#1038) is the one slot a consumer does not adopt verbatim. The rest of the snapshot
21
+ # is reused as it stands until the owner invalidates it; the template index is handed to
22
+ # `TemplateUnits.collect(previous:)`, which re-expands the claimed globs and revalidates each template
23
+ # against the filesystem, so a template edited, added or deleted on disk is seen on the next publish
24
+ # even though the snapshot itself did not move. What the carry saves is the plugin transform — for ERB
25
+ # (#393) an Erubi compile of every view in the project, otherwise paid per keystroke. The index is
26
+ # always built WITHOUT a buffer binding, so the shared snapshot never holds a unit compiled from an
27
+ # editor's in-flight bytes.
19
28
  ProjectScan = Data.define(
20
29
  :plugin_registry,
21
30
  :dependency_source_index,
22
31
  :synthetic_method_index,
23
32
  :project_patched_methods,
24
33
  :plugin_prepare_diagnostics,
25
- :pre_eval_diagnostics
34
+ :pre_eval_diagnostics,
35
+ :template_units
26
36
  )
27
37
  end
28
38
  end
@@ -83,7 +83,6 @@ module Rigor
83
83
  # Loads the project's configured plugins, runs every `#prepare`, and returns the union of every
84
84
  # published `:reachability_roots` and `:reachability_references` fact.
85
85
  #
86
- # @param configuration — the loaded project configuration.
87
86
  # @param plugin_requirer — how a plugin gem is brought into the process, called with the gem name. The
88
87
  # same seam `Analysis::Runner` exposes, so a spec can register a plugin class without publishing a
89
88
  # gem.