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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0d532a0b5cd13d8f15fd988426013b6c493df52bbea861601bc6e98fc50284d
4
- data.tar.gz: d6937138538b5863998da10507a967a3538354bd80885e2d8be29dce9552331f
3
+ metadata.gz: 69d121a9a96c5e7c796331dd5773a22e7981c2ea8ba22dc09cc4c722bc3d8b7c
4
+ data.tar.gz: 8e95c49716c2dae5c5fe5ad4883de2ca7bb12d396b5221f904e6d4ab9015cd5e
5
5
  SHA512:
6
- metadata.gz: 49df028217e81fd1dc56513dad0ba11cc4b755d231a06bacd822b848e88f30ad37108ba6c8c8a076f36af35ee3babbb159d2dba47162fc34ae109670fecb0107
7
- data.tar.gz: 977cca79b0eba516d23c4cf8dbc678bc0874420405959fc927150f10cd52c14f554c48e801db3f74849e1919dfe15caea6d8078539436aec88dab1b2838bef8d
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
- **Turn any Rails app into an AI-ready codebase one gem install.**
3
+ **Give AI agents a complete mental model of your Rails app not just files, but how everything connects.**
4
4
 
5
5
  [![Gem Version](https://img.shields.io/gem/v/rails-ai-context?color=brightgreen)](https://rubygems.org/gems/rails-ai-context)
6
6
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-green)](https://registry.modelcontextprotocol.io)
7
7
  [![CI](https://github.com/crisnahine/rails-ai-context/actions/workflows/ci.yml/badge.svg)](https://github.com/crisnahine/rails-ai-context/actions)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- You open Claude Code, Cursor, or Copilot and ask: *"Add a draft status to posts with a scheduled publish date."*
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 AI doesn't know your schema, your Devise setup, your Sidekiq jobs, or that `Post` already has an `enum :status`. It generates generic code that doesn't match your app.
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
- **rails-ai-context fixes this.** It auto-introspects your entire Rails app and feeds everything to your AI assistant — schema, models, routes, controllers, jobs, gems, auth, API, tests, config, and conventions — through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io).
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
- ## Proof: 37% Token Savings (Real Benchmark)
22
+ ```bash
23
+ bundle add rails-ai-context
24
+ rails generate rails_ai_context:install
25
+ rails ai:context
26
+ ```
25
27
 
26
- Same task *"Add status and date range filters to the Cooks index page"* 4 scenarios in parallel, same Rails app:
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
- | Setup | Tokens | Saved | What it knows |
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
- https://github.com/user-attachments/assets/14476243-1210-4e62-9dc5-9d4aa9caef7e
34
+ ---
43
35
 
36
+ ## What Changes
44
37
 
45
- **What each layer gives you:**
38
+ ### Without rails-ai-context
46
39
 
47
- | | Normal `/init` | rails-ai-context CLAUDE.md | rails-ai-context full |
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
- **~16,600 fewer tokens per task** vs no gem at all.
42
+ **Every mistake is a wasted iteration.**
56
43
 
57
- > **This was a simple task on a small 5-model app.** Real-world tasks are 3-10x more complex.
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
- | App size | Without gem | With rails-ai-context | Savings |
61
- |----------|-------------|----------------------|---------|
62
- | Small (5 models) | 45K tokens | 29K tokens | 37% |
63
- | Medium (30 models) | ~150K tokens | ~60K tokens | ~60% |
64
- | Large (100+ models) | ~500K+ tokens | ~100K tokens | ~80% |
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
- *Medium/large estimates based on schema.rb scaling (40 lines/table), model file scaling, and MCP summary-first workflow eliminating full-file reads.*
53
+ **Orient drill down act verify.** The first attempt is correct.
67
54
 
68
55
  ---
69
56
 
70
- ## Quick Start
57
+ ## Three Layers of Context
71
58
 
72
- ```bash
73
- bundle add rails-ai-context
74
- rails generate rails_ai_context:install
75
- rails ai:context
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
- That's it. Three commands. Your AI assistant now understands your entire Rails app.
65
+ **Progressive disclosure:** the agent gets the map for free, reference guides when relevant, and live GPS when building.
79
66
 
80
- The install generator creates `.mcp.json` for auto-discovery — Claude Code and Cursor detect it automatically. No manual MCP config needed.
67
+ ---
81
68
 
82
- > **[Full Guide](docs/GUIDE.md)** complete documentation with every command, parameter, and configuration option.
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
- ## How It Saves Tokens
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
- ![Token Comparison](https://raw.githubusercontent.com/crisnahine/rails-ai-context/main/docs/token-comparison.jpeg)
89
+ But token savings is the side effect. The real value:
89
90
 
90
- - `/init` saves 11% knows the framework but wastes tokens discovering models and tables
91
- - **CLAUDE.md saves 27%** complete Rails-specific map, zero discovery overhead
92
- - **Full MCP saves 37%** structured queries replace expensive full-file reads
93
- - MCP tools return `detail:"summary"` first (~55 tokens), then drill into specifics
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
- check_context_files
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.map { |check| send(check) }
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
- Check.new(name: "Schema", status: :pass, message: "db/schema.rb found", fix: nil)
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: "Schema", status: :warn, message: "db/schema.rb not found", fix: "Run `rails db:schema:dump` to generate it")
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 found in app/models/", fix: "Generate models with `rails generate model`")
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` to generate it")
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 found in app/controllers/", fix: "Generate controllers with `rails generate controller`")
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) && Dir.glob(File.join(dir, "**/*")).reject { |f| File.directory?(f) }.any?
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 found in app/views/", fix: "Views are generated alongside controllers")
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", fix: "Set up tests with `rails generate rspec:install` or use default Minitest")
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 found", fix: nil)
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 found in db/migrate/", fix: "Run `rails generate migration` to create one")
142
+ Check.new(name: "Migrations", status: :warn, message: "No migrations", fix: nil)
122
143
  end
123
144
  end
124
145
 
125
- def check_context_files
146
+ # ── Context file checks ───────────────────────────────────────────
147
+
148
+ def check_context_freshness
126
149
  claude_path = File.join(app.root, "CLAUDE.md")
127
- if File.exist?(claude_path)
128
- Check.new(name: "Context files", status: :pass, message: "CLAUDE.md exists", fix: nil)
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, message: "No context files generated yet", fix: "Run `rails ai:context` to generate them")
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, message: "MCP server failed to build: #{e.message}", fix: "Check mcp gem installation: `bundle info mcp`")
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 > /dev/null 2>&1")
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, message: "ripgrep not installed (code search will use slower Ruby fallback)", fix: "Install with `brew install ripgrep` or `apt install ripgrep`")
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 for live reload", fix: nil)
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, message: "`listen` gem not installed (MCP live reload unavailable)", fix: "Add to your Gemfile: gem 'listen', group: :development")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "0.15.8"
4
+ VERSION = "0.15.9"
5
5
  end
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.6",
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.6/rails-ai-context-mcp.mcpb",
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"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-ai-context
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.8
4
+ version: 0.15.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - crisnahine