rails-ai-context 4.5.2 → 4.6.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: d53c26bdd73eac84be7e9a49ea08c2a50dd0230f969cb29ae240cf2d08b9e4c3
4
- data.tar.gz: a65a518fab277ff246401cffb32701fb36048f65034b068e20f4b3fe76ddc4eb
3
+ metadata.gz: 7a6993d2929f87b6f9c29dffd5f16124a1d3ccb15ac3f8ba8f100364bf0feb06
4
+ data.tar.gz: '08c5fe8f51d126c1f7cc52ca9cc8ca0dc2edf5d1dba5f569b8ec5afaeecbb0b4'
5
5
  SHA512:
6
- metadata.gz: 0504e0e68f018d325189ccdfd153b7097d1204c906241a9a0dfea41586272b0c2ae296f81704c4e6c4ac9bb414aaab83c5de35e226b7aabff52b984d1bec451e
7
- data.tar.gz: f2c8a762bd3520edd09e719337043be0a9b7158d0099076118226012cea980ca1877c48459d78810c8e9a1c9d91bf526aad4a2c334521e19c5ac7b715bc44b1e
6
+ metadata.gz: 94ec45e9fd089ba075c1b1c2457bfbf6fc9f2fc9c6da31b2ccafc7e51e64ca9579a390e25b56816a0c1bf4023f3ecbf092124082d53681029dcea3a5446065f5
7
+ data.tar.gz: fbab834f047ab3ae3392501964b6e0de1c090af063872473df3e3b074a161536c306cf4afd51daf8e44df8f127ddbb94fd885b15f21610f0884e27159ae118e3
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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.6.0] — 2026-04-04
9
+
10
+ ### Added
11
+ - **Integration test suite** — 3 purpose-built Rails 8 apps exercising every gem feature end-to-end:
12
+ - `full_app` — comprehensive app (38 gems, 14 models, 15 controllers, 26 views, 5 jobs, 3 mailers, multi-database, ViewComponent, Stimulus, STI, polymorphic, AASM, PaperTrail, FriendlyId, encrypted attributes, CurrentAttributes, Flipper feature flags, Sentry monitoring, Pundit auth, Ransack search, Dry-rb, acts_as_tenant, Docker, Kamal, GitHub Actions CI, RSpec + FactoryBot)
13
+ - `api_app` — API-only app (Products/Orders/OrderItems, namespaced API v1 routes, CLI tool_mode)
14
+ - `minimal_app` — bare minimum app (single model, graceful degradation testing)
15
+ - **Master test runner** (`test_apps/run_all_tests.sh`) — validates Doctor, context generation, all 33 introspectors, all 39 MCP tools, Rake tasks, MCP server startup, and app-specific pattern detection across all 3 apps (222 tests)
16
+ - All 3 test apps achieve **100/100 AI Readiness Score**
17
+
18
+ ### Fixed
19
+ - **Standalone CLI `full_gem_path` crash** — `Gem.loaded_specs.delete_if { |_, spec| !spec.default_gem? }` in the exe file cleared gem specs needed by MCP SDK at runtime (`json-schema` gem's `full_gem_path` returned nil). Added `!ENV["BUNDLE_BIN_PATH"]` guard so cleanup only runs in true standalone mode, not under `bundle exec`. This bug affected ALL `rails-ai-context tool` commands in standalone mode.
20
+
21
+ ### Changed
22
+ - Test count: 1621 RSpec examples + 222 integration tests across 3 apps
23
+
8
24
  ## [4.5.2] — 2026-04-04
9
25
 
10
26
  ### Added
@@ -30,8 +46,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
30
46
  - **CLI error messages** — Clean error messages for all CLI error paths
31
47
  - **Rake/init parity** — `rake ai:context` and `init` command now match generator output
32
48
 
49
+ ### Refactored
50
+ - **SLOP audit: ~640 lines removed** — comprehensive audit eliminating superfluous abstractions, dead code, and duplicated patterns
51
+ - **CompactSerializerHelper** — extracted shared logic from ClaudeSerializer and OpencodeSerializer, eliminating ~75% duplication
52
+ - **StackOverviewHelper consolidation** — moved `project_root`, `detect_service_files`, `detect_job_files`, `detect_before_actions`, `scope_names`, `notable_gems_list`, `arch_labels_hash`, `pattern_labels_hash`, `write_rule_files` into shared module, replacing 30+ duplicate copies across 6 serializers
53
+ - **Atomic file writes** — `write_rule_files` uses temp file + rename for crash-safe context file generation
54
+ - **ConventionDetector → ConventionIntrospector** — renamed for naming consistency with all 33 other introspectors
55
+ - **MarkdownEscape inlined** — single-use module inlined into MarkdownSerializer as private method
56
+ - **RulesSerializer deleted** — dead code never called by ContextFileSerializer
57
+ - **BaseTool cleanup** — removed dead `auto_compress`, `app_size`, `session_queried?` methods
58
+ - **IntrospectionError deleted** — exception class never raised anywhere
59
+ - **mobile_paths config removed** — config option never read by any introspector, tool, or serializer
60
+ - **server_version** — changed from attr_accessor to method delegating to `VERSION` constant
61
+ - **Configuration constants** — extracted `DEFAULT_EXCLUDED_FILTERS`, `DEFAULT_EXCLUDED_MIDDLEWARE`, `DEFAULT_EXCLUDED_CONCERNS` as frozen constants
62
+ - **Detail spec consolidation** — merged 5 detail spec files into their base spec counterparts
63
+ - **Orphaned spec cleanup** — removed `gem_introspector_spec.rb` duplicate (canonical spec already exists under introspectors/)
64
+
33
65
  ### Changed
34
- - Test count: 1658 examples (76 new tests for Phase 2 features)
66
+ - Test count: 1621 examples (consolidated from 1658 no coverage lost, only duplicate/orphaned specs removed)
35
67
 
36
68
  ## [4.4.0] — 2026-04-03
37
69
 
data/CLAUDE.md CHANGED
@@ -11,12 +11,11 @@ structure to AI assistants via the Model Context Protocol (MCP).
11
11
  - `lib/rails_ai_context/introspectors/` — 33 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, components, accessibility, performance, frontend_frameworks)
