rigortype 0.3.9 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/data/core_overlay/enumerable.rbs +51 -0
  4. data/data/core_overlay/enumerator.rbs +84 -0
  5. data/data/core_overlay/hash_rbs3.rbs +41 -0
  6. data/data/core_overlay/process.rbs +40 -0
  7. data/data/core_overlay/string_io.rbs +33 -0
  8. data/data/effects/core.yml +3 -3
  9. data/data/gem_overlay/activesupport/core_ext.rbs +127 -4
  10. data/docs/handbook/03-narrowing.md +95 -10
  11. data/docs/handbook/04-tuples-and-shapes.md +8 -6
  12. data/docs/handbook/07-rbs-and-extended.md +16 -11
  13. data/docs/handbook/08-understanding-errors.md +3 -2
  14. data/docs/handbook/10-sorbet.md +9 -10
  15. data/docs/handbook/11-sig-gen.md +454 -13
  16. data/docs/install.md +87 -15
  17. data/docs/manual/02-cli-reference.md +65 -4
  18. data/docs/manual/03-configuration.md +12 -0
  19. data/docs/manual/04-diagnostics.md +4 -0
  20. data/docs/manual/07-plugins.md +1 -1
  21. data/docs/manual/08-skills.md +4 -3
  22. data/docs/manual/10-mcp-server.md +3 -2
  23. data/docs/manual/12-caching.md +43 -4
  24. data/docs/manual/14-rails-quickstart.md +19 -11
  25. data/docs/manual/16-rbs-extended-annotations.md +40 -7
  26. data/docs/manual/19-effect-labels.md +10 -0
  27. data/docs/manual/plugins/README.md +7 -0
  28. data/docs/manual/plugins/rigor-actioncable.md +8 -1
  29. data/docs/manual/plugins/rigor-actionmailer.md +7 -0
  30. data/docs/manual/plugins/rigor-actionpack.md +243 -0
  31. data/docs/manual/plugins/rigor-active-model-serializers.md +153 -0
  32. data/docs/manual/plugins/rigor-activejob.md +7 -0
  33. data/docs/manual/plugins/rigor-activerecord.md +110 -4
  34. data/docs/manual/plugins/rigor-activestorage.md +8 -1
  35. data/docs/manual/plugins/rigor-activesupport-core-ext.md +11 -5
  36. data/docs/manual/plugins/rigor-grape.md +106 -0
  37. data/docs/manual/plugins/rigor-graphql.md +23 -2
  38. data/docs/manual/plugins/rigor-pundit.md +8 -1
  39. data/docs/manual/plugins/rigor-rails-i18n.md +10 -5
  40. data/docs/manual/plugins/rigor-rails-routes.md +7 -0
  41. data/docs/manual/plugins/rigor-rbs-inline.md +212 -25
  42. data/docs/manual/plugins/rigor-sidekiq.md +8 -1
  43. data/docs/manual/plugins/rigor-sorbet.md +21 -5
  44. data/exe/rigor +19 -4
  45. data/lib/rigor/analysis/baseline.rb +1 -1
  46. data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +167 -2
  47. data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +189 -0
  48. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +5 -3
  49. data/lib/rigor/analysis/check_rules/rule_ids.rb +20 -5
  50. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  51. data/lib/rigor/analysis/check_rules/source_arity.rb +427 -0
  52. data/lib/rigor/analysis/check_rules/special_global_setters.rb +146 -0
  53. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +36 -2
  54. data/lib/rigor/analysis/check_rules.rb +638 -136
  55. data/lib/rigor/analysis/dependency_recorder.rb +222 -17
  56. data/lib/rigor/analysis/fact_store.rb +9 -0
  57. data/lib/rigor/analysis/incremental.rb +26 -0
  58. data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
  59. data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
  60. data/lib/rigor/analysis/incremental_session.rb +309 -53
  61. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
  62. data/lib/rigor/analysis/project_scan.rb +11 -1
  63. data/lib/rigor/analysis/reachability/plugin_roots.rb +0 -1
  64. data/lib/rigor/analysis/reachability/scan_cache.rb +3 -1
  65. data/lib/rigor/analysis/rule_catalog.rb +127 -0
  66. data/lib/rigor/analysis/run_cache_key.rb +46 -18
  67. data/lib/rigor/analysis/run_stats.rb +2 -2
  68. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +168 -12
  69. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +9 -4
  70. data/lib/rigor/analysis/runner/pool_coordinator.rb +109 -17
  71. data/lib/rigor/analysis/runner/project_pre_passes.rb +44 -17
  72. data/lib/rigor/analysis/runner.rb +603 -54
  73. data/lib/rigor/analysis/shadow_harness.rb +27 -0
  74. data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
  75. data/lib/rigor/analysis/template_unit_collector.rb +303 -0
  76. data/lib/rigor/analysis/template_unit_paths.rb +93 -0
  77. data/lib/rigor/analysis/template_unit_positions.rb +293 -0
  78. data/lib/rigor/analysis/template_units.rb +409 -0
  79. data/lib/rigor/analysis/worker_session.rb +57 -13
  80. data/lib/rigor/bleeding_edge.rb +0 -2
  81. data/lib/rigor/builtins/hkt_builtins.rb +1 -0
  82. data/lib/rigor/builtins/imported_refinements.rb +4 -0
  83. data/lib/rigor/builtins/regex_refinement.rb +17 -9
  84. data/lib/rigor/builtins/static_return_refinements.rb +2 -0
  85. data/lib/rigor/cache/descriptor.rb +45 -1
  86. data/lib/rigor/cache/engine_source.rb +29 -1
  87. data/lib/rigor/cache/file_digest.rb +28 -0
  88. data/lib/rigor/cache/incremental_snapshot.rb +102 -8
  89. data/lib/rigor/cache/rbs_cache_producer.rb +4 -0
  90. data/lib/rigor/cache/rbs_descriptor.rb +37 -7
  91. data/lib/rigor/cache/store.rb +63 -4
  92. data/lib/rigor/cache/value_digest.rb +325 -0
  93. data/lib/rigor/ci_detector.rb +1 -0
  94. data/lib/rigor/cli/check_command.rb +83 -13
  95. data/lib/rigor/cli/doc_links.rb +1 -1
  96. data/lib/rigor/cli/docs_command.rb +4 -4
  97. data/lib/rigor/cli/doctor_command.rb +1 -1
  98. data/lib/rigor/cli/effects_report.rb +2 -2
  99. data/lib/rigor/cli/plugin_command.rb +3 -3
  100. data/lib/rigor/cli/prism_colorizer.rb +0 -1
  101. data/lib/rigor/cli/sig_gen_command.rb +210 -29
  102. data/lib/rigor/cli/skill_command.rb +1 -1
  103. data/lib/rigor/cli/skill_describe.rb +0 -1
  104. data/lib/rigor/cli/type_of_command.rb +19 -5
  105. data/lib/rigor/cli/type_of_renderer.rb +20 -9
  106. data/lib/rigor/cli/type_of_template_probe.rb +189 -0
  107. data/lib/rigor/cli/unused_command.rb +3 -3
  108. data/lib/rigor/cli.rb +21 -3
  109. data/lib/rigor/config_audit.rb +3 -2
  110. data/lib/rigor/config_path.rb +29 -0
  111. data/lib/rigor/configuration/severity_profile.rb +24 -3
  112. data/lib/rigor/configuration.rb +71 -7
  113. data/lib/rigor/effects/ancestry_recorder.rb +191 -0
  114. data/lib/rigor/effects/attribution.rb +11 -2
  115. data/lib/rigor/effects/callee_rule.rb +368 -0
  116. data/lib/rigor/effects/catalog.rb +7 -4
  117. data/lib/rigor/effects/collector.rb +11 -5
  118. data/lib/rigor/effects/config_envelopes.rb +9 -2
  119. data/lib/rigor/effects/definition_context.rb +179 -0
  120. data/lib/rigor/effects/effect_table.rb +11 -3
  121. data/lib/rigor/effects/envelope_check.rb +1 -1
  122. data/lib/rigor/effects/envelope_index.rb +15 -0
  123. data/lib/rigor/effects/file_collection.rb +59 -5
  124. data/lib/rigor/effects/framework_units.rb +1 -1
  125. data/lib/rigor/effects/identity.rb +16 -0
  126. data/lib/rigor/effects/local_ownership.rb +38 -12
  127. data/lib/rigor/effects/method_key.rb +21 -0
  128. data/lib/rigor/effects/mutation_classifier.rb +23 -12
  129. data/lib/rigor/effects/plugin_facts.rb +49 -37
  130. data/lib/rigor/effects/propagator.rb +295 -16
  131. data/lib/rigor/effects/registry.rb +1 -1
  132. data/lib/rigor/effects/scanner.rb +121 -72
  133. data/lib/rigor/effects/signature_sources.rb +1 -1
  134. data/lib/rigor/effects/snapshot.rb +2 -1
  135. data/lib/rigor/effects/summary.rb +27 -4
  136. data/lib/rigor/effects/unit_scan.rb +385 -30
  137. data/lib/rigor/effects/visibility.rb +101 -0
  138. data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
  139. data/lib/rigor/environment/lockfile_resolver.rb +27 -1
  140. data/lib/rigor/environment/member_consistency/comparator.rb +708 -0
  141. data/lib/rigor/environment/member_consistency.rb +298 -0
  142. data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
  143. data/lib/rigor/environment/rbs_loader.rb +400 -134
  144. data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
  145. data/lib/rigor/environment.rb +104 -96
  146. data/lib/rigor/hashing/xxh3.rb +264 -0
  147. data/lib/rigor/inference/acceptance.rb +139 -9
  148. data/lib/rigor/inference/block_auto_splat.rb +216 -0
  149. data/lib/rigor/inference/block_call_timing.rb +338 -0
  150. data/lib/rigor/inference/block_parameter_binder.rb +88 -26
  151. data/lib/rigor/inference/block_repetition.rb +71 -0
  152. data/lib/rigor/inference/body_fixpoint.rb +2 -1
  153. data/lib/rigor/inference/budget_trace.rb +2 -1
  154. data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
  155. data/lib/rigor/inference/builtins/string_catalog.rb +1 -1
  156. data/lib/rigor/inference/captured_locals.rb +393 -15
  157. data/lib/rigor/inference/closure_escape_analyzer.rb +157 -13
  158. data/lib/rigor/inference/content_join.rb +200 -27
  159. data/lib/rigor/inference/declaration_walk/context.rb +195 -0
  160. data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
  161. data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
  162. data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
  163. data/lib/rigor/inference/declaration_walk.rb +7 -0
  164. data/lib/rigor/inference/def_node_resolver.rb +3 -2
  165. data/lib/rigor/inference/def_return_typer.rb +11 -7
  166. data/lib/rigor/inference/define_method_block_self.rb +64 -0
  167. data/lib/rigor/inference/definer_resolution.rb +266 -0
  168. data/lib/rigor/inference/element_read_widening.rb +22 -10
  169. data/lib/rigor/inference/error_info.rb +196 -0
  170. data/lib/rigor/inference/expression_typer.rb +1665 -538
  171. data/lib/rigor/inference/external_ancestor_resolution.rb +264 -0
  172. data/lib/rigor/inference/fresh_frame_blocks.rb +127 -0
  173. data/lib/rigor/inference/global_write_census.rb +239 -0
  174. data/lib/rigor/inference/guard_rebinding.rb +488 -0
  175. data/lib/rigor/inference/hash_lookup_mutation.rb +88 -0
  176. data/lib/rigor/inference/index_write_widening.rb +16 -3
  177. data/lib/rigor/inference/indexed_narrowing.rb +61 -9
  178. data/lib/rigor/inference/jump_targets.rb +82 -0
  179. data/lib/rigor/inference/last_line/implicit_self.rb +210 -0
  180. data/lib/rigor/inference/last_line/self_evidence.rb +329 -0
  181. data/lib/rigor/inference/last_line.rb +340 -0
  182. data/lib/rigor/inference/last_status.rb +144 -0
  183. data/lib/rigor/inference/macro_block_self_type.rb +179 -17
  184. data/lib/rigor/inference/match_rebinding/calls.rb +281 -0
  185. data/lib/rigor/inference/match_rebinding/frame.rb +148 -0
  186. data/lib/rigor/inference/match_rebinding/operands.rb +236 -0
  187. data/lib/rigor/inference/match_rebinding/self_calls.rb +83 -0
  188. data/lib/rigor/inference/match_rebinding.rb +392 -0
  189. data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
  190. data/lib/rigor/inference/method_dispatcher/block_folding.rb +85 -24
  191. data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +147 -0
  192. data/lib/rigor/inference/method_dispatcher/hash_transform_keys_folding.rb +191 -0
  193. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +4 -0
  194. data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +159 -0
  195. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +113 -73
  196. data/lib/rigor/inference/method_dispatcher/process_folding.rb +56 -0
  197. data/lib/rigor/inference/method_dispatcher/proven_overload.rb +72 -0
  198. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +901 -38
  199. data/lib/rigor/inference/method_dispatcher/self_substitute.rb +142 -0
  200. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +128 -53
  201. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +1 -1
  202. data/lib/rigor/inference/method_dispatcher.rb +214 -13
  203. data/lib/rigor/inference/method_parameter_binder.rb +8 -3
  204. data/lib/rigor/inference/module_function_state.rb +213 -0
  205. data/lib/rigor/inference/multi_target_binder.rb +345 -52
  206. data/lib/rigor/inference/mutation_rejoin.rb +4 -1
  207. data/lib/rigor/inference/mutation_widening.rb +70 -48
  208. data/lib/rigor/inference/narrowing.rb +674 -124
  209. data/lib/rigor/inference/operand_effects.rb +167 -0
  210. data/lib/rigor/inference/operand_walk.rb +88 -0
  211. data/lib/rigor/inference/optimistic_origin.rb +152 -9
  212. data/lib/rigor/inference/parameter_inference_collector.rb +13 -11
  213. data/lib/rigor/inference/project_method_ownership.rb +136 -0
  214. data/lib/rigor/inference/project_patched_methods.rb +7 -2
  215. data/lib/rigor/inference/project_patched_scanner.rb +7 -3
  216. data/lib/rigor/inference/receiver_alias.rb +90 -1
  217. data/lib/rigor/inference/receiver_blind_block.rb +219 -0
  218. data/lib/rigor/inference/refinement_mutation.rb +15 -11
  219. data/lib/rigor/inference/repeated_or_writes.rb +463 -0
  220. data/lib/rigor/inference/return_barrier.rb +54 -0
  221. data/lib/rigor/inference/rewrite_mutation.rb +120 -0
  222. data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
  223. data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
  224. data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
  225. data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
  226. data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
  227. data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
  228. data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
  229. data/lib/rigor/inference/scope_indexer.rb +5913 -701
  230. data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
  231. data/lib/rigor/inference/statement_evaluator.rb +3249 -497
  232. data/lib/rigor/inference/stored_block_call.rb +54 -0
  233. data/lib/rigor/inference/string_mutation.rb +44 -7
  234. data/lib/rigor/inference/unknown_store_widening.rb +200 -0
  235. data/lib/rigor/inference/unthreaded_rebinds.rb +282 -0
  236. data/lib/rigor/language_server/debouncer.rb +0 -1
  237. data/lib/rigor/language_server/diagnostic_publisher.rb +3 -2
  238. data/lib/rigor/language_server/hover_renderer.rb +3 -3
  239. data/lib/rigor/language_server/project_context.rb +5 -3
  240. data/lib/rigor/mcp/server.rb +2 -1
  241. data/lib/rigor/plugin/base.rb +196 -13
  242. data/lib/rigor/plugin/box_probe.rb +91 -0
  243. data/lib/rigor/plugin/bundled_catalog.rb +1 -1
  244. data/lib/rigor/plugin/effect_attribution.rb +58 -4
  245. data/lib/rigor/plugin/io_boundary.rb +82 -4
  246. data/lib/rigor/plugin/isolation.rb +1 -1
  247. data/lib/rigor/plugin/loader.rb +2 -1
  248. data/lib/rigor/plugin/macro/block_as_method.rb +45 -6
  249. data/lib/rigor/plugin/manifest.rb +71 -10
  250. data/lib/rigor/plugin/registry.rb +35 -1
  251. data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
  252. data/lib/rigor/plugin/template_unit.rb +196 -0
  253. data/lib/rigor/plugin/trust_policy.rb +3 -3
  254. data/lib/rigor/plugin.rb +1 -0
  255. data/lib/rigor/protection/discovery_seed.rb +20 -9
  256. data/lib/rigor/protection/kill_signature.rb +0 -1
  257. data/lib/rigor/protection/mutation_cache.rb +4 -5
  258. data/lib/rigor/rbs_extended.rb +27 -0
  259. data/lib/rigor/reflection/constant_ancestors.rb +146 -0
  260. data/lib/rigor/reflection/constant_path.rb +41 -13
  261. data/lib/rigor/reflection.rb +73 -107
  262. data/lib/rigor/scope/discovery_index.rb +351 -2
  263. data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
  264. data/lib/rigor/scope/resolution_chain.rb +884 -0
  265. data/lib/rigor/scope.rb +981 -198
  266. data/lib/rigor/sig_gen/alias_index.rb +289 -0
  267. data/lib/rigor/sig_gen/classification.rb +22 -5
  268. data/lib/rigor/sig_gen/declaration_equivalence.rb +127 -0
  269. data/lib/rigor/sig_gen/effect_annotation.rb +202 -0
  270. data/lib/rigor/sig_gen/generator.rb +580 -47
  271. data/lib/rigor/sig_gen/inline_declarations.rb +184 -0
  272. data/lib/rigor/sig_gen/method_candidate.rb +47 -3
  273. data/lib/rigor/sig_gen/observation_collector.rb +1 -1
  274. data/lib/rigor/sig_gen/renderer.rb +124 -9
  275. data/lib/rigor/sig_gen/skip_reason_catalog.rb +44 -1
  276. data/lib/rigor/sig_gen/write_result.rb +19 -3
  277. data/lib/rigor/sig_gen/writer.rb +166 -35
  278. data/lib/rigor/sig_gen.rb +3 -0
  279. data/lib/rigor/signature_path_audit.rb +1 -1
  280. data/lib/rigor/source/node_walker.rb +0 -3
  281. data/lib/rigor/source/parameter_envelope.rb +72 -0
  282. data/lib/rigor/source.rb +1 -0
  283. data/lib/rigor/type/combinator.rb +112 -12
  284. data/lib/rigor/type/difference.rb +1 -0
  285. data/lib/rigor/type/hash_shape.rb +1 -1
  286. data/lib/rigor/type/refined.rb +1 -0
  287. data/lib/rigor/version.rb +1 -1
  288. data/lib/rigor.rb +1 -0
  289. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +1 -0
  290. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +9 -5
  291. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -4
  292. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +0 -1
  293. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_scan.rb +96 -0
  294. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +65 -8
  295. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/erb_compiler.rb +270 -0
  296. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +402 -0
  297. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +370 -0
  298. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_units.rb +132 -0
  299. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +239 -4
  300. data/plugins/rigor-actionpack/sig/action_view.rbs +43 -0
  301. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +220 -0
  302. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_index.rb +56 -0
  303. data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +263 -0
  304. data/plugins/rigor-active-model-serializers/lib/rigor-active-model-serializers.rb +7 -0
  305. data/plugins/rigor-active-model-serializers/sig/active_model_serializers.rbs +45 -0
  306. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +1 -0
  307. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +9 -5
  308. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +35 -3
  309. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +72 -12
  310. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +380 -3
  311. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +14 -6
  312. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +281 -35
  313. data/plugins/rigor-activerecord/sig/active_record/relation.rbs +206 -31
  314. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -14
  315. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +9 -2
  316. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +171 -5
  317. data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +2 -0
  318. data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +1 -0
  319. data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +1 -1
  320. data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
  321. data/plugins/rigor-grape/lib/rigor/plugin/grape.rb +141 -0
  322. data/plugins/rigor-grape/lib/rigor-grape.rb +6 -0
  323. data/plugins/rigor-grape/sig/grape.rbs +263 -0
  324. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +72 -2
  325. data/plugins/rigor-graphql/sig/graphql.rbs +485 -0
  326. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +2 -0
  327. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +11 -7
  328. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +35 -37
  329. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +0 -1
  330. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +2 -0
  331. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/doorkeeper_routes.rb +1 -0
  332. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +9 -17
  333. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline/same_line_annotations.rb +149 -0
  334. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +217 -36
  335. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +1 -0
  336. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +9 -5
  337. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +0 -1
  338. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +46 -1
  339. data/plugins/rigor-sorbet/sig/sorbet.rbs +277 -0
  340. data/sig/rigor/analysis/baseline.rbs +69 -7
  341. data/sig/rigor/analysis/fact_store.rbs +1 -1
  342. data/sig/rigor/analysis/project_scan.rbs +74 -0
  343. data/sig/rigor/effects/config_envelopes.rbs +100 -0
  344. data/sig/rigor/effects/effect_table.rbs +60 -0
  345. data/sig/rigor/effects/envelope.rbs +97 -0
  346. data/sig/rigor/effects/envelope_index.rbs +33 -0
  347. data/sig/rigor/effects/file_collection.rbs +81 -0
  348. data/sig/rigor/effects/label.rbs +26 -0
  349. data/sig/rigor/effects/label_set.rbs +46 -0
  350. data/sig/rigor/effects/method_key.rbs +24 -0
  351. data/sig/rigor/effects/origin.rbs +50 -0
  352. data/sig/rigor/effects/plugin_facts.rbs +141 -0
  353. data/sig/rigor/effects/registry.rbs +68 -0
  354. data/sig/rigor/effects/summary.rbs +50 -0
  355. data/sig/rigor/effects/taint_cause.rbs +12 -0
  356. data/sig/rigor/environment.rbs +14 -10
  357. data/sig/rigor/inference/optimistic_origin.rbs +10 -0
  358. data/sig/rigor/inference.rbs +6 -4
  359. data/sig/rigor/plugin/additional_initializer.rbs +36 -0
  360. data/sig/rigor/plugin/base.rbs +30 -7
  361. data/sig/rigor/plugin/effect_ancestry.rbs +27 -0
  362. data/sig/rigor/plugin/effect_attribution.rbs +66 -0
  363. data/sig/rigor/plugin/effect_edge.rbs +33 -0
  364. data/sig/rigor/plugin/effect_entry_points.rbs +25 -0
  365. data/sig/rigor/plugin/io_boundary.rbs +2 -1
  366. data/sig/rigor/plugin/loader.rbs +3 -3
  367. data/sig/rigor/plugin/manifest.rbs +50 -11
  368. data/sig/rigor/plugin/protocol_contract.rbs +68 -0
  369. data/sig/rigor/plugin/registry.rbs +62 -1
  370. data/sig/rigor/plugin.rbs +1 -1
  371. data/sig/rigor/rbs_extended.rbs +1 -1
  372. data/sig/rigor/reflection.rbs +7 -6
  373. data/sig/rigor/scope.rbs +177 -20
  374. data/sig/rigor/sig_gen/skip_reason_catalog.rbs +14 -7
  375. data/sig/rigor/source.rbs +4 -4
  376. data/sig/rigor/testing.rbs +10 -4
  377. data/sig/rigor/type.rbs +6 -0
  378. data/sig/rigor.rbs +38 -20
  379. data/skills/rigor-ask/SKILL.md +8 -5
  380. data/skills/rigor-baseline-reduce/SKILL.md +4 -6
  381. data/skills/rigor-ci-setup/SKILL.md +17 -21
  382. data/skills/rigor-doctor/SKILL.md +24 -22
  383. data/skills/rigor-doctor/references/01-checks.md +97 -33
  384. data/skills/rigor-editor-setup/SKILL.md +6 -4
  385. data/skills/rigor-mcp-setup/SKILL.md +5 -4
  386. data/skills/rigor-monkeypatch-resolve/SKILL.md +5 -3
  387. data/skills/rigor-next-steps/SKILL.md +4 -2
  388. data/skills/rigor-plugin-author/SKILL.md +19 -23
  389. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +9 -8
  390. data/skills/rigor-plugin-author/references/02-walker-and-types.md +12 -25
  391. data/skills/rigor-plugin-author/references/03-test-and-ship.md +6 -8
  392. data/skills/rigor-plugin-review/SKILL.md +6 -4
  393. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +2 -1
  394. data/skills/rigor-plugin-tune/SKILL.md +4 -2
  395. data/skills/rigor-project-init/SKILL.md +46 -18
  396. data/skills/rigor-project-init/references/01-detect.md +13 -9
  397. data/skills/rigor-project-init/references/02-configure.md +40 -10
  398. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +14 -14
  399. data/skills/rigor-project-init/references/04-sig-uplift.md +27 -14
  400. data/skills/rigor-protection-uplift/SKILL.md +4 -6
  401. data/skills/rigor-rbs-setup/SKILL.md +4 -2
  402. data/skills/rigor-type-oracle/SKILL.md +4 -6
  403. data/skills/rigor-type-oracle/references/01-oracle-commands.md +12 -8
  404. data/skills/rigor-type-oracle/references/03-gap-protocol.md +0 -3
  405. data/skills/rigor-unused-adjudicate/SKILL.md +4 -2
  406. data/skills/rigor-upgrade/SKILL.md +13 -8
  407. metadata +132 -1
