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
@@ -18,10 +18,11 @@ module Rigor
18
18
  # overrides {#init} to wire up any state it needs from the
19
19
  # injected service container.
20
20
  #
21
- # Slice 1 ships only the registration / loading plumbing. The
22
- # protocol hooks (dynamic-return contributions, type-specifying
23
- # contributions, dynamic reflection) land in subsequent v0.1.0
24
- # slices and arrive as additional methods on this class.
21
+ # This class implements all plugin protocol hooks: per-call
22
+ # return-type contributions (`dynamic_return`), narrowing-fact
23
+ # contributions (`type_specifier`), AST node rules (`node_rule`),
24
+ # and producer/cache hooks. Cumulative implementation per the
25
+ # ADR-37 / ADR-52 slice chain.
25
26
  #
26
27
  # Example plugin:
27
28
  #
@@ -74,23 +75,59 @@ module Rigor
74
75
  # argument; the same params Hash mixes into the cache
75
76
  # key per `Cache::Descriptor#cache_key_for`.
76
77
  #
77
- # `serialize:` / `deserialize:` are forwarded verbatim to
78
- # `Cache::Store#fetch_or_compute`. Default round-trip is
79
- # `Marshal.dump` / `Marshal.load` per the v0.0.9 callable
80
- # surface; producers whose return values are not Marshal-
81
- # clean must supply their own pair.
78
+ # `serialize:` / `deserialize:` apply to the producer's
79
+ # return VALUE (the cache layer wraps them around the
80
+ # record-and-validate entry pair itself). Default
81
+ # round-trip is `Marshal.dump` / `Marshal.load` per the
82
+ # v0.0.9 callable surface; producers whose return values
83
+ # are not Marshal-clean must supply their own pair.
84
+ #
85
+ # `watch:` (ADR-60 WD3) declares the glob coverage of a
86
+ # discovery-style producer — the files whose addition /
87
+ # removal / edit must invalidate the cached value even
88
+ # when the producer block never read them individually
89
+ # (e.g. it globbed a directory itself). It is either
90
+ #
91
+ # - a static Array of `[roots, pattern, ...]` tuples
92
+ # (`roots` a String or Array of Strings; one or more
93
+ # glob-pattern suffixes per tuple — the same shape
94
+ # {#glob_descriptor} takes), or
95
+ # - a Proc, run through `instance_exec` on the plugin
96
+ # instance at `cache_for` invocation time (NEVER at
97
+ # class-definition time — search roots are typically
98
+ # computed in `#init` from config), returning the same
99
+ # tuple Array.
100
+ #
101
+ # The evaluated tuples become {Cache::Descriptor::GlobEntry}
102
+ # rows in the dependency descriptor recorded after the
103
+ # block runs; `Descriptor#fresh?` re-globs + re-digests on
104
+ # the next run.
82
105
  #
83
106
  # Producer ids are auto-prefixed `plugin.<manifest.id>.`
84
107
  # at the cache layer (slice 6-C) so plugin-side ids cannot
85
108
  # collide with built-in producers.
86
- def producer(id, serialize: nil, deserialize: nil, &block)
109
+ def producer(id, watch: nil, serialize: nil, deserialize: nil, &block)
87
110
  raise ArgumentError, "Plugin::Base.producer requires a block body" if block.nil?
88
111
 
112
+ validate_producer_watch!(watch)
89
113
  @producers ||= {}
90
- @producers[id.to_sym] = { block: block, serialize: serialize, deserialize: deserialize }.freeze
114
+ @producers[id.to_sym] = {
115
+ block: block, watch: watch, serialize: serialize, deserialize: deserialize
116
+ }.freeze
91
117
  id.to_sym
92
118
  end
93
119
 
120
+ # ADR-60 WD3 — `watch:` is nil (no glob coverage), a static
121
+ # tuple Array, or a Proc evaluated per `cache_for` call.
122
+ def validate_producer_watch!(watch)
123
+ return if watch.nil? || watch.is_a?(Array) || watch.respond_to?(:call)
124
+
125
+ raise ArgumentError,
126
+ "Plugin::Base.producer watch: must be nil, an Array of [roots, pattern, ...] tuples, " \
127
+ "or a Proc returning one, got #{watch.inspect}"
128
+ end
129
+ private :validate_producer_watch!
130
+
94
131
  # Frozen snapshot of the producer table. Inherited
