rails-ai-context 5.16.2 → 5.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -10
  3. data/AGENTS.md +15 -0
  4. data/CHANGELOG.md +61 -0
  5. data/CONTRIBUTING.md +1 -1
  6. data/app/controllers/rails_ai_context/mcp_controller.rb +8 -1
  7. data/docs/ARCHITECTURE.md +5 -5
  8. data/docs/CONFIGURATION.md +15 -4
  9. data/docs/INTROSPECTORS.md +4 -3
  10. data/docs/SECURITY.md +27 -16
  11. data/docs/agents/domain.md +38 -0
  12. data/docs/agents/issue-tracker.md +45 -0
  13. data/docs/agents/triage-labels.md +15 -0
  14. data/lib/rails_ai_context/configuration.rb +3 -1
  15. data/lib/rails_ai_context/detail_level.rb +40 -0
  16. data/lib/rails_ai_context/introspectors/convention_introspector.rb +14 -11
  17. data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
  18. data/lib/rails_ai_context/introspectors/gem_introspector.rb +0 -2
  19. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +11 -0
  20. data/lib/rails_ai_context/introspectors/performance_introspector.rb +5 -40
  21. data/lib/rails_ai_context/introspectors/schema_introspector.rb +85 -182
  22. data/lib/rails_ai_context/introspectors/schema_reader.rb +166 -0
  23. data/lib/rails_ai_context/introspectors/source_introspector.rb +2 -4
  24. data/lib/rails_ai_context/mcp_config_generator.rb +5 -12
  25. data/lib/rails_ai_context/safe_file.rb +14 -0
  26. data/lib/rails_ai_context/serializers/claude_serializer.rb +7 -9
  27. data/lib/rails_ai_context/serializers/context_file_serializer.rb +1 -8
  28. data/lib/rails_ai_context/serializers/context_mode_dispatch.rb +25 -0
  29. data/lib/rails_ai_context/serializers/copilot_serializer.rb +4 -13
  30. data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
  31. data/lib/rails_ai_context/serializers/opencode_serializer.rb +4 -13
  32. data/lib/rails_ai_context/serializers/section_marker_writer.rb +1 -6
  33. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -1
  34. data/lib/rails_ai_context/tools/analyze_feature.rb +1 -1
  35. data/lib/rails_ai_context/tools/diagnose.rb +2 -2
  36. data/lib/rails_ai_context/tools/generate_test.rb +1 -1
  37. data/lib/rails_ai_context/tools/get_callbacks.rb +2 -2
  38. data/lib/rails_ai_context/tools/get_concern.rb +2 -2
  39. data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -1
  40. data/lib/rails_ai_context/tools/get_helper_methods.rb +1 -1
  41. data/lib/rails_ai_context/tools/get_schema.rb +1 -1
  42. data/lib/rails_ai_context/tools/get_test_info.rb +1 -1
  43. data/lib/rails_ai_context/tools/get_view.rb +1 -1
  44. data/lib/rails_ai_context/tools/onboard.rb +3 -3
  45. data/lib/rails_ai_context/tools/performance_check.rb +3 -3
  46. data/lib/rails_ai_context/tools/query.rb +10 -7
  47. data/lib/rails_ai_context/tools/review_changes.rb +2 -2
  48. data/lib/rails_ai_context/tools/runtime_info.rb +4 -4
  49. data/lib/rails_ai_context/tools/search_code.rb +2 -2
  50. data/lib/rails_ai_context/tools/validate.rb +2 -864
  51. data/lib/rails_ai_context/tools/validate_semantics.rb +879 -0
  52. data/lib/rails_ai_context/version.rb +1 -1
  53. metadata +9 -2
  54. data/docs/superpowers/plans/2026-05-24-prism-ast-migration.md +0 -1218
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eea6e2b178ca5641f69ba9e78d20faa743e9639c706b2a8f847e60f854618b97
4
- data.tar.gz: d688b1ac2b70859c0f2cdfecfb731bf0dda802743924664967f15680b4e9042f
3
+ metadata.gz: ebb8d4e0ebed93183c0c019071ee63d097fb8c7495f551393991000aa5d06e7b
4
+ data.tar.gz: 2141cf0a281a173da2f0fa34927be843f93600ced5cdd67ccd6eb4aeef261ce0
5
5
  SHA512:
6
- metadata.gz: bb7f069772ba9a58014b905eb68629cf8024b767c7cd01f409fbcd4418f252e04b7d0618c39160bcc7f2364e4535240372da158d267284f721fb365409216699
7
- data.tar.gz: b22aac0130e6ee73829982f5dce526036c5e944ed2104f93664ec89868df7a1db26ea12412f5d3c6d73c7daa9a4421e2e61bf7054f67814fde12b9da70c3a206
6
+ metadata.gz: e1bdda115ee2c82a43af9f6389ad18204629539e5867db0ae36d8c4fe86f50547084315470ed5842adccbced095026e2a220f32626d1c1e43600b595d7224afb
7
+ data.tar.gz: 197fe45172ce6888fd89ee63ecc505f5e014f1b3c759a918a7244025958ce9d17801f4bf1a0ac3ce401a93063b0f3d75998b4ec485f6bdbe4df9449be333920d
data/.rubocop.yml CHANGED
@@ -11,13 +11,6 @@ AllCops:
11
11
  Style/Documentation:
12
12
  Enabled: false
13
13
 
14
- Metrics/MethodLength:
15
- Max: 25
16
-
17
- Metrics/ClassLength:
18
- Max: 200
19
-
20
- Metrics/BlockLength:
21
- Exclude:
22
- - "spec/**/*"
23
- - "*.gemspec"
14
+ # rubocop-rails-omakase disables the whole Metrics department. Setting Max on a
15
+ # disabled cop does not re-enable it, so size limits here would enforce nothing.
16
+ # Turning it on would need ~40 existing files brought under the limit first.
data/AGENTS.md ADDED
@@ -0,0 +1,15 @@
1
+ # rails-ai-context
2
+
3
+ ## Agent skills
4
+
5
+ ### Issue tracker
6
+
7
+ Issues live as GitHub issues on `crisnahine/rails-ai-context`, driven with the `gh` CLI. See `docs/agents/issue-tracker.md`.
8
+
9
+ ### Triage labels
10
+
11
+ The five canonical roles, each label string equal to its name (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
12
+
13
+ ### Domain docs
14
+
15
+ Single-context: `CONTEXT.md` and `docs/adr/` at the repo root. See `docs/agents/domain.md`.
data/CHANGELOG.md CHANGED
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [5.17.0] - 2026-08-09
9
+
10
+ ### Added
11
+
12
+ - **`config.query_allowed_columns`** exempts a column name from the built-in
13
+ sensitive list used by `rails_query`. The rejection message told you to
14
+ subtract from `query_redacted_columns`, which could never work: that list is
15
+ unioned with a frozen suffix list, so an app with its own
16
+ `oauth_applications.secret` had no way to query it.
17
+
18
+ ### Fixed
19
+
20
+ - **Documented security behaviour now matches the code.** The ReDoS timeout on
21
+ user-supplied regexes needs `Regexp.timeout`, which Ruby 3.1 does not have,
22
+ so `SECURITY.md` no longer promises it unconditionally on a version the gem
23
+ still supports. Layer 4 was documented as post-execution redaction; it has
24
+ rejected the query outright since 5.8.1, which is what the doc now says.
25
+ - **`config.introspectors = %i[source]` no longer appears to be supported.**
26
+ `SourceIntrospector` was listed in the introspector table but is
27
+ infrastructure, not a registered introspector, so configuring it raised
28
+ `ConfigurationError`.
29
+ - **Corrected documented defaults**, which had drifted: `sensitive_patterns` is
30
+ 27 patterns not 8, `excluded_middleware` 25 not 24, `excluded_filters` 5 not
31
+ 3, and the listener count is 21. `extra_app_paths` and
32
+ `instrumentation_include_arguments` were undocumented.
33
+ - **Secondary database dumps report their own generated columns.** Parsing
34
+ `db/queue_schema.rb` read generated columns from the primary `db/schema.rb`
35
+ instead of the dump being parsed.
36
+ - **Polymorphic foreign keys no longer always report a missing index.** The
37
+ compound check compared one joined string against two column names, so it
38
+ could never match an existing index.
39
+ - **A column is no longer treated as indexed because a wider column name is.**
40
+ Index matching compared substrings, so an index on `user_id` counted as
41
+ covering `id`.
42
+
43
+ ### Changed
44
+
45
+ - **schema.rb is read through one AST-backed reader.** `SchemaIntrospector`,
46
+ `PerformanceIntrospector` and `ConventionIntrospector` each parsed the dump
47
+ line by line with their own `create_table` regex; they now share
48
+ `SchemaReader`, built on the existing `SchemaDslListener`, as do the static
49
+ tier's table parse and the check-constraint, enum and generated-column
50
+ reads that each walked the dump separately.
51
+ - **The static schema tier reports four things differently.** Where no live
52
+ connection is available and `db/schema.rb` is parsed instead: a `t.references`
53
+ or `t.belongs_to` column is reported by its foreign key name (`author_id`,
54
+ not `author`), which is what the live tier has always reported; a column
55
+ default written as a proc reports its source rather than being omitted; a
56
+ default split across lines is picked up; and an expression index declared at
57
+ the top level is flagged `expression: true`, as an in-table one already was.
58
+ - **`rails_validate`'s Rails-aware rules moved to `ValidateSemantics`.** The
59
+ tool entry point kept syntax validation and orchestration; the 15 lint rules
60
+ now live in their own class. Behaviour is unchanged.
61
+ - **`detail` has a type.** `DetailLevel` defines the three levels, their
62
+ ordering and normalization. An unrecognised value now reads as `standard`
63
+ rather than silently selecting whichever branch happened to be last.
64
+ - **Soft-delete detection prefers the schema.** A `deleted_at` column in the
65
+ dump now drives the `soft_delete` convention, instead of matching the word
66
+ anywhere in model source. Apps dumping `structure.sql` keep the old source
67
+ match, and the `acts_as_paranoid` / `discard` macro check is unchanged.
68
+
8
69
  ## [5.16.2] - 2026-07-17
9
70
 
10
71
  ### Fixed
data/CONTRIBUTING.md CHANGED
@@ -20,7 +20,7 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
20
20
  lib/rails_ai_context/
21
21
  ├── cli/ # CLI tool runner (tool_runner.rb) - executes MCP tools from rake/Thor
22
22
  ├── introspectors/ # 39 introspectors (schema, models, routes, etc.)
23
- ├── tools/ # 38 MCP tools with detail levels and pagination
23
+ ├── tools/ # 39 MCP tools with detail levels and pagination
24
24
  ├── serializers/ # Per-assistant formatters + shared ToolGuideHelper
25
25
  ├── server.rb # MCP server setup (stdio + HTTP)
26
26
  ├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
@@ -29,7 +29,14 @@ module RailsAiContext
29
29
  def handle
30
30
  status_code, rack_headers, body = self.class.mcp_transport.handle_request(request)
31
31
  self.status = status_code
32
- rack_headers.each { |k, v| response.headers[k] = v }
32
+ rack_headers.each do |k, v|
33
+ # mcp >= 1.0 returns Rack 3-style lowercase header keys. Rails 7.0's
34
+ # response header hash is case-sensitive (Rack 2), so a lowercase
35
+ # "content-type" never registers and Rails falls back to text/html on
36
+ # an otherwise valid JSON body. Write it with the canonical case.
37
+ key = k.casecmp("content-type").zero? ? "Content-Type" : k
38
+ response.headers[key] = v
39
+ end
33
40
 
34
41
  if body.respond_to?(:each)
35
42
  # Plain enumerable body (initialize, errors, JSON mode): join to a
data/docs/ARCHITECTURE.md CHANGED
@@ -25,8 +25,8 @@ graph TD
25
25
 
26
26
  subgraph engine["Introspection Engine"]
27
27
  direction LR
28
- I["Introspectors\n31 modules\nPresets\nCached"]
29
- AST["AST Engine\nPrism\n20 listeners\nConfidence tags"]
28
+ I["Introspectors\n39 modules\nPresets\nCached"]
29
+ AST["AST Engine\nPrism\n21 listeners\nConfidence tags"]
30
30
  H["Hydration Layer\nSchema hints\ninjected into\ntool responses"]
31
31
  end
32
32
 
@@ -73,7 +73,7 @@ sequenceDiagram
73
73
  alt cache hit (TTL + fingerprint valid)
74
74
  Cache-->>TR: cached context
75
75
  else cache miss
76
- Cache->>App: introspect (31 modules)
76
+ Cache->>App: introspect (39 modules)
77
77
  App-->>Cache: structured data
78
78
  Cache-->>TR: fresh context
79
79
  end
@@ -115,8 +115,8 @@ The `Introspector` orchestrator runs configured introspectors and merges results
115
115
  **Prism AST parsing** replaced all regex-based Ruby source parsing in v5.2.0.
116
116
 
117
117
  - **AstCache** - Thread-safe parse cache (`Concurrent::Map`), keyed by path + SHA256 + mtime
118
- - **SourceIntrospector** - Single-pass Prism Dispatcher walks the AST once, feeds events to all 7 listeners simultaneously
119
- - **7 Listeners** - Associations, Validations, Scopes, Enums, Callbacks, Macros, Methods
118
+ - **SourceIntrospector** - Single-pass Prism Dispatcher walks the AST once, feeding every registered listener simultaneously
119
+ - **21 Listeners** - Associations, Validations, Scopes, Enums, Callbacks, Macros and Methods are the default map for model analysis; the rest are used through targeted walks over schema dumps, migrations, Gemfiles, rake tasks and initializers
120
120
  - **Confidence** - Every result carries `[VERIFIED]` (static literals) or `[INFERRED]` (dynamic expressions)
121
121
 
122
122
  ### Tool Registry (`lib/rails_ai_context/tools/base_tool.rb`)
@@ -91,8 +91,8 @@ preset: full
91
91
  | `excluded_models` | Array | 8 framework models | Models to skip during introspection |
92
92
  | `excluded_controllers` | Array | 2 framework controllers | Controllers to skip |
93
93
  | `excluded_route_prefixes` | Array | 6 framework prefixes | Route prefixes to skip |
94
- | `excluded_filters` | Array | 3 framework filters | Controller filters to skip |
95
- | `excluded_middleware` | Array | 24 framework middleware | Middleware to skip in listing |
94
+ | `excluded_filters` | Array | 5 framework filters | Controller filters to skip |
95
+ | `excluded_middleware` | Array | 25 framework middleware | Middleware to skip in listing |
96
96
  | `excluded_paths` | Array | `["node_modules", "tmp", "log", "vendor", ".git", "doc", "docs"]` | Paths excluded from search |
97
97
  | `excluded_association_names` | Array | 7 framework associations | Association names to hide from model output |
98
98
  | `excluded_concerns` | Array of Regex | Framework concerns | Concerns to skip (supports regex) |
@@ -116,6 +116,16 @@ preset: full
116
116
  | `search_extensions` | Array | `["rb", "js", "erb", "yml", "yaml", "json", "ts", "tsx", "vue", "svelte", "haml", "slim"]` | File extensions to search |
117
117
  | `concern_paths` | Array | `["app/models/concerns", "app/controllers/concerns"]` | Paths to scan for concerns |
118
118
  | `frontend_paths` | Array | `nil` (auto-detect) | Override frontend file paths |
119
+ | `extra_app_paths` | Array | `[]` | Extra directories under the app root to treat as application code |
120
+
121
+ ### Instrumentation
122
+
123
+ | Option | Type | Default | Description |
124
+ |:-------|:-----|:--------|:------------|
125
+ | `instrumentation_include_arguments` | Boolean | `false` | Include tool arguments in instrumentation events |
126
+
127
+ > [!WARNING]
128
+ > `instrumentation_include_arguments` forwards raw tool arguments to your subscribers, which for `rails_query` means the SQL text and for other tools can mean environment variable names. Leave it off unless your subscriber is as trusted as your logs.
119
129
 
120
130
  ### Database Query Safety
121
131
 
@@ -123,7 +133,8 @@ preset: full
123
133
  |:-------|:-----|:--------|:-----------|:------------|
124
134
  | `query_timeout` | Integer | `5` | - | SQL query timeout in seconds |
125
135
  | `query_row_limit` | Integer | `100` | 1–1000 | Maximum rows returned |
126
- | `query_redacted_columns` | Array | 10+ patterns | - | Column names/suffixes to redact |
136
+ | `query_redacted_columns` | Array | 14 patterns | - | Column names that cause a query to be rejected |
137
+ | `query_allowed_columns` | Array | `[]` | - | Column names exempted from the built-in sensitive list |
127
138
  | `allow_query_in_production` | Boolean | `false` | - | Allow `rails_query` tool in production |
128
139
 
129
140
  ### Logs
@@ -136,7 +147,7 @@ preset: full
136
147
 
137
148
  | Option | Type | Default | Description |
138
149
  |:-------|:-----|:--------|:------------|
139
- | `sensitive_patterns` | Array | 8 patterns | File patterns blocked from search/read (`.env*`, `*.key`, `*.pem`, `credentials.yml.enc`, etc.) |
150
+ | `sensitive_patterns` | Array | 27 patterns | File patterns blocked from search/read (`.env*`, `*.key`, `*.pem`, `config/credentials.yml.enc`, `.ssh/*`, etc.) |
140
151
 
141
152
  ### Extensibility
142
153
 
@@ -94,7 +94,6 @@ end
94
94
 
95
95
  | Introspector | Key | What it extracts |
96
96
  |:-------------|:----|:-----------------|
97
- | SourceIntrospector | `:source` | Prism AST: associations, validations, scopes, enums, callbacks, macros, methods |
98
97
  | MigrationIntrospector | `:migrations` | Migration files, versions, reversibility |
99
98
  | SeedsIntrospector | `:seeds` | Seed file analysis |
100
99
  | DatabaseStatsIntrospector | `:database_stats` | Table sizes, row counts, index stats |
@@ -170,9 +169,11 @@ These introspectors map directly onto [`RAILS_NERVOUS_SYSTEM.md`](../RAILS_NERVO
170
169
 
171
170
  ## AST-based introspection
172
171
 
173
- The **SourceIntrospector** uses Prism AST parsing for model analysis. It runs a single-pass Dispatcher that walks the AST once and feeds events to 7 listeners simultaneously:
172
+ The **SourceIntrospector** uses Prism AST parsing for model analysis. It is infrastructure shared by the introspectors above rather than an introspector you can enable: there is no `:source` key for `config.introspectors`.
174
173
 
175
- ### 7 Prism listeners
174
+ It runs a single-pass Dispatcher that walks the AST once and feeds events to all registered listeners simultaneously. Model analysis uses the seven below by default; the other fourteen listener classes are used through targeted walks (schema dumps, migrations, Gemfiles, rake tasks, initializers, and so on), for 21 in total.
175
+
176
+ ### The 7 default Prism listeners
176
177
 
177
178
  | Listener | What it detects |
178
179
  |:---------|:---------------|
data/docs/SECURITY.md CHANGED
@@ -33,8 +33,9 @@ flowchart LR
33
33
  L1 -->|"Blocked:\nINSERT, DROP,\nUNION SELECT..."| R1[Rejected]
34
34
  L1 -->|SELECT only| L2{Layer 2\nDatabase Read-Only}
35
35
  L2 -->|"SET TRANSACTION\nREAD ONLY\n+ timeout"| L3{Layer 3\nRow Limit}
36
- L3 -->|"Cap: 1000 rows\nDefault: 100"| L4{Layer 4\nColumn Redaction}
37
- L4 -->|"password_digest\napi_key → [REDACTED]"| OK[Safe Result]
36
+ L3 -->|"Cap: 1000 rows\nDefault: 100"| L4{Layer 4\nSensitive Columns}
37
+ L4 -->|"names password_digest,\napi_key, ..."| R1
38
+ L4 -->|"no sensitive column"| OK[Safe Result]
38
39
 
39
40
  style R1 fill:#e74c3c,stroke:#c0392b,color:#fff
40
41
  style OK fill:#27ae60,stroke:#1e8449,color:#fff
@@ -75,13 +76,19 @@ All queries execute inside a transaction, then rollback (even if they could writ
75
76
  - Configurable: `config.query_row_limit` (hard cap: 1000)
76
77
  - Applied as `LIMIT` clause appended to query
77
78
 
78
- ### Layer 4 - Column redaction
79
+ ### Layer 4 - Sensitive column rejection
79
80
 
80
- Sensitive column values are replaced with `[REDACTED]`:
81
+ A query that names a sensitive column is **rejected before execution**, not redacted after it:
81
82
 
82
83
  **Default redacted patterns:** `password_digest`, `encrypted_password`, `password_hash`, `reset_password_token`, `confirmation_token`, `unlock_token`, `otp_secret`, `session_data`, `secret_key`, `api_key`, `api_secret`, `access_token`, `refresh_token`, `jti`
83
84
 
84
- Redaction matches by **name** and **suffix** - `SELECT password_digest AS pd` still redacts `pd` because the column name is tracked through the query result.
85
+ Matching is by name, case-insensitive and word-bounded, against both the defaults above and `config.query_redacted_columns`. `SELECT password_digest AS pd FROM users` is blocked outright: post-execution redaction reads the column names the database returns, which the caller controls through aliases and expressions, so it cannot be relied on.
86
+
87
+ If one of your own columns merely looks sensitive (an `oauth_applications.secret`, say), exempt it by name:
88
+
89
+ ```ruby
90
+ config.query_allowed_columns = %w[secret]
91
+ ```
85
92
 
86
93
  ### Environment guard
87
94
 
@@ -96,15 +103,16 @@ The `rails_search_code` and file-reading tools block access to sensitive files:
96
103
 
97
104
  ### Default patterns
98
105
 
99
- ```
100
- .env*
101
- *.key
102
- *.pem
103
- credentials.yml.enc
104
- master.key
105
- secret_key_base
106
- config/secrets.yml
107
- config/credentials/*
106
+ ```text
107
+ .env .env.*
108
+ config/master.key
109
+ config/credentials.yml.enc config/credentials/*.yml.enc
110
+ config/database.yml config/secrets.yml
111
+ config/cable.yml config/storage.yml
112
+ config/mongoid.yml config/redis.yml
113
+ *.pem *.key *.p12 *.pfx *.jks *.keystore
114
+ **/id_rsa **/id_ed25519 **/id_ecdsa **/id_dsa
115
+ .ssh/* .aws/credentials .aws/config .netrc .pgpass .my.cnf
108
116
  ```
109
117
 
110
118
  ### AI context file exclusions
@@ -158,7 +166,7 @@ File type parameters accept only alphanumeric characters.
158
166
 
159
167
  ## Regex injection prevention
160
168
 
161
- User-supplied regex patterns have a 1-second timeout:
169
+ On Ruby 3.2 and newer, user-supplied regex patterns have a 1-second timeout:
162
170
 
163
171
  ```ruby
164
172
  Regexp.new(pattern, timeout: 1)
@@ -166,6 +174,9 @@ Regexp.new(pattern, timeout: 1)
166
174
 
167
175
  Complex patterns that would cause catastrophic backtracking raise `RegexpError` instead of hanging.
168
176
 
177
+ > [!WARNING]
178
+ > `Regexp.timeout` does not exist on Ruby 3.1, which this gem still supports. There the timeout is skipped, and a pattern crafted to backtrack catastrophically can hang the process serving the tool. If you expose `rails_search_code` to input you do not control, run it on Ruby 3.2 or newer.
179
+
169
180
  ---
170
181
 
171
182
  ## Safe file reading
@@ -207,7 +218,7 @@ When using HTTP transport (Rack middleware or McpController):
207
218
 
208
219
  - **Default bind**: `127.0.0.1` (localhost only - not exposed to network)
209
220
  - **`auto_mount` is `false` by default** - must be explicitly enabled
210
- - **Doctor checks** warn if `auto_mount` is true (security flag)
221
+ - **Doctor checks** fail if `auto_mount` is true in production, and report it as enabled elsewhere
211
222
 
212
223
  The McpController uses thread-safe transport initialization with mutex synchronization.
213
224
 
@@ -0,0 +1,38 @@
1
+ # Domain Docs
2
+
3
+ How the engineering skills should consume this repo's domain documentation when exploring the codebase.
4
+
5
+ This repo is **single-context**: one `CONTEXT.md` and one `docs/adr/` at the root.
6
+
7
+ ## Before exploring, read these
8
+
9
+ - **`CONTEXT.md`** at the repo root: the glossary and domain overview.
10
+ - **`docs/adr/`**: read ADRs that touch the area you're about to work in.
11
+
12
+ If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
13
+
14
+ ## File structure
15
+
16
+ ```text
17
+ /
18
+ ├── CONTEXT.md
19
+ ├── docs/adr/
20
+ │ ├── 0001-example-decision.md
21
+ │ └── 0002-another-decision.md
22
+ ├── lib/
23
+ └── spec/
24
+ ```
25
+
26
+ If this repo ever splits into multiple bounded contexts, add a root `CONTEXT-MAP.md` pointing at one `CONTEXT.md` per context, and update this file.
27
+
28
+ ## Use the glossary's vocabulary
29
+
30
+ When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
31
+
32
+ If the concept you need isn't in the glossary yet, that's a signal: either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
33
+
34
+ ## Flag ADR conflicts
35
+
36
+ If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
37
+
38
+ > _Contradicts ADR-0007 (event-sourced orders), but worth reopening because..._
@@ -0,0 +1,45 @@
1
+ # Issue tracker: GitHub
2
+
3
+ Issues and specs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
4
+
5
+ ## Conventions
6
+
7
+ - **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
8
+ - **Read an issue**: `gh issue view <number> --comments`, filtering comments by `jq` and also fetching labels.
9
+ - **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
10
+ - **Comment on an issue**: `gh issue comment <number> --body "..."`
11
+ - **Apply / remove labels**: `gh issue edit <number> --add-label "..."` / `--remove-label "..."`
12
+ - **Close**: `gh issue close <number> --comment "..."`
13
+
14
+ Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone.
15
+
16
+ ## Pull requests as a triage surface
17
+
18
+ **PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_
19
+
20
+ When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents:
21
+
22
+ - **Read a PR**: `gh pr view <number> --comments` and `gh pr diff <number>` for the diff.
23
+ - **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`).
24
+ - **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`.
25
+
26
+ GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`.
27
+
28
+ ## When a skill says "publish to the issue tracker"
29
+
30
+ Create a GitHub issue.
31
+
32
+ ## When a skill says "fetch the relevant ticket"
33
+
34
+ Run `gh issue view <number> --comments`.
35
+
36
+ ## Wayfinding operations
37
+
38
+ Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets.
39
+
40
+ - **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`.
41
+ - **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #<map>` at the top of the child body. Labels: `wayfinder:<type>` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev.
42
+ - **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos/<owner>/<repo>/issues/<child>/dependencies/blocked_by -F issue_id=<blocker-db-id>`, where `<blocker-db-id>` is the blocker's numeric **database id** (`gh api repos/<owner>/<repo>/issues/<n> --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #<n>, #<n>` line at the top of the child body. A ticket is unblocked when every blocker is closed.
43
+ - **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins.
44
+ - **Claim**: `gh issue edit <n> --add-assignee @me` — the session's first write.
45
+ - **Resolve**: `gh issue comment <n> --body "<answer>"`, then `gh issue close <n>`, then append a context pointer (gist + link) to the map's Decisions-so-far.
@@ -0,0 +1,15 @@
1
+ # Triage Labels
2
+
3
+ The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
4
+
5
+ | Label in mattpocock/skills | Label in our tracker | Meaning |
6
+ | -------------------------- | -------------------- | ---------------------------------------- |
7
+ | `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
8
+ | `needs-info` | `needs-info` | Waiting on reporter for more information |
9
+ | `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
10
+ | `ready-for-human` | `ready-for-human` | Requires human implementation |
11
+ | `wontfix` | `wontfix` | Will not be actioned |
12
+
13
+ When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
14
+
15
+ Edit the right-hand column to match whatever vocabulary you actually use.
@@ -21,7 +21,7 @@ module RailsAiContext
21
21
  sensitive_patterns search_extensions concern_paths frontend_paths extra_app_paths
22
22
  max_file_size max_test_file_size max_schema_file_size max_view_total_size
23
23
  max_view_file_size max_search_results max_validate_files
24
- query_timeout query_row_limit query_redacted_columns allow_query_in_production
24
+ query_timeout query_row_limit query_redacted_columns query_allowed_columns allow_query_in_production
25
25
  log_lines introspectors
26
26
  hydration_enabled hydration_max_hints
27
27
  instrumentation_include_arguments
@@ -244,6 +244,7 @@ module RailsAiContext
244
244
  attr_accessor :query_timeout # Statement timeout in seconds (default: 5)
245
245
  attr_accessor :query_row_limit # Max rows returned (default: 100, hard cap: 1000)
246
246
  attr_accessor :query_redacted_columns # Column names whose values are redacted in output
247
+ attr_accessor :query_allowed_columns # Column names to exempt from the built-in sensitive list
247
248
  attr_accessor :allow_query_in_production # Allow rails_query in production (default: false)
248
249
 
249
250
  # Log reading settings (rails_read_logs)
@@ -317,6 +318,7 @@ module RailsAiContext
317
318
  otp_secret session_data secret_key
318
319
  api_key api_secret access_token refresh_token jti
319
320
  ]
321
+ @query_allowed_columns = []
320
322
  @allow_query_in_production = false
321
323
  @log_lines = 50
322
324
  @hydration_enabled = true
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ # The `detail` parameter shared by most tools. Values arrive over the wire as
5
+ # strings, so this stays string-compatible rather than wrapping them; what it
6
+ # adds is one definition of the allowed values and an ordering, so callers can
7
+ # ask "is this at least standard?" instead of comparing literals.
8
+ module DetailLevel
9
+ SUMMARY = "summary"
10
+ STANDARD = "standard"
11
+ FULL = "full"
12
+
13
+ ALL = [ SUMMARY, STANDARD, FULL ].freeze
14
+ DEFAULT = STANDARD
15
+
16
+ ORDER = { SUMMARY => 0, STANDARD => 1, FULL => 2 }.freeze
17
+
18
+ def self.valid?(detail)
19
+ ALL.include?(detail.to_s)
20
+ end
21
+
22
+ # Unknown values read as the default rather than falling through to
23
+ # whichever branch happens to be last.
24
+ def self.normalize(detail)
25
+ valid?(detail) ? detail.to_s : DEFAULT
26
+ end
27
+
28
+ def self.at_least?(detail, minimum)
29
+ ORDER.fetch(normalize(detail)) >= ORDER.fetch(minimum)
30
+ end
31
+
32
+ def self.full?(detail)
33
+ normalize(detail) == FULL
34
+ end
35
+
36
+ def self.summary?(detail)
37
+ normalize(detail) == SUMMARY
38
+ end
39
+ end
40
+ end
@@ -102,21 +102,21 @@ module RailsAiContext
102
102
 
103
103
  # STI detection via AST: extract parent class from ClassNode, check schema
104
104
  app_model_names = model_files.filter_map { |f| File.basename(f, ".rb").camelize }
105
- schema_path = File.join(root, "db/schema.rb")
106
- schema_content = File.exist?(schema_path) ? (RailsAiContext::SafeFile.read(schema_path) || "") : ""
105
+ schema = SchemaReader.new(File.join(root, "db/schema.rb"))
106
+ schema_readable = schema.tables.any?
107
107
 
108
108
  has_sti_subclass = false
109
109
  has_inheritance_column = false
110
110
  has_current_attributes = false
111
- has_deleted_at = false
112
111
 
113
112
  model_files.first(500).each do |f|
114
113
  parent = extract_parent_class(f)
115
114
  if parent && app_model_names.include?(parent) && parent != "ApplicationRecord"
116
115
  parent_table = parent.underscore.pluralize
117
- if schema_content.match?(/create_table\s+"#{Regexp.escape(parent_table)}".*?t\.\w+\s+"type"/m)
118
- has_sti_subclass = true
119
- end
116
+ # Only the dump says whether the parent table carries a type
117
+ # column, so an unreadable schema leaves this to the
118
+ # inheritance_column check below rather than a source guess.
119
+ has_sti_subclass = true if schema_readable && schema.column?(parent_table, "type")
120
120
  end
121
121
 
122
122
  superclass = extract_superclass_path(f)
@@ -127,12 +127,15 @@ module RailsAiContext
127
127
  inh: -> { Listeners::ChainedCallListener.new(:inheritance_column=) }
128
128
  })
129
129
  has_inheritance_column = true if inheritance_check[:inh].any?
130
+ end
130
131
 
131
- # deleted_at is a keyword/content check, not structural -- keep regex
132
- unless has_deleted_at
133
- src = RailsAiContext::SafeFile.read(f)
134
- has_deleted_at = true if src&.match?(/deleted_at/)
135
- end
132
+ # A soft-delete column is a schema fact, so prefer the dump. Apps on
133
+ # structure.sql have no schema.rb to read, and fall back to the
134
+ # looser source match.
135
+ has_deleted_at = if schema_readable
136
+ schema.any_column?("deleted_at")
137
+ else
138
+ model_files.first(500).any? { |f| RailsAiContext::SafeFile.read(f)&.match?(/deleted_at/) }
136
139
  end
137
140
 
138
141
  patterns << "sti" if has_inheritance_column || has_sti_subclass
@@ -4,7 +4,7 @@ module RailsAiContext
4
4
  module Introspectors
5
5
  # Discovers mounted Rails engines and Rack apps from config/routes.rb.
6
6
  # Identifies well-known engines and provides context about what each does.
7
- class EngineIntrospector # rubocop:disable Metrics/ClassLength
7
+ class EngineIntrospector
8
8
  attr_reader :app
9
9
 
10
10
  KNOWN_ENGINES = {
@@ -9,7 +9,6 @@ module RailsAiContext
9
9
 
10
10
  # Known gems that significantly affect how the app works.
11
11
  # The AI needs to know about these to give accurate advice.
12
- # rubocop:disable Metrics/CollectionLiteralLength
13
12
  NOTABLE_GEMS = {
14
13
  # Auth
15
14
  "devise" => { category: :auth, note: "Authentication via Devise. Check User model for devise modules." },
@@ -147,7 +146,6 @@ module RailsAiContext
147
146
  "bullet" => { category: :utilities, note: "N+1 query detection via Bullet." },
148
147
  "rack-attack" => { category: :utilities, note: "Rate limiting and throttling via Rack::Attack." }
149
148
  }.freeze
150
- # rubocop:enable Metrics/CollectionLiteralLength
151
149
 
152
150
  def initialize(app)
153
151
  @app = app
@@ -179,10 +179,21 @@ module RailsAiContext
179
179
  column_type: node.name.to_s,
180
180
  name: col_name,
181
181
  options: options,
182
+ # A proc default (`default: -> { "now()" }`) has no literal value,
183
+ # so keep its source for callers that report defaults verbatim.
184
+ default_source: default_source(node),
182
185
  location: node.location.start_line
183
186
  }
184
187
  end
185
188
 
189
+ def default_source(node)
190
+ args = node.arguments&.arguments || []
191
+ assoc = args.grep(Prism::KeywordHashNode)
192
+ .flat_map(&:elements)
193
+ .find { |e| e.is_a?(Prism::AssocNode) && extract_key(e.key) == :default }
194
+ assoc&.value&.slice
195
+ end
196
+
186
197
  def extract_index(node)
187
198
  args = node.arguments&.arguments || []
188
199
  columns = resolve_index_columns(args.first)