rigortype 0.1.15 → 0.1.17

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 (220) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/exe/rigor +19 -0
  4. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  5. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +209 -0
  6. data/lib/rigor/analysis/check_rules.rb +174 -71
  7. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  8. data/lib/rigor/analysis/diagnostic.rb +58 -0
  9. data/lib/rigor/analysis/incremental.rb +162 -0
  10. data/lib/rigor/analysis/incremental_session.rb +337 -0
  11. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  12. data/lib/rigor/analysis/runner.rb +485 -29
  13. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  14. data/lib/rigor/analysis/worker_session.rb +3 -2
  15. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  16. data/lib/rigor/cache/descriptor.rb +56 -51
  17. data/lib/rigor/cache/incremental_snapshot.rb +147 -0
  18. data/lib/rigor/cache/rbs_cache_producer.rb +30 -0
  19. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  20. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  21. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  22. data/lib/rigor/cache/rbs_environment.rb +2 -8
  23. data/lib/rigor/cache/rbs_instance_definitions.rb +3 -16
  24. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  25. data/lib/rigor/cache/store.rb +99 -1
  26. data/lib/rigor/cli/annotate_command.rb +2 -7
  27. data/lib/rigor/cli/baseline_command.rb +2 -7
  28. data/lib/rigor/cli/command.rb +47 -0
  29. data/lib/rigor/cli/coverage_command.rb +3 -23
  30. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  31. data/lib/rigor/cli/diff_command.rb +3 -7
  32. data/lib/rigor/cli/explain_command.rb +2 -7
  33. data/lib/rigor/cli/lsp_command.rb +3 -7
  34. data/lib/rigor/cli/mcp_command.rb +3 -7
  35. data/lib/rigor/cli/options.rb +57 -0
  36. data/lib/rigor/cli/plugin_command.rb +3 -7
  37. data/lib/rigor/cli/plugins_command.rb +52 -10
  38. data/lib/rigor/cli/plugins_renderer.rb +86 -1
  39. data/lib/rigor/cli/renderable.rb +26 -0
  40. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  41. data/lib/rigor/cli/skill_command.rb +3 -7
  42. data/lib/rigor/cli/triage_command.rb +2 -7
  43. data/lib/rigor/cli/type_of_command.rb +5 -38
  44. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  45. data/lib/rigor/cli/type_scan_command.rb +3 -23
  46. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  47. data/lib/rigor/cli.rb +260 -48
  48. data/lib/rigor/configuration/dependencies.rb +18 -1
  49. data/lib/rigor/configuration/severity_profile.rb +22 -3
  50. data/lib/rigor/configuration.rb +13 -3
  51. data/lib/rigor/environment/rbs_loader.rb +335 -4
  52. data/lib/rigor/environment.rb +8 -2
  53. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  54. data/lib/rigor/inference/budget_trace.rb +137 -0
  55. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  56. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  57. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  58. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  59. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  60. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  61. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  62. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  63. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  64. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  65. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  73. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  74. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  75. data/lib/rigor/inference/expression_typer.rb +149 -22
  76. data/lib/rigor/inference/hkt_reducer.rb +2 -0
  77. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  78. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  79. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  80. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  81. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  82. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  83. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  84. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  85. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  86. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  87. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  88. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -6
  89. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +100 -23
  90. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  91. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  92. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  93. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  94. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  95. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  96. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  97. data/lib/rigor/inference/method_dispatcher.rb +147 -60
  98. data/lib/rigor/inference/narrowing.rb +202 -5
  99. data/lib/rigor/inference/precision_scanner.rb +60 -1
  100. data/lib/rigor/inference/scope_indexer.rb +257 -11
  101. data/lib/rigor/inference/statement_evaluator.rb +110 -26
  102. data/lib/rigor/inference/synthetic_method_index.rb +23 -4
  103. data/lib/rigor/inference/synthetic_method_scanner.rb +148 -14
  104. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  105. data/lib/rigor/language_server/completion_provider.rb +4 -4
  106. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  107. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  108. data/lib/rigor/language_server/hover_provider.rb +4 -4
  109. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  110. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  111. data/lib/rigor/plugin/additional_initializer.rb +108 -0
  112. data/lib/rigor/plugin/base.rb +337 -2
  113. data/lib/rigor/plugin/box.rb +64 -0
  114. data/lib/rigor/plugin/inflector.rb +121 -0
  115. data/lib/rigor/plugin/isolation.rb +191 -0
  116. data/lib/rigor/plugin/macro/nested_class_template.rb +140 -0
  117. data/lib/rigor/plugin/macro.rb +1 -0
  118. data/lib/rigor/plugin/manifest.rb +120 -23
  119. data/lib/rigor/plugin/node_context.rb +62 -0
  120. data/lib/rigor/plugin/registry.rb +49 -1
  121. data/lib/rigor/plugin.rb +3 -0
  122. data/lib/rigor/rbs_extended/conformance_checker.rb +208 -0
  123. data/lib/rigor/rbs_extended.rb +39 -0
  124. data/lib/rigor/scope.rb +123 -9
  125. data/lib/rigor/sig_gen/generator.rb +2 -3
  126. data/lib/rigor/sig_gen/observation_collector.rb +2 -2
  127. data/lib/rigor/source/literals.rb +118 -0
  128. data/lib/rigor/source/node_walker.rb +26 -0
  129. data/lib/rigor/source.rb +1 -0
  130. data/lib/rigor/type/acceptance_router.rb +19 -0
  131. data/lib/rigor/type/accepts_result.rb +3 -10
  132. data/lib/rigor/type/app.rb +3 -7
  133. data/lib/rigor/type/bot.rb +2 -3
  134. data/lib/rigor/type/bound_method.rb +5 -12
  135. data/lib/rigor/type/combinator.rb +23 -1
  136. data/lib/rigor/type/constant.rb +2 -3
  137. data/lib/rigor/type/data_class.rb +80 -0
  138. data/lib/rigor/type/data_instance.rb +100 -0
  139. data/lib/rigor/type/difference.rb +5 -10
  140. data/lib/rigor/type/dynamic.rb +5 -10
  141. data/lib/rigor/type/hash_shape.rb +5 -15
  142. data/lib/rigor/type/integer_range.rb +5 -10
  143. data/lib/rigor/type/intersection.rb +5 -10
  144. data/lib/rigor/type/nominal.rb +5 -10
  145. data/lib/rigor/type/refined.rb +5 -10
  146. data/lib/rigor/type/singleton.rb +5 -10
  147. data/lib/rigor/type/top.rb +2 -3
  148. data/lib/rigor/type/tuple.rb +5 -10
  149. data/lib/rigor/type/union.rb +69 -10
  150. data/lib/rigor/type.rb +2 -0
  151. data/lib/rigor/value_semantics.rb +77 -0
  152. data/lib/rigor/version.rb +1 -1
  153. data/lib/rigor.rb +2 -0
  154. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +31 -53
  155. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +21 -23
  156. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +38 -59
  157. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +7 -13
  158. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +22 -33
  159. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +298 -413
  160. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +69 -71
  161. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +24 -34
  162. data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +18 -16
  163. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +4 -46
  164. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
  165. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +1 -1
  166. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +17 -12
  167. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -8
  168. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +2 -7
  169. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +2 -6
  170. data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +4 -3
  171. data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
  172. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +40 -45
  173. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +7 -17
  174. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +20 -42
  175. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +7 -4
  176. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +4 -8
  177. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +188 -0
  178. data/plugins/rigor-mangrove/lib/rigor-mangrove.rb +3 -0
  179. data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +4 -0
  180. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +24 -8
  181. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +31 -48
  182. data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +21 -23
  183. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +54 -82
  184. data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +25 -25
  185. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +63 -147
  186. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -17
  187. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +23 -114
  188. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +48 -33
  189. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
  190. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +6 -3
  191. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +4 -2
  192. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +13 -12
  193. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +28 -40
  194. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +44 -47
  195. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +11 -10
  196. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +45 -87
  197. data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +11 -12
  198. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +29 -42
  199. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +20 -19
  200. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +73 -0
  201. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +43 -1
  202. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +21 -29
  203. data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +36 -96
  204. data/sig/rigor/cache.rbs +19 -0
  205. data/sig/rigor/inference.rbs +22 -0
  206. data/sig/rigor/plugin/access_denied_error.rbs +3 -1
  207. data/sig/rigor/plugin/base.rbs +58 -3
  208. data/sig/rigor/plugin/io_boundary.rbs +3 -0
  209. data/sig/rigor/plugin/manifest.rbs +31 -1
  210. data/sig/rigor/rbs_extended.rbs +2 -0
  211. data/sig/rigor/scope.rbs +5 -0
  212. data/sig/rigor/source.rbs +12 -0
  213. data/sig/rigor/type.rbs +58 -1
  214. data/sig/rigor.rbs +11 -1
  215. data/skills/rigor-plugin-author/SKILL.md +13 -9
  216. data/skills/rigor-plugin-author/references/01-plan-and-scaffold.md +6 -5
  217. data/skills/rigor-plugin-author/references/02-walker-and-types.md +159 -75
  218. data/skills/rigor-plugin-author/references/03-test-and-ship.md +3 -3
  219. metadata +73 -2
  220. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +0 -114
