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
@@ -0,0 +1,325 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Rigor
6
+ module Cache
7
+ # Issue #1574 — a digest of a Ruby VALUE, not of its object graph. The same value digests to the same hex
8
+ # string whether a producer computed it in this process or a cache entry served it from disk, whatever
9
+ # Strings, Arrays or Hashes happen to share one object.
10
+ #
11
+ # `Marshal.dump` is not that function. It writes an object reached twice once and back-references it after,
12
+ # and a Marshal round trip does not keep that sharing: `Marshal.load` rebuilds a String unfrozen, and
13
+ # `Hash#[]=` then stores a frozen copy of it as the key rather than the String itself. A frozen String used
14
+ # both as a row's field and as the key of an index Hash (rigor-sidekiq's `worker_index`, rigor-rails-i18n's
15
+ # `locale_index`) therefore dumps to one set of bytes when computed and another when served, and a digest of
16
+ # those bytes flipped the ADR-88 fact-surface fingerprint on every run that switched between the two.
17
+ #
18
+ # The encoding below walks the value as a tree. Each node is written as a type tag plus its content, and
19
+ # sizes are length-prefixed, so the encoding is unambiguous and two values encode alike only when they are
20
+ # alike. Nothing in it reads identity or frozenness. An object (a Data, a Struct, or any other non-core
21
+ # object) is always written as its class name and the SHA-256 of its own encoding. That digest is taken once
22
+ # per object and reused wherever the object is reached again, so a row shared between an index's list and
23
+ # its by-name Hash is walked once, and the encoding is the same whether an object is shared or not.
24
+ #
25
+ # What it keeps, so a changed value still changes the digest:
26
+ #
27
+ # - every class distinction Marshal keeps: Integer vs Float, String vs Symbol, a subclass of String / Array /
28
+ # Hash / Set vs the base class, and the class name of every Data, Struct and plain object;
29
+ # - Hash and Set iteration ORDER. Two Hashes equal under `==` but built in another order digest differently.
30
+ # A consumer may iterate a fact in order (the first matching row wins), and Marshal keeps the order, so the
31
+ # computed and served forms agree on it; digesting the order is the direction that can only over-invalidate;
32
+ # - a Hash's `default` value, the `compare_by_identity` flag of a Hash or Set, a Range's `exclude_end?`, a
33
+ # Regexp's options, and a non-ASCII String's encoding. ASCII-only Strings digest alike in every encoding, as
34
+ # `==` has it.
35
+ #
36
+ # What it leaves out, although Marshal keeps it:
37
+ #
38
+ # - instance variables set on a String, Array, Hash or Set, which `==` ignores too;
39
+ # - modules an object was `extend`ed with (Marshal refuses singleton methods, and this does not look);
40
+ # - the hidden fields a core exception subclass keeps beside its message, backtrace and cause
41
+ # (`NameError#name`, `SystemCallError#errno`), which reach the digest only through the message, and the
42
+ # hidden fields any other C-implemented class keeps outside `instance_variables`.
43
+ #
44
+ # A plain object digests as its instance variables, sorted by name; a Data or Struct as its members and its
45
+ # instance variables; an Exception as its message, backtrace, cause and instance variables. Each is read
46
+ # through the core class's own method, so a subclass that overrides `to_h`, `each_pair`, `message` or
47
+ # `backtrace` cannot hide a field. An object that defines `marshal_dump` or `_dump` digests as what that
48
+ # returns, which is what Marshal keeps of it.
49
+ #
50
+ # A value that cannot be digested this way raises {Uncanonicalisable}: an object whose state lives outside
51
+ # its instance variables (a Proc, an IO, a Mutex: what `Marshal.dump` refuses), a Hash with a default proc, an
52
+ # anonymous class, or a structure nested more than {MAX_DEPTH} deep, which is what a cycle becomes. A caller
53
+ # must treat that as "unknown", never as "unchanged".
54
+ module ValueDigest
55
+ # The value has no canonical encoding. Fail closed: treat the value as moved.
56
+ class Uncanonicalisable < StandardError; end
57
+
58
+ # Containers nested deeper than this are refused. No fact is this deep; a cyclic structure is infinitely
59
+ # deep, so this is also the cycle check, and it fires well before the Ruby stack would.
60
+ MAX_DEPTH = 256
61
+
62
+ module_function
63
+
64
+ # The hex SHA-256 of `value`'s canonical encoding.
65
+ def hexdigest(value)
66
+ Encoder.new.hexdigest(value)
67
+ rescue SystemStackError
68
+ raise Uncanonicalisable, "#{value.class} is nested too deeply to digest"
69
+ end
70
+
71
+ # One walk of one value. The canonical bytes are fed to the SHA-256 in chunks, so a multi-megabyte fact
72
+ # never has its whole encoding in memory at once.
73
+ class Encoder
74
+ FLUSH_AT = 64 * 1024
75
+ private_constant :FLUSH_AT
76
+
77
+ def initialize
78
+ @sha = Digest::SHA256.new
79
+ @out = String.new(capacity: FLUSH_AT * 2, encoding: Encoding::BINARY)
80
+ @depth = 0
81
+ @objects = {}.compare_by_identity # object => the SHA-256 of its encoding
82
+ @symbols = {} # Symbol => its encoding
83
+ @plain = {}.compare_by_identity # class => whether Marshal writes its instances as their ivars
84
+ # The core readers, taken unbound so an override on the value's class cannot change what is read. Held
85
+ # per walk rather than in constants, which must stay Ractor-shareable (#1064).
86
+ @data_to_h = Data.instance_method(:to_h)
87
+ @struct_each_pair = Struct.instance_method(:each_pair)
88
+ @exception_readers = %i[to_s backtrace cause].map { |name| Exception.instance_method(name) }
89
+ @instance_variables = Kernel.instance_method(:instance_variables)
90
+ @instance_variable_get = Kernel.instance_method(:instance_variable_get)
91
+ end
92
+
93
+ def hexdigest(value)
94
+ write(value)
95
+ @sha.update(@out)
96
+ @sha.hexdigest
97
+ end
98
+
99
+ private
100
+
101
+ def write(value)
102
+ case value
103
+ when String then write_string(value)
104
+ when Symbol then @out.append_as_bytes(@symbols[value] ||= encoded("y", value.name))
105
+ when Hash then enter { write_hash(value) }
106
+ when Array then enter { write_array(value) }
107
+ when Set then enter { write_set(value) }
108
+ when Integer then @out.append_as_bytes("i", value.to_s, ";")
109
+ when nil then @out.append_as_bytes("n")
110
+ when true then @out.append_as_bytes("t")
111
+ when false then @out.append_as_bytes("f")
112
+ else write_other(value)
113
+ end
114
+ end
115
+
116
+ def write_other(value)
117
+ case value
118
+ when Float then @out.append_as_bytes("d", value.to_s, ";")
119
+ when Range then enter { write_range(value) }
120
+ when Regexp then write_regexp(value)
121
+ when Rational then @out.append_as_bytes("q", value.numerator.to_s, "/", value.denominator.to_s, ";")
122
+ when Complex then enter { write_pair("c", value.real, value.imaginary) }
123
+ when Module then write_bytes(value.is_a?(Class) ? "k" : "m", class_name(value))
124
+ else write_object(value)
125
+ end
126
+ end
127
+
128
+ # The common case, an ASCII-only plain String, is written inline: it is most of the nodes of a large fact.
129
+ def write_string(value)
130
+ if value.ascii_only? && value.instance_of?(String)
131
+ @out.append_as_bytes("s:", value.bytesize.to_s, ":", value)
132
+ else
133
+ write_subclass(value, String)
134
+ write_bytes("s", value)
135
+ end
136
+ flush if @out.bytesize >= FLUSH_AT
137
+ end
138
+
139
+ # A tag and a String's bytes, length-prefixed. The encoding is written only for a String with non-ASCII
140
+ # bytes: ASCII-only Strings compare equal whatever their encoding, and one built from a Symbol is
141
+ # US-ASCII.
142
+ def write_bytes(tag, string)
143
+ @out.append_as_bytes(encoded(tag, string))
144
+ end
145
+
146
+ def encoded(tag, string)
147
+ encoding = string.ascii_only? ? "" : string.encoding.name
148
+ "#{tag}#{encoding}:#{string.bytesize}:".b.append_as_bytes(string)
149
+ end
150
+
151
+ def write_hash(value)
152
+ raise Uncanonicalisable, "a Hash with a default proc" if value.default_proc
153
+
154
+ write_subclass(value, Hash)
155
+ default = value.default
156
+ @out.append_as_bytes("h", value.compare_by_identity? ? "I" : "", default.nil? ? "" : "D",
157
+ value.size.to_s, ":")
158
+ write(default) unless default.nil?
159
+ value.each_pair do |key, entry|
160
+ write(key)
161
+ write(entry)
162
+ end
163
+ end
164
+
165
+ def write_array(value)
166
+ write_subclass(value, Array)
167
+ @out.append_as_bytes("a", value.size.to_s, ":")
168
+ value.each { |element| write(element) }
169
+ end
170
+
171
+ def write_set(value)
172
+ write_subclass(value, Set)
173
+ @out.append_as_bytes("e", value.compare_by_identity? ? "I" : "", value.size.to_s, ":")
174
+ value.each { |element| write(element) }
175
+ end
176
+
177
+ def write_range(value)
178
+ write_pair(value.exclude_end? ? "rx" : "ri", value.begin, value.end)
179
+ end
180
+
181
+ def write_pair(tag, first, second)
182
+ @out.append_as_bytes(tag)
183
+ write(first)
184
+ write(second)
185
+ end
186
+
187
+ def write_regexp(value)
188
+ @out.append_as_bytes("g", value.options.to_s, ";")
189
+ write_bytes("s", value.source)
190
+ end
191
+
192
+ # Every object that is not a core value: its class name and the digest of its content, taken once per
193
+ # object however often the value reaches it.
194
+ def write_object(value)
195
+ digest = @objects[value] ||= enter { object_digest(value) }
196
+ write_bytes("o", class_name(value.class))
197
+ @out.append_as_bytes(digest)
198
+ end
199
+
200
+ # The object's content, walked into a SHA-256 of its own. A `marshal_dump` / `_dump` hook decides what
201
+ # Marshal keeps of an object, so it decides the digest too; otherwise the object is its members (a Data or
202
+ # Struct) or its message, backtrace and cause (an Exception), then its instance variables.
203
+ def object_digest(value)
204
+ outer_sha = @sha
205
+ outer_out = @out
206
+ @sha = Digest::SHA256.new
207
+ @out = +"".b
208
+ write_content(value)
209
+ @sha.update(@out)
210
+ @sha.digest
211
+ ensure
212
+ @sha = outer_sha
213
+ @out = outer_out
214
+ end
215
+
216
+ def write_content(value)
217
+ if value.respond_to?(:marshal_dump, true)
218
+ @out.append_as_bytes("U")
219
+ write(value.send(:marshal_dump))
220
+ elsif value.is_a?(Time)
221
+ # `Time#_dump` keeps the offset and zone as instance variables on the String it returns, which the
222
+ # String encoding leaves out.
223
+ @out.append_as_bytes("z")
224
+ [value.to_r, value.utc_offset, value.zone].each { |part| write(part) }
225
+ elsif value.respond_to?(:_dump, true)
226
+ @out.append_as_bytes("u")
227
+ write(value.send(:_dump, -1))
228
+ else
229
+ write_members(value)
230
+ end
231
+ end
232
+
233
+ def write_members(value)
234
+ case value
235
+ when Data then write_fields("D", @data_to_h.bind_call(value))
236
+ when Struct then write_fields("T", struct_members(value))
237
+ when Exception then write_exception(value)
238
+ else
239
+ assert_plain(value)
240
+ @out.append_as_bytes("V")
241
+ end
242
+ write_fields("@", instance_variables(value))
243
+ end
244
+
245
+ def struct_members(value)
246
+ members = []
247
+ @struct_each_pair.bind_call(value) { |name, field| members << [name, field] }
248
+ members
249
+ end
250
+
251
+ # Marshal keeps an exception's message, backtrace and cause in hidden instance variables, which
252
+ # `instance_variables` does not list: two exceptions that differ only in their message would otherwise
253
+ # digest alike. A plugin that records parse errors as exceptions depends on the message moving the digest.
254
+ def write_exception(value)
255
+ @out.append_as_bytes("X")
256
+ @exception_readers.each { |reader| write(reader.bind_call(value)) }
257
+ end
258
+
259
+ def instance_variables(value)
260
+ @instance_variables.bind_call(value).sort.map { |name| [name, @instance_variable_get.bind_call(value, name)] }
261
+ end
262
+
263
+ def write_fields(tag, pairs)
264
+ fields = pairs.to_a
265
+ @out.append_as_bytes(tag, fields.size.to_s, ":")
266
+ fields.each do |name, field|
267
+ write(name.to_sym)
268
+ write(field)
269
+ end
270
+ end
271
+
272
+ # An object's instance variables are all of its state only when Marshal writes it that way. `Marshal.dump`
273
+ # refuses a Proc, an IO, a Mutex or a Thread (C-level state and no `_dump_data`), and so does this. The
274
+ # depth limit of 1 makes the check cost one object: Marshal decides whether it can write the object
275
+ # before it writes any instance variable, and raises `ArgumentError` on reaching the first one. It is
276
+ # asked once per class, since whether a class keeps state outside Ruby does not vary by instance.
277
+ #
278
+ # The `ArgumentError` cannot tell a listed instance variable from a hidden one that a C-implemented class
279
+ # keeps, so an Exception, whose message and backtrace are hidden, is read explicitly above instead.
280
+ def assert_plain(value)
281
+ klass = value.class
282
+ plain = @plain.fetch(klass) do
283
+ @plain[klass] = begin
284
+ Marshal.dump(value, 1)
285
+ true
286
+ rescue ArgumentError # "exceed depth limit": the object itself was written
287
+ true
288
+ rescue TypeError
289
+ false
290
+ end
291
+ end
292
+ raise Uncanonicalisable, "#{klass} keeps its state outside its instance variables" unless plain
293
+ end
294
+
295
+ # A subclass of a core value keeps its class through Marshal, so the class is part of the value.
296
+ def write_subclass(value, base)
297
+ klass = value.class
298
+ write_bytes("C", class_name(klass)) unless klass.equal?(base)
299
+ end
300
+
301
+ def class_name(klass)
302
+ klass.name || raise(Uncanonicalisable, "an anonymous #{klass.class.name.downcase} has no name to digest")
303
+ end
304
+
305
+ def enter
306
+ raise Uncanonicalisable, "a value nested deeper than #{MAX_DEPTH} (a cycle?)" if @depth >= MAX_DEPTH
307
+
308
+ @depth += 1
309
+ begin
310
+ result = yield
311
+ ensure
312
+ @depth -= 1
313
+ end
314
+ flush if @out.bytesize >= FLUSH_AT
315
+ result
316
+ end
317
+
318
+ def flush
319
+ @sha.update(@out)
320
+ @out.clear
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
@@ -61,6 +61,7 @@ module Rigor
61
61
  { id: "ci", name: "CI", format: nil, tier: :reviewdog,
62
62
  var: "CI", match: :truthy }
