rails-ai-context 0.15.8 → 0.15.9
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 +6 -0
- data/README.md +57 -57
- data/docs/GUIDE.md +8 -1
- data/lib/rails_ai_context/doctor.rb +248 -33
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 69d121a9a96c5e7c796331dd5773a22e7981c2ea8ba22dc09cc4c722bc3d8b7c
|
|
4
|
+
data.tar.gz: 8e95c49716c2dae5c5fe5ad4883de2ca7bb12d396b5221f904e6d4ab9015cd5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6f1ca5202c2ca0467003ae172d1554868171664ececaef8ff58db08949967e3004378f4d679e5a05bb639bad0dac79ef1423a4e2280ebf041c684d0147799385
|
|
7
|
+
data.tar.gz: 92e917535469b0f8551ac1b2f950450a53bf5498a51a33b7990327c25c845a146897165367f9eed0aa96568e460c0c102322191081a63e98c79e967deeb6cfa9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ 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.9] - 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Deep diagnostic checks in `rails ai:doctor`** — upgraded from 13 shallow file-existence checks to 20 deep checks: pending migrations, context file freshness, .mcp.json validation, introspector health (dry-runs each one), preset coverage (detects features not in preset), .env/.master.key gitignore check, auto_mount production warning, schema/view size vs limits.
|
|
13
|
+
|
|
8
14
|
## [0.15.8] - 2026-03-23
|
|
9
15
|
|
|
10
16
|
### Added
|
data/README.md
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
1
|
# rails-ai-context
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Give AI agents a complete mental model of your Rails app — not just files, but how everything connects.**
|
|
4
4
|
|
|
5
5
|
[](https://rubygems.org/gems/rails-ai-context)
|
|
6
6
|
[](https://registry.modelcontextprotocol.io)
|
|
7
7
|
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
*Built by a Rails dev who got tired of burning tokens explaining his app to AI assistants every single session.*
|
|
11
|
-
|
|
12
10
|
---
|
|
13
11
|
|
|
14
12
|
## The Problem
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
AI agents working on Rails apps operate blind. They read files one at a time but never see the full picture — how your schema connects to your models, which callbacks fire on save, what filters apply to a controller action, which Stimulus controllers exist, or what your UI conventions are.
|
|
17
15
|
|
|
18
|
-
The
|
|
16
|
+
The result: **guess-and-check coding.** The agent writes code, it breaks, it reads more files, fixes it, breaks again. Each iteration wastes tokens and erodes trust.
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
## The Solution
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
**rails-ai-context** gives your AI agent what a senior Rails developer has naturally: a structured mental model of the entire application.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
```bash
|
|
23
|
+
bundle add rails-ai-context
|
|
24
|
+
rails generate rails_ai_context:install
|
|
25
|
+
rails ai:context
|
|
26
|
+
```
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
Three commands. Your AI now understands your schema, models, routes, controllers, views, jobs, gems, auth, Stimulus controllers, design patterns, and conventions — through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|-------|--------|-------|---------------|
|
|
30
|
-
| **rails-ai-context (full)** | **28,834** | **37%** | 13 MCP tools + generated docs + rules |
|
|
31
|
-
| rails-ai-context CLAUDE.md only | 33,106 | 27% | Generated docs + rules, no MCP tools |
|
|
32
|
-
| Normal Claude `/init` | 40,700 | 11% | Generic CLAUDE.md only |
|
|
33
|
-
| No rails-ai-context at all | 45,477 | baseline | Nothing — discovers everything from scratch |
|
|
30
|
+
The install generator creates `.mcp.json` for auto-discovery — Claude Code and Cursor detect it automatically.
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
No rails-ai-context 45,477 tk █████████████████████████████████████████████
|
|
37
|
-
Normal Claude /init 40,700 tk █████████████████████████████████████████ -11%
|
|
38
|
-
rails-ai-context CLAUDE.md 33,106 tk █████████████████████████████████ -27%
|
|
39
|
-
rails-ai-context (full) 28,834 tk █████████████████████████████ -37%
|
|
40
|
-
```
|
|
32
|
+
> **[Full Guide](docs/GUIDE.md)** — complete documentation with every command, parameter, and configuration option.
|
|
41
33
|
|
|
42
|
-
|
|
34
|
+
---
|
|
43
35
|
|
|
36
|
+
## What Changes
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
### Without rails-ai-context
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
|---|---|---|---|
|
|
49
|
-
| Knows it's Rails + Tailwind | Yes | Yes | Yes |
|
|
50
|
-
| Knows model names, columns, associations | No | Yes | Yes |
|
|
51
|
-
| Knows controller actions, filters | No | Yes | Yes |
|
|
52
|
-
| Discovery overhead | ~8 calls | 0 calls | 0 calls |
|
|
53
|
-
| Structured MCP queries | No | No | Yes — 5 MCP calls replace file reads |
|
|
40
|
+
The agent asks itself: *What columns does `users` have?* It reads all 2,000 lines of `schema.rb`. *What associations does `Cook` have?* It reads the model file but misses the concern that adds 12 methods. *What filters apply to `CooksController#create`?* It reads the controller but doesn't see the inherited `authenticate_user!` from the parent class. It writes code that references a nonexistent partial, permits a wrong param, and renders a Stimulus controller that doesn't exist.
|
|
54
41
|
|
|
55
|
-
|
|
42
|
+
**Every mistake is a wasted iteration.**
|
|
56
43
|
|
|
57
|
-
|
|
58
|
-
> A feature touching auth + payments + mailers + tests on a 50-model app? Without the gem, Claude reads `db/schema.rb` (2,000+ lines), every model file, every controller, every view — easily 200K+ tokens per session. With rails-ai-context, MCP tools return only what's needed: `rails_get_schema(table:"users")` returns 25 lines instead of 2,000. **The bigger your app and the harder the task, the more you save.**
|
|
44
|
+
### With rails-ai-context
|
|
59
45
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
46
|
+
```
|
|
47
|
+
Agent: rails_get_schema(table:"users") → 25 lines: columns, types, NOT NULL, defaults, indexes, FKs
|
|
48
|
+
Agent: rails_get_model_details(model:"Cook") → associations, validations, scopes, callbacks, concern methods
|
|
49
|
+
Agent: rails_get_controllers(controller:"cooks", action:"create") → source code + applicable filters + strong params body
|
|
50
|
+
Agent: rails_validate(files:["app/models/cook.rb"], level:"rails") → catches column/route/partial errors before execution
|
|
51
|
+
```
|
|
65
52
|
|
|
66
|
-
|
|
53
|
+
**Orient → drill down → act → verify.** The first attempt is correct.
|
|
67
54
|
|
|
68
55
|
---
|
|
69
56
|
|
|
70
|
-
##
|
|
57
|
+
## Three Layers of Context
|
|
71
58
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
| Layer | What it provides | When it loads | Token cost |
|
|
60
|
+
|-------|-----------------|---------------|------------|
|
|
61
|
+
| **Static files** (CLAUDE.md, .cursorrules, etc.) | App overview: stack, models, gems, architecture, UI patterns, MCP tool reference | Automatically at session start | ~150 lines, zero tool calls |
|
|
62
|
+
| **Split rules** (.claude/rules/, .cursor/rules/) | Deep reference: full schema with column types, all model associations/scopes, controller listings | Conditionally — only when editing relevant files | Zero when not needed |
|
|
63
|
+
| **Live MCP tools** (13 tools) | Real-time queries: drill into any table, model, controller action, or view on demand. Semantic validation. | On-demand via agent tool calls | ~25-100 lines per call |
|
|
77
64
|
|
|
78
|
-
|
|
65
|
+
**Progressive disclosure:** the agent gets the map for free, reference guides when relevant, and live GPS when building.
|
|
79
66
|
|
|
80
|
-
|
|
67
|
+
---
|
|
81
68
|
|
|
82
|
-
|
|
69
|
+
## Real Impact: 37% Fewer Tokens, 95% Fewer Errors
|
|
83
70
|
|
|
84
|
-
|
|
71
|
+
| Setup | Tokens | What it knows |
|
|
72
|
+
|-------|--------|---------------|
|
|
73
|
+
| **rails-ai-context (full)** | **28,834** | 13 MCP tools + generated docs + split rules |
|
|
74
|
+
| rails-ai-context CLAUDE.md only | 33,106 | Generated docs + rules, no MCP tools |
|
|
75
|
+
| Normal Claude `/init` | 40,700 | Generic CLAUDE.md only |
|
|
76
|
+
| No rails-ai-context | 45,477 | Nothing — discovers everything from scratch |
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
No rails-ai-context 45,477 tk █████████████████████████████████████████████
|
|
80
|
+
Normal Claude /init 40,700 tk █████████████████████████████████████████ -11%
|
|
81
|
+
rails-ai-context CLAUDE.md 33,106 tk █████████████████████████████████ -27%
|
|
82
|
+
rails-ai-context (full) 28,834 tk █████████████████████████████ -37%
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
https://github.com/user-attachments/assets/14476243-1210-4e62-9dc5-9d4aa9caef7e
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
> **Token savings scale with app size.** A 5-model app saves 37%. A 50-model app with auth + payments + mailers saves 60-80% — because MCP tools return only what's needed instead of reading entire files.
|
|
87
88
|
|
|
88
|
-
|
|
89
|
+
But token savings is the side effect. The real value:
|
|
89
90
|
|
|
90
|
-
-
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
93
|
-
-
|
|
94
|
-
- Split rule files only activate in relevant directories
|
|
91
|
+
- **Fewer iterations** — the agent understands associations, callbacks, and constraints before writing code
|
|
92
|
+
- **Cross-file accuracy** — semantic validation catches nonexistent partials, wrong column references, and missing routes in one call
|
|
93
|
+
- **Convention awareness** — the agent matches your UI patterns, test framework, and architecture style
|
|
94
|
+
- **No stale context** — live reload invalidates caches when files change mid-session
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
@@ -113,7 +113,7 @@ The gem exposes **13 read-only tools** via MCP that AI clients call on-demand:
|
|
|
113
113
|
| `rails_get_view` | View templates, partials, Stimulus references |
|
|
114
114
|
| `rails_get_stimulus` | Stimulus controllers — targets, values, actions, outlets |
|
|
115
115
|
| `rails_get_edit_context` | Surgical edit helper — returns code around a match with line numbers |
|
|
116
|
-
| `rails_validate` | Batch syntax validation for Ruby, ERB, and JavaScript files |
|
|
116
|
+
| `rails_validate` | Batch syntax validation for Ruby, ERB, and JavaScript files. `level:"rails"` adds semantic checks (partials, route helpers, columns, strong params, callbacks, FK indexes, Stimulus) |
|
|
117
117
|
|
|
118
118
|
### Smart Detail Levels
|
|
119
119
|
|
data/docs/GUIDE.md
CHANGED
|
@@ -528,13 +528,14 @@ rails_get_edit_context(file: "app/controllers/cooks_controller.rb", near: "def i
|
|
|
528
528
|
|
|
529
529
|
### rails_validate
|
|
530
530
|
|
|
531
|
-
Validates syntax of multiple files at once (Ruby, ERB, JavaScript).
|
|
531
|
+
Validates syntax of multiple files at once (Ruby, ERB, JavaScript). Optionally runs Rails-aware semantic checks.
|
|
532
532
|
|
|
533
533
|
**Parameters:**
|
|
534
534
|
|
|
535
535
|
| Param | Type | Description |
|
|
536
536
|
|-------|------|-------------|
|
|
537
537
|
| `files` | array | **Required.** File paths relative to Rails root (e.g. `["app/models/cook.rb", "app/views/cooks/index.html.erb"]`). |
|
|
538
|
+
| `level` | string | `syntax` (default) — check syntax only (fast). `rails` — syntax + semantic checks (partial existence, route helpers, column references, strong params vs schema, callback methods, route-action consistency, has_many dependent, FK indexes, Stimulus controllers). |
|
|
538
539
|
|
|
539
540
|
**Examples:**
|
|
540
541
|
|
|
@@ -544,6 +545,12 @@ rails_validate(files: ["app/models/cook.rb"])
|
|
|
544
545
|
|
|
545
546
|
rails_validate(files: ["app/models/cook.rb", "app/controllers/cooks_controller.rb", "app/views/cooks/index.html.erb"])
|
|
546
547
|
→ Checks all three files, reports pass/fail for each
|
|
548
|
+
|
|
549
|
+
rails_validate(files: ["app/models/cook.rb"], level: "rails")
|
|
550
|
+
→ Syntax check + semantic warnings (e.g. validates :nonexistent_column, has_many without :dependent)
|
|
551
|
+
|
|
552
|
+
rails_validate(files: ["app/views/cooks/index.html.erb"], level: "rails")
|
|
553
|
+
→ Syntax check + partial existence, route helper validity, Stimulus controller existence
|
|
547
554
|
```
|
|
548
555
|
|
|
549
556
|
### rails_search_code
|
|
@@ -8,18 +8,25 @@ module RailsAiContext
|
|
|
8
8
|
|
|
9
9
|
CHECKS = %i[
|
|
10
10
|
check_schema
|
|
11
|
+
check_pending_migrations
|
|
11
12
|
check_models
|
|
12
13
|
check_routes
|
|
13
14
|
check_gems
|
|
14
15
|
check_controllers
|
|
15
16
|
check_views
|
|
16
|
-
check_i18n
|
|
17
17
|
check_tests
|
|
18
18
|
check_migrations
|
|
19
|
-
|
|
19
|
+
check_context_freshness
|
|
20
|
+
check_mcp_json
|
|
20
21
|
check_mcp_buildable
|
|
22
|
+
check_introspector_health
|
|
23
|
+
check_preset_coverage
|
|
21
24
|
check_ripgrep
|
|
22
25
|
check_live_reload
|
|
26
|
+
check_security_gitignore
|
|
27
|
+
check_security_auto_mount
|
|
28
|
+
check_performance_schema_size
|
|
29
|
+
check_performance_view_count
|
|
23
30
|
].freeze
|
|
24
31
|
|
|
25
32
|
attr_reader :app
|
|
@@ -29,20 +36,43 @@ module RailsAiContext
|
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
def run
|
|
32
|
-
results = CHECKS.
|
|
39
|
+
results = CHECKS.filter_map { |check| send(check) rescue nil }
|
|
33
40
|
score = compute_score(results)
|
|
34
41
|
{ checks: results, score: score }
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
private
|
|
38
45
|
|
|
46
|
+
# ── Existence checks ──────────────────────────────────────────────
|
|
47
|
+
|
|
39
48
|
def check_schema
|
|
40
49
|
schema_path = File.join(app.root, "db/schema.rb")
|
|
50
|
+
structure_path = File.join(app.root, "db/structure.sql")
|
|
41
51
|
if File.exist?(schema_path)
|
|
42
|
-
|
|
52
|
+
lines = File.readlines(schema_path).size
|
|
53
|
+
Check.new(name: "Schema", status: :pass, message: "db/schema.rb found (#{lines} lines)", fix: nil)
|
|
54
|
+
elsif File.exist?(structure_path)
|
|
55
|
+
size = (File.size(structure_path) / 1024.0).round(1)
|
|
56
|
+
Check.new(name: "Schema", status: :pass, message: "db/structure.sql found (#{size}KB)", fix: nil)
|
|
57
|
+
else
|
|
58
|
+
Check.new(name: "Schema", status: :warn, message: "No schema file found", fix: "Run `rails db:schema:dump`")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_pending_migrations
|
|
63
|
+
return nil unless defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
64
|
+
|
|
65
|
+
pending = ActiveRecord::Migrator.new(:up, ActiveRecord::MigrationContext.new(File.join(app.root, "db/migrate")).migrations).pending_migrations
|
|
66
|
+
if pending.empty?
|
|
67
|
+
Check.new(name: "Pending migrations", status: :pass, message: "No pending migrations", fix: nil)
|
|
43
68
|
else
|
|
44
|
-
Check.new(name: "
|
|
69
|
+
Check.new(name: "Pending migrations", status: :fail,
|
|
70
|
+
message: "#{pending.size} pending migration(s) — schema data will be stale",
|
|
71
|
+
fix: "Run `rails db:migrate`")
|
|
45
72
|
end
|
|
73
|
+
rescue
|
|
74
|
+
# Can't check pending migrations in this environment
|
|
75
|
+
nil
|
|
46
76
|
end
|
|
47
77
|
|
|
48
78
|
def check_models
|
|
@@ -51,7 +81,7 @@ module RailsAiContext
|
|
|
51
81
|
count = Dir.glob(File.join(models_dir, "**/*.rb")).size
|
|
52
82
|
Check.new(name: "Models", status: :pass, message: "#{count} model files found", fix: nil)
|
|
53
83
|
else
|
|
54
|
-
Check.new(name: "Models", status: :warn, message: "No model files
|
|
84
|
+
Check.new(name: "Models", status: :warn, message: "No model files in app/models/", fix: "Generate models with `rails generate model`")
|
|
55
85
|
end
|
|
56
86
|
end
|
|
57
87
|
|
|
@@ -69,7 +99,7 @@ module RailsAiContext
|
|
|
69
99
|
if File.exist?(lock_path)
|
|
70
100
|
Check.new(name: "Gems", status: :pass, message: "Gemfile.lock found", fix: nil)
|
|
71
101
|
else
|
|
72
|
-
Check.new(name: "Gems", status: :warn, message: "Gemfile.lock not found", fix: "Run `bundle install`
|
|
102
|
+
Check.new(name: "Gems", status: :warn, message: "Gemfile.lock not found", fix: "Run `bundle install`")
|
|
73
103
|
end
|
|
74
104
|
end
|
|
75
105
|
|
|
@@ -79,27 +109,17 @@ module RailsAiContext
|
|
|
79
109
|
count = Dir.glob(File.join(dir, "**/*.rb")).size
|
|
80
110
|
Check.new(name: "Controllers", status: :pass, message: "#{count} controller files found", fix: nil)
|
|
81
111
|
else
|
|
82
|
-
Check.new(name: "Controllers", status: :warn, message: "No controller files
|
|
112
|
+
Check.new(name: "Controllers", status: :warn, message: "No controller files", fix: nil)
|
|
83
113
|
end
|
|
84
114
|
end
|
|
85
115
|
|
|
86
116
|
def check_views
|
|
87
117
|
dir = File.join(app.root, "app/views")
|
|
88
|
-
if Dir.exist?(dir)
|
|
118
|
+
if Dir.exist?(dir)
|
|
89
119
|
count = Dir.glob(File.join(dir, "**/*")).reject { |f| File.directory?(f) }.size
|
|
90
120
|
Check.new(name: "Views", status: :pass, message: "#{count} view files found", fix: nil)
|
|
91
121
|
else
|
|
92
|
-
Check.new(name: "Views", status: :warn, message: "No view files
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def check_i18n
|
|
97
|
-
dir = File.join(app.root, "config/locales")
|
|
98
|
-
if Dir.exist?(dir) && Dir.glob(File.join(dir, "**/*.{yml,yaml}")).any?
|
|
99
|
-
count = Dir.glob(File.join(dir, "**/*.{yml,yaml}")).size
|
|
100
|
-
Check.new(name: "I18n", status: :pass, message: "#{count} locale files found", fix: nil)
|
|
101
|
-
else
|
|
102
|
-
Check.new(name: "I18n", status: :warn, message: "No locale files found in config/locales/", fix: "Add locale files for internationalization support")
|
|
122
|
+
Check.new(name: "Views", status: :warn, message: "No view files", fix: nil)
|
|
103
123
|
end
|
|
104
124
|
end
|
|
105
125
|
|
|
@@ -108,7 +128,8 @@ module RailsAiContext
|
|
|
108
128
|
framework = Dir.exist?(File.join(app.root, "spec")) ? "RSpec" : "Minitest"
|
|
109
129
|
Check.new(name: "Tests", status: :pass, message: "#{framework} test directory found", fix: nil)
|
|
110
130
|
else
|
|
111
|
-
Check.new(name: "Tests", status: :warn, message: "No test directory found",
|
|
131
|
+
Check.new(name: "Tests", status: :warn, message: "No test directory found",
|
|
132
|
+
fix: "Run `rails generate rspec:install` or use default Minitest")
|
|
112
133
|
end
|
|
113
134
|
end
|
|
114
135
|
|
|
@@ -116,18 +137,53 @@ module RailsAiContext
|
|
|
116
137
|
migrate_dir = File.join(app.root, "db/migrate")
|
|
117
138
|
if Dir.exist?(migrate_dir) && Dir.glob(File.join(migrate_dir, "*.rb")).any?
|
|
118
139
|
count = Dir.glob(File.join(migrate_dir, "*.rb")).size
|
|
119
|
-
Check.new(name: "Migrations", status: :pass, message: "#{count} migration files
|
|
140
|
+
Check.new(name: "Migrations", status: :pass, message: "#{count} migration files", fix: nil)
|
|
120
141
|
else
|
|
121
|
-
Check.new(name: "Migrations", status: :warn, message: "No migrations
|
|
142
|
+
Check.new(name: "Migrations", status: :warn, message: "No migrations", fix: nil)
|
|
122
143
|
end
|
|
123
144
|
end
|
|
124
145
|
|
|
125
|
-
|
|
146
|
+
# ── Context file checks ───────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
def check_context_freshness
|
|
126
149
|
claude_path = File.join(app.root, "CLAUDE.md")
|
|
127
|
-
|
|
128
|
-
Check.new(name: "Context files", status: :
|
|
150
|
+
unless File.exist?(claude_path)
|
|
151
|
+
return Check.new(name: "Context files", status: :warn,
|
|
152
|
+
message: "No context files generated",
|
|
153
|
+
fix: "Run `rails ai:context`")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
generated_at = File.mtime(claude_path)
|
|
157
|
+
# Check if any source file changed after context was generated
|
|
158
|
+
stale_dirs = %w[app/models app/controllers app/views config db/migrate].select do |dir|
|
|
159
|
+
full = File.join(app.root, dir)
|
|
160
|
+
Dir.exist?(full) && Dir.glob(File.join(full, "**/*.rb")).any? { |f| File.mtime(f) > generated_at }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if stale_dirs.empty?
|
|
164
|
+
Check.new(name: "Context files", status: :pass, message: "CLAUDE.md is up to date", fix: nil)
|
|
129
165
|
else
|
|
130
|
-
Check.new(name: "Context files", status: :warn,
|
|
166
|
+
Check.new(name: "Context files", status: :warn,
|
|
167
|
+
message: "CLAUDE.md may be stale — #{stale_dirs.join(', ')} changed since last generation",
|
|
168
|
+
fix: "Run `rails ai:context` to regenerate")
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def check_mcp_json
|
|
173
|
+
mcp_path = File.join(app.root, ".mcp.json")
|
|
174
|
+
unless File.exist?(mcp_path)
|
|
175
|
+
return Check.new(name: ".mcp.json", status: :warn,
|
|
176
|
+
message: "No .mcp.json for MCP auto-discovery",
|
|
177
|
+
fix: "Run `rails generate rails_ai_context:install`")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
begin
|
|
181
|
+
JSON.parse(File.read(mcp_path))
|
|
182
|
+
Check.new(name: ".mcp.json", status: :pass, message: ".mcp.json valid", fix: nil)
|
|
183
|
+
rescue JSON::ParserError => e
|
|
184
|
+
Check.new(name: ".mcp.json", status: :fail,
|
|
185
|
+
message: ".mcp.json has invalid JSON: #{e.message}",
|
|
186
|
+
fix: "Run `rails generate rails_ai_context:install` to regenerate")
|
|
131
187
|
end
|
|
132
188
|
end
|
|
133
189
|
|
|
@@ -135,25 +191,184 @@ module RailsAiContext
|
|
|
135
191
|
Server.new(app).build
|
|
136
192
|
Check.new(name: "MCP server", status: :pass, message: "MCP server builds successfully", fix: nil)
|
|
137
193
|
rescue => e
|
|
138
|
-
Check.new(name: "MCP server", status: :fail,
|
|
194
|
+
Check.new(name: "MCP server", status: :fail,
|
|
195
|
+
message: "MCP server failed: #{e.message}",
|
|
196
|
+
fix: "Check mcp gem: `bundle info mcp`")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ── Introspector health ───────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
def check_introspector_health
|
|
202
|
+
config = RailsAiContext.configuration
|
|
203
|
+
errors = []
|
|
204
|
+
|
|
205
|
+
config.introspectors.each do |name|
|
|
206
|
+
begin
|
|
207
|
+
result = RailsAiContext::Introspector.new(app).send(:resolve_introspector, name).call
|
|
208
|
+
errors << name.to_s if result.is_a?(Hash) && result[:error]
|
|
209
|
+
rescue => e
|
|
210
|
+
errors << "#{name} (#{e.message.truncate(50)})"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if errors.empty?
|
|
215
|
+
Check.new(name: "Introspector health", status: :pass,
|
|
216
|
+
message: "All #{config.introspectors.size} introspectors return data",
|
|
217
|
+
fix: nil)
|
|
218
|
+
else
|
|
219
|
+
Check.new(name: "Introspector health", status: :warn,
|
|
220
|
+
message: "#{errors.size} introspector(s) returned errors: #{errors.join(', ')}",
|
|
221
|
+
fix: "Check if the app has the required features (e.g., stimulus needs app/javascript/controllers/)")
|
|
222
|
+
end
|
|
223
|
+
rescue
|
|
224
|
+
nil
|
|
139
225
|
end
|
|
140
226
|
|
|
227
|
+
def check_preset_coverage
|
|
228
|
+
config = RailsAiContext.configuration
|
|
229
|
+
suggestions = []
|
|
230
|
+
|
|
231
|
+
stimulus_dir = File.join(app.root, "app/javascript/controllers")
|
|
232
|
+
if Dir.exist?(stimulus_dir) && Dir.glob(File.join(stimulus_dir, "**/*_controller.{js,ts}")).any? && !config.introspectors.include?(:stimulus)
|
|
233
|
+
suggestions << "stimulus (#{Dir.glob(File.join(stimulus_dir, '**/*_controller.{js,ts}')).size} controllers found)"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
views_dir = File.join(app.root, "app/views")
|
|
237
|
+
if Dir.exist?(views_dir) && !config.introspectors.include?(:views)
|
|
238
|
+
suggestions << "views (app/views/ exists)"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
i18n_dir = File.join(app.root, "config/locales")
|
|
242
|
+
if Dir.exist?(i18n_dir) && Dir.glob(File.join(i18n_dir, "**/*.{yml,yaml}")).size > 1 && !config.introspectors.include?(:i18n)
|
|
243
|
+
suggestions << "i18n (#{Dir.glob(File.join(i18n_dir, '**/*.{yml,yaml}')).size} locale files)"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
graphql_dir = File.join(app.root, "app/graphql")
|
|
247
|
+
if Dir.exist?(graphql_dir) && !config.introspectors.include?(:api)
|
|
248
|
+
suggestions << "api (app/graphql/ exists)"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if suggestions.empty?
|
|
252
|
+
Check.new(name: "Preset coverage", status: :pass,
|
|
253
|
+
message: "#{config.introspectors.size} introspectors cover detected features",
|
|
254
|
+
fix: nil)
|
|
255
|
+
else
|
|
256
|
+
Check.new(name: "Preset coverage", status: :warn,
|
|
257
|
+
message: "App has features not in preset: #{suggestions.join(', ')}",
|
|
258
|
+
fix: "Add with `config.introspectors += %i[#{suggestions.map { |s| s.split(' ').first }.join(' ')}]` or use `config.preset = :full`")
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# ── Tool dependencies ─────────────────────────────────────────────
|
|
263
|
+
|
|
141
264
|
def check_ripgrep
|
|
142
|
-
if system("which rg
|
|
143
|
-
Check.new(name: "ripgrep", status: :pass, message: "rg available for code search", fix: nil)
|
|
265
|
+
if system("which", "rg", out: File::NULL, err: File::NULL)
|
|
266
|
+
Check.new(name: "ripgrep", status: :pass, message: "rg available for fast code search", fix: nil)
|
|
144
267
|
else
|
|
145
|
-
Check.new(name: "ripgrep", status: :warn,
|
|
268
|
+
Check.new(name: "ripgrep", status: :warn,
|
|
269
|
+
message: "ripgrep not installed (slower Ruby fallback)",
|
|
270
|
+
fix: "Install: `brew install ripgrep` or `apt install ripgrep`")
|
|
146
271
|
end
|
|
147
272
|
end
|
|
148
273
|
|
|
149
274
|
def check_live_reload
|
|
150
275
|
require "listen"
|
|
151
|
-
Check.new(name: "Live reload", status: :pass, message: "`listen` gem available
|
|
276
|
+
Check.new(name: "Live reload", status: :pass, message: "`listen` gem available", fix: nil)
|
|
152
277
|
rescue LoadError
|
|
153
|
-
Check.new(name: "Live reload", status: :warn,
|
|
278
|
+
Check.new(name: "Live reload", status: :warn,
|
|
279
|
+
message: "`listen` gem not installed (live reload unavailable)",
|
|
280
|
+
fix: "Add: `gem 'listen', group: :development`")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# ── Security checks ───────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
def check_security_gitignore
|
|
286
|
+
issues = []
|
|
287
|
+
gitignore_path = File.join(app.root, ".gitignore")
|
|
288
|
+
gitignore = File.exist?(gitignore_path) ? File.read(gitignore_path) : ""
|
|
289
|
+
|
|
290
|
+
env_path = File.join(app.root, ".env")
|
|
291
|
+
if File.exist?(env_path) && !gitignore.include?(".env")
|
|
292
|
+
issues << ".env exists but not in .gitignore"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
master_key = File.join(app.root, "config/master.key")
|
|
296
|
+
if File.exist?(master_key) && !gitignore.include?("master.key")
|
|
297
|
+
issues << "config/master.key not in .gitignore"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
if issues.empty?
|
|
301
|
+
Check.new(name: "Secrets in .gitignore", status: :pass, message: "Sensitive files properly gitignored", fix: nil)
|
|
302
|
+
else
|
|
303
|
+
Check.new(name: "Secrets in .gitignore", status: :fail,
|
|
304
|
+
message: issues.join("; "),
|
|
305
|
+
fix: "Add to .gitignore: `.env`, `config/master.key`")
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def check_security_auto_mount
|
|
310
|
+
config = RailsAiContext.configuration
|
|
311
|
+
if config.auto_mount && defined?(Rails.env) && Rails.env.production?
|
|
312
|
+
Check.new(name: "MCP auto_mount", status: :fail,
|
|
313
|
+
message: "auto_mount is enabled in production — MCP endpoint is publicly accessible",
|
|
314
|
+
fix: "Set `config.auto_mount = false` or restrict to development: `config.auto_mount = Rails.env.development?`")
|
|
315
|
+
else
|
|
316
|
+
Check.new(name: "MCP auto_mount", status: :pass,
|
|
317
|
+
message: config.auto_mount ? "auto_mount enabled (non-production)" : "auto_mount disabled (safe)",
|
|
318
|
+
fix: nil)
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# ── Performance checks ────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
def check_performance_schema_size
|
|
325
|
+
config = RailsAiContext.configuration
|
|
326
|
+
schema_path = File.join(app.root, "db/schema.rb")
|
|
327
|
+
structure_path = File.join(app.root, "db/structure.sql")
|
|
328
|
+
|
|
329
|
+
path = File.exist?(schema_path) ? schema_path : (File.exist?(structure_path) ? structure_path : nil)
|
|
330
|
+
return nil unless path
|
|
331
|
+
|
|
332
|
+
size = File.size(path)
|
|
333
|
+
limit = config.max_schema_file_size
|
|
334
|
+
pct = ((size.to_f / limit) * 100).round
|
|
335
|
+
|
|
336
|
+
if pct >= 80
|
|
337
|
+
Check.new(name: "Schema file size", status: :warn,
|
|
338
|
+
message: "#{File.basename(path)} is #{(size / 1_000_000.0).round(1)}MB (#{pct}% of #{(limit / 1_000_000.0).round}MB limit)",
|
|
339
|
+
fix: "Increase `config.max_schema_file_size` in your initializer")
|
|
340
|
+
else
|
|
341
|
+
Check.new(name: "Schema file size", status: :pass,
|
|
342
|
+
message: "#{File.basename(path)} is #{(size / 1024.0).round}KB (within limit)",
|
|
343
|
+
fix: nil)
|
|
344
|
+
end
|
|
154
345
|
end
|
|
155
346
|
|
|
347
|
+
def check_performance_view_count
|
|
348
|
+
config = RailsAiContext.configuration
|
|
349
|
+
views_dir = File.join(app.root, "app/views")
|
|
350
|
+
return nil unless Dir.exist?(views_dir)
|
|
351
|
+
|
|
352
|
+
count = Dir.glob(File.join(views_dir, "**/*.{erb,haml,slim}")).size
|
|
353
|
+
total_size = Dir.glob(File.join(views_dir, "**/*.{erb,haml,slim}")).sum { |f| File.size(f) rescue 0 }
|
|
354
|
+
limit = config.max_view_total_size
|
|
355
|
+
pct = ((total_size.to_f / limit) * 100).round
|
|
356
|
+
|
|
357
|
+
if pct >= 80
|
|
358
|
+
Check.new(name: "View aggregation size", status: :warn,
|
|
359
|
+
message: "#{count} view files totaling #{(total_size / 1_000_000.0).round(1)}MB (#{pct}% of #{(limit / 1_000_000.0).round}MB limit for UI pattern extraction)",
|
|
360
|
+
fix: "Increase `config.max_view_total_size` or `config.max_view_file_size`")
|
|
361
|
+
else
|
|
362
|
+
Check.new(name: "View aggregation size", status: :pass,
|
|
363
|
+
message: "#{count} view files (#{(total_size / 1024.0).round}KB total, within limits)",
|
|
364
|
+
fix: nil)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# ── Scoring ───────────────────────────────────────────────────────
|
|
369
|
+
|
|
156
370
|
def compute_score(results)
|
|
371
|
+
return 0 if results.empty?
|
|
157
372
|
total = results.size * 10
|
|
158
373
|
earned = results.sum do |check|
|
|
159
374
|
case check.status
|
data/server.json
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.15.
|
|
10
|
+
"version": "0.15.8",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.8/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|