@@ -9,6 +9,7 @@ require_relative "../plugin/services"
9
9
  require_relative "../reflection"
10
10
  require_relative "../type/combinator"
11
11
  require_relative "plugins_renderer"
12
+ require_relative "command"
12
13
 
13
14
  module Rigor
14
15
  class CLI
@@ -32,7 +33,15 @@ module Rigor
32
33
  # `trait_registries:` / `external_files:` /
33
34
  # `type_node_resolvers:` / `hkt_registrations:` /
34
35
  # `hkt_definitions:` / `protocol_contracts:` /
35
- # `source_rbs_synthesizer:`).
36
+ # `source_rbs_synthesizer:`);
37
+ # - the ADR-37 narrow extension protocols read off the plugin
38
+ # class — `node_rule` node types, `dynamic_return` receivers,
39
+ # `type_specifier` methods.
40
+ #
41
+ # `--capabilities` switches to a focused catalogue of just the
42
+ # narrow-protocol gate values + produced/consumed facts (ADR-37
43
+ # § "Machine-readable capability catalogue") — the AI-legibility
44
+ # surface that lets an agent enumerate what every plugin does.
36
45
  #
37
46
  # Output formats: `text` (default, human-readable table) and
38
47
  # `json` (for tooling — SKILLs, CI gates, editor integrations).
