rails-ai-context 0.12.0 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2116ee4ebdfbdfeeef7e91c6a4ebd9d618a84ed182cdc1d012b90c362b2a6cf
4
- data.tar.gz: e1a084f0f348a1c549c123aa305e75c51eb45632c1c46dec396c1e6f36b3dc4e
3
+ metadata.gz: f4f1bb24cd5ba8134913a1ac5ba7bbf28720a89aa1b80d9673692389d329a2ea
4
+ data.tar.gz: bc7afb4e33a1e2935beefa4e0bdefc879b7b1d486a9d8b694040d51abd764a9e
5
5
  SHA512:
6
- metadata.gz: 2d919dc0670b50c31496866b354f215413828af302cd4d63089577d64a3253f1e5150a326ef29de16d32a29a30b3ada440a16ca1500284109e92aceb4904014b
7
- data.tar.gz: f12682e40ce7733d1dd068fd47055ba74b40e474735dea7633ae38ae6b403d8027cfae89eb5ab3eab4e3d44f9acc10253148b467267305a22bd2e482f39b177d
6
+ metadata.gz: 92889b3f4f6be035241beb7cc8cd95633d44867b6c4b3e0c1e6bc2bc461c51ae649e91f49a07fa32d6d55ed22af408f271a4c744a7ceb8e970355ef2289b7d25
7
+ data.tar.gz: 8d4d623a38995ebf0b31dd673e4bfb80fb459b5ce16169562b7a1b3e0ffba2c6f2549f0508ae000c256e7eddc290cdaf234604f2eba70b302c9f0e8c9f59d4c8
data/CHANGELOG.md CHANGED
@@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.13.0] - 2026-03-20
9
+
10
+ ### Added
11
+
12
+ - **`rails_validate` MCP tool** — batch syntax validation for Ruby, ERB, and JavaScript files. Replaces separate `ruby -c`, ERB check, and `node -c` calls. Returns pass/fail for each file with error details. Uses `Open3.capture2e` (no shell execution). Falls back to brace-matching when Node.js is unavailable.
13
+ - **Model constants extraction** — introspects `STATUSES = %w[...]` style constants and includes them in model context.
14
+ - **Global before_actions in controller rules** — OpenCode AGENTS.md now shows ApplicationController before_actions.
15
+ - **Service objects and jobs listed** — OpenCode controller AGENTS.md now lists service objects and background jobs.
16
+ - **Validate spec** — 8 tests covering happy path, syntax errors, path traversal, MAX_FILES, unsupported types.
17
+
18
+ ### Security
19
+
20
+ - **Validate tool uses Open3 array form** — no shell execution for `ruby -c`, ERB compilation, or `node -c`. Fixed critical shell quoting bug in ERB validation that caused it to always fail.
21
+ - **File size limit** on JavaScript fallback validation (2MB).
22
+ - **`which node` check uses array form** — `system("which", "node")` instead of shell string.
23
+
24
+ ### Fixed
25
+
26
+ - ERB validation was broken due to shell quoting bug (backticks + nested quotes). Replaced with `Open3.capture2e("ruby", "-e", script, ARGV[0])`.
27
+ - Rubocop offenses in validate.rb (18 spacing issues auto-corrected).
28
+
29
+ ## [0.12.0] - 2026-03-20
30
+
31
+ ### Added
32
+
33
+ - **Design Token Introspector** — auto-detects CSS framework and extracts tokens from Tailwind v3/v4, Bootstrap/Sass, plain CSS custom properties, Webpacker-era stylesheets, and ViewComponent sidecar CSS. Tested across 8 CSS setups. Added to standard preset.
34
+ - **`rails_get_edit_context` MCP tool** — purpose-built for surgical edits. Returns code around a match point with line numbers. Replaces the Read + Edit workflow with a single call.
35
+ - **Line numbers in action source** — `rails_get_controllers(action: "index")` now returns start/end line numbers for targeted editing.
36
+ - **Model file structure** — `rails_get_model_details(model: "Cook")` now returns line ranges for each section (associations, validations, scopes, etc.).
37
+
38
+ ### Changed
39
+
40
+ - **MCP instructions updated** — "Use MCP for reference files (schema, routes, tests). Read directly if you'll edit." Prevents unnecessary double-reads.
41
+ - **UI pattern extractor rewritten** — semantic labels (primary/secondary/danger), deduplication, 12+ component types, color scheme + radius + form layout extraction, framework-agnostic.
42
+ - **Schema rules include column types** — `status:string, intake:jsonb` instead of just names. Also shows foreign keys, indexes, and enum values.
43
+ - **View standard detail enhanced** — shows partial fields, helper methods, and shared partials.
44
+
45
+ ### Security
46
+
47
+ - **File.realpath symlink protection** on all file-reading tools (get_view, get_edit_context, get_test_info, search_code).
48
+ - **File size limits** — 2MB on controllers/models/views, 500KB on test files.
49
+ - **Ripgrep flag injection prevention** — `--` separator before user pattern.
50
+ - **Nil guards** on all component rendering across 10 serializers.
51
+ - **Non-greedy regex** — ReDoS prevention in card/input/label pattern matching.
52
+ - **UTF-8 encoding safety** — all File.read calls handle binary/non-UTF-8 files gracefully.
53
+
54
+ ### Fixed
55
+
56
+ - Off-by-one in model structure section line ranges.
57
+ - Stimulus sort crash on nil controller name.
58
+ - Secondary button picking up disabled states (`cursor-not-allowed`).
59
+ - Progress bars misclassified as badges.
60
+ - Input detection picking up alert divs instead of actual inputs.
61
+
62
+ ## [0.11.0] - 2026-03-20
63
+
64
+ ### Added
65
+
66
+ - **UI pattern extraction** — scans all views for repeated CSS class patterns. Detects buttons, cards, inputs, labels, badges, links, headings, flashes, alerts. Added to ALL serializers (root files + split rules for Claude, Cursor, Windsurf, Copilot, OpenCode).
67
+ - **View partial structure** — `rails_get_view(detail: "standard")` shows model fields and helper methods used by each partial.
68
+ - **Schema column names** — `.claude/rules/rails-schema.md` shows key column names with types, foreign keys, indexes, and enum values. Keeps polymorphic `_type`, STI `type`, and soft-delete `deleted_at` columns.
69
+
8
70
  ## [0.10.0] - 2026-03-19
