rigortype 0.3.8 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (447) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/capability_roles/capability_roles.rbs +36 -0
  4. data/data/gem_overlay/activesupport/core_ext.rbs +109 -13
  5. data/docs/handbook/02-everyday-types.md +17 -14
  6. data/docs/handbook/03-narrowing.md +37 -4
  7. data/docs/handbook/05-methods-and-blocks.md +1 -1
  8. data/docs/handbook/06-classes.md +2 -2
  9. data/docs/handbook/07-rbs-and-extended.md +14 -1
  10. data/docs/handbook/11-sig-gen.md +72 -1
  11. data/docs/handbook/README.md +1 -1
  12. data/docs/handbook/appendix-elixir.md +2 -2
  13. data/docs/handbook/appendix-go.md +2 -2
  14. data/docs/handbook/appendix-java-csharp.md +2 -2
  15. data/docs/handbook/appendix-mypy.md +2 -2
  16. data/docs/handbook/appendix-phpstan.md +1 -1
  17. data/docs/handbook/appendix-rust.md +1 -1
  18. data/docs/handbook/appendix-type-theory.md +4 -4
  19. data/docs/handbook/appendix-typescript.md +1 -1
  20. data/docs/llms.txt +2 -0
  21. data/docs/manual/02-cli-reference.md +32 -4
  22. data/docs/manual/03-configuration.md +11 -0
  23. data/docs/manual/04-diagnostics.md +11 -0
  24. data/docs/manual/07-plugins.md +15 -5
  25. data/docs/manual/08-skills.md +23 -2
  26. data/docs/manual/11-ci.md +9 -0
  27. data/docs/manual/15-type-protection-coverage.md +8 -0
  28. data/docs/manual/16-rbs-extended-annotations.md +6 -1
  29. data/docs/manual/18-removing-dead-code.md +10 -8
  30. data/docs/manual/plugins/rigor-actionpack.md +18 -0
  31. data/docs/manual/plugins/rigor-activerecord.md +28 -0
  32. data/docs/manual/plugins/rigor-activesupport-core-ext.md +32 -2
  33. data/docs/manual/plugins/rigor-rbs-inline.md +42 -1
  34. data/lib/rigor/analysis/baseline.rb +2 -2
  35. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +1 -1
  36. data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +1 -5
  37. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +10 -2
  38. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +1 -1
  39. data/lib/rigor/analysis/check_rules/published_constant_guard.rb +15 -6
  40. data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -1
  41. data/lib/rigor/analysis/check_rules/rule_walk.rb +39 -4
  42. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +1 -1
  43. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +1 -1
  44. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +2 -2
  45. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +0 -1
  46. data/lib/rigor/analysis/check_rules.rb +200 -32
  47. data/lib/rigor/analysis/crash_signature.rb +8 -24
  48. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +1 -1
  49. data/lib/rigor/analysis/dependency_source_inference/builder.rb +0 -2
  50. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +0 -2
  51. data/lib/rigor/analysis/dependency_source_inference/index.rb +5 -5
  52. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +1 -2
  53. data/lib/rigor/analysis/dependency_source_inference/walker.rb +4 -4
  54. data/lib/rigor/analysis/effects_cache_probe.rb +3 -4
  55. data/lib/rigor/analysis/erb_template_detector.rb +1 -2
  56. data/lib/rigor/analysis/fact_store.rb +6 -1
  57. data/lib/rigor/analysis/incremental.rb +16 -0
  58. data/lib/rigor/analysis/incremental_session.rb +81 -22
  59. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +0 -1
  60. data/lib/rigor/analysis/reachability/graph.rb +3 -5
  61. data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -5
  62. data/lib/rigor/analysis/reachability/project_files.rb +2 -2
  63. data/lib/rigor/analysis/reachability/scan.rb +5 -5
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +2 -2
  65. data/lib/rigor/analysis/reachability/signature_scan.rb +2 -2
  66. data/lib/rigor/analysis/result.rb +1 -3
  67. data/lib/rigor/analysis/rule_catalog.rb +4 -1
  68. data/lib/rigor/analysis/run_cache_key.rb +1 -1
  69. data/lib/rigor/analysis/run_cache_probe.rb +3 -5
  70. data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -8
  71. data/lib/rigor/analysis/runner/declaration_position.rb +1 -2
  72. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +177 -26
  73. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +2 -3
  74. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -10
  75. data/lib/rigor/analysis/runner/pool_coordinator.rb +256 -88
  76. data/lib/rigor/analysis/runner/project_pre_passes.rb +12 -6
  77. data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
  78. data/lib/rigor/analysis/runner.rb +133 -35
  79. data/lib/rigor/analysis/worker_session.rb +9 -10
  80. data/lib/rigor/bleeding_edge.rb +12 -24
  81. data/lib/rigor/builtins/hkt_builtins.rb +2 -2
  82. data/lib/rigor/builtins/imported_refinements.rb +155 -21
  83. data/lib/rigor/builtins/predefined_constant_refinements.rb +5 -7
  84. data/lib/rigor/builtins/regex_refinement.rb +4 -4
  85. data/lib/rigor/builtins/static_return_refinements.rb +5 -6
  86. data/lib/rigor/cache/annotation_location.rb +2 -4
  87. data/lib/rigor/cache/descriptor.rb +58 -20
  88. data/lib/rigor/cache/engine_source.rb +4 -4
  89. data/lib/rigor/cache/file_digest.rb +9 -1
  90. data/lib/rigor/cache/incremental_snapshot.rb +40 -6
  91. data/lib/rigor/cache/rbs_class_ancestor_table.rb +0 -3
  92. data/lib/rigor/cache/rbs_class_type_param_names.rb +0 -3
  93. data/lib/rigor/cache/rbs_constant_table.rb +0 -3
  94. data/lib/rigor/cache/rbs_descriptor.rb +79 -11
  95. data/lib/rigor/cache/rbs_environment.rb +8 -4
  96. data/lib/rigor/cache/rbs_known_class_names.rb +0 -3
  97. data/lib/rigor/cache/store.rb +77 -32
  98. data/lib/rigor/cli/annotate_command.rb +5 -6
  99. data/lib/rigor/cli/check_command.rb +57 -10
  100. data/lib/rigor/cli/check_invocation.rb +6 -11
  101. data/lib/rigor/cli/check_runner_factory.rb +1 -5
  102. data/lib/rigor/cli/coverage_command.rb +1 -1
  103. data/lib/rigor/cli/coverage_mutation.rb +12 -3
  104. data/lib/rigor/cli/coverage_scan.rb +1 -5
  105. data/lib/rigor/cli/diff_command.rb +1 -1
  106. data/lib/rigor/cli/doc_links.rb +3 -3
  107. data/lib/rigor/cli/docs_command.rb +2 -2
  108. data/lib/rigor/cli/doctor_command.rb +85 -38
  109. data/lib/rigor/cli/effects_command.rb +2 -2
  110. data/lib/rigor/cli/effects_diff_renderer.rb +3 -3
  111. data/lib/rigor/cli/effects_snapshot_command.rb +1 -1
  112. data/lib/rigor/cli/explain_command.rb +34 -1
  113. data/lib/rigor/cli/fused_protection_report.rb +7 -1
  114. data/lib/rigor/cli/lsp_command.rb +1 -1
  115. data/lib/rigor/cli/mcp_command.rb +1 -1
  116. data/lib/rigor/cli/measurement_integrity_warning.rb +4 -5
  117. data/lib/rigor/cli/mutation_fork_scan.rb +6 -6
  118. data/lib/rigor/cli/mutation_protection_report.rb +7 -1
  119. data/lib/rigor/cli/plugin_command.rb +1 -1
  120. data/lib/rigor/cli/plugins_command.rb +2 -1
  121. data/lib/rigor/cli/prism_colorizer.rb +2 -2
  122. data/lib/rigor/cli/protection_fork_scan.rb +6 -6
  123. data/lib/rigor/cli/show_bleedingedge_command.rb +1 -1
  124. data/lib/rigor/cli/sig_gen_command.rb +2 -2
  125. data/lib/rigor/cli/skill_command.rb +1 -1
  126. data/lib/rigor/cli/skill_deep_probe.rb +4 -4
  127. data/lib/rigor/cli/skill_describe.rb +36 -35
  128. data/lib/rigor/cli/trace_command.rb +4 -4
  129. data/lib/rigor/cli/trace_renderer.rb +5 -6
  130. data/lib/rigor/cli/triage_command.rb +1 -1
  131. data/lib/rigor/cli/type_of_command.rb +10 -6
  132. data/lib/rigor/cli/type_scan_command.rb +4 -4
  133. data/lib/rigor/cli/unused_command.rb +11 -3
  134. data/lib/rigor/cli/upgrade_command.rb +1 -1
  135. data/lib/rigor/cli.rb +68 -6
  136. data/lib/rigor/config_audit.rb +30 -5
  137. data/lib/rigor/configuration/severity_profile.rb +6 -6
  138. data/lib/rigor/configuration.rb +43 -4
  139. data/lib/rigor/effects/attribution.rb +1 -3
  140. data/lib/rigor/effects/config_envelopes.rb +6 -8
  141. data/lib/rigor/effects/definition_lines.rb +25 -6
  142. data/lib/rigor/effects/effect_table.rb +0 -1
  143. data/lib/rigor/effects/entry_points.rb +0 -2
  144. data/lib/rigor/effects/envelope_check.rb +8 -8
  145. data/lib/rigor/effects/envelope_index.rb +5 -8
  146. data/lib/rigor/effects/framework_units.rb +4 -6
  147. data/lib/rigor/effects/identity.rb +4 -6
  148. data/lib/rigor/effects/inline_anchor.rb +7 -7
  149. data/lib/rigor/effects/label_intent.rb +3 -4
  150. data/lib/rigor/effects/liskov_check.rb +8 -8
  151. data/lib/rigor/effects/method_key.rb +1 -1
  152. data/lib/rigor/effects/plugin_facts.rb +4 -6
  153. data/lib/rigor/effects/propagator.rb +2 -3
  154. data/lib/rigor/effects/scanner.rb +1 -1
  155. data/lib/rigor/effects/signature_sources.rb +3 -5
  156. data/lib/rigor/effects/snapshot.rb +10 -10
  157. data/lib/rigor/effects/snapshot_diff.rb +1 -1
  158. data/lib/rigor/effects/unit_scan.rb +10 -10
  159. data/lib/rigor/effects/unknown_label_check.rb +3 -8
  160. data/lib/rigor/effects/unknown_label_report.rb +3 -4
  161. data/lib/rigor/environment/bundle_sig_discovery.rb +6 -6
  162. data/lib/rigor/environment/class_registry.rb +3 -1
  163. data/lib/rigor/environment/failure_slot.rb +2 -2
  164. data/lib/rigor/environment/installed_gem_set.rb +85 -0
  165. data/lib/rigor/environment/lockfile_resolver.rb +51 -4
  166. data/lib/rigor/environment/missing_gem_constant_index.rb +4 -4
  167. data/lib/rigor/environment/rbs_collection_discovery.rb +5 -5
  168. data/lib/rigor/environment/rbs_coverage_report.rb +5 -5
  169. data/lib/rigor/environment/rbs_hierarchy.rb +3 -1
  170. data/lib/rigor/environment/rbs_loader.rb +388 -59
  171. data/lib/rigor/environment.rb +105 -20
  172. data/lib/rigor/flow_contribution/merger.rb +0 -2
  173. data/lib/rigor/flow_contribution.rb +11 -13
  174. data/lib/rigor/inference/acceptance.rb +153 -8
  175. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  176. data/lib/rigor/inference/body_fixpoint.rb +5 -5
  177. data/lib/rigor/inference/budget_trace.rb +1 -5
  178. data/lib/rigor/inference/captured_locals.rb +2 -3
  179. data/lib/rigor/inference/closure_escape_analyzer.rb +2 -4
  180. data/lib/rigor/inference/coverage_scanner.rb +4 -6
  181. data/lib/rigor/inference/dynamic_origin.rb +1 -1
  182. data/lib/rigor/inference/element_read_widening.rb +168 -0
  183. data/lib/rigor/inference/expression_typer.rb +763 -143
  184. data/lib/rigor/inference/fork_map.rb +5 -7
  185. data/lib/rigor/inference/hkt_reducer.rb +2 -3
  186. data/lib/rigor/inference/hkt_registry.rb +4 -7
  187. data/lib/rigor/inference/index_write_widening.rb +1 -5
  188. data/lib/rigor/inference/macro_block_self_type.rb +1 -4
  189. data/lib/rigor/inference/method_dispatcher/block_folding.rb +2 -9
  190. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +1 -1
  191. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +305 -19
  192. data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
  193. data/lib/rigor/inference/method_dispatcher/file_folding.rb +1 -1
  194. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +2 -2
  195. data/lib/rigor/inference/method_dispatcher/json_folding.rb +1 -1
  196. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +7 -1
  197. data/lib/rigor/inference/method_dispatcher/math_folding.rb +41 -3
  198. data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -5
  199. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +61 -47
  200. data/lib/rigor/inference/method_dispatcher/random_folding.rb +82 -0
  201. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +165 -31
  202. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +25 -4
  203. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +2 -8
  204. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +1 -1
  205. data/lib/rigor/inference/method_dispatcher/set_folding.rb +1 -1
  206. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -1
  207. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +1 -1
  208. data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +1 -2
  209. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -5
  210. data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +85 -4
  211. data/lib/rigor/inference/method_dispatcher/time_folding.rb +1 -1
  212. data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +1 -2
  213. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +1 -1
  214. data/lib/rigor/inference/method_dispatcher.rb +12 -13
  215. data/lib/rigor/inference/method_parameter_binder.rb +4 -6
  216. data/lib/rigor/inference/multi_target_binder.rb +1 -3
  217. data/lib/rigor/inference/mutation_rejoin.rb +168 -0
  218. data/lib/rigor/inference/mutation_widening.rb +100 -51
  219. data/lib/rigor/inference/narrowing.rb +299 -46
  220. data/lib/rigor/inference/optimistic_origin.rb +1 -7
  221. data/lib/rigor/inference/origin_lookup.rb +2 -3
  222. data/lib/rigor/inference/parameter_inference_collector.rb +5 -6
  223. data/lib/rigor/inference/pre_eval_constants.rb +6 -5
  224. data/lib/rigor/inference/precision_scanner.rb +3 -4
  225. data/lib/rigor/inference/project_patched_methods.rb +2 -2
  226. data/lib/rigor/inference/project_patched_scanner.rb +3 -3
  227. data/lib/rigor/inference/protection_scanner.rb +1 -2
  228. data/lib/rigor/inference/range_constant.rb +57 -0
  229. data/lib/rigor/inference/rbs_type_translator.rb +49 -11
  230. data/lib/rigor/inference/receiver_alias.rb +3 -3
  231. data/lib/rigor/inference/refinement_mutation.rb +72 -0
  232. data/lib/rigor/inference/scope_indexer.rb +688 -151
  233. data/lib/rigor/inference/statement_evaluator.rb +221 -50
  234. data/lib/rigor/inference/string_mutation.rb +60 -0
  235. data/lib/rigor/inference/struct_fold_safety.rb +6 -7
  236. data/lib/rigor/inference/synthetic_method_index.rb +1 -2
  237. data/lib/rigor/inference/synthetic_method_scanner.rb +4 -6
  238. data/lib/rigor/inference/version_guard.rb +28 -21
  239. data/lib/rigor/inference/void_origin.rb +3 -3
  240. data/lib/rigor/inference/void_tail_summary.rb +2 -4
  241. data/lib/rigor/language_server/buffer_table.rb +4 -4
  242. data/lib/rigor/language_server/completion_provider.rb +1 -1
  243. data/lib/rigor/language_server/debouncer.rb +1 -1
  244. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  245. data/lib/rigor/language_server/document_symbol_provider.rb +1 -1
  246. data/lib/rigor/language_server/folding_range_provider.rb +1 -1
  247. data/lib/rigor/language_server/hover_provider.rb +1 -1
  248. data/lib/rigor/language_server/hover_renderer.rb +2 -2
  249. data/lib/rigor/language_server/incremental_sync.rb +6 -6
  250. data/lib/rigor/language_server/project_context.rb +2 -5
  251. data/lib/rigor/language_server/publish_batcher.rb +2 -2
  252. data/lib/rigor/language_server/selection_range_provider.rb +2 -2
  253. data/lib/rigor/language_server/server.rb +9 -9
  254. data/lib/rigor/language_server/signature_help_provider.rb +1 -1
  255. data/lib/rigor/language_server/uri.rb +1 -1
  256. data/lib/rigor/plugin/base.rb +90 -21
  257. data/lib/rigor/plugin/bundled_catalog.rb +167 -0
  258. data/lib/rigor/plugin/effect_attribution.rb +7 -7
  259. data/lib/rigor/plugin/effect_entry_points.rb +3 -3
  260. data/lib/rigor/plugin/fact_store.rb +6 -6
  261. data/lib/rigor/plugin/io_boundary.rb +93 -8
  262. data/lib/rigor/plugin/isolation.rb +29 -6
  263. data/lib/rigor/plugin/loader.rb +31 -12
  264. data/lib/rigor/plugin/macro/heredoc_template.rb +1 -1
  265. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  266. data/lib/rigor/plugin/manifest.rb +23 -1
  267. data/lib/rigor/plugin/registry.rb +42 -12
  268. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +12 -1
  269. data/lib/rigor/plugin/trust_policy.rb +2 -4
  270. data/lib/rigor/plugin/type_node_resolver.rb +3 -3
  271. data/lib/rigor/plugin_gap_advisory.rb +96 -0
  272. data/lib/rigor/project_environment.rb +138 -0
  273. data/lib/rigor/protection/analysis_guard.rb +93 -14
  274. data/lib/rigor/protection/closure_kill_oracle.rb +67 -23
  275. data/lib/rigor/protection/dependency_closure.rb +5 -8
  276. data/lib/rigor/protection/diagnostic_oracle.rb +2 -2
  277. data/lib/rigor/protection/discovery_seed.rb +11 -11
  278. data/lib/rigor/protection/kill_signature.rb +2 -4
  279. data/lib/rigor/protection/measurement_integrity.rb +1 -3
  280. data/lib/rigor/protection/mutation_cache.rb +6 -9
  281. data/lib/rigor/protection/mutation_scanner.rb +12 -15
  282. data/lib/rigor/protection/mutator.rb +2 -1
  283. data/lib/rigor/protection/test_suite_oracle.rb +5 -5
  284. data/lib/rigor/rbs_extended/conformance_checker.rb +4 -3
  285. data/lib/rigor/rbs_extended/envelope_scanner.rb +4 -6
  286. data/lib/rigor/rbs_extended/reporter.rb +36 -10
  287. data/lib/rigor/rbs_extended.rb +61 -21
  288. data/lib/rigor/reflection/constant_path.rb +126 -0
  289. data/lib/rigor/reflection.rb +26 -10
  290. data/lib/rigor/runtime/jit.rb +6 -8
  291. data/lib/rigor/scope/discovery_index.rb +30 -0
  292. data/lib/rigor/scope.rb +203 -4
  293. data/lib/rigor/sig_gen/generator.rb +115 -22
  294. data/lib/rigor/sig_gen/layout_index.rb +3 -4
  295. data/lib/rigor/sig_gen/meta_class_shape.rb +3 -5
  296. data/lib/rigor/sig_gen/observation_collector.rb +17 -15
  297. data/lib/rigor/sig_gen/path_mapper.rb +4 -6
  298. data/lib/rigor/sig_gen/rbs_validity.rb +4 -4
  299. data/lib/rigor/sig_gen/renderer.rb +4 -5
  300. data/lib/rigor/sig_gen/skip_reason_catalog.rb +111 -0
  301. data/lib/rigor/sig_gen/superclass_spelling.rb +27 -0
  302. data/lib/rigor/sig_gen/type_elaborator.rb +1 -3
  303. data/lib/rigor/sig_gen/writer.rb +23 -6
  304. data/lib/rigor/signature_path_audit.rb +153 -6
  305. data/lib/rigor/source/literals.rb +0 -23
  306. data/lib/rigor/source/node_children.rb +0 -2
  307. data/lib/rigor/source/node_locator.rb +5 -11
  308. data/lib/rigor/source/node_walker.rb +2 -5
  309. data/lib/rigor/triage/catalogue.rb +1 -3
  310. data/lib/rigor/triage.rb +3 -5
  311. data/lib/rigor/type/accepts_result.rb +24 -6
  312. data/lib/rigor/type/combinator.rb +68 -5
  313. data/lib/rigor/type/data_class.rb +2 -2
  314. data/lib/rigor/type/data_instance.rb +4 -4
  315. data/lib/rigor/type/float_range.rb +128 -0
  316. data/lib/rigor/type/hash_shape.rb +5 -5
  317. data/lib/rigor/type/integer_range.rb +13 -8
  318. data/lib/rigor/type/nominal.rb +8 -2
  319. data/lib/rigor/type/refined.rb +3 -3
  320. data/lib/rigor/type/struct_class.rb +3 -3
  321. data/lib/rigor/type/struct_instance.rb +4 -4
  322. data/lib/rigor/type.rb +1 -0
  323. data/lib/rigor/type_node/generic.rb +1 -1
  324. data/lib/rigor/type_node/range_literal.rb +25 -0
  325. data/lib/rigor/type_node/resolver_chain.rb +1 -1
  326. data/lib/rigor/type_node.rb +1 -0
  327. data/lib/rigor/version.rb +1 -1
  328. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +0 -4
  329. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +0 -1
  330. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +0 -2
  331. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +1 -0
  332. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +0 -4
  333. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +3 -5
  334. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +1 -4
  335. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +1 -0
  336. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +33 -30
  337. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +0 -1
  338. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +1 -3
  339. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +28 -2
  340. data/plugins/rigor-actionpack/sig/action_controller.rbs +71 -0
  341. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +0 -4
  342. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -1
  343. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +0 -1
  344. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +0 -2
  345. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +1 -1
  346. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +1 -0
  347. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +3 -4
  348. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +1 -2
  349. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +1 -2
  350. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +42 -7
  351. data/plugins/rigor-activerecord/sig/active_record/framework.rbs +123 -0
  352. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -0
  353. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -2
  354. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +119 -14
  355. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +1 -0
  356. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +1 -0
  357. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +9 -7
  358. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +7 -3
  359. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +1 -0
  360. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +8 -6
  361. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +11 -7
  362. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +13 -11
  363. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +9 -4
  364. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  365. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +0 -5
  366. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +0 -1
  367. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +0 -1
  368. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +8 -1
  369. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +36 -4
  370. data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +1 -0
  371. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +8 -6
  372. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +6 -3
  373. data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +1 -0
  374. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +2 -1
  375. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +0 -3
  376. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +1 -0
  377. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +0 -5
  378. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -2
  379. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +0 -1
  380. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +0 -1
  381. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +1 -0
  382. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +0 -12
  383. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +1 -3
  384. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +0 -1
  385. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -0
  386. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +5 -5
  387. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +1 -10
  388. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +3 -3
  389. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +2 -2
  390. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +2 -2
  391. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +2 -2
  392. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +9 -10
  393. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +2 -3
  394. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -0
  395. data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +1 -0
  396. data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +13 -1
  397. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +128 -3
  398. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +0 -3
  399. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +0 -3
  400. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -5
  401. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +2 -3
  402. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +1 -0
  403. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +0 -3
  404. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +1 -0
  405. data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +1 -0
  406. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +1 -9
  407. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +1 -0
  408. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +0 -4
  409. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +1 -1
  410. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -1
  411. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +0 -1
  412. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +0 -2
  413. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +1 -0
  414. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -0
  415. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +2 -5
  416. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +1 -4
  417. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +3 -9
  418. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +4 -4
  419. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +1 -2
  420. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +4 -5
  421. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +7 -5
  422. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +5 -5
  423. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +1 -0
  424. data/sig/rigor/analysis/reachability/scan_cache.rbs +12 -0
  425. data/sig/rigor/cache.rbs +2 -3
  426. data/sig/rigor/environment.rbs +5 -0
  427. data/sig/rigor/inference.rbs +3 -10
  428. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  429. data/sig/rigor/plugin/manifest.rbs +2 -1
  430. data/sig/rigor/reflection.rbs +3 -1
  431. data/sig/rigor/scope.rbs +24 -3
  432. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -0
  433. data/sig/rigor/type.rbs +27 -1
  434. data/sig/rigor.rbs +1 -0
  435. data/skills/rigor-ask/SKILL.md +8 -6
  436. data/skills/rigor-plugin-author/references/02-walker-and-types.md +13 -2
  437. data/skills/rigor-project-init/SKILL.md +6 -3
  438. data/skills/rigor-project-init/references/02-configure.md +10 -6
  439. data/skills/rigor-project-init/references/06-agent-contract.md +67 -0
  440. data/skills/rigor-type-oracle/SKILL.md +228 -0
  441. data/skills/rigor-type-oracle/references/01-oracle-commands.md +261 -0
  442. data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +52 -0
  443. data/skills/rigor-type-oracle/references/03-gap-protocol.md +128 -0
  444. data/skills/rigor-unused-adjudicate/SKILL.md +8 -5
  445. metadata +26 -3
  446. data/lib/rigor/cli/probe_environment.rb +0 -85
  447. data/sig/rigor/inference/builtins/numeric_catalog.rbs +0 -3
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The plugin entry point rather than {Loader} directly: {Registry} closes over constants the package only
4
+ # defines in `plugin.rb`'s order, so requiring a leaf of the package first leaves it half-built.
5
+ require_relative "../plugin"
6
+ require_relative "loader"
7
+
8
+ module Rigor
9
+ module Plugin
10
+ # ADR-96 WD1/WD2 — the gem-to-plugin index behind the plugin-gap advisory: for every plugin the engine
11
+ # bundles, the gems its manifest declares in `target_gems:`.
12
+ #
13
+ # The index is read off the manifests, never listed here. ADR-96's finding was that the same knowledge
14
+ # sat in four hand-maintained copies (`rigor doctor`, `rigor skill describe`, the project-init prose
15
+ # table, `plugins/README.md`) and none of them was the plugin; a fifth copy in this file would reproduce
16
+ # exactly the drift the field exists to end.
17
+ #
18
+ # Building it requires each bundled plugin's entry file. That loads plugin *classes* — it never runs
19
+ # plugin analysis, which is ADR-96 Criterion 2's line: gem presence is evidence for advice, not for
20
+ # execution. The gem-to-id registrations the requires produce are recorded through
21
+ # {Rigor::Plugin.record_gem_registration}, so a {Loader} running afterwards in the same process still
22
+ # resolves a bare-string `plugins:` entry whose `require` now no-ops (ADR-88 WD4b).
23
+ module BundledCatalog
24
+ # One bundled plugin: the gem name a `plugins:` entry spells, and the gems it models.
25
+ Entry = Data.define(:gem_name, :plugin_id, :target_gems)
26
+
27
+ BUNDLED_PLUGINS_ROOT = File.join(Loader::ENGINE_ROOT, "plugins")
28
+
29
+ @entries = nil
30
+ @load_failures = {}
31
+ @mutex = Mutex.new
32
+
33
+ class << self
34
+ # Every bundled plugin that declares at least one target gem, sorted by gem name. Memoised: the
35
+ # requires are process-wide and idempotent, and both callers ask once per command.
36
+ def entries
37
+ @mutex.synchronize { @entries ||= build_entries }
38
+ end
39
+
40
+ # The bundled plugins modelling `gem_name`, as `Gemfile.lock` spells it.
41
+ def for_gem(gem_name)
42
+ name = gem_name.to_s
43
+ entries.select { |entry| entry.target_gems.include?(name) }
44
+ end
45
+
46
+ # The bundled plugin gems whose entry file did not load this process, as `gem name => message`.
47
+ # Empty on a healthy process; a non-empty entry is why a bundled plugin is missing from {entries}.
48
+ def load_failures
49
+ entries
50
+ @mutex.synchronize { @load_failures.dup.freeze }
51
+ end
52
+
53
+ # Drops the memo. For specs that stub the engine root only.
54
+ #
55
+ # {load_failures} deliberately survives: `require` is idempotent, so a rebuild after a reset no
56
+ # longer reaches the file that raised and would report a clean process while the entry is still
57
+ # missing.
58
+ def reset!
59
+ @mutex.synchronize { @entries = nil }
60
+ end
61
+
62
+ private
63
+
64
+ # Bundledness is decided from the paths this module itself resolved and required, never by asking
65
+ # {FirstParty}: that predicate memoises a `File.file?` answer for the whole process, so one spec
66
+ # stubbing the engine root fixes every later answer in the suite — and the catalogue would then
67
+ # silently drop the plugins the advisory exists to name.
68
+ def build_entries
69
+ bundled = require_bundled_plugins
70
+ loaded_plugin_manifests.filter_map do |manifest|
71
+ gem_name = bundled[manifest.id]
72
+ next if gem_name.nil?
73
+ next if manifest.target_gems.empty?
74
+
75
+ Entry.new(gem_name: gem_name, plugin_id: manifest.id, target_gems: manifest.target_gems)
76
+ end.sort_by(&:gem_name).freeze
77
+ end
78
+
79
+ # The manifests of every plugin class the process has loaded, one per id. Read off the classes
80
+ # rather than {Rigor::Plugin.registered}: `require` is idempotent, so a plugin required earlier and
81
+ # then dropped by `Rigor::Plugin.unregister!` (every spec does this) is absent from the registry
82
+ # while still loaded, and an index built from the registry at that moment silently lacks it — the
83
+ # advisory then reads the project's own enabled plugin as a gap and fails `rigor doctor`.
84
+ #
85
+ # A plugin class whose source file sits under the bundled `plugins/` tree wins its id outright. The
86
+ # suite defines throwaway `Class.new(Rigor::Plugin::Base)` plugins that reuse a bundled id (a
87
+ # `stub_const`-ed fake `activerecord`, for one), and those carry no `target_gems:`; picking one of
88
+ # them for the id erased the real plugin from the index in whatever `ObjectSpace` order that
89
+ # process happened to have. An anonymous class is still consulted, but only for an id no bundled
90
+ # class claims.
91
+ def loaded_plugin_manifests
92
+ bundled = {}
93
+ other = {}
94
+ ObjectSpace.each_object(Class) do |klass|
95
+ next unless klass < Plugin::Base
96
+
97
+ manifest = manifest_of(klass)
98
+ next if manifest.nil?
99
+
100
+ (bundled_source?(klass) ? bundled : other)[manifest.id] ||= manifest
101
+ end
102
+ other.merge(bundled).values
103
+ end
104
+
105
+ def manifest_of(klass)
106
+ klass.manifest
107
+ rescue ArgumentError
108
+ nil
109
+ end
110
+
111
+ # Whether `klass` was defined by a file in the engine's own `plugins/` tree. An anonymous class
112
+ # answers false, and so does one whose constant a spec has since removed — which is exactly the
113
+ # shadowing case, since `stub_const` restores the name to nil at the end of the example.
114
+ def bundled_source?(klass)
115
+ name = klass.name
116
+ return false if name.nil?
117
+
118
+ location = Object.const_source_location(name)
119
+ return false if location.nil? || location.first.nil?
120
+
121
+ location.first.start_with?("#{BUNDLED_PLUGINS_ROOT}#{File::SEPARATOR}")
122
+ rescue NameError
123
+ false
124
+ end
125
+
126
+ # @return the gems whose entry file this call resolved, as `plugin id => gem name`.
127
+ def require_bundled_plugins
128
+ return {} unless File.directory?(BUNDLED_PLUGINS_ROOT)
129
+
130
+ add_bundled_lib_dirs
131
+ Dir.children(BUNDLED_PLUGINS_ROOT).sort.each_with_object({}) do |gem_name, resolved|
132
+ path = Loader.bundled_plugin_path(gem_name)
133
+ next if path.nil?
134
+
135
+ resolved[gem_name.delete_prefix(FirstParty::GEM_PREFIX)] = gem_name
136
+ require_and_record(gem_name, path)
137
+ end
138
+ end
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
+ # A plugin gem that fails to load is skipped rather than fatal: the catalogue exists to *advise*, and
152
+ # a broken bundled copy is already reported by `rigor doctor`'s plugin checks for the plugins the
153
+ # project actually enabled. The failure is recorded in {load_failures} so a missing entry stays
154
+ # diagnosable instead of silently narrowing the index.
155
+ def require_and_record(gem_name, path)
156
+ before = Plugin.registered.keys
157
+ require path
158
+ newly_registered = Plugin.registered.keys - before
159
+ Plugin.record_gem_registration(gem_name, newly_registered) unless newly_registered.empty?
160
+ rescue ::LoadError, StandardError => e
161
+ @load_failures[gem_name] = "#{e.class}: #{e.message}"
162
+ nil
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -77,15 +77,15 @@ module Rigor
77
77
  # callable whose body is supplied by the application.
