rails-ai-context 4.6.0 → 4.7.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 +8 -0
- data/CLAUDE.md +2 -1
- data/README.md +49 -22
- data/docs/GUIDE.md +7 -0
- data/lib/generators/rails_ai_context/install/install_generator.rb +5 -0
- data/lib/rails_ai_context/configuration.rb +7 -0
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +2 -22
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +4 -24
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +4 -25
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +2 -9
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +37 -0
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +85 -100
- data/lib/rails_ai_context/tools/base_tool.rb +54 -0
- data/lib/rails_ai_context/tools/dependency_graph.rb +1 -2
- data/lib/rails_ai_context/tools/generate_test.rb +1 -2
- data/lib/rails_ai_context/tools/get_callbacks.rb +3 -35
- data/lib/rails_ai_context/tools/get_concern.rb +4 -31
- data/lib/rails_ai_context/tools/get_context.rb +1 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +1 -2
- data/lib/rails_ai_context/tools/get_view.rb +2 -2
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +3 -3
- metadata +9 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c780366dc085c17be730aded4bbe8548b0e5e71c5c810caed96104a9379363fa
|
|
4
|
+
data.tar.gz: f5c46207b643df1a7dc1c5563c1d9e932f772ce5e27eb1839d9f14ddde116bd0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 934c4d703203801494a8addde164c71e56119e5701dd454574b0362d2a6d68523b6f0830654ff81eaa1d36fac7eca457374960e4e9fe54731f32431fe23ea8a4
|
|
7
|
+
data.tar.gz: ca2bf9e27db44fb35b8d9bd984748b7fce1108a013ef7ea6ec5a01757c48187b6ce6985c529ee8bf4df7cc9abd59085da8c356b3c73052e5196b07cc9a9a3790
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ 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
|
+
## [4.7.0] — 2026-04-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Anti-Hallucination Protocol** — 6-rule verification section embedded in every generated context file (CLAUDE.md, AGENTS.md, .claude/rules/, .cursor/rules/, .github/instructions/, copilot-instructions.md). Targets specific AI failure modes: statistical priors overriding observed facts, pattern completion beating verification, inheritance blindness, empty-output-as-permission, stale-context-lies. Rules force AI to verify column/association/route/method/gem names before writing, mark assumptions with `[ASSUMPTION]` prefix, check inheritance chains, and re-query after writes. Enabled by default via new `config.anti_hallucination_rules` option (boolean, default: `true`). Set `false` to skip.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Repositioning: ground truth, not token savings** — the gem's mission is now explicit about what it actually does: stop AI from guessing your Rails app. Token savings are a side-effect, not the product. Updated README headline, "What stops being wrong" section (replaces "Measured token savings"), gemspec summary/description, server.json MCP registry description, docs/GUIDE.md intro, and the tools guide embedded in every generated CLAUDE.md/AGENTS.md/.cursor/rules. The core pitch: AI queries your running app for real schema, real associations, real filters — and writes correct code on the first try instead of iterating through corrections.
|
|
15
|
+
|
|
8
16
|
## [4.6.0] — 2026-04-04
|
|
9
17
|
|
|
10
18
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -63,11 +63,12 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
63
63
|
35. **YAML config** — `.rails-ai-context.yml` as alternative to initializer. Supports all config options except `custom_tools` (Ruby classes) and `excluded_concerns` (regex). Precedence: initializer > YAML > defaults.
|
|
64
64
|
36. **Config auto-loading** — `Configuration.auto_load!` checks `configured_via_block?` flag. If initializer ran, YAML is skipped. Corrupted YAML degrades gracefully with a warning.
|
|
65
65
|
37. **Three install paths** — In-Gemfile (`rails generate rails_ai_context:install`), Standalone (`rails-ai-context init`), Zero config (just run `rails-ai-context serve` with defaults). Users can switch between paths freely; `.mcp.json` command is updated on re-init/re-install.
|
|
66
|
+
38. **Anti-Hallucination Protocol** — 6-rule verification section embedded in every generated context file (CLAUDE.md, AGENTS.md, .claude/rules/, .cursor/rules/, .github/instructions/). Targets AI failure modes: statistical priors overriding facts, pattern completion beating verification, stale context. Toggleable via `config.anti_hallucination_rules` (default: true). Rendered by `tools_anti_hallucination_section` in `tool_guide_helper.rb`, placed between intro and detail_guidance in both full and compact render methods.
|
|
66
67
|
|
|
67
68
|
## Testing
|
|
68
69
|
|
|
69
70
|
```bash
|
|
70
|
-
bundle exec rspec # Run specs (
|
|
71
|
+
bundle exec rspec # Run specs (1627 examples)
|
|
71
72
|
bundle exec rubocop # Lint
|
|
72
73
|
```
|
|
73
74
|
|
data/README.md
CHANGED
|
@@ -35,12 +35,12 @@ You've seen it. Your AI:
|
|
|
35
35
|
- **Uses the wrong association name** — `user.posts` when it's `user.articles`
|
|
36
36
|
- **Generates tests that don't match your patterns** — factories when you use fixtures, or the reverse
|
|
37
37
|
- **Adds a gem you already have** — or calls an API from one you don't
|
|
38
|
-
- **Reads 2,000 lines of schema.rb** to answer a question about one table
|
|
39
38
|
- **Misses `before_action` filters from parent controllers** — then wonders why auth fails
|
|
39
|
+
- **Invents a method** that isn't in your codebase — then you spend 10 minutes finding out
|
|
40
40
|
|
|
41
41
|
You catch it. You fix it. You re-prompt. It breaks something else.
|
|
42
42
|
|
|
43
|
-
**
|
|
43
|
+
**The real cost of AI coding isn't the tokens — it's the correction loop.** Every guess is a round-trip: you catch it, you fix it, you re-prompt, and something adjacent breaks. This gem kills the guessing at its source.
|
|
44
44
|
|
|
45
45
|
<br>
|
|
46
46
|
|
|
@@ -82,37 +82,41 @@ One call returns: definition + source code + every caller grouped by type + test
|
|
|
82
82
|
|
|
83
83
|
<br>
|
|
84
84
|
|
|
85
|
-
##
|
|
85
|
+
## What stops being wrong
|
|
86
86
|
|
|
87
|
-
Real
|
|
87
|
+
Real scenarios where AI goes sideways — and what it does instead with ground truth:
|
|
88
88
|
|
|
89
|
-
|
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
89
|
+
| You ask AI to... | Without — AI guesses | With — AI verifies first |
|
|
90
|
+
|:-----|:-----|:-----|
|
|
91
|
+
| Add a `subscription_tier` column to users | Writes the migration, duplicates an existing column | Reads live schema, spots `subscription_status` already exists, asks before migrating |
|
|
92
|
+
| Call `user.posts` in a controller | Uses the guess; runtime `NoMethodError` | Resolves the actual association (`user.articles`) from the model |
|
|
93
|
+
| Write tests for a new model | Scaffolds with FactoryBot | Detects your fixture-based suite and matches it |
|
|
94
|
+
| Fix a failing create action | Misses inherited `before_action :authenticate_user!` | Returns parent-controller filters inline with the action source |
|
|
95
|
+
| Build a dashboard page | Invents Tailwind classes from memory | Returns your actual button/card/alert patterns, copy-paste ready |
|
|
96
|
+
| Trace where `can_cook?` is used | Reads 6 files sequentially, still misses callers | Single call: definition + source + every caller + tests |
|
|
96
97
|
|
|
97
98
|
<details>
|
|
98
|
-
<summary><strong>
|
|
99
|
+
<summary><strong>Verify it on your own app</strong></summary>
|
|
99
100
|
|
|
100
101
|
<br>
|
|
101
102
|
|
|
103
|
+
Run these before and after installing to see what changes in *your* codebase:
|
|
104
|
+
|
|
102
105
|
```bash
|
|
103
|
-
# Schema:
|
|
104
|
-
|
|
105
|
-
rails 'ai:tool[schema]' table=users | wc -c
|
|
106
|
+
# Schema: does AI know what columns exist?
|
|
107
|
+
rails 'ai:tool[schema]' table=users
|
|
106
108
|
|
|
107
|
-
# Trace:
|
|
108
|
-
rails 'ai:tool[search_code]' pattern=your_method match_type=trace
|
|
109
|
+
# Trace: find every caller of a method across the codebase
|
|
110
|
+
rails 'ai:tool[search_code]' pattern=your_method match_type=trace
|
|
111
|
+
|
|
112
|
+
# Model: associations, scopes, callbacks, concerns — all resolved
|
|
113
|
+
rails 'ai:tool[model_details]' model=User
|
|
109
114
|
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
rails 'ai:tool[model_details]' model=User | wc -c
|
|
115
|
+
# Controllers: action source + inherited filters + strong params in one shot
|
|
116
|
+
rails 'ai:tool[controllers]' controller=UsersController action=create
|
|
113
117
|
```
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
Compare what AI outputs with and without these tools wired in. The difference is measured in *corrections avoided*, not bytes saved.
|
|
116
120
|
|
|
117
121
|
</details>
|
|
118
122
|
|
|
@@ -230,7 +234,7 @@ rails 'ai:tool[stimulus]' controller=chart
|
|
|
230
234
|
|
|
231
235
|
## 39 Tools
|
|
232
236
|
|
|
233
|
-
Every tool is **read-only** and returns
|
|
237
|
+
Every tool is **read-only** and returns data verified against your actual app — not guesses, not training data.
|
|
234
238
|
|
|
235
239
|
<details>
|
|
236
240
|
<summary><strong>Search & Trace</strong></summary>
|
|
@@ -339,6 +343,28 @@ Every tool is **read-only** and returns structured, token-efficient context.
|
|
|
339
343
|
|
|
340
344
|
<br>
|
|
341
345
|
|
|
346
|
+
## Anti-Hallucination Protocol
|
|
347
|
+
|
|
348
|
+
Every generated context file ships with **6 rules that force AI verification** before writing code. The protocol targets the exact cognitive failures that produce confident-wrong code: statistical priors overriding observed facts, pattern completion beating verification, stale context lies.
|
|
349
|
+
|
|
350
|
+
<details>
|
|
351
|
+
<summary><strong>The 6 rules (shown to AI in every CLAUDE.md / .cursor/rules / .github/instructions)</strong></summary>
|
|
352
|
+
|
|
353
|
+
<br>
|
|
354
|
+
|
|
355
|
+
1. **Verify before you write.** Never reference a column, association, route, helper, method, class, partial, or gem not verified in THIS project via a tool call in THIS turn. Never invent names that "sound right."
|
|
356
|
+
2. **Mark every assumption.** If proceeding without verification, prefix with `[ASSUMPTION]`. Silent assumptions forbidden. "I'd need to check X first" is a preferred answer.
|
|
357
|
+
3. **Training data describes average Rails. This app isn't average.** When something feels "obviously" standard Rails, query anyway. Check `rails_get_conventions` + `rails_get_gems` BEFORE scaffolding.
|
|
358
|
+
4. **Check the inheritance chain before every edit.** Inherited `before_action` filters, concerns, includes, STI parents. Inheritance is never flat.
|
|
359
|
+
5. **Empty tool output is information, not permission.** "0 callers found" signals investigation, not license to proceed on guesses.
|
|
360
|
+
6. **Stale context lies. Re-query after writes.** Earlier tool output may be wrong after edits.
|
|
361
|
+
|
|
362
|
+
Enabled by default. Disable with `config.anti_hallucination_rules = false` if you prefer your own rules.
|
|
363
|
+
|
|
364
|
+
</details>
|
|
365
|
+
|
|
366
|
+
<br>
|
|
367
|
+
|
|
342
368
|
## How it works
|
|
343
369
|
|
|
344
370
|
```
|
|
@@ -419,6 +445,7 @@ end
|
|
|
419
445
|
| `preset` | `:full` | `:full` (33 introspectors) or `:standard` (19) |
|
|
420
446
|
| `context_mode` | `:compact` | `:compact` (150 lines) or `:full` |
|
|
421
447
|
| `generate_root_files` | `true` | Set `false` for split rules only |
|
|
448
|
+
| `anti_hallucination_rules` | `true` | Embed 6-rule verification protocol in generated context files |
|
|
422
449
|
| `cache_ttl` | `60` | Cache TTL in seconds |
|
|
423
450
|
| `max_tool_response_chars` | `200_000` | Safety cap for tool responses |
|
|
424
451
|
| `live_reload` | `:auto` | `:auto`, `true`, or `false` |
|
data/docs/GUIDE.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
> Full documentation for [rails-ai-context](https://github.com/crisnahine/rails-ai-context).
|
|
4
4
|
> For a quick overview, see the [README](../README.md).
|
|
5
|
+
>
|
|
6
|
+
> **Why this gem exists:** AI coding assistants guess your Rails app. They invent columns,
|
|
7
|
+
> use wrong association names, miss inherited filters, and scaffold tests that don't match
|
|
8
|
+
> your patterns. This gem turns your running app into the source of truth — so agents query
|
|
9
|
+
> real schema, real associations, and real conventions on demand, and write correct code
|
|
10
|
+
> on the first try.
|
|
5
11
|
|
|
6
12
|
---
|
|
7
13
|
|
|
@@ -1290,6 +1296,7 @@ end
|
|
|
1290
1296
|
| `server_name` | String | `"rails-ai-context"` | MCP server name |
|
|
1291
1297
|
| `server_version` | String | gem version | MCP server version |
|
|
1292
1298
|
| `generate_root_files` | Boolean | `true` | Generate root files (CLAUDE.md, etc.) — set `false` for split rules only |
|
|
1299
|
+
| `anti_hallucination_rules` | Boolean | `true` | Embed 6-rule Anti-Hallucination Protocol in generated context files — set `false` to skip |
|
|
1293
1300
|
| `max_file_size` | Integer | `5_000_000` | Per-file read limit for tools (5MB) |
|
|
1294
1301
|
| `max_test_file_size` | Integer | `1_000_000` | Test file read limit (1MB) |
|
|
1295
1302
|
| `max_schema_file_size` | Integer | `10_000_000` | schema.rb / structure.sql parse limit (10MB) |
|
|
@@ -187,6 +187,11 @@ module RailsAiContext
|
|
|
187
187
|
# Whether to generate root files (CLAUDE.md, AGENTS.md, etc.)
|
|
188
188
|
# Set false to only generate split rules (.claude/rules/, .cursor/rules/, etc.)
|
|
189
189
|
# config.generate_root_files = true
|
|
190
|
+
|
|
191
|
+
# Anti-Hallucination Protocol: 6-rule verification section embedded in every
|
|
192
|
+
# generated context file. Forces AI to verify facts before writing code.
|
|
193
|
+
# Default: true. Set false to skip the protocol entirely.
|
|
194
|
+
# config.anti_hallucination_rules = true
|
|
190
195
|
SECTION
|
|
191
196
|
"Models & Filtering" => <<~SECTION,
|
|
192
197
|
# ── Models & Filtering ────────────────────────────────────────────
|
|
@@ -13,6 +13,7 @@ module RailsAiContext
|
|
|
13
13
|
# All YAML-supported keys (explicit allowlist for safety)
|
|
14
14
|
YAML_KEYS = %i[
|
|
15
15
|
ai_tools tool_mode preset context_mode generate_root_files claude_max_lines
|
|
16
|
+
anti_hallucination_rules
|
|
16
17
|
server_name cache_ttl max_tool_response_chars
|
|
17
18
|
live_reload live_reload_debounce auto_mount http_path http_bind http_port
|
|
18
19
|
output_dir skip_tools excluded_models excluded_controllers
|
|
@@ -133,6 +134,11 @@ module RailsAiContext
|
|
|
133
134
|
# When false, only generates split rule files (.claude/rules/, .cursor/rules/, etc.)
|
|
134
135
|
attr_accessor :generate_root_files
|
|
135
136
|
|
|
137
|
+
# Whether to embed the Anti-Hallucination Protocol section in generated context files.
|
|
138
|
+
# Default: true. Set false to skip the 6-rule verification protocol in CLAUDE.md,
|
|
139
|
+
# AGENTS.md, .claude/rules/, .cursor/rules/, .github/instructions/.
|
|
140
|
+
attr_accessor :anti_hallucination_rules
|
|
141
|
+
|
|
136
142
|
# File size limits (bytes) — increase for larger projects
|
|
137
143
|
attr_accessor :max_file_size # Per-file read limit for tools (default: 2MB)
|
|
138
144
|
attr_accessor :max_test_file_size # Test file read limit (default: 500KB)
|
|
@@ -231,6 +237,7 @@ module RailsAiContext
|
|
|
231
237
|
@live_reload = :auto
|
|
232
238
|
@live_reload_debounce = 1.5
|
|
233
239
|
@generate_root_files = true
|
|
240
|
+
@anti_hallucination_rules = true
|
|
234
241
|
@max_file_size = 5_000_000
|
|
235
242
|
@max_test_file_size = 1_000_000
|
|
236
243
|
@max_schema_file_size = 10_000_000
|
|
@@ -77,15 +77,8 @@ module RailsAiContext
|
|
|
77
77
|
line += " (#{assoc_count}a, #{val_count}v)" if assoc_count > 0 || val_count > 0
|
|
78
78
|
line += " — #{top_assocs}" if top_assocs && !top_assocs.empty?
|
|
79
79
|
lines << line
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if scopes.any? || constants.any?
|
|
83
|
-
extras = []
|
|
84
|
-
scope_names = scope_names(scopes)
|
|
85
|
-
extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
|
|
86
|
-
constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
|
|
87
|
-
lines << " #{extras.join(' | ')}"
|
|
88
|
-
end
|
|
80
|
+
extras = model_extras_line(data)
|
|
81
|
+
lines << extras if extras
|
|
89
82
|
end
|
|
90
83
|
lines << "- _...#{models.size - max_show} more (use `rails_get_model_details` tool)_" if models.size > max_show
|
|
91
84
|
lines << ""
|
|
@@ -108,19 +101,6 @@ module RailsAiContext
|
|
|
108
101
|
lines
|
|
109
102
|
end
|
|
110
103
|
|
|
111
|
-
def render_conventions
|
|
112
|
-
conv = context[:conventions]
|
|
113
|
-
return [] unless conv.is_a?(Hash) && !conv[:error]
|
|
114
|
-
|
|
115
|
-
config_files = conv[:config_files] || []
|
|
116
|
-
return [] if config_files.empty?
|
|
117
|
-
|
|
118
|
-
lines = [ "## Key config files" ]
|
|
119
|
-
config_files.first(5).each { |f| lines << "- `#{f}`" }
|
|
120
|
-
lines << ""
|
|
121
|
-
lines
|
|
122
|
-
end
|
|
123
|
-
|
|
124
104
|
def render_commands
|
|
125
105
|
test_cmd = detect_test_command
|
|
126
106
|
[
|
|
@@ -107,15 +107,8 @@ module RailsAiContext
|
|
|
107
107
|
data = models[name]
|
|
108
108
|
assocs = (data[:associations] || []).size
|
|
109
109
|
lines << "- #{name} (#{assocs} associations)"
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if scopes.any? || constants.any?
|
|
113
|
-
extras = []
|
|
114
|
-
scope_names = scope_names(scopes)
|
|
115
|
-
extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
|
|
116
|
-
constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
|
|
117
|
-
lines << " #{extras.join(' | ')}"
|
|
118
|
-
end
|
|
110
|
+
extras = model_extras_line(data)
|
|
111
|
+
lines << extras if extras
|
|
119
112
|
end
|
|
120
113
|
|
|
121
114
|
lines << "- ...#{models.size - 30} more" if models.size > 30
|
|
@@ -139,11 +132,7 @@ module RailsAiContext
|
|
|
139
132
|
""
|
|
140
133
|
]
|
|
141
134
|
|
|
142
|
-
|
|
143
|
-
info = controllers[name]
|
|
144
|
-
actions = info[:actions]&.size || 0
|
|
145
|
-
lines << "- #{name} (#{actions} actions)"
|
|
146
|
-
end
|
|
135
|
+
lines.concat(render_compact_controllers_list(controllers))
|
|
147
136
|
|
|
148
137
|
lines.join("\n")
|
|
149
138
|
end
|
|
@@ -163,16 +152,7 @@ module RailsAiContext
|
|
|
163
152
|
|
|
164
153
|
lines.concat(render_design_system_full(context))
|
|
165
154
|
|
|
166
|
-
|
|
167
|
-
stim = context[:stimulus]
|
|
168
|
-
if stim.is_a?(Hash) && !stim[:error]
|
|
169
|
-
controllers = stim[:controllers] || []
|
|
170
|
-
if controllers.any?
|
|
171
|
-
names = controllers.map { |c| c[:name] || c[:file]&.gsub("_controller.js", "") }.compact.sort
|
|
172
|
-
lines << "" << "## Stimulus controllers"
|
|
173
|
-
lines << names.join(", ")
|
|
174
|
-
end
|
|
175
|
-
end
|
|
155
|
+
lines.concat(render_stimulus_section(context))
|
|
176
156
|
|
|
177
157
|
lines.join("\n")
|
|
178
158
|
end
|
|
@@ -120,15 +120,8 @@ module RailsAiContext
|
|
|
120
120
|
data = models[name]
|
|
121
121
|
assocs = (data[:associations] || []).size
|
|
122
122
|
lines << "- #{name} (#{assocs} associations, table: #{data[:table_name] || '?'})"
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if scopes.any? || constants.any?
|
|
126
|
-
extras = []
|
|
127
|
-
scope_names = scope_names(scopes)
|
|
128
|
-
extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
|
|
129
|
-
constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
|
|
130
|
-
lines << " #{extras.join(' | ')}"
|
|
131
|
-
end
|
|
123
|
+
extras = model_extras_line(data)
|
|
124
|
+
lines << extras if extras
|
|
132
125
|
end
|
|
133
126
|
|
|
134
127
|
lines << "- ...#{models.size - 30} more" if models.size > 30
|
|
@@ -156,13 +149,8 @@ module RailsAiContext
|
|
|
156
149
|
""
|
|
157
150
|
]
|
|
158
151
|
|
|
159
|
-
|
|
160
|
-
info = controllers[name]
|
|
161
|
-
action_count = info[:actions]&.size || 0
|
|
162
|
-
lines << "- #{name} (#{action_count} actions)"
|
|
163
|
-
end
|
|
152
|
+
lines.concat(render_compact_controllers_list(controllers))
|
|
164
153
|
|
|
165
|
-
lines << "- ...#{controllers.size - 25} more" if controllers.size > 25
|
|
166
154
|
lines << ""
|
|
167
155
|
lines << "Use `rails_get_controllers` MCP tool with controller:\"Name\" for full detail."
|
|
168
156
|
|
|
@@ -198,16 +186,7 @@ module RailsAiContext
|
|
|
198
186
|
end
|
|
199
187
|
rescue => e; $stderr.puts "[rails-ai-context] Serializer section skipped: #{e.message}"; end
|
|
200
188
|
|
|
201
|
-
|
|
202
|
-
stim = context[:stimulus]
|
|
203
|
-
if stim.is_a?(Hash) && !stim[:error]
|
|
204
|
-
controllers = stim[:controllers] || []
|
|
205
|
-
if controllers.any?
|
|
206
|
-
names = controllers.map { |c| c[:name] || c[:file]&.gsub("_controller.js", "") }.compact.sort
|
|
207
|
-
lines << "" << "## Stimulus controllers"
|
|
208
|
-
lines << names.join(", ")
|
|
209
|
-
end
|
|
210
|
-
end
|
|
189
|
+
lines.concat(render_stimulus_section(context))
|
|
211
190
|
|
|
212
191
|
lines.join("\n")
|
|
213
192
|
end
|
|
@@ -56,15 +56,8 @@ module RailsAiContext
|
|
|
56
56
|
line += " — #{assocs}" unless assocs.empty?
|
|
57
57
|
line += " [#{vals}v]" if vals > 0
|
|
58
58
|
lines << line
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if scopes.any? || constants.any?
|
|
62
|
-
extras = []
|
|
63
|
-
scope_names = scope_names(scopes)
|
|
64
|
-
extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
|
|
65
|
-
constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
|
|
66
|
-
lines << " #{extras.join(' | ')}"
|
|
67
|
-
end
|
|
59
|
+
extras = model_extras_line(data)
|
|
60
|
+
lines << extras if extras
|
|
68
61
|
end
|
|
69
62
|
|
|
70
63
|
lines << "- _...#{models.size - 30} more_" if models.size > 30
|
|
@@ -113,6 +113,43 @@ module RailsAiContext
|
|
|
113
113
|
scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# Render a compact controllers listing: "- Name (N actions)" + "...X more".
|
|
117
|
+
# Shared by cursor_rules and copilot_instructions serializers.
|
|
118
|
+
def render_compact_controllers_list(controllers_hash, limit: 25)
|
|
119
|
+
lines = []
|
|
120
|
+
controllers_hash.keys.sort.first(limit).each do |name|
|
|
121
|
+
info = controllers_hash[name]
|
|
122
|
+
action_count = info[:actions]&.size || 0
|
|
123
|
+
lines << "- #{name} (#{action_count} actions)"
|
|
124
|
+
end
|
|
125
|
+
lines << "- ...#{controllers_hash.size - limit} more" if controllers_hash.size > limit
|
|
126
|
+
lines
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Render a Stimulus controllers section from context[:stimulus].
|
|
130
|
+
# Returns lines or [] if no Stimulus controllers.
|
|
131
|
+
def render_stimulus_section(ctx = context)
|
|
132
|
+
stim = ctx[:stimulus]
|
|
133
|
+
return [] unless stim.is_a?(Hash) && !stim[:error]
|
|
134
|
+
controllers = stim[:controllers] || []
|
|
135
|
+
return [] if controllers.empty?
|
|
136
|
+
names = controllers.map { |c| c[:name] || c[:file]&.gsub("_controller.js", "") }.compact.sort
|
|
137
|
+
[ "", "## Stimulus controllers", names.join(", ") ]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Render scopes and constants as a one-line extras summary for a model entry.
|
|
141
|
+
# Returns " scopes: a, b | STATUS: draft, active" or nil if no extras exist.
|
|
142
|
+
# Shared by cursor_rules, opencode_rules, copilot_instructions, compact_serializer_helper.
|
|
143
|
+
def model_extras_line(data)
|
|
144
|
+
scopes = data[:scopes] || []
|
|
145
|
+
constants = data[:constants] || []
|
|
146
|
+
return nil unless scopes.any? || constants.any?
|
|
147
|
+
extras = []
|
|
148
|
+
extras << "scopes: #{scope_names(scopes).join(', ')}" if scopes.any?
|
|
149
|
+
constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
|
|
150
|
+
" #{extras.join(' | ')}"
|
|
151
|
+
end
|
|
152
|
+
|
|
116
153
|
# Extract notable gems with triple-fallback for varying introspector output shapes.
|
|
117
154
|
def notable_gems_list(gems_data)
|
|
118
155
|
return [] unless gems_data.is_a?(Hash) && !gems_data[:error]
|
|
@@ -32,13 +32,15 @@ module RailsAiContext
|
|
|
32
32
|
when :cli
|
|
33
33
|
[
|
|
34
34
|
"This project has 39 introspection tools. **MANDATORY — use these instead of reading files.**",
|
|
35
|
-
"They return
|
|
35
|
+
"They return ground truth from the running app: real schema, real associations, real filters — not guesses.",
|
|
36
|
+
"Read files ONLY when you are about to Edit them.",
|
|
36
37
|
""
|
|
37
38
|
]
|
|
38
39
|
else
|
|
39
40
|
[
|
|
40
41
|
"This project has 39 MCP tools via `rails ai:serve` (configured in `.mcp.json`).",
|
|
41
|
-
"**MANDATORY — use these instead of reading files.** They return
|
|
42
|
+
"**MANDATORY — use these instead of reading files.** They return ground truth from the running app:",
|
|
43
|
+
"real schema, real associations, real filters — not guesses from file reads.",
|
|
42
44
|
"Read files ONLY when you are about to Edit them.",
|
|
43
45
|
"If MCP tools are not connected, use CLI fallback: `#{cli_cmd("TOOL_NAME", "param=value")}`",
|
|
44
46
|
""
|
|
@@ -46,6 +48,26 @@ module RailsAiContext
|
|
|
46
48
|
end
|
|
47
49
|
end
|
|
48
50
|
|
|
51
|
+
def tools_anti_hallucination_section
|
|
52
|
+
return [] unless RailsAiContext.configuration.anti_hallucination_rules
|
|
53
|
+
|
|
54
|
+
[
|
|
55
|
+
"### Anti-Hallucination Protocol — Verify Before You Write",
|
|
56
|
+
"",
|
|
57
|
+
"AI assistants produce confident-wrong code when statistical priors from training",
|
|
58
|
+
"data override observed facts in the current project. These 6 rules force",
|
|
59
|
+
"verification at the exact moments hallucination is most likely.",
|
|
60
|
+
"",
|
|
61
|
+
"1. **Verify before you write.** Never reference a column, association, route, helper, method, class, partial, or gem you have NOT verified in THIS project via a tool call in THIS turn. If it's not verified here, verify it now. Never invent names that \"sound right.\"",
|
|
62
|
+
"2. **Mark every assumption.** If you must proceed without verification, prefix the relevant output with `[ASSUMPTION]` and state what you're assuming and why. Silent assumptions are forbidden. \"I'd need to check X first\" is a valid and preferred answer.",
|
|
63
|
+
"3. **Training data describes average Rails. This app isn't average.** When something feels \"obviously\" like standard Rails, query anyway. Factories vs fixtures? Pundit vs CanCan? Devise vs has_secure_password? Check `rails_get_conventions` and `rails_get_gems` BEFORE scaffolding anything.",
|
|
64
|
+
"4. **Check the inheritance chain before every edit.** Before writing a controller action: inherited `before_action` filters and ancestor classes. Before writing a model method: concerns, includes, STI parents. Inheritance is never flat.",
|
|
65
|
+
"5. **Empty tool output is information, not permission.** \"0 callers found,\" \"no validations,\" or a missing model is a signal to investigate or confirm with the user — not a license to proceed on guesses. Follow `_Next:` hints.",
|
|
66
|
+
"6. **Stale context lies. Re-query after writes.** After any edit, tool output from earlier in this turn may be wrong. Re-query the affected tool before the next write.",
|
|
67
|
+
""
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
|
|
49
71
|
def tools_detail_guidance
|
|
50
72
|
detail_param = tool_mode == :cli ? "detail=summary" : "detail:\"summary\""
|
|
51
73
|
[
|
|
@@ -131,7 +153,7 @@ module RailsAiContext
|
|
|
131
153
|
"- **Don't call tools without a target** — `get_model_details()` without `model:` returns a paginated list, not an error. Always specify what you want.",
|
|
132
154
|
"- **Don't skip validation** — run `#{validate_tool}` after EVERY edit. It catches syntax errors AND Rails-specific issues (missing partials, bad column refs).",
|
|
133
155
|
"- **Don't ignore cross-references** — tool responses include `_Next:` hints suggesting the best follow-up call. Follow them.",
|
|
134
|
-
"- **Don't call `detail:\"full\"` first** — start with `summary` to find your target, then drill in. Full responses
|
|
156
|
+
"- **Don't call `detail:\"full\"` first** — start with `summary` to find your target, then drill in. Full responses bury the signal.",
|
|
135
157
|
""
|
|
136
158
|
]
|
|
137
159
|
end
|
|
@@ -166,108 +188,69 @@ module RailsAiContext
|
|
|
166
188
|
end
|
|
167
189
|
end
|
|
168
190
|
|
|
169
|
-
def tools_table
|
|
191
|
+
def tools_table
|
|
170
192
|
lines = [ "### All 39 Tools", "" ]
|
|
171
|
-
|
|
172
|
-
if tool_mode == :cli
|
|
173
|
-
lines.concat(tools_table_cli)
|
|
174
|
-
else
|
|
175
|
-
lines.concat(tools_table_mcp_and_cli)
|
|
176
|
-
end
|
|
177
|
-
|
|
193
|
+
lines.concat(build_tools_table(include_mcp: tool_mode != :cli))
|
|
178
194
|
lines
|
|
179
195
|
end
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
end
|
|
197
|
+
# Single source of truth for the tools table.
|
|
198
|
+
# Each row is [mcp_call, cli_name, cli_args, description].
|
|
199
|
+
# Set include_mcp: false for CLI-only 2-column table.
|
|
200
|
+
TOOL_ROWS = [
|
|
201
|
+
[ 'rails_get_context(model:"X")', "context", "model=X", "**START HERE** — schema + model + controller + routes + views in one call" ],
|
|
202
|
+
[ 'rails_analyze_feature(feature:"X")', "analyze_feature", "feature=X", "Full-stack: models + controllers + routes + services + jobs + views + tests" ],
|
|
203
|
+
[ 'rails_search_code(pattern:"X", match_type:"trace")', "search_code", "pattern=X match_type=trace", 'Search + trace: definition, source, callers, test coverage. Also: `match_type:"any"` for regex search' ],
|
|
204
|
+
[ 'rails_get_controllers(controller:"X", action:"Y")', "controllers", "controller=X action=Y", "Action source + inherited filters + render map + private methods" ],
|
|
205
|
+
[ 'rails_validate(files:[...], level:"rails")', "validate", "files=a.rb,b.rb level=rails", "Syntax + semantic validation (run after EVERY edit)" ],
|
|
206
|
+
[ 'rails_get_schema(table:"X")', "schema", "table=X", "Columns with [indexed]/[unique]/[encrypted]/[default] hints" ],
|
|
207
|
+
[ 'rails_get_model_details(model:"X")', "model_details", "model=X", "Associations, validations, scopes, enums, macros, delegations" ],
|
|
208
|
+
[ 'rails_get_routes(controller:"X")', "routes", "controller=X", "Routes with code-ready helpers and controller filters inline" ],
|
|
209
|
+
[ 'rails_get_view(controller:"X")', "view", "controller=X", "Templates with ivars, Turbo wiring, Stimulus refs, partial locals" ],
|
|
210
|
+
[ "rails_get_design_system", "design_system", nil, "Canonical HTML/ERB copy-paste patterns for buttons, inputs, cards" ],
|
|
211
|
+
[ 'rails_get_stimulus(controller:"X")', "stimulus", "controller=X", "Targets, values, actions + HTML data-attributes + view lookup" ],
|
|
212
|
+
[ 'rails_get_test_info(model:"X")', "test_info", "model=X", "Tests + fixture contents + test template" ],
|
|
213
|
+
[ 'rails_get_concern(name:"X", detail:"full")', "concern", "name=X detail=full", "Concern methods with source + which models include it" ],
|
|
214
|
+
[ 'rails_get_callbacks(model:"X")', "callbacks", "model=X", "Callbacks in Rails execution order with source" ],
|
|
215
|
+
[ 'rails_get_edit_context(file:"X", near:"Y")', "edit_context", "file=X near=Y", "Code around a match with class/method context" ],
|
|
216
|
+
[ "rails_get_service_pattern", "service_pattern", nil, "Service objects: interface, dependencies, side effects, callers" ],
|
|
217
|
+
[ "rails_get_job_pattern", "job_pattern", nil, "Jobs: queue, retries, guard clauses, broadcasts, schedules" ],
|
|
218
|
+
[ "rails_get_env", "env", nil, "Environment variables + credentials keys (not values)" ],
|
|
219
|
+
[ 'rails_get_partial_interface(partial:"X")', "partial_interface", "partial=X", "Partial locals contract: what to pass + usage examples" ],
|
|
220
|
+
[ "rails_get_turbo_map", "turbo_map", nil, "Turbo Stream/Frame wiring + mismatch warnings" ],
|
|
221
|
+
[ "rails_get_helper_methods", "helper_methods", nil, "App + framework helpers with view cross-references" ],
|
|
222
|
+
[ "rails_get_config", "config", nil, "Database adapter, auth, assets, cache, queue, Action Cable" ],
|
|
223
|
+
[ "rails_get_gems", "gems", nil, "Notable gems with versions, categories, config file locations" ],
|
|
224
|
+
[ "rails_get_conventions", "conventions", nil, "App patterns: auth checks, flash messages, test patterns" ],
|
|
225
|
+
[ "rails_security_scan", "security_scan", nil, "Brakeman static analysis: SQL injection, XSS, mass assignment" ],
|
|
226
|
+
[ 'rails_get_component_catalog(component:"X")', "component_catalog", "component=X", "ViewComponent/Phlex: props, slots, previews, usage" ],
|
|
227
|
+
[ 'rails_performance_check(model:"X")', "performance_check", "model=X", "N+1 risks, missing indexes, Model.all anti-patterns" ],
|
|
228
|
+
[ 'rails_dependency_graph(model:"X")', "dependency_graph", "model=X", "Model association graph as Mermaid diagram" ],
|
|
229
|
+
[ 'rails_migration_advisor(action:"X", table:"Y")', "migration_advisor", "action=X table=Y", "Generate migration code, flag irreversible ops" ],
|
|
230
|
+
[ "rails_get_frontend_stack", "frontend_stack", nil, "React/Vue/Svelte/Angular, Inertia, TypeScript, package manager" ],
|
|
231
|
+
[ 'rails_search_docs(query:"X")', "search_docs", "query=X", "Bundled topic index with weighted keyword search, on-demand GitHub fetch" ],
|
|
232
|
+
[ 'rails_query(sql:"X")', "query", "sql=X", "Safe read-only SQL queries with timeout, row limit, column redaction" ],
|
|
233
|
+
[ 'rails_read_logs(level:"X")', "read_logs", "level=X", "Reverse file tail with level filtering and sensitive data redaction" ],
|
|
234
|
+
[ 'rails_generate_test(model:"X")', "generate_test", "model=X", "Generate test scaffolding matching project patterns (framework, factories, style)" ],
|
|
235
|
+
[ 'rails_diagnose(error:"X")', "diagnose", 'error="X"', "One-call error diagnosis: context + git changes + logs + fix suggestions" ],
|
|
236
|
+
[ 'rails_review_changes(ref:"main")', "review_changes", "ref=main", "PR/commit review: file context + warnings (missing indexes, removed validations)" ],
|
|
237
|
+
[ 'rails_onboard(detail:"standard")', "onboard", "detail=standard", "Narrative app walkthrough for new developers or AI agents" ],
|
|
238
|
+
[ 'rails_runtime_info(detail:"standard")', "runtime_info", "detail=standard", "Live runtime: DB pool, table sizes, cache stats, job queues, pending migrations" ],
|
|
239
|
+
[ 'rails_session_context(action:"status")', "session_context", "action=status", "Track what you've already queried, avoid redundant calls" ]
|
|
240
|
+
].freeze
|
|
226
241
|
|
|
227
|
-
def
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
"| `#{cli_cmd("view", "controller=X")}` | Templates with ivars, Turbo wiring, Stimulus refs, partial locals |",
|
|
240
|
-
"| `#{cli_cmd("design_system")}` | Canonical HTML/ERB copy-paste patterns for buttons, inputs, cards |",
|
|
241
|
-
"| `#{cli_cmd("stimulus", "controller=X")}` | Targets, values, actions + HTML data-attributes + view lookup |",
|
|
242
|
-
"| `#{cli_cmd("test_info", "model=X")}` | Tests + fixture contents + test template |",
|
|
243
|
-
"| `#{cli_cmd("concern", "name=X detail=full")}` | Concern methods with source + which models include it |",
|
|
244
|
-
"| `#{cli_cmd("callbacks", "model=X")}` | Callbacks in Rails execution order with source |",
|
|
245
|
-
"| `#{cli_cmd("edit_context", "file=X near=Y")}` | Code around a match with class/method context |",
|
|
246
|
-
"| `#{cli_cmd("service_pattern")}` | Service objects: interface, dependencies, side effects, callers |",
|
|
247
|
-
"| `#{cli_cmd("job_pattern")}` | Jobs: queue, retries, guard clauses, broadcasts, schedules |",
|
|
248
|
-
"| `#{cli_cmd("env")}` | Environment variables + credentials keys (not values) |",
|
|
249
|
-
"| `#{cli_cmd("partial_interface", "partial=X")}` | Partial locals contract: what to pass + usage examples |",
|
|
250
|
-
"| `#{cli_cmd("turbo_map")}` | Turbo Stream/Frame wiring + mismatch warnings |",
|
|
251
|
-
"| `#{cli_cmd("helper_methods")}` | App + framework helpers with view cross-references |",
|
|
252
|
-
"| `#{cli_cmd("config")}` | Database adapter, auth, assets, cache, queue, Action Cable |",
|
|
253
|
-
"| `#{cli_cmd("gems")}` | Notable gems with versions, categories, config file locations |",
|
|
254
|
-
"| `#{cli_cmd("conventions")}` | App patterns: auth checks, flash messages, test patterns |",
|
|
255
|
-
"| `#{cli_cmd("security_scan")}` | Brakeman static analysis: SQL injection, XSS, mass assignment |",
|
|
256
|
-
"| `#{cli_cmd("component_catalog", "component=X")}` | ViewComponent/Phlex: props, slots, previews, usage |",
|
|
257
|
-
"| `#{cli_cmd("performance_check", "model=X")}` | N+1 risks, missing indexes, Model.all anti-patterns |",
|
|
258
|
-
"| `#{cli_cmd("dependency_graph", "model=X")}` | Model association graph as Mermaid diagram |",
|
|
259
|
-
"| `#{cli_cmd("migration_advisor", "action=X table=Y")}` | Generate migration code, flag irreversible ops |",
|
|
260
|
-
"| `#{cli_cmd("frontend_stack")}` | React/Vue/Svelte/Angular, Inertia, TypeScript, package manager |",
|
|
261
|
-
"| `#{cli_cmd("search_docs", "query=X")}` | Bundled topic index with weighted keyword search, on-demand GitHub fetch |",
|
|
262
|
-
"| `#{cli_cmd("query", "sql=X")}` | Safe read-only SQL queries with timeout, row limit, column redaction |",
|
|
263
|
-
"| `#{cli_cmd("read_logs", "level=X")}` | Reverse file tail with level filtering and sensitive data redaction |",
|
|
264
|
-
"| `#{cli_cmd("generate_test", "model=X")}` | Generate test scaffolding matching project patterns (framework, factories, style) |",
|
|
265
|
-
"| `#{cli_cmd("diagnose", "error=\"X\"")}` | One-call error diagnosis: context + git changes + logs + fix suggestions |",
|
|
266
|
-
"| `#{cli_cmd("review_changes", "ref=main")}` | PR/commit review: file context + warnings (missing indexes, removed validations) |",
|
|
267
|
-
"| `#{cli_cmd("onboard", "detail=standard")}` | Narrative app walkthrough for new developers or AI agents |",
|
|
268
|
-
"| `#{cli_cmd("runtime_info", "detail=standard")}` | Live runtime: DB pool, table sizes, cache stats, job queues, pending migrations |",
|
|
269
|
-
"| `#{cli_cmd("session_context", "action=status")}` | Track what you've already queried, avoid redundant calls |"
|
|
270
|
-
]
|
|
242
|
+
def build_tools_table(include_mcp:)
|
|
243
|
+
# For CLI-only tables, `match_type=any` uses `=` (not `:`), so we tweak description.
|
|
244
|
+
rows = TOOL_ROWS.map do |mcp_call, cli_name, cli_args, desc|
|
|
245
|
+
cli = cli_cmd(cli_name, cli_args)
|
|
246
|
+
if include_mcp
|
|
247
|
+
"| `#{mcp_call}` | `#{cli}` | #{desc} |"
|
|
248
|
+
else
|
|
249
|
+
"| `#{cli}` | #{desc.gsub('match_type:"any"', "match_type=any")} |"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
header = include_mcp ? [ "| MCP | CLI | What it does |", "|-----|-----|-------------|" ] : [ "| CLI | What it does |", "|-----|-------------|" ]
|
|
253
|
+
header + rows
|
|
271
254
|
end
|
|
272
255
|
|
|
273
256
|
# Full tool guide section — used by split rules files (.claude/rules/, .cursor/rules/, etc.)
|
|
@@ -276,6 +259,7 @@ module RailsAiContext
|
|
|
276
259
|
lines << tools_header
|
|
277
260
|
lines << ""
|
|
278
261
|
lines.concat(tools_intro)
|
|
262
|
+
lines.concat(tools_anti_hallucination_section)
|
|
279
263
|
lines.concat(tools_detail_guidance)
|
|
280
264
|
lines.concat(tools_power_tool_section)
|
|
281
265
|
lines.concat(tools_workflow_section)
|
|
@@ -292,6 +276,7 @@ module RailsAiContext
|
|
|
292
276
|
lines << tools_header
|
|
293
277
|
lines << ""
|
|
294
278
|
lines.concat(tools_intro)
|
|
279
|
+
lines.concat(tools_anti_hallucination_section)
|
|
295
280
|
lines.concat(tools_power_tool_section)
|
|
296
281
|
lines.concat(tools_workflow_section)
|
|
297
282
|
lines.concat(tools_antipatterns_section)
|
|
@@ -153,6 +153,60 @@ module RailsAiContext
|
|
|
153
153
|
SHARED_CACHE[:fingerprint] || "none"
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
# Case-insensitive fuzzy key lookup for hashes keyed by class/table names.
|
|
157
|
+
# Tries exact, underscore, singularize, and classify variants. Returns matching key or nil.
|
|
158
|
+
# Shared by get_model_details, get_callbacks, get_context, generate_test, dependency_graph.
|
|
159
|
+
def fuzzy_find_key(keys, query)
|
|
160
|
+
return nil if query.nil? || keys.nil? || keys.empty?
|
|
161
|
+
q = query.to_s.strip
|
|
162
|
+
return nil if q.empty?
|
|
163
|
+
q_down = q.downcase
|
|
164
|
+
q_under = q.underscore.downcase
|
|
165
|
+
|
|
166
|
+
keys.find { |k| k.to_s.downcase == q_down } ||
|
|
167
|
+
keys.find { |k| k.to_s.underscore.downcase == q_under } ||
|
|
168
|
+
keys.find { |k| k.to_s.downcase == q.singularize.downcase } ||
|
|
169
|
+
keys.find { |k| k.to_s.downcase == q.classify.downcase }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Extract method source from a source string via indentation-based matching.
|
|
173
|
+
# Returns { code:, start_line:, end_line: } or nil. Shared by get_callbacks, get_concern.
|
|
174
|
+
def extract_method_source_from_string(source, method_name)
|
|
175
|
+
source_lines = source.lines
|
|
176
|
+
escaped = Regexp.escape(method_name.to_s)
|
|
177
|
+
# ? and ! ARE word boundaries, so skip \b after them
|
|
178
|
+
pattern = if method_name.to_s.end_with?("?", "!")
|
|
179
|
+
/\A\s*def\s+#{escaped}/
|
|
180
|
+
else
|
|
181
|
+
/\A\s*def\s+#{escaped}\b/
|
|
182
|
+
end
|
|
183
|
+
start_idx = source_lines.index { |l| l.match?(pattern) }
|
|
184
|
+
return nil unless start_idx
|
|
185
|
+
|
|
186
|
+
def_indent = source_lines[start_idx][/\A\s*/].length
|
|
187
|
+
result = []
|
|
188
|
+
end_idx = start_idx
|
|
189
|
+
|
|
190
|
+
source_lines[start_idx..].each_with_index do |line, i|
|
|
191
|
+
result << line.rstrip
|
|
192
|
+
end_idx = start_idx + i
|
|
193
|
+
break if i > 0 && line.match?(/\A\s{#{def_indent}}end\b/)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
{ code: result.join("\n"), start_line: start_idx + 1, end_line: end_idx + 1 }
|
|
197
|
+
rescue => e
|
|
198
|
+
$stderr.puts "[rails-ai-context] extract_method_source_from_string failed: #{e.message}" if ENV["DEBUG"]
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Extract method source from a file path. Reads file safely. Returns hash or nil.
|
|
203
|
+
def extract_method_source_from_file(path, method_name)
|
|
204
|
+
return nil unless File.exist?(path)
|
|
205
|
+
return nil if File.size(path) > RailsAiContext.configuration.max_file_size
|
|
206
|
+
source = RailsAiContext::SafeFile.read(path) || ""
|
|
207
|
+
extract_method_source_from_string(source, method_name)
|
|
208
|
+
end
|
|
209
|
+
|
|
156
210
|
# Store call params for the current tool invocation (thread-safe)
|
|
157
211
|
def set_call_params(**params)
|
|
158
212
|
Thread.current[:rails_ai_context_call_params] = params.reject { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
|
|
@@ -148,8 +148,7 @@ module RailsAiContext
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
def find_model_key(query, keys)
|
|
151
|
-
keys
|
|
152
|
-
keys.find { |k| k.underscore.downcase == query.downcase }
|
|
151
|
+
fuzzy_find_key(keys, query)
|
|
153
152
|
end
|
|
154
153
|
|
|
155
154
|
def extract_subgraph(graph, center, depth)
|
|
@@ -99,8 +99,7 @@ module RailsAiContext
|
|
|
99
99
|
|
|
100
100
|
def generate_model_test(model_name, framework, patterns, tests_data)
|
|
101
101
|
models = cached_context[:models] || {}
|
|
102
|
-
key = models.keys
|
|
103
|
-
models.keys.find { |k| k.underscore == model_name.underscore }
|
|
102
|
+
key = fuzzy_find_key(models.keys, model_name)
|
|
104
103
|
unless key
|
|
105
104
|
return not_found_response("Model", model_name, models.keys.sort,
|
|
106
105
|
recovery_tool: "Call rails_get_model_details(detail:\"summary\") to see all models")
|
|
@@ -57,7 +57,7 @@ module RailsAiContext
|
|
|
57
57
|
|
|
58
58
|
# Specific model — show callbacks in execution order
|
|
59
59
|
if model
|
|
60
|
-
key = models.keys
|
|
60
|
+
key = fuzzy_find_key(models.keys, model) || model
|
|
61
61
|
data = models[key]
|
|
62
62
|
unless data
|
|
63
63
|
return not_found_response("Model", model, models.keys.sort,
|
|
@@ -122,7 +122,7 @@ module RailsAiContext
|
|
|
122
122
|
concern_callbacks.each do |concern_name, info|
|
|
123
123
|
lines << "### #{concern_name}"
|
|
124
124
|
info[:callbacks].each do |cb|
|
|
125
|
-
source =
|
|
125
|
+
source = extract_method_source_from_file(info[:path], cb[:method_name])
|
|
126
126
|
lines << "- #{cb[:declaration]}"
|
|
127
127
|
if source
|
|
128
128
|
lines << "```ruby"
|
|
@@ -227,39 +227,7 @@ module RailsAiContext
|
|
|
227
227
|
|
|
228
228
|
private_class_method def self.extract_callback_source(model_name, method_name)
|
|
229
229
|
path = Rails.root.join("app", "models", "#{model_name.underscore}.rb")
|
|
230
|
-
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
private_class_method def self.extract_method_source(path, method_name)
|
|
234
|
-
return nil unless File.exist?(path)
|
|
235
|
-
return nil if File.size(path) > RailsAiContext.configuration.max_file_size
|
|
236
|
-
|
|
237
|
-
source_lines = (RailsAiContext::SafeFile.read(path) || "").lines
|
|
238
|
-
method_str = method_name.to_s
|
|
239
|
-
|
|
240
|
-
# Find method definition (could be public or private)
|
|
241
|
-
start_idx = source_lines.index { |l| l.match?(/\A\s*def\s+#{Regexp.escape(method_str)}\b/) }
|
|
242
|
-
return nil unless start_idx
|
|
243
|
-
|
|
244
|
-
# Use indentation-based matching
|
|
245
|
-
def_indent = source_lines[start_idx][/\A\s*/].length
|
|
246
|
-
result = []
|
|
247
|
-
end_idx = start_idx
|
|
248
|
-
|
|
249
|
-
source_lines[start_idx..].each_with_index do |line, i|
|
|
250
|
-
result << line.rstrip
|
|
251
|
-
end_idx = start_idx + i
|
|
252
|
-
break if i > 0 && line.match?(/\A\s{#{def_indent}}end\b/)
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
{
|
|
256
|
-
code: result.join("\n"),
|
|
257
|
-
start_line: start_idx + 1,
|
|
258
|
-
end_line: end_idx + 1
|
|
259
|
-
}
|
|
260
|
-
rescue => e
|
|
261
|
-
$stderr.puts "[rails-ai-context] extract_method_source failed: #{e.message}" if ENV["DEBUG"]
|
|
262
|
-
nil
|
|
230
|
+
extract_method_source_from_file(path, method_name)
|
|
263
231
|
end
|
|
264
232
|
|
|
265
233
|
private_class_method def self.find_concern_callbacks(model_name, data)
|
|
@@ -130,11 +130,11 @@ module RailsAiContext
|
|
|
130
130
|
if detail == "full"
|
|
131
131
|
public_methods.each do |m|
|
|
132
132
|
method_name = m.to_s.split("(").first
|
|
133
|
-
method_source =
|
|
133
|
+
method_source = extract_method_source_from_string(source, method_name)
|
|
134
134
|
if method_source
|
|
135
135
|
lines << "### #{m}"
|
|
136
136
|
lines << "```ruby"
|
|
137
|
-
lines << method_source
|
|
137
|
+
lines << method_source[:code]
|
|
138
138
|
lines << "```"
|
|
139
139
|
lines << ""
|
|
140
140
|
else
|
|
@@ -154,11 +154,11 @@ module RailsAiContext
|
|
|
154
154
|
class_methods.each do |m|
|
|
155
155
|
method_name = m.to_s.split("(").first
|
|
156
156
|
# Try both `def method_name` and `def self.method_name`
|
|
157
|
-
method_source =
|
|
157
|
+
method_source = extract_method_source_from_string(source, method_name) || extract_method_source_from_string(source, "self.#{method_name}")
|
|
158
158
|
if method_source
|
|
159
159
|
lines << "### #{m}"
|
|
160
160
|
lines << "```ruby"
|
|
161
|
-
lines << method_source
|
|
161
|
+
lines << method_source[:code]
|
|
162
162
|
lines << "```"
|
|
163
163
|
lines << ""
|
|
164
164
|
else
|
|
@@ -388,33 +388,6 @@ module RailsAiContext
|
|
|
388
388
|
[]
|
|
389
389
|
end
|
|
390
390
|
|
|
391
|
-
# Extract method source from raw source string using indentation-based matching
|
|
392
|
-
private_class_method def self.extract_method_source(source, method_name)
|
|
393
|
-
source_lines = source.lines
|
|
394
|
-
escaped = Regexp.escape(method_name.to_s)
|
|
395
|
-
# Don't use \b after ? or ! — they ARE word boundaries
|
|
396
|
-
pattern = if method_name.to_s.end_with?("?") || method_name.to_s.end_with?("!")
|
|
397
|
-
/\A\s*def\s+#{escaped}/
|
|
398
|
-
else
|
|
399
|
-
/\A\s*def\s+#{escaped}\b/
|
|
400
|
-
end
|
|
401
|
-
start_idx = source_lines.index { |l| l.match?(pattern) }
|
|
402
|
-
return nil unless start_idx
|
|
403
|
-
|
|
404
|
-
def_indent = source_lines[start_idx][/\A\s*/].length
|
|
405
|
-
result = []
|
|
406
|
-
|
|
407
|
-
source_lines[start_idx..].each_with_index do |line, i|
|
|
408
|
-
result << line.rstrip
|
|
409
|
-
break if i > 0 && line.match?(/\A\s{#{def_indent}}end\b/)
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
result.join("\n")
|
|
413
|
-
rescue => e
|
|
414
|
-
$stderr.puts "[rails-ai-context] extract_method_source failed: #{e.message}" if ENV["DEBUG"]
|
|
415
|
-
nil
|
|
416
|
-
end
|
|
417
|
-
|
|
418
391
|
private_class_method def self.find_includers(concern_name, root, concern_type)
|
|
419
392
|
includers = []
|
|
420
393
|
search_dirs = []
|
|
@@ -214,9 +214,7 @@ module RailsAiContext
|
|
|
214
214
|
# Normalize: try as-is, then singularized, then classified
|
|
215
215
|
ctx = cached_context
|
|
216
216
|
models = ctx[:models] || {}
|
|
217
|
-
key = models.keys
|
|
218
|
-
models.keys.find { |k| k.downcase == model_name.singularize.downcase } ||
|
|
219
|
-
models.keys.find { |k| k.downcase == model_name.classify.downcase }
|
|
217
|
+
key = fuzzy_find_key(models.keys, model_name)
|
|
220
218
|
|
|
221
219
|
resolved_name = key || model_name
|
|
222
220
|
|
|
@@ -41,8 +41,7 @@ module RailsAiContext
|
|
|
41
41
|
# Specific model — always full detail (strip whitespace for fuzzy input)
|
|
42
42
|
if model
|
|
43
43
|
model = model.strip
|
|
44
|
-
|
|
45
|
-
key = models.keys.find { |k| k.downcase == model.downcase || k.underscore == model_under } || model
|
|
44
|
+
key = fuzzy_find_key(models.keys, model) || model
|
|
46
45
|
data = models[key]
|
|
47
46
|
unless data
|
|
48
47
|
return not_found_response("Model", model, models.keys.sort,
|
|
@@ -246,13 +246,13 @@ module RailsAiContext
|
|
|
246
246
|
text_response("# #{path}\n\n```erb\n#{content}\n```")
|
|
247
247
|
end
|
|
248
248
|
|
|
249
|
-
# Strip inline SVG blocks
|
|
249
|
+
# Strip inline SVG blocks — they're visual noise that buries the signal AI needs.
|
|
250
250
|
# Replaces <svg ...>...</svg> with a compact placeholder.
|
|
251
251
|
private_class_method def self.strip_svg(content)
|
|
252
252
|
content.gsub(/<svg\b[^>]*>.*?<\/svg>/m, "<!-- svg icon -->")
|
|
253
253
|
end
|
|
254
254
|
|
|
255
|
-
# Compress repeated long Tailwind class strings
|
|
255
|
+
# Compress repeated long Tailwind class strings so the meaningful markup stays readable.
|
|
256
256
|
# Replaces duplicate class="..." with a CSS variable reference after first occurrence.
|
|
257
257
|
private_class_method def self.compress_tailwind(content)
|
|
258
258
|
class_counts = Hash.new(0)
|
data/server.json
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.crisnahine/rails-ai-context",
|
|
4
4
|
"title": "Rails AI Context",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Stop AI from guessing your Rails app. 39 read-only tools give coding agents ground truth — schema, models, routes, conventions — on demand. MCP or CLI. Standalone or in-Gemfile.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.7.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v4.
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v4.7.0/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.
|
|
4
|
+
version: 4.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -154,11 +154,12 @@ dependencies:
|
|
|
154
154
|
- !ruby/object:Gem::Version
|
|
155
155
|
version: '1.4'
|
|
156
156
|
description: |
|
|
157
|
-
rails-ai-context
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
(wrong columns, missing partials,
|
|
157
|
+
rails-ai-context turns your running Rails app into the source of truth for AI
|
|
158
|
+
coding assistants. Instead of guessing from training data or stale file reads,
|
|
159
|
+
agents query 39 live tools (via MCP server or CLI) to get your actual schema,
|
|
160
|
+
associations, routes, inherited filters, conventions, and test patterns.
|
|
161
|
+
Semantic validation catches cross-file errors (wrong columns, missing partials,
|
|
162
|
+
broken routes) before code runs — so AI writes correct code on the first try.
|
|
162
163
|
Auto-generates context files for Claude Code, Cursor, GitHub Copilot, and
|
|
163
164
|
OpenCode. Works standalone or in-Gemfile.
|
|
164
165
|
email:
|
|
@@ -332,6 +333,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
332
333
|
requirements: []
|
|
333
334
|
rubygems_version: 3.6.9
|
|
334
335
|
specification_version: 4
|
|
335
|
-
summary:
|
|
336
|
-
|
|
336
|
+
summary: Stop AI from guessing your Rails app. 39 tools give coding agents ground
|
|
337
|
+
truth — schema, models, routes, conventions — on demand. MCP or CLI.
|
|
337
338
|
test_files: []
|