95
132
  # producers from a superclass are intentionally NOT
96
133
  # surfaced — Plugin::Base subclasses do not chain
@@ -445,6 +482,13 @@ module Rigor
445
482
  # memo-on-first-dispatch is a Hash-content mutation, sound even on
446
483
  # a self-freezing plugin.
447
484
  @dynamic_return_runtime_cache = {}
485
+ # ADR-60 WD4 — nil-inclusive memo tables for the authoring
486
+ # helpers ({#read_fact} / {#producer_value} / {#producer_error}).
487
+ # Allocated here, before any subclass `initialize` self-freeze,
488
+ # for the same reason: a populate is a Hash-content mutation.
489
+ @fact_cache = {}
490
+ @producer_value_cache = {}
491
+ @producer_errors = {}
448
492
  end
449
493
 
450
494
  # Override in subclasses to wire any state the plugin needs
@@ -500,10 +544,11 @@ module Rigor
500
544
  #
501
545
  # `path` is the analysed file path; `scope` is the entry
502
546
  # `Rigor::Scope` after `ScopeIndexer` ran; `root` is the
503
- # parsed `Prism::Node` root. Plugin authors traverse `root`
504
- # themselves if they need node-scoped rules the
505
- # `Rule<TNode>` API ADR-2 § "Custom rules" mentions stays
506
- # deferred to v0.1.x.
547
+ # parsed `Prism::Node` root. Plugin authors can traverse
548
+ # `root` themselves or declare rules via the `.node_rule` DSL
549
+ # (ADR-37, shipped). The PHPStan-style `Rule<TNode>` base
550
+ # class mentioned in ADR-2 was superseded by the block-based
551
+ # `.node_rule` DSL.
507
552
  #
508
553
  # Default returns `[]` so plugins that contribute through
509
554
  # other channels (e.g. slice-4 narrowing contributions,
@@ -625,6 +670,69 @@ module Rigor
625
670
  )
626
671
  end
627
672
 
673
+ # ADR-60 WD4 — maps a plugin's own violation objects to
674
+ # `Rigor::Analysis::Diagnostic`s through {#diagnostic}, absorbing
675
+ # the `violations.map { |v| diagnostic(node, …) }` block the
676
+ # node-rule plugins otherwise repeat. Each violation duck-types:
677
+ # `#message` (required); optional `#node` (the Prism node to
678
+ # position at — falls back to the `node:` argument, the common
679
+ # "all violations point at the same call" case), `#location` (a
680
+ # sub-location such as `node.message_loc`), `#severity` (defaults
681
+ # `:error`), and `#rule`. Returns an Array suitable for direct
682
+ # return from `#diagnostics_for_file` / a `node_rule` block.
683
+ def diagnostics_for(violations, path:, node: nil)
684
+ Array(violations).map do |violation|
685
+ target = (violation.node if violation.respond_to?(:node)) || node
686
+ diagnostic(
687
+ target,
688
+ path: path,
689
+ message: violation.message,
690
+ severity: (violation.respond_to?(:severity) && violation.severity) || :error,
691
+ rule: (violation.rule if violation.respond_to?(:rule)),
692
+ location: (violation.location if violation.respond_to?(:location))
693
+ )
694
+ end
695
+ end
696
+
697
+ # ADR-60 WD4 — reads a cross-plugin fact (ADR-9) published by
698
+ # another plugin's `#prepare` hook, memoised per `(plugin_id,
699
+ # name)` on this instance INCLUDING a nil result. The nil-inclusive
700
+ # memo retires the hand-rolled `@x_resolved` flag the discovery
701
+ # plugins carried to distinguish "fact not published" from "not yet
702
+ # read". `services.fact_store` is the only sanctioned cross-plugin
703
+ # channel; a fact no loaded producer published reads as nil.
704
+ def read_fact(plugin_id:, name:)
705
+ key = [plugin_id.to_s, name.to_sym].freeze
706
+ return @fact_cache[key] if @fact_cache.key?(key)
707
+
708
+ @fact_cache[key] = services.fact_store.read(plugin_id: plugin_id.to_s, name: name.to_sym)
709
+ end
710
+
711
+ # ADR-60 WD4 — runs a declared {.producer} through {#cache_for}
712
+ # and returns its value, memoised per `(id, params)` INCLUDING nil.
713
+ # A `StandardError` the producer raises (a malformed project file,
714
+ # an I/O failure) is rescued, recorded for {#producer_error}, and
715
+ # yields nil — so one bad project file degrades a plugin to silence
716
+ # rather than aborting the whole run. This is the `*_index_or_nil`
717
+ # shape the discovery plugins hand-rolled, named once.
718
+ def producer_value(id, params: {})
719
+ key = [id.to_sym, params].freeze
720
+ return @producer_value_cache[key] if @producer_value_cache.key?(key)
721
+
722
+ @producer_value_cache[key] = cache_for(id, params: params).call
723
+ rescue StandardError => e
724
+ @producer_errors[id.to_sym] = e
725
+ @producer_value_cache[key] = nil
726
+ end
727
+
728
+ # ADR-60 WD4 — the `StandardError` a prior {#producer_value} call
729
+ # rescued for `id`, or nil when it succeeded or was never called.
730
+ # Plugins surface it as a load-error diagnostic from
731
+ # `#diagnostics_for_file`.
732
+ def producer_error(id)
733
+ @producer_errors[id.to_sym]
734
+ end
735
+
628
736
  # Boilerplate-reduction helper (review §1.3): the "did you mean …?"
