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
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Hashing
5
+ # Pure-Ruby XXH3-64 (seed 0, default secret) — the content hash for the `rigor lens` anchors of
6
+ # ADR-113 WD6, landed ahead of `rigor lens` itself (#1083). A port of the upstream xxHash scalar
7
+ # path (`xxhash.h`'s `XXH3_64bits` with `XXH3_kSecret`), pinned by the upstream sanity-check
8
+ # vectors and cross-checked against lisplens's `xxhash-rust` output; see
9
+ # `spec/rigor/hashing/xxh3_spec.rb`.
10
+ #
11
+ # Pure Ruby per ADR-31: a native `xxhash` gem is a supply-chain addition to hash a few hundred
12
+ # bytes per call. Only the seedless/default-secret variant is implemented — the `rigor lens`
13
+ # anchors are the sole consumer, and they match lisplens ADR-0008's form exactly:
14
+ #
15
+ # - {.anchor_hash} is `xxh3_64 & 0xffff` printed as 4 lowercase hex digits (a per-row anchor).
16
+ # - {.file_hash} is the full 64-bit value printed as 16 lowercase hex digits (a file-level guard).
17
+ #
18
+ # Every arithmetic op below is modulo 2^64; `& MASK64` marks the spots where the C code wraps.
19
+ module XXH3
20
+ # Constant and helper names keep upstream's `xxhash.h` identifiers (PRIME64_1, accumulate_512,
21
+ # …) so the port stays diffable against the reference scalar path.
22
+ # rubocop:disable Naming/VariableNumber
23
+ MASK64 = (1 << 64) - 1
24
+ MASK32 = (1 << 32) - 1
25
+
26
+ PRIME32_1 = 0x9E3779B1
27
+ PRIME32_2 = 0x85EBCA77
28
+ PRIME32_3 = 0xC2B2AE3D
29
+ PRIME64_1 = 0x9E3779B185EBCA87
30
+ PRIME64_2 = 0xC2B2AE3D27D4EB4F
31
+ PRIME64_3 = 0x165667B19E3779F9
32
+ PRIME64_4 = 0x85EBCA77C2B2AE63
33
+ PRIME64_5 = 0x27D4EB2F165667C5
34
+ PRIME_MX1 = 0x165667919E3779F9
35
+ PRIME_MX2 = 0x9FB21C651E98DF25
36
+
37
+ STRIPE_LEN = 64
38
+ SECRET_CONSUME_RATE = 8
39
+ ACC_NB = STRIPE_LEN / 8
40
+ MIDSIZE_MAX = 240
41
+ SECRET_SIZE_MIN = 136
42
+ MIDSIZE_STARTOFFSET = 3
43
+ MIDSIZE_LASTOFFSET = 17
44
+ SECRET_LASTACC_START = 7
45
+ SECRET_MERGEACCS_START = 11
46
+
47
+ # `XXH3_kSecret` — the 192-byte default secret taken from FARSH, verbatim from `xxhash.h`
48
+ # (one hex pair per upstream byte literal).
49
+ SECRET = [
50
+ "b8fe6c3923a44bbe7c01812cf721ad1cded46de9839097db7240a4a4b7b3671f" \
51
+ "cb79e64eccc0e578825ad07dccff7221b8084674f743248ee03590e6813a264c" \
52
+ "3c2852bb91c300cb88d0658b1b532ea371644897a20df94e3819ef46a9deacd8" \
53
+ "a8fa763fe39c343ff9dcbbc7c70b4f1d8a51e04bcdb45931c89f7ec9d9787364" \
54
+ "eac5ac8334d3ebc3c581a0fffa1363eb170ddd51b7f0da49d316552629d4689e" \
55
+ "2b16be587d47a1fc8ff8b8d17ad031ce45cb3a8f95160428afd7fbcabb4b407e"
56
+ ].pack("H*").freeze
57
+
58
+ # XXH3_INIT_ACC
59
+ INIT_ACC = [PRIME32_3, PRIME64_1, PRIME64_2, PRIME64_3, PRIME64_4, PRIME32_2, PRIME64_5, PRIME32_1].freeze
60
+
61
+ private_constant :MASK64, :MASK32, :PRIME32_1, :PRIME32_2, :PRIME32_3, :PRIME64_1, :PRIME64_2, :PRIME64_3,
62
+ :PRIME64_4, :PRIME64_5, :PRIME_MX1, :PRIME_MX2, :STRIPE_LEN, :SECRET_CONSUME_RATE,
63
+ :ACC_NB, :MIDSIZE_MAX, :SECRET_SIZE_MIN, :MIDSIZE_STARTOFFSET, :MIDSIZE_LASTOFFSET,
64
+ :SECRET_LASTACC_START, :SECRET_MERGEACCS_START, :SECRET, :INIT_ACC
65
+
66
+ class << self
67
+ # XXH3_64bits of `bytes`, hashed as raw bytes — the string's encoding is ignored.
68
+ def digest64(bytes)
69
+ len = bytes.bytesize
70
+ if len <= 16 then len_0to16(bytes, len)
71
+ elsif len <= 128 then len_17to128(bytes, len)
72
+ elsif len <= MIDSIZE_MAX then len_129to240(bytes, len)
73
+ else hash_long(bytes, len)
74
+ end
75
+ end
76
+
77
+ # lisplens ADR-0008's `anchor_hash`: the low 16 bits of the digest as 4 lowercase hex digits.
78
+ def anchor_hash(bytes)
79
+ format("%04x", digest64(bytes) & 0xffff)
80
+ end
81
+
82
+ # lisplens ADR-0008's `file_hash`: the full digest as 16 lowercase hex digits.
83
+ def file_hash(bytes)
84
+ format("%016x", digest64(bytes))
85
+ end
86
+
87
+ private
88
+
89
+ # --- short keys: len <= 16 ---
90
+
91
+ def len_0to16(bytes, len)
92
+ if len > 8 then len_9to16(bytes, len)
93
+ elsif len >= 4 then len_4to8(bytes, len)
94
+ elsif len.positive? then len_1to3(bytes, len)
95
+ else
96
+ xxh64_avalanche(u64(SECRET, 56) ^ u64(SECRET, 64))
97
+ end
98
+ end
99
+
100
+ def len_1to3(bytes, len)
101
+ c1 = bytes.getbyte(0)
102
+ c2 = bytes.getbyte(len >> 1)
103
+ c3 = bytes.getbyte(len - 1)
104
+ combined = (c1 << 16) | (c2 << 24) | c3 | (len << 8)
105
+ bitflip = u32(SECRET, 0) ^ u32(SECRET, 4)
106
+ xxh64_avalanche(combined ^ bitflip)
107
+ end
108
+
109
+ def len_4to8(bytes, len)
110
+ input1 = u32(bytes, 0)
111
+ input2 = u32(bytes, len - 4)
112
+ bitflip = u64(SECRET, 8) ^ u64(SECRET, 16)
113
+ keyed = (input2 + (input1 << 32)) ^ bitflip
114
+ rrmxmx(keyed, len)
115
+ end
116
+
117
+ def len_9to16(bytes, len)
118
+ bitflip1 = u64(SECRET, 24) ^ u64(SECRET, 32)
119
+ bitflip2 = u64(SECRET, 40) ^ u64(SECRET, 48)
120
+ input_lo = u64(bytes, 0) ^ bitflip1
121
+ input_hi = u64(bytes, len - 8) ^ bitflip2
122
+ acc = (len + swap64(input_lo) + input_hi + mul128_fold64(input_lo, input_hi)) & MASK64
123
+ xxh3_avalanche(acc)
124
+ end
125
+
126
+ # --- mid-size keys: 17 <= len <= 240 (mum-hash on 16-byte chunks) ---
127
+
128
+ def mix16b(bytes, in_off, sec_off)
129
+ input_lo = u64(bytes, in_off)
130
+ input_hi = u64(bytes, in_off + 8)
131
+ mul128_fold64(input_lo ^ u64(SECRET, sec_off), input_hi ^ u64(SECRET, sec_off + 8))
132
+ end
133
+
134
+ def len_17to128(bytes, len)
135
+ acc = len * PRIME64_1
136
+ if len > 32
137
+ if len > 64
138
+ if len > 96
139
+ acc += mix16b(bytes, 48, 96)
140
+ acc += mix16b(bytes, len - 64, 112)
141
+ end
142
+ acc += mix16b(bytes, 32, 64)
143
+ acc += mix16b(bytes, len - 48, 80)
144
+ end
145
+ acc += mix16b(bytes, 16, 32)
146
+ acc += mix16b(bytes, len - 32, 48)
147
+ end
148
+ acc += mix16b(bytes, 0, 0)
149
+ acc += mix16b(bytes, len - 16, 16)
150
+ xxh3_avalanche(acc & MASK64)
151
+ end
152
+
153
+ def len_129to240(bytes, len)
154
+ acc = len * PRIME64_1
155
+ nb_rounds = len / 16
156
+ 8.times { |i| acc += mix16b(bytes, 16 * i, 16 * i) }
157
+ acc_end = mix16b(bytes, len - 16, SECRET_SIZE_MIN - MIDSIZE_LASTOFFSET)
158
+ acc = xxh3_avalanche(acc & MASK64)
159
+ (8...nb_rounds).each do |i|
160
+ acc_end += mix16b(bytes, 16 * i, (16 * (i - 8)) + MIDSIZE_STARTOFFSET)
161
+ end
162
+ xxh3_avalanche((acc + acc_end) & MASK64)
163
+ end
164
+
165
+ # --- long keys: len > 240 (striped accumulate + scramble, then merge) ---
166
+
167
+ def hash_long(bytes, len)
168
+ acc = INIT_ACC.dup
169
+ secret_size = SECRET.bytesize
170
+ nb_stripes_per_block = (secret_size - STRIPE_LEN) / SECRET_CONSUME_RATE
171
+ block_len = STRIPE_LEN * nb_stripes_per_block
172
+ nb_blocks = (len - 1) / block_len
173
+
174
+ nb_blocks.times do |n|
175
+ accumulate_stripes(acc, bytes, n * block_len, nb_stripes_per_block)
176
+ scramble(acc, secret_size - STRIPE_LEN)
177
+ end
178
+
179
+ last_off = nb_blocks * block_len
180
+ accumulate_stripes(acc, bytes, last_off, (len - 1 - last_off) / STRIPE_LEN)
181
+ accumulate_512(acc, bytes, len - STRIPE_LEN, secret_size - STRIPE_LEN - SECRET_LASTACC_START)
182
+
183
+ merge_accs(acc, len * PRIME64_1)
184
+ end
185
+
186
+ def accumulate_stripes(acc, bytes, base_off, nb_stripes)
187
+ nb_stripes.times do |s|
188
+ accumulate_512(acc, bytes, base_off + (s * STRIPE_LEN), s * SECRET_CONSUME_RATE)
189
+ end
190
+ end
191
+
192
+ # XXH3_accumulate_512_scalar: each 64-byte stripe folds into the 8-lane accumulator — the
193
+ # data value is added to the swapped adjacent lane, and the secret-keyed value contributes a
194
+ # 32x32->64 product to its own lane.
195
+ def accumulate_512(acc, bytes, in_off, sec_off)
196
+ ACC_NB.times do |i|
197
+ data_val = u64(bytes, in_off + (8 * i))
198
+ data_key = data_val ^ u64(SECRET, sec_off + (8 * i))
199
+ acc[i ^ 1] = (acc[i ^ 1] + data_val) & MASK64
200
+ acc[i] = (acc[i] + ((data_key & MASK32) * (data_key >> 32))) & MASK64
201
+ end
202
+ end
203
+
204
+ # XXH3_scrambleAcc_scalar
205
+ def scramble(acc, sec_off)
206
+ ACC_NB.times do |i|
207
+ acc[i] = ((acc[i] ^ (acc[i] >> 47) ^ u64(SECRET, sec_off + (8 * i))) * PRIME32_1) & MASK64
208
+ end
209
+ end
210
+
211
+ # XXH3_mergeAccs over the default secret, at SECRET_MERGEACCS_START.
212
+ def merge_accs(acc, start)
213
+ result = start
214
+ 4.times do |i|
215
+ result += mul128_fold64(acc[2 * i] ^ u64(SECRET, SECRET_MERGEACCS_START + (16 * i)),
216
+ acc[(2 * i) + 1] ^ u64(SECRET, SECRET_MERGEACCS_START + (16 * i) + 8))
217
+ end
218
+ xxh3_avalanche(result & MASK64)
219
+ end
220
+
221
+ # --- primitives ---
222
+
223
+ def u32(bytes, off) = bytes.unpack1("L<", offset: off)
224
+
225
+ def u64(bytes, off) = bytes.unpack1("Q<", offset: off)
226
+
227
+ def swap64(value) = [value].pack("Q>").unpack1("Q<")
228
+
229
+ def rotl64(value, shift) = ((value << shift) | (value >> (64 - shift))) & MASK64
230
+
231
+ def mul128_fold64(lhs, rhs)
232
+ product = lhs * rhs
233
+ (product & MASK64) ^ (product >> 64)
234
+ end
235
+
236
+ # XXH3_avalanche — the fast finalizer used when the input is already partially mixed.
237
+ def xxh3_avalanche(hash)
238
+ hash = (hash ^ (hash >> 37)) & MASK64
239
+ hash = (hash * PRIME_MX1) & MASK64
240
+ hash ^ (hash >> 32)
241
+ end
242
+
243
+ # XXH64_avalanche — the stronger finalizer for the shortest keys.
244
+ def xxh64_avalanche(hash)
245
+ hash ^= hash >> 33
246
+ hash = (hash * PRIME64_2) & MASK64
247
+ hash ^= hash >> 29
248
+ hash = (hash * PRIME64_3) & MASK64
249
+ hash ^ (hash >> 32)
250
+ end
251
+
252
+ # XXH3_rrmxmx — the len 4-8 finalizer (Pelle Evensen's mix).
253
+ def rrmxmx(hash, len)
254
+ hash = (hash ^ rotl64(hash, 49) ^ rotl64(hash, 24)) & MASK64
255
+ hash = (hash * PRIME_MX2) & MASK64
256
+ hash ^= (hash >> 35) + len
257
+ hash = (hash * PRIME_MX2) & MASK64
258
+ hash ^ (hash >> 28)
259
+ end
260
+ end
261
+ # rubocop:enable Naming/VariableNumber
262
+ end
263
+ end
264
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../type"
4
4
  require_relative "range_constant"
