rails-ai-context 5.32.0 → 5.32.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -0
  3. data/CONTEXT.md +10 -8
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +14 -11
  6. data/SECURITY.md +4 -4
  7. data/docs/ARCHITECTURE.md +11 -10
  8. data/docs/CLI.md +33 -7
  9. data/docs/COMPATIBILITY.md +69 -32
  10. data/docs/CONFIGURATION.md +10 -8
  11. data/docs/CUSTOM_TOOLS.md +8 -8
  12. data/docs/FAQ.md +6 -5
  13. data/docs/GUIDE.md +154 -80
  14. data/docs/INTROSPECTORS.md +25 -17
  15. data/docs/QUICKSTART.md +14 -3
  16. data/docs/RECIPES.md +50 -40
  17. data/docs/SECURITY.md +23 -13
  18. data/docs/SETUP.md +23 -26
  19. data/docs/STANDALONE.md +7 -3
  20. data/docs/TOOLS.md +79 -45
  21. data/docs/TROUBLESHOOTING.md +12 -11
  22. data/exe/rails-ai-context +8 -4
  23. data/lib/generators/rails_ai_context/install/install_generator.rb +1 -1
  24. data/lib/rails_ai_context/configuration.rb +2 -2
  25. data/lib/rails_ai_context/database_yml.rb +52 -2
  26. data/lib/rails_ai_context/install/ai_tool.rb +11 -2
  27. data/lib/rails_ai_context/install/cleanup.rb +69 -23
  28. data/lib/rails_ai_context/install/program.rb +4 -1
  29. data/lib/rails_ai_context/install/selection_record.rb +20 -2
  30. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +30 -9
  31. data/lib/rails_ai_context/introspectors/model_introspector.rb +1 -1
  32. data/lib/rails_ai_context/introspectors/pg_naming.rb +133 -0
  33. data/lib/rails_ai_context/introspectors/schema_conventions.rb +0 -6
  34. data/lib/rails_ai_context/introspectors/schema_introspector.rb +188 -51
  35. data/lib/rails_ai_context/introspectors/schema_reader.rb +98 -12
  36. data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +56 -33
  37. data/lib/rails_ai_context/payload.rb +25 -2
  38. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +2 -2
  39. data/lib/rails_ai_context/server.rb +2 -2
  40. data/lib/rails_ai_context/tasks/rails_ai_context.rake +12 -4
  41. data/lib/rails_ai_context/test_helper.rb +2 -0
  42. data/lib/rails_ai_context/tools/base_tool.rb +3 -1
  43. data/lib/rails_ai_context/tools/dependency_graph.rb +2 -2
  44. data/lib/rails_ai_context/tools/get_concern.rb +1 -1
  45. data/lib/rails_ai_context/tools/get_env.rb +1 -1
  46. data/lib/rails_ai_context/tools/get_gems.rb +2 -2
  47. data/lib/rails_ai_context/tools/get_model_details.rb +2 -0
  48. data/lib/rails_ai_context/tools/get_schema.rb +17 -14
  49. data/lib/rails_ai_context/tools/query.rb +107 -39
  50. data/lib/rails_ai_context/tools/search_code.rb +20 -16
  51. data/lib/rails_ai_context/version.rb +1 -1
  52. data/server.json +1 -1
  53. metadata +2 -1
@@ -84,20 +84,20 @@ end
84
84
 
85
85
  | Introspector | Key | What it extracts |
86
86
  |:-------------|:----|:-----------------|
