coatepec 0.6.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddcc044bcc53590ba19d988b0db8ea0aea4efd7db102bd6718196e61ab9ab820
4
- data.tar.gz: 1b8b779b5bb59068a7fdd5fc90e7d44b900d50f1918e1d68877a9ccad24bf1e7
3
+ metadata.gz: 550a60896b3c267b12b5db85b787c52d93b97dd2b95409c8bf78943ea63a27d0
4
+ data.tar.gz: '09c6cbbc7c00dd604d2cb5dfbabbb0101aea3cadf5d24070f3254a144bb45012'
5
5
  SHA512:
6
- metadata.gz: 56fce0755730ab151c81fb43f88a28d6e081b242c3e9bb31fac71578c290e864efbc5e5313702a4c015b95a661cdd601f1701979c98dbbdae1a0cfa06e0be2fa
7
- data.tar.gz: 4101b3595de972478ac414f3c019b413ec991cfe1d8ea0fbb251010fad6d699eaeb849818a1c4dbcfe813b47b41af220b68a7f971efe287dbbb8c598d8ff9763
6
+ metadata.gz: 484557dd0635d21f7669a12b6accab0684e4850e4ac91b6718b733bd23372aceaa5b903f387facb22fdc6b0c2d0a09dfb4805ace67e6b24c66f95c482240d69b
7
+ data.tar.gz: e3fcc83c1f752dfba27b1260584b2ee1d0df5707e36163d049015babe269290a2467b3018c0e3696d8d6c4d4fcaf5bf8b2e325a3792ac7d3bcb085cad433e73c
data/.rubocop.yml CHANGED
@@ -15,6 +15,11 @@ Style/StringLiterals:
15
15
  Style/StringLiteralsInInterpolation:
16
16
  EnforcedStyle: double_quotes
17
17
 
18
+ Metrics/ParameterLists:
19
+ # Keyword-only signatures here mirror the MCP tools' input schemas, and a tool
20
+ # gaining an input shouldn't force a refactor; positional lists stay capped at 5.
21
+ CountKeywordArgs: false
22
+
18
23
  Metrics/BlockLength:
19
24
  # RSpec's describe/context/it DSL naturally produces long blocks; this is the
20
25
  # standard exclusion used by rubocop-rspec's own default config. inherit_mode
