rails-ai-context 0.3.0 → 0.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/CLAUDE.md +1 -1
  4. data/README.md +22 -8
  5. data/lib/rails_ai_context/configuration.rb +1 -1
  6. data/lib/rails_ai_context/doctor.rb +43 -0
  7. data/lib/rails_ai_context/fingerprinter.rb +9 -1
  8. data/lib/rails_ai_context/introspector.rb +14 -0
  9. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +50 -0
  10. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +48 -0
  11. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +81 -0
  12. data/lib/rails_ai_context/introspectors/api_introspector.rb +92 -0
  13. data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +92 -0
  14. data/lib/rails_ai_context/introspectors/auth_introspector.rb +115 -0
  15. data/lib/rails_ai_context/introspectors/config_introspector.rb +85 -0
  16. data/lib/rails_ai_context/introspectors/controller_introspector.rb +135 -0
  17. data/lib/rails_ai_context/introspectors/convention_detector.rb +20 -1
  18. data/lib/rails_ai_context/introspectors/devops_introspector.rb +111 -0
  19. data/lib/rails_ai_context/introspectors/gem_introspector.rb +51 -1
  20. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +66 -0
  21. data/lib/rails_ai_context/introspectors/model_introspector.rb +42 -2
  22. data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +68 -0
  23. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +19 -3
  24. data/lib/rails_ai_context/introspectors/test_introspector.rb +137 -0
  25. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +80 -0
  26. data/lib/rails_ai_context/introspectors/view_introspector.rb +130 -0
  27. data/lib/rails_ai_context/resources.rb +18 -0
  28. data/lib/rails_ai_context/serializers/markdown_serializer.rb +241 -1
  29. data/lib/rails_ai_context/server.rb +4 -1
  30. data/lib/rails_ai_context/tools/get_config.rb +41 -0
  31. data/lib/rails_ai_context/tools/get_controllers.rb +67 -0
  32. data/lib/rails_ai_context/tools/get_routes.rb +1 -1
  33. data/lib/rails_ai_context/tools/get_schema.rb +5 -3
  34. data/lib/rails_ai_context/tools/get_test_info.rb +34 -0
  35. data/lib/rails_ai_context/version.rb +1 -1
  36. data/lib/rails_ai_context.rb +17 -0
  37. data/rails-ai-context.gemspec +2 -1
  38. metadata +20 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fa5518e88de666f981967ae1557990aadea0804c192511c4cbf204d6b45f146
4
- data.tar.gz: e39c7430277939e504aa3056ac0ac85f9f347dd008ff907627a032d7607ad21f
3
+ metadata.gz: 6f3efeed4bfe44c20ff766c0398b1d8a526a13c053b47d2a93d8faa320abfdda
4
+ data.tar.gz: 2cfe5512db995e2b08a2f9516793f1e68e787e8b76092780370ea6b04e280f69
5
5
  SHA512:
6
- metadata.gz: d1709f1becfbe976d7cd7a62690e90b31dbe5d61b2d09d9c9001ec43d0ce2d344d840d310d42d65ad9c1bf86bba2a0eb3285cb88eb074e131a373904224f7813
7
- data.tar.gz: 04714a3b181c91844c27b7b64ad2c2e9db963a16ae5ee037721f01afd518bad151b4d60dee8df78a69638b0fbd87af2c18288b70d14c62a72e72bd44f23f0c2d
6
+ metadata.gz: 4ccbff85f3a231a4c207522834387662af03240511c2bbbf491023e2b6361b68ea1e109cc3d8b383f02c9989db5542e555b6579b01037c25757acb3d84cc4525
7
+ data.tar.gz: 62a53606b4e98f331e716a44a8e424479e2cd61649de82814778feb1f4b3de23607f6a1f35fb6664c465fc130c9f2a140d928c6ce8864c4d4038f7ed27ddbcbc
data/CHANGELOG.md CHANGED
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-03-18
9
+
10
+ ### Added
11
+
12
+ - **14 new introspectors** — Controllers, Views, Turbo/Hotwire, I18n, Config, Active Storage, Action Text, Auth, API, Tests, Rake Tasks, Asset Pipeline, DevOps, Action Mailbox
13
+ - **3 new MCP tools** — `rails_get_controllers`, `rails_get_config`, `rails_get_test_info`
14
+ - **3 new MCP resources** — `rails://controllers`, `rails://config`, `rails://tests`
15
+ - **Model introspector enhancements** — Extracts `has_secure_password`, `encrypts`, `normalizes`, `delegate`, `serialize`, `store`, `generates_token_for`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `broadcasts_to` via source parsing
16
+ - **Stimulus introspector enhancements** — Extracts `outlets` and `classes` from controllers
17
+ - **Gem introspector enhancements** — 30+ new notable gems: monitoring (Sentry, Datadog, New Relic, Skylight), admin (ActiveAdmin, Administrate, Avo), pagination (Pagy, Kaminari), search (Ransack, pg_search, Searchkick), forms (SimpleForm), utilities (Faraday, Flipper, Bullet, Rack::Attack), and more
18
+ - **Convention detector enhancements** — Detects concerns, validators, policies, serializers, notifiers, Phlex, PWA, encrypted attributes, normalizations
19
+ - **Markdown serializer sections** — All 14 new introspector sections rendered in generated context files
20
+ - **Doctor enhancements** — 4 new checks: controllers, views, i18n, tests (11 total)
21
+ - **Fingerprinter expansion** — Watches `app/controllers`, `app/views`, `app/jobs`, `app/mailers`, `app/channels`, `app/javascript/controllers`, `config/initializers`, `lib/tasks`; glob now covers `.rb`, `.rake`, `.js`, `.ts`, `.erb`, `.haml`, `.slim`, `.yml`
22
+
23
+ ### Fixed
24
+
25
+ - **YAML parsing** — `YAML.load_file` calls now pass `permitted_classes: [Symbol], aliases: true` for Psych 4 (Ruby 3.1+) compatibility
26
+ - **Rake task parser** — Fixed `@last_desc` instance variable leaking between files; fixed namespace tracking with indent-based stack
27
+ - **Vite detection** — Changed `File.exist?("vite.config")` to `Dir.glob("vite.config.*")` to match `.js`/`.ts`/`.mjs` extensions
28
+ - **Health check regex** — Added word boundaries to avoid false positives on substrings (e.g. "groups" matching "up")
29
+ - **Multi-attribute macros** — `normalizes :email, :name` now captures all attributes, not just the first
30
+ - **Stimulus action regex** — Requires `method(args) {` pattern to avoid matching control flow keywords
31
+ - **Controller respond_to** — Simplified format extraction to avoid nested `end` keyword issues
32
+ - **GetRoutes nil guard** — Added `|| {}` fallback for `by_controller` to prevent crash on partial introspection data
33
+ - **GetSchema nil guard** — Added `|| {}` fallback for `schema[:tables]` to prevent crash on partial schema data
34
+ - **View layout discovery** — Added `File.file?` filter to exclude directories from layout listing
35
+ - **Fingerprinter glob** — Changed from `**/*.rb` to multi-extension glob to detect changes in `.rake`, `.js`, `.ts`, `.erb` files
36
+
37
+ ### Changed
38
+
39
+ - Default introspectors expanded from 7 to 21
40
+ - MCP tools expanded from 6 to 9
41
+ - Static MCP resources expanded from 4 to 7
42
+ - Doctor checks expanded from 7 to 11
43
+ - Test suite expanded from 149 to 247 examples with exact value assertions
44
+
8
45
  ## [0.3.0] - 2026-03-18
9
46
 
10
47
  ### Added
data/CLAUDE.md CHANGED
@@ -7,7 +7,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
7
7
 
8
8
  - `lib/rails_ai_context.rb` — Main entry point, public API
