rigortype 0.4.7 → 0.4.8

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 (268) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/data/core_overlay/bigdecimal.rbs +33 -0
  4. data/data/core_overlay/resolv.rbs +30 -0
  5. data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/docs/handbook/01-getting-started.md +1 -1
  8. data/docs/handbook/11-sig-gen.md +35 -0
  9. data/docs/install.md +20 -0
  10. data/docs/manual/01-installation.md +67 -26
  11. data/docs/manual/02-cli-reference.md +307 -91
  12. data/docs/manual/03-configuration.md +41 -3
  13. data/docs/manual/04-diagnostics.md +5 -4
  14. data/docs/manual/08-skills.md +11 -0
  15. data/docs/manual/09-editor-integration.md +8 -0
  16. data/docs/manual/14-rails-quickstart.md +1 -1
  17. data/docs/manual/19-effect-labels.md +3 -4
  18. data/docs/manual/plugins/rigor-actionpack.md +15 -6
  19. data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
  20. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  21. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  22. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  23. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
  24. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
  25. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
  26. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  27. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
  28. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  29. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  30. data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
  31. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
  32. data/lib/rigor/analysis/check_rules.rb +598 -99
  33. data/lib/rigor/analysis/crash_signature.rb +43 -1
  34. data/lib/rigor/analysis/dependency_recorder.rb +233 -76
  35. data/lib/rigor/analysis/incremental.rb +11 -1
  36. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
  37. data/lib/rigor/analysis/incremental_session.rb +279 -37
  38. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  39. data/lib/rigor/analysis/rule_catalog.rb +37 -1
  40. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  41. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  42. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
  43. data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
  44. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
  45. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
  46. data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
  47. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  48. data/lib/rigor/analysis/runner.rb +222 -39
  49. data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
  50. data/lib/rigor/analysis/worker_session.rb +42 -41
  51. data/lib/rigor/bleeding_edge.rb +19 -4
  52. data/lib/rigor/cache/file_digest.rb +39 -31
  53. data/lib/rigor/cache/incremental_snapshot.rb +66 -7
  54. data/lib/rigor/cache/rbs_descriptor.rb +37 -2
  55. data/lib/rigor/cache/rbs_environment.rb +5 -1
  56. data/lib/rigor/cli/baseline_command.rb +15 -2
  57. data/lib/rigor/cli/check_command.rb +4 -2
  58. data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
  59. data/lib/rigor/cli/lens_command.rb +17 -2
  60. data/lib/rigor/cli/sig_gen_command.rb +8 -0
  61. data/lib/rigor/cli/skill_describe.rb +1 -0
  62. data/lib/rigor/cli.rb +5 -2
  63. data/lib/rigor/config_audit.rb +32 -18
  64. data/lib/rigor/configuration/severity_profile.rb +9 -0
  65. data/lib/rigor/configuration.rb +32 -5
  66. data/lib/rigor/effects/collector.rb +53 -21
  67. data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
  68. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  69. data/lib/rigor/environment/default_libraries.rb +17 -0
  70. data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
  71. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  72. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  73. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  74. data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
  75. data/lib/rigor/environment/rbs_loader.rb +312 -63
  76. data/lib/rigor/environment.rb +131 -35
  77. data/lib/rigor/inference/acceptance.rb +50 -15
  78. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  79. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  80. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  81. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  82. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  83. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  84. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  85. data/lib/rigor/inference/block_run_count.rb +9 -2
  86. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  87. data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
  88. data/lib/rigor/inference/captured_locals.rb +2 -0
  89. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  90. data/lib/rigor/inference/clause_widening.rb +1 -1
  91. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  92. data/lib/rigor/inference/construction_escape.rb +56 -15
  93. data/lib/rigor/inference/declaration_walk.rb +4 -0
  94. data/lib/rigor/inference/def_node_resolver.rb +32 -14
  95. data/lib/rigor/inference/def_return_typer.rb +1 -1
  96. data/lib/rigor/inference/definer_resolution.rb +15 -7
  97. data/lib/rigor/inference/definers_index.rb +72 -16
  98. data/lib/rigor/inference/error_info.rb +16 -0
  99. data/lib/rigor/inference/expression_typer.rb +1459 -287
  100. data/lib/rigor/inference/flow_tracer.rb +31 -14
  101. data/lib/rigor/inference/fork_map.rb +3 -0
  102. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  103. data/lib/rigor/inference/global_write_census.rb +8 -6
  104. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  105. data/lib/rigor/inference/indexed_narrowing.rb +57 -8
  106. data/lib/rigor/inference/ivar_writer_census.rb +106 -11
  107. data/lib/rigor/inference/key_presence_guard.rb +51 -25
  108. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  109. data/lib/rigor/inference/last_line.rb +5 -2
  110. data/lib/rigor/inference/loop_value_record.rb +63 -15
  111. data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
  112. data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
  113. data/lib/rigor/inference/match_rebinding.rb +21 -8
  114. data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
  115. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  116. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  117. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  118. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  119. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  120. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  121. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
  122. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
  123. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
  124. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
  125. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  126. data/lib/rigor/inference/method_dispatcher.rb +66 -18
  127. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  128. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  129. data/lib/rigor/inference/module_function_state.rb +13 -2
  130. data/lib/rigor/inference/multi_target_binder.rb +1 -1
  131. data/lib/rigor/inference/mutation_widening.rb +44 -4
  132. data/lib/rigor/inference/narrowing.rb +201 -27
  133. data/lib/rigor/inference/operand_effects.rb +43 -2
  134. data/lib/rigor/inference/optimistic_origin.rb +133 -8
  135. data/lib/rigor/inference/origin_lookup.rb +1 -1
  136. data/lib/rigor/inference/precision_scanner.rb +1 -1
  137. data/lib/rigor/inference/project_method_ownership.rb +28 -10
  138. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  139. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  140. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  141. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  142. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  143. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  144. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
  145. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
  146. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  147. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  148. data/lib/rigor/inference/remover_holder_scan.rb +76 -22
  149. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  150. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
  151. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
  152. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  153. data/lib/rigor/inference/scope_indexer.rb +2079 -183
  154. data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
  155. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  156. data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
  157. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  158. data/lib/rigor/inference/statement_evaluator.rb +970 -250
  159. data/lib/rigor/inference/string_mutation.rb +14 -0
  160. data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
  161. data/lib/rigor/inference/toplevel_def_census.rb +14 -6
  162. data/lib/rigor/inference/unknown_store_widening.rb +1 -1
  163. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  164. data/lib/rigor/inference/void_tail_summary.rb +29 -10
  165. data/lib/rigor/lens/claims/comparison.rb +78 -0
  166. data/lib/rigor/lens/claims/request.rb +131 -0
  167. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  168. data/lib/rigor/lens/claims.rb +208 -0
  169. data/lib/rigor/lens/contract_reader.rb +5 -0
  170. data/lib/rigor/lens/document.rb +1 -1
  171. data/lib/rigor/lens/file_reader.rb +21 -8
  172. data/lib/rigor/lens/run.rb +8 -0
  173. data/lib/rigor/lens.rb +1 -1
  174. data/lib/rigor/library_probe.rb +68 -0
  175. data/lib/rigor/memo_audit.rb +282 -0
  176. data/lib/rigor/plugin/base.rb +15 -2
  177. data/lib/rigor/plugin/io_boundary.rb +25 -3
  178. data/lib/rigor/plugin/manifest.rb +7 -3
  179. data/lib/rigor/plugin/registry.rb +13 -2
  180. data/lib/rigor/plugin/trust_policy.rb +46 -0
  181. data/lib/rigor/protection/mutation_cache.rb +4 -1
  182. data/lib/rigor/reflection/constant_ancestors.rb +79 -22
  183. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  184. data/lib/rigor/reflection/declarations.rb +58 -0
  185. data/lib/rigor/reflection/project_method.rb +177 -0
  186. data/lib/rigor/reflection.rb +2 -0
  187. data/lib/rigor/rooted_glob.rb +18 -1
  188. data/lib/rigor/run_scoped_slots.rb +20 -20
  189. data/lib/rigor/scope/discovery_index.rb +149 -2
  190. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  191. data/lib/rigor/scope/resolution_chain.rb +900 -55
  192. data/lib/rigor/scope.rb +307 -34
  193. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  194. data/lib/rigor/sig_gen/classification.rb +14 -1
  195. data/lib/rigor/sig_gen/generator.rb +222 -13
  196. data/lib/rigor/sig_gen/method_candidate.rb +14 -0
  197. data/lib/rigor/sig_gen/observation_collector.rb +11 -2
  198. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  199. data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
  200. data/lib/rigor/sig_gen/writer.rb +21 -0
  201. data/lib/rigor/sig_gen.rb +1 -0
  202. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  203. data/lib/rigor/source/node_children.rb +13 -0
  204. data/lib/rigor/type/combinator.rb +164 -8
  205. data/lib/rigor/type/interface.rb +60 -0
  206. data/lib/rigor/type.rb +1 -0
  207. data/lib/rigor/type_of_memo/serving.rb +99 -0
  208. data/lib/rigor/type_of_memo/stats.rb +43 -0
  209. data/lib/rigor/type_of_memo.rb +337 -0
  210. data/lib/rigor/typing_state.rb +1094 -0
  211. data/lib/rigor/version.rb +1 -1
  212. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
  213. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
  214. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  215. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  216. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  217. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  218. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  219. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
  221. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  222. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
  223. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  224. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
  225. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
  226. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
  227. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  228. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  229. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
  230. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  231. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
  232. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
  233. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
  234. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
  235. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
  236. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  237. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  238. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
  239. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  240. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
  241. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  242. data/sig/rigor/analysis/fact_store.rbs +2 -3
  243. data/sig/rigor/cache/descriptor.rbs +14 -0
  244. data/sig/rigor/environment.rbs +4 -1
  245. data/sig/rigor/plugin/base.rbs +53 -19
  246. data/sig/rigor/plugin/interfaces.rbs +51 -0
  247. data/sig/rigor/plugin/io_boundary.rbs +2 -3
  248. data/sig/rigor/plugin/node_context.rbs +21 -0
  249. data/sig/rigor/reflection.rbs +37 -0
  250. data/sig/rigor/scope.rbs +22 -3
  251. data/sig/rigor/trinary.rbs +1 -0
  252. data/sig/rigor/type.rbs +9 -3
  253. data/sig/rigor.rbs +3 -2
  254. data/skills/rigor-ask/SKILL.md +1 -0
  255. data/skills/rigor-project-init/references/01-detect.md +1 -0
  256. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
  257. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  258. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  259. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  260. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  261. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  262. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  263. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  264. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  265. data/skills/rigor-type-oracle/SKILL.md +26 -1
  266. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  267. metadata +48 -2
  268. data/lib/rigor/inference/memo_audit.rb +0 -55
