rubocop-legion 0.1.6 → 0.1.8

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: f841ecd6e5821651ad020bd243791971cf7ab5dca138178d90d216e8a58a48ba
4
- data.tar.gz: acd221dbf9f924e6f81b21569262ea6a8aebbe42d4753356f9750cd3cc0c13a0
3
+ metadata.gz: '0668cc51aab49685f7ecb368113a7103253ec4d3aca22de2970c88ff4ae351f3'
4
+ data.tar.gz: abc9644c2e563387778df1459aa7abd81f49e79c14e7cc13e038546ceab40a56
5
5
  SHA512:
6
- metadata.gz: 5933d7f5010bc89b49a95e3d4c0080d752d583fab9a4c11e5fc3a244a0e45938be35eeb6f6851386452d72eca5c135434b6fe1d7dc6cb2a5be163bf7a0213576
7
- data.tar.gz: 62b31e59d175e345383a02764b3c1c32a7599e70af80e31d8f0685730579c5396d9cdd783e19fb16ca18809e43b33cead79b6ad03f444acc77045fe4c2dd6399
6
+ metadata.gz: 43d9efec4fb9f8fc5af8eced138ed626c29211017382b4c71b84f3191b2b5316624e64821e657cf4e8a61426f51404c147c32522d623c3cce3c50d51f9738afe
7
+ data.tar.gz: 17807228e3e91cd2e4ac0be778a469256adbe691e7b0f1b6399344cb6d1ecffe6e699a5db7ffcf274a42d1a0df531842b32e218f89230d1ae1ced828ef733fbd
data/.rubocop.yml CHANGED
@@ -9,6 +9,15 @@ Style/Documentation:
9
9
  Metrics/MethodLength:
10
10
  Max: 20
11
11
 
12
+ Metrics/AbcSize:
13
+ Max: 35
14
+
15
+ Metrics/CyclomaticComplexity:
16
+ Max: 20
17
+
18
+ Metrics/PerceivedComplexity:
19
+ Max: 20
20
+
12
21
  Naming/FileName:
13
22
  Exclude:
14
23
  - 'lib/rubocop-legion.rb'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.8] - 2026-06-16
4
+
5
+ ### Added
6
+ - New cop `Legion/Framework/NoUnderscorePrefixedKwargs`: ban `_foo:` keyword arguments and `**_rest` splats; autocorrects to plain names / `**opts`
7
+ - New cop `Legion/Framework/NoInlineSettingDefaults`: flag `Legion::Settings[...] || <literal>` and shadow-default patterns (tunables belong in `settings.rb`)
8
+ - New cop `Legion/Framework/NoDirectDispatch`: ban `*_direct` methods on `Legion::LLM` modules (every pipeline exit must route through the governed pipeline)
9
+ - New cop `Legion/Framework/NoShapeDuckTyping` (disabled by default): flag `respond_to?` on canonical shape methods and string-key fallback access on canonical bodies
10
+
11
+ ## [0.1.7] - 2026-03-29
12
+
13
+ ### Added
14
+ - New cop `Legion/HelperMigration/DirectTransport`: use `transport_*` helpers instead of `Legion::Transport::Connection`/`Spool` methods (auto-fix)
15
+ - New cop `Legion/HelperMigration/DirectKnowledge`: use `query_knowledge`/`ingest_knowledge` helpers instead of `Legion::Apollo`/`Legion::Apollo::Local` methods (auto-fix)
16
+
17
+ ### Changed
18
+ - Renamed `Legion/HelperMigration/DirectLlmEmbed` to `Legion/HelperMigration/DirectLlm` — now covers `chat`, `ask`, `structured`, `embed_batch` in addition to `embed`
19
+ - Expanded `Legion/HelperMigration/DirectJson` to cover `parse`, `generate`, `pretty_generate`
20
+ - Expanded `Legion/HelperMigration/DirectCache` to cover `fetch`, `connected?`
21
+ - Expanded `Legion/HelperMigration/DirectLocalCache` to cover `delete`, `fetch`
22
+ - Expanded `Legion/HelperMigration/DirectCrypt` to cover `write`
23
+
3
24
  ## [0.1.6] - 2026-03-29
4
25
 
5
26
  ### Added
data/CLAUDE.md CHANGED
@@ -1,114 +1,56 @@
1
1
  # rubocop-legion
2
2
 
3
- **Parent**: `/Users/miverso2/rubymine/legion/CLAUDE.md`
4
-
5
- ## What is This?
6
-
7
- Custom RuboCop plugin gem for the LegionIO ecosystem. Provides 45 AST-based cops across 6 departments. Uses the new RuboCop Plugin API (1.72+, lint_roller-based) with auto-discovery via gemspec metadata.
8
-
9
- **GitHub**: https://github.com/LegionIO/rubocop-legion
10
- **RubyGems**: https://rubygems.org/gems/rubocop-legion
11
- **License**: MIT
3
+ Custom RuboCop plugin gem for the LegionIO ecosystem. Provides 47 AST-based cops across 6 departments. Uses the RuboCop Plugin API (1.72+, lint_roller-based) with auto-discovery via gemspec metadata.
12
4
 
13
5
  ## Shared Config Profiles
14
6
 
15
- The gem ships shared `.rubocop.yml` profiles so repos don't duplicate config:
16
-
17
7
  - `config/base.yml` — all shared settings (AllCops, Layout, Metrics, Style, Naming, Performance, ThreadSafety)
18
- - `config/lex.yml` — inherits base, adds plugins (rubocop-legion + performance + thread_safety) + `ParameterLists Max: 8`
19
- - `config/core.yml` — inherits base, adds plugins (rubocop-legion + performance + thread_safety) + `ParameterLists Max: 10, CountKeywordArgs: false`
20
-
21
- ### Bundled Plugins
8
+ - `config/lex.yml` — inherits base, adds plugins + `ParameterLists Max: 8`
9
+ - `config/core.yml` — inherits base, adds plugins + `ParameterLists Max: 10, CountKeywordArgs: false`
22
10
 
23
- Both profiles load `rubocop-performance` (52 cops) and `rubocop-thread_safety` (6 cops) as runtime dependencies. ThreadSafety defaults tuned for LegionIO: `NewThread` excludes service/connection files, `ClassInstanceVariable` excludes singletons, `RackMiddlewareInstanceVariable` disabled, `DirChdir` allows block form.
11
+ Both profiles load `rubocop-performance` (52 cops) and `rubocop-thread_safety` (6 cops) as runtime dependencies.
24
12
 
25
13
  **LEX repos**: `inherit_gem: { rubocop-legion: config/lex.yml }`
26
14
  **Core repos**: `inherit_gem: { rubocop-legion: config/core.yml }`
27
15
 
28
- Repo-specific overrides go below the `inherit_gem` directive. Version-locked with the gem — bump gem version = all repos get updated config.
29
-
30
- ## Cop Scoping
31
-
32
- Cops are scoped by gem type — no per-repo configuration needed:
33
-
34
- - **Universal** (9 cops): Fire on all LegionIO gems
35
- - **Library-specific** (6 cops): Fire on all gems but only trigger when using Sequel, Sinatra, Thor, Faraday, or cache
36
- - **LEX-only** (28 cops): Scoped to `lib/legion/extensions/**/*.rb` via Include directive — never fire on core `legion-*` libraries
37
-
38
- ## Departments and Cops
39
-
40
- ### Universal — 10 cops
16
+ ## Cop Departments
41
17
 