@@ -43,10 +43,39 @@ module Rigor
43
43
  # - `#cache_descriptor` — flushes the accumulated entries into a fresh {Cache::Descriptor} for the
44
44
  # contribution that built it. URL fetches contribute `ConfigEntry` rows keyed `"url:#{url}"` with the
45
45
  # response body's SHA-256 so contributions invalidate when the remote document changes.
46
+ # - `#replay(descriptor)` — records a {Cache::Descriptor}'s rows as though this boundary had observed
47
+ # them (#1558). A producer served from its own cache ran no block and read nothing, so
48
+ # {Plugin::Base#cache_for} replays the served entry's stored dependency rows here; the run-result
49
+ # descriptor and any enclosing producer's descriptor then carry the served value's inputs.
46
50
  class IoBoundary
47
51
  URL_TIMEOUT_SECONDS = 10
48
52
  URL_MAX_BYTES = 10 * 1024 * 1024
49
53
 
54
+ ATTRIBUTION_KEY = :rigor_io_boundary_attribution
55
+ private_constant :ATTRIBUTION_KEY
56
+
57
+ # ADR-45 WD2 (#1507) — runs the block with `sink` (an Array) receiving every file and listing row ANY
58
+ # boundary records on this thread meanwhile, in addition to the boundary's own table, and returns the
59
+ # block's value. `Analysis::Runner` wraps each file's analysis in one when it records dependencies, so the
60
+ # `--incremental` session can keep the reads one file's analysis made beside that file's cached answer: a
61
+ # later run that serves the answer without re-analysing the file still has to validate what it read.
62
+ #
63
+ # A thread variable rather than a fiber-local, so a read a plugin makes from inside an `Enumerator` still
64
+ # lands in the sink. The boundary's own table is unchanged by it, including its first-existence-row-stands
65
+ # rule: the sink records what each read observed. Only reads are credited: a value a plugin memoised reaches
66
+ # later files without one, so the first file to trigger the read is the only one credited with it.
67
+ def self.attributing(sink)
68
+ previous = Thread.current.thread_variable_get(ATTRIBUTION_KEY)
69
+ Thread.current.thread_variable_set(ATTRIBUTION_KEY, sink)
70
+ yield
71
+ ensure
72
+ Thread.current.thread_variable_set(ATTRIBUTION_KEY, previous)
73
+ end
74
+
75
+ def self.attribute(entry)
76
+ Thread.current.thread_variable_get(ATTRIBUTION_KEY)&.push(entry)
77
+ end
78
+
50
79
  attr_reader :policy, :plugin_id