5
+ require_relative "content_join"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -74,10 +75,64 @@ module Rigor
74
75
  }.freeze
75
76
  private_constant :TYPE_HANDLERS
76
77
 
78
+ UNREAD_HASH_REFUSED_KEY = :__rigor_acceptance_unread_hash_refused__
79
+ private_constant :UNREAD_HASH_REFUSED_KEY
80
+
81
+ # Whether a `maybe` for `param` against `arg` rests on a record that could not read a hash's entries: a
82
+ # `Hash` with a gradual arm ({#gradual_hash?}) or an open `HashShape`. The record cannot prove the key set
83
+ # of either, and its `maybe` climbs through whatever holds the record. `OverloadSelector`'s strict pass
84
+ # reads that `maybe` as no evidence for the overload, so this re-asks with each such record answering
85
+ # `no` and reports whether the pair then fails. A `maybe` from anywhere else (a subclass only the Ruby
86
+ # source declares, a splat under a parameter that is no record) survives the re-ask and still counts.
87
+ def maybe_rests_on_unread_hash?(param, arg)
88
+ return false unless contains_type?(param) { |type| type.is_a?(Type::HashShape) }
89
+ return false unless contains_type?(arg) { |type| unread_hash?(type) }
90
+
91
+ refusing_unread_hashes { accepts(param, arg, mode: :gradual).no? }
92
+ end
93
+
77
94
  # rubocop:disable-next Metrics/ClassLength
