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
@@ -93,6 +93,7 @@ module Rigor
93
93
  ["IO", :read, :singleton] => IO_READ_NO_LENGTH,
94
94
  ["File", :read, :singleton] => IO_READ_NO_LENGTH
95
95
  }.freeze
96
+ Ractor.make_shareable(OVERRIDES)
96
97
  private_constant :OVERRIDES
97
98
 
98
99
  # Looks up a refined return type for the given call.
@@ -124,6 +125,7 @@ module Rigor
124
125
  acc[mname] ||= []
125
126
  acc[mname] << owner unless acc[mname].include?(owner)
126
127
  end.freeze
128
+ Ractor.make_shareable(OWNERS_BY_METHOD)
127
129
  private_constant :OWNERS_BY_METHOD
128
130
 
129
131
  # @return the candidate owner class names for a bare method-name
@@ -53,7 +53,36 @@ module Rigor
53
53
  # interface a `conforms-to` names) invalidates the warm entry. A pre-9 entry carries no signature-root
54
54
  # row and would validate fresh across exactly that edit — the #577 reasoning, one slot over — so cached
55
55
  # entries must read as misses once and rebuild carrying the rows.
56
- SCHEMA_VERSION = 9
56
+ # v10: #1123 — the def-index seed bundles gain the `:prepends` table, the instance-ancestor-ORDER half
57
+ # of the `include` table (`prepend M`, and `Recv.prepend(M)`), which `Scope#user_def_through_ancestors`
58
+ # searches ahead of a class's own `def`s. A pre-10 bundle would silently contribute no prepends for an
59
+ # unchanged file, so that file's consumers would keep answering with the pre-fix MRO — cached bundles
60
+ # must read as misses once and rebuild carrying the slot.
61
+ # v11: #1558 — a plugin producer served from its own cache now replays its entry's dependency rows into
62
+ # the plugin's IoBoundary, so the run-result entry and the entry of any producer computed after it carry
63
+ # the served value's inputs. A pre-11 entry written while a producer hit lacks those rows and would
64
+ # validate fresh across exactly the edit they exist to catch. A checkout's source digest already moved
65
+ # every such key, and a release clears the root through `Store::PAYLOAD_ABI_VERSION`; the bump closes
66
+ # the remaining window, a same-`VERSION` build installed as `gems/rigortype-<VERSION>`, which
67
+ # {EngineSource} treats as version-pinned (#285) and so keys on the version alone.
68
+ # v12: the def-index seed bundles gain the `:unpositioned_mixins` table, the mixin edges whose order is
69
+ # not a fact, which an ancestor-order reader declines on. A pre-12 bundle would silently contribute no
70
+ # such edge for an unchanged file, so cached bundles must read as misses once and rebuild carrying it.
71
+ # v13/v14: #1587 — the prepend table keeps every statement (a repeated `prepend` is no longer collapsed at
72
+ # the freeze) and a module written to both the include and the prepend table, or a repeated `extend`, is
73
+ # named unpositioned. A pre-14 bundle would hold the collapsed prepend list and miss the taint for an
74
+ # unchanged file, so cached bundles must read as misses once and rebuild.
75
+ # v15: #1592 — an `extend` (or a `class << self` include) written inside a block the walk used to skip is
76
+ # now recorded and named unpositioned. A pre-15 bundle would hold neither for an unchanged file.
77
+ # v16: ADR-119 WD1 — the def-index seed bundles gain the `:siblings` Hash (the `possible_*` / `contested_*`
78
+ # halves of the discovery pairs). A pre-16 bundle would contribute no sibling for an unchanged file, so cached
79
+ # bundles must read as misses once and rebuild carrying it.
80
+ # v17: ADR-119 WD3 — the def-contribution producers fill the siblings (a conditional `def`'s name possible, its
81
+ # def-node slot and envelope contested). A pre-17 bundle carries the empty siblings of the walk before them, so
82
+ # an unchanged file would fold as "no possible fact" where a cold walk records one.
83
+ # v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
84
+ # carries it empty, so an unchanged file's conditional visibility would fold as certain.
85
+ SCHEMA_VERSION = 18
57
86
 
58
87
  # Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
59
88
  # struct so no caller can mutate after the entry is in a Descriptor.
@@ -100,6 +129,21 @@ module Rigor
100
129
  new(path: path, comparator: :stat, value: packed)
101
130
  end