51
80
 
52
81
  def initialize(policy:, plugin_id:, http_client: DefaultHttpClient.new)
@@ -61,7 +90,7 @@ module Rigor
61
90
  @first_refused_path = nil
62
91
  end
63
92
 
64
- # Issue #959 — `TrustPolicy#allow_read?` refuses a path purely by `File.expand_path` comparison
93
+ # Issue #959 — `TrustPolicy#allow_read?` refuses a path purely by `File.absolute_path` comparison
65
94
  # (ADR-2's documented bound; no `File.realpath`), so a project rooted under a symlink (macOS'
66
95
  # `/tmp` → `/private/tmp` alias is the common case) can have every plugin read refused with no
67
96
  # visible cause: the boundary answered "not readable" and the plugin swallowed or surfaced only its
@@ -96,7 +125,7 @@ module Rigor
96
125
  # outcome is recorded: a path that exists but cannot be read (`EISDIR`, `EACCES`) is a failure the
97
126
  # plugin reports, not a probe of absence, and records nothing — as before.
98
127
  def read_file(path)
99
- absolute = File.expand_path(path.to_s)
128
+ absolute = File.absolute_path(path.to_s)
100
129
  unless @policy.allow_read?(absolute)
101
130
  record_refusal(absolute)
102
131
  raise AccessDeniedError.new(
@@ -159,7 +188,7 @@ module Rigor
159
188
  # @param path — project directory; relative paths expand against the working directory
160
189
  # @return the absolute paths directly under `path` in `Dir.glob` order; empty when not a directory
161
190
  def list_directory(path)
162
- absolute = File.expand_path(path.to_s)
191
+ absolute = File.absolute_path(path.to_s)
163
192
  entries = Dir.glob(File.join(absolute, "*"))
164
193
  unless @policy.allow_read?(absolute)
165
194
  record_refusal(absolute)
@@ -202,8 +231,53 @@ module Rigor
202
231
  Cache::Descriptor.new(files: files, configs: configs, globs: globs)
203
232
  end
204
233
 
234
+ # #1558 — records every row of `descriptor` as though this boundary had observed it. The caller is
235
+ # {Plugin::Base#cache_for}: a producer served from its own cache ran no block, so without the replay
236
+ # the value's inputs reached neither the ADR-45 run-result descriptor nor the dependency descriptor of
237
+ # a producer computing around it, and both went on validating fresh after those inputs changed.
238
+ #
239
+ # The rows are the ones a fresh hit just validated, so they describe the current filesystem, and they
240
+ # are merged by the precedence the live recorders use. A content row (`:stat` / `:digest` / `:mtime`)
241
+ # replaces an existence row for its path, as {#read_file} does after a probe. Any other collision
242
+ # keeps the row already here: an existence row never displaces one, as in {#probe}, and between two
243
+ # content rows or two glob rows for one slot the live row carries the newer stat tuple, which keeps
244
+ # the next warm run on its stat fast path. The tables are keyed by path, glob slot and URL key, so
245
+ # replaying the same descriptor twice adds nothing. Row order reaches no cache key, because
246
+ # {Cache::Descriptor#to_canonical_hash} sorts every slot.
247
+ #
248
+ # The policy is not consulted: the rows were recorded under it when the entry was written, and replay
249
+ # reads no file. A {Cache::Descriptor::GlobEntry} keeps its `mode`, which is part of its slot key.
250
+ #
251
+ # ADR-45 WD2 (#1507) — every replayed row is also handed to the {.attributing} sink, the ones a held row
252
+ # wins over included, exactly as the live recorders hand over what each read observed: a producer first
253
+ # asked while a file is analysed (rigor-actionpack's `:controller_index`, rigor-rails-i18n's
254
+ # `:locale_index`) serves that file its inputs, and the `--incremental` slot must credit them to it. On a
255
+ # miss the replayed descriptor is the whole boundary's ({Plugin::Base#producer_dependency_descriptor}), so
256
+ # the file is also credited with rows other reads recorded; that over-credits, which only ever makes the
257
+ # slot decline sooner.
258
+ #
259
+ # @param descriptor — a dependency descriptor; only its `files`, `configs` and `globs` slots are read,
260
+ # the only slots a boundary records
261
+ def replay(descriptor)
262
+ @mutex.synchronize do
263
+ descriptor.files.each { |entry| replay_file_entry(entry) }
264
+ descriptor.configs.each { |entry| @config_entries[entry.key] ||= entry }
265
+ descriptor.globs.each { |entry| @glob_entries[entry.slot_key] ||= entry }
266
+ end
267
+ (descriptor.files + descriptor.globs).each { |entry| self.class.attribute(entry) }
268
+ nil
269
+ end
270
+
205
271
  private
206
272
 
273
+ # The {#replay} precedence for one file row; the caller holds `@mutex`.
274
+ def replay_file_entry(entry)
275
+ current = @file_entries[entry.path]
276
+ return if current && !(current.comparator == :exists && entry.comparator != :exists)
277
+
278
+ @file_entries[entry.path] = entry
279
+ end
280
+
207
281
  # ADR-45 WD1b (#613) — the shared body of {#file?} / {#directory?}.
208
282
  #
209
283
  # The probe answers TRUTHFULLY for every path, in or out of the trusted-read scope, and never raises:
@@ -222,7 +296,7 @@ module Rigor
222
296
  # reverse) → NOTHING, the same bound `#read_file` pins for `EISDIR`: the probe observed neither a
223
297
  # usable file nor an absence, and either row would misdescribe what the plugin saw.
224
298
  def probe(path)
225
- absolute = File.expand_path(path.to_s)
299
+ absolute = File.absolute_path(path.to_s)
226
300
  answer = yield(absolute)
227
301
  unless @policy.allow_read?(absolute)
228
302
  record_refusal(absolute)
@@ -247,6 +321,7 @@ module Rigor
247
321
  # A content row replaces an earlier existence row for the same path outright: the file appeared and
248
322
  # its bytes were consumed, and a content row validates existence as well as content.
249
323
  @mutex.synchronize { @file_entries[path] = entry }
324
+ self.class.attribute(entry)
250
325
  end
251
326
 
252
327
  # ADR-45 WD1 (#577) — the absence row for a probed-but-missing path. A content row already recorded
@@ -263,6 +338,7 @@ module Rigor
263
338
  def record_absence_entry(path)
264
339
  entry = Cache::Descriptor::FileEntry.absent(path: path)
265
340
  @mutex.synchronize { @file_entries[path] ||= entry }
341
+ self.class.attribute(entry)
266
342
  end
267
343
 
268
344
  # ADR-45 WD1b (#613) — the presence row: fresh while the probed path exists, stale once it is gone.
@@ -273,6 +349,7 @@ module Rigor
273
349
  def record_presence_entry(path)
274
350
  entry = Cache::Descriptor::FileEntry.present(path: path)
275
351
  @mutex.synchronize { @file_entries[path] ||= entry }
352
+ self.class.attribute(entry)
276
353
  end
277
354
 
278
355
  # ADR-45 WD1c (#629) — the listing row for {#list_directory}, deduplicated per (root, pattern) slot.
@@ -281,6 +358,7 @@ module Rigor
281
358
  def record_glob_entry(root, pattern)
282
359
  entry = Cache::Descriptor::GlobEntry.compute(root: root, pattern: pattern)
283
360
  @mutex.synchronize { @glob_entries[entry.slot_key] = entry }
361
+ self.class.attribute(entry)
284
362
  end
285
363
 
286
364
  # Issue #959 — records that `path` fell outside every {TrustPolicy} read root, for {#refusal_summary}.
@@ -47,7 +47,7 @@ module Rigor
47
47
  # build resolves the same root and skips the write — #414), and reading an unshareable value out of
48
48
  # a module ivar from a non-main Ractor is the same `Ractor::IsolationError` as writing one.
49
49
  def target_bundle_root=(root)
50
- @target_bundle_root = root.nil? ? nil : File.expand_path(root.to_s).freeze
50
+ @target_bundle_root = root.nil? ? nil : File.absolute_path(root.to_s).freeze
51
51
  end
52
52
 
53
53
  # Requires `feature`, falling back to the analyzed project's bundler install tree. The fallback
@@ -35,13 +35,14 @@ module Rigor
35
35
  suffix = "/#{gem_name}.rb"
36
36
  $LOADED_FEATURES.rfind { |feature| feature.end_with?(suffix) }
37
37
  end
38
+ Ractor.make_shareable(FEATURE_RESOLVER)
38
39
 
39
40
  # #194 slice 2 (ADR-93 WD5) — the engine's own root, anchored from THIS file's location: the loader
40
41
  # lives at `<root>/lib/rigor/plugin/loader.rb`, so three levels up is the engine root. It resolves
41
42
  # identically in a git checkout and inside an installed `rigortype` gem, because the gem packages the
42
43
  # `plugins/` tree at the same relative path — which is exactly what makes it a trustworthy anchor for
43
44
  # the engine's own bundled plugin copies.
44
- ENGINE_ROOT = File.expand_path("../../..", __dir__)
45
+ ENGINE_ROOT = File.expand_path("../../..", __dir__).freeze
45
46
 
46
47
  # @param requirer — takes a gem name OR an absolute file path (#194 slice 2 — a bundled plugin is
47
48
  # required by its {.bundled_plugin_path}) and returns truthy on successful require. Defaulted to
@@ -36,9 +36,14 @@ module Rigor
36
36
  # - `method_names` — Array of Symbol method names. A call shape `<receiver_subclass>.get('/path') { ... }`
37
37
  # matches when `:get` is in this list. (Named `verbs:` before ADR-60 WD2 normalised the macro
38
38
  # value-object vocabulary.)
39
- # - `self_type` — Symbol selecting the kind of `self`-binding the substrate applies inside the block.
40
- # Slice 1a accepts only `:receiver_instance` (the block runs as an instance method of the receiver
41
- # class). Other kinds (`:receiver_singleton`, `:dsl_recorder`) are reserved for later slices.
39
+ # - `self_type` — the `self`-binding the substrate applies inside the block. `:receiver_instance`
40
+ # (the default) types `self` as an instance of the receiver class — the Sinatra contract. A String
41
+ # binds `self` to a *named* class instead of the receiver: `"Grape::Endpoint"` binds
42
+ # `Nominal[Grape::Endpoint]` (the block is `instance_eval`'d on an instance of that class, e.g. a
43
+ # route body on `Grape::Endpoint`), and `"singleton(Grape::API::Instance)"` binds
44
+ # `Singleton[Grape::API::Instance]` (the block is `instance_eval`'d on that *class object*, e.g.
45
+ # a `namespace` body on `Grape::API::Instance`). Reserved Symbol names (`:receiver_singleton`,
46
+ # `:dsl_recorder`) remain unaccepted.
42
47
  #
43
48
  # ## Ractor-shareability
44
49
  #
@@ -48,6 +53,13 @@ module Rigor
48
53
  SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
49
54
  VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
50
55
 
56
+ CLASS_NAME_PATTERN = /[A-Z]\w*(?:::[A-Z]\w*)*/
57
+ # `self_type: "Foo::Bar"` — the block is `instance_eval`'d on an instance of `Foo::Bar`.
58
+ NAMED_SELF_TYPE_PATTERN = /\A#{CLASS_NAME_PATTERN}\z/
59
+ # `self_type: "singleton(Foo::Bar)"` — the block is `instance_eval`'d on the `Foo::Bar` class
60
+ # object itself (Grape's `namespace`/`route_param` bodies run on `Grape::API::Instance`).
61
+ SINGLETON_SELF_TYPE_PATTERN = /\Asingleton\((#{CLASS_NAME_PATTERN})\)\z/
62
+
51
63
  attr_reader :receiver_constraint, :method_names, :self_type
52
64
 
53
65
  def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
@@ -57,7 +69,7 @@ module Rigor
57
69
 
58
70
  @receiver_constraint = receiver_constraint.dup.freeze
59
71
  @method_names = method_names.map(&:to_sym).freeze
60
- @self_type = self_type
72
+ @self_type = self_type.is_a?(String) ? self_type.dup.freeze : self_type
61
73
  freeze
62
74
  end
63
75
 
@@ -81,6 +93,31 @@ module Rigor
81
93
  [receiver_constraint, method_names, self_type].hash
82
94
  end
83
95
 
96
+ # The class name a String `self_type` binds `self` to — `"Foo::Bar"` for both the plain and the
97
+ # `singleton(Foo::Bar)` form — or `nil` for `:receiver_instance`.
98
+ def self_type_name
99
+ return nil unless self_type.is_a?(String)
100
+
101
+ self_type[SINGLETON_SELF_TYPE_PATTERN, 1] || self_type
102
+ end
103
+
104
+ # Whether a String `self_type` binds the *class object* (`"singleton(Foo::Bar)"`) rather than an
105
+ # instance (`"Foo::Bar"`). Nominal-receiver call sites only match instance-binding entries —
106
+ # a `singleton(...)` binding exists solely for `instance_eval`-on-class contracts, which always
107
+ # surface as `Singleton[X]` receivers.
108
+ def singleton_binding?
109
+ self_type.is_a?(String) && self_type.match?(SINGLETON_SELF_TYPE_PATTERN)
110
+ end
111
+
112
+ # Whether `self_type` names a class whose *instance* the block is `instance_eval`'d on
113
+ # (`"Foo::Bar"`). These entries are the only ones that also match `Nominal[Y]` receivers: inside
114
+ # an already-narrowed `instance_eval` body (`self : Nominal[ParamsScope]`), a nested call on
115
+ # `self` re-enters the same context. `:receiver_instance` keeps its original Singleton-only
116
+ # contract — a nominal receiver calling a class-level verb is a different (unmodelled) shape.
117
+ def named_instance_binding?
118
+ self_type.is_a?(String) && !singleton_binding?
119
+ end
120
+
84
121
  private
85
122
 
86
123
  def validate_receiver_constraint!(value)
@@ -108,10 +145,12 @@ module Rigor
108
145
 
109
146
  def validate_self_type!(self_type)
110
147
  return if VALID_SELF_TYPES.include?(self_type)
148
+ return if self_type.is_a?(String) &&
149
+ (self_type.match?(NAMED_SELF_TYPE_PATTERN) || self_type.match?(SINGLETON_SELF_TYPE_PATTERN))
111
150
 
112
151
  raise ArgumentError,
113
- "Plugin::Macro::BlockAsMethod#self_type must be one of #{VALID_SELF_TYPES.inspect}, " \
114
- "got #{self_type.inspect}"
152
+ "Plugin::Macro::BlockAsMethod#self_type must be one of #{VALID_SELF_TYPES.inspect} " \
153
+ "or a class-name String ('Foo::Bar' / 'singleton(Foo::Bar)'), got #{self_type.inspect}"
115
154
  end
116
155
  end
117
156
  end
@@ -49,23 +49,32 @@ module Rigor
49
49
  attr_reader :target_gems
50
50
 
51
51
  attr_reader :id, :version, :description, :config_schema, :config_defaults, :produces, :consumes,
52
- :owns_receivers, :open_receivers, :type_node_resolvers, :block_as_methods,
52
+ :owns_receivers, :open_receivers, :rbs_complete_ancestors, :rbs_complete_extends,
53
+ :type_node_resolvers,
54
+ :block_as_methods,
53
55
  :heredoc_templates, :nested_class_templates, :trait_registries,
54
56
  :hkt_registrations, :hkt_definitions, :signature_paths, :protocol_contracts,
55
57
  :source_rbs_synthesizer, :additional_initializers,
56
58
  :effect_root, :effect_labels, :effect_attributions, :effect_edges, :effect_entry_points,
57
59
  :effect_ancestry
58
60
 
59
- def initialize( # rubocop:disable Metrics/ParameterLists
61
+ # #392 — the project-relative globs whose files this plugin compiles into {TemplateUnit}s. Purely a
62
+ # CLAIM: the engine globs these, reads each match's bytes and offers them to
63
+ # {Base#template_units_for_file}, which is where the transform itself lives. A plugin declaring none
64
+ # is never asked, and a run whose plugins declare none does no extra globbing at all.
65
+ attr_reader :template_globs
66
+
67
+ def initialize( # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
60
68
  id:, version:,
61
69
  description: nil, config_schema: {}, target_gems: [],
62
- produces: [], consumes: [], owns_receivers: [], open_receivers: [], type_node_resolvers: [],
70
+ produces: [], consumes: [], owns_receivers: [], open_receivers: [], rbs_complete_ancestors: [],
71
+ rbs_complete_extends: [], type_node_resolvers: [],
63
72
  block_as_methods: [], heredoc_templates: [], nested_class_templates: [],
64
73
  trait_registries: [],
65
74
  hkt_registrations: [], hkt_definitions: [], signature_paths: [], protocol_contracts: [],
66
75
  source_rbs_synthesizer: nil, additional_initializers: [],
67
76
  effect_root: nil, effect_labels: [], effect_attributions: [], effect_edges: [],
68
- effect_entry_points: [], effect_ancestry: []
77
+ effect_entry_points: [], effect_ancestry: [], template_globs: []
69
78
  )
70
79
  validate_id!(id)
71
80
  validate_version!(version)
@@ -74,6 +83,8 @@ module Rigor
74
83
  validate_target_gems!(target_gems)
75
84
  validate_owns_receivers!(owns_receivers)
76
85
  validate_open_receivers!(open_receivers)
86
+ validate_rbs_complete_ancestors!(rbs_complete_ancestors)
87
+ validate_rbs_complete_extends!(rbs_complete_extends)
77
88
  validate_type_node_resolvers!(type_node_resolvers)
78
89
  validate_block_as_methods!(block_as_methods)
79
90
  validate_heredoc_templates!(heredoc_templates)
@@ -93,10 +104,11 @@ module Rigor
93
104
  hkt_registrations, hkt_definitions, signature_paths, protocol_contracts,
94
105
  source_rbs_synthesizer)
95
106
  assign_target_gems(target_gems)
107
+ assign_rbs_complete_ancestors(rbs_complete_ancestors, rbs_complete_extends)
96
108
  assign_nested_class_templates(nested_class_templates)
97
109
  assign_additional_initializers(additional_initializers)
98
110
  assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
99
- effect_entry_points, effect_ancestry)
111
+ effect_entry_points, effect_ancestry, template_globs)
100
112
  freeze
101
113
  end
102
114
 
@@ -133,6 +145,13 @@ module Rigor
133
145
  end
134
146
  private :assign_target_gems
135
147
 
148
+ # ADR-43 WD4 — assigned outside assign_fields (which already carries the maximum positional arity).
149
+ def assign_rbs_complete_ancestors(rbs_complete_ancestors, rbs_complete_extends)
150
+ @rbs_complete_ancestors = rbs_complete_ancestors.map { |c| c.to_s.dup.freeze }.freeze
151
+ @rbs_complete_extends = rbs_complete_extends.map { |c| c.to_s.dup.freeze }.freeze
152
+ end
153
+ private :assign_rbs_complete_ancestors
154
+
136
155
  # Assigned outside assign_fields (which already carries the maximum positional arity) — set in
137
156
  # `initialize` before the final freeze. ADR-36 nested-class emission tier.
138
157
  def assign_nested_class_templates(nested_class_templates)
@@ -162,15 +181,27 @@ module Rigor
162
181
  private :validate_effect_fields!
163
182
 
164
183
  def assign_effect_fields(effect_root, effect_labels, effect_attributions, effect_edges,
165
- effect_entry_points, effect_ancestry)
184
+ effect_entry_points, effect_ancestry, template_globs)
166
185
  @effect_root = effect_root.nil? ? nil : effect_root.to_s.dup.freeze
