rails-ai-context 5.16.1 → 5.17.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -10
  3. data/AGENTS.md +15 -0
  4. data/CHANGELOG.md +128 -0
  5. data/CONTRIBUTING.md +1 -1
  6. data/README.md +3 -3
  7. data/app/controllers/rails_ai_context/mcp_controller.rb +44 -2
  8. data/docs/ARCHITECTURE.md +9 -9
  9. data/docs/CLI.md +2 -2
  10. data/docs/CONFIGURATION.md +15 -4
  11. data/docs/CUSTOM_TOOLS.md +2 -2
  12. data/docs/FAQ.md +3 -3
  13. data/docs/GUIDE.md +4 -4
  14. data/docs/INTROSPECTORS.md +4 -3
  15. data/docs/QUICKSTART.md +1 -1
  16. data/docs/SECURITY.md +29 -18
  17. data/docs/SETUP.md +3 -1
  18. data/docs/STANDALONE.md +2 -2
  19. data/docs/TOOLS.md +2 -2
  20. data/docs/TROUBLESHOOTING.md +2 -2
  21. data/docs/agents/domain.md +38 -0
  22. data/docs/agents/issue-tracker.md +45 -0
  23. data/docs/agents/triage-labels.md +15 -0
  24. data/docs/index.md +1 -1
  25. data/exe/rails-ai-context +29 -5
  26. data/lib/rails_ai_context/configuration.rb +3 -1
  27. data/lib/rails_ai_context/detail_level.rb +40 -0
  28. data/lib/rails_ai_context/introspectors/convention_introspector.rb +14 -11
  29. data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
  30. data/lib/rails_ai_context/introspectors/gem_introspector.rb +0 -2
  31. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +11 -0
  32. data/lib/rails_ai_context/introspectors/performance_introspector.rb +5 -40
  33. data/lib/rails_ai_context/introspectors/schema_introspector.rb +85 -182
  34. data/lib/rails_ai_context/introspectors/schema_reader.rb +166 -0
  35. data/lib/rails_ai_context/introspectors/source_introspector.rb +2 -4
  36. data/lib/rails_ai_context/mcp_config_generator.rb +5 -12
  37. data/lib/rails_ai_context/resources.rb +11 -0
  38. data/lib/rails_ai_context/safe_file.rb +14 -0
  39. data/lib/rails_ai_context/serializers/claude_serializer.rb +7 -9
  40. data/lib/rails_ai_context/serializers/context_file_serializer.rb +1 -8
  41. data/lib/rails_ai_context/serializers/context_mode_dispatch.rb +25 -0
  42. data/lib/rails_ai_context/serializers/copilot_serializer.rb +4 -13
  43. data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
  44. data/lib/rails_ai_context/serializers/opencode_serializer.rb +4 -13
  45. data/lib/rails_ai_context/serializers/section_marker_writer.rb +1 -6
  46. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -1
  47. data/lib/rails_ai_context/tasks/rails_ai_context.rake +10 -7
  48. data/lib/rails_ai_context/tools/analyze_feature.rb +1 -1
  49. data/lib/rails_ai_context/tools/diagnose.rb +2 -2
  50. data/lib/rails_ai_context/tools/generate_test.rb +1 -1
  51. data/lib/rails_ai_context/tools/get_callbacks.rb +2 -2
  52. data/lib/rails_ai_context/tools/get_concern.rb +2 -2
  53. data/lib/rails_ai_context/tools/get_conventions.rb +18 -1
  54. data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -1
  55. data/lib/rails_ai_context/tools/get_helper_methods.rb +20 -5
  56. data/lib/rails_ai_context/tools/get_schema.rb +1 -1
  57. data/lib/rails_ai_context/tools/get_test_info.rb +1 -1
  58. data/lib/rails_ai_context/tools/get_view.rb +1 -1
  59. data/lib/rails_ai_context/tools/onboard.rb +3 -3
  60. data/lib/rails_ai_context/tools/performance_check.rb +3 -3
  61. data/lib/rails_ai_context/tools/query.rb +10 -7
  62. data/lib/rails_ai_context/tools/review_changes.rb +18 -2
  63. data/lib/rails_ai_context/tools/runtime_info.rb +4 -4
  64. data/lib/rails_ai_context/tools/search_code.rb +2 -2
  65. data/lib/rails_ai_context/tools/validate.rb +5 -853
  66. data/lib/rails_ai_context/tools/validate_semantics.rb +879 -0
  67. data/lib/rails_ai_context/version.rb +1 -1
  68. data/lib/rails_ai_context/vfs.rb +26 -2
  69. metadata +9 -2
  70. data/docs/superpowers/plans/2026-05-24-prism-ast-migration.md +0 -1218
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91f2141cb80001b16aa88dc0512da99e1f8b72069716920ddf93ac5011c0074b
4
- data.tar.gz: fd468f67533f4e7d5f9a00f27af3508a3fe1df2834321a66ad7411d8e4e83701
3
+ metadata.gz: ebb8d4e0ebed93183c0c019071ee63d097fb8c7495f551393991000aa5d06e7b
4
+ data.tar.gz: 2141cf0a281a173da2f0fa34927be843f93600ced5cdd67ccd6eb4aeef261ce0
5
5
  SHA512:
6
- metadata.gz: 59a7f897a5241af025bf4b383d60b12548f08a6fdaf7c80cb0acb398b5984134367bf01b8fc9441a842fd99c197bea59e325b3fa3365ef274249a4e80e7a6306
7
- data.tar.gz: 2adc3898c6fd3df5026d59a8aef9d4141ee53d902f36797b64c5fce2e1a4698144cb25212a7a14478b10124afba6cde680c8cbb78dfbdb791fbcf9a16f03ad5b
6
+ metadata.gz: e1bdda115ee2c82a43af9f6389ad18204629539e5867db0ae36d8c4fe86f50547084315470ed5842adccbced095026e2a220f32626d1c1e43600b595d7224afb
7
+ data.tar.gz: 197fe45172ce6888fd89ee63ecc505f5e014f1b3c759a918a7244025958ce9d17801f4bf1a0ac3ce401a93063b0f3d75998b4ec485f6bdbe4df9449be333920d
data/.rubocop.yml CHANGED
@@ -11,13 +11,6 @@ AllCops:
11
11
  Style/Documentation:
12
12
  Enabled: false
13
13
 
14
- Metrics/MethodLength:
15
- Max: 25
16
-
17
- Metrics/ClassLength:
18
- Max: 200
19
-
20
- Metrics/BlockLength:
21
- Exclude:
22
- - "spec/**/*"
23
- - "*.gemspec"
14
+ # rubocop-rails-omakase disables the whole Metrics department. Setting Max on a
15
+ # disabled cop does not re-enable it, so size limits here would enforce nothing.
16
+ # Turning it on would need ~40 existing files brought under the limit first.
data/AGENTS.md ADDED
@@ -0,0 +1,15 @@
1
+ # rails-ai-context
2
+
3
+ ## Agent skills
4
+
5
+ ### Issue tracker
6
+
7
+ Issues live as GitHub issues on `crisnahine/rails-ai-context`, driven with the `gh` CLI. See `docs/agents/issue-tracker.md`.
8
+
9
+ ### Triage labels
10
+
11
+ The five canonical roles, each label string equal to its name (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
12
+
13
+ ### Domain docs
14
+
15
+ Single-context: `CONTEXT.md` and `docs/adr/` at the repo root. See `docs/agents/domain.md`.
data/CHANGELOG.md CHANGED
@@ -5,6 +5,134 @@ 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.17.0] - 2026-08-09
9
+
10
+ ### Added
11
+
12
+ - **`config.query_allowed_columns`** exempts a column name from the built-in
13
+ sensitive list used by `rails_query`. The rejection message told you to
14
+ subtract from `query_redacted_columns`, which could never work: that list is
15
+ unioned with a frozen suffix list, so an app with its own
16
+ `oauth_applications.secret` had no way to query it.
17
+
18
+ ### Fixed
19
+
20
+ - **Documented security behaviour now matches the code.** The ReDoS timeout on
21
+ user-supplied regexes needs `Regexp.timeout`, which Ruby 3.1 does not have,
22
+ so `SECURITY.md` no longer promises it unconditionally on a version the gem
23
+ still supports. Layer 4 was documented as post-execution redaction; it has
24
+ rejected the query outright since 5.8.1, which is what the doc now says.
25
+ - **`config.introspectors = %i[source]` no longer appears to be supported.**
26
+ `SourceIntrospector` was listed in the introspector table but is
27
+ infrastructure, not a registered introspector, so configuring it raised
28
+ `ConfigurationError`.
29
+ - **Corrected documented defaults**, which had drifted: `sensitive_patterns` is
30
+ 27 patterns not 8, `excluded_middleware` 25 not 24, `excluded_filters` 5 not
31
+ 3, and the listener count is 21. `extra_app_paths` and
32
+ `instrumentation_include_arguments` were undocumented.
33
+ - **Secondary database dumps report their own generated columns.** Parsing
34
+ `db/queue_schema.rb` read generated columns from the primary `db/schema.rb`
35
+ instead of the dump being parsed.
36
+ - **Polymorphic foreign keys no longer always report a missing index.** The
37
+ compound check compared one joined string against two column names, so it
38
+ could never match an existing index.
39
+ - **A column is no longer treated as indexed because a wider column name is.**
40
+ Index matching compared substrings, so an index on `user_id` counted as
41
+ covering `id`.
42
+
43
+ ### Changed
44
+
45
+ - **schema.rb is read through one AST-backed reader.** `SchemaIntrospector`,
46
+ `PerformanceIntrospector` and `ConventionIntrospector` each parsed the dump
47
+ line by line with their own `create_table` regex; they now share
48
+ `SchemaReader`, built on the existing `SchemaDslListener`, as do the static
49
+ tier's table parse and the check-constraint, enum and generated-column
50
+ reads that each walked the dump separately.
51
+ - **The static schema tier reports four things differently.** Where no live
52
+ connection is available and `db/schema.rb` is parsed instead: a `t.references`
53
+ or `t.belongs_to` column is reported by its foreign key name (`author_id`,
54
+ not `author`), which is what the live tier has always reported; a column
55
+ default written as a proc reports its source rather than being omitted; a
56
+ default split across lines is picked up; and an expression index declared at
57
+ the top level is flagged `expression: true`, as an in-table one already was.
58
+ - **`rails_validate`'s Rails-aware rules moved to `ValidateSemantics`.** The
59
+ tool entry point kept syntax validation and orchestration; the 15 lint rules
60
+ now live in their own class. Behaviour is unchanged.
61
+ - **`detail` has a type.** `DetailLevel` defines the three levels, their
62
+ ordering and normalization. An unrecognised value now reads as `standard`
63
+ rather than silently selecting whichever branch happened to be last.
64
+ - **Soft-delete detection prefers the schema.** A `deleted_at` column in the
65
+ dump now drives the `soft_delete` convention, instead of matching the word
66
+ anywhere in model source. Apps dumping `structure.sql` keep the old source
67
+ match, and the `acts_as_paranoid` / `discard` macro check is unchanged.
68
+
69
+ ## [5.16.2] - 2026-07-17
70
+
71
+ ### Fixed
72
+
73
+ - **Standalone static tier works when installed via `gem install`.**
74
+ `tool <name> --no-boot`, `tool <name> --help`, and `tool --list` outside
75
+ an app all died with `cannot load such file -- active_support/...`. The
76
+ RubyGems binstub eagerly activates the whole dependency tree, so the
77
+ exe's framework-path strip left every rails-family spec still flagged as
78
+ activated and `Gem.try_activate` refused to re-add the stripped paths.
79
+ The exe now stashes the stripped specs and splices their load paths back
80
+ at every app-less require site (Bundler-pinned paths still win after a
81
+ boot). Verified through the real binstub in an empty directory, in an
82
+ app whose Gemfile does not include the gem, and on the boot-failed
83
+ static fallback.
84
+ - **Engine-mounted MCP survives mcp SDK 0.24.** The SDK's SSE writer now
85
+ calls `stream.flush` after every event, and
86
+ `ActionController::Live::Buffer` defines no `flush` - every SSE-mode
87
+ `tools/call` through `mount RailsAiContext::Engine` 500'd on all
88
+ supported Rails versions (the payload still arrived, then the request
89
+ died). The controller now hands the transport a flush-capable stream.
90
+ Verified with live curl sessions on booted Rails 7.1 and 8.1 apps.
91
+ - **Engine-mounted GET streams actually stay open.** The transport
92
+ registers the server-push stream and returns; Live then closed the
93
+ response as soon as the action returned, so the SSE channel died
94
+ instantly, `notifications/tools/list_changed` could never be delivered,
95
+ and (once held open) clients would have waited up to 30s for response
96
+ headers. The action now commits headers immediately with an SSE comment
97
+ and holds the thread until the transport or the client closes the
98
+ stream. SETUP.md documents the one-thread-per-connected-client cost.
99
+ - **Helpers keep their namespaces.** `app/helpers/admin/dashboard_helper.rb`
100
+ was reported as `DashboardHelper`, and looking it up by its real constant
101
+ name `Admin::DashboardHelper` failed. Module names now derive from the
102
+ path under `app/helpers` (`app/helpers/concerns` stays its own root,
103
+ matching the railties autoload glob), and exact path matches win before
104
+ basename fallbacks so a top-level helper is not shadowed by a namespaced
105
+ one.
106
+ - **`rails_validate` checks qualified render targets in Ruby files.**
107
+ `render partial: "posts/missing"` in a controller or service passed
108
+ `level:"rails"` silently (the partial check only ran for ERB). Bare
109
+ `render "posts/show"` keeps template semantics in Ruby files, so the
110
+ controller template-render idiom is not false-flagged. Tool descriptions
111
+ now state exactly which column references are checked
112
+ (validates/permit/callbacks).
113
+ - **`rails_review_changes` describes untracked files.** New files rendered
114
+ as empty headings (a fresh app right after `rails new` plus install
115
+ showed 20 of them); they now read `_new file, N lines_`, and only the
116
+ HEAD flow makes that claim (a committed-then-reverted file against an
117
+ older ref reads `_no diff available_`).
118
+ - **Views resource resolves extension-less paths.** Reading
119
+ `rails-ai-context://views/posts/index` now serves
120
+ `posts/index.html.erb`; a same-named directory no longer defeats the
121
+ lookup, and sensitive-file candidates are rejected so the
122
+ not-found/not-allowed message split cannot act as an existence oracle
123
+ for secrets placed under `app/views`.
124
+ - **Resource templates accept both URI schemes.** `rails://` and
125
+ `rails-ai-context://` now both resolve for the controllers, views, and
126
+ routes templates, and contents echo the URI the client requested.
127
+ - **`preset` output no longer scrambles under pipes** in either CLI:
128
+ all framing (banner, separators, "Running:" labels) goes to stderr and
129
+ stdout carries pure tool output.
130
+ - **Context files stop leaking internal tokens.** Architecture sections
131
+ showed raw keys (`concerns_models`, `pwa`, `solid_queue`) next to
132
+ humanized labels; every token the convention introspector emits now has
133
+ a label. Stale doc counts corrected across README and docs (39 tools,
134
+ 25 doctor checks).
135
+
8
136
  ## [5.16.1] - 2026-07-12