78
95
  class << self
79
96
  private
80
97
 
98
+ # The flag lives on the thread, not on a parameter, because the record may sit any number of `accepts`
99
+ # calls below the one asked (a tuple element, a `Hash` value, a union member), and those calls go
100
+ # through each carrier's `Type#accepts`. It is set only for the duration of the block.
101
+ def refusing_unread_hashes
102
+ previous = Thread.current[UNREAD_HASH_REFUSED_KEY]
103
+ Thread.current[UNREAD_HASH_REFUSED_KEY] = true
104
+ yield
105
+ ensure
106
+ Thread.current[UNREAD_HASH_REFUSED_KEY] = previous
107
+ end
108
+
109
+ # The `maybe` a record answers for a hash whose entries it cannot read, or `no` inside
110
+ # {#refusing_unread_hashes}.
111
+ def unread_hash_maybe(mode, reason)
112
+ return hash_shape_no(mode, "#{reason} (refused as evidence)") if Thread.current[UNREAD_HASH_REFUSED_KEY]
113
+
114
+ Type::AcceptsResult.maybe(mode: mode, reasons: reason)
115
+ end
116
+
117
+ def unread_hash?(type)
118
+ gradual_hash?(type) || (type.is_a?(Type::HashShape) && type.open?)
119
+ end
120
+
121
+ def contains_type?(type, &)
122
+ return true if yield(type)
123
+
124
+ components =
125
+ case type
126
+ when Type::Union then type.members
127
+ when Type::Nominal then type.type_args
128
+ when Type::Tuple then type.elements
129
+ when Type::HashShape then type.pairs.values
130
+ when Type::Difference then [type.base]
131
+ else return false
132
+ end
133
+ components.any? { |component| contains_type?(component, &) }
134
+ end
135
+
81
136
  def accepts_one(self_type, other_type, mode)