@@ -52,15 +61,9 @@ module Rigor
52
61
  # the RBS environment without conflict (requires constructing
53
62
  # the Environment, which is heavier than the loader-only
54
63
  # pass this slice does).
55
- class PluginsCommand
64
+ class PluginsCommand < Command # rubocop:disable Metrics/ClassLength
56
65
  USAGE = "Usage: rigor plugins [options]"
57
66
 
58
- def initialize(argv:, out: $stdout, err: $stderr)
59
- @argv = argv
60
- @out = out
61
- @err = err
62
- end
63
-
64
67
  # @return [Integer] CLI exit status.
65
68
  def run
66
69
  options = parse_options
@@ -69,7 +72,7 @@ module Rigor
69
72
  rows = build_rows(configuration)
70
73
 
71
74
  renderer = PluginsRenderer.new(rows: rows, configuration_path: config_path)
72
- @out.puts(options.fetch(:format) == "json" ? renderer.json : renderer.text)
75
+ @out.puts(render(renderer, options))
73
76
 
74
77
  any_load_errors = rows.any? { |row| row.fetch(:status) == :load_error }
75
78
  return 1 if any_load_errors && options.fetch(:strict)
@@ -79,13 +82,32 @@ module Rigor
79
82
 
80
83
  private
81
84
 
