rails-ai-context 4.3.0 → 4.3.1
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 +46 -8
- data/CLAUDE.md +4 -4
- data/CONTRIBUTING.md +1 -1
- data/README.md +7 -7
- data/SECURITY.md +2 -1
- data/docs/GUIDE.md +3 -3
- data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
- data/lib/rails_ai_context/configuration.rb +4 -2
- data/lib/rails_ai_context/doctor.rb +6 -1
- data/lib/rails_ai_context/fingerprinter.rb +24 -0
- data/lib/rails_ai_context/introspectors/component_introspector.rb +122 -7
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +18 -10
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +183 -6
- data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +61 -8
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +10 -19
- data/lib/rails_ai_context/serializers/claude_serializer.rb +13 -1
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +14 -3
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/design_system_helper.rb +8 -1
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +0 -1
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +15 -9
- data/lib/rails_ai_context/server.rb +8 -1
- data/lib/rails_ai_context/tools/base_tool.rb +63 -1
- data/lib/rails_ai_context/tools/diagnose.rb +132 -5
- data/lib/rails_ai_context/tools/generate_test.rb +58 -6
- data/lib/rails_ai_context/tools/get_callbacks.rb +27 -4
- data/lib/rails_ai_context/tools/get_component_catalog.rb +11 -2
- data/lib/rails_ai_context/tools/get_context.rb +70 -8
- data/lib/rails_ai_context/tools/get_conventions.rb +59 -0
- data/lib/rails_ai_context/tools/get_design_system.rb +45 -7
- data/lib/rails_ai_context/tools/get_edit_context.rb +3 -2
- data/lib/rails_ai_context/tools/get_env.rb +51 -24
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +100 -9
- data/lib/rails_ai_context/tools/get_model_details.rb +19 -0
- data/lib/rails_ai_context/tools/get_partial_interface.rb +1 -1
- data/lib/rails_ai_context/tools/get_stimulus.rb +13 -7
- data/lib/rails_ai_context/tools/get_turbo_map.rb +35 -2
- data/lib/rails_ai_context/tools/get_view.rb +65 -9
- data/lib/rails_ai_context/tools/migration_advisor.rb +4 -0
- data/lib/rails_ai_context/tools/onboard.rb +308 -6
- data/lib/rails_ai_context/tools/query.rb +4 -2
- data/lib/rails_ai_context/tools/read_logs.rb +4 -1
- data/lib/rails_ai_context/tools/review_changes.rb +14 -5
- data/lib/rails_ai_context/tools/runtime_info.rb +289 -0
- data/lib/rails_ai_context/tools/search_code.rb +23 -4
- data/lib/rails_ai_context/tools/security_scan.rb +7 -1
- data/lib/rails_ai_context/tools/session_context.rb +132 -0
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7ba5dfa48fbbc7efaa498640785dd290523f434ed1f7244849f31046f8359e62
|
|
4
|
+
data.tar.gz: 2dca436c86b470b51b38e745a47c64ee3908b7e342677c842940b9515dcf57f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 64c07284bc9d61136ff8dee427396fd0237fc628e9ca8cd08524022a7d9031521360b49526a93205cb6c3990284e9096aedead5d88d2fc0d75ad2f58475d30c6
|
|
7
|
+
data.tar.gz: d82fbfea6afbe2e7c44f22afa1b8f4201fbb6cf9fda17362675f46000e911bece69a3384133b19bc30d5ffe3c553e4cbb935cbd6031df92f21c6dfc3eb32aa78
|
data/CHANGELOG.md
CHANGED
|
@@ -5,21 +5,59 @@ 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
|
+
## [4.3.1] — 2026-04-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **performance_check false positives** — now parses `t.index` inside `create_table` blocks (was only parsing `add_index` outside blocks, missing inline indexes)
|
|
12
|
+
- **review_changes overflow** — capped at 20 files with 30 diff lines each; remaining files listed without diff to prevent 200K+ char responses
|
|
13
|
+
- **get_context ivar cross-check** — now follows `render :other_template` references (create rendering :new on failure no longer shows false positives)
|
|
14
|
+
- **generate_test setup block** — always generates `setup do` with factory/fixture/inline fallback; minitest tests no longer reference undefined instance variables
|
|
15
|
+
- **session_context auto-tracking** — `text_response()` now auto-records every tool call; `session_context(action:"status")` shows what was queried without manual `mark:` calls
|
|
16
|
+
- **search_code AI file exclusion** — excludes CLAUDE.md, AGENTS.md, .claude/, .cursor/, .cursorrules, .github/copilot-instructions.md, .ai-context.json from results
|
|
17
|
+
- **diagnose output truncation** — per-section size limits (3K chars each) + total output cap (20K) prevent overflow
|
|
18
|
+
- **diagnose NameError classification** — `NameError: uninitialized constant` now correctly classified as `:name_error`, not `:nil_reference`
|
|
19
|
+
- **diagnose specific inference** — identifies nil receivers, missing `authenticate_user!`, and `set_*` before_actions from code context
|
|
20
|
+
- **onboard purpose inference** — quick mode now infers app purpose from models, jobs, services, gems (e.g., "news aggregation app with RSS, YouTube, Reddit ingestion")
|
|
21
|
+
- **onboard adapter resolution** — resolves `static_parse` adapter name from config or gems instead of showing internal implementation detail
|
|
22
|
+
- **security_scan transparency** — "no warnings" response now lists which check categories were run (e.g., "SQL injection, XSS, mass assignment")
|
|
23
|
+
- **read_logs filename filter** — `available_log_files` now rejects filenames with non-standard characters
|
|
24
|
+
- **Phlex view support** — get_view detects Phlex views (.rb), extracts component renders and helper calls
|
|
25
|
+
- **Component introspector Phlex** — discovers Phlex components alongside ViewComponent
|
|
26
|
+
- **Schema introspector array columns** — detects PostgreSQL `array: true` columns from schema.rb
|
|
27
|
+
- **search_code regex injection** — `definition` and `class` match types now escape user input with `Regexp.escape` (previously raw interpolation could crash with metacharacters like `(`, `[`, `{`)
|
|
28
|
+
- **sensitive file bypass on macOS** — all 3 `sensitive_file?` implementations now use `FNM_CASEFOLD` flag; `.ENV`, `Master.Key`, `.PEM` variants no longer bypass the block on case-insensitive filesystems
|
|
29
|
+
- **doctor silent exception swallowing** — `rescue nil` replaced with `rescue StandardError` + stderr logging; broken health checks are now reported instead of silently skipped
|
|
30
|
+
- **context file race condition** — `write_plain` and `write_with_markers` now use atomic write (temp file + rename) to prevent partial writes from concurrent generators
|
|
31
|
+
- **performance_introspector O(n*m) scan** — `detect_model_all_in_controllers` now builds a single combined regex instead of scanning each controller once per model
|
|
32
|
+
- **HTTP transport non-loopback warning** — MCP server now logs a warning when `http_bind` is set to a non-loopback address (no authentication on the HTTP transport)
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **`rails_runtime_info`** — live runtime state: DB connection pool, table sizes (PG/MySQL/SQLite), pending migrations, cache stats (Redis hit rate + memory), Sidekiq queue depth, job adapter detection
|
|
36
|
+
- **`rails_session_context`** — session-aware context tracking with auto-recording; `action:"status"` shows what tools were called, `action:"summary"` for compressed recap, `action:"reset"` to clear
|
|
37
|
+
- **`auto_compress` helper** — BaseTool method that auto-downgrades detail when response approaches 85% of max chars
|
|
38
|
+
- **`not_found_response` dedup** — no longer suggests the exact same string the user typed
|
|
39
|
+
- **get_frontend_stack Hotwire** — reports Stimulus controllers, Turbo config, importmap pins for Hotwire/importmap apps (not just React/Vue)
|
|
40
|
+
- **get_component_catalog guidance** — returns actionable message for partial-based apps: "Use get_partial_interface or get_view"
|
|
41
|
+
- **get_context feature enrichment** — `feature:` mode now also searches controllers and services by name when analyze_feature misses them
|
|
42
|
+
- **Fingerprinter gem development** — includes gem lib/ directory mtime when using path gem (local dev cache invalidation)
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- Tool count: 37 → 39
|
|
46
|
+
- Test count: 1052 → 1170
|
|
47
|
+
- Standard preset now includes turbo, auth, accessibility, performance, i18n (was 14 introspectors, now 19)
|
|
48
|
+
|
|
8
49
|
## [4.3.0] — 2026-04-01
|
|
9
50
|
|
|
10
51
|
### Added
|
|
11
|
-
- **`rails_onboard`** — narrative app walkthrough (quick/standard/full)
|
|
12
|
-
- **`rails_generate_test`** —
|
|
13
|
-
- **`rails_diagnose`** — one-call error diagnosis
|
|
14
|
-
- **`rails_review_changes`** — PR/commit review
|
|
15
|
-
- **Improved AI instructions** — workflow sequencing
|
|
16
|
-
- **Compact tool name list** — root files (CLAUDE.md, AGENTS.md) now include all 37 tool names in a dense format that fits within the 150-line compact mode limit
|
|
52
|
+
- **`rails_onboard`** — narrative app walkthrough (quick/standard/full)
|
|
53
|
+
- **`rails_generate_test`** — test scaffolding matching project patterns
|
|
54
|
+
- **`rails_diagnose`** — one-call error diagnosis with classification + context + git + logs
|
|
55
|
+
- **`rails_review_changes`** — PR/commit review with per-file context + warnings
|
|
56
|
+
- **Improved AI instructions** — workflow sequencing, detail guidance, anti-patterns, get_context as power tool
|
|
17
57
|
|
|
18
58
|
### Changed
|
|
19
59
|
- Tool count: 33 → 37
|
|
20
60
|
- Test count: 1016 → 1052
|
|
21
|
-
- Root file render order: commands and rules before tool guide to prevent truncation
|
|
22
|
-
- Cursor rules description: fixed stale "25 tools" → "37 tools"
|
|
23
61
|
|
|
24
62
|
## [4.2.3] — 2026-04-01
|
|
25
63
|
|
data/CLAUDE.md
CHANGED
|
@@ -9,7 +9,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
9
9
|
- `lib/rails_ai_context/configuration.rb` — User-facing config with presets (:standard, :full)
|
|
10
10
|
- `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
|
|
11
11
|
- `lib/rails_ai_context/introspectors/` — 33 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, view_templates, design_tokens, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database, components, accessibility, performance, frontend_frameworks)
|
|
12
|
-
- `lib/rails_ai_context/tools/` —
|
|
12
|
+
- `lib/rails_ai_context/tools/` — 39 MCP tools using the official mcp SDK
|
|
13
13
|
- `lib/rails_ai_context/cli/` — CLI tool runner (`tool_runner.rb`) — executes MCP tools from rake/Thor
|
|
14
14
|
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection, tool_guide_helper, design_system_helper, stack_overview_helper)
|
|
15
15
|
- `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
|
|
@@ -40,12 +40,12 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
40
40
|
13. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.github/instructions/`
|
|
41
41
|
14. **Section markers** — root file content wrapped in `<!-- BEGIN/END rails-ai-context -->` to preserve user content
|
|
42
42
|
15. **generate_root_files toggle** — when false, skip root files (CLAUDE.md, etc.), only generate split rules
|
|
43
|
-
16. **custom_tools API** — `config.custom_tools` array lets users register additional MCP::Tool subclasses alongside the
|
|
43
|
+
16. **custom_tools API** — `config.custom_tools` array lets users register additional MCP::Tool subclasses alongside the 39 built-in tools
|
|
44
44
|
17. **Design system extraction** — view templates analyzed for canonical examples, color palette, typography, responsive patterns, interactive states, dark mode
|
|
45
45
|
18. **skip_tools API** — `config.skip_tools` array lets users exclude specific built-in tools (e.g. `%w[rails_security_scan]`)
|
|
46
46
|
19. **Security scanning** — optional Brakeman integration via `rails_security_scan` tool (graceful degradation if not installed)
|
|
47
47
|
20. **tool_mode config** — `:mcp` (default, MCP primary + CLI fallback) or `:cli` (CLI only, no MCP server needed). Selected during install.
|
|
48
|
-
21. **CLI tool access** — all
|
|
48
|
+
21. **CLI tool access** — all 39 MCP tools callable from terminal: `rails ai:tool[schema]`, `rails-ai-context tool schema`. Tool name resolution: `schema` → `get_schema` → `rails_get_schema`.
|
|
49
49
|
22. **Shared ToolGuideHelper** — serializers use a shared module for tool reference sections, rendering MCP or CLI syntax based on `tool_mode`
|
|
50
50
|
23. **Component catalog** — ViewComponent/Phlex introspection: props, slots, previews, sidecar assets, usage examples via `rails_get_component_catalog`
|
|
51
51
|
24. **Accessibility scanning** — ARIA attributes, semantic HTML, screen reader text, alt text, landmark roles, accessibility score via AccessibilityIntrospector
|
|
@@ -60,7 +60,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
60
60
|
## Testing
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
bundle exec rspec # Run specs (
|
|
63
|
+
bundle exec rspec # Run specs (1170 examples)
|
|
64
64
|
bundle exec rubocop # Lint
|
|
65
65
|
```
|
|
66
66
|
|
data/CONTRIBUTING.md
CHANGED
|
@@ -20,7 +20,7 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
|
|
|
20
20
|
lib/rails_ai_context/
|
|
21
21
|
├── cli/ # CLI tool runner (tool_runner.rb) — executes MCP tools from rake/Thor
|
|
22
22
|
├── introspectors/ # 33 introspectors (schema, models, routes, etc.)
|
|
23
|
-
├── tools/ #
|
|
23
|
+
├── tools/ # 39 MCP tools with detail levels and pagination
|
|
24
24
|
├── serializers/ # Per-assistant formatters + shared ToolGuideHelper
|
|
25
25
|
├── server.rb # MCP server setup (stdio + HTTP)
|
|
26
26
|
├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
|
data/README.md
CHANGED
|
@@ -8,19 +8,19 @@
|
|
|
8
8
|
[](https://registry.modelcontextprotocol.io)
|
|
9
9
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
10
10
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
11
|
-
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
14
|
**Works with:** Claude Code • Cursor • GitHub Copilot • OpenCode • Any terminal
|
|
15
15
|
|
|
16
|
-
> Built by a Rails developer with 10+ years of production experience. AI assisted — the same way it assists me shipping features at work. I designed the architecture, made every decision, reviewed every line, and wrote
|
|
16
|
+
> Built by a Rails developer with 10+ years of production experience. AI assisted — the same way it assists me shipping features at work. I designed the architecture, made every decision, reviewed every line, and wrote 1170 tests. This gem exists because I understand Rails deeply enough to know exactly what AI agents get wrong and what context they need to get it right.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
gem "rails-ai-context", group: :development
|
|
20
20
|
rails generate rails_ai_context:install
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
That's it. Your AI now has
|
|
23
|
+
That's it. Your AI now has 39 tools that understand your entire Rails app — via MCP server or CLI. Zero config.
|
|
24
24
|
|
|
25
25
|
> **[Full Guide →](docs/GUIDE.md)** — every command, every parameter, every configuration option.
|
|
26
26
|
|
|
@@ -50,7 +50,7 @@ rails 'ai:tool[schema]' table=users
|
|
|
50
50
|
rails 'ai:tool[analyze_feature]' feature=billing
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
Same
|
|
53
|
+
Same 39 tools. Same output. AI agents run these as shell commands. **Works in any terminal, any AI tool, any workflow.** No MCP client required.
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
@@ -174,7 +174,7 @@ Tested on a real Rails 8 app (5 models, 19 controllers, 95 routes):
|
|
|
174
174
|
|
|
175
175
|
---
|
|
176
176
|
|
|
177
|
-
##
|
|
177
|
+
## 39 Tools
|
|
178
178
|
|
|
179
179
|
Every tool is **read-only** and returns structured, token-efficient data.
|
|
180
180
|
|
|
@@ -246,7 +246,7 @@ Every tool is **read-only** and returns structured, token-efficient data.
|
|
|
246
246
|
▼ ▼ ▼
|
|
247
247
|
┌──────────────────┐ ┌────────────┐ ┌────────────────────┐
|
|
248
248
|
│ Static Files │ │ MCP Server │ │ CLI Tools │
|
|
249
|
-
│ CLAUDE.md │ │
|
|
249
|
+
│ CLAUDE.md │ │ 39 tools │ │ Same 39 tools │
|
|
250
250
|
│ .cursor/rules/ │ │ stdio/HTTP │ │ No server needed │
|
|
251
251
|
│ .github/instr... │ │ .mcp.json │ │ rails 'ai:tool[X]' │
|
|
252
252
|
└──────────────────┘ └────────────┘ └────────────────────┘
|
|
@@ -282,7 +282,7 @@ MCP auto-discovery: `.mcp.json` is detected automatically by Claude Code and Cur
|
|
|
282
282
|
| Command | What it does |
|
|
283
283
|
|---------|-------------|
|
|
284
284
|
| `rails ai:context` | Generate context files for your AI tools |
|
|
285
|
-
| `rails 'ai:tool[NAME]'` | Run any of the
|
|
285
|
+
| `rails 'ai:tool[NAME]'` | Run any of the 39 tools from the CLI |
|
|
286
286
|
| `rails ai:tool` | List all available tools with short names |
|
|
287
287
|
| `rails ai:serve` | Start MCP server (stdio) |
|
|
288
288
|
| `rails ai:doctor` | Diagnostics + AI readiness score |
|
data/SECURITY.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| Version | Supported |
|
|
6
6
|
|---------|--------------------|
|
|
7
|
+
| 4.3.x | :white_check_mark: |
|
|
7
8
|
| 4.2.x | :white_check_mark: (4.2.1 includes security hardening) |
|
|
8
9
|
| 4.1.x | :white_check_mark: |
|
|
9
10
|
| 4.0.x | :white_check_mark: |
|
|
@@ -26,7 +27,7 @@ If you discover a security vulnerability in rails-ai-context, please report it r
|
|
|
26
27
|
|
|
27
28
|
## Security Design
|
|
28
29
|
|
|
29
|
-
- All
|
|
30
|
+
- All 39 MCP tools are **read-only** and never modify your application or database.
|
|
30
31
|
- **Sensitive file blocking** — configurable `sensitive_patterns` blocks access to `.env`, `*.key`, `*.pem`, `credentials.yml.enc` across all search and read tools. Patterns are checked in `rails_search_code`, `rails_get_edit_context`, and all new tools.
|
|
31
32
|
- **Path traversal protection** — all file-reading tools validate paths with `File.realpath()` against `Rails.root` to prevent directory escape.
|
|
32
33
|
- **Command injection prevention** — code search uses `Open3.capture2` with array arguments (never shell strings). The `--` flag separator prevents pattern injection.
|
data/docs/GUIDE.md
CHANGED
|
@@ -252,7 +252,7 @@ rails ai:context:claude # Use this instead (no quoting needed)
|
|
|
252
252
|
|
|
253
253
|
## CLI Tools
|
|
254
254
|
|
|
255
|
-
All
|
|
255
|
+
All 39 MCP tools can be run directly from the terminal — no MCP server or AI client needed.
|
|
256
256
|
|
|
257
257
|
### Rake
|
|
258
258
|
|
|
@@ -316,7 +316,7 @@ The `tool_mode` is selected during `rails generate rails_ai_context:install`.
|
|
|
316
316
|
|
|
317
317
|
## MCP Tools — Full Reference
|
|
318
318
|
|
|
319
|
-
All
|
|
319
|
+
All 39 tools are **read-only** and **idempotent** — they never modify your application or database.
|
|
320
320
|
|
|
321
321
|
### rails_get_schema
|
|
322
322
|
|
|
@@ -1117,7 +1117,7 @@ RailsAiContext.configure do |config|
|
|
|
1117
1117
|
end
|
|
1118
1118
|
```
|
|
1119
1119
|
|
|
1120
|
-
Both transports are **read-only** — they expose the same
|
|
1120
|
+
Both transports are **read-only** — they expose the same 39 tools and never modify your app.
|
|
1121
1121
|
|
|
1122
1122
|
---
|
|
1123
1123
|
|
|
@@ -436,9 +436,9 @@ module RailsAiContext
|
|
|
436
436
|
say ""
|
|
437
437
|
say "Commands:", :yellow
|
|
438
438
|
say " rails ai:context # Regenerate context files"
|
|
439
|
-
say " rails 'ai:tool[schema]' # Run any of the
|
|
439
|
+
say " rails 'ai:tool[schema]' # Run any of the 39 tools from CLI"
|
|
440
440
|
if @tool_mode == :mcp
|
|
441
|
-
say " rails ai:serve # Start MCP server (
|
|
441
|
+
say " rails ai:serve # Start MCP server (39 live tools)"
|
|
442
442
|
end
|
|
443
443
|
say " rails ai:doctor # Check AI readiness"
|
|
444
444
|
say " rails ai:inspect # Print introspection summary"
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module RailsAiContext
|
|
4
4
|
class Configuration
|
|
5
5
|
PRESETS = {
|
|
6
|
-
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus
|
|
6
|
+
standard: %i[schema models routes jobs gems conventions controllers tests migrations stimulus
|
|
7
|
+
view_templates design_tokens config components
|
|
8
|
+
turbo auth accessibility performance i18n],
|
|
7
9
|
full: %i[schema models routes jobs gems conventions stimulus controllers views view_templates design_tokens turbo
|
|
8
10
|
i18n config active_storage action_text auth api tests rake_tasks assets
|
|
9
11
|
devops action_mailbox migrations seeds middleware engines multi_database
|
|
@@ -111,7 +113,7 @@ module RailsAiContext
|
|
|
111
113
|
@server_name = "rails-ai-context"
|
|
112
114
|
@server_version = RailsAiContext::VERSION
|
|
113
115
|
@introspectors = PRESETS[:full].dup
|
|
114
|
-
@excluded_paths = %w[node_modules tmp log vendor .git]
|
|
116
|
+
@excluded_paths = %w[node_modules tmp log vendor .git doc docs]
|
|
115
117
|
@sensitive_patterns = %w[
|
|
116
118
|
.env .env.* config/master.key config/credentials.yml.enc
|
|
117
119
|
config/credentials/*.yml.enc *.pem *.key
|
|
@@ -38,7 +38,12 @@ module RailsAiContext
|
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def run
|
|
41
|
-
results = CHECKS.filter_map
|
|
41
|
+
results = CHECKS.filter_map do |check|
|
|
42
|
+
send(check)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
$stderr.puts "[rails-ai-context] Doctor check #{check} failed: #{e.class}: #{e.message}"
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
42
47
|
score = compute_score(results)
|
|
43
48
|
{ checks: results, score: score }
|
|
44
49
|
end
|
|
@@ -36,6 +36,17 @@ module RailsAiContext
|
|
|
36
36
|
root = app.root.to_s
|
|
37
37
|
digest = Digest::SHA256.new
|
|
38
38
|
|
|
39
|
+
# Include the gem's own version so cache invalidates during gem development
|
|
40
|
+
digest.update(RailsAiContext::VERSION)
|
|
41
|
+
|
|
42
|
+
# Include gem lib directory mtime when using a local/path gem (development mode)
|
|
43
|
+
gem_lib = File.expand_path("../../..", __FILE__)
|
|
44
|
+
if gem_lib.start_with?(root) || (defined?(Bundler) && local_gem_path?)
|
|
45
|
+
Dir.glob(File.join(gem_lib, "**/*.rb")).sort.each do |path|
|
|
46
|
+
digest.update(File.mtime(path).to_f.to_s)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
39
50
|
WATCHED_FILES.each do |file|
|
|
40
51
|
path = File.join(root, file)
|
|
41
52
|
digest.update(File.mtime(path).to_f.to_s) if File.exist?(path)
|
|
@@ -53,6 +64,19 @@ module RailsAiContext
|
|
|
53
64
|
digest.hexdigest
|
|
54
65
|
end
|
|
55
66
|
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Detect if this gem is loaded via a local path (path: in Gemfile)
|
|
70
|
+
def local_gem_path?
|
|
71
|
+
spec = Bundler.rubygems.find_name("rails-ai-context").first
|
|
72
|
+
return false unless spec
|
|
73
|
+
spec.source.is_a?(Bundler::Source::Path)
|
|
74
|
+
rescue
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
public
|
|
79
|
+
|
|
56
80
|
def changed?(app, previous)
|
|
57
81
|
compute(app) != previous
|
|
58
82
|
end
|
|
@@ -49,11 +49,15 @@ module RailsAiContext
|
|
|
49
49
|
class_name = extract_class_name(content)
|
|
50
50
|
return nil unless class_name
|
|
51
51
|
|
|
52
|
+
props = extract_props(content)
|
|
53
|
+
enum_values = extract_enum_values(content)
|
|
54
|
+
attach_enum_values_to_props(props, enum_values, content)
|
|
55
|
+
|
|
52
56
|
component = {
|
|
53
57
|
name: class_name,
|
|
54
58
|
file: relative,
|
|
55
59
|
type: detect_component_type(content),
|
|
56
|
-
props:
|
|
60
|
+
props: props,
|
|
57
61
|
slots: extract_slots(content)
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -67,21 +71,60 @@ module RailsAiContext
|
|
|
67
71
|
end
|
|
68
72
|
|
|
69
73
|
def extract_class_name(content)
|
|
70
|
-
|
|
71
|
-
match
|
|
74
|
+
# Extract fully qualified class name (e.g., Components::Articles::Article)
|
|
75
|
+
match = content.match(/class\s+([\w:]+)/)
|
|
76
|
+
return nil unless match
|
|
77
|
+
|
|
78
|
+
full_name = match[1]
|
|
79
|
+
# Return the last meaningful segment for display, but keep namespace context
|
|
80
|
+
# e.g., "Components::Articles::Article" → "Articles::Article"
|
|
81
|
+
# "RubyUI::Button" → "Button"
|
|
82
|
+
# "AlertComponent" → "AlertComponent"
|
|
83
|
+
parts = full_name.split("::")
|
|
84
|
+
if parts.size > 2 && parts.first == "Components"
|
|
85
|
+
parts[1..].join("::")
|
|
86
|
+
elsif parts.size > 1 && %w[Components RubyUI].include?(parts.first)
|
|
87
|
+
parts.last
|
|
88
|
+
else
|
|
89
|
+
full_name
|
|
90
|
+
end
|
|
72
91
|
end
|
|
73
92
|
|
|
74
93
|
def detect_component_type(content)
|
|
75
|
-
if content.match?(/< (ViewComponent::Base|ApplicationComponent)/)
|
|
94
|
+
if content.match?(/< (ViewComponent::Base|ApplicationComponent)\b/)
|
|
76
95
|
:view_component
|
|
77
|
-
elsif content.match?(/< (Phlex::HTML|Phlex::SVG|ApplicationView|ApplicationComponent)/)
|
|
78
|
-
content.match?(
|
|
96
|
+
elsif content.match?(/< (Phlex::HTML|Phlex::SVG|ApplicationView|ApplicationComponent)\b/) ||
|
|
97
|
+
(content.match?(/< \S+/) && inherits_from_phlex_base?(content))
|
|
79
98
|
:phlex
|
|
80
99
|
else
|
|
81
100
|
:unknown
|
|
82
101
|
end
|
|
83
102
|
end
|
|
84
103
|
|
|
104
|
+
def inherits_from_phlex_base?(content)
|
|
105
|
+
parent_match = content.match(/class\s+\S+\s*<\s*(\S+)/)
|
|
106
|
+
return false unless parent_match
|
|
107
|
+
|
|
108
|
+
parent_class = parent_match[1]
|
|
109
|
+
@phlex_bases ||= detect_phlex_bases
|
|
110
|
+
@phlex_bases.include?(parent_class)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def detect_phlex_bases
|
|
114
|
+
bases = Set.new
|
|
115
|
+
return bases unless Dir.exist?(components_dir)
|
|
116
|
+
|
|
117
|
+
Dir.glob(File.join(components_dir, "**/*.rb")).each do |path|
|
|
118
|
+
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
119
|
+
if content.match?(/< (Phlex::HTML|Phlex::SVG)\b/)
|
|
120
|
+
match = content.match(/class\s+(\S+)\s*</)
|
|
121
|
+
bases << match[1] if match
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
bases
|
|
126
|
+
end
|
|
127
|
+
|
|
85
128
|
def extract_props(content)
|
|
86
129
|
# Extract from initialize method parameters
|
|
87
130
|
init_match = content.match(/def initialize\(([^)]*)\)/m)
|
|
@@ -128,7 +171,7 @@ module RailsAiContext
|
|
|
128
171
|
end
|
|
129
172
|
|
|
130
173
|
# Phlex slots: def slot_name(&block)
|
|
131
|
-
if content
|
|
174
|
+
if detect_component_type(content) == :phlex
|
|
132
175
|
content.scan(/def\s+(\w+)\s*\(\s*&\s*\w*\s*\)/).each do |name,|
|
|
133
176
|
next if %w[initialize template view_template before_template after_template].include?(name)
|
|
134
177
|
slots << { name: name, type: :phlex_slot }
|
|
@@ -138,6 +181,78 @@ module RailsAiContext
|
|
|
138
181
|
slots
|
|
139
182
|
end
|
|
140
183
|
|
|
184
|
+
# Extracts enumerable values from constants and case statements.
|
|
185
|
+
# Returns a hash mapping downcased constant/variable names to arrays of symbol values.
|
|
186
|
+
# Detects three patterns:
|
|
187
|
+
# 1. Hash constants: VARIANTS = { primary: "...", secondary: "..." } -> keys
|
|
188
|
+
# 2. Array constants: SIZES = [:sm, :md, :lg] -> elements
|
|
189
|
+
# 3. Case statements: case @variant; when :primary; when :secondary -> when values
|
|
190
|
+
def extract_enum_values(content)
|
|
191
|
+
enums = {}
|
|
192
|
+
|
|
193
|
+
# Pattern 1: Hash constants — NAME = { key: "value", ... }
|
|
194
|
+
content.scan(/([A-Z][A-Z_0-9]*)\s*=\s*\{([^}]*)\}/m) do |name, body|
|
|
195
|
+
keys = body.scan(/(\w+):/).map(&:first)
|
|
196
|
+
enums[name.downcase] = keys if keys.any?
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Pattern 2: Array constants — NAME = [:sym, :sym, ...]
|
|
200
|
+
content.scan(/([A-Z][A-Z_0-9]*)\s*=\s*\[([^\]]*)\]/) do |name, body|
|
|
201
|
+
values = body.scan(/:(\w+)/).map(&:first)
|
|
202
|
+
enums[name.downcase] = values if values.any?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Pattern 3: Case statements — case @ivar; when :val1 ... when :val2
|
|
206
|
+
# Use a non-greedy match that stops at the next `end`, `case`, or `def` keyword
|
|
207
|
+
content.scan(/case\s+@(\w+)\s*\n(.*?)(?=\n\s*(?:end|case|def)\b)/m) do |ivar, block|
|
|
208
|
+
values = block.scan(/when\s+:(\w+)/).map(&:first)
|
|
209
|
+
next if values.empty?
|
|
210
|
+
# Merge with existing values for same ivar (handles multiple case blocks)
|
|
211
|
+
existing = enums[ivar] || []
|
|
212
|
+
enums[ivar] = (existing + values).uniq
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
enums
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Matches extracted enum values to props by:
|
|
219
|
+
# 1. Direct ivar match: prop "variant" matches case @variant values
|
|
220
|
+
# 2. Constant name match: prop "size" matches SIZES constant, prop "variant" matches VARIANTS constant
|
|
221
|
+
# 3. Constant usage in initialize: @size referenced as SIZES[@size] matches prop "size"
|
|
222
|
+
def attach_enum_values_to_props(props, enum_values, content)
|
|
223
|
+
props.each do |prop|
|
|
224
|
+
name = prop[:name]
|
|
225
|
+
values = nil
|
|
226
|
+
|
|
227
|
+
# Direct match: prop name matches case @ivar
|
|
228
|
+
values = enum_values[name] if enum_values.key?(name)
|
|
229
|
+
|
|
230
|
+
# Constant name match: prop "size" -> SIZES, prop "variant" -> VARIANTS/COLORS
|
|
231
|
+
unless values
|
|
232
|
+
# Try pluralized forms and common naming patterns
|
|
233
|
+
candidates = [ name.upcase + "S", name.upcase + "ES", name.upcase ]
|
|
234
|
+
candidates.each do |candidate|
|
|
235
|
+
if enum_values.key?(candidate.downcase)
|
|
236
|
+
values = enum_values[candidate.downcase]
|
|
237
|
+
break
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Constant usage match: find CONST[@ivar] patterns in the file
|
|
243
|
+
unless values
|
|
244
|
+
content.scan(/([A-Z][A-Z_0-9]*)\[@#{name}\]/) do |const_name,|
|
|
245
|
+
if enum_values.key?(const_name.downcase)
|
|
246
|
+
values = enum_values[const_name.downcase]
|
|
247
|
+
break
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
prop[:values] = values if values&.any?
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
141
256
|
def find_preview(component_path, class_name)
|
|
142
257
|
# Check common preview locations
|
|
143
258
|
preview_name = class_name.sub(/Component\z/, "").underscore
|
|
@@ -54,6 +54,10 @@ module RailsAiContext
|
|
|
54
54
|
elsif (idx = line.match(/add_index\s+"#{Regexp.escape(current_table)}",\s+(?:"(\w+)"|\[([^\]]+)\])/))
|
|
55
55
|
col_name = idx[1] || idx[2]&.gsub(/["'\s]/, "")
|
|
56
56
|
tables[current_table][:indexes] << col_name
|
|
57
|
+
elsif (tidx = line.match(/t\.index\s+\[([^\]]+)\]/))
|
|
58
|
+
# t.index ["col_name"] inside create_table block
|
|
59
|
+
cols = tidx[1].gsub(/["'\s]/, "").split(",")
|
|
60
|
+
cols.each { |c| tables[current_table][:indexes] << c }
|
|
57
61
|
end
|
|
58
62
|
end
|
|
59
63
|
|
|
@@ -226,20 +230,23 @@ module RailsAiContext
|
|
|
226
230
|
|
|
227
231
|
return findings if model_names.empty?
|
|
228
232
|
|
|
233
|
+
# Build a single regex matching any model's .all call to avoid O(n*m) scanning
|
|
234
|
+
escaped_names = model_names.map { |n| Regexp.escape(n) }
|
|
235
|
+
combined_pattern = /(#{escaped_names.join("|")})\.all\b/
|
|
236
|
+
|
|
229
237
|
Dir.glob(File.join(controllers_dir, "**/*.rb")).each do |path|
|
|
230
238
|
content = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
231
239
|
relative = path.sub("#{root}/", "")
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
end
|
|
241
|
+
content.scan(combined_pattern).each do |match|
|
|
242
|
+
model_name = match[0]
|
|
243
|
+
findings << {
|
|
244
|
+
controller: relative,
|
|
245
|
+
model: model_name,
|
|
246
|
+
suggestion: "#{model_name}.all loads all records into memory. Consider pagination or scoping."
|
|
247
|
+
}
|
|
241
248
|
end
|
|
242
|
-
rescue
|
|
249
|
+
rescue StandardError
|
|
243
250
|
next
|
|
244
251
|
end
|
|
245
252
|
|
|
@@ -277,7 +284,8 @@ module RailsAiContext
|
|
|
277
284
|
total_issues = result[:n_plus_one_risks].size +
|
|
278
285
|
result[:missing_counter_cache].size +
|
|
279
286
|
result[:missing_fk_indexes].size +
|
|
280
|
-
result[:model_all_in_controllers].size
|
|
287
|
+
result[:model_all_in_controllers].size +
|
|
288
|
+
result[:eager_load_candidates].size
|
|
281
289
|
|
|
282
290
|
{
|
|
283
291
|
total_issues: total_issues,
|