@@ -169,6 +169,14 @@ module Rigor
169
169
  # worker captures the file's cross-file reads for {#drain_dependencies}; the recorder's own disabled
170
170
  # fast path makes the unwrapped case free.
171
171
  def analyze(path)
172
+ diagnostics = analyze_attributed(path)
173
+ return diagnostics unless Inference::MemoAudit.enabled?
174
+
175
+ # ADR-129 WD5 (#2785) — the audit's mismatches this file raised, as `Runner#analyze_file` reports them.
176
+ CrashSignature.with_swallowed(path, diagnostics, Inference::MemoAudit.drain)
177
+ end
178
+
179
+ def analyze_attributed(path)
172
180
  return analyze_with_effects(path) unless @record_dependencies
173
181
 
174
182
  diagnostics = nil
@@ -204,6 +212,7 @@ module Rigor
204
212
  diagnostics
205
213
  end
206
214
  private :analyze_with_effects
215
+ private :analyze_attributed
207
216
 
208
217
  # ADR-103 WD6 / #386 — the worker-side mirror of `Runner#effect_envelope_index`. Derived from the
209
218
  # same configuration and the session's own environment, so a worker's `≤` lane is the one the
@@ -227,6 +236,20 @@ module Rigor
227
236
  end
228
237
  private :effect_envelope_index
229
238
 