data/CHANGELOG.md CHANGED
@@ -1,5 +1,147 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0
4
+
5
+ - Tool responses are now compact JSON rather than pretty-printed (22-32%
6
+ smaller on measured payloads); set `COATEPEC_PRETTY=1` on the server
7
+ process to restore indentation.
8
+ - `rails_spec_run` now omits passing examples from `examples` by default --
9
+ a 49-test green run drops from ~3,700 tokens to ~160 -- and `summary` gains
10
+ `pending_count`. Pass the new `include_passing: true` input to get the full
11
+ roster back (still capped at 500 examples). `rails_spec_flaky_check` is
12
+ unaffected: it still sees every example.
13
+ - `examples[].description` (and `flaky_examples[].description`) is omitted
14
+ when it would only repeat `id`, which is always the case for Minitest.
15
+ - `rails_routes` (with `engines: "include"`) and `rails_controller` now
16
+ include the routes of engines mounted in the application, expanded one
17
+ level deep (an engine mounted inside another engine stays an opaque mount
18
+ route, the same boundary `bin/rails routes` draws). Paths carry the mount
19
+ point, so `/widget_admin/audits` is what both tools report, and a
20
+ controller living inside an engine no longer has all of its actions listed
21
+ under `unroutable_actions`.
22
+ - `rails_routes` gains an `engine` column and `rails_controller`'s
23
+ `actions[].routes` entries an `engine` field: `null` for an application
24
+ route, the engine's class name otherwise. `rails_routes`' `query` matches
25
+ against it like every other column, so `query: "Avo::Engine"` with
26
+ `engines: "include"` or `"only"` returns exactly that engine's routes.
27
+ Application routes are listed before engine routes.
28
+ - An engine route's `name` (`route_name` in `rails_controller`) is relative
29
+ to its engine: it is reached through the mount's helper,
30
+ `<mount name>.<name>_path`, where the mount name is the `name` of the mount
31
+ route itself -- never as a top-level url helper.
32
+ - `rails_routes` and `rails_controller` now omit routes Rails marks
33
+ `internal` -- its own `/rails/info` and friends -- matching
34
+ `bin/rails routes`. This is a behaviour change for callers that relied on
35
+ seeing them.
36
+ - Add Minitest support to `rails_spec_run` and `rails_spec_flaky_check`:
37
+ selectors under a `test/` root ending in `_test.rb` run through Rails'
38
+ Minitest runner in the same warm worker, with the same
39
+ `example`/`seed`/`fail_fast`/`timeout_seconds` inputs and the same result
40
+ shape (`id` is `ClassName#test_method`; a skip is `"pending"`, an error is
41
+ `"failed"`). The framework is chosen per call from the selector paths --
42
+ an app that has both `spec/` and `test/` works without configuration; one
43
+ call may not mix the two (`mixed_test_frameworks`).
44
+ - The Minitest child sets `PARALLEL_WORKERS=1`, so a selection above Rails'
45
+ parallelization threshold runs serially under the call's single timeout
46
+ instead of forking a worker tree.
47
+ - `file:LINE` selection is implemented by Coatepec itself rather than relying
48
+ on Rails' line filtering, which is absent in apps generated with
49
+ `--skip-test` and targets a method Minitest 6 no longer calls on Rails 7.1.
50
+ - Path validation now runs before the framework is required, so an invalid
51
+ path on a Minitest-only app reports `invalid_spec_path` instead of
52
+ `unsupported_test_framework`. `invalid_spec_path` keeps its code for both
53
+ frameworks.
54
+ - `rails_model` collapses validators with identical name, attributes and
55
+ options into one entry (a concern and the model body declaring the same
56
+ validation used to appear twice); the 200-item cap now counts distinct
57
+ validators.
58
+ - `rails_routes` defaults to 100 routes per page (was 50 -- engine expansion
59
+ roughly doubled route counts when engines are included) and returns
60
+ `next_offset` for the follow-up call, `null` on the last page.
61
+ - `rails_spec_run`: when several tests fail with the same error text (a
62
+ broken layout erroring every controller test, say), `stdout` keeps the
63
+ first failure block and replaces each repeat with one roll-up line
64
+ naming the other tests -- a 5-error controller run drops from ~4,850 B to
65
+ roughly a third of that. Backtrace frames and RSpec's `Failure/Error:`
66
+ source line are ignored when deciding that two blocks match.
67
+ - `rails_spec_run` gains `include_stdout` (`failures`, the default; `always`;
68
+ `never`). A passing run's `stdout` is `null` by default -- its progress
69
+ dots and summary line only repeat `summary` -- and a failing run's is
70
+ returned; `always` and `never` override that either way. The key stays so
71
+ the result shape is uniform. `stderr` is always returned.
72
+ - `rails_routes` returns application routes only by default and gains
73
+ `engines` to change that: `include` lists the routes of mounted engines
74
+ too -- the routes 0.8.0 added by expanding engines -- and `only` lists just
75
+ those; the default matches pre-0.8.0 output (application routes, the mount
76
+ route included). Every response carries `engines` (the filter applied) and
77
+ `engines_excluded` (how many routes matching `query` were withheld), so the
78
+ omission is stated, never silent. The filter applies after `query` and
79
+ before paging, so `matched` and `next_offset` describe the kept set; an
80
+ engine's mount route counts as an application route. Measured on an app
81
+ with a mounted admin engine, six typical route queries cost 57% less
82
+ context with engine routes withheld.
83
+ - `rails_model` cuts an array-valued validator option longer than 20 entries
84
+ to its first 20 and adds `<option>_count` (the full length) and
85
+ `<option>_truncated: true` beside it -- a 249-code `inclusion` list no
86
+ longer costs 1.4 KB per model. Shorter lists are unchanged and carry no
87
+ sibling keys.
88
+ - Every tool response's `meta` is now just `duration_ms`; `project_root` is
89
+ reported once by `rails_runtime_status` (beside `environment`) instead of
90
+ on every call.
91
+ - `rails_spec_run` results carry `child_pid`, `signaled`, `termsig`,
92
+ `stopsig` and `coredump` only when the child did not exit normally (a
93
+ timeout kill or a crash); a normal run reports `status`, `exit_code` and
94
+ the output fields alongside the usual `summary` and `examples`.
95
+ - `rails_model` returns `name`, `table_name`, `primary_key`, `abstract_class`
96
+ and `counts` (the size of each of its four lists, after validator
97
+ de-duplication and the 200-item caps) by default, and gains `fields`, an
98
+ array of `columns`/`associations`/`validators`/`enums` naming the lists to
99
+ include; omitted or `[]` means counts only. Callers that read the lists
100
+ must now ask for them -- the default response is roughly an order of
101
+ magnitude smaller than the full one on a typical model.
102
+ - `rails_routes` returns `columns` (`name`, `verb`, `path`, `controller`,
103
+ `action`, `engine`) and `rows` instead of one object per route -- the six
104
+ key names were a third of every item's bytes -- and both `rails_routes`
105
+ and `rails_controller` report paths without the `(.:format)` suffix Rails
106
+ appends to most routes.
107
+ - `rails_spec_run`'s `summary` gains `error_count` (how many of
108
+ `failure_count` were errors rather than assertion failures) and
109
+ `assertion_count` (Minitest's assertion total), so a green run answers
110
+ "how many assertions ran?" without `stdout` and a failing run's counts
111
+ match the `0 failures, 5 errors` line beside them. Both are `null` for
112
+ RSpec, which reports neither.
113
+
114
+ ## 0.7.0
115
+
116
+ - Add `rails_controller`: reports an `ActionController` controller's
117
+ actions, action callbacks, and included concerns. Admits
118
+ `ActionController::API` controllers as well as `ActionController::Base`
119
+ ones.
120
+ - Each action is cross-referenced against `Rails.application.routes`,
121
+ the same route table `rails_routes` reads: `actions[].routes` lists the
122
+ verb/path/route name reaching that action (`path` is Rails' raw route
123
+ spec, `(.:format)` suffix included, byte-identical to `rails_routes`'
124
+ own `path` for the same route); `unroutable_actions` lists action
125
+ methods no route reaches (probable dead code); `routes_without_action`
126
+ lists route action names the controller doesn't define -- a request to
127
+ one of those raises `AbstractController::ActionNotFound` in production,
128
+ making this the tool's most actionable output.
129
+ - `callbacks[]` reports each `before`/`after`/`around` filter's `only`/
130
+ `except` action restriction (an array, or `nil` if unrestricted -- `nil`
131
+ and `[]` are distinct and both preserved) and any remaining `if`/
132
+ `unless` condition (a symbol by name; a Proc reported as `"(block)"`,
133
+ never serialized directly, since `Proc#to_s` leaks the app's absolute
134
+ source path).
135
+ - `concerns` lists app-defined modules only, included directly or
136
+ inherited from a base class; framework modules are excluded.
137
+ - **Known limitations, both by design:** strong parameters
138
+ (`params.require(...).permit(...)`) are not reported -- they exist only
139
+ as code inside a method body, never as class metadata, and recovering
140
+ them would require source parsing, which this gem does not do. Only the
141
+ main app's route table is read, so a controller mounted inside an
142
+ engine has its actions reported as unroutable even where the engine's
143
+ own routes reach them -- the same boundary `rails_routes` already has.
144
+
3
145
  ## 0.6.0
4
146
 
5
147
  - Add `rails_spec_flaky_check`: runs a spec selection multiple times with
data/README.md CHANGED
@@ -57,8 +57,8 @@ MCP client
57
57
  v
58
58
  Coatepec parent (Rails-free)
59
59
  `-- private NDJSON --> test worker (Rails "test", booted lazily, kept warm)
60
- |-- Linux: Process.fork --> isolated RSpec child
61
- `-- macOS: Process.spawn --> fresh RSpec process (default)
60
+ |-- Linux: Process.fork --> isolated RSpec/Minitest child
61
+ `-- macOS: Process.spawn --> fresh RSpec/Minitest process (default)
62
62
  Process.fork, guarded --> opt-in, see Configuration
63
63
  ```
64
64
 
@@ -75,6 +75,11 @@ macos_fork_unsafe_gems: [some_gem] # extends the built-in fork-unsafe denylist
75
75
  A missing file means every setting takes its default -- this file is never
76
76
  required.
77
77
 
78
+ Tool responses are compact JSON -- no indentation, nothing downstream reads
79
+ it. Set `COATEPEC_PRETTY=1` in the MCP server's `env` (the same place as
80
+ `OBJC_DISABLE_INITIALIZE_FORK_SAFETY` in the JSON example below) to
81
+ pretty-print them when you are reading the sidecar by hand.
82
+
78
83
  ### macOS fork (experimental, opt-in)