87
- | SchemaIntrospector | `:schema` | Database tables, columns, types, indexes, defaults, encrypted hints |
87
+ | SchemaIntrospector | `:schema` | Database tables, columns, types, indexes, defaults, check constraints, enum types, extensions |
88
88
  | 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` |
89
89
  | RouteIntrospector | `:routes` | Routes with helpers, HTTP methods, constraints |
90
- | ControllerIntrospector | `:controllers` | Actions, filters, strong params, render paths |
90
+ | ControllerIntrospector | `:controllers` | Actions, filters, strong params, concerns, `rescue_from` handlers, rate limits |
91
91
  | ViewIntrospector | `:views` | View files, layouts, partials |
92
- | ViewTemplateIntrospector | `:view_templates` | Template content with ivars, Turbo frames, Stimulus refs |
92
+ | 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 |
93
93
 
94
94
  ### Models & Data
95
95
 
96
96
  | Introspector | Key | What it extracts |
97
97
  |:-------------|:----|:-----------------|
98
- | MigrationIntrospector | `:migrations` | Migration files, versions, reversibility. `recent` and `pending` entries are `{ version:, name: }`, and the name is the migration class (`CreatePosts`) in both tiers |
98
+ | 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 |
99
99
  | SeedsIntrospector | `:seeds` | Seed file analysis |
100
- | DatabaseStatsIntrospector | `:database_stats` | Table sizes, row counts, index stats |
100
+ | DatabaseStatsIntrospector | `:database_stats` | Approximate row counts per table, plus dead rows on PostgreSQL |
101
101
  | MultiDatabaseIntrospector | `:multi_database` | Multi-database configuration |
102
102
 
103
103
  ### Frontend
@@ -114,14 +114,14 @@ end
114
114
 
115
115
  | Introspector | Key | What it extracts |
116
116
  |:-------------|:----|:-----------------|
117
- | ConfigIntrospector | `:config` | Database, cache, queue, Action Cable config |
117
+ | ConfigIntrospector | `:config` | Cache store, session store, time zone, queue adapter, mailer settings, middleware stack, initializers, `Current` attributes, error monitoring |
118
118
  | GemIntrospector | `:gems` | Notable gems with versions and categories |
119
- | ConventionIntrospector | `:conventions` | Auth patterns, flash messages, test patterns |
119
+ | ConventionIntrospector | `:conventions` | Architecture markers (service objects, Hotwire, GraphQL and others), model patterns (STI, soft delete, state machines and others), directory structure, config files |
120
120
  | I18nIntrospector | `:i18n` | Locale files, translation keys |
121
121
  | 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 |
122
122
  | EngineIntrospector | `:engines` | Mounted engines |
123
123
  | 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 |
124
- | DevopsIntrospector | `:devops` | Dockerfile, CI config, deployment |
124
+ | DevOpsIntrospector | `:devops` | Puma config, Procfile, health check, Dockerfile, deployment tool |
125
125
 
126
126
  ### Jobs & Services
127
127
 
@@ -159,7 +159,7 @@ These introspectors map directly onto the internal RAILS_NERVOUS_SYSTEM checklis
159
159
  | Introspector | Key | Nervous-system § | What it extracts |
160
160
  |:-------------|:----|:---|:-----------------|
161
161
  | InitializerIntrospector | `:initializers` | §2 | `Rails.application.initializers` graph: name, owner, `before:`/`after:` edges, block `source_location`, per-file `config/initializers/*.rb` summary |
162
- | 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`) |
162
+ | 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`) |
163
163
  | 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 |
164
164
  | 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 |
165
165
  | 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) |
@@ -183,9 +183,9 @@ It runs a single-pass Dispatcher that walks the AST once and feeds events to all
183
183
  | ValidationsListener | `validates`, `validates_*_of`, custom `validate :method`, under the options of an enclosing `with_options` block |
184
184
  | ScopesListener | `scope :name, -> { ... }`, `lambda { ... }` and the block form |
185
185
  | EnumsListener | Rails 7+ and legacy enum syntax, prefix/suffix options |
186
- | CallbacksListener | All AR callback types including `around_*`, `after_touch`, `after_initialize` and `after_find`; `after_commit` with `on:` resolution; a callback object by its constant, a block as `[inline_block]`; `if:`/`unless:` kept as the source wrote them, and the options of an enclosing `with_options` block |
186
+ | 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 |
187
187
  | 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 |
188
- | 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 one in `class_methods do`; `include_initialize: true` adds the constructor a caller reports on its own |
188
+ | 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 |
189
189
  | 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 |
190
190
 
191
191
  ### The targeted-walk listeners
@@ -200,7 +200,7 @@ Passed to `SourceIntrospector.walk(path, key => Listener)` when a specific file
200
200
  | 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 |
201
201
  | 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` |
202
202
  | ChainedCallListener | Calls on a receiver: `ChainedCallListener.new(:includes)`, or `receiver: :inflect` to pin the receiver. Reports the receiver name |
203
- | 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, def or `Foo.class_eval`; 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 |
203
+ | 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 |
204
204
  | ClassDefinitionListener | Class definitions with their superclass and the nesting a bare superclass is read in |
205
205
  | 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` |
206
206
  | ComponentStructureListener | ViewComponent and Phlex structure: `renders_one`/`renders_many`, slot methods, hash/array constant tables, `case @ivar` variant branching, `CONST[@ivar]` indexing |
@@ -210,11 +210,11 @@ Passed to `SourceIntrospector.walk(path, key => Listener)` when a specific file
210
210
  | AutoloadIgnoreListener | The lib subdirectories `autoload_lib(ignore:)` and `autoload_lib_once(ignore:)` keep out of autoloading, as `lib/<name>`. Literal strings and symbols only |
211
211
  | 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 |
212
212
  | 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 |
213
- | FixturePathsListener | Fixture directories a test helper sets: `fixture_paths =`/`<<`/`+=`/`push` and the older `fixture_path =`, on `self`, `config`, a constant or no receiver, 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` |
214
- | 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` and no paths when it cannot read them), and each `FactoryBot.find_definitions`/`FactoryBot.reload` as `{ load: }`, in source order; `::FactoryBot` counts |
213
+ | 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` |
214
+ | 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 |
215
215
  | 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 |
216
216
  | 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 |
217
- | SchemaDslListener | `schema.rb`: `create_table`, `t.string`, `t.column`, `t.index`, `add_foreign_key`, `create_enum`, `create_view`, `create_virtual_table`, and the comment the dumper writes for a table it could not describe |
217
+ | 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 |
218
218
  | MigrationDslListener | Migration DSL: `create_table`, `add_column`, `add_index`, `add_reference`, and friends |
219
219
  | MigrationReplayListener | What a replay needs beside the DSL: `def down`, `down`/`revert` blocks, and `t.timestamps` |
220
220
  | 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/...` |
@@ -288,6 +288,14 @@ same wherever it is asked. Those live as their own modules under
288
288
  | `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 |
289
289
  | `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 |
290
290
  | `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 |
291
+ | `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 |
292
+ | `StructureSqlReader` | A `structure.sql` dump (PostgreSQL, MySQL or SQLite) as the same table shape the other static schema sources produce |
293
+ | `MigrationReplay` | The tables of an app with no schema dump, replayed from its migrations in the dump readers' shape |
294
+ | `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 |
295
+ | `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 |
296
+ | `SqliteVirtualTables` | SQLite's virtual tables and their shadow tables, read from `sqlite_master`. The schema and database stats introspectors ask it |
297
+ | `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 |
298
+ | `NodeSource` | A node's source with the bodies of the heredocs it opens, which Prism's `slice` leaves out |
291
299
 
292
300
  ### Confidence tagging
293
301
 
@@ -305,7 +313,7 @@ has_many :posts, class_name: name → [INFERRED] (name is a variable)
305
313
 
306
314
  Thread-safe parse cache using `Concurrent::Map`:
307
315
 
308
- - Keyed by: file path + SHA256 content hash + mtime, so a changed file is parsed again
316
+ - Keyed by: the SHA256 hash of the file's content, so a changed file is parsed again
309
317
  - A file whose stat (mtime, size, inode) matches the last read answers without reading or hashing it, but only once that file was already two seconds older than the read, so a same-size rewrite within one mtime tick is still read
310
318
  - Bounded at 500 parses; a recorded stat is dropped with its parse
311
319
  - Shared by all AST-based introspectors
@@ -325,7 +333,7 @@ Introspection results are cached at three levels:
325
333
  2. **AST cache** - Per-file parse results, invalidated by file content change (SHA256), with a stat shortcut for a file older than the read
326
334
  3. **Run cache** - File lists, stats and directory answers for one introspection run, dropped when it ends
327
335
 
328
- The **Fingerprinter** computes a composite SHA256 from all watched directories (`app/`, `config/`, `db/`, `lib/`, `rakelib/`, `test/`, `spec/`, `Gemfile.lock`, `config.ru`, the Rakefile). When the fingerprint changes, the introspection cache is invalidated even if TTL hasn't expired.
336
+ 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.
329
337
 
330
338
  **Live Reload** watches these directories and calls `reset_all_caches!` when changes are detected, then notifies connected MCP clients via `notify_resources_list_changed`.
331
339
 
data/docs/QUICKSTART.md CHANGED
@@ -22,13 +22,21 @@ rails generate rails_ai_context:install
22
22
 
23
23
  The generator asks two questions:
24
24
 
25
- 1. **Which AI tools do you use?** - Claude Code, Cursor, GitHub Copilot, OpenCode, Codex CLI, or all
25
+ 1. **Which AI tools do you use?** - Claude Code, Cursor, GitHub Copilot, OpenCode, Codex CLI, or all. There is no "none" choice: an empty answer, or one with no valid number, selects all five.
26
26
  2. **What should rails-ai-context write?** - MCP config + context files (default), context files only (CLI mode), or MCP config only
27
27
 
28
28
  The third answer is MCP-only: the server and the CLI answer in full, and your
29
29
  own `CLAUDE.md`, `AGENTS.md` and rules files are left alone. Non-interactively
30
30
  that is `--mcp-only`.
31
31
 
32
+ Three more prompts appear only when they apply:
33
+
34
+ - **Remove their generated files?** - on a re-run that drops a tool you picked before. Default: keep them. A yes removes only what the gem generated, never a rule file or a line you wrote.
35
+ - **Install a pre-commit hook that validates Rails references?** - in a git repo that has no pre-commit hook yet. Default: no.
36
+ - **Delete them?** - in a terminal, when rule files left over from before v5.0.0 are found. Default: no.
37
+
38
+ `--defaults` skips every prompt and takes each default: all five tools, keep the files, MCP config + context files, no hook, keep legacy files. Every step is listed in [What the install generator does](GUIDE.md#what-the-install-generator-does).
39
+
32
40
  That's it. Your AI tool now has live access to your schema, models, routes, controllers, views, and conventions.
33
41
 
34
42
  > [!TIP]
@@ -50,6 +58,8 @@ rails-ai-context init
50
58
  rails-ai-context serve
51
59
  ```
52
60
 
61
+ `init` asks the same two questions, and the cleanup and legacy-file prompts when they apply. It has no pre-commit hook prompt and no `--defaults` flag, and it does not create `config/initializers/rails_ai_context.rb`.
62
+
53
63
  Works with every Ruby version manager (rbenv, rvm, asdf, mise, chruby, system). [Learn more about standalone mode →](STANDALONE.md)
54
64
 
55
65
  ## What just happened?
@@ -59,9 +69,10 @@ The install generator created:
59
69
  | File | Purpose |
60
70
  |:-----|:--------|
61
71
  | `.mcp.json` / `.cursor/mcp.json` / `.vscode/mcp.json` / `opencode.json` / `.codex/config.toml` | MCP auto-discovery - your AI tool detects these on project open |
62
- | `CLAUDE.md` / `.cursor/rules/` / `.cursorrules` / `.github/instructions/` / `AGENTS.md` | Static context rules your AI reads |
72
+ | `CLAUDE.md` + `.claude/rules/` / `.cursor/rules/` + `.cursorrules` / `.github/copilot-instructions.md` + `.github/instructions/` / `AGENTS.md` | Static context rules your AI reads |
73
+ | `.ai-context.json` | The same context as JSON (added to `.gitignore`) |
63
74
  | `config/initializers/rails_ai_context.rb` | All configuration options |
64
- | `.rails-ai-context.yml` | Config for standalone mode |
75
+ | `.rails-ai-context.yml` | Your tool selection, and the config file for standalone mode |
65
76
 
66
77
  ## Verify it works
67
78
 
data/docs/RECIPES.md CHANGED
@@ -49,15 +49,18 @@
49
49
  ```
50
50
  ## Table: users
51
51
 
52
+ **Models:** User
53
+ **Primary key:** id
54
+
52
55
  | Column | Type | Null | Default |
53
56
  |--------|------|------|---------|
54
- | id | integer | NO | |
55
- | email | string | NO | [unique] |
56
- | subscription_status | string | yes | "free" |
57
- | created_at | datetime | NO | |
57
+ | id | integer | **NO** | |
58
+ | email | string | **NO** | |
59
+ | subscription_status | string | yes | free |
60
+ | created_at | datetime | **NO** | |
58
61
 
59
62
  ### Indexes
60
- - index_users_on_email (unique)
63
+ - `index_users_on_email` on (email) (unique)
61
64
  ```
62
65
 
63
66
  </details>
@@ -66,7 +69,7 @@ AI sees `subscription_status` already exists - asks before proceeding instead of
66
69
 
67
70
  ```
68
71
  → rails_get_model_details(model: "User")
69
- → rails_migration_advisor(action: "add_column", table: "users", columns: "subscription_tier:string")
72
+ → rails_migration_advisor(action: "add_column", table: "users", column: "subscription_tier", type: "string")
70
73
  ```
71
74
 
72
75
  **Without ground truth:** AI writes a migration blindly, possibly duplicating a column or missing an index.
@@ -88,7 +91,7 @@ rails_get_schema(table: "posts")
88
91
  # → Actual column names, types, constraints
89
92
 
90
93
  rails_diagnose(error: "ActiveRecord::RecordInvalid", file: "app/controllers/posts_controller.rb")
91
- # → Classification + context + git blame + log correlation
94
+ # → Classification + context + recent git log + log correlation
92
95
  ```
93
96
 
94
97
  AI sees the full picture: the parent controller's `authenticate_user!` filter, the actual strong params whitelist, and the real column names. No guessing.
@@ -110,23 +113,24 @@ AI sees the full picture: the parent controller's `authenticate_user!` filter, t
110
113
  # Feature Analysis: notifications
111
114
 
112
115
  ## Models (1)
116
+
113
117
  ### Notification
114
- Table: notifications
115
- Columns: user_id:bigint, title:string, read_at:datetime
116
- Associations: belongs_to :user
118
+ **Table:** `notifications`
119
+ **Columns:** user_id:bigint, title:string, read_at:datetime
120
+ **Associations:** belongs_to :user
117
121
 
118
122
  ## Controllers (1)
119
- ### NotificationsController
120
- Actions: index, mark_read
121
- Filters: before_action authenticate_user!
122
123
 
123
- ## Jobs (1)
124
- ### NotificationBroadcastJob
125
- Queue: default | Retries: 3
124
+ ### NotificationsController
125
+ - **Actions:** index, mark_read
126
+ - **Filters:** before authenticate_user!
126
127
 
127
128
  ## Routes (2)
128
- GET /notifications → notifications#index
129
- POST /notifications/:id → notifications#mark_read
129
+ - `GET` `/notifications` → notifications#index `notifications_path`
130
+ - `POST` `/notifications/:id/mark_read` → notifications#mark_read `mark_read_notification_path(@record)`
131
+
132
+ ## Jobs (1)
133
+ - `app/jobs/notification_broadcast_job.rb` (queue: default, retry_on StandardError, attempts: 3, perform(notification_id))
130
134
  ```
131
135
 
132
136
  </details>
@@ -157,31 +161,37 @@ AI scaffolds the feature matching your app's actual patterns - not generic Rails
157
161
  <details open>
158
162
  <summary>Example output</summary>
159
163
 
160
- ```
161
- # Trace: publishable?
164
+ ````
165
+ # Trace: `publishable?`
162
166
 
163
167
  ## Definition
164
- app/models/post.rb:45 in Post
168
+ **app/models/post.rb:45** in `class Post`
169
+ ```ruby
165
170
  def publishable?
166
171
  title.present? && body.present? && approved?
167
172
  end
173
+ ```
168
174
 
169
- ## Called from (4 sites)
170
-
171
- **Controllers** (2)
172
- app/controllers/posts_controller.rb:12 before_action :ensure_publishable
173
- app/controllers/admin/posts_controller.rb:8 if @post.publishable?
175
+ ## Calls internally
176
+ - `title`
177
+ - `body`
178
+ - `approved?`
174
179
 
175
- **Views** (1)
176
- app/views/posts/show.html.erb:8 <% if @post.publishable? %>
180
+ ## Called from (3 sites)
181
+ ### app/controllers/admin/posts_controller.rb (Controller)
182
+ 8: render :draft unless @post.publishable?
183
+ ### app/controllers/posts_controller.rb (Controller)
184
+ 12: redirect_to @post unless @post.publishable?
185
+ ### app/views/posts/show.html.erb (View)
186
+ 8: <% if @post.publishable? %>
177
187
 
178
- **Tests** (1)
179
- spec/models/post_spec.rb:92 expect(post.publishable?).to be true
180
- ```
188
+ ## Tested by (1 reference)
189
+ - `spec/models/post_spec.rb` (1 reference)
190
+ ````
181
191
 
182
192
  </details>
183
193
 
184
- One call. Definition + source + every caller grouped by type + tests. **Replaces 4-5 sequential file reads.**
194
+ One call. Definition + source + every caller grouped by file and labelled by type + tests. **Replaces 4-5 sequential file reads.**
185
195
 
186
196
  ---
187
197
 
@@ -196,7 +206,7 @@ rails_onboard(detail: "full")
196
206
  rails_dependency_graph(format: "mermaid")
197
207
  # → Visual model relationship graph
198
208
 
199
- rails_get_gems(detail: "full")
209
+ rails_get_gems()
200
210
  # → Every notable gem with version, category, and config location
201
211
  ```
202
212
 
@@ -226,7 +236,7 @@ AI generates tests that actually run, using your test helper setup, your factory
226
236
  **Ask your AI:** "The dashboard view is broken"
227
237
 
228
238
  ```bash
229
- rails_get_view(controller: "dashboard", action: "index")
239
+ rails_get_view(path: "dashboard/index.html.erb")
230
240
  # → Template source with ivars, Turbo frames, Stimulus controllers, partial locals
231
241
 
232
242
  rails_get_partial_interface(partial: "dashboard/stats_card")
@@ -243,10 +253,10 @@ rails_get_stimulus(controller: "chart")
243
253
  **Ask your AI:** "Review the changes in this branch"
244
254
 
245
255
  ```bash
246
- rails_review_changes(ref: "HEAD~3..HEAD")
256
+ rails_review_changes(ref: "HEAD~3")
247
257
  # → Per-file context + structural warnings
248
258
 
249
- rails_validate(files: "app/controllers/users_controller.rb,app/models/user.rb", level: "security")
259
+ rails_validate(files: ["app/controllers/users_controller.rb", "app/models/user.rb"], level: "rails")
250
260
  # → Syntax + semantic + Brakeman scan
251
261
 
252
262
  rails_performance_check()
@@ -279,7 +289,7 @@ rails_query(sql: "SELECT * FROM orders JOIN users ON ...", explain: true)
279
289
  **Ask your AI:** "What does the Searchable concern do?"
280
290
 
281
291
  ```bash
282
- rails_get_concern(concern: "Searchable")
292
+ rails_get_concern(name: "Searchable")
283
293
  # → Methods (including class_methods block), source code, which models include it
284
294
 
285
295
  rails_search_code(pattern: "include Searchable", match_type: "any")
@@ -310,7 +320,7 @@ rails_get_frontend_stack()
310
320
  rails_runtime_info(detail: "full")
311
321
  # → DB pool stats, table sizes, pending migrations, cache stats, queue depth
312
322
 
313
- rails_read_logs(level: "error", lines: 100)
323
+ rails_read_logs(level: "ERROR", lines: 100)
314
324
  # → Recent errors with sensitive data redacted
315
325
 
316
326
  rails_security_scan()
@@ -329,8 +339,8 @@ rails_get_schema(table: "users")
329
339
  rails_get_model_details(model: "User")
330
340
 
331
341
  # Follow-up: "What context do I have so far?"
332
- rails_session_context()
333
- # → Lists all prior tool calls with params and summaries
342
+ rails_session_context(action: "status")
343
+ # → Lists all prior tool calls with params and when each ran
334
344
  ```
335
345
 
336
346
  This prevents redundant queries and helps AI maintain conversation context.
data/docs/SECURITY.md CHANGED
@@ -49,11 +49,12 @@ flowchart LR
49
49
 
50
50
  Before any query reaches the database:
51
51
 
52
- - Strips comments: block (`/* */`), line (`--`), MySQL (`#` at line start)
52
+ - Strips comments: block (`/* */`), line (`--`), and on MySQL `#`. A comment marker inside a quoted string or identifier is data and stays. The query that runs is this stripped text, never the raw input, so nothing the checks did not read reaches the database
53
53
  - **Blocks write keywords**: INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, CREATE, GRANT, REVOKE, SET, COPY, MERGE, REPLACE