63
63
  ].freeze
64
+ Ractor.make_shareable(PROVIDERS)
64
65
 
65
66
  module_function
66
67
 
@@ -59,8 +59,8 @@ module Rigor
59
59
  # ADR-87 WD4 — try to serve the whole run from the ADR-45 cache before booting the engine. A hit boots
60
60
  # only CLI + config + cache + digest code (no `rigor/inference`), skipping the plugin prepass + env
61
61
  # build entirely.
62
- probed = try_run_cache_hit(configuration, options, buffer, cache_root)
63
- return finalize_cache_hit(probed, configuration, options, config_warnings) unless probed.nil?
62
+ served = serve_from_run_cache(configuration, options, buffer, cache_root, config_warnings)
63
+ return served unless served.nil?
64
64
 
65
65
  load_check_dependencies
66
66
  special = dispatch_special_check_mode(configuration, options, cache_root, buffer)
@@ -113,6 +113,16 @@ module Rigor
113
113
  false
114
114
  end
115
115
 
116
+ # The exit code of a run served whole from a run-result slot, engine-free, or nil to run the engine: the
117
+ # plain slot for an ordinary check (ADR-87 WD4), the incremental session's for `--incremental` (ADR-45 WD2).
118
+ def serve_from_run_cache(configuration, options, buffer, cache_root, config_warnings)
119
+ probed = try_run_cache_hit(configuration, options, buffer, cache_root)
120
+ return finalize_cache_hit(probed, configuration, options, config_warnings) unless probed.nil?
121
+
122
+ hit = try_incremental_run_hit(configuration, options, buffer, cache_root)
123
+ hit && finalize_incremental_hit(hit, configuration, options)
124
+ end
125
+
116
126
  # ADR-87 WD4 — attempt the boot-slimming run-cache hit. Returns the cached {Analysis::Result} (severity