9
137
 
10
138
  ### Fixed
data/CONTRIBUTING.md CHANGED
@@ -20,7 +20,7 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
20
20
  lib/rails_ai_context/
21
21
  ├── cli/ # CLI tool runner (tool_runner.rb) - executes MCP tools from rake/Thor
22
22
  ├── introspectors/ # 39 introspectors (schema, models, routes, etc.)
23
- ├── tools/ # 38 MCP tools with detail levels and pagination
23
+ ├── tools/ # 39 MCP tools with detail levels and pagination
24
24
  ├── serializers/ # Per-assistant formatters + shared ToolGuideHelper
25
25
  ├── server.rb # MCP server setup (stdio + HTTP)
26
26
  ├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
data/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
  <br>
22
22
  [![Ruby](https://img.shields.io/badge/Ruby-3.1%20%7C%203.2%20%7C%203.3%20%7C%203.4-CC342D)](https://github.com/crisnahine/rails-ai-context)
23
23
  [![Rails](https://img.shields.io/badge/Rails-7.0%20%7C%207.1%20%7C%207.2%20%7C%208.0%20%7C%208.1-CC0000)](https://github.com/crisnahine/rails-ai-context)
24
- [![Tests](https://img.shields.io/badge/Tests-2501%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
24
+ [![Tests](https://img.shields.io/badge/Tests-2608%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
25
25
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
26
26
 
27
27
  </div>
@@ -245,7 +245,7 @@ rails 'ai:tool[stimulus]' controller=chart
245
245
 
246
246
  <br>
247
247
 
248
- ## 38 Tools
248
+ ## 39 Tools
249
249
 
250
250
  Every tool is **read-only** and returns data verified against your actual app - not guesses, not training data.
251
251
 
@@ -509,7 +509,7 @@ empty listings.
509
509
 
510
510
  ## Build your own tools
511
511
 
512
- Register custom MCP tools alongside the 38 built-in ones:
512
+ Register custom MCP tools alongside the 39 built-in ones:
513
513
 
514
514
  ```ruby
515
515
  # app/mcp_tools/rails_get_business_metrics.rb
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "delegate"
4
+
3
5
  module RailsAiContext
4
6
  # Rails controller for serving MCP over Streamable HTTP.
5
7
  # Alternative to the Rack middleware - integrates with Rails routing,
@@ -16,10 +18,25 @@ module RailsAiContext
16
18
  # supported Rails versions.
17
19
  include ActionController::Live
18
20
 
21
+ # The MCP SDK's SSE writer calls stream.flush after every event, but
22
+ # Live's buffer writes straight through to the client and defines no
23
+ # flush - the NoMethodError would 500 the request after the payload was
24
+ # already delivered and tear down long-lived GET streams.
25
+ class FlushableStream < SimpleDelegator
26
+ def flush; end
27
+ end
28
+
19
29
  def handle
20
30
  status_code, rack_headers, body = self.class.mcp_transport.handle_request(request)
21
31
  self.status = status_code
22
- rack_headers.each { |k, v| response.headers[k] = v }
32
+ rack_headers.each do |k, v|
33
+ # mcp >= 1.0 returns Rack 3-style lowercase header keys. Rails 7.0's
34
+ # response header hash is case-sensitive (Rack 2), so a lowercase
35
+ # "content-type" never registers and Rails falls back to text/html on
36
+ # an otherwise valid JSON body. Write it with the canonical case.
37
+ key = k.casecmp("content-type").zero? ? "Content-Type" : k
38
+ response.headers[key] = v
39
+ end
23
40
 
24
41
  if body.respond_to?(:each)
25
42
  # Plain enumerable body (initialize, errors, JSON mode): join to a
@@ -29,8 +46,25 @@ module RailsAiContext
29
46
  body.close if body.respond_to?(:close)
30
47
  self.response_body = chunks.join
31
48
  elsif body.respond_to?(:call)
49
+ stream = response.stream
50
+ stream = FlushableStream.new(stream) unless stream.respond_to?(:flush)
32
51
  begin
33
- body.call(response.stream)
52
+ body.call(stream)
53
+ # A GET opens the server-push channel: the transport registers the
54
+ # stream and returns, expecting it to outlive this call. Live closes
55
+ # the response when the action returns, so hold the thread until the
56
+ # transport's keepalive (or the client) closes the stream. Live also
57
+ # sends headers only on the first write - the transport writes
58
+ # nothing until its first keepalive ping, so commit with an SSE
59
+ # comment up front or clients sit waiting on headers.
60
+ if request.get?
61
+ begin
62
+ stream.write(": connected\n\n")
63
+ rescue IOError
64
+ nil
65
+ end
66
+ wait_for_stream_close
67
+ end
34
68
  ensure
35
69
  begin
36
70
  response.stream.close
@@ -43,6 +77,14 @@ module RailsAiContext
43
77
  end
44
78
  end
45
79
 
80
+ private
81
+
82
+ def wait_for_stream_close
83
+ sleep 0.5 until response.stream.closed?
84
+ rescue IOError
85
+ nil
86
+ end
87
+
46
88
  class << self
47
89
  # Class-level memoization - transport persists across requests.
48
90
  # Thread-safe: MCP::Server and transport are stateless for reads.
data/docs/ARCHITECTURE.md CHANGED
@@ -25,12 +25,12 @@ graph TD
25
25
 
26
26
  subgraph engine["Introspection Engine"]
27
27
  direction LR
28
- I["Introspectors\n31 modules\nPresets\nCached"]
29
- AST["AST Engine\nPrism\n20 listeners\nConfidence tags"]
28
+ I["Introspectors\n39 modules\nPresets\nCached"]
29
+ AST["AST Engine\nPrism\n21 listeners\nConfidence tags"]
30
30
  H["Hydration Layer\nSchema hints\ninjected into\ntool responses"]
31
31
  end
32
32
 
33
- engine --> R["Tool Registry\n38 tools auto-discovered via inherited\n+ custom_tools - skip_tools = active tools"]
33
+ engine --> R["Tool Registry\n39 tools auto-discovered via inherited\n+ custom_tools - skip_tools = active tools"]
34
34
  end
35
35
 
36
36
  R --> MCP
@@ -40,7 +40,7 @@ graph TD
40
40
  subgraph outputs["Output"]
41
41
  direction LR
42
42
  MCP["MCP Server\nstdio / HTTP\nResources\nVFS URIs"]
43
- CLI["CLI Runner\nRake / Thor\nSame 38 tools\nNo server needed"]
43
+ CLI["CLI Runner\nRake / Thor\nSame 39 tools\nNo server needed"]
44
44
  S["Serializers\n14 modules\nStatic files\nPer-AI-tool"]
45
45
  end
46
46
 
@@ -73,7 +73,7 @@ sequenceDiagram
73
73
  alt cache hit (TTL + fingerprint valid)
74
74
  Cache-->>TR: cached context
75
75
  else cache miss
76
- Cache->>App: introspect (31 modules)
76
+ Cache->>App: introspect (39 modules)
77
77
  App-->>Cache: structured data
78
78
  Cache-->>TR: fresh context
79
79
  end
@@ -115,8 +115,8 @@ The `Introspector` orchestrator runs configured introspectors and merges results
115
115
  **Prism AST parsing** replaced all regex-based Ruby source parsing in v5.2.0.
116
116
 
117
117
  - **AstCache** - Thread-safe parse cache (`Concurrent::Map`), keyed by path + SHA256 + mtime
118
- - **SourceIntrospector** - Single-pass Prism Dispatcher walks the AST once, feeds events to all 7 listeners simultaneously
119
- - **7 Listeners** - Associations, Validations, Scopes, Enums, Callbacks, Macros, Methods
118
+ - **SourceIntrospector** - Single-pass Prism Dispatcher walks the AST once, feeding every registered listener simultaneously
119
+ - **21 Listeners** - Associations, Validations, Scopes, Enums, Callbacks, Macros and Methods are the default map for model analysis; the rest are used through targeted walks over schema dumps, migrations, Gemfiles, rake tasks and initializers
120
120
  - **Confidence** - Every result carries `[VERIFIED]` (static literals) or `[INFERRED]` (dynamic expressions)
121
121
 
122
122
  ### Tool Registry (`lib/rails_ai_context/tools/base_tool.rb`)
@@ -192,7 +192,7 @@ Thor-based CLI that works standalone (no Gemfile entry):
192
192
 
193
193
  - `ToolRunner` - Parses CLI args, resolves tool names, executes tools, formats output
194
194
  - Supports `--json` mode for machine-readable output
195
- - Same 38 tools available as MCP and CLI
195
+ - Same 39 tools available as MCP and CLI
196
196
 
197
197
  ### Caching
198
198
 
@@ -215,7 +215,7 @@ SHA256-based change detection:
215
215
  ## Key design decisions
216
216
 
217
217
  1. **Official MCP SDK** - Not a custom protocol. Uses `mcp` gem's `MCP::Tool`, `MCP::Server`, transports.
218
- 2. **Read-only tools** - All 38 tools annotated as non-destructive. Defense-in-depth for query tool.
218
+ 2. **Read-only tools** - All 39 tools annotated as non-destructive. Defense-in-depth for query tool.
219
219
  3. **Graceful degradation** - Works without database (parses schema.rb as text), without Brakeman, without ripgrep, without listen gem.
220
220
  4. **Zeitwerk autoloading** - Files loaded on-demand. No `require_relative` in the gem.
221
221
  5. **Diff-aware generation** - Context file regeneration skips unchanged files using fingerprinting.
data/docs/CLI.md CHANGED
@@ -40,7 +40,7 @@ rails-ai-context serve --transport http --port 6029 # HTTP transport
40
40
 
41
41
  ### `tool`
42
42
 
43
- Run any of the 38 MCP tools from the terminal.
43
+ Run any of the 39 MCP tools from the terminal.
44
44
 
45
45
  ```bash
46
46
  # Rake syntax
@@ -93,7 +93,7 @@ rails-ai-context context --format claude # Specific format
93
93
 
94
94
  ### `doctor`
95
95
 
96
- Run 23 diagnostic checks and report AI readiness score.
96
+ Run the diagnostic checks and report an AI readiness score.
97
97
 
98
98
  ```bash
99
99
  rails ai:doctor
@@ -91,8 +91,8 @@ preset: full
91
91
  | `excluded_models` | Array | 8 framework models | Models to skip during introspection |
92
92
  | `excluded_controllers` | Array | 2 framework controllers | Controllers to skip |
93
93
  | `excluded_route_prefixes` | Array | 6 framework prefixes | Route prefixes to skip |
94
- | `excluded_filters` | Array | 3 framework filters | Controller filters to skip |
95
- | `excluded_middleware` | Array | 24 framework middleware | Middleware to skip in listing |
94
+ | `excluded_filters` | Array | 5 framework filters | Controller filters to skip |
95
+ | `excluded_middleware` | Array | 25 framework middleware | Middleware to skip in listing |
96
96
  | `excluded_paths` | Array | `["node_modules", "tmp", "log", "vendor", ".git", "doc", "docs"]` | Paths excluded from search |
97
97
  | `excluded_association_names` | Array | 7 framework associations | Association names to hide from model output |
98
98
  | `excluded_concerns` | Array of Regex | Framework concerns | Concerns to skip (supports regex) |
@@ -116,6 +116,16 @@ preset: full
116
116
  | `search_extensions` | Array | `["rb", "js", "erb", "yml", "yaml", "json", "ts", "tsx", "vue", "svelte", "haml", "slim"]` | File extensions to search |
117
117
  | `concern_paths` | Array | `["app/models/concerns", "app/controllers/concerns"]` | Paths to scan for concerns |
118
118
  | `frontend_paths` | Array | `nil` (auto-detect) | Override frontend file paths |
119
+ | `extra_app_paths` | Array | `[]` | Extra directories under the app root to treat as application code |
120
+
121
+ ### Instrumentation
122
+
123
+ | Option | Type | Default | Description |
124
+ |:-------|:-----|:--------|:------------|
125
+ | `instrumentation_include_arguments` | Boolean | `false` | Include tool arguments in instrumentation events |
126
+
127
+ > [!WARNING]
128
+ > `instrumentation_include_arguments` forwards raw tool arguments to your subscribers, which for `rails_query` means the SQL text and for other tools can mean environment variable names. Leave it off unless your subscriber is as trusted as your logs.
119
129
 
120
130
  ### Database Query Safety
121
131
 
@@ -123,7 +133,8 @@ preset: full
123
133
  |:-------|:-----|:--------|:-----------|:------------|
124
134
  | `query_timeout` | Integer | `5` | - | SQL query timeout in seconds |
125
135
  | `query_row_limit` | Integer | `100` | 1–1000 | Maximum rows returned |
126
- | `query_redacted_columns` | Array | 10+ patterns | - | Column names/suffixes to redact |
136
+ | `query_redacted_columns` | Array | 14 patterns | - | Column names that cause a query to be rejected |
137
+ | `query_allowed_columns` | Array | `[]` | - | Column names exempted from the built-in sensitive list |
127
138
  | `allow_query_in_production` | Boolean | `false` | - | Allow `rails_query` tool in production |
128
139
 
129
140
  ### Logs
@@ -136,7 +147,7 @@ preset: full
136
147
 
137
148
  | Option | Type | Default | Description |
138
149
  |:-------|:-----|:--------|:------------|
139
- | `sensitive_patterns` | Array | 8 patterns | File patterns blocked from search/read (`.env*`, `*.key`, `*.pem`, `credentials.yml.enc`, etc.) |
150
+ | `sensitive_patterns` | Array | 27 patterns | File patterns blocked from search/read (`.env*`, `*.key`, `*.pem`, `config/credentials.yml.enc`, `.ssh/*`, etc.) |
140
151
 
141
152
  ### Extensibility
142
153
 
data/docs/CUSTOM_TOOLS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Custom Tools
4
4
 
5
- **Build your own MCP tools that run alongside the 38 built-in ones.**
5
+ **Build your own MCP tools that run alongside the 39 built-in ones.**
6
6
 
7
7
  [Tools Reference](TOOLS.md) · [Configuration](CONFIGURATION.md) · [Architecture](ARCHITECTURE.md) · [FAQ](FAQ.md)
8
8
 
@@ -11,7 +11,7 @@
11
11
  ---
12
12
 
13
13
  > [!NOTE]
14
- > Custom tools have full access to your Rails environment - ActiveRecord, services, mailers, everything. They appear alongside the 38 built-in tools in both MCP and CLI.
14
+ > Custom tools have full access to your Rails environment - ActiveRecord, services, mailers, everything. They appear alongside the 39 built-in tools in both MCP and CLI.
15
15
 
16
16
  ## Creating a custom tool
17
17
 
data/docs/FAQ.md CHANGED
@@ -25,7 +25,7 @@ Claude Code, Cursor, GitHub Copilot, OpenCode, and Codex CLI. Each gets tailored
25
25
  No. The gem works three ways:
26
26
  1. **MCP server** - AI calls tools via the protocol (best experience)
27
27
  2. **Static files** - Generated context files (CLAUDE.md, .cursor/rules/, etc.)
28
- 3. **CLI** - Same 38 tools from the terminal, no server needed
28
+ 3. **CLI** - Same 39 tools from the terminal, no server needed
29
29
 
30
30
  ### Is this safe for production?
31
31
 
@@ -47,7 +47,7 @@ Yes. The gem gracefully degrades - it parses `db/schema.rb` as text when no data
47
47
 
48
48
  ### Can I switch between Gemfile and standalone?
49
49
 
50
- Yes, freely. Both generate identical context files and provide the same 38 tools. Just re-run the install/init to update MCP config files.
50
+ Yes, freely. Both generate identical context files and provide the same 39 tools. Just re-run the install/init to update MCP config files.
51
51
 
52
52
  ### Do I need to commit the generated files?
53
53
 
@@ -69,7 +69,7 @@ Start with `rails_onboard` for an app overview, `rails_analyze_feature` for feat
69
69
 
70
70
  ### Can I add my own tools?
71
71
 
72
- Yes. See [Custom Tools](CUSTOM_TOOLS.md). Create an `MCP::Tool` subclass, register it via `config.custom_tools`, and it appears alongside the 38 built-in tools.
72
+ Yes. See [Custom Tools](CUSTOM_TOOLS.md). Create an `MCP::Tool` subclass, register it via `config.custom_tools`, and it appears alongside the 39 built-in tools.
73
73
 
74
74
  ### Can I remove built-in tools?
75
75
 
data/docs/GUIDE.md CHANGED
@@ -18,7 +18,7 @@
18
18
  | Guide | Description |
19
19
  |:------|:------------|
20
20
  | [Quickstart](QUICKSTART.md) | Get running in 5 minutes |
21
- | [Tools Reference](TOOLS.md) | All 38 MCP tools with parameters |
21
+ | [Tools Reference](TOOLS.md) | All 39 MCP tools with parameters |
22
22
  | [Recipes](RECIPES.md) | Real-world workflows and examples |
23
23
  | [Custom Tools](CUSTOM_TOOLS.md) | Build your own MCP tools |
24
24
  | [Configuration](CONFIGURATION.md) | Every config option |
@@ -283,7 +283,7 @@ rails ai:context:claude # Use this instead (no quoting needed)
283
283
 
284
284
  ## CLI Tools
285
285
 
286
- All 38 MCP tools can be run directly from the terminal - no MCP server or AI client needed.
286
+ All 39 MCP tools can be run directly from the terminal - no MCP server or AI client needed.
287
287
 
288
288
  ### Rake
289
289
 
@@ -349,7 +349,7 @@ The `tool_mode` is selected during `rails generate rails_ai_context:install`.
349
349
 
350
350
  ## MCP Tools - Full Reference
351
351
 
352
- All 38 tools are **read-only** and **idempotent** - they never modify your application or database.
352
+ All 39 tools are **read-only** and **idempotent** - they never modify your application or database.
353
353
 
354
354
  ### rails_get_schema
355
355
 
@@ -1169,7 +1169,7 @@ if defined?(RailsAiContext)
1169
1169
  end
1170
1170
  ```
1171
1171
 
1172
- Both transports are **read-only** - they expose the same 38 tools and never modify your app.
1172
+ Both transports are **read-only** - they expose the same 39 tools and never modify your app.
1173
1173
 
1174
1174
  ### Controller Transport (Alternative)
1175
1175
 
@@ -94,7 +94,6 @@ end
94
94
 
95
95
  | Introspector | Key | What it extracts |
96
96
  |:-------------|:----|:-----------------|
97
- | SourceIntrospector | `:source` | Prism AST: associations, validations, scopes, enums, callbacks, macros, methods |
98
97
  | MigrationIntrospector | `:migrations` | Migration files, versions, reversibility |
99
98
  | SeedsIntrospector | `:seeds` | Seed file analysis |
100
99
  | DatabaseStatsIntrospector | `:database_stats` | Table sizes, row counts, index stats |
@@ -170,9 +169,11 @@ These introspectors map directly onto [`RAILS_NERVOUS_SYSTEM.md`](../RAILS_NERVO
170
169
 
171
170
  ## AST-based introspection
172
171
 
173
- The **SourceIntrospector** uses Prism AST parsing for model analysis. It runs a single-pass Dispatcher that walks the AST once and feeds events to 7 listeners simultaneously:
172
+ The **SourceIntrospector** uses Prism AST parsing for model analysis. It is infrastructure shared by the introspectors above rather than an introspector you can enable: there is no `:source` key for `config.introspectors`.
174
173
 
175
- ### 7 Prism listeners
174
+ It runs a single-pass Dispatcher that walks the AST once and feeds events to all registered listeners simultaneously. Model analysis uses the seven below by default; the other fourteen listener classes are used through targeted walks (schema dumps, migrations, Gemfiles, rake tasks, initializers, and so on), for 21 in total.
175
+
176
+ ### The 7 default Prism listeners
176
177
 
177
178
  | Listener | What it detects |
178
179
  |:---------|:---------------|
data/docs/QUICKSTART.md CHANGED
@@ -62,7 +62,7 @@ The install generator created:
62
62
  ## Verify it works
63
63
 
64
64
  ```bash
65
- # Check AI readiness (23 diagnostic checks)
65
+ # Check AI readiness (25 diagnostic checks)
66
66
  rails ai:doctor # In-Gemfile
67
67
  rails-ai-context doctor # Standalone
68
68
 
data/docs/SECURITY.md CHANGED
@@ -11,11 +11,11 @@
11
11
  ---
12
12
 
13
13
  > [!CAUTION]
14
- > This gem is designed for **development environments**. The query tool is disabled in production by default. Sensitive files are blocked. All 38 tools are read-only.
14
+ > This gem is designed for **development environments**. The query tool is disabled in production by default. Sensitive files are blocked. All 39 tools are read-only.
15
15
 
16
16
  ## Design principles
17
17
 
18
- 1. **Read-only by design** - All 38 tools are annotated as non-destructive in the MCP protocol
18
+ 1. **Read-only by design** - All 39 tools are annotated as non-destructive in the MCP protocol
19
19
  2. **Defense in depth** - Multiple security layers, not single points of failure
20
20
  3. **Sensitive data blocking** - Configurable patterns prevent access to secrets
21
21
  4. **Offline by default** - No network calls except optional `rails_search_docs` with `fetch: true`
@@ -33,8 +33,9 @@ flowchart LR
33
33
  L1 -->|"Blocked:\nINSERT, DROP,\nUNION SELECT..."| R1[Rejected]
34
34
  L1 -->|SELECT only| L2{Layer 2\nDatabase Read-Only}
35
35
  L2 -->|"SET TRANSACTION\nREAD ONLY\n+ timeout"| L3{Layer 3\nRow Limit}
36
- L3 -->|"Cap: 1000 rows\nDefault: 100"| L4{Layer 4\nColumn Redaction}
37
- L4 -->|"password_digest\napi_key → [REDACTED]"| OK[Safe Result]
36
+ L3 -->|"Cap: 1000 rows\nDefault: 100"| L4{Layer 4\nSensitive Columns}
37
+ L4 -->|"names password_digest,\napi_key, ..."| R1
38
+ L4 -->|"no sensitive column"| OK[Safe Result]
38
39
 
39
40
  style R1 fill:#e74c3c,stroke:#c0392b,color:#fff
40
41
  style OK fill:#27ae60,stroke:#1e8449,color:#fff
@@ -75,13 +76,19 @@ All queries execute inside a transaction, then rollback (even if they could writ
75
76
  - Configurable: `config.query_row_limit` (hard cap: 1000)
76
77
  - Applied as `LIMIT` clause appended to query
77
78
 
78
- ### Layer 4 - Column redaction
79
+ ### Layer 4 - Sensitive column rejection
79
80
 
80
- Sensitive column values are replaced with `[REDACTED]`:
81
+ A query that names a sensitive column is **rejected before execution**, not redacted after it:
81
82
 
82
83
  **Default redacted patterns:** `password_digest`, `encrypted_password`, `password_hash`, `reset_password_token`, `confirmation_token`, `unlock_token`, `otp_secret`, `session_data`, `secret_key`, `api_key`, `api_secret`, `access_token`, `refresh_token`, `jti`
83
84
 
84
- Redaction matches by **name** and **suffix** - `SELECT password_digest AS pd` still redacts `pd` because the column name is tracked through the query result.
85
+ Matching is by name, case-insensitive and word-bounded, against both the defaults above and `config.query_redacted_columns`. `SELECT password_digest AS pd FROM users` is blocked outright: post-execution redaction reads the column names the database returns, which the caller controls through aliases and expressions, so it cannot be relied on.
86
+
87
+ If one of your own columns merely looks sensitive (an `oauth_applications.secret`, say), exempt it by name:
88
+
89
+ ```ruby
90
+ config.query_allowed_columns = %w[secret]
91
+ ```
85
92
 
86
93
  ### Environment guard
87
94
 
@@ -96,15 +103,16 @@ The `rails_search_code` and file-reading tools block access to sensitive files:
96
103
 
97
104
  ### Default patterns
98
105
 
99
- ```
100
- .env*
101
- *.key
102
- *.pem
103
- credentials.yml.enc
104
- master.key
105
- secret_key_base
106
- config/secrets.yml
107
- config/credentials/*
106
+ ```text
107
+ .env .env.*
108
+ config/master.key
109
+ config/credentials.yml.enc config/credentials/*.yml.enc
110
+ config/database.yml config/secrets.yml
111
+ config/cable.yml config/storage.yml
112
+ config/mongoid.yml config/redis.yml
113
+ *.pem *.key *.p12 *.pfx *.jks *.keystore
114
+ **/id_rsa **/id_ed25519 **/id_ecdsa **/id_dsa
115
+ .ssh/* .aws/credentials .aws/config .netrc .pgpass .my.cnf
108
116
  ```
109
117
 
110
118
  ### AI context file exclusions
@@ -158,7 +166,7 @@ File type parameters accept only alphanumeric characters.
158
166
 
159
167
  ## Regex injection prevention
160
168
 
161
- User-supplied regex patterns have a 1-second timeout:
169
+ On Ruby 3.2 and newer, user-supplied regex patterns have a 1-second timeout:
162
170
 
163
171
  ```ruby
164
172
  Regexp.new(pattern, timeout: 1)
@@ -166,6 +174,9 @@ Regexp.new(pattern, timeout: 1)
166
174
 
167
175
  Complex patterns that would cause catastrophic backtracking raise `RegexpError` instead of hanging.
168
176
 
177
+ > [!WARNING]
178
+ > `Regexp.timeout` does not exist on Ruby 3.1, which this gem still supports. There the timeout is skipped, and a pattern crafted to backtrack catastrophically can hang the process serving the tool. If you expose `rails_search_code` to input you do not control, run it on Ruby 3.2 or newer.
179
+
169
180
  ---
170
181
 
171
182
  ## Safe file reading
@@ -207,7 +218,7 @@ When using HTTP transport (Rack middleware or McpController):
207
218
 
208
219
  - **Default bind**: `127.0.0.1` (localhost only - not exposed to network)
209
220
  - **`auto_mount` is `false` by default** - must be explicitly enabled
210
- - **Doctor checks** warn if `auto_mount` is true (security flag)
221
+ - **Doctor checks** fail if `auto_mount` is true in production, and report it as enabled elsewhere
211
222
 
212
223
  The McpController uses thread-safe transport initialization with mutex synchronization.
213
224