9
71
 
10
72
  ### Added
data/CLAUDE.md CHANGED
@@ -9,7 +9,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
9
9
  - `lib/rails_ai_context/configuration.rb` — User-facing config with presets (:standard, :full)
10
10
  - `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
11
11
  - `lib/rails_ai_context/introspectors/` — 29 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, view_templates, design_tokens, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database)
12
- - `lib/rails_ai_context/tools/` — 12 MCP tools using the official mcp SDK
12
+ - `lib/rails_ai_context/tools/` — 13 MCP tools using the official mcp SDK
13
13
  - `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON)
14
14
  - `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
15
15
  - `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
@@ -42,7 +42,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
42
42
  ## Testing
43
43
 
44
44
  ```bash
45
- bundle exec rspec # Run specs (481 examples)
45
+ bundle exec rspec # Run specs (491 examples)
46
46
  bundle exec rubocop # Lint
47
47
  ```
48
48
 
data/CONTRIBUTING.md CHANGED
@@ -19,7 +19,7 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
19
19
  ```
20
20
  lib/rails_ai_context/
21
21
  ├── introspectors/ # 29 introspectors (schema, models, routes, etc.)
22
- ├── tools/ # 12 MCP tools with detail levels and pagination
22
+ ├── tools/ # 13 MCP tools with detail levels and pagination
23
23
  ├── serializers/ # Per-assistant formatters (claude, opencode, cursor, windsurf, copilot, JSON)
24
24
  ├── server.rb # MCP server setup (stdio + HTTP)
25
25
  ├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
data/README.md CHANGED
@@ -27,7 +27,7 @@ Same task — *"Add status and date range filters to the Cooks index page"* —
27
27
 
28
28
  | Setup | Tokens | Saved | What it knows |
29
29
  |-------|--------|-------|---------------|
30
- | **rails-ai-context (full)** | **28,834** | **37%** | 12 MCP tools + generated docs + rules |
30
+ | **rails-ai-context (full)** | **28,834** | **37%** | 13 MCP tools + generated docs + rules |
31
31
  | rails-ai-context CLAUDE.md only | 33,106 | 27% | Generated docs + rules, no MCP tools |
32
32
  | Normal Claude `/init` | 40,700 | 11% | Generic CLAUDE.md only |
33
33
  | No rails-ai-context at all | 45,477 | baseline | Nothing — discovers everything from scratch |
@@ -95,9 +95,9 @@ The install generator creates `.mcp.json` for auto-discovery — Claude Code and
95
95
 
96
96
  ---
97
97
 
98
- ## 12 Live MCP Tools
98
+ ## 13 Live MCP Tools
99
99
 
100
- The gem exposes **12 read-only tools** via MCP that AI clients call on-demand:
100
+ The gem exposes **13 read-only tools** via MCP that AI clients call on-demand:
101
101
 
102
102
  | Tool | What it returns |
103
103
  |------|----------------|
@@ -113,6 +113,7 @@ The gem exposes **12 read-only tools** via MCP that AI clients call on-demand:
113
113
  | `rails_get_view` | View templates, partials, Stimulus references |
114
114
  | `rails_get_stimulus` | Stimulus controllers — targets, values, actions, outlets |
115
115
  | `rails_get_edit_context` | Surgical edit helper — returns code around a match with line numbers |
116
+ | `rails_validate` | Batch syntax validation for Ruby, ERB, and JavaScript files |
116
117
 
117
118
  ### Smart Detail Levels
118
119
 
@@ -352,10 +353,10 @@ Works with every Rails architecture — auto-detects what's relevant:
352
353
  | Setup | Coverage | Notes |
353
354
  |-------|----------|-------|
354
355
  | Rails full-stack (ERB + Hotwire) | 29/29 | All introspectors relevant |
355
- | Rails + Inertia.js (React/Vue) | ~22/27 | Views/Turbo partially useful, backend fully covered |
356
- | Rails API + React/Next.js SPA | ~20/27 | Schema, models, routes, API, auth, jobs — all covered |
357
- | Rails API + mobile app | ~20/27 | Same as SPA — backend introspection is identical |
358
- | Rails engine (mountable gem) | ~15/27 | Core introspectors (schema, models, routes, gems) work |
356
+ | Rails + Inertia.js (React/Vue) | ~22/29 | Views/Turbo partially useful, backend fully covered |
357
+ | Rails API + React/Next.js SPA | ~20/29 | Schema, models, routes, API, auth, jobs — all covered |
358
+ | Rails API + mobile app | ~20/29 | Same as SPA — backend introspection is identical |
359
+ | Rails engine (mountable gem) | ~15/29 | Core introspectors (schema, models, routes, gems) work |
359
360
 
360
361
  Frontend introspectors (views, Turbo, Stimulus, assets) degrade gracefully — they report nothing when those features aren't present.
361
362
 
@@ -390,7 +391,7 @@ The gem parses `db/schema.rb` as text when no database is connected. Works in CI
390
391
  ```bash
