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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -8
  3. data/CLAUDE.md +4 -4
  4. data/CONTRIBUTING.md +1 -1
  5. data/README.md +7 -7
  6. data/SECURITY.md +2 -1
  7. data/docs/GUIDE.md +3 -3
  8. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -2
  9. data/lib/rails_ai_context/configuration.rb +4 -2
  10. data/lib/rails_ai_context/doctor.rb +6 -1
  11. data/lib/rails_ai_context/fingerprinter.rb +24 -0
  12. data/lib/rails_ai_context/introspectors/component_introspector.rb +122 -7
  13. data/lib/rails_ai_context/introspectors/performance_introspector.rb +18 -10
  14. data/lib/rails_ai_context/introspectors/schema_introspector.rb +183 -6
  15. data/lib/rails_ai_context/introspectors/view_introspector.rb +2 -2
  16. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +61 -8
  17. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +10 -19
  18. data/lib/rails_ai_context/serializers/claude_serializer.rb +13 -1
  19. data/lib/rails_ai_context/serializers/context_file_serializer.rb +14 -3
  20. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
  21. data/lib/rails_ai_context/serializers/design_system_helper.rb +8 -1
  22. data/lib/rails_ai_context/serializers/opencode_serializer.rb +0 -1
  23. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +15 -9
  24. data/lib/rails_ai_context/server.rb +8 -1
  25. data/lib/rails_ai_context/tools/base_tool.rb +63 -1
  26. data/lib/rails_ai_context/tools/diagnose.rb +132 -5
  27. data/lib/rails_ai_context/tools/generate_test.rb +58 -6
  28. data/lib/rails_ai_context/tools/get_callbacks.rb +27 -4
  29. data/lib/rails_ai_context/tools/get_component_catalog.rb +11 -2
  30. data/lib/rails_ai_context/tools/get_context.rb +70 -8
  31. data/lib/rails_ai_context/tools/get_conventions.rb +59 -0
  32. data/lib/rails_ai_context/tools/get_design_system.rb +45 -7
  33. data/lib/rails_ai_context/tools/get_edit_context.rb +3 -2
  34. data/lib/rails_ai_context/tools/get_env.rb +51 -24
  35. data/lib/rails_ai_context/tools/get_frontend_stack.rb +100 -9
  36. data/lib/rails_ai_context/tools/get_model_details.rb +19 -0
  37. data/lib/rails_ai_context/tools/get_partial_interface.rb +1 -1
  38. data/lib/rails_ai_context/tools/get_stimulus.rb +13 -7
  39. data/lib/rails_ai_context/tools/get_turbo_map.rb +35 -2
  40. data/lib/rails_ai_context/tools/get_view.rb +65 -9
  41. data/lib/rails_ai_context/tools/migration_advisor.rb +4 -0
  42. data/lib/rails_ai_context/tools/onboard.rb +308 -6
  43. data/lib/rails_ai_context/tools/query.rb +4 -2
  44. data/lib/rails_ai_context/tools/read_logs.rb +4 -1
  45. data/lib/rails_ai_context/tools/review_changes.rb +14 -5
  46. data/lib/rails_ai_context/tools/runtime_info.rb +289 -0
  47. data/lib/rails_ai_context/tools/search_code.rb +23 -4
  48. data/lib/rails_ai_context/tools/security_scan.rb +7 -1
  49. data/lib/rails_ai_context/tools/session_context.rb +132 -0
  50. data/lib/rails_ai_context/version.rb +1 -1
  51. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7cd3960b55660ccf3b1c8da3c6af0aeef18ba57a9a906f02cd136b976bf66976
4
- data.tar.gz: '0185228442a21266ba75f01375d8883dc176b6b140b764e59aafa8044fa7faed'
3
+ metadata.gz: 7ba5dfa48fbbc7efaa498640785dd290523f434ed1f7244849f31046f8359e62
4
+ data.tar.gz: 2dca436c86b470b51b38e745a47c64ee3908b7e342677c842940b9515dcf57f2
5
5
  SHA512:
6
- metadata.gz: 117c13423c12bd5e4a204290716639c5f30fcd960290c5cd3535c5b37911567f7678341bb15f5dcff88e75f6c321a740fe8c4d7adb0a5d5a5936aa2c8b679bc0
7
- data.tar.gz: 101b71249f3e49a40ec0bd8417a71bd76ea7b1907b36d94abf4182ead812fd76f33e9b353c9b14f1e78de680f6a1eeebf0659097cc3ec7afe4f06e7b48bf2017
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) for new developers and AI agents: stack, data model, authentication, key flows, jobs, frontend, testing, getting started
12
- - **`rails_generate_test`** — generates test scaffolding matching project patterns: detects framework (RSpec/Minitest), factories vs fixtures, assertion style, Devise helpers; covers validations, associations, scopes, enums, request specs with routes
13
- - **`rails_diagnose`** — one-call error diagnosis: parses error classifies (nil_reference, record_not_found, validation_failure, routing, strong_params, schema_mismatch) → gathers controller/model/schema context shows recent git changes → pulls error logs → suggests fix
14
- - **`rails_review_changes`** — PR/commit review context: classifies changed files, pulls per-file context (model → schema, controller → routes, migration → affected models), detects warnings (missing indexes, removed validations, no test changes)
15
- - **Improved AI instructions** — workflow sequencing (step-by-step tool call order), `detail` parameter guidance, anti-patterns section, `get_context` promoted as power tool, app-specific footer rules from conventions introspector
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/` — 37 MCP tools using the official mcp SDK
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 37 built-in tools
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 37 MCP tools callable from terminal: `rails ai:tool[schema]`, `rails-ai-context tool schema`. Tool name resolution: `schema` → `get_schema` → `rails_get_schema`.
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 (1052 examples)
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/ # 37 MCP tools with detail levels and pagination
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
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-green)](https://registry.modelcontextprotocol.io)
9
9
  [![Ruby](https://img.shields.io/badge/Ruby-3.2%20%7C%203.3%20%7C%203.4-red)](https://github.com/crisnahine/rails-ai-context)
10
10
  [![Rails](https://img.shields.io/badge/Rails-7.1%20%7C%207.2%20%7C%208.0-red)](https://github.com/crisnahine/rails-ai-context)
11
- [![Tests](https://img.shields.io/badge/Tests-1052%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
11
+ [![Tests](https://img.shields.io/badge/Tests-1170%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
13
 
14
14
  **Works with:** Claude Code &bull; Cursor &bull; GitHub Copilot &bull; OpenCode &bull; 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 1052 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.
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 37 tools that understand your entire Rails app — via MCP server or CLI. Zero config.
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 37 tools. Same output. AI agents run these as shell commands. **Works in any terminal, any AI tool, any workflow.** No MCP client required.
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
- ## 37 Tools
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 │ │ 37 tools │ │ Same 37 tools │
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 37 tools from the CLI |
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 37 MCP tools are **read-only** and never modify your application or database.
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 37 MCP tools can be run directly from the terminal — no MCP server or AI client needed.
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 37 tools are **read-only** and **idempotent** — they never modify your application or database.
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 37 tools and never modify your app.
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 37 tools from CLI"
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 (37 live tools)"
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 view_templates design_tokens config components],
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 { |check| send(check) rescue nil }
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: extract_props(content),
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
- match = content.match(/class\s+(\w+)/)
71
- match[1] if 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?(/def (view_)?template/)
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.match?(/< Phlex/)
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
- model_names.each do |model_name|
234
- content.scan(/#{Regexp.escape(model_name)}\.all\b/).each do
235
- findings << {
236
- controller: relative,
237
- model: model_name,
238
- suggestion: "#{model_name}.all loads all records into memory. Consider pagination or scoping."
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,