78
78
  TAINT_CAUSES = %w[template-not-analysed opaque-callable].freeze
79
79
 
80
- # @param receiver [String] a class name or a receiver path (see above)
81
- # @param method [Symbol, String] the selector this row colours
82
- # @param singleton [Boolean] whether the row is `Receiver.method` rather than `Receiver#method`.
80
+ # @param receiver a class name or a receiver path (see above)
81
+ # @param method the selector this row colours
82
+ # @param singleton whether the row is `Receiver.method` rather than `Receiver#method`.
83
83
  # Meaningless — and ignored — for a receiver path, whose head already fixes the receiver object.
84
- # @param labels [Array<String>] the effect labels the call contributes
85
- # @param narrow [String, nil] a {Rigor::Effects::Narrowing} handler name, when the call's own
84
+ # @param labels the effect labels the call contributes
85
+ # @param narrow a {Rigor::Effects::Narrowing} handler name, when the call's own
86
86
  # argument literals settle a question the row cannot (`connection.execute("SELECT …")`)
87
- # @param discharge [Boolean] see above; honoured only for a first-party bundled plugin
88
- # @param why [String] the audit justification, required exactly as `data/effects/core.yml` requires
87
+ # @param discharge see above; honoured only for a first-party bundled plugin
88
+ # @param why the audit justification, required exactly as `data/effects/core.yml` requires
89
89
  # one of every row: a label with no stated reason is a claim nobody can review.
