rigortype 0.2.7 → 0.2.9

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 (433) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/08-understanding-errors.md +3 -3
  8. data/docs/manual/01-installation.md +1 -0
  9. data/docs/manual/02-cli-reference.md +16 -7
  10. data/docs/manual/04-diagnostics.md +7 -4
  11. data/docs/manual/06-baseline.md +5 -2
  12. data/docs/manual/07-plugins.md +1 -1
  13. data/docs/manual/14-rails-quickstart.md +4 -2
  14. data/docs/manual/15-type-protection-coverage.md +27 -4
  15. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  16. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  17. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  18. data/lib/rigor/analysis/baseline.rb +56 -96
  19. data/lib/rigor/analysis/buffer_binding.rb +13 -20
  20. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +29 -48
  21. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +35 -54
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +23 -37
  23. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +16 -23
  24. data/lib/rigor/analysis/check_rules/rule_walk.rb +48 -71
  25. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +19 -28
  26. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +48 -67
  27. data/lib/rigor/analysis/check_rules.rb +174 -274
  28. data/lib/rigor/analysis/dependency_recorder.rb +26 -38
  29. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +13 -25
  30. data/lib/rigor/analysis/dependency_source_inference/builder.rb +17 -30
  31. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +15 -26
  32. data/lib/rigor/analysis/dependency_source_inference/index.rb +45 -76
  33. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +23 -40
  34. data/lib/rigor/analysis/dependency_source_inference/walker.rb +50 -83
  35. data/lib/rigor/analysis/dependency_source_inference.rb +6 -10
  36. data/lib/rigor/analysis/diagnostic.rb +48 -75
  37. data/lib/rigor/analysis/erb_template_detector.rb +13 -18
  38. data/lib/rigor/analysis/fact_store.rb +12 -22
  39. data/lib/rigor/analysis/incremental.rb +47 -63
  40. data/lib/rigor/analysis/incremental_session.rb +74 -100
  41. data/lib/rigor/analysis/project_scan.rb +12 -23
  42. data/lib/rigor/analysis/result.rb +3 -5
  43. data/lib/rigor/analysis/rule_catalog.rb +67 -95
  44. data/lib/rigor/analysis/run_stats.rb +38 -61
  45. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +101 -160
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +121 -188
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +62 -96
  48. data/lib/rigor/analysis/runner/run_snapshots.rb +12 -18
  49. data/lib/rigor/analysis/runner.rb +220 -333
  50. data/lib/rigor/analysis/self_call_resolution_recorder.rb +30 -43
  51. data/lib/rigor/analysis/worker_session.rb +64 -102
  52. data/lib/rigor/ast/type_node.rb +6 -8
  53. data/lib/rigor/ast.rb +6 -10
  54. data/lib/rigor/bleeding_edge.rb +27 -35
  55. data/lib/rigor/builtins/hkt_builtins.rb +7 -14
  56. data/lib/rigor/builtins/imported_refinements.rb +106 -149
  57. data/lib/rigor/builtins/predefined_constant_refinements.rb +51 -65
  58. data/lib/rigor/builtins/regex_refinement.rb +32 -43
  59. data/lib/rigor/builtins/static_return_refinements.rb +57 -81
  60. data/lib/rigor/cache/descriptor.rb +58 -94
  61. data/lib/rigor/cache/incremental_snapshot.rb +32 -48
  62. data/lib/rigor/cache/rbs_cache_producer.rb +10 -15
  63. data/lib/rigor/cache/rbs_class_ancestor_table.rb +10 -16
  64. data/lib/rigor/cache/rbs_class_type_param_names.rb +10 -16
  65. data/lib/rigor/cache/rbs_constant_table.rb +8 -13
  66. data/lib/rigor/cache/rbs_descriptor.rb +8 -13
  67. data/lib/rigor/cache/rbs_environment.rb +11 -19
  68. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +11 -19
  69. data/lib/rigor/cache/rbs_known_class_names.rb +8 -12
  70. data/lib/rigor/cache/store.rb +248 -186
  71. data/lib/rigor/cli/annotate_command.rb +51 -88
  72. data/lib/rigor/cli/baseline_command.rb +10 -17
  73. data/lib/rigor/cli/check_command.rb +121 -199
  74. data/lib/rigor/cli/check_runner_factory.rb +8 -14
  75. data/lib/rigor/cli/ci_detector.rb +9 -14
  76. data/lib/rigor/cli/command.rb +8 -14
  77. data/lib/rigor/cli/coverage_command.rb +62 -48
  78. data/lib/rigor/cli/coverage_mutation.rb +14 -19
  79. data/lib/rigor/cli/coverage_report.rb +0 -2
  80. data/lib/rigor/cli/coverage_scan.rb +7 -11
  81. data/lib/rigor/cli/diagnostic_formats.rb +46 -62
  82. data/lib/rigor/cli/diff_command.rb +10 -20
  83. data/lib/rigor/cli/docs_command.rb +26 -40
  84. data/lib/rigor/cli/doctor_command.rb +8 -9
  85. data/lib/rigor/cli/explain_command.rb +4 -7
  86. data/lib/rigor/cli/fused_protection_renderer.rb +4 -5
  87. data/lib/rigor/cli/fused_protection_report.rb +6 -8
  88. data/lib/rigor/cli/lsp_command.rb +11 -19
  89. data/lib/rigor/cli/mcp_command.rb +4 -6
  90. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -4
  91. data/lib/rigor/cli/mutation_protection_report.rb +6 -9
  92. data/lib/rigor/cli/options.rb +11 -19
  93. data/lib/rigor/cli/plugin_command.rb +18 -30
  94. data/lib/rigor/cli/plugins_command.rb +29 -51
  95. data/lib/rigor/cli/plugins_renderer.rb +12 -20
  96. data/lib/rigor/cli/prism_colorizer.rb +13 -19
  97. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  98. data/lib/rigor/cli/protection_renderer.rb +5 -7
  99. data/lib/rigor/cli/protection_report.rb +34 -18
  100. data/lib/rigor/cli/renderable.rb +4 -6
  101. data/lib/rigor/cli/show_bleedingedge_command.rb +5 -8
  102. data/lib/rigor/cli/sig_gen_command.rb +14 -26
  103. data/lib/rigor/cli/skill_command.rb +30 -47
  104. data/lib/rigor/cli/skill_describe.rb +40 -64
  105. data/lib/rigor/cli/trace_command.rb +9 -16
  106. data/lib/rigor/cli/trace_renderer.rb +35 -51
  107. data/lib/rigor/cli/triage_command.rb +6 -10
  108. data/lib/rigor/cli/triage_renderer.rb +1 -2
  109. data/lib/rigor/cli/type_of_command.rb +14 -23
  110. data/lib/rigor/cli/type_of_renderer.rb +3 -5
  111. data/lib/rigor/cli/type_scan_command.rb +6 -10
  112. data/lib/rigor/cli/type_scan_renderer.rb +3 -5
  113. data/lib/rigor/cli/type_scan_report.rb +2 -3
  114. data/lib/rigor/cli/upgrade_command.rb +2 -3
  115. data/lib/rigor/cli.rb +16 -28
  116. data/lib/rigor/config_audit.rb +35 -44
  117. data/lib/rigor/configuration/dependencies.rb +41 -70
  118. data/lib/rigor/configuration/severity_profile.rb +28 -42
  119. data/lib/rigor/configuration.rb +150 -241
  120. data/lib/rigor/environment/bundle_sig_discovery.rb +74 -118
  121. data/lib/rigor/environment/class_registry.rb +19 -26
  122. data/lib/rigor/environment/constant_type_cache_holder.rb +14 -21
  123. data/lib/rigor/environment/hkt_registry_holder.rb +6 -12
  124. data/lib/rigor/environment/lockfile_resolver.rb +31 -47
  125. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  126. data/lib/rigor/environment/rbs_collection_discovery.rb +39 -63
  127. data/lib/rigor/environment/rbs_coverage_report.rb +24 -42
  128. data/lib/rigor/environment/rbs_loader.rb +420 -518
  129. data/lib/rigor/environment/reflection.rb +28 -53
  130. data/lib/rigor/environment/reporters.rb +13 -24
  131. data/lib/rigor/environment.rb +225 -304
  132. data/lib/rigor/flow_contribution/conflict.rb +16 -27
  133. data/lib/rigor/flow_contribution/element.rb +7 -12
  134. data/lib/rigor/flow_contribution/fact.rb +33 -57
  135. data/lib/rigor/flow_contribution/merge_result.rb +6 -9
  136. data/lib/rigor/flow_contribution/merger.rb +32 -47
  137. data/lib/rigor/flow_contribution.rb +37 -55
  138. data/lib/rigor/inference/acceptance.rb +133 -219
  139. data/lib/rigor/inference/block_parameter_binder.rb +52 -82
  140. data/lib/rigor/inference/body_fixpoint.rb +22 -30
  141. data/lib/rigor/inference/budget_trace.rb +43 -63
  142. data/lib/rigor/inference/builtins/array_catalog.rb +4 -6
  143. data/lib/rigor/inference/builtins/comparable_catalog.rb +4 -6
  144. data/lib/rigor/inference/builtins/complex_catalog.rb +13 -19
  145. data/lib/rigor/inference/builtins/date_catalog.rb +38 -67
  146. data/lib/rigor/inference/builtins/encoding_catalog.rb +22 -36
  147. data/lib/rigor/inference/builtins/enumerable_catalog.rb +4 -6
  148. data/lib/rigor/inference/builtins/exception_catalog.rb +38 -57
  149. data/lib/rigor/inference/builtins/hash_catalog.rb +9 -14
  150. data/lib/rigor/inference/builtins/method_catalog.rb +29 -48
  151. data/lib/rigor/inference/builtins/numeric_catalog.rb +13 -18
  152. data/lib/rigor/inference/builtins/pathname_catalog.rb +8 -13
  153. data/lib/rigor/inference/builtins/proc_catalog.rb +38 -62
  154. data/lib/rigor/inference/builtins/random_catalog.rb +21 -32
  155. data/lib/rigor/inference/builtins/range_catalog.rb +12 -21
  156. data/lib/rigor/inference/builtins/rational_catalog.rb +9 -15
  157. data/lib/rigor/inference/builtins/re_catalog.rb +29 -48
  158. data/lib/rigor/inference/builtins/set_catalog.rb +18 -27
  159. data/lib/rigor/inference/builtins/string_catalog.rb +12 -19
  160. data/lib/rigor/inference/builtins/struct_catalog.rb +18 -30
  161. data/lib/rigor/inference/builtins/time_catalog.rb +28 -44
  162. data/lib/rigor/inference/closure_escape_analyzer.rb +44 -67
  163. data/lib/rigor/inference/coverage_scanner.rb +13 -18
  164. data/lib/rigor/inference/def_return_typer.rb +8 -14
  165. data/lib/rigor/inference/dynamic_origin.rb +23 -20
  166. data/lib/rigor/inference/expression_typer.rb +794 -1154
  167. data/lib/rigor/inference/fallback.rb +8 -12
  168. data/lib/rigor/inference/fallback_tracer.rb +4 -10
  169. data/lib/rigor/inference/flow_tracer.rb +25 -36
  170. data/lib/rigor/inference/fork_map.rb +87 -0
  171. data/lib/rigor/inference/hkt_body.rb +45 -68
  172. data/lib/rigor/inference/hkt_body_parser.rb +33 -52
  173. data/lib/rigor/inference/hkt_reducer.rb +38 -59
  174. data/lib/rigor/inference/hkt_registry.rb +50 -76
  175. data/lib/rigor/inference/indexed_narrowing.rb +55 -81
  176. data/lib/rigor/inference/macro_block_self_type.rb +21 -34
  177. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +11 -18
  178. data/lib/rigor/inference/method_dispatcher/block_folding.rb +49 -83
  179. data/lib/rigor/inference/method_dispatcher/call_context.rb +16 -23
  180. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +13 -22
  181. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +307 -454
  182. data/lib/rigor/inference/method_dispatcher/data_folding.rb +24 -34
  183. data/lib/rigor/inference/method_dispatcher/file_folding.rb +22 -37
  184. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +52 -81
  185. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +51 -78
  186. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +60 -97
  187. data/lib/rigor/inference/method_dispatcher/math_folding.rb +23 -34
  188. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +18 -26
  189. data/lib/rigor/inference/method_dispatcher/method_folding.rb +37 -71
  190. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +114 -182
  191. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +122 -193
  192. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +15 -24
  193. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +64 -93
  194. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +31 -45
  195. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -9
  196. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +356 -511
  197. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +22 -32
  198. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +15 -21
  199. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +66 -82
  200. data/lib/rigor/inference/method_dispatcher/time_folding.rb +10 -15
  201. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -13
  202. data/lib/rigor/inference/method_dispatcher.rb +378 -524
  203. data/lib/rigor/inference/method_parameter_binder.rb +81 -114
  204. data/lib/rigor/inference/multi_target_binder.rb +51 -68
  205. data/lib/rigor/inference/mutation_widening.rb +115 -158
  206. data/lib/rigor/inference/narrowing.rb +627 -727
  207. data/lib/rigor/inference/origin_lookup.rb +38 -0
  208. data/lib/rigor/inference/parameter_inference_collector.rb +154 -109
  209. data/lib/rigor/inference/precision_scanner.rb +26 -35
  210. data/lib/rigor/inference/project_patched_methods.rb +20 -32
  211. data/lib/rigor/inference/project_patched_scanner.rb +23 -37
  212. data/lib/rigor/inference/protection_scanner.rb +16 -18
  213. data/lib/rigor/inference/rbs_type_translator.rb +49 -66
  214. data/lib/rigor/inference/scope_indexer.rb +473 -821
  215. data/lib/rigor/inference/statement_evaluator.rb +754 -1114
  216. data/lib/rigor/inference/struct_fold_safety.rb +34 -48
  217. data/lib/rigor/inference/synthetic_method.rb +9 -16
  218. data/lib/rigor/inference/synthetic_method_index.rb +20 -35
  219. data/lib/rigor/inference/synthetic_method_scanner.rb +63 -103
  220. data/lib/rigor/language_server/buffer_resolution.rb +6 -9
  221. data/lib/rigor/language_server/buffer_table.rb +11 -18
  222. data/lib/rigor/language_server/completion_provider.rb +69 -116
  223. data/lib/rigor/language_server/debouncer.rb +16 -25
  224. data/lib/rigor/language_server/diagnostic_publisher.rb +26 -40
  225. data/lib/rigor/language_server/document_symbol_provider.rb +14 -19
  226. data/lib/rigor/language_server/folding_range_provider.rb +11 -19
  227. data/lib/rigor/language_server/hover_provider.rb +8 -11
  228. data/lib/rigor/language_server/hover_renderer.rb +56 -93
  229. data/lib/rigor/language_server/loop.rb +14 -19
  230. data/lib/rigor/language_server/project_context.rb +46 -74
  231. data/lib/rigor/language_server/selection_range_provider.rb +9 -12
  232. data/lib/rigor/language_server/server.rb +55 -83
  233. data/lib/rigor/language_server/signature_help_provider.rb +29 -46
  234. data/lib/rigor/language_server/synchronized_writer.rb +4 -7
  235. data/lib/rigor/language_server/uri.rb +8 -13
  236. data/lib/rigor/language_server.rb +4 -6
  237. data/lib/rigor/mcp/loop.rb +2 -3
  238. data/lib/rigor/mcp/server.rb +4 -7
  239. data/lib/rigor/mcp.rb +3 -6
  240. data/lib/rigor/plugin/access_denied_error.rb +5 -8
  241. data/lib/rigor/plugin/additional_initializer.rb +21 -31
  242. data/lib/rigor/plugin/base.rb +335 -518
  243. data/lib/rigor/plugin/blueprint.rb +14 -23
  244. data/lib/rigor/plugin/box.rb +18 -29
  245. data/lib/rigor/plugin/fact_store.rb +16 -26
  246. data/lib/rigor/plugin/inflector.rb +37 -53
  247. data/lib/rigor/plugin/io_boundary.rb +33 -56
  248. data/lib/rigor/plugin/isolation.rb +42 -55
  249. data/lib/rigor/plugin/load_error.rb +10 -15
  250. data/lib/rigor/plugin/loader.rb +30 -49
  251. data/lib/rigor/plugin/macro/block_as_method.rb +20 -32
  252. data/lib/rigor/plugin/macro/heredoc_template.rb +35 -58
  253. data/lib/rigor/plugin/macro/nested_class_template.rb +22 -36
  254. data/lib/rigor/plugin/macro/trait_registry.rb +34 -51
  255. data/lib/rigor/plugin/macro.rb +10 -15
  256. data/lib/rigor/plugin/manifest.rb +85 -144
  257. data/lib/rigor/plugin/node_context.rb +14 -22
  258. data/lib/rigor/plugin/node_rule_walk.rb +49 -74
  259. data/lib/rigor/plugin/protocol_contract.rb +25 -39
  260. data/lib/rigor/plugin/registry.rb +132 -205
  261. data/lib/rigor/plugin/services.rb +21 -33
  262. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -19
  263. data/lib/rigor/plugin/trust_policy.rb +24 -38
  264. data/lib/rigor/plugin/type_node_resolver.rb +15 -24
  265. data/lib/rigor/protection/diagnostic_oracle.rb +11 -13
  266. data/lib/rigor/protection/mutation_scanner.rb +27 -35
  267. data/lib/rigor/protection/mutator.rb +50 -70
  268. data/lib/rigor/protection/test_suite_oracle.rb +20 -27
  269. data/lib/rigor/rbs_extended/conformance_checker.rb +48 -68
  270. data/lib/rigor/rbs_extended/hkt_directives.rb +28 -54
  271. data/lib/rigor/rbs_extended/reporter.rb +24 -40
  272. data/lib/rigor/rbs_extended.rb +107 -197
  273. data/lib/rigor/reflection.rb +68 -86
  274. data/lib/rigor/scope/discovery_index.rb +14 -19
  275. data/lib/rigor/scope.rb +255 -310
  276. data/lib/rigor/sig_gen/classification.rb +6 -10
  277. data/lib/rigor/sig_gen/generator.rb +197 -323
  278. data/lib/rigor/sig_gen/layout_index.rb +12 -20
  279. data/lib/rigor/sig_gen/method_candidate.rb +12 -17
  280. data/lib/rigor/sig_gen/observation_collector.rb +38 -70
  281. data/lib/rigor/sig_gen/observed_call.rb +13 -23
  282. data/lib/rigor/sig_gen/path_mapper.rb +17 -29
  283. data/lib/rigor/sig_gen/renderer.rb +7 -13
  284. data/lib/rigor/sig_gen/type_elaborator.rb +15 -28
  285. data/lib/rigor/sig_gen/write_result.rb +8 -16
  286. data/lib/rigor/sig_gen/writer.rb +95 -174
  287. data/lib/rigor/sig_gen.rb +3 -6
  288. data/lib/rigor/signature_path_audit.rb +24 -30
  289. data/lib/rigor/source/constant_path.rb +10 -14
  290. data/lib/rigor/source/literals.rb +31 -45
  291. data/lib/rigor/source/node_locator.rb +9 -11
  292. data/lib/rigor/source/node_walker.rb +9 -13
  293. data/lib/rigor/source.rb +3 -4
  294. data/lib/rigor/testing.rb +16 -20
  295. data/lib/rigor/triage/catalogue.rb +38 -62
  296. data/lib/rigor/triage.rb +31 -52
  297. data/lib/rigor/trinary.rb +9 -13
  298. data/lib/rigor/type/acceptance_router.rb +4 -6
  299. data/lib/rigor/type/accepts_result.rb +10 -14
  300. data/lib/rigor/type/app.rb +19 -27
  301. data/lib/rigor/type/bot.rb +4 -6
  302. data/lib/rigor/type/bound_method.rb +10 -15
  303. data/lib/rigor/type/combinator.rb +165 -257
  304. data/lib/rigor/type/constant.rb +23 -34
  305. data/lib/rigor/type/data_class.rb +10 -15
  306. data/lib/rigor/type/data_instance.rb +14 -20
  307. data/lib/rigor/type/difference.rb +21 -32
  308. data/lib/rigor/type/dynamic.rb +3 -5
  309. data/lib/rigor/type/hash_shape.rb +32 -18
  310. data/lib/rigor/type/integer_range.rb +11 -16
  311. data/lib/rigor/type/intersection.rb +27 -42
  312. data/lib/rigor/type/nominal.rb +10 -15
  313. data/lib/rigor/type/plain_lattice.rb +9 -13
  314. data/lib/rigor/type/refined.rb +67 -114
  315. data/lib/rigor/type/singleton.rb +4 -6
  316. data/lib/rigor/type/struct_class.rb +11 -16
  317. data/lib/rigor/type/struct_instance.rb +15 -21
  318. data/lib/rigor/type/tuple.rb +14 -19
  319. data/lib/rigor/type/union.rb +30 -42
  320. data/lib/rigor/type_node/generic.rb +14 -26
  321. data/lib/rigor/type_node/identifier.rb +12 -19
  322. data/lib/rigor/type_node.rb +3 -12
  323. data/lib/rigor/value_semantics.rb +16 -21
  324. data/lib/rigor/version.rb +1 -1
  325. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +18 -30
  326. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +16 -29
  327. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +12 -23
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +22 -39
  329. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +26 -46
  330. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +51 -87
  331. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +10 -17
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +26 -39
  333. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +141 -184
  334. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +30 -51
  335. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +57 -92
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +92 -108
  337. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +13 -22
  338. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +15 -25
  339. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +8 -13
  340. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +16 -24
  341. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +40 -48
  342. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +140 -129
  343. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +56 -59
  344. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +27 -41
  345. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +16 -24
  346. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +168 -241
  348. data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +3 -5
  349. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +8 -14
  350. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +9 -15
  351. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +4 -7
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +23 -41
  353. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +3 -5
  354. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +6 -10
  355. data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +8 -11
  356. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  357. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +30 -45
  358. data/plugins/rigor-devise/lib/rigor-devise.rb +3 -5
  359. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +43 -69
  360. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +26 -35
  361. data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +3 -5
  362. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +30 -47
  363. data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +3 -5
  364. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +67 -113
  365. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +29 -46
  366. data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +3 -5
  367. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -20
  368. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +14 -23
  369. data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +2 -3
  370. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +22 -37
  371. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +28 -46
  372. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +13 -17
  373. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -47
  374. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +52 -80
  375. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +21 -31
  376. data/plugins/rigor-graphql/lib/rigor-graphql.rb +3 -5
  377. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +11 -17
  378. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +20 -34
  379. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +45 -75
  380. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +22 -36
  381. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +22 -41
  382. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +17 -32
  383. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +6 -12
  384. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +4 -8
  385. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +17 -32
  386. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -18
  387. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +58 -92
  388. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +20 -34
  389. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +20 -31
  390. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +49 -82
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +55 -92
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +51 -90
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +17 -30
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
  395. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +38 -70
  396. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +61 -75
  397. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +319 -505
  398. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +91 -96
  399. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +32 -54
  400. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +8 -13
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +9 -16
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +24 -39
  403. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +13 -22
  404. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +34 -56
  405. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +22 -42
  406. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +43 -75
  407. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +27 -39
  408. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +14 -19
  409. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +20 -34
  410. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +28 -47
  411. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +21 -37
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +13 -22
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +18 -31
  414. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +6 -10
  415. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +14 -29
  416. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +23 -41
  417. data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +3 -5
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +23 -38
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +57 -96
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +22 -35
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +41 -69
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +6 -10
  423. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +24 -41
  424. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +29 -50
  425. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +55 -88
  426. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +159 -262
  427. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +21 -29
  428. data/plugins/rigor-statesman/lib/rigor-statesman.rb +3 -5
  429. data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +23 -37
  430. data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +4 -6
  431. data/sig/rigor/environment.rbs +1 -0
  432. data/sig/rigor/scope.rbs +6 -0
  433. metadata +9 -1
