rigortype 0.1.18 → 0.2.0

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +159 -224
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
  4. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
  5. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +32 -23
  6. data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
  7. data/lib/rigor/analysis/check_rules/rule_walk.rb +151 -23
  8. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
  9. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
  10. data/lib/rigor/analysis/check_rules.rb +756 -132
  11. data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
  12. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
  13. data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
  14. data/lib/rigor/analysis/diagnostic.rb +8 -0
  15. data/lib/rigor/analysis/fact_store.rb +5 -4
  16. data/lib/rigor/analysis/rule_catalog.rb +153 -6
  17. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +19 -18
  18. data/lib/rigor/analysis/runner/project_pre_passes.rb +13 -9
  19. data/lib/rigor/analysis/runner.rb +75 -27
  20. data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
  21. data/lib/rigor/analysis/worker_session.rb +31 -25
  22. data/lib/rigor/bleeding_edge.rb +123 -0
  23. data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
  24. data/lib/rigor/cache/descriptor.rb +86 -8
  25. data/lib/rigor/cache/rbs_descriptor.rb +2 -1
  26. data/lib/rigor/cache/store.rb +5 -3
  27. data/lib/rigor/cli/annotate_command.rb +122 -16
  28. data/lib/rigor/cli/baseline_command.rb +4 -3
  29. data/lib/rigor/cli/check_command.rb +118 -16
  30. data/lib/rigor/cli/coverage_command.rb +148 -16
  31. data/lib/rigor/cli/coverage_scan.rb +57 -0
  32. data/lib/rigor/cli/explain_command.rb +2 -0
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
  35. data/lib/rigor/cli/mutation_protection_report.rb +73 -0
  36. data/lib/rigor/cli/options.rb +9 -0
  37. data/lib/rigor/cli/plugins_command.rb +4 -5
  38. data/lib/rigor/cli/plugins_renderer.rb +0 -2
  39. data/lib/rigor/cli/protection_renderer.rb +63 -0
  40. data/lib/rigor/cli/protection_report.rb +68 -0
  41. data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
  42. data/lib/rigor/cli/sig_gen_command.rb +2 -1
  43. data/lib/rigor/cli/trace_command.rb +2 -1
  44. data/lib/rigor/cli/triage_command.rb +8 -4
  45. data/lib/rigor/cli/triage_renderer.rb +15 -1
  46. data/lib/rigor/cli/type_of_command.rb +1 -1
  47. data/lib/rigor/cli/type_scan_command.rb +2 -1
  48. data/lib/rigor/cli.rb +12 -3
  49. data/lib/rigor/configuration/dependencies.rb +2 -4
  50. data/lib/rigor/configuration/severity_profile.rb +13 -1
  51. data/lib/rigor/configuration.rb +100 -6
  52. data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
  53. data/lib/rigor/environment/class_registry.rb +4 -3
  54. data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
  55. data/lib/rigor/environment/lockfile_resolver.rb +1 -1
  56. data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
  57. data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
  58. data/lib/rigor/environment/rbs_loader.rb +74 -5
  59. data/lib/rigor/environment.rb +17 -7
  60. data/lib/rigor/flow_contribution/fact.rb +1 -1
  61. data/lib/rigor/flow_contribution.rb +3 -5
  62. data/lib/rigor/inference/acceptance.rb +17 -9
  63. data/lib/rigor/inference/block_parameter_binder.rb +2 -3
  64. data/lib/rigor/inference/body_fixpoint.rb +89 -0
  65. data/lib/rigor/inference/budget_trace.rb +29 -2
  66. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
  67. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
  68. data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
  69. data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
  70. data/lib/rigor/inference/expression_typer.rb +1072 -71
  71. data/lib/rigor/inference/hkt_body.rb +8 -11
  72. data/lib/rigor/inference/hkt_body_parser.rb +10 -12
  73. data/lib/rigor/inference/hkt_registry.rb +10 -11
  74. data/lib/rigor/inference/macro_block_self_type.rb +2 -2
  75. data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
  76. data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
  77. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +210 -35
  78. data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
  79. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
  80. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
  81. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
  82. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
  83. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
  84. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
  85. data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
  86. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
  87. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +237 -24
  88. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
  89. data/lib/rigor/inference/method_dispatcher.rb +112 -49
  90. data/lib/rigor/inference/method_parameter_binder.rb +56 -2
  91. data/lib/rigor/inference/multi_target_binder.rb +46 -3
  92. data/lib/rigor/inference/mutation_widening.rb +147 -11
  93. data/lib/rigor/inference/narrowing.rb +284 -53
  94. data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
  95. data/lib/rigor/inference/project_patched_methods.rb +4 -7
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -13
  97. data/lib/rigor/inference/protection_scanner.rb +86 -0
  98. data/lib/rigor/inference/scope_indexer.rb +821 -76
  99. data/lib/rigor/inference/statement_evaluator.rb +1179 -102
  100. data/lib/rigor/inference/struct_fold_safety.rb +181 -0
  101. data/lib/rigor/inference/synthetic_method.rb +7 -7
  102. data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
  103. data/lib/rigor/language_server/completion_provider.rb +6 -12
  104. data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
  105. data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
  106. data/lib/rigor/language_server/hover_provider.rb +2 -3
  107. data/lib/rigor/language_server/hover_renderer.rb +2 -11
  108. data/lib/rigor/language_server/server.rb +9 -17
  109. data/lib/rigor/language_server.rb +4 -5
  110. data/lib/rigor/plugin/base.rb +245 -87
  111. data/lib/rigor/plugin/macro/block_as_method.rb +25 -25
  112. data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
  113. data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
  114. data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
  115. data/lib/rigor/plugin/macro.rb +6 -8
  116. data/lib/rigor/plugin/manifest.rb +49 -90
  117. data/lib/rigor/plugin/node_rule_walk.rb +59 -14
  118. data/lib/rigor/plugin/registry.rb +18 -18
  119. data/lib/rigor/plugin/type_node_resolver.rb +6 -8
  120. data/lib/rigor/protection/mutation_scanner.rb +120 -0
  121. data/lib/rigor/protection/mutator.rb +246 -0
  122. data/lib/rigor/rbs_extended.rb +24 -36
  123. data/lib/rigor/reflection.rb +4 -7
  124. data/lib/rigor/scope/discovery_index.rb +16 -2
  125. data/lib/rigor/scope.rb +185 -16
  126. data/lib/rigor/sig_gen/generator.rb +8 -0
  127. data/lib/rigor/sig_gen/observed_call.rb +3 -3
  128. data/lib/rigor/sig_gen/writer.rb +40 -2
  129. data/lib/rigor/source/constant_path.rb +62 -0
  130. data/lib/rigor/source.rb +1 -0
  131. data/lib/rigor/triage/catalogue.rb +4 -19
  132. data/lib/rigor/triage.rb +69 -1
  133. data/lib/rigor/type/bound_method.rb +2 -11
  134. data/lib/rigor/type/combinator.rb +45 -3
  135. data/lib/rigor/type/constant.rb +2 -11
  136. data/lib/rigor/type/data_class.rb +2 -11
  137. data/lib/rigor/type/data_instance.rb +2 -11
  138. data/lib/rigor/type/hash_shape.rb +2 -11
  139. data/lib/rigor/type/integer_range.rb +2 -11
  140. data/lib/rigor/type/intersection.rb +2 -11
  141. data/lib/rigor/type/nominal.rb +2 -11
  142. data/lib/rigor/type/plain_lattice.rb +37 -0
  143. data/lib/rigor/type/refined.rb +72 -13
  144. data/lib/rigor/type/singleton.rb +2 -11
  145. data/lib/rigor/type/struct_class.rb +75 -0
  146. data/lib/rigor/type/struct_instance.rb +93 -0
  147. data/lib/rigor/type/tuple.rb +5 -15
  148. data/lib/rigor/type.rb +2 -0
  149. data/lib/rigor/version.rb +1 -1
  150. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
  151. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
  152. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +16 -32
  153. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
  154. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
  155. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
  156. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +34 -100
  157. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
  158. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
  159. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  160. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +26 -27
  161. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
  162. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +9 -8
  163. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
  164. data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
  165. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
  166. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
  167. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
  168. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
  169. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
  170. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +18 -49
  171. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
  172. data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
  173. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +4 -4
  174. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
  175. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
  176. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
  177. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
  178. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +22 -35
  179. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
  180. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
  181. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +16 -23
  182. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  183. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
  184. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
  185. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  186. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +21 -27
  187. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
  188. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
  189. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
  190. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
  191. data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
  192. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
  193. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
  194. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
  195. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
  196. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
  197. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +52 -40
  198. data/sig/rigor/analysis/fact_store.rbs +3 -0
  199. data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
  200. data/sig/rigor/plugin/base.rbs +5 -2
  201. data/sig/rigor/plugin/manifest.rbs +1 -2
  202. data/sig/rigor/scope.rbs +18 -1
  203. data/sig/rigor/type.rbs +37 -1
  204. data/sig/rigor.rbs +1 -1
  205. data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
  206. data/skills/rigor-plugin-author/SKILL.md +6 -4
  207. data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
  208. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
  209. metadata +25 -2
  210. data/lib/rigor/plugin/macro/external_file.rb +0 -143