42
- - **ConstantSafety** (4): `BareDataDefine`, `BareProcess`, `BareJson` (all error, auto-fix) — prefix with `::` inside `module Legion`. `InheritParam` (convention, auto-fix) — pass `false` to `const_defined?`/`const_get`.
43
- - **RescueLogging** (3): `BareRescue` (warning, auto-fix) — capture with `=> e`. `NoCapture` (convention, no auto-fix) — exception class without capture. `SilentCapture` (warning, no auto-fix) — captured but never logged/re-raised. Skips `_`-prefixed vars. All skip inline rescue modifiers.
44
- - **Singleton** (1): `UseInstance` (error, auto-fix) — `.instance` not `.new` for configurable singleton classes.
45
- - **Framework/MutexNestedSync** (1): Nested `synchronize` blocks risk deadlock.
46
- - **Framework/ModuleFunctionPrivate** (1): `private` after `module_function` resets visibility.
18
+ | Department | Count | Scope |
19
+ |------------|-------|-------|
20
+ | ConstantSafety | 4 | Universal |
21
+ | RescueLogging | 3 | Universal |
22
+ | Singleton | 1 | Universal |
23
+ | Framework | 8 | Universal + Library-specific |
24
+ | HelperMigration | 13 | LEX-only |
25
+ | Extension | 18 | LEX-only |
47
26
 
48
- ### Library-Specific6 cops
49
-
50
- - `EagerSequelModel` — `Sequel::Model(:table)` at require time
51
- - `SinatraHostAuth` — Sinatra 4.0+ `set :host_authorization`
52
- - `ThorReservedRun` — Thor 1.5+ reserves `run`
53
- - `FaradayXmlMiddleware` — Faraday 2.0+ removed `:xml`
54
- - `CacheTimeCoercion` — Time→String after cache round-trip
55
- - `ApiStringKeys` — `Legion::JSON.load` returns symbol keys (scoped to `lib/legion/extensions/**/*.rb`)
56
-
57
- ### LEX-Only — 29 cops
58
-
59
- - **HelperMigration** (11): `DirectLogging`, `OldLoggingMethods`, `DirectJson`, `DirectCache`, `DirectLocalCache`, `DirectCrypt`, `DirectData`, `DirectLlmEmbed`, `RequireDefinedGuard` (all auto-fix) — use per-extension helpers, not global singletons. `LoggingGuard` (no auto-fix) — remove unnecessary `respond_to?(:log_warn)` / `defined?(Legion::Logging)` guards. `DefinedTransportGuard` (no auto-fix) — use `transport_connected?` instead of `defined?(Legion::Transport)`.
60
- - **Extension** (18): `ActorSingularModule` (auto-fix), `RunnerPluralModule` (auto-fix), `CoreExtendGuard` (auto-fix), `RunnerMustBeModule`, `RunnerIncludeHelpers`, `ActorInheritance`, `EveryActorRequiresTime`, `SelfContainedActorRunnerClass`, `HookMissingRunnerClass`, `AbsorberMissingPattern`, `AbsorberMissingAbsorbMethod`, `DefinitionCallMismatched`, `RunnerReturnHash`, `SettingsKeyMethod` (auto-fix), `SettingsBracketMultiArg` (auto-fix), `LlmAskKwargs`, `ActorEnabledSideEffects`, `DataRequiredWithoutMigrations`.
27
+ Cops are scoped by gem type no per-repo configuration needed. LEX-only cops fire on `lib/legion/extensions/**/*.rb` via Include directive.
61
28
 
62
29
  ## Architecture
63
30
 
64
31
  ```
65
32
  rubocop-legion/
66
33
  ├── lib/
67
- │ ├── rubocop-legion.rb # Entry point, requires all cops
34
+ │ ├── rubocop-legion.rb # Entry point
68
35
  │ └── rubocop/
69
- │ ├── legion.rb # Namespace declarations
70
36
  │ ├── legion/
71
- │ │ ├── version.rb
72
37
  │ │ └── plugin.rb # LintRoller::Plugin (auto-discovery)
73
38
  │ └── cop/legion/
74
- │ ├── helper_migration/ # 7 cops (lex-only)
39
+ │ ├── helper_migration/ # 13 cops (lex-only)
75
40
  │ ├── constant_safety/ # 4 cops (universal)
76
41
  │ ├── singleton/ # 1 cop (universal)
77
42
  │ ├── rescue_logging/ # 3 cops (universal)
78
- │ ├── framework/ # 7 cops (universal + library-specific)
79
- │ └── extension/ # 17 cops (lex-only)
43
+ │ ├── framework/ # 8 cops (universal + library-specific)
44
+ │ └── extension/ # 18 cops (lex-only)
80
45
  ├── config/
81
46
  │ └── default.yml # All cop defaults, Include/Exclude scoping
82
47
  └── spec/ # mirrors lib/ structure
83
48
  ```
84
49
 
85
- ## Key Implementation Details
86
-
87
- - Plugin entry point: `RuboCop::Legion::Plugin` (LintRoller-based, registered via gemspec metadata `default_lint_roller_plugin`)
88
- - All cops inherit from `RuboCop::Cop::Base`
89
- - Auto-correctable cops use `extend AutoCorrector`
90
- - AST matching via `def_node_matcher` and `def_node_search`
91
- - Specs use `RuboCop::RSpec::Support` with `:config` shared context and `expect_offense`/`expect_correction`
92
- - `BareRescue` and `NoCapture` skip rescue modifiers (inline `rescue`) to avoid syntax corruption
93
- - `NoCapture` has no auto-correct to prevent correction loop with `Lint/UselessAssignment`
94
- - `SilentCapture` skips `_`-prefixed variables (Ruby unused convention)
95
-
96
50
  ## Development
97
51
 
98
52
  ```bash
99
53
  bundle install
100
- bundle exec rspec # 322 specs
54
+ bundle exec rspec # 350 specs
101
55
  bundle exec rubocop # Self-linting
102
56
  ```
103
-
104
- ## Common Per-Repo Overrides
105
-
106
- ```yaml
107
- # Repos using Faraday JSON middleware (string keys, not Legion::JSON symbol keys)
108
- Legion/Framework/ApiStringKeys:
109
- Enabled: false
110
- ```
111
-
112
- ---
113
-
114
- **Maintained By**: Matthew Iverson (@Esity)
data/README.md CHANGED
@@ -85,19 +85,21 @@ No per-repo configuration needed for scoping. If a cop doesn't apply to your gem
85
85
  | Framework | `CacheTimeCoercion` | cache_get | convention | no | Time objects become Strings after cache round-trip |
86
86
  | Framework | `ApiStringKeys` | Legion::JSON.load | warning | yes | `Legion::JSON.load` returns symbol keys — use `body[:key]` |
87
87
 
88
- ### LEX Extensions Only (`lib/legion/extensions/**/*.rb`) — 29 cops
88
+ ### LEX Extensions Only (`lib/legion/extensions/**/*.rb`) — 31 cops
89
89
 
90
90
  | Department | Cop | Severity | Auto-fix | Description |
91
91
  |---|---|---|---|---|
92
92
  | HelperMigration | `DirectLogging` | warning | yes | Use `log.method` instead of `Legion::Logging.method` |
93
93
  | HelperMigration | `OldLoggingMethods` | warning | yes | Use `log.method` instead of deprecated `log_method` helpers |
