rigortype 0.4.6 → 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 (550) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  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 +45 -13
  6. data/data/gem_overlay/addressable/addressable.rbs +40 -0
  7. data/data/vendored_gem_sigs/bundler/bundler.rbs +1 -1
  8. data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +4 -3
  9. data/docs/handbook/01-getting-started.md +6 -3
  10. data/docs/handbook/11-sig-gen.md +85 -3
  11. data/docs/handbook/appendix-phpstan.md +4 -4
  12. data/docs/install.md +20 -0
  13. data/docs/manual/01-installation.md +67 -26
  14. data/docs/manual/02-cli-reference.md +455 -113
  15. data/docs/manual/03-configuration.md +76 -5
  16. data/docs/manual/04-diagnostics.md +5 -4
  17. data/docs/manual/06-baseline.md +6 -4
  18. data/docs/manual/08-skills.md +11 -0
  19. data/docs/manual/09-editor-integration.md +21 -2
  20. data/docs/manual/10-mcp-server.md +38 -2
  21. data/docs/manual/12-caching.md +4 -3
  22. data/docs/manual/14-rails-quickstart.md +1 -1
  23. data/docs/manual/19-effect-labels.md +3 -4
  24. data/docs/manual/plugins/rigor-actionpack.md +32 -15
  25. data/docs/manual/plugins/rigor-activesupport-core-ext.md +23 -1
  26. data/docs/manual/plugins/rigor-sorbet.md +17 -3
  27. data/lib/rigor/analysis/baseline.rb +24 -27
  28. data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
  29. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +5 -6
  30. data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
  31. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +2 -2
  32. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +12 -4
  33. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +2 -2
  34. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +85 -13
  35. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +16 -4
  36. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -1
  37. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -8
  38. data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +41 -12
  40. data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
  41. data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +3 -3
  42. data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
  43. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +1 -2
  44. data/lib/rigor/analysis/check_rules/rule_ids.rb +9 -2
  45. data/lib/rigor/analysis/check_rules/rule_walk.rb +9 -7
  46. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +3 -4
  47. data/lib/rigor/analysis/check_rules/special_global_setters.rb +3 -3
  48. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  49. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +2 -2
  50. data/lib/rigor/analysis/check_rules.rb +720 -141
  51. data/lib/rigor/analysis/crash_signature.rb +45 -3
  52. data/lib/rigor/analysis/dependency_recorder.rb +237 -80
  53. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +14 -3
  54. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  55. data/lib/rigor/analysis/dependency_source_inference/index.rb +6 -8
  56. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  57. data/lib/rigor/analysis/edit_replay.rb +93 -6
  58. data/lib/rigor/analysis/fact_store.rb +3 -3
  59. data/lib/rigor/analysis/incremental.rb +11 -1
  60. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +4 -3
  61. data/lib/rigor/analysis/incremental_run_slot.rb +2 -2
  62. data/lib/rigor/analysis/incremental_session.rb +306 -53
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
  64. data/lib/rigor/analysis/rule_catalog.rb +39 -3
  65. data/lib/rigor/analysis/run_cache_key.rb +27 -3
  66. data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
  69. data/lib/rigor/analysis/runner/declaration_position.rb +8 -8
  70. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +264 -112
  71. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +4 -9
  72. data/lib/rigor/analysis/runner/pool_coordinator.rb +173 -328
  73. data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
  74. data/lib/rigor/analysis/runner/run_level_facts.rb +304 -0
  75. data/lib/rigor/analysis/runner/run_snapshots.rb +44 -54
  76. data/lib/rigor/analysis/runner/worker_payload.rb +36 -0
  77. data/lib/rigor/analysis/runner.rb +298 -109
  78. data/lib/rigor/analysis/self_call_resolution_recorder.rb +62 -23
  79. data/lib/rigor/analysis/template_unit_positions.rb +2 -3
  80. data/lib/rigor/analysis/template_units.rb +9 -1
  81. data/lib/rigor/analysis/worker_session.rb +65 -71
  82. data/lib/rigor/bleeding_edge.rb +22 -10
  83. data/lib/rigor/builtins/imported_refinements.rb +6 -8
  84. data/lib/rigor/builtins/predefined_constant_refinements.rb +1 -1
  85. data/lib/rigor/builtins/regex_refinement.rb +4 -6
  86. data/lib/rigor/cache/descriptor.rb +6 -5
  87. data/lib/rigor/cache/file_digest.rb +39 -31
  88. data/lib/rigor/cache/incremental_snapshot.rb +95 -17
  89. data/lib/rigor/cache/rbs_cache_producer.rb +2 -2
  90. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  91. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -2
  92. data/lib/rigor/cache/rbs_constant_table.rb +1 -1
  93. data/lib/rigor/cache/rbs_descriptor.rb +90 -4
  94. data/lib/rigor/cache/rbs_environment.rb +8 -1
  95. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +2 -2
  96. data/lib/rigor/cache/rbs_known_class_names.rb +1 -1
  97. data/lib/rigor/cache/store.rb +11 -10
  98. data/lib/rigor/ci_detector.rb +1 -1
  99. data/lib/rigor/cli/annotate_command.rb +24 -19
  100. data/lib/rigor/cli/baseline_command.rb +160 -57
  101. data/lib/rigor/cli/catalogue.rb +256 -0
  102. data/lib/rigor/cli/check_command.rb +112 -104
  103. data/lib/rigor/cli/check_runner_factory.rb +2 -2
  104. data/lib/rigor/cli/command.rb +15 -1
  105. data/lib/rigor/cli/coverage_command.rb +21 -8
  106. data/lib/rigor/cli/coverage_mutation.rb +5 -7
  107. data/lib/rigor/cli/coverage_renderer.rb +2 -1
  108. data/lib/rigor/cli/coverage_scan.rb +3 -5
  109. data/lib/rigor/cli/diff_command.rb +25 -20
  110. data/lib/rigor/cli/docs_command.rb +1 -2
  111. data/lib/rigor/cli/doctor_command.rb +12 -12
  112. data/lib/rigor/cli/effects_command.rb +50 -40
  113. data/lib/rigor/cli/effects_diff_renderer.rb +18 -17
  114. data/lib/rigor/cli/effects_explain_renderer.rb +13 -12
  115. data/lib/rigor/cli/effects_renderer.rb +2 -1
  116. data/lib/rigor/cli/effects_snapshot_command.rb +26 -18
  117. data/lib/rigor/cli/explain_command.rb +18 -8
  118. data/lib/rigor/cli/fused_protection_renderer.rb +2 -1
  119. data/lib/rigor/cli/json_document.rb +41 -0
  120. data/lib/rigor/cli/lens_command.rb +29 -10
  121. data/lib/rigor/cli/lsp_command.rb +250 -23
  122. data/lib/rigor/cli/mcp_command.rb +12 -7
  123. data/lib/rigor/cli/mutation_fork_scan.rb +1 -1
  124. data/lib/rigor/cli/mutation_protection_renderer.rb +3 -2
  125. data/lib/rigor/cli/options.rb +40 -1
  126. data/lib/rigor/cli/plugin_command.rb +93 -46
  127. data/lib/rigor/cli/plugins_command.rb +17 -18
  128. data/lib/rigor/cli/plugins_renderer.rb +10 -6
  129. data/lib/rigor/cli/protection_fork_scan.rb +2 -2
  130. data/lib/rigor/cli/protection_renderer.rb +2 -1
  131. data/lib/rigor/cli/show_bleedingedge_command.rb +21 -15
  132. data/lib/rigor/cli/sig_gen_command.rb +92 -27
  133. data/lib/rigor/cli/skill_command.rb +32 -3
  134. data/lib/rigor/cli/skill_deep_probe.rb +1 -1
  135. data/lib/rigor/cli/skill_describe.rb +2 -1
  136. data/lib/rigor/cli/trace_command.rb +14 -9
  137. data/lib/rigor/cli/triage_command.rb +10 -11
  138. data/lib/rigor/cli/triage_renderer.rb +2 -1
  139. data/lib/rigor/cli/type_of_command.rb +16 -11
  140. data/lib/rigor/cli/type_of_project_scope.rb +1 -4
  141. data/lib/rigor/cli/type_of_renderer.rb +4 -1
  142. data/lib/rigor/cli/type_scan_command.rb +14 -9
  143. data/lib/rigor/cli/type_scan_renderer.rb +2 -1
  144. data/lib/rigor/cli/unused_command.rb +27 -17
  145. data/lib/rigor/cli/upgrade_command.rb +5 -0
  146. data/lib/rigor/cli.rb +214 -56
  147. data/lib/rigor/config_audit.rb +33 -19
  148. data/lib/rigor/configuration/severity_profile.rb +14 -5
  149. data/lib/rigor/configuration.rb +104 -35
  150. data/lib/rigor/effects/collector.rb +53 -21
  151. data/lib/rigor/effects/definition_context.rb +0 -2
  152. data/lib/rigor/effects/envelope.rb +1 -1
  153. data/lib/rigor/effects/framework_units.rb +1 -1
  154. data/lib/rigor/effects/mutation_classifier.rb +4 -7
  155. data/lib/rigor/effects/propagator.rb +1 -1
  156. data/lib/rigor/effects/registry.rb +2 -2
  157. data/lib/rigor/effects/snapshot.rb +1 -1
  158. data/lib/rigor/effects/taint_cause.rb +0 -2
  159. data/lib/rigor/effects/unit_scan.rb +9 -13
  160. data/lib/rigor/environment/bundle_sig_discovery.rb +64 -33
  161. data/lib/rigor/environment/class_registry.rb +3 -3
  162. data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
  163. data/lib/rigor/environment/core_root.rb +77 -0
  164. data/lib/rigor/environment/default_libraries.rb +18 -1
  165. data/lib/rigor/environment/deferred_signature_plan.rb +381 -0
  166. data/lib/rigor/environment/gated_signature_guard.rb +1 -1
  167. data/lib/rigor/environment/gated_signature_plan.rb +5 -2
  168. data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
  169. data/lib/rigor/environment/lockfile_resolver.rb +28 -12
  170. data/lib/rigor/environment/member_consistency/comparator.rb +18 -1
  171. data/lib/rigor/environment/member_consistency.rb +56 -7
  172. data/lib/rigor/environment/missing_gem_constant_index.rb +3 -2
  173. data/lib/rigor/environment/project_path_gems.rb +124 -0
  174. data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
  175. data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
  176. data/lib/rigor/environment/rbs_hierarchy.rb +12 -15
  177. data/lib/rigor/environment/rbs_loader.rb +772 -110
  178. data/lib/rigor/environment/reflection.rb +7 -7
  179. data/lib/rigor/environment/required_features.rb +0 -3
  180. data/lib/rigor/environment/ruby_library_roots.rb +1 -1
  181. data/lib/rigor/environment/vendored_signature_plan.rb +153 -0
  182. data/lib/rigor/environment.rb +169 -42
  183. data/lib/rigor/flow_contribution/merger.rb +4 -4
  184. data/lib/rigor/inference/acceptance.rb +136 -40
  185. data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
  186. data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
  187. data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
  188. data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
  189. data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
  190. data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
  191. data/lib/rigor/inference/block_auto_splat.rb +1 -1
  192. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  193. data/lib/rigor/inference/block_run_count.rb +185 -0
  194. data/lib/rigor/inference/budget_trace.rb +8 -2
  195. data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
  196. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -2
  197. data/lib/rigor/inference/builtins/numeric_catalog.rb +2 -5
  198. data/lib/rigor/inference/builtins/random_catalog.rb +2 -2
  199. data/lib/rigor/inference/callee_mutation_chain.rb +88 -77
  200. data/lib/rigor/inference/callee_write_census.rb +815 -0
  201. data/lib/rigor/inference/captured_locals.rb +2 -0
  202. data/lib/rigor/inference/census_foreign_block.rb +354 -0
  203. data/lib/rigor/inference/clause_widening.rb +1 -1
  204. data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
  205. data/lib/rigor/inference/construction_escape.rb +258 -0
  206. data/lib/rigor/inference/declaration_walk/traversal.rb +1 -1
  207. data/lib/rigor/inference/declaration_walk.rb +4 -0
  208. data/lib/rigor/inference/deep_erasure.rb +39 -7
  209. data/lib/rigor/inference/def_handle.rb +1 -1
  210. data/lib/rigor/inference/def_node_resolver.rb +33 -15
  211. data/lib/rigor/inference/def_return_typer.rb +47 -5
  212. data/lib/rigor/inference/definer_resolution.rb +19 -9
  213. data/lib/rigor/inference/definers_index.rb +162 -0
  214. data/lib/rigor/inference/direct_ivar_writes.rb +300 -0
  215. data/lib/rigor/inference/element_read_widening.rb +49 -3
  216. data/lib/rigor/inference/error_info.rb +16 -0
  217. data/lib/rigor/inference/expression_typer.rb +1559 -320
  218. data/lib/rigor/inference/external_ancestor_resolution.rb +3 -4
  219. data/lib/rigor/inference/flow_tracer.rb +31 -14
  220. data/lib/rigor/inference/fork_map.rb +3 -0
  221. data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
  222. data/lib/rigor/inference/global_write_census.rb +81 -5
  223. data/lib/rigor/inference/global_writer_reach.rb +260 -0
  224. data/lib/rigor/inference/hkt_body.rb +1 -2
  225. data/lib/rigor/inference/hkt_body_parser.rb +3 -5
  226. data/lib/rigor/inference/hkt_reducer.rb +2 -2
  227. data/lib/rigor/inference/hkt_registry.rb +3 -5
  228. data/lib/rigor/inference/in_effect_refinements.rb +7 -0
  229. data/lib/rigor/inference/indexed_narrowing.rb +61 -15
  230. data/lib/rigor/inference/intervening_ivar_reset.rb +26 -4
  231. data/lib/rigor/inference/ivar_writer_census.rb +154 -8
  232. data/lib/rigor/inference/key_presence_guard.rb +51 -24
  233. data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
  234. data/lib/rigor/inference/last_line.rb +5 -2
  235. data/lib/rigor/inference/loop_value_record.rb +66 -14
  236. data/lib/rigor/inference/match_rebinding/frame.rb +57 -0
  237. data/lib/rigor/inference/match_rebinding/yielding_match.rb +101 -0
  238. data/lib/rigor/inference/match_rebinding.rb +28 -5
  239. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +1 -1
  240. data/lib/rigor/inference/method_dispatcher/argument_distribution.rb +40 -0
  241. data/lib/rigor/inference/method_dispatcher/block_folding.rb +44 -30
  242. data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
  243. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +3 -3
  244. data/lib/rigor/inference/method_dispatcher/contested_overloads.rb +89 -0
  245. data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
  246. data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
  247. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
  248. data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
  249. data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
  250. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -1
  251. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +1 -1
  252. data/lib/rigor/inference/method_dispatcher/no_match_fallback.rb +54 -0
  253. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +51 -27
  254. data/lib/rigor/inference/method_dispatcher/random_folding.rb +1 -1
  255. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +606 -78
  256. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +1 -1
  257. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +2 -2
  258. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +3 -3
  259. data/lib/rigor/inference/method_dispatcher/set_folding.rb +9 -7
  260. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +467 -84
  261. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +0 -1
  262. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +50 -17
  263. data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
  264. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +5 -5
  265. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +4 -6
  266. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  267. data/lib/rigor/inference/method_dispatcher.rb +123 -25
  268. data/lib/rigor/inference/method_parameter_binder.rb +136 -56
  269. data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
  270. data/lib/rigor/inference/module_function_state.rb +24 -3
  271. data/lib/rigor/inference/multi_target_binder.rb +2 -2
  272. data/lib/rigor/inference/mutation_rejoin.rb +240 -4
  273. data/lib/rigor/inference/mutation_widening.rb +199 -29
  274. data/lib/rigor/inference/narrowing.rb +238 -50
  275. data/lib/rigor/inference/operand_effects.rb +43 -2
  276. data/lib/rigor/inference/optimistic_origin.rb +134 -9
  277. data/lib/rigor/inference/origin_lookup.rb +1 -1
  278. data/lib/rigor/inference/out_buffer_mutation.rb +72 -0
  279. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  280. data/lib/rigor/inference/pre_eval_constants.rb +1 -1
  281. data/lib/rigor/inference/precision_scanner.rb +1 -1
  282. data/lib/rigor/inference/project_method_ownership.rb +30 -9
  283. data/lib/rigor/inference/project_patched_methods.rb +65 -13
  284. data/lib/rigor/inference/project_patched_scanner.rb +120 -25
  285. data/lib/rigor/inference/project_patched_visibility.rb +39 -3
  286. data/lib/rigor/inference/project_yielder.rb +11 -17
  287. data/lib/rigor/inference/rbs_type_translator.rb +68 -15
  288. data/lib/rigor/inference/receiver_alias.rb +20 -0
  289. data/lib/rigor/inference/receiver_writer_call.rb +67 -0
  290. data/lib/rigor/inference/recursion_rebinding.rb +157 -0
  291. data/lib/rigor/inference/refined_dispatch/constructed_class.rb +57 -17
  292. data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -16
  293. data/lib/rigor/inference/refined_dispatch.rb +13 -0
  294. data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
  295. data/lib/rigor/inference/remover_holder_scan.rb +287 -0
  296. data/lib/rigor/inference/remover_narrowing.rb +224 -0
  297. data/lib/rigor/inference/rewrite_mutation.rb +53 -2
  298. data/lib/rigor/inference/scope_indexer/certainty.rb +0 -1
  299. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
  300. data/lib/rigor/inference/scope_indexer/constant_ivar_value.rb +93 -0
  301. data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +70 -26
  302. data/lib/rigor/inference/scope_indexer/derived_param_value.rb +112 -0
  303. data/lib/rigor/inference/scope_indexer/empty_seed.rb +99 -0
  304. data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +295 -0
  305. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
  306. data/lib/rigor/inference/scope_indexer.rb +2213 -226
  307. data/lib/rigor/inference/self_call_ivar_writes.rb +103 -12
  308. data/lib/rigor/inference/self_preserving_block.rb +87 -0
  309. data/lib/rigor/inference/set_mutation.rb +189 -0
  310. data/lib/rigor/inference/singleton_hook_decline.rb +42 -8
  311. data/lib/rigor/inference/singleton_object_constant.rb +32 -2
  312. data/lib/rigor/inference/statement_evaluator.rb +3268 -358
  313. data/lib/rigor/inference/string_mutation.rb +14 -0
  314. data/lib/rigor/inference/struct_fold_safety.rb +4 -6
  315. data/lib/rigor/inference/synthetic_method_scanner.rb +40 -17
  316. data/lib/rigor/inference/toplevel_def_census.rb +67 -0
  317. data/lib/rigor/inference/unknown_store_widening.rb +103 -4
  318. data/lib/rigor/inference/unthreaded_rebinds.rb +2 -3
  319. data/lib/rigor/inference/untyped_rejoin.rb +1 -1
  320. data/lib/rigor/inference/void_tail_summary.rb +29 -9
  321. data/lib/rigor/language_server/buffer_table.rb +3 -3
  322. data/lib/rigor/language_server/completion_provider.rb +26 -20
  323. data/lib/rigor/language_server/config_warning_announcer.rb +28 -0
  324. data/lib/rigor/language_server/diagnostic_publisher.rb +8 -3
  325. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  326. data/lib/rigor/language_server/folding_range_provider.rb +1 -2
  327. data/lib/rigor/language_server/hover_provider.rb +1 -7
  328. data/lib/rigor/language_server/hover_renderer.rb +6 -8
  329. data/lib/rigor/language_server/project_context.rb +39 -1
  330. data/lib/rigor/language_server/publish_batcher.rb +3 -4
  331. data/lib/rigor/language_server/selection_range_provider.rb +2 -4
  332. data/lib/rigor/language_server/server.rb +7 -3
  333. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  334. data/lib/rigor/lens/claims/comparison.rb +78 -0
  335. data/lib/rigor/lens/claims/request.rb +131 -0
  336. data/lib/rigor/lens/claims/type_reader.rb +216 -0
  337. data/lib/rigor/lens/claims.rb +208 -0
  338. data/lib/rigor/lens/contract_reader.rb +5 -0
  339. data/lib/rigor/lens/document.rb +1 -1
  340. data/lib/rigor/lens/file_reader.rb +21 -8
  341. data/lib/rigor/lens/plugins.rb +1 -1
  342. data/lib/rigor/lens/run.rb +8 -0
  343. data/lib/rigor/lens.rb +1 -1
  344. data/lib/rigor/library_probe.rb +68 -0
  345. data/lib/rigor/mcp/server.rb +73 -2
  346. data/lib/rigor/mcp.rb +1 -1
  347. data/lib/rigor/memo_audit.rb +282 -0
  348. data/lib/rigor/plugin/access_denied_error.rb +1 -2
  349. data/lib/rigor/plugin/base.rb +15 -2
  350. data/lib/rigor/plugin/bundled_catalog.rb +1 -12
  351. data/lib/rigor/plugin/io_boundary.rb +26 -4
  352. data/lib/rigor/plugin/loader.rb +25 -8
  353. data/lib/rigor/plugin/macro/heredoc_template.rb +7 -8
  354. data/lib/rigor/plugin/macro/trait_registry.rb +6 -9
  355. data/lib/rigor/plugin/manifest.rb +16 -16
  356. data/lib/rigor/plugin/registry.rb +16 -5
  357. data/lib/rigor/plugin/services.rb +2 -3
  358. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +11 -0
  359. data/lib/rigor/plugin/trust_policy.rb +48 -2
  360. data/lib/rigor/plugin/type_node_resolver.rb +2 -4
  361. data/lib/rigor/plugin.rb +6 -8
  362. data/lib/rigor/plugin_gap_advisory.rb +5 -1
  363. data/lib/rigor/project_environment.rb +16 -2
  364. data/lib/rigor/protection/analysis_guard.rb +1 -1
  365. data/lib/rigor/protection/discovery_seed.rb +44 -5
  366. data/lib/rigor/protection/mutation_cache.rb +4 -1
  367. data/lib/rigor/protection/mutator.rb +1 -1
  368. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  369. data/lib/rigor/rbs_extended/hkt_directives.rb +6 -10
  370. data/lib/rigor/rbs_extended/reporter.rb +27 -3
  371. data/lib/rigor/rbs_extended.rb +2 -3
  372. data/lib/rigor/reflection/constant_ancestors.rb +81 -22
  373. data/lib/rigor/reflection/declaration_collectors.rb +55 -0
  374. data/lib/rigor/reflection/declarations.rb +58 -0
  375. data/lib/rigor/reflection/project_method.rb +177 -0
  376. data/lib/rigor/reflection.rb +2 -0
  377. data/lib/rigor/rooted_glob.rb +32 -1
  378. data/lib/rigor/run_scoped_slots.rb +38 -0
  379. data/lib/rigor/runtime/jit.rb +1 -1
  380. data/lib/rigor/scope/discovery_index.rb +168 -8
  381. data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
  382. data/lib/rigor/scope/resolution_chain.rb +901 -54
  383. data/lib/rigor/scope.rb +644 -37
  384. data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
  385. data/lib/rigor/sig_gen/classification.rb +18 -1
  386. data/lib/rigor/sig_gen/effect_annotation.rb +3 -3
  387. data/lib/rigor/sig_gen/generator.rb +588 -66
  388. data/lib/rigor/sig_gen/generic_header_scan.rb +189 -0
  389. data/lib/rigor/sig_gen/inline_declarations.rb +2 -2
  390. data/lib/rigor/sig_gen/layout_index.rb +3 -3
  391. data/lib/rigor/sig_gen/method_candidate.rb +37 -4
  392. data/lib/rigor/sig_gen/observation_collector.rb +14 -7
  393. data/lib/rigor/sig_gen/observed_call.rb +1 -1
  394. data/lib/rigor/sig_gen/path_mapper.rb +6 -0
  395. data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
  396. data/lib/rigor/sig_gen/renderer.rb +15 -9
  397. data/lib/rigor/sig_gen/skip_reason_catalog.rb +62 -2
  398. data/lib/rigor/sig_gen/type_name_qualification.rb +69 -0
  399. data/lib/rigor/sig_gen/write_reporter.rb +63 -14
  400. data/lib/rigor/sig_gen/write_result.rb +23 -12
  401. data/lib/rigor/sig_gen/writer.rb +238 -37
  402. data/lib/rigor/sig_gen.rb +1 -0
  403. data/lib/rigor/source/forwardable_delegators.rb +122 -0
  404. data/lib/rigor/source/node_children.rb +13 -0
  405. data/lib/rigor/type/acceptance_router.rb +2 -2
  406. data/lib/rigor/type/accepts_result.rb +1 -1
  407. data/lib/rigor/type/app.rb +4 -6
  408. data/lib/rigor/type/combinator.rb +173 -21
  409. data/lib/rigor/type/constant.rb +4 -5
  410. data/lib/rigor/type/hash_shape.rb +21 -40
  411. data/lib/rigor/type/interface.rb +60 -0
  412. data/lib/rigor/type/union.rb +1 -1
  413. data/lib/rigor/type.rb +1 -0
  414. data/lib/rigor/type_node/identifier.rb +1 -2
  415. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  416. data/lib/rigor/type_node/string_literal.rb +1 -2
  417. data/lib/rigor/type_node/symbol_literal.rb +1 -2
  418. data/lib/rigor/type_node/union.rb +3 -6
  419. data/lib/rigor/type_of_memo/serving.rb +99 -0
  420. data/lib/rigor/type_of_memo/stats.rb +43 -0
  421. data/lib/rigor/type_of_memo.rb +337 -0
  422. data/lib/rigor/typing_state.rb +1094 -0
  423. data/lib/rigor/value_semantics.rb +1 -10
  424. data/lib/rigor/version.rb +1 -1
  425. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +2 -2
  426. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +2 -3
  427. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -1
  428. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +38 -36
  429. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +4 -2
  430. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -4
  431. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -1
  432. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -5
  433. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +0 -1
  434. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +2 -1
  435. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +3 -3
  436. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +115 -36
  437. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
  438. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
  439. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  440. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
  441. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
  442. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -3
  443. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
  444. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +3 -2
  445. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +2 -2
  446. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +21 -31
  447. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +10 -16
  448. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +132 -33
  449. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -6
  450. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
  451. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +54 -17
  452. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
  453. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +3 -4
  454. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +1 -1
  455. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +11 -14
  456. data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +1 -1
  457. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +1 -1
  458. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/symbolize_keys.rb +102 -0
  459. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +30 -9
  460. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +55 -13
  461. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
  462. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
  463. data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +3 -3
  464. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +1 -3
  465. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +6 -6
  466. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +5 -6
  467. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
  468. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +2 -3
  469. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -8
  470. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +0 -1
  471. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
  472. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +10 -18
  473. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +0 -1
  474. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +0 -2
  475. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +8 -7
  476. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +0 -2
  477. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +6 -7
  478. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
  479. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +2 -4
  480. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +2 -5
  481. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +8 -2
  482. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +6 -6
  483. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
  484. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +3 -8
  485. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +2 -3
  486. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -8
  487. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +200 -4
  488. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -3
  489. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +1 -1
  490. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +4 -11
  491. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +4 -3
  492. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +2 -1
  493. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +5 -5
  494. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -2
  495. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +3 -4
  496. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +6 -8
  497. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +3 -2
  498. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +4 -5
  499. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +6 -7
  500. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +210 -3
  501. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
  502. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +2 -2
  503. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
  504. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +97 -24
  505. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +2 -3
  506. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
  507. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +4 -8
  508. data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +4 -4
  509. data/sig/rigor/analysis/baseline.rbs +0 -3
  510. data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
  511. data/sig/rigor/analysis/fact_store.rbs +2 -2
  512. data/sig/rigor/analysis/reachability/scan_cache.rbs +1 -1
  513. data/sig/rigor/cache/descriptor.rbs +14 -0
  514. data/sig/rigor/cli/diff_command.rbs +1 -1
  515. data/sig/rigor/cli/explain_command.rbs +1 -1
  516. data/sig/rigor/effects/registry.rbs +2 -2
  517. data/sig/rigor/environment.rbs +4 -1
  518. data/sig/rigor/inference.rbs +4 -1
  519. data/sig/rigor/plugin/additional_initializer.rbs +0 -5
  520. data/sig/rigor/plugin/base.rbs +53 -18
  521. data/sig/rigor/plugin/effect_attribution.rbs +3 -3
  522. data/sig/rigor/plugin/effect_edge.rbs +1 -1
  523. data/sig/rigor/plugin/effect_entry_points.rbs +3 -3
  524. data/sig/rigor/plugin/interfaces.rbs +51 -0
  525. data/sig/rigor/plugin/io_boundary.rbs +3 -3
  526. data/sig/rigor/plugin/manifest.rbs +9 -9
  527. data/sig/rigor/plugin/node_context.rbs +21 -0
  528. data/sig/rigor/plugin/protocol_contract.rbs +3 -7
  529. data/sig/rigor/reflection.rbs +37 -0
  530. data/sig/rigor/scope.rbs +43 -4
  531. data/sig/rigor/trinary.rbs +1 -0
  532. data/sig/rigor/type.rbs +9 -3
  533. data/sig/rigor.rbs +3 -2
  534. data/skills/rigor-ask/SKILL.md +1 -0
  535. data/skills/rigor-plugin-author/SKILL.md +4 -4
  536. data/skills/rigor-plugin-author/references/02-walker-and-types.md +5 -5
  537. data/skills/rigor-plugin-review/SKILL.md +3 -3
  538. data/skills/rigor-project-init/references/01-detect.md +1 -0
  539. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +17 -2
  540. data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
  541. data/skills/rigor-rbs-contract/SKILL.md +147 -0
  542. data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
  543. data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
  544. data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
  545. data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
  546. data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
  547. data/skills/rigor-rbs-setup/SKILL.md +3 -1
  548. data/skills/rigor-type-oracle/SKILL.md +26 -1
  549. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
  550. metadata +82 -3