@@ -27,10 +27,12 @@ module Rigor
27
27
  # whose direct superclass is in `model_base_classes`, and
28
28
  # composes them with the schema table into a {ModelIndex}.
29
29
  #
30
- # Both producers ride `Plugin::Base#cache_for`. The descriptor
31
- # auto-includes the digests of every file the boundary read,
32
- # so editing `db/schema.rb` or any model file invalidates
33
- # exactly the right cache entry.
30
+ # Both producers ride `Plugin::Base#cache_for` (ADR-60 WD3
31
+ # record-and-validate): each producer's in-block boundary reads
32
+ # are captured into its dependency descriptor after the block
33
+ # runs, and `model_index`'s `watch:` covers model-file additions,
34
+ # so editing `db/schema.rb`, editing any model, or adding a new
35
+ # model file invalidates exactly the right cache entry.
34
36
  #
35
37
  # The per-file `#diagnostics_for_file` hook delegates to
36
38
  # {Analyzer}, which walks Prism and emits diagnostics for
@@ -73,10 +75,9 @@ module Rigor
73
75
  "model_base_classes" => { kind: :array, default: %w[ApplicationRecord ActiveRecord::Base] }
74
76
  },
75
77
  produces: [:model_index],
76
- # ADR-25 — the bundled `ActiveRecord::Relation` RBS, the
77
- # type `flow_contribution_for`'s relation-typed call sites
78
- # (`has_many` accessors, `Model.where`, scopes) dispatch
79
- # against.
78
+ # ADR-25 — the bundled `ActiveRecord::Relation` RBS that
79
+ # relation-typed call sites (`has_many` accessors,
80
+ # `Model.where`, scopes) dispatch against.
80
81
  signature_paths: ["sig"],
