rails-ai-context 0.6.0 → 0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03fa54ba8f83f42735ee47382cfac1c5007486dbe1287f6e06a5f5ba1c884dce
4
- data.tar.gz: '082eb0840ce4d1208d2acf33ebdede21437fe743709e404b3d3d87bba53038f0'
3
+ metadata.gz: a4aecabdd89f6a8bb9cb32189de791a98ebe632e3dcdb7406c4ae334eda9cb71
4
+ data.tar.gz: 546be87729c99383fd4eed6cb94df487f106fac6035e7a683902bf764694297b
5
5
  SHA512:
6
- metadata.gz: ab1c6d28c824bfcfb022178a385d158ecebcb5858eb6d2ee2e65d4b0cf032d33c314dfd9d614545c30deb20bb4d047be1f6db2e22a86818ef66ffb6fc294cdbe
7
- data.tar.gz: d4318f848e29b00ef5e4f19bc918e5be03d149ec2a59597e1c7ac7497628fef8a443fefc6e96bf117fbac7d548d38c3dcd62020b4d163a7c0860f390ad38eabf
6
+ metadata.gz: cf2ebe640b6dcf2d98b48dac91efb7fde81804f26d3f2b71e4f6ee59de23ed10d15e4e8b0ae5a4fe6ffc5dff2d4fb6904a99e269153da9a54909050b23d7c123
7
+ data.tar.gz: 9f62a8d2e6a9e73fad88bf5bbf86881976f80aa687cee579b2116d26c3d3719d4bbb82e4226d6193531efe6299abeff8272eee82190fe4703efd8e427865e133
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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.7.0] - 2026-03-19
9
+
10
+ ### Added
11
+
12
+ - **Detail levels on MCP tools** — `detail:"summary"`, `detail:"standard"` (default), `detail:"full"` on `rails_get_schema`, `rails_get_routes`, `rails_get_model_details`, `rails_get_controllers`. AI calls summary first, then drills down. Based on Anthropic's recommended MCP pattern.
13
+ - **Pagination** — `limit` and `offset` parameters on schema and routes tools for apps with hundreds of tables/routes.
14
+ - **Response size safety net** — Configurable hard cap (`max_tool_response_chars`, default 120K) on tool responses. Truncated responses include hints to use filters.
15
+ - **Compact CLAUDE.md** — New `:compact` context mode (default) generates ≤150 lines per Claude Code's official recommendation. Contains stack overview, key models, and MCP tool usage guide.
16
+ - **Full mode preserved** — `config.context_mode = :full` retains the existing full-dump behavior. Also available via `rails ai:context:full` or `CONTEXT_MODE=full`.
17
+ - **`.claude/rules/` generation** — Generates quick-reference files in `.claude/rules/` for schema and models. Auto-loaded by Claude Code alongside CLAUDE.md.
18
+ - **Cursor MDC rules** — Generates `.cursor/rules/*.mdc` files with YAML frontmatter (globs, alwaysApply). Project overview is always-on; model/controller rules auto-attach when working in matching directories. Legacy `.cursorrules` kept for backward compatibility.
19
+ - **Windsurf 6K compliance** — `.windsurfrules` is now hard-capped at 5,800 characters (within Windsurf's 6,000 char limit). Generates `.windsurf/rules/*.md` for the new rules format.
20
+ - **Copilot path-specific instructions** — Generates `.github/instructions/*.instructions.md` with `applyTo` frontmatter for model and controller contexts. Main `copilot-instructions.md` respects compact mode (≤500 lines).
21
+ - **`rails ai:context:full` task** — Dedicated rake task for full context dump.
22
+ - **Configurable limits** — `claude_max_lines` (default: 150), `max_tool_response_chars` (default: 120K).
23
+
24
+ ### Changed
25
+
26
+ - Default `context_mode` is now `:compact` (was implicitly `:full`). Existing behavior available via `config.context_mode = :full`.
27
+ - Tools default to `detail:"standard"` which returns bounded results, not unlimited.
28
+ - All tools return pagination hints when results are truncated.
29
+ - `.windsurfrules` now uses dedicated `WindsurfSerializer` instead of sharing `RulesSerializer` with Cursor.
30
+
8
31
  ## [0.6.0] - 2026-03-18
9
32
 
10
33
  ### Added
data/CLAUDE.md CHANGED
@@ -10,7 +10,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
10
10
  - `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
11
11
  - `lib/rails_ai_context/introspectors/` — 27 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database)
12
12
  - `lib/rails_ai_context/tools/` — 9 MCP tools using the official mcp SDK
13
- - `lib/rails_ai_context/serializers/` — Output formatters (claude, rules, copilot, markdown, JSON)
13
+ - `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, 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)
16
16
  - `lib/rails_ai_context/middleware.rb` — Rack middleware for auto-mounting MCP HTTP endpoint