167
186
  @effect_labels = effect_labels.map { |label| label.to_s.dup.freeze }.uniq.sort.freeze
168
187
  @effect_attributions = effect_attributions.dup.freeze
169
188
  @effect_edges = effect_edges.dup.freeze
170
189
  @effect_entry_points = effect_entry_points.dup.freeze
171
190
  @effect_ancestry = effect_ancestry.dup.freeze
191
+ assign_template_globs(template_globs)
172
192
  end
173
193
  private :assign_effect_fields
194
+
195
+ # #392 — a glob is a String, and an absolute one is refused: a template unit is a project file, and
196
+ # a plugin reaching outside the project root is the trust boundary ADR-2 § "Plugin Trust and I/O
197
+ # Policy" draws. Validated and assigned in one call, because `#initialize` is at its line budget.
198
+ def assign_template_globs(globs)
199
+ validate_array_of!("template_globs", globs, "non-empty relative glob Strings") do |glob|
200
+ glob.is_a?(String) && !glob.empty? && !glob.start_with?("/") && !glob.include?("..")
201
+ end
202
+ @template_globs = globs.map { |glob| glob.to_s.dup.freeze }.uniq.freeze
203
+ end
204
+ private :assign_template_globs
174
205
  # rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
175
206
 
176
207
  public