102
131
 
132
+ # A `:stat` row whose recording instant is no later than `limit_ns`: any file whose recorded mtime is at or
133
+ # after `limit_ns` is then racy, and validation re-hashes it instead of trusting its stat tuple. Any other
134
+ # row, and one whose instant is already earlier, is returned as is.
135
+ def with_recording_instant_at_most(limit_ns)
136
+ return self unless comparator == :stat
137
+
138
+ parts = value.split
139
+ return self unless parts.size == 6 && Integer(parts[5], 10) > limit_ns
140
+
141
+ parts[5] = limit_ns.to_s
142
+ self.class.new(path: path, comparator: comparator, value: parts.join(" "))
143
+ rescue ArgumentError
144
+ self
145
+ end
146
+
103
147
  # ADR-45 WD1 (#577) — the absence row: fresh while `path` does not exist, stale the moment anything
104
148
  # (a file, a directory, a symlink) comes into existence there. {Plugin::IoBoundary#read_file}
105
149
  # records one when a read fails because the path is missing, so a value computed on "X is absent"
@@ -3,6 +3,7 @@
3
3
  require "digest"
4
4
 
5
5
  require_relative "../version"
6
+ require_relative "descriptor"
6
7
 
7
8
  module Rigor
8
9
  module Cache
@@ -63,6 +64,9 @@ module Rigor
63
64
  # to whatever else happened to be there, which is exactly the weakening this module forbids.
64
65
  REQUIRED_DIRECTORY = "lib"
65
66
 
67
+ # The config-row key every computed-value slot carries the identity under.
68
+ CONFIG_KEY = "engine-source"
69
+
66
70
  module_function
67
71
 
68
72
  # @return the gem root — the directory holding `lib/`, three levels above this file.
@@ -100,10 +104,34 @@ module Rigor
100
104
  # exists at all. The same reading covers the long-running `rigor lsp` process.
101
105
  #
102
106
  # {Unavailable} propagates and is deliberately NOT memoised: the ivar is only assigned on success.
107
+ #
108
+ # The memo is FROZEN because a Ractor-pool worker reads it (#1055): the auto-wired ADR-93
109
+ # `rigor-rbs-inline` synthesizer reaches `.key_config_entries` from inside
110
+ # `Environment.for_project`, which is inside `WorkerSession#initialize`, which is inside the worker's
111
+ # own Ractor — and a non-main Ractor may read a class/module ivar only when its value is shareable.
112
+ # An unfrozen digest String therefore killed every worker in its constructor on any run with the
113
+ # synthesizer on, which is every real CLI run (the spec suite pins it off). The worker never WRITES
114
+ # it: `PoolCoordinator#analyze_files_in_pool` pre-warms this on the main Ractor first, which is where
115
+ # the argument above already says the value belongs.
103
116
  def process_identity
104
117
  return @process_identity if defined?(@process_identity)
105
118
 
106
- @process_identity = identity
119
+ @process_identity = identity.freeze
120
+ end
121
+
122
+ # The `engine-source` key row for a cache whose value this engine computed, built from
123
+ # {.process_identity}: empty for a version-pinned tree, so a released gem's keys stay byte-identical.
124
+ #
125
+ # Issue #1009 — a slot keyed only on its inputs and a plugin's manifest version outlives the engine
126
+ # that filled it. The per-file synthesizer slot and every plugin producer were keyed that way, so after
127
+ # an engine or bundled-plugin edit the run-result key moved and the run re-analysed, but through the
128
+ # previous build's synthesized RBS and producer values. {Unavailable} propagates, so a caller that
129
+ # cannot identify the engine bypasses its cache instead of falling back to the weaker key.
130
+ def key_config_entries
131
+ identity = process_identity
132
+ return [] if identity.nil?
133
+
134
+ [Descriptor::ConfigEntry.new(key: CONFIG_KEY, value_hash: Digest::SHA256.hexdigest(identity))]
107
135
  end
108
136
 
109
137
  # Discards the {.process_identity} memo; production code MUST NOT call this — a run that recomputed
@@ -112,6 +112,34 @@ module Rigor
112
112
  hexdigest(path) == digest
113
113
  end
114
114
 