82
137
  handler = TYPE_HANDLERS[self_type.class]
83
138
  return send(handler, self_type, other_type, mode) if handler
@@ -755,10 +810,15 @@ module Rigor
755
810
  # `Difference[base, removed]` accepts another type X when the base accepts X *and* X's value set is
756
811
  # provably disjoint from `removed`. The disjointness test is the subtle part — it is NOT the same as
757
812
  # `removed.accepts(X)`, because `Nominal[String]` includes `""` even though `Constant[""]` does not
758
- # "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is
759
- # itself a `Constant` carrier (compare values directly) or another `Difference` with the same
760
- # removed value (already exhibits the disjointness). Any other shape — Nominal, Union, IntegerRange
761
- # — could overlap the removed value, so the difference rejects it under gradual mode.
813
+ # "accept" `Nominal[String]`. The conservative rule here: we can prove disjointness only when X is a
814
+ # carrier whose value set is visibly apart from `removed` — a `Constant` (compare values directly),
815
+ # another `Difference` with the same removed value (already exhibits the disjointness), an
816
+ # `IntegerRange` that skips an Integer removed value, a `Tuple` whose fixed arity differs from a
817
+ # removed `Tuple`, or a `HashShape` with a required key a closed removed `HashShape` cannot hold. The
818
+ # last three cover the empty witnesses of `non-zero-int`, `non-empty-array[T]` and
819
+ # `non-empty-hash[K, V]`. Any other carrier — a Nominal above all — is no proof and the difference
820
+ # rejects it under gradual mode, even where its values happen to exclude `removed`. A Union or
821
+ # Intersection argument never reaches this rule whole: `accepts` splits it into members first.
762
822
  def accepts_difference(self_type, other_type, mode)
