rails-ai-context 5.30.0 → 5.30.2

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +159 -0
  3. data/docs/ARCHITECTURE.md +5 -1
  4. data/docs/CLI.md +1 -1
  5. data/docs/COMPATIBILITY.md +7 -1
  6. data/docs/CONFIGURATION.md +2 -2
  7. data/docs/FAQ.md +1 -1
  8. data/docs/GUIDE.md +1 -1
  9. data/exe/rails-ai-context +3 -4
  10. data/lib/rails_ai_context/action_filters.rb +69 -6
  11. data/lib/rails_ai_context/boot_manager.rb +21 -5
  12. data/lib/rails_ai_context/cli/entry_boot.rb +3 -2
  13. data/lib/rails_ai_context/cli/tool_runner.rb +22 -3
  14. data/lib/rails_ai_context/concern_paths.rb +2 -1
  15. data/lib/rails_ai_context/configuration.rb +6 -6
  16. data/lib/rails_ai_context/erb_source.rb +22 -3
  17. data/lib/rails_ai_context/introspector.rb +13 -2
  18. data/lib/rails_ai_context/introspectors/controller_introspector.rb +43 -16
  19. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +10 -15
  20. data/lib/rails_ai_context/introspectors/eager_load.rb +22 -10
  21. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +4 -3
  22. data/lib/rails_ai_context/introspectors/job_introspector.rb +24 -10
  23. data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +12 -0
  24. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +9 -1
  25. data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +13 -6
  26. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +83 -27
  27. data/lib/rails_ai_context/introspectors/listeners/with_options_scope.rb +49 -12
  28. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +15 -0
  29. data/lib/rails_ai_context/introspectors/migration_replay.rb +2 -0
  30. data/lib/rails_ai_context/introspectors/model_introspector.rb +14 -3
  31. data/lib/rails_ai_context/introspectors/pg_partitions.rb +73 -0
  32. data/lib/rails_ai_context/introspectors/route_introspector.rb +62 -4
  33. data/lib/rails_ai_context/introspectors/schema_conventions.rb +17 -2
  34. data/lib/rails_ai_context/introspectors/schema_introspector.rb +13 -7
  35. data/lib/rails_ai_context/introspectors/schema_reader.rb +28 -2
  36. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +8 -4
  37. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +18 -16
  38. data/lib/rails_ai_context/migration_status.rb +6 -1
  39. data/lib/rails_ai_context/pending_migrations.rb +3 -2
  40. data/lib/rails_ai_context/portable_path.rb +14 -0
  41. data/lib/rails_ai_context/schema_adapter.rb +6 -1
  42. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +1 -1
  43. data/lib/rails_ai_context/serializers/context_file_serializer.rb +2 -2
  44. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +5 -0
  45. data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -1
  46. data/lib/rails_ai_context/tools/base_tool.rb +9 -8
  47. data/lib/rails_ai_context/tools/diagnose.rb +4 -0
  48. data/lib/rails_ai_context/tools/generate_test.rb +19 -0
  49. data/lib/rails_ai_context/tools/get_model_details.rb +1 -1
  50. data/lib/rails_ai_context/tools/get_partial_interface.rb +18 -4
  51. data/lib/rails_ai_context/tools/get_routes.rb +19 -9
  52. data/lib/rails_ai_context/tools/get_schema.rb +24 -5
  53. data/lib/rails_ai_context/tools/get_service_pattern.rb +44 -25
  54. data/lib/rails_ai_context/tools/get_test_info.rb +8 -0
  55. data/lib/rails_ai_context/tools/migration_advisor.rb +1 -1
  56. data/lib/rails_ai_context/tools/query.rb +8 -7
  57. data/lib/rails_ai_context/tools/runtime_info.rb +13 -10
  58. data/lib/rails_ai_context/tools/validate_semantics.rb +56 -14
  59. data/lib/rails_ai_context/version.rb +1 -1
  60. data/server.json +1 -1
  61. metadata +2 -1
@@ -39,6 +39,8 @@ module RailsAiContext
39
39
  @singleton_method_names_by_scope = Hash.new { |hash, key| hash[key] = Set.new }
40
40
  @scope_stack = [ TOP_LEVEL_SCOPE ]
41
41
  @method_context_stack = []
42
+ @uncertain_blocks = 0
43
+ @hook_params = []
42
44
  end
43
45
 
44
46
  def local_route_method_defined?(helper, scope, method_kind)
@@ -64,18 +66,24 @@ module RailsAiContext
64
66
  def visit_def_node(node)
65
67
  method_kind = node.receiver ? :singleton : :instance
66
68
  method_names_for(method_kind, current_scope) << node.name.to_s