85
+ # Picks the renderer view. `--capabilities` switches to the
86
+ # focused extension-protocol catalogue (ADR-37 § "Machine-readable
87
+ # capability catalogue") — per plugin, only the gate values that
88
+ # tell a reader (or an AI agent) exactly what the plugin
89
+ # contributes: the node-rule node types, the dynamic-return
90
+ # receivers, the type-specifier methods, and the produced /
91
+ # consumed facts. The default view stays the full activation report.
92
+ def render(renderer, options)
93
+ json = options.fetch(:format) == "json"
94
+ if options.fetch(:capabilities)
95
+ json ? renderer.capabilities_json : renderer.capabilities_text
96
+ else
97
+ json ? renderer.json : renderer.text
98
+ end
99
+ end
100
+
82
101
  def parse_options
83
- options = { config: nil, format: "text", strict: false }
102
+ options = { config: nil, format: "text", strict: false, capabilities: false }
84
103
  OptionParser.new do |opts|
85
104
  opts.banner = USAGE
86
105
  opts.on("--config=PATH", "Path to the Rigor configuration file") { |v| options[:config] = v }
87
106
  opts.on("--format=FORMAT", "Output format: text (default) or json") { |v| options[:format] = v }
88
107
  opts.on("--strict", "Exit 1 if any plugin failed to load (CI gate)") { options[:strict] = true }
108
+ opts.on("--capabilities", "Emit the per-plugin extension-protocol catalogue (ADR-37)") do
109
+ options[:capabilities] = true
110
+ end
89
111
  end.parse!(@argv)
90
112
  validate!(options)
91
113
  options
@@ -165,9 +187,25 @@ module Rigor
165
187
  manifest = plugin.manifest
166
188
  identity_fields(gem_name, manifest, config)
167
189
  .merge(extension_fields(plugin, manifest))
190
+ .merge(narrow_protocol_fields(plugin))
168
191
  .merge(load_error: nil)
169
192
  end
170
193
 
194
+ # ADR-37 narrow extension protocols. Unlike the 10 declarative
195
+ # manifest fields, these are class-level DSLs (`node_rule` /
196
+ # `dynamic_return` / `type_specifier`), so they are read off the
197
+ # plugin class rather than the manifest. The gate values — node
198
+ # types, receiver class names, specified method names — are the
199
+ # greppable, enumerable surface the capability catalogue exposes.
200
+ def narrow_protocol_fields(plugin)
201
+ klass = plugin.class
202
+ {
203
+ node_rule_types: klass.node_rules.map { |r| r[:node_type].name }.uniq,
204
+ dynamic_return_receivers: klass.dynamic_returns.flat_map { |r| r[:receivers] }.uniq,
205
+ type_specifier_methods: klass.type_specifiers.flat_map { |r| r[:methods] }.map(&:to_s).uniq
206
+ }
207
+ end
208
+
171
209
  def identity_fields(gem_name, manifest, config)
172
210
  {
173
211
  gem: gem_name,
@@ -225,6 +263,9 @@ module Rigor
225
263
  hkt_definitions: 0,
226
264
  protocol_contracts: 0,
227
265
  source_rbs_synthesizer: false,
266
+ node_rule_types: [],
267
+ dynamic_return_receivers: [],
268
+ type_specifier_methods: [],
228
269
  load_error: error&.message || "plugin did not register or could not be matched to a registered class"
229
270
  }
230
271
  end
@@ -299,6 +340,7 @@ module Rigor
299
340
  external_files: 0, type_node_resolvers: 0,
300
341
  hkt_registrations: 0, hkt_definitions: 0,
301
342
  protocol_contracts: 0, source_rbs_synthesizer: false,
343
+ node_rule_types: [], dynamic_return_receivers: [], type_specifier_methods: [],
302
344
  load_error: error.message
303
345
  }
304
346
  end
@@ -13,7 +13,7 @@ module Rigor
13
13
  # tooling (SKILLs, CI, editor integrations) while text is
14
14
  # for interactive inspection. Rows are printed in the order
