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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -10
- data/AGENTS.md +15 -0
- data/CHANGELOG.md +61 -0
- data/CONTRIBUTING.md +1 -1
- data/app/controllers/rails_ai_context/mcp_controller.rb +8 -1
- data/docs/ARCHITECTURE.md +5 -5
- data/docs/CONFIGURATION.md +15 -4
- data/docs/INTROSPECTORS.md +4 -3
- data/docs/SECURITY.md +27 -16
- data/docs/agents/domain.md +38 -0
- data/docs/agents/issue-tracker.md +45 -0
- data/docs/agents/triage-labels.md +15 -0
- data/lib/rails_ai_context/configuration.rb +3 -1
- data/lib/rails_ai_context/detail_level.rb +40 -0
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +14 -11
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +0 -2
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +11 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +5 -40
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +85 -182
- data/lib/rails_ai_context/introspectors/schema_reader.rb +166 -0
- data/lib/rails_ai_context/introspectors/source_introspector.rb +2 -4
- data/lib/rails_ai_context/mcp_config_generator.rb +5 -12
- data/lib/rails_ai_context/safe_file.rb +14 -0
- data/lib/rails_ai_context/serializers/claude_serializer.rb +7 -9
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +1 -8
- data/lib/rails_ai_context/serializers/context_mode_dispatch.rb +25 -0
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +4 -13
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +4 -13
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +1 -6
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -1
- data/lib/rails_ai_context/tools/analyze_feature.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +2 -2
- data/lib/rails_ai_context/tools/generate_test.rb +1 -1
- data/lib/rails_ai_context/tools/get_callbacks.rb +2 -2
- data/lib/rails_ai_context/tools/get_concern.rb +2 -2
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +1 -1
- data/lib/rails_ai_context/tools/get_schema.rb +1 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +1 -1
- data/lib/rails_ai_context/tools/get_view.rb +1 -1
- data/lib/rails_ai_context/tools/onboard.rb +3 -3
- data/lib/rails_ai_context/tools/performance_check.rb +3 -3
- data/lib/rails_ai_context/tools/query.rb +10 -7
- data/lib/rails_ai_context/tools/review_changes.rb +2 -2
- data/lib/rails_ai_context/tools/runtime_info.rb +4 -4
- data/lib/rails_ai_context/tools/search_code.rb +2 -2
- data/lib/rails_ai_context/tools/validate.rb +2 -864
- data/lib/rails_ai_context/tools/validate_semantics.rb +879 -0
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +9 -2
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebb8d4e0ebed93183c0c019071ee63d097fb8c7495f551393991000aa5d06e7b
|
|
4
|
+
data.tar.gz: 2141cf0a281a173da2f0fa34927be843f93600ced5cdd67ccd6eb4aeef261ce0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
15
|
-
|
|
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/ #
|
|
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
|
|
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\
|
|
29
|
-
AST["AST Engine\nPrism\
|
|
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 (
|
|
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,
|
|
119
|
-
- **
|
|
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`)
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -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 |
|
|
95
|
-
| `excluded_middleware` | Array |
|
|
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 |
|
|
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 |
|
|
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
|
|
data/docs/INTROSPECTORS.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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\
|
|
37
|
-
L4 -->|"password_digest
|
|
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 -
|
|
79
|
+
### Layer 4 - Sensitive column rejection
|
|
79
80
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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**
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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)
|