rigortype 0.1.12 → 0.1.14

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 182bad9de02b3b4579fe1c385fa740e30f2df85cad36d8c21647dfb09004b9eb
4
- data.tar.gz: 398d4ebc670530522696122117592bd59b60d7f899ae0a8cd58b11c8506ec608
3
+ metadata.gz: b1dbcd9b168a06cc8d5f26e0a096f19496c3cebdc8864fb56d966741b8a42577
4
+ data.tar.gz: 39e428c763e8d8cac6d9743d40d5d654bfaec56362d41e338a6dac974dff27cf
5
5
  SHA512:
6
- metadata.gz: abd25775ea4973023dc7ef7132669a0bf556604c7378caef60aa3c2fbae4ef79bc2651cd3499cecb8046507214dbdc4cb56bdb953c301ec42b0ebb86291202b6
7
- data.tar.gz: 9281f16a4b2d39847aaaf408844920dfb9f2e6a914df3544182d6d3832f28f03ca63d8a19aa54ca80977f3431351717a59d7ba704f3361ee92697be21d6193ab
6
+ metadata.gz: 6cd6a4d0b52fcd2e1e2bacddd6120154ccbe075ef86615c38d7e6d1d942ea158f55abaeb03f01c0a23315a187076912315af98ce74faa4c174c4f6c5c98eac74
7
+ data.tar.gz: 1d7b600e3dd97a38bf1ac08231a7aa07635210723adf349c014d9100ccc2b0a868deb8a6479d99207e1c0b5e4cf07c158f0f0cb3bda325ec5c7d143e4f5ae90b
data/README.md CHANGED
@@ -49,8 +49,18 @@ Rigor is a tool, not a library — install it independently, **not** in
49
49
  your project's `Gemfile`. It runs on Ruby 4.0, regardless of which Ruby
50
50
  version your project targets.
51
51
 