115
+ # ADR-45 WD2 (#1507) — `packed` re-packed against `path`'s current stat, for a writer about to record it
116
+ # again: when the tuple moved (a `touch`, a checkout) or the entry is racy, but the file still hashes to the
117
+ # recorded digest, the new pack carries the current tuple and this run's recording instant, so the next
118
+ # validation is one `File::Stat` again rather than a re-hash every time.
119
+ #
120
+ # The hash is taken fresh, never from the per-run memo: a memoised digest describes the bytes as they were
121
+ # when change detection read them, and pairing it with a later stat would vouch for bytes written since. And
122
+ # the re-pack is kept only when the tuple it packed, taken after the hash, is the one taken before it: a
123
+ # write landing while the file is hashed moves the change time, and leaves `packed` as it was.
124
+ #
125
+ # `packed` comes back unchanged when its tuple still matches and it is not racy, and whenever the bytes no
126
+ # longer match the digest: a row that must stay stale is never refreshed into a fresh one. nil when `packed`
127
+ # is not a stat pack. A stat failure (the file is gone) returns `packed` unchanged, which then validates as
128
+ # stale.
129
+ def self.refresh_stat(path, packed)
130
+ parsed = parse_stat(packed)
131
+ return nil if parsed.nil?
132
+
133
+ before = File.stat(path)
134
+ return packed if !racy?(parsed) && tuple_matches?(before, parsed)
135
+ return packed unless Digest::SHA256.file(path).hexdigest == parsed[0]
136
+
137
+ repacked = pack_stat(path, parsed[0])
138
+ repacked && tuple_matches?(before, parse_stat(repacked)) ? repacked : packed
139
+ rescue SystemCallError
140
+ packed
141
+ end
142
+
115
143
  # The content-digest field of a packed `:stat` entry ({.pack_stat}); nil when the entry is absent or not
116
144
  # well-formed. Exposed for the caller that must compare the recorded digest against bytes it hashes
117
145
  # itself rather than against the file on disk — the incremental session's editor-mode freshness check,
@@ -5,6 +5,10 @@ require "digest"
5
5
  require "zlib"
6
6
 
7
7
  require_relative "engine_source"
8
+ require_relative "store"
9
+ require_relative "../analysis/shadow_harness"
10
+ require_relative "../analysis/run_cache_key"
11
+ require_relative "../environment"
8
12
 
9
13
  module Rigor
10
14
  module Cache
@@ -106,7 +110,65 @@ module Rigor
106
110
  # blob has no such section and would deserialise it as nil, which reads as "nothing cached" and is
107
111
  # therefore not misread — but a snapshot written by an engine that did not yet REPLAY them was also
108
112
  # written by one whose narrowed runs under-reported, so the gate rejects it and the next run is cold.