391
392
  git clone https://github.com/crisnahine/rails-ai-context.git
392
393
  cd rails-ai-context && bundle install
393
- bundle exec rspec # 481 examples
394
+ bundle exec rspec # 491 examples
394
395
  bundle exec rubocop # Lint
395
396
  ```
396
397
 
data/demo_script.sh CHANGED
@@ -8,7 +8,7 @@ echo 'Fetching gem metadata from https://rubygems.org...'
8
8
  sleep 0.3
9
9
  echo 'Resolving dependencies...'
10
10
  sleep 0.3
11
- echo 'Installing rails-ai-context 0.12.0'
11
+ echo 'Installing rails-ai-context 0.13.0'
12
12
  echo ''
13
13
  sleep 1
14
14
 
data/docs/GUIDE.md CHANGED
@@ -246,7 +246,7 @@ rails ai:context:claude # Use this instead (no quoting needed)
246
246
 
247
247
  ## MCP Tools — Full Reference
248
248
 
249
- All 12 tools are **read-only** and **idempotent** — they never modify your application or database.
249
+ All 13 tools are **read-only** and **idempotent** — they never modify your application or database.
250
250
 
251
251
  ### rails_get_schema
252
252
 
@@ -568,7 +568,7 @@ RailsAiContext.configure do |config|
568
568
  end
569
569
  ```
570
570
 
571
- Both transports are **read-only** — they expose the same 12 tools and never modify your app.
571
+ Both transports are **read-only** — they expose the same 13 tools and never modify your app.
572
572
 
573
573
  ---
574
574
 
@@ -673,7 +673,7 @@ All split rules include an app overview file, so no context is lost when root fi
673
673
 
674
674
  ## Introspectors — Full List
675
675
 
676
- ### Standard preset (9 introspectors)
676
+ ### Standard preset (12 introspectors)
677
677
 
678
678
  These run by default. Fast and cover core Rails structure.
679
679
 
@@ -689,7 +689,7 @@ These run by default. Fast and cover core Rails structure.
689
689
  | `tests` | Test framework (rspec/minitest), factories/fixtures with locations and counts, system tests, CI config files, coverage tool, test helpers, VCR cassettes. |
690
690
  | `migrations` | Total count, schema version, pending migrations, recent migration history with detected actions (create_table, add_column, etc.), migration statistics. |
691
691
 