@@ -155,6 +155,10 @@ module Rigor
155
155
  ].freeze
156
156
  Ractor.make_shareable(TOOLS)
157
157
 
158
+ # The tools that read a configuration — the ones taking a `config` argument — and so report its warnings.
159
+ CONFIG_TOOLS = TOOLS.filter_map { |tool| tool[:name] if tool.dig(:inputSchema, :properties, :config) }.freeze
160
+ Ractor.make_shareable(CONFIG_TOOLS)
161
+
158
162
  def initialize(config_path: nil, err: $stderr)
159
163
  @config_path = config_path
160
164
  @err = err
@@ -165,7 +169,7 @@ module Rigor
165
169
  id = request["id"]
166
170
  method_name = request["method"]
167
171
 
168
- # Notifications carry no `id` and require no response.
172
+ # A notification carries no `id` and requires no response.
169
173
  return nil if id.nil?
170
174
 
171
175
  case method_name
@@ -181,8 +185,38 @@ module Rigor
181
185
  end
182
186
  end
183
187
 
188
+ # ADR-126 WD4 — `rigor_type_of` and `rigor_explain` print documents whose top-level shape changes in the next
189
+ # minor, and their CLI says so on stderr, which `tool_result` drops once there is a document. The notice is
190
+ # carried to the agent as its own `text` content block, the same text the CLI printed, so the document block
191
+ # stays byte-for-byte what the CLI wrote. When stdout is empty the error text already is stderr.
192
+ SHAPE_NOTICE_TOOLS = %w[rigor_type_of rigor_explain].freeze
193
+
184
194
  private