109
- SCHEMA = 22
113
+ # 23: issue #992 gives each seed bundle a `parameter_envelopes` table — the joined positional envelope
114
+ # of every method a file records, plus its module / dynamic-surface marks — which `call.wrong-arity`
115
+ # reads for a method no signature declares. A pre-23 bundle folds as "no envelopes", which only ever
116
+ # withholds a check, but a warm run that withholds where a cold run fires is still the
117
+ # `--verify-incremental` divergence 14 was bumped for.
118
+ # 24: issue #1097 gives each seed bundle a `deferred_ranges` table — the per-file def / block /
119
+ # lambda body ranges `Scope#*_def_shadows_call?` orders a project-defined `sig` override
120
+ # against. A pre-24 bundle would fold as "no ranges", which the predicate reads as "cannot
121
+ # order → shadow" — a warm run declining the binding where a cold run binds.
122
+ # 25: issue #1097 extends each `deferred_ranges` row to `[start, end, name, kind, owner]` —
123
+ # owner-scoped ordering. A 24 bundle folds as "owner = nil", which the predicate reads as
124
+ # "cannot order", under-shadowing `module_function` defs whose only table evidence is the row
125
+ # itself.
126
+ # 26: issue #1123 gives each seed bundle a `prepends` table — the instance-ancestor ORDER of a
127
+ # class's `prepend`ed modules, which `Scope#user_def_through_ancestors` searches ahead of the
128
+ # class's own `def`s. A pre-26 bundle would fold as "prepends nothing": the warm run would answer
129
+ # the pre-fix MRO where a cold run answers the prepended module, exactly the `--verify-incremental`
130
+ # divergence 14 was bumped for (and a silently WRONG dispatch answer, not a withheld check).
131
+ # 27: issue #1173 re-MEANs each seed bundle's `includes` VALUES without changing their shape: the
132
+ # lists now store instance-ancestor SEARCH order (nearest first, prepends ahead of includes)
133
+ # instead of call order — the same flip 17 made for def-row `nesting`. A pre-27 blob deserialises
134
+ # cleanly and would serve the old call order on a warm run while a cold run searches nearer-first:
135
+ # `include A; include B` would resolve A's methods where a cold run resolves B's — the
136
+ # `--verify-incremental` divergence 14 was bumped for.
137
+ # 28: issue #1120 gives each seed bundle a `refinements` table — the `def`s of every `refine X do … end`
138
+ # body, which `call.undefined-method` reads where a `using` of the refining module is in effect. A
139
+ # pre-28 bundle would fold as "refines nothing", and a warm run would report a refined call a cold run
140
+ # declines.
141
+ # 29: issue #1367 gives each seed bundle a `global_write_census` — the globals its `alias` statements name and
142
+ # the `write` / `to_str` / `to_int` / hatch definitions and top-level mixins it holds, which the `global.*`
143
+ # write rules decline on project-wide. A pre-29 bundle would fold as "none of those", and a warm run would
144
+ # report a write a cold run exempts.
145
+ # 30: issue #1536 gives each seed bundle a `source_rbs_digest` — the digest of what every loaded source-RBS
146
+ # synthesizer contributes for the file — which ADR-89 WD1 now compares beside `declaration_signature`
147
+ # in place of switching the gate off whenever `rigor-rbs-inline` is configured. A pre-30 bundle
148
+ # carries none, which the session reads as "unknown" and so as moved: sound, but every first edit to
149
+ # each file would re-analyse the whole project, so the gate rejects the blob and the next run is cold.
150
+ # 31: issue #1574 digests `plugin_fact_digest` and a seed bundle's `source_rbs_digest` with
151
+ # {ValueDigest} instead of over `Marshal.dump` bytes, so the same value digests alike whether a producer
152
+ # computed it or its cache entry served it. A pre-31 digest is in the old form and would never match, so
153
+ # the next run would go cold anyway; the bump makes that a clean miss instead of a "plugin fact surface
154
+ # changed" note that nothing changed.
155
+ # 32: each seed bundle gains an `unpositioned_mixins` table — the file's mixin edges whose order is not a
156
+ # fact (`Scope::DiscoveryIndex#unpositioned_mixins`), which an ancestor-order reader declines on. A pre-32
157
+ # bundle would fold as "every edge positioned", and a warm run would answer an order a cold run declines.
158
+ # 33: the mixin producers change what a seed bundle holds (#1587 — the prepend table keeps every statement,
159
+ # and a both-kind module or a repeated `extend` is named unpositioned), so a pre-33 bundle folds as the
160
+ # collapsed, positioned table and a warm run would answer an order a cold run declines.
161
+ # 34: #1592 — an `extend` or a `class << self` include inside a block the extends walk used to skip is now
162
+ # recorded and named unpositioned, so a pre-34 bundle folds as if the edge did not exist.
163
+ # 35: ADR-119 WD1 — each seed bundle gains a `siblings` Hash, the `possible_*` / `contested_*` siblings of the
164
+ # `Scope::DiscoveryIndex` members that may admit a possible fact. Every sibling is empty until a producer
165
+ # fills one, so a pre-35 bundle folds as "no possible fact", which is what it would hold today; the bump keeps
166
+ # that true once one does.
167
+ # 36: ADR-119 WD3 — the def-contribution producers fill those siblings, so a pre-36 bundle folds an unchanged
168
+ # file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
169
+ # 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
170
+ # carries empty.
171
+ SCHEMA = 37
110
172
 
111
173
  # The persisted per-file state.
112
174
  # `cache` maps an analyzed file to its diagnostics.
@@ -178,8 +240,13 @@ module Rigor
178
240
  # rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the
179
241
  # resolved configuration, the analysis **roots** (the path arguments, e.g. `["lib"]`, NOT the expanded
180
242
  # file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is