69
+ hook = Introspectors::Listeners::WithOptionsScope.hook_param(node)
70
+ @hook_params << hook if hook
67
71
  with_method_context(method_kind) { super }
72
+ ensure
73
+ @hook_params.pop if hook
68
74
  end
69
75
 
70
76
  def visit_call_node(node)
71
77
  case node.name
72
78
  when :render then extract_render(node)
73
- when :validates then extract_validates(node)
79
+ when :validates then extract_validates(node) if @uncertain_blocks.zero?
74
80
  when :permit then extract_permit(node)
75
81
  # An encrypted attribute (Lockbox's has_encrypted keeps `<name>_ciphertext`)
76
82
  # and a store key are attributes with no column of their own name.
77
- when :attribute, :attr_accessor, :attr_writer, :has_encrypted, :encrypts, :attr_encrypted
83
+ when :attribute, :attr_accessor, :attr_reader, :attr_writer, :has_encrypted, :encrypts, :attr_encrypted
78
84
  extract_virtual_attributes(node)
85
+ # `alias_attribute :name, :lastname` reads the second name's column.
86
+ when :alias_attribute then extract_virtual_attributes(node, only: 1)
79
87
  when :store_accessor then extract_virtual_attributes(node, skip: 1)
80
88
  when :store then extract_store_accessors(node)
81
89
  else
@@ -86,11 +94,15 @@ module RailsAiContext
86
94
  scope: current_scope,
87
95
  method_kind: current_method_kind
88
96
  }
89
- elsif CALLBACK_NAMES.include?(node.name) && node.receiver.nil?
97
+ elsif CALLBACK_NAMES.include?(node.name) && node.receiver.nil? && @uncertain_blocks.zero?
90
98
  extract_callback(node)
91
99
  end
92
100
  end
101
+ uncertain = Introspectors::Listeners::WithOptionsScope.uncertain_block?(node) { |name| @hook_params.include?(name) }
102
+ @uncertain_blocks += 1 if uncertain
93
103
  super
104
+ ensure
105
+ @uncertain_blocks -= 1 if uncertain
94
106
  end
95
107
 
96
108
  private
@@ -167,10 +179,11 @@ module RailsAiContext
167
179
  # `attribute :foo` and `attr_accessor :foo` are real readers with no
168
180
  # column behind them, and a migration for one is a column nobody
169
181
  # wants.
170
- def extract_virtual_attributes(node, skip: 0)
182
+ def extract_virtual_attributes(node, skip: 0, only: nil)
171
183
  return unless node.receiver.nil?
172
184
 
173
- (node.arguments&.arguments || []).drop(skip).each do |arg|
185
+ args = (node.arguments&.arguments || []).drop(skip)
186
+ (only ? args.first(only) : args).each do |arg|
174
187
  @virtual_attributes << arg.unescaped if arg.is_a?(Prism::SymbolNode)
175
188
  end
176
189
  end
@@ -488,7 +501,7 @@ module RailsAiContext
488
501
  columns = table_columns.dup
489
502
  model_data[:associations]&.each do |a|
490
503
  columns << a[:name] if a[:name]
491
- columns << a[:foreign_key] if a[:foreign_key]
504
+ columns.merge(Array(a[:foreign_key]))
492
505
  end
493
506
 
494
507
  { columns: columns, table_columns: table_columns, table: table_name, model: model_name, model_data: model_data }
@@ -518,7 +531,7 @@ module RailsAiContext
518
531
 
519
532
  valid = Set.new
520
533
  table_data[:columns]&.each { |c| valid << c[:name] }
521
- model_data[:associations]&.each { |a| valid << a[:name]; valid << a[:foreign_key] if a[:foreign_key] }
534
+ model_data[:associations]&.each { |a| valid << a[:name]; valid.merge(Array(a[:foreign_key])) }
522
535
  valid.merge(%w[id _destroy created_at updated_at])
523
536
 
524
537
  # When JSONB columns exist, plain-word params may be keys inside JSONB columns.
@@ -644,6 +657,7 @@ module RailsAiContext
644
657
 
645
658
  declared = Introspectors::SourceIntrospector
646
659
  .walk_source(content.to_s, { associations: Introspectors::Listeners::AssociationsListener })[:associations]
660
+ .reject { |a| a[:scope_uncertain] }
647
661
  .map { |a| a[:name].to_s }.to_set
648
662
 
649
663
  (model_data[:associations] || []).each do |assoc|
@@ -671,26 +685,54 @@ module RailsAiContext
671
685
 
672
686
  table_name = model_data[:table_name]
673
687
  table_data = schema[:tables] && schema[:tables][table_name]
674
- return warnings unless table_data
688
+ # A static table whose block called what no reader interprets has columns and indexes unknown.
689
+ return warnings if table_data.nil? || table_data[:unread_calls]
675
690
 