54
54
  - **Blocks lock clauses**: FOR UPDATE, FOR SHARE, FOR NO KEY UPDATE
55
55
  - **Blocks dangerous SHOW**: GRANTS, PROCESSLIST, BINLOG, SLAVE, MASTER, REPLICAS
56
- - **Blocks SELECT INTO**: prevents table creation via SELECT
56
+ - **Blocks SELECT INTO**: prevents table creation via SELECT, and `INTO OUTFILE` / `INTO DUMPFILE` writes to disk
57
+ - **Blocks file and network functions**: `pg_read_file`, `pg_read_binary_file`, `pg_ls_dir`, `pg_stat_file`, `lo_import`, `lo_export`, `dblink*`, `LOAD DATA`, `LOAD_FILE`, `load_extension` and their relatives, checked before and after comment stripping
57
58
  - **Blocks multi-statements**: multiple semicolons
58
59
  - **Blocks injection patterns**: OR 1=1, OR true, OR ''='', UNION SELECT
59
60
  - **Allows only**: SELECT, WITH, SHOW, EXPLAIN, DESCRIBE, DESC
@@ -68,7 +69,7 @@ After validation, the query runs inside a transaction:
68
69
  | MySQL | `SET TRANSACTION READ ONLY` + `MAX_EXECUTION_TIME` hint |