15
15
  # the loader resolved them.
16
- class PluginsRenderer
16
+ class PluginsRenderer # rubocop:disable Metrics/ClassLength
17
17
  def initialize(rows:, configuration_path:)
18
18
  @rows = rows
19
19
  @configuration_path = configuration_path
@@ -42,8 +42,74 @@ module Rigor
42
42
  )
43
43
  end
44
44
 
45
+ # ADR-37 § "Machine-readable capability catalogue" — the focused
46
+ # per-plugin extension-protocol dump. Only loaded plugins appear
47
+ # (a plugin that failed to load contributes no capabilities), and
48
+ # each carries only the gate values an agent enumerates to learn
49
+ # what the plugin does: node-rule node types, dynamic-return
50
+ # receivers, type-specifier methods, and produced / consumed facts.
51
+ def capabilities_json
52
+ JSON.pretty_generate(
53
+ {
54
+ "configuration" => @configuration_path,
55
+ "capabilities" => loaded_rows.map { |row| capabilities_json_for(row) }
56
+ }
57
+ )
58
+ end
59
+
60
+ def capabilities_text
61
+ lines = ["Plugin capability catalogue (ADR-37 narrow extension protocols)", ""]
62
+ loaded = loaded_rows
63
+ if loaded.empty?
64
+ lines << " (no plugins loaded)"
65
+ else
66
+ loaded.each_with_index do |row, index|
67
+ lines.concat(capability_lines(row))
68
+ lines << "" unless index == loaded.size - 1
69
+ end
70
+ end
71
+ lines.join("\n")
72
+ end
73
+
45
74
  private
46
75
 
76
+ def loaded_rows
77
+ @rows.select { |r| r[:status] == :loaded }
78
+ end
79
+
80
+ def capability_lines(row)
81
+ lines = [" #{row[:id]} v#{row[:version]} (#{row[:gem]})"]
82
+ capability_surfaces(row).each { |surface| lines << " #{surface}" }
83
+ lines << " (no narrow extension protocols declared)" if lines.size == 1
84
+ lines
85
+ end
86
+
87
+ # The non-empty capability surfaces for a plugin, each as a
88
+ # `label: a, b, c` string. Data-driven so the catalogue stays a
89
+ # single source of truth shared between the text and JSON views.
90
+ def capability_surfaces(row)
91
+ [
92
+ ["node_rule", row[:node_rule_types]],
93
+ ["dynamic_return receivers", row[:dynamic_return_receivers]],
94
+ ["type_specifier methods", row[:type_specifier_methods]],
95
+ ["produces", row[:produces]],
96
+ ["consumes", row[:consumes]]
97
+ ].filter_map { |label, values| "#{label}: #{values.join(', ')}" if values.any? }
98
+ end
99
+
100
+ def capabilities_json_for(row)
101
+ {
102
+ "id" => row[:id],
103
+ "gem" => row[:gem],
104
+ "version" => row[:version],
105
+ "node_rule_types" => row[:node_rule_types],
106
+ "dynamic_return_receivers" => row[:dynamic_return_receivers],
107
+ "type_specifier_methods" => row[:type_specifier_methods],
108
+ "produces" => row[:produces],
109
+ "consumes" => row[:consumes]
110
+ }
111
+ end
112
+
47
113
  def header
48
114
  loaded = @rows.count { |r| r[:status] == :loaded }
49
115
  errored = @rows.count { |r| r[:status] == :load_error }
@@ -99,6 +165,22 @@ module Rigor
99
165
  lines << " owns_receivers: #{row[:owns_receivers].join(', ')}" if row[:owns_receivers].any?
100
166
  lines << " produces: #{row[:produces].join(', ')}" if row[:produces].any?
101
167
  lines << " consumes: #{row[:consumes].join(', ')}" if row[:consumes].any?
