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 +4 -4
- data/CHANGELOG.md +33 -1
- data/CLAUDE.md +2 -3
- data/README.md +2 -2
- data/SECURITY.md +3 -0
- data/exe/rails-ai-context +5 -1
- data/lib/generators/rails_ai_context/install/install_generator.rb +0 -1
- data/lib/rails_ai_context/configuration.rb +37 -30
- data/lib/rails_ai_context/introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/{convention_detector.rb → convention_introspector.rb} +1 -1
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +13 -40
- data/lib/rails_ai_context/serializers/claude_serializer.rb +11 -191
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +161 -0
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +14 -52
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +3 -3
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +16 -55
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +14 -6
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +17 -52
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -141
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +84 -0
- data/lib/rails_ai_context/tools/base_tool.rb +0 -29
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context.rb +0 -1
- data/server.json +2 -2
- metadata +3 -18
- data/lib/rails_ai_context/markdown_escape.rb +0 -15
- data/lib/rails_ai_context/serializers/rules_serializer.rb +0 -155
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a6993d2929f87b6f9c29dffd5f16124a1d3ccb15ac3f8ba8f100364bf0feb06
|
|
4
|
+
data.tar.gz: '08c5fe8f51d126c1f7cc52ca9cc8ca0dc2edf5d1dba5f569b8ec5afaeecbb0b4'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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,
|
|
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 (
|
|
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
|
[](https://registry.modelcontextprotocol.io)
|
|
20
20
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
21
21
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
22
|
-
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
23
23
|
[](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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
216
|
-
|
|
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::
|
|
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)
|
data/lib/rails_ai_context/introspectors/{convention_detector.rb → convention_introspector.rb}
RENAMED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
42
|
+
lines.concat(render_tools_guide_compact)
|
|
42
43
|
|
|
43
|
-
|
|
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 =
|
|
164
|
-
pattern_labels =
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|