rails-ai-context 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -2
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +47 -0
- data/README.md +72 -20
- data/exe/rails-ai-context +30 -2
- data/lib/rails_ai_context/configuration.rb +5 -1
- data/lib/rails_ai_context/doctor.rb +106 -0
- data/lib/rails_ai_context/engine.rb +8 -0
- data/lib/rails_ai_context/fingerprinter.rb +46 -0
- data/lib/rails_ai_context/introspector.rb +2 -0
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +39 -0
- data/lib/rails_ai_context/introspectors/model_introspector.rb +10 -8
- data/lib/rails_ai_context/introspectors/route_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +65 -0
- data/lib/rails_ai_context/middleware.rb +39 -0
- data/lib/rails_ai_context/resources.rb +87 -0
- data/lib/rails_ai_context/serializers/claude_serializer.rb +45 -0
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +16 -10
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +29 -0
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +6 -6
- data/lib/rails_ai_context/serializers/rules_serializer.rb +26 -0
- data/lib/rails_ai_context/server.rb +6 -2
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +42 -7
- data/lib/rails_ai_context/tools/base_tool.rb +15 -3
- data/lib/rails_ai_context/tools/get_conventions.rb +1 -1
- data/lib/rails_ai_context/tools/get_gems.rb +2 -2
- data/lib/rails_ai_context/tools/get_model_details.rb +1 -1
- data/lib/rails_ai_context/tools/get_routes.rb +1 -1
- data/lib/rails_ai_context/tools/get_schema.rb +1 -1
- data/lib/rails_ai_context/tools/search_code.rb +42 -9
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/watcher.rb +75 -0
- data/lib/rails_ai_context.rb +14 -1
- data/rails-ai-context.gemspec +4 -4
- metadata +13 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fa5518e88de666f981967ae1557990aadea0804c192511c4cbf204d6b45f146
|
|
4
|
+
data.tar.gz: e39c7430277939e504aa3056ac0ac85f9f347dd008ff907627a032d7607ad21f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1709f1becfbe976d7cd7a62690e90b31dbe5d61b2d09d9c9001ec43d0ce2d344d840d310d42d65ad9c1bf86bba2a0eb3285cb88eb074e131a373904224f7813
|
|
7
|
+
data.tar.gz: 04714a3b181c91844c27b7b64ad2c2e9db963a16ae5ee037721f01afd518bad151b4d60dee8df78a69638b0fbd87af2c18288b70d14c62a72e72bd44f23f0c2d
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ 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.3.0] - 2026-03-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Cache invalidation** — TTL + file fingerprinting for MCP tool cache (replaces permanent `||=` cache)
|
|
13
|
+
- **MCP Resources** — Static resources (`rails://schema`, `rails://routes`, `rails://conventions`, `rails://gems`) and resource template (`rails://models/{name}`)
|
|
14
|
+
- **Per-assistant serializers** — Claude gets behavioral rules, Cursor/Windsurf get compact rules, Copilot gets task-oriented GFM
|
|
15
|
+
- **Stimulus introspector** — Extracts Stimulus controller targets, values, and actions from JS/TS files
|
|
16
|
+
- **Database stats introspector** — Opt-in PostgreSQL approximate row counts via `pg_stat_user_tables`
|
|
17
|
+
- **Auto-mount HTTP middleware** — Rack middleware for MCP endpoint when `config.auto_mount = true`
|
|
18
|
+
- **Diff-aware regeneration** — Context file generation skips unchanged files
|
|
19
|
+
- **`rails ai:doctor`** — Diagnostic command with AI readiness score (0-100)
|
|
20
|
+
- **`rails ai:watch`** — File watcher that auto-regenerates context files on change (requires `listen` gem)
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Shell injection in SearchCode** — Replaced backtick execution with `Open3.capture2` array form; added file_type validation, max_results cap, and path traversal protection
|
|
25
|
+
- **Scope extraction** — Fixed broken `model.methods.grep(/^_scope_/)` by parsing source files for `scope :name` declarations
|
|
26
|
+
- **Route introspector** — Fixed `route.internal?` compatibility with Rails 8.1
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- `generate_context` now returns `{ written: [], skipped: [] }` instead of flat array
|
|
31
|
+
- Default introspectors now include `:stimulus`
|
|
32
|
+
|
|
8
33
|
## [0.2.0] - 2026-03-18
|
|
9
34
|
|
|
10
35
|
### Added
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# CLAUDE.md — rails-ai-context development guide
|
|
2
|
+
|
|
3
|
+
This is a Ruby gem that auto-introspects Rails applications and exposes their
|
|
4
|
+
structure to AI assistants via the Model Context Protocol (MCP).
|
|
5
|
+
|
|
6
|
+
## Architecture
|
|
7
|
+
|
|
8
|
+
- `lib/rails_ai_context.rb` — Main entry point, public API
|
|
9
|
+
- `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
|
|
10
|
+
- `lib/rails_ai_context/introspectors/` — Individual introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats)
|
|
11
|
+
- `lib/rails_ai_context/tools/` — MCP tools using the official mcp SDK
|
|
12
|
+
- `lib/rails_ai_context/serializers/` — Output formatters (claude, rules, copilot, markdown, JSON)
|
|
13
|
+
- `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
|
|
14
|
+
- `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
|
|
15
|
+
- `lib/rails_ai_context/middleware.rb` — Rack middleware for auto-mounting MCP HTTP endpoint
|
|
16
|
+
- `lib/rails_ai_context/fingerprinter.rb` — SHA256 file fingerprinting for cache invalidation
|
|
17
|
+
- `lib/rails_ai_context/doctor.rb` — Diagnostic checks and AI readiness scoring
|
|
18
|
+
- `lib/rails_ai_context/watcher.rb` — File watcher for auto-regenerating context files
|
|
19
|
+
- `lib/rails_ai_context/engine.rb` — Rails Engine for auto-integration
|
|
20
|
+
|
|
21
|
+
## Key Design Decisions
|
|
22
|
+
|
|
23
|
+
1. **Built on official mcp SDK** — not a custom protocol implementation
|
|
24
|
+
2. **Zero-config** — Railtie auto-registers at boot, introspects without setup
|
|
25
|
+
3. **Graceful degradation** — works without DB by parsing schema.rb as text
|
|
26
|
+
4. **Read-only tools only** — all MCP tools are annotated as non-destructive
|
|
27
|
+
5. **Dual output** — static files (CLAUDE.md) + live MCP server (stdio/HTTP)
|
|
28
|
+
6. **Diff-aware** — context regeneration skips unchanged files
|
|
29
|
+
7. **Per-assistant serializers** — each AI tool gets tailored output format
|
|
30
|
+
|
|
31
|
+
## Testing
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle exec rspec # Run specs
|
|
35
|
+
bundle exec rubocop # Lint
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Uses combustion gem for testing Rails engine behavior in isolation.
|
|
39
|
+
|
|
40
|
+
## Conventions
|
|
41
|
+
|
|
42
|
+
- Ruby 3.2+ features OK (pattern matching, etc.)
|
|
43
|
+
- Follow rubocop-rails-omakase style
|
|
44
|
+
- Every introspector returns a Hash, never raises (wraps errors in `{ error: msg }`)
|
|
45
|
+
- MCP tools return `MCP::Tool::Response` objects per SDK convention
|
|
46
|
+
- All tools prefixed with `rails_` per MCP naming best practices
|
|
47
|
+
- `generate_context` returns `{ written: [], skipped: [] }` hash
|
data/README.md
CHANGED
|
@@ -26,12 +26,13 @@ bundle add rails-ai-context
|
|
|
26
26
|
|
|
27
27
|
That's it. Now your AI assistant knows:
|
|
28
28
|
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
29
|
+
- **Every table, column, index, and foreign key** in your database
|
|
30
|
+
- **Every model** with its associations, validations, scopes, enums, and callbacks
|
|
31
|
+
- **Every route** with HTTP verbs, paths, and controller actions
|
|
32
|
+
- **Every background job**, mailer, and Action Cable channel
|
|
33
|
+
- **Every notable gem** and what it means (Devise = auth, Sidekiq = jobs, Turbo = Hotwire)
|
|
34
|
+
- **Your architecture patterns**: service objects, STI, polymorphism, state machines, multi-tenancy
|
|
35
|
+
- **Stimulus controllers** with targets, values, and actions
|
|
35
36
|
|
|
36
37
|
---
|
|
37
38
|
|
|
@@ -51,12 +52,12 @@ rails ai:context
|
|
|
51
52
|
```
|
|
52
53
|
|
|
53
54
|
This creates:
|
|
54
|
-
- `CLAUDE.md` — for Claude Code
|
|
55
|
-
- `.cursorrules` — for Cursor
|
|
56
|
-
- `.windsurfrules` — for Windsurf
|
|
57
|
-
- `.github/copilot-instructions.md` — for GitHub Copilot
|
|
55
|
+
- `CLAUDE.md` — for Claude Code (with behavioral rules)
|
|
56
|
+
- `.cursorrules` — for Cursor (compact rules format)
|
|
57
|
+
- `.windsurfrules` — for Windsurf (compact rules format)
|
|
58
|
+
- `.github/copilot-instructions.md` — for GitHub Copilot (task-oriented)
|
|
58
59
|
|
|
59
|
-
**Commit these files.** Your entire team gets smarter AI assistance.
|
|
60
|
+
Each file is tailored to the AI assistant's preferred format. **Commit these files.** Your entire team gets smarter AI assistance.
|
|
60
61
|
|
|
61
62
|
### 3. Start the MCP Server
|
|
62
63
|
|
|
@@ -97,6 +98,18 @@ The gem exposes 6 tools via MCP that AI clients can call:
|
|
|
97
98
|
|
|
98
99
|
All tools are **read-only** — they never modify your application or database.
|
|
99
100
|
|
|
101
|
+
## MCP Resources
|
|
102
|
+
|
|
103
|
+
In addition to tools, the gem registers MCP resources that AI clients can read directly:
|
|
104
|
+
|
|
105
|
+
| Resource | Description |
|
|
106
|
+
|----------|-------------|
|
|
107
|
+
| `rails://schema` | Full database schema (JSON) |
|
|
108
|
+
| `rails://routes` | All routes (JSON) |
|
|
109
|
+
| `rails://conventions` | Detected patterns and architecture (JSON) |
|
|
110
|
+
| `rails://gems` | Notable gems with categories (JSON) |
|
|
111
|
+
| `rails://models/{name}` | Per-model details (resource template) |
|
|
112
|
+
|
|
100
113
|
---
|
|
101
114
|
|
|
102
115
|
## How It Works
|
|
@@ -124,9 +137,10 @@ All tools are **read-only** — they never modify your application or database.
|
|
|
124
137
|
(reads file) any MCP client
|
|
125
138
|
```
|
|
126
139
|
|
|
127
|
-
**
|
|
140
|
+
**Three modes:**
|
|
128
141
|
1. **Static files** (`rails ai:context`) — generates markdown files that AI tools read as project context. Zero runtime cost. Works everywhere.
|
|
129
142
|
2. **MCP server** (`rails ai:serve`) — live introspection tools that AI clients call on-demand. Richer, always up-to-date.
|
|
143
|
+
3. **Watch mode** (`rails ai:watch`) — auto-regenerates context files when your code changes.
|
|
130
144
|
|
|
131
145
|
---
|
|
132
146
|
|
|
@@ -145,20 +159,50 @@ RailsAiContext.configure do |config|
|
|
|
145
159
|
config.auto_mount = true
|
|
146
160
|
config.http_path = "/mcp"
|
|
147
161
|
config.http_port = 6029
|
|
162
|
+
|
|
163
|
+
# Cache TTL for MCP tool responses (seconds)
|
|
164
|
+
config.cache_ttl = 30
|
|
165
|
+
|
|
166
|
+
# Enable Postgres row count stats (opt-in)
|
|
167
|
+
# config.introspectors += [:database_stats]
|
|
148
168
|
end
|
|
149
169
|
```
|
|
150
170
|
|
|
151
171
|
---
|
|
152
172
|
|
|
173
|
+
## Diagnostics
|
|
174
|
+
|
|
175
|
+
Check your app's AI readiness:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
rails ai:doctor
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Reports pass/warn/fail for schema, models, routes, gems, context files, MCP server, and ripgrep. Includes fix suggestions and an AI readiness score (0-100).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Stimulus Support
|
|
186
|
+
|
|
187
|
+
The gem automatically detects Stimulus controllers and extracts:
|
|
188
|
+
- Controller names (derived from filenames)
|
|
189
|
+
- Static targets
|
|
190
|
+
- Static values
|
|
191
|
+
- Action methods
|
|
192
|
+
|
|
193
|
+
This gives AI assistants context about your frontend JavaScript alongside your backend Ruby.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
153
197
|
## Supported AI Assistants
|
|
154
198
|
|
|
155
|
-
| AI Assistant | Context File | Command |
|
|
156
|
-
|
|
157
|
-
| Claude Code | `CLAUDE.md` | `rails ai:context:claude` |
|
|
158
|
-
| Cursor | `.cursorrules` | `rails ai:context:cursor` |
|
|
159
|
-
| Windsurf | `.windsurfrules` | `rails ai:context:windsurf` |
|
|
160
|
-
| GitHub Copilot | `.github/copilot-instructions.md` | `rails ai:context:copilot` |
|
|
161
|
-
| JSON (generic) | `.ai-context.json` | `rails ai:context:json` |
|
|
199
|
+
| AI Assistant | Context File | Format | Command |
|
|
200
|
+
|--------------|-------------|--------|---------|
|
|
201
|
+
| Claude Code | `CLAUDE.md` | Verbose + behavioral rules | `rails ai:context:claude` |
|
|
202
|
+
| Cursor | `.cursorrules` | Compact imperative rules | `rails ai:context:cursor` |
|
|
203
|
+
| Windsurf | `.windsurfrules` | Compact imperative rules | `rails ai:context:windsurf` |
|
|
204
|
+
| GitHub Copilot | `.github/copilot-instructions.md` | Task-oriented GFM | `rails ai:context:copilot` |
|
|
205
|
+
| JSON (generic) | `.ai-context.json` | Structured JSON | `rails ai:context:json` |
|
|
162
206
|
|
|
163
207
|
---
|
|
164
208
|
|
|
@@ -166,7 +210,7 @@ end
|
|
|
166
210
|
|
|
167
211
|
| Command | Description |
|
|
168
212
|
|---------|-------------|
|
|
169
|
-
| `rails ai:context` | Generate all context files (
|
|
213
|
+
| `rails ai:context` | Generate all context files (skips unchanged) |
|
|
170
214
|
| `rails ai:context:claude` | Generate CLAUDE.md only |
|
|
171
215
|
| `rails ai:context:cursor` | Generate .cursorrules only |
|
|
172
216
|
| `rails ai:context:windsurf` | Generate .windsurfrules only |
|
|
@@ -175,6 +219,8 @@ end
|
|
|
175
219
|
| `rails ai:serve` | Start MCP server (stdio, for Claude Code) |
|
|
176
220
|
| `rails ai:serve_http` | Start MCP server (HTTP, for remote clients) |
|
|
177
221
|
| `rails ai:inspect` | Print introspection summary to stdout |
|
|
222
|
+
| `rails ai:doctor` | Run diagnostics and report AI readiness score |
|
|
223
|
+
| `rails ai:watch` | Watch for changes and auto-regenerate context files |
|
|
178
224
|
|
|
179
225
|
> **zsh users:** The bracket syntax `rails ai:context_for[claude]` requires quoting in zsh (`rails 'ai:context_for[claude]'`). The named tasks above (`rails ai:context:claude`) work without quoting in any shell.
|
|
180
226
|
|
|
@@ -196,6 +242,8 @@ The gem gracefully degrades when no database is connected — it parses `db/sche
|
|
|
196
242
|
- Ruby >= 3.2
|
|
197
243
|
- Rails >= 7.1
|
|
198
244
|
- [mcp](https://github.com/modelcontextprotocol/ruby-sdk) (official MCP SDK, installed automatically)
|
|
245
|
+
- Optional: `listen` gem for watch mode
|
|
246
|
+
- Optional: `ripgrep` for fast code search (falls back to Ruby)
|
|
199
247
|
|
|
200
248
|
---
|
|
201
249
|
|
|
@@ -225,6 +273,10 @@ bundle exec rspec
|
|
|
225
273
|
|
|
226
274
|
Bug reports and pull requests welcome at https://github.com/crisnahine/rails-ai-context.
|
|
227
275
|
|
|
276
|
+
## Sponsorship
|
|
277
|
+
|
|
278
|
+
If rails-ai-context helps your workflow, consider supporting the project — [become a monthly sponsor or buy me a coffee](https://github.com/sponsors/crisnahine).
|
|
279
|
+
|
|
228
280
|
## License
|
|
229
281
|
|
|
230
282
|
[MIT License](LICENSE)
|
data/exe/rails-ai-context
CHANGED
|
@@ -28,8 +28,9 @@ module RailsAiContext
|
|
|
28
28
|
|
|
29
29
|
format = options[:format].to_sym
|
|
30
30
|
$stderr.puts "Introspecting Rails app..."
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
result = RailsAiContext.generate_context(format: format)
|
|
32
|
+
result[:written].each { |f| $stderr.puts " Written: #{f}" }
|
|
33
|
+
result[:skipped].each { |f| $stderr.puts " Skipped: #{f} (unchanged)" }
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
desc "inspect", "Print introspection summary"
|
|
@@ -42,6 +43,33 @@ module RailsAiContext
|
|
|
42
43
|
puts JSON.pretty_generate(context)
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
desc "watch", "Watch for changes and auto-regenerate context files"
|
|
47
|
+
def watch
|
|
48
|
+
boot_rails!
|
|
49
|
+
require "rails_ai_context"
|
|
50
|
+
|
|
51
|
+
RailsAiContext::Watcher.new.start
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "doctor", "Run diagnostic checks and report AI readiness score"
|
|
55
|
+
def doctor
|
|
56
|
+
boot_rails!
|
|
57
|
+
require "rails_ai_context"
|
|
58
|
+
|
|
59
|
+
result = RailsAiContext::Doctor.new.run
|
|
60
|
+
result[:checks].each do |check|
|
|
61
|
+
icon = case check.status
|
|
62
|
+
when :pass then "PASS"
|
|
63
|
+
when :warn then "WARN"
|
|
64
|
+
when :fail then "FAIL"
|
|
65
|
+
end
|
|
66
|
+
$stderr.puts " [#{icon}] #{check.name}: #{check.message}"
|
|
67
|
+
$stderr.puts " Fix: #{check.fix}" if check.fix
|
|
68
|
+
end
|
|
69
|
+
$stderr.puts ""
|
|
70
|
+
$stderr.puts "AI Readiness Score: #{result[:score]}/100"
|
|
71
|
+
end
|
|
72
|
+
|
|
45
73
|
desc "version", "Print version"
|
|
46
74
|
def version
|
|
47
75
|
require_relative "../rails_ai_context/version"
|
|
@@ -26,10 +26,13 @@ module RailsAiContext
|
|
|
26
26
|
# Maximum depth for association traversal
|
|
27
27
|
attr_accessor :max_association_depth
|
|
28
28
|
|
|
29
|
+
# TTL in seconds for cached introspection (default: 30)
|
|
30
|
+
attr_accessor :cache_ttl
|
|
31
|
+
|
|
29
32
|
def initialize
|
|
30
33
|
@server_name = "rails-ai-context"
|
|
31
34
|
@server_version = RailsAiContext::VERSION
|
|
32
|
-
@introspectors = %i[schema models routes jobs gems conventions]
|
|
35
|
+
@introspectors = %i[schema models routes jobs gems conventions stimulus]
|
|
33
36
|
@excluded_paths = %w[node_modules tmp log vendor .git]
|
|
34
37
|
@auto_mount = false
|
|
35
38
|
@http_path = "/mcp"
|
|
@@ -43,6 +46,7 @@ module RailsAiContext
|
|
|
43
46
|
ActionMailbox::InboundEmail ActionMailbox::Record
|
|
44
47
|
]
|
|
45
48
|
@max_association_depth = 2
|
|
49
|
+
@cache_ttl = 30
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def output_dir_for(app)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
# Diagnostic checker that validates the environment and reports
|
|
5
|
+
# AI readiness with pass/warn/fail checks and a readiness score.
|
|
6
|
+
class Doctor
|
|
7
|
+
Check = Data.define(:name, :status, :message, :fix)
|
|
8
|
+
|
|
9
|
+
CHECKS = %i[
|
|
10
|
+
check_schema
|
|
11
|
+
check_models
|
|
12
|
+
check_routes
|
|
13
|
+
check_gems
|
|
14
|
+
check_context_files
|
|
15
|
+
check_mcp_buildable
|
|
16
|
+
check_ripgrep
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
attr_reader :app
|
|
20
|
+
|
|
21
|
+
def initialize(app = nil)
|
|
22
|
+
@app = app || Rails.application
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
results = CHECKS.map { |check| send(check) }
|
|
27
|
+
score = compute_score(results)
|
|
28
|
+
{ checks: results, score: score }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def check_schema
|
|
34
|
+
schema_path = File.join(app.root, "db/schema.rb")
|
|
35
|
+
if File.exist?(schema_path)
|
|
36
|
+
Check.new(name: "Schema", status: :pass, message: "db/schema.rb found", fix: nil)
|
|
37
|
+
else
|
|
38
|
+
Check.new(name: "Schema", status: :warn, message: "db/schema.rb not found", fix: "Run `rails db:schema:dump` to generate it")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def check_models
|
|
43
|
+
models_dir = File.join(app.root, "app/models")
|
|
44
|
+
if Dir.exist?(models_dir) && Dir.glob(File.join(models_dir, "**/*.rb")).any?
|
|
45
|
+
count = Dir.glob(File.join(models_dir, "**/*.rb")).size
|
|
46
|
+
Check.new(name: "Models", status: :pass, message: "#{count} model files found", fix: nil)
|
|
47
|
+
else
|
|
48
|
+
Check.new(name: "Models", status: :warn, message: "No model files found in app/models/", fix: "Generate models with `rails generate model`")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def check_routes
|
|
53
|
+
routes_path = File.join(app.root, "config/routes.rb")
|
|
54
|
+
if File.exist?(routes_path)
|
|
55
|
+
Check.new(name: "Routes", status: :pass, message: "config/routes.rb found", fix: nil)
|
|
56
|
+
else
|
|
57
|
+
Check.new(name: "Routes", status: :fail, message: "config/routes.rb not found", fix: "Ensure you're in a Rails app root directory")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check_gems
|
|
62
|
+
lock_path = File.join(app.root, "Gemfile.lock")
|
|
63
|
+
if File.exist?(lock_path)
|
|
64
|
+
Check.new(name: "Gems", status: :pass, message: "Gemfile.lock found", fix: nil)
|
|
65
|
+
else
|
|
66
|
+
Check.new(name: "Gems", status: :warn, message: "Gemfile.lock not found", fix: "Run `bundle install` to generate it")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_context_files
|
|
71
|
+
claude_path = File.join(app.root, "CLAUDE.md")
|
|
72
|
+
if File.exist?(claude_path)
|
|
73
|
+
Check.new(name: "Context files", status: :pass, message: "CLAUDE.md exists", fix: nil)
|
|
74
|
+
else
|
|
75
|
+
Check.new(name: "Context files", status: :warn, message: "No context files generated yet", fix: "Run `rails ai:context` to generate them")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def check_mcp_buildable
|
|
80
|
+
Server.new(app).build
|
|
81
|
+
Check.new(name: "MCP server", status: :pass, message: "MCP server builds successfully", fix: nil)
|
|
82
|
+
rescue => e
|
|
83
|
+
Check.new(name: "MCP server", status: :fail, message: "MCP server failed to build: #{e.message}", fix: "Check mcp gem installation: `bundle info mcp`")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_ripgrep
|
|
87
|
+
if system("which rg > /dev/null 2>&1")
|
|
88
|
+
Check.new(name: "ripgrep", status: :pass, message: "rg available for code search", fix: nil)
|
|
89
|
+
else
|
|
90
|
+
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`")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def compute_score(results)
|
|
95
|
+
total = results.size * 10
|
|
96
|
+
earned = results.sum do |check|
|
|
97
|
+
case check.status
|
|
98
|
+
when :pass then 10
|
|
99
|
+
when :warn then 5
|
|
100
|
+
else 0
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
((earned.to_f / total) * 100).round
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -8,6 +8,14 @@ module RailsAiContext
|
|
|
8
8
|
Rails.application.config.rails_ai_context = RailsAiContext.configuration
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Auto-mount MCP HTTP middleware when configured
|
|
12
|
+
initializer "rails_ai_context.middleware" do |app|
|
|
13
|
+
if RailsAiContext.configuration.auto_mount
|
|
14
|
+
require_relative "middleware"
|
|
15
|
+
app.middleware.use RailsAiContext::Middleware
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
# Register Rake tasks
|
|
12
20
|
rake_tasks do
|
|
13
21
|
load File.expand_path("tasks/rails_ai_context.rake", __dir__)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module RailsAiContext
|
|
6
|
+
# Computes a SHA256 fingerprint of key application files to detect changes.
|
|
7
|
+
# Used by BaseTool to invalidate cached introspection when files change.
|
|
8
|
+
class Fingerprinter
|
|
9
|
+
WATCHED_FILES = %w[
|
|
10
|
+
db/schema.rb
|
|
11
|
+
config/routes.rb
|
|
12
|
+
Gemfile.lock
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
WATCHED_DIRS = %w[
|
|
16
|
+
app/models
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def compute(app)
|
|
21
|
+
root = app.root.to_s
|
|
22
|
+
digest = Digest::SHA256.new
|
|
23
|
+
|
|
24
|
+
WATCHED_FILES.each do |file|
|
|
25
|
+
path = File.join(root, file)
|
|
26
|
+
digest.update(File.mtime(path).to_f.to_s) if File.exist?(path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
WATCHED_DIRS.each do |dir|
|
|
30
|
+
full_dir = File.join(root, dir)
|
|
31
|
+
next unless Dir.exist?(full_dir)
|
|
32
|
+
|
|
33
|
+
Dir.glob(File.join(full_dir, "**/*.rb")).sort.each do |path|
|
|
34
|
+
digest.update(File.mtime(path).to_f.to_s)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
digest.hexdigest
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def changed?(app, previous)
|
|
42
|
+
compute(app) != previous
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -53,6 +53,8 @@ module RailsAiContext
|
|
|
53
53
|
when :jobs then Introspectors::JobIntrospector.new(app)
|
|
54
54
|
when :gems then Introspectors::GemIntrospector.new(app)
|
|
55
55
|
when :conventions then Introspectors::ConventionDetector.new(app)
|
|
56
|
+
when :stimulus then Introspectors::StimulusIntrospector.new(app)
|
|
57
|
+
when :database_stats then Introspectors::DatabaseStatsIntrospector.new(app)
|
|
56
58
|
else
|
|
57
59
|
raise ConfigurationError, "Unknown introspector: #{name}"
|
|
58
60
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
# Collects approximate row counts from PostgreSQL's pg_stat_user_tables.
|
|
6
|
+
# Only activates for PostgreSQL adapter; returns { skipped: true } otherwise.
|
|
7
|
+
class DatabaseStatsIntrospector
|
|
8
|
+
attr_reader :app
|
|
9
|
+
|
|
10
|
+
def initialize(app)
|
|
11
|
+
@app = app
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
return { skipped: true, reason: "ActiveRecord not available" } unless defined?(ActiveRecord::Base)
|
|
16
|
+
|
|
17
|
+
adapter = ActiveRecord::Base.connection.adapter_name.downcase
|
|
18
|
+
unless adapter.include?("postgresql")
|
|
19
|
+
return { skipped: true, reason: "Only available for PostgreSQL (current: #{adapter})" }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
rows = ActiveRecord::Base.connection.select_all(<<~SQL)
|
|
23
|
+
SELECT relname AS table_name,
|
|
24
|
+
n_live_tup AS approximate_row_count
|
|
25
|
+
FROM pg_stat_user_tables
|
|
26
|
+
ORDER BY n_live_tup DESC
|
|
27
|
+
SQL
|
|
28
|
+
|
|
29
|
+
tables = rows.map do |row|
|
|
30
|
+
{ table: row["table_name"], approximate_rows: row["approximate_row_count"].to_i }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
{ adapter: "postgresql", tables: tables, total_tables: tables.size }
|
|
34
|
+
rescue => e
|
|
35
|
+
{ error: e.message }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -86,18 +86,20 @@ module RailsAiContext
|
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def extract_scopes(model)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
else
|
|
94
|
-
# Fallback: check for scope definitions via reflection
|
|
95
|
-
model.methods.grep(/^_scope_/).map { |m| m.to_s.sub("_scope_", "") }
|
|
96
|
-
end
|
|
89
|
+
source_path = model_source_path(model)
|
|
90
|
+
return [] unless source_path && File.exist?(source_path)
|
|
91
|
+
|
|
92
|
+
File.read(source_path).scan(/^\s*scope\s+:(\w+)/).flatten
|
|
97
93
|
rescue
|
|
98
94
|
[]
|
|
99
95
|
end
|
|
100
96
|
|
|
97
|
+
def model_source_path(model)
|
|
98
|
+
root = app.root.to_s
|
|
99
|
+
underscored = model.name.underscore
|
|
100
|
+
File.join(root, "app", "models", "#{underscored}.rb")
|
|
101
|
+
end
|
|
102
|
+
|
|
101
103
|
def extract_enums(model)
|
|
102
104
|
return {} unless model.respond_to?(:defined_enums)
|
|
103
105
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
# Scans Stimulus controllers and extracts targets, values, and actions.
|
|
6
|
+
class StimulusIntrospector
|
|
7
|
+
attr_reader :app
|
|
8
|
+
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
root = app.root.to_s
|
|
15
|
+
controllers_dir = File.join(root, "app/javascript/controllers")
|
|
16
|
+
return { controllers: [] } unless Dir.exist?(controllers_dir)
|
|
17
|
+
|
|
18
|
+
controllers = Dir.glob(File.join(controllers_dir, "**/*_controller.{js,ts}")).sort.filter_map do |path|
|
|
19
|
+
parse_controller(path, controllers_dir)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
{ controllers: controllers }
|
|
23
|
+
rescue => e
|
|
24
|
+
{ error: e.message }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def parse_controller(path, base_dir)
|
|
30
|
+
relative = path.sub("#{base_dir}/", "")
|
|
31
|
+
name = relative.sub(/_controller\.(js|ts)\z/, "").tr("/", "--")
|
|
32
|
+
content = File.read(path)
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
name: name,
|
|
36
|
+
file: relative,
|
|
37
|
+
targets: extract_targets(content),
|
|
38
|
+
values: extract_values(content),
|
|
39
|
+
actions: extract_actions(content)
|
|
40
|
+
}
|
|
41
|
+
rescue => e
|
|
42
|
+
{ name: File.basename(path), error: e.message }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_targets(content)
|
|
46
|
+
match = content.match(/static\s+targets\s*=\s*\[([^\]]*)\]/)
|
|
47
|
+
return [] unless match
|
|
48
|
+
|
|
49
|
+
match[1].scan(/["'](\w+)["']/).flatten
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_values(content)
|
|
53
|
+
match = content.match(/static\s+values\s*=\s*\{([^}]*)\}/m)
|
|
54
|
+
return {} unless match
|
|
55
|
+
|
|
56
|
+
match[1].scan(/(\w+)\s*:\s*(\w+)/).to_h
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def extract_actions(content)
|
|
60
|
+
content.scan(/^\s*(\w+)\s*\(/).flatten
|
|
61
|
+
.reject { |m| %w[constructor connect disconnect initialize].include?(m) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|