239
+ # The scope a file's analysis starts from: the project seed, with its template units.
240
+ def seeded_scope(path)
241
+ @template_units.seed(seed_project_scope(Scope.empty(environment: @environment, source_path: path)), path)
242
+ end
243
+
244
+ # The main pass over `root`, which the ADR-129 stage-3 shadow ({TypeOfMemo}) counts apart from the rule walk
245
+ # that follows it.
246
+ def index_main_pass(root, scope)
247
+ TypeOfMemo.begin_file
248
+ index = Inference::ScopeIndexer.index(root, default_scope: scope, widen_clauses: true)
249
+ TypeOfMemo.phase!(:rule)
250
+ index
251
+ end
252
+
230
253
  def analyze_body(path)
231
254
  parse_result = parse_source(path)
232
255
  unless parse_result.errors.empty?
@@ -237,12 +260,10 @@ module Rigor
237
260
 
238
261
  root = parse_result.value
239
262
  Effects::Collector.record_root(root)
240
- scope = @template_units.seed(
241
- seed_project_scope(Scope.empty(environment: @environment, source_path: path)), path
242
- )
243
- index = Inference::ScopeIndexer.index(root, default_scope: scope, widen_clauses: true)
263
+ scope = seeded_scope(path)
264
+ index = index_main_pass(root, scope)
244
265
  # ADR-53 B4 — built-in collectors + plugin node rules share one walk.
245
- node_collectors = CheckRules.build_node_collectors(path, index, root)
266
+ node_collectors = built_in_collectors(path, index, root)
246
267
  node_results = node_rule_results_by_plugin(path, root, scope, node_collectors, index)
247
268
  diagnostics = CheckRules.diagnose(
248
269
  path: path,
@@ -258,11 +279,21 @@ module Rigor
258
279
  diagnostics + explain_diagnostics(path, parse_result.value, scope)
259
280
  rescue Errno::ENOENT => e
260
281
  [analyzer_error(path, e.message)]
261
- rescue StandardError => e
262
- [analyzer_error(path, CrashSignature.check_rule_message(e))]
282
+ rescue StandardError, SystemStackError => e
283
+ [analyzer_error(path, CrashSignature.contained_message!(e, parse_result))]
284
+ ensure
285
+ TypeOfMemo.end_file
263
286
  end
264
287
  private :analyze_body
265
288
 
289
+ # The built-in collectors for one file; `plugin.api-surface`'s joins only when the rule can report.
290
+ def built_in_collectors(path, index, root)
291
+ CheckRules.build_node_collectors(
292
+ path, index, root, plugin_api_surface: CheckRules::PluginApiSurface.active?(@configuration)
293
+ )
294
+ end
295
+ private :built_in_collectors
296
+
266
297
  # Read-once snapshot of the per-worker reporters so the caller (or the eventual Phase 4b pool
267
298
  # aggregator) can merge into a single coordinator-side reporter. Both reporters dedupe at write time,
268
299
  # so a post-hoc concat + de-dup at the entry-key level is sound.
@@ -419,40 +450,10 @@ module Rigor
419
450
  Prism.parse(File.read(physical), filepath: path, version: @configuration.target_ruby)
420
451
  end
421
452
 
422
- # Mirrors {Runner#build_trust_policy}. Deriving trust inside the session keeps the substrate decoupled
423
- # from the coordinator; configuration is already Ractor-shareable.
453
+ # {Rigor::Plugin::TrustPolicy.for_configuration} is the single derivation `rigor check`, pooled
454
+ # workers and `rigor unused`'s plugin roots share (#2586); configuration is already Ractor-shareable.
424
455
  def build_trust_policy
425
- trusted_gems = @configuration.plugins.map { |entry| trusted_gem_name(entry) }.uniq
426
- roots = [Dir.pwd]
427
- Array(@configuration.signature_paths).each { |sp| roots << File.absolute_path(sp) }
428
- trusted_gems.each do |gem_name|
429
- path = trusted_gem_root(gem_name)
430
- roots << path if path
431
- end
432
- @configuration.plugins_io_allowed_paths.each { |p| roots << File.absolute_path(p) }
433
-
434
- Plugin::TrustPolicy.new(
435
- trusted_gems: trusted_gems,
436
- allowed_read_roots: roots,
437
- network_policy: @configuration.plugins_io_network,
438
- allowed_url_hosts: @configuration.plugins_io_allowed_url_hosts
439
- )
440
- end
441
-
442
- def trusted_gem_name(entry)
443
- case entry
444
- when String then entry
445
- when Hash then entry["gem"] || entry["id"]
446
- end
447
- end
448
-
449
- def trusted_gem_root(gem_name)
450
- return nil if gem_name.nil? || gem_name.empty?
451
-
452
- spec = Gem.loaded_specs[gem_name]
453
- spec&.full_gem_path
454
- rescue StandardError
455
- nil
456
+ Plugin::TrustPolicy.for_configuration(@configuration)
456
457
  end
457
458
 
458
459
  def run_plugin_prepare
@@ -473,7 +474,7 @@ module Rigor
473
474
  def plugin_prepare_error_diagnostic(plugin, error)
474
475
  plugin_id = safe_plugin_id(plugin)
475
476
  Diagnostic.new(
476
- path: ".rigor.yml",
477
+ path: @configuration.config_display_path,
477
478
  line: 1,
478
479
  column: 1,
479
480
  message: "plugin #{plugin_id.inspect} raised during prepare: " \
@@ -126,6 +126,20 @@ module Rigor
126
126
  "static.value-use.void" => :warning
127
127
  }.freeze
128
128
  ),
129
+ # ADR-128 WD6 / ADR-130 slice 2 (#2686). Off by default because a new required diagnostic is an ADR-50
130
+ # Decision 3 new discipline; it graduates at the next hard break.
131
+ Feature.new(
132
+ id: "plugin-api-surface",
133
+ kind: :severity,
134
+ summary: "In plugin source (code inside a class that inherits `Rigor::Plugin::Base`), a call on a value " \
135
+ "typed by a `Rigor::Plugin` interface (`_Scope`, `_Environment`, `_Services`, ...) to a member " \
136
+ "the interface does not declare becomes a `:warning` (`plugin.api-surface`). The engine object " \
137
+ "answers such a call today, so it is not `call.undefined-method`; it is outside the plugin API " \
138
+ "Rigor promises to keep (ADR-128).",
139
+ severity_overrides: {
140
+ "plugin.api-surface" => :warning
141
+ }.freeze
142
+ ),
129
143
  Feature.new(
130
144
  id: "discovery-seeded-mutation-sites",
131
145
  kind: :behaviour,
@@ -156,7 +170,7 @@ module Rigor
156
170
  "kills — every surviving breakage there is one the analyzer reports nowhere at all, not one " \
157
171
  "it reports in a caller."
158
172
  ),