81
82
  # ADR-26 — `ActiveRecord::Relation` is an "open" receiver:
82
83
  # it delegates an unbounded set of user-defined scopes /
@@ -87,7 +88,7 @@ module Rigor
87
88
  )
88
89
 
89
90
  # The class the bundled `sig/active_record/relation.rbs`
90
- # describes; `flow_contribution_for` contributes
91
+ # describes; `dynamic_return` contributes
91
92
  # `ActiveRecord::Relation[Model]` for relation-returning
92
93
  # call sites (`has_many` accessors, `Model.where`, scopes).
93
94
  RELATION_CLASS_NAME = "ActiveRecord::Relation"
@@ -101,8 +102,11 @@ module Rigor
101
102
  end
102
103
 
103
104
  # Cached: model index. Walks every model file, then composes
104
- # the rows with the cached schema table.
105
- producer :model_index do |_params|
105
+ # the rows with the cached schema table. `watch:` (ADR-60 WD3)
106
+ # covers model-file additions; the discoverer's in-block reads
107
+ # are captured into the record-and-validate dependency
108
+ # descriptor after the block runs.
109
+ producer :model_index, watch: -> { [[@model_search_paths, "**/*.rb"]] } do |_params|
106
110
  rows = ModelDiscoverer.new(
107
111
  io_boundary: io_boundary,
108
112
  search_paths: @model_search_paths,
@@ -256,9 +260,8 @@ module Rigor
256
260
  names
257
261
  end
258
262
 
259
- # The migrated body of the legacy `flow_contribution_for` —
260
- # same resolution order, returning the bare type the
261
- # `dynamic_return` contract expects.
263
+ # Resolution body for `dynamic_return` — same four-path
264
+ # order, returning the bare type the contract expects.
262
265
  def contribution_return_type(call_node, scope)
263
266
  return nil unless call_node.is_a?(Prism::CallNode)
264
267
 
@@ -571,16 +574,11 @@ module Rigor
571
574
  table = schema_table_or_nil
572
575
  return nil if table.nil?
573
576
 
574
- # Walk model files first so the IoBoundary's digest list
575
- # captures them BEFORE `cache_for` snapshots the
576
- # descriptor (the same "read first, cache_for second"
577
- # pattern documented at the top of rigor-routes).
578
- ModelDiscoverer.new(
579
- io_boundary: io_boundary,
580
- search_paths: @model_search_paths,
581
- base_classes: @model_base_classes
582
- ).discover
583
-
577
+ # ADR-60 WD3 record-and-validate: the producer's own in-block
578
+ # `ModelDiscoverer` reads are captured into the dependency
579
+ # descriptor after the block runs, and the producer's `watch:`
580
+ # covers model-file additions so no priming walk is needed
581
+ # (it used to run the discover twice).
584
582
  @model_index = cache_for(:model_index, params: {}).call
585
583
  rescue StandardError => e
586
584
  @load_errors << "model index build failed: #{e.class}: #{e.message}"
@@ -599,9 +597,10 @@ module Rigor
599
597
  return nil if @schema_load_attempted
600
598
 
601
599
  @schema_load_attempted = true
602
- # Same pattern: read schema file via boundary, then call
603
- # cache_for so the descriptor includes the file digest.
604
- io_boundary.read_file(@schema_file)
600
+ # ADR-60 WD3 record-and-validate: the producer reads
601
+ # `@schema_file` in-block, and that read is captured into the
602
+ # dependency descriptor after the block runs — so no priming
603
+ # read is needed here.
605
604
  @schema_table = cache_for(:schema_table, params: {}).call
606
605
  rescue Plugin::AccessDeniedError => e
607
606
  @load_errors << "rigor-activerecord: #{e.message}"
@@ -11,13 +11,11 @@ module Rigor
11
11
  # plugin recognised so users can verify the model →
12
12
  # attachment mapping the plugin sees.
13
13
  #
14
- # No `:error` diagnostics in this slice — the
15
- # `dynamic_return` return-type narrowing carries
16
- # the type-checking value; surfacing unknown attachment
17
- # names as errors requires a coupled receiver-class
18
- # narrowing pass that the integration spec doesn't yet
19
- # rely on. A future slice can add `unknown-attachment`
20
- # similar to `rigor-activerecord`'s `unknown-column`.
14
+ # No `:error` diagnostics here — the `dynamic_return`
15
+ # return-type narrowing carries the type-checking value.
16
+ # An `unknown-attachment` rule (similar to
17
+ # `rigor-activerecord`'s `unknown-column`) is deferred:
18
+ # it requires a coupled receiver-class narrowing pass.
21
19
  class Analyzer
22
20
  attr_reader :diagnostics
23
21
 
@@ -54,8 +54,11 @@ module Rigor
54
54
  )
