rigortype 0.1.9 → 0.1.11
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/rigor/analysis/baseline.rb +51 -15
- data/lib/rigor/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +57 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli.rb +88 -5
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +159 -4
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +143 -12
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +115 -7
- data/lib/rigor/inference/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +26 -2
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +28 -5
- data/lib/rigor/plugin/manifest.rb +33 -5
- data/lib/rigor/plugin/registry.rb +21 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +190 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +189 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +81 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +142 -0
- data/plugins/rigor-actioncable/lib/rigor-actioncable.rb +3 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +178 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +310 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +76 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +177 -0
- data/plugins/rigor-actionmailer/lib/rigor-actionmailer.rb +3 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +589 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +150 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +123 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +247 -0
- data/plugins/rigor-actionpack/lib/rigor-actionpack.rb +3 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +114 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +177 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +65 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +117 -0
- data/plugins/rigor-activejob/lib/rigor-activejob.rb +3 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +273 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/inflector.rb +114 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +561 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +194 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +240 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +94 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +514 -0
- data/plugins/rigor-activerecord/lib/rigor-activerecord.rb +8 -0
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +182 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +78 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +162 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +43 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +170 -0
- data/plugins/rigor-activestorage/lib/rigor-activestorage.rb +8 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +34 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor-activesupport-core-ext.rb +20 -0
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +463 -0
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +108 -0
- data/plugins/rigor-devise/lib/rigor-devise.rb +8 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +285 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +124 -0
- data/plugins/rigor-dry-schema/lib/rigor-dry-schema.rb +8 -0
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +116 -0
- data/plugins/rigor-dry-struct/lib/rigor-dry-struct.rb +8 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types/alias_scanner.rb +341 -0
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +120 -0
- data/plugins/rigor-dry-types/lib/rigor-dry-types.rb +8 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +120 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +85 -0
- data/plugins/rigor-dry-validation/lib/rigor-dry-validation.rb +7 -0
- data/plugins/rigor-dry-validation/sig/dry_validation.rbs +25 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +177 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +242 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +56 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +174 -0
- data/plugins/rigor-factorybot/lib/rigor-factorybot.rb +3 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +409 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +114 -0
- data/plugins/rigor-graphql/lib/rigor-graphql.rb +8 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +124 -0
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami.rb +111 -0
- data/plugins/rigor-hanami/lib/rigor-hanami.rb +3 -0
- data/plugins/rigor-hanami/sig/hanami_action.rbs +78 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +302 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +72 -0
- data/plugins/rigor-minitest/lib/rigor-minitest.rb +3 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +194 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +140 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_index.rb +65 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +130 -0
- data/plugins/rigor-pundit/lib/rigor-pundit.rb +3 -0
- data/plugins/rigor-rails/lib/rigor-rails.rb +31 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +277 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +108 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +138 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +167 -0
- data/plugins/rigor-rails-i18n/lib/rigor-rails-i18n.rb +3 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +161 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +103 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +490 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +158 -0
- data/plugins/rigor-rails-routes/lib/rigor-rails-routes.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +163 -0
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +24 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/analyzer.rb +110 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +200 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +170 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +233 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +190 -0
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +188 -0
- data/plugins/rigor-rspec/lib/rigor-rspec.rb +3 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/have_http_status_analyzer.rb +128 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +60 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails.rb +75 -0
- data/plugins/rigor-rspec-rails/lib/rigor-rspec-rails.rb +3 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +266 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +113 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor-shoulda-matchers.rb +3 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +152 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +190 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +61 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +124 -0
- data/plugins/rigor-sidekiq/lib/rigor-sidekiq.rb +3 -0
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +85 -0
- data/plugins/rigor-sinatra/lib/rigor-sinatra.rb +8 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +108 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +250 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +95 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +226 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +28 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +154 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +100 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +323 -0
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +660 -0
- data/plugins/rigor-sorbet/lib/rigor-sorbet.rb +3 -0
- data/plugins/rigor-statesman/lib/rigor/plugin/statesman.rb +209 -0
- data/plugins/rigor-statesman/lib/rigor-statesman.rb +8 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor/plugin/typescript_utility_types.rb +163 -0
- data/plugins/rigor-typescript-utility-types/lib/rigor-typescript-utility-types.rb +9 -0
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +180 -1
|
@@ -217,6 +217,16 @@ module Rigor
|
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
def dispatch_nominal_size(nominal, method_name, args)
|
|
220
|
+
if nominal.class_name == "String" && args.size == 1
|
|
221
|
+
string_binary = dispatch_string_binary_from_arg(method_name, args.first)
|
|
222
|
+
return string_binary if string_binary
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if nominal.class_name == "Integer" && args.size == 1
|
|
226
|
+
integer_binary = dispatch_integer_binary_from_arg(method_name, args.first)
|
|
227
|
+
return integer_binary if integer_binary
|
|
228
|
+
end
|
|
229
|
+
|
|
220
230
|
return nil unless args.empty?
|
|
221
231
|
|
|
222
232
|
selectors = SIZE_RETURNING_NOMINALS[nominal.class_name]
|
|
@@ -225,6 +235,38 @@ module Rigor
|
|
|
225
235
|
Type::Combinator.non_negative_int
|
|
226
236
|
end
|
|
227
237
|
|
|
238
|
+
# Arg-type-driven String binary projections for any String-typed
|
|
239
|
+
# receiver (including Nominal, Refined, and Difference fallbacks).
|
|
240
|
+
# Called before the no-arg size guard so binary operators are seen.
|
|
241
|
+
#
|
|
242
|
+
# - `String + non-empty-string` → `non-empty-string`
|
|
243
|
+
# (arg guarantees the concatenation is non-empty)
|
|
244
|
+
# - `String * Constant[0]` → `Constant[""]`
|
|
245
|
+
# (every string repeated 0 times is the empty string)
|
|
246
|
+
def dispatch_string_binary_from_arg(method_name, arg)
|
|
247
|
+
case method_name
|
|
248
|
+
when :+
|
|
249
|
+
return Type::Combinator.non_empty_string if Type::Combinator.non_empty_string_compatible?(arg)
|
|
250
|
+
when :*
|
|
251
|
+
if arg.is_a?(Type::Constant) && arg.value.is_a?(Integer) && arg.value.zero?
|
|
252
|
+
return Type::Combinator.constant_of("")
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Arg-type-driven Integer binary projections for any Integer-typed
|
|
259
|
+
# receiver (including Nominal, Refined, and Difference fallbacks).
|
|
260
|
+
#
|
|
261
|
+
# - `Integer * Constant[0]` → `Constant[0]`
|
|
262
|
+
# (any integer multiplied by 0 is 0)
|
|
263
|
+
def dispatch_integer_binary_from_arg(method_name, arg)
|
|
264
|
+
return nil unless method_name == :*
|
|
265
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer) && arg.value.zero?
|
|
266
|
+
|
|
267
|
+
Type::Combinator.constant_of(0)
|
|
268
|
+
end
|
|
269
|
+
|
|
228
270
|
# `IntegerRange#to_s` precision (v0.1.1 Track 1 slice 5b).
|
|
229
271
|
# When the range's lower bound is `>= 0`, every member is
|
|
230
272
|
# a non-negative integer and `to_s(base)` returns a
|
|
@@ -281,7 +323,7 @@ module Rigor
|
|
|
281
323
|
return nil unless base.is_a?(Type::Nominal)
|
|
282
324
|
|
|
283
325
|
if removes_empty_witness?(difference)
|
|
284
|
-
precise = empty_removal_projection(
|
|
326
|
+
precise = empty_removal_projection(difference, method_name, args)
|
|
285
327
|
return precise if precise
|
|
286
328
|
end
|
|
287
329
|
|
|
@@ -305,14 +347,62 @@ module Rigor
|
|
|
305
347
|
!!(predicate && predicate.call(difference.removed))
|
|
306
348
|
end
|
|
307
349
|
|
|
308
|
-
|
|
309
|
-
|
|
350
|
+
# Methods on a non-empty String that preserve non-emptiness
|
|
351
|
+
# (they transform characters but never reduce the string to "").
|
|
352
|
+
NON_EMPTY_STRING_PRESERVING_UNARY = Set[:upcase, :downcase, :capitalize, :swapcase, :reverse].freeze
|
|
353
|
+
# Methods on non-zero-int that return a non-zero-int (identity ops).
|
|
354
|
+
# Negation of a non-zero integer is non-zero; `to_i`/`to_int` are
|
|
355
|
+
# identity operations on Integer.
|
|
356
|
+
NON_ZERO_INT_PRESERVING_UNARY = Set[:-@, :+@, :to_i, :to_int].freeze
|
|
357
|
+
private_constant :NON_EMPTY_STRING_PRESERVING_UNARY, :NON_ZERO_INT_PRESERVING_UNARY
|
|
310
358
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
359
|
+
def empty_removal_projection(difference, method_name, args)
|
|
360
|
+
base = difference.base
|
|
361
|
+
return empty_removal_unary(difference, base, method_name) if args.empty?
|
|
362
|
+
|
|
363
|
+
empty_removal_binary(difference, base, method_name, args)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def empty_removal_unary(difference, base, method_name)
|
|
367
|
+
return size_returning_for_empty_removal(base, method_name) if
|
|
368
|
+
%i[size length count bytesize].include?(method_name)
|
|
369
|
+
|
|
370
|
+
predicate_result = empty_predicate_projection(base, method_name)
|
|
371
|
+
return predicate_result if predicate_result
|
|
372
|
+
|
|
373
|
+
return difference if base.class_name == "String" &&
|
|
374
|
+
NON_EMPTY_STRING_PRESERVING_UNARY.include?(method_name)
|
|
314
375
|
|
|
315
|
-
|
|
376
|
+
non_zero_int_unary_projection(difference, base, method_name)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def non_zero_int_unary_projection(difference, base, method_name)
|
|
380
|
+
return nil unless base.class_name == "Integer"
|
|
381
|
+
return Type::Combinator.positive_int if %i[abs magnitude].include?(method_name)
|
|
382
|
+
return difference if NON_ZERO_INT_PRESERVING_UNARY.include?(method_name)
|
|
383
|
+
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def empty_removal_binary(difference, base, method_name, args)
|
|
388
|
+
return empty_string_binary(difference, method_name, args) if base.class_name == "String"
|
|
389
|
+
return empty_integer_binary(difference, method_name, args) if base.class_name == "Integer"
|
|
390
|
+
|
|
391
|
+
nil
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def empty_string_binary(difference, method_name, args)
|
|
395
|
+
return difference if method_name == :+ && args.size == 1
|
|
396
|
+
return non_empty_string_repeat(difference, args.first) if method_name == :* && args.size == 1
|
|
397
|
+
|
|
398
|
+
nil
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def empty_integer_binary(difference, method_name, args)
|
|
402
|
+
return nil unless method_name == :* && args.size == 1
|
|
403
|
+
return nil unless Type::Combinator.non_zero_int_compatible?(args.first)
|
|
404
|
+
|
|
405
|
+
difference
|
|
316
406
|
end
|
|
317
407
|
|
|
318
408
|
def empty_predicate_projection(base, method_name)
|
|
@@ -324,6 +414,24 @@ module Rigor
|
|
|
324
414
|
end
|
|
325
415
|
end
|
|
326
416
|
|
|
417
|
+
# `non-empty-string * n` result:
|
|
418
|
+
# - `n == 0` → `Constant[""]` (any string repeated 0 times is empty)
|
|
419
|
+
# - `n >= 1` → `difference` (non-empty-string stays non-empty)
|
|
420
|
+
# - otherwise → nil (fall through, e.g. unknown n or non-negative-int)
|
|
421
|
+
def non_empty_string_repeat(difference, arg)
|
|
422
|
+
case arg
|
|
423
|
+
when Type::Constant
|
|
424
|
+
return nil unless arg.value.is_a?(Integer)
|
|
425
|
+
|
|
426
|
+
return Type::Combinator.constant_of("") if arg.value.zero?
|
|
427
|
+
return difference if arg.value.positive?
|
|
428
|
+
when Type::IntegerRange
|
|
429
|
+
return Type::Combinator.constant_of("") if arg.lower.zero? && arg.upper.zero?
|
|
430
|
+
return difference if arg.lower >= 1
|
|
431
|
+
end
|
|
432
|
+
nil
|
|
433
|
+
end
|
|
434
|
+
|
|
327
435
|
def size_returning_for_empty_removal(base, method_name)
|
|
328
436
|
return nil if base.class_name == "Integer" # Integer has no size method on Difference
|
|
329
437
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../source/node_walker"
|
|
4
|
+
require_relative "../scope"
|
|
5
|
+
require_relative "scope_indexer"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Measures the *type quality* of inferred expressions — not whether the
|
|
10
|
+
# engine recognises an AST node class (that is `CoverageScanner`'s job),
|
|
11
|
+
# but whether the type it produces carries useful static information.
|
|
12
|
+
#
|
|
13
|
+
# Each visited node is classified into one of eight precision tiers:
|
|
14
|
+
#
|
|
15
|
+
# :constant — Constant[T]: literal value known exactly
|
|
16
|
+
# :nominal — Nominal/Singleton: class identity known
|
|
17
|
+
# :shaped — Tuple/HashShape/IntegerRange/App: structure known
|
|
18
|
+
# :refined — Refined: narrowed by a predicate/assertion
|
|
19
|
+
# :bot — Bot: unreachable branch (definitively precise)
|
|
20
|
+
# :dynamic_specific — Dynamic[X] where X is not Top: origin partial
|
|
21
|
+
# :dynamic_top — Dynamic[Top]: completely opaque (the "untyped" hole)
|
|
22
|
+
# :top — Top: universal supertype (no information)
|
|
23
|
+
#
|
|
24
|
+
# The summary exposes `precision_ratio` (constant+nominal+shaped+refined+bot
|
|
25
|
+
# over total) and `opaque_ratio` (dynamic_top+top over total).
|
|
26
|
+
#
|
|
27
|
+
# For Union types the *worst* member tier is used, since the union is only
|
|
28
|
+
# as precise as its least-precise constituent. Intersection uses the *best*
|
|
29
|
+
# member (the most specific side wins). Difference follows its base type.
|
|
30
|
+
class PrecisionScanner
|
|
31
|
+
TIERS = %i[
|
|
32
|
+
constant nominal shaped refined bot
|
|
33
|
+
dynamic_specific dynamic_top top
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
TIER_RANK = TIERS.each_with_index.to_h.freeze
|
|
37
|
+
private_constant :TIER_RANK
|
|
38
|
+
|
|
39
|
+
PRECISE_TIERS = %i[constant nominal shaped refined bot].to_set.freeze
|
|
40
|
+
private_constant :PRECISE_TIERS
|
|
41
|
+
|
|
42
|
+
# Per-file result. Immutable value object.
|
|
43
|
+
class FileResult < Data.define(:total, :tier_counts)
|
|
44
|
+
def precise_count
|
|
45
|
+
PRECISE_TIERS.sum { |t| tier_counts.fetch(t, 0) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def dynamic_top_count
|
|
49
|
+
tier_counts.fetch(:dynamic_top, 0)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def dynamic_specific_count
|
|
53
|
+
tier_counts.fetch(:dynamic_specific, 0)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def dynamic_count
|
|
57
|
+
dynamic_top_count + dynamic_specific_count
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def opaque_count
|
|
61
|
+
tier_counts.fetch(:dynamic_top, 0) + tier_counts.fetch(:top, 0)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def precision_ratio
|
|
65
|
+
return 1.0 if total.zero?
|
|
66
|
+
|
|
67
|
+
precise_count.fdiv(total)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def opaque_ratio
|
|
71
|
+
return 0.0 if total.zero?
|
|
72
|
+
|
|
73
|
+
opaque_count.fdiv(total)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param scope [Rigor::Scope] base scope for type inference.
|
|
78
|
+
def initialize(scope: nil)
|
|
79
|
+
@scope = scope || Scope.empty
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @param root [Prism::Node] the parsed AST
|
|
83
|
+
# @return [FileResult]
|
|
84
|
+
def scan(root)
|
|
85
|
+
scope_index = ScopeIndexer.index(root, default_scope: @scope)
|
|
86
|
+
tier_counts = TIERS.to_h { |t| [t, 0] }
|
|
87
|
+
total = 0
|
|
88
|
+
|
|
89
|
+
Source::NodeWalker.each(root) do |node|
|
|
90
|
+
type = scope_index[node].type_of(node)
|
|
91
|
+
tier = classify(type)
|
|
92
|
+
tier_counts[tier] += 1
|
|
93
|
+
total += 1
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
FileResult.new(total: total, tier_counts: tier_counts)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def classify(type)
|
|
102
|
+
case type
|
|
103
|
+
when Type::Bot then :bot
|
|
104
|
+
when Type::Top then :top
|
|
105
|
+
when Type::Constant then :constant
|
|
106
|
+
when Type::Nominal, Type::Singleton then :nominal
|
|
107
|
+
when Type::Tuple, Type::HashShape,
|
|
108
|
+
Type::IntegerRange, Type::App then :shaped
|
|
109
|
+
when Type::Refined then :refined
|
|
110
|
+
when Type::Dynamic then classify_dynamic(type)
|
|
111
|
+
when Type::Union then worst_of(type.members)
|
|
112
|
+
when Type::Intersection then best_of(type.members)
|
|
113
|
+
when Type::Difference then classify(type.base)
|
|
114
|
+
else :dynamic_top
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def classify_dynamic(type)
|
|
119
|
+
type.static_facet.is_a?(Type::Top) ? :dynamic_top : :dynamic_specific
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def worst_of(members)
|
|
123
|
+
members.map { |m| classify(m) }.max_by { |t| TIER_RANK[t] } || :dynamic_top
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def best_of(members)
|
|
127
|
+
members.map { |m| classify(m) }.min_by { |t| TIER_RANK[t] } || :dynamic_top
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -347,7 +347,20 @@ module Rigor
|
|
|
347
347
|
# result type is precise (`Constant[:even]` instead of the
|
|
348
348
|
# joined `Constant[:even] | Constant[:odd]`).
|
|
349
349
|
live = live_branch_for_if(node, pred_type, post_pred)
|
|
350
|
-
|
|
350
|
+
if live
|
|
351
|
+
live_type, _live_scope = live
|
|
352
|
+
# When the provably-live then-branch terminates and there is no
|
|
353
|
+
# else, apply the same falsey-scope narrowing as the standard
|
|
354
|
+
# early-return path below. Without this, `return if @ivar.nil?`
|
|
355
|
+
# with an ivar seeded as Constant[nil] (making nil? = Constant[true]
|
|
356
|
+
# and the then-branch "provably live") propagates the un-narrowed
|
|
357
|
+
# nil scope past the guard instead of Bot.
|
|
358
|
+
if branch_terminates?(node.statements, live_type) && node.subsequent.nil?
|
|
359
|
+
_, falsey_scope = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
360
|
+
return [live_type, falsey_scope]
|
|
361
|
+
end
|
|
362
|
+
return live
|
|
363
|
+
end
|
|
351
364
|
|
|
352
365
|
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
353
366
|
then_type, then_scope = eval_branch_or_nil(node.statements, truthy_scope)
|
|
@@ -376,7 +389,18 @@ module Rigor
|
|
|
376
389
|
pred_type, post_pred = sub_eval(node.predicate, scope)
|
|
377
390
|
|
|
378
391
|
live = live_branch_for_unless(node, pred_type, post_pred)
|
|
379
|
-
|
|
392
|
+
if live
|
|
393
|
+
live_type, _live_scope = live
|
|
394
|
+
# Mirror of the eval_if fix: when the provably-live unless-body
|
|
395
|
+
# terminates and there is no else, apply the truthy-scope narrowing
|
|
396
|
+
# so `return unless @ivar` with a nil-seeded ivar doesn't propagate
|
|
397
|
+
# the nil scope past the guard.
|
|
398
|
+
if branch_terminates?(node.statements, live_type) && node.else_clause.nil?
|
|
399
|
+
truthy_scope, = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
400
|
+
return [live_type, truthy_scope]
|
|
401
|
+
end
|
|
402
|
+
return live
|
|
403
|
+
end
|
|
380
404
|
|
|
381
405
|
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, post_pred)
|
|
382
406
|
then_type, then_scope = eval_branch_or_nil(node.statements, falsey_scope)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module MCP
|
|
7
|
+
# Reads newline-delimited JSON-RPC 2.0 messages from `input`,
|
|
8
|
+
# dispatches each to `server`, and writes responses to `output`.
|
|
9
|
+
# Runs until input reaches EOF (the client closes the connection).
|
|
10
|
+
class Loop
|
|
11
|
+
def initialize(input:, output:, server:)
|
|
12
|
+
@input = input
|
|
13
|
+
@output = output
|
|
14
|
+
@server = server
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
@input.each_line do |raw|
|
|
19
|
+
line = raw.chomp
|
|
20
|
+
next if line.empty?
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
request = JSON.parse(line)
|
|
24
|
+
rescue JSON::ParserError => e
|
|
25
|
+
write_response({ "jsonrpc" => "2.0", "id" => nil,
|
|
26
|
+
"error" => { "code" => -32_700, "message" => "Parse error: #{e.message}" } })
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
response = @server.handle(request)
|
|
31
|
+
write_response(response) if response
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def write_response(response)
|
|
38
|
+
@output.puts(JSON.generate(response))
|
|
39
|
+
@output.flush
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module MCP
|
|
8
|
+
# JSON-RPC 2.0 dispatcher for the MCP server.
|
|
9
|
+
#
|
|
10
|
+
# Each public `handle` call takes a parsed request hash and returns
|
|
11
|
+
# a response hash (or nil for notifications that require no reply).
|
|
12
|
+
# Tool implementations delegate to `CLI.new(argv, out:, err:).run`
|
|
13
|
+
# with StringIO capture — every tool stays in sync with its CLI
|
|
14
|
+
# counterpart automatically (ADR-33 WD4).
|
|
15
|
+
class Server # rubocop:disable Metrics/ClassLength
|
|
16
|
+
PROTOCOL_VERSION = "2024-11-05"
|
|
17
|
+
|
|
18
|
+
TOOLS = [
|
|
19
|
+
{
|
|
20
|
+
name: "rigor_check",
|
|
21
|
+
description: "Analyze Ruby files for type errors, undefined methods, arity mismatches, " \
|
|
22
|
+
"and nil-receiver risks. Returns a JSON diagnostic report.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
paths: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { type: "string" },
|
|
29
|
+
description: "Files or directories to analyze (required)"
|
|
30
|
+
},
|
|
31
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
32
|
+
},
|
|
33
|
+
required: ["paths"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "rigor_type_of",
|
|
38
|
+
description: "Get the inferred type of the expression at a specific location in a Ruby file.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
file: { type: "string", description: "Path to the Ruby file" },
|
|
43
|
+
line: { type: "integer", description: "1-based line number" },
|
|
44
|
+
col: { type: "integer", description: "1-based column number" },
|
|
45
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
46
|
+
},
|
|
47
|
+
required: %w[file line col]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "rigor_triage",
|
|
52
|
+
description: "Summarize a project's diagnostics: rule distribution, per-file hotspots, " \
|
|
53
|
+
"and heuristic hints for the most common error clusters. Returns JSON. " \
|
|
54
|
+
"Useful for understanding the shape of a diagnostic set before deciding what to fix.",
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
paths: {
|
|
59
|
+
type: "array",
|
|
60
|
+
items: { type: "string" },
|
|
61
|
+
description: "Files or directories to analyze (defaults to configured paths)"
|
|
62
|
+
},
|
|
63
|
+
top: { type: "integer", description: "Number of hotspot files to include (default: 10)" },
|
|
64
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "rigor_annotate",
|
|
70
|
+
description: "Return the given Ruby source file with each line's last-expression type " \
|
|
71
|
+
"appended as a comment. Useful for understanding how Rigor infers types " \
|
|
72
|
+
"through a file.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
file: { type: "string", description: "Path to the Ruby file to annotate" },
|
|
77
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
78
|
+
},
|
|
79
|
+
required: ["file"]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "rigor_sig_gen",
|
|
84
|
+
description: "Generate RBS skeleton signatures inferred from Ruby source files. " \
|
|
85
|
+
"Returns a JSON report of candidates with their classifications " \
|
|
86
|
+
"(new-file, new-method, tighter-return, equivalent, skipped).",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
paths: {
|
|
91
|
+
type: "array",
|
|
92
|
+
items: { type: "string" },
|
|
93
|
+
description: "Files or directories to generate signatures for (defaults to configured paths)"
|
|
94
|
+
},
|
|
95
|
+
params: {
|
|
96
|
+
type: "string",
|
|
97
|
+
enum: %w[untyped observed],
|
|
98
|
+
description: "Parameter policy: untyped (default) or observed " \
|
|
99
|
+
"(harvests call-site argument types from spec/)"
|
|
100
|
+
},
|
|
101
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "rigor_explain",
|
|
107
|
+
description: "Look up the description of one or all Rigor diagnostic rules. " \
|
|
108
|
+
"Returns JSON. Without a rule argument, returns the full catalog.",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
properties: {
|
|
112
|
+
rule: {
|
|
113
|
+
type: "string",
|
|
114
|
+
description: "Rule ID, legacy alias, or family prefix (call, flow, assert, dump, def). " \
|
|
115
|
+
"Omit to list every rule."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "rigor_coverage",
|
|
122
|
+
description: "Report type-precision coverage: the ratio of expressions Rigor types as " \
|
|
123
|
+
"Constant / Nominal / shaped / refined (precise) vs Dynamic or top (opaque). " \
|
|
124
|
+
"Returns JSON. Useful for measuring the impact of adding new fold rules.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
paths: {
|
|
129
|
+
type: "array",
|
|
130
|
+
items: { type: "string" },
|
|
131
|
+
description: "Files or directories to scan (required)"
|
|
132
|
+
},
|
|
133
|
+
config: { type: "string", description: "Path to .rigor.yml (optional)" }
|
|
134
|
+
},
|
|
135
|
+
required: ["paths"]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
].freeze
|
|
139
|
+
|
|
140
|
+
def initialize(config_path: nil, err: $stderr)
|
|
141
|
+
@config_path = config_path
|
|
142
|
+
@err = err
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Dispatches a parsed JSON-RPC request hash.
|
|
146
|
+
# Returns nil for notifications (requests without an `id`).
|
|
147
|
+
def handle(request)
|
|
148
|
+
id = request["id"]
|
|
149
|
+
method_name = request["method"]
|
|
150
|
+
|
|
151
|
+
# Notifications carry no `id` and require no response.
|
|
152
|
+
return nil if id.nil?
|
|
153
|
+
|
|
154
|
+
case method_name
|
|
155
|
+
when "initialize" then handle_initialize(id)
|
|
156
|
+
when "ping" then success(id, {})
|
|
157
|
+
when "tools/list" then success(id, { tools: TOOLS })
|
|
158
|
+
when "tools/call"
|
|
159
|
+
call_tool(id,
|
|
160
|
+
request.dig("params", "name"),
|
|
161
|
+
request.dig("params", "arguments") || {})
|
|
162
|
+
else
|
|
163
|
+
error(id, -32_601, "Method not found: #{method_name.inspect}")
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def handle_initialize(id)
|
|
170
|
+
require_relative "../version"
|
|
171
|
+
success(id, {
|
|
172
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
173
|
+
capabilities: { tools: { listChanged: false } },
|
|
174
|
+
serverInfo: { name: "rigor", version: Rigor::VERSION }
|
|
175
|
+
})
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def call_tool(id, name, args)
|
|
179
|
+
argv = build_argv(name, args)
|
|
180
|
+
return error(id, -32_602, "Unknown tool: #{name.inspect}") unless argv
|
|
181
|
+
|
|
182
|
+
out_io = StringIO.new
|
|
183
|
+
err_io = StringIO.new
|
|
184
|
+
require_relative "../cli"
|
|
185
|
+
exit_code = CLI.new(argv, out: out_io, err: err_io).run
|
|
186
|
+
|
|
187
|
+
is_error = exit_code == CLI::EXIT_USAGE
|
|
188
|
+
text = out_io.string
|
|
189
|
+
text = err_io.string if text.empty? && is_error
|
|
190
|
+
|
|
191
|
+
success(id, { content: [{ type: "text", text: text }], isError: is_error })
|
|
192
|
+
rescue StandardError => e
|
|
193
|
+
@err.puts("rigor mcp: #{e.class}: #{e.message}")
|
|
194
|
+
@err.puts(e.backtrace.first(5).join("\n")) if e.backtrace
|
|
195
|
+
success(id, {
|
|
196
|
+
content: [{ type: "text", text: "Internal error: #{e.message}" }],
|
|
197
|
+
isError: true
|
|
198
|
+
})
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def build_argv(name, args) # rubocop:disable Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/AbcSize,Metrics/PerceivedComplexity
|
|
202
|
+
effective_config = args["config"] || @config_path
|
|
203
|
+
|
|
204
|
+
case name
|
|
205
|
+
when "rigor_check"
|
|
206
|
+
argv = ["check", "--format=json", "--no-stats"]
|
|
207
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
208
|
+
argv += Array(args["paths"])
|
|
209
|
+
argv
|
|
210
|
+
|
|
211
|
+
when "rigor_type_of"
|
|
212
|
+
return nil unless args["file"] && args["line"] && args["col"]
|
|
213
|
+
|
|
214
|
+
argv = ["type-of", "--format=json"]
|
|
215
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
216
|
+
argv << "#{args['file']}:#{args['line']}:#{args['col']}"
|
|
217
|
+
argv
|
|
218
|
+
|
|
219
|
+
when "rigor_triage"
|
|
220
|
+
argv = ["triage", "--format=json"]
|
|
221
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
222
|
+
argv << "--top=#{args['top']}" if args["top"]
|
|
223
|
+
argv += Array(args["paths"])
|
|
224
|
+
argv
|
|
225
|
+
|
|
226
|
+
when "rigor_annotate"
|
|
227
|
+
return nil unless args["file"]
|
|
228
|
+
|
|
229
|
+
argv = ["annotate", "--no-color"]
|
|
230
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
231
|
+
argv << args["file"]
|
|
232
|
+
argv
|
|
233
|
+
|
|
234
|
+
when "rigor_sig_gen"
|
|
235
|
+
argv = ["sig-gen", "--print", "--format=json"]
|
|
236
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
237
|
+
argv << "--params=#{args['params']}" if args["params"]
|
|
238
|
+
argv += Array(args["paths"])
|
|
239
|
+
argv
|
|
240
|
+
|
|
241
|
+
when "rigor_explain"
|
|
242
|
+
argv = ["explain", "--format=json"]
|
|
243
|
+
argv << args["rule"] if args["rule"]
|
|
244
|
+
argv
|
|
245
|
+
|
|
246
|
+
when "rigor_coverage"
|
|
247
|
+
argv = ["coverage", "--format=json"]
|
|
248
|
+
argv << "--config=#{effective_config}" if effective_config
|
|
249
|
+
argv += Array(args["paths"])
|
|
250
|
+
argv
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def success(id, result)
|
|
255
|
+
{ "jsonrpc" => "2.0", "id" => id, "result" => result }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def error(id, code, message)
|
|
259
|
+
{ "jsonrpc" => "2.0", "id" => id, "error" => { "code" => code, "message" => message } }
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
data/lib/rigor/mcp.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# The MCP (Model Context Protocol) server subsystem.
|
|
5
|
+
# See `docs/adr/33-mcp-server.md` for the design.
|
|
6
|
+
#
|
|
7
|
+
# Entry point: `rigor mcp --transport stdio`.
|
|
8
|
+
# The server exposes Rigor's analysis tools (check, type-of, triage,
|
|
9
|
+
# annotate, sig-gen, explain, coverage) as MCP tool calls over a
|
|
10
|
+
# newline-delimited JSON-RPC 2.0 stdio stream.
|
|
11
|
+
module MCP
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative "mcp/server"
|
|
16
|
+
require_relative "mcp/loop"
|