90
90
  def initialize(receiver:, method:, labels:, why:, singleton: false, narrow: nil, discharge: false, # rubocop:disable Metrics/ParameterLists
91
91
  within: nil, on_result: false, taint: nil)
@@ -21,10 +21,10 @@ module Rigor
21
21
  class EffectEntryPoints
22
22
  attr_reader :name, :globs, :why
23
23
 
24
- # @param name [String, Symbol] the token `reach:` adopts. Must satisfy
24
+ # @param name the token `reach:` adopts. Must satisfy
25
25
  # {Rigor::Effects::EntryPoints::NAME_PATTERN}, checked at registration.
26
- # @param globs [Array<String>] project-relative path globs
27
- # @param why [String] what the preset stands for, for the plugin's README and `rigor effects`' help
26
+ # @param globs project-relative path globs
27
+ # @param why what the preset stands for, for the plugin's README and `rigor effects`' help
28
28
  def initialize(name:, globs:, why: "")
29
29
  @name = name.to_s.dup.freeze
30
30
  @globs = Array(globs).map { |glob| glob.to_s.dup.freeze }.uniq.sort.freeze
@@ -42,9 +42,9 @@ module Rigor
42
42
  # Writes a `(plugin_id, name) -> value` triple. Idempotent if the same value is published twice (`==`);
