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,123 @@
1
+ # Active Record's framework namespaces — issue #534 item 7.
2
+ #
3
+ # The 2026-09-01 corpus opacity sweep counted `ActiveRecord::RecordNotFound` 115 times on
4
+ # mastodon, modal in `rescue` clauses, and `ActiveModel` 165 / `Arel` 35 — every one of them
5
+ # resolving to `untyped` because nothing in the bundle named the constant. This file names them,
6
+ # and nothing more.
7
+ #
8
+ # ## The admission rule: a namespace or a rescued error, never a base class
9
+ #
10
+ # Declaring a constant makes it RBS-KNOWN, and a known class with an incomplete signature turns
11
+ # every member the signature omits into a false `call.undefined-method`. That risk is not
12
+ # uniform, so this file admits exactly two shapes:
13
+ #
14
+ # - **Bare namespace modules.** Nobody dispatches on a module used only as a constant path
15
+ # (`ActiveModel::Type::Boolean`, `Arel::Nodes`), so an empty declaration asserts nothing.
16
+ # - **Exception classes**, which are dispatched on — as the `=> e` of a rescue — but whose whole
17
+ # inherited surface is `StandardError`'s, which Rigor's core RBS already carries. Each is listed
18
+ # in the manifest's `open_receivers:` (ADR-26) as well, so the framework-specific readers this
19
+ # file does not enumerate (`RecordInvalid#record`, `StatementInvalid#cause`, …) stay lenient.
20
+ #
21
+ # What is deliberately NOT here is every class a project INHERITS from. `ActiveRecord::Base` is
22
+ # the case that matters: declaring it empty would close every model in the project, and turn
23
+ # `Account.where(...)`, `record.save!` and the whole generated column surface into
24
+ # `call.undefined-method` on working Rails code. The plugin types models through its model index
25
+ # instead, and the namespace stays open for it. The same argument keeps `ActiveModel::Model` and
26
+ # the other mixin modules bare rather than populated.
27
+ #
28
+ # The exception hierarchy is transcribed from Rails v8.0.2 `activerecord/lib/active_record/errors.rb`;
29
+ # a class whose superclass could not be stated with certainty was left out rather than guessed,
30
+ # because a wrong superclass is a wrong `rescue` reachability answer.
31
+
32
+ module ActiveRecord
33
+ class ActiveRecordError < StandardError
34
+ end
35
+
36
+ class RecordNotFound < ActiveRecordError
37
+ end
38
+
39
+ class RecordInvalid < ActiveRecordError
40
+ end
41
+
42
+ class RecordNotSaved < ActiveRecordError
43
+ end
44
+
45
+ class RecordNotDestroyed < ActiveRecordError
46
+ end
47
+
48
+ class ReadOnlyRecord < ActiveRecordError
49
+ end
50
+
51
+ class Rollback < ActiveRecordError
52
+ end
53
+
54
+ class StaleObjectError < ActiveRecordError
55
+ end
56
+
57
+ class ConnectionNotEstablished < ActiveRecordError
58
+ end
59
+
60
+ class StatementInvalid < ActiveRecordError
61
+ end
62
+
63
+ class WrappedDatabaseException < StatementInvalid
64
+ end
65
+
66
+ class RecordNotUnique < WrappedDatabaseException
67
+ end
68
+ end
69
+
70
+ # Active Model ships as its own gem, but `activerecord` depends on it unconditionally, so a project
71
+ # that enables this plugin always has it. There is no `rigor-activemodel`, and inventing one to hold
72
+ # eleven empty modules would be a plugin whose whole content is this file.
73
+ module ActiveModel
74
+ module Model
75
+ end
76
+
77
+ module API
78
+ end
79
+
80
+ module Validations
81
+ end
82
+
83
+ module Serialization
84
+ end
85
+
86
+ module Conversion
87
+ end
88
+
89
+ module Naming
90
+ end
91
+
92
+ module Attributes
93
+ end
94
+
95
+ module Dirty
96
+ end
97
+
98
+ module SecurePassword
99
+ end
100
+
101
+ module Callbacks
102
+ end
103
+
104
+ module Type
105
+ end
106
+
107
+ class ValidationError < StandardError
108
+ end
109
+ end
110
+
111
+ # Arel ships INSIDE the activerecord gem, so this plugin is its owner by the "the row lives in the
112
+ # plugin that owns the gem" rule. `Arel.sql` is the one member the corpus calls; the signature is
113
+ # written to accept anything because its extra binds are positional-or-keyword and an arity claim
114
+ # here would be a false `call.wrong-arity` on a shape this file has no reason to police.
115
+ module Arel
116
+ module Nodes
117
+ end
118
+
119
+ module Predications
120
+ end
121
+
122
+ def self.sql: (untyped, *untyped, **untyped) -> untyped
123
+ end
@@ -37,6 +37,7 @@ module Rigor
37
37
  class Activestorage < Rigor::Plugin::Base
