rails-ai-context 5.9.0 → 5.9.1
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 +20 -0
- data/Rakefile +28 -0
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +3 -3
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +3 -3
- data/lib/rails_ai_context/tools/diagnose.rb +5 -5
- data/lib/rails_ai_context/tools/get_concern.rb +5 -3
- data/lib/rails_ai_context/tools/review_changes.rb +4 -4
- data/lib/rails_ai_context/tools/validate.rb +3 -3
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63fa43e2446217a8b226ba3fd59b0345c7e4fec6300acd0ab877354339187dac
|
|
4
|
+
data.tar.gz: 0d12192067351b3e28e1ae8ba16792be31a8e340e356b4b261249345a2295fa3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6d3f4378d84b890464cdda8c550195430987c05970b892184bda610fb727f7614d943b46141e4b3f7b39642503deacf71b76fca933923aada849e1c39f887fef
|
|
7
|
+
data.tar.gz: 4632cbf34995fb74922c19f89988d7bd4a5dfb3cdac98ab78e8ab3d08bac11f84c3cab0df68364b46740c133fdda22bd7311400dd5381d857b611cb4de2fe6e7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.9.1] — 2026-04-20
|
|
9
|
+
|
|
10
|
+
### Fixed — `GetConcern` missed plural concern names (#78)
|
|
11
|
+
|
|
12
|
+
Thanks to [@johan--](https://github.com/johan--) for the report and fix.
|
|
13
|
+
|
|
14
|
+
`rails_get_concern`'s includer search built its include-pattern regex via `String#classify`, which singularizes its input. Concerns with intentionally plural module names — `WorksheetImports`, `PaperTrailEvents`, `SoftDeletables`, etc. — got demodulized to `WorksheetImport` and the `include WorksheetImports` line in the model never matched. The tool reported no includers even when the concern was in use.
|
|
15
|
+
|
|
16
|
+
Switched to `String#camelize`, which normalizes case (so lowercase input like `plan_limitable` → `PlanLimitable` still works) **without** singularizing. This also restores consistency with the three other `.camelize` calls already used in `get_concern.rb` for the same "file basename / module name → class name" conversion. Covered by a new spec in `get_concern_spec.rb` that exercises the plural-name case end-to-end.
|
|
17
|
+
|
|
18
|
+
### Fixed — internal invariant compliance
|
|
19
|
+
|
|
20
|
+
- **`validate.rb` now routes all Prism parses through `AstCache`.** The Ruby syntax validator was calling `Prism.parse_file` directly and the ERB + semantic-visitor paths `Prism.parse` on string input, bypassing the cache entirely for the first and violating the "all Prism parses must flow through `RailsAiContext::AstCache`" invariant (`.results/3-identify-architecture.json:33`) for all three. Now uses `AstCache.parse(path)` for on-disk sources (picking up the existing size-cap + content-hash caching) and `AstCache.parse_string(source)` for synthetic strings. Note: `AstCache.parse` enforces a 5 MB `MAX_PARSE_SIZE` cap; Ruby files above that size fall through the existing `rescue` to the `ruby -c` subprocess validator, which returns errors but not Prism warnings — a graceful degradation that affects only pathologically large source files.
|
|
21
|
+
- **`Listeners::BaseListener` uses `Confidence::INFERRED` constant.** Replaced three hardcoded `"[INFERRED]"` literals in `extract_first_symbol`, `extract_key`, and `extract_value` with `RailsAiContext::Confidence::INFERRED`. Value is identical; constant reference prevents drift if the marker string is ever versioned.
|
|
22
|
+
- **Diagnostic `$stderr.puts` in `rescue` blocks now `ENV["DEBUG"]`-gated.** 12 previously-unconditional stderr writes across `tools/diagnose.rb` (5), `tools/review_changes.rb` (4), and `serializers/stack_overview_helper.rb` (3) were logging under normal operation whenever an optional context-enrichment step failed. These were never visible to most users but polluted stderr in MCP/CLI logs. Now silent unless `DEBUG=1`, matching the convention used everywhere else in the gem.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Prism-discipline regression spec** (`spec/lib/rails_ai_context/ast_cache_discipline_spec.rb`). Scans every `lib/**/*.rb` file (excluding `ast_cache.rb`) for direct `Prism.parse` / `Prism.parse_file` / `Prism.parse_string` calls and fails if any are found. Prevents re-introduction of the bypass that `validate.rb` had.
|
|
27
|
+
|
|
8
28
|
## [5.9.0] — 2026-04-16
|
|
9
29
|
|
|
10
30
|
### Fixed — Cursor chat agent didn't detect rules
|
data/Rakefile
CHANGED
|
@@ -43,4 +43,32 @@ RSpec::Core::RakeTask.new(:e2e) do |t|
|
|
|
43
43
|
ENV["E2E"] = "1"
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
namespace :e2e do
|
|
47
|
+
desc "Run e2e specs in parallel via parallel_tests (opt-in; uses all CPUs)"
|
|
48
|
+
task :parallel do
|
|
49
|
+
# Opt-in parallelization. Each parallel_tests worker is a separate
|
|
50
|
+
# process — it will rebuild its own shared-fixture memoization and
|
|
51
|
+
# its own built .gem artefact. Still a net win because rspec-level
|
|
52
|
+
# wall-clock scales with the slowest worker, not the sum.
|
|
53
|
+
#
|
|
54
|
+
# Caveats:
|
|
55
|
+
# - postgres_install_spec is excluded here because its DB name is
|
|
56
|
+
# derived from E2E_DB_SUFFIX and workers would collide unless you
|
|
57
|
+
# pre-create per-worker databases. Run `rake e2e` separately if
|
|
58
|
+
# you need postgres coverage.
|
|
59
|
+
# - Shared BUNDLE_PATH is NOT used in parallel mode — two workers
|
|
60
|
+
# writing to the same bundle path can corrupt native extension
|
|
61
|
+
# builds. Each worker gets its own implicit bundle directory.
|
|
62
|
+
require "shellwords"
|
|
63
|
+
spec_files = Dir["spec/e2e/**/*_spec.rb"].reject { |p| p.include?("postgres_install_spec") }
|
|
64
|
+
cmd = [
|
|
65
|
+
"bundle", "exec", "parallel_rspec",
|
|
66
|
+
"--serialize-stdout",
|
|
67
|
+
"--"
|
|
68
|
+
] + spec_files
|
|
69
|
+
env = { "E2E" => "1", "BUNDLE_PATH" => nil }
|
|
70
|
+
sh(env, *cmd) { |ok, _res| abort("parallel_rspec failed") unless ok }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
46
74
|
task default: :spec
|
|
@@ -23,7 +23,7 @@ module RailsAiContext
|
|
|
23
23
|
case arg
|
|
24
24
|
when Prism::SymbolNode then arg.value.to_sym
|
|
25
25
|
when Prism::StringNode then arg.unescaped.to_sym
|
|
26
|
-
else
|
|
26
|
+
else RailsAiContext::Confidence::INFERRED
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -56,7 +56,7 @@ module RailsAiContext
|
|
|
56
56
|
case node
|
|
57
57
|
when Prism::SymbolNode then node.value.to_sym
|
|
58
58
|
when Prism::StringNode then node.unescaped.to_sym
|
|
59
|
-
else
|
|
59
|
+
else RailsAiContext::Confidence::INFERRED
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -74,7 +74,7 @@ module RailsAiContext
|
|
|
74
74
|
when Prism::ArrayNode then node.elements.map { |e| extract_value(e) }
|
|
75
75
|
when Prism::HashNode then hash_node_to_hash(node)
|
|
76
76
|
when Prism::KeywordHashNode then hash_node_to_hash(node)
|
|
77
|
-
else
|
|
77
|
+
else RailsAiContext::Confidence::INFERRED
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -186,7 +186,7 @@ module RailsAiContext
|
|
|
186
186
|
.map { |f| File.basename(f, ".rb").camelize }
|
|
187
187
|
.reject { |s| s == "ApplicationService" }
|
|
188
188
|
rescue => e
|
|
189
|
-
$stderr.puts "[rails-ai-context] Service file scan skipped: #{e.message}"
|
|
189
|
+
$stderr.puts "[rails-ai-context] Service file scan skipped: #{e.message}" if ENV["DEBUG"]
|
|
190
190
|
[]
|
|
191
191
|
end
|
|
192
192
|
|
|
@@ -198,7 +198,7 @@ module RailsAiContext
|
|
|
198
198
|
.map { |f| File.basename(f, ".rb").camelize }
|
|
199
199
|
.reject { |j| j == "ApplicationJob" }
|
|
200
200
|
rescue => e
|
|
201
|
-
$stderr.puts "[rails-ai-context] Job file scan skipped: #{e.message}"
|
|
201
|
+
$stderr.puts "[rails-ai-context] Job file scan skipped: #{e.message}" if ENV["DEBUG"]
|
|
202
202
|
[]
|
|
203
203
|
end
|
|
204
204
|
|
|
@@ -208,7 +208,7 @@ module RailsAiContext
|
|
|
208
208
|
return [] unless File.exist?(app_ctrl_file)
|
|
209
209
|
File.read(app_ctrl_file).scan(/before_action\s+:([\w!?]+)/).flatten
|
|
210
210
|
rescue => e
|
|
211
|
-
$stderr.puts "[rails-ai-context] Before actions scan skipped: #{e.message}"
|
|
211
|
+
$stderr.puts "[rails-ai-context] Before actions scan skipped: #{e.message}" if ENV["DEBUG"]
|
|
212
212
|
[]
|
|
213
213
|
end
|
|
214
214
|
end
|
|
@@ -265,7 +265,7 @@ module RailsAiContext
|
|
|
265
265
|
lines << text
|
|
266
266
|
lines << ""
|
|
267
267
|
end
|
|
268
|
-
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}"; end
|
|
268
|
+
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
269
269
|
end
|
|
270
270
|
end
|
|
271
271
|
|
|
@@ -284,7 +284,7 @@ module RailsAiContext
|
|
|
284
284
|
lines << text
|
|
285
285
|
lines << ""
|
|
286
286
|
end
|
|
287
|
-
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}"; end
|
|
287
|
+
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
288
288
|
end
|
|
289
289
|
end
|
|
290
290
|
|
|
@@ -298,7 +298,7 @@ module RailsAiContext
|
|
|
298
298
|
lines << text
|
|
299
299
|
lines << ""
|
|
300
300
|
end
|
|
301
|
-
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}"; end
|
|
301
|
+
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
302
302
|
end
|
|
303
303
|
|
|
304
304
|
lines
|
|
@@ -329,7 +329,7 @@ module RailsAiContext
|
|
|
329
329
|
"This variable may not be set in all code paths — check if it's assigned before use, " \
|
|
330
330
|
"or use `#{receiver}&.#{method}` for safe navigation."
|
|
331
331
|
end
|
|
332
|
-
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}"; end
|
|
332
|
+
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
333
333
|
end
|
|
334
334
|
end
|
|
335
335
|
|
|
@@ -346,7 +346,7 @@ module RailsAiContext
|
|
|
346
346
|
"The record with the given ID doesn't exist or doesn't belong to the current user. " \
|
|
347
347
|
"Check if the record was deleted or if the user is authorized to access it."
|
|
348
348
|
end
|
|
349
|
-
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}"; end
|
|
349
|
+
rescue => e; $stderr.puts "[rails-ai-context] Diagnosis step skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
350
350
|
end
|
|
351
351
|
end
|
|
352
352
|
|
|
@@ -470,9 +470,11 @@ module RailsAiContext
|
|
|
470
470
|
|
|
471
471
|
max_size = RailsAiContext.configuration.max_file_size
|
|
472
472
|
# Build pattern: match `include ConcernName` or `include ModuleName::ConcernName`
|
|
473
|
-
# Handle both simple and namespaced concern names
|
|
474
|
-
#
|
|
475
|
-
|
|
473
|
+
# Handle both simple and namespaced concern names.
|
|
474
|
+
# Use `camelize` (not `classify`) — `classify` singularizes, which drops
|
|
475
|
+
# the final `s` from plural concern names like `WorksheetImports` and
|
|
476
|
+
# then fails to match `include WorksheetImports` in the model.
|
|
477
|
+
simple_name = concern_name.demodulize.camelize
|
|
476
478
|
pattern = /^\s*include\s+(?:\w+::)*#{Regexp.escape(simple_name)}\b/
|
|
477
479
|
|
|
478
480
|
search_dirs.each do |dir|
|
|
@@ -186,7 +186,7 @@ module RailsAiContext
|
|
|
186
186
|
result = GetModelDetails.call(model: model_name, detail: "standard")
|
|
187
187
|
text = result.content.first[:text]
|
|
188
188
|
lines << "" << "**Model context:** #{model_name}" unless text.include?("not found")
|
|
189
|
-
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}"; end
|
|
189
|
+
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
190
190
|
|
|
191
191
|
when :controller
|
|
192
192
|
ctrl_name = File.basename(file, ".rb").camelize
|
|
@@ -195,7 +195,7 @@ module RailsAiContext
|
|
|
195
195
|
result = GetRoutes.call(controller: snake, detail: "summary")
|
|
196
196
|
text = result.content.first[:text]
|
|
197
197
|
lines << "" << "**Routes:**" << text unless text.include?("not found") || text.include?("No routes")
|
|
198
|
-
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}"; end
|
|
198
|
+
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
199
199
|
|
|
200
200
|
when :migration
|
|
201
201
|
# Parse migration for table/column info
|
|
@@ -211,7 +211,7 @@ module RailsAiContext
|
|
|
211
211
|
result = GetSchema.call(table: t, detail: "summary")
|
|
212
212
|
text = result.content.first[:text]
|
|
213
213
|
lines << " #{t}: #{text.lines.first&.strip}" unless text.include?("not found")
|
|
214
|
-
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}"; end
|
|
214
|
+
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
215
215
|
end
|
|
216
216
|
end
|
|
217
217
|
end
|
|
@@ -221,7 +221,7 @@ module RailsAiContext
|
|
|
221
221
|
begin
|
|
222
222
|
result = GetRoutes.call(detail: "summary")
|
|
223
223
|
lines << "" << "**Current routes:** #{result.content.first[:text].lines.first&.strip}"
|
|
224
|
-
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}"; end
|
|
224
|
+
rescue => e; $stderr.puts "[rails-ai-context] Context lookup skipped: #{e.message}" if ENV["DEBUG"]; end
|
|
225
225
|
end
|
|
226
226
|
|
|
227
227
|
lines << ""
|
|
@@ -170,7 +170,7 @@ module RailsAiContext
|
|
|
170
170
|
end
|
|
171
171
|
|
|
172
172
|
private_class_method def self.validate_ruby_prism(full_path)
|
|
173
|
-
result =
|
|
173
|
+
result = AstCache.parse(full_path.to_s)
|
|
174
174
|
basename = File.basename(full_path.to_s)
|
|
175
175
|
warnings = result.warnings.map do |w|
|
|
176
176
|
"#{basename}:#{w.location.start_line}:#{w.location.start_column}: warning: #{w.message}"
|
|
@@ -213,7 +213,7 @@ module RailsAiContext
|
|
|
213
213
|
erb_src.force_encoding("UTF-8")
|
|
214
214
|
compiled = "# encoding: utf-8\ndef __erb_syntax_check\n#{erb_src}\nend"
|
|
215
215
|
|
|
216
|
-
result =
|
|
216
|
+
result = AstCache.parse_string(compiled)
|
|
217
217
|
if result.success?
|
|
218
218
|
[ true, nil, [] ]
|
|
219
219
|
else
|
|
@@ -457,7 +457,7 @@ module RailsAiContext
|
|
|
457
457
|
content
|
|
458
458
|
end
|
|
459
459
|
|
|
460
|
-
result =
|
|
460
|
+
result = AstCache.parse_string(source)
|
|
461
461
|
visitor = RailsSemanticVisitor.new
|
|
462
462
|
result.value.accept(visitor)
|
|
463
463
|
visitor
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.9.
|
|
4
|
+
version: 5.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -181,6 +181,20 @@ dependencies:
|
|
|
181
181
|
- - "~>"
|
|
182
182
|
- !ruby/object:Gem::Version
|
|
183
183
|
version: '1.4'
|
|
184
|
+
- !ruby/object:Gem::Dependency
|
|
185
|
+
name: parallel_tests
|
|
186
|
+
requirement: !ruby/object:Gem::Requirement
|
|
187
|
+
requirements:
|
|
188
|
+
- - "~>"
|
|
189
|
+
- !ruby/object:Gem::Version
|
|
190
|
+
version: '5.0'
|
|
191
|
+
type: :development
|
|
192
|
+
prerelease: false
|
|
193
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
194
|
+
requirements:
|
|
195
|
+
- - "~>"
|
|
196
|
+
- !ruby/object:Gem::Version
|
|
197
|
+
version: '5.0'
|
|
184
198
|
description: |
|
|
185
199
|
rails-ai-context turns your running Rails app into the source of truth for AI
|
|
186
200
|
coding assistants. Instead of guessing from training data or stale file reads,
|