168
+ lines.concat(narrow_protocol_lines(row))
169
+ lines
170
+ end
171
+
172
+ # ADR-37 narrow extension protocols (node_rule / dynamic_return /
173
+ # type_specifier). Surfaced in the full report alongside the
174
+ # declarative surfaces; `--capabilities` is the focused view.
175
+ def narrow_protocol_lines(row)
176
+ lines = []
177
+ lines << " node_rule: #{row[:node_rule_types].join(', ')}" if row[:node_rule_types].any?
178
+ if row[:dynamic_return_receivers].any?
179
+ lines << " dynamic_return receivers: #{row[:dynamic_return_receivers].join(', ')}"
180
+ end
181
+ if row[:type_specifier_methods].any?
182
+ lines << " type_specifier methods: #{row[:type_specifier_methods].join(', ')}"
183
+ end
102
184
  lines
103
185
  end
104
186
 
@@ -157,6 +239,9 @@ module Rigor
157
239
  "hkt_definitions" => row[:hkt_definitions],
158
240
  "protocol_contracts" => row[:protocol_contracts],
159
241
  "source_rbs_synthesizer" => row[:source_rbs_synthesizer],
242
+ "node_rule_types" => row[:node_rule_types],
243
+ "dynamic_return_receivers" => row[:dynamic_return_receivers],
244
+ "type_specifier_methods" => row[:type_specifier_methods],
160
245
  "load_error" => row[:load_error]
161
246
  }
162
247
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optionparser"
4
+
5
+ module Rigor
6
+ class CLI
7
+ # Output-format dispatch shared by the `--format text|json` renderers.
8
+ #
9
+ # Each renderer included this and then implemented `render_text` /
10
+ # `render_json`; the `render(data, format:)` entry point — route by
11
+ # the format string, raise one consistent `OptionParser::InvalidArgument`
12
+ # on anything else — was copied verbatim into every one. Centralising
13
+ # it keeps the unsupported-format wording and the text/json contract
14
+ # in a single place as new renderers and formats are added.
15
+ module Renderable
16
+ def render(data, format:)
17
+ case format
18
+ when "text" then render_text(data)
19
+ when "json" then render_json(data)
20
+ else
21
+ raise OptionParser::InvalidArgument, "unsupported format: #{format}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -4,6 +4,7 @@ require "optionparser"
4
4
 
5
5
  require_relative "../configuration"
6
6
  require_relative "../sig_gen"
7
+ require_relative "command"
7
8
 
8
9
  module Rigor
9
10
  class CLI
@@ -34,19 +35,13 @@ module Rigor
34
35
  # `--params=observed-strict` stays reserved-but-inert until
35
36
  # the capability-role catalog ships (rejected with a usage
36
37
  # error so the surface stays stable).
37
- class SigGenCommand
38
+ class SigGenCommand < Command
38
39
  USAGE = "Usage: rigor sig-gen [options] [paths]"
39
40
 
40
41
  VALID_MODES = %w[print diff write].freeze
41
42
  VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
42
43
  VALID_FORMATS = %w[text json].freeze
43
44
 
44
- def initialize(argv:, out:, err:)
45
- @argv = argv
46
- @out = out
47
- @err = err
48
- end
49
-
50
45
  # @return [Integer] CLI exit status.
51
46
  def run
52
47
  options = parse_options
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "command"
4
+
3
5
  require "optparse"
4
6
 
5
7
  module Rigor
@@ -29,7 +31,7 @@ module Rigor
29
31
  # as input to a Read tool.
30
32
  #
31
33
  # `rigor skill` with no subcommand is an alias for `list`.
32
- class SkillCommand
34
+ class SkillCommand < Command
33
35
  USAGE = <<~USAGE
34
36
  Usage: rigor skill <subcommand> [args]
35
37
 
@@ -48,12 +50,6 @@ module Rigor
48
50
  # `lib/rigor/cli/skill_command.rb` that is three directories up.
49
51
  SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
50
52
 
51
- def initialize(argv:, out: $stdout, err: $stderr)
52
- @argv = argv
53
- @out = out
54
- @err = err
55
- end
56
-
57
53
  # @return [Integer] CLI exit status.
58
54
  def run
