rails-ai-context 5.1.0 → 5.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -0
  3. data/CONTRIBUTING.md +13 -0
  4. data/README.md +13 -11
  5. data/docs/GUIDE.md +114 -100
  6. data/lib/generators/rails_ai_context/install/install_generator.rb +60 -4
  7. data/lib/rails_ai_context/ast_cache.rb +79 -0
  8. data/lib/rails_ai_context/confidence.rb +45 -0
  9. data/lib/rails_ai_context/configuration.rb +7 -0
  10. data/lib/rails_ai_context/hydration_result.rb +14 -0
  11. data/lib/rails_ai_context/hydrators/controller_hydrator.rb +51 -0
  12. data/lib/rails_ai_context/hydrators/hydration_formatter.rb +56 -0
  13. data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +73 -0
  14. data/lib/rails_ai_context/hydrators/view_hydrator.rb +56 -0
  15. data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +27 -0
  16. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +106 -0
  17. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +69 -0
  18. data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +100 -0
  19. data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +124 -0
  20. data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +134 -0
  21. data/lib/rails_ai_context/introspectors/listeners/model_reference_listener.rb +106 -0
  22. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +40 -0
  23. data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +87 -0
  24. data/lib/rails_ai_context/introspectors/model_introspector.rb +246 -339
  25. data/lib/rails_ai_context/introspectors/source_introspector.rb +72 -0
  26. data/lib/rails_ai_context/schema_hint.rb +28 -0
  27. data/lib/rails_ai_context/tools/base_tool.rb +1 -0
  28. data/lib/rails_ai_context/tools/get_context.rb +11 -0
  29. data/lib/rails_ai_context/tools/get_controllers.rb +15 -0
  30. data/lib/rails_ai_context/tools/get_view.rb +14 -0
  31. data/lib/rails_ai_context/tools/validate.rb +9 -37
  32. data/lib/rails_ai_context/version.rb +1 -1
  33. data/server.json +3 -3
  34. metadata +51 -7
  35. data/CLAUDE.md +0 -85
  36. data/docs/token-comparison.jpeg +0 -0
  37. /data/{demo-trace.gif → demo/demo-trace.gif} +0 -0
  38. /data/{demo-trace.tape → demo/demo-trace.tape} +0 -0
  39. /data/{demo.gif → demo/demo.gif} +0 -0
  40. /data/{demo.tape → demo/demo.tape} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1edfff7102dd3dd9e07d8686bb62c6017ff79492a46c36645a453b7b39a4983c
4
- data.tar.gz: 2e9bb943d9986b169de0c133cb4458bb9a8678a5b32608ebf92ba80b3637c406
3
+ metadata.gz: a9d802d024a373d0552ee2b70cf3d5d755fd79f951b8335da948563a7f488af4
4
+ data.tar.gz: fabf3246fb0df9a97294bca58dac86d4bccc7ffd881fb1da69dc262d03e43fba
5
5
  SHA512:
6
- metadata.gz: ff8184662fc4c3b181d199dc0a3085935bc545cef488361b4b4596a2941630a424cff14ba175e22db244234d6a448cb6d3352863a1ff9ce3a07c28b89d2279c0
7
- data.tar.gz: 79ec7f4d524a581b5dff5f67c58c1271743fe708131e5223fc91b23799e0d803561b7b713aba080662fe8f79f6e0601d0ce233a2763ab6bee1f9f50eb5ca3898
6
+ metadata.gz: 40fbc9d7455f857de366de800f91ddfa19b9b752cfced2905c2bedc0576a3d54ee4a4c89eccb701904b0eb6cb8babd106cb699182a5eab98981a02d60ff14d6f
7
+ data.tar.gz: e640c259017a222d43a32d15a8c7e70f9888eb3a8df3ad172159efa17371003196ade1f2bf561683a319fa4cebc6b16262f72aee56dea636d2be5fa0a5c7dba7
data/CHANGELOG.md CHANGED
@@ -5,6 +5,73 @@ 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.3.0] — 2026-04-07
9
+
10
+ ### Added — Phase 2: Cross-Tool Semantic Hydration (Ground Truth Engine Blueprint #38)
11
+
12
+ Controller and view tools now automatically inject schema hints for referenced models, eliminating the need for follow-up tool calls.
13
+
14
+ - **SchemaHint** (`lib/rails_ai_context/schema_hint.rb`) — Immutable `Data.define` value object carrying model ground truth: table, columns, associations, validations, primary key, and `[VERIFIED]`/`[INFERRED]` confidence tag.
15
+
16
+ - **HydrationResult** — Wraps hints + warnings for downstream formatting.
17
+
18
+ - **SchemaHintBuilder** (`lib/rails_ai_context/hydrators/schema_hint_builder.rb`) — Resolves model names to `SchemaHint` objects from cached introspection context. Case-insensitive lookup, batch builder with configurable cap.
19
+
20
+ - **HydrationFormatter** (`lib/rails_ai_context/hydrators/hydration_formatter.rb`) — Renders `SchemaHint` objects as compact Markdown `## Schema Hints` sections with columns (capped at 10), associations, and validations.
21
+
22
+ - **ControllerHydrator** (`lib/rails_ai_context/hydrators/controller_hydrator.rb`) — Parses controller source via Prism AST to detect model references (constant receivers, `params.require` keys, ivar writes), then builds schema hints.
23
+
24
+ - **ViewHydrator** (`lib/rails_ai_context/hydrators/view_hydrator.rb`) — Maps instance variable names to models by convention (`@post` → `Post`, `@posts` → `Post`). Filters framework ivars (page, query, flash, etc.).
25
+
26
+ - **ModelReferenceListener** (`lib/rails_ai_context/introspectors/listeners/model_reference_listener.rb`) — Prism Dispatcher listener for controller-specific model detection. Not registered in `LISTENER_MAP` — used standalone by `ControllerHydrator`.
27
+
28
+ - **Tool integrations:**
29
+ - `GetControllers` — schema hints injected into both action source and controller overview
30
+ - `GetContext` — hydrates combined controller+view ivars in action context mode
31
+ - `GetView` — hydrates instance variables from view templates in standard detail
32
+
33
+ - **Configuration:** `hydration_enabled` (default: true), `hydration_max_hints` (default: 5). Both YAML-configurable.
34
+
35
+ - **65 new specs** covering SchemaHint, HydrationResult, SchemaHintBuilder, HydrationFormatter, ModelReferenceListener, ControllerHydrator, ViewHydrator, tool-level hydration integration (GetControllers, GetView), and configuration (defaults, YAML loading, max_hints propagation).
36
+
37
+ ## [5.2.0] — 2026-04-07
38
+
39
+ ### Added — Phase 1: Prism AST Foundation (Ground Truth Engine Blueprint #36)
40
+
41
+ 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).
42
+
43
+ - **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.
44
+
45
+ - **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.
46
+
47
+ - **7 Prism Listener classes** (`lib/rails_ai_context/introspectors/listeners/`):
48
+ - `AssociationsListener` — `belongs_to`, `has_many`, `has_one`, `has_and_belongs_to_many`
49
+ - `ValidationsListener` — `validates`, `validates_*_of`, custom `validate :method`
50
+ - `ScopesListener` — `scope :name, -> { ... }`
51
+ - `EnumsListener` — Rails 7+ and legacy enum syntax with prefix/suffix options
52
+ - `CallbacksListener` — all AR callback types including `after_commit` with `on:` resolution
53
+ - `MacrosListener` — `encrypts`, `normalizes`, `delegate`, `has_secure_password`, `serialize`, `store`, `has_one_attached`, `has_many_attached`, `has_rich_text`, `generates_token_for`, `attribute` API
54
+ - `MethodsListener` — `def`/`def self.` with visibility tracking, parameter extraction, `class << self` support
55
+
56
+ - **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.
57
+
58
+ - **73 new specs** covering AstCache, SourceIntrospector integration, and all 7 listener classes with edge cases (multi-line associations, legacy enums, visibility tracking, parameter extraction).
59
+
60
+ ### Changed
61
+
62
+ - **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.
63
+
64
+ - **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).
65
+
66
+ ### Dependencies
67
+
68
+ - Added `prism >= 0.28` (stdlib in Ruby 3.3+, gem for 3.2)
69
+ - Added `concurrent-ruby >= 1.2` (thread-safe AST cache; already transitive via Rails)
70
+
71
+ ### Why
72
+
73
+ 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.
74
+
8
75
  ## [5.1.0] — 2026-04-06