@@ -7,98 +7,99 @@ require "monitor"
7
7
  require "securerandom"
8
8
  require "zlib"
9
9
 
10
+ require_relative "../version"
10
11
  require_relative "descriptor"
11
12
 
12
13
  module Rigor
13
14
  module Cache
14
- # Filesystem-backed cache store. Schema, layout, file format,
15
- # atomicity, and locking are fixed by [ADR-6](../../../docs/adr/6-cache-persistence-backend.md);
16
- # callers use `#fetch_or_compute` (producer-keyed),
17
- # `#fetch_or_validate` (record-and-validate for discovered-dep
18
- # caches, ADR-45), `#stats`, `#evict!`, and `.disk_inventory`,
19
- # plus the [`Rigor::Cache::Descriptor`](descriptor.rb) value object.
15
+ # Filesystem-backed cache store. Schema, layout, file format, atomicity, and locking are fixed by
16
+ # [ADR-6](../../../docs/adr/6-cache-persistence-backend.md); callers use `#fetch_or_compute`
17
+ # (producer-keyed), `#fetch_or_validate` (record-and-validate for discovered-dep caches, ADR-45), `#stats`,
18
+ # `#evict!`, and `.disk_inventory`, plus the [`Rigor::Cache::Descriptor`](descriptor.rb) value object.
20
19
  #