629
737
  # suggestion every diagnostic-emitting plugin otherwise hand-rolls.
630
738
  # Returns the closest of `candidates` to `name` via
@@ -695,32 +803,38 @@ module Rigor
695
803
  @io_boundary ||= services.io_boundary_for(manifest.id)
696
804
  end
697
805
 
698
- # ADR-7 § "Slice 6-A" — returns a callable that performs
699
- # a `Cache::Store#fetch_or_compute` round-trip for the
700
- # named producer. The descriptor (per ADR-7 § "Slice
701
- # 6-B") is auto-assembled from the plugin's
702
- # `PluginEntry` template (id, version, config_hash) and
703
- # the {IoBoundary} read history. The producer id is
704
- # auto-prefixed `plugin.<manifest.id>.` per ADR-7 §
705
- # "Slice 6-C" so plugin caches stay sandboxed from
706
- # built-in producers.
806
+ # ADR-7 § "Slice 6-A" / ADR-60 WD3 — returns a callable that
807
+ # performs a `Cache::Store#fetch_or_validate` round-trip for
808
+ # the named producer (the ADR-45 record-and-validate path).
809
+ # The entry is KEYED on the stable identity inputs — the
810
+ # plugin's `PluginEntry` template (id, version, config_hash)
811
+ # composed with the optional `descriptor:` extras and
812
+ # stores, beside the value, a DEPENDENCY descriptor recorded
813
+ # AFTER the producer block ran: the {IoBoundary}'s
814
+ # post-compute read history plus the evaluated `watch:`
815
+ # {Cache::Descriptor::GlobEntry} rows. In-block reads are
816
+ # therefore always captured (the structural stale-cache
817
+ # hazard `fetch_or_compute`'s call-time snapshot carried);
818
+ # the next run re-validates the recorded dependencies by
819
+ # re-digest (`Descriptor#fresh?`) and recomputes when any
820
+ # changed. The producer id is auto-prefixed
821
+ # `plugin.<manifest.id>.` per ADR-7 § "Slice 6-C" so plugin
822
+ # caches stay sandboxed from built-in producers.
707
823
  #
708
824
  # When `services.cache_store` is `nil` (e.g. CLI
709
825
  # `--no-cache`), the callable bypasses the cache and
710
826
  # runs the producer block every time — same semantics
711
827
  # as the v0.0.9 cache surface for built-in producers.
712
828
  #
713
- # `descriptor:` (optional, ADR-7 § "Slice 6" follow-up)
714
- # supplies extra `Cache::Descriptor` rows the plugin
715
- # author wants to compose into the auto-built descriptor
716
- # typically gem-version `GemEntry`, configuration-file
717
- # `FileEntry` digests, or `ConfigEntry` rows for external
718
- # state the {IoBoundary} cannot capture itself. The
719
- # passed descriptor composes via `Cache::Descriptor.compose`
720
- # with the auto-built one (PluginEntry template + boundary
721
- # reads); per-slot conflicts raise
722
- # `Cache::Descriptor::Conflict` to make divergent inputs
723
- # visible rather than silently shadowing.
829
+ # `descriptor:` (optional) supplies extra `Cache::Descriptor`
830
+ # rows for IDENTITY inputs — gem-version `GemEntry` pins,
831
+ # `ConfigEntry` rows for external state that compose into
832
+ # the cache KEY via `Cache::Descriptor.compose`; per-slot
833
+ # conflicts raise `Cache::Descriptor::Conflict` to make
834
+ # divergent inputs visible rather than silently shadowing.
835
+ # A key change is a miss, so the invalidation effect of the
836
+ # legacy `glob_descriptor`-as-`descriptor:` idiom is
837
+ # preserved unchanged.
724
838
  def cache_for(producer_id, params: {}, descriptor: nil)