79
84
 
80
85
  On macOS, `rails_spec_run` normally spawns a fresh `bundle exec rspec`
@@ -125,38 +130,129 @@ a no-op.
125
130
 
126
131
  | Tool | Input | Notes |
127
132
  |---|---|---|
128
- | `rails_spec_run` | `paths: string[1..100]`, `example?`, `seed?`, `fail_fast?`, `timeout_seconds?` (1..900, default 120) | Isolated per run; output capped at 256 KiB per stream |
129
- | `rails_runtime_status` | `{}` | Reports Ruby/Rails versions, worker PID, boot_id, lifecycle state |
133
+ | `rails_spec_run` | `paths: string[1..100]`, `example?`, `seed?`, `fail_fast?`, `timeout_seconds?` (1..900, default 120), `include_passing?`, `include_stdout?` (`failures` default, `always`, `never`) | RSpec (`spec/**/*_spec.rb`) or Minitest (`test/**/*_test.rb`), chosen from the paths; isolated per run; output capped at 256 KiB per stream; failure blocks in `stdout` that repeat an earlier block's error verbatim are replaced by a roll-up line naming the tests; returns `summary` counts (`example_count`, `failure_count`, `error_count`, `pending_count`, `assertion_count`, `duration`; the error and assertion counts come from Minitest and are `null` for RSpec) plus only the failed/pending `examples`, pass `include_passing: true` for the full roster (still capped at 500 examples); `stdout` is returned only for failing runs by default; `include_stdout: "always"` keeps it for green runs, `"never"` drops it always (the key stays, `null`); `stderr` is always returned; `child_pid`/`signaled`/`termsig`/`stopsig`/`coredump` appear only when the child did not exit normally (a timeout kill or a crash) |
134
+ | `rails_runtime_status` | `{}` | Reports Ruby/Rails versions, worker PID, boot_id, lifecycle state, and the project root (`project_root`) |
130
135
  | `rails_runtime_restart` | `{}` | Unconditionally respawns the worker, discarding its warm boot |