94
- | HelperMigration | `DirectJson` | convention | yes | Use `json_load`/`json_dump` instead of `Legion::JSON` |
95
- | HelperMigration | `DirectCache` | warning | yes | Use `cache_get`/`cache_set` instead of `Legion::Cache` |
96
- | HelperMigration | `DirectLocalCache` | warning | yes | Use `local_cache_get`/`local_cache_set` instead of `Legion::Cache::Local` |
97
- | HelperMigration | `DirectCrypt` | warning | yes | Use `vault_get`/`vault_exist?` instead of `Legion::Crypt` |
94
+ | HelperMigration | `DirectJson` | convention | yes | Use `json_*` helpers instead of `Legion::JSON` methods |
95
+ | HelperMigration | `DirectCache` | warning | yes | Use `cache_*` helpers instead of `Legion::Cache` methods |
96
+ | HelperMigration | `DirectLocalCache` | warning | yes | Use `local_cache_*` helpers instead of `Legion::Cache::Local` methods |
97
+ | HelperMigration | `DirectCrypt` | warning | yes | Use `vault_*` helpers instead of `Legion::Crypt` methods |
98
98
  | HelperMigration | `LoggingGuard` | convention | no | Remove unnecessary `respond_to?(:log_warn)` / `defined?(Legion::Logging)` guards |
99
99
  | HelperMigration | `DirectData` | convention | yes | Use `data_connection`/`local_data_*` instead of `Legion::Data::Connection`/`Local` |
100
- | HelperMigration | `DirectLlmEmbed` | convention | yes | Use `llm_embed` instead of `Legion::LLM.embed` |
100
+ | HelperMigration | `DirectLlm` | convention | yes | Use `llm_*` helpers instead of `Legion::LLM` methods |
101
+ | HelperMigration | `DirectTransport` | convention | yes | Use `transport_*` helpers instead of `Legion::Transport::Connection`/`Spool` |
102
+ | HelperMigration | `DirectKnowledge` | convention | yes | Use `query_knowledge`/`ingest_knowledge` instead of `Legion::Apollo` methods |
101
103
  | HelperMigration | `RequireDefinedGuard` | convention | yes | Remove `if defined?(Legion::...)` guard from `require` statements |
102
104
  | HelperMigration | `DefinedTransportGuard` | convention | no | Use `transport_connected?` instead of `defined?(Legion::Transport)` |
103
105
  | Extension | `ActorSingularModule` | error | yes | Use `module Actor` (singular) — framework discovers `Actor`, not `Actors` |
@@ -119,7 +121,7 @@ No per-repo configuration needed for scoping. If a cop doesn't apply to your gem
119
121
  | Extension | `ActorEnabledSideEffects` | convention | no | `enabled?` runs during boot — keep side-effect-free |
120
122
  | Extension | `DataRequiredWithoutMigrations` | warning | no | `data_required?` returns true but migrations may be missing |
121
123
 
122
- **Total: 45 custom cops** across 6 departments, 19 auto-correctable.
124
+ **Total: 47 custom cops** across 6 departments, 21 auto-correctable.
123
125
 
124
126
  ### Bundled Plugins
125
127
 
data/config/default.yml CHANGED
@@ -20,28 +20,32 @@ Legion/HelperMigration/OldLoggingMethods:
20
20
  VersionAdded: '0.1'
21
21
 
22
22
  Legion/HelperMigration/DirectJson:
23
- Description: 'Use `json_load`/`json_dump` helpers instead of `Legion::JSON.load`/`.dump`.'
23
+ Description: 'Use `json_*` helpers instead of `Legion::JSON` methods.'
24
24
  Enabled: true
25
25
  Severity: convention
26
26
  VersionAdded: '0.1'
27
+ VersionChanged: '0.1.7'
27
28
 
28
29
  Legion/HelperMigration/DirectCache:
29
- Description: 'Use `cache_get`/`cache_set`/`cache_delete` helpers instead of `Legion::Cache` methods.'
30
+ Description: 'Use `cache_*` helpers instead of `Legion::Cache` methods.'
30
31
  Enabled: true
31
32
  Severity: warning
32
33
  VersionAdded: '0.1'
34
+ VersionChanged: '0.1.7'
33
35
 
34
36
  Legion/HelperMigration/DirectLocalCache:
35
- Description: 'Use `local_cache_get`/`local_cache_set` helpers instead of `Legion::Cache::Local` methods.'
37
+ Description: 'Use `local_cache_*` helpers instead of `Legion::Cache::Local` methods.'
36
38
  Enabled: true
37
39
  Severity: warning
38
40
  VersionAdded: '0.1'
41
+ VersionChanged: '0.1.7'
39
42
 
40
43
  Legion/HelperMigration/DirectCrypt:
41
- Description: 'Use `vault_get`/`vault_exist?` helpers instead of `Legion::Crypt` methods.'
44
+ Description: 'Use `vault_*` helpers instead of `Legion::Crypt` methods.'
42
45
  Enabled: true
43
46
  Severity: warning
44
47
  VersionAdded: '0.1'
48
+ VersionChanged: '0.1.7'
45
49
 
46
50
  Legion/HelperMigration/LoggingGuard:
47
51
  Description: 'Remove unnecessary `respond_to?(:log_warn)` or `defined?(Legion::Logging)` guards.'
@@ -55,11 +59,24 @@ Legion/HelperMigration/DirectData:
55
59
  Severity: convention
56
60
  VersionAdded: '0.1.4'
57
61
 
58
- Legion/HelperMigration/DirectLlmEmbed:
59
- Description: 'Use `llm_embed` helper instead of `Legion::LLM.embed`.'
62
+ Legion/HelperMigration/DirectLlm:
63
+ Description: 'Use `llm_*` helpers instead of `Legion::LLM` methods.'
60
64
  Enabled: true
61
65
  Severity: convention
62
66
  VersionAdded: '0.1.4'
67
+ VersionChanged: '0.1.7'
68
+
69
+ Legion/HelperMigration/DirectTransport:
70
+ Description: 'Use `transport_*` helpers instead of `Legion::Transport::Connection`/`Spool` methods.'
71
+ Enabled: true
72
+ Severity: convention
73
+ VersionAdded: '0.1.7'
74
+
75
+ Legion/HelperMigration/DirectKnowledge:
76
+ Description: 'Use `query_knowledge`/`ingest_knowledge` helpers instead of `Legion::Apollo` methods.'
77
+ Enabled: true
78
+ Severity: convention
79
+ VersionAdded: '0.1.7'
63
80
 
64
81
  Legion/HelperMigration/RequireDefinedGuard:
65
82
  Description: 'Remove `if defined?(Legion::...)` guard from `require`/`require_relative` statements.'
@@ -319,3 +336,30 @@ Legion/Extension/DefinitionCallMismatched:
319
336
  Enabled: true
320
337
  Severity: error
321
338
  VersionAdded: '0.1.4'