676
691
  # Only flag columns that are ACTUAL foreign keys (declared via add_foreign_key or belongs_to)
677
692
  declared_fk_columns = (table_data[:foreign_keys] || []).map { |fk| fk[:column] }
678
- assoc_fk_columns = (model_data[:associations] || [])
679
- .select { |a| a[:type] == "belongs_to" }
680
- .map { |a| a[:foreign_key] }
681
- .compact
682
- fk_columns = (declared_fk_columns + assoc_fk_columns).uniq
693
+ belongs_to = (model_data[:associations] || []).select { |a| a[:type] == "belongs_to" && column_key?(a) }
694
+ table_columns = Array(table_data[:columns]).map { |c| c[:name].to_s }
695
+ # A key the table lacks is a broken association, not an unindexed column.
696
+ absent, belongs_to = belongs_to.partition do |a|
697
+ table_columns.any? && !(Array(a[:foreign_key]).map(&:to_s) - table_columns).empty?
698
+ end
699
+ absent.each do |a|
700
+ warnings << "belongs_to :#{a[:name]} - column \"#{Introspectors::SchemaConventions.key_text(a[:foreign_key])}\" not found in #{table_name} table. " \
701
+ "Fix: pass `foreign_key:` naming the real column, or add the column"
702
+ end
703
+ fk_columns = (declared_fk_columns + belongs_to.map { |a| a[:foreign_key] }).uniq
683
704
 
684
705
  indexed = Introspectors::SchemaConventions.lookup_indexed_columns(table_data)
706
+ polymorphic_pairs = (model_data[:associations] || [])
707
+ .select { |a| a[:type] == "belongs_to" && a[:polymorphic] && a[:foreign_key] }
708
+ .to_h { |a| [ a[:foreign_key].to_s, [ (a[:foreign_type] || a.dig(:options, :foreign_type) || "#{a[:name]}_type").to_s, a[:foreign_key].to_s ] ] }
685
709
 
686
710
  fk_columns.each do |col|
687
- unless indexed.include?(col)
711
+ # `t.references polymorphic: true` indexes [type, id], led by the type.
712
+ pair = polymorphic_pairs[col.to_s]
713
+ next if pair && Introspectors::SchemaConventions.leading_index?(table_data[:indexes], pair)
714
+
715
+ if col.is_a?(Array)
716
+ primary_key = { columns: Array(table_data[:primary_key] || table_data.dig(:options, :primary_key)) }
717
+ next if Introspectors::SchemaConventions.leading_index?(Array(table_data[:indexes]) + [ primary_key ], col)
718
+
719
+ warnings << "#{Introspectors::SchemaConventions.key_text(col)} in #{table_name} - foreign key without an index leading with its columns (slow queries). " \
720
+ "Fix: `add_index :#{table_name}, [#{col.map { |c| ":#{c}" }.join(", ")}]`"
721
+ elsif !indexed.include?(col)
688
722
  warnings << "#{col} in #{table_name} - foreign key without index (slow queries). Fix: `rails g migration AddIndexTo#{table_name.camelize} #{col}:index`"
689
723
  end
690
724
  end
691
725
  warnings
692
726
  end
693
727
 
728
+ # A key the source names as columns, on a declaration the class itself makes.
729
+ private_class_method def self.column_key?(assoc)
730
+ return false if assoc[:computed_foreign_key] || assoc[:scope_uncertain]
731
+
732
+ columns = Array(assoc[:foreign_key])
733
+ columns.any? && columns.all? { |c| c.to_s.match?(/\A\w+\z/) }
734
+ end
735
+
694
736
  # ── CHECK 9: Stimulus controller existence ───────────────────────
695
737
 
696
738
  private_class_method def self.check_stimulus_controllers(content, context)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "5.30.0"
4
+ VERSION = "5.30.2"
5
5
  end
data/server.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/crisnahine/rails-ai-context",
8
8
  "source": "github"
9
9
  },
10
- "version": "5.30.0",
10
+ "version": "5.30.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "mcpb",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-ai-context
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.30.0
4
+ version: 5.30.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine
@@ -398,6 +398,7 @@ files:
398
398
  - lib/rails_ai_context/introspectors/node_source.rb
399
399
  - lib/rails_ai_context/introspectors/observability_introspector.rb
400
400
  - lib/rails_ai_context/introspectors/performance_introspector.rb
401
+ - lib/rails_ai_context/introspectors/pg_partitions.rb
401
402
  - lib/rails_ai_context/introspectors/rake_task_introspector.rb
402
403
  - lib/rails_ai_context/introspectors/retry_policy.rb
403
404
  - lib/rails_ai_context/introspectors/route_introspector.rb