21
- # Read failures (missing file, bad magic, format-version mismatch,
22
- # corrupt SHA-256 trailer, un-inflatable or unmarshal-able payload)
23
- # are silently treated as cache misses; the producer block reruns
24
- # and the next write replaces the bad entry. The trailing SHA-256
25
- # catches accidental corruption (partial writes, FS errors); it is
26
- # **not** a security boundary, per ADR-2's trusted-gem trust model.
20
+ # Read failures (missing file, bad magic, format-version mismatch, corrupt SHA-256 trailer, un-inflatable
21
+ # or unmarshal-able payload) are silently treated as cache misses; the producer block reruns and the next
22
+ # write replaces the bad entry. The trailing SHA-256 catches accidental corruption (partial writes, FS
23
+ # errors); it is **not** a security boundary, per ADR-2's trusted-gem trust model.
27
24
  class Store # rubocop:disable Metrics/ClassLength
28
- # On-disk byte-layout version. Bumped on incompatible format
29
- # changes (independent of {Descriptor::SCHEMA_VERSION}, which
30
- # covers the descriptor schema rather than the byte layout).
31
- # v2 (ADR-54 WD2): the value payload is zlib-deflated on write
32
- # and inflated on read Marshal blobs compress to 13–16 % at
33
- # an inflate cost an order of magnitude below their
34
- # `Marshal.load`. v1 entries fail the header check and read as
35
- # silent misses; the `schema_version.txt` marker additionally
36
- # carries this version, so the first writable run after a bump
37
- # clears the root and reclaims the unreadable bytes.
25
+ # On-disk byte-layout version. Bumped on incompatible format changes (independent of
26
+ # {Descriptor::SCHEMA_VERSION}, which covers the descriptor schema rather than the byte layout). v2
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
29
+ # fail the header check and read as silent misses; the `schema_version.txt` marker additionally carries
30
+ # this version, so the first writable run after a bump clears the root and reclaims the unreadable
31
+ # bytes.
38
32
  FORMAT_VERSION = 2