339
+
340
+ # Legion/Framework — N×N routing guard cops (Phase 6 enforcement)
341
+
342
+ Legion/Framework/NoUnderscorePrefixedKwargs:
343
+ Description: 'Ban underscore-prefixed kwargs and `**_rest` splats. Use plain kwarg, defaulted kwarg, or `**opts`.'
344
+ Enabled: true
345
+ Severity: warning
346
+ VersionAdded: '0.1.8'
347
+
348
+ Legion/Framework/NoInlineSettingDefaults:
349
+ Description: 'Flag `Legion::Settings[...] || <literal>` and shadow-default patterns. Defaults belong in `settings.rb`.'
350
+ Enabled: true
351
+ Severity: warning
352
+ VersionAdded: '0.1.8'
353
+
354
+ Legion/Framework/NoDirectDispatch:
355
+ Description: 'Ban `_direct` methods on `Legion::LLM` modules. All inference routes through the governed pipeline.'
356
+ Enabled: true
357
+ Severity: error
358
+ VersionAdded: '0.1.8'
359
+
360
+ Legion/Framework/NoShapeDuckTyping:
361
+ Description: 'Flag `respond_to?` on canonical shape methods and string-key fallback access. Disabled by default.'
362
+ Enabled: false
363
+ Severity: convention
364
+ VersionAdded: '0.1.8'
365
+ Safe: false
@@ -59,7 +59,11 @@ module RuboCop
59
59
 
60
60
  node.body.each_node(:send).any? do |send_node|
61
61
  send_node.method_name == :time && send_node.receiver.nil?
62
- end
62
+ end || defines_time_method?(node)
63
+ end
64
+
65
+ def defines_time_method?(node)
66
+ node.body.each_node(:def).any? { |def_node| def_node.method_name == :time }
63
67
  end
64
68
  end
