rubocop-legion 0.1.8 → 0.1.10
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/CHANGELOG.md +7 -0
- data/config/default.yml +35 -0
- data/lib/rubocop/cop/legion/llm/no_loop_do.rb +41 -0
- data/lib/rubocop/cop/legion/llm/rescue_log_level.rb +61 -0
- data/lib/rubocop/cop/legion/llm/settings_access_path.rb +62 -0
- data/lib/rubocop/cop/legion/llm/taxonomy_enum.rb +102 -0
- data/lib/rubocop/legion/version.rb +1 -1
- data/lib/rubocop-legion.rb +6 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d9316d83d8676fe5adb80fd7500f3daece09203ece25f0ce0c81887d19982ca
|
|
4
|
+
data.tar.gz: 646cab78aebc73b233882dcfc594d6c0efbea0acb3e2881e02a1f888ec5513e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 83f63897f48898014d992e98b11cdfd9f2b830e405b2fd49bff484acffdaaeafd853dea8016baaa280787302a0c15a701e2f1d0c71274d1bf0fa04761574e8bd
|
|
7
|
+
data.tar.gz: 69aa65b8634cef75041fe37a57b87ca54bdc911a59fc12710193a66550b71c01926831618ef9100b775c5fc498d0705d8eaab50af11c5c1446d36ab801be51ae
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.10] - 2026-07-31
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `Legion/Llm/TaxonomyEnum` no longer over-matches: `:type` is only validated when it appears alongside `:tier` or `:circuit_state` in the same hash (taxonomy context). Previously any `{ type: :string }` or schema hash triggered a false positive.
|
|
7
|
+
- Added `Legion/Llm` department to `config/default.yml` — all four cops (`TaxonomyEnum`, `SettingsAccessPath`, `NoLoopDo`, `RescueLogLevel`) were loaded but never registered in the config, causing consumer repos to report "unrecognized cop" when referencing `Legion/Llm/TaxonomyEnum`.
|
|
8
|
+
- `TaxonomyEnum` is now Include-scoped to `lib/legion/llm/**/*.rb` and `lib/legion/extensions/llm/**/*.rb` so it only runs on LLM routing code.
|
|
9
|
+
|
|
3
10
|
## [0.1.8] - 2026-06-16
|
|
4
11
|
|
|
5
12
|
### Added
|
data/config/default.yml
CHANGED
|
@@ -363,3 +363,38 @@ Legion/Framework/NoShapeDuckTyping:
|
|
|
363
363
|
Severity: convention
|
|
364
364
|
VersionAdded: '0.1.8'
|
|
365
365
|
Safe: false
|
|
366
|
+
|
|
367
|
+
# Legion/Llm — cops specific to legion-llm and lex-llm-* provider pipelines
|
|
368
|
+
|
|
369
|
+
Legion/Llm:
|
|
370
|
+
Enabled: true
|
|
371
|
+
Include:
|
|
372
|
+
- 'lib/legion/llm/**/*.rb'
|
|
373
|
+
- 'lib/legion/extensions/llm/**/*.rb'
|
|
374
|
+
|
|
375
|
+
Legion/Llm/TaxonomyEnum:
|
|
376
|
+
Description: 'Validate :tier, :type, and :circuit_state literals against LegionIO taxonomy enums.'
|
|
377
|
+
Enabled: true
|
|
378
|
+
Severity: convention
|
|
379
|
+
VersionAdded: '0.1.9'
|
|
380
|
+
Include:
|
|
381
|
+
- 'lib/legion/llm/**/*.rb'
|
|
382
|
+
- 'lib/legion/extensions/llm/**/*.rb'
|
|
383
|
+
|
|
384
|
+
Legion/Llm/SettingsAccessPath:
|
|
385
|
+
Description: 'Use `Legion::Settings.dig(...)` instead of `Legion::Settings.loader.settings[...][...]`.'
|
|
386
|
+
Enabled: true
|
|
387
|
+
Severity: convention
|
|
388
|
+
VersionAdded: '0.1.9'
|
|
389
|
+
|
|
390
|
+
Legion/Llm/NoLoopDo:
|
|
391
|
+
Description: 'Ban `loop do` — use bounded iteration (`N.times`, `each`, or `while` with explicit bound).'
|
|
392
|
+
Enabled: true
|
|
393
|
+
Severity: warning
|
|
394
|
+
VersionAdded: '0.1.9'
|
|
395
|
+
|
|
396
|
+
Legion/Llm/RescueLogLevel:
|
|
397
|
+
Description: 'Minimum log level for `handle_exception` in rescue blocks is `:warn`.'
|
|
398
|
+
Enabled: true
|
|
399
|
+
Severity: warning
|
|
400
|
+
VersionAdded: '0.1.9'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Llm
|
|
7
|
+
# Flags `loop do` in legion-llm production code. Unbounded loops have caused
|
|
8
|
+
# runaway threads and hard-to-debug hangs. Use `N.times`, `each`, or a
|
|
9
|
+
# `while` with an explicit counter instead.
|
|
10
|
+
#
|
|
11
|
+
# No auto-correct is provided — the fix requires choosing the correct bound.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad
|
|
15
|
+
# loop do
|
|
16
|
+
# attempt = dispatch_request
|
|
17
|
+
# break if attempt.success?
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# # good
|
|
21
|
+
# MAX_ATTEMPTS.times do
|
|
22
|
+
# attempt = dispatch_request
|
|
23
|
+
# break if attempt.success?
|
|
24
|
+
# end
|
|
25
|
+
class NoLoopDo < Base
|
|
26
|
+
MSG = '`loop do` is prohibited — use bounded iteration (`N.times`, `each`, ' \
|
|
27
|
+
'or `while` with an explicit decrement) instead.'
|
|
28
|
+
|
|
29
|
+
def on_block(node)
|
|
30
|
+
return unless node.send_node.method_name == :loop
|
|
31
|
+
return unless node.send_node.receiver.nil?
|
|
32
|
+
|
|
33
|
+
add_offense(node.send_node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
alias on_numblock on_block
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Llm
|
|
7
|
+
# Flags `handle_exception(e, level: :debug)` in rescue blocks. Exception
|
|
8
|
+
# handlers must use at least `:warn` so that errors are visible in
|
|
9
|
+
# production logs without enabling debug mode.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# rescue StandardError => e
|
|
14
|
+
# handle_exception(e, level: :debug, operation: 'foo')
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# rescue StandardError => e
|
|
19
|
+
# handle_exception(e, level: :warn, operation: 'foo')
|
|
20
|
+
# end
|
|
21
|
+
class RescueLogLevel < Base
|
|
22
|
+
MSG = '`handle_exception` called with `level: :debug` in a rescue. ' \
|
|
23
|
+
'Minimum log level for exceptions is `:warn`.'
|
|
24
|
+
|
|
25
|
+
DEBUG_LEVELS = %i[debug trace verbose].freeze
|
|
26
|
+
|
|
27
|
+
def on_send(node)
|
|
28
|
+
return unless node.method_name == :handle_exception
|
|
29
|
+
return unless inside_rescue?(node)
|
|
30
|
+
return unless debug_level?(node)
|
|
31
|
+
|
|
32
|
+
add_offense(node)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def debug_level?(node)
|
|
38
|
+
pairs = node.arguments.flat_map do |arg|
|
|
39
|
+
if arg.hash_type?
|
|
40
|
+
arg.children
|
|
41
|
+
elsif arg.pair_type?
|
|
42
|
+
[arg]
|
|
43
|
+
else
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
pairs.any? do |pair|
|
|
48
|
+
key, val = pair.children
|
|
49
|
+
key.sym_type? && key.value == :level &&
|
|
50
|
+
val.sym_type? && DEBUG_LEVELS.include?(val.value)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def inside_rescue?(node)
|
|
55
|
+
node.each_ancestor(:resbody).any?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Llm
|
|
7
|
+
# Flags direct write-path access to `Legion::Settings.loader.settings[...][...]`
|
|
8
|
+
# outside test files. Production code must use `Legion::Settings.dig(...)` so all
|
|
9
|
+
# reads go through the typed, validated accessor.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# Legion::Settings.loader.settings[:extensions][:llm][:default_model] = 'gemma-12b'
|
|
14
|
+
# Legion::Settings.loader.settings[:llm][:timeout]
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# Legion::Settings.dig(:extensions, :llm, :default_model)
|
|
18
|
+
class SettingsAccessPath < Base
|
|
19
|
+
MSG = 'Use `Legion::Settings.dig(...)` instead of ' \
|
|
20
|
+
'`Legion::Settings.loader.settings[...][...]`.'
|
|
21
|
+
|
|
22
|
+
def on_send(node)
|
|
23
|
+
return unless loader_settings_chain?(node)
|
|
24
|
+
|
|
25
|
+
add_offense(node)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def loader_settings_chain?(node)
|
|
31
|
+
return false unless %i[[] []=].include?(node.method_name)
|
|
32
|
+
|
|
33
|
+
# For []=, the second index arg and the value are children[1] and [2]
|
|
34
|
+
# The receiver of the outer []= is the inner [] node
|
|
35
|
+
receiver = node.receiver
|
|
36
|
+
return false unless receiver&.send_type? && receiver.method_name == :[]
|
|
37
|
+
|
|
38
|
+
# receiver.receiver should be Legion::Settings.loader.settings
|
|
39
|
+
inner = receiver.receiver
|
|
40
|
+
return false unless inner&.send_type? && inner.method_name == :settings
|
|
41
|
+
|
|
42
|
+
loader_recv = inner.receiver
|
|
43
|
+
return false unless loader_recv&.send_type? && loader_recv.method_name == :loader
|
|
44
|
+
|
|
45
|
+
settings_const?(loader_recv.receiver)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def settings_const?(node)
|
|
49
|
+
return false unless node&.const_type?
|
|
50
|
+
return false unless node.children.last == :Settings
|
|
51
|
+
|
|
52
|
+
parent = node.children.first
|
|
53
|
+
return true if parent.nil?
|
|
54
|
+
return false unless parent.const_type? && parent.children.last == :Legion
|
|
55
|
+
|
|
56
|
+
parent.children.first.nil?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Legion
|
|
6
|
+
module Llm
|
|
7
|
+
# Validates lane field literals against the LegionIO taxonomy enums.
|
|
8
|
+
# Flags `:tier`, `:type`, and `:circuit_state` literals that are not in
|
|
9
|
+
# the known-valid sets, catching typos at lint time.
|
|
10
|
+
#
|
|
11
|
+
# `:tier` and `:circuit_state` are domain-specific keys and are always
|
|
12
|
+
# validated. `:type` is only validated when it appears in a hash that
|
|
13
|
+
# also contains `:tier` or `:circuit_state`, since `:type` alone is too
|
|
14
|
+
# common in non-taxonomy contexts.
|
|
15
|
+
#
|
|
16
|
+
# Valid values (from Inventory::DEFAULT_PROVIDER_TIERS and the SSOT design):
|
|
17
|
+
# tier: :local, :fleet, :cloud, :frontier, :direct
|
|
18
|
+
# type: :inference, :embedding, :moderation, :rerank, :image
|
|
19
|
+
# circuit_state: :closed, :open, :half_open
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# # bad (typo)
|
|
23
|
+
# { tier: :fronteir }
|
|
24
|
+
# health[:circuit_state] = :half_opened
|
|
25
|
+
# { tier: :cloud, type: :inferencing }
|
|
26
|
+
#
|
|
27
|
+
# # good
|
|
28
|
+
# { tier: :frontier }
|
|
29
|
+
# health[:circuit_state] = :half_open
|
|
30
|
+
# { tier: :cloud, type: :inference }
|
|
31
|
+
#
|
|
32
|
+
# # ignored (`:type` alone, no taxonomy context)
|
|
33
|
+
# { type: :string }
|
|
34
|
+
# { type: :integer }
|
|
35
|
+
class TaxonomyEnum < Base
|
|
36
|
+
VALID_TIERS = %i[local fleet cloud frontier direct].freeze
|
|
37
|
+
VALID_TYPES = %i[inference embedding moderation rerank image].freeze
|
|
38
|
+
VALID_CIRCUIT_STATES = %i[closed open half_open].freeze
|
|
39
|
+
|
|
40
|
+
TAXONOMY_KEYS = %i[tier type circuit_state].freeze
|
|
41
|
+
CONTEXT_KEYS = %i[tier circuit_state].freeze
|
|
42
|
+
|
|
43
|
+
MSG_TIER = 'Unknown tier `%<val>s`. Valid: %<valid>s.'
|
|
44
|
+
MSG_TYPE = 'Unknown type `%<val>s`. Valid: %<valid>s.'
|
|
45
|
+
MSG_CIRCUIT = 'Unknown circuit_state `%<val>s`. Valid: %<valid>s.'
|
|
46
|
+
|
|
47
|
+
def on_pair(node)
|
|
48
|
+
key, value = node.children
|
|
49
|
+
return unless key.sym_type? && value.sym_type?
|
|
50
|
+
|
|
51
|
+
check_taxonomy(key.value, value, node)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def on_send(node)
|
|
55
|
+
return unless node.method_name == :[]=
|
|
56
|
+
return unless node.arguments.size == 2
|
|
57
|
+
|
|
58
|
+
key_node, val_node = node.arguments
|
|
59
|
+
return unless key_node.sym_type? && val_node.sym_type?
|
|
60
|
+
|
|
61
|
+
check_taxonomy(key_node.value, val_node, node)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def check_taxonomy(key, val_node, parent_node)
|
|
67
|
+
val = val_node.value
|
|
68
|
+
case key
|
|
69
|
+
when :tier
|
|
70
|
+
return if VALID_TIERS.include?(val)
|
|
71
|
+
|
|
72
|
+
add_offense(parent_node,
|
|
73
|
+
message: format(MSG_TIER, val: val, valid: VALID_TIERS.join(', ')))
|
|
74
|
+
when :type
|
|
75
|
+
return if VALID_TYPES.include?(val)
|
|
76
|
+
return unless taxonomy_context?(parent_node)
|
|
77
|
+
|
|
78
|
+
add_offense(parent_node,
|
|
79
|
+
message: format(MSG_TYPE, val: val, valid: VALID_TYPES.join(', ')))
|
|
80
|
+
when :circuit_state
|
|
81
|
+
return if VALID_CIRCUIT_STATES.include?(val)
|
|
82
|
+
|
|
83
|
+
add_offense(parent_node,
|
|
84
|
+
message: format(MSG_CIRCUIT, val: val, valid: VALID_CIRCUIT_STATES.join(', ')))
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def taxonomy_context?(pair_node)
|
|
89
|
+
hash_node = pair_node.parent
|
|
90
|
+
return false unless hash_node&.hash_type?
|
|
91
|
+
|
|
92
|
+
sibling_keys = hash_node.pairs.filter_map do |pair|
|
|
93
|
+
pair.key.value if pair.key.sym_type?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sibling_keys.intersect?(CONTEXT_KEYS)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/rubocop-legion.rb
CHANGED
|
@@ -68,3 +68,9 @@ require 'rubocop/cop/legion/extension/absorber_missing_pattern'
|
|
|
68
68
|
require 'rubocop/cop/legion/extension/absorber_missing_absorb_method'
|
|
69
69
|
require 'rubocop/cop/legion/extension/definition_call_mismatched'
|
|
70
70
|
require 'rubocop/cop/legion/extension/actor_enabled_side_effects'
|
|
71
|
+
|
|
72
|
+
# Legion/Llm
|
|
73
|
+
require 'rubocop/cop/legion/llm/settings_access_path'
|
|
74
|
+
require 'rubocop/cop/legion/llm/no_loop_do'
|
|
75
|
+
require 'rubocop/cop/legion/llm/rescue_log_level'
|
|
76
|
+
require 'rubocop/cop/legion/llm/taxonomy_enum'
|
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.
|
|
4
|
+
version: 0.1.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -163,6 +163,10 @@ files:
|
|
|
163
163
|
- lib/rubocop/cop/legion/helper_migration/logging_guard.rb
|
|
164
164
|
- lib/rubocop/cop/legion/helper_migration/old_logging_methods.rb
|
|
165
165
|
- lib/rubocop/cop/legion/helper_migration/require_defined_guard.rb
|
|
166
|
+
- lib/rubocop/cop/legion/llm/no_loop_do.rb
|
|
167
|
+
- lib/rubocop/cop/legion/llm/rescue_log_level.rb
|
|
168
|
+
- lib/rubocop/cop/legion/llm/settings_access_path.rb
|
|
169
|
+
- lib/rubocop/cop/legion/llm/taxonomy_enum.rb
|
|
166
170
|
- lib/rubocop/cop/legion/rescue_logging/bare_rescue.rb
|
|
167
171
|
- lib/rubocop/cop/legion/rescue_logging/no_capture.rb
|
|
168
172
|
- lib/rubocop/cop/legion/rescue_logging/silent_capture.rb
|