39
33
 
40
- # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte
41
- # format version.
34
+ # Payload ABI version. Store values are mostly Marshal blobs of Rigor/RBS objects, so a Rigor release
35
+ # upgrade is an ABI boundary even when the byte layout and descriptor schema are unchanged. Folding the
36
+ # gem version into the root marker makes installed-version upgrades rebuild rather than silently reuse a
37
+ # blob whose class layout still happens to unmarshal.
38
+ PAYLOAD_ABI_VERSION = Rigor::VERSION
39
+
40
+ # Whole-project producers are content-keyed, so dependency / signature churn writes a new entry and leaves
41
+ # the old generation unreachable. The global 256 MB cap is intentionally generous and often never fires on
42
+ # one project, so these producers get a small generation cap as a second compaction axis. Per-file / plugin
43
+ # producers are deliberately absent from this table: many current entries under one producer id can be live.
44
+ GENERATION_CAP_BY_PRODUCER = {
45
+ "analysis.run-diagnostics" => 16,
46
+ "rbs.class_ancestor_table" => 2,
47
+ "rbs.class_type_param_names" => 2,
48
+ "rbs.constant_type_table" => 2,
49
+ "rbs.environment" => 2,
50
+ "rbs.known_class_names" => 2
51
+ }.freeze
52
+
53
+ STALE_TEMP_FILE_AGE_SECONDS = 60 * 60
54
+
55
+ # Header literal: 5-byte ASCII magic, 1-byte separator, 1-byte format version.
42
56
  HEADER = "RIGOR\x00#{FORMAT_VERSION.chr}".b.freeze
