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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f300193f1fb692df6f1404e884f9efddbafc85b2cadee89adaa995f49e69ca0
4
- data.tar.gz: befb6b7b12702fe0ff805786d1e7628d1c2cca0fd1a7f85d7a655a0521bde69a
3
+ metadata.gz: 63fa43e2446217a8b226ba3fd59b0345c7e4fec6300acd0ab877354339187dac
4
+ data.tar.gz: 0d12192067351b3e28e1ae8ba16792be31a8e340e356b4b261249345a2295fa3
5
5
  SHA512:
6
- metadata.gz: 95950aab08d9547799c7271d838cb145deee30a7b29930ffc8bbe2c83ff8b130a1ecb8eb53722d025c91658d7ea9365c5a5bb81aafce796aa51b28c91b0980e8
7
- data.tar.gz: 43a6e75ff67650e5ab91b95cab532fc9a09fa1c60e52728825a145101fcdd54e2aabb59d250fcdb5c3eede77309b9c6030023f9ba3857a45dc9b1824031790c0
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 "[INFERRED]"
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 "[INFERRED]"
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 "[INFERRED]"
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
- # Classify to handle lowercase input: "plan_limitable" "PlanLimitable"
475
- simple_name = concern_name.demodulize.classify
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 = Prism.parse_file(full_path.to_s)
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 = Prism.parse(compiled)
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 = Prism.parse(source)
460
+ result = AstCache.parse_string(source)
461
461
  visitor = RailsSemanticVisitor.new
462
462
  result.value.accept(visitor)
463
463
  visitor
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "5.9.0"
4
+ VERSION = "5.9.1"
5
5
  end
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.0
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,