725
839
  producer = self.class.producers[producer_id.to_sym]
726
840
  unless producer
@@ -733,16 +847,18 @@ module Rigor
733
847
  return compute unless store
734
848
 
735
849
  prefixed_id = "plugin.#{manifest.id}.#{producer_id}"
736
- composed_descriptor = compose_cache_descriptor(descriptor)
850
+ key_descriptor = compose_key_descriptor(descriptor)
737
851
  lambda do
738
- store.fetch_or_compute(
852
+ store.fetch_or_validate(
739
853
  producer_id: prefixed_id,
854
+ key_descriptor: key_descriptor,
740
855
  params: params,
741
- descriptor: composed_descriptor,
742
- serialize: producer[:serialize],
743
- deserialize: producer[:deserialize],
744
- &compute
745
- )
856
+ serialize: pair_serializer(producer[:serialize]),
857
+ deserialize: pair_deserializer(producer[:deserialize])
858
+ ) do
859
+ value = compute.call
860
+ [value, producer_dependency_descriptor(producer)]
861
+ end
746
862
  end
747
863
  end
748
864
 
@@ -754,31 +870,13 @@ module Rigor
754
870
  # descriptor), or any removal (the previously-matched file
755
871
  # drops out).
756
872
  #
757
- # Pass the returned descriptor as `cache_for(..., descriptor: …)`
758
- # so the cache key reflects the project files the producer
759
- # reads from. Without it, `Plugin::Base#cache_for`'s
760
- # auto-built descriptor only includes files the
761
- # {Plugin::IoBoundary} has already read in the current
762
- # process empty on the first call of a fresh process so
763
- # the cache key is identical regardless of project state and
764
- # warm runs return stale producer output when files have
765
- # changed between sessions.
766
- #
767
- # Discovery-style producers (`actioncable`'s `:channel_index`,
768
- # `actionmailer`'s `:mailer_index`, `rails-i18n`'s
769
- # `:locale_index`) all follow the same pattern: walk a glob
770
- # under one or more search roots, parse / read every match,
771
- # build a typed index. They MUST call this helper at the
772
- # `cache_for(descriptor: …)` site to be cache-correct under
773
- # the persistent `Cache::Store` `rigor check` uses by
774
- # default.
775
- #
776
- # The helper pays one SHA-256 read per matched file at
777
- # call time; the producer block typically re-reads through
778
- # `io_boundary.read_file` so the cost is doubled. For
779
- # discovery globs in the 10-100 file range this is
780
- # negligible (~ms) relative to the parse + walk the
781
- # producer does on cache miss.
873
+ # ADR-60 WD3 made this **private**: the declared way for a
874
+ # discovery-style producer to cover its glob is `producer
875
+ # watch:` (one {Cache::Descriptor::GlobEntry} per glob in the
876
+ # record-and-validate dependency descriptor), not a hand-built
877
+ # descriptor composed into the cache *key*. The method survives
878
+ # only as the building block for the rare producer that needs
879
+ # `FileEntry` rows directly; plugin code calls `watch:`.
782
880
  #
783
881
  # @param roots [Array<String>] search roots (relative to
784
882
  # the project root, or absolute paths)
@@ -798,6 +896,7 @@ module Rigor
798
896
  end
799
897
  Cache::Descriptor.new(files: entries)
800
898
  end
899
+ private :glob_descriptor
801
900
 
802
901
  private
803
902
 
@@ -923,17 +1022,6 @@ module Rigor
923
1022
  matched.uniq.sort.select { |path| File.file?(path) }
924
1023
  end
925
1024
 
