rails-ai-context 5.9.1 → 5.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63fa43e2446217a8b226ba3fd59b0345c7e4fec6300acd0ab877354339187dac
4
- data.tar.gz: 0d12192067351b3e28e1ae8ba16792be31a8e340e356b4b261249345a2295fa3
3
+ metadata.gz: b022525ce9aa13c5b6d5f21a46f13db5ae84dac04f346adc40eeb2b315f9fcaa
4
+ data.tar.gz: 9e9f7bd60ecdab6a5d1599e560762c8c7d95a8973f5d001c4573d00f55b2a594
5
5
  SHA512:
6
- metadata.gz: 6d3f4378d84b890464cdda8c550195430987c05970b892184bda610fb727f7614d943b46141e4b3f7b39642503deacf71b76fca933923aada849e1c39f887fef
7
- data.tar.gz: 4632cbf34995fb74922c19f89988d7bd4a5dfb3cdac98ab78e8ab3d08bac11f84c3cab0df68364b46740c133fdda22bd7311400dd5381d857b611cb4de2fe6e7
6
+ metadata.gz: 7dca71cfea9fb5106b40f5eaebd119e36bcd1cbcbdd41b8dcd8ba4b11eb802dbb647d0c90db88b4d21a98d303ed2ed42f605ca7278a245a76155fc57cb0366cf
7
+ data.tar.gz: baaeaa8a5eec91ba86c6aee32f5a0d459ebcc7f14c1eaec64e7110afe7368985e005b92dcdee960606525806d652cd63f5eb1092773765cbe831857cffde75a1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ 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.10.0] — 2026-04-20
9
+
10
+ ### Added — 8 new introspectors closing RAILS_NERVOUS_SYSTEM.md gaps
11
+
12
+ An audit against [`RAILS_NERVOUS_SYSTEM.md`](RAILS_NERVOUS_SYSTEM.md) identified 9 framework sections where introspection was missing or partial. This release ships the 8 introspectors needed to close them (the 9th — §11 Query interface — was already covered by `:conventions` via `load_async` / `.async_*` scanning). All are wired into `PRESETS[:full]`. The gem now exposes **39 introspectors** (up from 31).
13
+
14
+ - **`InitializerIntrospector` (`:initializers`, §2).** Enumerates `Rails.application.initializers` — every initializer's name, owner, declared `before:` / `after:` ordering edges, and the `source_location` of its block. Also summarizes each `config/initializers/*.rb` file (initializer count + the top `config.*` setters it touches) so AI can jump straight to user-owned boot code.
15
+ - **`AutoloadIntrospector` (`:autoload`, §3).** Zeitwerk presence, both `Rails.autoloaders.main` and `.once` with their collapsed dirs + ignored paths + root dirs, raw `autoload_paths` / `autoload_once_paths` / `eager_load_paths`, the resolved `eager_load` boolean, plus custom inflection rules extracted from `config/initializers/*.rb` (`inflect.acronym` / `.plural` / `.singular` / `.irregular` / `.uncountable` / `.human`). Paths are root-relative.
16
+ - **`ConnectionPoolIntrospector` (`:connection_pool`, §10).** Per-database adapter config: `pool`, `checkout_timeout`, `idle_timeout`, `reaping_frequency`, `prepared_statements`, `advisory_locks`, replica flag, role, connection-handler pool counts per role (`:writing` / `:reading`), and automatic shard selector detection (Rails 7.1+ `ActiveRecord::Middleware::ShardSelector`). Complements `:database_stats` (which only returns row counts).
17
+ - **`ActiveSupportIntrospector` (`:active_support`, §17).** Covers ActiveSupport runtime surface other introspectors leave untouched: Concerns in `app/**/concerns/` (with `ActiveSupport::Concern` / `included do` / `class_methods do` flags), `Rails.application.deprecators` registry keys, MessageEncryptor + MessageVerifier usage scan across `lib/` + `app/`, TaggedLogging configuration (`config.log_tags` + initializer-based `ActiveSupport::TaggedLogging.new`), active on-load hooks, and cache-store options.
18
+ - **`CredentialsIntrospector` (`:credentials`, §30).** Default `config/credentials.yml.enc` + every per-env `config/credentials/<env>.yml.enc`, master-key source resolution (`env:RAILS_MASTER_KEY` / `file:config/master.key` / `missing`), `config.require_master_key` flag, arbitrary encrypted configs (`config/<name>.yml.enc` pairs), and top-level credential **key names only** — decrypted hash is inspected for `.keys` and nothing more. A regression spec asserts no known credential value appears in the output.
19
+ - **`SecurityIntrospector` (`:security`, §32).** Framework-level security controls `auth_introspector` doesn't cover: `config.force_ssl`, SSL options (HSTS `expires` / `subdomains` / `preload`, `redirect`, `secure_cookies`), `config.hosts` + `host_authorization` options, `ContentSecurityPolicy` directives (including `report_only`), `PermissionsPolicy` directives, CSRF config (`protect_from_forgery` declaration, `per_form_csrf_tokens`, `forgery_protection_origin_check`), cookie session options (`:key`, `:secure`, `:httponly`, `:same_site`, `:domain`, `:path`, `:expire_after`), and Rails 7.2+ `allow_browser` calls per controller.
20
+ - **`ObservabilityIntrospector` (`:observability`, §34 + §38).** `ActiveSupport::LogSubscriber.log_subscribers` catalog (class + namespace), full `ActiveSupport::Notifications` subscriber registry walked via `@string_subscribers` / `@other_subscribers` / legacy `@subscribers` (handles Rails 7.0/7.1/8.x variants — grouped by pattern with subscriber count + sample class name), `ActionDispatch::ServerTiming` middleware detection + `config.server_timing` flag, Rails 8.1 `event_reporter` availability, log level + tags + `colorize_logging`, and a static catalog of 60+ canonical Rails event names across 10 subsystems (`action_controller`, `action_view`, `active_record`, `active_job`, `action_mailer`, `action_mailbox`, `action_cable`, `active_support`, `active_storage`, `railties`).
21
+ - **`EnvIntrospector` (`:env`, §36).** Curated catalog of 30+ Rails-related ENV vars (core, server, bundler, assets, boot, secrets, database, cache, deploy, platform, observability, testing) partitioned into `set` / `unset`. Safe vars (`RAILS_ENV`, `RAILS_MAX_THREADS`, `PORT`, etc.) return their value. Sensitive vars (`SECRET_KEY_BASE`, `RAILS_MASTER_KEY`, `DATABASE_URL`, `REDIS_URL`, `KAMAL_REGISTRY_PASSWORD`, etc.) return `redacted: true` only — the value never leaves the process. Also scans `config/`/`app/`/`lib/` for app-specific `ENV["X"]` / `ENV.fetch("X")` references beyond the catalog.
22
+
23
+ ### Why these specifically
24
+
25
+ Each corresponds to a `RAILS_NERVOUS_SYSTEM.md` section the audit flagged as uncovered. Partial-coverage sections (§2 filenames, §3 Zeitwerk presence, §17 CurrentAttributes, §27 Solid Trifecta, §30 boolean, §32 CORS/CSP/force_ssl, §34 N+1 anti-patterns, §36 puma/procfile) are preserved — the new introspectors complement rather than replace existing ones.
26
+
27
+ ### Preset wiring
28
+
29
+ All 8 are in `PRESETS[:full]`. None are in `PRESETS[:standard]` — they're framework-runtime data most valuable in `full` mode where comprehensive context outranks boot speed. `config.introspectors.size` for `:full` is now `39` (from `31`). The configuration spec was updated accordingly.
30
+
31
+ ### Tests
32
+
33
+ Every new introspector ships with a unit spec under `spec/lib/rails_ai_context/introspectors/*_introspector_spec.rb`, plus an orchestrator-level assertion in `spec/lib/rails_ai_context/introspector_spec.rb` that all 8 keys land in the context hash, plus a real-Rails-app e2e spec at `spec/e2e/nervous_system_introspectors_spec.rb` (8 examples, run via `E2E=1`). Specs assert shape guarantees, error absence, category-specific invariants, and the `CredentialsIntrospector` / `EnvIntrospector` specs include explicit sentinel-value leak assertions (a secret is injected, then the output hash is searched for the sentinel string). The full non-e2e suite runs **2154 examples, 0 failures**.
34
+
35
+ ### Fixed — post-review hardening
36
+
37
+ Three parallel code reviews (security/data-leak, Rails-version correctness, CLAUDE.md invariant compliance) surfaced the following issues, all addressed in this release:
38
+
39
+ - **`ObservabilityIntrospector#detect_event_reporter` crashed on Rails 8.1.** The original code called `reporter.tagged` without a block to read "registered tags". `ActiveSupport::EventReporter#tagged` delegates unconditionally to `TagStack#with_tags(&block)` which `yield`s — a blockless call raises `LocalJumpError`. The outer `rescue` caught it and returned `{ available: false }`, which silently misreported Rails 8.1 apps as lacking the event reporter and dropped the `subscriber_count` too. The `entry[:tags]` line was removed; `tagged` is stack-scoped context, not an introspectable keyspace. `subscriber_count` is still reported.
40
+ - **`ObservabilityIntrospector#extract_subscribers_from_notifier` had dead code.** A "legacy `@subscribers` (array)" fallback claimed to support Rails 7.0, but Rails 7.0 already used `@string_subscribers` + `@other_subscribers`. The flat `@subscribers` ivar hasn't existed since Rails ≤ 5.x, so the branch was unreachable across the entire 7.1 / 7.2 / 8.0 CI matrix. Removed. Also: the `subscriber_raw_pattern` helper now unwraps `ActiveSupport::Notifications::Fanout::Subscribers::Matcher` one level deep so Regexp-pattern subscribers surface as `"pattern.source"` instead of `"#<…::Matcher:0x…>"`.
41
+ - **`CredentialsIntrospector` leaked paths via `e.message`.** Both the top-level `rescue` and `inspect_default_credentials`'s rescue returned `{ error: e.message }` in the output hash. OS-level errors (`Errno::EACCES`, `Errno::ENOENT`) and OpenSSL decryption failures include absolute paths with the OS username in their message — credentials-adjacent data that shouldn't leave the process. Both rescues now return `{ error: "…failed", exception_class: e.class.name }`; `e.message` stays in the `ENV["DEBUG"]`-gated stderr log where it's fine. New regression specs inject a `Errno::EACCES` with a path containing `/Users/alice/secret/master.key` and assert neither `"/Users/alice"` nor `"alice/secret"` appears anywhere in the output.
42
+ - **`EnvIntrospector` classified `BUNDLE_PATH` and `BUNDLE_GEMFILE` as safe-to-return.** Both are absolute filesystem paths that usually contain the OS username (e.g. `/Users/alice/.bundle`). Flipped to `safe: false` so only presence is reported, matching the treatment of other path-containing vars (`DATABASE_URL`, `REDIS_URL`, etc.).
43
+ - **`ActiveSupportIntrospector` + `EnvIntrospector` had non-deterministic directory walks.** Both called `Dir.glob(...).first(2000)` to cap traversal on large monorepos, but `Dir.glob` ordering is filesystem-dependent, so the selected 2000-file slice could differ run-to-run. Both now call `Dir.glob(...).sort.first(2000)`, matching the `.sort` already used by the other new introspectors.
44
+ - **Spec-coverage gaps on ivar-derived paths.** The reviewers flagged that three silently-failing paths had no assertions: initializer `:source` capture (via `@block.source_location`), connection_pool `:pool_config` shape, and the `@other_subscribers` Regexp-pattern branch in the Fanout walk. All three now have targeted assertions — a silent drift if any of these ivars is renamed upstream will now fail CI.
45
+
8
46
  ## [5.9.1] — 2026-04-20