59
55
  subcommand = @argv.shift || "list"
@@ -7,6 +7,7 @@ require_relative "../analysis/runner"
7
7
  require_relative "../cache/store"
8
8
  require_relative "../triage"
9
9
  require_relative "triage_renderer"
10
+ require_relative "command"
10
11
 
11
12
  module Rigor
12
13
  class CLI
@@ -18,16 +19,10 @@ module Rigor
18
19
  # Read-only and advisory (WD4): never edits config, never
19
20
  # writes a baseline. Always exits 0 — it is an inspection
20
21
  # command, not a gate (`rigor check` remains the gate).
21
- class TriageCommand
22
+ class TriageCommand < Command
22
23
  USAGE = "Usage: rigor triage [options] [paths]"
23
24
  DEFAULT_SECTIONS = %i[distribution hotspots hints].freeze
24
25
 
25
- def initialize(argv:, out:, err:)
26
- @argv = argv
27
- @out = out
28
- @err = err
29
- end
30
-
31
26
  # @return [Integer] CLI exit status (always 0).
32
27
  def run
33
28
  options = parse_options
@@ -11,6 +11,8 @@ require_relative "../source/node_locator"
11
11
  require_relative "../inference/fallback_tracer"
12
12
  require_relative "../inference/scope_indexer"
13
13
  require_relative "type_of_renderer"
14
+ require_relative "command"
15
+ require_relative "options"
14
16
 
15
17
  module Rigor
16
18
  class CLI
@@ -25,21 +27,15 @@ module Rigor
25
27
  # dispatching and lets us evolve the type-of UX (extra flags, watch mode,
26
28
  # streaming output) without bloating the CLI shell. Output formatting is
27
29
  # delegated to {TypeOfRenderer}.
28
- class TypeOfCommand
30
+ class TypeOfCommand < Command
29
31
  USAGE = "Usage: rigor type-of [options] FILE:LINE:COL"
30
32
 
31
33
  Result = Data.define(:file, :line, :column, :node, :type, :tracer)
32
34
 
33
- def initialize(argv:, out:, err:)
34
- @argv = argv
35
- @out = out
36
- @err = err
37
- end
38
-
39
35
  # @return [Integer] CLI exit status.
40
36
  def run
41
37
  options = parse_options
42
- buffer = resolve_buffer_binding(options)
38
+ buffer = Options.resolve_buffer_binding(options, err: @err)
43
39
  return CLI::EXIT_USAGE if buffer == :usage_error
44
40
 
45
41
  target = parse_position_argument(@argv)
@@ -58,42 +54,13 @@ module Rigor
58
54
  opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
59
55
  opts.on("--trace", "Record fail-soft fallbacks via FallbackTracer") { options[:trace] = true }
60
56
  opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