117
127
  # profile applied, no stats — matching a cache-served `Runner#run`) on a hit, or nil to fall through to
118
128
  # the full engine path. Only an ordinary sequential check whose result IS cache-served the same way is
@@ -141,8 +151,9 @@ module Rigor
141
151
 
142
152
  # Any signal that per-file analysis would run in a worker pool (CLI `--workers`, the env override, or the
143
153
  # config default). Deliberately over-declines (a false positive only forgoes the fast lane); mirrors
144
- # {CheckRunnerFactory.resolve_workers} without requiring it (that pulls in the engine).
145
- def pool_workers_configured?(options, configuration)
154
+ # {CheckRunnerFactory.resolve_workers} without requiring it (that pulls in the engine). `unreadable` is the
155
+ # answer for a count that does not parse, which the full path's resolver raises on.
156
+ def pool_workers_configured?(options, configuration, unreadable: false)
146
157
  cli = options[:workers]
147
158
  return Integer(cli).positive? if cli
148
159
 
@@ -151,7 +162,41 @@ module Rigor
151
162
 
152
163
  configuration.parallel_workers.positive?
153
164
  rescue ArgumentError
154
- false
165
+ unreadable
166
+ end
167
+
168
+ # ADR-45 WD2 (#1507) — `rigor check --incremental`'s engine-free null run: serve the run-result slot the
169
+ # incremental session wrote after its last run, when every row of it still validates. Returns an
170
+ # {Analysis::IncrementalRunSlot::Hit}, or nil to run the full incremental path.
171
+ def try_incremental_run_hit(configuration, options, buffer, cache_root)
172
+ return nil unless incremental_run_hit_eligible?(configuration, options, buffer)
173
+
174
+ require_relative "../analysis/incremental_run_slot"
175
+ Analysis::IncrementalRunSlot.serve(
176
+ configuration: configuration, cache_root: cache_root, paths: @argv.empty? ? configuration.paths : @argv
177
+ )
178
+ end
179
+
180
+ # A plain `--incremental` run the session would answer as a warm recheck and write a slot for. Every other
181
+ # shape declines, and the full path handles it exactly as before: an editor buffer (never saved), `--no-cache`
182
+ # (no store to hold a slot), `--verify-incremental`, a worker pool (the session writes no slot for one: a
183
+ # worker's plugin reads never reach the process that records them), and `--explain`, which the session
184
+ # ignores today (#1533) — the slot is keyed without it, so the probe declines rather than answer for a flag
185
+ # its writer never saw. The rest mirror the plain probe: `--coverage`, `--cache-stats` and the
186
+ # `RIGOR_*_TRACE` probes each want the full path's own output.
187
+ def incremental_run_hit_eligible?(configuration, options, buffer)
188
+ options.fetch(:incremental) && buffer.nil? &&
189
+ !options.fetch(:no_cache) && !options.fetch(:explain) && !options.fetch(:coverage) &&
190
+ !options.fetch(:cache_stats) && !options.fetch(:verify_incremental) &&
191
+ !pool_workers_configured?(options, configuration, unreadable: true) &&
192
+ ENV["RIGOR_BUDGET_TRACE"].to_s.empty? && ENV["RIGOR_HEAP_TRACE"].to_s.empty?
193
+ end
194
+
195
+ # The tail {#run_incremental_check} gives a warm run, from the slot's answer: the same banner, baseline
196
+ # filter, output and exit code. No fact-surface note, since a warm run never has one.
197
+ def finalize_incremental_hit(hit, configuration, options)
198
+ report_incremental_run(true, hit.file_count)
199
+ write_incremental_result(hit.result.diagnostics, configuration, options)
155
200
  end