@@ -196,7 +227,7 @@ module Rigor
196
227
  errors
197
228
  end
198
229
 
199
- def to_h # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
230
+ def to_h # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
200
231
  {
201
232
  "id" => id,
202
233
  "version" => version,
@@ -208,6 +239,8 @@ module Rigor
208
239
  "consumes" => consumes.map { |c| consumption_hash(c) },
209
240
  "owns_receivers" => owns_receivers,
210
241
  "open_receivers" => open_receivers,
242
+ "rbs_complete_ancestors" => rbs_complete_ancestors,
243
+ "rbs_complete_extends" => rbs_complete_extends,
211
244
  "type_node_resolvers" => type_node_resolvers.map { |r| r.class.name },
212
245
  "block_as_methods" => block_as_methods.map(&:to_h),
213
246
  "heredoc_templates" => heredoc_templates.map(&:to_h),
@@ -224,7 +257,8 @@ module Rigor
224
257
  "effect_attributions" => effect_attributions.map(&:to_h),
225
258
  "effect_edges" => effect_edges.map(&:to_h),
226
259
  "effect_entry_points" => effect_entry_points.map(&:to_h),
227
- "effect_ancestry" => effect_ancestry.map(&:to_h)
260
+ "effect_ancestry" => effect_ancestry.map(&:to_h),
261
+ "template_globs" => template_globs
228
262
  }