43
57
 
44
58
  VALID_PRODUCER_ID = /\A[a-z][a-z0-9._-]*\z/
45
59
 
46
60
  # @param root [String] cache root directory.
47
- # @param read_only [Boolean] when true, every disk-side
48
- # side-effect is suppressed: `fetch_or_compute` still
49
- # reads existing entries (hits) and still runs the
50
- # producer block on miss, but it does NOT write the
51
- # produced value to disk, does NOT update the
52
- # `schema_version.txt` marker, and does NOT touch the
53
- # on-disk root directory. The in-process memo is still
54
- # populated so repeated lookups within the same run stay
55
- # cheap. Used by editor mode so multiple buffer-mode
56
- # invocations can read from the same cache concurrently
57
- # without churning it. See
58
- # `docs/design/20260516-editor-mode.md` § "Cache behaviour".
61
+ # @param read_only [Boolean] when true, every disk-side side-effect is suppressed: `fetch_or_compute`
62
+ # still reads existing entries (hits, gated on a current `schema_version.txt` marker — see
63
+ # {#ensure_schema_version!}) and still runs the producer block on miss, but it does NOT write the
64
+ # produced value to disk, does NOT update the marker, and does NOT touch the on-disk root directory.
65
+ # The in-process memo is still populated so repeated lookups within the same run stay cheap. Used by
66
+ # editor mode so multiple buffer-mode invocations can read from the same cache concurrently without
67
+ # churning it. See `docs/design/20260516-editor-mode.md` § "Cache behaviour".
59
68
  def initialize(root:, read_only: false, max_bytes: nil)
60
69
  @root = root.to_s.dup.freeze
61
70
  @read_only = read_only
62
71
  @max_bytes = max_bytes&.then { |n| Integer(n) }
63
- @schema_version_ensured = false
72
+ # Tri-state: nil = not yet checked, true/false = the disk tier's availability for this Store's
73
+ # lifetime (see {#ensure_schema_version!}).
74
+ @disk_available = nil
64
75
  @hits = 0
65
76
  @misses = 0
66
77
  @writes = 0
67
78
  @by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
68
- # Process-level in-memory layer keyed by
69
- # `(producer_id, cache_key)`. Avoids the disk read +
70
- # `Marshal.load` cost (the dominant share of repeated
71
- # cache-hit calls per stackprof) when many short-lived
72
- # `Analysis::Runner` instances share one `Store` the
73
- # spec process, the LSP daemon's repeated re-check
74
- # path, and any other "many runs, same project" loop.
75
- # Keys are content-derived (descriptor digests), so
76
- # cross-fixture contamination is impossible.
79
+ # Process-level in-memory layer keyed by `(producer_id, cache_key)`. Avoids the disk read +
80
+ # `Marshal.load` cost (the dominant share of repeated cache-hit calls per stackprof) when many
81
+ # short-lived `Analysis::Runner` instances share one `Store` — the spec process, the LSP daemon's
82
+ # repeated re-check path, and any other "many runs, same project" loop. Keys are content-derived
83
+ # (descriptor digests), so cross-fixture contamination is impossible.
77
84
  @memo = {}
78
- # `Analysis::Runner` walks files concurrently (file-
79
- # level parallelism); the per-file workers share one
80
- # Store. The monitor guards `@memo` + the counter
81
- # hashes against concurrent writes. The Monitor is
82
- # re-entrant so producer blocks can recursively
83
- # consult the Store (e.g. one cache layer building on
84
- # another) without dead-locking.
85
+ # `Analysis::Runner` walks files concurrently (file-level parallelism); the per-file workers share
86
+ # one Store. The monitor guards `@memo` + the counter hashes against concurrent writes. The Monitor
87
+ # is re-entrant so producer blocks can recursively consult the Store (e.g. one cache layer building
88
+ # on another) without dead-locking.
85
89
  @monitor = Monitor.new
86
90
  end
87
91
 
88
92
  attr_reader :root
89
93
 
90
- # @return [Boolean] whether this Store suppresses disk writes
91
- # (`schema_version.txt`, entry creation). Reads are
92
- # unaffected.
94
+ # @return [Boolean] whether this Store suppresses disk writes (`schema_version.txt`, entry creation).
95
+ # Reads are unaffected.
93
96
  def read_only?
94
97
  @read_only
95
98
  end
96
99
 
97
- # Returns a frozen snapshot of this Store's per-run hit / miss /
98
- # write counters. The bookkeeping is in-memory only — every new
99
- # `Store.new` starts at zero so the counters reflect activity
100
- # against this specific instance rather than the on-disk cache
101
- # state. Disk-level state is reported separately by
100
+ # Returns a frozen snapshot of this Store's per-run hit / miss / write counters. The bookkeeping is
101
+ # in-memory only — every new `Store.new` starts at zero — so the counters reflect activity against this
102
+ # specific instance rather than the on-disk cache state. Disk-level state is reported separately by
102
103
  # {.disk_inventory}.
103
104
  #
104
105
  # @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
@@ -109,28 +110,21 @@ module Rigor
109
110
  end
110
111
  end
111
112
 
112
- # Walks the on-disk cache rooted at `root` and reports a
113
- # producer-level inventory. Used by `rigor check --cache-stats`
114
- # to surface cache size and per-producer entry counts without
115
- # depending on in-process counters (which only reflect the
116
- # current run).
113
+ # Walks the on-disk cache rooted at `root` and reports a producer-level inventory. Used by
114
+ # `rigor check --cache-stats` to surface cache size and per-producer entry counts without depending on
115
+ # in-process counters (which only reflect the current run).
117
116
  #
118
- # @return [Hash] `{ root:, schema_version:, total_entries:,
119
- # total_bytes:, producers: [{ id:, entries:, bytes: }, ...] }`.
120
- # When the root does not exist or has no schema-version
121
- # marker, `schema_version` is nil and the producer list is
122
- # empty.
117
+ # @return [Hash] `{ root:, schema_version:, total_entries:, total_bytes:, producers: [{ id:, entries:,
118
+ # bytes: }, ...] }`. When the root does not exist or has no schema-version marker, `schema_version` is
119
+ # nil and the producer list is empty.
123
120
  #
124
- # The `schema_version.txt` marker content. Covers BOTH
125
- # invalidation axes: the descriptor schema and the on-disk byte
126
- # layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the
127
- # old entries permanently unreadable (header mismatch miss)
128
- # but, alone, would never reclaim their bytes — they can sit
129
- # below the eviction cap forever. Folding the format version
130
- # into the marker routes the bump through the established
131
- # clear-the-root path instead.
121
+ # The `schema_version.txt` marker content. Covers BOTH invalidation axes: the descriptor schema and the
122
+ # on-disk byte layout ({FORMAT_VERSION}, ADR-54). A format bump leaves the old entries permanently
123
+ # unreadable (header mismatch → miss) but, alone, would never reclaim their bytes — they can sit below
124
+ # the eviction cap forever. Folding the format version into the marker routes the bump through the
125
+ # established clear-the-root path instead.
132
126
  def self.schema_marker_value