156
201
 
157
202
  # ADR-87 WD4 — the engine-free tail for a cache-served hit: baseline filter + output + exit code,
@@ -237,9 +282,15 @@ module Rigor
237
282
  def run_incremental_check(configuration, options, cache_root, buffer = nil)
238
283
  require_relative "check_runner_factory"
239
284
  paths = @argv.empty? ? nil : @argv
240
- fingerprint = Cache::IncrementalSnapshot.fingerprint(
241
- configuration: configuration, roots: paths || configuration.paths
242
- )
285
+ # `--no-cache` is a cold run that touches no persistent state: a nil fingerprint is the session's "uncomputable
286
+ # inputs" signal, so it neither loads the snapshot, nor saves it, nor writes the run-result slot.
287
+ fingerprint = if options.fetch(:no_cache)
288
+ nil
289
+ else
290
+ Cache::IncrementalSnapshot.fingerprint(
291
+ configuration: configuration, roots: paths || configuration.paths
292
+ )
293
+ end
243
294
  snapshot = Cache::IncrementalSnapshot.new(root: cache_root)
244
295
  store = incremental_cache_store(configuration, options, cache_root)
245
296
  session = Analysis::IncrementalSession.new(
@@ -258,12 +309,23 @@ module Rigor
258
309
  # The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
259
310
  # `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
260
311
  # built just to size the banner (recon §3 — the analysis tree was expanded three times per recheck).
261
- @err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
262
- "(#{session.analyzed_files.size} files)")
312
+ report_incremental_run(warm, session.analyzed_files.size)
263
313
  emit_incremental_fact_surface_notes(session)
