rails-ai-context

45 MCP tools that give AI agents live access to your Rails schema, models, routes & conventions.

View the Project on GitHub crisnahine/rails-ai-context

Introspectors

40 modules that extract structured data from your Rails application.

Architecture · Configuration · Tools Reference · Security


How introspectors work

Each introspector:

  1. Subclasses Introspectors::Base, taking the app handle
  2. Examines a specific aspect of your Rails app (schema, models, routes, etc.)
  3. Returns a Hash with structured data, and raises on failure: Introspector#call turns a raised section into { error: msg } and logs a warning
  4. Results are cached with TTL + SHA256 fingerprint invalidation
  5. Runs as part of a preset (:standard or :full) or can be configured individually

Presets

:full (default) - all 40 introspectors

Full AI context. Covers every aspect of your app.

:standard - 17 introspectors

Lightweight subset for faster generation:

schema, models, routes, jobs, gems, conventions, controllers,
tests, migrations, stimulus, view_templates, config, components,
turbo, auth, performance, i18n

Preset comparison

graph LR
    subgraph standard["Standard Preset - 17 introspectors"]
        direction TB
        S1["schema"] ~~~ S2["models"] ~~~ S3["routes"]
        S4["controllers"] ~~~ S5["jobs"] ~~~ S6["gems"]
        S7["conventions"] ~~~ S8["tests"] ~~~ S9["migrations"]
        S10["stimulus"] ~~~ S11["view_templates"] ~~~ S12["config"]
        S13["components"] ~~~ S14["turbo"] ~~~ S15["auth"]
        S16["performance"] ~~~ S17["i18n"]
    end

    subgraph full_only["Full Preset adds +23"]
        direction TB
        F1["views"] ~~~ F2["database_stats"] ~~~ F3["api"]
        F4["active_storage"] ~~~ F5["action_text"] ~~~ F6["action_mailbox"]
        F7["rake_tasks"] ~~~ F8["assets"] ~~~ F9["devops"]
        F10["seeds"] ~~~ F11["middleware"] ~~~ F12["engines"]
        F13["multi_database"] ~~~ F14["frontend_frameworks"]
        F15["initializers"] ~~~ F16["autoload"] ~~~ F17["connection_pool"]
        F18["active_support"] ~~~ F19["credentials"] ~~~ F20["security"]
        F21["observability"] ~~~ F22["env"] ~~~ F23["env_config"]
    end

    standard --> full_only

    style standard fill:#3498db,stroke:#2980b9,color:#fff
    style full_only fill:#9b59b6,stroke:#8e44ad,color:#fff

Custom list

RailsAiContext.configure do |config|
  config.introspectors = %i[schema models routes controllers views]
end

All 40 introspectors

Core

Introspector Key What it extracts
SchemaIntrospector :schema Database tables, columns, types, indexes, defaults, check constraints, enum types, extensions
ModelIntrospector :models Associations, validations, scopes, enums, concerns (AST-based). Both tiers merge what the included concerns declare, tagged from_concern:; a concern whose file could not be read is listed in concerns_unread, a base class whose file could not be read in bases_unread, and the number of concerns excluded_concerns hid in concerns_hidden
RouteIntrospector :routes Routes with helpers, HTTP methods, constraints
ControllerIntrospector :controllers Actions, filters, strong params, concerns, rescue_from handlers, rate limits
ViewIntrospector :views View files, layouts, partials
ViewTemplateIntrospector :view_templates Each template with its line count, ivars, rendered partials and Stimulus refs; each partial with its line count, model fields and helper calls

Models & Data

Introspector Key What it extracts
MigrationIntrospector :migrations Migration files, versions, the schema actions each one takes. recent and pending entries are { version:, name: }, and the name is the migration class (CreatePosts) in both tiers
SeedsIntrospector :seeds Seed file analysis
DatabaseStatsIntrospector :database_stats Approximate row counts per table, plus dead rows on PostgreSQL
MultiDatabaseIntrospector :multi_database Multi-database configuration