926
- # ADR-7 § "Slice 6-B" — composes the per-call cache
927
- # descriptor from (1) the plugin's PluginEntry template
928
- # and (2) the IoBoundary's accumulated FileEntry rows.
929
- def build_plugin_cache_descriptor
930
- boundary_descriptor = io_boundary.cache_descriptor
931
- Cache::Descriptor.new(
932
- plugins: [plugin_entry],
933
- files: boundary_descriptor.files
934
- )
935
- end
936
-
937
1025
  public
938
1026
 
939
1027
  # ADR-32 WD5 — the `Cache::Descriptor::PluginEntry`
@@ -962,20 +1050,90 @@ module Rigor
962
1050
 
963
1051
  private
964
1052
 
965
- # ADR-7 § "Slice 6" follow-up composes the auto-built
966
- # cache descriptor with an optional plugin-author-supplied
967
- # extension. Extra `GemEntry` / `FileEntry` / `ConfigEntry`
968
- # rows the plugin needs (gem-version pins, external
969
- # configuration files, sibling-plugin state) flow through
970
- # `Cache::Descriptor.compose`; the union behaviour matches
971
- # built-in producers (`RbsConstantTable`, `RbsEnvironment`).
972
- def compose_cache_descriptor(extra)
973
- auto_built = build_plugin_cache_descriptor
1053
+ # ADR-60 WD3 the cache KEY descriptor: the plugin's
1054
+ # PluginEntry template composed with an optional
1055
+ # plugin-author-supplied extension carrying IDENTITY inputs
1056
+ # (gem-version pins, `ConfigEntry` rows, configuration-file
1057
+ # digests). The IoBoundary read history deliberately does NOT
1058
+ # enter the key it is recorded post-compute into the
1059
+ # dependency descriptor instead (see
1060
+ # {#producer_dependency_descriptor}).
1061
+ def compose_key_descriptor(extra)
1062
+ auto_built = Cache::Descriptor.new(plugins: [plugin_entry])
974
1063
  return auto_built if extra.nil?
975
1064
 
976
1065
  Cache::Descriptor.compose(auto_built, extra)
977
1066
  end
978
1067
 
1068
+ # ADR-60 WD3 — the dependency descriptor stored beside the
1069
+ # producer's value, built AFTER the block ran so every
1070
+ # in-block `io_boundary` read is captured, plus the evaluated
1071
+ # `watch:` glob rows.
1072
+ #
1073
+ # The boundary snapshot may carry `ConfigEntry` rows (URL
1074
+ # fetches, see {IoBoundary#open_url}). `Descriptor#fresh?`
1075
+ # refuses any non-file/glob slot, so including them makes the
1076
+ # entry permanently stale → the producer recomputes EVERY run.
1077
+ # That is deliberate: it is sound (never stale) and
1078
+ # URL-reading producers are rare; a remote document has no
1079
+ # cheap local re-validation anyway.
1080
+ def producer_dependency_descriptor(producer)
1081
+ boundary = io_boundary.cache_descriptor
1082
+ Cache::Descriptor.new(
1083
+ files: boundary.files,
1084
+ configs: boundary.configs,
1085
+ globs: watch_glob_entries(producer[:watch])
1086
+ )
1087
+ end
1088
+
1089
+ # ADR-60 WD3 — evaluates a producer's `watch:` declaration
1090
+ # into {Cache::Descriptor::GlobEntry} rows. A Proc is
1091
+ # `instance_exec`'d on this plugin instance (so `#init`-built
1092
+ # search roots are in scope); the result — like the static
1093
+ # form — is an Array of `[roots, pattern, ...]` tuples, one
1094
+ # GlobEntry per (root, pattern) pair. Roots are expanded to
1095
+ # absolute paths (matching {#glob_descriptor}) so freshness
1096
+ # re-validation does not depend on the validating process's
1097
+ # working directory.
1098
+ def watch_glob_entries(watch)
1099
+ return [] if watch.nil?
1100
+
1101
+ tuples = watch.respond_to?(:call) ? instance_exec(&watch) : watch
1102
+ Array(tuples).flat_map do |tuple|
1103
+ roots, *patterns = Array(tuple)
1104
+ Array(roots).flat_map do |root|
1105
+ absolute = File.expand_path(root.to_s)
1106
+ patterns.map { |pattern| Cache::Descriptor::GlobEntry.compute(root: absolute, pattern: pattern.to_s) }
1107
+ end
1108
+ end.uniq
1109
+ end
1110
+
1111
+ # ADR-60 WD3 — `fetch_or_validate` stores a
1112
+ # `[value, dependency_descriptor]` pair, but the producer's
1113
+ # declared `serialize:`/`deserialize:` contract covers the
1114
+ # VALUE alone. These wrappers apply the custom callable to the
1115
+ # value half and Marshal the descriptor half, so a producer
1116
+ # with a non-Marshal-clean value keeps working unchanged. A
1117
+ # nil callable returns nil — the store's default whole-pair
1118
+ # Marshal round-trip applies.
1119
+ def pair_serializer(serialize)
1120
+ return nil if serialize.nil?
1121
+
1122
+ lambda do |pair|
1123
+ value, dependency_descriptor = pair
1124
+ Marshal.dump([serialize.call(value).b, Marshal.dump(dependency_descriptor)]).b
1125
+ end
1126
+ end
1127
+
1128
+ def pair_deserializer(deserialize)
1129
+ return nil if deserialize.nil?
1130
+
1131
+ lambda do |bytes|
1132
+ value_bytes, descriptor_bytes = Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
1133
+ [deserialize.call(value_bytes), Marshal.load(descriptor_bytes)] # rubocop:disable Security/MarshalLoad
1134
+ end
1135
+ end
1136
+
979
1137
  def digest_config(config)
