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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '0668cc51aab49685f7ecb368113a7103253ec4d3aca22de2970c88ff4ae351f3'
4
- data.tar.gz: abc9644c2e563387778df1459aa7abd81f49e79c14e7cc13e038546ceab40a56
3
+ metadata.gz: 4d9316d83d8676fe5adb80fd7500f3daece09203ece25f0ce0c81887d19982ca
4
+ data.tar.gz: 646cab78aebc73b233882dcfc594d6c0efbea0acb3e2881e02a1f888ec5513e4
5
5
  SHA512:
6
- metadata.gz: 43d9efec4fb9f8fc5af8eced138ed626c29211017382b4c71b84f3191b2b5316624e64821e657cf4e8a61426f51404c147c32522d623c3cce3c50d51f9738afe
7
- data.tar.gz: 17807228e3e91cd2e4ac0be778a469256adbe691e7b0f1b6399344cb6d1ecffe6e699a5db7ffcf274a42d1a0df531842b32e218f89230d1ae1ced828ef733fbd
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Legion
5
- VERSION = '0.1.8'
5
+ VERSION = '0.1.10'
6
6
  end
7
7
  end
@@ -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.8
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