9
76
 
10
77
  ### 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
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-green)](https://registry.modelcontextprotocol.io)
20
20
  [![Ruby](https://img.shields.io/badge/Ruby-3.2%20%7C%203.3%20%7C%203.4-CC342D)](https://github.com/crisnahine/rails-ai-context)
21
21
  [![Rails](https://img.shields.io/badge/Rails-7.1%20%7C%207.2%20%7C%208.0-CC0000)](https://github.com/crisnahine/rails-ai-context)
22
- [![Tests](https://img.shields.io/badge/Tests-1565%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
22
+ [![Tests](https://img.shields.io/badge/Tests-1731%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
23
23
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- ![Install demo](demo.gif)
65
+ ![Install demo](demo/demo.gif)
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. It gets the right answer the first time.
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
- ![Trace demo](demo-trace.gif)
77
+ ![Trace demo](demo/demo-trace.gif)
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` | Associations, validations, scopes, enums, macros, delegations |
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
- Parses everything. Caches results. Sensible defaults. │
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.configure do |config|
431
- config.ai_tools = %i[claude cursor] # Which AI tools to generate for
432
- config.tool_mode = :mcp # :mcp (default) or :cli
433
- config.preset = :full # :full (31 introspectors) or :standard (17)
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
- 1565 tests. 38 tools. 31 introspectors. Standalone or in-Gemfile.<br>
475
+ 1731 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.configure do |config|
125
- config.context_mode = :full # or :compact (default)
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.configure do |config|
310
- # :mcp (default) — MCP primary, CLI as fallback
311
- # :cliCLI only, no MCP server needed
312
- config.tool_mode = :mcp
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
 
@@ -427,7 +431,7 @@ rails_get_routes(detail: "standard", limit: 20, offset: 100)
427
431
 
428
432
  ### rails_get_controllers
429
433
 
430
- Returns controller details: actions, filters, strong params, concerns.
434
+ Returns controller details: actions, filters, strong params, concerns. Automatically includes **Schema Hints** for models referenced in the controller (via Prism AST detection).
431
435
 
432
436
  **Parameters:**
433
437
 
@@ -545,7 +549,7 @@ rails_get_stimulus(detail: "full")
545
549
 
546
550
  ### rails_get_view
547
551
 
548
- Returns view template contents, partials, and Stimulus controller references.
552
+ Returns view template contents, partials, and Stimulus controller references. In standard detail, includes **Schema Hints** for models inferred from instance variables.
549
553
 
550
554
  **Parameters:**
551
555
 
@@ -942,7 +946,7 @@ rails_get_turbo_map(controller: "messages", detail: "full")
942
946
 
943
947
  ### rails_get_context
944
948
 
945
- Get cross-layer context in a single call — combines schema, model, controller, routes, views, stimulus, and tests. Use when you need full context for implementing a feature or modifying an action.
949
+ Get cross-layer context in a single call — combines schema, model, controller, routes, views, stimulus, and tests. Automatically includes **Schema Hints** for models referenced in controller/view code. Use when you need full context for implementing a feature or modifying an action.
946
950
 
947
951
  **Parameters:**
948
952
 
@@ -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.configure do |config|
1114
- config.auto_mount = true
1115
- config.http_path = "/mcp" # default
1116
- config.http_port = 6029 # default
1117
- config.http_bind = "127.0.0.1" # default (localhost only)
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.configure do |config|
1130
- # --- Introspectors ---
1135
+ if defined?(RailsAiContext)
1136
+ RailsAiContext.configure do |config|
1137
+ # --- Introspectors ---
1131
1138
 
1132
- # Presets: :full (31 introspectors, default) or :standard (17)
1133
- config.preset = :full
1139
+ # Presets: :full (31 introspectors, default) or :standard (17)
1140
+ config.preset = :full
1134
1141
 
1135
- # Cherry-pick on top of a preset
1136
- config.introspectors += %i[views turbo auth api]
1142
+ # Cherry-pick on top of a preset
1143
+ config.introspectors += %i[views turbo auth api]
1137
1144
 
1138
- # --- Context files ---
1145
+ # --- Context files ---
1139
1146
 
1140
- # Context mode: :compact (default) or :full
1141
- config.context_mode = :compact
1147
+ # Context mode: :compact (default) or :full
1148
+ config.context_mode = :compact
1142
1149
 
1143
- # Max lines for CLAUDE.md in compact mode
1144
- config.claude_max_lines = 150
1150
+ # Max lines for CLAUDE.md in compact mode
1151
+ config.claude_max_lines = 150
1145
1152
 
1146
- # Output directory for context files (default: Rails.root)
1147
- # config.output_dir = "/custom/path"
1153
+ # Output directory for context files (default: Rails.root)
1154
+ # config.output_dir = "/custom/path"
1148
1155
 
1149
- # --- MCP tools ---
1156
+ # --- MCP tools ---
1150
1157
 
1151
- # Tool mode: :mcp (MCP primary + CLI fallback) or :cli (CLI only)
1152
- config.tool_mode = :mcp
1158
+ # Tool mode: :mcp (MCP primary + CLI fallback) or :cli (CLI only)
1159
+ config.tool_mode = :mcp
1153
1160
 
1154
- # Max response size for tool results (safety net)
1155
- config.max_tool_response_chars = 200_000
1161
+ # Max response size for tool results (safety net)
1162
+ config.max_tool_response_chars = 200_000
1156
1163
 
1157
- # Cache TTL for introspection results (seconds)
1158
- config.cache_ttl = 60
1164
+ # Cache TTL for introspection results (seconds)
1165
+ config.cache_ttl = 60
1159
1166
 
1160
- # Additional MCP tool classes to register alongside built-in tools
1161
- # config.custom_tools = [MyApp::Tools::CustomTool]
1167
+ # Additional MCP tool classes to register alongside built-in tools
1168
+ # config.custom_tools = [MyApp::Tools::CustomTool]
1162
1169
 
1163
- # Exclude specific built-in tools (e.g. if you don't use Brakeman)
1164
- # config.skip_tools = %w[rails_security_scan]
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
- # --- Exclusions ---
1173
+ # --- Exclusions ---
1167
1174
 
1168
- # Models to skip during introspection
1169
- config.excluded_models += %w[AdminUser InternalAuditLog]
1175
+ # Models to skip during introspection
1176
+ config.excluded_models += %w[AdminUser InternalAuditLog]
1170
1177
 
1171
- # Paths to exclude from code search
1172
- config.excluded_paths += %w[vendor/bundle]
1178
+ # Paths to exclude from code search
1179
+ config.excluded_paths += %w[vendor/bundle]
1173
1180
 
1174
- # Sensitive file patterns blocked from search and read tools
1175
- # config.sensitive_patterns += %w[config/my_secret.yml]
1181
+ # Sensitive file patterns blocked from search and read tools
1182
+ # config.sensitive_patterns += %w[config/my_secret.yml]
1176
1183
 
1177
- # Controllers hidden from listings (e.g. Devise internals)
1178
- # config.excluded_controllers += %w[MyInternalController]
1184
+ # Controllers hidden from listings (e.g. Devise internals)
1185
+ # config.excluded_controllers += %w[MyInternalController]
1179
1186
 
1180
- # Route prefixes hidden with app_only (e.g. admin frameworks)
1181
- # config.excluded_route_prefixes += %w[admin/]
1187
+ # Route prefixes hidden with app_only (e.g. admin frameworks)
1188
+ # config.excluded_route_prefixes += %w[admin/]
1182
1189
 
1183
- # Regex patterns for concerns to hide from model output
1184
- # config.excluded_concerns += [/MyInternal::/]
1190
+ # Regex patterns for concerns to hide from model output
1191
+ # config.excluded_concerns += [/MyInternal::/]
1185
1192
 
1186
- # Framework filter names hidden from controller output
1187
- # config.excluded_filters += %w[my_internal_filter]
1193
+ # Framework filter names hidden from controller output
1194
+ # config.excluded_filters += %w[my_internal_filter]
1188
1195
 
1189
- # Default middleware hidden from config output
1190
- # config.excluded_middleware += %w[MyMiddleware]
1196
+ # Default middleware hidden from config output
1197
+ # config.excluded_middleware += %w[MyMiddleware]
1191
1198
 
1192
- # --- File size limits ---
1199
+ # --- File size limits ---
1193
1200
 
1194
- # Per-file read limit for tools (default: 5MB)
1195
- # config.max_file_size = 5_000_000
1201
+ # Per-file read limit for tools (default: 5MB)
1202
+ # config.max_file_size = 5_000_000
1196
1203
 
1197
- # Test file read limit (default: 1MB)
1198
- # config.max_test_file_size = 1_000_000
1204
+ # Test file read limit (default: 1MB)
1205
+ # config.max_test_file_size = 1_000_000
1199
1206
 
1200
- # schema.rb / structure.sql parse limit (default: 10MB)
1201
- # config.max_schema_file_size = 10_000_000
1207
+ # schema.rb / structure.sql parse limit (default: 10MB)
1208
+ # config.max_schema_file_size = 10_000_000
1202
1209
 
1203
- # Total aggregated view content for UI patterns (default: 10MB)
1204
- # config.max_view_total_size = 10_000_000
1210
+ # Total aggregated view content for UI patterns (default: 10MB)
1211
+ # config.max_view_total_size = 10_000_000
1205
1212
 
1206
- # Per-view file during aggregation (default: 1MB)
1207
- # config.max_view_file_size = 1_000_000
1213
+ # Per-view file during aggregation (default: 1MB)
1214
+ # config.max_view_file_size = 1_000_000
1208
1215
 
1209
- # Max search results per call (default: 200)
1210
- # config.max_search_results = 200
1216
+ # Max search results per call (default: 200)
1217
+ # config.max_search_results = 200
1211
1218
 
1212
- # Max files per validate call (default: 50)
1213
- # config.max_validate_files = 50
1219
+ # Max files per validate call (default: 50)
1220
+ # config.max_validate_files = 50
1214
1221
 
1215
- # --- Search and file discovery ---
1222
+ # --- Search and file discovery ---
1216
1223
 
1217
- # File extensions for Ruby fallback search
1218
- # config.search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
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
- # Where to look for concern source files
1221
- # config.concern_paths = %w[app/models/concerns app/controllers/concerns]
1227
+ # Where to look for concern source files
1228
+ # config.concern_paths = %w[app/models/concerns app/controllers/concerns]
1222
1229
 
1223
- # --- Live reload ---
1230
+ # --- Live reload ---
1224
1231
 
1225
- # Auto-invalidate MCP tool caches on file changes
1226
- # :auto — enable if `listen` gem is available (default)
1227
- # true — enable, raise if `listen` is missing
1228
- # false — disable entirely
1229
- config.live_reload = :auto
1230
- config.live_reload_debounce = 1.5 # seconds
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
- # --- HTTP MCP endpoint ---
1239
+ # --- HTTP MCP endpoint ---
1233
1240
 
1234
- # Auto-mount Rack middleware for HTTP MCP
1235
- config.auto_mount = false
1236
- config.http_path = "/mcp"
1237
- config.http_bind = "127.0.0.1"
1238
- config.http_port = 6029
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
 
@@ -1267,6 +1275,8 @@ end
1267
1275
  | `server_version` | String | gem version | MCP server version |
1268
1276
  | `generate_root_files` | Boolean | `true` | Generate root files (CLAUDE.md, etc.) — set `false` for split rules only |
1269
1277
  | `anti_hallucination_rules` | Boolean | `true` | Embed 6-rule Anti-Hallucination Protocol in generated context files — set `false` to skip |
1278
+ | `hydration_enabled` | Boolean | `true` | Inject schema hints into controller/view tool responses |
1279
+ | `hydration_max_hints` | Integer | `5` | Max schema hints per tool response |
1270
1280
  | `max_file_size` | Integer | `5_000_000` | Per-file read limit for tools (5MB) |
1271
1281
  | `max_test_file_size` | Integer | `1_000_000` | Test file read limit (1MB) |
1272
1282
  | `max_schema_file_size` | Integer | `10_000_000` | schema.rb / structure.sql parse limit (10MB) |
@@ -1291,8 +1301,10 @@ By default, `rails ai:context` generates root files (CLAUDE.md, AGENTS.md, etc.)
1291
1301
  **Skip root files:** If you prefer to maintain root files yourself and only want split rules (`.claude/rules/`, `.cursor/rules/`, `.github/instructions/`):
1292
1302
 
1293
1303
  ```ruby
1294
- RailsAiContext.configure do |config|
1295
- config.generate_root_files = false
1304
+ if defined?(RailsAiContext)
1305
+ RailsAiContext.configure do |config|
1306
+ config.generate_root_files = false
1307
+ end
1296
1308
  end
1297
1309
  ```
1298
1310
 
@@ -1309,7 +1321,7 @@ Core Rails structure only. Use `config.preset = :standard` for a lighter footpri
1309
1321
  | Introspector | What it discovers |
1310
1322
  |-------------|-------------------|
1311
1323
  | `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`. |
1324
+ | `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
1325
  | `routes` | All routes with HTTP verbs, paths, controller actions, route names, API namespaces, mounted engines. |
1314
1326
  | `jobs` | ActiveJob classes with queue names. Mailers with action methods. Action Cable channels. |
1315
1327
  | `gems` | 70+ notable gems categorized: auth, background_jobs, admin, monitoring, search, pagination, forms, file_upload, testing, linting, security, api, frontend, utilities. |
@@ -1543,14 +1555,16 @@ Live reload is **enabled by default** when the `listen` gem is available. No con
1543
1555
  ### Configuration
1544
1556
 
1545
1557
  ```ruby
1546
- RailsAiContext.configure do |config|
1547
- # :auto (default) — enable if `listen` gem is available, skip silently otherwise
1548
- # true — enable, raise if `listen` gem is missing
1549
- # false disable entirely
1550
- config.live_reload = :auto
1551
-
1552
- # Debounce interval in seconds (default: 1.5)
1553
- config.live_reload_debounce = 1.5
1558
+ if defined?(RailsAiContext)
1559
+ RailsAiContext.configure do |config|
1560
+ # :auto (default) — enable if `listen` gem is available, skip silently otherwise
1561
+ # true enable, raise if `listen` gem is missing
1562
+ # false — disable entirely
1563
+ config.live_reload = :auto
1564
+
1565
+ # Debounce interval in seconds (default: 1.5)
1566
+ config.live_reload_debounce = 1.5
1567
+ end
1554
1568
  end
1555
1569
  ```
1556
1570
 
@@ -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.rindex(/^end\b/)
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]*#?\s*#{Regexp.escape(key)}\s*=.*$/
357
+ pattern = /^([ \t]*)#?\s*#{Regexp.escape(key)}\s*=.*$/
354
358
  if content.match?(pattern)
355
- updated = content.sub(pattern, new_line)
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(' ')}]"