980
1138
  canonical = Cache::Descriptor.canonicalize_value(config || {})
981
1139
  Digest::SHA256.hexdigest(JSON.generate(canonical))
@@ -4,9 +4,9 @@ module Rigor
4
4
  module Plugin
5
5
  module Macro
6
6
  # ADR-16 Tier A declaration: "the block passed to a
7
- # class-level DSL call of one of `verbs` runs as an instance
8
- # method on `receiver_constraint`'s subclass tree, with
9
- # `self` typed accordingly."
7
+ # class-level DSL call of one of `method_names` runs as an
8
+ # instance method on `receiver_constraint`'s subclass tree,
9
+ # with `self` typed accordingly."
10
10
  #
11
11
  # Authored on a plugin manifest:
12
12
  #
@@ -16,7 +16,7 @@ module Rigor
16
16
  # block_as_methods: [
17
17
  # Rigor::Plugin::Macro::BlockAsMethod.new(
18
18
  # receiver_constraint: "Sinatra::Base",
19
- # verbs: %i[get post put delete head options patch link unlink]
19
+ # method_names: %i[get post put delete head options patch link unlink]
20
20
  # )
21
21
  # ]
22
22
  # )
@@ -29,10 +29,9 @@ module Rigor
29
29
  # class-level methods whose block argument runs as if it were
30
30
  # an instance method of the receiver.
31
31
  #
32
- # Slice 1a (this file) is **the contract only**. The engine
33
- # hook that consults registered entries and narrows
34
- # `Scope#self_type` for a block whose enclosing call matches
35
- # arrives in slice 1b.
32
+ # Engine wiring: `Inference::MacroBlockSelfType.narrow_self_type_for`
33
+ # (called from expression_typer.rb) consults registered entries
34
+ # and narrows `Scope#self_type` for matching block call sites.
36
35
  #
37
36
  # ## Fields
38
37
  #
@@ -41,9 +40,10 @@ module Rigor
41
40
  # for the entry to fire. For Sinatra modular-style this is
42
41
  # `"Sinatra::Base"`; the substrate's class-context match
43
42
  # accepts every subclass.
44
- # - `verbs` — Array of Symbol method names. A call shape
43
+ # - `method_names` — Array of Symbol method names. A call shape
45
44
  # `<receiver_subclass>.get('/path') { ... }` matches when
46
- # `:get` is in this list.
45
+ # `:get` is in this list. (Named `verbs:` before ADR-60 WD2
46
+ # normalised the macro value-object vocabulary.)
47
47
  # - `self_type` — Symbol selecting the kind of `self`-binding
48
48
  # the substrate applies inside the block. Slice 1a accepts
49
49
  # only `:receiver_instance` (the block runs as an instance
@@ -53,22 +53,22 @@ module Rigor
53
53
  # ## Ractor-shareability
54
54
  #
55
55
  # All fields are frozen at construction (ADR-15 Phase 1).