185
195
 
196
+ def finish_tool_result(name, args, out, err, exit_code)
197
+ result = with_config_warnings(tool_result(out, err, exit_code), name, args, out)
198
+ result = with_shape_notice(result, name, out, err)
199
+ with_path_warnings(result, name, out, err)
200
+ end
201
+
202
+ # #2206 — `rigor coverage` writes its path warnings (a missing path skipped beside files it measures) to stderr,
203
+ # which a successful run drops. The tool carries the CLI's whole stderr (path warnings and any other warnings)
204
+ # as a second text block, unfiltered, so an agent sees what was skipped.
205
+ def with_path_warnings(result, name, out, err)
206
+ return result unless name == "rigor_coverage" && !out.empty? && !err.strip.empty?
207
+
208
+ result.merge(content: result.fetch(:content) + [{ type: "text", text: err }])
209
+ end
210
+
211
+ def with_shape_notice(result, name, out, err)
212
+ return result unless SHAPE_NOTICE_TOOLS.include?(name) && !out.empty?
213
+
214
+ notice = err[CLI::JsonDocument::NOTICE_PATTERN]
215
+ return result unless notice
216
+
217
+ result.merge(content: result.fetch(:content) + [{ type: "text", text: notice }])
218
+ end
219
+
186
220
  def handle_initialize(id)