763
823
  base_result = accepts(self_type.base, other_type, mode: mode)
764
824
  return base_result if base_result.no?
@@ -787,9 +847,27 @@ module Rigor
787
847
  # Disjointness is monotonic over Intersection: if any member is provably disjoint from
788
848
  # `removed`, the meet is too.
789
849
  other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
850
+ when Type::IntegerRange
851
+ removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && !other_type.covers?(removed.value)
852
+ when Type::Tuple
853
+ # A Tuple's arity is fixed, so it shares no inhabitant with a Tuple of another arity — the
854
+ # zero-arity witness `non-empty-array[T]` removes included.
855
+ removed.is_a?(Type::Tuple) && removed.elements.size != other_type.elements.size
856
+ when Type::HashShape
857
+ hash_shape_excludes_removed?(other_type, removed)
790
858
  end
791
859
  end
792
860
 
861
+ # Every inhabitant of `shape` holds each of its required keys, and a closed `removed` shape holds no
862
+ # key outside its pairs, so one required key `removed` does not list makes the two disjoint. Against
863
+ # the closed `{}` witness `non-empty-hash[K, V]` removes, that is any required key; an optional key
864
+ # proves nothing, since the shape is inhabited without it.
865
+ def hash_shape_excludes_removed?(shape, removed)
866
+ return false unless removed.is_a?(Type::HashShape) && removed.closed?
867
+
868
+ shape.required_keys.any? { |key| !removed.pairs.key?(key) }
869
+ end
870
+
793
871
  # `Refined[base, predicate]` accepts another type X when the base accepts the *base* of X *and* X is
