rails-ai-context 5.1.0 → 5.2.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/CHANGELOG.md +38 -0
- data/CONTRIBUTING.md +13 -0
- data/README.md +13 -11
- data/docs/GUIDE.md +109 -97
- data/lib/generators/rails_ai_context/install/install_generator.rb +60 -4
- data/lib/rails_ai_context/ast_cache.rb +79 -0
- data/lib/rails_ai_context/confidence.rb +45 -0
- data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +27 -0
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +106 -0
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +69 -0
- data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +100 -0
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +124 -0
- data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +134 -0
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +40 -0
- data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +87 -0
- data/lib/rails_ai_context/introspectors/model_introspector.rb +246 -339
- data/lib/rails_ai_context/introspectors/source_introspector.rb +72 -0
- data/lib/rails_ai_context/tools/base_tool.rb +1 -0
- data/lib/rails_ai_context/tools/validate.rb +9 -37
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +3 -3
- metadata +44 -7
- data/CLAUDE.md +0 -85
- data/docs/token-comparison.jpeg +0 -0
- /data/{demo-trace.gif → demo/demo-trace.gif} +0 -0
- /data/{demo-trace.tape → demo/demo-trace.tape} +0 -0
- /data/{demo.gif → demo/demo.gif} +0 -0
- /data/{demo.tape → demo/demo.tape} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1dd01df89a380a86e14436ae9275c5db3f88b0e4fc56ea6df7d308f2c468472
|
|
4
|
+
data.tar.gz: 6f2efe8319fa533b8ea19cc0ace32460b24cb9b96873af0956c4b126041cd14b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1447735cb99e717f1fa43a1fa54cc89fe1d6e0a836fcf156a7a66cede53c321328fbe4088a0c72103d91fe9c49de88f537cb59cddcbe1782773416892f76c728
|
|
7
|
+
data.tar.gz: 57b9e93748454c414563d453661eb4b51f94a063f15ac12a7073d6695b3cbc1e7fb1db323be3972ac1f830c1b21d73fcc65dc8fc1d5f170c8787cd9c8031881d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,44 @@ 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
|
+
## [5.2.0] — 2026-04-07
|
|
9
|
+
|
|
10
|
+
### Added — Phase 1: Prism AST Foundation (Ground Truth Engine Blueprint #36)
|
|
11
|
+
|
|
12
|
+
System-wide AST migration replacing all regex-based Ruby source parsing with Prism AST visitors. This is the foundation layer for the Ground Truth Engine transformation (#37).
|
|
13
|
+
|
|
14
|
+
- **AstCache** (`lib/rails_ai_context/ast_cache.rb`) — Thread-safe Prism parse cache backed by `Concurrent::Map`. Keyed by path + SHA256 content hash + mtime. Invalidates automatically on file change. Shared by all AST-based introspectors.
|
|
15
|
+
|
|
16
|
+
- **VERIFIED/INFERRED confidence contract** — `Confidence.for_node(node)` determines whether an AST node's arguments are all static literals (`[VERIFIED]`) or contain dynamic expressions (`[INFERRED]`). Called from listeners via `BaseListener#confidence_for(node)`. Every source-level introspection result now carries a confidence tag.
|
|
17
|
+
|
|
18
|
+
- **7 Prism Listener classes** (`lib/rails_ai_context/introspectors/listeners/`):
|
|
19
|
+
- `AssociationsListener` — `belongs_to`, `has_many`, `has_one`, `has_and_belongs_to_many`
|
|
20
|
+
- `ValidationsListener` — `validates`, `validates_*_of`, custom `validate :method`
|
|
21
|
+
- `ScopesListener` — `scope :name, -> { ... }`
|
|
22
|
+
- `EnumsListener` — Rails 7+ and legacy enum syntax with prefix/suffix options
|
|
23
|
+
- `CallbacksListener` — all AR callback types including `after_commit` with `on:` resolution
|
|
24
|
+
- `MacrosListener` — `encrypts`, `normalizes`, `delegate`, `has_secure_password`, `serialize`, `store`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `generates_token_for`, `attribute` API
|
|
25
|
+
- `MethodsListener` — `def`/`def self.` with visibility tracking, parameter extraction, `class << self` support
|
|
26
|
+
|
|
27
|
+
- **SourceIntrospector** (`lib/rails_ai_context/introspectors/source_introspector.rb`) — Single-pass Prism Dispatcher that walks the AST once and feeds events to all 7 listeners simultaneously. Available as `SourceIntrospector.call(path)` for file-based introspection or `SourceIntrospector.from_source(string)` for in-memory parsing.
|
|
28
|
+
|
|
29
|
+
- **73 new specs** covering AstCache, SourceIntrospector integration, and all 7 listener classes with edge cases (multi-line associations, legacy enums, visibility tracking, parameter extraction).
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **ModelIntrospector** rewritten to use AST-based source parsing via `SourceIntrospector` instead of regex. Reflection-based extraction (associations via AR, validations via AR, enums via AR) preserved where it provides runtime accuracy. All `source.scan(...)`, `source.each_line`, and `line.match?(...)` patterns in model introspection eliminated.
|
|
34
|
+
|
|
35
|
+
- **Install generator** now wraps `config/initializers/rails_ai_context.rb` in `if defined?(RailsAiContext)` so apps with the gem in `group :development` only don't crash in test/production. Re-install upgrades existing unguarded initializers and preserves indentation. All README and GUIDE initializer examples updated to the guarded form (#35).
|
|
36
|
+
|
|
37
|
+
### Dependencies
|
|
38
|
+
|
|
39
|
+
- Added `prism >= 0.28` (stdlib in Ruby 3.3+, gem for 3.2)
|
|
40
|
+
- Added `concurrent-ruby >= 1.2` (thread-safe AST cache; already transitive via Rails)
|
|
41
|
+
|
|
42
|
+
### Why
|
|
43
|
+
|
|
44
|
+
Regex-based Ruby source parsing was the #3 critical finding in the architecture audit: it breaks on heredocs, multi-line DSL calls, `class << self` blocks, and metaprogrammed constructs. Prism AST provides 100% syntax-level accuracy. The single-pass Dispatcher pattern means parsing a 500-line model file runs all 7 listeners in one tree walk — no repeated I/O or re-parsing. The confidence tagging gives AI agents explicit signal about what data is ground truth vs. what requires runtime verification.
|
|
45
|
+
|
|
8
46
|
## [5.1.0] — 2026-04-06
|
|
9
47
|
|
|
10
48
|
### Fixed
|
data/CONTRIBUTING.md
CHANGED
|
@@ -44,6 +44,19 @@ lib/rails_ai_context/
|
|
|
44
44
|
4. Register in `Server::TOOLS`
|
|
45
45
|
5. Write specs in `spec/lib/rails_ai_context/tools/your_tool_spec.rb`
|
|
46
46
|
|
|
47
|
+
## Adding a Prism Listener
|
|
48
|
+
|
|
49
|
+
Listeners extract specific concerns (associations, validations, etc.) from the AST via `Prism::Dispatcher` events.
|
|
50
|
+
|
|
51
|
+
1. Create `lib/rails_ai_context/introspectors/listeners/your_listener.rb` inheriting from `BaseListener`
|
|
52
|
+
2. Implement `on_call_node_enter(node)` and/or `on_def_node_enter(node)` — only the events your concern needs
|
|
53
|
+
3. Use `confidence_for(node)` from `BaseListener` to tag results `[VERIFIED]` or `[INFERRED]`
|
|
54
|
+
4. Store results in `@results` (accessed via `#results`)
|
|
55
|
+
5. Register the key/class pair in `SourceIntrospector::LISTENER_MAP`
|
|
56
|
+
6. Write specs in `spec/lib/rails_ai_context/introspectors/listeners/your_listener_spec.rb`
|
|
57
|
+
|
|
58
|
+
See existing listeners in `lib/rails_ai_context/introspectors/listeners/` for reference patterns.
|
|
59
|
+
|
|
47
60
|
## Adding a CLI Tool Interface
|
|
48
61
|
|
|
49
62
|
The `ToolRunner` (`lib/rails_ai_context/cli/tool_runner.rb`) handles CLI execution of all MCP tools. It is tested in `spec/lib/rails_ai_context/cli/tool_runner_spec.rb`. If you add a new MCP tool, it is automatically available via CLI — no extra registration needed. Tool name resolution (`schema` → `get_schema` → `rails_get_schema`) works for all tools.
|
data/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
[](https://registry.modelcontextprotocol.io)
|
|
20
20
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
21
21
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
22
|
-
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
23
23
|
[](LICENSE)
|
|
24
24
|
|
|
25
25
|
</div>
|
|
@@ -62,11 +62,11 @@ rails-ai-context serve # start MCP server
|
|
|
62
62
|
|
|
63
63
|
<div align="center">
|
|
64
64
|
|
|
65
|
-

|
|
65
|
+

|
|
66
66
|
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
|
-
Now your AI doesn't guess — it **asks your app directly.** 38 tools that query your schema, models, routes, controllers, views, and conventions on demand.
|
|
69
|
+
Now your AI doesn't guess — it **asks your app directly.** 38 tools that query your schema, models, routes, controllers, views, and conventions on demand. Model introspection uses Prism AST parsing — every result carries a `[VERIFIED]` or `[INFERRED]` confidence tag so AI knows what's ground truth and what needs runtime checking.
|
|
70
70
|
|
|
71
71
|
<br>
|
|
72
72
|
|
|
@@ -74,7 +74,7 @@ Now your AI doesn't guess — it **asks your app directly.** 38 tools that query
|
|
|
74
74
|
|
|
75
75
|
<div align="center">
|
|
76
76
|
|
|
77
|
-

|
|
77
|
+

|
|
78
78
|
|
|
79
79
|
</div>
|
|
80
80
|
|
|
@@ -263,7 +263,7 @@ Every tool is **read-only** and returns data verified against your actual app
|
|
|
263
263
|
| Tool | What it does |
|
|
264
264
|
|:-----|:------------|
|
|
265
265
|
| `get_schema` | Columns with indexed/unique/encrypted/default hints |
|
|
266
|
-
| `get_model_details` |
|
|
266
|
+
| `get_model_details` | AST-parsed associations, validations, scopes, enums, macros — each result tagged `[VERIFIED]` or `[INFERRED]` |
|
|
267
267
|
| `get_callbacks` | Callbacks in Rails execution order with source |
|
|
268
268
|
| `get_concern` | Concern methods + source + which models include it |
|
|
269
269
|
|
|
@@ -375,7 +375,7 @@ Enabled by default. Disable with `config.anti_hallucination_rules = false` if yo
|
|
|
375
375
|
▼
|
|
376
376
|
┌─────────────────────────────────────────────────────────┐
|
|
377
377
|
│ rails-ai-context │
|
|
378
|
-
│
|
|
378
|
+
│ Prism AST parsing. Cached. Confidence-tagged results. │
|
|
379
379
|
└────────┬──────────────────┬──────────────┬──────────────┘
|
|
380
380
|
│ │ │
|
|
381
381
|
▼ ▼ ▼
|
|
@@ -427,10 +427,12 @@ Both paths ask which AI tools you use and whether you want MCP or CLI mode. `.mc
|
|
|
427
427
|
|
|
428
428
|
```ruby
|
|
429
429
|
# config/initializers/rails_ai_context.rb
|
|
430
|
-
RailsAiContext
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
430
|
+
if defined?(RailsAiContext)
|
|
431
|
+
RailsAiContext.configure do |config|
|
|
432
|
+
config.ai_tools = %i[claude cursor] # Which AI tools to generate for
|
|
433
|
+
config.tool_mode = :mcp # :mcp (default) or :cli
|
|
434
|
+
config.preset = :full # :full (31 introspectors) or :standard (17)
|
|
435
|
+
end
|
|
434
436
|
end
|
|
435
437
|
```
|
|
436
438
|
|
|
@@ -470,7 +472,7 @@ end
|
|
|
470
472
|
## About
|
|
471
473
|
|
|
472
474
|
Built by a Rails developer with 10+ years of production experience.<br>
|
|
473
|
-
|
|
475
|
+
1668 tests. 38 tools. 31 introspectors. Standalone or in-Gemfile.<br>
|
|
474
476
|
MIT licensed. [Contributions welcome.](CONTRIBUTING.md)
|
|
475
477
|
|
|
476
478
|
<br>
|
data/docs/GUIDE.md
CHANGED
|
@@ -121,8 +121,10 @@ CONTEXT_MODE=full rails ai:context:copilot
|
|
|
121
121
|
|
|
122
122
|
```ruby
|
|
123
123
|
# config/initializers/rails_ai_context.rb
|
|
124
|
-
RailsAiContext
|
|
125
|
-
|
|
124
|
+
if defined?(RailsAiContext)
|
|
125
|
+
RailsAiContext.configure do |config|
|
|
126
|
+
config.context_mode = :full # or :compact (default)
|
|
127
|
+
end
|
|
126
128
|
end
|
|
127
129
|
```
|
|
128
130
|
|
|
@@ -306,10 +308,12 @@ Short names are resolved automatically:
|
|
|
306
308
|
The `tool_mode` config controls how tool references appear in generated context files:
|
|
307
309
|
|
|
308
310
|
```ruby
|
|
309
|
-
RailsAiContext
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
if defined?(RailsAiContext)
|
|
312
|
+
RailsAiContext.configure do |config|
|
|
313
|
+
# :mcp (default) — MCP primary, CLI as fallback
|
|
314
|
+
# :cli — CLI only, no MCP server needed
|
|
315
|
+
config.tool_mode = :mcp
|
|
316
|
+
end
|
|
313
317
|
end
|
|
314
318
|
```
|
|
315
319
|
|
|
@@ -359,7 +363,7 @@ rails_get_schema(detail: "full", format: "json")
|
|
|
359
363
|
|
|
360
364
|
### rails_get_model_details
|
|
361
365
|
|
|
362
|
-
Returns model details: associations, validations, scopes, enums, callbacks, concerns.
|
|
366
|
+
Returns model details: associations, validations, scopes, enums, callbacks, concerns. Source parsing uses Prism AST — every result carries a `[VERIFIED]` (static literal arguments) or `[INFERRED]` (dynamic expressions) confidence tag.
|
|
363
367
|
|
|
364
368
|
**Parameters:**
|
|
365
369
|
|
|
@@ -1110,11 +1114,13 @@ rails ai:serve_http
|
|
|
1110
1114
|
Or auto-mount inside your Rails app (no separate process):
|
|
1111
1115
|
|
|
1112
1116
|
```ruby
|
|
1113
|
-
RailsAiContext
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1117
|
+
if defined?(RailsAiContext)
|
|
1118
|
+
RailsAiContext.configure do |config|
|
|
1119
|
+
config.auto_mount = true
|
|
1120
|
+
config.http_path = "/mcp" # default
|
|
1121
|
+
config.http_port = 6029 # default
|
|
1122
|
+
config.http_bind = "127.0.0.1" # default (localhost only)
|
|
1123
|
+
end
|
|
1118
1124
|
end
|
|
1119
1125
|
```
|
|
1120
1126
|
|
|
@@ -1126,116 +1132,118 @@ Both transports are **read-only** — they expose the same 38 tools and never mo
|
|
|
1126
1132
|
|
|
1127
1133
|
```ruby
|
|
1128
1134
|
# config/initializers/rails_ai_context.rb
|
|
1129
|
-
RailsAiContext
|
|
1130
|
-
|
|
1135
|
+
if defined?(RailsAiContext)
|
|
1136
|
+
RailsAiContext.configure do |config|
|
|
1137
|
+
# --- Introspectors ---
|
|
1131
1138
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1139
|
+
# Presets: :full (31 introspectors, default) or :standard (17)
|
|
1140
|
+
config.preset = :full
|
|
1134
1141
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1142
|
+
# Cherry-pick on top of a preset
|
|
1143
|
+
config.introspectors += %i[views turbo auth api]
|
|
1137
1144
|
|
|
1138
|
-
|
|
1145
|
+
# --- Context files ---
|
|
1139
1146
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1147
|
+
# Context mode: :compact (default) or :full
|
|
1148
|
+
config.context_mode = :compact
|
|
1142
1149
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1150
|
+
# Max lines for CLAUDE.md in compact mode
|
|
1151
|
+
config.claude_max_lines = 150
|
|
1145
1152
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1153
|
+
# Output directory for context files (default: Rails.root)
|
|
1154
|
+
# config.output_dir = "/custom/path"
|
|
1148
1155
|
|
|
1149
|
-
|
|
1156
|
+
# --- MCP tools ---
|
|
1150
1157
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1158
|
+
# Tool mode: :mcp (MCP primary + CLI fallback) or :cli (CLI only)
|
|
1159
|
+
config.tool_mode = :mcp
|
|
1153
1160
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1161
|
+
# Max response size for tool results (safety net)
|
|
1162
|
+
config.max_tool_response_chars = 200_000
|
|
1156
1163
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1164
|
+
# Cache TTL for introspection results (seconds)
|
|
1165
|
+
config.cache_ttl = 60
|
|
1159
1166
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1167
|
+
# Additional MCP tool classes to register alongside built-in tools
|
|
1168
|
+
# config.custom_tools = [MyApp::Tools::CustomTool]
|
|
1162
1169
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1170
|
+
# Exclude specific built-in tools (e.g. if you don't use Brakeman)
|
|
1171
|
+
# config.skip_tools = %w[rails_security_scan]
|
|
1165
1172
|
|
|
1166
|
-
|
|
1173
|
+
# --- Exclusions ---
|
|
1167
1174
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1175
|
+
# Models to skip during introspection
|
|
1176
|
+
config.excluded_models += %w[AdminUser InternalAuditLog]
|
|
1170
1177
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1178
|
+
# Paths to exclude from code search
|
|
1179
|
+
config.excluded_paths += %w[vendor/bundle]
|
|
1173
1180
|
|
|
1174
|
-
|
|
1175
|
-
|
|
1181
|
+
# Sensitive file patterns blocked from search and read tools
|
|
1182
|
+
# config.sensitive_patterns += %w[config/my_secret.yml]
|
|
1176
1183
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1184
|
+
# Controllers hidden from listings (e.g. Devise internals)
|
|
1185
|
+
# config.excluded_controllers += %w[MyInternalController]
|
|
1179
1186
|
|
|
1180
|
-
|
|
1181
|
-
|
|
1187
|
+
# Route prefixes hidden with app_only (e.g. admin frameworks)
|
|
1188
|
+
# config.excluded_route_prefixes += %w[admin/]
|
|
1182
1189
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1190
|
+
# Regex patterns for concerns to hide from model output
|
|
1191
|
+
# config.excluded_concerns += [/MyInternal::/]
|
|
1185
1192
|
|
|
1186
|
-
|
|
1187
|
-
|
|
1193
|
+
# Framework filter names hidden from controller output
|
|
1194
|
+
# config.excluded_filters += %w[my_internal_filter]
|
|
1188
1195
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1196
|
+
# Default middleware hidden from config output
|
|
1197
|
+
# config.excluded_middleware += %w[MyMiddleware]
|
|
1191
1198
|
|
|
1192
|
-
|
|
1199
|
+
# --- File size limits ---
|
|
1193
1200
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1201
|
+
# Per-file read limit for tools (default: 5MB)
|
|
1202
|
+
# config.max_file_size = 5_000_000
|
|
1196
1203
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1204
|
+
# Test file read limit (default: 1MB)
|
|
1205
|
+
# config.max_test_file_size = 1_000_000
|
|
1199
1206
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1207
|
+
# schema.rb / structure.sql parse limit (default: 10MB)
|
|
1208
|
+
# config.max_schema_file_size = 10_000_000
|
|
1202
1209
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1210
|
+
# Total aggregated view content for UI patterns (default: 10MB)
|
|
1211
|
+
# config.max_view_total_size = 10_000_000
|
|
1205
1212
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1213
|
+
# Per-view file during aggregation (default: 1MB)
|
|
1214
|
+
# config.max_view_file_size = 1_000_000
|
|
1208
1215
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1216
|
+
# Max search results per call (default: 200)
|
|
1217
|
+
# config.max_search_results = 200
|
|
1211
1218
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1219
|
+
# Max files per validate call (default: 50)
|
|
1220
|
+
# config.max_validate_files = 50
|
|
1214
1221
|
|
|
1215
|
-
|
|
1222
|
+
# --- Search and file discovery ---
|
|
1216
1223
|
|
|
1217
|
-
|
|
1218
|
-
|
|
1224
|
+
# File extensions for Ruby fallback search
|
|
1225
|
+
# config.search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
|
|
1219
1226
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1227
|
+
# Where to look for concern source files
|
|
1228
|
+
# config.concern_paths = %w[app/models/concerns app/controllers/concerns]
|
|
1222
1229
|
|
|
1223
|
-
|
|
1230
|
+
# --- Live reload ---
|
|
1224
1231
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1232
|
+
# Auto-invalidate MCP tool caches on file changes
|
|
1233
|
+
# :auto — enable if `listen` gem is available (default)
|
|
1234
|
+
# true — enable, raise if `listen` is missing
|
|
1235
|
+
# false — disable entirely
|
|
1236
|
+
config.live_reload = :auto
|
|
1237
|
+
config.live_reload_debounce = 1.5 # seconds
|
|
1231
1238
|
|
|
1232
|
-
|
|
1239
|
+
# --- HTTP MCP endpoint ---
|
|
1233
1240
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1241
|
+
# Auto-mount Rack middleware for HTTP MCP
|
|
1242
|
+
config.auto_mount = false
|
|
1243
|
+
config.http_path = "/mcp"
|
|
1244
|
+
config.http_bind = "127.0.0.1"
|
|
1245
|
+
config.http_port = 6029
|
|
1246
|
+
end
|
|
1239
1247
|
end
|
|
1240
1248
|
```
|
|
1241
1249
|
|
|
@@ -1291,8 +1299,10 @@ By default, `rails ai:context` generates root files (CLAUDE.md, AGENTS.md, etc.)
|
|
|
1291
1299
|
**Skip root files:** If you prefer to maintain root files yourself and only want split rules (`.claude/rules/`, `.cursor/rules/`, `.github/instructions/`):
|
|
1292
1300
|
|
|
1293
1301
|
```ruby
|
|
1294
|
-
RailsAiContext
|
|
1295
|
-
|
|
1302
|
+
if defined?(RailsAiContext)
|
|
1303
|
+
RailsAiContext.configure do |config|
|
|
1304
|
+
config.generate_root_files = false
|
|
1305
|
+
end
|
|
1296
1306
|
end
|
|
1297
1307
|
```
|
|
1298
1308
|
|
|
@@ -1309,7 +1319,7 @@ Core Rails structure only. Use `config.preset = :standard` for a lighter footpri
|
|
|
1309
1319
|
| Introspector | What it discovers |
|
|
1310
1320
|
|-------------|-------------------|
|
|
1311
1321
|
| `schema` | Tables, columns, types, indexes, foreign keys, primary keys. Falls back to `db/schema.rb` parsing when no DB connected. |
|
|
1312
|
-
| `models` | Associations, validations, scopes, enums, callbacks, concerns, instance methods, class methods. Source-level macros: `has_secure_password`, `encrypts`, `normalizes`, `delegate`, `serialize`, `store`, `generates_token_for`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `broadcasts_to`. |
|
|
1322
|
+
| `models` | Associations, validations, scopes, enums, callbacks, concerns, instance methods, class methods. Source-level macros via Prism AST (single-pass, 7 listeners): `has_secure_password`, `encrypts`, `normalizes`, `delegate`, `serialize`, `store`, `generates_token_for`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `broadcasts_to`. Every result tagged `[VERIFIED]` or `[INFERRED]`. |
|
|
1313
1323
|
| `routes` | All routes with HTTP verbs, paths, controller actions, route names, API namespaces, mounted engines. |
|
|
1314
1324
|
| `jobs` | ActiveJob classes with queue names. Mailers with action methods. Action Cable channels. |
|
|
1315
1325
|
| `gems` | 70+ notable gems categorized: auth, background_jobs, admin, monitoring, search, pagination, forms, file_upload, testing, linting, security, api, frontend, utilities. |
|
|
@@ -1543,14 +1553,16 @@ Live reload is **enabled by default** when the `listen` gem is available. No con
|
|
|
1543
1553
|
### Configuration
|
|
1544
1554
|
|
|
1545
1555
|
```ruby
|
|
1546
|
-
RailsAiContext
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1556
|
+
if defined?(RailsAiContext)
|
|
1557
|
+
RailsAiContext.configure do |config|
|
|
1558
|
+
# :auto (default) — enable if `listen` gem is available, skip silently otherwise
|
|
1559
|
+
# true — enable, raise if `listen` gem is missing
|
|
1560
|
+
# false — disable entirely
|
|
1561
|
+
config.live_reload = :auto
|
|
1562
|
+
|
|
1563
|
+
# Debounce interval in seconds (default: 1.5)
|
|
1564
|
+
config.live_reload_debounce = 1.5
|
|
1565
|
+
end
|
|
1554
1566
|
end
|
|
1555
1567
|
```
|
|
1556
1568
|
|
|
@@ -310,6 +310,7 @@ module RailsAiContext
|
|
|
310
310
|
end
|
|
311
311
|
|
|
312
312
|
content += "end\n"
|
|
313
|
+
content, = ensure_initializer_guard(content)
|
|
313
314
|
|
|
314
315
|
create_file path, content
|
|
315
316
|
say "Created #{path} with all #{CONFIG_SECTIONS.size} config sections", :green
|
|
@@ -332,13 +333,16 @@ module RailsAiContext
|
|
|
332
333
|
marker = "── #{name}"
|
|
333
334
|
next if existing.include?(marker)
|
|
334
335
|
|
|
335
|
-
insert_point = existing
|
|
336
|
+
insert_point = configure_block_end_index(existing)
|
|
336
337
|
if insert_point
|
|
337
|
-
existing = existing.insert(insert_point, "\n#{section_content}\n")
|
|
338
|
+
existing = existing.insert(insert_point, "\n#{reindent_section_content(section_content, existing)}\n")
|
|
338
339
|
changes << "section: #{name}"
|
|
339
340
|
end
|
|
340
341
|
end
|
|
341
342
|
|
|
343
|
+
existing, changed = ensure_initializer_guard(existing)
|
|
344
|
+
changes << "guard" if changed
|
|
345
|
+
|
|
342
346
|
if changes.any?
|
|
343
347
|
File.write(full_path, existing)
|
|
344
348
|
say "Updated #{full_path.relative_path_from(Rails.root)}: #{changes.join(', ')}", :green
|
|
@@ -350,9 +354,11 @@ module RailsAiContext
|
|
|
350
354
|
# Replace or uncomment a config line. Returns [new_content, changed?]
|
|
351
355
|
def update_config_line(content, key, new_line)
|
|
352
356
|
# Match both commented and uncommented versions of this config key
|
|
353
|
-
pattern = /^[ \t]
|
|
357
|
+
pattern = /^([ \t]*)#?\s*#{Regexp.escape(key)}\s*=.*$/
|
|
354
358
|
if content.match?(pattern)
|
|
355
|
-
updated = content.sub(pattern
|
|
359
|
+
updated = content.sub(pattern) do
|
|
360
|
+
"#{Regexp.last_match(1)}#{new_line.lstrip}"
|
|
361
|
+
end
|
|
356
362
|
[ updated, updated != content ]
|
|
357
363
|
else
|
|
358
364
|
# Key not found at all — don't add (it's in a section that will be added)
|
|
@@ -360,6 +366,56 @@ module RailsAiContext
|
|
|
360
366
|
end
|
|
361
367
|
end
|
|
362
368
|
|
|
369
|
+
def configure_block_end_index(content)
|
|
370
|
+
end_positions = []
|
|
371
|
+
content.to_enum(:scan, /^[ \t]*end\b/).each do
|
|
372
|
+
end_positions << Regexp.last_match.begin(0)
|
|
373
|
+
end
|
|
374
|
+
return nil if end_positions.empty?
|
|
375
|
+
|
|
376
|
+
if guarded_initializer?(content) && end_positions.size >= 2
|
|
377
|
+
end_positions[-2]
|
|
378
|
+
else
|
|
379
|
+
end_positions[-1]
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def ensure_initializer_guard(content)
|
|
384
|
+
return [ content, false ] if guarded_initializer?(content)
|
|
385
|
+
|
|
386
|
+
header_match = content.match(/\A# frozen_string_literal: true\n(?:\n)?/)
|
|
387
|
+
header = header_match ? "# frozen_string_literal: true\n\n" : ""
|
|
388
|
+
body = header_match ? content.delete_prefix(header_match[0]) : content
|
|
389
|
+
body = "#{body}\n" unless body.end_with?("\n")
|
|
390
|
+
|
|
391
|
+
wrapped = "#{header}if defined?(RailsAiContext)\n#{indent_content(body)}end\n"
|
|
392
|
+
[ wrapped, wrapped != content ]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def reindent_section_content(section_content, content)
|
|
396
|
+
indent = configure_body_indent(content)
|
|
397
|
+
section_content.lines.map do |line|
|
|
398
|
+
next line if line == "\n"
|
|
399
|
+
|
|
400
|
+
"#{indent}#{line.sub(/\A[ \t]{0,2}/, "")}"
|
|
401
|
+
end.join
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def configure_body_indent(content)
|
|
405
|
+
match = content.match(/^([ \t]*)RailsAiContext\.configure do \|config\|$/)
|
|
406
|
+
return " " unless match
|
|
407
|
+
|
|
408
|
+
"#{match[1]} "
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def guarded_initializer?(content)
|
|
412
|
+
content.match?(/^[ \t]*if defined\?\(RailsAiContext\)$/)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def indent_content(content)
|
|
416
|
+
content.lines.map { |line| line == "\n" ? line : " #{line}" }.join
|
|
417
|
+
end
|
|
418
|
+
|
|
363
419
|
def build_ai_tools_line
|
|
364
420
|
# Always write uncommented so re-install can detect previous selection
|
|
365
421
|
" config.ai_tools = %i[#{@selected_formats.join(' ')}]"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "concurrent"
|
|
5
|
+
require "prism"
|
|
6
|
+
|
|
7
|
+
module RailsAiContext
|
|
8
|
+
# Thread-safe AST parse cache backed by Concurrent::Map.
|
|
9
|
+
# Keyed by path + content hash + mtime — automatically invalidates
|
|
10
|
+
# when file content changes. Used by all Prism-based introspectors.
|
|
11
|
+
#
|
|
12
|
+
# Bounded: evicts oldest entries when MAX_SIZE is exceeded.
|
|
13
|
+
module AstCache
|
|
14
|
+
STORE = Concurrent::Map.new
|
|
15
|
+
MAX_SIZE = 500
|
|
16
|
+
EVICTION_MUTEX = Mutex.new
|
|
17
|
+
|
|
18
|
+
# Max file size for parsing (default: matches config.max_file_size).
|
|
19
|
+
# Public API — callers don't need to pre-check size.
|
|
20
|
+
MAX_PARSE_SIZE = 5_000_000
|
|
21
|
+
|
|
22
|
+
# Parse a Ruby source file and cache the result.
|
|
23
|
+
# Returns a Prism::ParseResult. Rejects files exceeding MAX_PARSE_SIZE.
|
|
24
|
+
#
|
|
25
|
+
# Reads content first, then checks size — avoids TOCTOU race where the
|
|
26
|
+
# file could change between File.size and File.read.
|
|
27
|
+
def self.parse(path)
|
|
28
|
+
content = File.read(path)
|
|
29
|
+
size = content.bytesize
|
|
30
|
+
raise ArgumentError, "File too large for AST parsing: #{path} (#{size} bytes, max #{MAX_PARSE_SIZE})" if size > MAX_PARSE_SIZE
|
|
31
|
+
|
|
32
|
+
mtime = File.mtime(path).to_i
|
|
33
|
+
key = "#{path}:#{Digest::SHA256.hexdigest(content)}:#{mtime}"
|
|
34
|
+
|
|
35
|
+
cached = STORE[key]
|
|
36
|
+
return cached if cached
|
|
37
|
+
|
|
38
|
+
# Evict BEFORE inserting to avoid running inside compute_if_absent
|
|
39
|
+
evict_if_full
|
|
40
|
+
|
|
41
|
+
STORE.compute_if_absent(key) { Prism.parse(content) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Parse a Ruby source string (no caching).
|
|
45
|
+
# Returns a Prism::ParseResult.
|
|
46
|
+
def self.parse_string(source)
|
|
47
|
+
Prism.parse(source)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Invalidate all cached entries for a given path.
|
|
51
|
+
def self.invalidate(path)
|
|
52
|
+
prefix = "#{path}:"
|
|
53
|
+
STORE.each_key do |k|
|
|
54
|
+
STORE.delete(k) if k.start_with?(prefix)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Clear the entire cache.
|
|
59
|
+
def self.clear
|
|
60
|
+
STORE.clear
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Number of cached entries (for diagnostics).
|
|
64
|
+
def self.size
|
|
65
|
+
STORE.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Evict ~25% of entries when cache exceeds MAX_SIZE.
|
|
69
|
+
# Synchronized to prevent multiple threads from over-evicting simultaneously.
|
|
70
|
+
def self.evict_if_full
|
|
71
|
+
EVICTION_MUTEX.synchronize do
|
|
72
|
+
return if STORE.size < MAX_SIZE
|
|
73
|
+
keys = STORE.keys
|
|
74
|
+
keys.first(keys.size / 4).each { |k| STORE.delete(k) }
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
private_class_method :evict_if_full
|
|
78
|
+
end
|
|
79
|
+
end
|