187
221
  require_relative "../version"
188
222
  success(id, {
@@ -201,7 +235,7 @@ module Rigor
201
235
  require_relative "../cli"
202
236
  exit_code = with_captured_streams(out_io, err_io) { CLI.new(argv, out: out_io, err: err_io).run }
203
237
 
204
- success(id, tool_result(out_io.string, err_io.string, exit_code))
238
+ success(id, finish_tool_result(name, args, out_io.string, err_io.string, exit_code))
205
239
  rescue SystemExit => e
206
240
  # Defence against any command that exits: it must not take the server down with it.
207
241
  success(id, { content: [{ type: "text", text: "Command exited (status #{e.status})" }], isError: true })
@@ -221,6 +255,43 @@ module Rigor
221
255
  { content: [{ type: "text", text: out.empty? ? err : out }], isError: is_error }
222
256
  end
223
257
 
258
+ # Issue #2231 — `rigor check` puts {ConfigAudit} warnings in its JSON `config_warnings`; the other tools read the
259
+ # same configuration and said nothing about it, so an agent saw `call.undefined-method` on a project whose
260
+ # `.rigor.yml` misspelt the key meant to silence it and got no hint why. Every tool that takes a `config`
261
+ # argument now carries them: a second `text` content block holding `{"config_warnings": [...]}`, each entry the
262
+ # shape check's JSON uses. The tool's own document is left byte-for-byte as its CLI printed it, and an output
263
+ # that already carries the key (check's) gets no second copy. Nothing is added when there is nothing to say,
264
+ # nor when the configuration does not load — the CLI has already reported that.
265
+ def with_config_warnings(result, name, args, out)
266
+ return result unless CONFIG_TOOLS.include?(name)
267
+
268
+ warnings = config_warnings(present_config(args["config"]) || @config_path)
269
+ return result if warnings.empty? || carries_config_warnings?(out)
270
+
271
+ block = { type: "text", text: JSON.generate({ "config_warnings" => warnings.map(&:to_h) }) }
272
+ result.merge(content: result.fetch(:content) + [block])
273
+ end
274
+
275
+ # Only a configuration that does not load is "no warnings to show": the CLI has already reported that error.
276
+ # A failure in ConfigAudit itself is a bug and surfaces as an internal error rather than reading as clean.
277
+ def config_warnings(config_path)
278
+ require_relative "../configuration"
279
+ require_relative "../config_audit"
280
+ begin
281
+ configuration = Configuration.load(config_path)
282
+ rescue ConfigurationError
283
+ return []
284
+ end
285
+ ConfigAudit.warnings(configuration)
286
+ end
287
+
288
+ def carries_config_warnings?(out)
289
+ document = JSON.parse(out)
290
+ document.is_a?(Hash) && document.key?("config_warnings")
291
+ rescue JSON::ParserError
292
+ false
293
+ end
294
+
224
295
  # The JSON-RPC channel is the process's stdout, so nothing a command writes past its `out:` (an option
225
296
  # parser's `--help`, a stray `puts`) may reach it while the command runs.
226
297
  def with_captured_streams(out_io, err_io)
data/lib/rigor/mcp.rb CHANGED
@@ -4,7 +4,7 @@ module Rigor
4
4
  # The MCP (Model Context Protocol) server subsystem. See `docs/adr/33-mcp-server.md` for the design.
5
5
  #
6
6
  # Entry point: `rigor mcp --transport stdio`. The server exposes Rigor's analysis tools (check, type-of, triage,
7
- # annotate, sig-gen, explain, coverage) as MCP tool calls over a newline-delimited JSON-RPC 2.0 stdio stream.
7
+ # lens, annotate, sig-gen, explain, coverage) as MCP tool calls over a newline-delimited JSON-RPC 2.0 stdio stream.
8
8
  module MCP
9
9
  end
10
10
  end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "typing_state"
4
+
5
+ module Rigor
6
+ # ADR-127 WD3 — the memo audit. A memo hit is correct only when the key holds every input the value
7
+ # was computed from, and a key that misses one answers a caller with another caller's value (#2516, #2524). With the
8
+ # audit on, every hit in the callee floor's memos (`resolved_census`, the unrecorded hand-off memo, the successor
9
+ # store and `def_census`) recomputes the value and raises {Mismatch} when it differs from the stored one, so a key
10
+ # defect fails on the first hit instead of waiting for an order that exposes it.
11
+ #
12
+ # Like {BudgetTrace}, a switch for specs and CI (`RIGOR_MEMO_AUDIT=1` in the environment, read once at load, or
13
+ # {enable!}): off by default, a single boolean check when off. A recomputation
14
+ # runs with the audit suspended on its thread and the memos bypassed ({suspended?}), so it reads nothing stored.
15
+ #
16
+ # A mismatch raises, and is also kept on this thread's list until {drain} takes it (ADR-129 WD5, #2785): the
17
+ # typing path rescues `StandardError` in many places, and a raise one of them swallows would turn the defect into
18
+ # an ordinary fallback answer. The runner drains the list after each file and reports what it holds the way it
19
+ # reports a mismatch no rescue caught, and {raise_undrained!} fails the run on what a file never drained.
20
+ #
21
+ # **The key audit** (ADR-129 WD1) checks the other half at every miss: a Memo, or a Table a computation fills (the
22
+ # ADR-84 return memo), whose miss computation reads a Frame must key on the state id or on that Frame's value.
23
+ # {key_audit} runs a miss computation with its Frame reads noted ({TypingState::KeyAudit}) and compares them with
24
+ # the Frame terms the key read through {TypingState::State#key_frame} when {key} built it. A read Frame the key
25
+ # does not carry, and that neither the computation installed itself nor the site declared ({key_declaration}), is a
26
+ # {KeyMismatch}: kept on this thread's list like a raised mismatch, and not raised, since the computation's answer
27
+ # is correct for this caller.
28
+ module MemoAudit
29
+ # A memo hit whose stored value differs from a fresh computation.
30
+ class Mismatch < StandardError; end
31
+
32
+ # A memo miss whose computation read a Frame its key does not carry (ADR-129 WD1's key audit).
33
+ class KeyMismatch < Mismatch; end
34
+
35
+ # What a miss site declares beside its key terms (ADR-129 WD1), by Frame slot name:
36
+ # - `forced`: Frames the memo installs at a constant (or a value derived from its key) before the key is built,
37
+ # so the computation reads that form rather than the caller's;
38
+ # - `gaps`: `slot name => reason` naming the issue of a known key gap, read without a key term and reported as a
39
+ # named exception rather than a mismatch;
40
+ # - `carried`: `slot name => reason` for Frames another key term carries (a run-level flag, fixed while the run
41
+ # generation the memo's bucket keys on is), counted as key terms.
42
+ KeyDeclaration = Data.define(:forced, :gaps, :carried)
43
+
44
+ # ADR-129 WD1 — a Frame, installed around a recomputation. `@enabled` is a process switch, outside the typing
45
+ # state (WD5). G1′ witness: a recomputation types a callee body, and the memo hits inside it read the flag.
46
+ SUSPENDED = TypingState.frame(
47
+ :memo_audit_suspended, reason: "set around an audit recomputation, which bypasses every memo",
48
+ witness: "spec/fixtures/typing_state_witnesses/memo_audit_suspended.rb"
49
+ )
50
+ private_constant :SUSPENDED
51
+
52
+ # ADR-129 WD1 — a Resource: the mismatches raised on this thread and not drained yet. Typing never reads it; an
53
+ # append refuses every open capture, which only an audited run reaches.
54
+ MISMATCHES = TypingState.resource(
55
+ :memo_audit_mismatches,
56
+ run_scoped: true,
57
+ reason: "the audit's mismatches on this thread, appended where raised, drained per file, never read by typing"
58
+ )
59
+ private_constant :MISMATCHES
60
+
61
+ EMPTY = [].freeze
62
+ private_constant :EMPTY
63
+
64
+ # ADR-129 WD1 — the Frames every key audit names as exceptions rather than mismatches, with their reasons: the
65
+ # audit's own switch and the process switches whose installed recorders a computation may file into.
66
+ KEY_AUDIT_SWITCHES = {
67
+ memo_audit_suspended: "the audit's own switch: a nested hit's check reads it",
68
+ flow_tracer: "a process switch (WD5): the tracer is an output, and the type_of memo is shut under one (WD4)",
69
+ effects_collector: "a process switch (WD5): recording into its contents touches a Resource",
70
+ dependency_recorder: "a process switch (WD5): the recorder files ADR-46 dependency edges, which no type reads",
71
+ self_call_resolution_recorder: "a process switch (WD5): its records are a journaled Table, never read back"
72
+ }.freeze
73
+
74
+ # ADR-129 WD1 — a Resource: the named exceptions key audits on this thread met, `[label, slot name] => reason`.
75
+ # Typing never reads it.
76
+ KEY_EXCEPTIONS = TypingState.resource(
77
+ :memo_audit_key_exceptions,
78
+ run_scoped: true,
79
+ reason: "the key audit's named exceptions met on this thread, for specs and reports; never read by typing"
80
+ )
81
+ private_constant :KEY_EXCEPTIONS
82
+
83
+ NO_DECLARATION = KeyDeclaration.new(forced: EMPTY, gaps: {}.freeze, carried: {}.freeze)
84
+ private_constant :NO_DECLARATION
85
+
86
+ @enabled = %w[1 true].include?(ENV["RIGOR_MEMO_AUDIT"].to_s.downcase)
87
+
88
+ module_function
89
+
90
+ def enabled? = @enabled
91
+
92
+ # True while an audit recomputation runs on this thread. The callee floor's memos are then bypassed (read and
93
+ # write), so the recomputation reads no stored entry of another memo: a defect shared across a table (a stale
94
+ # `resolved_census(:edges)` entry) would otherwise make the recomputed value match the stored one.
95
+ def suspended? = TypingState.current[SUSPENDED]
96
+
97
+ def enable! = (@enabled = true)
98
+
99
+ def disable! = (@enabled = false)
100
+
101
+ # `stored`, after checking it against `yield` when the audit is on. `label` and the key parts (`key`, `key2`) name
102
+ # the memo entry in the failure; they are passed apart so an audit that is off allocates nothing.
103
+ def check(label, stored, key, key2 = nil, &)
104
+ return stored unless @enabled && !TypingState.current[SUSPENDED]
105
+
106
+ fresh = suspended(&)
107
+ return stored if fresh == stored
108
+
109
+ mismatch = Mismatch.new(
110
+ "#{label} memo hit differs from a fresh computation for #{[key, key2].inspect[0, 120]}: " \
111
+ "stored #{stored.inspect[0, 200]}, fresh #{fresh.inspect[0, 200]}"
112
+ )
113
+ state = TypingState.current
114
+ state.append(MISMATCHES, mismatch)
115
+ raise mismatch
116
+ end
117
+
118
+ # ADR-129 G1′ — a `type_of` begins on this thread, for the matching {leave_type_of}; the caller guards on
119
+ # {enabled?}, so an audit that is off costs one boolean check. Fails (as a {Mismatch} on the list, not raised) on an
120
+ # installed `typing_unreachable` Frame ({TypingState::FrameAudit}).
121
+ def enter_type_of
122
+ state = TypingState.current
123
+ state.extend(TypingState::FrameAudit) unless state.is_a?(TypingState::FrameAudit)
124
+ state.enter_type_of
125
+ end
126
+
127
+ # ADR-129 G1′ — the `type_of` memo's shadow reached its table on `state`; the caller guards on {enabled?}. Fails (as
128
+ # a {Mismatch} on the list, not raised) on an installed `memo_shut_while_installed` Frame.
129
+ def note_memo_lookup(state)
130
+ state.extend(TypingState::FrameAudit) unless state.is_a?(TypingState::FrameAudit)
131
+ state.note_memo_lookup
132
+ end
133
+
134
+ # ADR-129 G1′ — the `type_of` {enter_type_of} tracked ends.
135
+ def leave_type_of
136
+ TypingState.current.leave_type_of
137
+ end
138
+
139
+ # Keeps one {Mismatch} per message on the list until it is drained: a Frame declaration that does not hold is
140
+ # reported at every `type_of` it meets.
141
+ def report_frame_declaration(state, message)
142
+ pending = state.read(MISMATCHES)
143
+ return if pending&.any? { |mismatch| mismatch.message == message }
144
+
145
+ mismatch = Mismatch.new(message)
146
+ mismatch.set_backtrace(caller.drop_while { |frame| frame.include?("/typing_state.rb:") })
147
+ state.append(MISMATCHES, mismatch)
148
+ end
149
+
150
+ # The mismatches raised on this thread since the last drain, oldest first, and an empty list for the next file.
151
+ # Each has been raised, so it carries the backtrace a crash row is built from.
152
+ def drain
153
+ state = TypingState.current
154
+ mismatches = state.read(MISMATCHES)
155
+ return EMPTY if mismatches.nil? || mismatches.empty?
156
+
157
+ state.write(MISMATCHES, nil)
158
+ mismatches
159
+ end
160
+
161
+ # ADR-129 WD1 — checks a `carried:` claim at the install of a run-level flag (`flag`, ADR-130's erasure or
162
+ # dispatch-off frame). The return memo counts the flag as carried by its bucket's run-generation key, which holds
163
+ # only while the flag is installed around a whole run: with the audit on, an install that finds the return memo
164
+ # holding entries (a run in progress, or one that was not released) raises instead of silently mixing answers
165
+ # computed under both values.
166
+ def check_run_level_install(flag)
167
+ return unless @enabled
168
+
169
+ held = TypingState.current[TypingState.slot(:user_method_return_memo).index]
170
+ return if held.nil? || held[1].empty?
171
+
172
+ raise Mismatch, "#{flag} installed while the return memo holds entries: the memo's key carries it only as a " \
173
+ "flag installed around a whole run (ADR-129 WD1 key audit)"
174
+ end
175
+
176
+ # Raises the first mismatch no file drained (one raised outside every file's analysis), so a run that ends with
177
+ # one fails instead of releasing it with the run's other slots.
178
+ def raise_undrained!
179
+ return unless @enabled
180
+
181
+ mismatches = drain
182
+ raise mismatches.first unless mismatches.empty?
183
+ end
184
+
185
+ def suspended(&)
186
+ TypingState.current.with(SUSPENDED, true, &)
187
+ end
188
+
189
+ # A frozen {KeyDeclaration} for a miss site, built once into a constant so an audit that is off allocates nothing.
190
+ def key_declaration(forced: EMPTY, gaps: {}, carried: {})
191
+ KeyDeclaration.new(forced: forced.map(&:to_sym).freeze, gaps: gaps.transform_keys(&:to_sym).freeze,
192
+ carried: carried.transform_keys(&:to_sym).freeze)
193
+ end
194
+
195
+ # The key the block builds. When the audit is on, the Frames the block reads through
196
+ # {TypingState::State#key_frame} are remembered as the key's terms for the {key_audit} of the miss that follows.
197
+ def key(&)
198
+ return yield unless @enabled
199
+
200
+ auditing_state.collect_key_terms(&)
201
+ end
202
+
203
+ # The block's value: a memo's miss computation, run under ADR-129 WD1's Memo rule
204
+ # ({TypingState::State#memo_miss}: a miss that writes a Table while a capture is open touches), keyed by `key`
205
+ # ({key}, or nil for a key that reads no Frame) and declared by `declaration` ({key_declaration}). When the audit
206
+ # is on, a Frame the computation reads that the key does not carry is a {KeyMismatch} on this thread's list, named
207
+ # by `label` (the slot's `audit:` or `computed:` label), unless it is internal, forced, carried, a declared gap or
208
+ # a {KEY_AUDIT_SWITCHES} switch.
209
+ #
210
+ # A computation that raises or throws is not judged, and its audit is closed on the way out. A computation that
211
+ # returns is judged whether or not its memo then stores it (the return memo's WD3 bracket may refuse it), which
212
+ # only reports more. An audit recomputation ({suspended?}) is not judged either, but it still opens the site's
213
+ # declaration, so a gap it declares reaches the audits around it as it does on a miss: the verdict of an enclosing
214
+ # miss does not depend on whether this memo hit.
215
+ def key_audit(label, declaration = nil, key = nil, &)
216
+ return TypingState.current.memo_miss(&) unless @enabled
217
+
218
+ state = auditing_state
219
+ declaration ||= NO_DECLARATION
220
+ judged = !state[SUSPENDED]
221
+ terms = state.key_terms_for(key) if judged
222
+ state.open_key_audit(label, declaration.gaps)
223
+ begin
224
+ value = state.memo_miss(&)
225
+ ensure
226
+ reads = state.close_key_audit
227
+ end
228
+ judge_key_audit(state, label, declaration, terms, reads) if judged
229
+ value
230
+ end
231
+
232
+ # The named exceptions key audits on this thread have met since the run began, `[label, slot name] => reason`.
233
+ def key_exceptions
234
+ TypingState.current.read(KEY_EXCEPTIONS) || {}
235
+ end
236
+
237
+ def auditing_state
238
+ state = TypingState.current
239
+ state.extend(TypingState::KeyAudit) unless state.is_a?(TypingState::KeyAudit)
240
+ state
241
+ end
242
+
243
+ def judge_key_audit(state, label, declaration, terms, reads)
244
+ return if reads.empty? || terms.include?(TypingState::KeyAudit::STATE_ID)
245
+
246
+ reads.each do |slot, via|
247
+ name = TypingState.slots[slot].name
248
+ next note_key_exception(state, label, name, "through the #{via[0]} miss: #{via[1]}") unless via == true
249
+ next if terms.include?(slot) || declaration.forced.include?(name) || declaration.carried.key?(name)
250
+
251
+ reason = key_exception_reason(declaration, name)
252
+ reason ? note_key_exception(state, label, name, reason) : report_key_mismatch(state, label, name)
253
+ end
254
+ end
255
+
256
+ # Why a miss may read the Frame `name` without a key term, or nil: a gap its site declares, or a switch.
257
+ def key_exception_reason(declaration, name) = declaration.gaps[name] || KEY_AUDIT_SWITCHES[name]
258
+
259
+ def note_key_exception(state, label, name, reason)
260
+ seen = state.acquire(KEY_EXCEPTIONS) { {} }
261
+ seen[[label, name]] ||= reason
262
+ end
263
+
264
+ # Keeps one {KeyMismatch} per message on the list until it is drained: a key defect repeats at every miss.
265
+ def report_key_mismatch(state, label, name)
266
+ message = "key audit: the #{label} miss read the Frame #{name}, which its key does not carry"
267
+ pending = state.read(MISMATCHES)
268
+ return if pending&.any? { |mismatch| mismatch.message == message }
269
+
270
+ mismatch = KeyMismatch.new(message)
271
+ mismatch.set_backtrace(caller.drop_while { |frame| frame.include?("/memo_audit.rb:") })
272
+ state.append(MISMATCHES, mismatch)
273
+ end
274
+ end
275
+
276
+ # The engine reaches the audit as `Inference::MemoAudit`; it lives at `lib/rigor/memo_audit.rb` so the value layer
277
+ # (`Reflection`, `Scope`) can load it without loading an `inference/` file (ADR-104's warm effects probe budget), as
278
+ # `TypingState` does (#2796).
279
+ module Inference
280
+ MemoAudit = Rigor::MemoAudit
281
+ end
282
+ end
@@ -6,8 +6,7 @@ module Rigor
6
6
  # analyzer can convert the exception into a `:plugin_loader` diagnostic without crashing `rigor check`.
7
7
  #
8
8
  # The policy is documented in [ADR-2 § "Plugin Trust and I/O Policy"](../../../docs/adr/2-extension-api.md).
9
- # Slice 2's surface lists `:read_outside_scope` and `:network_disabled` as the two reason codes; future slices
10
- # may extend the set.
9
+ # `reason:` is one of `:read_outside_scope`, `:network_disabled`, `:url_fetch_failed` or `:url_body_too_large`.
11
10
  class AccessDeniedError < StandardError
12
11
  attr_reader :reason, :resource
13
12
 
@@ -945,6 +945,14 @@ module Rigor
945
945
  @io_boundary ||= services.io_boundary_for(manifest.id)
946
946
  end
947
947
 
948
+ # The boundary {#io_boundary} has built, or nil when this plugin never asked for one, without building it: a
949
+ # plugin that never built one read nothing through it. Engine-internal ({Analysis::RunDisclosureCredit} sends it,
950
+ # being outside the class), so it is private and part of no plugin-facing contract.
951
+ #
952
+ # @api private
953
+ def built_io_boundary = @io_boundary
954
+ private :built_io_boundary
955
+
948
956
  # ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that performs a `Cache::Store#fetch_or_validate`
949
957
  # round-trip for the named producer (the ADR-45 record-and-validate path). The entry is KEYED on the
950
958
  # stable identity inputs — the plugin's `PluginEntry` template (id, version, config_hash) composed with
@@ -1064,14 +1072,19 @@ module Rigor
1064
1072
  # The gate for one `dynamic_return` rule. Method-name gate first — a Symbol-array probe vs the receiver
1065
1073
  # ancestry resolution below (ADR-52 WD1); both are pure predicates, so order only affects cost. A
1066
1074
  # receiver-less rule (ADR-52 WD2) skips the ancestry check entirely and fires on the method name alone.
1075
+ # The engine also offers the nameless nodes of #2717 (the `&:sym` BlockArgumentNode, the compound
1076
+ # write nodes) to a plugin `build_name_gates` left ungated; only `CallNode` answers `name`, so a
1077
+ # name-gated rule declines them (`include?(nil)` is false) rather than raising past the later rules
1078
+ # (#2764).
1067
1079
  def dynamic_return_rule_applies?(rule, call_node, receiver_type, environment, scope)
1068
- return false if rule[:methods] && !resolved_dynamic_return_methods(rule).include?(call_node.name)
1080
+ name = call_node.respond_to?(:name) ? call_node.name : nil
1081
+ return false if rule[:methods] && !resolved_dynamic_return_methods(rule).include?(name)
1069
1082
 
1070
1083
  if rule[:file_methods]
1071
1084
  # The path is read here, not in `dynamic_return_type`, so a spec-double scope without
1072
1085
  # `source_path` only affects `file_methods:` rules (other gate forms never touch it).
1073
1086
  path = scope.respond_to?(:source_path) ? scope.source_path : nil
1074
- return false unless resolved_dynamic_return_file_methods(rule, path).include?(call_node.name)
1087
+ return false unless resolved_dynamic_return_file_methods(rule, path).include?(name)
1075
1088
  end
1076
1089
 
1077
1090
  entries = resolved_dynamic_return_receiver_entries(rule)
@@ -127,7 +127,7 @@ module Rigor
127
127
  def require_bundled_plugins
128
128
  return {} unless File.directory?(BUNDLED_PLUGINS_ROOT)
129
129
 
130
- add_bundled_lib_dirs
130
+ Loader.add_bundled_lib_dirs
131
131
  Dir.children(BUNDLED_PLUGINS_ROOT).sort.each_with_object({}) do |gem_name, resolved|
132
132
  path = Loader.bundled_plugin_path(gem_name)
133
133
  next if path.nil?
@@ -137,17 +137,6 @@ module Rigor
137
137
  end
138
138
  end
139
139
 
140
- # A bundled plugin's entry file requires its own tree by bare feature name (`require
141
- # "rigor/plugin/activestorage"`), which resolves only with that plugin's `lib` on the load path — the
142
- # loader gets it from the bundle, and this catalogue has to arrange it itself. Prepending the engine's
143
- # own copies matches ADR-93 WD5's anti-skew rule, where the bundled copy always wins.
144
- def add_bundled_lib_dirs
145
- Dir.children(BUNDLED_PLUGINS_ROOT).sort.each do |gem_name|
146
- dir = File.join(BUNDLED_PLUGINS_ROOT, gem_name, "lib")
147
- $LOAD_PATH.unshift(dir) if File.directory?(dir) && !$LOAD_PATH.include?(dir)
148
- end
149
- end
150
-
151
140
  # A plugin gem that fails to load is skipped rather than fatal: the catalogue exists to *advise*, and
152
141
  # a broken bundled copy is already reported by `rigor doctor`'s plugin checks for the plugins the
153
142
  # project actually enabled. The failure is recorded in {load_failures} so a missing entry stays
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "digest"
4
+ require_relative "../typing_state"
4
5
 
5
6
  require_relative "access_denied_error"
6
7
  require_relative "../rooted_glob"
@@ -17,7 +18,7 @@ module Rigor
17
18
  # scope and feed the cache descriptor; contributions built on top of out-of-scope reads will not
18
19
  # invalidate correctly.
19
20
  #
20
- # Slice 2 ships a minimal surface:
21
+ # The surface:
21
22
  #
22
23
  # - `#read_file(path)` — validates against the policy, returns the file's contents, and adds a
23
24
  # digest-keyed {Cache::Descriptor::FileEntry} to the boundary's accumulated descriptor. A read that
@@ -52,11 +53,20 @@ module Rigor
52
53
  # descriptor and any enclosing producer's descriptor then carry the served value's inputs.
53
54
  class IoBoundary
54
55
  URL_TIMEOUT_SECONDS = 10
55
- URL_MAX_BYTES = 10 * 1024 * 1024
56
+ URL_MAX_BYTES = 10_485_760 # 10 MiB
56
57
 
57
58
  ATTRIBUTION_KEY = :rigor_io_boundary_attribution
58
59
  private_constant :ATTRIBUTION_KEY
59
60
 
61
+ # ADR-129 WD3 / #2814 — the attribution sink is a Resource: a `type_of` memo hit that served an answer without
62
+ # re-running the plugin read would not append what the miss appended, so every {.attribute} refuses each open
63
+ # capture. Its storage stays the thread variable above (ADR-129 WD5 shape 2); this slot declares its kind.
64
+ SINK = TypingState.resource(
65
+ :io_boundary_attribution,
66
+ reason: "every plugin boundary read appends to the attributing sink, which a memo hit would not replay"
67
+ )
68
+ private_constant :SINK
69
+
60
70
  # ADR-45 WD2 (#1507) — runs the block with `sink` (an Array) receiving every file and listing row ANY
61
71
  # boundary records on this thread meanwhile, in addition to the boundary's own table, and returns the
62
72
  # block's value. `Analysis::Runner` wraps each file's analysis in one when it records dependencies, so the
@@ -67,16 +77,28 @@ module Rigor
67
77
  # lands in the sink. The boundary's own table is unchanged by it, including its first-existence-row-stands
68
78
  # rule: the sink records what each read observed. Only reads are credited: a value a plugin memoised reaches
69
79
  # later files without one, so the first file to trigger the read is the only one credited with it.
80
+ #
81
+ # The installing fiber's typing state rides with the sink, so a read from inside an `Enumerator` refuses the
82
+ # captures open where the analysis runs, not only those of the `Enumerator`'s own fiber ({.attribute}).
70
83
  def self.attributing(sink)
71
84
  previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
72
- Thread.current.thread_variable_set(ATTRIBUTION_KEY, sink)
85
+ Thread.current.thread_variable_set(ATTRIBUTION_KEY, [sink, TypingState.current].freeze)
73
86
  yield
74
87
  ensure
75
88
  Thread.current.thread_variable_set(ATTRIBUTION_KEY, previous)
76
89
  end
77
90
 
91
+ # Records `entry` in the attributing sink, if one is installed, and touches {SINK} on the calling fiber's typing
92
+ # state and on the installing fiber's (ADR-129 WD3, #2814).
78
93
  def self.attribute(entry)
79
- Thread.current.thread_variable_get(ATTRIBUTION_KEY)&.push(entry)
94
+ state = TypingState.current
95
+ state.touch(SINK)
96
+ sink, owner = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
97
+ return nil if sink.nil?
98
+
99
+ owner.touch(SINK)
100
+ sink.push(entry)
101
+ nil
80
102
  end
81
103
 
82
104
  attr_reader :policy, :plugin_id
@@ -90,6 +90,20 @@ module Rigor
90
90
  File.directory?(path) ? path : nil
91
91
  end
92
92
 
93
+ # Puts every bundled plugin's `lib` on `$LOAD_PATH`, so a bundled plugin that requires another by gem
94
+ # name (`require "rigor-ffi"` from `rigor-ethon`, `rigor-sassc`, `rigor-ffi-rzmq` and `rigor-rbnacl`)
95
+ # resolves without a bundle. The engine's own copies go first, which matches ADR-93 WD5's anti-skew
96
+ # rule: the bundled copy always wins. Idempotent; a trimmed packaging with no `plugins/` tree adds none.
97
+ def self.add_bundled_lib_dirs
98
+ root = File.join(ENGINE_ROOT, "plugins")
99
+ return unless File.directory?(root)
100
+
101
+ Dir.children(root).sort.each do |gem_name|
102
+ dir = File.join(root, gem_name, "lib")
103
+ $LOAD_PATH.unshift(dir) if File.directory?(dir) && !$LOAD_PATH.include?(dir)
104
+ end
105
+ end
106
+
93
107
  # @param entries — the raw `plugins:` list from the configuration.
94
108
  def load(entries)
95
109
  plugins = []
@@ -233,7 +247,13 @@ module Rigor
233
247
  # `feature_resolver` is unaffected: an absolute-path require's `$LOADED_FEATURES` entry still ends in
234
248
  # `/<gem>.rb`, the suffix {FEATURE_RESOLVER} matches.
235
249
  def require_gem!(entry)
236
- @requirer.call(self.class.bundled_plugin_path(entry[:gem]) || entry[:gem])
250
+ path = self.class.bundled_plugin_path(entry[:gem])
251
+ if path
252
+ self.class.add_bundled_lib_dirs
253
+ @requirer.call(path)
254
+ else
255
+ @requirer.call(entry[:gem])
256
+ end
237
257
  rescue ::LoadError => e
238
258
  raise LoadError.new(
239
259
  "could not load plugin gem #{entry[:gem].inspect}: #{e.message}",
@@ -272,13 +292,10 @@ module Rigor
272
292
  end
273
293
  end
274
294
 
275
- # A gem that registers more than one plugin is a meta-gem only when NONE of the ids is its own. The
276
- # FFI family (`rigor-sassc`, `rigor-ethon`, `rigor-ffi-rzmq`, `rigor-rbnacl`) each `require "rigor-ffi"`
277
- # for the shared binding analyzer, so listing one of them ALONE registered two plugins — its own and
278
- # `ffi` — and the meta-gem branch rejected the entry, telling the user to list `rigor-ffi` instead,
279
- # which activates the wrong plugin. The failure was load-order dependent (listing `rigor-ffi` first
280
- # made the nested require a no-op and the entry loaded), so it never showed up in the suite, where
281
- # some earlier example has always loaded `rigor-ffi`.
295
+ # A gem that registers several plugins is a meta-gem only when NONE of the ids is its own. The FFI family
296
+ # (`rigor-sassc`, `rigor-ethon`, `rigor-ffi-rzmq`, `rigor-rbnacl`) each `require "rigor-ffi"`, so listing
297
+ # one alone registered `ffi` too and was wrongly rejected as a meta-gem. The bug was load-order dependent
298
+ # and hid in the suite, where an earlier example always loaded `rigor-ffi`.
282
299
  #
283
300
  # `rigor-rails` stays a meta-gem under this rule: it registers eight ids and none of them is `rails`.
284
301
  def own_id_among(gem_name, ids)
@@ -48,11 +48,11 @@ module Rigor
48
48
  #
49
49
  # Slice 2 ships at the **floor**: each emit row's `name:` is the source of truth for the synthetic
50
50
  # method's name (a single `"\#{name}"` placeholder gets interpolated with the literal symbol argument at
51
- # `symbol_arg_position`). The `returns:` strings are **recorded in the manifest but not resolved**; the
52
- # engine emits synthetic methods with `Dynamic[T]` returns plus a `macro.tier_c.unresolved-return`
53
- # provenance marker. Precise return-type resolution via ADR-13's `Plugin::TypeNodeResolver` is the
54
- # **ceiling**, deferred to a later slice — the `returns:` declarations cost nothing to write today and
55
- # unlock precision then.
51
+ # `symbol_arg_position`). The synthetic method's return type is the `returns_from_arg:` lookup when it
52
+ # resolves, else a non-empty `returns:` string (`SyntheticMethodScanner#resolve_emit_return_type`); the
53
+ # dispatcher turns it into a nominal type through `MethodDispatcher#promote_via_return_type` when the
54
+ # environment knows the class. With neither, an `"untyped"` / `"void"` placeholder, or an unknown class, the
55
+ # method answers `Dynamic[Top]`. Resolution through ADR-13's `Plugin::TypeNodeResolver` is not wired.
56
56
  #
57
57
  # Engine wiring: `Inference::SyntheticMethodScanner` (slice 2b, `synthetic_method_scanner.rb`) consumes
58
58
  # `manifest.heredoc_templates`. Worked consumers: `plugins/rigor-dry-struct/` and
@@ -98,7 +98,7 @@ module Rigor
98
98
  # `\#{name}` with the call-site literal symbol) and its declared return type. The return type can be
99
99
  # a static String (resolved via `Environment#nominal_for_name` per ADR-16 slice 6b) or a
100
100
  # per-call-site lookup ({ReturnsFromArg}) — see
101
- # [ADR-18](../../../../../docs/adr/18-substrate-per-call-site-return-type.md). When both are nil, the
101
+ # [ADR-18](../../../../docs/adr/18-substrate-per-call-site-return-type.md). When both are nil, the
102
102
  # synthesised method's return type falls back to `Dynamic[Top]`.
103
103
  class Emit
104
104
  attr_reader :name, :returns, :returns_from_arg
@@ -147,8 +147,7 @@ module Rigor
147
147
  # lookup_via: { plugin_id: "dry-types", fact: :dry_type_aliases }
148
148
  # }
149
149
  #
150
- # Slice 1 (this file) ships the value class + validation only. The scanner-side arg-position
151
- # extraction + fact-store lookup land in slice 2 / 3.
150
+ # The scanner's `resolve_emit_return_type` does the arg-position extraction and fact-store lookup.
152
151
  class ReturnsFromArg
153
152
  attr_reader :position, :plugin_id, :fact
154
153