159
- # ADR-103 WD15 — the owner-ruled preview of the v0.4.0 default. `:behaviour` because it moves no rule's
173
+ # ADR-103 WD15 — the owner-ruled preview of the v0.5.0 default. `:behaviour` because it moves no rule's
160
174
  # severity; safe under the header comment's cache-identity rule for a reason specific to this feature
161
175
  # rather than the generic WD13 argument, so it is spelled out here: `effects:` collection is
162
176
  # OBSERVATIONAL (ADR-103 WD13) and the `effects:` block itself is deliberately absent from the
@@ -174,9 +188,10 @@ module Rigor
174
188
  "it had written `effects: {}` — effect collection, the `rigor effects` verbs' cache sharing, " \
175
189
  "and `effects.check` all turn on with every sub-key at its default. Writing `effects: false` " \
176
190
  "explicitly still opts out, and a project that writes its own `effects:` block keeps that " \
177
- "block's shape untouched either way. Graduates to default-on at v0.4.0 (owner ruling, " \
178
- "2026-08-17): ADR-50 § WD7 reads \"at a major\" as \"at v0.4.0\" for this feature on the 0.x " \
179
- "evaluation line. See ADR-103 § WD15 for the preconditions that gate the flip."
191
+ "block's shape untouched either way. Graduates to default-on at v0.5.0 (owner ruling, " \
192
+ "2026-10-26, moving the v0.4.0 date of 2026-08-17): ADR-50 § WD7 reads \"at a major\" as the " \
193
+ "version that ruling names, for this feature on the 0.x evaluation line. See ADR-103 § WD15 " \
194
+ "for the preconditions that gate the flip and § WD18 for the move."
180
195
  )
181
196
  ].freeze
182
197
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "digest"
4
4
 
5
+ require_relative "../typing_state"
6
+
5
7
  module Rigor
6
8
  module Cache
7
9
  # Per-run content-digest memo AND the ADR-87 stat-then-digest freshness choke point. Within a single
@@ -20,20 +22,38 @@ module Rigor
20
22
  # zero SHA-256 bytes. The SHA-256 digest remains the sole change AUTHORITY — the stat tier only decides
21
23
  # whether the digest needs recomputing.
22
24
  #
23
- # Soundness: the memo + recording-instant + strict flag are process-local (thread-locals, so Ractor-safe
24
- # and never crossing a fork boundary), never persisted to disk, and scoped to exactly one run via
25
- # {.with_run} (which installs a fresh table and restores the previous one on exit — a second run, or a run
25
+ # Soundness: the memo + recording-instant + strict flag are process-local (a per-thread {TypingState}
26
+ # Frame, so Ractor-safe and never crossing a fork boundary), never persisted to disk, and scoped to exactly one
27
+ # run via {.with_run} (which installs a fresh table and restores the previous one on exit — a second run, or a run
26
28
  # that re-reads a file the user edited between runs, gets a clean table). Within a single run the analyzer
27
29
  # already assumes the filesystem is stable — {Descriptor#fresh?} validates the whole dependency set against
28
30
  # the live tree in one pass and never re-checks mid-run — so caching a coherent per-run snapshot changes
29
31
  # nothing about what a run observes. Outside a run scope (no active table) every call digests directly,
30
32
  # identical to a bare `Digest::SHA256.file`.
31
33
  module FileDigest
32
- MEMO_KEY = :rigor_cache_file_digest_memo
33
- STAT_KEY = :rigor_cache_file_stat_memo
34
- INSTANT_KEY = :rigor_cache_recording_instant
35
- STRICT_KEY = :rigor_cache_strict_validation
36
- private_constant :MEMO_KEY, :STAT_KEY, :INSTANT_KEY, :STRICT_KEY
34
+ # One run's state: the digest table, the stat table, the recording instant and the strict flag, installed
35
+ # together by {.with_run}.
36
+ Run = Data.define(:digests, :stats, :instant, :strict)
37
+ private_constant :Run
38
+
39
+ # ADR-129 WD1 — a Frame: {.with_run} installs the run and restores the previous one on the way out, and whether
40
+ # one is installed changes answers (outside a run every call digests or stats directly, the recording instant
41
+ # is the moment of the call, and validation is not strict). The installed {Run} is frozen; its two tables are
42
+ # extensional caches its owner fills in place without a touch (Memos by kind, whose entry is fixed once
43
+ # written): `digests` is keyed on the path and reads the file's bytes, `stats` is keyed on the path and reads
44
+ # its `File::Stat`, both under the run's premise that the filesystem is stable. The instant and the flag are
45
+ # read at install time.
46
+ RUN = TypingState.frame(
47
+ :file_digest_run,
48
+ memos: %i[digests stats],
49
+ reason: "installed per run by with_run; the digest and stat tables are extensional caches keyed on the path, " \
50
+ "filled in place, both reading the filesystem under the run's stable-filesystem premise, and the " \
51
+ "recording instant and strict flag are fixed at install",
52
+ # G1′: the run wraps the typing of every file, but the digest and stat lookups are the cache layer's, made while
53
+ # the RBS environment is prepared and its producers fetched, before anything types (#2883).
54
+ typing_never_reads: true
55
+ )
56
+ private_constant :RUN
37
57
 
38
58
  # Set in the environment to force the strict digest-always validation path for a single run, regardless
39
59
  # of the `cache.validation` config setting (the env wins). The escape hatch for a filesystem whose stat
@@ -46,21 +66,9 @@ module Rigor
46
66
  # captured at run start — a file whose recorded mtime is not strictly older than it is treated as racy
47
67
  # (always re-hashed), catching a file edited during the analysis window. `strict:` forces the