131
- | `rails_routes` | `query?`, `limit?` (1..200, default 50), `offset?` | Case-insensitive filter across name/verb/path/controller/action |
132
- | `rails_model` | `name` (constant path, e.g. `Widget` or `Admin::Widget`) | ActiveRecord models only; columns, associations, validators, enums -- no row data |
133
- | `rails_spec_flaky_check` | `paths`, `example?`, `timeout_seconds?` (per round, 1..900), `runs?` (2..20, default 5) | Runs the selection `runs` times with a fresh random seed each round; reports examples whose status was inconsistent across runs |
136
+ | `rails_routes` | `query?`, `limit?` (1..200, default 100), `offset?`, `engines?` (`exclude` default, `include`, `only`) | Returns `columns` (`name`, `verb`, `path`, `controller`, `action`, `engine`) and `rows` in that order; paths omit the `(.:format)` suffix Rails appends. Case-insensitive filter across name/verb/path/controller/action/engine. Application routes only by default; `engines: "include"` adds the routes of mounted engines (one level deep, paths prefixed with the mount point, `engine` naming the engine class; `null` for an application route), `"only"` returns just those. Every response carries `engines` (the filter applied) and `engines_excluded` (how many routes matching `query` the filter withheld), so nothing is hidden silently. The filter applies after `query` and before paging, so `matched`/`next_offset` describe the kept set. The engine's mount route counts as an application route. Routes Rails marks `internal` are omitted, like `bin/rails routes`. `next_offset` is the offset of the next page, or `null` on the last one |
137
+ | `rails_model` | `name` (constant path, e.g. `Widget` or `Admin::Widget`), `fields?` (any of `columns`, `associations`, `validators`, `enums`; omitted or `[]` means none) | Returns `name`, `table_name`, `primary_key`, `abstract_class` and `counts` (the size of each of the four lists, each capped at 200) by default and only the lists named in `fields` -- so a how-many question costs a few dozen bytes and `fields: ["columns"]` asks for the one list you need; ActiveRecord models only; columns, associations, validators, enums -- no row data; validators are de-duplicated by class, attributes and options (a concern and the model body declaring the same validation count once), so the count can be lower than `klass.validators.size`; an array-valued validator option longer than 20 entries (a country-code `inclusion` list, say) is cut to its first 20 with `<option>_count` and `<option>_truncated: true` beside it |
138
+ | `rails_controller` | `name` (constant path, e.g. `WidgetsController` or `Admin::ReportsController`) | Actions, action callbacks, concerns, and the routes reaching each action -- no request dispatch |
139
+ | `rails_spec_flaky_check` | `paths`, `example?`, `timeout_seconds?` (per round, 1..900), `runs?` (2..20, default 5) | Runs the selection `runs` times with a fresh random seed each round; reports tests whose status was inconsistent across runs; RSpec or Minitest, chosen from the paths |
134
140
 
135
141
  ### Example queries
136
142
 
137
143
  `rails_routes`:
138
144
 
139
- - "What are all the routes in this app?" -- `rails_routes()`
145
+ - "What are all the routes in this app?" -- `rails_routes()`, the
146
+ application's own; add `engines: "include"` for the mounted engines' too.
140
147
  - "What's the URL for widgets?" -- `rails_routes(query: "widget")`. `query` is a
141
148
  case-insensitive substring match across name, verb, path, controller, *and*
142
149
  action -- not just the path -- so a resource name alone typically returns
143
150
  every route for that resource (index/create/new/...); narrow further with
144
- something like `query: "new_widget"` to hit one route by name.
151
+ something like `query: "new_widget"` to hit one route by name. The call
152
+ already leaves out the admin engine's scaffolding, which is often two thirds
153
+ of every match; the response's `engines_excluded` says how many engine routes
154
+ it held back. Pass `engines: "include"` to see them too, or
155
+ `engines: "only"` for just the engine's.
145
156
  - "Which routes accept POST?" -- `rails_routes(query: "POST")`, the same
146
157
  substring match applied to the verb column.
158
+ - "Which routes does the Avo engine add?" --
159
+ `rails_routes(query: "Avo::Engine", engines: "only")` -- the substring match
160
+ covers the `engine` column too, so an engine's class name returns exactly the
161
+ routes mounted from it. Application routes always sort before engine routes,
162
+ so an unfiltered listing (with `engines: "include"`) reads the way
163
+ `bin/rails routes` does.
164
+ - An engine route's `name` is relative to its engine, not a top-level url
165
+ helper: the row `["audits", "GET", "/widget_admin/audits",
166
+ "widget_admin/audits", "index", "WidgetAdmin::Engine"]` is reached as
167
+ `widget_admin.audits_path` -- `<mount name>.<name>_path`, where the mount
168
+ name is the `name` of the mount route itself -- and `audits_path` alone
169
+ does not exist on the application.
170
+ - An engine mounted at two paths is listed under both mount points, once per
171
+ prefixed path, whenever engine routes are in scope, since each of those
172
+ paths is a real, reachable URL.
173
+ `bin/rails routes` keys engines by endpoint and so lists such an engine
174
+ only once.
175
+ - Paging: when `next_offset` is not `null`, call again with
176
+ `offset: next_offset` to get the next page.
147
177
 
148
178
  `rails_model`:
149
179
 
150
180
  - "What columns does Widget have, and which are nullable?" --
151
- `rails_model(name: "Widget")` -- see `columns[].null`, `columns[].sql_type`,
152
- `columns[].default`.
153
- - "What validations and associations does Widget enforce?" -- same call --
181
+ `rails_model(name: "Widget", fields: ["columns"])` -- see `columns[].null`,
182
+ `columns[].sql_type`, `columns[].default`.
183
+ - "What validations and associations does Widget enforce?" --
184
+ `rails_model(name: "Widget", fields: ["validators", "associations"])` --
154
185
  see `validators` and `associations`.