43
43
  # raises {Conflict} if the values differ.
44
44
  #
45
- # @param plugin_id [String] producing plugin's manifest id.
46
- # @param name [Symbol, String] fact name (canonicalised to Symbol for lookup).
47
- # @param value [Object] frozen-shape value object the producer chose to publish. The value is stored
45
+ # @param plugin_id producing plugin's manifest id.
46
+ # @param name fact name (canonicalised to Symbol for lookup).
47
+ # @param value frozen-shape value object the producer chose to publish. The value is stored
48
48
  # as-is.
49
49
  def publish(plugin_id:, name:, value:)
50
50
  plugin_id = plugin_id.to_s
@@ -60,19 +60,19 @@ module Rigor
60
60
  nil
61
61
  end
62
62
 
63
- # @return [Object, nil] the published value, or `nil` when no fact is registered. Reads do NOT establish
63
+ # @return the published value, or `nil` when no fact is registered. Reads do NOT establish
64
64
  # a dependency — `manifest(consumes:)` (slice 4) is the dependency declaration mechanism.
65
65
  def read(plugin_id:, name:)
66
66
  fact = @mutex.synchronize { @facts[[plugin_id.to_s, name.to_sym]] }
67
67
  fact&.value
68
68
  end
69
69
 
70
- # @return [Boolean] whether a fact is registered.
70
+ # @return whether a fact is registered.
71
71
  def published?(plugin_id:, name:)