65
69
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Legion
6
+ module Framework
7
+ # Bans defining methods matching `/_direct\z/` on `Legion::LLM` modules.
8
+ # Per G16, all pipeline bypasses (`chat_direct`, `embed_direct`,
9
+ # `structured_direct`) are removed — internal callers route through the
10
+ # governed pipeline with an appropriate profile.
11
+ #
12
+ # No auto-correct is provided because the fix requires routing through
13
+ # the inference pipeline.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # module Legion::LLM
18
+ # def chat_direct(message:)
19
+ # # bypasses metering, audit, ledger
20
+ # end
21
+ # end
22
+ #
23
+ # # bad
24
+ # module Legion::LLM
25
+ # def self.embed_direct(text:)
26
+ # end
27
+ # end
28
+ #
29
+ # # good
30
+ # module Legion::LLM
31
+ # def chat(message:)
32
+ # # routes through pipeline
33
+ # end
34
+ # end
35
+ class NoDirectDispatch < Base
36
+ MSG = 'Method `%<name>s` matches `/_direct\\z/` on a `Legion::LLM` module. ' \
37
+ 'Pipeline bypasses are not allowed — route through the governed pipeline.'
38
+
39
+ DIRECT_PATTERN = /_direct\z/
40
+
41
+ def on_def(node)
42
+ return unless node.method_name.to_s.match?(DIRECT_PATTERN)
43
+ return unless inside_legion_llm?(node)
44
+
45
+ add_offense(node.loc.name, message: format(MSG, name: node.method_name))
46
+ end
47
+
48
+ def on_defs(node)
49
+ return unless node.method_name.to_s.match?(DIRECT_PATTERN)
50
+ return unless inside_legion_llm?(node)
51
+
52
+ add_offense(node.loc.name, message: format(MSG, name: node.method_name))
53
+ end
54
+
55
+ private
56
+
57
+ def inside_legion_llm?(node)
58
+ node.each_ancestor(:module, :class).any? do |ancestor|
59
+ legion_llm_module?(ancestor)
60
+ end
61
+ end
62
+
63
+ def legion_llm_module?(mod_node)
64
+ return false unless mod_node.module_type?
65
+
66
+ # Check compact form: Legion::LLM or Legion::LLM::Something
67
+ ident = mod_node.identifier
68
+ return true if compact_legion_llm?(ident)
69
+
70
+ # Check nested form: module Legion; module LLM; ...
71
+ # The identifier is just `LLM` but parent is `module Legion`
72
+ return true if nested_legion_llm?(mod_node)
73
+
74
+ false
75
+ end
76
+
77
+ def compact_legion_llm?(ident)
78
+ parts = const_parts(ident)
79
+ parts.length >= 2 && parts[0] == :Legion && parts[1] == :LLM
80
+ end
81
+
82
+ def nested_legion_llm?(mod_node)
83
+ return false unless mod_node.identifier.const_type?
84
+ return false unless mod_node.identifier.children.last == :LLM
85
+
86
+ # Check if parent module is `Legion`
87
+ parent = mod_node.parent
88
+ return false unless parent&.module_type?
89
+ return false unless parent&.identifier&.const_type?
90
+
91
+ parent.identifier.children.last == :Legion
92
+ end
93
+
94
+ def const_parts(node)
95
+ return [] unless node&.const_type?
96
+
97
+ child = node.children.first
98
+ name = node.children.last
99
+ if child&.const_type?
100
+ const_parts(child) + [name]
101
+ else
102
+ [name]
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Legion
6
+ module Framework
7
+ # Detects `Legion::Settings[...]` reads combined with inline literal
8
+ # fallbacks (`|| <literal>`) or shadow-default patterns
9
+ # (`x = N unless x.positive?`). Per G13, all defaults belong in
10
+ # `settings.rb` — inline fallbacks are dead code after
11
+ # `register_defaults!` merge.
12
+ #
13
+ # No auto-correct is provided because the fix requires moving the
14
+ # literal into the appropriate `*_defaults` group in settings.rb.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # max_retries = Legion::Settings[:llm][:max_retries] || 3
19
+ # timeout = Legion::Settings[:timeout] || 30
20
+ #
21
+ # # bad (shadow default)
22
+ # max_retries = Legion::Settings[:llm][:max_retries]
23
+ # max_retries = 200 unless max_retries.positive?
24
+ #
25
+ # # good
26
+ # max_retries = Legion::Settings[:llm][:max_retries]
27
+ class NoInlineSettingDefaults < Base
28
+ MSG_OR = 'Inline default `%<default>s` after `Legion::Settings` read. ' \
29
+ 'Move the default into `settings.rb` instead.'
30
+ MSG_SHADOW = 'Shadow-default pattern after `Legion::Settings` read. ' \
31
+ 'Move the default into `settings.rb` instead.'
32
+
33
+ def on_or(node)
34
+ left = node.children.first
35
+ right = node.children.last
36
+ return unless settings_read?(left)
37
+ return unless right && literal?(right)
38
+
39
+ add_offense(node, message: format(MSG_OR, default: right.source))
40
+ end
41
+
42
+ def on_if(node)
43
+ cond = node.condition
44
+ return unless cond&.send_type?
45
+
46
+ receiver = cond.receiver
47
+ return unless receiver&.lvar_type?
48
+
49
+ method_name = cond.method_name
50
+ return unless %i[positive? nil? zero? empty? blank?].include?(method_name)
51
+
52
+ var_name = receiver.children.first
53
+ return unless preceded_by_settings_read?(node, var_name)
54
+
55
+ add_offense(node, message: MSG_SHADOW)
56
+ end
57
+
58
+ private
59
+
60
+ def settings_read?(node)
61
+ return false unless node&.send_type?
62
+ return true if direct_settings_access?(node)
63
+ return true if nested_settings_access?(node)
64
+
65
+ false
66
+ end
67
+
68
+ def direct_settings_access?(node)
69
+ node.method_name == :[] &&
70
+ node.receiver&.const_type? &&
71
+ node.receiver.children.last == :Settings &&
72
+ legion_const?(node.receiver.children.first)
73
+ end
74
+
75
+ def nested_settings_access?(node)
76
+ node.method_name == :[] &&
77
+ node.receiver&.send_type? &&
78
+ node.receiver.method_name == :[] &&
79
+ direct_settings_access?(node.receiver)
80
+ end
81
+
82
+ def legion_const?(node)
83
+ return true if node.nil?
84
+ return false unless node.const_type?
85
+
86
+ node.children.last == :Legion
87
+ end
88
+
89
+ def literal?(node)
90
+ %i[str int float sym].include?(node.type)
91
+ end
92
+
93
+ def preceded_by_settings_read?(if_node, var_name)
94
+ parent = if_node.parent
95
+ return false unless parent&.begin_type?
96
+
97
+ idx = parent.children.index(if_node)
98
+ return false unless idx&.positive?
99
+
100
+ prev = parent.children[idx - 1]
101
+ return false unless prev&.lvasgn_type?
102
+ return false unless prev.children.first == var_name
103
+
104
+ settings_read?(prev.children.last)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Legion
6
+ module Framework
7
+ # Flags `respond_to?` checks for canonical response shape methods
8
+ # (`thinking`, `tool_calls`, `content`, `stop_reason`) and string-key
9
+ # fallback access (`x[:k] || x['k']`). Per R10, downstream consumers
10
+ # of canonical types should rely on the struct interface, not duck-type.
11
+ #
12
+ # Ships **disabled by default**. Enable per-repo or per-directory once
13
+ # the canonical migration is complete.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # response.respond_to?(:thinking)
18
+ # response.respond_to?(:tool_calls)
19
+ # tc[:name] || tc['name']
20
+ #
21
+ # # good
22
+ # response.thinking
23
+ # response.tool_calls
24
+ # tc[:name]
25
+ class NoShapeDuckTyping < Base
26
+ MSG_RESPOND = 'Use canonical struct access instead of `respond_to?(:%<method>s)`. ' \
27
+ 'Downstream of translators, the shape is guaranteed.'
28
+ MSG_FALLBACK = 'String-key fallback `%<key>s` suggests duck-typed access. ' \
29
+ 'Use canonical struct access or symbol keys consistently.'
30
+
31
+ CANONICAL_METHODS = %w[thinking tool_calls content stop_reason].freeze
32
+
33
+ def on_send(node)
34
+ check_respond_to(node)
35
+ end
36
+
37
+ def on_or(node)
38
+ check_string_key_fallback(node)
39
+ end
40
+
41
+ private
42
+
43
+ def check_respond_to(node)
44
+ return unless node.method_name == :respond_to?
45
+ return unless node.arguments.length == 1
46
+
47
+ arg = node.arguments.first
48
+ method_name = case arg.type
49
+ when :sym then arg.value.to_s
50
+ when :str then arg.value
51
+ else return
52
+ end
53
+
54
+ return unless CANONICAL_METHODS.include?(method_name)
55
+
56
+ add_offense(node, message: format(MSG_RESPOND, method: method_name))
57
+ end
58
+
59
+ def check_string_key_fallback(node)
60
+ left = node.children.first
61
+ right = node.children.last
62
+ return unless left&.send_type? && right&.send_type?
63
+ return unless left.method_name == :[] && right.method_name == :[]
64
+
65
+ # Both must access the same receiver
66
+ left_recv = left.receiver
67
+ right_recv = right.receiver
68
+ return unless left_recv && right_recv
69
+ return unless same_receiver?(left_recv, right_recv)
70
+
71
+ # One must be sym, the other str
72
+ left_key = left.arguments.first
73
+ right_key = right.arguments.first
74
+ return unless left_key && right_key
75
+
76
+ has_sym = left_key.sym_type? || right_key.sym_type?
77
+ has_str = left_key.str_type? || right_key.str_type?
78
+ return unless has_sym && has_str
79
+
80
+ # Extract the key name from the symbol side
81
+ key_node = left_key.sym_type? ? left_key : right_key
82
+ return unless key_node
83
+
84
+ add_offense(node, message: format(MSG_FALLBACK, key: key_node.value))
85
+ end
86
+
87
+ def same_receiver?(left, right)
88
+ # Both lvars with same name
89
+ return true if left.lvar_type? && right.lvar_type? && left.children.first == right.children.first
90
+
91
+ # Both sends with same method name and no receiver (bare identifier)
92
+ left.send_type? && right.send_type? &&
93
+ left.receiver.nil? && right.receiver.nil? &&
94
+ left.method_name == right.method_name
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Legion
6
+ module Framework
7
+ # Bans underscore-prefixed keyword arguments and `**_rest`/`**_` splat
8
+ # parameters in method definitions. The N×N routing design requires
9
+ # explicit kwarg signatures: required → plain kwarg, optional → defaulted
10
+ # kwarg, passthrough → `**opts` at the end.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # def foo(_bar:); end
15
+ # def foo(**_rest); end
16
+ # def foo(**_); end
17
+ #
18
+ # # good
19
+ # def foo(bar:); end
20
+ # def foo(bar: nil); end
21
+ # def foo(**opts); end
22
+ class NoUnderscorePrefixedKwargs < Base
23
+ extend AutoCorrector
24
+
25
+ MSG_KWARG = 'Underscore-prefixed kwarg `%<name>s` is not allowed. ' \
26
+ 'Use plain kwarg (required), defaulted kwarg (optional), ' \
27
+ 'or `**opts` for passthrough.'
28
+ MSG_SPLAT = 'Underscore-prefixed kwarg splat `%<name>s` is not allowed. ' \
29
+ 'Use `**opts` for passthrough.'
30
+
31
+ def on_def(node)
32
+ check_params(node)
33
+ end
34
+
35
+ def on_defs(node)
36
+ check_params(node)
37
+ end
38
+
39
+ private
40
+
41
+ def check_params(method_node)
42
+ args = method_node.arguments
43
+ return unless args
44
+
45
+ args.each_child_node do |arg|
46
+ case arg.type
47
+ when :kwarg, :kwoptarg
48
+ check_kwarg(arg)
49
+ when :kwrestarg
50
+ check_kwrestarg(arg)
51
+ end
52
+ end
53
+ end
54
+
55
+ def check_kwarg(arg)
56
+ name = arg.children.first
57
+ return unless name.is_a?(Symbol)
58
+ return unless name.to_s.start_with?('_')
59
+
60
+ add_offense(arg, message: format(MSG_KWARG, name: name)) do |corrector|
61
+ corrected = name.to_s.sub(/\A_+/, '')
62
+ corrected = 'arg' if corrected.empty?
63
+ corrector.replace(arg.source_range, kwarg_replacement(corrected, arg))
64
+ end
65
+ end
66
+
67
+ def check_kwrestarg(arg)
68
+ name = arg.children.first
69
+ return unless name.is_a?(Symbol)
70
+ return unless name.to_s.start_with?('_')
71
+
72
+ add_offense(arg, message: format(MSG_SPLAT, name: name)) do |corrector|
73
+ corrector.replace(arg.source_range, '**opts')
74
+ end
75
+ end
76
+
77
+ def kwarg_replacement(name, arg)
78
+ if arg.kwoptarg_type?
79
+ default = arg.children.last
80
+ "#{name}: #{default.source}"
81
+ else
82
+ "#{name}:"
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -4,36 +4,42 @@ module RuboCop
4
4
  module Cop
5
5
  module Legion
6
6
  module HelperMigration