264
314
  write_incremental_cache_stats(session, cache_root, store, options.fetch(:format)) if
265
315
  options.fetch(:cache_stats)
266
316
 
317
+ write_incremental_result(diagnostics, configuration, options)
318
+ end
319
+
320
+ def report_incremental_run(warm, file_count)
321
+ @err.puts("rigor: --incremental #{warm ? 'warm — reused cached diagnostics' : 'cold — full analysis'} " \
322
+ "(#{file_count} files)")
323
+ end
324
+
325
+ # The incremental tail, shared by the full path and the ADR-45 WD2 slot hit so a served run prints exactly
326
+ # what the full path would. It is not the ordinary check's tail — no `config_warnings` block in the JSON,
327
+ # no CI-detected annotations, no `--baseline-strict` verdict — which is why the hit cannot reuse that one.
328
+ def write_incremental_result(diagnostics, configuration, options)
267
329
  result = apply_baseline_filter(Analysis::Result.new(diagnostics: diagnostics, stats: nil), configuration,
268
330
  options)
269
331
  write_result(result, options.fetch(:format), fail_on: options.fetch(:fail_on))
@@ -282,8 +344,13 @@ module Rigor
282
344
  def run_editor_mode_option_b(session, snapshot, fingerprint, configuration, options)
283
345
  result = session.run_buffer_recheck(snapshot: snapshot, fingerprint: fingerprint)