181
- # handled incrementally by the session, not a full rebuild), the resolved gem set (`Gemfile.lock` /
182
- # `rbs_collection`), and the project's own RBS (`signature_paths` file contents). Built WITHOUT
243
+ # handled incrementally by the session, not a full rebuild), the resolved gem set (the lockfiles
244
+ # {Analysis::RunCacheKey.resolved_lockfile_paths} resolves — `bundler.lockfile:` / `rbs_collection.lockfile:`,
245
+ # else `./Gemfile.lock` / `./rbs_collection.lock.yaml` — the same files the run-result key digests), the
246
+ # project's own RBS (the `sig/` roots the environment actually loads: the explicit
247
+ # `signature_paths`, or the auto-detected `<root>/sig` when `signature_paths` is nil), and the contents of
248
+ # every `pre_eval:` file (its PATH is in the configuration, but an edit to a file outside the analysed
249
+ # paths changes no analysed file, so it needs its own part). Built WITHOUT
183
250
  # constructing the RBS environment so the warm path can gate the load cheaply, before the costly env
184
251
  # build. The `--verify-incremental` gate is the safety net for any under-capture (it would surface as an
185
252
  # incremental-vs-full mismatch). Returns nil on any error → the caller falls back to a non-persisted run.
@@ -193,16 +260,22 @@ module Rigor
193
260
  # part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
194
261
  # survive the upgrade untouched.
195
262
  def self.fingerprint(configuration:, roots:)
263
+ bundler_lock, collection_lock = Analysis::RunCacheKey.resolved_lockfile_paths(configuration)
196
264
  parts = [
197
265
  "engine:#{Rigor::VERSION}:#{SCHEMA}",
198
266
  "config:#{Digest::SHA256.hexdigest(Marshal.dump(configuration.to_h))}",
199
267
  "roots:#{Array(roots).map(&:to_s).sort.join("\n")}",
200
- "gems:#{digest_file_if_present('Gemfile.lock')}",
201
- "rbs_collection:#{digest_file_if_present('rbs_collection.lock.yaml')}",
202
- "sig:#{digest_signature_paths(configuration.signature_paths)}"
268
+ "gems:#{digest_file_if_present(bundler_lock)}",
269
+ "rbs_collection:#{digest_file_if_present(collection_lock)}",
270
+ "sig:#{digest_signature_paths(project_sig_roots(configuration))}",
271
+ "pre_eval:#{digest_pre_eval(configuration.pre_eval)}"
203
272
  ]
204
273
  identity = EngineSource.process_identity
205
274
  parts << "engine-source:#{identity}" if identity
275
+ # ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
276
+ # wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
277
+ shadow = Analysis::ShadowHarness.cache_identity
278
+ parts << "shadow-harness:#{shadow}" if shadow
206
279
  Digest::SHA256.hexdigest(parts.join("\x00"))
207
280
  rescue StandardError
208
281
  # {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
@@ -214,10 +287,29 @@ module Rigor
214
287
  end
215
288
 
216
289
  def self.digest_file_if_present(path)
217
- File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
290
+ path = path&.to_s
291
+ path && File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
218
292
  end
219
293
  private_class_method :digest_file_if_present
220
294
 
295
+ # Content-digest every `pre_eval:` file in configuration order, so an edit to one invalidates the snapshot
296
+ # even when it lies outside the analysed paths. A missing file digests as "absent", so creating it counts too.
297
+ def self.digest_pre_eval(paths)
298
+ digest = Digest::SHA256.new
299
+ Array(paths).each do |path|
300
+ digest << path.to_s << "\0" << digest_file_if_present(path.to_s) << "\0"
301
+ end
302
+ digest.hexdigest
303
+ end
304
+ private_class_method :digest_pre_eval
305
+
306
+ # The `sig/` roots the environment loads for this configuration: the configured `signature_paths`, or the
307
+ # auto-detected `<root>/sig` when those are nil ({Environment.project_signature_roots}).
308
+ def self.project_sig_roots(configuration)
309
+ Environment.project_signature_roots(configuration.signature_paths).map(&:to_s)
310
+ end
311
+ private_class_method :project_sig_roots
312
+
221
313
  # Content-digest every `.rbs` under the configured signature paths (sorted for determinism) so a project
222
314
  # RBS edit invalidates the snapshot. Sig trees are small; content (not mtime) keeps it stable across
223
315
  # checkouts.
@@ -319,7 +411,9 @@ module Rigor
319
411
  effects_identity: payload.effects_identity,
320
412
  run_level_rows: payload.run_level_rows
321
413
  )
322
- blob = Zlib::Deflate.deflate(raw)
414
+ # #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
415
+ # on the edit path; see {Store::DEFLATE_LEVEL}.
416
+ blob = Zlib::Deflate.deflate(raw, Store::DEFLATE_LEVEL)
323
417
  tmp = "#{@path}.#{Process.pid}.tmp"