48
68
  # digest-always path for `cache.validation: digest`.
49
- def self.with_run(strict: false)
50
- previous_memo = Thread.current[MEMO_KEY]
51
- previous_stat = Thread.current[STAT_KEY]
52
- previous_instant = Thread.current[INSTANT_KEY]
53
- previous_strict = Thread.current[STRICT_KEY]
54
- Thread.current[MEMO_KEY] = {}
55
- Thread.current[STAT_KEY] = {}
56
- Thread.current[INSTANT_KEY] = now_ns
57
- Thread.current[STRICT_KEY] = strict
58
- yield
59
- ensure
60
- Thread.current[MEMO_KEY] = previous_memo
61
- Thread.current[STAT_KEY] = previous_stat
62
- Thread.current[INSTANT_KEY] = previous_instant
63
- Thread.current[STRICT_KEY] = previous_strict
69
+ def self.with_run(strict: false, &)
70
+ run = Run.new(digests: {}, stats: {}, instant: now_ns, strict: strict)
71
+ TypingState.current.with(RUN, run, &)
64
72
  end
65
73
 
66
74
  # The SHA-256 hex digest of `path`'s current content. Served from the active per-run table when one is
@@ -68,10 +76,10 @@ module Rigor
68
76
  # read failure (missing / unreadable file) propagates exactly as `Digest::SHA256.file` would and is NOT
69
77
  # memoised, so a caller's own rescue path is unchanged.
70
78
  def self.hexdigest(path)
71
- memo = Thread.current[MEMO_KEY]
72
- return Digest::SHA256.file(path).hexdigest if memo.nil?
79
+ run = TypingState.current[RUN]
80
+ return Digest::SHA256.file(path).hexdigest if run.nil?
73
81
 
74
- memo[path] ||= Digest::SHA256.file(path).hexdigest
82
+ run.digests[path] ||= Digest::SHA256.file(path).hexdigest
75
83
  end
76
84
 
77
85
  # @return whether digest-always validation is forced for this run. The env var wins over the
@@ -79,7 +87,7 @@ module Rigor
79
87
  def self.strict_validation?
80
88
  return true if ENV[STRICT_ENV] == "1"
81
89
 
82
- Thread.current[STRICT_KEY] == true
90
+ TypingState.current[RUN]&.strict == true
83
91
  end
84
92
 
85
93
  # ADR-87 WD1 — packs the `:stat` FileEntry value: `"<digest> <size> <mtime_ns> <ctime_ns> <inode>
@@ -156,14 +164,14 @@ module Rigor
156
164
  # describe that moment rather than an earlier probe's. A stat failure propagates un-memoised, exactly
157
165
  # as {.hexdigest} treats a read failure.
158
166
  def self.validation_stat(path)
159
- memo = Thread.current[STAT_KEY]
160
- return File.stat(path) if memo.nil?
167
+ run = TypingState.current[RUN]
168
+ return File.stat(path) if run.nil?
161
169
 
162
- memo[path] ||= File.stat(path)
170
+ run.stats[path] ||= File.stat(path)
163
171
  end
164
172
 
165
173
  def self.recording_instant_ns
166
- Thread.current[INSTANT_KEY] || now_ns
174
+ TypingState.current[RUN]&.instant || now_ns
167
175
  end
168
176
 
169
177
  # value layout: [digest, size, mtime_ns, ctime_ns, inode, recording_instant_ns]
@@ -202,7 +202,24 @@ module Rigor
202
202
  # 47: issues #2079 / #2170 — each seed bundle's `global_write_census` also records each instance-side ivar
203
203
  # write's coarse value (`[:own_write_type, class, name, value, path]`), which the class-ivar census joins across
204
204
  # files and along the chain; a pre-47 bundle carries none, so a warm run would keep a seed a cold run widens.
205
- SCHEMA = 47
205
+ # 48: issue #1237 — a `return_summaries` key may carry a third element, `true`, for a call key a splat call
206
+ # observed, which the gate re-drives with no positional correspondence. A pre-48 summary holds such a call under a
207
+ # positional key the engine bound as one argument, so a warm run would re-drive it the old way.
208
+ # 49: issue #3116 — a `return_summaries` entry for a def observed under more keys than the per-def cap is
209
+ # `{ keys: [], returns: [], effects:, overflow: true }`. A pre-49 summary holds the first eight keys of such a
210
+ # def with no marker, so a warm run would skip the reader of a key it never recorded.
211
+ # 50: issue #3160 — a `return_summaries` entry holds every key under which a current reader observes the def,
212
+ # merged across the runs that observed them, and a def whose keys cannot all be listed has no entry (the
213
+ # `overflow` form of 49 is gone). A pre-50 summary holds only what the last run that analysed the def's file
214
+ # saw, so a warm run would skip a reader added, or left out of that run, since.
215
+ # 51: issue #3180 — a reader the ADR-89 WD2 gate skipped holds the dependency edges the gate's probe recorded
216
+ # for the def it passed, and the summaries hold the keys that probe observed. A pre-51 snapshot's skipped
217
+ # readers lack the edges and keys of whatever a gate-passed def came to reach, so a warm run would skip them.
218
+ # 52: issues #3186 / #3187 — a `return_summaries` entry also holds `guard_constants`, the published-constant
219
+ # spellings the truthiness rule's one hop reads in the def's body, and a gate-skipped reader holds the plugin
220
+ # reads and run-scoped registrations the gate's probe made. A pre-52 snapshot's skipped readers lack those
221
+ # plugin rows, so a warm run would serve them past an edit of what the plugin read.
222
+ SCHEMA = 52
206
223
 
207
224
  # The persisted per-file state.
208
225
  # `cache` maps an analyzed file to its diagnostics.
@@ -233,10 +250,15 @@ module Rigor
233
250
  # fingerprint cannot see.
234
251
  # ADR-89 WD2:
235
252
  # `return_summaries` maps a `[path, "Class#method" | "Class.method"]` to the callee's persisted