72
72
  @mutex.synchronize { @facts.key?([plugin_id.to_s, name.to_sym]) }
73
73
  end
74
74
 
75
- # @yield [Fact] every published fact in publication order.
75
+ # @yield every published fact in publication order.
76
76
  def each_fact(&)
77
77
  snapshot = @mutex.synchronize { @facts.values }
78
78
  snapshot.each(&)
@@ -27,6 +27,12 @@ module Rigor
27
27
  # A plugin that gates its read or its glob on one of these shapes its result on the answer, so the
28
28
  # answer is a dependency; probing through `File` directly leaves that dependency unrecorded and the
29
29
  # warm run serves the pre-appearance result.
30
+ # - `#list_directory(path)` — the directory-LISTING fingerprint (ADR-45 WD1c, #629): returns the
31
+ # directory's entry paths and records ONE {Cache::Descriptor::GlobEntry} for `path/*`, so any file
32
+ # added to or removed from the directory (and any content edit under it) invalidates. It is the row
33
+ # for a plugin that asks "which of these N candidate files exists here?" — rigor-actionpack tries
34
+ # nine template extensions per `render` call — where N per-path probe rows would put thousands of
35
+ # validations on every warm run and one listing row puts one per directory.
30
36
  # - `#open_url(url)` — fetches the URL when the policy permits it (`network_policy: :allowlist` plus an