692
- ### Full preset (29 introspectors)
692
+ ### Full preset (28 introspectors)
693
693
 
694
694
  Includes all standard introspectors plus:
695
695
 
@@ -813,10 +813,10 @@ OpenCode uses **per-directory lazy-loading**: when the agent reads a file, it wa
813
813
  | Setup | Coverage | Notes |
814
814
  |-------|----------|-------|
815
815
  | Rails full-stack (ERB + Hotwire) | 29/29 | All introspectors relevant |
816
- | Rails + Inertia.js (React/Vue) | ~22/27 | Views/Turbo partially useful, backend fully covered |
817
- | Rails API + React/Next.js SPA | ~20/27 | Schema, models, routes, API, auth, jobs — all covered |
818
- | Rails API + mobile app | ~20/27 | Same as SPA — backend introspection is identical |
819
- | Rails engine (mountable gem) | ~15/27 | Core introspectors (schema, models, routes, gems) work |
816
+ | Rails + Inertia.js (React/Vue) | ~22/29 | Views/Turbo partially useful, backend fully covered |
817
+ | Rails API + React/Next.js SPA | ~20/29 | Schema, models, routes, API, auth, jobs — all covered |
818
+ | Rails API + mobile app | ~20/29 | Same as SPA — backend introspection is identical |
819
+ | Rails engine (mountable gem) | ~15/29 | Core introspectors (schema, models, routes, gems) work |
820
820
 
821
821
  Frontend introspectors (views, Turbo, Stimulus, assets) degrade gracefully — they report nothing when those features aren't present.
822
822
 
@@ -179,6 +179,12 @@ module RailsAiContext
179
179
  macros[:serialize] = source.scan(/\bserialize\s+:(\w+)/).flatten if source.match?(/\bserialize\s+:/)
180
180
  macros[:store] = source.scan(/\bstore(?:_accessor)?\s+:(\w+)/).flatten if source.match?(/\bstore(?:_accessor)?\s+:/)
181
181
 
182
+ # Constants with value lists (e.g. STATUSES = %w[pending completed])
183
+ constants = source.scan(/\b([A-Z][A-Z_]+)\s*=\s*%[wi]\[([^\]]+)\]/).map do |name, values|
184
+ { name: name, values: values.split }
185
+ end
186
+ macros[:constants] = constants if constants.any?
187
+
182
188
  # Delegations
183
189
  delegations = source.scan(/\bdelegate\s+(.+?),\s*to:\s*:(\w+)/m).map do |methods_str, target|
184
190
  { methods: methods_str.scan(/:(\w+)/).flatten, to: target }
@@ -77,6 +77,19 @@ module RailsAiContext
77
77
  (conv[:architecture] || []).first(5).each { |p| lines << "- #{p}" }
78
78
  end
79
79
 
80
+ # ApplicationController before_actions — apply to all controllers
81
+ begin
82
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
83
+ app_ctrl_file = File.join(root, "app", "controllers", "application_controller.rb")
84
+ if File.exist?(app_ctrl_file)
85
+ source = File.read(app_ctrl_file)
86
+ before_actions = source.scan(/before_action\s+:(\w+)/).flatten
87
+ if before_actions.any?
88
+ lines << "" << "**Global before_actions:** #{before_actions.join(', ')}"
89
+ end
90
+ end
91
+ rescue; end
92
+
80
93
  lines << ""
81
94
  lines << "Use MCP tools for detailed data. Start with `detail:\"summary\"`."
82
95
 
@@ -92,8 +105,8 @@ module RailsAiContext
92
105
  lines = [
93
106
  "# Database Tables (#{tables.size})",
94
107
  "",
95
- "DO NOT read db/schema.rb directly. Use the `rails_get_schema` MCP tool instead.",
96
- "Call with `detail:\"summary\"` first, then `table:\"name\"` for specifics.",
108
+ "All columns with types are listed below no need to read db/schema.rb.",
109
+ "For indexes, foreign keys, or constraints, use `rails_get_schema(table:\"name\")`.",
97
110
  ""
98
111
  ]
99
112
 
@@ -118,8 +131,7 @@ module RailsAiContext
118
131
  true
119
132
  end
120
133
 
121
- col_sample = key_cols.first(10).map { |c| "#{c[:name]}:#{c[:type]}" }
122
- col_sample << "..." if key_cols.size > 10
134
+ col_sample = key_cols.map { |c| "#{c[:name]}:#{c[:type]}" }
123
135
  col_str = col_sample.any? ? " — #{col_sample.join(', ')}" : ""
124
136
 
125
137
  # Foreign keys