324
418
  File.binwrite(tmp, blob)
325
419
  File.rename(tmp, @path)
@@ -29,6 +29,10 @@ module Rigor
29
29
  # files, same libraries), so the loader memoises one build per process instead of re-digesting every
30
30
  # .rbs file once per producer.
31
31
  descriptor = loader.rbs_cache_descriptor
32
+ # Issue #1014 — nil is an engine the descriptor could not identify ({EngineSource::Unavailable}).
33
+ # Compute uncached rather than keying the value without the engine that produced it.
34
+ return compute(loader) if descriptor.nil?
35
+
32
36
  store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor,
33
37
  generation_cap: generation_cap) do
34
38
  compute(loader)
@@ -3,20 +3,47 @@
3
3
  require "digest"
4
4
 
5
5
  require_relative "descriptor"
6
+ require_relative "engine_source"
6
7
  require_relative "file_digest"
7
8
 
8
9
  module Rigor
9
10
  module Cache
10
11
  # Shared descriptor builder for cache producers that depend on the RBS environment (constant table,
11
- # known-class set, future Marshal-clean reflection artefacts). Every consumer attaches the same three
12
- # slots, so factoring the construction here keeps the producers small and ensures invalidation behaves
13
- # identically across them.
12
+ # known-class set, future Marshal-clean reflection artefacts). Every consumer attaches the same slots —
13
+ # the `rbs` gem row, a digest row per signature file, and the `rbs.libraries` / `rbs.virtual_rbs` /
14
+ # `rbs.deferred_signature_paths` / `engine-source` config rows — so factoring the construction here keeps
15
+ # the producers small and ensures invalidation behaves identically across them.
14
16
  module RbsDescriptor
17
+ # The key every `rbs.*` producer shares ({RbsCacheProducer#fetch}): `rbs.environment`,
18
+ # `rbs.constant_type_table`, `rbs.class_ancestor_table`, `rbs.known_class_names`,
19
+ # `rbs.class_type_param_names`.
20
+ #
21
+ # Issue #1014 — the engine-source row is here, not in {.config_entries}, because these are
22
+ # computed-value slots: what they hold is not the RBS the descriptor digests but what Rigor's own code
23
+ # made of it. `rbs.constant_type_table` stores `Inference::RbsTypeTranslator.translate` output and
24
+ # `rbs.class_ancestor_table` the chains `RbsClassAncestorTable.compute` walks out of
25
+ # `instance_definition`, so an engine edit to either moved the value while the key stood still: the
26
+ # run-result key moved (#285) and the run re-analysed, then read the previous build's table. Both were
27
+ # reproduced cross-build on a fixture — a true positive dropped, and a `flow.unreachable-clause` false
28
+ # positive served — before this row was added.
29
+ #
30
+ # {.config_entries} deliberately does NOT get it: that half is what the ADR-45 run cache key reads
31
+ # ({.build_run}), and `RunCacheKey` already contributes the identical row from
32
+ # `EngineSource.key_config_entries` itself. Adding it there would duplicate the row in the run key and
33
+ # would have to be reconstructible by the boot-slimming probe, which builds `config_entries` without a
34
+ # loader.
35
+ #
36
+ # @raise EngineSource::Unavailable — an engine that cannot be identified must not be keyed by its
37
+ # inputs alone; {RbsCacheProducer.fetch} turns this into an uncached compute.
15
38
  def self.build(loader)
39
+ # FIRST, before {.file_entries} digests the whole signature tree: the {EngineSource::Unavailable}
40
+ # path discards this descriptor entirely, and a SHA-256 of every `.rbs` under every signature root is
41
+ # the expensive way to reach that conclusion.
42
+ engine = EngineSource.key_config_entries
16
43
  Descriptor.new(
17
44
  gems: [rbs_gem_entry],
18
45
  files: file_entries(loader),
19
- configs: config_entries(loader) + env_only_config_entries(loader)
46
+ configs: config_entries(loader) + env_only_config_entries(loader) + engine
20
47
  )
21
48
  end
22
49
 
@@ -25,8 +52,10 @@ module Rigor
25
52
  # MISS, by the dependency descriptor ({Runner#run_dependency_descriptor}). So a warm HIT never digests