31
37
  # `allowed_url_hosts` match) and raises {AccessDeniedError} otherwise. v0.1.2 ships the allowlist
32
38
  # surface; the default project policy still has `network_policy: :disabled` so plugins that want network
@@ -48,8 +54,32 @@ module Rigor
48
54
  @plugin_id = plugin_id.to_s.dup.freeze
49
55
  @file_entries = {}
50
56
  @config_entries = {}
57
+ @glob_entries = {}
51
58
  @http_client = http_client
52
59
  @mutex = Mutex.new
60
+ @refusal_count = 0
61
+ @first_refused_path = nil
62
+ end
63
+
64
+ # Issue #959 — `TrustPolicy#allow_read?` refuses a path purely by `File.expand_path` comparison
65
+ # (ADR-2's documented bound; no `File.realpath`), so a project rooted under a symlink (macOS'
66
+ # `/tmp` → `/private/tmp` alias is the common case) can have every plugin read refused with no
67
+ # visible cause: the boundary answered "not readable" and the plugin swallowed or surfaced only its
68
+ # own generic error. `#refusal_summary` is the accumulated record `Analysis::Runner::DiagnosticAggregator`
69
+ # reads once per run to surface ONE `:info` row per plugin that hit this, naming the count, the first
70
+ # refused path, and the read root it fell outside (so the message can suggest the fix: `cd "$(pwd -P)"`
71
+ # on a symlinked project root, or a `trust:` / `plugins_io.allowed_paths:` entry).
72
+ #
73
+ # @return `nil` when nothing was refused, else `{count:, first_path:, nearest_root:}` — `nearest_root`
74
+ # is the policy's first configured read root (the project root in the common case), named as the
75
+ # root a reader should compare the refused path against; it is a hint, not a claim that this
76
+ # specific root is the nearest by path distance.
77
+ def refusal_summary
78
+ @mutex.synchronize do
79
+ return nil if @refusal_count.zero?
80
+
81
+ { count: @refusal_count, first_path: @first_refused_path, nearest_root: @policy.allowed_read_roots.first }
82
+ end
53
83
  end
54
84
 
55
85
  # Reads the file at `path` after validating it against the policy. Raises {AccessDeniedError} when the
@@ -68,6 +98,7 @@ module Rigor
68
98
  def read_file(path)
69
99
  absolute = File.expand_path(path.to_s)
70
100
  unless @policy.allow_read?(absolute)
101
+ record_refusal(absolute)
71
102
  raise AccessDeniedError.new(
72
103
  "plugin #{@plugin_id.inspect} cannot read #{absolute.inspect}: " \
73
104
  "path is outside the trusted-read scope",
@@ -93,8 +124,8 @@ module Rigor
93
124
  # `return nil unless File.file?(config)` gate: the miss shaped the result and left no edge for the
94
125
  # file's later appearance.
95
126
  #
96
- # @param path [String] project path; relative paths expand against the working directory
97
- # @return [Boolean] `File.file?`'s answer, unchanged
127
+ # @param path project path; relative paths expand against the working directory
128
+ # @return `File.file?`'s answer, unchanged
98
129
  def file?(path)
99
130
  probe(path) { |absolute| File.file?(absolute) }
100
131
  end
@@ -103,12 +134,42 @@ module Rigor
103
134
  # existence row. The discovery shape (`next [] unless directory?(root)` before a `Dir.glob`) depends
104
135
  # on the root existing exactly as a config read depends on the config file existing.
105
136
  #
106
- # @param path [String] project path; relative paths expand against the working directory
107
- # @return [Boolean] `File.directory?`'s answer, unchanged
137
+ # @param path project path; relative paths expand against the working directory
138
+ # @return `File.directory?`'s answer, unchanged
108
139
  def directory?(path)
109
140
  probe(path) { |absolute| File.directory?(absolute) }
110
141
  end
111
142
 
143
+ # ADR-45 WD1c (#629) — the directory-listing fingerprint. Returns the paths `path` currently holds
144
+ # (`[]` when it is not a directory), and records a single {Cache::Descriptor::GlobEntry} over
145
+ # `path/*` so the recorded dependency is the directory's whole listing rather than one row per
146
+ # name the caller went on to test. An addition, a removal, or an edit under the directory all move
147
+ # the glob signature; a caller that answered "no such file" from the listing therefore invalidates
148
+ # the moment the file appears.
149
+ #
150
+ # Why not N {#file?} probes: a caller like rigor-actionpack's template lookup tries an extension
151
+ # family per candidate, so per-path rows scale with (renders × extensions × view roots) — thousands
152
+ # of stat validations per warm run on a Rails-scale project — while the listing row scales with the
153
+ # number of directories actually consulted. The listing row is also the STRICTLY stronger dependency
154
+ # of the two: it covers names the caller never thought to probe.
155
+ #
156
+ # Recording, not the answer, is what the policy gates, exactly as in {#probe}: an out-of-scope
157
+ # directory is listed truthfully and contributes no row.
158
+ #
159
+ # @param path — project directory; relative paths expand against the working directory
160
+ # @return the absolute paths directly under `path` in `Dir.glob` order; empty when not a directory
161
+ def list_directory(path)
162
+ absolute = File.expand_path(path.to_s)
163
+ entries = Dir.glob(File.join(absolute, "*"))
164
+ unless @policy.allow_read?(absolute)
165
+ record_refusal(absolute)
166
+ return entries
167
+ end
168
+
169
+ record_glob_entry(absolute, "*")
170
+ entries
171
+ end
172
+
112
173
  # Fetches the URL when the policy permits it. Returns the response body. Raises {AccessDeniedError} when
113
174
  # the policy is `:disabled`, the URL scheme is not `https`, the host is not on the allowlist, the
114
175
  # response is non-2xx, the body exceeds {URL_MAX_BYTES}, or the request times out
@@ -131,12 +192,14 @@ module Rigor
131
192
  body
132
193
  end
133
194
 
134
- # @return [Rigor::Cache::Descriptor] frozen snapshot of every file / URL the boundary has read so far.
195
+ # @return frozen snapshot of every file / URL the boundary has read so far.
135
196
  # Calling this multiple times yields equal descriptors; subsequent reads expand the underlying record
136
197
  # tables.
137
198
  def cache_descriptor
138
- files, configs = @mutex.synchronize { [@file_entries.values.dup, @config_entries.values.dup] }
139
- Cache::Descriptor.new(files: files, configs: configs)
199
+ files, configs, globs = @mutex.synchronize do
200
+ [@file_entries.values.dup, @config_entries.values.dup, @glob_entries.values.dup]
201
+ end
202
+ Cache::Descriptor.new(files: files, configs: configs, globs: globs)
140
203
  end
141
204
 
142
205
  private
@@ -161,7 +224,10 @@ module Rigor
161
224
  def probe(path)
162
225
  absolute = File.expand_path(path.to_s)
163
226
  answer = yield(absolute)
164
- return answer unless @policy.allow_read?(absolute)
227
+ unless @policy.allow_read?(absolute)
228
+ record_refusal(absolute)
229
+ return answer
230
+ end
165
231
 
166
232
  if answer
167
233
  record_presence_entry(absolute)
@@ -209,6 +275,25 @@ module Rigor
209
275
  @mutex.synchronize { @file_entries[path] ||= entry }
210
276
  end
211
277
 
278
+ # ADR-45 WD1c (#629) — the listing row for {#list_directory}, deduplicated per (root, pattern) slot.
279
+ # Recomputed on each call rather than `||=`d, so a directory listed again after a mid-run mutation
280
+ # carries the signature of the state the LAST reader saw.
281
+ def record_glob_entry(root, pattern)
282
+ entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern)
283
+ @mutex.synchronize { @glob_entries[entry.slot_key] = entry }
284
+ end
285
+
286
+ # Issue #959 — records that `path` fell outside every {TrustPolicy} read root, for {#refusal_summary}.
287
+ # Only the count and the FIRST path are kept (never the full list): the diagnostic reports one row per
288
+ # plugin regardless of how many paths it tried, so a plugin that walks a whole out-of-scope subtree
289
+ # costs one comparison per call here, not an unbounded accumulation.
290
+ def record_refusal(path)
291
+ @mutex.synchronize do
292
+ @refusal_count += 1
293
+ @first_refused_path ||= path
294
+ end
295
+ end
296
+
212
297
  def record_url_entry(url, body)