284
346
  if result.nil?
285
- @err.puts("rigor: --incremental has no reusable snapshot for this project; analysing the buffer " \
286
- "alone (run `rigor check --incremental` once to enable whole-project editor mode).")
347
+ if options.fetch(:no_cache)
348
+ @err.puts("rigor: --no-cache skips the incremental snapshot, so the buffer is analysed on its own " \
349
+ "(no whole-project editor mode).")
350
+ else
351
+ @err.puts("rigor: --incremental has no reusable snapshot for this project; analysing the buffer " \
352
+ "alone (run `rigor check --incremental` once to enable whole-project editor mode).")
353
+ end
287
354
  return nil
288
355
  end
289
356
 
@@ -523,7 +590,10 @@ module Rigor
523
590
  options[:coverage] = true
524
591
  end
525
592
  opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
526
- opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
593
+ opts.on("--no-cache",
594
+ "Disable the persistent cache for this run (with --incremental: no snapshot is read or written)") do
595
+ options[:no_cache] = true
596
+ end
527
597
  opts.on("--[no-]stats",
528
598
  "Print run summary (files, classes, memory, wall time) to stderr (default: on)") do |value|
529
599
  options[:stats] = value
@@ -31,7 +31,7 @@ module Rigor
31
31
  private_constant :MARKDOWN_LINK