@@ -158,8 +170,8 @@ module RailsAiContext
158
170
  lines = [
159
171
  "# ActiveRecord Models (#{models.size})",
160
172
  "",
161
- "DO NOT read model files to check associations/validations. Use `rails_get_model_details` MCP tool instead.",
162
- "Call with `detail:\"summary\"` first, then `model:\"Name\"` for specifics.",
173
+ "Check this file first for associations, scopes, constants, and validations.",
174
+ "If you need more detail (callbacks, methods, business logic), use `rails_get_model_details(model:\"Name\")` or Read the file directly.",
163
175
  ""
164
176
  ]
165
177
 
@@ -172,6 +184,39 @@ module RailsAiContext
172
184
  line += " (table: #{table})" if table
173
185
  line += " — #{assocs} assocs, #{vals} validations"
174
186
  lines << line
187
+
188
+ # Include app-specific concerns (filter out Rails/gem internals)
189
+ noise = %w[GeneratedAssociationMethods GeneratedAttributeMethods Kernel PP ObjectMixin
190
+ GlobalID Bullet ActionText Turbo ActiveStorage JSON]
191
+ concerns = (data[:concerns] || []).select { |c|
192
+ !noise.any? { |n| c.include?(n) } && !c.start_with?("Devise") && !c.include?("::")
193
+ }
194
+ lines << " concerns: #{concerns.join(', ')}" if concerns.any?
195
+
196
+ # Include scopes so agents know available query methods
197
+ scopes = data[:scopes] || []
198
+ lines << " scopes: #{scopes.join(', ')}" if scopes.any?
199
+
200
+ # Include app-specific instance methods (filter out Rails-generated ones)
201
+ generated_patterns = %w[build_ create_ reload_ reset_ _changed? _previously_changed?
202
+ _ids _ids= _before_last_save _before_type_cast _came_from_user?
203
+ _for_database _in_database _was]
204
+ methods = (data[:instance_methods] || []).reject { |m|
205
+ generated_patterns.any? { |p| m.include?(p) } || m.end_with?("=")
206
+ }.first(10)
207
+ lines << " methods: #{methods.join(', ')}" if methods.any?
208
+
209
+ # Include constants (e.g. STATUSES, MODES) so agents know valid values
210
+ constants = data[:constants] || []
211
+ constants.each do |c|
212
+ lines << " #{c[:name]}: #{c[:values].join(', ')}"
213
+ end
214
+
215
+ # Include enums so agents know valid values
216
+ enums = data[:enums] || {}
217
+ enums.each do |attr, values|
218
+ lines << " #{attr}: #{values.join(', ')}"
219
+ end
175
220
  end
176
221
 
177
222
  lines.join("\n")
@@ -210,6 +255,45 @@ module RailsAiContext
210
255
  lines << "- Grid: #{fl[:grid]}" if fl[:grid]
211
256
  end
212
257
 
258
+ # Shared partials — so agents reuse them instead of recreating
259
+ begin
260
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
261
+ shared_dir = File.join(root, "app", "views", "shared")
262
+ if Dir.exist?(shared_dir)
263
+ partials = Dir.glob(File.join(shared_dir, "_*.html.erb"))
264
+ .map { |f| File.basename(f) }
265
+ .sort
266
+ if partials.any?
267
+ lines << "" << "## Shared partials (app/views/shared/)"
268
+ partials.each { |p| lines << "- #{p}" }
269
+ end
270
+ end
271
+ rescue; end
272
+
273
+ # Helpers — so agents use existing helpers instead of creating new ones
274
+ begin
275
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
276
+ helper_file = File.join(root, "app", "helpers", "application_helper.rb")
277
+ if File.exist?(helper_file)
278
+ helper_methods = File.read(helper_file).scan(/def\s+(\w+)/).flatten
279
+ if helper_methods.any?
280
+ lines << "" << "## Helpers (ApplicationHelper)"
281
+ lines << helper_methods.map { |m| "- #{m}" }.join("\n")
282
+ end
283
+ end
284
+ rescue; end
285
+
286
+ # Stimulus controllers — so agents reuse existing controllers
287
+ stim = context[:stimulus]
288
+ if stim.is_a?(Hash) && !stim[:error]
289
+ controllers = stim[:controllers] || []
290
+ if controllers.any?
291
+ names = controllers.map { |c| c[:name] || c[:file]&.gsub("_controller.js", "") }.compact.sort
292
+ lines << "" << "## Stimulus controllers"
293
+ lines << names.join(", ")
294
+ end
295
+ end
296
+
213
297
  lines.join("\n")
214
298
  end
215
299
 