794
872
  # provably contained in the predicate's value set. The base check is delegated to
795
873
  # `accepts(self.base, X.base)` so handlers like `accepts_nominal` see Nominal-vs-Nominal and return
@@ -924,11 +1002,15 @@ module Rigor
924
1002
 
925
1003
  # HashShape{k1: T1, ...} accepts another HashShape when every required key of self is required on
926
1004
  # the other side and Ti accepts Ui (depth covariant). Optional keys may be absent on the other side;
927
- # when present, their values are checked. A closed self rejects known or possible extra keys. Other
928
- # types are rejected; the converse direction (a Nominal accepting a HashShape) is handled by
1005
+ # when present, their values are checked. A closed self rejects known extra keys, and answers at most
1006
+ # `maybe` for an open source: `h = { a: 1 }; h.default = 0` reopens a shape whose key set is still
1007
+ # exactly the literal's. Other types are rejected, except a `Hash` with a gradual arm (see
1008
+ # {#gradual_hash?}); the converse direction (a Nominal accepting a HashShape) is handled by
929
1009
  # `accepts_nominal` via projection.
930
1010
  def accepts_hash_shape(self_type, other_type, mode)
931
1011
  unless other_type.is_a?(Type::HashShape)
1012
+ return gradual_hash_verdict(self_type, other_type, mode) if gradual_hash?(other_type)
1013
+
932
1014
  return Type::AcceptsResult.no(
933
1015
  mode: mode,
934
1016
  reasons: "HashShape does not accept #{other_type.class}"
@@ -939,8 +1021,6 @@ module Rigor
939
1021
  return hash_shape_no(mode, "HashShape missing required keys: #{missing.inspect}") unless missing.empty?
940
1022
 
941
1023
  if self_type.closed?
942
- return hash_shape_no(mode, "HashShape closed target rejects open source") if other_type.open?
943
-
944
1024
  extra = other_type.pairs.keys - self_type.pairs.keys
945
1025
  unless extra.empty?
946
1026
  return hash_shape_no(mode, "HashShape closed target rejects extra keys: #{extra.inspect}")
@@ -948,9 +1028,59 @@ module Rigor
948
1028
  end
949
1029
 
950
1030
  per_entry = hash_shape_entry_results(self_type, other_type, mode)
951
- combine_arg_results(per_entry, mode)
1031
+ result = combine_arg_results(per_entry, mode)
1032
+ return result unless result.yes? && self_type.closed? && other_type.open?
1033
+
1034
+ unread_hash_maybe(mode, "HashShape closed target cannot check open source")
1035
+ end
1036
+
1037
+ # A `Hash` nominal — directly or as the base of a `Difference` (`non-empty-hash[K, V]`) — that is raw or
1038
+ # carries a `Dynamic` arm in a type argument. A hash literal with a `**splat` entry carries the arm by
1039
+ # design, `Hash.new` filled key by key reads as the raw `Hash`, and a declared `Hash[Symbol, untyped]`
1040
+ # is the author saying the hash may hold anything (the reading `MutationRejoin#regrowable_carrier?`
1041
+ # gives a declared gradual arm). The runtime value may then hold exactly the record's keys, so a shape
1042
+ # cannot reject it on the arm's account, the way `Hash[K, V]` accepts a raw `Hash` as maybe. A `Hash`
1043
+ # subclass is not read: its entries need not be the record's (`HashWithIndifferentAccess` stores String
1044
+ # keys).
1045
+ def gradual_hash?(type)
1046
+ nominal = type.is_a?(Type::Difference) ? type.base : type
1047
+ nominal.is_a?(Type::Nominal) && nominal.class_name == "Hash" &&
1048
+ (nominal.type_args.empty? || nominal.type_args.any? { |arg| gradual_arm?(arg) })
1049
+ end
1050
+
1051
+ # `maybe`, unless an argument without a gradual arm already rules the record out: a precise `K` that
1052
+ # rejects a required key (`Hash[String, untyped]` never holds `:a`), or a precise `V` none of whose
1053
+ # members a required key's type accepts.
1054
+ def gradual_hash_verdict(self_type, other_type, mode)
1055
+ nominal = other_type.is_a?(Type::Difference) ? other_type.base : other_type
1056
+ key_type, value_type = nominal.type_args
1057
+ missing = unholdable_keys(self_type, key_type, mode)
1058
+ return hash_shape_no(mode, "Hash key type cannot hold required keys: #{missing.inspect}") if missing.any?
1059
+
1060
+ refused = refused_value_keys(self_type, value_type, mode)
1061
+ return hash_shape_no(mode, "Hash value type refused for required keys: #{refused.inspect}") if refused.any?
1062
+
1063
+ unread_hash_maybe(mode, "HashShape cannot check #{other_type.describe(:short)}")
952
1064
  end
953
1065
 
1066
+ def unholdable_keys(self_type, key_type, mode)
1067
+ return [] if key_type.nil? || gradual_arm?(key_type)
1068
+
1069
+ self_type.required_keys.select { |key| key_type.accepts(Type::Combinator.constant_of(key), mode: mode).no? }
1070
+ end
1071
+
1072
+ def refused_value_keys(self_type, value_type, mode)
1073
+ return [] if value_type.nil? || gradual_arm?(value_type)
1074
+
1075
+ members = ContentJoin.union_members(value_type)
1076
+ self_type.required_keys.select do |key|
1077
+ formal = self_type.pairs.fetch(key)
1078
+ members.all? { |member| accepts(formal, member, mode: mode).no? }
1079
+ end
1080
+ end
1081
+
1082
+ def gradual_arm?(type) = ContentJoin.union_members(type).any?(Type::Dynamic)
1083
+
954
1084
  def hash_shape_entry_results(self_type, other_type, mode)
955
1085
  self_type.pairs.filter_map do |key, formal|
956
1086
  next unless other_type.pairs.key?(key)