133
- "#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
127
+ "#{PAYLOAD_ABI_VERSION}.#{Descriptor::SCHEMA_VERSION}.#{FORMAT_VERSION}"
134
128
  end
135
129
 
136
130
  def self.disk_inventory(root:)
@@ -166,35 +160,25 @@ module Rigor
166
160
  end
167
161
  private_class_method :collect_producers
168
162
 
169
- # @param producer_id [String] stable cache namespace; only
170
- # `[a-z][a-z0-9._-]*` is accepted.
171
- # @param params [Hash] producer inputs; mixed into the cache key
172
- # via {Descriptor#cache_key_for}.
173
- # @param descriptor [Rigor::Cache::Descriptor] the invalidation
174
- # descriptor for the value being cached.
175
- # @param serialize [#call, nil] optional callable that turns the
176
- # producer's return value into a binary `String`. Defaults to
177
- # `Marshal.dump(value).b`. Producers whose return values are
178
- # not `Marshal`-clean (RBS-native objects with `RBS::Location`
179
- # members, raw `IO`, …) MUST provide a serialiser. The pair
180
- # `(serialize, deserialize)` MUST round-trip a producer that
181
- # reads with one strategy and writes with another corrupts
182
- # its own cache slice.
183
- # @param deserialize [#call, nil] optional callable that turns
184
- # bytes back into the producer's value. Defaults to
185
- # `Marshal.load`. Any exception (`StandardError`) raised by
186
- # the deserialiser is treated as a cache miss — the entry is
187
- # considered corrupt, the producer block reruns, and the
188
- # next write overwrites it. This is consistent with the
189
- # fault-tolerance contract for the default `Marshal.load`
163
+ # @param producer_id [String] stable cache namespace; only `[a-z][a-z0-9._-]*` is accepted.
164
+ # @param params [Hash] producer inputs; mixed into the cache key via {Descriptor#cache_key_for}.
165
+ # @param descriptor [Rigor::Cache::Descriptor] the invalidation descriptor for the value being cached.
166
+ # @param serialize [#call, nil] optional callable that turns the producer's return value into a binary
167
+ # `String`. Defaults to `Marshal.dump(value).b`. Producers whose return values are not
168
+ # `Marshal`-clean (RBS-native objects with `RBS::Location` members, raw `IO`, …) MUST provide a
169
+ # serialiser. The pair `(serialize, deserialize)` MUST round-trip — a producer that reads with one
170
+ # strategy and writes with another corrupts its own cache slice.
171
+ # @param deserialize [#call, nil] optional callable that turns bytes back into the producer's value.
172
+ # Defaults to `Marshal.load`. Any exception (`StandardError`) raised by the deserialiser is treated as
173
+ # a cache miss — the entry is considered corrupt, the producer block reruns, and the next write
174
+ # overwrites it. This is consistent with the fault-tolerance contract for the default `Marshal.load`
190
175
  # path.
191
176
  # @yieldreturn the value to cache.
192
- # @return the cached value (loaded from disk on hit; produced by
193
- # the block on miss).
177
+ # @return the cached value (loaded from disk on hit; produced by the block on miss).
194
178
  def fetch_or_compute(producer_id:, params:, descriptor:,
195
179
  serialize: nil, deserialize: nil, &block)
196
180
  validate_producer_id!(producer_id)
197
- ensure_schema_version!
181
+ disk = ensure_schema_version!
198
182
 
199
183
  key = descriptor.cache_key_for(producer_id: producer_id, params: params)
200
184
  memo_key = [producer_id, key].freeze
@@ -204,8 +188,8 @@ module Rigor
204
188
  return memoed
205
189
  end
206
190
 
207
- path = entry_path(producer_id, key)
208
- cached = read_entry(path, deserialize: deserialize)
191
+ path = disk ? entry_path(producer_id, key) : nil
192
+ cached = path && read_entry(path, deserialize: deserialize)
209
193
  unless cached.nil?
210
194
  @monitor.synchronize do
211
195
  record(:hits, producer_id)
@@ -215,35 +199,31 @@ module Rigor
215
199
  end
216
200
 
217
201
  value = block.call
218
- write_entry(path, descriptor, value, serialize: serialize) unless @read_only
202
+ wrote = path && try_write_entry(path, descriptor, value, serialize: serialize)
219
203
  @monitor.synchronize do
220
204
  record(:misses, producer_id)
221
- record(:writes, producer_id) unless @read_only
205
+ record(:writes, producer_id) if wrote
222
206
  @memo[memo_key] = value
223
207
  end
224
208
  value
225
209
  end
226
210
 
227
- # ADR-45 — record-and-validate variant. Unlike {fetch_or_compute},
228
- # which keys the entry on its descriptor (so the inputs MUST be
229
- # known before running), this keys on `key_descriptor` (the stable
230
- # inputs known up front) and stores, alongside the value, a
231
- # `dependency_descriptor` of the files the value actually read
232
- # including inputs discovered DURING the computation (e.g. a plugin
233
- # reading a file mid-analysis). On the next run the stored
234
- # dependencies are re-validated against the filesystem
211
+ # ADR-45 — record-and-validate variant. Unlike {fetch_or_compute}, which keys the entry on its
212
+ # descriptor (so the inputs MUST be known before running), this keys on `key_descriptor` (the stable
213
+ # inputs known up front) and stores, alongside the value, a `dependency_descriptor` of the files the
214
+ # value actually read including inputs discovered DURING the computation (e.g. a plugin reading a
215
+ # file mid-analysis). On the next run the stored dependencies are re-validated against the filesystem
235
216
  # ({Descriptor#fresh?}); a stale dependency forces a recompute.
236
217
  #
237
- # The block MUST return `[value, dependency_descriptor]`. Disk reads
238
- # are not in-process-memoisedvalidation always re-checks the
239
- # filesystem — but a single run only looks up once.
218
+ # The block MUST return `[value, dependency_descriptor]`. Disk reads are not in-process-memoised —
219
+ # validation always re-checks the filesystem but a single run only looks up once.
240
220
  def fetch_or_validate(producer_id:, key_descriptor:, params: {}, serialize: nil, deserialize: nil)
241
221
  validate_producer_id!(producer_id)
242
- ensure_schema_version!
222
+ disk = ensure_schema_version!
243
223
 
244
224
  key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
245
- path = entry_path(producer_id, key)
246
- cached = read_entry(path, deserialize: deserialize)
225
+ path = disk ? entry_path(producer_id, key) : nil
226
+ cached = path && read_entry(path, deserialize: deserialize)
247
227
  if cached && (pair = cached.value).is_a?(Array) && pair.size == 2 &&
248
228
  pair[1].is_a?(Descriptor) && pair[1].fresh?
249
229
  @monitor.synchronize { record(:hits, producer_id) }
@@ -251,18 +231,7 @@ module Rigor
251
231
  end
252
232
 
253
233
  value, dependency_descriptor = block_given? ? yield : [nil, Descriptor.new]