12
12
  - `lib/rails_ai_context/tools/` — 39 MCP tools using the official mcp SDK
13
13
  - `lib/rails_ai_context/cli/` — CLI tool runner (`tool_runner.rb`) — executes MCP tools from rake/Thor
14
- - `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection, tool_guide_helper, design_system_helper, stack_overview_helper)
14
+ - `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, copilot, copilot_instructions, markdown, JSON, context_file_serializer, compact_serializer_helper, test_command_detection, tool_guide_helper, design_system_helper, stack_overview_helper)
15
15
  - `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
16
16
  - `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
17
17
  - `lib/rails_ai_context/middleware.rb` — Rack middleware for auto-mounting MCP HTTP endpoint
18
18
  - `lib/rails_ai_context/safe_file.rb` — Safe file reading with size limits and error handling
19
- - `lib/rails_ai_context/markdown_escape.rb` — Escapes markdown special characters in dynamic content
20
19
  - `lib/rails_ai_context/fingerprinter.rb` — SHA256 file fingerprinting for cache invalidation
21
20
  - `lib/rails_ai_context/doctor.rb` — Diagnostic checks and AI readiness scoring
22
21
  - `lib/rails_ai_context/live_reload.rb` — MCP live reload: watches files, invalidates caches, notifies AI clients
@@ -68,7 +67,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
68
67
  ## Testing
69
68
 
70
69
  ```bash
71
- bundle exec rspec # Run specs (1658 examples)
70
+ bundle exec rspec # Run specs (1621 examples)
72
71
  bundle exec rubocop # Lint
73
72
  ```
74
73
 