52
- The recommended setup uses [`mise`](https://mise.jdx.dev/), which
53
- provisions both Ruby 4.0 and Rigor pinned per project:
52
+ **Using an AI coding agent?** Hand it this prompt and it will detect
53
+ your environment, install Rigor, and kick off the project-init Skill
54
+ automatically:
55
+
56
+ ```
57
+ Install Rigor in this project by following the instructions at
58
+ https://raw.githubusercontent.com/rigortype/rigor/refs/heads/master/docs/install.md
59
+ ```
60
+
61
+ **Manual install** — the recommended path uses
62
+ [`mise`](https://mise.jdx.dev/), which provisions both Ruby 4.0 and
63
+ Rigor pinned per project:
54
64
 
55
65
  ```sh
56
66
  mise use ruby@4.0
@@ -57,6 +57,7 @@ module Rigor
57
57
  # system; new rules MUST register here so user configuration
58
58
  # can refer to them.
59
59
  RULE_UNDEFINED_METHOD = "call.undefined-method"
60
+ RULE_UNRESOLVED_TOPLEVEL = "call.unresolved-toplevel"
60
61
  RULE_WRONG_ARITY = "call.wrong-arity"
61
62
  RULE_ARGUMENT_TYPE = "call.argument-type-mismatch"
62
63
  RULE_NIL_RECEIVER = "call.possible-nil-receiver"
@@ -72,6 +73,7 @@ module Rigor
72
73
 
73
74
  ALL_RULES = [
74
75
  RULE_UNDEFINED_METHOD,
76
+ RULE_UNRESOLVED_TOPLEVEL,
75
77
  RULE_WRONG_ARITY,
76
78
  RULE_ARGUMENT_TYPE,
77
79
  RULE_NIL_RECEIVER,
@@ -162,6 +164,7 @@ module Rigor
162
164
  def call_node_diagnostics(path, node, scope_index)
163
165
  [
164
166
  undefined_method_diagnostic(path, node, scope_index),
167
+ unresolved_toplevel_diagnostic(path, node, scope_index),
165
168
  wrong_arity_diagnostic(path, node, scope_index),
166
169
  argument_type_diagnostic(path, node, scope_index),
167
170
  nil_receiver_diagnostic(path, node, scope_index),
@@ -365,10 +368,14 @@ module Rigor
365
368
  return nil if open_receiver?(class_name, scope)
366
369
 
367
370
  # Slice 7 phase 12 — suppress when the user has
368
- # declared the method in source (instance `def`,
369
- # `def self.foo`, or recognised `define_method`).
371
+ # declared the method in source (`def` /
372
+ # `define_method`) OR in a `pre_eval:` monkey-patch
373
+ # file (ADR-17). Both paths are project-side method
374
+ # contributions the dispatcher already resolved; the
375
+ # rule must not surface a false `undefined-method`
376
+ # for them.
370
377
  kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
371
- return nil if scope.discovered_method?(class_name, call_node.name, kind)
378
+ return nil if source_declared_method?(scope, class_name, call_node.name, kind)
372
379
 
373
380
  return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
374
381
 
@@ -404,6 +411,92 @@ module Rigor
404
411
  scope.environment.rbs_module?(receiver_type.class_name)
405
412
  end
406
413
 
414
+ # Combined suppression probe for `undefined-method` /
415
+ # `unresolved-toplevel`. Returns true when the method is
416
+ # declared by any project-side contributor the dispatcher
417
+ # already resolves: an in-source `def` / `define_method`
418
+ # (`scope.discovered_method?`) OR an ADR-17 `pre_eval:`
419
+ # monkey-patch (`Environment#project_patched_methods`).
420
+ # Both paths sit at the same dispatcher precedence; the
421
+ # check must hold them together so neither rule fires a
422
+ # false positive.
423
+ def source_declared_method?(scope, class_name, method_name, kind)
424
+ return true if scope.discovered_method?(class_name, method_name, kind)
425
+
426
+ project_patched_method?(scope, class_name, method_name, kind)
427
+ end
428
+
429
+ # ADR-17 § "Inference contract" — consults
430
+ # `Environment#project_patched_methods` so a `def` declared
431
+ # in a `pre_eval:` file suppresses the diagnostic at the
432
+ # same dispatcher precedence the registry holds for type
433
+ # inference (between plugins and dependency-source).
434
+ # Returns false when the environment carries no registry
435
+ # (legacy path) or the lookup misses.
436
+ def project_patched_method?(scope, class_name, method_name, kind)
437
+ environment = scope.environment
438
+ registry = environment&.project_patched_methods
439
+ return false if registry.nil? || registry.empty?
440
+
441
+ !registry.lookup(class_name: class_name.to_s, method_name: method_name.to_sym, kind: kind).nil?
442
+ end
443
+
444
+ # ADR-34 — `call.unresolved-toplevel`. Fires on an
445
+ # implicit-self call (no explicit receiver) at toplevel
446
+ # scope (`scope.toplevel?`, i.e. outside any class /
447
+ # module body) whose name does not resolve against:
448
+ #
449
+ # 1. A same-file toplevel `def` via
450
+ # {Scope#top_level_def_for}.
451
+ # 2. The ADR-17 `ProjectPatchedMethods` registry under
452
+ # `(Object, name, :instance)` — projects declare
453
+ # their toplevel-injecting monkey-patches in
454
+ # `.rigor.yml`'s `pre_eval:` array as the canonical
455
+ # opt-out per ADR-34 WD2.
456
+ # 3. The standard `Kernel` / `Object` private-method
457
+ # surface (`puts`, `p`, `require`, `loop`, `raise`,
458
+ # …) drawn from the loaded RBS environment.
459
+ #
460
+ # The rule deliberately does NOT generalise to
461
+ # implicit-self calls inside `def` / `class` / `module`
462
+ # bodies — ADR-24 WD3's lenient-on-unresolved default
463
+ # stays in force there. ADR-24 WD4's gated class-body
464
+ # diagnostic is a separate decision this ADR does not
465
+ # open.
466
+ #
467
+ # Authored severity is `:warning`; the severity profile
468
+ # remaps it (`strict` → `:error`, `balanced` →
469
+ # `:warning`, `lenient` → `:off` / suppressed).
470
+ def unresolved_toplevel_diagnostic(path, call_node, scope_index)
471
+ return nil unless call_node.receiver.nil?
472
+
473
+ scope = scope_index[call_node]
474
+ return nil if scope.nil?
475
+ return nil unless scope.toplevel?
476
+
477
+ name = call_node.name
478
+ return nil if scope.top_level_def_for(name)
479
+ return nil if source_declared_method?(scope, "Object", name, :instance)
480
+ return nil if Rigor::Reflection.instance_method_definition("Object", name, scope: scope)
481
+
482
+ build_unresolved_toplevel_diagnostic(path, call_node)
483
+ end
484
+
485
+ def build_unresolved_toplevel_diagnostic(path, call_node)
486
+ location = call_node.message_loc || call_node.location
487
+ Diagnostic.new(
488
+ path: path,
489
+ line: location.start_line,
490
+ column: location.start_column + 1,
491
+ message: "unresolved toplevel call to `#{call_node.name}`. " \
492
+ "If a project file defines `#{call_node.name}` via a toplevel " \
493
+ "`def` or a monkey-patch on Object/Kernel, list that file in " \
494
+ "`.rigor.yml`'s `pre_eval:` (ADR-17) so the analyzer sees it.",
495
+ severity: :warning,
496
+ rule: RULE_UNRESOLVED_TOPLEVEL
497
+ )
498
+ end
499
+
407
500
  # Returns a qualified class name for the in-scope check.
408
501
  # Nominal / Singleton carry a single-class identity
409
502
  # directly. Constant projects to its value's class.
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Rigor
6
+ class CLI
7
+ # `rigor skill` — discover and print the SKILL.md files
8
+ # bundled with the `rigortype` gem.
9
+ #
10
+ # Rigor ships a small set of Agent Skills under `skills/` that
11
+ # walk an AI coding agent through onboarding (`rigor-project-init`),
12
+ # baseline reduction (`rigor-baseline-reduce`), and authoring a
13
+ # plugin (`rigor-plugin-author`). When Rigor is installed via
14
+ # `mise` / `gem install` / etc. the SKILL files live inside the
15
+ # gem checkout — the project being analysed has no copy, so an
16
+ # AI agent has no a priori way to find them.
17
+ #
18
+ # This command exposes the bundled skills via three subcommands:
19
+ #
20
+ # - `rigor skill list` — table of name + absolute path.
21
+ # - `rigor skill print <name>` — short header (paths + how to use)
22
+ # followed by the SKILL.md body. This
23
+ # is the form AI agents should call;
24
+ # the inline body plus the header's
25
+ # absolute paths together let the
26
+ # agent act with or without a file
27
+ # reading tool.
28
+ # - `rigor skill path <name>` — one-line absolute path, suitable
29
+ # as input to a Read tool.
30
+ #
31
+ # `rigor skill` with no subcommand is an alias for `list`.
32
+ class SkillCommand
33
+ USAGE = <<~USAGE
34
+ Usage: rigor skill <subcommand> [args]
35
+
36
+ Subcommands:
37
+ list List bundled skills (default when no subcommand given)
38
+ print <name> Print the SKILL.md body for <name> to stdout, with a header
39
+ path <name> Print the absolute path of the SKILL.md file for <name>
40
+
41
+ Examples:
42
+ rigor skill list
43
+ rigor skill print rigor-project-init
44
+ rigor skill path rigor-baseline-reduce
45
+ USAGE
46
+
47
+ # The bundled skills live at `<gem_root>/skills/`. From
48
+ # `lib/rigor/cli/skill_command.rb` that is three directories up.
49
+ SKILLS_ROOT = File.expand_path("../../../skills", __dir__)
50
+
51
+ def initialize(argv:, out: $stdout, err: $stderr)
52
+ @argv = argv
53
+ @out = out
54
+ @err = err
55
+ end
56
+
57
+ # @return [Integer] CLI exit status.
58
+ def run
59
+ subcommand = @argv.shift || "list"
60
+
61
+ case subcommand
62
+ when "list" then run_list
63
+ when "print" then run_print
64
+ when "path" then run_path
65
+ when "-h", "--help", "help"
66
+ print_usage(@out)
67
+ 0
68
+ else
69
+ @err.puts("Unknown subcommand: #{subcommand}")
70
+ print_usage(@err)
71
+ Rigor::CLI::EXIT_USAGE
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def run_list
78
+ skills = discover_skills
79
+ if skills.empty?
80
+ @err.puts("No bundled skills found under #{SKILLS_ROOT}")
81
+ return 1
82
+ end
83
+
84
+ width = skills.map { |s| s.fetch(:name).length }.max
85
+ skills.each do |skill|
86
+ @out.puts(format("%-#{width}s %s", skill.fetch(:name), skill.fetch(:path)))
87
+ end
88
+ 0
89
+ end
90
+
91
+ def run_print
92
+ name = @argv.shift
93
+ return usage_error("`print` requires a skill name") if name.nil?
94
+
95
+ skill = find_skill(name)
96
+ return name_error(name) if skill.nil?
97
+
98
+ @out.puts(render_print_header(skill))
99
+ @out.puts
100
+ @out.write(File.read(skill.fetch(:path)))
101
+ 0
102
+ end
103
+
104
+ def run_path
105
+ name = @argv.shift
106
+ return usage_error("`path` requires a skill name") if name.nil?
107
+
108
+ skill = find_skill(name)
109
+ return name_error(name) if skill.nil?
110
+
111
+ @out.puts(skill.fetch(:path))
112
+ 0
113
+ end
114
+
115
+ # The header that precedes the SKILL.md body when an agent
116
+ # runs `rigor skill print <name>`. Kept as `# `-prefixed
117
+ # comment lines so the combined output remains parseable as
118
+ # markdown — anything below `---` (the SKILL frontmatter
119
+ # marker) is unchanged.
120
+ def render_print_header(skill)
121
+ references_dir = File.join(File.dirname(skill.fetch(:path)), "references")
122
+ ref_line = if File.directory?(references_dir)
123
+ "# References: #{references_dir}/ (read referenced `references/NN-*.md` files from here)"
124
+ else
125
+ "# References: (none)"
126
+ end
127
+ <<~HEADER.chomp
128
+ # Rigor skill: #{skill.fetch(:name)}
129
+ # Source: #{skill.fetch(:path)}
130
+ #{ref_line}
131
+ #
132
+ # The body below is the canonical SKILL definition shipped with
133
+ # rigortype #{Rigor::VERSION}. Follow its instructions.
134
+ HEADER
135
+ end
136
+
137
+ def discover_skills
138
+ return [] unless File.directory?(SKILLS_ROOT)
139
+
140
+ Dir.children(SKILLS_ROOT).sort.filter_map do |name|
141
+ skill_md = File.join(SKILLS_ROOT, name, "SKILL.md")
142
+ next unless File.file?(skill_md)
143
+
144
+ { name: name, path: skill_md }
145
+ end
146
+ end
147
+
148
+ def find_skill(name)
149
+ discover_skills.find { |s| s.fetch(:name) == name }
150
+ end
151
+
152
+ def name_error(name)
153
+ @err.puts("Unknown skill: #{name}")
154
+ @err.puts("Available skills:")
155
+ discover_skills.each { |s| @err.puts(" #{s.fetch(:name)}") }
156
+ 1
157
+ end
158
+
159
+ def usage_error(message)
160
+ @err.puts(message)
161
+ print_usage(@err)
162
+ Rigor::CLI::EXIT_USAGE
163
+ end
164
+
165
+ def print_usage(io)
166
+ io.puts(USAGE)
167
+ end
168
+ end
169
+ end
170
+ end
data/lib/rigor/cli.rb CHANGED
@@ -33,7 +33,8 @@ module Rigor
33
33
  "triage" => :run_triage,
34
34
  "coverage" => :run_coverage,
35
35
  "plugins" => :run_plugins,
36
- "playground" => :run_playground
36
+ "playground" => :run_playground,
37
+ "skill" => :run_skill
37
38
  }.freeze
38
39
 
39
40
  def self.start(argv = ARGV, out: $stdout, err: $stderr)
@@ -635,6 +636,12 @@ module Rigor
635
636
  Rigor::CLI::PlaygroundCommand.new(@argv[1..], @out, @err).run
636
637
  end
637
638
 
639
+ def run_skill
640
+ require_relative "cli/skill_command"
641
+
642
+ CLI::SkillCommand.new(argv: @argv, out: @out, err: @err).run
643
+ end
644
+
638
645
  def write_result(result, format)
639
646
  case format
640
647
  when "json"
@@ -682,6 +689,7 @@ module Rigor
682
689
  coverage Report type-precision coverage (precise vs Dynamic ratio)
683
690
  plugins Report activation status of every configured plugin
684
691
  playground Start the browser playground (requires rigor-playground gem)
692
+ skill List or print bundled Agent Skills (rigor-project-init, ...)
685
693
  version Print the Rigor version
686
694
  help Print this help
687
695
  HELP
@@ -39,6 +39,7 @@ module Rigor
39
39
  PROFILES = {
40
40
  lenient: {
41
41
  "call.undefined-method" => :error,
42
+ "call.unresolved-toplevel" => :off,
42
43
  "call.wrong-arity" => :error,
43
44
  "call.argument-type-mismatch" => :warning,
44
45
  "call.possible-nil-receiver" => :warning,
@@ -54,6 +55,7 @@ module Rigor
54
55
  }.freeze,
55
56
  balanced: {
56
57
  "call.undefined-method" => :error,
58
+ "call.unresolved-toplevel" => :warning,
57
59
  "call.wrong-arity" => :error,
58
60
  "call.argument-type-mismatch" => :error,
59
61
  "call.possible-nil-receiver" => :error,
@@ -69,6 +71,7 @@ module Rigor
69
71
  }.freeze,
70
72
  strict: {
71
73
  "call.undefined-method" => :error,
74
+ "call.unresolved-toplevel" => :error,
72
75
  "call.wrong-arity" => :error,
73
76
  "call.argument-type-mismatch" => :error,
74
77
  "call.possible-nil-receiver" => :error,
@@ -34,6 +34,20 @@ module Rigor
34
34
  # `sig/`), and `local` (a user-managed RBS dir) — all
35
35
  # produce a directory under the collection root and are
36
36
  # admitted.
37
+ #
38
+ # The `source.type` filter alone is NOT sufficient: gems
39
+ # that were extracted from Ruby's stdlib into standalone
40
+ # default gems (e.g. `cgi`, `logger`, `base64`, `csv`,
41
+ # `bigdecimal`) are published in `ruby/gem_rbs_collection`
42
+ # under a `git` source type, yet rigor ALSO loads them
43
+ # from its bundled stdlib via `DEFAULT_LIBRARIES`. Loading
44
+ # both copies triggers the very
45
+ # `RBS::DuplicatedDeclarationError` this module exists to
46
+ # avoid (observed on a Rails 8 app: `.gem_rbs_collection/
47
+ # cgi/0.5/` vs the bundled `stdlib/cgi`). The
48
+ # `skip_gem_names:` parameter lets the caller pass the set
49
+ # of library names rigor already loads so those gems are
50
+ # dropped regardless of `source.type`.
37
51
  SKIPPED_SOURCE_TYPES = Set["stdlib"].freeze
38
52
 
39
53
  DEFAULT_COLLECTION_PATH = ".gem_rbs_collection"
@@ -47,14 +61,21 @@ module Rigor
47
61
  # @param auto_detect [Boolean] when true and
48
62
  # `lockfile_path:` is nil, look for
49
63
  # `<project_root>/rbs_collection.lock.yaml`.
64
+ # @param skip_gem_names [Array<String>, Set<String>] gem
65
+ # names rigor already loads from its bundled stdlib (the
66
+ # merged `DEFAULT_LIBRARIES + libraries:` set). Entries
67
+ # whose `name` is in this set are dropped regardless of
68
+ # `source.type` to avoid `RBS::DuplicatedDeclarationError`
69
+ # on stdlib-extracted default gems. Defaults to empty.
50
70
  # @return [Array<Pathname>] every
51
71
  # `<collection_path>/<gem-name>/<gem-version>/`
52
72
  # directory listed in the lockfile whose entry has a
53
- # non-skipped source type and whose directory exists on
54
- # disk. Returns `[]` when no lockfile is resolvable,
55
- # when the YAML is unreadable, or when the collection
56
- # path doesn't exist.
57
- def self.discover(lockfile_path:, project_root: Dir.pwd, auto_detect: true)
73
+ # non-skipped source type, whose `name` is not in
74
+ # `skip_gem_names:`, and whose directory exists on disk.
75
+ # Returns `[]` when no lockfile is resolvable, when the
76
+ # YAML is unreadable, or when the collection path
77
+ # doesn't exist.
78
+ def self.discover(lockfile_path:, project_root: Dir.pwd, auto_detect: true, skip_gem_names: [])
58
79
  resolved = resolve_lockfile_path(
59
80
  lockfile_path: lockfile_path,
60
81
  project_root: project_root,
@@ -68,7 +89,7 @@ module Rigor
68
89
  collection_root = resolve_collection_root(resolved, data)
69
90
  return [] unless collection_root.directory?
70
91
 
71
- gem_paths_from(collection_root, data)
92
+ gem_paths_from(collection_root, data, skip_gem_names.to_set)
72
93
  end
73
94
 
74
95
  # Returns the resolved lockfile path (`Pathname`) or `nil`
@@ -105,7 +126,7 @@ module Rigor
105
126
  end
106
127
  private_class_method :resolve_collection_root
107
128
 
108
- def self.gem_paths_from(collection_root, data)
129
+ def self.gem_paths_from(collection_root, data, skip_gem_names)
109
130
  Array(data["gems"]).filter_map do |entry|
110
131
  next unless entry.is_a?(Hash)
111
132
 
@@ -115,6 +136,7 @@ module Rigor
115
136
  name = entry["name"]
116
137
  version = entry["version"]
117
138
  next if name.nil? || version.nil?
139
+ next if skip_gem_names.include?(name.to_s)
118
140
 
119
141
  gem_root = collection_root + name.to_s + version.to_s
120
142
  gem_root if gem_root.directory?
@@ -122,6 +122,23 @@ module Rigor
122
122
  Pathname(File.join(VENDORED_GEM_SIGS_ROOT, gem_dir))
123
123
  end
124
124
  end
125
+
126
+ # Gem names whose RBS ships under
127
+ # `data/vendored_gem_sigs/<gem>/`. The directory walk is
128
+ # the source of truth (the `README.md` sibling is not a
129
+ # gem and is excluded). Callers building the RBS env use
130
+ # this set to drop the matching `rbs collection install`
131
+ # directory before it double-declares against the
132
+ # vendored copy — the same hazard `DEFAULT_LIBRARIES`
133
+ # creates for stdlib-extracted gems. See
134
+ # `RbsCollectionDiscovery`'s `skip_gem_names:`.
135
+ def vendored_gem_names
136
+ return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
137
+
138
+ Dir.children(VENDORED_GEM_SIGS_ROOT).reject do |child|
139
+ File.file?(File.join(VENDORED_GEM_SIGS_ROOT, child))
140
+ end
141
+ end
125
142
  end
126
143
 
127
144
  attr_reader :libraries, :signature_paths, :cache_store, :virtual_rbs
@@ -263,11 +263,22 @@ module Rigor
263
263
  # resulting `rbs_collection.lock.yaml` and feed each
264
264
  # gem's `<collection_path>/<name>/<version>/` directory
265
265
  # into `signature_paths:`. Stdlib-typed entries are
266
- # skipped (already covered by `DEFAULT_LIBRARIES`).
266
+ # skipped (already covered by `DEFAULT_LIBRARIES`), as
267
+ # are gems whose RBS rigor already loads from another
268
+ # source — stdlib-extracted default gems (`cgi`,
269
+ # `logger`, …, shipped by the collection under a `git`
270
+ # source) and the `data/vendored_gem_sigs/` bundle
271
+ # (`redis`, `nokogiri`, `pg`, …). `skip_gem_names:`
272
+ # passes both sets so the collection copy doesn't
273
+ # double-declare against rigor's bundled RBS (the
274
+ # `RBS::DuplicatedDeclarationError` hazard).
275
+ merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
276
+ skip_gem_names = merged_libraries + RbsLoader.vendored_gem_names
267
277
  collection_paths = RbsCollectionDiscovery.discover(
268
278
  lockfile_path: rbs_collection_lockfile,
269
279
  project_root: root,
270
- auto_detect: rbs_collection_auto_detect
280
+ auto_detect: rbs_collection_auto_detect,
281
+ skip_gem_names: skip_gem_names
271
282
  ).map(&:to_s)
272
283
  # ADR-25 — RBS signature directories contributed by loaded
273
284
  # plugins via their manifest `signature_paths:`. Resolved
@@ -278,7 +289,6 @@ module Rigor
278
289
  # degrades through the same O7 failure-memo path.
279
290
  plugin_sig_paths = plugin_registry ? plugin_registry.signature_paths.map(&:to_s) : []
280
291
  loader_signature_paths = resolved_paths + plugin_sig_paths + gem_sig_paths + collection_paths
281
- merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
282
292
  # ADR-32 WD4 + WD5 — invoke each loaded plugin's
283
293
  # `source_rbs_synthesizer` once per project source file
284
294
  # and collect non-nil `[filename, rbs_source]` pairs.
data/lib/rigor/scope.rb CHANGED
@@ -309,6 +309,20 @@ module Rigor
309
309
  table[method_name.to_sym] == kind
310
310
  end
311
311
 
312
+ # ADR-34 § "Decision" — predicate identifying a toplevel-shaped
313
+ # scope (no enclosing `class` / `module` body). True at the top
314
+ # of a file AND inside a top-level `def` body (since toplevel
315
+ # defs leave `self_type` nil per the existing scope-construction
316
+ # contract, mirroring how ADR-24's `adoptable_self_call_result?`
317
+ # also keys on `self_type.nil?` for the same context). Used by
318
+ # `CheckRules#unresolved_toplevel_diagnostic` to gate the
319
+ # `call.unresolved-toplevel` rule so it fires only outside
320
+ # class / module bodies, where Rails-DSL metaprogramming
321
+ # leniency (ADR-24 WD3 → WD4) does not apply.
322
+ def toplevel?
323
+ @self_type.nil?
324
+ end
325
+
312
326
  def with_discovered_methods(table)
313
327
  rebuild(discovered_methods: table)
314
328
  end
data/lib/rigor/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rigor
4
- VERSION = "0.1.12"
4
+ VERSION = "0.1.14"
5
5
  end
@@ -51,6 +51,7 @@ module Rigor
51
51
  def self.reset_default!: () -> void
52
52
  def self.build_env_for: (libraries: Array[String], signature_paths: Array[String | _ToPath]) -> untyped
53
53
  def self.vendored_gem_sig_paths: () -> Array[Pathname]
54
+ def self.vendored_gem_names: () -> Array[String]
54
55
 
55
56
  def initialize: (?libraries: Array[String], ?signature_paths: Array[String | _ToPath], ?cache_store: untyped?) -> void
56
57
  def class_known?: (String | Symbol name) -> bool
data/sig/rigor/scope.rbs CHANGED
@@ -58,6 +58,7 @@ module Rigor
58
58
  def with_discovered_def_nodes: (Hash[String, Hash[Symbol, untyped]] table) -> Scope
59
59
  def user_def_for: (String | Symbol class_name, String | Symbol method_name) -> untyped?
60
60
  def top_level_def_for: (String | Symbol method_name) -> untyped?
61
+ def toplevel?: () -> bool
61
62
  def with_discovered_method_visibilities: (Hash[String, Hash[Symbol, Symbol]] table) -> Scope
62
63
  def discovered_method_visibility: (String | Symbol class_name, String | Symbol method_name) -> Symbol?
63
64
  def superclass_of: (String | Symbol class_name) -> String?