69
70
  | SQLite | A read-only connection in a child process, killed at timeout |
70
71
 
71
- All queries execute inside a transaction, then rollback (even if they could write, they can't).
72
+ On PostgreSQL and MySQL the query executes inside a transaction, then rolls back (even if it could write, it can't). Any other adapter has no database-level guard: the query runs with Layer 1 validation and the row limit only.
72
73
 
73
74
  A SQLite query runs in-process under `PRAGMA query_only = ON`, with no time
74
75
  limit, for an in-memory database, on a platform without `fork`, or when it
@@ -87,7 +88,8 @@ lock in WAL mode does not block it.
87
88
 
88
89
  - Default: 100 rows
89
90
  - Configurable: `config.query_row_limit` (hard cap: 1000)
90
- - Applied as `LIMIT` clause appended to query
91
+ - Applied as a `LIMIT` clause appended to the query. A `LIMIT` or `FETCH FIRST` that ends the query is lowered to the cap, and one inside a subquery is left as written
92
+ - The rows that come back are cut to the cap too, so the answer holds it whatever the database made of the text
91
93
 
92
94
  ### Layer 4 - Sensitive column rejection
93
95
 
@@ -95,7 +97,7 @@ A query that names a sensitive column is **rejected before execution**, not reda
95
97
 
96
98
  **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`
97
99
 
98
- 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.
100
+ Those are the defaults of `config.query_redacted_columns`. A fixed built-in list is checked as well, which adds `password_reset_token`, `remember_token`, `secret` and `private_key`. Matching is by name, case-insensitive and word-bounded, against both lists. `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.
99
101
 
100
102
  If one of your own columns merely looks sensitive (an `oauth_applications.secret`, say), exempt it by name:
101
103
 
@@ -103,6 +105,11 @@ If one of your own columns merely looks sensitive (an `oauth_applications.secret
103
105
  config.query_allowed_columns = %w[secret]
104
106
  ```
105
107
 
108
+ Results are redacted as well: a returned column comes back as `[FILTERED]` when
109
+ its name is in `config.query_redacted_columns`, contains `password`, `secret` or
110
+ `token`, or ends in `key`, `digest` or `hash`. `SHOW`, `DESCRIBE` and `EXPLAIN`
111
+ output is not redacted.
112
+
106
113
  The exemption covers the results too: an allowed name comes back unredacted. A
107
114
  column declared with `encrypts` stays `[FILTERED]` either way.
108
115
 
@@ -120,13 +127,15 @@ The `rails_search_code` and file-reading tools block access to sensitive files:
120
127
  ### Default patterns
121
128
 
122
129
  ```text
123
- .env .env.*
130
+ .env .env.* *.env .envrc
124
131
  config/master.key
125
132
  config/credentials.yml.enc config/credentials/*.yml.enc
126
- config/database.yml config/secrets.yml
133
+ config/database.yml config/secrets*.yml config/secrets*.yml.enc
134
+ config/application.yml
135
+ config/settings.local.yml config/settings/*.local.yml
127
136
  config/cable.yml config/storage.yml
128
137
  config/mongoid.yml config/redis.yml
129
- *.pem *.key *.p12 *.pfx *.jks *.keystore
138
+ *.pem *.key *.p8 *.p12 *.pfx *.jks *.keystore
130
139
  **/id_rsa **/id_ed25519 **/id_ecdsa **/id_dsa
131
140
  .ssh/* .aws/credentials .aws/config .netrc .pgpass .my.cnf
132
141
  ```
@@ -164,7 +173,8 @@ All file-reading operations validate paths against `Rails.root`:
164
173
 
165
174
  ```ruby
166
175
  real_path = File.realpath(requested_path)
167
- raise unless real_path.start_with?(Rails.root.to_s)
176
+ root = File.realpath(Rails.root.to_s)
177
+ raise unless real_path == root || real_path.start_with?(root + File::SEPARATOR)
168
178
  ```
169
179
 
170
180
  The VFS (`rails-ai-context://views/{path}`) applies the same protection for view template reads.
@@ -216,7 +226,7 @@ Search tools use array-based command execution (never shell strings):
216
226
 
217
227
  ```ruby
218
228
  # Safe: array form
219
- Open3.capture2("rg", "--no-heading", pattern, "--", directory)
229
+ Open3.capture3("rg", "--no-heading", "--", pattern, directory)
220
230
 
221
231
  # Pattern injection prevented by -- separator
222
232
  ```
@@ -227,7 +237,7 @@ File type parameters accept only alphanumeric characters.
227
237
 
228
238
  ## Regex injection prevention
229
239
 
230
- On Ruby 3.2 and newer, user-supplied regex patterns have a 1-second timeout:
240
+ On Ruby 3.2 and newer, user-supplied regex patterns have a 1-second timeout (2 seconds in the Ruby search fallback):
231
241
 
232
242
  ```ruby
233
243
  Regexp.new(pattern, timeout: 1)
@@ -319,7 +329,7 @@ The McpController uses thread-safe transport initialization with mutex synchroni
319
329
  ## Credential handling
320
330
 
321
331
  - `rails_get_env` returns credential **keys**, never values
322
- - Environment variable values are not exposed
332
+ - The process's environment variable values are never read. A default written in code, and a placeholder from `.env.example`, `.env.sample` or `.env.template`, is shown after redaction
323
333
  - `config/credentials/*.yml.enc` is in the sensitive patterns list
324
334
 
325
335
  ---
@@ -328,7 +338,7 @@ The McpController uses thread-safe transport initialization with mutex synchroni
328
338
 
329
339
  Email crisjosephnahine@gmail.com. Response within 48 hours.
330
340
 
331
- Supported versions: 4.0.x and later (4.2.1+ includes security hardening). See the repo root `SECURITY.md` for the full policy.
341
+ Supported versions: only the latest 5.x release gets security fixes. See the repo root `SECURITY.md` for the full policy.
332
342
 
333
343
  ---
334
344
 
data/docs/SETUP.md CHANGED
@@ -31,18 +31,12 @@ flowchart TD
31
31
  A[Start] --> B{Can you modify\nthe Gemfile?}
32
32
  B -->|Yes| C[In-Gemfile]
33
33
  B -->|No| D[Standalone]
34
- C --> E{Need MCP\nserver?}
35
- D --> E
36
- E -->|"Yes - best experience"| F[MCP Mode]
37
- E -->|"No - terminal only"| G[CLI Mode]
38
- F --> H{Which AI tools?}
39
- G --> H
40
- H --> I["Claude Code"]
41
- H --> J["Cursor"]
42
- H --> K["GitHub Copilot"]
43
- H --> L["OpenCode"]
44
- H --> M["Codex CLI"]
45
- H --> N["All of them"]
34
+ C --> H{Which AI tools?}
35
+ D --> H
36
+ H -->|"Claude Code, Cursor, GitHub Copilot,\nOpenCode, Codex CLI, or all"| E{What should\nit write?}
37
+ E -->|"1 - default"| F[MCP config +\ncontext files]
38
+ E -->|"2 - no MCP server"| G[Context files only\nCLI mode]
39
+ E -->|"3 - keep your own files"| O[MCP config only]
46
40
 
47
41
  style C fill:#27ae60,stroke:#1e8449,color:#fff
48
42
  style D fill:#3498db,stroke:#2980b9,color:#fff
@@ -61,10 +55,11 @@ rails generate rails_ai_context:install # Select "Claude Code"
61
55
  This creates:
62
56
  - `.mcp.json` - MCP auto-discovery config (auto-detected on project open)
63
57
  - `CLAUDE.md` - Root context file
64
- - `.claude/rules/rails-schema.md` - Schema rules (loaded when editing `db/` files)
58
+ - `.claude/rules/rails-schema.md` - Schema rules (loaded when editing the schema dump or `db/migrate/` files)
65
59
  - `.claude/rules/rails-models.md` - Model rules (loaded when editing `app/models/`)
66
60
  - `.claude/rules/rails-context.md` - General context rules (always loaded)
67
61
  - `.claude/rules/rails-mcp-tools.md` - Tool reference (always loaded)
62
+ - `.claude/rules/rails-components.md` - Component rules (loaded when editing `app/components/` or `app/views/components/`, written only when the app has view components)
68
63
 
69
64
  Keeping your own `CLAUDE.md`? Add `--mcp-only` and only `.mcp.json` is
70
65
  written; every context file is left alone. See
@@ -79,12 +74,14 @@ If you need to configure manually, create `.mcp.json`:
79
74
  "mcpServers": {
80
75
  "rails-ai-context": {
81
76
  "command": "bundle",
82
- "args": ["exec", "rails", "ai:serve"]
77
+ "args": ["exec", "rails-ai-context", "serve"]
83
78
  }
84
79
  }
85
80
  }
86
81
  ```
87
82
 
83
+ This is what the generator writes for an in-Gemfile install. A standalone install has no `bundle exec`: the command is `rails-ai-context` and the args are `["serve"]`. The same goes for the other tools below.
84
+
88
85
  ### Split rules with `paths:` frontmatter
89
86
 
90
87
  Claude Code loads `.claude/rules/` files conditionally based on YAML frontmatter:
@@ -92,12 +89,11 @@ Claude Code loads `.claude/rules/` files conditionally based on YAML frontmatter
92
89
  ```yaml
93
90
  ---
94
91
  paths:
95
- - db/**
96
- - app/models/**
92
+ - "app/models/**/*.rb"
97
93
  ---
98
94
  ```
99
95
 
100
- Schema and model rules use this to only load when relevant files are being edited.
96
+ Schema, model and component rules use this to only load when relevant files are being edited.
101
97
 
102
98
  ---
103
99
 
@@ -115,7 +111,7 @@ This creates:
115
111
  - `.cursor/rules/rails-models.mdc` - Model rules (Type 2: glob `app/models/**/*.rb`)
116
112
  - `.cursor/rules/rails-controllers.mdc` - Controller rules (Type 2: glob `app/controllers/**/*.rb`)
117
113
  - `.cursor/rules/rails-mcp-tools.mdc` - Tool reference (Type 3: agent-requested)
118
- - `.cursorrules` - **legacy single-file fallback** at the project root. Cursor's chat agent doesn't always detect `.cursor/rules/*.mdc` (reported in v5.9.0 release QA); this file is parsed verbatim by every Cursor build and contains the same compact project context as `CLAUDE.md`.
114
+ - `.cursorrules` - **legacy single-file fallback** at the project root. Cursor's chat agent doesn't always detect `.cursor/rules/*.mdc` (reported in v5.9.0 release QA); this file is parsed verbatim by every Cursor build and contains the same compact project context `CLAUDE.md` gets in the default compact mode. It stays compact when `context_mode` is `:full`.
119
115
 
120
116
  ### Manual MCP config
121
117
 
@@ -126,7 +122,7 @@ Create `.cursor/mcp.json`:
126
122
  "mcpServers": {
127
123
  "rails-ai-context": {
128
124
  "command": "bundle",
129
- "args": ["exec", "rails", "ai:serve"]
125
+ "args": ["exec", "rails-ai-context", "serve"]
130
126
  }
131
127
  }
132
128
  }
@@ -138,7 +134,7 @@ The MCP tools rule uses `alwaysApply: false` with a descriptive `description:` f
138
134
 
139
135
  ```markdown
140
136
  ---
141
- description: 45 MCP tools for Rails introspection - schema, models, routes, controllers, views
137
+ description: "Rails MCP tools reference - 45 tools for schema, models, routes, controllers, search, testing, and more"
142
138
  alwaysApply: false
143
139
  ---
144
140
  ```
@@ -170,7 +166,7 @@ Create `.vscode/mcp.json` (note: `servers` key, not `mcpServers`):
170
166
  "servers": {
171
167
  "rails-ai-context": {
172
168
  "command": "bundle",
173
- "args": ["exec", "rails", "ai:serve"]
169
+ "args": ["exec", "rails-ai-context", "serve"]
174
170
  }
175
171
  }
176
172
  }
@@ -178,12 +174,13 @@ Create `.vscode/mcp.json` (note: `servers` key, not `mcpServers`):
178
174
 
179
175
  ### Frontmatter for agent discovery
180
176
 
181
- Copilot instruction files include `name:` and `description:` YAML frontmatter:
177
+ Copilot instruction files include `applyTo:`, `name:` and `description:` YAML frontmatter:
182
178
 
183
179
  ```markdown
184
180
  ---
185
- name: Rails Models
186
- description: Model associations, validations, and schema for this Rails app
181
+ applyTo: "app/models/**/*.rb"
182
+ name: "Rails Models Reference"
183
+ description: "ActiveRecord models - associations, validations, scopes, enums"
187
184
  ---
188
185
  ```
189
186
 
@@ -212,7 +209,7 @@ Create `opencode.json`:
212
209
  "mcp": {
213
210
  "rails-ai-context": {
214
211
  "type": "local",
215
- "command": ["bundle", "exec", "rails", "ai:serve"]
212
+ "command": ["bundle", "exec", "rails-ai-context", "serve"]
216
213
  }
217
214
  }
218
215
  }
@@ -241,7 +238,7 @@ Create `.codex/config.toml`:
241
238
  ```toml
242
239
  [mcp_servers.rails-ai-context]
243
240
  command = "bundle"
244
- args = ["exec", "rails", "ai:serve"]
241
+ args = ["exec", "rails-ai-context", "serve"]
245
242
 
246
243
  [mcp_servers.rails-ai-context.env]
247
244
  PATH = "/Users/you/.rbenv/shims:/usr/local/bin:/usr/bin"