213
298
  digest = Digest::SHA256.hexdigest(body)
214
299
  key = "url:#{url}"
@@ -7,8 +7,8 @@ module Rigor
7
7
  # ADR-39 slice 5 — the selectable isolation strategy for target-library invocation. A plugin invokes a
8
8
  # pure method on a trusted target library (e.g. `ActiveSupport::Inflector.pluralize("post")`) through
9
9
  # {.call}; how much the invocation is isolated from Rigor's own process is a **configurable strategy**
10
- # (`RIGOR_PLUGIN_ISOLATION` env; the `exe/rigor` launcher maps `.rigor.yml`'s `plugins_isolation:` onto it
11
- # before re-exec). Three backends behind one interface:
10
+ # (`.rigor.yml`'s `plugins_isolation:` key, or the `RIGOR_PLUGIN_ISOLATION` env, which wins over it
11
+ # see {.strategy_name}). Three backends behind one interface:
12
12
  #
13
13
  # - `none` — load into the main space and call directly. Lowest cost; no isolation. Used as the fallback
14
14
  # where fork is unavailable; fine because the invoked library is trusted + pure.
@@ -98,11 +98,34 @@ module Rigor
98
98
  # {#backend}).
99
99
  DEFAULT = "process"
100
100
 
101
- # The configured strategy name (`RIGOR_PLUGIN_ISOLATION`), defaulting to {DEFAULT} for any unset /
102
- # unrecognised value.
101
+ # The strategy the loaded {Rigor::Configuration} named (`plugins_isolation:`), or nil when the
102
+ # project named none. Handed over as a module ivar for the same reason {target_bundle_root} is: the
103
+ # invocation happens deep inside a plugin, several layers below anything holding the configuration.
104
+ def configured_strategy
105
+ @configured_strategy
106
+ end
107
+
108
+ # Frozen, like {target_bundle_root=}, so a Ractor-pool worker may read it (an unshareable value in a
109
+ # module ivar is a `Ractor::IsolationError` to read from a non-main Ractor, not only to write).
110
+ def configured_strategy=(name)
111
+ @configured_strategy = name.nil? ? nil : name.to_s.dup.freeze
112
+ end
113
+
114
+ # The strategy name in effect, defaulting to {DEFAULT} for any unset / unrecognised value.
115
+ #
116
+ # `RIGOR_PLUGIN_ISOLATION` WINS over the configuration (#911). The variable is the operator's
117
+ # one-invocation override — the thing you reach for when the fork worker misbehaves on this
118
+ # machine, or when a CI image cannot fork at all — and an override a committed file can veto is not
119
+ # an override. The config key is the project-wide default the override departs from. It is also the
120
+ # only ordering under which `ruby_box` works at all: `exe/rigor` re-execs on the variable alone.
103
121
  def strategy_name
104
- name = ENV["RIGOR_PLUGIN_ISOLATION"].to_s
105
- STRATEGIES.include?(name) ? name : DEFAULT
122
+ from_env = ENV["RIGOR_PLUGIN_ISOLATION"].to_s
123
+ return from_env if STRATEGIES.include?(from_env)
124
+
125
+ from_config = configured_strategy.to_s
126
+ return from_config if STRATEGIES.include?(from_config)
127
+
128
+ DEFAULT
106
129
  end
107
130
 
108
131
  # Invokes `receiver.method(*args)` on a target library, requiring `feature` first, under the configured
@@ -43,12 +43,11 @@ module Rigor
43
43
  # the engine's own bundled plugin copies.
44
44
  ENGINE_ROOT = File.expand_path("../../..", __dir__)
45
45
 
46
- # @param services [Rigor::Plugin::Services]
47
- # @param requirer [#call] takes a gem name OR an absolute file path (#194 slice 2 — a bundled plugin is
46
+ # @param requirer — takes a gem name OR an absolute file path (#194 slice 2 — a bundled plugin is
48
47
  # required by its {.bundled_plugin_path}) and returns truthy on successful require. Defaulted to
49
48
  # `Kernel.require` via a lambda, which accepts both forms; the spec injects a fake to avoid touching
50
49
  # the real load path.
51
- # @param feature_resolver [#call] takes a gem name and returns the absolute path `require` resolved it
50
+ # @param feature_resolver takes a gem name and returns the absolute path `require` resolved it
52
51
  # to (or nil). Defaulted to {FEATURE_RESOLVER}; the spec injects a fake so it never has to mutate the
53
52
  # real `$LOADED_FEATURES` global.
54
53
  def initialize(services:, requirer: ->(name) { require name }, feature_resolver: FEATURE_RESOLVER)
@@ -90,8 +89,7 @@ module Rigor
90
89
  File.directory?(path) ? path : nil
91
90
  end
92
91
 
93
- # @param entries [Array<String, Hash>] the raw `plugins:` list from the configuration.
94
- # @return [Registry]
92
+ # @param entries the raw `plugins:` list from the configuration.
95
93
  def load(entries)
96
94
  plugins = []
97
95
  load_errors = []
@@ -266,16 +264,37 @@ module Rigor
266
264
  when 1
267
265
  Plugin.registered_for(newly_registered.first)
268
266
  else
