rails-ai-context 4.3.2 → 4.4.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -53
  3. data/CLAUDE.md +3 -1
  4. data/README.md +268 -197
  5. data/demo-trace.gif +0 -0
  6. data/demo-trace.tape +21 -0
  7. data/demo.gif +0 -0
  8. data/demo.tape +33 -0
  9. data/docs/GUIDE.md +9 -9
  10. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -1
  11. data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
  12. data/lib/rails_ai_context/configuration.rb +25 -1
  13. data/lib/rails_ai_context/doctor.rb +4 -2
  14. data/lib/rails_ai_context/fingerprinter.rb +6 -1
  15. data/lib/rails_ai_context/introspectors/accessibility_introspector.rb +52 -1
  16. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +10 -2
  17. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +22 -2
  18. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +50 -4
  19. data/lib/rails_ai_context/introspectors/api_introspector.rb +41 -5
  20. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +10 -4
  21. data/lib/rails_ai_context/introspectors/auth_introspector.rb +62 -7
  22. data/lib/rails_ai_context/introspectors/component_introspector.rb +6 -0
  23. data/lib/rails_ai_context/introspectors/config_introspector.rb +59 -9
  24. data/lib/rails_ai_context/introspectors/controller_introspector.rb +45 -13
  25. data/lib/rails_ai_context/introspectors/convention_detector.rb +25 -2
  26. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +58 -4
  27. data/lib/rails_ai_context/introspectors/design_token_introspector.rb +27 -5
  28. data/lib/rails_ai_context/introspectors/devops_introspector.rb +15 -8
  29. data/lib/rails_ai_context/introspectors/engine_introspector.rb +12 -3
  30. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +36 -1
  31. data/lib/rails_ai_context/introspectors/gem_introspector.rb +47 -1
  32. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +49 -3
  33. data/lib/rails_ai_context/introspectors/job_introspector.rb +48 -5
  34. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +24 -3
  35. data/lib/rails_ai_context/introspectors/migration_introspector.rb +4 -1
  36. data/lib/rails_ai_context/introspectors/model_introspector.rb +108 -11
  37. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +57 -12
  38. data/lib/rails_ai_context/introspectors/performance_introspector.rb +34 -9
  39. data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +12 -2
  40. data/lib/rails_ai_context/introspectors/route_introspector.rb +25 -8
  41. data/lib/rails_ai_context/introspectors/schema_introspector.rb +45 -7
  42. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +5 -2
  43. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +59 -6
  44. data/lib/rails_ai_context/introspectors/test_introspector.rb +50 -5
  45. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +44 -13
  46. data/lib/rails_ai_context/introspectors/view_introspector.rb +46 -7
  47. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +25 -7
  48. data/lib/rails_ai_context/resources.rb +1 -1
  49. data/lib/rails_ai_context/server.rb +6 -3
  50. data/lib/rails_ai_context/tasks/rails_ai_context.rake +8 -4
  51. data/lib/rails_ai_context/tools/analyze_feature.rb +66 -19
  52. data/lib/rails_ai_context/tools/base_tool.rb +1 -1
  53. data/lib/rails_ai_context/tools/diagnose.rb +4 -2
  54. data/lib/rails_ai_context/tools/get_callbacks.rb +4 -2
  55. data/lib/rails_ai_context/tools/get_concern.rb +12 -6
  56. data/lib/rails_ai_context/tools/get_controllers.rb +10 -5
  57. data/lib/rails_ai_context/tools/get_conventions.rb +4 -2
  58. data/lib/rails_ai_context/tools/get_design_system.rb +2 -1
  59. data/lib/rails_ai_context/tools/get_env.rb +8 -4
  60. data/lib/rails_ai_context/tools/get_helper_methods.rb +6 -3
  61. data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
  62. data/lib/rails_ai_context/tools/get_model_details.rb +10 -5
  63. data/lib/rails_ai_context/tools/get_partial_interface.rb +14 -7
  64. data/lib/rails_ai_context/tools/get_schema.rb +2 -1
  65. data/lib/rails_ai_context/tools/get_service_pattern.rb +2 -1
  66. data/lib/rails_ai_context/tools/get_stimulus.rb +2 -1
  67. data/lib/rails_ai_context/tools/get_test_info.rb +4 -2
  68. data/lib/rails_ai_context/tools/get_turbo_map.rb +22 -11
  69. data/lib/rails_ai_context/tools/get_view.rb +6 -3
  70. data/lib/rails_ai_context/tools/migration_advisor.rb +2 -1
  71. data/lib/rails_ai_context/tools/onboard.rb +2 -1
  72. data/lib/rails_ai_context/tools/performance_check.rb +2 -1
  73. data/lib/rails_ai_context/tools/query.rb +5 -1
  74. data/lib/rails_ai_context/tools/read_logs.rb +3 -0
  75. data/lib/rails_ai_context/tools/runtime_info.rb +10 -5
  76. data/lib/rails_ai_context/tools/search_code.rb +8 -4
  77. data/lib/rails_ai_context/tools/search_docs.rb +2 -1
  78. data/lib/rails_ai_context/tools/session_context.rb +2 -1
  79. data/lib/rails_ai_context/tools/validate.rb +16 -8
  80. data/lib/rails_ai_context/version.rb +1 -1
  81. metadata +5 -1