7
- # Detects direct calls to `Legion::Cache.get`, `.set`, and `.delete`
8
- # and suggests using the `cache_get`, `cache_set`, `cache_delete` helpers.
7
+ # Detects direct calls to `Legion::Cache` methods and suggests using
8
+ # the `cache_*` helpers instead.
9
9
  #
10
10
  # @example
11
11
  # # bad
12
12
  # Legion::Cache.get('key')
13
13
  # Legion::Cache.set('key', value)
14
14
  # Legion::Cache.delete('key')
15
+ # Legion::Cache.fetch('key') { compute }
16
+ # Legion::Cache.connected?
15
17
  #
16
18
  # # good
17
19
  # cache_get('key')
18
20
  # cache_set('key', value)
19
21
  # cache_delete('key')
22
+ # cache_fetch('key') { compute }
23
+ # cache_connected?
20
24
  class DirectCache < RuboCop::Cop::Base
21
25
  extend AutoCorrector
22
26
 
23
27
  MSG = 'Use `%<helper>s` instead of `Legion::Cache.%<method>s`. ' \
24
28
  'Include the appropriate cache helper mixin.'
25
29
 
26
- RESTRICT_ON_SEND = %i[get set delete].freeze
30
+ RESTRICT_ON_SEND = %i[get set delete fetch connected?].freeze
27
31
 
28
32
  HELPER_MAP = {
29
33
  get: 'cache_get',
30
34
  set: 'cache_set',
31
- delete: 'cache_delete'
35
+ delete: 'cache_delete',
36
+ fetch: 'cache_fetch',
37
+ connected?: 'cache_connected?'
32
38
  }.freeze
33
39
 
34
40
  # @!method legion_cache_call?(node)
35
41
  def_node_matcher :legion_cache_call?, <<~PATTERN
36
- (send (const (const nil? :Legion) :Cache) {:get :set :delete} ...)
42
+ (send (const (const nil? :Legion) :Cache) {:get :set :delete :fetch :connected?} ...)
37
43
  PATTERN
38
44
 
39
45
  def on_send(node)
@@ -44,8 +50,12 @@ module RuboCop
44
50
  message = format(MSG, helper: helper, method: method_name)
45
51
 
46
52
  add_offense(node, message: message) do |corrector|
47
- args_source = node.arguments.map(&:source).join(', ')
48
- corrector.replace(node, "#{helper}(#{args_source})")
53
+ if node.arguments.empty?
54
+ corrector.replace(node, helper)
55
+ else
56
+ args_source = node.arguments.map(&:source).join(', ')
57
+ corrector.replace(node, "#{helper}(#{args_source})")
58
+ end
49
59
  end
50
60
  end
51
61
  end
@@ -4,33 +4,36 @@ module RuboCop
4
4
  module Cop
5
5
  module Legion
6
6
  module HelperMigration
7
- # Detects direct calls to `Legion::Crypt.get` and `Legion::Crypt.exist?`
8
- # and suggests using the `vault_get` / `vault_exist?` helpers instead.
7
+ # Detects direct calls to `Legion::Crypt` methods and suggests using
8
+ # the `vault_*` helpers instead.
9
9
  #
10
10
  # @example
11
11
  # # bad
12
12
  # Legion::Crypt.get('secret/path')
13
13
  # Legion::Crypt.exist?('secret/path')
14
+ # Legion::Crypt.write('secret/path', data)
14
15
  #
15
16
  # # good
16
17
  # vault_get('secret/path')
17
18
  # vault_exist?('secret/path')
19
+ # vault_write('secret/path', data)
18
20
  class DirectCrypt < RuboCop::Cop::Base
19
21
  extend AutoCorrector
20
22
 
21
23
  MSG = 'Use `%<helper>s` instead of `Legion::Crypt.%<method>s`. ' \
22
24
  'Include the appropriate Vault/Crypt helper mixin.'
23
25
 
24
- RESTRICT_ON_SEND = %i[get exist?].freeze
26
+ RESTRICT_ON_SEND = %i[get exist? write].freeze
25
27
 
26
28
  HELPER_MAP = {
27
29
  get: 'vault_get',
28
- exist?: 'vault_exist?'
30
+ exist?: 'vault_exist?',
31
+ write: 'vault_write'
29
32
  }.freeze
30
33
 
31
34
  # @!method legion_crypt_call?(node)
32
35
  def_node_matcher :legion_crypt_call?, <<~PATTERN
33
- (send (const (const nil? :Legion) :Crypt) {:get :exist?} ...)
36
+ (send (const (const nil? :Legion) :Crypt) {:get :exist? :write} ...)
34
37
  PATTERN
35
38
 
36
39
  def on_send(node)
@@ -4,33 +4,42 @@ module RuboCop
4
4
  module Cop
5
5
  module Legion
6
6
  module HelperMigration
7
- # Detects direct calls to `Legion::JSON.load` and `Legion::JSON.dump`
8
- # and suggests using the `json_load` / `json_dump` helpers instead.
7
+ # Detects direct calls to `Legion::JSON` methods and suggests using
8
+ # the `json_*` helpers instead.
9
9
  #
10
10
  # @example
11
11
  # # bad
12
12
  # Legion::JSON.load(str)
13
13
  # Legion::JSON.dump(obj)
14
+ # Legion::JSON.parse(str)
15
+ # Legion::JSON.generate(obj)
16
+ # Legion::JSON.pretty_generate(obj)
14
17
  #
15
18
  # # good
16
19
  # json_load(str)
17
20
  # json_dump(obj)
21
+ # json_parse(str)
22
+ # json_generate(obj)
23
+ # json_pretty_generate(obj)
18
24
  class DirectJson < RuboCop::Cop::Base
19
25
  extend AutoCorrector
20
26
 
21
27
  MSG = 'Use `%<helper>s` instead of `Legion::JSON.%<method>s`. ' \
22
28
  'Include the appropriate JSON helper mixin.'
23
29
 
24
- RESTRICT_ON_SEND = %i[load dump].freeze
30
+ RESTRICT_ON_SEND = %i[load dump parse generate pretty_generate].freeze
25
31
 
26
32
  HELPER_MAP = {
27
33
  load: 'json_load',
28
- dump: 'json_dump'
34
+ dump: 'json_dump',
35
+ parse: 'json_parse',
36
+ generate: 'json_generate',
37
+ pretty_generate: 'json_pretty_generate'
29
38
  }.freeze
30
39
 
31
40
  # @!method legion_json_call?(node)
32
41
  def_node_matcher :legion_json_call?, <<~PATTERN
33
- (send (const (const nil? :Legion) :JSON) {:load :dump} ...)
42
+ (send (const (const nil? :Legion) :JSON) {:load :dump :parse :generate :pretty_generate} ...)
34
43
  PATTERN
35
44
 