26
53
  # the (large, vendored) RBS tree. {RunDescriptor} is NOT a {Descriptor} — it is never composed, hashed,
27
54
  # or `==`'d, only its four readers are consulted — so deferring `files` costs no soundness, and `gems`
28
- # / `configs` match {.build}'s shared slots (the key is unchanged; the one slot the env key adds on
29
- # top, {.env_only_config_entries}, is never read here).
55
+ # / `configs` match {.build}'s shared slots (the key is unchanged; the slots the env key adds on top,
56
+ # {.env_only_config_entries} and the #1014 `engine-source` row, are never read here — `RunCacheKey`
57
+ # contributes the identical engine-source row itself, from the same
58
+ # `EngineSource.key_config_entries`).
30
59
  def self.build_run(loader)
31
60
  RunDescriptor.new(loader: loader, gems: [rbs_gem_entry], configs: config_entries(loader))
32
61
  end
@@ -112,7 +141,8 @@ module Rigor
112
141
  # overlays. Rigor's own `data/` trees ({RbsLoader.vendored_gem_sig_paths}, `.core_overlay_sig_paths`,
113
142
  # `.capability_role_sig_paths`) are engine-owned: a file appears under one only when the engine tree is
114
143
  # edited, which normally arrives with the `lib/` change that reads it and so re-keys every
115
- # computed-value slot through {EngineSource}. They get file rows and no glob — a glob there would cost
144
+ # computed-value slot through {EngineSource} — including, since #1014, this descriptor itself, which
145
+ # until then carried no such row. They get file rows and no glob — a glob there would cost
116
146
  # every project a stat walk of the vendored gem-signature tree per warm run to catch an
117
147
  # engine-development edit.
118
148
  def self.glob_entries(loader)
@@ -25,7 +25,8 @@ module Rigor
25
25
  # On-disk byte-layout version. Bumped on incompatible format changes (independent of
26
26
  # {Descriptor::SCHEMA_VERSION}, which covers the descriptor schema rather than the byte layout). v2
27
27
  # (ADR-54 WD2): the value payload is zlib-deflated on write and inflated on read — Marshal blobs
28
- # compress to 13–16 % at an inflate cost an order of magnitude below their `Marshal.load`. v1 entries
28
+ # compressed to 13–16 % at zlib's default level (now {DEFLATE_LEVEL}) at an inflate cost an order of
29
+ # magnitude below their `Marshal.load`. v1 entries
29
30
  # fail the header check and read as silent misses; the `schema_version.txt` marker additionally carries
30
31
  # this version, so the first writable run after a bump clears the root and reclaims the unreadable
31
32
  # bytes.
@@ -60,12 +61,19 @@ module Rigor
60
61
 
61
62
  STALE_TEMP_FILE_AGE_SECONDS = 60 * 60
62
63
 
64
+ # #1507 — the zlib level for everything written under the cache root: these value payloads (ADR-54 WD2),
65
+ # the incremental snapshot, and `rigor unused`'s scan bundle. The snapshot and the scan bundle are
66
+ # rewritten whole whenever a run changed anything, so their deflate is on the edit path. On Mastodon the
67
+ # default level took 122 ms for the snapshot (9.5 MB raw) and 131 ms for `rbs.environment` (22.8 MB raw);
68
+ # `BEST_SPEED` takes 38 ms and 49 ms, for a cache about a quarter larger. Inflate reads any level, so the
69
+ # level is not part of the format.
70
+ DEFLATE_LEVEL = Zlib::BEST_SPEED
71
+
63
72
  # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte format version.
64
73
  HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
65
74
 
66
75
  VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
67
76
 
68
- # @param root — cache root directory.
69
77
  # @param read_only — when true, every disk-side side-effect is suppressed: `fetch_or_compute`
70
78
  # still reads existing entries (hits, gated on a current `schema_version.txt` marker — see
71
79
  # {#ensure_schema_version!}) and still runs the producer block on miss, but it does NOT write the
@@ -242,8 +250,13 @@ module Rigor
242
250
  # validation always re-checks the filesystem — but a single run only looks up once.
243
251
  #
244
252
  # `generation_cap:` carries the same producer-declared compaction budget as {#fetch_or_compute}.