data/demo.tape ADDED
@@ -0,0 +1,33 @@
1
+ # VHS demo tape for rails-ai-context install
2
+ Output demo.gif
3
+
4
+ Set Shell zsh
5
+ Set FontSize 14
6
+ Set Width 950
7
+ Set Height 520
8
+ Set Padding 12
9
+ Set Theme "Catppuccin Mocha"
10
+ Set TypingSpeed 35ms
11
+
12
+ Type "cd /Users/crisjosephnahine/Documents/Projects/daily-content-chef"
13
+ Enter
14
+ Sleep 1s
15
+
16
+ Type "rails generate rails_ai_context:install"
17
+ Sleep 400ms
18
+ Enter
19
+ Sleep 3s
20
+
21
+ # Select Claude Code + Cursor
22
+ Type "1,2"
23
+ Sleep 300ms
24
+ Enter
25
+ Sleep 2s
26
+
27
+ # Select MCP mode (yes)
28
+ Type "1"
29
+ Sleep 300ms
30
+ Enter
31
+ Sleep 4s
32
+
33
+ Sleep 2s
data/docs/GUIDE.md CHANGED
@@ -1128,7 +1128,7 @@ Both transports are **read-only** — they expose the same 39 tools and never mo
1128
1128
  RailsAiContext.configure do |config|
1129
1129
  # --- Introspectors ---
1130
1130
 
1131
- # Presets: :full (32 introspectors, default) or :standard (14 core)
1131
+ # Presets: :full (33 introspectors, default) or :standard (19)
1132
1132
  config.preset = :full
1133
1133
 
1134
1134
  # Cherry-pick on top of a preset
@@ -1300,7 +1300,7 @@ All split rules include an app overview file, so no context is lost when root fi
1300
1300
 
1301
1301
  ## Introspectors — Full List
1302
1302
 
1303
- ### Standard preset (14 introspectors)
1303
+ ### Standard preset (19 introspectors)
1304
1304
 
1305
1305
  Core Rails structure only. Use `config.preset = :standard` for a lighter footprint.
1306
1306
 
@@ -1320,19 +1320,21 @@ Core Rails structure only. Use `config.preset = :standard` for a lighter footpri
1320
1320
  | `view_templates` | View file contents, partial references, Stimulus data attributes, UI pattern extraction, model field usage in partials. |
1321
1321
  | `design_tokens` | Auto-detects CSS framework (Tailwind v3/v4, Bootstrap, Sass, plain CSS) and extracts design tokens from config files and built CSS. |
1322
1322
  | `components` | ViewComponent/Phlex components: props, slots, previews, sidecar assets, usage examples. |