36
45
  def on_send(node)
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Legion
6
+ module HelperMigration
7
+ # Detects direct calls to `Legion::Apollo` and `Legion::Apollo::Local`
8
+ # methods and suggests using the `query_knowledge` / `ingest_knowledge`
9
+ # helpers instead.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # Legion::Apollo.query(text: 'search')
14
+ # Legion::Apollo.ingest(content, tags: [:foo])
15
+ # Legion::Apollo::Local.query(text: 'search')
16
+ # Legion::Apollo::Local.ingest(content)
17
+ #
18
+ # # good
19
+ # query_knowledge(text: 'search')
20
+ # ingest_knowledge(content, tags: [:foo])
21
+ # query_knowledge(text: 'search', scope: :local)
22
+ # ingest_knowledge(content, scope: :local)
23
+ class DirectKnowledge < RuboCop::Cop::Base
24
+ extend AutoCorrector
25
+
26
+ MSG = 'Use `%<helper>s` instead of `%<receiver>s.%<method>s`. ' \
27
+ 'Include the knowledge helper mixin.'
28
+
29
+ RESTRICT_ON_SEND = %i[query ingest].freeze
30
+
31
+ GLOBAL_MAP = {
32
+ query: 'query_knowledge',
33
+ ingest: 'ingest_knowledge'
34
+ }.freeze
35
+
36
+ # @!method apollo_global_call?(node)
37
+ def_node_matcher :apollo_global_call?, <<~PATTERN
38
+ (send (const (const nil? :Legion) :Apollo) {:query :ingest} ...)
39
+ PATTERN
40
+
41
+ # @!method apollo_local_call?(node)
42
+ def_node_matcher :apollo_local_call?, <<~PATTERN
43
+ (send (const (const (const nil? :Legion) :Apollo) :Local) {:query :ingest} ...)
44
+ PATTERN
45
+
46
+ def on_send(node)
47
+ if apollo_global_call?(node)
48
+ register_global_offense(node)
49
+ elsif apollo_local_call?(node)
50
+ register_local_offense(node)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def register_global_offense(node)
57
+ method_name = node.method_name
58
+ helper = GLOBAL_MAP[method_name]
59
+ message = format(MSG, helper: helper, receiver: 'Legion::Apollo', method: method_name)
60
+
61
+ add_offense(node, message: message) do |corrector|
62
+ args_source = node.arguments.map(&:source).join(', ')
63
+ corrector.replace(node, "#{helper}(#{args_source})")
64
+ end
65
+ end
66
+
67
+ def register_local_offense(node)
68
+ method_name = node.method_name
69
+ helper = GLOBAL_MAP[method_name]
70
+ message = format(MSG, helper: helper, receiver: 'Legion::Apollo::Local', method: method_name)
71
+
72
+ add_offense(node, message: message) do |corrector|
73
+ args_source = node.arguments.map(&:source).join(', ')
74
+ scope_arg = args_source.empty? ? 'scope: :local' : "#{args_source}, scope: :local"
75
+ corrector.replace(node, "#{helper}(#{scope_arg})")
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Legion
6
+ module HelperMigration
7
+ # Detects direct calls to `Legion::LLM` methods and suggests using
8
+ # the `llm_*` helpers from `Legion::Extensions::Helpers::LLM`.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # Legion::LLM.embed(text)
13
+ # Legion::LLM.chat(message, intent: :moderate)
14
+ # Legion::LLM.ask(message: 'hello')
15
+ # Legion::LLM.structured(messages: [], schema: {})
16
+ # Legion::LLM.embed_batch(texts)
17
+ #
18
+ # # good
19
+ # llm_embed(text)
20
+ # llm_chat(message, intent: :moderate)
21
+ # llm_ask(message: 'hello')
22
+ # llm_structured(messages: [], schema: {})
23
+ # llm_embed_batch(texts)
24
+ class DirectLlm < RuboCop::Cop::Base
25
+ extend AutoCorrector
26
+
27
+ MSG = 'Use `%<helper>s` instead of `Legion::LLM.%<method>s`. ' \
28
+ 'Include the LLM helper mixin.'
29
+
30
+ RESTRICT_ON_SEND = %i[embed chat ask structured embed_batch].freeze
31
+
32
+ HELPER_MAP = {
33
+ embed: 'llm_embed',
34
+ chat: 'llm_chat',
35
+ ask: 'llm_ask',
36
+ structured: 'llm_structured',
37
+ embed_batch: 'llm_embed_batch'
38
+ }.freeze
39
+
40
+ # @!method legion_llm_call?(node)
41
+ def_node_matcher :legion_llm_call?, <<~PATTERN
42
+ (send (const (const nil? :Legion) :LLM) {:embed :chat :ask :structured :embed_batch} ...)
43
+ PATTERN
44
+
45
+ def on_send(node)
46
+ return unless legion_llm_call?(node)
47
+
48
+ method_name = node.method_name
49
+ helper = HELPER_MAP[method_name]
50
+ message = format(MSG, helper: helper, method: method_name)
51
+
52
+ add_offense(node, message: message) do |corrector|
53
+ args_source = node.arguments.map(&:source).join(', ')
54
+ corrector.replace(node, "#{helper}(#{args_source})")
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -4,33 +4,39 @@ module RuboCop
4
4
  module Cop
5
5
  module Legion
6
6
  module HelperMigration
7
- # Detects direct calls to `Legion::Cache::Local.get` and `.set`
8
- # and suggests using the `local_cache_get` / `local_cache_set` helpers.
7
+ # Detects direct calls to `Legion::Cache::Local` methods and suggests
8
+ # using the `local_cache_*` helpers instead.
9
9
  #
10
10
  # @example
11
11
  # # bad
12
12
  # Legion::Cache::Local.get('key')
13
13
  # Legion::Cache::Local.set('key', value)
14
+ # Legion::Cache::Local.delete('key')
15
+ # Legion::Cache::Local.fetch('key') { compute }
14
16
  #
15
17
  # # good
16
18
  # local_cache_get('key')
17
19
  # local_cache_set('key', value)
20
+ # local_cache_delete('key')
21
+ # local_cache_fetch('key') { compute }
18
22
  class DirectLocalCache < RuboCop::Cop::Base
19
23
  extend AutoCorrector
20
24
 
21
25
  MSG = 'Use `%<helper>s` instead of `Legion::Cache::Local.%<method>s`. ' \
22
26
  'Include the appropriate local cache helper mixin.'
23
27
 
24
- RESTRICT_ON_SEND = %i[get set].freeze
28
+ RESTRICT_ON_SEND = %i[get set delete fetch].freeze
25
29
 
26
30
  HELPER_MAP = {
27
31
  get: 'local_cache_get',
28
- set: 'local_cache_set'
32
+ set: 'local_cache_set',
33
+ delete: 'local_cache_delete',
34
+ fetch: 'local_cache_fetch'
29
35
  }.freeze
30
36
 
31
37
  # @!method legion_local_cache_call?(node)
32
38
  def_node_matcher :legion_local_cache_call?, <<~PATTERN
33
- (send (const (const (const nil? :Legion) :Cache) :Local) {:get :set} ...)
39
+ (send (const (const (const nil? :Legion) :Cache) :Local) {:get :set :delete :fetch} ...)
34
40
  PATTERN
35
41
 