Frontend

Introspector Key What it extracts
StimulusIntrospector :stimulus Controllers, targets, values, actions
TurboIntrospector :turbo Turbo wiring with each entry’s file and line: turbo_frames, model_broadcasts, explicit_broadcasts, stream_subscriptions
AssetPipelineIntrospector :assets Asset pipeline configuration, manifests
FrontendFrameworkIntrospector :frontend_frameworks React/Vue/Svelte/Angular detection
ComponentIntrospector :components ViewComponent/Phlex: props, slots, previews

Config & Infrastructure

Introspector Key What it extracts
ConfigIntrospector :config Cache store, session store, time zone, queue adapter, mailer settings, middleware stack, initializers, Current attributes, error monitoring
GemIntrospector :gems Notable gems with versions and categories
ConventionIntrospector :conventions Architecture markers (service objects, Hotwire, GraphQL and others), model patterns (STI, soft delete, state machines and others), directory structure, config files
I18nIntrospector :i18n Locale files, translation keys
MiddlewareIntrospector :middleware Rack middleware stack. The static tier declares an alternate source rather than an empty stack: without a booted app it answers only the file facts it can read. config.ru’s top-level use and map calls (rackup) are read on both tiers with ConditionalMacroListener, each with the if it sits under; a map whose block runs the app itself is a path prefix, so the use calls inside it are listed instead of the map
EngineIntrospector :engines Mounted engines
EnvConfigIntrospector :env_config Per-environment config files: notable toggles (force_ssl, eager_load, caching, queue adapter), assigned config keys, plus the keys config/application.rb sets and each config_for file’s keys
DevOpsIntrospector :devops Puma config, Procfile, health check, Dockerfile, deployment tool

Jobs & Services

Introspector Key What it extracts
JobIntrospector :jobs Background jobs and Sidekiq workers, read from app/jobs, app/workers and app/sidekiq, and mailers (anywhere under app/, by parent chain): queue, retries, sidekiq_options, any sidekiq_throttle, schedules, and the file: each one is defined in
RakeTaskIntrospector :rake_tasks Custom rake tasks from the Rakefile, lib/tasks and rakelib; the app’s generators (lib/generators), generator template overrides (lib/templates) and Railties under lib/

Security & Auth

Introspector Key What it extracts
AuthIntrospector :auth Authentication framework (Devise, etc.)
ApiIntrospector :api API configuration, versioning, serializers

Rails Features

Introspector Key What it extracts
ActiveStorageIntrospector :active_storage Attachments, variants, services
ActionTextIntrospector :action_text Rich text attributes
ActionMailboxIntrospector :action_mailbox Mailbox routing rules

Analysis

Introspector Key What it extracts
TestIntrospector :tests Test framework, file counts, coverage hints
PerformanceIntrospector :performance N+1 risks, missing indexes, counter_cache hints

Runtime & Framework Internals

These introspectors map directly onto the internal RAILS_NERVOUS_SYSTEM checklist sections and capture framework-level surface that :config, :auth, and :middleware don’t.

Introspector Key Nervous-system § What it extracts
InitializerIntrospector :initializers §2 Rails.application.initializers graph: name, owner, before:/after: edges, block source_location, per-file config/initializers/*.rb summary
AutoloadIntrospector :autoload §3 Zeitwerk presence, autoloaders (:main / :once) with collapsed + ignored dirs, autoload_paths, eager_load_paths, the root directories each loader holds (other engines’ and push_dir roots, with the namespace they load under), custom inflections (acronym, plural, singular, irregular, uncountable, human)
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
ActiveSupportIntrospector :active_support §17 Concerns in app/**/concerns/ (ActiveSupport::Concern flags, included do/class_methods do blocks), deprecators registry, MessageEncryptor/Verifier usage, notification subscriptions read from source (subscribe, monotonic_subscribe, attach_to), TaggedLogging config, common on-load hooks, cache store options
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)
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
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)
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