32
32
 
33
33
  # Where the gem's `docs/` tree sits, so a rendered path can be made repository-relative.
34
- GEM_ROOT = File.expand_path("../../..", __dir__)
34
+ GEM_ROOT = File.expand_path("../../..", __dir__).freeze
35
35
  private_constant :GEM_ROOT
36
36
 
37
37
  module_function
@@ -47,10 +47,10 @@ module Rigor
47
47
  USAGE
48
48
 
49
49
  # The bundled docs live at `<gem_root>/docs/`. From `lib/rigor/cli/docs_command.rb` that is three directories up.
50
- DOCS_ROOT = File.expand_path("../../../docs", __dir__)
51
- MANUAL_ROOT = File.join(DOCS_ROOT, "manual")
52
- HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook")
53
- LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt")
50
+ DOCS_ROOT = File.expand_path("../../../docs", __dir__).freeze
51
+ MANUAL_ROOT = File.join(DOCS_ROOT, "manual").freeze
52
+ HANDBOOK_ROOT = File.join(DOCS_ROOT, "handbook").freeze
53
+ LLMS_INDEX = File.join(DOCS_ROOT, "llms.txt").freeze
54
54
 
55
55
  # @return CLI exit status.
56
56
  def run
@@ -246,7 +246,7 @@ module Rigor
246
246
  def real_or_expanded(path)
247
247
  File.realpath(path)
248
248
  rescue StandardError
249
- File.expand_path(path)
249
+ File.absolute_path(path)
250
250
  end
251
251
 
252
252
  # ADR-25 carry-over — the plain `bundle install` layout. {Environment::BundleSigDiscovery} detects a
@@ -121,7 +121,7 @@ module Rigor
121
121
  # syntax: this is the argument a shell just tab-completed, and `rigor effects app/models` meaning
122
122
  # "that directory" is the only reading a reader would guess.
123
123
  def self.normalize_scope(scope)
124
- Array(scope).map { |path| File.expand_path(path.to_s.chomp("/")) }.freeze
124
+ Array(scope).map { |path| File.absolute_path(path.to_s.chomp("/")) }.freeze
125
125
  end
126
126
  private_class_method :normalize_scope
127
127
 
@@ -130,7 +130,7 @@ module Rigor
130
130
  return false if paths.nil? || paths.empty?
131
131
 
132
132
  paths.any? do |path|
133
- absolute = File.expand_path(path)
133
+ absolute = File.absolute_path(path)
134
134
  roots.any? { |root| absolute == root || absolute.start_with?("#{root}/") }
135
135
  end
136
136
  end
@@ -60,9 +60,9 @@ module Rigor
60
60
 
61
61
  # The bundled plugins/examples/source live at `<gem_root>/...`. From `lib/rigor/cli/plugin_command.rb` the gem
62
62
  # root is three directories up (matching SkillCommand::SKILLS_ROOT).
63
- GEM_ROOT = File.expand_path("../../..", __dir__)
64
- PLUGINS_ROOT = File.join(GEM_ROOT, "plugins")
65
- EXAMPLES_ROOT = File.join(GEM_ROOT, "examples")
63
+ GEM_ROOT = File.expand_path("../../..", __dir__).freeze
64
+ PLUGINS_ROOT = File.join(GEM_ROOT, "plugins").freeze
65
+ EXAMPLES_ROOT = File.join(GEM_ROOT, "examples").freeze
66
66
 
67
67
  # @return CLI exit status.
68
68
  def run
@@ -35,7 +35,6 @@ module Rigor
35
35
 
36
36
  VARIABLE_TOKENS = %i[INSTANCE_VARIABLE CLASS_VARIABLE GLOBAL_VARIABLE].freeze
37
37
 
38
- # @param source — Ruby source.
39
38
  # @return the source with ANSI colour escapes, or
40
39
  # the input unchanged when lexing surfaces an error.
41
40
  def colorize(source)