253
+ #
254
+ # `on_hit:` is called with the entry's stored dependency descriptor on a fresh hit, before the value
255
+ # is returned, and never on a miss. A caller whose own dependency record must carry a served value's
256
+ # inputs replays them from it (#1558, {Plugin::Base#cache_for}): a hit runs no block, so nothing else
257
+ # reports what the value was computed from.
245
258
  def fetch_or_validate(producer_id:, key_descriptor:, generation_cap:, params: {},
246
- serialize: nil, deserialize: nil)
259
+ serialize: nil, deserialize: nil, on_hit: nil)
247
260
  validate_producer_id!(producer_id)
248
261
  declare_generation_cap(producer_id, generation_cap)
249
262
  disk = ensure_schema_version!
@@ -253,6 +266,7 @@ module Rigor
253
266
  cached = path && read_entry(path, deserialize: deserialize)
254
267
  if (validated = fresh_pair_value(cached))
255
268
  @monitor.synchronize { record(:hits, producer_id) }
269
+ on_hit&.call(validated[1])
256
270
  return validated[0]
257
271
  end
258
272
 
@@ -286,6 +300,51 @@ module Rigor
286
300
  validated[0]
287
301
  end
288
302
 
303
+ # ADR-45 WD2 (#1507) — a record-and-validate entry's value WITHOUT validating its dependencies. The
304
+ # `--incremental` session reads the previous run-result slot to carry forward the rows a narrowed run
305
+ # cannot re-derive, and that slot is expected to be stale: the edit that started this run is in it. So
306
+ # the answer here is never served — only rows are taken from it, and they are validated again, with
307
+ # everything else, the next time the slot is read. nil on a miss, a malformed entry or an unavailable
308
+ # disk tier. Records neither a hit nor a miss, since nothing was looked up to be served.
309
+ def peek_unvalidated(producer_id:, key_descriptor:, params: {})
310
+ validate_producer_id!(producer_id)
311
+ return nil unless ensure_schema_version!
312
+
313
+ key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
314
+ pair = read_entry(entry_path(producer_id, key))&.value
315
+ return nil unless pair.is_a?(Array) && pair.size == 2 && pair[1].is_a?(Descriptor)
316
+
317
+ pair[0]
318
+ end
319
+
320
+ # ADR-45 WD2 (#1507) — the write half of {#fetch_or_validate} on its own, and unconditional. That method
321
+ # writes only after it failed to validate what is there; a writer that has just computed an answer of its
322
+ # own must store THAT answer even when the entry already on disk still validates, because it is the one
323
+ # the run printed. Same entry format and the same failure contract as {#fetch_or_validate}'s write: a
324
+ # disk-side failure is swallowed, a value `Marshal` cannot dump raises. Returns whether it wrote.
325
+ def store_validated(producer_id:, key_descriptor:, generation_cap:, value:, dependencies:, params: {})
326
+ validate_producer_id!(producer_id)
327
+ declare_generation_cap(producer_id, generation_cap)
328
+ return false unless ensure_schema_version!
329
+
330
+ key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
331
+ wrote = try_write_entry(entry_path(producer_id, key), key_descriptor, [value, dependencies])
332
+ @monitor.synchronize { record(:writes, producer_id) } if wrote
333
+ wrote
334
+ end
335
+
336
+ # ADR-45 WD2 (#1507) — removes one entry, for a writer that has just superseded it under another key and
337
+ # knows nothing will ask for it again. Best-effort like every other unlink here; a no-op on a read-only
338
+ # store.
339
+ def discard(producer_id:, key_descriptor:, params: {})
340
+ validate_producer_id!(producer_id)
341
+ return if @read_only || !ensure_schema_version!
342
+
343
+ path = entry_path(producer_id, key_descriptor.cache_key_for(producer_id: producer_id, params: params))
344
+ unlink_entry_and_shard?(path) if File.file?(path)
345
+ nil
346
+ end
347
+
289
348
  # ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only.
290
349
  #
291
350
  # The generation cap of pass 2 comes from what the producers themselves declared through this Store's
@@ -470,7 +529,7 @@ module Rigor
470
529
  FileUtils.mkdir_p(File.dirname(path))
471
530
 
472
531
  descriptor_bytes = descriptor.to_canonical_bytes
473
- value_bytes = Zlib::Deflate.deflate(serialize_value(value, serialize))
532
+ value_bytes = Zlib::Deflate.deflate(serialize_value(value, serialize), DEFLATE_LEVEL)
474
533
 
475
534
  body = +"".b
476
535
  body << HEADER