236
- # behavioural surface `{ keys:, returns:, effects: }` — the observed `[receiver, arg_types]` call keys
237
- # (Marshal-clean type tuples), their `describe(:short)` return descriptors, and the content-mutated
238
- # parameter positions. A recheck re-evaluates a declaration-stable changed callee at these keys and,
239
- # when every return + the effects are unchanged, skips its symbol dependents.
253
+ # behavioural surface `{ keys:, returns:, effects:, guard_constants: }` — the observed `[receiver, arg_types]`
254
+ # call keys (issue #1237: `[receiver, arg_types, true]` for a key a splat call observed)
255
+ # (Marshal-clean type tuples), their `describe(:short)` return descriptors, the content-mutated
256
+ # parameter positions, and (issue #3186) the published-constant spellings the body reads.
257
+ # A recheck re-evaluates a declaration-stable changed callee at these keys and,
258
+ # when every return, the effects and the spellings are unchanged, skips its symbol dependents.
259
+ # Issues #3116 / #3160 — an entry
260
+ # holds every key under which a current symbol dependent observes the def; a def whose keys cannot all be
261
+ # listed (more than the per-def cap, among others) has no entry, and the recheck never skips its dependents.
240
262
  # ADR-67 WD6c lift:
241
263
  # `param_table` is the `parameter_inference:` seed table (`[class, method, kind] => {param => Type}`)
242
264
  # the run that wrote the snapshot seeded its analysis from — `{}` when the gate is off. A recheck
@@ -333,8 +355,7 @@ module Rigor
333
355
  "sig:#{digest_signature_paths(project_sig_roots(configuration))}",
334
356
  "pre_eval:#{digest_pre_eval(configuration.pre_eval)}"
335
357
  ]
336
- parts.concat(core_root_parts(configuration))
337
- parts.concat(dependency_source_parts(configuration))
358
+ parts.concat(project_input_parts(configuration))
338
359
  identity = EngineSource.process_identity
339
360
  parts << "engine-source:#{identity}" if identity
340
361
  # Issue #1700 — the gated vendored signatures the run's source activates. A file that gains or drops a
@@ -360,12 +381,34 @@ module Rigor
360
381
  # Issue #2517 — a selected core is read like the project's own `sig/`, so an edit in place to one of its files
361
382
  # must drop the snapshot (and the language server's session, which keys on this fingerprint). No part for the
362
383
  # default core, so every other project's fingerprint is as it was.
384
+ def self.extra_signature_parts(configuration)
385
+ core_root_parts(configuration) + bundle_sig_parts(configuration)
386
+ end
387
+ private_class_method :extra_signature_parts
388
+
363
389
  def self.core_root_parts(configuration)
364
390
  core_root = configuration.core_root
365
391
  core_root.nil? ? [] : ["core:#{digest_signature_paths([core_root])}"]
366
392
  end
367
393
  private_class_method :core_root_parts
368
394
 
395
+ # Issue #2987 — the bundle gems' own `sig/` directories the environment loads: which gems have one and what
396
+ # their `.rbs` files hold. The lockfile digest above covers a gem's version, not a `sig/` appearing under an
397
+ # unchanged version, nor an edit in place to one. Shared with the language server's session key. No part for a
398
+ # project whose bundle holds none, so its fingerprint is as it was.
399
+ def self.bundle_sig_parts(configuration)
400
+ locked = Environment::LockfileResolver.locked_gems(
401
+ lockfile_path: configuration.bundler_lockfile, project_root: Dir.pwd,
402
+ auto_detect: configuration.bundler_auto_detect
403
+ )
404
+ dirs = Environment::BundleSigDiscovery.discover(
405
+ bundle_path: configuration.bundler_bundle_path, project_root: Dir.pwd,
406
+ auto_detect: configuration.bundler_auto_detect, locked_gems: locked.empty? ? nil : locked
407
+ ).map(&:to_s).sort
408
+ dirs.empty? ? [] : ["bundle-sig:#{dirs.join("\n")}:#{digest_signature_paths(dirs)}"]
409
+ end
410
+ private_class_method :bundle_sig_parts
411
+
369
412
  def self.required_features(configuration, roots)
370
413
  files = Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns,
371
414
  base: configuration.exclude_base)
@@ -383,6 +426,22 @@ module Rigor
383
426
  end
384
427
  private_class_method :dependency_source_parts
385
428
 
429
+ # The parts read off the project beside its configuration, in the order the fingerprint has always joined them:
430
+ # the extra signature roots, the `dependencies.source_inference` gems, and (#3030) the collection setup hint.
431
+ def self.project_input_parts(configuration)
432
+ extra_signature_parts(configuration) + dependency_source_parts(configuration) +
433
+ collection_setup_hint_parts(configuration)
434
+ end
435
+ private_class_method :project_input_parts
436
+
437
+ # Issue #3030 — the `rbs.coverage.collection-not-installed` hint, a run-level row read off the working directory
438
+ # that no other part covers. No part without a hint, so every other project's fingerprint is as it was.
439
+ def self.collection_setup_hint_parts(configuration)
440
+ hint = Analysis::RunCacheKey.collection_setup_hint(configuration)
441
+ hint ? ["rbs-collection-hint:#{hint}"] : []
442
+ end
443
+ private_class_method :collection_setup_hint_parts
444
+
386
445
  def self.digest_file_if_present(path)
387
446
  path = path&.to_s
388
447
  path && File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
@@ -77,7 +77,8 @@ module Rigor
77
77
  # reconstructs {.config_entries} without loading a plugin, so an entry there that it cannot rebuild
78
78
  # would turn the warm fast lane into a permanent miss on every project that enables a bundled plugin.
79
79
  def self.env_only_config_entries(loader)
80
- [deferred_signature_paths_entry(loader), project_signature_paths_entry(loader), core_root_entry(loader)].compact
80
+ [deferred_signature_paths_entry(loader), project_signature_paths_entry(loader), core_root_entry(loader),
81
+ subordinate_signature_paths_entry(loader), collection_signature_paths_entry(loader)].compact
81
82
  end
82
83
 
83
84
  # Issue #2517 — which directory the env loaded core from. The selected core's files are digested by
@@ -142,6 +143,40 @@ module Rigor
142
143
 
143
144
  private_class_method :deferred_signature_paths_entry
144
145
 