55
55
 
56
56
  # Cached: attachment index. Walks every `.rb` file under
57
- # `model_search_paths` for `has_*_attached` macros.
58
- producer :attachment_index do |_params|
57
+ # `model_search_paths` for `has_*_attached` macros. `watch:`
58
+ # (ADR-60 WD3) covers model-file additions; the discoverer's
59
+ # in-block reads are captured into the record-and-validate
60
+ # dependency descriptor after the block runs.
61
+ producer :attachment_index, watch: -> { [[@model_search_paths, "**/*.rb"]] } do |_params|
59
62
  rows = AttachmentDiscoverer.new(
60
63
  io_boundary: io_boundary,
61
64
  search_paths: @model_search_paths
@@ -126,12 +129,10 @@ module Rigor
126
129
  def attachment_index
127
130
  return @attachment_index if @attachment_index
128
131
 
129
- # Walk first so the IoBoundary's digest list captures
130
- # the model file digests before cache_for snapshots.
131
- AttachmentDiscoverer.new(
132
- io_boundary: io_boundary,
133
- search_paths: @model_search_paths
134
- ).discover
132
+ # ADR-60 WD3 record-and-validate: the producer's in-block
133
+ # `AttachmentDiscoverer` reads are captured into the
134
+ # dependency descriptor after the block runs, and the
135
+ # producer's `watch:` covers model-file additions.
135
136
  @attachment_index = cache_for(:attachment_index, params: {}).call
136
137
  rescue Plugin::AccessDeniedError => e
137
138
  @load_errors << "rigor-activestorage: #{e.message}"
@@ -44,19 +44,17 @@ module Rigor
44
44
  # `send_reset_password_instructions`, etc. resolve through the
45
45
  # synthetic-method tier without `call.undefined-method`.
46
46
  #
47
- # ## Floor / ceiling per ADR-16 WD13
47
+ # ## Precision tier
48
48
  #
49
- # Slice 3 ships at the **floor**: synthesised method names
50
- # emit and the dispatcher's `try_synthetic_method` tier
51
- # returns `Type::Combinator.untyped` (Dynamic[T]) for every
52
- # match. Per the slice-3 design judgment (1) the precision
53
- # promotion looking up the module's authored RBS return
54
- # type at dispatch time is **slice-6 ceiling work** and is
55
- # NOT a delivery commitment of slice 3c. The `origin_module:`
56
- # provenance field is recorded so the ceiling slice can
57
- # promote without rescanning.
49
+ # The scanner records `origin_module:` in each synthetic
50
+ # method's provenance. The dispatcher's slice-6a TierB path
51
+ # (`promote_via_origin_module`) redispatches on
52
+ # `Nominal[origin_module]` via `RbsDispatch`, so Devise's
53
+ # authored RBS return types win: `valid_password?` returns
54
+ # `bool`, not `Dynamic[T]`. Unknown return types degrade
55
+ # gracefully to `Dynamic[T]`.
58
56
  #
59
- # ## Scope (slice 3c minimum)
57
+ # ## Scope
60
58
  #
61
59
  # - Recognises model-side `devise :a, :b` on any AR::Base
62
60
  # subclass; trait symbol set mirrors `lib/devise/modules.rb`.
@@ -35,16 +35,15 @@ module Rigor
35
35
  # other files then dispatch through the synthetic record rather
36
36
  # than falling through to `call.undefined-method`.
37
37
  #
38
- # ## Floor / ceiling per ADR-16 WD13
38
+ # ## Precision model (ADR-16 WD13 + ADR-18)
39
39
  #
40
- # Slice 2 ships at the **floor**: the synthetic reader's return
41
- # type degrades to `Dynamic[T]`. The manifest's `returns: "Object"`
42
- # is recorded but not resolved precise return-type promotion
43
- # (so `attribute :city, Types::String` makes `address.city`
44
- # return `String`) is the **ceiling**, deferred to slice 6
45
- # (ADR-13 `Plugin::TypeNodeResolver` chain). The plugin's manifest
46
- # value of `returns:` would today be the upstream gem's reader
47
- # return shape; slice 6 unlocks precision without re-authoring.
40
+ # The synthetic reader's return type is resolved via ADR-18's
41
+ # `returns_from_arg:` fact lookup: the call-site's second argument
42
+ # (`Types::String` etc.) is looked up through the `:dry_type_aliases`
43
+ # fact published by `rigor-dry-types`, yielding `Nominal[String]`
44
+ # for common cases. When the lookup misses (e.g. inline method-chain
45
+ # argument whose chain-head isn't currently extracted), the row
46
+ # falls back to `Dynamic[Top]` silently.
48
47
  #
49
48
  # ## Scope (slice 2c minimum)
50
49
  #
@@ -22,9 +22,8 @@ module Rigor
22
22
  # Other dry-rb adapter plugins consume this fact:
23
23
  #
24
24
  # - `rigor-dry-struct` reads it so `attribute :city, Types::String`
25
- # can promote `address.city` from `Dynamic[T]` to `Nominal[String]`
26
- # (gated on the slice-6 precision-promotion work + ADR-13
27
- # resolver chain).
25
+ # promotes `address.city` from `Dynamic[Top]` to `Nominal[String]`
26
+ # via ADR-18's `returns_from_arg:` fact lookup.
28
27
  # - `rigor-dry-validation` / `rigor-dry-schema` read it for
29
28
  # per-key type recognition in `schema { … }` / `params { … }`
30
29
  # blocks (separate plugin slice).
@@ -43,17 +42,19 @@ module Rigor
43
42
  # "<UnderlyingClass>" }` so consumers can match on the
44
43
  # qualified constant name they see in source.
45
44
  #
46
- # The **ceiling** (slice 2+):
45
+ # Implemented beyond the floor:
47
46
  #
48
- # - Recognises nested namespaces (`Types::Coercible::Integer`,
47
+ # - Nested-namespace aliases (`Types::Coercible::Integer`,
49
48
  # `Types::Strict::Symbol`, `Types::Params::Bool`,
50
- # `Types::JSON::Date`) — each is a separate dry-types
51
- # "category" with its own coercion semantics.
52
- # - Recognises user-authored compositions
53
- # (`Types::String.constrained(min_size: 1)`,
54
- # `Email = Types::String.constrained(format: …)`) so the
55
- # alias surface extends beyond the canonical names.
56
- # - Emits `dry-types.unknown-alias` / `dry-types.alias-shadow`
49
+ # `Types::JSON::Date`) — the four coercion categories each
50
+ # map to the same underlying class as their canonical shortcut.
51
+ # - User-authored compositions (`Email = Types::String.constrained(...)`,
52
+ # transitive resolution through composition chains) — the alias
53
+ # surface extends beyond the 15 canonical names.
54
+ #
55
+ # Deferred:
56
+ #
57
+ # - `dry-types.unknown-alias` / `dry-types.alias-shadow`
57
58
  # diagnostics when downstream code references a name that
58
59
  # wasn't published.
59
60
  #
@@ -21,10 +21,9 @@ module Rigor
21
21
  # cross-plugin fact.
22
22
  # - Ships an RBS overlay (`sig/dry_validation.rbs`) typing
23
23
  # `Dry::Validation::Contract#call` (returns Result) and
24
- # `Dry::Validation::Result#{success?, failure?, to_h}`. Users
25
- # add the path to their `.rigor.yml`'s `signature_paths:` so
26
- # `contract.call(input).to_h` infers cleanly. See the README
27
- # for the wiring step.
24
+ # `Dry::Validation::Result#{success?, failure?, to_h}`.
25
+ # The manifest's `signature_paths: ["sig"]` auto-contributes
26
+ # the overlay (ADR-25) no project-side wiring needed.
28
27
  #
29
28
  # Slice 2 (deferred, per design note):
30
29
  #
@@ -86,14 +86,14 @@ module Rigor
86
86
  # a `Prism::SymbolNode` is treated as a literal
87
87
  # attribute reference.
88
88
  #
89
- # Phase 1 (c) — when `model_index` (the cross-plugin
90
- # `:model_index` fact published by rigor-activerecord)
91
- # is present, the effective accepted key set is the
92
- # UNION of the factory's declared attributes plus the
93
- # corresponding model's columns. FactoryBot's runtime
94
- # accepts any AR attribute regardless of whether the
95
- # factory declared it, so the cross-check broadens the
96
- # acceptance accordingly.
89
+ # When `model_index` (the cross-plugin `:model_index`
90
+ # fact published by rigor-activerecord) is present,
91
+ # the effective accepted key set is the UNION of the
92
+ # factory's declared attributes plus the corresponding
93
+ # model's columns. FactoryBot's runtime accepts any AR
94
+ # attribute regardless of whether the factory declared
95
+ # it, so the cross-check broadens the acceptance
96
+ # accordingly.
97
97
  def unknown_attribute_violations(call_node, entry, model_index)
98
98
  accepted_keys, suggestion_dictionary = effective_keys(entry, model_index)
99
99
  attr_spell_checker = DidYouMean::SpellChecker.new(dictionary: suggestion_dictionary)
@@ -23,8 +23,8 @@ module Rigor
23
23
  # - `factory :users, aliases: [:author] do ... end` — alias form
24
24
  #
25
25
  # Inside a factory block, attribute declarations come in
26
- # several shapes. Phase 1 (a) recognises the literal-name
27
- # forms only (Symbol arg / String arg):
26
+ # several shapes. Only literal-name forms are recognised
27
+ # (Symbol arg / String arg):
28
28
  #
29
29
  # - `name { "Alice" }` — implicit attribute via
30
30
  # `method_missing` with a block (FactoryBot's modern
@@ -116,8 +116,8 @@ module Rigor
116
116
  Rigor::Source::Literals.symbol_or_string_name(call_node.arguments&.arguments&.first)
117
117
  end
118
118
 
119
- # Pillar 2 Slice 3 — resolve the model class name for
120
- # the factory. Three sources, in priority order:
119
+ # Resolves the model class name for the factory.
120
+ # Three sources, in priority order:
121
121
  #
122
122
  # 1. Explicit `class: <Const>` keyword arg —
123
123
  # ConstantReadNode / ConstantPathNode value.
@@ -188,11 +188,10 @@ module Rigor
188
188
  attributes
189
189
  end
190
190
 
191
- # Walks the block body collecting attribute names. The
192
- # recogniser looks at top-level statements only
193
- # attributes inside `trait :admin do ... end` or other
194
- # nested blocks are NOT collected in Phase 1 (a)
195
- # (traits ship in a follow-up).
191
+ # Walks the block body collecting attribute names. Only
192
+ # top-level statements are examined attributes inside
193
+ # `trait :admin do ... end` or other nested blocks are
194
+ # not collected (traits deferred to a follow-up).
196
195
  def collect_attributes_from(node, accumulator)
197
196
  return unless node.is_a?(Prism::Node)
198
197
 
@@ -206,8 +205,7 @@ module Rigor
206
205
  def record_attribute(node, accumulator)
207
206
  return unless node.is_a?(Prism::CallNode) && node.receiver.nil?
208
207
  # Skip association / sequence / trait / framework
209
- # methods — Phase 1 (a) only records plain attribute
210
- # declarations.
208
+ # methods — only plain attribute declarations are recorded.
211
209
  return if SKIPPED_METHODS.include?(node.name)
212
210
 
213
211
  name = if node.name == :add_attribute
@@ -4,15 +4,14 @@ module Rigor
4
4
  module Plugin
5
5
  class Factorybot < Rigor::Plugin::Base
6
6
  # Per-run frozen index of discovered FactoryBot factories
7
- # and the attribute keys each declares. Phase 1 (a) keys
8
- # only the **literal symbol/string** factory name + the
9
- # **literal symbol** attribute names; sequences,
10
- # parent/child relationships, traits, and dynamically-
11
- # named factories ship behind later slices.
7
+ # and the attribute keys each declares. Indexes only
8
+ # **literal symbol/string** factory names + **literal
9
+ # symbol** attribute names; sequences, parent/child
10
+ # relationships, traits, and dynamically-named factories
11
+ # are deferred to follow-up slices.
12
12
  #
13
- # v0.2.0 (Pillar 2 Slice 3) adds `model_class` to each
14
- # entry the inferred or explicit class the factory
15
- # builds. Resolved from:
13
+ # Each entry carries a `model_class` the inferred or
14
+ # explicit class the factory builds. Resolved from:
16
15
  #
17
16
  # 1. An explicit `factory :user, class: User do`
18
17
  # keyword option (ConstantReadNode / ConstantPathNode
@@ -13,12 +13,10 @@ module Rigor
13
13
  # attributes_for / *_list family against a per-run index
14
14
  # built from `factory_search_paths`.
15
15
  #
16
- # **Phase 1 (a)** of the FactoryBot plugin family — the
17
- # self-contained slice. Recognises factory NAMES + literal
18
- # ATTRIBUTE KEYS in the call's keyword hash. Phase 1 (c)
19
- # ships the AR column cross-check via the
20
- # `rigor-activerecord` `:model_index` ADR-9 fact, after
21
- # `rigor-activerecord` adds the matching publish hook.
16
+ # Recognises factory NAMES + literal ATTRIBUTE KEYS in
17
+ # the call's keyword hash. The AR column cross-check uses
18
+ # the `rigor-activerecord` `:model_index` ADR-9 fact when
19
+ # that plugin is loaded (optional `consumes:`).
22
20
  # Traits, sequences, parent / child factories, and dynamic
23
21
  # factory names are deferred to follow-up slices.
24
22
  #
@@ -54,9 +52,9 @@ module Rigor
54
52
  # `.build_stubbed_list`. The legacy `FactoryGirl` constant
55
53
  # is recognised identically. Implicit-receiver calls
56
54
  # (`create(:name)` inside an `include FactoryBot::Syntax::Methods`
57
- # context) are NOT recognised in Phase 1 (a) too many
58
- # false positives on plain `create` calls outside test
59
- # files; this needs receiver-type inference (Phase 1 (b)).
55
+ # context) are NOT recognised too many false positives on
56
+ # plain `create` calls outside test files; deferred until
57
+ # receiver-type inference can disambiguate.
60
58
  #
61
59
  # ## What's recognised inside `factory :name do ... end`
62
60
  #
@@ -87,19 +85,15 @@ module Rigor
87
85
  ]