1323
+ | `turbo` | Turbo Frames (IDs and files), Turbo Stream templates, model broadcasts (`broadcasts_to`, `broadcasts`). |
1324
+ | `auth` | Devise models with modules, Rails 8 built-in auth, has_secure_password, Pundit policies, CanCanCan, CORS config, CSP config. |
1325
+ | `accessibility` | ARIA attributes, semantic HTML elements, screen reader text, alt text coverage, landmark roles, accessibility score. |
1326
+ | `performance` | N+1 query risks, missing counter_cache, missing FK indexes, Model.all anti-patterns, eager load candidates. |
1327
+ | `i18n` | Default locale, available locales, locale files with key counts, backend class, parse errors. |
1323
1328
 
1324
- ### Full preset (32 introspectors) — default
1329
+ ### Full preset (33 introspectors) — default
1325
1330
 
1326
1331
  Includes all standard introspectors plus:
1327
1332
 
1328
1333
  | Introspector | What it discovers |
1329
1334
  |-------------|-------------------|
1330
1335
  | `views` | Layouts, templates grouped by controller, partials (per-controller and shared), helpers with methods, template engines (erb, haml, slim), view components. |
1331
- | `turbo` | Turbo Frames (IDs and files), Turbo Stream templates, model broadcasts (`broadcasts_to`, `broadcasts`). |
1332
- | `i18n` | Default locale, available locales, locale files with key counts, backend class, parse errors. |
1333
1336
  | `active_storage` | Attachments (has_one_attached, has_many_attached per model), storage services, direct upload config. |
1334
1337
  | `action_text` | Rich text fields (has_rich_text per model), Action Text installation status. |
1335
- | `auth` | Devise models with modules, Rails 8 built-in auth, has_secure_password, Pundit policies, CanCanCan, CORS config, CSP config. |
1336
1338
  | `api` | API-only mode, API versioning (from directory structure), serializers (Jbuilder, AMS, etc.), GraphQL (types, mutations), rate limiting (Rack::Attack). |
1337
1339
  | `rake_tasks` | Custom rake tasks in `lib/tasks/` with names, descriptions, namespaces, file paths. |
1338
1340
  | `assets` | Asset pipeline (Propshaft/Sprockets), JS bundler (importmap/esbuild/webpack/vite), CSS framework, importmap pins, manifest files. |
@@ -1342,10 +1344,8 @@ Includes all standard introspectors plus:
1342
1344
  | `middleware` | Custom Rack middleware in app/middleware/ with detected patterns (auth, rate limiting, tenant isolation, logging). Full middleware stack. |
1343
1345
  | `engines` | Mounted Rails engines from routes.rb with paths and descriptions for 23+ known engines (Sidekiq::Web, Flipper::UI, PgHero, ActiveAdmin, etc.). |
1344
1346
  | `multi_database` | Multiple databases, replicas, sharding config, model-specific `connects_to` declarations. database.yml parsing fallback. |
1345
- | `accessibility` | ARIA attributes, semantic HTML elements, screen reader text, alt text coverage, landmark roles, accessibility score. |
1346
- | `performance` | N+1 query risks, missing counter_cache, missing FK indexes, Model.all anti-patterns, eager load candidates. |
1347
1347
  | `frontend_frameworks` | Frontend JS framework detection (React/Vue/Svelte/Angular), mounting strategy (Inertia/react-rails), TypeScript config, state management, package manager. |
1348
- | `database_stats` | PostgreSQL approximate row counts via `pg_stat_user_tables`. **Opt-in only** not in any preset, add manually: `config.introspectors += [:database_stats]`. |
1348
+ | `database_stats` | PostgreSQL approximate row counts via `pg_stat_user_tables`. Gracefully skips on non-PostgreSQL adapters. |
1349
1349
 
1350
1350
  ### Using the standard preset
1351
1351
 
@@ -377,7 +377,8 @@ module RailsAiContext
377
377
  return nil unless match
378
378
 
379
379
  match[1].split.map(&:to_sym)
380
- rescue
380
+ rescue => e
381
+ $stderr.puts "[rails-ai-context] read_previous_ai_tools failed: #{e.message}" if ENV["DEBUG"]
381
382
  nil
382
383
  end
383
384
  end # no_tasks
@@ -178,7 +178,7 @@ module RailsAiContext
178
178
  next
179
179
  end
180
180
 
181
- value = args[i + 1]
181
+ value = (i + 1 < args.size) ? args[i + 1] : nil
182
182
  if value && !value.start_with?("--")
