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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +37 -0
- data/CLAUDE.md +1 -1
- data/README.md +22 -8
- data/lib/rails_ai_context/configuration.rb +1 -1
- data/lib/rails_ai_context/doctor.rb +43 -0
- data/lib/rails_ai_context/fingerprinter.rb +9 -1
- data/lib/rails_ai_context/introspector.rb +14 -0
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +50 -0
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +48 -0
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +81 -0
- data/lib/rails_ai_context/introspectors/api_introspector.rb +92 -0
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +92 -0
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +115 -0
- data/lib/rails_ai_context/introspectors/config_introspector.rb +85 -0
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +135 -0
- data/lib/rails_ai_context/introspectors/convention_detector.rb +20 -1
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +111 -0
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +51 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +66 -0
- data/lib/rails_ai_context/introspectors/model_introspector.rb +42 -2
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +68 -0
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +19 -3
- data/lib/rails_ai_context/introspectors/test_introspector.rb +137 -0
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +80 -0
- data/lib/rails_ai_context/introspectors/view_introspector.rb +130 -0
- data/lib/rails_ai_context/resources.rb +18 -0
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +241 -1
- data/lib/rails_ai_context/server.rb +4 -1
- data/lib/rails_ai_context/tools/get_config.rb +41 -0
- data/lib/rails_ai_context/tools/get_controllers.rb +67 -0
- data/lib/rails_ai_context/tools/get_routes.rb +1 -1
- data/lib/rails_ai_context/tools/get_schema.rb +5 -3
- data/lib/rails_ai_context/tools/get_test_info.rb +34 -0
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +17 -0
- data/rails-ai-context.gemspec +2 -1
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f3efeed4bfe44c20ff766c0398b1d8a526a13c053b47d2a93d8faa320abfdda
|
|
4
|
+
data.tar.gz: 2cfe5512db995e2b08a2f9516793f1e68e787e8b76092780370ea6b04e280f69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
7
7
|
[](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
|
|
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
|
-
- **
|
|
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
|
|
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
|
|
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
|