@@ -218,26 +302,30 @@ module RailsAiContext
218
302
  "# Rails MCP Tools — ALWAYS Use These First",
219
303
  "",
220
304
  "IMPORTANT: This project has live MCP tools that return parsed, up-to-date data.",
221
- "ALWAYS use these tools BEFORE reading files like db/schema.rb, config/routes.rb, or model source files.",
222
- "The tools return structured, token-efficient summaries. Reading raw files wastes tokens and may be stale.",
305
+ "Use these tools for reference-only files (schema, routes, tests). For files you will edit, Read them directly.",
306
+ "The tools return structured, token-efficient summaries with line numbers.",
223
307
  "",
224
308
  "## When to use MCP tools vs Read",
225
309
  "- Use MCP for files you WON'T edit (schema, routes, understanding context)",
226
310
  "- For files you WILL edit, just Read them directly — you need Read before Edit anyway",
227
311
  "- Use MCP for orientation (summary calls) on large codebases",
228
312
  "- Skip MCP when CLAUDE.md + rules already have the info you need",
313
+ "- Do NOT call rails_get_model_details if CLAUDE.md already shows the model's associations and column types",
314
+ "- Do NOT call rails_get_stimulus just to check if Stimulus exists — CLAUDE.md confirms it",
229
315
  "",
230
- "## MCP tools return line numbers — use for surgical edits",
231
- "- `rails_get_controllers(action:\"index\")` returns source with line numbers",
232
- "- `rails_get_model_details(model:\"Cook\")` returns file structure with line ranges",
233
- "- `rails_get_view(path:\"cooks/index.html.erb\")` returns full template",
316
+ "## After editing ALWAYS use rails_validate (not Bash)",
317
+ "- `rails_validate(files:[\"app/models/cook.rb\", \"app/controllers/cooks_controller.rb\", \"app/views/cooks/index.html.erb\"])` one call checks all",
318
+ "- Do NOT run `ruby -c`, `erb` checks, or `node -c` separately — use rails_validate instead",
319
+ "- Do NOT re-read files to verify edits. Trust your Edit and validate syntax only.",
234
320
  "",
235
- "## Reference-only files — ALWAYS use MCP instead of Read",
236
- "- DO NOT read db/schema.rb — use `rails_get_schema` instead",
237
- "- DO NOT read config/routes.rb — use `rails_get_routes` instead",
238
- "- DO NOT read test files for patterns use `rails_get_test_info(detail:\"full\")` instead",
321
+ "## Reference-only files — check rules first, then MCP or Read if needed",
322
+ "- db/schema.rb — column names and types are in rails-schema.md rules. Read only if you need constraints/defaults.",
323
+ "- config/routes.rb — use `rails_get_routes` for reference. Read directly if you'll add routes.",
324
+ "- Model files scopes, constants, enums are in rails-models.md rules. Read for business logic/methods.",
325
+ "- app/javascript/controllers/index.js — Stimulus auto-registers controllers. No need to read.",
326
+ "- Test files — use `rails_get_test_info(detail:\"full\")` for patterns.",
239
327
  "",
240
- "## Tools (12)",
328
+ "## Tools (13)",
241
329
  "",
242
330
  "**rails_get_schema** — database tables, columns, indexes, foreign keys",
243
331
  "- `rails_get_schema(detail:\"summary\")` — all tables with column counts",
@@ -271,6 +359,9 @@ module RailsAiContext
271
359
  "**rails_get_edit_context** — surgical edit helper with line numbers",
272
360
  "- `rails_get_edit_context(file:\"app/models/cook.rb\", near:\"scope\")` — returns code around match with line numbers",
273
361
  "",
362
+ "**rails_validate** — syntax checker for edited files",
363
+ "- `rails_validate(files:[\"app/models/cook.rb\"])` — checks Ruby, ERB, JS syntax in one call",
364
+ "",
274
365
  "**rails_get_config** — cache store, session, timezone, middleware, initializers",
275
366
  "**rails_get_gems** — notable gems categorized by function",
276
367
  "**rails_get_conventions** — architecture patterns, directory structure",
@@ -122,6 +122,14 @@ module RailsAiContext
122
122
  line += " (#{assoc_count}a, #{val_count}v)" if assoc_count > 0 || val_count > 0
123
123
  line += " — #{top_assocs}" if top_assocs && !top_assocs.empty?
124
124
  lines << line
125
+ scopes = (data[:scopes] || [])
126
+ constants = (data[:constants] || [])
127
+ if scopes.any? || constants.any?
128
+ extras = []
129
+ extras << "scopes: #{scopes.join(', ')}" if scopes.any?
130
+ constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
131
+ lines << " #{extras.join(' | ')}"
132
+ end
125
133
  end