146
+ # Issue #2978 — which of the loader's `signature_paths:` stand down member by member to the rest (a bundle gem's
147
+ # collection copy behind its own `sig/`) changes the env built from byte-identical files, as the deferred
148
+ # partition does. Absent when there is none, so every other project keeps its key.
149
+ def self.subordinate_signature_paths_entry(loader)
150
+ return nil unless loader.respond_to?(:subordinate_signature_paths)
151
+
152
+ subordinate = Array(loader.subordinate_signature_paths).map(&:to_s).sort
153
+ return nil if subordinate.empty?
154
+
155
+ Descriptor::ConfigEntry.new(
156
+ key: "rbs.subordinate_signature_paths",
157
+ value_hash: Digest::SHA256.hexdigest(subordinate.join("\n"))
158
+ )
159
+ end
160
+
161
+ private_class_method :subordinate_signature_paths_entry
162
+
163
+ # Issue #3030 — which of the loader's `signature_paths:` are `rbs collection` gem directories decides whether
164
+ # their `_`-prefixed directories load, so it changes the env built from byte-identical files. Absent when
165
+ # there is none, so every project without a collection lockfile keeps its key.
166
+ def self.collection_signature_paths_entry(loader)
167
+ return nil unless loader.respond_to?(:collection_signature_paths)
168
+
169
+ collection = Array(loader.collection_signature_paths).map(&:to_s).sort
170
+ return nil if collection.empty?
171
+
172
+ Descriptor::ConfigEntry.new(
173
+ key: "rbs.collection_signature_paths",
174
+ value_hash: Digest::SHA256.hexdigest(collection.join("\n"))
175
+ )
176
+ end
177
+
178
+ private_class_method :collection_signature_paths_entry
179
+
145
180
  # Public (ADR-87 WD4) so the boot-slimming run-cache probe reconstructs the identical `gems` +
146
181
  # `rbs.libraries` key slots the runner writes, from the config-derived library list, without a loader.
147
182
  def self.rbs_gem_entry
@@ -208,7 +243,7 @@ module Rigor
208
243
  end
209
244
 
210
245
  # @param library_names — the loader's merged library list (or, on the WD4 probe
211
- # path, `Environment::DEFAULT_LIBRARIES + config.libraries` reconstructed without a loader).
246
+ # path, `Environment::BaseLibraries.resolve` reconstructed without a loader).
212
247
  def self.libraries_entry(library_names)
213
248
  sorted = library_names.map(&:to_s).sort
214
249
  Descriptor::ConfigEntry.new(
@@ -36,7 +36,11 @@ module Rigor
36
36
  loader.respond_to?(:deferred_signature_paths) ? loader.deferred_signature_paths : [],
37
37
  project_signature_paths:
38
38
  loader.respond_to?(:project_signature_paths) ? loader.project_signature_paths : [],
39
- core_root: loader.respond_to?(:core_root) ? loader.core_root : nil
39
+ core_root: loader.respond_to?(:core_root) ? loader.core_root : nil,
40
+ subordinate_signature_paths:
41
+ loader.respond_to?(:subordinate_signature_paths) ? loader.subordinate_signature_paths : [],
42
+ collection_signature_paths:
43
+ loader.respond_to?(:collection_signature_paths) ? loader.collection_signature_paths : []
40
44
  )
41
45
  end
42
46
 
@@ -176,7 +176,7 @@ module Rigor
176
176
 
177
177
  baseline = Analysis::Baseline.from_diagnostics(diagnostics, match_mode: options.fetch(:match_mode),
178
178
  project_root: Dir.pwd)
179
- File.write(path, baseline.to_yaml)
179
+ return EXIT_USAGE unless write_baseline_file(path, baseline.to_yaml)
180
180
 
181
181
  covered = baseline.buckets.sum(&:count)