183
183
  result[key] = coerce_value(value, prop)
184
184
  i += 2
@@ -6,7 +6,7 @@ module RailsAiContext
6
6
  standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus
7
7
  view_templates design_tokens config components
8
8
  turbo auth accessibility performance i18n],
9
- full: %i[schema models routes jobs gems conventions stimulus controllers views view_templates design_tokens turbo
9
+ full: %i[schema models routes jobs gems conventions stimulus database_stats controllers views view_templates design_tokens turbo
10
10
  i18n config active_storage action_text auth api tests rake_tasks assets
11
11
  devops action_mailbox migrations seeds middleware engines multi_database
12
12
  components accessibility performance frontend_frameworks]
@@ -198,5 +198,29 @@ module RailsAiContext
198
198
  def output_dir_for(app)
199
199
  @output_dir || app.root.to_s
200
200
  end
201
+
202
+ def http_port=(value)
203
+ value = value.to_i
204
+ raise ArgumentError, "http_port must be between 1 and 65535 (got #{value})" unless value.between?(1, 65535)
205
+ @http_port = value
206
+ end
207
+
208
+ def cache_ttl=(value)
209
+ value = value.to_i
210
+ raise ArgumentError, "cache_ttl must be positive (got #{value})" unless value > 0
211
+ @cache_ttl = value
212
+ end
213
+
214
+ def max_tool_response_chars=(value)
215
+ value = value.to_i
216
+ raise ArgumentError, "max_tool_response_chars must be positive (got #{value})" unless value > 0
217
+ @max_tool_response_chars = value
218
+ end
219
+
220
+ def query_row_limit=(value)
221
+ value = value.to_i
222
+ raise ArgumentError, "query_row_limit must be between 1 and 1000 (got #{value})" unless value.between?(1, 1000)
223
+ @query_row_limit = value
224
+ end
201
225
  end
202
226
  end
@@ -84,7 +84,8 @@ module RailsAiContext
84
84
  message: "#{pending.size} pending migration(s) — schema data will be stale",
85
85
  fix: "Run `rails db:migrate`")
86
86
  end
87
- rescue
87
+ rescue => e
88
+ $stderr.puts "[rails-ai-context] check_pending_migrations failed: #{e.message}" if ENV["DEBUG"]
88
89
  # Can't check pending migrations in this environment
89
90
  nil
90
91
  end
@@ -234,7 +235,8 @@ module RailsAiContext
234
235
  message: "#{errors.size} introspector(s) returned errors: #{errors.join(', ')}",
235
236
  fix: "Check if the app has the required features (e.g., stimulus needs app/javascript/controllers/)")
236
237
  end
237
- rescue
238
+ rescue => e
239
+ $stderr.puts "[rails-ai-context] check_introspector_health failed: #{e.message}" if ENV["DEBUG"]
238
240
  nil
239
241
  end
240
242
 
@@ -50,6 +50,8 @@ module RailsAiContext
50
50
  WATCHED_FILES.each do |file|
51
51
  path = File.join(root, file)
52
52
  digest.update(File.mtime(path).to_f.to_s) if File.exist?(path)
53
+ rescue Errno::ENOENT
54
+ # File deleted between exist? check and mtime read — skip
53
55
  end
54
56
 
55
57
  WATCHED_DIRS.each do |dir|
@@ -58,6 +60,8 @@ module RailsAiContext
58
60
 
59
61
  Dir.glob(File.join(full_dir, "**/*.{rb,rake,js,ts,erb,haml,slim,yml}")).sort.each do |path|
60
62
  digest.update(File.mtime(path).to_f.to_s)
63
+ rescue Errno::ENOENT
64
+ # File deleted between glob and mtime read — skip
61
65
  end
62
66
  end
63
67
 
@@ -71,7 +75,8 @@ module RailsAiContext
71
75
  spec = Bundler.rubygems.find_name("rails-ai-context").first
72
76
  return false unless spec
73
77
  spec.source.is_a?(Bundler::Source::Path)
74
- rescue
78
+ rescue => e
79
+ $stderr.puts "[rails-ai-context] local_gem_path? failed: #{e.message}" if ENV["DEBUG"]
75
80
  false