229
263
  end
230
264
 
@@ -233,10 +267,11 @@ module Rigor
233
267
  # otherwise. A third-party plugin's `effect_root:` is ignored here (and warned about by the registry),
234
268
  # so it opens only the root named after itself.
235
269
  def effect_owner
236
- return id if effect_root.nil?
270
+ root = effect_root
271
+ return id if root.nil?
237
272
  return id unless FirstParty.bundled?(id)
238
273
 
239
- effect_root
274
+ root
240
275
  end
241
276
 
242
277
  # Whether this plugin's `effect_attributions:` may carry `discharge: true` (ADR-103 WD6).
@@ -388,6 +423,32 @@ module Rigor
388
423
  validate_array_of!("open_receivers", open_receivers, "non-empty String") { |c| c.is_a?(String) && !c.empty? }
389
424
  end
390
425
 
426
+ # ADR-43 WD4 — `rbs_complete_ancestors:` declares the class names this plugin's bundled RBS covers
427
+ # COMPLETELY: a method call the signature does not declare is genuinely absent at runtime. A
428
+ # Ruby-source subclass of a listed class may then bridge inherited calls to the ancestor's RBS
429
+ # (the ADR-43 allow-list's manifest-declared half). The dual of `open_receivers:` — that field
430
+ # marks a class open to suppress diagnostics on an unbounded surface; this one marks a class
431
+ # closed to ENABLE them on subclasses. Listing a class whose signature is partial would turn
432
+ # every omitted inherited method into a `call.undefined-method` false positive on subclass
433
+ # receivers, so only classes the plugin's `signature_paths:` declares exhaustively belong here.
434
+ def validate_rbs_complete_ancestors!(rbs_complete_ancestors)
435
+ validate_array_of!("rbs_complete_ancestors", rbs_complete_ancestors, "non-empty String") do |c|
436
+ c.is_a?(String) && !c.empty?
437
+ end
438
+ end
439
+
440
+ # `rbs_complete_extends:` is the extend-edge twin of `rbs_complete_ancestors:` (ADR-43 WD4): it names
441
+ # the MODULES whose bundled RBS instance surface is complete, so a project `extend M` in a class or
442
+ # module body lets singleton-side calls on that class resolve M's declared instance methods
443
+ # (`class F; extend T::Sig; sig { ... }; end` reaching `T::Sig#sig`). Same completeness rule: a
444
+ # module whose declared surface is partial does not belong here — the extend edge would misreport
445
+ # real calls as `call.undefined-method`.
446
+ def validate_rbs_complete_extends!(rbs_complete_extends)
447
+ validate_array_of!("rbs_complete_extends", rbs_complete_extends, "non-empty String") do |c|
448
+ c.is_a?(String) && !c.empty?
449
+ end
450
+ end
451
+
391
452
  # ADR-13 slice 2 — `type_node_resolvers:` declares the plugin-supplied `TypeNodeResolver` instances the