AST-based introspection

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.

It runs a single-pass Dispatcher that walks the AST once and feeds events to all registered listeners simultaneously. Model analysis uses the eight below by default; the rest are used through targeted walks (schema dumps, migrations, Gemfiles, rake tasks, initializers, components, and so on).

The 8 default Prism listeners

Listener What it detects
AssociationsListener belongs_to, has_many, has_one, has_and_belongs_to_many, the polymorphic belongs_to of delegated_type, the belongs_to of acts_as_tenant (tenant defaults to :account), under the options of an enclosing with_options block
ValidationsListener validates, validates_*_of, custom validate :method, under the options of an enclosing with_options block
ScopesListener scope :name, -> { ... }, lambda { ... } and the block form
EnumsListener Rails 7+ and legacy enum syntax, prefix/suffix options
CallbacksListener All AR callback types including around_*, after_touch, after_initialize and after_find; after_commit with on: resolution; a callback object as the source wrote it (Normalizer, Normalizer.new), a block as [inline_block]; if:/unless: kept as the source wrote them, and the options of an enclosing with_options block
MacrosListener (each attribute macro and has_secure_password with its keyword options as written, under written) encrypts, normalizes, delegate, has_secure_password, serialize, store, has_one_attached, has_many_attached, has_rich_text, generates_token_for, attribute, alias_attribute, store_accessor, self.ignored_columns, has_secure_token, accepts_nested_attributes_for, attr_readonly, query_constraints, connects_to as written (a child carries its base’s, with the base’s name under declared_in), the class settings self.inheritance_column/store_full_sti_class/strict_loading_by_default/implicit_order_column/locking_column = as written, an aasm block’s states, initial state, events and transitions, and model gems’ class macros (GEM_MACROS: has_paper_trail, friendly_id, mount_uploader, monetize, pg_search_scope, acts_as_list and others) as written, without their block
MethodsListener def/def self., visibility tracking, parameter extraction, class << self, the methods delegate and Forwardable’s def_delegators/def_delegator define (public unless private: true, and with no end_location, since a delegation has no body), and the methods alias, alias_method, attr_*, define_method with a literal name, class_attribute and cattr_*/mattr_* define (an alias keeps its original’s visibility and parameters; Active Support’s accessors are public on the class and, unless an option turns them off, on instances); each def carries offset/end_offset, which SourceIntrospector.outside_defs pairs against a call’s offset to tell a call on the same line as a def from one inside it; a Struct.new/Class.new/Module.new/Data.define block assigned to a constant is that constant’s body, and a def self.x in included do is marked class_methods_block like a def x in class_methods do; include_initialize: true adds the constructor a caller reports on its own
MixinsListener include, prepend, extend, singleton_class.include and singleton_class.prepend (singleton_class.extend is not read), flagging the ones that reach the ancestor chain, as reflection reports them. GitLab’s prepend_mod_with("Note"), include_mod_with, extend_mod_with and their _mod forms record EE::Note and JH::Note flagged edition; ConcernMembership.owned_by keeps one only where the app has its file, and credits Note.prepend X to Note

The targeted-walk listeners

Passed to SourceIntrospector.walk(path, key => Listener) when a specific file needs reading. Several take arguments, so one class serves many callers.