38
38
  manifest(
39
39
  id: "activestorage",
40
+ target_gems: ["activestorage"],
40
41
  # Bumped 2026-09-02 (#621) — attachment owners are keyed by their de-rooted name and a reopened
41
42
  # class's attachments are UNIONed rather than clobbered, so a cached 0.1.0 index can be missing the
42
43
  # attachments the merge restores.
@@ -42,9 +42,13 @@ module Rigor
42
42
  class ActivesupportCoreExt < Rigor::Plugin::Base
43
43
  manifest(
44
44
  id: "activesupport-core-ext",
45
+ target_gems: ["activesupport"],
46
+ # Bumped 2026-09-10 (#534 item 7) — `ActiveSupport::Concern` is declared, so `extend
47
+ # ActiveSupport::Concern` resolves instead of reading as an unresolved constant.
48
+ #
45
49
  # Bumped 2026-09-02 (#632) — `ActiveSupport::Duration`'s reader surface (`ago`/`to_i`/`iso8601`/…)
46
50
  # is now declared, open_receivers-protected; see the class comment above.
47
- version: "0.4.0",
51
+ version: "0.5.0",
48
52
  description: "RBS bundle for the most-frequently-flagged ActiveSupport core_ext extensions, " \
49
53
  "plus the `ActiveSupport::Duration` type of the numeric time multipliers and its " \
50
54
  "reader surface.",
@@ -55,7 +59,10 @@ module Rigor
55
59
  # `call.undefined-method` fire against it. Distinct from the RECEIVER-side FP the multiplier
56
60
  # `dynamic_return` gate guards (`Time#day` vs `Duration#day`, in the class comment above) — this is
57
61
  # the class's OWN unenumerable member set.
58
- open_receivers: ["ActiveSupport::Duration"],
62
+ # `ActiveSupport::Concern` (#534 item 7) is here for the same reason at a different scale: the
63
+ # declaration names `included` / `prepended` / `class_methods` so `extend ActiveSupport::Concern`
64
+ # resolves, and everything else the module really responds to must stay lenient.
65
+ open_receivers: ["ActiveSupport::Duration", "ActiveSupport::Concern"],
59
66
  # ADR-103 WD10 (#387) — the IMPURE half of ActiveSupport: the clock, the notification bus and
60
67
  # `CurrentAttributes`. The `%a{pure}` sweep over the predicate surface is issue #388 and lands in
61
68
  # `sig/`, not here. See {Effects}.
@@ -163,6 +163,34 @@ class Object
163
163
  # exception to the try-shaped-dispatch rule above (design note § 11.2).
164
164
  %a{pure}
165
165
  def in?: (untyped) -> bool
166
+
167
+ # Issue #673 — `core_ext/object/to_query`, `core_ext/object/duplicable` and
168
+ # `core_ext/object/instance_variables` all hang off `Object`, so every
169
+ # RBS-known receiver reaches them; declaring them only on `Hash` (as
170
+ # `to_param` / `to_query` were) or only on `NilClass` (as `duplicable?` /
171
+ # `instance_values` were) left `"x".to_param`, `1.duplicable?` and friends a
172
+ # `call.undefined-method` on ordinary Rails code.
173
+ #
174
+ # `to_param` is `to_s`; `to_query(key)` interpolates two `CGI.escape` results.
175
+ # `key` is `untyped` because the body only calls `to_param` on it, and both
176
+ # `:name` and `"user[name]"` are ordinary call sites. Neither is `%a{pure}`:
177
+ # both dispatch `to_param` on a value whose own override is invisible here,
178
+ # the same reason `Hash#to_query` below carries no envelope.
179
+ def to_param: () -> String
180
+ def to_query: (untyped key) -> String
181
+
182
+ # `bool` and not `true`: `Object#duplicable?` answers true, but the
183
+ # `Singleton` (and, on a Ruby where methods are not duplicable, `Method` /
184
+ # `UnboundMethod`) overrides answer false. `-> true` on the universal
185
+ # receiver would make every `if x.duplicable?` an always-truthy branch —
186
+ # a false positive on the only shape anybody writes.
187
+ %a{pure}
188
+ def duplicable?: () -> bool
189
+
190
+ # Keys without the leading `@` for `instance_values`, with it for
191
+ # `instance_variable_names`.
192
+ def instance_values: () -> Hash[String, untyped]
193
+ def instance_variable_names: () -> Array[String]
166
194
  end
167
195
 
168
196
  # `nil.blank?` / `nil.present?` / `nil.try` are the most frequent
@@ -185,6 +213,13 @@ class NilClass
185
213
  %a{pure}
186
214
  def duplicable?: () -> true
187
215
  def instance_values: () -> Hash[String, untyped]
216
+ # `core_ext/object/to_query` overrides `to_param` on the three singleton
217
+ # values to return `self` rather than `Object#to_param`'s `to_s`, so the
218
+ # `Object` row above would carry a wrong return here. `NilClass#to_query`
219
+ # is its own method (`CGI.escape(key.to_param)`), still a String.
220
+ %a{pure}
221
+ def to_param: () -> nil
222
+ def to_query: (untyped key) -> String
188
223
  end
189
224
 
190
225
  class TrueClass
@@ -192,6 +227,8 @@ class TrueClass
192
227
  def blank?: () -> false
193
228
  %a{pure}
194
229
  def present?: () -> true
230
+ %a{pure}
231
+ def to_param: () -> true
195
232
  end
196
233
 
197
234
  class FalseClass
@@ -199,6 +236,8 @@ class FalseClass
199
236
  def blank?: () -> true
200
237
  %a{pure}
201
238
  def present?: () -> false
239
+ %a{pure}
240
+ def to_param: () -> false
202
241
  end
203
242
 
204
243
  # ---------------------------------------------------------------
@@ -486,6 +525,22 @@ end
486
525
  # purpose — some are already covered by the separate `dynamic_return` rule for `+`/`-`/`*` below, the rest
487
526
  # are simply outside this issue's scope and remain silently `Dynamic` rather than a guess.
488
527
  module ActiveSupport
528
+ # Issue #534 item 7 — `ActiveSupport::Concern` was 87 unresolved constant sites on mastodon, essentially
529
+ # all of them the `extend ActiveSupport::Concern` line at the top of a concern module. Naming it is a
530
+ # constant-resolution fix and nothing more: the three members are declared because `extend`ing a module
531
+ # puts them on the extender's singleton, and leaving them out of a now-RBS-known module is what would
532
+ # turn `included do … end` into a false `call.undefined-method` in every concern in the project. The
533
+ # blocks are `(?) -> untyped` because their body is evaluated against the INCLUDING class, which this
534
+ # signature has no way to name.
535
+ #
536
+ # `Concern` is listed in the manifest's `open_receivers:` for the rest of its surface (`append_features`,
537
+ # `class_methods`' generated `ClassMethods` constant, …).
538
+ module Concern
539
+ def included: (?untyped base) ?{ (?) -> untyped } -> untyped
540
+ def prepended: (?untyped base) ?{ (?) -> untyped } -> untyped
541
+ def class_methods: () { (?) -> untyped } -> untyped
542
+ end
543
+
489
544
  class Duration
490
545
  # The `ago` family (issue #659). `since(time = ::Time.current)` and `ago(time = ::Time.current)` are
491
546
  # the only two real methods; `from_now` / `after` are `alias`es of the first and `until` / `before`
@@ -501,13 +556,17 @@ module ActiveSupport
501
556
  # help there: it protects a Duration receiver, and the diagnostic lands on the `Time` the call
502
557
  # RETURNS. #658 declared that surface and #670 the `Date` / `DateTime` twins, so the chains resolve.
503
558
  #
504
- # `() -> Time` is the correct class, not a compromise. Under a zone these answer an
505
- # `ActiveSupport::TimeWithZone` (verified by calling them), and Rails overrides `TimeWithZone#is_a?`
506
- # to answer true for `::Time` — so `Time` is the class Rails ITSELF tells callers the value is, and
507
- # `Time.current` above already answers `Time` for the same reason. A `Time | TimeWithZone` union was
508
- # measured and rejected: it draws no `undefined-method`, but types every downstream call
509
- # `Dynamic[top]` (`iso8601`, `beginning_of_day`, `to_date`, `- 1.day`), which is worth no more than
510
- # leaving them undeclared while still firing a parameter mismatch on the TimeWithZone arm.
559
+ # `() -> TimeWithZone` since #673, where the class these actually build under a zone (verified by
560
+ # calling them) was finally declared — as a `::Time` SUBCLASS, so nothing a `Time` return bought is
561
+ # given up: every `Time` instance method still resolves on the result, every `Time`-typed parameter
562
+ # still accepts it, and the four TWZ-only readers (`time_zone`, `time`, `period`, `comparable_time`)
563
+ # stop being a false `call.undefined-method` on `1.hour.ago`. It was `() -> Time` before that, on the
564
+ # ground that Rails overrides `TimeWithZone#is_a?` to answer true for `::Time` true, and the
565
+ # subclass is the same claim made where the analyser can also see the extra readers. A
566
+ # `Time | TimeWithZone` UNION was measured and rejected: it draws no `undefined-method`, but types
567
+ # every downstream call `Dynamic[top]` (`iso8601`, `beginning_of_day`, `to_date`, `- 1.day`), which is
568
+ # worth no more than leaving them undeclared while still firing a parameter mismatch on the
569
+ # TimeWithZone arm.
511
570
  #
512
571
  # The with-an-argument form stays `(untyped) -> untyped` on purpose, and this is not caution: the
513
572
  # runtime return depends on the argument's class AND on whether the duration carries a date-scale
@@ -520,17 +579,17 @@ module ActiveSupport
520
579
  # what stops `created_at > 7.days.ago` folding to a constant. They are already attributed `nondet.time`
521
580
  # in `lib/rigor/plugin/activesupport_core_ext/effects.rb` — annotating them here would duplicate an
522
581
  # envelope the manifest already owns, and risk the two disagreeing.
523
- def ago: () -> Time
582
+ def ago: () -> TimeWithZone
524
583
  | (untyped time) -> untyped
525
- def until: () -> Time
584
+ def until: () -> TimeWithZone
526
585
  | (untyped time) -> untyped
527
- def before: () -> Time
586
+ def before: () -> TimeWithZone
528
587
  | (untyped time) -> untyped
529
- def since: () -> Time
588
+ def since: () -> TimeWithZone
530
589
  | (untyped time) -> untyped
531
- def from_now: () -> Time
590
+ def from_now: () -> TimeWithZone
532
591
  | (untyped time) -> untyped
533
- def after: () -> Time
592
+ def after: () -> TimeWithZone
534
593
  | (untyped time) -> untyped
535
594
 
536
595
  # `to_i` returns `@value.to_i` — always Integer, whether `@value` itself is Integer (`1.day`) or Float
@@ -574,6 +633,48 @@ module ActiveSupport
574
633
  %a{pure}
575
634
  def parts: () -> Hash[Symbol, Integer | Float]
576
635
  end
636
+
637
+ # ---------------------------------------------------------------
638
+ # ActiveSupport::TimeWithZone — issue #673
639
+ # ---------------------------------------------------------------
640
+ #
641
+ # `Time.current` and the `Duration#ago` family above really answer a `TimeWithZone` under a zone, and
642
+ # this class was not modelled at all, so the four readers a TWZ has and a `Time` does not —
643
+ # `time_zone`, `time`, `period`, `comparable_time` — were a false `call.undefined-method` on every
644
+ # `Time.current` receiver. Declaring them on `Time` instead would have been wrong: a plain `Time`
645
+ # genuinely lacks all four, and the diagnostic would stop firing where it is correct.
646
+ #
647
+ # `< ::Time` rather than a `Time | TimeWithZone` union at the producers. The union was measured during
648
+ # #632 and rejected — it types every downstream call `Dynamic[top]`. Inheritance is both cheaper and
649
+ # more honest: Rails overrides `TimeWithZone#is_a?` to answer true for `::Time`, so `Time` is the class
650
+ # Rails itself tells callers a TWZ is, and TWZ's `method_missing` forwards every name it does not
651
+ # define to the wrapped `::Time`, which is exactly what inheriting `Time`'s surface models. A TWZ
652
+ # therefore satisfies every `Time`-typed parameter, and chains keep resolving instead of collapsing.
653
+ #
654
+ # The cost, taken deliberately: `Time.current` answers a plain `Time` when `Time.zone` is nil, so
655
+ # `Time.current.time_zone` in a zone-less activesupport-only project is a real `NoMethodError` this
656
+ # now types. Rails sets `config.time_zone` ("UTC") by default, so the shape is rare, and the false
657
+ # positive it replaces fired on correct Rails code.
658
+ class TimeWithZone < ::Time
659
+ # `Time`'s own `initialize` takes the `(year, month, …)` parts; inheriting it would make
660
+ # `TimeWithZone.new(utc, zone)` — how Rails constructs one — a `call.argument-type-mismatch`.
661
+ # `untyped` because the zone and period carriers (`ActiveSupport::TimeZone`,
662
+ # `TZInfo::TimezonePeriod`) are not declared anywhere in this bundle.
663
+ def initialize: (untyped utc_time, untyped time_zone, ?untyped local_time, ?untyped period) -> void
664
+
665
+ # The `attr_reader`; an `ActiveSupport::TimeZone`, undeclared here.
666
+ %a{pure}
667
+ def time_zone: () -> untyped
668
+
669
+ # The wall-clock `::Time` in the zone. `utc` / `getutc` / `getgm` / `gmtime` are `Time`'s own names
670
+ # and stay inherited; `comparable_time` is the alias of `utc` that `Time` does not carry. Neither is
671
+ # `%a{pure}`: both memoise into an ivar on first call.
672
+ def time: () -> ::Time
673
+ def comparable_time: () -> ::Time
674
+
675
+ # A `TZInfo::TimezonePeriod`, undeclared here. Memoised like `time`, so not `%a{pure}`.
676
+ def period: () -> untyped
677
+ end
577
678
  end
578
679
 
579
680
  # ---------------------------------------------------------------
@@ -584,7 +685,11 @@ class Time
584
685
  # NOT %a{pure}: already attributed nondet.time/global.read/global.write
585
686
  # by `lib/rigor/plugin/activesupport_core_ext/effects.rb` (issue #387).
586
687
  # Duplicating an RBS envelope here risks the two disagreeing.
587
- def self.current: () -> Time
688
+ #
689
+ # `Time.zone ? Time.zone.now : Time.now` — an `ActiveSupport::TimeWithZone` under a zone, which since
690
+ # #673 is declared as a `::Time` subclass so the four TWZ-only readers resolve without the rest of the
691
+ # chain losing its `Time` typing. See that class's own note for the zone-less cost.
692
+ def self.current: () -> ActiveSupport::TimeWithZone
588
693
  def self.zone: () -> untyped # ActiveSupport::TimeZone | nil
589
694
  def self.zone=: (String | Symbol | untyped) -> untyped
590
695
 
@@ -60,6 +60,7 @@ module Rigor
60
60
  class Devise < Rigor::Plugin::Base
61
61
  manifest(
62
62
  id: "devise",
63
+ target_gems: ["devise"],
63
64
  version: "0.1.0",
64
65
  description: "Recognises Devise's `devise :strategy` DSL via ADR-16 Tier B.",
65
66
  trait_registries: [
@@ -44,6 +44,7 @@ module Rigor
44
44
 
45
45
  manifest(
46
46
  id: "dry-monads",
47
+ target_gems: ["dry-monads"],
47
48
  version: "0.1.0",
48
49
  description: "Lightweight HKT support for dry-monads Result and Maybe carriers",
49
50
  hkt_registrations: [RESULT_REGISTRATION, MAYBE_REGISTRATION],
@@ -40,24 +40,26 @@ module Rigor
40
40
 
41
41
  module_function
42
42
 
43
- # @param paths [Array<String>] absolute paths to `.rb` files the project's `paths:` resolves to.
44
- # @param type_aliases [Hash{String => String}] the ADR-9 `:dry_type_aliases` fact published by
43
+ # @param paths absolute paths to `.rb` files the project's `paths:` resolves to.
44
+ # @param io_boundary the plugin's {Rigor::Plugin::IoBoundary}; every source read goes
45
+ # through it (#630) so each scanned file is a recorded cache dependency.
46
+ # @param type_aliases — the ADR-9 `:dry_type_aliases` fact published by
45
47
  # `rigor-dry-types` when loaded. Used to resolve `value(Types::Email)` references to their
46
48
  # underlying class. Empty when the plugin isn't loaded.
47
- # @return [Hash{String => Hash{Symbol => Hash{Symbol => String}}}] frozen per-schema typed-key
49
+ # @return frozen per-schema typed-key
48
50
  # table. Empty when no recognisable schema declaration is found.
49
- def scan(paths:, type_aliases: {})
51
+ def scan(paths:, io_boundary:, type_aliases: {})
50
52
  table = {}
51
53
  paths.each do |path|
52
- scan_file(path, type_aliases).each do |schema_const, shape|
54
+ scan_file(path, io_boundary, type_aliases).each do |schema_const, shape|
53
55
  table[schema_const] ||= shape
54
56
  end
55
57
  end
56
58
  table.freeze
57
59
  end
58
60
 
59
- def scan_file(path, type_aliases)
60
- source = File.read(path)
61
+ def scan_file(path, io_boundary, type_aliases)
62
+ source = io_boundary.read_file(path)
61
63
  parse_result = Prism.parse(source, filepath: path)
62
64
  return {} unless parse_result.errors.empty?
63
65
 
@@ -78,6 +78,7 @@ module Rigor
78
78
  class DrySchema < Rigor::Plugin::Base
79
79
  manifest(
80
80
  id: "dry-schema",
81
+ target_gems: ["dry-schema"],
81
82
  version: "0.1.0",
82
83
  description: "Recognises `Dry::Schema.{Params,JSON,define} { ... }` declarations " \
83
84
  "and publishes the per-schema typed-key table.",
@@ -130,7 +131,8 @@ module Rigor
130
131
  # `paths:`, parse errors degrade silently.
131
132
  def prepare(services)
132
133
  type_aliases = services.fact_store.read(plugin_id: "dry-types", name: :dry_type_aliases) || {}
133
- table = SchemaScanner.scan(paths: scannable_paths(services), type_aliases: type_aliases)
134
+ table = SchemaScanner.scan(paths: scannable_paths(services), io_boundary: io_boundary,
135
+ type_aliases: type_aliases)
134
136
  return if table.empty?
135
137
 
136
138
  services.fact_store.publish(
@@ -164,11 +166,13 @@ module Rigor
164
166
  shapes[name] = entry.nil? ? nil : ResultShape.build(entry)
165
167
  end
166
168
 
169
+ # ADR-45 WD1b (#613 / #630) — the classification probes go through the boundary, so an entry that
170
+ # is not there yet (or stops being a directory) is a recorded dependency of the scan's input set.
167
171
  def scannable_paths(services)
168
172
  @scannable_paths ||= services.configuration.paths.flat_map do |entry|
169
- if File.directory?(entry)
173
+ if io_boundary.directory?(entry)
170
174
  Dir.glob(File.join(entry, "**", "*.rb"), sort: true)
171
- elsif File.file?(entry) && entry.end_with?(".rb")
175
+ elsif io_boundary.file?(entry) && entry.end_with?(".rb")
172
176
  [entry]
173
177
  else
174
178
  []
@@ -51,6 +51,7 @@ module Rigor
51
51
  class DryStruct < Rigor::Plugin::Base
52
52
  manifest(
53
53
  id: "dry-struct",
54
+ target_gems: ["dry-struct"],
54
55
  version: "0.2.0",
55
56
  description: "Recognises dry-struct `attribute :name, T` DSL via ADR-16 Tier C; " \
56
57
  "promotes the reader's return type through ADR-18's `returns_from_arg:` " \
@@ -42,11 +42,13 @@ module Rigor
42
42
 
43
43
  module_function
44
44
 
45
- # @param paths [Array<String>] absolute paths to `.rb` files the project's `paths:` resolves to.
46
- # @return [Hash{String => String}] frozen `{aliased_name => underlying_class_name}` map. Empty
45
+ # @param paths absolute paths to `.rb` files the project's `paths:` resolves to.
46
+ # @param io_boundary the plugin's {Rigor::Plugin::IoBoundary}; every source read goes
47
+ # through it (#630) so each scanned file is a recorded cache dependency.
48
+ # @return frozen `{aliased_name => underlying_class_name}` map. Empty
47
49
  # when no `include Dry.Types()` declaration is found.
48
- def scan(paths:)
49
- results = paths.flat_map { |path| scan_file(path) }
50
+ def scan(paths:, io_boundary:)
51
+ results = paths.flat_map { |path| scan_file(path, io_boundary) }
50
52
  modules = results.map { |r| r[:module_name] }.uniq
51
53
  return {}.freeze if modules.empty?
52
54
 
@@ -75,8 +77,8 @@ module Rigor
75
77
  end
76
78
  private_class_method :canonical_table
77
79
 
78
- def scan_file(path)
79
- source = File.read(path)
80
+ def scan_file(path, io_boundary)
81
+ source = io_boundary.read_file(path)
80
82
  parse_result = Prism.parse(source, filepath: path)
81
83
  return [] unless parse_result.errors.empty?
82
84
 
@@ -58,6 +58,7 @@ module Rigor
58
58
  class DryTypes < Rigor::Plugin::Base
59
59
  manifest(
60
60
  id: "dry-types",
61
+ target_gems: ["dry-types"],
61
62
  version: "0.1.0",
62
63
  description: "Recognises `module X; include Dry.Types(); end` and publishes the alias table.",
63
64
  produces: [:dry_type_aliases]
@@ -74,10 +75,11 @@ module Rigor
74
75
  # `watch:` covers exactly the files the scan reads: one glob per project `paths:` entry, co-extensive
75
76
  # with {#scannable_paths}. A {Cache::Descriptor::GlobEntry} digests every file matching its glob, so a
76
77
  # content edit, a file addition, and a file removal anywhere under those paths all move the digest and
77
- # invalidate the entry. The scan reads no file individually through the `IoBoundary`, so the evaluated
78
- # `watch:` globs are the entire dependency descriptor and they fully cover the scan's input.
78
+ # invalidate the entry. Since #630 the scan ALSO reads each file through the `IoBoundary`, so the
79
+ # descriptor carries a per-file row beside the globs: the globs alone already covered the scan's
80
+ # input, and the per-file rows are what a subset or incremental run has to invalidate on.
79
81
  producer :dry_type_aliases, watch: -> { alias_watch_globs } do |_params|
80
- AliasScanner.scan(paths: scannable_paths)
82
+ AliasScanner.scan(paths: scannable_paths, io_boundary: io_boundary)
81
83
  end
82
84
 
83
85
  # Builds the (cached) alias table and publishes it via the ADR-9 fact store. `producer_value` runs the
@@ -102,11 +104,13 @@ module Rigor
102
104
  # Resolves the project's `paths:` to a flat list of `.rb` files the scanner walks. Mirrors
103
105
  # `Analysis::Runner`'s `expand_paths` floor; we don't need the runner's full exclude/sort surface
104
106
  # because the alias table is a union — any duplicate scan is a no-op.
107
+ # ADR-45 WD1b (#613 / #630) — the classification probes go through the boundary, so an entry that
108
+ # is not there yet (or stops being a directory) is a recorded dependency of the scan's input set.
105
109
  def scannable_paths
106
110
  @scannable_paths ||= services.configuration.paths.flat_map do |entry|
107
- if File.directory?(entry)
111
+ if io_boundary.directory?(entry)
108
112
  Dir.glob(File.join(entry, "**", "*.rb"), sort: true)
109
- elsif File.file?(entry) && entry.end_with?(".rb")
113
+ elsif io_boundary.file?(entry) && entry.end_with?(".rb")
110
114
  [entry]
111
115
  else
112
116
  []
@@ -122,9 +126,9 @@ module Rigor
122
126
  # would read is watched, so any edit / addition / removal under those paths invalidates the cache.
123
127
  def alias_watch_globs
124
128
  services.configuration.paths.filter_map do |entry|
125
- if File.directory?(entry)
129
+ if io_boundary.directory?(entry)
126
130
  [entry, "**/*.rb"]
127
- elsif File.file?(entry) && entry.end_with?(".rb")
131
+ elsif io_boundary.file?(entry) && entry.end_with?(".rb")
128
132
  [File.dirname(entry), File.basename(entry)]
129
133
  end
130
134
  end
@@ -22,17 +22,19 @@ module Rigor
22
22
 
23
23
  module_function
24
24
 
25
- # @param paths [Array<String>] absolute paths to `.rb` files the project's `paths:` resolves to.
26
- # @return [Array<String>] frozen, sorted list of recognized contract class FQNs (e.g.
25
+ # @param paths absolute paths to `.rb` files the project's `paths:` resolves to.
26
+ # @param io_boundary the plugin's {Rigor::Plugin::IoBoundary}; every source read goes
27
+ # through it (#630) so each scanned file is a recorded cache dependency.
28
+ # @return frozen, sorted list of recognized contract class FQNs (e.g.
27
29
  # `["App::NewUserContract", "Types::EmailContract"]`).
28
- def scan(paths:)
30
+ def scan(paths:, io_boundary:)
29
31
  contracts = []
30
- paths.each { |path| contracts.concat(scan_file(path)) }
32
+ paths.each { |path| contracts.concat(scan_file(path, io_boundary)) }
31
33
  contracts.uniq.sort.freeze
32
34
  end
33
35
 
34
- def scan_file(path)
35
- source = File.read(path)
36
+ def scan_file(path, io_boundary)
37
+ source = io_boundary.read_file(path)
36
38
  parse_result = Prism.parse(source, filepath: path)
37
39
  return [] unless parse_result.errors.empty?
38
40
 
@@ -50,24 +52,24 @@ module Rigor
50
52
  # not a degrade-gracefully fallback, because this plugin ships no required/optional walker of
51
53
  # its own.
52
54
  #
53
- # @return [Hash{String => Hash{Symbol => Hash}}] contract FQN => `{params: <shape>}` and/or
55
+ # @return contract FQN => `{params: <shape>}` and/or
54
56
  # `{json: <shape>}` (only the recognised key(s) are present; a contract with neither present
55
57
  # contributes nothing). Each `<shape>` is exactly {DrySchema::SchemaScanner.collect_schema_shape}'s
56
58
  # `{required:, optional:, unmodelled:}` return.
57
- def scan_schema_blocks(paths:, type_aliases: {})
59
+ def scan_schema_blocks(paths:, io_boundary:, type_aliases: {})
58
60
  return {} unless Rigor::Plugin.registered_for("dry-schema")
59
61
 
60
62
  table = {}
61
63
  paths.each do |path|
62
- scan_file_schema_blocks(path, type_aliases).each do |fqn, shapes|
64
+ scan_file_schema_blocks(path, io_boundary, type_aliases).each do |fqn, shapes|
63
65
  table[fqn] ||= shapes
64
66
  end
65
67
  end
66
68
  table.freeze
67
69
  end
68
70
 
69
- def scan_file_schema_blocks(path, type_aliases)
70
- source = File.read(path)
71
+ def scan_file_schema_blocks(path, io_boundary, type_aliases)
72
+ source = io_boundary.read_file(path)
71
73
  parse_result = Prism.parse(source, filepath: path)
72
74
  return {} unless parse_result.errors.empty?
73
75
 
@@ -44,6 +44,7 @@ module Rigor
44
44
  class DryValidation < Rigor::Plugin::Base
45
45
  manifest(
46
46
  id: "dry-validation",
47
+ target_gems: ["dry-validation"],
47
48
  version: "0.1.0",
48
49
  description: "Recognises `class T < Dry::Validation::Contract` subclasses, publishes the " \
49
50
  "contract FQN set, and (with rigor-dry-schema loaded) refines each contract's " \
@@ -94,7 +95,7 @@ module Rigor
94
95
  end
95
96
 
96
97
  def prepare(services)
97
- contracts = ContractScanner.scan(paths: scannable_paths(services))
98
+ contracts = ContractScanner.scan(paths: scannable_paths(services), io_boundary: io_boundary)
98
99
  unless contracts.empty?
99
100
  services.fact_store.publish(
100
101
  plugin_id: manifest.id,
@@ -104,7 +105,9 @@ module Rigor
104
105
  end
105
106
 
106
107
  type_aliases = services.fact_store.read(plugin_id: "dry-types", name: :dry_type_aliases) || {}
107
- params_table = ContractScanner.scan_schema_blocks(paths: scannable_paths(services), type_aliases: type_aliases)
108
+ params_table = ContractScanner.scan_schema_blocks(paths: scannable_paths(services),
109
+ io_boundary: io_boundary,
110
+ type_aliases: type_aliases)
108
111
  return if params_table.empty?
109
112
 
110
113
  services.fact_store.publish(
@@ -137,11 +140,13 @@ module Rigor
137
140
  shapes[name] = row.nil? ? nil : ParamsShape.build(row)
138
141
  end
139
142
 
143
+ # ADR-45 WD1b (#613 / #630) — the classification probes go through the boundary, so an entry that
144
+ # is not there yet (or stops being a directory) is a recorded dependency of the scan's input set.
140
145
  def scannable_paths(services)
141
146
  @scannable_paths ||= services.configuration.paths.flat_map do |entry|
142
- if File.directory?(entry)
147
+ if io_boundary.directory?(entry)
143
148
  Dir.glob(File.join(entry, "**", "*.rb"), sort: true)
144
- elsif File.file?(entry) && entry.end_with?(".rb")
149
+ elsif io_boundary.file?(entry) && entry.end_with?(".rb")
145
150
  [entry]
146
151
  else
147
152
  []
@@ -8,6 +8,7 @@ module Rigor
8
8
  class Ethon < Base
9
9
  manifest(
10
10
  id: "ethon",
11
+ target_gems: ["ethon"],
11
12
  version: "0.1.0",
12
13
  description: "Models Ethon / Libcurl options, dynamic return types, and multi-loop operations.",
13
14
  signature_paths: ["sig"]