126
134
  lines << "- _...#{models.size - max_show} more (use `rails_get_model_details` tool)_" if models.size > max_show
127
135
  lines << ""
@@ -155,6 +163,30 @@ module RailsAiContext
155
163
  lines = [ "## Architecture" ]
156
164
  arch.each { |p| lines << "- #{p}" }
157
165
  patterns.first(8).each { |p| lines << "- #{p}" }
166
+
167
+ # List service objects and jobs from conventions directory_structure
168
+ dir_struct = conv[:directory_structure] || {}
169
+
170
+ if dir_struct["app/services"]
171
+ begin
172
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
173
+ service_files = Dir.glob(File.join(root, "app", "services", "*.rb"))
174
+ .map { |f| File.basename(f, ".rb").camelize }
175
+ .reject { |s| s == "ApplicationService" }
176
+ lines << "" << "**Services:** #{service_files.join(', ')}" if service_files.any?
177
+ rescue; end
178
+ end
179
+
180
+ if dir_struct["app/jobs"]
181
+ begin
182
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
183
+ job_files = Dir.glob(File.join(root, "app", "jobs", "*.rb"))
184
+ .map { |f| File.basename(f, ".rb").camelize }
185
+ .reject { |j| j == "ApplicationJob" }
186
+ lines << "**Jobs:** #{job_files.join(', ')}" if job_files.any?
187
+ rescue; end
188
+ end
189
+
158
190
  lines << ""
159
191
  lines
160
192
  end
@@ -193,7 +225,7 @@ module RailsAiContext
193
225
 
194
226
  def render_mcp_guide # rubocop:disable Metrics/MethodLength
195
227
  [
196
- "## MCP Tools (12) — ALWAYS Use These First",
228
+ "## MCP Tools (13) — ALWAYS Use These First",
197
229
  "",
198
230
  "Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
199
231
  "MCP tools return line numbers. Start with `detail:\"summary\"`.",
@@ -206,6 +238,7 @@ module RailsAiContext
206
238
  "- `rails_get_stimulus(detail:\"summary\")` → `(controller:\"name\")` — targets, actions, values",
207
239
  "- `rails_get_test_info(detail:\"full\")` — fixtures, factories, helpers; `(model:\"Cook\")` — tests",
208
240
  "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
241
+ "- `rails_validate(files:[\"path/to/file.rb\"])` — validate Ruby, ERB, JS syntax in one call",
209
242
  ""
210
243
  ]
211
244
  end
@@ -240,6 +273,9 @@ module RailsAiContext
240
273
  "- Use the MCP tools to check schema before writing migrations",
241
274
  "- Match existing code style",
242
275
  "- Run tests after changes",
276
+ "- After editing, ALWAYS use `rails_validate(files:[...])` — do NOT use separate ruby -c / erb / node -c calls",
277
+ "- Do NOT re-read files to verify edits — trust your Edit, validate syntax only",
278
+ "- Stimulus controllers auto-register — no manual import in controllers/index.js needed",
243
279
  ""
244
280
  ]
245
281
  end
@@ -78,6 +78,41 @@ module RailsAiContext
78
78
  (conv[:architecture] || []).first(5).each { |p| lines << "- #{p}" }
79
79
  end
80
80
 
81
+ # List service objects
82
+ begin
83
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
84
+ services_dir = File.join(root, "app", "services")
85
+ if Dir.exist?(services_dir)
86
+ service_files = Dir.glob(File.join(services_dir, "*.rb"))
87
+ .map { |f| File.basename(f, ".rb").camelize }
88
+ .reject { |s| s == "ApplicationService" }
89
+ lines << "- Services: #{service_files.join(', ')}" if service_files.any?
90
+ end
91
+ rescue; end
92
+
93
+ # List jobs
94
+ begin
95
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
96
+ jobs_dir = File.join(root, "app", "jobs")
97
+ if Dir.exist?(jobs_dir)
98
+ job_files = Dir.glob(File.join(jobs_dir, "*.rb"))
99
+ .map { |f| File.basename(f, ".rb").camelize }
100
+ .reject { |j| j == "ApplicationJob" }
101
+ lines << "- Jobs: #{job_files.join(', ')}" if job_files.any?
102
+ end
103
+ rescue; end
104
+
105
+ # ApplicationController before_actions
106
+ begin
107
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
108
+ app_ctrl = File.join(root, "app", "controllers", "application_controller.rb")
109
+ if File.exist?(app_ctrl)
110
+ source = File.read(app_ctrl)
111
+ before_actions = source.scan(/before_action\s+:(\w+)/).flatten
112
+ lines << "" << "**Global before_actions:** #{before_actions.join(', ')}" if before_actions.any?
113
+ end
114
+ rescue; end
115
+
81
116
  lines << ""