88
86
  )
89
87
 
90
- producer :factory_index do |_params|
88
+ producer :factory_index, watch: -> { [[@factory_search_paths, "**/*.rb"]] } do |_params|
91
89
  FactoryDiscoverer.new(
92
90
  io_boundary: io_boundary,
93
91
  search_paths: @factory_search_paths
94
92
  ).discover
95
93
  end
96
94
 
97
- def init(services)
98
- @services = services
95
+ def init(_services)
99
96
  @factory_search_paths = Array(config.fetch("factory_search_paths")).map(&:to_s)
100
- @factory_index = nil
101
- @model_index = nil
102
- @model_index_resolved = false
103
97
  end
104
98
 
105
99
  # ADR-37 — per-call factory/attribute validation over the
@@ -108,42 +102,17 @@ module Rigor
108
102
  # is positioned via `diagnostic(node, location:)`. No file-level
109
103
  # diagnostic remains, so there is no `diagnostics_for_file`.
110
104
  node_rule Prism::CallNode do |node, _scope, path|
111
- index = factory_index_or_nil
105
+ index = producer_value(:factory_index)
112
106
  next [] if index.nil? || index.empty?
113
107
 
114
- Analyzer.violations_for(
115
- call_node: node, factory_index: index, model_index: model_index_or_nil
116
- ).map do |violation|
117
- diagnostic(
118
- node, path: path, location: violation.location,
119
- message: violation.message, severity: violation.severity, rule: violation.rule
120
- )
121
- end
122
- end
123
-
124
- private
125
-
126
- # Phase 1 (c) — lazily resolves the :model_index fact
127
- # from rigor-activerecord. Returns nil when
128
- # rigor-activerecord isn't loaded or hasn't published
129
- # an index; the analyzer treats nil as "no cross-check"
130
- # and falls back to Phase 1 (a) behaviour (factory
131
- # attributes only).
132
- def model_index_or_nil
133
- return @model_index if @model_index_resolved
134
-
135
- @model_index = @services.fact_store.read(plugin_id: "activerecord", name: :model_index)
136
- @model_index_resolved = true
137
- @model_index
138
- end
139
-
140
- def factory_index_or_nil
141
- return @factory_index if @factory_index
142
-
143
- descriptor = glob_descriptor(@factory_search_paths, "**/*.rb")
144
- @factory_index = cache_for(:factory_index, params: {}, descriptor: descriptor).call
145
- rescue StandardError
146
- nil
108
+ # `:model_index` is rigor-activerecord's published fact (ADR-9);
109
+ # nil when that plugin isn't loaded, in which case the analyzer
110
+ # falls back to factory-attributes-only checking.
111
+ violations = Analyzer.violations_for(
112
+ call_node: node, factory_index: index,
113
+ model_index: read_fact(plugin_id: "activerecord", name: :model_index)
114
+ )
115
+ diagnostics_for(violations, path: path, node: node)
147
116
  end