36
42
  def on_send(node)
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Legion
6
+ module HelperMigration
7
+ # Detects direct calls to `Legion::Transport::Connection` methods
8
+ # and suggests using the `transport_*` helpers instead.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # Legion::Transport::Connection.session_open?
13
+ # Legion::Transport::Connection.channel_open?
14
+ # Legion::Transport::Connection.lite_mode?
15
+ # Legion::Transport::Connection.channel
16
+ #
17
+ # # good
18
+ # transport_session_open?
19
+ # transport_channel_open?
20
+ # transport_lite_mode?
21
+ # transport_channel
22
+ class DirectTransport < RuboCop::Cop::Base
23
+ extend AutoCorrector
24
+
25
+ MSG = 'Use `%<helper>s` instead of `%<receiver>s.%<method>s`. ' \
26
+ 'Include the transport helper mixin.'
27
+
28
+ RESTRICT_ON_SEND = %i[session_open? channel_open? lite_mode? channel count].freeze
29
+
30
+ CONNECTION_MAP = {
31
+ session_open?: 'transport_session_open?',
32
+ channel_open?: 'transport_channel_open?',
33
+ lite_mode?: 'transport_lite_mode?',
34
+ channel: 'transport_channel'
35
+ }.freeze
36
+
37
+ SPOOL_MAP = {
38
+ count: 'transport_spool_count'
39
+ }.freeze
40
+
41
+ # @!method transport_connection_call?(node)
42
+ def_node_matcher :transport_connection_call?, <<~PATTERN
43
+ (send (const (const (const nil? :Legion) :Transport) :Connection) {:session_open? :channel_open? :lite_mode? :channel} ...)
44
+ PATTERN
45
+
46
+ # @!method transport_spool_call?(node)
47
+ def_node_matcher :transport_spool_call?, <<~PATTERN
48
+ (send (const (const (const nil? :Legion) :Transport) :Spool) :count ...)
49
+ PATTERN
50
+
51
+ def on_send(node)
52
+ if transport_connection_call?(node)
53
+ register_offense(node, 'Legion::Transport::Connection', CONNECTION_MAP)
54
+ elsif transport_spool_call?(node)
55
+ register_offense(node, 'Legion::Transport::Spool', SPOOL_MAP)
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def register_offense(node, receiver, helper_map)
62
+ method_name = node.method_name
63
+ helper = helper_map[method_name]
64
+ message = format(MSG, helper: helper, receiver: receiver, method: method_name)
65
+
66
+ add_offense(node, message: message) do |corrector|
67
+ args_source = node.arguments.map(&:source).join(', ')
68
+ replacement = node.arguments.empty? ? helper : "#{helper}(#{args_source})"
69
+ corrector.replace(node, replacement)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Legion
5
- VERSION = '0.1.6'
5
+ VERSION = '0.1.8'
6
6
  end
7
7
  end
@@ -15,7 +15,9 @@ require 'rubocop/cop/legion/helper_migration/direct_local_cache'
15
15
  require 'rubocop/cop/legion/helper_migration/direct_crypt'
16
16
  require 'rubocop/cop/legion/helper_migration/logging_guard'
17
17
  require 'rubocop/cop/legion/helper_migration/direct_data'
18
- require 'rubocop/cop/legion/helper_migration/direct_llm_embed'
18
+ require 'rubocop/cop/legion/helper_migration/direct_llm'
19
+ require 'rubocop/cop/legion/helper_migration/direct_transport'
20
+ require 'rubocop/cop/legion/helper_migration/direct_knowledge'
19
21
  require 'rubocop/cop/legion/helper_migration/require_defined_guard'
20
22
  require 'rubocop/cop/legion/helper_migration/defined_transport_guard'
21
23
 
@@ -42,6 +44,10 @@ require 'rubocop/cop/legion/framework/module_function_private'
42
44
  require 'rubocop/cop/legion/framework/cache_time_coercion'
43
45
  require 'rubocop/cop/legion/framework/api_string_keys'
44
46
  require 'rubocop/cop/legion/framework/mutex_nested_sync'
47
+ require 'rubocop/cop/legion/framework/no_underscore_prefixed_kwargs'
48
+ require 'rubocop/cop/legion/framework/no_inline_setting_defaults'
49
+ require 'rubocop/cop/legion/framework/no_direct_dispatch'
50
+ require 'rubocop/cop/legion/framework/no_shape_duck_typing'
45
51
 
46
52
  # Legion/Extension
47
53
  require 'rubocop/cop/legion/extension/actor_singular_module'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-legion
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -144,6 +144,10 @@ files:
144
144
  - lib/rubocop/cop/legion/framework/faraday_xml_middleware.rb
145
145
  - lib/rubocop/cop/legion/framework/module_function_private.rb
146
146
  - lib/rubocop/cop/legion/framework/mutex_nested_sync.rb
147
+ - lib/rubocop/cop/legion/framework/no_direct_dispatch.rb
148
+ - lib/rubocop/cop/legion/framework/no_inline_setting_defaults.rb
149
+ - lib/rubocop/cop/legion/framework/no_shape_duck_typing.rb
150
+ - lib/rubocop/cop/legion/framework/no_underscore_prefixed_kwargs.rb
147
151
  - lib/rubocop/cop/legion/framework/sinatra_host_auth.rb
148
152
  - lib/rubocop/cop/legion/framework/thor_reserved_run.rb
149
153
  - lib/rubocop/cop/legion/helper_migration/defined_transport_guard.rb
@@ -151,9 +155,11 @@ files:
151
155
  - lib/rubocop/cop/legion/helper_migration/direct_crypt.rb
152
156
  - lib/rubocop/cop/legion/helper_migration/direct_data.rb
153
157
  - lib/rubocop/cop/legion/helper_migration/direct_json.rb
154
- - lib/rubocop/cop/legion/helper_migration/direct_llm_embed.rb
158
+ - lib/rubocop/cop/legion/helper_migration/direct_knowledge.rb
159
+ - lib/rubocop/cop/legion/helper_migration/direct_llm.rb
155
160
  - lib/rubocop/cop/legion/helper_migration/direct_local_cache.rb
156
161
  - lib/rubocop/cop/legion/helper_migration/direct_logging.rb
162
+ - lib/rubocop/cop/legion/helper_migration/direct_transport.rb
157
163
  - lib/rubocop/cop/legion/helper_migration/logging_guard.rb
158
164
  - lib/rubocop/cop/legion/helper_migration/old_logging_methods.rb
159
165
  - lib/rubocop/cop/legion/helper_migration/require_defined_guard.rb
@@ -189,7 +195,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
195
  - !ruby/object:Gem::Version
190
196
  version: '0'
191
197
  requirements: []
192
- rubygems_version: 4.0.8
198
+ rubygems_version: 3.6.9
193
199
  specification_version: 4
194
200
  summary: LegionIO code quality cops for RuboCop
195
201
  test_files: []
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Cop
5
- module Legion
6
- module HelperMigration
7
- # Detects direct calls to `Legion::LLM.embed` and suggests using the
8
- # `llm_embed` helper from `Legion::Extensions::Helpers::LLM`.
9
- #
10
- # @example
11
- # # bad
12
- # Legion::LLM.embed(text)
13
- # Legion::LLM.embed(text, provider: :bedrock)
14
- #
15
- # # good
16
- # llm_embed(text)
17
- # llm_embed(text, provider: :bedrock)
18
- class DirectLlmEmbed < RuboCop::Cop::Base
19
- extend AutoCorrector
20
-
21
- MSG = 'Use `llm_embed` instead of `Legion::LLM.embed`. ' \
22
- 'Include `Legion::Extensions::Helpers::LLM` via the LLM helper mixin.'
23
-
24
- RESTRICT_ON_SEND = %i[embed].freeze
25
-
26
- # @!method legion_llm_embed?(node)
27
- def_node_matcher :legion_llm_embed?, <<~PATTERN
28
- (send (const (const nil? :Legion) :LLM) :embed ...)
29
- PATTERN
30
-
31
- def on_send(node)
32
- return unless legion_llm_embed?(node)
33
-
34
- add_offense(node, message: MSG) do |corrector|
35
- args_source = node.arguments.map(&:source).join(', ')
36
- corrector.replace(node, "llm_embed(#{args_source})")
37
- end
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end