rails-ai-context 0.13.1 → 0.15.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 +59 -0
- data/CLAUDE.md +1 -1
- data/README.md +9 -4
- data/docs/GUIDE.md +3 -3
- data/lib/rails_ai_context/configuration.rb +8 -1
- data/lib/rails_ai_context/introspectors/config_introspector.rb +43 -6
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +138 -5
- data/lib/rails_ai_context/introspectors/model_introspector.rb +44 -3
- data/lib/rails_ai_context/introspectors/route_introspector.rb +3 -0
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +58 -7
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +37 -3
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +14 -4
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +22 -4
- data/lib/rails_ai_context/serializers/claude_serializer.rb +34 -7
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +3 -2
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +3 -2
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +5 -2
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +33 -7
- data/lib/rails_ai_context/serializers/windsurf_serializer.rb +3 -2
- data/lib/rails_ai_context/tools/get_config.rb +40 -4
- data/lib/rails_ai_context/tools/get_controllers.rb +72 -24
- data/lib/rails_ai_context/tools/get_conventions.rb +10 -3
- data/lib/rails_ai_context/tools/get_edit_context.rb +36 -8
- data/lib/rails_ai_context/tools/get_gems.rb +2 -4
- data/lib/rails_ai_context/tools/get_model_details.rb +45 -11
- data/lib/rails_ai_context/tools/get_routes.rb +125 -16
- data/lib/rails_ai_context/tools/get_schema.rb +25 -5
- data/lib/rails_ai_context/tools/get_stimulus.rb +24 -5
- data/lib/rails_ai_context/tools/get_test_info.rb +16 -3
- data/lib/rails_ai_context/tools/get_view.rb +82 -25
- data/lib/rails_ai_context/tools/search_code.rb +35 -5
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +2 -2
- metadata +1 -3
- data/demo.tape +0 -16
- data/demo_script.sh +0 -93
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 840dab1aeabc3323d5aca5702d95e712f7a013f24d05327424674262eee46de9
|
|
4
|
+
data.tar.gz: 2eed45a1bc5e79b9806f691c9e88ca2ced55660d53b74f56d8f354963c9ee497
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac390f1d5b6b148f33fdac979d98269695fed76bceaaaab6b18c0682819a271a8dfdbaaab00f6c35eaaa2a867f652bc46c8d3d88199c5cd3bd1a87e20a0b3320
|
|
7
|
+
data.tar.gz: 0eb688c270bf08ddb5b70fc3dfbcca19432de3c6922c8a0f784081d82c117458fc3e136a56a962ee3988a9263a1ae9d9eff48716e4321e57bbc92d533622b249
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,65 @@ 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.15.0] - 2026-03-22
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **Sensitive file blocking** — `search_code` and `get_edit_context` now block access to `.env*`, `*.key`, `*.pem`, `config/master.key`, `config/credentials.yml.enc`. Configurable via `config.sensitive_patterns`.
|
|
13
|
+
- **Credentials key names redacted** — Replaced `credentials_keys` (exposed names like `stripe_secret_key`) with `credentials_configured` boolean. No more information disclosure via JSON output or MCP resources.
|
|
14
|
+
- **View content size cap** — `collect_all_view_content` capped at 5MB total / 500KB per file to prevent memory exhaustion.
|
|
15
|
+
- **Schema file size limits** — 10MB limit on `schema.rb`/`structure.sql` parsing. Cached `schema.rb` reads to avoid re-reading per table.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Token optimization (~1,500-2,700 tokens/session saved)**:
|
|
20
|
+
- Filter framework filters (`verify_authenticity_token`, etc.) from controller output
|
|
21
|
+
- Filter framework/gem concerns (`Devise::*`, `Turbo::*`, `*::Generated*`) from models
|
|
22
|
+
- Combine duplicate PUT/PATCH routes into single `PATCH|PUT` entry
|
|
23
|
+
- Only show Nullable/Default columns when they have meaningful values
|
|
24
|
+
- Drop gem version numbers from default output
|
|
25
|
+
- Single HTML naming hint for Stimulus (not per-controller)
|
|
26
|
+
- Only show non-default middleware and initializers in config
|
|
27
|
+
- Group sibling controllers/routes with identical structure
|
|
28
|
+
- Compress repeated Tailwind classes in view full output
|
|
29
|
+
- Strip inline SVGs from view content
|
|
30
|
+
- Separate active vs lifecycle-only Stimulus controllers
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Controller staleness** — Source-file parsing for actions/filters instead of Ruby reflection. Filesystem discovery for new controllers not yet loaded as classes.
|
|
35
|
+
- **Schema `t.index` format** — Parse indexes inside `create_table` blocks (not just `add_index` outside).
|
|
36
|
+
- **Stimulus nested values** — Brace-depth counting for single-line `{ active: { type: String, default: "overview" } }`.
|
|
37
|
+
- **Stimulus phantom `type:Number`** — Exclude `type`/`default` as value names (JS keywords, not Stimulus values).
|
|
38
|
+
- **Search context_lines** — Use `--field-context-separator=:` for ripgrep `-C` output compatibility.
|
|
39
|
+
- **Schema defaults** — Supplement live DB nil defaults with values from `schema.rb`.
|
|
40
|
+
- **Config missing data** — Added `queue_adapter` and `mailer` settings to config introspector and tool.
|
|
41
|
+
- **View garbled fields** — Only extract from `@variable.field` patterns (not arbitrary method chains).
|
|
42
|
+
- **View shared partials** — `controller:"shared"` now finds partials in `app/views/shared/`.
|
|
43
|
+
- **View full detail** — Lists available controllers when no controller specified.
|
|
44
|
+
- **Edit context hint** — "Also found" only shown for matches outside the context window.
|
|
45
|
+
- **Model file structure** — Compressed to single-line format.
|
|
46
|
+
- **Strong params body** — Action detail now shows the actual `permit(...)` call.
|
|
47
|
+
- **AR-generated methods** — Filter `build_*`, `*_ids=`, etc. from model instance methods.
|
|
48
|
+
|
|
49
|
+
## [0.14.0] - 2026-03-20
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **Schema 0 indexes** — Fixed composite index parsing in schema.rb (regex didn't match array syntax) and structure.sql (`.first` only took first column). Both single and composite indexes now extracted correctly.
|
|
54
|
+
- **Stale routes after editing routes.rb** — Route introspector now calls `routes_reloader.execute_if_updated` to force Rails to reload routes before extraction.
|
|
55
|
+
- **Config "not available"** — Added `:config` to `:standard` preset. Was `:full` only, so default users never saw config data.
|
|
56
|
+
- **Stimulus values lost name** — Fixed parsing for both simple (`name: Type`) and complex (`name: { type: Type, default: val }`) formats. Now shows `max: Number (default: 3)`.
|
|
57
|
+
- **Model concerns noise** — Filtered out internal Rails modules (ActiveRecord::, ActiveModel::, Kernel, JSON::, etc.) from concerns list.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **Route helpers in standard detail** — `rails_get_routes(detail: "standard")` now includes route helper names alongside paths.
|
|
62
|
+
- **`app_only` filter for routes** — `rails_get_routes(app_only: true)` (default) hides internal Rails routes (Active Storage, Action Mailbox, Conductor).
|
|
63
|
+
- **Search context lines** — `rails_search_code(context_lines: 2)` adds surrounding lines to matches (passes `-C` to ripgrep).
|
|
64
|
+
- **Stimulus dash/underscore normalization** — Both `weekly-chart` and `weekly_chart` work for controller lookup. Output shows HTML `data-controller` attribute.
|
|
65
|
+
- **Model public method signatures** — `rails_get_model_details(model: "Cook")` shows method names with params from source, stopping at private boundary.
|
|
66
|
+
|
|
8
67
|
## [0.13.0] - 2026-03-20
|
|
9
68
|
|
|
10
69
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -32,7 +32,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
32
32
|
6. **Diff-aware** — context regeneration skips unchanged files
|
|
33
33
|
7. **Per-assistant serializers** — each AI tool gets tailored output format
|
|
34
34
|
8. **Zeitwerk autoloading** — files loaded on-demand, not all upfront
|
|
35
|
-
9. **Introspector presets** — `:standard` (
|
|
35
|
+
9. **Introspector presets** — `:standard` (13 core) default, `:full` (28) for power users
|
|
36
36
|
10. **MCP auto-discovery** — `.mcp.json` generated by install generator
|
|
37
37
|
11. **Compact by default** — context files ≤150 lines, MCP tools use `detail` parameter (summary/standard/full)
|
|
38
38
|
12. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.windsurf/rules/`, `.github/instructions/`
|
data/README.md
CHANGED
|
@@ -147,8 +147,9 @@ your-rails-app/
|
|
|
147
147
|
│ ├── CLAUDE.md ≤150 lines (compact)
|
|
148
148
|
│ └── .claude/rules/
|
|
149
149
|
│ ├── rails-context.md app overview
|
|
150
|
-
│ ├── rails-schema.md table listing
|
|
150
|
+
│ ├── rails-schema.md table listing + column types
|
|
151
151
|
│ ├── rails-models.md model listing
|
|
152
|
+
│ ├── rails-ui-patterns.md CSS/Tailwind component patterns
|
|
152
153
|
│ └── rails-mcp-tools.md full tool reference
|
|
153
154
|
│
|
|
154
155
|
├── 🟢 Cursor
|
|
@@ -156,6 +157,7 @@ your-rails-app/
|
|
|
156
157
|
│ ├── rails-project.mdc alwaysApply: true
|
|
157
158
|
│ ├── rails-models.mdc globs: app/models/**
|
|
158
159
|
│ ├── rails-controllers.mdc globs: app/controllers/**
|
|
160
|
+
│ ├── rails-ui-patterns.mdc globs: app/views/**
|
|
159
161
|
│ └── rails-mcp-tools.mdc alwaysApply: true
|
|
160
162
|
│
|
|
161
163
|
├── ⚡ OpenCode
|
|
@@ -167,13 +169,16 @@ your-rails-app/
|
|
|
167
169
|
│ ├── .windsurfrules ≤5,800 chars (6K limit)
|
|
168
170
|
│ └── .windsurf/rules/
|
|
169
171
|
│ ├── rails-context.md project overview
|
|
172
|
+
│ ├── rails-ui-patterns.md CSS component patterns
|
|
170
173
|
│ └── rails-mcp-tools.md tool reference
|
|
171
174
|
│
|
|
172
175
|
├── 🟠 GitHub Copilot
|
|
173
176
|
│ ├── .github/copilot-instructions.md ≤500 lines (compact)
|
|
174
177
|
│ └── .github/instructions/
|
|
178
|
+
│ ├── rails-context.instructions.md applyTo: **/*
|
|
175
179
|
│ ├── rails-models.instructions.md applyTo: app/models/**
|
|
176
180
|
│ ├── rails-controllers.instructions.md applyTo: app/controllers/**
|
|
181
|
+
│ ├── rails-ui-patterns.instructions.md applyTo: app/views/**
|
|
177
182
|
│ └── rails-mcp-tools.instructions.md applyTo: **/*
|
|
178
183
|
│
|
|
179
184
|
├── 📋 .ai-context.json full JSON (programmatic)
|
|
@@ -203,7 +208,7 @@ Root files (CLAUDE.md, AGENTS.md, etc.) use **section markers** — your custom
|
|
|
203
208
|
| **DevOps** | Puma, Procfile, Docker, deployment tools, asset pipeline |
|
|
204
209
|
| **Architecture** | Service objects, STI, polymorphism, state machines, multi-tenancy, engines |
|
|
205
210
|
|
|
206
|
-
29 introspectors total. The `:standard` preset runs
|
|
211
|
+
29 introspectors total. The `:standard` preset runs 13 core ones by default; use `:full` for 28 (`database_stats` is opt-in, PostgreSQL only).
|
|
207
212
|
|
|
208
213
|
---
|
|
209
214
|
|
|
@@ -257,7 +262,7 @@ end
|
|
|
257
262
|
```ruby
|
|
258
263
|
# config/initializers/rails_ai_context.rb
|
|
259
264
|
RailsAiContext.configure do |config|
|
|
260
|
-
# Presets: :standard (
|
|
265
|
+
# Presets: :standard (13 introspectors, default) or :full (all 28)
|
|
261
266
|
config.preset = :standard
|
|
262
267
|
|
|
263
268
|
# Cherry-pick on top of a preset
|
|
@@ -291,7 +296,7 @@ end
|
|
|
291
296
|
| Option | Default | Description |
|
|
292
297
|
|--------|---------|-------------|
|
|
293
298
|
| `preset` | `:standard` | Introspector preset (`:standard` or `:full`) |
|
|
294
|
-
| `introspectors` |
|
|
299
|
+
| `introspectors` | 13 core | Array of introspector symbols |
|
|
295
300
|
| `context_mode` | `:compact` | `:compact` (≤150 lines) or `:full` (dump everything) |
|
|
296
301
|
| `claude_max_lines` | `150` | Max lines for CLAUDE.md in compact mode |
|
|
297
302
|
| `max_tool_response_chars` | `120_000` | Safety cap for MCP tool responses |
|
data/docs/GUIDE.md
CHANGED
|
@@ -579,7 +579,7 @@ Both transports are **read-only** — they expose the same 13 tools and never mo
|
|
|
579
579
|
RailsAiContext.configure do |config|
|
|
580
580
|
# --- Introspectors ---
|
|
581
581
|
|
|
582
|
-
# Presets: :standard (
|
|
582
|
+
# Presets: :standard (13 core, default) or :full (all 28)
|
|
583
583
|
config.preset = :standard
|
|
584
584
|
|
|
585
585
|
# Cherry-pick on top of a preset
|
|
@@ -636,7 +636,7 @@ end
|
|
|
636
636
|
| Option | Type | Default | Description |
|
|
637
637
|
|--------|------|---------|-------------|
|
|
638
638
|
| `preset` | Symbol | `:standard` | Introspector preset (`:standard` or `:full`) |
|
|
639
|
-
| `introspectors` | Array |
|
|
639
|
+
| `introspectors` | Array | 13 core symbols | Which introspectors to run |
|
|
640
640
|
| `context_mode` | Symbol | `:compact` | `:compact` or `:full` |
|
|
641
641
|
| `claude_max_lines` | Integer | `150` | Max lines for CLAUDE.md in compact mode |
|
|
642
642
|
| `max_tool_response_chars` | Integer | `120_000` | Safety cap for MCP tool responses |
|
|
@@ -673,7 +673,7 @@ All split rules include an app overview file, so no context is lost when root fi
|
|
|
673
673
|
|
|
674
674
|
## Introspectors — Full List
|
|
675
675
|
|
|
676
|
-
### Standard preset (
|
|
676
|
+
### Standard preset (13 introspectors)
|
|
677
677
|
|
|
678
678
|
These run by default. Fast and cover core Rails structure.
|
|
679
679
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
class Configuration
|
|
5
5
|
PRESETS = {
|
|
6
|
-
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus view_templates design_tokens],
|
|
6
|
+
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus view_templates design_tokens config],
|
|
7
7
|
full: %i[schema models routes jobs gems conventions stimulus controllers views view_templates design_tokens turbo
|
|
8
8
|
i18n config active_storage action_text auth api tests rake_tasks assets
|
|
9
9
|
devops action_mailbox migrations seeds middleware engines multi_database]
|
|
@@ -18,6 +18,9 @@ module RailsAiContext
|
|
|
18
18
|
# Paths to exclude from code search
|
|
19
19
|
attr_accessor :excluded_paths
|
|
20
20
|
|
|
21
|
+
# Sensitive file patterns blocked from search and read tools
|
|
22
|
+
attr_accessor :sensitive_patterns
|
|
23
|
+
|
|
21
24
|
# Whether to auto-mount the MCP HTTP endpoint
|
|
22
25
|
attr_accessor :auto_mount
|
|
23
26
|
|
|
@@ -62,6 +65,10 @@ module RailsAiContext
|
|
|
62
65
|
@server_version = RailsAiContext::VERSION
|
|
63
66
|
@introspectors = PRESETS[:standard].dup
|
|
64
67
|
@excluded_paths = %w[node_modules tmp log vendor .git]
|
|
68
|
+
@sensitive_patterns = %w[
|
|
69
|
+
.env .env.* config/master.key config/credentials.yml.enc
|
|
70
|
+
config/credentials/*.yml.enc *.pem *.key
|
|
71
|
+
]
|
|
65
72
|
@auto_mount = false
|
|
66
73
|
@http_path = "/mcp"
|
|
67
74
|
@http_bind = "127.0.0.1"
|
|
@@ -16,11 +16,13 @@ module RailsAiContext
|
|
|
16
16
|
cache_store: detect_cache_store,
|
|
17
17
|
session_store: detect_session_store,
|
|
18
18
|
timezone: app.config.time_zone.to_s,
|
|
19
|
+
queue_adapter: detect_queue_adapter,
|
|
20
|
+
mailer: detect_mailer_settings,
|
|
19
21
|
middleware_stack: extract_middleware,
|
|
20
22
|
initializers: extract_initializers,
|
|
21
|
-
|
|
23
|
+
credentials_configured: credentials_configured?,
|
|
22
24
|
current_attributes: detect_current_attributes
|
|
23
|
-
}
|
|
25
|
+
}.compact
|
|
24
26
|
rescue => e
|
|
25
27
|
{ error: e.message }
|
|
26
28
|
end
|
|
@@ -46,6 +48,40 @@ module RailsAiContext
|
|
|
46
48
|
app.config.session_store&.name rescue "unknown"
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
def detect_queue_adapter
|
|
52
|
+
adapter = app.config.active_job.queue_adapter
|
|
53
|
+
case adapter
|
|
54
|
+
when Symbol then adapter.to_s
|
|
55
|
+
when Class then adapter.name
|
|
56
|
+
else adapter.to_s
|
|
57
|
+
end
|
|
58
|
+
rescue
|
|
59
|
+
"unknown"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def detect_mailer_settings
|
|
63
|
+
mailer_config = app.config.action_mailer
|
|
64
|
+
settings = {}
|
|
65
|
+
|
|
66
|
+
if mailer_config.respond_to?(:delivery_method) && mailer_config.delivery_method
|
|
67
|
+
settings[:delivery_method] = mailer_config.delivery_method.to_s
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if mailer_config.respond_to?(:default_options) && mailer_config.default_options.is_a?(Hash)
|
|
71
|
+
from = mailer_config.default_options[:from]
|
|
72
|
+
settings[:default_from] = from if from
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if mailer_config.respond_to?(:default_url_options) && mailer_config.default_url_options.is_a?(Hash)
|
|
76
|
+
host = mailer_config.default_url_options[:host]
|
|
77
|
+
settings[:default_url_host] = host if host
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
settings.empty? ? nil : settings
|
|
81
|
+
rescue
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
49
85
|
def extract_middleware
|
|
50
86
|
app.middleware.map { |m| m.name || m.klass.to_s }.uniq
|
|
51
87
|
rescue
|
|
@@ -59,12 +95,13 @@ module RailsAiContext
|
|
|
59
95
|
Dir.glob(File.join(dir, "*.rb")).map { |f| File.basename(f) }.sort
|
|
60
96
|
end
|
|
61
97
|
|
|
62
|
-
|
|
98
|
+
# Returns whether credentials are configured (boolean).
|
|
99
|
+
# Does NOT expose key names — those could reveal integrated services.
|
|
100
|
+
def credentials_configured?
|
|
63
101
|
creds = app.credentials
|
|
64
|
-
|
|
65
|
-
creds.config.keys.map(&:to_s).sort
|
|
102
|
+
creds.respond_to?(:config) && creds.config.keys.any?
|
|
66
103
|
rescue
|
|
67
|
-
|
|
104
|
+
false
|
|
68
105
|
end
|
|
69
106
|
|
|
70
107
|
def detect_current_attributes
|
|
@@ -4,9 +4,18 @@ module RailsAiContext
|
|
|
4
4
|
module Introspectors
|
|
5
5
|
# Discovers controllers and extracts filters, strong params,
|
|
6
6
|
# respond_to formats, concerns, actions, and API detection.
|
|
7
|
+
# Uses source-file parsing (not just Ruby reflection) so that
|
|
8
|
+
# changes made mid-session are always visible.
|
|
7
9
|
class ControllerIntrospector
|
|
8
10
|
attr_reader :app
|
|
9
11
|
|
|
12
|
+
# Framework filters inherited from ActionController::Base — suppress to reduce noise
|
|
13
|
+
FRAMEWORK_FILTERS = %w[
|
|
14
|
+
verify_authenticity_token verify_same_origin_request
|
|
15
|
+
turbo_tracking_request_id handle_unverified_request
|
|
16
|
+
mark_for_same_origin_verification
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
10
19
|
def initialize(app)
|
|
11
20
|
@app = app
|
|
12
21
|
end
|
|
@@ -21,6 +30,12 @@ module RailsAiContext
|
|
|
21
30
|
hash[ctrl.name] = { error: e.message }
|
|
22
31
|
end
|
|
23
32
|
|
|
33
|
+
# Discover controllers from filesystem that may not be loaded as classes
|
|
34
|
+
discover_from_filesystem.each do |name, path|
|
|
35
|
+
next if result.key?(name)
|
|
36
|
+
result[name] = extract_details_from_source(path)
|
|
37
|
+
end
|
|
38
|
+
|
|
24
39
|
{ controllers: result }
|
|
25
40
|
rescue => e
|
|
26
41
|
{ error: e.message }
|
|
@@ -29,7 +44,16 @@ module RailsAiContext
|
|
|
29
44
|
private
|
|
30
45
|
|
|
31
46
|
def eager_load_controllers!
|
|
32
|
-
|
|
47
|
+
return if Rails.application.config.eager_load
|
|
48
|
+
|
|
49
|
+
# Use targeted eager_load_dir to pick up newly created controller files
|
|
50
|
+
controllers_path = File.join(app.root, "app", "controllers")
|
|
51
|
+
if defined?(Zeitwerk) && Dir.exist?(controllers_path) &&
|
|
52
|
+
Rails.autoloaders.respond_to?(:main) && Rails.autoloaders.main.respond_to?(:eager_load_dir)
|
|
53
|
+
Rails.autoloaders.main.eager_load_dir(controllers_path)
|
|
54
|
+
else
|
|
55
|
+
Rails.application.eager_load!
|
|
56
|
+
end
|
|
33
57
|
rescue
|
|
34
58
|
nil
|
|
35
59
|
end
|
|
@@ -46,14 +70,45 @@ module RailsAiContext
|
|
|
46
70
|
end.uniq.sort_by(&:name)
|
|
47
71
|
end
|
|
48
72
|
|
|
73
|
+
# Scan filesystem for controller files not yet loaded as classes
|
|
74
|
+
def discover_from_filesystem
|
|
75
|
+
controllers_dir = File.join(app.root, "app", "controllers")
|
|
76
|
+
return {} unless Dir.exist?(controllers_dir)
|
|
77
|
+
|
|
78
|
+
Dir.glob(File.join(controllers_dir, "**/*_controller.rb")).each_with_object({}) do |path, hash|
|
|
79
|
+
relative = path.sub("#{controllers_dir}/", "")
|
|
80
|
+
class_name = relative.sub(/\.rb\z/, "").split("/").map(&:camelize).join("::")
|
|
81
|
+
next if class_name == "ApplicationController"
|
|
82
|
+
next if class_name.start_with?("Rails::", "ActionMailbox::", "ActiveStorage::")
|
|
83
|
+
hash[class_name] = path
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Extract details purely from source file (for controllers not loaded as classes)
|
|
88
|
+
def extract_details_from_source(path)
|
|
89
|
+
source = File.read(path)
|
|
90
|
+
parent = source.match(/class\s+\S+\s*<\s*(\S+)/)&.send(:[], 1) || "Unknown"
|
|
91
|
+
{
|
|
92
|
+
parent_class: parent,
|
|
93
|
+
api_controller: parent.include?("API"),
|
|
94
|
+
actions: extract_actions_from_source(source),
|
|
95
|
+
filters: extract_filters_from_source(source),
|
|
96
|
+
concerns: extract_concerns_from_source(source),
|
|
97
|
+
strong_params: extract_strong_params(source),
|
|
98
|
+
respond_to_formats: extract_respond_to(source)
|
|
99
|
+
}.compact
|
|
100
|
+
rescue => e
|
|
101
|
+
{ error: e.message }
|
|
102
|
+
end
|
|
103
|
+
|
|
49
104
|
def extract_controller_details(ctrl)
|
|
50
105
|
source = read_source(ctrl)
|
|
51
106
|
|
|
52
107
|
{
|
|
53
108
|
parent_class: ctrl.superclass.name,
|
|
54
109
|
api_controller: api_controller?(ctrl),
|
|
55
|
-
actions: extract_actions(ctrl),
|
|
56
|
-
filters: extract_filters(ctrl),
|
|
110
|
+
actions: extract_actions(ctrl, source),
|
|
111
|
+
filters: extract_filters(ctrl, source),
|
|
57
112
|
concerns: extract_concerns(ctrl),
|
|
58
113
|
strong_params: extract_strong_params(source),
|
|
59
114
|
respond_to_formats: extract_respond_to(source)
|
|
@@ -65,17 +120,51 @@ module RailsAiContext
|
|
|
65
120
|
false
|
|
66
121
|
end
|
|
67
122
|
|
|
68
|
-
|
|
123
|
+
# Prefer source-based parsing for actions — always reflects current file state.
|
|
124
|
+
# Falls back to reflection for controllers without readable source files.
|
|
125
|
+
def extract_actions(ctrl, source = nil)
|
|
126
|
+
if source
|
|
127
|
+
actions = extract_actions_from_source(source)
|
|
128
|
+
return actions if actions.any?
|
|
129
|
+
end
|
|
69
130
|
ctrl.action_methods.to_a.sort
|
|
70
131
|
rescue
|
|
71
132
|
[]
|
|
72
133
|
end
|
|
73
134
|
|
|
74
|
-
def
|
|
135
|
+
def extract_actions_from_source(source)
|
|
136
|
+
in_private = false
|
|
137
|
+
actions = []
|
|
138
|
+
|
|
139
|
+
source.each_line do |line|
|
|
140
|
+
if line.match?(/\A\s*(private|protected)\s*$/)
|
|
141
|
+
in_private = true
|
|
142
|
+
elsif line.match?(/\A\s*public\s*$/)
|
|
143
|
+
in_private = false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
next if in_private
|
|
147
|
+
|
|
148
|
+
if (match = line.match(/\A\s*def\s+(\w+[?!]?)/))
|
|
149
|
+
actions << match[1] unless match[1].start_with?("_")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
actions.sort
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Prefer source-based parsing for filters — always reflects current file state.
|
|
157
|
+
# Falls back to reflection for controllers without readable source files.
|
|
158
|
+
def extract_filters(ctrl, source = nil)
|
|
159
|
+
if source
|
|
160
|
+
filters = extract_filters_from_source(source)
|
|
161
|
+
return filters if filters.any?
|
|
162
|
+
end
|
|
75
163
|
return [] unless ctrl.respond_to?(:_process_action_callbacks)
|
|
76
164
|
|
|
77
165
|
ctrl._process_action_callbacks.filter_map do |cb|
|
|
78
166
|
next if cb.filter.is_a?(Proc) || cb.filter.to_s.start_with?("_")
|
|
167
|
+
next if FRAMEWORK_FILTERS.include?(cb.filter.to_s)
|
|
79
168
|
|
|
80
169
|
filter = { name: cb.filter.to_s, kind: cb.kind.to_s }
|
|
81
170
|
filter[:only] = cb.instance_variable_get(:@if)&.filter_map { |c| extract_action_condition(c) }&.flatten
|
|
@@ -88,6 +177,46 @@ module RailsAiContext
|
|
|
88
177
|
[]
|
|
89
178
|
end
|
|
90
179
|
|
|
180
|
+
def extract_filters_from_source(source)
|
|
181
|
+
filters = []
|
|
182
|
+
source.each_line do |line|
|
|
183
|
+
next unless (match = line.match(
|
|
184
|
+
/\A\s*(before_action|after_action|around_action|prepend_before_action|append_before_action)\s+:(\w+[?!]?)/
|
|
185
|
+
))
|
|
186
|
+
|
|
187
|
+
kind = match[1].sub(/_action\z/, "").sub(/\A(?:prepend|append)_/, "")
|
|
188
|
+
filter = { name: match[2], kind: kind }
|
|
189
|
+
|
|
190
|
+
only = parse_action_constraint(line, "only")
|
|
191
|
+
except = parse_action_constraint(line, "except")
|
|
192
|
+
filter[:only] = only if only&.any?
|
|
193
|
+
filter[:except] = except if except&.any?
|
|
194
|
+
filters << filter
|
|
195
|
+
end
|
|
196
|
+
filters
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def parse_action_constraint(line, key)
|
|
200
|
+
return nil unless line.include?("#{key}:")
|
|
201
|
+
|
|
202
|
+
# %i[...] or %w[...] format
|
|
203
|
+
if (match = line.match(/#{key}:\s*%[iwIW]\[([^\]]+)\]/))
|
|
204
|
+
return match[1].split(/\s+/)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# [...] format with symbols
|
|
208
|
+
if (match = line.match(/#{key}:\s*\[([^\]]+)\]/))
|
|
209
|
+
return match[1].scan(/:(\w+[?!]?)/).flatten
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Single symbol format
|
|
213
|
+
if (match = line.match(/#{key}:\s*:(\w+[?!]?)/))
|
|
214
|
+
return [ match[1] ]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
91
220
|
def extract_action_condition(condition)
|
|
92
221
|
return nil unless condition.is_a?(String) || condition.respond_to?(:to_s)
|
|
93
222
|
match = condition.to_s.match(/action_name\s*==\s*['"](\w+)['"]/)
|
|
@@ -104,6 +233,10 @@ module RailsAiContext
|
|
|
104
233
|
[]
|
|
105
234
|
end
|
|
106
235
|
|
|
236
|
+
def extract_concerns_from_source(source)
|
|
237
|
+
source.scan(/^\s*include\s+(\w+(?:::\w+)*)/).flatten
|
|
238
|
+
end
|
|
239
|
+
|
|
107
240
|
def extract_strong_params(source)
|
|
108
241
|
return [] if source.nil?
|
|
109
242
|
|
|
@@ -29,7 +29,16 @@ module RailsAiContext
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
31
|
def eager_load_models!
|
|
32
|
-
|
|
32
|
+
return if Rails.application.config.eager_load
|
|
33
|
+
|
|
34
|
+
# Use targeted eager_load_dir to pick up newly created model files
|
|
35
|
+
models_path = File.join(app.root, "app", "models")
|
|
36
|
+
if defined?(Zeitwerk) && Dir.exist?(models_path) &&
|
|
37
|
+
Rails.autoloaders.respond_to?(:main) && Rails.autoloaders.main.respond_to?(:eager_load_dir)
|
|
38
|
+
Rails.autoloaders.main.eager_load_dir(models_path)
|
|
39
|
+
else
|
|
40
|
+
Rails.application.eager_load!
|
|
41
|
+
end
|
|
33
42
|
rescue
|
|
34
43
|
# In some environments (CI, Claude Code) eager_load may partially fail
|
|
35
44
|
nil
|
|
@@ -140,11 +149,20 @@ module RailsAiContext
|
|
|
140
149
|
def extract_concerns(model)
|
|
141
150
|
model.ancestors
|
|
142
151
|
.select { |mod| mod.is_a?(Module) && !mod.is_a?(Class) }
|
|
143
|
-
.reject { |mod| mod.name
|
|
152
|
+
.reject { |mod| framework_concern?(mod.name) }
|
|
144
153
|
.map(&:name)
|
|
145
154
|
.compact
|
|
146
155
|
end
|
|
147
156
|
|
|
157
|
+
def framework_concern?(name)
|
|
158
|
+
return true if name.nil?
|
|
159
|
+
return true if name.include?("::Generated")
|
|
160
|
+
return true if name.match?(/\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage|ActionDispatch|ActionController|ActionView|AbstractController)/)
|
|
161
|
+
return true if name.match?(/\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/)
|
|
162
|
+
return true if %w[Kernel JSON PP Marshal MessagePack].include?(name)
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
148
166
|
def extract_public_class_methods(model)
|
|
149
167
|
(model.methods - ActiveRecord::Base.methods - Object.methods)
|
|
150
168
|
.reject { |m| m.to_s.start_with?("_", "autosave") }
|
|
@@ -154,13 +172,36 @@ module RailsAiContext
|
|
|
154
172
|
end
|
|
155
173
|
|
|
156
174
|
def extract_public_instance_methods(model)
|
|
175
|
+
generated = generated_association_methods(model)
|
|
176
|
+
|
|
157
177
|
(model.instance_methods - ActiveRecord::Base.instance_methods - Object.instance_methods)
|
|
158
|
-
.reject { |m|
|
|
178
|
+
.reject { |m|
|
|
179
|
+
ms = m.to_s
|
|
180
|
+
ms.start_with?("_", "autosave", "validate_associated") || generated.include?(ms)
|
|
181
|
+
}
|
|
159
182
|
.sort
|
|
160
183
|
.first(30)
|
|
161
184
|
.map(&:to_s)
|
|
162
185
|
end
|
|
163
186
|
|
|
187
|
+
# Build list of AR-generated association helper method names to exclude
|
|
188
|
+
def generated_association_methods(model)
|
|
189
|
+
methods = []
|
|
190
|
+
model.reflect_on_all_associations.each do |assoc|
|
|
191
|
+
name = assoc.name.to_s
|
|
192
|
+
singular = name.singularize
|
|
193
|
+
methods.concat(%W[
|
|
194
|
+
build_#{name} create_#{name} create_#{name}!
|
|
195
|
+
reload_#{name} reset_#{name}
|
|
196
|
+
#{name}_changed? #{name}_previously_changed?
|
|
197
|
+
#{singular}_ids #{singular}_ids=
|
|
198
|
+
])
|
|
199
|
+
end
|
|
200
|
+
methods
|
|
201
|
+
rescue
|
|
202
|
+
[]
|
|
203
|
+
end
|
|
204
|
+
|
|
164
205
|
def extract_source_macros(model)
|
|
165
206
|
path = model_source_path(model)
|
|
166
207
|
return {} unless path && File.exist?(path)
|
|
@@ -26,6 +26,9 @@ module RailsAiContext
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
28
|
def extract_routes
|
|
29
|
+
# Force Rails to reload routes if routes.rb has changed
|
|
30
|
+
app.routes_reloader&.execute_if_updated rescue nil
|
|
31
|
+
|
|
29
32
|
app.routes.routes.filter_map do |route|
|
|
30
33
|
next if route.respond_to?(:internal?) && route.internal?
|
|
31
34
|
next if route.defaults[:controller].blank?
|