61
- opts.on("--tmp-file=PATH",
62
- "Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
63
- options[:tmp_file] = value
64
- end
65
- opts.on("--instead-of=PATH",
66
- "Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
67
- options[:instead_of] = value
68
- end
57
+ Options.add_editor_mode(opts, options)
69
58
  end
70
59
  parser.parse!(@argv)
71
60
 
72
61
  options
73
62
  end
74
63
 
75
- # Mirrors `Rigor::CLI#resolve_buffer_binding` (the `check`
76
- # path). Returns nil / BufferBinding / :usage_error. The
77
- # symbol return path lets the caller translate to
78
- # `CLI::EXIT_USAGE` without raising.
79
- def resolve_buffer_binding(options)
80
- tmp = options[:tmp_file]
81
- instead = options[:instead_of]
82
- return nil if tmp.nil? && instead.nil?
83
-
84
- if tmp.nil? || instead.nil?
85
- @err.puts("--tmp-file and --instead-of must appear together")
86
- return :usage_error
87
- end
88
-
89
- unless File.file?(tmp)
90
- @err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
91
- return :usage_error
92
- end
93
-
94
- Rigor::Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
95
- end
96
-
97
64
  def execute(target:, options:, buffer: nil)
98
65
  file, line, column = target
99
66
  # Under editor mode the logical `file` may not exist on disk
@@ -3,6 +3,8 @@
3
3
  require "json"
4
4
  require "optionparser"
5
5
 
6
+ require_relative "renderable"
7
+
6
8
  module Rigor
7
9
  class CLI
8
10
  # Renders a `TypeOfCommand::Result` as either human-readable text or a
@@ -12,19 +14,12 @@ module Rigor
12
14
  # output formats (sexp, lsp-style hover payloads, color decoration) can
13
15
  # plug in without disturbing argument parsing or the inference call site.
14
16
  class TypeOfRenderer
17
+ include Renderable
18
+
15
19
  def initialize(out:)
16
20
  @out = out
17
21
  end
18
22
 
19
- def render(result, format:)
20
- case format
21
- when "text" then render_text(result)
22
- when "json" then render_json(result)
23
- else
24
- raise OptionParser::InvalidArgument, "unsupported format: #{format}"
25
- end
26
- end
27
-
28
23
  private
29
24
 
30
25
  def render_text(result)
@@ -9,6 +9,7 @@ require_relative "../inference/coverage_scanner"
9
9
  require_relative "../scope"
10
10
  require_relative "type_scan_renderer"
11
11
  require_relative "type_scan_report"
12
+ require_relative "command"
12
13
 
13
14
  module Rigor
14
15
  class CLI
@@ -19,21 +20,15 @@ module Rigor
19
20
  # the inference engine's directly recognized classes. It is the project's
20
21
  # primary CI gate for tracking how much of an input source the engine can
21
22
  # name without falling back to `Dynamic[Top]`.
22
- class TypeScanCommand
23
+ class TypeScanCommand < Command
23
24
  USAGE = "Usage: rigor type-scan [options] PATH..."
24
25
 
25
26
  LocatedEvent = Data.define(:file, :event)
26
27
 
27
- def initialize(argv:, out:, err:)
28
- @argv = argv
29
- @out = out
30
- @err = err
31
- end
32
-
33
28
  # @return [Integer] CLI exit status.
34
29
  def run
35
30
  options = parse_options
36
- paths = collect_paths(@argv)
31
+ paths = collect_paths(@argv, command_name: "type-scan")
37
32
  return CLI::EXIT_USAGE if paths.nil?
38
33
  return usage_error if paths.empty?
39
34
 
@@ -67,21 +62,6 @@ module Rigor
67
62
  options
68
63
  end
69
64
 
70
- def collect_paths(args)
71
- paths = []
72
- args.each do |arg|
73
- if File.directory?(arg)
74
- paths.concat(Dir.glob(File.join(arg, "**/*.rb")))
75
- elsif File.file?(arg)
76
- paths << arg
77
- else
78
- @err.puts("type-scan: not a file or directory: #{arg}")
79
- return nil
80
- end
81
- end
82
- paths.uniq
83
- end
84
-
85
65
  def usage_error
86
66
  @err.puts("type-scan: at least one path is required")
87
67
  @err.puts(USAGE)
@@ -3,6 +3,8 @@
3
3
  require "json"
4
4
  require "optionparser"
5
5
 
6
+ require_relative "renderable"
7
+
6
8
  module Rigor
7
9
  class CLI
8
10
  # Renders a `TypeScanCommand::Report` as either a terminal-friendly text
@@ -11,19 +13,12 @@ module Rigor
11
13
  # the two formats stay in lockstep; that pairing is why this class is a
12
14
  # bit longer than the default class-length budget.
13
15
  class TypeScanRenderer
16
+ include Renderable
17
+
14
18
  def initialize(out:)
15
19
  @out = out
16
20
  end
17
21
 
18
- def render(report, format:)
19
- case format
20
- when "text" then render_text(report)
21
- when "json" then render_json(report)
22
- else
23
- raise OptionParser::InvalidArgument, "unsupported format: #{format}"
24
- end
25
- end
26
-
27
22
  private
28
23
 
29
24
  def render_text(report)