76
81
  end
77
82
 
@@ -21,6 +21,10 @@ module RailsAiContext
21
21
  images: analyze_images(views),
22
22
  labels: analyze_labels(views),
23
23
  landmarks: extract_landmarks(views),
24
+ heading_hierarchy: analyze_heading_hierarchy(views),
25
+ skip_links: detect_skip_links(views),
26
+ live_regions: count_live_regions(views),
27
+ form_inputs: analyze_form_inputs(views),
24
28
  summary: build_summary(views)
25
29
  }
26
30
  rescue => e
@@ -50,7 +54,8 @@ module RailsAiContext
50
54
  Dir.glob(File.join(dir, "**/*.{erb,haml,slim,html}")).each do |path|
51
55
  content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
52
56
  views << { file: path.sub("#{root}/", ""), content: content }
53
- rescue
57
+ rescue => e
58
+ $stderr.puts "[rails-ai-context] collect_view_content failed: #{e.message}" if ENV["DEBUG"]
54
59
  next
55
60
  end
56
61
  end
@@ -149,6 +154,52 @@ module RailsAiContext
149
154
  landmarks
150
155
  end
151
156
 
157
+ def analyze_heading_hierarchy(views)
158
+ all_content = views.map { |v| v[:content] }.join("\n")
159
+ counts = {}
160
+ (1..6).each do |level|
161
+ count = all_content.scan(/<h#{level}[\s>]/i).size
162
+ counts["h#{level}"] = count if count > 0
163
+ end
164
+ counts
165
+ rescue => e
166
+ $stderr.puts "[rails-ai-context] analyze_heading_hierarchy failed: #{e.message}" if ENV["DEBUG"]
167
+ {}
168
+ end
169
+
170
+ def detect_skip_links(views)
171
+ all_content = views.map { |v| v[:content] }.join("\n")
172
+ {
173
+ skip_to_content: all_content.scan(/skip.to.(?:content|main)/i).size > 0,
174
+ skip_navigation: all_content.scan(/skip.nav/i).size > 0
175
+ }
176
+ rescue => e
177
+ $stderr.puts "[rails-ai-context] detect_skip_links failed: #{e.message}" if ENV["DEBUG"]
178
+ {}
179
+ end
180
+
181
+ def count_live_regions(views)
182
+ all_content = views.map { |v| v[:content] }.join("\n")
183
+ {
184
+ aria_live: all_content.scan(/aria-live=["'](\w+)["']/).flatten.tally,
185
+ aria_atomic: all_content.scan(/aria-atomic=/).size
186
+ }
187
+ rescue => e
188
+ $stderr.puts "[rails-ai-context] count_live_regions failed: #{e.message}" if ENV["DEBUG"]
189
+ {}
190
+ end
191
+
192
+ def analyze_form_inputs(views)
193
+ all_content = views.map { |v| v[:content] }.join("\n")
194
+ {
195
+ required: all_content.scan(/\brequired\b/).size,
196
+ input_types: all_content.scan(/type=["'](\w+)["']/).flatten.tally
197
+ }
198
+ rescue => e
199
+ $stderr.puts "[rails-ai-context] analyze_form_inputs failed: #{e.message}" if ENV["DEBUG"]
200
+ {}
201
+ end
202
+
152
203
  def build_summary(views)
153
204
  all_content = views.map { |v| v[:content] }.join("\n")
154
205
 
@@ -40,8 +40,16 @@ module RailsAiContext
40
40
  { pattern: match[0], action: match[1] }
41
41
  end
42
42
 
43
- { name: name, file: relative, routing: routing }
44
- rescue
43
+ # Extract callbacks
44
+ callbacks = content.scan(/\b(before_processing|after_processing|around_processing)\s+:(\w+)/).map do |type, method|
45
+ { type: type, method: method }
46
+ end
47
+
48
+ entry = { name: name, file: relative, routing: routing }
49
+ entry[:callbacks] = callbacks if callbacks.any?
50
+ entry
51
+ rescue => e
52
+ $stderr.puts "[rails-ai-context] extract_mailboxes failed: #{e.message}" if ENV["DEBUG"]
45
53
  nil
46
54
  end.sort_by { |m| m[:name] }
47
55
  end
@@ -13,7 +13,8 @@ module RailsAiContext
13
13
  def call
14
14
  {
15
15
  installed: defined?(ActionText) ? true : false,
16
- rich_text_fields: extract_rich_text_fields
16
+ rich_text_fields: extract_rich_text_fields,
17
+ trix_customizations: detect_trix_customizations
17
18
  }
18
19
  rescue => e
19
20
  { error: e.message }
@@ -25,6 +26,24 @@ module RailsAiContext
25
26
  app.root.to_s
26
27
  end
27
28
 
29
+ def detect_trix_customizations
30
+ customs = []
31
+ js_dirs = [ File.join(app.root, "app", "javascript"), File.join(app.root, "app", "assets", "javascripts") ]
32
+ js_dirs.each do |dir|
33
+ next unless Dir.exist?(dir)
34
+ Dir.glob(File.join(dir, "**", "*.{js,ts}")).each do |path|
35
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
36
+ customs << "custom_toolbar" if content.match?(/Trix\.config\.toolbar/)
37
+ customs << "custom_attachment" if content.match?(/trix-attachment|Trix\.Attachment/)
38
+ customs << "custom_editor" if content.match?(/trix-initialize|trix-change/)
39
+ end
40
+ end
41
+ customs.uniq
42
+ rescue => e
43
+ $stderr.puts "[rails-ai-context] detect_trix_customizations failed: #{e.message}" if ENV["DEBUG"]
44
+ []
45
+ end
46
+
28
47
  def extract_rich_text_fields
29
48
  models_dir = File.join(root, "app/models")
30
49
  return [] unless Dir.exist?(models_dir)
@@ -40,7 +59,8 @@ module RailsAiContext
40
59
  end
41
60
 
42
61
  fields.sort_by { |f| [ f[:model], f[:field] ] }
43
- rescue
62
+ rescue => e
63
+ $stderr.puts "[rails-ai-context] extract_rich_text_fields failed: #{e.message}" if ENV["DEBUG"]
44
64
  []
45
65
  end
46
66
  end
@@ -16,7 +16,9 @@ module RailsAiContext
16
16
  installed: defined?(ActiveStorage) ? true : false,
17
17
  attachments: extract_attachments,
18
18
  storage_services: extract_storage_services,
19
- direct_upload: detect_direct_upload
19
+ direct_upload: detect_direct_upload,
20
+ validations: extract_attachment_validations,
21
+ variants: extract_variants
20
22
  }
21
23
  rescue => e
22
24
  { error: e.message }
@@ -47,7 +49,8 @@ module RailsAiContext
47
49
  end
48
50
 
49
51
  attachments.sort_by { |a| [ a[:model], a[:name] ] }
50
- rescue
52
+ rescue => e
53
+ $stderr.puts "[rails-ai-context] extract_attachments failed: #{e.message}" if ENV["DEBUG"]
51
54
  []
52
55
  end
53
56
 
@@ -58,7 +61,49 @@ module RailsAiContext
58
61
  require "yaml"
59
62
  config = YAML.load_file(config_path, permitted_classes: [ Symbol ], aliases: true) || {}
60
63
  config.keys.sort
61
- rescue
64
+ rescue => e
65
+ $stderr.puts "[rails-ai-context] extract_storage_services failed: #{e.message}" if ENV["DEBUG"]
66
+ []
67
+ end
68
+
69
+ def extract_attachment_validations
70
+ validations = []
71
+ models_dir = File.join(app.root, "app", "models")
72
+ return validations unless Dir.exist?(models_dir)
73
+
74
+ Dir.glob(File.join(models_dir, "**", "*.rb")).each do |path|
75
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
76
+ model = File.basename(path, ".rb").camelize
77
+ content.each_line do |line|
78
+ if (match = line.match(/validates?\s+:(\w+),.*content_type:/))
79
+ validations << { model: model, attachment: match[1], type: "content_type" }
80
+ end
81
+ if (match = line.match(/validates?\s+:(\w+),.*size:/))
82
+ validations << { model: model, attachment: match[1], type: "size" }
83
+ end
84
+ end
85
+ end
86
+ validations
87
+ rescue => e
88
+ $stderr.puts "[rails-ai-context] extract_attachment_validations failed: #{e.message}" if ENV["DEBUG"]
89
+ []
90
+ end
91
+
92
+ def extract_variants
93
+ variants = []
94
+ models_dir = File.join(app.root, "app", "models")
95
+ return variants unless Dir.exist?(models_dir)
96
+
97
+ Dir.glob(File.join(models_dir, "**", "*.rb")).each do |path|
98
+ content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
99
+ model = File.basename(path, ".rb").camelize
100
+ content.scan(/\.variant\s*\(\s*:(\w+)/).each do |name|
101
+ variants << { model: model, name: name[0] }
102
+ end
103
+ end
104
+ variants
105
+ rescue => e
106
+ $stderr.puts "[rails-ai-context] extract_variants failed: #{e.message}" if ENV["DEBUG"]
62
107
  []
63
108
  end
64
109
 
@@ -71,7 +116,8 @@ module RailsAiContext
71
116
  Dir.glob(File.join(dir, "**/*")).any? do |f|
72
117
  next false if File.directory?(f)
73
118
  File.read(f).match?(/direct.upload|DirectUpload|direct_upload/)
74
- rescue
119
+ rescue => e
120
+ $stderr.puts "[rails-ai-context] detect_direct_upload failed: #{e.message}" if ENV["DEBUG"]
75
121
  false
76
122
  end
77
123
  end
@@ -20,7 +20,9 @@ module RailsAiContext
20
20
  rate_limiting: detect_rate_limiting,
21
21
  openapi_spec: detect_openapi_specs,
22
22
  cors_config: detect_cors_config,
23
- api_client_generation: detect_api_client_generation
23
+ api_client_generation: detect_api_client_generation,
24
+ graphql_details: extract_graphql_details,
25
+ pagination: detect_pagination
24
26
  }
25
27
  rescue => e
26
28
  { error: e.message }
@@ -85,7 +87,8 @@ module RailsAiContext
85
87
  .map { |path| path.sub("#{root}/", "") }
86
88
  .sort
87
89
  .uniq
88
- rescue
90
+ rescue => e
91
+ $stderr.puts "[rails-ai-context] detect_openapi_specs failed: #{e.message}" if ENV["DEBUG"]
89
92
  []
90
93
  end
91
94
 
@@ -99,7 +102,8 @@ module RailsAiContext
99
102
  end
100
103
 
101
104
  { file: "config/initializers/cors.rb", origins: origins }
102
- rescue
105
+ rescue => e
106
+ $stderr.puts "[rails-ai-context] detect_cors_config failed: #{e.message}" if ENV["DEBUG"]
103
107
  nil
104
108
  end
105
109
 
@@ -111,10 +115,41 @@ module RailsAiContext
111
115
  codegen_tools = %w[openapi-typescript @graphql-codegen/cli orval]
112
116
 
113
117
  codegen_tools.select { |tool| content.include?(%("#{tool}")) }
114
- rescue
118
+ rescue => e
119
+ $stderr.puts "[rails-ai-context] detect_api_client_generation failed: #{e.message}" if ENV["DEBUG"]
115
120
  []
116
121
  end
117
122
 
123
+ def extract_graphql_details
124
+ graphql_dir = File.join(app.root, "app", "graphql")
125
+ return nil unless Dir.exist?(graphql_dir)
126
+
127
+ details = {}
128
+ details[:resolvers] = Dir.glob(File.join(graphql_dir, "**", "*resolver*")).map { |f| File.basename(f, ".rb").camelize }
129
+ details[:subscriptions] = Dir.glob(File.join(graphql_dir, "**", "subscriptions", "*.rb")).map { |f| File.basename(f, ".rb").camelize }
130
+ details[:dataloaders] = Dir.glob(File.join(graphql_dir, "**", "{loaders,dataloaders}", "*.rb")).map { |f| File.basename(f, ".rb").camelize }
131
+ details.reject { |_, v| v.empty? }
132
+ rescue => e
133
+ $stderr.puts "[rails-ai-context] extract_graphql_details failed: #{e.message}" if ENV["DEBUG"]
134
+ nil
135
+ end
136
+
137
+ def detect_pagination
138
+ gemfile_lock = File.join(app.root, "Gemfile.lock")
139
+ return nil unless File.exist?(gemfile_lock)
140
+ content = File.read(gemfile_lock)
141
+
142
+ strategies = []
143
+ strategies << "pagy" if content.include?("pagy")
144
+ strategies << "kaminari" if content.include?("kaminari")
145
+ strategies << "will_paginate" if content.include?("will_paginate")
146
+ strategies << "cursor" if content.include?("graphql-pro") # cursor-based pagination
147
+ strategies.empty? ? nil : strategies
148
+ rescue => e
149
+ $stderr.puts "[rails-ai-context] detect_pagination failed: #{e.message}" if ENV["DEBUG"]
150
+ nil
151
+ end
152
+
118
153
  def detect_rate_limiting
119
154
  # Rack::Attack
120
155
  init_path = File.join(root, "config/initializers/rack_attack.rb")
@@ -126,7 +161,8 @@ module RailsAiContext
126
161
  Dir.glob(File.join(controllers_dir, "**/*.rb")).each do |path|
127
162
  content = File.read(path)
128
163
  return { rails_rate_limiting: true } if content.match?(/rate_limit\b/)
129
- rescue
164
+ rescue => e
165
+ $stderr.puts "[rails-ai-context] detect_rate_limiting failed: #{e.message}" if ENV["DEBUG"]
130
166
  next
131
167
  end
132
168
  end
@@ -42,7 +42,8 @@ module RailsAiContext
42
42
 
43
43
  content = File.read(path)
44
44
  content.scan(/pin\s+["']([^"']+)["']/).flatten.sort
45
- rescue
45
+ rescue => e
46
+ $stderr.puts "[rails-ai-context] extract_importmap_pins failed: #{e.message}" if ENV["DEBUG"]
46
47
  []
47
48
  end
48
49
 
@@ -50,14 +51,17 @@ module RailsAiContext
50
51
  lock_content = read_gemfile_lock
51
52
  return nil unless lock_content
52
53
 
53
- return "tailwindcss" if lock_content.include?("tailwindcss-rails (")
54
+ return "tailwindcss" if lock_content.include?("tailwindcss-rails (") || package_json_has?("tailwindcss")
54
55
  return "bootstrap" if lock_content.include?("bootstrap (") || package_json_has?("bootstrap")
55
56
  return "bulma" if package_json_has?("bulma")
57
+ return "foundation" if package_json_has?("foundation-sites")
58
+ return "postcss" if package_json_has?("postcss") && !package_json_has?("tailwindcss")
56
59
  nil
57
60
  end
58
61
 
59
62
  def detect_js_bundler
60
63
  return "importmap" if File.exist?(File.join(root, "config/importmap.rb"))
64
+ return "bun" if File.exist?(File.join(root, "bun.lockb")) || File.exist?(File.join(root, "bunfig.toml"))
61
65
  return "esbuild" if package_json_has?("esbuild")
62
66
  return "webpack" if File.exist?(File.join(root, "config/webpack")) || package_json_has?("webpack")
63
67
  return "vite" if Dir.glob(File.join(root, "vite.config.*")).any?
@@ -76,7 +80,8 @@ module RailsAiContext
76
80
  def read_gemfile_lock
77
81
  path = File.join(root, "Gemfile.lock")
78
82
  File.exist?(path) ? File.read(path) : nil
79
- rescue
83
+ rescue => e
84
+ $stderr.puts "[rails-ai-context] read_gemfile_lock failed: #{e.message}" if ENV["DEBUG"]
80
85
  nil
81
86
  end
82
87
 
@@ -84,7 +89,8 @@ module RailsAiContext
84
89
  path = File.join(root, "package.json")
85
90
  return false unless File.exist?(path)
86
91
  File.read(path).include?("\"#{package}\"")
87
- rescue
92
+ rescue => e
93
+ $stderr.puts "[rails-ai-context] package_json_has? failed: #{e.message}" if ENV["DEBUG"]
88
94
  false
89
95
  end
90
96
  end