254
- wrote = false
255
- unless @read_only
256
- # A cache write must never break the run. If the value is not
257
- # Marshal-clean (or any disk error occurs) skip caching and
258
- # return the freshly-computed value — the next run recomputes.
259
- begin
260
- write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
261
- wrote = true
262
- rescue StandardError
263
- wrote = false
264
- end
265
- end
234
+ wrote = path && try_write_entry(path, key_descriptor, [value, dependency_descriptor], serialize: serialize)
266
235
  @monitor.synchronize do
267
236
  record(:misses, producer_id)
268
237
  record(:writes, producer_id) if wrote
@@ -270,29 +239,30 @@ module Rigor
270
239
  value
271
240
  end
272
241
 
273
- # ADR-6 § "Eviction" — LRU pass over the on-disk cache. No-op when
274
- # `max_bytes:` was not configured or the store is read-only.
275
- # Walks all `.entry` files, sorts by mtime ascending (oldest = least
276
- # recently used), and unlinks from the oldest until the total is at
277
- # or below the cap. Touch-on-disk-read ({read_entry}) is the
278
- # cross-process LRU signal: every disk hit (not in-process-memo hit)
279
- # updates the mtime so recently-read entries survive the eviction pass.
280
- # Any FS error is swallowed eviction must never break a run.
242
+ # ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only. Stale
243
+ # temp file cleanup and the whole-project generation cap run regardless of `max_bytes:` — they reclaim
244
+ # provably-dead bytes (leaked temp files, unreachable content-keyed generations) rather than enforcing a
245
+ # size budget, so an explicitly unbounded store (`max_bytes: nil`) still benefits from them. The
246
+ # size-based LRU pass below stays gated on `max_bytes:` being configured: it walks all remaining
247
+ # `.entry` files, sorts by mtime ascending (oldest = least recently used), and unlinks from the oldest
248
+ # until the total is at or below the cap. Touch-on-disk-read ({read_entry}) is the cross-process LRU
249
+ # signal: every disk hit (not in-process-memo hit) updates the mtime so recently-read entries survive
250
+ # the eviction pass. Any FS error is swallowed — eviction must never break a run.
281
251
  def evict!
282
- return if @max_bytes.nil? || @read_only
252
+ return if @read_only
253
+
254
+ cleanup_stale_temp_files
255
+ entries = evict_excess_generations(collect_entry_stats)
256
+ return if @max_bytes.nil?
283
257
 
284
- entries = collect_entry_stats
285
- total = entries.sum { |e| e[:bytes] }
258
+ total = entries.sum { |e| e[:bytes] }
286
259
  return if total <= @max_bytes
287
260
 
288
261
  entries.sort_by! { |e| e[:mtime] }
289
262
  entries.each do |entry|
290
263
  break if total <= @max_bytes
291
264
 
292
- File.unlink(entry[:path])
293
- total -= entry[:bytes]
294
- rescue StandardError
295
- next
265
+ total -= entry[:bytes] if unlink_entry(entry[:path])
296
266
  end
297
267
  nil
298
268
  rescue StandardError
@@ -324,9 +294,8 @@ module Rigor
324
294
  File.join(@root, producer_id, key[0, 2], "#{key[2..]}.entry")
325
295
  end
326
296
 
327
- # Reads and validates one entry file. Any failure (missing,
328
- # short, bad magic, bad version, bad checksum, unmarshal-able)
329
- # returns nil so the caller treats it as a cache miss.
297
+ # Reads and validates one entry file. Any failure (missing, short, bad magic, bad version, bad
298
+ # checksum, unmarshal-able) returns nil so the caller treats it as a cache miss.
330
299
  def read_entry(path, deserialize: nil)
331
300
  return nil unless File.file?(path)
332
301
 
@@ -344,8 +313,8 @@ module Rigor
344
313
  Entry.new(descriptor_bytes, value)
345
314
  end
346
315
 
347
- # Validates the magic + format-version header and the trailing
348
- # SHA-256 over everything before the trailer.
316
+ # Validates the magic + format-version header and the trailing SHA-256 over everything before the
317
+ # trailer.
349
318
  def envelope_valid?(bytes)
350
319
  return false if bytes.bytesize < HEADER.bytesize + 32
351
320
  return false unless bytes.byteslice(0, HEADER.bytesize) == HEADER
@@ -354,8 +323,8 @@ module Rigor
354
323
  Digest::SHA256.digest(bytes.byteslice(0, bytes.bytesize - 32)) == trailer
355
324
  end
356
325
 
357
- # Splits the body into (descriptor_bytes, value_bytes). Returns
358
- # `[nil, nil]` on a malformed varint or length-overrun.
326
+ # Splits the body into (descriptor_bytes, value_bytes). Returns `[nil, nil]` on a malformed varint or
327
+ # length-overrun.
359
328
  def parse_body(body)
360
329
  offset = 0
361
330
  descriptor_len, offset = read_varint(body, offset)
@@ -374,9 +343,8 @@ module Rigor
374
343
  LOAD_FAILED = Object.new.freeze
375
344
  private_constant :LOAD_FAILED
376
345
 
377
- # Inflates the stored value payload (ADR-54 WD2), then hands the
378
- # raw bytes to the deserialiser. Any failure corrupt deflate
379
- # stream included — reads as a miss.
346
+ # Inflates the stored value payload (ADR-54 WD2), then hands the raw bytes to the deserialiser. Any
347
+ # failure corrupt deflate stream includedreads as a miss.
380
348
  def safe_load(bytes, deserialize)
381
349
  raw = Zlib::Inflate.inflate(bytes)
382
350
  if deserialize
@@ -421,42 +389,68 @@ module Rigor
421
389
  File.open(path, File::RDWR | File::CREAT, 0o644) do |lock_fd|
422
390
  lock_fd.flock(File::LOCK_EX)
423
391
  tmp = "#{path}.tmp.#{Process.pid}.#{SecureRandom.hex(4)}"
424
- File.open(tmp, "wb") do |f|
425
- f.write(body)
426
- f.fsync
392
+ begin
393
+ File.open(tmp, "wb") do |f|
394
+ f.write(body)
395
+ f.fsync
396
+ end
397
+ File.rename(tmp, path)
398
+ ensure
399
+ # A failed write/rename must not leak its temp file — rely on the 1-hour `cleanup_stale_temp_files`
400
+ # sweep only as a backstop for crashes that skip this ensure entirely.
401
+ unlink_entry(tmp) if File.exist?(tmp)
427
402
  end
428
- File.rename(tmp, path)
403
+ fsync_directory(File.dirname(path))
429
404
  end
430
405
  end
431
406
 
407
+ # Checks (and, for a writable store, repairs) the `schema_version.txt` marker, and reports whether the
408
+ # disk tier is usable for the rest of this Store's lifetime. The result is memoized in `@disk_available`
409
+ # — one check per Store is enough; a benign double-check under a thread race would just repeat
410
+ # idempotent work.
411
+ #
412
+ # A writable store clears the cache root and rewrites the marker on a stale/missing marker, then reports
413
+ # available. A read-only store never touches the root — no mkdir, no marker write, no destructive clear
414
+ # — so it reports available ONLY when the on-disk marker already matches current exactly; a stale or
415
+ # missing marker (e.g. a Rigor upgrade with no writable run yet, as in LSP / editor mode) reports
416
+ # unavailable rather than risk unmarshalling a payload from a different ABI. Any filesystem failure
417
+ # (permission, disk full, deleted root) also reports unavailable, degrading this Store instance to
418
+ # in-memory-memo-only for the rest of its lifetime — the producer block still runs, its result still
419
+ # lands in `@memo`, and disk reads/writes are simply skipped.
420
+ #
421
+ # @return [Boolean]
432
422
  def ensure_schema_version!