186
+ - "How many columns, associations, validators and enums does Event have?" --
187
+ `rails_model(name: "Event")` -- see `counts`; ask for the lists with
188
+ `fields` when you need them.
189
+ - A long allow-list is summarised, not enumerated: `validates :country_code,
190
+ inclusion: { in: ISO_CODES }` with 249 codes comes back as `"in"` holding
191
+ the first 20, `"in_count": 249` and `"in_truncated": true`. A list of 20 or
192
+ fewer has no `_count`/`_truncated` siblings.
155
193
  - "What happens if I ask about a non-model class, like a controller?" --
156
194
  `rails_model(name: "ApplicationController")` raises `not_active_record_model`
157
195
  rather than introspecting it (a nonexistent constant raises `model_not_found`
158
196
  instead) -- the tool only ever reflects on `ActiveRecord::Base` descendants.
159
197
 
198
+ `rails_controller`:
199
+
200
+ - "What actions does WidgetsController define, and what routes reach them?" --
201
+ `rails_controller(name: "WidgetsController")` -- see `actions[].routes`,
202
+ each with `verb`, `path` (without the `(.:format)` suffix -- byte-identical
203
+ to the same route's `path` from `rails_routes`), `route_name`, and `engine`
204
+ (`null` for an application route, the engine's class name for a route that
205
+ reaches this controller through a mount).
206
+ - "Does this controller have dead code, or a route that will 500?" -- same
207
+ call -- see `unroutable_actions` (action methods no route reaches --
208
+ probably dead code) and `routes_without_action` (action names the route
209
+ table expects but the controller doesn't define -- a request to that route
210
+ raises `AbstractController::ActionNotFound` in production; this is the
211
+ tool's most actionable output).
212
+ - "What before/after/around filters run on this controller's actions, and
213
+ under what conditions?" -- same call -- see `callbacks[]`: `kind`
214
+ (`"before"`/`"after"`/`"around"`), `filter` (the method name, or `"(block)"`
215
+ for a Proc), `only`/`except` (arrays of action names the filter is
216
+ restricted to/excluded from, or `nil` if unrestricted -- `nil` and `[]` mean
217
+ different things, so both are preserved: `nil` means no `only:`/`except:`
218
+ was given at all, while `[]` means one *was* given but names no action this
219
+ controller actually defines -- e.g. a typo or an action that was since
220
+ removed -- so `only: []` never runs and `except: []` never skips), and
221
+ `if`/`unless` (any remaining conditional, by symbol name or `"(block)"`).
222
+ - "What concerns does this controller pull in?" -- same call -- see
223
+ `concerns`: app-defined modules only, whether included directly or
224
+ inherited from a base class; framework modules (`ActionController::Base`
225
+ and everything above it in the ancestor chain) are excluded.
226
+ - Unlike `concerns`, `callbacks[]` is *not* filtered to app code: it is the
227
+ controller's full callback chain, so callbacks Rails itself installs show
228
+ up too -- `verify_authenticity_token` / `verify_same_origin_request` from
229
+ the default forgery protection, and a `"(block)"` entry for macros like
230
+ `allow_browser` that register a Proc. Read the list as "everything that
231
+ runs around an action", not "everything this app wrote".
232
+ - `rails_controller` admits `ActionController::API` controllers as well as
233
+ `ActionController::Base` ones. A malformed constant name raises
234
+ `invalid_controller_name`; a name that doesn't resolve raises
235
+ `controller_not_found`; a name that resolves but isn't an
236
+ `ActionController` descendant (a plain class, a model) raises
237
+ `not_action_controller`.
238
+
239
+ `rails_controller` has one deliberate limitation:
240
+
241
+ - **Strong parameters are not reported.** `params.require(:widget).permit(:name,
242
+ :size)` exists only as code inside a private method body, never as
243
+ queryable class metadata -- the only way to recover a permit-list is to
244
+ parse source, which this gem does not do (see `ROADMAP.md`'s "Considered
245
+ and set aside" section for why source parsing is out of scope generally).
246
+
247
+ Routes drawn by an engine mounted in the application are cross-referenced
248
+ like any other, with the mount point on the path and the engine's class name
249
+ in `engine` -- so a controller that lives inside an engine reports its real
250
+ routes rather than listing every action as unroutable. Their `route_name` is
251
+ engine-local, reached as `<mount name>.<route_name>_path`, exactly as in
252
+ `rails_routes`. Expansion goes one level deep: an engine mounted inside
253
+ another engine stays an opaque mount route, the same boundary `bin/rails
254
+ routes` (and therefore `rails_routes`) draws.
255
+
160
256
  `rails_spec_flaky_check`:
161
257
 
162
258
  - "Is this spec flaky?" -- `rails_spec_flaky_check(paths: ["spec/models/widget_spec.rb"])`
@@ -175,6 +271,8 @@ a no-op.
175
271
  `rails_spec_run` already has), and each round samples a different subset,
176
272
  since execution order varies by design -- for suites this large, narrow
177
273
  `paths`/`example` rather than passing a very broad directory selection.
274
+ - `description` is only present when it differs from `id` (RSpec); Minitest
275
+ entries carry `id` only.
178
276
  - `statuses[]` only aligns positionally with `rounds[]` when no round
179
277
  crashed -- a round whose process itself failed contributes no entry to
180
278
  `statuses[]` (though it still appears in `rounds[]`), so treat positional
@@ -194,6 +292,30 @@ worker). For most apps this is invisible, but if you ever see behavior differ
194
292
  between Coatepec and your own `bundle exec rspec`, this is the first thing to
195
293
  suspect.
196
294
 
295
+ Minitest through `rails_spec_run`:
296
+
297
+ - "Run this Minitest file" -- `rails_spec_run(paths: ["test/models/widget_test.rb"])`;
298
+ the framework is decided by the path, and there is no separate
299
+ Minitest-named tool. `test/models/widget_test.rb:12` runs the one test
300
+ whose definition spans line 12, and a directory runs every `_test.rb`
301
+ under it.
302
+ - `example:` is a substring match on the test's method name
303
+ (`example: "reaches the"` matches `test_reaches_the_database`), passed to
304
+ Minitest as an escaped regex.
305
+ - Results use RSpec's vocabulary so the shape is identical: a Minitest skip
306
+ is `"pending"`, an error is `"failed"` and counts toward
307
+ `summary.failure_count` -- which is why that number can exceed the
308
+ `failures` Minitest prints in `stdout`; `summary.error_count` says how many
309
+ of them were errors, and `summary.assertion_count` is the assertion total
310
+ Minitest prints; `id` is `ClassName#test_method`.
311
+ - One call may not mix `spec/` and `test/` paths (`mixed_test_frameworks`).
312
+ - Rails' parallel testing is disabled in the child (`PARALLEL_WORKERS=1`), so
313
+ a large directory selection runs serially under the one timeout budget
314
+ rather than forking a worker tree.
315
+ - `file:LINE` works even when the app was generated with `--skip-test` (no
316
+ `rails/test_unit/railtie`) or runs Rails 7.1 with Minitest 6, both of which
317
+ break Rails' own line filtering; Coatepec installs its own.
318
+
197
319
  ### Restarts
198
320
 
199
321
  If any tool call fails with `sidecar_restart_required`, the target app's
@@ -214,11 +336,12 @@ it always respawns, even if the current worker looks healthy.
214
336
  ## Security boundary
215
337
 
216
338
  No eval, console, SQL/record access, shell, Rake, or file-write tool. Spec
217
- selectors must resolve inside an allowed spec root (`spec/`, `packs/*/spec/`,
218
- `engines/*/spec/`, `gems/*/spec/`); absolute paths, `..`, symlink escapes,
219
- non-`_spec.rb` files, and more than 100 selectors are rejected. RSpec still
220
- executes application-controlled code; only run Coatepec against a trusted
221
- checkout.
339
+ selectors must resolve inside an allowed spec or test root (`spec/`, `test/`,
340
+ and the `packs/*/`, `engines/*/`, `gems/*/` variants of each); absolute
341
+ paths, `..`, symlink escapes, files that are neither `_spec.rb` under a spec
342
+ root nor `_test.rb` under a test root, and more than 100 selectors are
343
+ rejected. RSpec and Minitest still execute application-controlled code; only
344
+ run Coatepec against a trusted checkout.
222
345
 
223
346
  ### Compared to Rails Active MCP
224
347
 
@@ -231,20 +354,22 @@ sophisticated bypasses of a denylist like that are always possible in
231
354
  principle.
232
355
 
233
356
  Coatepec takes the opposite approach: there's no eval, console, or SQL
234
- tool to begin with. `rails_spec_run` only ever executes RSpec files that
235
- already exist under the app's own allowed spec roots, and `rails_routes`/
236
- `rails_model` only ever call structured, read-only Rails APIs
237
- (`Rails.application.routes.routes`, `ActiveRecord` reflection) -- never
238
- `eval`, `const_get` on unvalidated input, or arbitrary method dispatch. If
357
+ tool to begin with. `rails_spec_run` only ever executes RSpec or Minitest
358
+ files that already exist under the app's own allowed spec/test roots, and `rails_routes`/
359
+ `rails_model`/`rails_controller` only ever call structured, read-only Rails
360
+ APIs (`Rails.application.routes.routes`, `ActiveRecord` reflection,
361
+ `ActionController` callback/action-method metadata) -- never `eval`,
362
+ `const_get` on unvalidated input, or arbitrary method dispatch. If
239
363
  you genuinely need a Rails console over MCP, Rails Active MCP is built for
240
364
  that; Coatepec is for teams who want an agent to run specs and read
241
365
  structure without ever handing it a REPL.
242
366
 
243
367
  ## Compatibility
244
368
 
245
- Ruby `>= 3.2`, Rails `>= 7.1, < 8.2`. CI tests three lanes: Rails 8.1 on
246
- Linux (primary), Rails 7.1 on Linux (compat), and Rails 8.1 on macOS (which
247
- is where the guarded-fork path above actually forks).
369
+ Ruby `>= 3.2`, Rails `>= 7.1, < 8.2`, Minitest 5.x and 6.x (the fixture
370
+ apps pin 6.0.6; the 5.x name-filter flag is unit-tested). CI tests three
371
+ lanes: Rails 8.1 on Linux (primary), Rails 7.1 on Linux (compat), and Rails
372
+ 8.1 on macOS (which is where the guarded-fork path above actually forks).
248
373
 
249
374
  ## Development
250
375
 
data/ROADMAP.md ADDED
@@ -0,0 +1,163 @@
1
+ # Roadmap
2
+
3
+ Ideas and known future work for Coatepec, roughly in the order they came up.
4
+ Nothing here is committed to a release; this is a place to write things down
5
+ before they're designed, not a promise.
6
+
7
+ ## Minitest support -- shipped in 0.8.0
8
+
9
+ Shipped through the existing `rails_spec_run`/`rails_spec_flaky_check`
10
+ tools. The open questions resolved as: one tool, dispatching by
11
+ *selector shape* (`test/**/*_test.rb` vs `spec/**/*_spec.rb`) rather
12
+ than by Gemfile, since that is per-request and unambiguous for apps
13
+ with both; structured per-test output comes from an in-process
14
+ `Minitest::AbstractReporter` writing RSpec's JSON shape, so
15
+ `Spec::Result` and `FlakyChecker` were reused unchanged; and Minitest's
16
+ `--seed` is a direct equivalent (16-bit, random order by default), so
17
+ the flaky checker works as-is. See
18
+ `docs/superpowers/specs/2026-09-05-minitest-support-design.md` (local-only)
19
+ for the design and the line-filtering finding.
20
+
21
+ ## Get stats on a spec/test run (TestProf / FactoryProf) — designed, implemented, paused
22
+
23
+ The general idea: a tool that runs a spec selection and comes back with
24
+ more than pass/fail — profiling data about the run itself (factory
25
+ creation counts/timing being the concrete first target, via
26
+ [TestProf](https://test-prof.evilmartians.io/)'s FactoryProf profiler,
27
+ which is the one TestProf profiler with real structured JSON output;
28
+ others like EventProf are text-only and out of scope for now).
29
+
30
+ A `rails_spec_profile` tool (runs a spec with TestProf's FactoryProf
31
+ enabled, returning factory usage stats alongside normal pass/fail data)
32
+ was fully designed and implemented, but paused before merging: TestProf's
33
+ `FPROF` env var only activates at Ruby's `require` time, not at RSpec-run
34
+ time as originally assumed, which makes it silently a no-op on
35
+ `ForkStrategy` (Linux's default) — confirmed empirically, not just
36
+ inferred. See `docs/superpowers/specs/2026-08-13-factory-prof-tool-design.md`
37
+ (local-only, not committed — see that repo's `.gitignore`) for the full
38
+ root-cause writeup and the fix options considered (forcing `SpawnStrategy`
39
+ for profiled runs specifically, vs. detecting and raising a clear error on
40
+ non-spawn strategies, vs. a Linux-only opt-out config knob). Paused
41
+ specifically to wait for real signal on how coatepec is actually used
42
+ (Linux vs. macOS, `macos_fork` adoption) before picking a fix, rather than
43
+ guessing.
44
+
45
+ ## Run Rails 8.1's built-in CI (`bin/ci`)
46
+
47
+ Rails 8.1 introduced `ActiveSupport::ContinuousIntegration`
48
+ (`activesupport/lib/active_support/continuous_integration.rb`) as the
49
+ engine behind a generated `bin/ci`/`config/ci.rb`: a small DSL (`step`,
50
+ `group`) that a new app's `config/ci.rb` uses to declare, by default,
51
+ `bin/setup`, `bin/rubocop`, `bin/bundler-audit`, `bin/importmap audit`
52
+ (if using importmap), `bin/brakeman --quiet --no-pager --exit-on-warn
53
+ --exit-on-error`, and the test suite -- the same steps the generated
54
+ GitHub Actions workflow runs, since that workflow just calls `bin/ci`.
55
+ A coatepec tool that ran this and reported back which steps
56
+ passed/failed would cover security audits (Brakeman, bundler-audit,
57
+ importmap audit) and style checks in one call, for any app that has
58
+ adopted this (Rails 8.1+ only -- confirmed against the actual generator
59
+ template and `ContinuousIntegration` source, not a blog summary).
60
+
61
+ **The real design obstacle, found while researching this (not yet
62
+ solved):** `ContinuousIntegration` has no structured output at all. Each
63
+ `step` runs via plain `system(*command)` and writes colorized terminal
64
+ text (`✅ Title passed in 1.2s` / `❌ Title failed in 1.2s`); the only
65
+ machine-readable signal is the overall process exit code
66
+ (`abort unless success?`). Two options for whenever this gets designed:
67
+ scrape that text (fragile -- it's an internal, unversioned Rails string
68
+ format, not a documented API), or require the target app's `config/ci.rb`
69
+ in-process and read `ContinuousIntegration#results` (an array of
70
+ `[success, title]` pairs) directly, intercepting the `abort` that
71
+ normally follows a failure -- more work, but reads real data instead of
72
+ parsing text designed for a terminal.
73
+
74
+ Also worth deciding: whether this becomes its own tool (`rails_ci_run`?)
75
+ or folds into an existing one, and whether "security audits" specifically
76
+ (Brakeman/bundler-audit/importmap audit, independent of the rest of
77
+ `bin/ci`) are worth exposing as a narrower, separate tool for apps that
78
+ don't use Rails 8.1's `bin/ci` scaffold at all but do have those gems.
79
+
80
+ ## Controller introspection -- shipped in 0.7.0
81
+
82
+ Shipped as `rails_controller` (`Coatepec::Introspection::Controller`) in
83
+ 0.7.0: actions, `before`/`after`/`around` callbacks with their `only`/`except`
84
+ restrictions and `if`/`unless` conditions, included concerns, and a
85
+ cross-reference of every action against `Rails.application.routes`
86
+ (`unroutable_actions` / `routes_without_action`). Strong parameters were
87
+ deliberately left out: a `permit` list exists only as code inside a method
88
+ body, and recovering it would mean source parsing (see "Considered and set
89
+ aside" below). See the README and CHANGELOG for the full contract.
90
+
91
+ ## Background job introspection -- built, then parked (PR #14, closed unmerged)
92
+
93
+ The original idea: a `rails_job`-style tool for `ActiveJob` classes --
94
+ queue name, retry/discard configuration, and callbacks -- following the
95
+ same bounded, read-only, real-API-not-source-parsing pattern as
96
+ `rails_model`, since coatepec has no visibility into background jobs at
97
+ all today.
98
+
99
+ It was fully built and reviewed as `rails_job`
100
+ (`Coatepec::Introspection::Job`), then **closed unmerged** in
101
+ [PR #14](https://github.com/mogox/coatepec/pull/14) -- the diff and its
102
+ review history stay there, so nothing needs re-deriving if this is picked
103
+ back up.
104
+
105
+ **Why it was parked:** the tool only sees `ActiveJob::Base` subclasses.
106
+ That's not an implementation shortcut -- it's the trust boundary doing its
107
+ job: the whole design reads real Rails introspection APIs
108
+ (`_perform_callbacks`, `rescue_handlers`, the `queue_name`/`priority` class
109
+ attributes) rather than parsing source, and those APIs exist only on
110
+ ActiveJob. An app whose jobs are native Sidekiq (`include Sidekiq::Job`)
111
+ or Delayed::Job classes gets `:not_active_job` and nothing else, because
112
+ those classes genuinely aren't ActiveJob jobs. (Sidekiq used *as the
113
+ ActiveJob queue adapter* is fine -- those jobs still subclass
114
+ `ApplicationJob`; it's hand-written Sidekiq/delayed_job worker classes
115
+ that fall outside.) Covering them would mean a second, adapter-specific
116
+ introspection path per backend -- `sidekiq_options` for retry/queue/dead,
117
+ `Delayed::Worker` config and `handle_asynchronously` for delayed_job --
118
+ which is a materially bigger design than the ActiveJob one, not a small
119
+ extension of it. Given a limited number of iterations to spend, that
120
+ budget goes to features usable across more real apps first.
121
+
122
+ Two constraints worth keeping if this is revisited:
123
+ - Even within ActiveJob, `retry_on` / `discard_on` / plain `rescue_from`
124
+ are indistinguishable, and neither macro's `wait:`/`attempts:`/`queue:`/
125
+ `priority:` options are introspectable -- ActiveJob closes over them
126
+ inside a Proc rather than storing them as class metadata. Only the
127
+ *list* of rescued exception classes is genuinely queryable.
128
+ - `queue_as { ... }` and `queue_with_priority { ... }` store unevaluated
129
+ app blocks. Reading them naively either executes app code at
130
+ introspection time or leaks the app's absolute source paths through
131
+ `Proc#to_s`. PR #14 has the fix for both; any future version needs the
132
+ same care.
133
+
134
+ ## Cross-file consistency validation
135
+
136
+ Distinct from anything coatepec does today: a tool that checks for drift
137
+ *across* files rather than introspecting one thing at a time -- a
138
+ `belongs_to`/`has_many` referencing a column or table that isn't in the
139
+ schema, that kind of thing. (The route-to-missing-action case is already
140
+ covered per controller by `rails_controller`'s `routes_without_action`,
141
+ shipped in 0.7.0; an app-wide sweep of the whole route table would be the
142
+ cross-file version of that same check.)
143
+ Surfaced while researching prior art (a competing tool does this via
144
+ source-code parsing); would need its own design for how to do it via
145
+ structured Rails APIs instead, consistent with how every other coatepec
146
+ tool avoids parsing source directly.
147
+
148
+ ## Considered and set aside
149
+
150
+ - **Environment variable / credentials discovery** (a prior-art tool
151
+ exposes this). Cuts directly against coatepec's "no credential access"
152
+ security boundary -- not a fit regardless of usefulness.
153
+ - **AST-based code pattern analysis** (concerns, callbacks, service
154
+ objects, helper methods, by parsing source rather than calling real
155
+ Rails APIs). A meaningfully different, heavier, more fragile approach
156
+ than every existing coatepec tool takes. Not ruled out forever, but a
157
+ clear departure from the project's current trust model, not a natural
158
+ extension of it.
159
+ - **Rails dev server lifecycle management** (start/stop/monitor `rails s`
160
+ via MCP -- a different tool in the ecosystem does exactly this). A
161
+ different category of feature (process lifecycle, not test/introspection)
162
+ from everything else on this list; noted here for completeness, not
163
+ actively being considered.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coatepec
4
+ module Introspection
5
+ # Caps the array-valued entries of a sanitized validator options hash (see SafeOptions) for rails_model output:
6
+ # 249 country codes just say "allow-list", and the count plus flag beside a cut list keep it self-describing.
7
+ module BoundedOptions
8
+ module_function
9
+
10
+ def call(options, max)
11
+ options.each_with_object({}) do |(key, value), bounded|
12
+ bounded[key] = value
13
+ next unless value.is_a?(Array) && value.size > max
14
+
15
+ bounded[key] = value.first(max)
16
+ bounded["#{key}_count"] = value.size
17
+ bounded["#{key}_truncated"] = true
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end