392
453
  # parser consults (in slice 3) when an RBS::Extended payload's named- or generic-type head misses the
393
454
  # built-in registry. Slice 2 carries the declarations on the manifest and the registry exposes them in
@@ -373,6 +373,30 @@ module Rigor
373
373
  @open_receivers_set.include?(class_name.to_s)
374
374
  end
375
375
 
376
+ # ADR-43 WD4 — the aggregate set of "RBS-complete ancestor" class names declared across loaded
377
+ # plugins (manifest `rbs_complete_ancestors:`). `RbsDispatch`'s inherited-call bridge consults it
378
+ # alongside the engine's own allow-list: a Ruby-source subclass of a listed class resolves
379
+ # inherited calls against the ancestor's RBS, and the signature-reading rules apply to them.
380
+ attr_reader :rbs_complete_ancestors
381
+
382
+ def rbs_complete_ancestor?(class_name)
383
+ return false if class_name.nil?
384
+
385
+ @rbs_complete_ancestors_set.include?(class_name.to_s)
386
+ end
387
+
388
+ # The extend-edge twin of `rbs_complete_ancestors` — the aggregate set of module names declared via
389
+ # manifest `rbs_complete_extends:`. `RbsDispatch`'s singleton bridge consults it for `extend M`
390
+ # edges in project source: `class F; extend T::Sig; sig { ... }; end` resolves `sig` through
391
+ # `T::Sig`'s bundled RBS instance surface.
392
+ attr_reader :rbs_complete_extends
393
+
394
+ def rbs_complete_extends?(class_name)
395
+ return false if class_name.nil?
396
+
397
+ @rbs_complete_extends_set.include?(class_name.to_s)
398
+ end
399
+
376
400
  # ADR-28 — flat, ordered list of every loaded plugin's path-scoped method-protocol contracts, in