433
- # Read-only stores never touch the cache root — no mkdir,
434
- # no marker write, no destructive clear on schema
435
- # mismatch. A stale or wrong-schema marker simply yields
436
- # nothing back (entries read through the version check
437
- # are content-keyed, so a write under the new schema
438
- # never collides with a read under the old). The next
439
- # writable run will repair the cache.
440
- return if @read_only
441
- # The marker is process-stable; one check per Store is
442
- # enough (a benign double-check under a thread race just
443
- # repeats idempotent work).
444
- return if @schema_version_ensured
423
+ return @disk_available unless @disk_available.nil?
445
424
 
446
- @schema_version_ensured = true
425
+ @disk_available = @read_only ? read_only_marker_current? : repair_writable_marker!
426
+ end
427
+
428
+ def read_only_marker_current?
429
+ marker = File.join(@root, "schema_version.txt")
430
+ return false unless File.file?(marker)
431
+
432
+ File.read(marker).strip == self.class.schema_marker_value
433
+ rescue StandardError
434
+ false
435
+ end
436
+
437
+ def repair_writable_marker!
447
438
  FileUtils.mkdir_p(@root)
448
439
  marker = File.join(@root, "schema_version.txt")
449
440
  current = self.class.schema_marker_value
450
441
 
451
442
  if File.file?(marker)
452
443
  on_disk = File.read(marker).strip
453
- return if on_disk == current
444
+ return true if on_disk == current
454
445
 
455
446
  clear_cache_root!
456
447
  end
457
448
 
458
449
  FileUtils.mkdir_p(@root)
459
450
  File.write(marker, "#{current}\n")
451
+ true
452
+ rescue StandardError
453
+ false
460
454
  end
461
455
 
462
456
  def clear_cache_root!
@@ -465,9 +459,29 @@ module Rigor
465
459
  end
466
460
  end
467
461
 
468
- # LEB128 unsigned varint encoder/decoder. Lengths fit easily in
469
- # five bytes (cap at 2^35); the cache layer never writes a value
470
- # larger than that in practice.
462
+ # Shared write path for both {#fetch_or_compute} and {#fetch_or_validate}. A cache write must never
463
+ # break the run: filesystem-side failures (permission, disk full, deleted root, read-only mount) are
464
+ # swallowed and read as "did not write". Programmer errors — a custom serializer that doesn't return a
465
+ # `String`, or any other producer contract violation — still raise so the bug is visible.
466
+ def try_write_entry(path, descriptor, value, serialize: nil)
467
+ return false if @read_only
468
+
469
+ write_entry(path, descriptor, value, serialize: serialize)
470
+ true
471
+ rescue SystemCallError, IOError
472
+ false
473
+ end
474
+
475
+ # Best-effort durability for the rename itself. Some platforms cannot fsync directories (or do not need to),
476
+ # so failures are ignored; the entry envelope still turns any lost/partial write into a miss.
477
+ def fsync_directory(dir)
478
+ File.open(dir, File::RDONLY, &:fsync)
479
+ rescue StandardError
480
+ nil
481
+ end
482
+
483
+ # LEB128 unsigned varint encoder/decoder. Lengths fit easily in five bytes (cap at 2^35); the cache
484
+ # layer never writes a value larger than that in practice.
471
485
  def write_varint(bytes, value)
472
486
  raise ArgumentError, "varint must be non-negative" if value.negative?
473
487
 
@@ -482,9 +496,8 @@ module Rigor
482
496
  end
483
497
  end
484
498
 
485
- # Updates both atime and mtime of `path` to the current time — the
486
- # cross-process LRU signal used by {evict!}. Best-effort: any FS
487
- # error (read-only mount, deleted file) is silently ignored.
499
+ # Updates both atime and mtime of `path` to the current time — the cross-process LRU signal used by
500
+ # {evict!}. Best-effort: any FS error (read-only mount, deleted file) is silently ignored.
488
501
  def touch_for_lru(path)
489
502
  now = Time.now
490
503
  File.utime(now, now, path)
@@ -492,17 +505,66 @@ module Rigor
492
505
  nil
493
506
  end
494
507
 
495
- # Returns an array of `{ path:, mtime:, bytes: }` hashes for every
496
- # `.entry` file under the cache root, skipping unreadable entries.
508
+ def cleanup_stale_temp_files
509
+ cutoff = Time.now - STALE_TEMP_FILE_AGE_SECONDS
510
+ Dir.glob(File.join(@root, "**", "*.tmp.*")).each do |path|
511
+ next unless File.file?(path)
512
+ next if File.mtime(path) > cutoff
513
+
514
+ unlink_entry(path)
515
+ rescue StandardError
516
+ next
517
+ end
518
+ rescue StandardError
519
+ nil
520
+ end
521
+
522
+ def evict_excess_generations(entries)
523
+ removed = {}
524
+ entries.group_by { |entry| entry[:producer] }.each do |producer, producer_entries|
525
+ cap = GENERATION_CAP_BY_PRODUCER[producer]
526
+ next if cap.nil? || producer_entries.size <= cap
527
+
528
+ producer_entries.sort_by { |entry| [entry[:mtime], entry[:path]] }
529
+ .first(producer_entries.size - cap)
530
+ .each do |entry|
531
+ removed[entry[:path]] = true if unlink_entry(entry[:path])
532
+ end
533
+ end
534
+ return entries if removed.empty?
535
+
536
+ entries.reject { |entry| removed[entry[:path]] }
537
+ end
538
+
539
+ def unlink_entry(path)
540
+ File.unlink(path)
541
+ true
542
+ rescue StandardError
543
+ false
544
+ end
545
+
546
+ # Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
547
+ # cache root, skipping unreadable entries.
497
548
  def collect_entry_stats
498
549
  Dir.glob(File.join(@root, "**", "*.entry")).filter_map do |path|
499
550
  stat = File.stat(path)
500
- { path: path, mtime: stat.mtime, bytes: stat.size }
551
+ producer = producer_id_for_entry(path)
552
+ next nil if producer.nil?
553
+
554
+ { path: path, producer: producer, mtime: stat.mtime, bytes: stat.size }
501
555
  rescue StandardError
502
556
  nil
503
557
  end
504
558
  end
505
559
 
560
+ def producer_id_for_entry(path)
561
+ root_prefix = @root.end_with?(File::SEPARATOR) ? @root : "#{@root}#{File::SEPARATOR}"
562
+ return nil unless path.start_with?(root_prefix)
563
+
564
+ producer = path.delete_prefix(root_prefix).split(File::SEPARATOR, 2).first
565
+ producer.empty? ? nil : producer
566
+ end
567
+
506
568
  def read_varint(bytes, offset)
507
569
  result = 0
508
570
  shift = 0