9
9
  - `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
10
- - `lib/rails_ai_context/introspectors/` — Individual introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats)
10
+ - `lib/rails_ai_context/introspectors/` — Individual introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox)
11
11
  - `lib/rails_ai_context/tools/` — MCP tools using the official mcp SDK
12
12
  - `lib/rails_ai_context/serializers/` — Output formatters (claude, rules, copilot, markdown, JSON)
13
13
  - `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![CI](https://github.com/crisnahine/rails-ai-context/actions/workflows/ci.yml/badge.svg)](https://github.com/crisnahine/rails-ai-context/actions)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
 
9
- `rails-ai-context` automatically introspects your Rails application and exposes your models, routes, schema, jobs, gems, and conventions to AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
9
+ `rails-ai-context` automatically introspects your Rails application and exposes your models, routes, schema, controllers, views, jobs, gems, auth, API, tests, config, and conventions to AI assistants through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
10
10
 
11
11
  **Your AI assistant instantly understands your entire Rails app. No configuration. No manual tool definitions. Just `bundle add` and go.**
12
12
 
@@ -27,12 +27,21 @@ bundle add rails-ai-context
27
27
  That's it. Now your AI assistant knows:
28
28
 
29
29
  - **Every table, column, index, and foreign key** in your database
30
- - **Every model** with its associations, validations, scopes, enums, and callbacks
30
+ - **Every model** with associations, validations, scopes, enums, callbacks, and macros (has_secure_password, encrypts, normalizes, etc.)
31
+ - **Every controller** with actions, filters, strong params, and concerns
32
+ - **Every view** with layouts, templates, partials, helpers, and template engines
31
33
  - **Every route** with HTTP verbs, paths, and controller actions
32
34
  - **Every background job**, mailer, and Action Cable channel
33
- - **Every notable gem** and what it means (Devise = auth, Sidekiq = jobs, Turbo = Hotwire)
35
+ - **Hotwire/Turbo** Turbo Frames, Turbo Streams, model broadcasts
36
+ - **Every notable gem** (70+) and what it means (Devise = auth, Sidekiq = jobs, Turbo = Hotwire)
37
+ - **Auth & security** — Devise modules, Pundit policies, CanCanCan, CORS, CSP
38
+ - **API layer** — serializers, GraphQL, versioning, rate limiting
39
+ - **Test infrastructure** — framework, factories/fixtures, CI config, coverage
40
+ - **Configuration** — cache store, session store, middleware, initializers
41
+ - **Asset pipeline** — Propshaft/Sprockets, importmaps, CSS framework, JS bundler
42
+ - **DevOps** — Puma config, Procfile, Docker, deployment tools
34
43
  - **Your architecture patterns**: service objects, STI, polymorphism, state machines, multi-tenancy
35
- - **Stimulus controllers** with targets, values, and actions
44
+ - **Stimulus controllers** with targets, values, actions, outlets, and classes
36
45
 
37
46
  ---
38
47
 
@@ -85,7 +94,7 @@ Or add to your Claude Code config (`~/.claude/claude_desktop_config.json`):
85
94
 
86
95
  ## MCP Tools
87
96
 
88
- The gem exposes 6 tools via MCP that AI clients can call:
97
+ The gem exposes 9 tools via MCP that AI clients can call:
89
98
 
90
99
  | Tool | Description | Annotations |
91
100
  |------|-------------|-------------|
@@ -95,6 +104,9 @@ The gem exposes 6 tools via MCP that AI clients can call:
95
104
  | `rails_get_gems` | Notable gems categorized by function with explanations | read-only, idempotent |
96
105
  | `rails_search_code` | Ripgrep-powered code search across the codebase | read-only, idempotent |
97
106
  | `rails_get_conventions` | Architecture patterns, directory structure, config files | read-only, idempotent |
107
+ | `rails_get_controllers` | Controller actions, filters, strong params, concerns | read-only, idempotent |
108
+ | `rails_get_config` | App configuration: cache, sessions, middleware, initializers | read-only, idempotent |
109
+ | `rails_get_test_info` | Test framework, factories, CI config, coverage | read-only, idempotent |
98
110
 
99
111
  All tools are **read-only** — they never modify your application or database.
100
112
 
@@ -108,6 +120,9 @@ In addition to tools, the gem registers MCP resources that AI clients can read d
108
120
  | `rails://routes` | All routes (JSON) |
109
121
  | `rails://conventions` | Detected patterns and architecture (JSON) |
110
122
  | `rails://gems` | Notable gems with categories (JSON) |
123
+ | `rails://controllers` | All controllers with actions and filters (JSON) |
124
+ | `rails://config` | Application configuration (JSON) |
125
+ | `rails://tests` | Test infrastructure details (JSON) |
111
126
  | `rails://models/{name}` | Per-model details (resource template) |
112
127
 
113
128
  ---
@@ -178,7 +193,7 @@ Check your app's AI readiness:
178
193
  rails ai:doctor
179
194
  ```
180
195
 
181
- Reports pass/warn/fail for schema, models, routes, gems, context files, MCP server, and ripgrep. Includes fix suggestions and an AI readiness score (0-100).
196
+ Reports pass/warn/fail for schema, models, routes, gems, controllers, views, i18n, tests, context files, MCP server, and ripgrep. Includes fix suggestions and an AI readiness score (0-100).
182
197
 
183
198
  ---
184
199
 
@@ -186,8 +201,7 @@ Reports pass/warn/fail for schema, models, routes, gems, context files, MCP serv
186
201
 
187
202
  The gem automatically detects Stimulus controllers and extracts:
188
203
  - Controller names (derived from filenames)
189
- - Static targets
190
- - Static values
204
+ - Static targets, values, outlets, and classes
191
205
  - Action methods
192
206
 
193
207
  This gives AI assistants context about your frontend JavaScript alongside your backend Ruby.
@@ -32,7 +32,7 @@ module RailsAiContext
32
32
  def initialize
33
33
  @server_name = "rails-ai-context"
34
34
  @server_version = RailsAiContext::VERSION
35
- @introspectors = %i[schema models routes jobs gems conventions stimulus]
35
+ @introspectors = %i[schema models routes jobs gems conventions stimulus controllers views turbo i18n config active_storage action_text auth api tests rake_tasks assets devops action_mailbox]
36
36
  @excluded_paths = %w[node_modules tmp log vendor .git]
37
37
  @auto_mount = false
38
38
  @http_path = "/mcp"
@@ -11,6 +11,10 @@ module RailsAiContext
11
11
  check_models
12
12
  check_routes
13
13
  check_gems
14
+ check_controllers
15
+ check_views
16
+ check_i18n
17
+ check_tests
14
18
  check_context_files
15
19
  check_mcp_buildable
16
20
  check_ripgrep
@@ -67,6 +71,45 @@ module RailsAiContext
67
71
  end
68
72
  end
69
73
 
74
+ def check_controllers
75
+ dir = File.join(app.root, "app/controllers")
76
+ if Dir.exist?(dir) && Dir.glob(File.join(dir, "**/*.rb")).any?
77
+ count = Dir.glob(File.join(dir, "**/*.rb")).size
78
+ Check.new(name: "Controllers", status: :pass, message: "#{count} controller files found", fix: nil)
79
+ else
80
+ Check.new(name: "Controllers", status: :warn, message: "No controller files found in app/controllers/", fix: "Generate controllers with `rails generate controller`")
81
+ end
82
+ end
83
+
84
+ def check_views
85
+ dir = File.join(app.root, "app/views")
86
+ if Dir.exist?(dir) && Dir.glob(File.join(dir, "**/*")).reject { |f| File.directory?(f) }.any?
87
+ count = Dir.glob(File.join(dir, "**/*")).reject { |f| File.directory?(f) }.size
88
+ Check.new(name: "Views", status: :pass, message: "#{count} view files found", fix: nil)
89
+ else
90
+ Check.new(name: "Views", status: :warn, message: "No view files found in app/views/", fix: "Views are generated alongside controllers")
91
+ end
92
+ end
93
+
94
+ def check_i18n
95
+ dir = File.join(app.root, "config/locales")
96
+ if Dir.exist?(dir) && Dir.glob(File.join(dir, "**/*.{yml,yaml}")).any?
97
+ count = Dir.glob(File.join(dir, "**/*.{yml,yaml}")).size
98
+ Check.new(name: "I18n", status: :pass, message: "#{count} locale files found", fix: nil)
99
+ else
100
+ Check.new(name: "I18n", status: :warn, message: "No locale files found in config/locales/", fix: "Add locale files for internationalization support")
101
+ end
102
+ end
103
+
104
+ def check_tests
105
+ if Dir.exist?(File.join(app.root, "spec")) || Dir.exist?(File.join(app.root, "test"))
106
+ framework = Dir.exist?(File.join(app.root, "spec")) ? "RSpec" : "Minitest"
107
+ Check.new(name: "Tests", status: :pass, message: "#{framework} test directory found", fix: nil)
108
+ else
109
+ Check.new(name: "Tests", status: :warn, message: "No test directory found", fix: "Set up tests with `rails generate rspec:install` or use default Minitest")
110
+ end
111
+ end
112
+
70
113
  def check_context_files
71
114
  claude_path = File.join(app.root, "CLAUDE.md")
72
115
  if File.exist?(claude_path)
@@ -14,6 +14,14 @@ module RailsAiContext
14
14
 
15
15
  WATCHED_DIRS = %w[
16
16
  app/models
17
+ app/controllers
18
+ app/views
19
+ app/jobs
20
+ app/mailers
21
+ app/channels
22
+ app/javascript/controllers
23
+ config/initializers
24
+ lib/tasks
17
25
  ].freeze
18
26
 
19
27
  class << self
@@ -30,7 +38,7 @@ module RailsAiContext
30
38
  full_dir = File.join(root, dir)
31
39
  next unless Dir.exist?(full_dir)
32
40
 
33
- Dir.glob(File.join(full_dir, "**/*.rb")).sort.each do |path|
41
+ Dir.glob(File.join(full_dir, "**/*.{rb,rake,js,ts,erb,haml,slim,yml}")).sort.each do |path|
34
42
  digest.update(File.mtime(path).to_f.to_s)
35
43
  end
36
44
  end
@@ -55,6 +55,20 @@ module RailsAiContext
55
55
  when :conventions then Introspectors::ConventionDetector.new(app)
56
56
  when :stimulus then Introspectors::StimulusIntrospector.new(app)
57
57
  when :database_stats then Introspectors::DatabaseStatsIntrospector.new(app)
58
+ when :controllers then Introspectors::ControllerIntrospector.new(app)
59
+ when :views then Introspectors::ViewIntrospector.new(app)
60
+ when :turbo then Introspectors::TurboIntrospector.new(app)
61
+ when :i18n then Introspectors::I18nIntrospector.new(app)
62
+ when :config then Introspectors::ConfigIntrospector.new(app)
63
+ when :active_storage then Introspectors::ActiveStorageIntrospector.new(app)
64
+ when :action_text then Introspectors::ActionTextIntrospector.new(app)
65
+ when :auth then Introspectors::AuthIntrospector.new(app)
66
+ when :api then Introspectors::ApiIntrospector.new(app)
67
+ when :tests then Introspectors::TestIntrospector.new(app)
68
+ when :rake_tasks then Introspectors::RakeTaskIntrospector.new(app)
69
+ when :assets then Introspectors::AssetPipelineIntrospector.new(app)
70
+ when :devops then Introspectors::DevOpsIntrospector.new(app)
71
+ when :action_mailbox then Introspectors::ActionMailboxIntrospector.new(app)
58
72
  else
59
73
  raise ConfigurationError, "Unknown introspector: #{name}"
60
74
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ # Discovers Action Mailbox setup: mailbox classes, routing patterns.
6
+ class ActionMailboxIntrospector
7
+ attr_reader :app
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call
14
+ {
15
+ installed: defined?(ActionMailbox) ? true : false,
16
+ mailboxes: extract_mailboxes
17
+ }
18
+ rescue => e
19
+ { error: e.message }
20
+ end
21
+
22
+ private
23
+
24
+ def root
25
+ app.root.to_s
26
+ end
27
+
28
+ def extract_mailboxes
29
+ dir = File.join(root, "app/mailboxes")
30
+ return [] unless Dir.exist?(dir)
31
+
32
+ Dir.glob(File.join(dir, "**/*.rb")).filter_map do |path|
33
+ relative = path.sub("#{dir}/", "")
34
+ next if relative == "application_mailbox.rb"
35
+
36
+ content = File.read(path)
37
+ name = File.basename(path, ".rb").camelize
38
+
39
+ routing = content.scan(/routing\s+(.+?)\s+=>\s+:(\w+)/).map do |match|
40
+ { pattern: match[0], action: match[1] }
41
+ end
42
+
43
+ { name: name, file: relative, routing: routing }
44
+ rescue
45
+ nil
46
+ end.sort_by { |m| m[:name] }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ # Discovers Action Text usage: rich text fields per model.
6
+ class ActionTextIntrospector
7
+ attr_reader :app
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call
14
+ {
15
+ installed: defined?(ActionText) ? true : false,
16
+ rich_text_fields: extract_rich_text_fields
17
+ }
18
+ rescue => e
19
+ { error: e.message }
20
+ end
21
+
22
+ private
23
+
24
+ def root
25
+ app.root.to_s
26
+ end
27
+
28
+ def extract_rich_text_fields
29
+ models_dir = File.join(root, "app/models")
30
+ return [] unless Dir.exist?(models_dir)
31
+
32
+ fields = []
33
+ Dir.glob(File.join(models_dir, "**/*.rb")).each do |path|
34
+ content = File.read(path)
35
+ model_name = File.basename(path, ".rb").camelize
36
+
37
+ content.scan(/has_rich_text\s+:(\w+)/).each do |match|
38
+ fields << { model: model_name, field: match[0] }
39
+ end
40
+ end
41
+
42
+ fields.sort_by { |f| [ f[:model], f[:field] ] }
43
+ rescue
44
+ []
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ # Discovers Active Storage usage: attachments, storage service config,
6
+ # direct upload detection.
7
+ class ActiveStorageIntrospector
8
+ attr_reader :app
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call
15
+ {
16
+ installed: defined?(ActiveStorage) ? true : false,
17
+ attachments: extract_attachments,
18
+ storage_services: extract_storage_services,
19
+ direct_upload: detect_direct_upload
20
+ }
21
+ rescue => e
22
+ { error: e.message }
23
+ end
24
+
25
+ private
26
+
27
+ def root
28
+ app.root.to_s
29
+ end
30
+
31
+ def extract_attachments
32
+ models_dir = File.join(root, "app/models")
33
+ return [] unless Dir.exist?(models_dir)
34
+
35
+ attachments = []
36
+ Dir.glob(File.join(models_dir, "**/*.rb")).each do |path|
37
+ content = File.read(path)
38
+ model_name = File.basename(path, ".rb").camelize
39
+
40
+ content.scan(/has_one_attached\s+:(\w+)/).each do |match|
41
+ attachments << { model: model_name, name: match[0], type: "has_one_attached" }
42
+ end
43
+
44
+ content.scan(/has_many_attached\s+:(\w+)/).each do |match|
45
+ attachments << { model: model_name, name: match[0], type: "has_many_attached" }
46
+ end
47
+ end
48
+
49
+ attachments.sort_by { |a| [ a[:model], a[:name] ] }
50
+ rescue
51
+ []
52
+ end
53
+
54
+ def extract_storage_services
55
+ config_path = File.join(root, "config/storage.yml")
56
+ return [] unless File.exist?(config_path)
57
+
58
+ require "yaml"
59
+ config = YAML.load_file(config_path, permitted_classes: [ Symbol ], aliases: true) || {}
60
+ config.keys.sort
61
+ rescue
62
+ []
63
+ end
64
+
65
+ def detect_direct_upload
66
+ views_dir = File.join(root, "app/views")
67
+ js_dir = File.join(root, "app/javascript")
68
+
69
+ [ views_dir, js_dir ].any? do |dir|
70
+ next false unless Dir.exist?(dir)
71
+ Dir.glob(File.join(dir, "**/*")).any? do |f|
72
+ next false if File.directory?(f)
73
+ File.read(f).match?(/direct.upload|DirectUpload|direct_upload/)
74
+ rescue
75
+ false
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ # Discovers API layer setup: api_only mode, serializers, GraphQL,
6
+ # versioning patterns, rate limiting.
7
+ class ApiIntrospector
8
+ attr_reader :app
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call
15
+ {
16
+ api_only: app.config.api_only,
17
+ serializers: detect_serializers,
18
+ graphql: detect_graphql,
19
+ api_versioning: detect_versioning,
20
+ rate_limiting: detect_rate_limiting
21
+ }
22
+ rescue => e
23
+ { error: e.message }
24
+ end
25
+
26
+ private
27
+
28
+ def root
29
+ app.root.to_s
30
+ end
31
+
32
+ def detect_serializers
33
+ result = {}
34
+
35
+ # Jbuilder templates
36
+ views_dir = File.join(root, "app/views")
37
+ if Dir.exist?(views_dir)
38
+ jbuilder_files = Dir.glob(File.join(views_dir, "**/*.jbuilder"))
39
+ result[:jbuilder] = jbuilder_files.size if jbuilder_files.any?
40
+ end
41
+
42
+ # Serializer classes (Alba, Blueprinter, JSONAPI, etc.)
43
+ serializers_dir = File.join(root, "app/serializers")
44
+ if Dir.exist?(serializers_dir)
45
+ files = Dir.glob(File.join(serializers_dir, "**/*.rb"))
46
+ result[:serializer_classes] = files.map { |f| f.sub("#{serializers_dir}/", "").sub(/\.rb\z/, "").camelize }.sort
47
+ end
48
+
49
+ result
50
+ end
51
+
52
+ def detect_graphql
53
+ graphql_dir = File.join(root, "app/graphql")
54
+ return nil unless Dir.exist?(graphql_dir)
55
+
56
+ types = Dir.glob(File.join(graphql_dir, "types/**/*.rb")).size
57
+ mutations = Dir.glob(File.join(graphql_dir, "mutations/**/*.rb")).size
58
+ queries = Dir.glob(File.join(graphql_dir, "queries/**/*.rb")).size
59
+
60
+ { types: types, mutations: mutations, queries: queries }
61
+ end
62
+
63
+ def detect_versioning
64
+ controllers_dir = File.join(root, "app/controllers")
65
+ return [] unless Dir.exist?(controllers_dir)
66
+
67
+ Dir.glob(File.join(controllers_dir, "api/v*/")).filter_map do |path|
68
+ File.basename(path)
69
+ end.sort
70
+ end
71
+
72
+ def detect_rate_limiting
73
+ # Rack::Attack
74
+ init_path = File.join(root, "config/initializers/rack_attack.rb")
75
+ return { rack_attack: true } if File.exist?(init_path)
76
+
77
+ # Rails 8 rate limiting
78
+ controllers_dir = File.join(root, "app/controllers")
79
+ if Dir.exist?(controllers_dir)
80
+ Dir.glob(File.join(controllers_dir, "**/*.rb")).each do |path|
81
+ content = File.read(path)
82
+ return { rails_rate_limiting: true } if content.match?(/rate_limit\b/)
83
+ rescue
84
+ next
85
+ end
86
+ end
87
+
88
+ {}
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ # Discovers asset pipeline configuration: Propshaft/Sprockets,
6
+ # importmap pins, CSS framework, JS bundler.
7
+ class AssetPipelineIntrospector
8
+ attr_reader :app
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call
15
+ {
16
+ pipeline: detect_pipeline,
17
+ importmap_pins: extract_importmap_pins,
18
+ css_framework: detect_css_framework,
19
+ js_bundler: detect_js_bundler,
20
+ manifest_files: detect_manifests
21
+ }
22
+ rescue => e
23
+ { error: e.message }
24
+ end
25
+
26
+ private
27
+
28
+ def root
29
+ app.root.to_s
30
+ end
31
+
32
+ def detect_pipeline
33
+ lock_content = read_gemfile_lock
34
+ return "propshaft" if lock_content&.include?("propshaft (")
35
+ return "sprockets" if lock_content&.include?("sprockets (")
36
+ "none"
37
+ end
38
+
39
+ def extract_importmap_pins
40
+ path = File.join(root, "config/importmap.rb")
41
+ return [] unless File.exist?(path)
42
+
43
+ content = File.read(path)
44
+ content.scan(/pin\s+["']([^"']+)["']/).flatten.sort
45
+ rescue
46
+ []
47
+ end
48
+
49
+ def detect_css_framework
50
+ lock_content = read_gemfile_lock
51
+ return nil unless lock_content
52
+
53
+ return "tailwindcss" if lock_content.include?("tailwindcss-rails (")
54
+ return "bootstrap" if lock_content.include?("bootstrap (") || package_json_has?("bootstrap")
55
+ return "bulma" if package_json_has?("bulma")
56
+ nil
57
+ end
58
+
59
+ def detect_js_bundler
60
+ return "importmap" if File.exist?(File.join(root, "config/importmap.rb"))
61
+ return "esbuild" if package_json_has?("esbuild")
62
+ return "webpack" if File.exist?(File.join(root, "config/webpack")) || package_json_has?("webpack")
63
+ return "vite" if Dir.glob(File.join(root, "vite.config.*")).any?
64
+ return "rollup" if package_json_has?("rollup")
65
+ nil
66
+ end
67
+
68
+ def detect_manifests
69
+ manifests = []
70
+ manifests << "manifest.js" if File.exist?(File.join(root, "app/assets/config/manifest.js"))
71
+ manifests << "package.json" if File.exist?(File.join(root, "package.json"))
72
+ manifests << "importmap.rb" if File.exist?(File.join(root, "config/importmap.rb"))
73
+ manifests
74
+ end
75
+
76
+ def read_gemfile_lock
77
+ path = File.join(root, "Gemfile.lock")
78
+ File.exist?(path) ? File.read(path) : nil
79
+ rescue
80
+ nil
81
+ end
82
+
83
+ def package_json_has?(package)
84
+ path = File.join(root, "package.json")
85
+ return false unless File.exist?(path)
86
+ File.read(path).include?("\"#{package}\"")
87
+ rescue
88
+ false
89
+ end
90
+ end
91
+ end
92
+ end