377
401
  # plugin registration order. Read from each plugin's `#protocol_contracts` (which the manifest backs
378
402
  # by default but a plugin MAY override to fold in per-project config). Consumed by
@@ -435,14 +459,24 @@ module Rigor
435
459
  # nil where {#guarded_manifest} caught a raising manifest read.
436
460
  def compile_aggregates(manifests)
437
461
  @additional_initializers = manifests.flat_map { |m| m&.additional_initializers || [] }.freeze
438
- @open_receivers = manifests.flat_map { |m| (m&.open_receivers || []).map(&:to_s) }.uniq.freeze
462
+ @open_receivers = aggregate_class_names(manifests, :open_receivers)
439
463
  @open_receivers_set = @open_receivers.to_set.freeze
464
+ @rbs_complete_ancestors = aggregate_class_names(manifests, :rbs_complete_ancestors)
465
+ @rbs_complete_ancestors_set = @rbs_complete_ancestors.to_set.freeze
466
+ @rbs_complete_extends = aggregate_class_names(manifests, :rbs_complete_extends)
467
+ @rbs_complete_extends_set = @rbs_complete_extends.to_set.freeze
440
468
  @type_node_resolvers = manifests.flat_map { |m| m&.type_node_resolvers || [] }.freeze
441
469
  @protocol_contracts = @plugins.flat_map { |p| safe_protocol_contracts(p) }.freeze
442
470
  @contracts_by_path = {}
443
471
  @effect_memo = {}
444
472
  end
445
473
 
474
+ # The shared shape of `open_receivers:` / `rbs_complete_ancestors:` — a flat uniq'd class-name list
475
+ # aggregated across manifests, each entry normalised to String.
476
+ def aggregate_class_names(manifests, field)
477
+ manifests.flat_map { |m| (m&.public_send(field) || []).map(&:to_s) }.uniq.freeze
478
+ end
479
+
446
480
  # Fail-soft per plugin: a plugin whose `effect_attributions:` override raises (a missing
447
481
  # `config/application.rb`, an unreadable path) contributes nothing rather than taking the run down.
448
482
  # An effect contribution is an enrichment; ADR-2 isolates plugin failures at the analyzer boundary
@@ -21,7 +21,12 @@ module Rigor
21
21
  # dropping an annotation it parsed. They are not the same news — the first says the file contributed
22
22
  # nothing, the second that it contributed all but one thing — so they carry distinct diagnostic ids.
23
23
  # Defaults to `:failed`, the pre-WD12 meaning, so an older caller records what it always did.
24
- Entry = Data.define(:plugin_id, :path, :message, :kind)
24
+ #
25
+ # `:contradiction` (ADR-112 WD5, issue #1075) is the consistency rule's error: a member two sources
26
+ # declare in ways that contradict, or a refinement outside its own declared type. It is the one kind
27
+ # positioned at a real line (`line`, nil for every other kind), because it usually sits in a `.rbs`
28
+ # file whose positions are exact; the others are file-level rows at line 1.
29
+ Entry = Data.define(:plugin_id, :path, :message, :kind, :line)
25
30
 
26
31
  def initialize
27
32
  @entries = []
@@ -34,12 +39,13 @@ module Rigor
34
39
  # file list and then drains each worker's stream into this one reporter, so without the dedup a
35
40
  # `--workers N` run printed N copies of a row a `--workers=0` run printed once. The same collapse the
36
41
  # {RbsExtended::Reporter} drain documents, for the same reason.
37
- def record(plugin_id:, path:, message:, kind: :failed)
42
+ def record(plugin_id:, path:, message:, kind: :failed, line: nil)
38
43
  entry = Entry.new(
39
44
  plugin_id: plugin_id.to_s.dup.freeze,
40
45
  path: path.to_s.dup.freeze,
41
46
  message: message.to_s.dup.freeze,
42
- kind: kind
47
+ kind: kind,
48
+ line: line
43
49
  )
44
50
  return nil if @seen.key?(entry)
45
51