@@ -32,11 +32,13 @@ structure to AI assistants via the Model Context Protocol (MCP).
32
32
  8. **Zeitwerk autoloading** — files loaded on-demand, not all upfront
33
33
  9. **Introspector presets** — `:standard` (9 core) default, `:full` (26) for power users
34
34
  10. **MCP auto-discovery** — `.mcp.json` generated by install generator
35
+ 11. **Compact by default** — context files ≤150 lines, MCP tools use `detail` parameter (summary/standard/full)
36
+ 12. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.windsurf/rules/`, `.github/instructions/`
35
37
 
36
38
  ## Testing
37
39
 
38
40
  ```bash
39
- bundle exec rspec # Run specs (282 examples)
41
+ bundle exec rspec # Run specs (348 examples)
40
42
  bundle exec rubocop # Lint
41
43
  ```
42
44
 
data/README.md CHANGED
@@ -69,22 +69,66 @@ This creates:
69
69
 
70
70
  Each file is tailored to the AI assistant's preferred format. **Commit these files.** Your entire team gets smarter AI assistance.
71
71
 
72
- ### 3. MCP Server (Auto-discovered)
72
+ ### 3. MCP Server
73
73
 
74
- The install generator creates a `.mcp.json` file that Claude Code and Cursor auto-detect **no manual config needed**. Just open your project.
74
+ The gem provides a live MCP server that AI clients can query on-demand for always-up-to-date introspection. Two transports are available:
75
75
 
76
- To start manually:
76
+ #### Auto-discovery (recommended)
77
+
78
+ The install generator creates a `.mcp.json` file in your project root:
79
+
80
+ ```json
81
+ {
82
+ "mcpServers": {
83
+ "rails-ai-context": {
84
+ "command": "bundle",
85
+ "args": ["exec", "rails", "ai:serve"]
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Claude Code** and **Cursor** auto-detect this file — no manual config needed. Just open your project and the MCP tools are available.
92
+
93
+ #### Manual setup per AI client
94
+
95
+ <details>
96
+ <summary><strong>Claude Code</strong></summary>
97
+
98
+ Already handled by `.mcp.json` auto-discovery. Or add manually:
77
99
 
78
100
  ```bash
79
- rails ai:serve
101
+ claude mcp add rails-ai-context -- bundle exec rails ai:serve
102
+ ```
103
+ </details>
104
+
105
+ <details>
106
+ <summary><strong>Claude Desktop</strong></summary>
107
+
108
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "rails-ai-context": {
114
+ "command": "bundle",
115
+ "args": ["exec", "rails", "ai:serve"],
116
+ "cwd": "/path/to/your/rails/app"
117
+ }
118
+ }
119
+ }
80
120
  ```
121
+ </details>
81
122
 
82
- Or configure manually in `~/.claude/claude_desktop_config.json`:
123
+ <details>
124
+ <summary><strong>Cursor</strong></summary>
125
+
126
+ Already handled by `.mcp.json` auto-discovery. Or add manually in Cursor Settings > MCP:
83
127
 
84
128
  ```json
85
129
  {
86
130
  "mcpServers": {
87
- "my-rails-app": {
131
+ "rails-ai-context": {
88
132
  "command": "bundle",
89
133
  "args": ["exec", "rails", "ai:serve"],
90
134
  "cwd": "/path/to/your/rails/app"
@@ -92,6 +136,31 @@ Or configure manually in `~/.claude/claude_desktop_config.json`:
92
136
  }
93
137
  }
94
138
  ```
139
+ </details>
140
+
141
+ #### HTTP transport (for remote clients)
142
+
143
+ For browser-based tools or remote AI clients, use the HTTP transport:
144
+
145
+ ```bash
146
+ rails ai:serve_http
147
+ ```
148
+
149
+ This starts a standalone HTTP server at `http://127.0.0.1:6029/mcp` using the Streamable HTTP transport.
150
+
151
+ Or auto-mount it inside your Rails app (no separate process needed):
152
+
153
+ ```ruby
154
+ # config/initializers/rails_ai_context.rb
155
+ RailsAiContext.configure do |config|
156
+ config.auto_mount = true
157
+ config.http_path = "/mcp" # default
158
+ end
159
+ ```
160
+
161
+ This inserts Rack middleware that handles MCP requests at `/mcp` and passes everything else through to your Rails app.
162
+
163
+ > **Note:** Both transports are **read-only** — they expose the same 9 introspection tools and never modify your application or database. The stdio transport (`ai:serve`) is recommended for local development; HTTP is for remote or programmatic access.
95
164
 
96
165
  ---
97
166
 
@@ -230,6 +299,24 @@ This gives AI assistants context about your frontend JavaScript alongside your b
230
299
 
231
300
  ---
232
301
 
302
+ ## Stack Compatibility
303
+
304
+ Works with every Rails architecture — the gem auto-detects what's relevant:
305
+
306
+ | Setup | Coverage | Notes |
307
+ |-------|----------|-------|
308
+ | Rails full-stack (ERB + Hotwire) | 27/27 | All introspectors relevant |
309
+ | Rails + Inertia.js (React/Vue) | ~22/27 | Views/Turbo partially useful, backend fully covered |
310
+ | Rails API + React/Next.js SPA | ~20/27 | Schema, models, routes, API, auth, jobs — all covered |
311
+ | Rails API + mobile app | ~20/27 | Same as SPA — backend introspection is identical |
312
+ | Rails engine (mountable gem) | ~15/27 | Core introspectors (schema, models, routes, gems) work |
313
+
314
+ Introspectors that target frontend concerns (views, Turbo, Stimulus, assets) are less relevant for API-only apps, but they degrade gracefully — they simply report nothing when those features aren't present.
315
+
316
+ > **Tip:** API-only apps can use the `:standard` preset (9 core introspectors) for faster introspection, or cherry-pick with `config.introspectors += %i[auth api]`. See [Configuration](#configuration).
317
+
318
+ ---
319
+
233
320
  ## Rake Tasks
234
321
 
235
322
  | Command | Description |
@@ -41,6 +41,17 @@ module RailsAiContext
41
41
  # Paths to exclude from code search
42
42
  # config.excluded_paths += %w[vendor/bundle]
43
43
 
44
+ # Context mode for generated files (CLAUDE.md, .cursorrules, etc.)
45
+ # :compact — smart, ≤150 lines, references MCP tools for details (default)
46
+ # :full — dumps everything into context files (good for small apps <30 models)
47
+ # config.context_mode = :compact
48
+
49
+ # Max lines for CLAUDE.md in compact mode
50
+ # config.claude_max_lines = 150
51
+
52
+ # Max response size for MCP tool results (chars). Safety net for large apps.
53
+ # config.max_tool_response_chars = 120_000
54
+
44
55
  # Auto-mount HTTP MCP endpoint at /mcp
45
56
  # config.auto_mount = false
46
57
  # config.http_path = "/mcp"
@@ -96,17 +107,21 @@ module RailsAiContext
96
107
  say " rails ai:serve # Start MCP server (stdio)"
97
108
  say " rails ai:inspect # Print introspection summary"
98
109
  say ""
99
- say "Supported AI assistants:", :yellow
100
- say " Claude Code → CLAUDE.md (rails ai:context:claude)"
101
- say " Cursor → .cursorrules (rails ai:context:cursor)"
102
- say " Windsurf → .windsurfrules (rails ai:context:windsurf)"
103
- say " GitHub Copilot → .github/copilot-instructions.md (rails ai:context:copilot)"
110
+ say "Generated files per AI tool:", :yellow
111
+ say " Claude Code → CLAUDE.md + .claude/rules/*.md"
112
+ say " Cursor → .cursorrules + .cursor/rules/*.mdc"
113
+ say " Windsurf → .windsurfrules + .windsurf/rules/*.md"
114
+ say " GitHub Copilot → .github/copilot-instructions.md + .github/instructions/*.instructions.md"
104
115
  say ""
105
116
  say "MCP auto-discovery:", :yellow
106
117
  say " .mcp.json is auto-detected by Claude Code and Cursor."
107
118
  say " No manual MCP config needed — just open your project."
108
119
  say ""
109
- say "Commit CLAUDE.md, .cursorrules, and .mcp.json so your team benefits!", :green
120
+ say "Context modes:", :yellow
121
+ say " rails ai:context # compact mode (default, smart for any app size)"
122
+ say " rails ai:context:full # full dump (good for small apps)"
123
+ say ""
124
+ say "Commit context files and .mcp.json so your team benefits!", :green
110
125
  end
111
126
  end
112
127
  end
@@ -33,6 +33,17 @@ module RailsAiContext
33
33
  # TTL in seconds for cached introspection (default: 30)
34
34
  attr_accessor :cache_ttl
35
35
 
36
+ # Context file generation mode
37
+ # :compact — ≤150 lines CLAUDE.md, references MCP tools for details (default)
38
+ # :full — current behavior, dumps everything into context files
39
+ attr_accessor :context_mode
40
+
41
+ # Max lines for generated CLAUDE.md (only applies in :compact mode)
42
+ attr_accessor :claude_max_lines
43
+
44
+ # Max characters for any single MCP tool response (safety net)
45
+ attr_accessor :max_tool_response_chars
46
+
36
47
  def initialize
37
48
  @server_name = "rails-ai-context"
38
49
  @server_version = RailsAiContext::VERSION
@@ -49,7 +60,10 @@ module RailsAiContext
49
60
  ActionText::RichText ActionText::EncryptedRichText
50
61
  ActionMailbox::InboundEmail ActionMailbox::Record
51
62
  ]
52
- @cache_ttl = 30
63
+ @cache_ttl = 30
64
+ @context_mode = :compact
65
+ @claude_max_lines = 150
66
+ @max_tool_response_chars = 120_000
53
67
  end
54
68
 
55
69
  def preset=(name)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Serializers
5
+ # Generates .claude/rules/ files for Claude Code auto-discovery.
6
+ # These provide quick-reference lists without bloating CLAUDE.md.
7
+ class ClaudeRulesSerializer
8
+ attr_reader :context
9
+
10
+ def initialize(context)
11
+ @context = context
12
+ end
13
+
14
+ # @param output_dir [String] Rails root path
15
+ # @return [Hash] { written: [paths], skipped: [paths] }
16
+ def call(output_dir)
17
+ rules_dir = File.join(output_dir, ".claude", "rules")
18
+ FileUtils.mkdir_p(rules_dir)
19
+
20
+ written = []
21
+ skipped = []
22
+
23
+ files = {
24
+ "rails-schema.md" => render_schema_reference,
25
+ "rails-models.md" => render_models_reference
26
+ }
27
+
28
+ files.each do |filename, content|
29
+ next unless content
30
+
31
+ filepath = File.join(rules_dir, filename)
32
+ if File.exist?(filepath) && File.read(filepath) == content
33
+ skipped << filepath
34
+ else
35
+ File.write(filepath, content)
36
+ written << filepath
37
+ end
38
+ end
39
+
40
+ { written: written, skipped: skipped }
41
+ end
42
+
43
+ private
44
+
45
+ def render_schema_reference
46
+ schema = context[:schema]
47
+ return nil unless schema.is_a?(Hash) && !schema[:error]
48
+ tables = schema[:tables] || {}
49
+ return nil if tables.empty?
50
+
51
+ lines = [
52
+ "# Database Tables (#{tables.size})",
53
+ "",
54
+ "For full column details, use the `rails_get_schema` MCP tool.",
55
+ "Call with `detail:\"summary\"` first, then `table:\"name\"` for specifics.",
56
+ ""
57
+ ]
58
+
59
+ tables.keys.sort.each do |name|
60
+ data = tables[name]
61
+ col_count = data[:columns]&.size || 0
62
+ pk = data[:primary_key] || "id"
63
+ lines << "- #{name} (#{col_count} cols, pk: #{pk})"
64
+ end
65
+
66
+ lines.join("\n")
67
+ end
68
+
69
+ def render_models_reference
70
+ models = context[:models]
71
+ return nil unless models.is_a?(Hash) && !models[:error]
72
+ return nil if models.empty?
73
+
74
+ lines = [
75
+ "# ActiveRecord Models (#{models.size})",
76
+ "",
77
+ "For full details, use `rails_get_model_details` MCP tool.",
78
+ "Call with no args to list all, then `model:\"Name\"` for specifics.",
79
+ ""
80
+ ]
81
+
82
+ models.keys.sort.each do |name|
83
+ data = models[name]
84
+ assocs = (data[:associations] || []).size
85
+ vals = (data[:validations] || []).size
86
+ table = data[:table_name]
87
+ line = "- #{name}"
88
+ line += " (table: #{table})" if table
89
+ line += " — #{assocs} assocs, #{vals} validations"
90
+ lines << line
91
+ end
92
+
93
+ lines.join("\n")
94
+ end
95
+ end
96
+ end
97
+ end
@@ -2,9 +2,219 @@
2
2
 
3
3
  module RailsAiContext
4
4
  module Serializers
5
- # Generates verbose markdown optimized for Claude Code.
6
- # Includes a behavioral rules section and full detail.
7
- class ClaudeSerializer < MarkdownSerializer
5
+ # Generates CLAUDE.md optimized for Claude Code.
6
+ # In :compact mode (default), produces ≤150 lines with MCP tool references.
7
+ # In :full mode, delegates to MarkdownSerializer with behavioral rules.
8
+ class ClaudeSerializer
9
+ attr_reader :context
10
+
11
+ def initialize(context)
12
+ @context = context
13
+ end
14
+
15
+ def call
16
+ if RailsAiContext.configuration.context_mode == :full
17
+ FullClaudeSerializer.new(context).call
18
+ else
19
+ render_compact
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def render_compact
26
+ lines = []
27
+ lines.concat(render_header)
28
+ lines.concat(render_stack_overview)
29
+ lines.concat(render_key_models)
30
+ lines.concat(render_notable_gems)
31
+ lines.concat(render_architecture)
32
+ lines.concat(render_mcp_guide)
33
+ lines.concat(render_conventions)
34
+ lines.concat(render_commands)
35
+ lines.concat(render_footer)
36
+
37
+ # Enforce max lines
38
+ max = RailsAiContext.configuration.claude_max_lines
39
+ if lines.size > max
40
+ lines = lines.first(max - 2)
41
+ lines << ""
42
+ lines << "_Context trimmed. Use MCP tools for full details._"
43
+ end
44
+
45
+ lines.join("\n")
46
+ end
47
+
48
+ def render_header
49
+ [
50
+ "# #{context[:app_name]} — AI Context",
51
+ "",
52
+ "> Rails #{context[:rails_version]} | Ruby #{context[:ruby_version]}",
53
+ "> Generated by rails-ai-context v#{RailsAiContext::VERSION}",
54
+ ""
55
+ ]
56
+ end
57
+
58
+ def render_stack_overview
59
+ lines = [ "## Stack" ]
60
+
61
+ schema = context[:schema]
62
+ if schema && !schema[:error]
63
+ lines << "- Database: #{schema[:adapter]} — #{schema[:total_tables]} tables"
64
+ end
65
+
66
+ models = context[:models]
67
+ if models.is_a?(Hash) && !models[:error]
68
+ lines << "- Models: #{models.size}"
69
+ end
70
+
71
+ routes = context[:routes]
72
+ if routes && !routes[:error]
73
+ ctrl_count = (routes[:by_controller] || {}).keys.size
74
+ lines << "- Routes: #{routes[:total_routes]} across #{ctrl_count} controllers"
75
+ end
76
+
77
+ auth = context[:auth]
78
+ if auth.is_a?(Hash) && !auth[:error]
79
+ parts = []
80
+ parts << "Devise" if auth.dig(:authentication, :devise)&.any?
81
+ parts << "Rails 8 auth" if auth.dig(:authentication, :rails_auth)
82
+ parts << "Pundit" if auth.dig(:authorization, :pundit)&.any?
83
+ parts << "CanCanCan" if auth.dig(:authorization, :cancancan)
84
+ lines << "- Auth: #{parts.join(' + ')}" if parts.any?
85
+ end
86
+
87
+ jobs = context[:jobs]
88
+ if jobs.is_a?(Hash) && !jobs[:error]
89
+ job_count = jobs[:jobs]&.size || 0
90
+ mailer_count = jobs[:mailers]&.size || 0
91
+ channel_count = jobs[:channels]&.size || 0
92
+ parts = []
93
+ parts << "#{job_count} jobs" if job_count > 0
94
+ parts << "#{mailer_count} mailers" if mailer_count > 0
95
+ parts << "#{channel_count} channels" if channel_count > 0
96
+ lines << "- Async: #{parts.join(', ')}" if parts.any?
97
+ end
98
+
99
+ migrations = context[:migrations]
100
+ if migrations.is_a?(Hash) && !migrations[:error]
101
+ pending = migrations[:pending]
102
+ lines << "- Migrations: #{migrations[:total]} total, #{pending&.size || 0} pending"
103
+ end
104
+
105
+ lines << ""
106
+ lines
107
+ end
108
+
109
+ def render_key_models
110
+ models = context[:models]
111
+ return [] unless models.is_a?(Hash) && !models[:error] && models.any?
112
+
113
+ max_show = 15
114
+ lines = [ "## Key models (#{models.size} total)" ]
115
+ models.keys.sort.first(max_show).each do |name|
116
+ data = models[name]
117
+ assoc_count = (data[:associations] || []).size
118
+ val_count = (data[:validations] || []).size
119
+ top_assocs = (data[:associations] || []).first(3).map { |a| "#{a[:type]} :#{a[:name]}" }.join(", ")
120
+ line = "- **#{name}**"
121
+ line += " (#{assoc_count}a, #{val_count}v)" if assoc_count > 0 || val_count > 0
122
+ line += " — #{top_assocs}" if top_assocs && !top_assocs.empty?
123
+ lines << line
124
+ end
125
+ lines << "- _...#{models.size - max_show} more (use `rails_get_model_details` tool)_" if models.size > max_show
126
+ lines << ""
127
+ lines
128
+ end
129
+
130
+ def render_notable_gems
131
+ gems = context[:gems]
132
+ return [] unless gems.is_a?(Hash) && !gems[:error]
133
+ notable = gems[:notable_gems] || gems[:notable] || gems[:detected] || []
134
+ return [] if notable.empty?
135
+
136
+ lines = [ "## Gems" ]
137
+ grouped = notable.group_by { |g| g[:category]&.to_s || "other" }
138
+ grouped.each do |category, gem_list|
139
+ names = gem_list.map { |g| g[:name] }.join(", ")
140
+ lines << "- **#{category}**: #{names}"
141
+ end
142
+ lines << ""
143
+ lines
144
+ end
145
+
146
+ def render_architecture
147
+ conv = context[:conventions]
148
+ return [] unless conv.is_a?(Hash) && !conv[:error]
149
+
150
+ arch = conv[:architecture] || []
151
+ patterns = conv[:patterns] || []
152
+ return [] if arch.empty? && patterns.empty?
153
+
154
+ lines = [ "## Architecture" ]
155
+ arch.each { |p| lines << "- #{p}" }
156
+ patterns.first(8).each { |p| lines << "- #{p}" }
157
+ lines << ""
158
+ lines
159
+ end
160
+
161
+ def render_mcp_guide
162
+ [
163
+ "## MCP tools (live introspection)",
164
+ "",
165
+ "This project exposes MCP tools. Always use `detail:\"summary\"` first,",
166
+ "then drill into specifics with `detail:\"full\"` + a filter.",
167
+ "",
168
+ "- `rails_get_schema` — tables, columns, indexes, foreign keys",
169
+ "- `rails_get_model_details` — associations, validations, scopes, enums",
170
+ "- `rails_get_routes` — HTTP verbs, paths, controller actions",
171
+ "- `rails_get_controllers` — actions, filters, strong params",
172
+ "- `rails_get_config` — cache, session, timezone, middleware",
173
+ "- `rails_get_test_info` — framework, factories, CI config",
174
+ "- `rails_get_gems` — categorized gem analysis",
175
+ "- `rails_get_conventions` — architecture and design patterns",
176
+ "- `rails_search_code` — regex search across the codebase",
177
+ ""
178
+ ]
179
+ end
180
+
181
+ def render_conventions
182
+ conv = context[:conventions]
183
+ return [] unless conv.is_a?(Hash) && !conv[:error]
184
+
185
+ config_files = conv[:config_files] || []
186
+ return [] if config_files.empty?
187
+
188
+ lines = [ "## Key config files" ]
189
+ config_files.first(5).each { |f| lines << "- `#{f}`" }
190
+ lines << ""
191
+ lines
192
+ end
193
+
194
+ def render_commands
195
+ [
196
+ "## Commands",
197
+ "- `bin/dev` — start dev server",
198
+ "- `bundle exec rspec` — run tests",
199
+ "- `rails db:migrate` — run pending migrations",
200
+ ""
201
+ ]
202
+ end
203
+
204
+ def render_footer
205
+ [
206
+ "## Rules",
207
+ "- Follow existing patterns and conventions",
208
+ "- Use the MCP tools to check schema before writing migrations",
209
+ "- Match existing code style",
210
+ "- Run tests after changes",
211
+ ""
212
+ ]
213
+ end
214
+ end
215
+
216
+ # Internal: full-mode Claude serializer (wraps MarkdownSerializer with behavioral rules)
217
+ class FullClaudeSerializer < MarkdownSerializer
8
218
  private
9
219
 
10
220
  def header
@@ -4,6 +4,7 @@ module RailsAiContext
4
4
  module Serializers
5
5
  # Orchestrates writing context files to disk in various formats.
6
6
  # Supports: CLAUDE.md, .cursorrules, .windsurfrules, .github/copilot-instructions.md, JSON
7
+ # Also generates split rule files for AI tools that support them.
7
8
  class ContextFileSerializer
8
9
  attr_reader :context, :format
9
10
 
@@ -50,6 +51,9 @@ module RailsAiContext
50
51
  end
51
52
  end
52
53
 
54
+ # Generate split rule files for all AI tools that support them
55
+ generate_split_rules(formats, output_dir, written, skipped)
56
+
53
57
  { written: written, skipped: skipped }
54
58
  end
55
59
 
@@ -59,11 +63,38 @@ module RailsAiContext
59
63
  case fmt
60
64
  when :json then JsonSerializer.new(context).call
61
65
  when :claude then ClaudeSerializer.new(context).call
62
- when :cursor, :windsurf then RulesSerializer.new(context).call
66
+ when :cursor then RulesSerializer.new(context).call
67
+ when :windsurf then WindsurfSerializer.new(context).call
63
68
  when :copilot then CopilotSerializer.new(context).call
64
69
  else MarkdownSerializer.new(context).call
65
70
  end
66
71
  end
72
+
73
+ def generate_split_rules(formats, output_dir, written, skipped)
74
+ if formats.include?(:claude)
75
+ result = ClaudeRulesSerializer.new(context).call(output_dir)
76
+ written.concat(result[:written])
77
+ skipped.concat(result[:skipped])
78
+ end
79
+
80
+ if formats.include?(:cursor)
81
+ result = CursorRulesSerializer.new(context).call(output_dir)
82
+ written.concat(result[:written])
83
+ skipped.concat(result[:skipped])
84
+ end
85
+
86
+ if formats.include?(:windsurf)
87
+ result = WindsurfRulesSerializer.new(context).call(output_dir)
88
+ written.concat(result[:written])
89
+ skipped.concat(result[:skipped])
90
+ end
91
+
92
+ if formats.include?(:copilot)
93
+ result = CopilotInstructionsSerializer.new(context).call(output_dir)
94
+ written.concat(result[:written])
95
+ skipped.concat(result[:skipped])
96
+ end
97
+ end
67
98
  end
68
99
  end
69
100
  end