9
47
 
10
48
  ### Fixed — `GetConcern` missed plural concern names (#78)
data/CONTRIBUTING.md CHANGED
@@ -19,7 +19,7 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
19
19
  ```
20
20
  lib/rails_ai_context/
21
21
  ├── cli/ # CLI tool runner (tool_runner.rb) — executes MCP tools from rake/Thor
22
- ├── introspectors/ # 31 introspectors (schema, models, routes, etc.)
22
+ ├── introspectors/ # 39 introspectors (schema, models, routes, etc.)
23
23
  ├── tools/ # 38 MCP tools with detail levels and pagination
24
24
  ├── serializers/ # Per-assistant formatters + shared ToolGuideHelper
25
25
  ├── server.rb # MCP server setup (stdio + HTTP)
data/README.md CHANGED
@@ -21,7 +21,7 @@
21
21
  <br>
22
22
  [![Ruby](https://img.shields.io/badge/Ruby-3.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.1%20%7C%207.2%20%7C%208.0-CC0000)](https://github.com/crisnahine/rails-ai-context)
24
- [![Tests](https://img.shields.io/badge/Tests-2078%20passing-brightgreen)](https://github.com/crisnahine/rails-ai-context/actions)
24
+ [![Tests](https://img.shields.io/badge/Tests-2154%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>
@@ -397,7 +397,7 @@ Enabled by default. Disable with `config.anti_hallucination_rules = false` if yo
397
397
 
398
398
  ```mermaid