148
117
  end
149
118
 
@@ -50,12 +50,13 @@ module Rigor
50
50
 
51
51
  # @param paths [Array<String>] absolute paths to `.rb` files
52
52
  # the project's `paths:` resolves to.
53
- # @return [Hash{Symbol => Hash}] frozen 3-key result with
53
+ # @return [Hash{Symbol => Hash}] frozen 4-key result:
54
54
  # `:types` (per-`Schema::Object` field table),
55
- # `:enums` (per-`Schema::Enum` value list), and
56
- # `:input_objects` (per-`Schema::InputObject` argument
57
- # table). Any subset may be empty when no recognisable
58
- # declaration of that kind is found.
55
+ # `:enums` (per-`Schema::Enum` value list),
56
+ # `:input_objects` (per-`Schema::InputObject` argument table),
57
+ # `:mutations` (per-`Schema::Mutation` arguments+fields table).
58
+ # Any subset may be empty when no recognisable declaration
59
+ # of that kind is found.
59
60
  def scan(paths:)
60
61
  acc = empty_accumulator
61
62
  paths.each do |path|
@@ -97,10 +98,9 @@ module Rigor
97
98
  private_class_method :scan_file
98
99
 
99
100
  # Walks the AST collecting `class X < GraphQL::Schema::Object`,