269
- raise LoadError.new(
270
- "plugin gem #{entry[:gem].inspect} bundles #{newly_registered.size} plugins " \
271
- "(#{newly_registered.sort.inspect}) and cannot be activated as a single `plugins:` entry — " \
272
- "it is a convenience meta-gem. List the individual plugin gems you want in `plugins:` " \
273
- "(e.g. `rigor-#{newly_registered.min}`), or select one with an explicit `id:` field.",
274
- plugin_ref: entry[:gem]
275
- )
267
+ own = own_id_among(entry[:gem], newly_registered)
268
+ raise meta_gem_error(entry, newly_registered) unless own
269
+
270
+ Plugin.registered_for(own)
276
271
  end
277
272
  end
278
273
 
274
+ # A gem that registers more than one plugin is a meta-gem only when NONE of the ids is its own. The
275
+ # FFI family (`rigor-sassc`, `rigor-ethon`, `rigor-ffi-rzmq`, `rigor-rbnacl`) each `require "rigor-ffi"`
276
+ # for the shared binding analyzer, so listing one of them ALONE registered two plugins — its own and
277
+ # `ffi` — and the meta-gem branch rejected the entry, telling the user to list `rigor-ffi` instead,
278
+ # which activates the wrong plugin. The failure was load-order dependent (listing `rigor-ffi` first
279
+ # made the nested require a no-op and the entry loaded), so it never showed up in the suite, where
280
+ # some earlier example has always loaded `rigor-ffi`.
281
+ #
282
+ # `rigor-rails` stays a meta-gem under this rule: it registers eight ids and none of them is `rails`.
283
+ def own_id_among(gem_name, ids)
284
+ own = gem_name.to_s.delete_prefix(Plugin::FirstParty::GEM_PREFIX)
285
+ ids.find { |id| id == own }
286
+ end
287
+
288
+ def meta_gem_error(entry, newly_registered)
289
+ LoadError.new(
290
+ "plugin gem #{entry[:gem].inspect} bundles #{newly_registered.size} plugins " \
291
+ "(#{newly_registered.sort.inspect}) and cannot be activated as a single `plugins:` entry — " \
292
+ "it is a convenience meta-gem. List the individual plugin gems you want in `plugins:` " \
293
+ "(e.g. `rigor-#{newly_registered.min}`), or select one with an explicit `id:` field.",
294
+ plugin_ref: entry[:gem]
295
+ )
296
+ end
297
+
279
298
  def validate_config!(manifest, config)
280
299
  errors = manifest.validate_config(config)
281
300
  return if errors.empty?
@@ -152,7 +152,7 @@ module Rigor
152
152
  class ReturnsFromArg
153
153
  attr_reader :position, :plugin_id, :fact
154
154
 
155
- # @return [ReturnsFromArg, nil] coerced value class for a Hash / nil / ReturnsFromArg input. Raises
155
+ # @return coerced value class for a Hash / nil / ReturnsFromArg input. Raises
156
156
  # on any other shape so manifest authoring failures surface at construction time.
157
157
  def self.coerce(value)
158
158
  return nil if value.nil?
@@ -105,7 +105,7 @@ module Rigor
105
105
  to_h.hash
106
106
  end
107
107
 
108
- # @return [String, nil] fully-qualified module name for the given trait symbol, or nil when the
108
+ # @return fully-qualified module name for the given trait symbol, or nil when the
109
109
  # registry doesn't know the symbol (caller emits a tier_b.unknown-trait provenance marker and falls
110
110
  # through).
111
111
  def module_for(symbol)
@@ -42,6 +42,12 @@ module Rigor
42
42
  # {Rigor::Effects::Label} accepts as a root.
43
43
  VALID_EFFECT_ROOT = /\A[a-z][a-z0-9_]*\z/
44
44
 
45
+ # ADR-96 WD1 — the gems a plugin models, spelled exactly as `Gemfile.lock` names them. Not derivable
46
+ # from the plugin id (`rigor-factorybot` models `factory_bot`, `rigor-rspec` models `rspec-core`), which
47
+ # is what makes it a declared field; an empty list is the meaningful answer for a plugin that models no
48
+ # gem at all.
49
+ attr_reader :target_gems
50
+
45
51
  attr_reader :id, :version, :description, :config_schema, :config_defaults, :produces, :consumes,
46
52
  :owns_receivers, :open_receivers, :type_node_resolvers, :block_as_methods,
47
53
  :heredoc_templates, :nested_class_templates, :trait_registries,
@@ -52,7 +58,7 @@ module Rigor
52
58
 
53
59
  def initialize( # rubocop:disable Metrics/ParameterLists
54
60
  id:, version:,
55
- description: nil, config_schema: {},
61
+ description: nil, config_schema: {}, target_gems: [],
56
62
  produces: [], consumes: [], owns_receivers: [], open_receivers: [], type_node_resolvers: [],
57
63
  block_as_methods: [], heredoc_templates: [], nested_class_templates: [],
58
64
  trait_registries: [],
@@ -65,6 +71,7 @@ module Rigor
65
71
  validate_version!(version)
66
72
  validate_config_schema!(config_schema)
67
73
  validate_produces!(produces)
74
+ validate_target_gems!(target_gems)
68
75
  validate_owns_receivers!(owns_receivers)
69
76
  validate_open_receivers!(open_receivers)
70
77
  validate_type_node_resolvers!(type_node_resolvers)
@@ -85,6 +92,7 @@ module Rigor
85
92
  open_receivers, type_node_resolvers, block_as_methods, heredoc_templates, trait_registries,
86
93
  hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
87
94
  source_rbs_synthesizer)
95
+ assign_target_gems(target_gems)
88
96
  assign_nested_class_templates(nested_class_templates)
89
97
  assign_additional_initializers(additional_initializers)
90
98
  assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
@@ -119,6 +127,12 @@ module Rigor
119
127
  @source_rbs_synthesizer = source_rbs_synthesizer
120
128
  end
121
129
 
130
+ # ADR-96 WD1 — assigned outside assign_fields, which already carries the maximum positional arity.
131
+ def assign_target_gems(target_gems)
132
+ @target_gems = target_gems.map { |g| g.to_s.dup.freeze }.uniq.freeze
133
+ end
134
+ private :assign_target_gems
135
+
122
136
  # Assigned outside assign_fields (which already carries the maximum positional arity) — set in
123
137
  # `initialize` before the final freeze. ADR-36 nested-class emission tier.
124
138
  def assign_nested_class_templates(nested_class_templates)
@@ -189,6 +203,7 @@ module Rigor
189
203
  "description" => description,
190
204
  "config_schema" => config_schema.to_h { |k, v| [k, v.to_s] },
191
205
  "config_defaults" => config_defaults,
206
+ "target_gems" => target_gems,
192
207
  "produces" => produces.map(&:to_s),
193
208
  "consumes" => consumes.map { |c| consumption_hash(c) },
194
209
  "owns_receivers" => owns_receivers,
@@ -344,6 +359,13 @@ module Rigor
344
359
  raise ArgumentError, "plugin manifest #{field} must be an Array of #{label}, got #{value.inspect}"
345
360
  end
346
361
 
362
+ # ADR-96 WD1 — every entry is a gem name as `Gemfile.lock` spells it. The uniform Array-of-String check
363
+ # is all the manifest can do: whether the named gem exists is a fact about RubyGems, not about the
364
+ # manifest, and a typo surfaces as an advisory that never fires rather than as a load failure.
365
+ def validate_target_gems!(target_gems)
366
+ validate_array_of!("target_gems", target_gems, "non-empty String") { |g| g.is_a?(String) && !g.empty? }
367
+ end
368
+
347
369
  def validate_produces!(produces)
348
370
  validate_array_of!("produces", produces, "Symbol/String") { |p| p.is_a?(Symbol) || p.is_a?(String) }
349
371
  end