rails-ai-context 5.30.2 → 5.31.0

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +173 -0
  3. data/docs/COMPATIBILITY.md +21 -7
  4. data/docs/CONFIGURATION.md +3 -0
  5. data/docs/GUIDE.md +7 -1
  6. data/docs/INTROSPECTORS.md +6 -1
  7. data/docs/SECURITY.md +23 -1
  8. data/docs/TOOLS.md +11 -2
  9. data/lib/rails_ai_context/action_filters.rb +2 -3
  10. data/lib/rails_ai_context/base_mixins.rb +44 -16
  11. data/lib/rails_ai_context/concern_macros.rb +698 -92
  12. data/lib/rails_ai_context/concern_membership.rb +6 -4
  13. data/lib/rails_ai_context/concern_paths.rb +29 -3
  14. data/lib/rails_ai_context/configuration.rb +2 -2
  15. data/lib/rails_ai_context/git_ignore.rb +13 -11
  16. data/lib/rails_ai_context/introspectors/action_presence.rb +5 -3
  17. data/lib/rails_ai_context/introspectors/action_resolver.rb +2 -1
  18. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +198 -18
  19. data/lib/rails_ai_context/introspectors/controller_introspector.rb +3 -3
  20. data/lib/rails_ai_context/introspectors/declared_constant.rb +15 -6
  21. data/lib/rails_ai_context/introspectors/job_introspector.rb +55 -10
  22. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +3 -3
  23. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +28 -1
  24. data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +3 -1
  25. data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +21 -28
  26. data/lib/rails_ai_context/introspectors/listeners/owner_scope.rb +67 -0
  27. data/lib/rails_ai_context/introspectors/listeners/proc_literal_listener.rb +42 -0
  28. data/lib/rails_ai_context/introspectors/model_introspector.rb +277 -118
  29. data/lib/rails_ai_context/introspectors/source_scan.rb +5 -3
  30. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +13 -4
  31. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +104 -21
  32. data/lib/rails_ai_context/method_name.rb +31 -0
  33. data/lib/rails_ai_context/output_guard.rb +6 -4
  34. data/lib/rails_ai_context/redaction.rb +8 -2
  35. data/lib/rails_ai_context/safe_file.rb +4 -1
  36. data/lib/rails_ai_context/safe_path.rb +17 -4
  37. data/lib/rails_ai_context/server.rb +26 -7
  38. data/lib/rails_ai_context/tools/base_tool.rb +11 -2
  39. data/lib/rails_ai_context/tools/get_callbacks.rb +14 -6
  40. data/lib/rails_ai_context/tools/get_concern.rb +1 -1
  41. data/lib/rails_ai_context/tools/get_conventions.rb +5 -7
  42. data/lib/rails_ai_context/tools/get_job_pattern.rb +8 -2
  43. data/lib/rails_ai_context/tools/get_model_details.rb +4 -3
  44. data/lib/rails_ai_context/tools/get_schema.rb +4 -0
  45. data/lib/rails_ai_context/tools/performance_check.rb +5 -13
  46. data/lib/rails_ai_context/tools/query.rb +122 -28
  47. data/lib/rails_ai_context/tools/search_code.rb +64 -22
  48. data/lib/rails_ai_context/tools/search_docs.rb +4 -5
  49. data/lib/rails_ai_context/tools/security_scan.rb +3 -1
  50. data/lib/rails_ai_context/version.rb +1 -1
  51. data/lib/rails_ai_context.rb +1 -1
  52. data/server.json +1 -1
  53. metadata +4 -1
@@ -7,6 +7,7 @@ module RailsAiContext
7
7
  # before_validation, after_save, after_commit, etc.
8
8
  class CallbacksListener < BaseListener
9
9
  include WithOptionsScope
10
+ include OwnerScope
10
11
 