Listener What it detects
GenericMacroListener Any receiver-less macro you name: GenericMacroListener.new(:devise, :rate_limit); any_receiver: true also counts base.macro, as a mixin’s self.included(base) hook writes it. Returns args, values (with a source-slice fallback), options, option values and option nodes, proc_lines (the line each block, ->, lambda {}, proc {} or Proc.new {} argument opens on, as Proc#source_location gives it), plus the nesting: parent_offset is the offset of the target macro call whose block this one sits in, paired against each call’s own offset rather than its line
ConditionalMacroListener GenericMacroListener plus condition, the if/unless/case branches the call sits under (BranchConditions), as if Rails.env.test?. Controller filters read it through FilterMacroListener, so before_action :x if Rails.env.test? is listed with its condition
FilterMacroListener ConditionalMacroListener plus callbacks, what each positional argument adds, named at its node: [:name, x] for a symbol, string or class, [:object, Klass] for an instance (Class for an anonymous class), [:lambda] for -> {} or lambda {}, [:block] for a proc, [:unread, source] for anything else. Used by ControllerFilters, so the static tier names each callback as the booted tier does
ConstructorMacroListener Class-body macros that define a constructor: T::Struct const/prop, Dry::Struct attribute/attribute?, dry-initializer param/option (with extend recorded so a consumer can check for Dry::Initializer), and the attr_extras initializers, method_object and static_facade. Each record adds owner, the one-line source, and params as [kind, name, default] with lambda defaults read as the value they return. Used by rails_get_service_pattern, which keeps a record only when the class is that library’s
GrapeApiListener A Grape API class body: each class with its superclass, version (with using:) and prefix, each verb endpoint with the namespaces around it (namespace, resource(s), group, segment, route_param as :id) and the requires/optional names of the params block before it, and each mount. Calls inside an endpoint body are skipped. Used by GrapeEndpoints, which decides which classes reach Grape::API and builds each path the way Grape does, for rails_get_routes
ChainedCallListener Calls on a receiver: ChainedCallListener.new(:includes), or receiver: :inflect to pin the receiver. Reports the receiver name
ConfigAssignmentListener config.key = value and config.a.b = value in initializers and config/environments/*.rb, plus bare config.jwt do ... end section references and settings written without = (<<, a call with arguments, +=/||=/&&=, a block), tagged write:. An assignment whose value is a config_for call carries config_for: (the YAML file it reads and its env:). An assignment whose value is a string or symbol written as one carries literal: true. An assignment whose value is not redacted carries the same arguments:, computed: and constants: MethodCallListener reports for the value as one argument. Takes a root name (:config by default, e.g. :DatabaseCleaner); the root on_load(:active_record) reads self.x = and base.x = for the block’s first param inside that ActiveSupport.on_load block (not inside a nested class or def; inside Foo.class_eval self.x = is not read while base.x = still is; a class_eval on that self, that param or a root constant keeps the root), and a root named Receiver.method (Apartment.configure) binds that block’s first param
ClassDefinitionListener Class definitions with their superclass and the nesting a bare superclass is read in
NestedConstantsListener The offset ranges of the modules and classes a class body nests, so a filter declared inside one is not read as the outer class’s own. Used by ControllerFilters
ComponentStructureListener ViewComponent and Phlex structure: renders_one/renders_many, slot methods, hash/array constant tables, case @ivar variant branching, CONST[@ivar] indexing
MiddlewareConfigListener The app’s own stack, reached through its config (config.middleware, Rails.configuration.middleware, app.config.middleware), through the app (Rails.application.middleware, app.middleware) or through the app’s own application class (MyApp::Application.config.middleware, given as app_class:), never another rack stack: any other constant anywhere in the chain (MyEngine.config.middleware, GoodJob::Engine.middleware) is an engine’s. Reads use, insert, insert_before, insert_after, unshift, swap, move_before, move_after, delete, and config.exceptions_app = as its own exceptions_app action
RouteFilesListener The route files config/application.rb puts in config.paths["config/routes.rb"]: an assignment (a list, .mapped or not), <</push/concat, unshift/prepend, Rails.root.join and literal Dir[...] globs, each as set/append/prepend. A list the app computes is recorded as computed
AutoloadPathsListener Autoload roots config/application.rb adds by hand: autoload_paths/eager_load_paths/autoload_once_paths appends, autoload_lib, and config.paths.add with eager_load:. Literal paths under the app root only
AutoloadIgnoreListener The lib subdirectories autoload_lib(ignore:) and autoload_lib_once(ignore:) keep out of autoloading, as lib/<name>. Literal strings and symbols only
PreviewPathsListener ViewComponent preview directories the config sets: view_component.previews.paths, preview_paths, preview_path, in the same literal forms as AutoloadPathsListener; with framework: :action_mailer, the mailer preview directories action_mailer.preview_paths and preview_path set
I18nLoadPathListener Locale files config.i18n.load_path or I18n.load_path adds: +=, <<, push, append, concat, with Dir[]/Dir.glob around the same literal forms as AutoloadPathsListener
FixturePathsListener Fixture directories a test helper sets: fixture_paths =/<</+=/push and the older fixture_path =, on self, config or a constant (<</push with no receiver too), in the same literal forms as AutoloadPathsListener plus File.expand_path("x", __dir__), File.expand_path("x", __FILE__) and File.join(__dir__, "x") when given the helper’s file; a write whose path it cannot read records :unread
DefinitionFilePathsListener Where a test helper points factory_bot: FactoryBot.definition_file_paths = (replaces the defaults) and <</+=/push/concat (adds), each write as { replace:, paths: } in the same forms as FixturePathsListener (unread: true when it cannot read every path, with paths holding those it could), and each FactoryBot.find_definitions/FactoryBot.reload as { load: }, in source order; ::FactoryBot counts
ViewPathsListener View roots config/application.rb adds to config.paths["app/views"]: unshift puts one before app/views, <</push/concat after it, in the same literal forms as AutoloadPathsListener; a write whose path it cannot read is left out
NamespacedRootsListener Roots config/application.rb or an initializer hands Zeitwerk under a namespace: push_dir(path, namespace: Const), as phlex:install writes, in the same literal forms as AutoloadPathsListener; a write whose path it cannot read is left out
SchemaDslListener schema.rb: create_table, t.string, t.column, t.index, add_foreign_key, create_enum, create_schema, create_view, create_virtual_table, the ActiveRecord::Schema[x.y] version stamp (a nil one for the unstamped Schema.define of Rails before 7.0), and the comment the dumper writes for a table it could not describe. Names drop public. unless raw_names: is set, which SchemaReader uses to name them against the search path
MigrationDslListener Migration DSL: create_table, add_column, add_index, add_reference, and friends
MigrationReplayListener What a replay needs beside the DSL: def down, down/revert blocks, and t.timestamps
RoutesDslListener config/routes.rb, resolving namespace/scope/resources nesting into flat routes (a controller: with a leading slash is absolute, as in Rails); routing concerns (concern definitions replayed at each concerns: site), with_options defaults merged under each inner call, match ... via: (or the via: of an enclosing scope) as one route answering each verb it names (GET|POST, and ANY for via: :all, as the booted table has it), the controller a scope(controller:) or a route’s own controller: names (with Rails’ a/b shorthand when no action is given), and the as:, param:, module:, path: and only:/except: options. A block drawn through an app class (ApiRouteSet::V1.draw(self) do) takes the path and as: prefixes that class’s self.prefix and mapper_prefix return as literals, or a literal prefix argument; a class whose prefix is not a literal, and that class’s own resources, are counted as unexpanded. Routes drawn into an engine’s table (Spree::Core::Engine.routes.draw) sit under the engine’s namespace and carry engine:, which the route introspector files under that engine’s mount, apart from the app’s count. A file pulled in by draw is walked inside the scope its draw sits in, once per scope that draws it, so draw :api under namespace :api and scope module: :v1 routes to api/v1/... at /api/...
MountListener mount Sidekiq::Web, at: "/sidekiq", the hash form, and a Rack app attached with match "/metrics", to: MetricsApp - mount is that call with a name derived. Paths carry the enclosing namespace/scope prefix; a scope whose own name is an expression yields no path rather than an unprefixed one, and scope path: nil adds no segment. A mounted app built by a call on a constant (Flipper::UI.app(Flipper)) is named by that call, arguments off. A mount under if/unless/case carries that condition
GemfileDslListener gem "name", "version", group :development do ... end and eval_gemfile "path" (a literal or File.expand_path("x", __dir__)) with the groups around it; ruby "3.3.6" with its engine: and engine_version:; an :unknown_gems entry for gemspec and for a gem or eval_gemfile whose argument is not a literal
RakeTaskDslListener namespace (with the span its block covers), desc, task, multitask in .rake files
EnvAccessListener ENV["KEY"], ENV.fetch("KEY"), ENV.fetch("KEY", default), and the ENV name Rails 8.2’s Rails.app.creds or Rails.app.envs require/option reads (option(:database, :host) is DATABASE__HOST)
MailboxRoutingListener Action Mailbox routing and processing callbacks
ModelReferenceListener Model constants used in controllers: Post.find, params.require(:post), ivar writes
VariantCallListener variant calls (ChainedCallListener with :variant preset)
ProcLiteralListener Proc literals: line, source, assigned constant
QueueAssignmentListener A queue a class body assigns outside any method: Resque’s @queue = :name, Que’s self.queue = "name", or a def self.queue returning one, a literal as its value, anything else as source
HttpClientCallListener Calls on an HTTP client constant (Faraday, Net::HTTP, HTTParty, RestClient, HTTP, Excon, Typhoeus, URI.open) with a literal URL, bare, wrapped in URI(...)/URI.parse(...) or as url:, and the host Net::HTTP.start/.new takes. rails_get_env names external services from it
ConstantReferenceListener References to constants by last name, qualified or not (ActiveSupport::MessageVerifier), skipping one that only qualifies a nested constant (MessageVerifier::InvalidSignature) and mentions in comments or strings. Used by the ActiveSupport introspector for message verifier and encryptor usage
MethodCallListener Call sites by name or pattern anywhere in a file, inside a def, a lambda or a block included, with arguments, options, receiver, line, offset, the owning class or module, and constants: (each argument that is a constant, or builds one with .new, mapped to the constant’s name). Used by the Turbo introspector for broadcast calls, the ActiveSupport introspector for notification subscriptions, and others

GenericMacroListener.new(*names, block_source: [:name]) adds block, the one-line source of the block those macros are given. call_source: [:name] adds text, the one-line source of the whole call (the Mongoid index reader). ProcLiteralListener is what the job introspector reads a queue_as Proc with.

Adding a listener

  1. Subclass BaseListener in lib/rails_ai_context/introspectors/listeners/. Use its helpers rather than re-reading nodes: extract_symbol_args, extract_keyword_options, extract_arg_values (source-slice fallback for expressions like 2.hours), extract_keyword_sources, extract_keyword_nodes, keyword_hash, constant_path_string.
  2. Implement the on_*_node_enter hooks you need and push plain hashes onto @results. Never return Prism nodes as the result itself; option_nodes is the one deliberate exception, for callers that must inspect an expression’s shape.
  3. Nothing to register. ListenerRegistration reads the events off the on_* methods you defined, inherited ones included, and raises if one names an event prism never dispatches.
  4. Add a spec of the same name under spec/lib/rails_ai_context/introspectors/listeners/.
  5. Add a row to the table above.

Choosing between AST and regex

Use the AST when the thing you want is a Ruby construct: a macro call and its arguments, a method definition, a class and its superclass, an assignment, a constant, a case. If you find yourself running a regex over text you already parsed, that is a parse of a parse. Fix it at the node.

Regex is the right tool, and stays, for:

Every remaining regex over .rb content carries a one-line comment saying which of these it is. If you add one without a reason, convert it instead.

Readers built on the listeners

Some questions take more than one walk to answer, and the answer has to be the same wherever it is asked. Those live as their own modules under Introspectors/, and a tool calls one rather than repeating the walk:

Module What it answers
DeclaredConstant The constant a source file calls its own class, against the one its path camelizes to
ActionPresence Whether a controller has an action: the public methods and define_method names of the controller, its ancestors up to Rails’ base and the modules they include, the templates at each ancestor’s view prefix, and the ancestors or modules no app source holds, which leave a missing action unverified. rails_validate and rails_generate_test both ask it
ControllerSettings The layout a controller renders in (declared on it or an ancestor, else the layouts/<controller_path> file Rails finds by name, walking up the chain) and the allow_browser, protect_from_forgery, add_flash_types, default_form_builder and wrap_parameters calls it and its ancestors make. rails_get_controllers and rails_get_view both ask it
TableName The table a model reads, from its own declarations
ActiveRecordSettings What config/application.rb, the environment file and the initializers set on Active Record: the table affixes, pluralize_table_names and schema_format, in the order Rails applies them. Read once per introspection run or tool call
HabtmJoinTables The join tables every has_and_belongs_to_many under the app’s code and lib names, lib patches and engines included, for the schema’s model-less table warning
SuperclassChain What a class inherits from, followed through the app’s own sources: the chain from a file’s class up to a named base, and the constant-to-source lookup over the app’s autoload roots that walks it
CallSiteExpansion What one call of a mixin’s macro-declaring method declares. attachable :receipt, has_one: true runs def attachable(name, opts = {}), whose body declares has_one name or has_many name by opts[:has_one]: the parameters bind to the call’s literal arguments, and a branch (if, unless, case) the literals decide is taken alone. Where they cannot decide, what every way through declares alike is listed once; the rest, a lone if’s body included, comes back under :conditional with its condition and is not counted. A block the method evaluates on another receiver (other.instance_eval) comes back under :foreign, named with the receiver and any condition it runs under
Includers Which classes and modules mix a module in, by include, prepend or extend: a written name resolves from the includer’s namespace outward, and the nearest module the app declares decides. rails_get_concern’s “Included by”, the service listing and the HABTM join-table owners all ask it
RetryPolicy What a job does when it raises, as a reader would write it: the macro, its exceptions, then attempts: and wait: whatever order the source put them in
SourceCalls Which other classes a file hands work to, off the call nodes: the verb list, the framework receivers left out, and the call or the class alone
ServiceClasses Which classes under app/services, app/interactions and app/interactors are services and which are only the base of one, for the tool’s listing and the generated files’ line alike
EnvReferences Every ENV name the app’s source reads, file by file, for rails_get_env and the context file’s env section alike: app, config and lib Ruby, ERB and config YAML, config.ru, db/seeds and the bin/ scripts whose shebang is Ruby, with config YAML on sensitive_patterns read for the names in its ERB tags only
AnywayConfigs Each Anyway::Config class under config/configs and app/configs, every class in a file, with the attributes its own body declares (a nested class keeps its macros) and the env name anyway_config reads each from: env_prefix, else config_name, else the name before Config. rails_get_env lists them
GemfileGems The one Gemfile read, off GemfileDslListener: its entries with options and groups (the gems section’s local gems and groups) and the gem names (every other asker), so a commented-out gem line is no gem anywhere; a file named by eval_gemfile is followed when it is inside the bundle’s directory (the app root, or the directory of the Gemfile config/boot.rb points at), and one it cannot read is an :unknown_gems entry. GemLock takes the declared Ruby and the gem names from it once the gem is loaded; the walk is kept until one of the files changes. Before the boot, GemLock walks the one Gemfile with AstWalk instead and leaves eval_gemfile unread
ModuleAliases Which app file a bare JS import specifier names: tsconfig/jsconfig compilerOptions.paths followed through extends (relative files and installed packages), and a vite/webpack/rspack resolve.alias written as a literal object. The Stimulus scan uses it to tie a registration or a base class to the controller file it imports
HelperNames The helper methods a view can call: every method the app’s helper modules define in every code root, and those of a module they include, found through the app’s autoload roots (lib among them) or in its enclosing namespace’s file (CanonicalURL::Helpers in canonical_url.rb). rails_get_partial_interface uses it so a helper call is not read as a local
Interaction Whether a class runs as an ActiveInteraction, following its superclass chain through the app’s own sources, and the filters it takes - inherited ones first, one per name, each carrying the filters nested inside its block. See the Interaction filter entry in CONTEXT.md
RecurringSchedules The recurring tasks each scheduler reads from its own file: Solid Queue’s config/recurring.yml, sidekiq-cron’s config/schedule.yml, sidekiq-scheduler’s section of config/sidekiq.yml, GoodJob’s config.good_job.cron and whenever’s config/schedule.rb. The job introspector hands it its own config walk, so a file it also reads for queue or mailer settings is walked once
AdminResources The models an admin gem exposes (ActiveAdmin, Trestle, Administrate, Avo, Madmin), from the folder each gem’s generator writes to, with ActiveAdmin’s permit_params. rails_get_conventions and rails_analyze_feature both ask it
SchemaDumpPath The primary database’s dump file as DatabaseTasks.schema_dump_path names it: database.yml’s schema_dump in the configured schema_format, then each format’s default file, and the secondary databases’ dumps. Every schema reader, the doctor and the pending-migration check ask it
ApartmentConfig The models an Apartment initializer keeps in the shared schema (excluded_models), or the source of a list the file computes. The model introspector asks it to say which models get one table per tenant
SchemaReader What a schema.rb dump declares: tables, columns, defaults, indexes, foreign keys, enum types and check constraints. SchemaReader.for(root) answers from whichever source the app committed: schema.rb, structure.sql, or a migration replay
StructureSqlReader A structure.sql dump (PostgreSQL, MySQL or SQLite) as the same table shape the other static schema sources produce
MigrationReplay The tables of an app with no schema dump, replayed from its migrations in the dump readers’ shape
SchemaConventions The Rails schema conventions every static source must agree on: the implicit id key and its type per adapter, what a reference declares, what t.timestamps expands to
PgNaming How Rails’ PostgreSQL connection and schema.rb dumper name extensions, enum types and relations, by Rails version, so the static tier names them as the booted app does
SqliteVirtualTables SQLite’s virtual tables and their shadow tables, read from sqlite_master. The schema and database stats introspectors ask it
RenderedRecord The records a bare render @post.comments hands to Rails, and the partial Rails renders for them. rails_get_partial_interface and the view template introspector ask it
NodeSource A node’s source with the bodies of the heredocs it opens, which Prism’s slice leaves out

Confidence tagging

Every AST result carries a confidence tag:

has_many :posts                    → [VERIFIED]
has_many :posts, class_name: name  → [INFERRED]  (name is a variable)

AstCache

Thread-safe parse cache using Concurrent::Map:

RunCache

Answers kept for one introspection run and dropped when it ends: the file list for a source kind, each file’s stat, the directories a kind lives in, concern directories and listings. A section asking again inside the run gets the first answer; the next run, and any tool called after it, asks the filesystem again.


Cache invalidation

Introspection results are cached at three levels:

  1. Introspection cache - Full context hash, invalidated by TTL (config.cache_ttl, default: 60s) and fingerprint change
  2. AST cache - Per-file parse results, invalidated by file content change (SHA256), with a stat shortcut for a file older than the read
  3. Run cache - File lists, stats and directory answers for one introspection run, dropped when it ends

The Fingerprinter computes a composite SHA256 from all watched directories (app/, config/, db/, lib/, rakelib/, test/, spec/, the Gemfile and Gemfile.lock (or gems.rb and gems.locked), package.json, tsconfig.json, config.ru, the Rakefile). When the fingerprint changes, the introspection cache is invalidated even if TTL hasn’t expired.

Live Reload watches these directories and calls reset_all_caches! when changes are detected, then notifies connected MCP clients via notify_resources_list_changed.


← Architecture · Security →

Back to Home