56
- # `verbs` is dup-frozen so the caller's mutable array does
57
- # not leak into the value. `Ractor.shareable?` returns true
58
- # after `#initialize`.
56
+ # `method_names` is dup-frozen so the caller's mutable array
57
+ # does not leak into the value. `Ractor.shareable?` returns
58
+ # true after `#initialize`.
59
59
  class BlockAsMethod
60
60
  SELF_TYPE_RECEIVER_INSTANCE = :receiver_instance
61
61
  VALID_SELF_TYPES = [SELF_TYPE_RECEIVER_INSTANCE].freeze
62
62
 
63
- attr_reader :receiver_constraint, :verbs, :self_type
63
+ attr_reader :receiver_constraint, :method_names, :self_type
64
64
 
65
- def initialize(receiver_constraint:, verbs:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
65
+ def initialize(receiver_constraint:, method_names:, self_type: SELF_TYPE_RECEIVER_INSTANCE)
66
66
  validate_receiver_constraint!(receiver_constraint)
67
- validate_verbs!(verbs)
67
+ validate_method_names!(method_names)
68
68
  validate_self_type!(self_type)
69
69
 
70
70
  @receiver_constraint = receiver_constraint.dup.freeze
71
- @verbs = verbs.map(&:to_sym).freeze
71
+ @method_names = method_names.map(&:to_sym).freeze
72
72
  @self_type = self_type
73
73
  freeze
74
74
  end
@@ -76,7 +76,7 @@ module Rigor
76
76
  def to_h
77
77
  {
78
78
  "receiver_constraint" => receiver_constraint,
79
- "verbs" => verbs.map(&:to_s),
79
+ "method_names" => method_names.map(&:to_s),
80
80
  "self_type" => self_type.to_s
81
81
  }
82
82
  end
@@ -84,13 +84,13 @@ module Rigor
84
84
  def ==(other)
85
85
  other.is_a?(BlockAsMethod) &&
86
86
  receiver_constraint == other.receiver_constraint &&
87
- verbs == other.verbs &&
87
+ method_names == other.method_names &&
88
88
  self_type == other.self_type
89
89
  end
90
90
  alias eql? ==
91
91
 
92
92
  def hash
93
- [receiver_constraint, verbs, self_type].hash
93
+ [receiver_constraint, method_names, self_type].hash
94
94
  end
95
95
 
96
96
  private
@@ -103,17 +103,17 @@ module Rigor
103
103
  "got #{value.inspect}"
104
104
  end
105
105
 
106
- def validate_verbs!(verbs)
107
- unless verbs.is_a?(Array) && !verbs.empty?
106
+ def validate_method_names!(method_names)
107
+ unless method_names.is_a?(Array) && !method_names.empty?
108
108
  raise ArgumentError,
109
- "Plugin::Macro::BlockAsMethod#verbs must be a non-empty Array, got #{verbs.inspect}"
109
+ "Plugin::Macro::BlockAsMethod#method_names must be a non-empty Array, got #{method_names.inspect}"
110
110
  end
111
111
 
112
- verbs.each do |v|
112
+ method_names.each do |v|
113
113
  next if v.is_a?(Symbol) || (v.is_a?(String) && !v.empty?)
114
114
 
115
115
  raise ArgumentError,
116
- "Plugin::Macro::BlockAsMethod#verbs entries must be Symbol/non-empty String, " \
116
+ "Plugin::Macro::BlockAsMethod#method_names entries must be Symbol/non-empty String, " \
117
117
  "got #{v.inspect}"
118
118
  end
119
119
  end
@@ -68,13 +68,10 @@ module Rigor
68
68
  # to a later slice — the `returns:` declarations cost
69
69
  # nothing to write today and unlock precision then.
70
70
  #
71
- # ## Slice 2a scope
72
- #
73
- # This file ships the value class only. Slice 2b wires the
74
- # pre-pass that scans Tier C call sites + the
75
- # `SyntheticMethodIndex` the dispatcher consults; slice 2c
76
- # authors `plugins/rigor-dry-struct/` and
77
- # `plugins/rigor-dry-types/` as the worked consumers.
71
+ # Engine wiring: `Inference::SyntheticMethodScanner` (slice 2b,
72
+ # `synthetic_method_scanner.rb`) consumes `manifest.heredoc_templates`.
73
+ # Worked consumers: `plugins/rigor-dry-struct/` and
74
+ # `plugins/rigor-dry-types/` (slice 2c).
78
75
  class HeredocTemplate
79
76
  NAME_PLACEHOLDER = "\#{name}"
80
77
 
@@ -33,7 +33,7 @@ module Rigor
33
33
  # receiver_constraint: "Mangrove::Enum", # `extend`-ed marker module
34
34
  # block_method: :variants, # the enclosing DSL block
35
35
  # variant_method: :variant, # each declaration call
36
- # name_arg_position: 0, # constant arg → nested class
36
+ # symbol_arg_position: 0, # constant arg → nested class
37
37
  # inner_arg_position: 1, # type arg → `#inner` return
38
38
  # inner_reader: :inner # the payload reader name
39
39
  # )
@@ -48,8 +48,10 @@ module Rigor
48
48
  # (`:variants`).
49
49
  # - `variant_method` — Symbol naming each declaration call
50
50
  # inside the block (`:variant`).
51
- # - `name_arg_position` — Integer (default 0): the argument
51
+ # - `symbol_arg_position` — Integer (default 0): the argument
52
52
  # index whose literal **constant** names the nested subclass.
53
+ # (Named `name_arg_position:` before ADR-60 WD2 normalised
54
+ # the macro value-object vocabulary.)
53
55
  # - `inner_arg_position` — Integer (default 1): the argument
54
56
  # index whose type expression becomes the `#inner` reader's
55
57
  # return type. Slice A resolves a constant type argument
@@ -69,21 +71,21 @@ module Rigor
69
71
  # `Environment#class_ordering`.
70
72
  class NestedClassTemplate
71
73
  attr_reader :receiver_constraint, :block_method, :variant_method,
72
- :name_arg_position, :inner_arg_position, :inner_reader
74
+ :symbol_arg_position, :inner_arg_position, :inner_reader
73
75
 
74
76
  def initialize(receiver_constraint:, block_method: :variants, variant_method: :variant,
75
- name_arg_position: 0, inner_arg_position: 1, inner_reader: :inner)
77
+ symbol_arg_position: 0, inner_arg_position: 1, inner_reader: :inner)
76
78
  validate_constraint!(receiver_constraint)