100
- # `class X < GraphQL::Schema::Enum`, and
101
- # `class X < GraphQL::Schema::InputObject` decls at any
102
- # nesting level. Returns a 3-key hash so the caller can
103
- # publish multiple cross-plugin facts from one walk.
101
+ # `Schema::Enum`, `Schema::InputObject`, and `Schema::Mutation`
102
+ # decls at any nesting level. Returns a 4-key hash so the
103
+ # caller can publish multiple cross-plugin facts from one walk.
104
104
  def collect_definitions(node, qualified_prefix)
105
105
  return empty_accumulator if node.nil?
106
106
 
@@ -203,10 +203,9 @@ module Rigor
203
203
  # The first positional must be a String literal — the
204
204
  # graphql-ruby `value` API also accepts a Symbol form
205
205
  # (`value :ACTIVE`) but the documented idiom is String.
206
- # Slice 2b only stores the GraphQL-side value name; the
207
- # optional `value:` kwarg (Ruby-side override) and
208
- # `description:` stay out of the published table for
209
- # the floor.
206
+ # Only the GraphQL-side value name is stored; the optional
207
+ # `value:` kwarg (Ruby-side override) and `description:`
208
+ # are omitted from the published table.
210
209
  def collect_values(body)
211
210
  return [] if body.nil?
212
211
 
@@ -25,10 +25,10 @@ module Rigor
25
25
  # resolver methods themselves; rigor's value here is producing a