data/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
  [![MCP Registry](https://img.shields.io/badge/MCP_Registry-listed-green)](https://registry.modelcontextprotocol.io)
20
20
  [![Ruby](https://img.shields.io/badge/Ruby-3.2%20%7C%203.3%20%7C%203.4-CC342D)](https://github.com/crisnahine/rails-ai-context)
21
21
  [![Rails](https://img.shields.io/badge/Rails-7.1%20%7C%207.2%20%7C%208.0-CC0000)](https://github.com/crisnahine/rails-ai-context)
22
- [![Tests](https://img.shields.io/badge/Tests-1529%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
22
+ [![Tests](https://img.shields.io/badge/Tests-1621%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
23
23
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
24
24
 
25
25
  </div>
@@ -444,7 +444,7 @@ end
444
444
  ## About
445
445
 
446
446
  Built by a Rails developer with 10+ years of production experience.<br>
447
- 1529 tests. 39 tools. 33 introspectors. Standalone or in-Gemfile.<br>
447
+ 1621 tests. 39 tools. 33 introspectors. Standalone or in-Gemfile.<br>
448
448
  MIT licensed. [Contributions welcome.](CONTRIBUTING.md)
449
449
 
450
450
  <br>
data/SECURITY.md CHANGED
@@ -4,6 +4,9 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |---------|--------------------|
7
+ | 4.6.x | :white_check_mark: |
8
+ | 4.5.x | :white_check_mark: |
9
+ | 4.4.x | :white_check_mark: |
7
10
  | 4.3.x | :white_check_mark: |
8
11
  | 4.2.x | :white_check_mark: (4.2.1 includes security hardening) |
9
12
  | 4.1.x | :white_check_mark: |
data/exe/rails-ai-context CHANGED
@@ -6,7 +6,11 @@
6
6
  # may need different versions of those deps (e.g., bigdecimal 4.1.0 activated here
7
7
  # but Gemfile.lock wants 4.0.1). Clear non-default gem activations so Bundler can
8
8
  # resolve cleanly. The $LOAD_PATH entries remain (Bundler.setup replaces them anyway).
9
- if defined?(Gem) && Gem.respond_to?(:loaded_specs)
9
+ #
10
+ # Skip this when running under `bundle exec` — Bundler has already resolved gems
11
+ # correctly, and clearing specs breaks gems that look up their own path at runtime
12
+ # (e.g., MCP SDK reads Gem.loaded_specs["json-schema"].full_gem_path).
13
+ if defined?(Gem) && Gem.respond_to?(:loaded_specs) && !ENV["BUNDLE_BIN_PATH"]
10
14
  Gem.loaded_specs.delete_if { |_, spec| !spec.default_gem? }
11
15
  end
12
16
 
@@ -257,7 +257,6 @@ module RailsAiContext
257
257
  # ── Frontend Framework Detection ─────────────────────────────────
258
258
  # Auto-detected from package.json, config/vite.json, etc. Override only if needed.
259
259
  # config.frontend_paths = ["app/frontend", "../web-client"]
260
- # config.mobile_paths = ["../mobile-app"]
261
260
  SECTION
262
261
  }.freeze
263
262
 
@@ -13,11 +13,11 @@ 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
- server_name server_version cache_ttl max_tool_response_chars
16
+ server_name cache_ttl max_tool_response_chars
17
17
  live_reload live_reload_debounce auto_mount http_path http_bind http_port
18
18
  output_dir skip_tools excluded_models excluded_controllers
19
19
  excluded_route_prefixes excluded_filters excluded_middleware excluded_paths
20
- sensitive_patterns search_extensions concern_paths frontend_paths mobile_paths
20
+ sensitive_patterns search_extensions concern_paths frontend_paths
21
21
  max_file_size max_test_file_size max_schema_file_size max_view_total_size
22
22
  max_view_file_size max_search_results max_validate_files
23
23
  query_timeout query_row_limit query_redacted_columns allow_query_in_production
@@ -79,7 +79,11 @@ module RailsAiContext
79
79
  }.freeze
80
80
 
81
81
  # MCP server settings
82
- attr_accessor :server_name, :server_version
82
+ attr_accessor :server_name
83
+
84
+ def server_version
85
+ RailsAiContext::VERSION
86
+ end
83
87
 
84
88
  # Which introspectors to run
85
89
  attr_accessor :introspectors
@@ -151,6 +155,33 @@ module RailsAiContext
151
155
  # Tool invocation mode: :mcp (MCP primary + CLI fallback) or :cli (CLI only)
152
156
  attr_accessor :tool_mode
153
157
 
158
+ DEFAULT_EXCLUDED_FILTERS = %w[
159
+ verify_authenticity_token verify_same_origin_request
160
+ turbo_tracking_request_id handle_unverified_request
161
+ mark_for_same_origin_verification
162
+ ].freeze
163
+
164
+ DEFAULT_EXCLUDED_MIDDLEWARE = %w[
165
+ Rack::Sendfile ActionDispatch::Static ActionDispatch::Executor
166
+ ActionDispatch::ServerTiming Rack::Runtime
167
+ ActionDispatch::RequestId ActionDispatch::RemoteIp
168
+ Rails::Rack::Logger ActionDispatch::ShowExceptions
169
+ ActionDispatch::DebugExceptions ActionDispatch::Callbacks
170
+ ActionDispatch::Cookies ActionDispatch::Session::CookieStore
171
+ ActionDispatch::Flash ActionDispatch::ContentSecurityPolicy::Middleware
172
+ ActionDispatch::PermissionsPolicy::Middleware ActionDispatch::ActionableExceptions
173
+ Rack::Head Rack::ConditionalGet Rack::ETag Rack::TempfileReaper
174
+ ActiveRecord::Migration::CheckPending ActionDispatch::HostAuthorization
175
+ Rack::MethodOverride ActionDispatch::Session::AbstractSecureStore
176
+ ].freeze
177
+
178
+ DEFAULT_EXCLUDED_CONCERNS = [
179
+ /::Generated/,
180
+ /\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage)/,
181
+ /\A(ActionDispatch|ActionController|ActionView|AbstractController)/,
182
+ /\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/
183
+ ].freeze
184
+
154
185
  # Filtering — customize what's hidden from AI output
155
186
  attr_accessor :excluded_controllers # Controller classes hidden from listings (e.g. DeviseController)
156
187
  attr_accessor :excluded_route_prefixes # Route controller prefixes hidden with app_only (e.g. action_mailbox/)
@@ -164,7 +195,6 @@ module RailsAiContext
164
195
 
165
196
  # Frontend framework detection (optional overrides — auto-detected if nil)
166
197
  attr_accessor :frontend_paths # User-declared frontend dirs (e.g. ["app/frontend", "../web-client"])
167
- attr_accessor :mobile_paths # User-declared mobile dirs (e.g. ["../mobile-app"])
168
198
 
169
199
  # Database query tool settings (rails_query)
170
200
  attr_accessor :query_timeout # Statement timeout in seconds (default: 5)
@@ -177,7 +207,6 @@ module RailsAiContext
177
207
 
178
208
  def initialize
179
209
  @server_name = "rails-ai-context"
180
- @server_version = RailsAiContext::VERSION
181
210
  @introspectors = PRESETS[:full].dup
182
211
  @excluded_paths = %w[node_modules tmp log vendor .git doc docs]
183
212
  @sensitive_patterns = %w[
@@ -211,30 +240,9 @@ module RailsAiContext
211
240
  @max_validate_files = 50
212
241
  @excluded_controllers = %w[DeviseController Devise::OmniauthCallbacksController]
213
242
  @excluded_route_prefixes = %w[action_mailbox/ active_storage/ rails/ conductor/ devise/ turbo/]
214
- @excluded_concerns = [
215
- /::Generated/,
216
- /\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage)/,
217
- /\A(ActionDispatch|ActionController|ActionView|AbstractController)/,
218
- /\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/
219
- ]
220
- @excluded_filters = %w[
221
- verify_authenticity_token verify_same_origin_request
222
- turbo_tracking_request_id handle_unverified_request
223
- mark_for_same_origin_verification
224
- ]
225
- @excluded_middleware = %w[
226
- Rack::Sendfile ActionDispatch::Static ActionDispatch::Executor
227
- ActionDispatch::ServerTiming Rack::Runtime
228
- ActionDispatch::RequestId ActionDispatch::RemoteIp
229
- Rails::Rack::Logger ActionDispatch::ShowExceptions
230
- ActionDispatch::DebugExceptions ActionDispatch::Callbacks
231
- ActionDispatch::Cookies ActionDispatch::Session::CookieStore
232
- ActionDispatch::Flash ActionDispatch::ContentSecurityPolicy::Middleware
233
- ActionDispatch::PermissionsPolicy::Middleware ActionDispatch::ActionableExceptions
234
- Rack::Head Rack::ConditionalGet Rack::ETag Rack::TempfileReaper
235
- ActiveRecord::Migration::CheckPending ActionDispatch::HostAuthorization
236
- Rack::MethodOverride ActionDispatch::Session::AbstractSecureStore
237
- ]
243
+ @excluded_concerns = DEFAULT_EXCLUDED_CONCERNS.dup
244
+ @excluded_filters = DEFAULT_EXCLUDED_FILTERS.dup
245
+ @excluded_middleware = DEFAULT_EXCLUDED_MIDDLEWARE.dup
238
246
  @custom_tools = []
239
247
  @skip_tools = []
240
248
  @ai_tools = nil
@@ -242,7 +250,6 @@ module RailsAiContext
242
250
  @search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
243
251
  @concern_paths = %w[app/models/concerns app/controllers/concerns]
244
252
  @frontend_paths = nil
245
- @mobile_paths = nil
246
253
  @query_timeout = 5
247
254
  @query_row_limit = 100
248
255
  @query_redacted_columns = %w[
@@ -63,7 +63,7 @@ module RailsAiContext
63
63
  when :routes then Introspectors::RouteIntrospector.new(app)
64
64
  when :jobs then Introspectors::JobIntrospector.new(app)
65
65
  when :gems then Introspectors::GemIntrospector.new(app)
66
- when :conventions then Introspectors::ConventionDetector.new(app)
66
+ when :conventions then Introspectors::ConventionIntrospector.new(app)
67
67
  when :stimulus then Introspectors::StimulusIntrospector.new(app)
68
68
  when :database_stats then Introspectors::DatabaseStatsIntrospector.new(app)
69
69
  when :controllers then Introspectors::ControllerIntrospector.new(app)
@@ -4,7 +4,7 @@ module RailsAiContext
4
4
  module Introspectors
5
5
  # Detects high-level Rails conventions and patterns in use,
6
6
  # giving AI assistants critical context about the app's architecture.
7
- class ConventionDetector
7
+ class ConventionIntrospector
8
8
  attr_reader :app
9
9
 
10
10
  def initialize(app)
@@ -19,34 +19,18 @@ module RailsAiContext
19
19
  # @return [Hash] { written: [paths], skipped: [paths] }
20
20
  def call(output_dir)
21
21
  rules_dir = File.join(output_dir, ".claude", "rules")
22
- FileUtils.mkdir_p(rules_dir)
23
-
24
- written = []
25
- skipped = []
26
22
 
27
23
  files = {
28
- "rails-context.md" => render_context_overview,
29
- "rails-schema.md" => render_schema_reference,
30
- "rails-models.md" => render_models_reference,
31
- "rails-ui-patterns.md" => render_ui_patterns_reference,
32
- "rails-mcp-tools.md" => render_mcp_tools_reference,
33
- "rails-components.md" => render_components_reference,
34
- "rails-accessibility.md" => render_accessibility_reference
24
+ File.join(rules_dir, "rails-context.md") => render_context_overview,
25
+ File.join(rules_dir, "rails-schema.md") => render_schema_reference,
26
+ File.join(rules_dir, "rails-models.md") => render_models_reference,
27
+ File.join(rules_dir, "rails-ui-patterns.md") => render_ui_patterns_reference,
28
+ File.join(rules_dir, "rails-mcp-tools.md") => render_mcp_tools_reference,
29
+ File.join(rules_dir, "rails-components.md") => render_components_reference,
30
+ File.join(rules_dir, "rails-accessibility.md") => render_accessibility_reference
35
31
  }
36
32
 
37
- files.each do |filename, content|
38
- next unless content
39
-
40
- filepath = File.join(rules_dir, filename)
41
- if File.exist?(filepath) && File.read(filepath) == content
42
- skipped << filepath
43
- else
44
- File.write(filepath, content)
45
- written << filepath
46
- end
47
- end
48
-
49
- { written: written, skipped: skipped }
33
+ write_rule_files(files)
50
34
  end
51
35
 
52
36
  private
@@ -74,17 +58,8 @@ module RailsAiContext
74
58
  lines.concat(full_preset_stack_lines)
75
59
 
76
60
  # ApplicationController before_actions — apply to all controllers
77
- begin
78
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
79
- app_ctrl_file = File.join(root, "app", "controllers", "application_controller.rb")
80
- if File.exist?(app_ctrl_file)
81
- source = File.read(app_ctrl_file)
82
- before_actions = source.scan(/before_action\s+:([\w!?]+)/).flatten
83
- if before_actions.any?
84
- lines << "" << "**Global before_actions:** #{before_actions.join(', ')}"
85
- end
86
- end
87
- rescue => e; $stderr.puts "[rails-ai-context] Serializer section skipped: #{e.message}"; end
61
+ before_actions = detect_before_actions
62
+ lines << "" << "**Global before_actions:** #{before_actions.join(', ')}" if before_actions.any?
88
63
 
89
64
  lines << ""
90
65
  lines << "ALWAYS use MCP tools for context — do NOT read reference files directly."
@@ -206,7 +181,7 @@ module RailsAiContext
206
181
 
207
182
  # Include scopes so agents know available query methods
208
183
  scopes = data[:scopes] || []
209
- scope_names = scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
184
+ scope_names = scope_names(scopes)
210
185
  lines << " scopes: #{scope_names.join(', ')}" if scopes.any?
211
186
 
212
187
  # Instance methods — filter Devise/framework internals that add noise
@@ -248,8 +223,7 @@ module RailsAiContext
248
223
 
249
224
  # Shared partials — so agents reuse them instead of recreating
250
225
  begin
251
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
252
- shared_dir = File.join(root, "app", "views", "shared")
226
+ shared_dir = File.join(project_root, "app", "views", "shared")
253
227
  if Dir.exist?(shared_dir)
254
228
  partials = Dir.glob(File.join(shared_dir, "_*.html.erb"))
255
229
  .map { |f| File.basename(f) }
@@ -263,8 +237,7 @@ module RailsAiContext
263
237
 
264
238
  # Helpers — so agents use existing helpers instead of creating new ones
265
239
  begin
266
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
267
- helper_file = File.join(root, "app", "helpers", "application_helper.rb")
240
+ helper_file = File.join(project_root, "app", "helpers", "application_helper.rb")
268
241
  if File.exist?(helper_file)
269
242
  helper_methods = File.read(helper_file).scan(/def\s+(\w+)/).flatten
270
243
  if helper_methods.any?
@@ -10,6 +10,7 @@ module RailsAiContext
10
10
  include StackOverviewHelper
11
11
  include DesignSystemHelper
12
12
  include ToolGuideHelper
13
+ include CompactSerializerHelper
13
14
 
14
15
  attr_reader :context
15
16
 
@@ -38,118 +39,9 @@ module RailsAiContext
38
39
  lines.concat(render_commands)
39
40
  lines.concat(render_warnings)
40
41
  lines.concat(render_footer)
41
- lines.concat(render_mcp_guide_compact)
42
+ lines.concat(render_tools_guide_compact)
42
43
 
43
- # Enforce max lines
44
- max = RailsAiContext.configuration.claude_max_lines
45
- if lines.size > max
46
- lines = lines.first(max - 2)
47
- lines << ""
48
- lines << "_Context trimmed. Use MCP tools for full details._"
49
- end
50
-
51
- lines.join("\n")
52
- end
53
-
54
- def render_header
55
- [
56
- "# #{context[:app_name]} — AI Context",
57
- "",
58
- "> Rails #{context[:rails_version]} | Ruby #{context[:ruby_version]}",
59
- "> Generated by rails-ai-context v#{RailsAiContext::VERSION}",
60
- ""
61
- ]
62
- end
63
-
64
- def render_stack_overview
65
- lines = [ "## Stack" ]
66
-
67
- schema = context[:schema]
68
- if schema && !schema[:error]
69
- lines << "- Database: #{schema[:adapter]} — #{schema[:total_tables]} tables"
70
- end
71
-
72
- models = context[:models]
73
- if models.is_a?(Hash) && !models[:error]
74
- lines << "- Models: #{models.size}"
75
- end
76
-
77
- routes = context[:routes]
78
- if routes && !routes[:error]
79
- # Count only app controllers (exclude internal Rails, Devise, Turbo routes)
80
- internal = %w[action_mailbox/ active_storage/ rails/ conductor/ devise/ turbo/]
81
- app_ctrls = (routes[:by_controller] || {}).keys.reject { |k| internal.any? { |p| k.downcase.start_with?(p) } }
82
- lines << "- Routes: #{routes[:total_routes]} across #{app_ctrls.size} controllers"
83
- end
84
-
85
- jobs = context[:jobs]
86
- if jobs.is_a?(Hash) && !jobs[:error]
87
- job_count = jobs[:jobs]&.size || 0
88
- mailer_count = jobs[:mailers]&.size || 0
89
- channel_count = jobs[:channels]&.size || 0
90
- parts = []
91
- parts << "#{job_count} jobs" if job_count > 0
92
- parts << "#{mailer_count} mailers" if mailer_count > 0
93
- parts << "#{channel_count} channels" if channel_count > 0
94
- lines << "- Async: #{parts.join(', ')}" if parts.any?
95
- end
96
-
97
- migrations = context[:migrations]
98
- if migrations.is_a?(Hash) && !migrations[:error]
99
- pending = migrations[:pending]
100
- lines << "- Migrations: #{migrations[:total]} total, #{pending&.size || 0} pending"
101
- end
102
-
103
- lines.concat(full_preset_stack_lines)
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] || []).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
- scopes = (data[:scopes] || [])
125
- constants = (data[:constants] || [])
126
- if scopes.any? || constants.any?
127
- extras = []
128
- scope_names = scopes.map { |s| s.is_a?(Hash) ? s[:name] : s }
129
- extras << "scopes: #{scope_names.join(', ')}" if scopes.any?
130
- constants.each { |c| extras << "#{c[:name]}: #{c[:values].join(', ')}" }
131
- lines << " #{extras.join(' | ')}"
132
- end
133
- end
134
- lines << "- _...#{models.size - max_show} more (use `rails_get_model_details` tool)_" if models.size > max_show
135
- lines << ""
136
- lines
137
- end
138
-
139
- def render_notable_gems
140
- gems = context[:gems]
141
- return [] unless gems.is_a?(Hash) && !gems[:error]
142
- notable = gems[:notable_gems] || gems[:notable] || gems[:detected] || []
143
- return [] if notable.empty?
144
-
145
- lines = [ "## Gems" ]
146
- grouped = notable.group_by { |g| g[:category]&.to_s || "other" }
147
- grouped.each do |category, gem_list|
148
- names = gem_list.map { |g| g[:name] }.join(", ")
149
- lines << "- **#{category}**: #{names}"
150
- end
151
- lines << ""
152
- lines
44
+ enforce_max_lines(lines)
153
45
  end
154
46
 
155
47
  def render_architecture
@@ -160,94 +52,35 @@ module RailsAiContext
160
52
  patterns = conv[:patterns] || []
161
53
  return [] if arch.empty? && patterns.empty?
162
54
 
163
- arch_labels = RailsAiContext::Tools::GetConventions::ARCH_LABELS rescue {}
164
- pattern_labels = RailsAiContext::Tools::GetConventions::PATTERN_LABELS rescue {}
55
+ arch_labels = arch_labels_hash
56
+ pattern_labels = pattern_labels_hash
165
57
 
166
58
  lines = [ "## Architecture" ]
167
59
  arch.each { |p| lines << "- #{arch_labels[p] || p}" }
168
60
  patterns.first(8).each { |p| lines << "- #{pattern_labels[p] || p}" }
169
61
 
170
- # List service objects and jobs from conventions directory_structure
171
62
  dir_struct = conv[:directory_structure] || {}
172
63
 
173
64
  if dir_struct["app/services"]
174
- begin
175
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
176
- service_files = Dir.glob(File.join(root, "app", "services", "*.rb"))
177
- .map { |f| File.basename(f, ".rb").camelize }
178
- .reject { |s| s == "ApplicationService" }
179
- lines << "" << "**Services:** #{service_files.join(', ')}" if service_files.any?
180
- rescue => e; $stderr.puts "[rails-ai-context] Serializer section skipped: #{e.message}"; end
65
+ services = detect_service_files
66
+ lines << "" << "**Services:** #{services.join(', ')}" if services.any?
181
67
  end
182
68
 
183
69
  if dir_struct["app/jobs"]
184
- begin
185
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
186
- job_files = Dir.glob(File.join(root, "app", "jobs", "*.rb"))
187
- .map { |f| File.basename(f, ".rb").camelize }
188
- .reject { |j| j == "ApplicationJob" }
189
- lines << "**Jobs:** #{job_files.join(', ')}" if job_files.any?
190
- rescue => e; $stderr.puts "[rails-ai-context] Serializer section skipped: #{e.message}"; end
70
+ jobs = detect_job_files
71
+ lines << "**Jobs:** #{jobs.join(', ')}" if jobs.any?
191
72
  end
192
73
 
193
74
  lines << ""
194
75
  lines
195
76
  end
196
77
 
197
- def render_ui_patterns
198
- render_design_system(context, max_lines: 30)
199
- end
200
-
201
- def render_mcp_guide
202
- render_tools_guide
203
- end
204
-
205
- def render_mcp_guide_compact
206
- render_tools_guide_compact
207
- end
208
-
209
- def render_conventions
210
- conv = context[:conventions]
211
- return [] unless conv.is_a?(Hash) && !conv[:error]
212
-
213
- config_files = conv[:config_files] || []
214
- return [] if config_files.empty?
215
-
216
- lines = [ "## Key config files" ]
217
- config_files.first(5).each { |f| lines << "- `#{f}`" }
218
- lines << ""
219
- lines
220
- end
221
-
222
- def render_commands
223
- test_cmd = detect_test_command
224
- [
225
- "## Commands",
226
- "- `bin/dev` — start dev server",
227
- "- `#{test_cmd}` — run tests",
228
- "- `rails db:migrate` — run pending migrations",
229
- ""
230
- ]
231
- end
232
-
233
- def render_warnings
234
- warnings = context[:_warnings]
235
- return [] if warnings.nil? || warnings.empty?
236
-
237
- lines = [ "", "## Warnings", "" ]
238
- warnings.each do |w|
239
- lines << "- **#{w[:introspector]}** skipped: #{w[:error]}"
240
- end
241
- lines
242
- end
243
-
244
78
  def render_footer
245
79
  test_cmd = detect_test_command
246
80
  lines = [ "## Rules" ]
247
81
  lines << "- Run `#{test_cmd}` after changes"
248
82
  lines << "- Do NOT re-read files to verify edits — trust your Edit, validate syntax only"
249
83
 
250
- # App-specific conventions from introspection
251
84
  conv = context[:conventions]
252
85
  if conv.is_a?(Hash) && !conv[:error]
253
86
  arch = conv[:architecture] || []
@@ -258,24 +91,13 @@ module RailsAiContext
258
91
  lines << "- Use query objects for complex queries" if patterns.include?("query_objects")
259
92
  end
260
93
 
261
- # Stimulus auto-register if detected
262
94
  stimulus = context[:stimulus]
263
95
  if stimulus.is_a?(Hash) && !stimulus[:error] && (stimulus[:controllers]&.any? || stimulus[:total_controllers]&.positive?)
264
96
  lines << "- Stimulus controllers auto-register — no manual import in controllers/index.js needed"
265
97
  end
266
98
 
267
- # Global before_actions critical for understanding controller flow
268
- begin
269
- root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
270
- app_ctrl_file = File.join(root, "app", "controllers", "application_controller.rb")
271
- if File.exist?(app_ctrl_file)
272
- source = File.read(app_ctrl_file)
273
- before_actions = source.scan(/before_action\s+:([\w!?]+)/).flatten
274
- if before_actions.any?
275
- lines << "- Global before_actions: #{before_actions.join(', ')}"
276
- end
277
- end
278
- rescue => e; $stderr.puts "[rails-ai-context] Serializer section skipped: #{e.message}"; end
99
+ before_actions = detect_before_actions
100
+ lines << "- Global before_actions: #{before_actions.join(', ')}" if before_actions.any?
279
101
 
280
102
  lines << ""
281
103
  lines
@@ -284,8 +106,6 @@ module RailsAiContext
284
106
 
285
107
  # Internal: full-mode Claude serializer (wraps MarkdownSerializer with behavioral rules)
286
108
  class FullClaudeSerializer < MarkdownSerializer
287
- include TestCommandDetection
288
-
289
109
  private
290
110
 
291
111
  def header