399
399
  graph TD
400
- A["Your Rails App\nmodels + schema + routes + controllers + views + jobs"] -->|"31 introspectors"| B
400
+ A["Your Rails App\nmodels + schema + routes + controllers + views + jobs"] -->|"39 introspectors"| B
401
401
 
402
402
  B["rails-ai-context\nPrism AST parsing · Cached · Confidence-tagged\nVFS: rails-ai-context:// URIs introspected fresh"]
403
403
 
@@ -459,7 +459,7 @@ Both paths ask which AI tools you use (Claude Code, Cursor, GitHub Copilot, Open
459
459
  | **[Configuration](docs/CONFIGURATION.md)** | 40+ config options with defaults |
460
460
  | **[AI Tool Setup](docs/SETUP.md)** | Claude, Cursor, Copilot, OpenCode, Codex |
461
461
  | **[Architecture](docs/ARCHITECTURE.md)** | System design and internals |
462
- | **[Introspectors](docs/INTROSPECTORS.md)** | All 31 introspectors and AST engine |
462
+ | **[Introspectors](docs/INTROSPECTORS.md)** | All 39 introspectors and AST engine |
463
463
  | **[Security](docs/SECURITY.md)** | 4-layer SQL safety and file blocking |
464
464
  | **[CLI Reference](docs/CLI.md)** | Commands and argument syntax |
465
465
  | **[Standalone](docs/STANDALONE.md)** | Use without Gemfile entry |
@@ -508,7 +508,7 @@ if defined?(RailsAiContext)
508
508
  RailsAiContext.configure do |config|
509
509
  config.ai_tools = %i[claude cursor] # Which AI tools to generate for
510
510
  config.tool_mode = :mcp # :mcp (default) or :cli
511
- config.preset = :full # :full (31 introspectors) or :standard (17)
511
+ config.preset = :full # :full (39 introspectors) or :standard (17)
512
512
  end
513
513
  end
514
514
  ```
@@ -543,7 +543,7 @@ end
543
543
  ## About
544
544
 
545
545
  Built by a Rails developer with 10+ years of production experience.<br>
546
- 2078 tests + 100-example e2e harness. 38 tools. 5 resource templates. 31 introspectors. Standalone or in-Gemfile.<br>
546
+ 2154 tests + 100-example e2e harness. 38 tools. 5 resource templates. 39 introspectors. Standalone or in-Gemfile.<br>
547
547
  MIT licensed. [Contributions welcome.](CONTRIBUTING.md)
548
548
 
549
549
  <br>
data/docs/ARCHITECTURE.md CHANGED
@@ -18,7 +18,7 @@ graph TD
18
18
  A["models + schema + routes + controllers + views + jobs + config"]
19
19
  end
20
20
 
21
- A -->|"31 introspectors"| gem
21
+ A -->|"39 introspectors"| gem
22
22
 
23
23
  subgraph gem["rails-ai-context"]
24
24
  direction TB
@@ -101,7 +101,7 @@ flowchart LR
101
101
 
102
102
  ### Introspectors (`lib/rails_ai_context/introspectors/`)
103
103
 
104
- 31 modules that extract structured data from your Rails app. Each introspector:
104
+ 39 modules that extract structured data from your Rails app. Each introspector:
105
105
 
106
106
  - Returns a Hash (never raises — wraps errors in `{ error: msg }`)
107
107
  - Is registered in `INTROSPECTOR_MAP` with a symbol key
@@ -56,7 +56,7 @@ preset: full
56
56
 
57
57
  | Option | Type | Default | Description |
58
58
  |:-------|:-----|:--------|:------------|
59
- | `preset` | Symbol | `:full` | `:full` (31 introspectors) or `:standard` (17 introspectors) |
59
+ | `preset` | Symbol | `:full` | `:full` (39 introspectors) or `:standard` (17 introspectors) |
60
60
  | `context_mode` | Symbol | `:compact` | `:compact` (context files capped at ~150 lines) or `:full` (no line cap) |
61
61
  | `introspectors` | Array of symbols | (from preset) | Override the introspector list directly |
62
62
  | `generate_root_files` | Boolean | `true` | Set `false` to generate split rules only, no root CLAUDE.md/AGENTS.md |
@@ -155,7 +155,7 @@ preset: full
155
155
 
156
156
  ## Presets
157
157
 
158
- ### `:full` (default) — 31 introspectors
158
+ ### `:full` (default) — 39 introspectors
159
159
 
160
160
  All available introspectors. Best for comprehensive context.
161
161
 
data/docs/FAQ.md CHANGED
@@ -99,7 +99,7 @@ PostgreSQL, MySQL, and SQLite. Each gets database-specific safety mechanisms (re
99
99
 
100
100
  ### What's the difference between `:full` and `:standard` preset?
101
101
 
102
- - **`:full`** (default) — 31 introspectors. Comprehensive context for every aspect of your app.
102
+ - **`:full`** (default) — 39 introspectors. Comprehensive context for every aspect of your app.
103
103
  - **`:standard`** — 17 introspectors. Faster, covers the essentials (schema, models, routes, controllers, tests, etc.).
104
104
 
105
105
  ### What's `:compact` vs `:full` context mode?
data/docs/GUIDE.md CHANGED
@@ -24,7 +24,7 @@
24
24
  | [Configuration](CONFIGURATION.md) | Every config option |
25
25
  | [AI Tool Setup](SETUP.md) | Per-editor setup |
26
26
  | [Architecture](ARCHITECTURE.md) | System design and internals |
27
- | [Introspectors](INTROSPECTORS.md) | All 31 introspectors |
27
+ | [Introspectors](INTROSPECTORS.md) | All 39 introspectors |
28
28
  | [Security](SECURITY.md) | Security model and SQL safety |
29
29
  | [CLI Reference](CLI.md) | All commands and argument syntax |
30
30
  | [Standalone Mode](STANDALONE.md) | Use without Gemfile |
@@ -1192,7 +1192,7 @@ if defined?(RailsAiContext)
1192
1192
  RailsAiContext.configure do |config|
1193
1193
  # --- Introspectors ---
1194
1194
 
1195
- # Presets: :full (31 introspectors, default) or :standard (17)
1195
+ # Presets: :full (39 introspectors, default) or :standard (17)
1196
1196
  config.preset = :full
1197
1197
 
1198
1198
  # Cherry-pick on top of a preset
@@ -1311,7 +1311,7 @@ end
1311
1311
  | Option | Type | Default | Description |
1312
1312
  |--------|------|---------|-------------|
1313
1313
  | `preset` | Symbol | `:full` | Introspector preset (`:full` or `:standard`) |
1314
- | `introspectors` | Array | 31 (full preset) | Which introspectors to run |
1314
+ | `introspectors` | Array | 39 (full preset) | Which introspectors to run |
1315
1315
  | `context_mode` | Symbol | `:compact` | `:compact` or `:full` |
1316
1316
  | `claude_max_lines` | Integer | `150` | Max lines for CLAUDE.md in compact mode |
1317
1317
  | `max_tool_response_chars` | Integer | `200_000` | Safety cap for MCP tool responses |
@@ -1398,7 +1398,7 @@ Core Rails structure only. Use `config.preset = :standard` for a lighter footpri
1398
1398
  | `performance` | N+1 query risks, missing counter_cache, missing FK indexes, Model.all anti-patterns, eager load candidates. |
1399
1399
  | `i18n` | Default locale, available locales, locale files with key counts, backend class, parse errors. |
1400
1400
 
1401
- ### Full preset (31 introspectors) — default
1401
+ ### Full preset (39 introspectors) — default
1402
1402
 
1403
1403
  Includes all standard introspectors plus:
1404
1404
 
@@ -1534,11 +1534,11 @@ OpenCode uses **per-directory lazy-loading**: when the agent reads a file, it wa
1534
1534
 
1535
1535
  | Setup | Coverage | Notes |
1536
1536
  |-------|----------|-------|
1537
- | Rails full-stack (ERB + Hotwire) | 31/31 | All introspectors relevant |
1538
- | Rails + Inertia.js (React/Vue) | ~25/31 | Views/Turbo partially useful, backend fully covered |
1539
- | Rails API + React/Next.js SPA | ~23/31 | Schema, models, routes, API, auth, jobs — all covered |
1540
- | Rails API + mobile app | ~23/31 | Same as SPA — backend introspection is identical |
1541
- | Rails engine (mountable gem) | ~18/31 | Core introspectors (schema, models, routes, gems) work |
1537
+ | Rails full-stack (ERB + Hotwire) | 39/39 | All introspectors relevant |
1538
+ | Rails + Inertia.js (React/Vue) | ~33/39 | Views/Turbo partially useful, backend fully covered |
1539
+ | Rails API + React/Next.js SPA | ~31/39 | Schema, models, routes, API, auth, jobs — all covered |
1540
+ | Rails API + mobile app | ~31/39 | Same as SPA — backend introspection is identical |
1541
+ | Rails engine (mountable gem) | ~26/39 | Core introspectors (schema, models, routes, gems) work |
1542
1542
 
1543
1543
  Frontend introspectors (views, Turbo, Stimulus, assets) degrade gracefully — they report nothing when those features aren't present.
1544
1544
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Introspectors
4
4
 
5
- **31 modules that extract structured data from your Rails application.**
5
+ **39 modules that extract structured data from your Rails application.**
6
6
 
7
7
  [Architecture](ARCHITECTURE.md) · [Configuration](CONFIGURATION.md) · [Tools Reference](TOOLS.md) · [Security](SECURITY.md)
8
8
 
@@ -21,7 +21,7 @@ Each introspector:
21
21
 
22
22
  ## Presets
23
23
 
24
- ### `:full` (default) — all 31 introspectors
24
+ ### `:full` (default) — all 39 introspectors
25
25
 
26
26
  Best for comprehensive AI context. Covers every aspect of your app.
27
27
 
@@ -49,13 +49,16 @@ graph LR
49
49
  S16["performance"] ~~~ S17["i18n"]
50
50
  end
51
51
 
52
- subgraph full_only["Full Preset adds +14"]
52
+ subgraph full_only["Full Preset adds +22"]
53
53
  direction TB
54
54
  F1["views"] ~~~ F2["database_stats"] ~~~ F3["api"]
55
55
  F4["active_storage"] ~~~ F5["action_text"] ~~~ F6["action_mailbox"]
56
56
  F7["rake_tasks"] ~~~ F8["assets"] ~~~ F9["devops"]
57
57
  F10["seeds"] ~~~ F11["middleware"] ~~~ F12["engines"]
58
58
  F13["multi_database"] ~~~ F14["frontend_frameworks"]
59
+ F15["initializers"] ~~~ F16["autoload"] ~~~ F17["connection_pool"]
60
+ F18["active_support"] ~~~ F19["credentials"] ~~~ F20["security"]
61
+ F21["observability"] ~~~ F22["env"]
59
62
  end
60
63
 
61
64
  standard --> full_only
@@ -74,7 +77,7 @@ end
74
77
 
75
78
  ---
76
79
 
77
- ## All 31 introspectors
80
+ ## All 39 introspectors
78
81
 
79
82
  ### Core
80
83
 
@@ -148,6 +151,21 @@ end
148
151
  | TestIntrospector | `:tests` | Test framework, file counts, coverage hints |
149
152
  | PerformanceIntrospector | `:performance` | N+1 risks, missing indexes, counter_cache hints |
150
153
 
154
+ ### Runtime & Framework Internals
155
+
156
+ These introspectors map directly onto [`RAILS_NERVOUS_SYSTEM.md`](../RAILS_NERVOUS_SYSTEM.md) sections and capture framework-level surface that `:config`, `:auth`, and `:middleware` don't.
157
+
158
+ | Introspector | Key | Nervous-system § | What it extracts |
159
+ |:-------------|:----|:---|:-----------------|
160
+ | InitializerIntrospector | `:initializers` | §2 | `Rails.application.initializers` graph: name, owner, `before:`/`after:` edges, block `source_location`, per-file `config/initializers/*.rb` summary |
161
+ | AutoloadIntrospector | `:autoload` | §3 | Zeitwerk presence, autoloaders (`:main` / `:once`) with collapsed + ignored dirs, `autoload_paths`, `eager_load_paths`, custom inflections (`acronym`, `plural`, `singular`, `irregular`) |
162
+ | ConnectionPoolIntrospector | `:connection_pool` | §10 | Per-database adapter config: pool size, `checkout_timeout`, `reaping_frequency`, `prepared_statements`, `advisory_locks`, replica flag, connection-handler roles, automatic shard selector detection |
163
+ | ActiveSupportIntrospector | `:active_support` | §17 | Concerns in `app/**/concerns/` (ActiveSupport::Concern flags, `included do`/`class_methods do` blocks), deprecators registry, MessageEncryptor/Verifier usage, TaggedLogging config, common on-load hooks, cache store options |
164
+ | CredentialsIntrospector | `:credentials` | §30 | Default + per-env encrypted files, master-key source (`env:RAILS_MASTER_KEY` vs `file:config/master.key` vs missing), `require_master_key` flag, arbitrary encrypted configs (`config/*.yml.enc`), top-level key **names only** (never values) |
165
+ | SecurityIntrospector | `:security` | §32 | `force_ssl`, SSL options (HSTS `expires`/`subdomains`/`preload`), `host_authorization` hosts, ContentSecurityPolicy directives + `report_only`, PermissionsPolicy directives, CSRF config (`protect_from_forgery`, `per_form_csrf_tokens`, `origin_check`), cookie session options, Rails 7.2+ `allow_browser` usage |
166
+ | ObservabilityIntrospector | `:observability` | §34 + §38 | `ActiveSupport::LogSubscriber.log_subscribers` catalog, AS::Notifications subscriber registry (pattern + count + sample class), `ActionDispatch::ServerTiming` middleware detection, Rails 8.1 `event_reporter` availability, log level + tags, canonical Rails event-name catalog (10 subsystems) |
167
+ | EnvIntrospector | `:env` | §36 | Catalog of 30+ Rails-related ENV vars partitioned into `set` / `unset`; safe vars (`RAILS_ENV`, `RAILS_MAX_THREADS`, etc.) return values, sensitive vars (`SECRET_KEY_BASE`, `DATABASE_URL`, `RAILS_MASTER_KEY`, etc.) return `redacted: true` only; scans `config/`/`app/`/`lib/` for app-specific `ENV["X"]` references |
168
+
151
169
  ---
152
170
 
153
171
  ## AST-based introspection
@@ -247,7 +247,7 @@ Without it, the tool reports "not installed" but the gem works fine otherwise.
247
247
 
248
248
  ### "Introspection is slow"
249
249
 
250
- 1. Use `:standard` preset (17 introspectors vs 31)
250
+ 1. Use `:standard` preset (17 introspectors vs 39)
251
251
  2. Increase cache TTL: `config.cache_ttl = 300`
252
252
  3. Check schema file size: `rails ai:doctor` warns if too large
253
253
  4. Check view count: many views slow down view introspection
data/docs/index.md CHANGED
@@ -25,7 +25,7 @@ Works with Claude Code, Cursor, GitHub Copilot, OpenCode, and Codex CLI.
25
25
  | **[Tools Reference](TOOLS.md)** | All 38 tools with every parameter |
26
26
  | **[Configuration](CONFIGURATION.md)** | 40+ config options with defaults |
27
27
  | **[CLI Reference](CLI.md)** | Commands and argument syntax |
28
- | **[Introspectors](INTROSPECTORS.md)** | All 31 introspectors and AST engine |
28
+ | **[Introspectors](INTROSPECTORS.md)** | All 39 introspectors and AST engine |
29
29
 
30
30
  ## Learn
31
31
 
@@ -78,7 +78,8 @@ module RailsAiContext
78
78
  full: %i[schema models routes jobs gems conventions stimulus database_stats controllers views view_templates turbo
79
79
  i18n config active_storage action_text auth api tests rake_tasks assets
80
80
  devops action_mailbox migrations seeds middleware engines multi_database
81
- components performance frontend_frameworks]
81
+ components performance frontend_frameworks
82
+ initializers autoload connection_pool active_support credentials security observability env]
82
83
  }.freeze
83
84
 
84
85
  # MCP server settings
@@ -80,7 +80,15 @@ module RailsAiContext
80
80
  multi_database: Introspectors::MultiDatabaseIntrospector,
81
81
  components: Introspectors::ComponentIntrospector,
82
82
  performance: Introspectors::PerformanceIntrospector,
83
- frontend_frameworks: Introspectors::FrontendFrameworkIntrospector
83
+ frontend_frameworks: Introspectors::FrontendFrameworkIntrospector,
84
+ initializers: Introspectors::InitializerIntrospector,
85
+ autoload: Introspectors::AutoloadIntrospector,
86
+ connection_pool: Introspectors::ConnectionPoolIntrospector,
87
+ active_support: Introspectors::ActiveSupportIntrospector,
88
+ credentials: Introspectors::CredentialsIntrospector,
89
+ security: Introspectors::SecurityIntrospector,
90
+ observability: Introspectors::ObservabilityIntrospector,
91
+ env: Introspectors::EnvIntrospector
84
92
  }.freeze
85
93
 
86
94
  private
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ # Extracts ActiveSupport runtime surface that other introspectors don't
6
+ # cover: Concerns registry (`app/**/concerns`), Deprecators registry,
7
+ # MessageEncryptor/MessageVerifier usage, and TaggedLogging tags.
8
+ # Covers RAILS_NERVOUS_SYSTEM.md §17 (ActiveSupport).
9
+ class ActiveSupportIntrospector
10
+ attr_reader :app
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call
17
+ {
18
+ concerns: extract_concerns,
19
+ deprecators: extract_deprecators,
20
+ message_verifier_usage: extract_message_verifier_usage,
21
+ tagged_logging: detect_tagged_logging,
22
+ on_load_hooks: common_on_load_hooks,
23
+ cache_usage: detect_cache_usage
24
+ }
25
+ rescue => e
26
+ $stderr.puts "[rails-ai-context] ActiveSupportIntrospector#call failed: #{e.message}" if ENV["DEBUG"]
27
+ { error: e.message }
28
+ end
29
+
30
+ private
31
+
32
+ def root
33
+ app.root.to_s
34
+ end
35
+
36
+ CONCERN_DIRS = %w[
37
+ app/models/concerns
38
+ app/controllers/concerns
39
+ app/jobs/concerns
40
+ app/mailers/concerns
41
+ app/channels/concerns
42
+ ].freeze
43
+
44
+ def extract_concerns
45
+ result = {}
46
+ CONCERN_DIRS.each do |rel_dir|
47
+ dir = File.join(root, rel_dir)
48
+ next unless Dir.exist?(dir)
49
+
50
+ modules = Dir.glob(File.join(dir, "**/*.rb")).sort.filter_map do |path|
51
+ content = RailsAiContext::SafeFile.read(path) or next
52
+ mod_name = File.basename(path, ".rb").camelize
53
+ entry = { name: mod_name, file: path.sub("#{root}/", "") }
54
+ entry[:uses_active_support_concern] = true if content.include?("ActiveSupport::Concern")
55
+ entry[:included_blocks] = content.scan(/^\s*included\s+do\b/).size
56
+ entry[:class_methods_block] = content.include?("class_methods do")
57
+ entry
58
+ end
59
+ result[rel_dir] = modules if modules.any?
60
+ end
61
+ result
62
+ rescue => e
63
+ $stderr.puts "[rails-ai-context] extract_concerns failed: #{e.message}" if ENV["DEBUG"]
64
+ {}
65
+ end
66
+
67
+ # Rails 7.1+ registers deprecators per gem/component via
68
+ # `Rails.application.deprecators`. Return the registered keys.
69
+ def extract_deprecators
70
+ return [] unless app.respond_to?(:deprecators)
71
+ registry = app.deprecators
72
+ return [] unless registry
73
+
74
+ keys = if registry.respond_to?(:each) && registry.respond_to?(:map)
75
+ registry.map { |name, _d| name.to_s }
76
+ elsif registry.instance_variable_defined?(:@deprecators)
77
+ (registry.instance_variable_get(:@deprecators) || {}).keys.map(&:to_s)
78
+ else
79
+ []
80
+ end
81
+ keys.compact.sort.uniq
82
+ rescue => e
83
+ $stderr.puts "[rails-ai-context] extract_deprecators failed: #{e.message}" if ENV["DEBUG"]
84
+ []
85
+ end
86
+
87
+ # Scan `lib/` + `app/` for calls into ActiveSupport::MessageEncryptor and
88
+ # ActiveSupport::MessageVerifier. Used for tokens, signed IDs, etc.
89
+ def extract_message_verifier_usage
90
+ hits = []
91
+ %w[lib app].each do |rel|
92
+ dir = File.join(root, rel)
93
+ next unless Dir.exist?(dir)
94
+
95
+ # Sort before slicing — Dir.glob ordering is filesystem-dependent
96
+ # and would produce non-deterministic output on large monorepos.
97
+ Dir.glob(File.join(dir, "**/*.rb")).sort.first(2000).each do |path|
98
+ content = RailsAiContext::SafeFile.read(path) or next
99
+ next unless content.match?(/MessageEncryptor|MessageVerifier/)
100
+ relative = path.sub("#{root}/", "")
101
+ hits << { file: relative, encryptor: content.include?("MessageEncryptor"), verifier: content.include?("MessageVerifier") }
102
+ end
103
+ end
104
+ hits
105
+ rescue => e
106
+ $stderr.puts "[rails-ai-context] extract_message_verifier_usage failed: #{e.message}" if ENV["DEBUG"]
107
+ []
108
+ end
109
+
110
+ def detect_tagged_logging
111
+ result = { configured: false }
112
+ config_logger = app.config.log_tags
113
+ if config_logger.is_a?(Array) && config_logger.any?
114
+ result[:configured] = true
115
+ result[:tags] = config_logger.map(&:to_s)
116
+ end
117
+
118
+ # Initializer pattern: Rails.logger = ActiveSupport::TaggedLogging.new(…)
119
+ Dir.glob(File.join(root, "config/initializers/*.rb")).each do |path|
120
+ content = RailsAiContext::SafeFile.read(path) or next
121
+ if content.include?("ActiveSupport::TaggedLogging")
122
+ result[:configured] = true
123
+ result[:initializer] = path.sub("#{root}/", "")
124
+ break
125
+ end
126
+ end
127
+ result
128
+ rescue => e
129
+ $stderr.puts "[rails-ai-context] detect_tagged_logging failed: #{e.message}" if ENV["DEBUG"]
130
+ { configured: false }
131
+ end
132
+
133
+ # The canonical lazy hooks that Railties expose. Report which have at
134
+ # least one subscriber attached so AI can reason about load-order.
135
+ COMMON_HOOKS = %i[
136
+ active_record before_initialize after_initialize
137
+ action_controller action_controller_base action_controller_api
138
+ action_view action_mailer active_job active_storage
139
+ action_cable action_text action_mailbox
140
+ ].freeze
141
+
142
+ def common_on_load_hooks
143
+ return [] unless defined?(ActiveSupport) && ActiveSupport.respond_to?(:on_load)
144
+ registry = ActiveSupport.instance_variable_get(:@load_hooks)
145
+ return [] unless registry.respond_to?(:each_key)
146
+
147
+ registry.each_key.filter_map do |name|
148
+ next unless COMMON_HOOKS.include?(name)
149
+ callbacks = registry[name]
150
+ callback_count = callbacks.respond_to?(:size) ? callbacks.size : 0
151
+ { hook: name.to_s, callbacks: callback_count } if callback_count > 0
152
+ end.sort_by { |h| h[:hook] }
153
+ rescue => e
154
+ $stderr.puts "[rails-ai-context] common_on_load_hooks failed: #{e.message}" if ENV["DEBUG"]
155
+ []
156
+ end
157
+
158
+ def detect_cache_usage
159
+ store = app.config.cache_store
160
+ entry = {
161
+ store: store.is_a?(Array) ? store.first.to_s : store.to_s
162
+ }
163
+ if store.is_a?(Array) && store.last.is_a?(Hash)
164
+ entry[:options] = store.last.keys.map(&:to_s)
165
+ end
166
+ entry
167
+ rescue => e
168
+ $stderr.puts "[rails-ai-context] detect_cache_usage failed: #{e.message}" if ENV["DEBUG"]
169
+ {}
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Introspectors
5
+ # Extracts the autoloading configuration: Zeitwerk vs Classic, custom
6
+ # inflections, autoload/eager-load paths, collapsed dirs, and ignored
7
+ # paths. Covers RAILS_NERVOUS_SYSTEM.md §3 (Autoloading — Zeitwerk).
8
+ class AutoloadIntrospector
9
+ attr_reader :app
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ # @return [Hash] autoloader configuration
16
+ def call
17
+ {
18
+ mode: detect_mode,
19
+ zeitwerk_available: zeitwerk_available?,
20
+ autoloaders: extract_autoloaders,
21
+ autoload_paths: relativize(app.config.autoload_paths),
22
+ autoload_once_paths: relativize(app.config.autoload_once_paths),
23
+ eager_load_paths: relativize(app.config.eager_load_paths),
24
+ eager_load: !!app.config.eager_load,
25
+ custom_inflections: extract_custom_inflections
26
+ }
27
+ rescue => e
28
+ $stderr.puts "[rails-ai-context] AutoloadIntrospector#call failed: #{e.message}" if ENV["DEBUG"]
29
+ { error: e.message }
30
+ end
31
+
32
+ private
33
+
34
+ def root
35
+ app.root.to_s
36
+ end
37
+
38
+ def zeitwerk_available?
39
+ defined?(Zeitwerk) && defined?(Rails) && Rails.respond_to?(:autoloaders) && Rails.autoloaders.respond_to?(:main)
40
+ end
41
+
42
+ def detect_mode
43
+ return "zeitwerk" if zeitwerk_available?
44
+ return "classic" if defined?(Rails) && app.config.respond_to?(:autoloader) && app.config.autoloader == :classic
45
+ "unknown"
46
+ end
47
+
48
+ # Return per-autoloader metadata: name, collapsed dirs, ignored paths.
49
+ # Rails exposes `Rails.autoloaders.main` and `.once` by default.
50
+ def extract_autoloaders
51
+ return [] unless zeitwerk_available?
52
+
53
+ %i[main once].filter_map do |kind|
54
+ loader = Rails.autoloaders.public_send(kind) if Rails.autoloaders.respond_to?(kind)
55
+ next unless loader
56
+
57
+ entry = { name: kind.to_s }
58
+ entry[:tag] = loader.tag.to_s if loader.respond_to?(:tag)
59
+ entry[:collapsed] = relativize(extract_collapsed(loader))
60
+ entry[:ignored] = relativize(extract_ignored(loader))
61
+ entry[:root_dirs] = relativize(extract_root_dirs(loader))
62
+ entry
63
+ rescue => e
64
+ $stderr.puts "[rails-ai-context] extract autoloader #{kind} failed: #{e.message}" if ENV["DEBUG"]
65
+ { name: kind.to_s, error: e.message }
66
+ end
67
+ end
68
+
69
+ def extract_collapsed(loader)
70
+ collapsed = loader.instance_variable_get(:@collapse_dirs)
71
+ return [] unless collapsed
72
+ collapsed.respond_to?(:to_a) ? collapsed.to_a.map(&:to_s) : []
73
+ rescue => e
74
+ $stderr.puts "[rails-ai-context] extract_collapsed failed: #{e.message}" if ENV["DEBUG"]
75
+ []
76
+ end
77
+
78
+ def extract_ignored(loader)
79
+ ignored = loader.instance_variable_get(:@ignored_paths)
80
+ return [] unless ignored
81
+ ignored.respond_to?(:to_a) ? ignored.to_a.map(&:to_s) : []
82
+ rescue => e
83
+ $stderr.puts "[rails-ai-context] extract_ignored failed: #{e.message}" if ENV["DEBUG"]
84
+ []
85
+ end
86
+
87
+ def extract_root_dirs(loader)
88
+ return loader.dirs.to_a if loader.respond_to?(:dirs) && loader.dirs.respond_to?(:to_a)
89
+ roots = loader.instance_variable_get(:@roots)
90
+ return roots.keys.map(&:to_s) if roots.respond_to?(:keys)
91
+ []
92
+ rescue => e
93
+ $stderr.puts "[rails-ai-context] extract_root_dirs failed: #{e.message}" if ENV["DEBUG"]
94
+ []
95
+ end
96
+
97
+ # Collect `inflect` blocks and `Zeitwerk::Inflector` customizations
98
+ # declared in config/initializers/*.rb.
99
+ def extract_custom_inflections
100
+ dir = File.join(root, "config/initializers")
101
+ return [] unless Dir.exist?(dir)
102
+
103
+ inflections = []
104
+ Dir.glob(File.join(dir, "*.rb")).sort.each do |path|
105
+ content = RailsAiContext::SafeFile.read(path) or next
106
+ rel = path.sub("#{root}/", "")
107
+
108
+ # inflect "api" => "API", "xml" => "XML"
109
+ content.scan(/inflect\(?\s*((?:["'][^"']+["']\s*=>\s*["'][^"']+["'],?\s*)+)/).each do |match|
110
+ pairs = match[0].scan(/["']([^"']+)["']\s*=>\s*["']([^"']+)["']/)
111
+ inflections.concat(pairs.map { |k, v| { file: rel, rule: "#{k} => #{v}" } })
112
+ end
113
+
114
+ # inflect.acronym "API" / inflect.plural /…/, "…" / inflect.irregular "…", "…"
115
+ # Matches both inside `do |inflect|` blocks and via direct Inflector calls.
116
+ content.scan(/\binflect\.(acronym|plural|singular|irregular|uncountable|human)\s+["']([^"']+)["'](?:\s*,\s*["']([^"']+)["'])?/).each do |method, arg1, arg2|
117
+ rule = arg2 ? "#{method}: #{arg1} => #{arg2}" : "#{method}: #{arg1}"
118
+ inflections << { file: rel, rule: rule }
119
+ end
120
+ end
121
+ inflections.uniq
122
+ rescue => e
123
+ $stderr.puts "[rails-ai-context] extract_custom_inflections failed: #{e.message}" if ENV["DEBUG"]
124
+ []
125
+ end
126
+
127
+ def relativize(paths)
128
+ Array(paths).map do |p|
129
+ s = p.to_s
130
+ s.start_with?(root) ? s.sub("#{root}/", "") : s
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end