rails-ai-context 5.0.0 → 5.1.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 +27 -0
- data/README.md +2 -2
- data/SECURITY.md +3 -0
- data/lib/generators/rails_ai_context/install/install_generator.rb +3 -2
- data/lib/rails_ai_context/introspector.rb +39 -35
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/api_introspector.rb +4 -4
- data/lib/rails_ai_context/introspectors/component_introspector.rb +5 -4
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +8 -3
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +4 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +6 -6
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +26 -6
- data/lib/rails_ai_context/introspectors/job_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/model_introspector.rb +5 -6
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +8 -8
- data/lib/rails_ai_context/introspectors/route_introspector.rb +2 -0
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +9 -4
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1edfff7102dd3dd9e07d8686bb62c6017ff79492a46c36645a453b7b39a4983c
|
|
4
|
+
data.tar.gz: 2e9bb943d9986b169de0c133cb4458bb9a8678a5b32608ebf92ba80b3637c406
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff8184662fc4c3b181d199dc0a3085935bc545cef488361b4b4596a2941630a424cff14ba175e22db244234d6a448cb6d3352863a1ff9ce3a07c28b89d2279c0
|
|
7
|
+
data.tar.gz: 79ec7f4d524a581b5dff5f67c58c1271743fe708131e5223fc91b23799e0d803561b7b713aba080662fe8f79f6e0601d0ce233a2763ab6bee1f9f50eb5ca3898
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ 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
|
+
## [5.1.0] — 2026-04-06
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
Accuracy fixes across 8 introspectors, eliminating false positives and capturing previously-missed signals. No public API changes; all 38 MCP tools retain their contracts.
|
|
13
|
+
|
|
14
|
+
- **ApiIntrospector** — pagination detection (`detect_pagination`) was substring-matching Gemfile.lock content, producing false positives on gems that merely contain the strategy name: `happypagy`, `kaminari-i18n`, transitive `pagy` dependencies. Now uses anchored lockfile regex (`^ pagy \(`) that only matches direct top-level dependencies. Same fix applied to `kaminari`, `will_paginate`, and `graphql-pro` detection.
|
|
15
|
+
- **DevOpsIntrospector** — health-check detection (`detect_health_check`) used an unanchored word regex (`\b(?:health|up|ping|status)\b`) that matched comments, controller names, and any line containing those words. Tightened to match only quoted route strings (`"/up"`, `"/healthz"`, `"/liveness"`, etc.) or the `rails_health_check` symbol. Also newly detects `/readiness`, `/alive`, and `/healthz` routes.
|
|
16
|
+
- **PerformanceIntrospector** — schema parsing (`parse_indexed_columns`) tracked table context with a boolean-ish `current_table` variable but never cleared it on `end` lines, so `add_index` statements after a `create_table` block matched both the inner block branch AND the outer branch, producing duplicate index entries. This polluted `missing_fk_indexes` analysis. Fixed via explicit `inside_create_table` state flag with block boundary detection. Also added `m` (multiline) flag to specific-association preload regex so `.includes(...)` calls spanning multiple lines are matched.
|
|
17
|
+
- **I18nIntrospector** — `count_keys_for_locale` only read `config/locales/{locale}.yml`, missing nested locale files that are the Rails convention for gem-added translations: `config/locales/devise.en.yml`, `config/locales/en/users.yml`, `config/locales/admin/en.yml`. New `find_locale_paths` method globs all YAML under `config/locales/**/*` and selects files whose basename equals the locale, ends with `.{locale}`, or lives under a `{locale}/` subfolder. In typical Rails apps this captures 2-10x more translation keys than the previous single-file read, making `translation_coverage` percentages meaningful.
|
|
18
|
+
- **JobIntrospector** — when a job class declared `queue_as ->(job) { ... }`, `job.queue_name` returned a Proc that was then called with no arguments, crashing or returning stale values. Now returns `"dynamic"` when queue is a Proc, matching the job's actual runtime behavior (queue is resolved per-invocation).
|
|
19
|
+
- **ModelIntrospector** — source-parsed class methods in `extract_source_class_methods` emitted a spurious `"self"` entry because `def self.foo` matched both the `def self.(\w+)` branch AND the generic `def (\w+)` branch inside `class << self` tracking. Restructured as `if/elsif` so each `def` line matches exactly one pattern. Also anchored `class << self` detection with `\b` to avoid partial-word matches.
|
|
20
|
+
- **RouteIntrospector** — `call` method could raise if `Rails.application.routes` was not yet loaded or a sub-method failed mid-extraction. Added a top-level rescue that returns `{ error: msg }`, matching the error contract used by every other introspector.
|
|
21
|
+
- **SeedsIntrospector** — `has_ordering` regex (`load.*order|require.*order|seeds.*\d+`) matched unrelated code like `require 'order'` or `seeds 001` in comments. Tightened to match actual ordering patterns: `Dir[...*.rb].sort`, `load "seeds/NN_foo.rb"`, `require_relative "seeds/NN_foo"`.
|
|
22
|
+
|
|
23
|
+
### Performance
|
|
24
|
+
|
|
25
|
+
- **ConventionIntrospector** — `gem_present?` was reading `Gemfile.lock` from disk 15 times per introspection pass (once per notable gem check). Memoized into a single read: **-93% I/O** (15 reads → 1 read). ~60% faster on typical apps.
|
|
26
|
+
- **ComponentIntrospector** — `build_summary` called `extract_components` again after `call` already computed it, doubling the filesystem walk and component parsing work. Now passes the result through: **-50% work**. ~50% faster.
|
|
27
|
+
- **GemIntrospector** — `categorize_gems(specs)` internally called `detect_notable_gems(specs)` after `call` had already called it, duplicating gem-list iteration and category lookup. Now accepts the notable-gem result directly: **-50% work**.
|
|
28
|
+
- **ActiveStorageIntrospector** — `uses_direct_uploads?` globbed `**/*` across `app/views` + `app/javascript`, reading every binary, image, font, and asset in those trees. Scoped to 9 relevant extensions (`erb,haml,slim,js,ts,jsx,tsx,mjs,rb`), avoiding wasteful I/O on irrelevant files.
|
|
29
|
+
- **Total**: ~14% cumulative speedup across all 12 modified introspectors on a medium-sized Rails app (23.66ms → 20.33ms).
|
|
30
|
+
|
|
31
|
+
### Why
|
|
32
|
+
|
|
33
|
+
Introspector output feeds every MCP tool response, every context file, and every rule file this gem generates. Silent inaccuracies (false-positive pagination detection, missed locale files, phantom duplicate indexes) compound: AI assistants make decisions based on this data, and incorrect data produces incorrect code suggestions. These fixes tighten the accuracy floor without changing any public interface.
|
|
34
|
+
|
|
8
35
|
## [5.0.0] — 2026-04-05
|
|
9
36
|
|
|
10
37
|
### Removed (BREAKING)
|
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>
|
|
@@ -470,7 +470,7 @@ end
|
|
|
470
470
|
## About
|
|
471
471
|
|
|
472
472
|
Built by a Rails developer with 10+ years of production experience.<br>
|
|
473
|
-
|
|
473
|
+
1565 tests. 38 tools. 31 introspectors. Standalone or in-Gemfile.<br>
|
|
474
474
|
MIT licensed. [Contributions welcome.](CONTRIBUTING.md)
|
|
475
475
|
|
|
476
476
|
<br>
|
data/SECURITY.md
CHANGED
|
@@ -459,9 +459,10 @@ module RailsAiContext
|
|
|
459
459
|
say ""
|
|
460
460
|
say "Commands:", :yellow
|
|
461
461
|
say " rails ai:context # Regenerate context files"
|
|
462
|
-
|
|
462
|
+
tool_count = RailsAiContext::Server::TOOLS.size
|
|
463
|
+
say " rails 'ai:tool[schema]' # Run any of the #{tool_count} tools from CLI"
|
|
463
464
|
if @tool_mode == :mcp
|
|
464
|
-
say " rails ai:serve # Start MCP server (
|
|
465
|
+
say " rails ai:serve # Start MCP server (#{tool_count} live tools)"
|
|
465
466
|
end
|
|
466
467
|
say " rails ai:doctor # Check AI readiness"
|
|
467
468
|
say " rails ai:inspect # Print introspection summary"
|
|
@@ -46,6 +46,43 @@ module RailsAiContext
|
|
|
46
46
|
context
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# Single source of truth: symbol → introspector class.
|
|
50
|
+
# Used by both the dispatcher below AND the Configuration presets validation,
|
|
51
|
+
# so adding/renaming introspectors only requires one edit.
|
|
52
|
+
INTROSPECTOR_MAP = {
|
|
53
|
+
schema: Introspectors::SchemaIntrospector,
|
|
54
|
+
models: Introspectors::ModelIntrospector,
|
|
55
|
+
routes: Introspectors::RouteIntrospector,
|
|
56
|
+
jobs: Introspectors::JobIntrospector,
|
|
57
|
+
gems: Introspectors::GemIntrospector,
|
|
58
|
+
conventions: Introspectors::ConventionIntrospector,
|
|
59
|
+
stimulus: Introspectors::StimulusIntrospector,
|
|
60
|
+
database_stats: Introspectors::DatabaseStatsIntrospector,
|
|
61
|
+
controllers: Introspectors::ControllerIntrospector,
|
|
62
|
+
views: Introspectors::ViewIntrospector,
|
|
63
|
+
view_templates: Introspectors::ViewTemplateIntrospector,
|
|
64
|
+
turbo: Introspectors::TurboIntrospector,
|
|
65
|
+
i18n: Introspectors::I18nIntrospector,
|
|
66
|
+
config: Introspectors::ConfigIntrospector,
|
|
67
|
+
active_storage: Introspectors::ActiveStorageIntrospector,
|
|
68
|
+
action_text: Introspectors::ActionTextIntrospector,
|
|
69
|
+
auth: Introspectors::AuthIntrospector,
|
|
70
|
+
api: Introspectors::ApiIntrospector,
|
|
71
|
+
tests: Introspectors::TestIntrospector,
|
|
72
|
+
rake_tasks: Introspectors::RakeTaskIntrospector,
|
|
73
|
+
assets: Introspectors::AssetPipelineIntrospector,
|
|
74
|
+
devops: Introspectors::DevOpsIntrospector,
|
|
75
|
+
action_mailbox: Introspectors::ActionMailboxIntrospector,
|
|
76
|
+
migrations: Introspectors::MigrationIntrospector,
|
|
77
|
+
seeds: Introspectors::SeedsIntrospector,
|
|
78
|
+
middleware: Introspectors::MiddlewareIntrospector,
|
|
79
|
+
engines: Introspectors::EngineIntrospector,
|
|
80
|
+
multi_database: Introspectors::MultiDatabaseIntrospector,
|
|
81
|
+
components: Introspectors::ComponentIntrospector,
|
|
82
|
+
performance: Introspectors::PerformanceIntrospector,
|
|
83
|
+
frontend_frameworks: Introspectors::FrontendFrameworkIntrospector
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
49
86
|
private
|
|
50
87
|
|
|
51
88
|
def app_name
|
|
@@ -57,41 +94,8 @@ module RailsAiContext
|
|
|
57
94
|
end
|
|
58
95
|
|
|
59
96
|
def resolve_introspector(name)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
when :models then Introspectors::ModelIntrospector.new(app)
|
|
63
|
-
when :routes then Introspectors::RouteIntrospector.new(app)
|
|
64
|
-
when :jobs then Introspectors::JobIntrospector.new(app)
|
|
65
|
-
when :gems then Introspectors::GemIntrospector.new(app)
|
|
66
|
-
when :conventions then Introspectors::ConventionIntrospector.new(app)
|
|
67
|
-
when :stimulus then Introspectors::StimulusIntrospector.new(app)
|
|
68
|
-
when :database_stats then Introspectors::DatabaseStatsIntrospector.new(app)
|
|
69
|
-
when :controllers then Introspectors::ControllerIntrospector.new(app)
|
|
70
|
-
when :views then Introspectors::ViewIntrospector.new(app)
|
|
71
|
-
when :view_templates then Introspectors::ViewTemplateIntrospector.new(app)
|
|
72
|
-
when :turbo then Introspectors::TurboIntrospector.new(app)
|
|
73
|
-
when :i18n then Introspectors::I18nIntrospector.new(app)
|
|
74
|
-
when :config then Introspectors::ConfigIntrospector.new(app)
|
|
75
|
-
when :active_storage then Introspectors::ActiveStorageIntrospector.new(app)
|
|
76
|
-
when :action_text then Introspectors::ActionTextIntrospector.new(app)
|
|
77
|
-
when :auth then Introspectors::AuthIntrospector.new(app)
|
|
78
|
-
when :api then Introspectors::ApiIntrospector.new(app)
|
|
79
|
-
when :tests then Introspectors::TestIntrospector.new(app)
|
|
80
|
-
when :rake_tasks then Introspectors::RakeTaskIntrospector.new(app)
|
|
81
|
-
when :assets then Introspectors::AssetPipelineIntrospector.new(app)
|
|
82
|
-
when :devops then Introspectors::DevOpsIntrospector.new(app)
|
|
83
|
-
when :action_mailbox then Introspectors::ActionMailboxIntrospector.new(app)
|
|
84
|
-
when :migrations then Introspectors::MigrationIntrospector.new(app)
|
|
85
|
-
when :seeds then Introspectors::SeedsIntrospector.new(app)
|
|
86
|
-
when :middleware then Introspectors::MiddlewareIntrospector.new(app)
|
|
87
|
-
when :engines then Introspectors::EngineIntrospector.new(app)
|
|
88
|
-
when :multi_database then Introspectors::MultiDatabaseIntrospector.new(app)
|
|
89
|
-
when :components then Introspectors::ComponentIntrospector.new(app)
|
|
90
|
-
when :performance then Introspectors::PerformanceIntrospector.new(app)
|
|
91
|
-
when :frontend_frameworks then Introspectors::FrontendFrameworkIntrospector.new(app)
|
|
92
|
-
else
|
|
93
|
-
raise ConfigurationError, "Unknown introspector: #{name}"
|
|
94
|
-
end
|
|
97
|
+
klass = INTROSPECTOR_MAP[name] or raise ConfigurationError, "Unknown introspector: #{name}"
|
|
98
|
+
klass.new(app)
|
|
95
99
|
end
|
|
96
100
|
end
|
|
97
101
|
end
|
|
@@ -113,7 +113,7 @@ module RailsAiContext
|
|
|
113
113
|
|
|
114
114
|
[ views_dir, js_dir ].any? do |dir|
|
|
115
115
|
next false unless Dir.exist?(dir)
|
|
116
|
-
Dir.glob(File.join(dir, "
|
|
116
|
+
Dir.glob(File.join(dir, "**/*.{erb,haml,slim,js,ts,jsx,tsx,mjs,rb}")).any? do |f|
|
|
117
117
|
next false if File.directory?(f)
|
|
118
118
|
(RailsAiContext::SafeFile.read(f) || "").match?(/direct.upload|DirectUpload|direct_upload/)
|
|
119
119
|
end
|
|
@@ -143,10 +143,10 @@ module RailsAiContext
|
|
|
143
143
|
return nil unless content
|
|
144
144
|
|
|
145
145
|
strategies = []
|
|
146
|
-
strategies << "pagy" if content.
|
|
147
|
-
strategies << "kaminari" if content.
|
|
148
|
-
strategies << "will_paginate" if content.
|
|
149
|
-
strategies << "cursor" if content.
|
|
146
|
+
strategies << "pagy" if content.match?(/^ pagy \(/)
|
|
147
|
+
strategies << "kaminari" if content.match?(/^ kaminari \(/)
|
|
148
|
+
strategies << "will_paginate" if content.match?(/^ will_paginate \(/)
|
|
149
|
+
strategies << "cursor" if content.match?(/^ graphql-pro \(/) # cursor-based pagination
|
|
150
150
|
strategies.empty? ? nil : strategies
|
|
151
151
|
rescue => e
|
|
152
152
|
$stderr.puts "[rails-ai-context] detect_pagination failed: #{e.message}" if ENV["DEBUG"]
|
|
@@ -12,9 +12,10 @@ module RailsAiContext
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call
|
|
15
|
+
components = extract_components
|
|
15
16
|
{
|
|
16
|
-
components:
|
|
17
|
-
summary: build_summary
|
|
17
|
+
components: components,
|
|
18
|
+
summary: build_summary(components)
|
|
18
19
|
}
|
|
19
20
|
rescue => e
|
|
20
21
|
{ error: e.message }
|
|
@@ -299,8 +300,8 @@ module RailsAiContext
|
|
|
299
300
|
assets.sort
|
|
300
301
|
end
|
|
301
302
|
|
|
302
|
-
def build_summary
|
|
303
|
-
components
|
|
303
|
+
def build_summary(components = nil)
|
|
304
|
+
components ||= extract_components
|
|
304
305
|
return {} if components.empty?
|
|
305
306
|
|
|
306
307
|
types = components.group_by { |c| c[:type] }
|
|
@@ -178,9 +178,14 @@ module RailsAiContext
|
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
def gem_present?(name)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
gemfile_lock_content.include?(" #{name} (")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def gemfile_lock_content
|
|
185
|
+
@gemfile_lock_content ||= begin
|
|
186
|
+
lock_path = File.join(root, "Gemfile.lock")
|
|
187
|
+
File.exist?(lock_path) ? (RailsAiContext::SafeFile.read(lock_path) || "") : ""
|
|
188
|
+
end
|
|
184
189
|
end
|
|
185
190
|
end
|
|
186
191
|
end
|
|
@@ -78,7 +78,10 @@ module RailsAiContext
|
|
|
78
78
|
|
|
79
79
|
content = RailsAiContext::SafeFile.read(routes_path)
|
|
80
80
|
return nil unless content
|
|
81
|
-
|
|
81
|
+
# Match health-check endpoints as quoted route strings only,
|
|
82
|
+
# to avoid false positives from comments or controller/action names.
|
|
83
|
+
return true if content.match?(%r{["']/?(?:up|health|ping|status|healthz|alive|liveness|readiness)["']})
|
|
84
|
+
return true if content.include?("rails_health_check")
|
|
82
85
|
nil
|
|
83
86
|
rescue => e
|
|
84
87
|
$stderr.puts "[rails-ai-context] detect_health_check failed: #{e.message}" if ENV["DEBUG"]
|
|
@@ -156,12 +156,13 @@ module RailsAiContext
|
|
|
156
156
|
return { error: "No Gemfile.lock found" } unless File.exist?(lock_path)
|
|
157
157
|
|
|
158
158
|
specs = parse_lockfile(lock_path)
|
|
159
|
+
notable = detect_notable_gems(specs)
|
|
159
160
|
|
|
160
161
|
{
|
|
161
162
|
total_gems: specs.size,
|
|
162
163
|
ruby_version: specs["ruby"]&.first,
|
|
163
|
-
notable_gems:
|
|
164
|
-
categories: categorize_gems(
|
|
164
|
+
notable_gems: notable,
|
|
165
|
+
categories: categorize_gems(notable),
|
|
165
166
|
local_gems: detect_local_gems,
|
|
166
167
|
gem_groups: detect_gem_groups
|
|
167
168
|
}
|
|
@@ -248,10 +249,9 @@ module RailsAiContext
|
|
|
248
249
|
end
|
|
249
250
|
end
|
|
250
251
|
|
|
251
|
-
def categorize_gems(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.transform_values { |gems| gems.map { |g| g[:name] } }
|
|
252
|
+
def categorize_gems(notable)
|
|
253
|
+
notable.group_by { |g| g[:category] }
|
|
254
|
+
.transform_values { |gems| gems.map { |g| g[:name] } }
|
|
255
255
|
end
|
|
256
256
|
end
|
|
257
257
|
end
|
|
@@ -93,17 +93,37 @@ module RailsAiContext
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def count_keys_for_locale(locale)
|
|
96
|
-
|
|
97
|
-
return 0
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
96
|
+
paths = find_locale_paths(locale)
|
|
97
|
+
return 0 if paths.empty?
|
|
98
|
+
paths.sum do |path|
|
|
99
|
+
content = RailsAiContext::SafeFile.read(path)
|
|
100
|
+
next 0 unless content
|
|
101
|
+
data = YAML.safe_load(content, permitted_classes: [ Symbol ])
|
|
102
|
+
count_nested_keys(data)
|
|
103
|
+
rescue
|
|
104
|
+
0
|
|
105
|
+
end
|
|
102
106
|
rescue => e
|
|
103
107
|
$stderr.puts "[rails-ai-context] count_keys_for_locale failed: #{e.message}" if ENV["DEBUG"]
|
|
104
108
|
0
|
|
105
109
|
end
|
|
106
110
|
|
|
111
|
+
# Finds all YAML files contributing translations for the given locale:
|
|
112
|
+
# config/locales/en.yml
|
|
113
|
+
# config/locales/devise.en.yml
|
|
114
|
+
# config/locales/en/users.yml
|
|
115
|
+
# config/locales/admin/en.yml
|
|
116
|
+
def find_locale_paths(locale)
|
|
117
|
+
base = File.join(app.root, "config", "locales")
|
|
118
|
+
return [] unless Dir.exist?(base)
|
|
119
|
+
loc = locale.to_s
|
|
120
|
+
Dir.glob(File.join(base, "**/*.{yml,yaml}")).select do |p|
|
|
121
|
+
name = File.basename(p, ".*")
|
|
122
|
+
rel = p.sub("#{base}/", "")
|
|
123
|
+
name == loc || name.end_with?(".#{loc}") || rel.start_with?("#{loc}/") || rel.include?("/#{loc}/")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
107
127
|
def count_nested_keys(hash, count = 0)
|
|
108
128
|
return count unless hash.is_a?(Hash)
|
|
109
129
|
hash.each_value { |v| count = v.is_a?(Hash) ? count_nested_keys(v, count) : count + 1 }
|
|
@@ -36,7 +36,7 @@ module RailsAiContext
|
|
|
36
36
|
job.name.start_with?("ActionMailer", "ActiveStorage::", "ActionMailbox::", "Turbo::", "Sentry::")
|
|
37
37
|
|
|
38
38
|
queue = job.queue_name
|
|
39
|
-
queue =
|
|
39
|
+
queue = "dynamic" if queue.is_a?(Proc)
|
|
40
40
|
|
|
41
41
|
{
|
|
42
42
|
name: job.name,
|
|
@@ -363,12 +363,11 @@ module RailsAiContext
|
|
|
363
363
|
methods = []
|
|
364
364
|
in_class_methods = false
|
|
365
365
|
source.each_line do |line|
|
|
366
|
-
in_class_methods = true if line.match?(/\A\s*
|
|
367
|
-
if line.match
|
|
368
|
-
methods <<
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
methods << line.match(/def (\w+)/)[1]
|
|
366
|
+
in_class_methods = true if line.match?(/\A\s*class << self\b/)
|
|
367
|
+
if (m = line.match(/\A\s*def self\.(\w+)/))
|
|
368
|
+
methods << m[1]
|
|
369
|
+
elsif in_class_methods && (m = line.match(/\A\s*def (\w+)/))
|
|
370
|
+
methods << m[1]
|
|
372
371
|
end
|
|
373
372
|
in_class_methods = false if in_class_methods && line.match?(/\A\s*end\s*$/) && !line.match?(/def/)
|
|
374
373
|
end
|
|
@@ -43,26 +43,26 @@ module RailsAiContext
|
|
|
43
43
|
tables = {}
|
|
44
44
|
|
|
45
45
|
current_table = nil
|
|
46
|
+
inside_create_table = false
|
|
46
47
|
content.each_line do |line|
|
|
47
48
|
if (match = line.match(/create_table\s+"(\w+)"/))
|
|
48
49
|
current_table = match[1]
|
|
50
|
+
inside_create_table = true
|
|
49
51
|
tables[current_table] = { columns: [], indexes: [] }
|
|
50
|
-
elsif
|
|
52
|
+
elsif inside_create_table && line.match?(/\A\s*end\b/)
|
|
53
|
+
inside_create_table = false
|
|
54
|
+
current_table = nil
|
|
55
|
+
elsif inside_create_table
|
|
51
56
|
if (col = line.match(/t\.(\w+)\s+"(\w+)"/))
|
|
52
57
|
tables[current_table][:columns] << { type: col[1], name: col[2] }
|
|
53
58
|
elsif (ref = line.match(/t\.references\s+"(\w+)"/))
|
|
54
59
|
tables[current_table][:columns] << { type: "references", name: "#{ref[1]}_id" }
|
|
55
|
-
elsif (idx = line.match(/add_index\s+"#{Regexp.escape(current_table)}",\s+(?:"(\w+)"|\[([^\]]+)\])/))
|
|
56
|
-
col_name = idx[1] || idx[2]&.gsub(/["'\s]/, "")
|
|
57
|
-
tables[current_table][:indexes] << col_name
|
|
58
60
|
elsif (tidx = line.match(/t\.index\s+\[([^\]]+)\]/))
|
|
59
61
|
# t.index ["col_name"] inside create_table block
|
|
60
62
|
cols = tidx[1].gsub(/["'\s]/, "").split(",")
|
|
61
63
|
cols.each { |c| tables[current_table][:indexes] << c }
|
|
62
64
|
end
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (idx = line.match(/add_index\s+"(\w+)",\s+(?:"(\w+)"|\[([^\]]+)\])/))
|
|
65
|
+
elsif (idx = line.match(/add_index\s+"(\w+)",\s+(?:"(\w+)"|\[([^\]]+)\])/))
|
|
66
66
|
table = idx[1]
|
|
67
67
|
col_name = idx[2] || idx[3]&.gsub(/["'\s]/, "")
|
|
68
68
|
tables[table][:indexes] << col_name if tables[table]
|
|
@@ -246,7 +246,7 @@ module RailsAiContext
|
|
|
246
246
|
combined = "#{query_chain}\n#{action_body}"
|
|
247
247
|
preload_re = /\.(#{PRELOAD_METHODS.join("|")})\(/
|
|
248
248
|
# Match both :assoc_name (symbol) and assoc_name: (hash key for nested includes)
|
|
249
|
-
specific_re = /\.(#{PRELOAD_METHODS.join("|")})\(.*(:#{Regexp.escape(assoc_name)}\b|#{Regexp.escape(assoc_name)}:)/
|
|
249
|
+
specific_re = /\.(#{PRELOAD_METHODS.join("|")})\(.*(:#{Regexp.escape(assoc_name)}\b|#{Regexp.escape(assoc_name)}:)/m
|
|
250
250
|
|
|
251
251
|
if combined.match?(specific_re)
|
|
252
252
|
:low
|
|
@@ -46,7 +46,7 @@ module RailsAiContext
|
|
|
46
46
|
uses_csv: content.match?(/CSV\.|require.*csv/i),
|
|
47
47
|
loads_directory: content.match?(/Dir\[|Dir\.glob|load.*seeds/),
|
|
48
48
|
environment_conditional: content.match?(/Rails\.env/),
|
|
49
|
-
has_ordering: content.match?(/load.*
|
|
49
|
+
has_ordering: content.match?(/Dir\[.*\*\.rb\]\.sort|load\s+["'].*_\d+\.rb|require_relative\s+["'].*_\d+/)
|
|
50
50
|
}
|
|
51
51
|
rescue => e
|
|
52
52
|
{ exists: false, error: e.message }
|
|
@@ -159,7 +159,7 @@ module RailsAiContext
|
|
|
159
159
|
def render_mcp_tools_rule
|
|
160
160
|
lines = [
|
|
161
161
|
"---",
|
|
162
|
-
"description: \"Rails tools (
|
|
162
|
+
"description: \"Rails tools (#{tool_count}) — MANDATORY, use before reading any reference files\"",
|
|
163
163
|
"alwaysApply: true",
|
|
164
164
|
"---",
|
|
165
165
|
""
|
|
@@ -23,22 +23,27 @@ module RailsAiContext
|
|
|
23
23
|
RailsAiContext.configuration.tool_mode
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Derived from Server::TOOLS — the single source of truth for tool count.
|
|
27
|
+
def tool_count
|
|
28
|
+
RailsAiContext::Server::TOOLS.size
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
def tools_header
|
|
27
|
-
"## Tools (
|
|
32
|
+
"## Tools (#{tool_count}) — MANDATORY, Use Before Read"
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def tools_intro
|
|
31
36
|
case tool_mode
|
|
32
37
|
when :cli
|
|
33
38
|
[
|
|
34
|
-
"This project has
|
|
39
|
+
"This project has #{tool_count} introspection tools. **MANDATORY — use these instead of reading files.**",
|
|
35
40
|
"They return ground truth from the running app: real schema, real associations, real filters — not guesses.",
|
|
36
41
|
"Read files ONLY when you are about to Edit them.",
|
|
37
42
|
""
|
|
38
43
|
]
|
|
39
44
|
else
|
|
40
45
|
[
|
|
41
|
-
"This project has
|
|
46
|
+
"This project has #{tool_count} MCP tools via `rails ai:serve` (configured in `.mcp.json`).",
|
|
42
47
|
"**MANDATORY — use these instead of reading files.** They return ground truth from the running app:",
|
|
43
48
|
"real schema, real associations, real filters — not guesses from file reads.",
|
|
44
49
|
"Read files ONLY when you are about to Edit them.",
|
|
@@ -189,7 +194,7 @@ module RailsAiContext
|
|
|
189
194
|
end
|
|
190
195
|
|
|
191
196
|
def tools_table
|
|
192
|
-
lines = [ "### All
|
|
197
|
+
lines = [ "### All #{tool_count} Tools", "" ]
|
|
193
198
|
lines.concat(build_tools_table(include_mcp: tool_mode != :cli))
|
|
194
199
|
lines
|
|
195
200
|
end
|
data/server.json
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "5.
|
|
10
|
+
"version": "5.1.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v5.
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v5.1.0/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|