77
79
  validate_method!(block_method, "block_method")
78
80
  validate_method!(variant_method, "variant_method")
79
- validate_position!(name_arg_position, "name_arg_position")
81
+ validate_position!(symbol_arg_position, "symbol_arg_position")
80
82
  validate_position!(inner_arg_position, "inner_arg_position")
81
83
  validate_method!(inner_reader, "inner_reader")
82
84
 
83
85
  @receiver_constraint = receiver_constraint.dup.freeze
84
86
  @block_method = block_method.to_sym
85
87
  @variant_method = variant_method.to_sym
86
- @name_arg_position = name_arg_position
88
+ @symbol_arg_position = symbol_arg_position
87
89
  @inner_arg_position = inner_arg_position
88
90
  @inner_reader = inner_reader.to_sym
89
91
  freeze
@@ -94,7 +96,7 @@ module Rigor
94
96
  "receiver_constraint" => receiver_constraint,
95
97
  "block_method" => block_method.to_s,
96
98
  "variant_method" => variant_method.to_s,
97
- "name_arg_position" => name_arg_position,
99
+ "symbol_arg_position" => symbol_arg_position,
98
100
  "inner_arg_position" => inner_arg_position,
99
101
  "inner_reader" => inner_reader.to_s
100
102
  }
@@ -78,12 +78,9 @@ module Rigor
78
78
  # facts (attr_reader / after_save / etc.); slice 3 emits
79
79
  # only the module's plain instance methods.
80
80
  #
81
- # ## Slice 3a scope
82
- #
83
- # This file ships the value class only. Slice 3b wires the
84
- # scanner that walks Tier B call sites + the per-method
85
- # explosion via `SyntheticMethodIndex`; slice 3c authors
86
- # `plugins/rigor-devise/` model side as the worked consumer.
81
+ # Engine wiring: `Inference::SyntheticMethodScanner#collect_trait_registries`
82
+ # (slice 3b) walks Tier B call sites and explodes per-method
83
+ # entries. Worked consumer: `plugins/rigor-devise/` (slice 3c).
87
84
  class TraitRegistry
88
85
  REST_POSITION = :rest
89
86