82
117
  lines << "Use MCP tools for detailed data. Start with `detail:\"summary\"`."
83
118
 
@@ -95,7 +130,7 @@ module RailsAiContext
95
130
  "",
96
131
  "# ActiveRecord Models (#{models.size})",
97
132
  "",
98
- "Use `rails_get_model_details` MCP tool for full details.",
133
+ "Check here first for scopes, constants, associations. Read model files for business logic/methods.",
99
134
  ""
100
135
  ]
101
136
 
@@ -103,6 +138,14 @@ module RailsAiContext
103
138
  data = models[name]
104
139
  assocs = (data[:associations] || []).size
105
140
  lines << "- #{name} (#{assocs} associations)"
141
+ scopes = (data[:scopes] || [])
142
+ constants = (data[:constants] || [])
143
+ if scopes.any? || constants.any?
144
+ extras = []
145
+ extras << "scopes: #{scopes.join(', ')}" if scopes.any?
146
+ constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
147
+ lines << " #{extras.join(' | ')}"
148
+ end
106
149
  end
107
150
 
108
151
  lines << "- ...#{models.size - 30} more" if models.size > 30
@@ -151,6 +194,43 @@ module RailsAiContext
151
194
  ]
152
195
  components.first(15).each { |c| next unless c[:label] && c[:classes]; lines << "- #{c[:label]}: `#{c[:classes]}`" }
153
196
 
197
+ # Shared partials
198
+ begin
199
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
200
+ shared_dir = File.join(root, "app", "views", "shared")
201
+ if Dir.exist?(shared_dir)
202
+ partials = Dir.glob(File.join(shared_dir, "_*.html.erb")).map { |f| File.basename(f) }.sort
203
+ if partials.any?
204
+ lines << "" << "## Shared partials"
205
+ partials.each { |p| lines << "- #{p}" }
206
+ end
207
+ end
208
+ rescue; end
209
+
210
+ # Helpers
211
+ begin
212
+ root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
213
+ helper_file = File.join(root, "app", "helpers", "application_helper.rb")
214
+ if File.exist?(helper_file)
215
+ helper_methods = File.read(helper_file).scan(/def\s+(\w+)/).flatten
216
+ if helper_methods.any?
217
+ lines << "" << "## Helpers (ApplicationHelper)"
218
+ helper_methods.each { |m| lines << "- #{m}" }
219
+ end
220
+ end
221
+ rescue; end
222
+
223
+ # Stimulus controllers
224
+ stim = context[:stimulus]
225
+ if stim.is_a?(Hash) && !stim[:error]
226
+ controllers = stim[:controllers] || []
227
+ if controllers.any?
228
+ names = controllers.map { |c| c[:name] || c[:file]&.gsub("_controller.js", "") }.compact.sort
229
+ lines << "" << "## Stimulus controllers"
230
+ lines << names.join(", ")
231
+ end
232
+ end
233
+
154
234
  lines.join("\n")
155
235
  end
156
236
 
@@ -160,7 +240,7 @@ module RailsAiContext
160
240
  "applyTo: \"**/*\"",
161
241
  "---",
162
242
  "",
163
- "# Rails MCP Tools (12) — Use These First",
243
+ "# Rails MCP Tools (13) — Use These First",
164
244
  "",
165
245
  "Use MCP for reference files (schema, routes, tests). Read directly if you'll edit.",
166
246
  "MCP tools return line numbers for surgical edits.",
@@ -173,7 +253,11 @@ module RailsAiContext
173
253
  "- `rails_get_view(controller:\"cooks\")` — view list; `(path:\"cooks/index.html.erb\")` — content",
174
254
  "- `rails_get_stimulus(detail:\"summary\")` → `(controller:\"name\")` — targets, actions, values",
175
255
  "- `rails_get_test_info(detail:\"full\")` — fixtures, factories, helpers; `(model:\"Cook\")` — existing tests",
176
- "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`"
256
+ "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_search_code`",
257
+ "- `rails_get_edit_context(file:\"path\", near:\"keyword\")` — surgical edit context with line numbers",
258
+ "- `rails_validate(files:[\"path\"])` — validate Ruby, ERB, JS syntax in one call",
259
+ "",
260
+ "After editing: use rails_validate to check syntax. Do NOT re-read files to verify."
177
261
  ]
178
262
 
179
263
  lines.join("\n")