26
26
  # static type table downstream consumers can cross-reference.
27
27
  #
28
- # ## What downstream consumers DO with `:graphql_type_table`
28
+ # ## What downstream consumers DO with the published facts
29
29
  #
30
- # The fact is the substrate for two future capabilities (both
31
- # demand-driven, NOT in slice 1):
30
+ # The tables are the substrate for two future capabilities
31
+ # (demand-driven, not yet implemented):
32
32
  #
33
33
  # - Resolver-method check: for each `field :name, Type` whose
34
34
  # `name` is also defined as a Ruby method on the class, verify
@@ -37,33 +37,25 @@ module Rigor
37
37
  # plugin could type `Schema.execute(query).to_h` against the
38
38
  # queried fields.
39
39
  #
40
- # ## Floor / ceiling (slice 1)
40
+ # ## What's recognised
41
41
  #
42
- # Slice 1 ships the **floor**:
42
+ # - `class T < GraphQL::Schema::Object` subclasses (including
43
+ # nested namespaces); `field :name, Type, null: ...`
44
+ # declarations with constant-reference or list-array types
45
+ # and GraphQL→Ruby scalar mapping.
46
+ # - `class T < GraphQL::Schema::Enum`; `value "ACTIVE"` calls.
47
+ # - `class T < GraphQL::Schema::InputObject` /
48
+ # `GraphQL::Schema::Mutation`; `argument :name, Type,
49
+ # required: ...` declarations.
50
+ # - No user-facing diagnostics yet.
43
51
  #
44
- # - Recognises `class T < GraphQL::Schema::Object` subclasses
45
- # (including nested namespaces: `class Types::User < ...`,
46
- # `module Types; class User < ...; end; end`).
47
- # - Recognises the `field :name, Type, **opts` declaration with:
48
- # - `Type` as a `ConstantReadNode` / `ConstantPathNode` (`String`
49
- # / `Integer` / `Boolean` / `Float` / `ID`, or a user-defined
50
- # `Types::OtherObject`).
51
- # - `null: true` / `null: false` keyword extracts nullability.
52
- # - Maps the canonical GraphQL scalar names to underlying Ruby
53
- # classes (`String` → `String`, `Integer` → `Integer`,
54
- # `Boolean` → `TrueClass`, `Float` → `Float`, `ID` → `String`).
55
- # - Publishes the table; no user-facing diagnostics yet.
52
+ # ## Deferred (demand-driven)
56
53
  #
57
- # The **ceiling** (future slices, demand-driven):
58
- #
59
- # - **`GraphQL::Schema::Enum`** with `value "ACTIVE"` calls.
60
- # - **`GraphQL::Schema::Mutation`** + **`GraphQL::Schema::InputObject`**.
61
- # - **List / Non-Null wrappers** (`[String]`, `String.array`).
62
54
  # - **`resolver:` / `mutation:` reroute** recognition.
63
55
  # - **String type expressions** (`field :foo, "User"`) — defeats
64
56
  # static resolution by design (graphql-ruby's `BuildType.parse_type`
65
57
  # constantizes at runtime); a future slice could surface these
66
- # as `graphql.string-type` `:info` diagnostics that point the
58
+ # as `graphql.string-type` `:info` diagnostics pointing the
67
59
  # user at the constant-reference form for static typing.
68
60
  class Graphql < Rigor::Plugin::Base
69
61
  manifest(
@@ -71,9 +71,9 @@ module Rigor
71
71
  # - `is_a?(Result::Ok)` / `Some` / `None` exhaustive
72
72
  # narrowing — core control-flow analysis over a sealed
73
73
  # hierarchy, not a plugin surface.
74
- # - The `variants do variant Const, Type end` Enum DSL needs
75
- # an ADR-16 nested-class emission tier (ADR-36). Today's
76
- # contract has no `const_set`-emitting macro substrate.
74
+ # - The `variants do variant Const, Type end` Enum DSL is handled
75
+ # via ADR-36 `nested_class_templates:` in this plugin's manifest
76
+ # (Slice A see `nested_class_templates:` block below).
77
77
  class Mangrove < Rigor::Plugin::Base
78
78
  manifest(
79
79
  id: "mangrove",
@@ -99,7 +99,7 @@ module Rigor
99
99
  receiver_constraint: "Mangrove::Enum",
100
100
  block_method: :variants,
101
101
  variant_method: :variant,
102
- name_arg_position: 0,
102
+ symbol_arg_position: 0,
103
103
  inner_arg_position: 1,
104
104
  inner_reader: :inner
105
105
  )
@@ -30,10 +30,10 @@ module Rigor
30
30
  #
31
31
  # ## Configuration
32
32
  #
33
- # No knobs in v0.1.0. Activate via `plugins: ["rigor-minitest"]`
34
- # in `.rigor.yml`.
33
+ # No configuration knobs. Activate via
34
+ # `plugins: ["rigor-minitest"]` in `.rigor.yml`.
35
35
  #
36
- # ## Limitations (v0.1.0)
36
+ # ## Limitations
37
37
  #
38
38
  # - **No `assert_raises(T) { ... }`** — that's a block-shape
39
39
  # matcher and Rigor's narrowing model is for