11
12
  CALLBACK_METHODS = %i[
12
13
  before_validation after_validation
@@ -39,6 +40,31 @@ module RailsAiContext
39
40
  end
40
41
  end
41
42
 
43
+ ON_EVENT = "after_commit_on_"
44
+
45
+ # Every commit spelling lands in the one after-commit kind of one chain.
46
+ def self.chain_key(type)
47
+ type.to_s.match?(/\Aafter_(\w+_)?commit/) ? "after_commit" : type.to_s
48
+ end
49
+
50
+ def self.transaction?(type)
51
+ type.to_s.match?(/\Aafter_(\w+_)?(commit|rollback)/)
52
+ end
53
+
54
+ def self.after?(type)
55
+ type.to_s.start_with?("after_")
56
+ end
57
+
58
+ # `after_commit on: :create` resolves to a type that names its event.
59
+ def self.names_event?(type)
60
+ type.to_s.start_with?(ON_EVENT)
61
+ end
62
+
63
+ # The event such a type names, `"create"`, or nil.
64
+ def self.event(type)
65
+ type.to_s.delete_prefix(ON_EVENT) if names_event?(type)
66
+ end
67
+
42
68
  private
43
69
 
44
70
  # `around_create Snowflake::Callbacks` names a real target;
@@ -66,6 +92,7 @@ module RailsAiContext
66
92
  type: callback_type,
67
93
  method: method_name,
68
94
  options: options,
95
+ owner: @owner_stack.dup,
69
96
  location: node.location.start_line,
70
97
  confidence: confidence
71
98
  }
@@ -77,7 +104,7 @@ module RailsAiContext
77
104
  # declaration for several events stays one callback, its on: kept.
78
105
  def resolve_callback_types(name, options)
79
106
  events = Array(options[:on])
80
- name == :after_commit && events.one? ? [ "after_commit_on_#{events.first}" ] : [ name.to_s ]
107
+ name == :after_commit && events.one? ? [ "#{ON_EVENT}#{events.first}" ] : [ name.to_s ]
81
108
  end
82
109
  end
83
110
  end
@@ -4,10 +4,11 @@ module RailsAiContext
4
4
  module Introspectors
5
5
  module Listeners
6
6
  class GenericMacroListener < BaseListener
7
- def initialize(*target_methods)
7
+ def initialize(*target_methods, block_source: [])
8
8
  super()
9
9
  @target_methods = target_methods.flatten.map(&:to_sym).to_set
10
10
  @enclosing = []
11
+ @block_source = block_source.to_set
11
12
  end
12
13
 
13
14
  def on_call_node_enter(node)
@@ -21,6 +22,7 @@ module RailsAiContext
21
22
  options: extract_keyword_options(node),
22
23
  option_values: extract_keyword_sources(node),
23
24
  option_nodes: extract_keyword_nodes(node),
25
+ block: (one_line_source(node.block) if node.block.is_a?(Prism::BlockNode) && @block_source.include?(node.name)),
24
26
  # Offsets, not line numbers: a one-line block puts the parent and
25
27
  # its nested calls on one line, and a consumer pairing them by
26
28
  # line then attaches the second child to the first.
@@ -14,28 +14,11 @@ module RailsAiContext
14
14
  MIXIN_MACROS = %i[include prepend extend].to_set.freeze
15
15
  ANCESTOR_MACROS = %i[include prepend].to_set.freeze
16
16
 
17
+ include OwnerScope
18
+
17
19
  def initialize
18
20
  super
19
21
  @singleton_depth = 0
20
- @owner_stack = []
21
- end
22
-
23
- # The class or module the mixin is written in, the way MethodsListener
24
- # records a method's: a nested class's include is not its module's.
25
- def on_class_node_enter(node)
26
- @owner_stack.push(constant_path_string(node.constant_path))
27
- end
28
-
29
- def on_class_node_leave(_node)
30
- @owner_stack.pop
31
- end
32
-
33
- def on_module_node_enter(node)
34
- @owner_stack.push(constant_path_string(node.constant_path))
35
- end
36
-
37
- def on_module_node_leave(_node)
38
- @owner_stack.pop
39
22
  end
40
23
 
41
24
  # `include` inside `class << self` lands on the singleton class, so it
@@ -48,27 +31,37 @@ module RailsAiContext
48
31
  @singleton_depth -= 1
49
32
  end
50
33
 
34
+ # `singleton_class` called on what stands for the class: nothing or `self` in its body, `base` in a hook.
35
+ # `singleton_class.include M` gives the class M's methods, as `extend` does, without `extended`.
36
+ def self.singleton_class_of?(node, &stands_for_class)
37
+ node.is_a?(Prism::CallNode) && node.name == :singleton_class && node.arguments.nil? && stands_for_class.call(node.receiver)
38
+ end
39
+
40
+ # The record of `node` mixing in `name`.
41
+ def self.record(node, macro, name, ancestor:, owner: [])
42
+ { macro: macro, name: name, ancestor: ancestor, owner: owner, location: node.location.start_line,
43
+ confidence: confidence_for(node) }
44
+ end
45
+
51
46
  # `Type.include(StatusPatch)` mixes into Type, not the class the line sits in, so the
52
47
  # record names Type as `receiver` and it is never an ancestor of the enclosing class.