182
182
  @err.puts(
@@ -415,7 +415,8 @@ module Rigor
415
415
  return 0 if options.fetch(:dry_run)
416
416
 
417
417
  pruned = baseline.without(cleared.map(&:bucket))
418
- File.write(options.fetch(:baseline), pruned.to_yaml)
418
+ return EXIT_USAGE unless write_baseline_file(options.fetch(:baseline), pruned.to_yaml)
419
+
419
420
  @err.puts("rigor: pruned #{cleared.size} bucket(s); baseline now has #{pruned.size} entries.")
420
421
  0
421
422
  end
@@ -439,6 +440,18 @@ module Rigor
439
440
 
440
441
  # ---- shared helpers ----------------------------------------
441
442
 
443
+ # #2576 — an output path that cannot be written is a usage error, the same rule `rigor init
444
+ # --path` follows (#2562): one `rigor baseline: cannot write PATH: REASON` line on stderr and
445
+ # exit 64, with no backtrace and no directory created on the way. The bare Errno backtrace read
446
+ # as an internal crash.
447
+ def write_baseline_file(path, content)
448
+ File.write(path, content)
449
+ true
450
+ rescue SystemCallError => e
451
+ @err.puts("rigor baseline: cannot write #{path}: #{e.message.sub(/ @ .*\z/m, '')}")
452
+ false
453
+ end
454
+
442
455
  def load_baseline_or_exit(path)
443
456
  unless File.exist?(path)
444
457
  @err.puts("rigor: baseline file not found: #{path}")
@@ -866,7 +866,8 @@ module Rigor
866
866
  return Configuration.load(options.fetch(:config)) unless options.fetch(:treat_all_as_inline_rbs)
867
867
 
868
868
  path = options.fetch(:config) || Configuration.discover
869
- data = path && File.exist?(path) ? Configuration.load_with_includes(path) : {}
869
+ config_file = path && File.exist?(path) ? path : nil
870
+ data = config_file ? Configuration.load_with_includes(config_file) : {}
870
871
  # ADR-103 WD15 — captured before `DEFAULTS.merge` below folds "no `effects:` key" and "`effects:
871
872
  # false`" into the same value; see `Configuration.load`'s identical capture.
872
873
  effects_key_present = data.key?("effects")
@@ -874,7 +875,8 @@ module Rigor
874
875
  data = data.dup
875
876
  data["plugins"] = inject_treat_all_as_inline_rbs(Array(data["plugins"]))
876
877
  Configuration.new(Configuration::DEFAULTS.merge(data), effects_key_present, target_ruby_key_present)
877
- .with_exclude_base(path && File.exist?(path) ? File.dirname(path) : nil)
878
+ .with_exclude_base(config_file && File.dirname(config_file))
879
+ .with_config_path(config_file)
878
880
  end
879
881
 
880
882
  # ADR-50 § WD2 — applies the `--bleeding-edge[=ids]` / `--no-bleeding-edge` CLI selection over the configured
@@ -91,7 +91,14 @@ module Rigor
91
91
  # exists" hazard: it records observations, so overwriting it is the whole point of running it.
92
92
  def write(configuration, snapshot)
93
93
  path = configuration.effects_snapshot_path
94
- File.write(path, snapshot.to_yaml)
94
+ begin
95
+ File.write(path, snapshot.to_yaml)
96
+ rescue SystemCallError => e
97
+ # #2576 — an `effects.snapshot.path:` that cannot be written is a usage error, the same rule
98
+ # `rigor init --path` follows (#2562): one line on stderr and exit 64, no backtrace.
99
+ @err.puts("rigor effects: cannot write #{path}: #{e.message.sub(/ @ .*\z/m, '')}")
100
+ return CLI::EXIT_USAGE
101
+ end
95
102
  @err.puts("rigor: wrote #{path} (#{snapshot.methods.size} method(s), #{snapshot.reach.size} reach entr" \
96
103
  "#{snapshot.reach.size == 1 ? 'y' : 'ies'})")
97
104
  # #436 — the note names what to write, not only what is missing: the same per-project preset
@@ -8,13 +8,14 @@ require_relative "options"
8
8
  require_relative "check_runner_factory"
9
9
  require_relative "../configuration"
10
10
  require_relative "../lens"
11
+ require_relative "../lens/claims"
11
12
 
12
13
  module Rigor
13
14
  class CLI
14
15
  # `rigor lens FILE...` — a declaration map of the files with type provenance, computed as `rigor check`
15
16
  # computes them (ADR-113; output contract docs/internal-spec/lens.md). Experimental until `lens/v1`.
16
17
  class LensCommand < Command
17
- USAGE = "Usage: rigor lens [--format=text|json] [--config=PATH] [--workers=N] FILE..."
18
+ USAGE = "Usage: rigor lens [--format=text|json] [--claims=REQUEST] [--config=PATH] [--workers=N] FILE..."
18
19
 
19
20
  def run
20
21
  options = parse_options
@@ -25,13 +26,19 @@ module Rigor
25
26
  config_path = options.fetch(:config) || Configuration.discover
26
27
  # The working directory: Rigor auto-detects sig/ and the lockfiles there (lens.md § Serialization).
27
28
  root = Dir.pwd
29
+ request = options[:claims] && Lens::Claims::Request.read(options[:claims], root: root, arguments: @argv)
28
30
  workers = CheckRunnerFactory.resolve_workers(options, configuration)
29
- outcome = Lens::Run.new(configuration: configuration, root: root, arguments: @argv, workers: workers).call
31
+ run = Lens::Run.new(configuration: configuration, root: root, arguments: @argv, workers: workers)
32
+ outcome = run.call
30
33
  document = Lens::Document.new(outcome: outcome, configuration: configuration, root: root,
31
34
  config_path: config_path).to_h
35
+ attach_claims(document, request, outcome, run)
32
36
  @err.puts("rigor lens: #{outcome.warm ? 'warm' : 'cold'} — #{@argv.size} target(s)")
33
37
  render(document, options.fetch(:format))
34
38
  outcome.targets.any? { |target| target.status == "ok" } ? 0 : 1
39
+ rescue Lens::Claims::IntegrityError => e
40
+ usage_error(e.message)
41
+ CLI::EXIT_USAGE
35
42
  end
36
43
 
37
44
  # The parser this command parses with, built without parsing anything: the catalogue reads the
@@ -42,15 +49,23 @@ module Rigor
42
49
  Options.add_format(opts, options, "Output format: text (default) or json")
43
50
  opts.on("--workers=N", Integer, "Worker processes for a cold run") { |value| options[:workers] = value }
44
51
  Options.add_config(opts, options)
52
+ opts.on("--claims=REQUEST", "Check documentation claims (requires --format=json)") do |value|
53
+ options[:claims] = value
54
+ end
45
55
  end
46
56
  end
47
57
 
48
58
  private
49
59
 
60
+ def attach_claims(document, request, outcome, run)
61
+ document["claims"] = Lens::Claims.new(request: request, outcome: outcome, run: run).call if request
62
+ end
63
+
50
64
  def parse_options
51
65
  options = { format: "text", config: nil, workers: nil }
52
66
  option_parser(options).parse!(@argv)
53
67
  return usage_error("no FILE given") if @argv.empty?
68
+ return usage_error("--claims requires --format=json") if options[:claims] && options[:format] != "json"
54
69
 
55
70
  options
56
71
  rescue OptionParser::ParseError => e
@@ -308,6 +308,14 @@ module Rigor
308
308
  # user asked for a write and did not get one, so the command must not report success — a green
309
309
  # `sig-gen --write` in CI would otherwise mean nothing.
310
310
  results.any? { |result| TARGET_REFUSALS.include?(result.action) } || !refused.empty? ? 1 : 0
311
+ rescue SigGen::Writer::WriteError => e
312
+ # #2576 — a target that cannot be written at all (permission denial, a directory where the
313
+ # `.rbs` belongs, a parent path that is a regular file) is a usage error, the same rule
314
+ # `rigor init --path` follows (#2562):
315
+ # one line on stderr and exit 64. The run stops at the first failure; targets already written
316
+ # stay written.
317
+ @err.puts("rigor sig-gen: #{e.message}")
318
+ CLI::EXIT_USAGE
311
319
  end
312
320
 
313
321
  # ADR-112 WD4 — the methods whose inline and `sig/` declarations disagree, refused because the run did not
@@ -130,6 +130,7 @@ module Rigor
130
130
  rigor-editor-setup
131
131
  rigor-mcp-setup
132
132
  rigor-protection-uplift
133
+ rigor-rbs-contract
133
134
  rigor-plugin-tune
134
135
  rigor-plugin-author
135
136
  rigor-upgrade