rubocop-legion 0.1.8 → 0.1.9

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: 88034e8168ad8e5634a580f3a07ca2f030bd46be31b6c9607dbc64a28935bcd0
4
+ data.tar.gz: 353647b902a3271fe17809769674778b528eb3e97dda8704934addf80b69b397
5
5
  SHA512:
6
- metadata.gz: 43d9efec4fb9f8fc5af8eced138ed626c29211017382b4c71b84f3191b2b5316624e64821e657cf4e8a61426f51404c147c32522d623c3cce3c50d51f9738afe
7
- data.tar.gz: 17807228e3e91cd2e4ac0be778a469256adbe691e7b0f1b6399344cb6d1ecffe6e699a5db7ffcf274a42d1a0df531842b32e218f89230d1ae1ced828ef733fbd
6
+ metadata.gz: 0b7d05703e9bd6762628d25f082a738fba9972f66eda2b650ab5e3c0dace6b3569df16c754f935f8f62caf861eaf3984018c3a4f3f73de01cc0871fcf91cfebd
7
+ data.tar.gz: d598e5090c4ba1242ec371fd2d3b935412f9fe8c4e24fe907a9e18bd6052999568bed0c252a983559e4b33e1c0502a724e02df5244db46b98c5cdad899f62e93
@@ -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,77 @@
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
+ # Valid values (from Inventory::DEFAULT_PROVIDER_TIERS and the SSOT design):
12
+ # tier: :local, :fleet, :cloud, :frontier, :direct
13
+ # type: :inference, :embedding, :moderation, :rerank, :image
14
+ # circuit_state: :closed, :open, :half_open
15
+ #
16
+ # @example
17
+ # # bad (typo)
18
+ # { tier: :fronteir }
19
+ # health[:circuit_state] = :half_opened
20
+ #
21
+ # # good
22
+ # { tier: :frontier }
23
+ # health[:circuit_state] = :half_open
24
+ class TaxonomyEnum < Base
25
+ VALID_TIERS = %i[local fleet cloud frontier direct].freeze
26
+ VALID_TYPES = %i[inference embedding moderation rerank image].freeze
27
+ VALID_CIRCUIT_STATES = %i[closed open half_open].freeze
28
+
29
+ MSG_TIER = 'Unknown tier `%<val>s`. Valid: %<valid>s.'
30
+ MSG_TYPE = 'Unknown type `%<val>s`. Valid: %<valid>s.'
31
+ MSG_CIRCUIT = 'Unknown circuit_state `%<val>s`. Valid: %<valid>s.'
32
+
33
+ def on_pair(node)
34
+ key, value = node.children
35
+ return unless key.sym_type? && value.sym_type?
36
+
37
+ check_taxonomy(key.value, value, node)
38
+ end
39
+
40
+ def on_send(node)
41
+ # health[:circuit_state] = :bad_value
42
+ return unless node.method_name == :[]=
43
+ return unless node.arguments.size == 2
44
+
45
+ key_node, val_node = node.arguments
46
+ return unless key_node.sym_type? && val_node.sym_type?
47
+
48
+ check_taxonomy(key_node.value, val_node, node)
49
+ end
50
+
51
+ private
52
+
53
+ def check_taxonomy(key, val_node, parent_node)
54
+ val = val_node.value
55
+ case key
56
+ when :tier
57
+ unless VALID_TIERS.include?(val)
58
+ add_offense(parent_node,
59
+ message: format(MSG_TIER, val: val, valid: VALID_TIERS.join(', ')))
60
+ end
61
+ when :type
62
+ unless VALID_TYPES.include?(val)
63
+ add_offense(parent_node,
64
+ message: format(MSG_TYPE, val: val, valid: VALID_TYPES.join(', ')))
65
+ end
66
+ when :circuit_state
67
+ unless VALID_CIRCUIT_STATES.include?(val)
68
+ msg = format(MSG_CIRCUIT, val: val, valid: VALID_CIRCUIT_STATES.join(', '))
69
+ add_offense(parent_node, message: msg)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Legion
5
- VERSION = '0.1.8'
5
+ VERSION = '0.1.9'
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.9
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