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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +138 -0
- data/CONTEXT.md +10 -8
- data/CONTRIBUTING.md +2 -2
- data/README.md +14 -11
- data/SECURITY.md +4 -4
- data/docs/ARCHITECTURE.md +11 -10
- data/docs/CLI.md +33 -7
- data/docs/COMPATIBILITY.md +69 -32
- data/docs/CONFIGURATION.md +10 -8
- data/docs/CUSTOM_TOOLS.md +8 -8
- data/docs/FAQ.md +6 -5
- data/docs/GUIDE.md +154 -80
- data/docs/INTROSPECTORS.md +25 -17
- data/docs/QUICKSTART.md +14 -3
- data/docs/RECIPES.md +50 -40
- data/docs/SECURITY.md +23 -13
- data/docs/SETUP.md +23 -26
- data/docs/STANDALONE.md +7 -3
- data/docs/TOOLS.md +79 -45
- data/docs/TROUBLESHOOTING.md +12 -11
- data/exe/rails-ai-context +8 -4
- data/lib/generators/rails_ai_context/install/install_generator.rb +1 -1
- data/lib/rails_ai_context/configuration.rb +2 -2
- data/lib/rails_ai_context/database_yml.rb +52 -2
- data/lib/rails_ai_context/install/ai_tool.rb +11 -2
- data/lib/rails_ai_context/install/cleanup.rb +69 -23
- data/lib/rails_ai_context/install/program.rb +4 -1
- data/lib/rails_ai_context/install/selection_record.rb +20 -2
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +30 -9
- data/lib/rails_ai_context/introspectors/model_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/pg_naming.rb +133 -0
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +0 -6
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +188 -51
- data/lib/rails_ai_context/introspectors/schema_reader.rb +98 -12
- data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +56 -33
- data/lib/rails_ai_context/payload.rb +25 -2
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +2 -2
- data/lib/rails_ai_context/server.rb +2 -2
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +12 -4
- data/lib/rails_ai_context/test_helper.rb +2 -0
- data/lib/rails_ai_context/tools/base_tool.rb +3 -1
- data/lib/rails_ai_context/tools/dependency_graph.rb +2 -2
- data/lib/rails_ai_context/tools/get_concern.rb +1 -1
- data/lib/rails_ai_context/tools/get_env.rb +1 -1
- data/lib/rails_ai_context/tools/get_gems.rb +2 -2
- data/lib/rails_ai_context/tools/get_model_details.rb +2 -0
- data/lib/rails_ai_context/tools/get_schema.rb +17 -14
- data/lib/rails_ai_context/tools/query.rb +107 -39
- data/lib/rails_ai_context/tools/search_code.rb +20 -16
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +1 -1
- metadata +2 -1
data/docs/INTROSPECTORS.md
CHANGED
|
@@ -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,
|
|
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,
|
|
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` |
|
|
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,
|
|
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` |
|
|
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` |
|
|
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` |
|
|
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
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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:
|
|
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/`
|
|
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` |
|
|
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 |
|
|
56
|
-
| subscription_status | string | yes |
|
|
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",
|
|
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
|
|
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
|
|
115
|
-
Columns
|
|
116
|
-
Associations
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
### NotificationsController
|
|
125
|
+
- **Actions:** index, mark_read
|
|
126
|
+
- **Filters:** before authenticate_user!
|
|
126
127
|
|
|
127
128
|
## Routes (2)
|
|
128
|
-
GET
|
|
129
|
-
POST
|
|
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
|
-
##
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
app/controllers/admin/posts_controller.rb:8 if @post.publishable?
|
|
175
|
+
## Calls internally
|
|
176
|
+
- `title`
|
|
177
|
+
- `body`
|
|
178
|
+
- `approved?`
|
|
174
179
|
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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: "
|
|
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(
|
|
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: "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
-
|
|
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:
|
|
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 -->
|
|
35
|
-
D -->
|
|
36
|
-
|
|
37
|
-
E -->|"
|
|
38
|
-
|
|
39
|
-
|
|
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", "
|
|
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
|
-
-
|
|
96
|
-
- app/models/**
|
|
92
|
+
- "app/models/**/*.rb"
|
|
97
93
|
---
|
|
98
94
|
```
|
|
99
95
|
|
|
100
|
-
Schema and
|
|
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
|
|
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", "
|
|
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:
|
|
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", "
|
|
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
|
-
|
|
186
|
-
|
|
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", "
|
|
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", "
|
|
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"
|