53
48
  def on_call_node_enter(node)
54
49
  receiver = node.receiver && constant_name(node.receiver)
55
- return unless node.receiver.nil? || receiver
50
+ singleton = self.class.singleton_class_of?(node.receiver) { |inner| inner.nil? || inner.is_a?(Prism::SelfNode) }
51
+ return unless node.receiver.nil? || receiver || singleton
56
52
 
57
53
  macro, arguments = mixin_call(node)
58
54
  return unless macro
55
+ return if singleton && macro == :extend
56
+
57
+ macro = :"singleton_#{macro}" if singleton
59
58
 
60
59
  arguments.each do |arg|
61
60
  name = constant_name(arg)
62
61
  next unless name
63
62
 
64
- record = {
65
- macro: macro,
66
- name: name,
67
- ancestor: receiver.nil? && @singleton_depth.zero? && ANCESTOR_MACROS.include?(macro),
68
- owner: @owner_stack.dup,
69
- location: node.location.start_line,
70
- confidence: confidence_for(node)
71
- }
63
+ ancestor = receiver.nil? && @singleton_depth.zero? && ANCESTOR_MACROS.include?(macro)
64
+ record = self.class.record(node, macro, name, ancestor: ancestor, owner: @owner_stack.dup)
72
65
  record[:receiver] = receiver if receiver
73
66
  @results << record
74
67
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ module Listeners
6
+ # The class or module a record is written in, so a nested class's
7
+ # declarations stay its own. `Sib = Class.new(Base) do` opens Sib.
8
+ module OwnerScope
9
+ def initialize
10
+ super
11
+ @owner_stack = []
12
+ @class_new_writes = []
13
+ end
14
+
15
+ def on_class_node_enter(node)
16
+ @owner_stack.push(constant_path_string(node.constant_path))
17
+ end
18
+
19
+ def on_class_node_leave(_node)
20
+ @owner_stack.pop
21
+ end
22
+
23
+ def on_module_node_enter(node)
24
+ @owner_stack.push(constant_path_string(node.constant_path))
25
+ end
26
+
27
+ def on_module_node_leave(_node)
28
+ @owner_stack.pop
29
+ end
30
+
31
+ def on_constant_write_node_enter(node)
32
+ enter_class_new(node, node.name.to_s)
33
+ end
34
+
35
+ def on_constant_write_node_leave(node)
36
+ leave_class_new(node)
37
+ end
38
+
39
+ def on_constant_path_write_node_enter(node)
40
+ enter_class_new(node, node.target.slice)
41
+ end
42
+
43
+ def on_constant_path_write_node_leave(node)
44
+ leave_class_new(node)
45
+ end
46
+
47
+ private
48
+
49
+ def enter_class_new(node, name)
50
+ value = node.value
51
+ return unless value.is_a?(Prism::CallNode) && value.name == :new && value.block &&
52
+ value.receiver.is_a?(Prism::ConstantReadNode) && value.receiver.name == :Class
53
+
54
+ @owner_stack.push(name)
55
+ @class_new_writes.push(node)
56
+ end
57
+
58
+ def leave_class_new(node)
59
+ return unless @class_new_writes.last.equal?(node)
60
+
61
+ @class_new_writes.pop
62
+ @owner_stack.pop
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ module Listeners
6
+ # Every Proc literal (`-> {}`, `lambda {}`, `proc {}`, `Proc.new {}`) with
7
+ # the line it starts on and the constant it is assigned to, if any.
8
+ class ProcLiteralListener < BaseListener
9
+ def initialize
10
+ super
11
+ @assigned = {}
12
+ end
13
+
14
+ def on_constant_write_node_enter(node)
15
+ @assigned[node.value.location.start_offset] = node.name.to_s
16
+ end
17
+
18
+ def on_lambda_node_enter(node)
19
+ record(node)
20
+ end
21
+
22
+ def on_call_node_enter(node)
23
+ record(node) if proc_call?(node)
24
+ end
25
+
26
+ private
27
+
28
+ def proc_call?(node)
29
+ return false unless node.block.is_a?(Prism::BlockNode)
30
+ return %i[lambda proc].include?(node.name) if node.receiver.nil?
31
+
32
+ node.name == :new && node.receiver.is_a?(Prism::ConstantReadNode) && node.receiver.name == :Proc
33
+ end
34
+
35
+ def record(node)
36
+ @results << { line: node.location.start_line, constant: @assigned[node.location.start_offset],
37
+ source: one_line_source(node) }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end