rails-ai-context 5.28.0 → 5.29.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/CHANGELOG.md +179 -0
- data/CONTEXT.md +37 -0
- data/docs/INTROSPECTORS.md +16 -3
- data/docs/TOOLS.md +31 -12
- data/docs/TROUBLESHOOTING.md +13 -0
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +6 -11
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +29 -7
- data/lib/rails_ai_context/introspectors/interaction.rb +127 -0
- data/lib/rails_ai_context/introspectors/job_introspector.rb +18 -1
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +34 -0
- data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +15 -0
- data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +110 -20
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +11 -2
- data/lib/rails_ai_context/introspectors/model_introspector.rb +42 -10
- data/lib/rails_ai_context/introspectors/route_introspector.rb +49 -16
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +30 -0
- data/lib/rails_ai_context/introspectors/superclass_chain.rb +98 -0
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +26 -0
- data/lib/rails_ai_context/payload.rb +10 -1
- data/lib/rails_ai_context/resources.rb +1 -1
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +4 -3
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +4 -2
- data/lib/rails_ai_context/tools/analyze_feature.rb +30 -2
- data/lib/rails_ai_context/tools/base_tool.rb +28 -0
- data/lib/rails_ai_context/tools/dependency_graph.rb +17 -10
- data/lib/rails_ai_context/tools/diagnose.rb +31 -0
- data/lib/rails_ai_context/tools/generate_test.rb +31 -24
- data/lib/rails_ai_context/tools/get_concern.rb +103 -1
- data/lib/rails_ai_context/tools/get_config.rb +8 -2
- data/lib/rails_ai_context/tools/get_context.rb +31 -13
- data/lib/rails_ai_context/tools/get_controllers.rb +3 -1
- data/lib/rails_ai_context/tools/get_engines.rb +9 -6
- data/lib/rails_ai_context/tools/get_env.rb +58 -15
- data/lib/rails_ai_context/tools/get_job_pattern.rb +44 -13
- data/lib/rails_ai_context/tools/get_model_details.rb +36 -9
- data/lib/rails_ai_context/tools/get_routes.rb +39 -5
- data/lib/rails_ai_context/tools/get_schema.rb +38 -4
- data/lib/rails_ai_context/tools/get_service_pattern.rb +85 -41
- data/lib/rails_ai_context/tools/get_view.rb +53 -19
- data/lib/rails_ai_context/tools/onboard.rb +15 -6
- data/lib/rails_ai_context/tools/search_code.rb +10 -4
- data/lib/rails_ai_context/tools/security_scan.rb +243 -53
- data/lib/rails_ai_context/tools/validate_semantics.rb +7 -0
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +25 -1
- data/server.json +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f036023367c149fd14eb8bc96106167cbd0c4ec7f042673930c74472a7caf44e
|
|
4
|
+
data.tar.gz: 1289481c7e5749263b762ebb9cc41a25504b4355896fd2215d2916e669132b34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ad0b4e2f8096894ceec5a5c9989472173e8fabd98c38b1e9fc5e4e0ac3b7d90ad49473d6808f592f5d5b0c6ca9c6a897d7d2d6d79a064bbff0b6a44da19a0aa
|
|
7
|
+
data.tar.gz: ea624bf293f2e40a49d0d62579ca10c224200ab9e0f6681cdeb3f62955a599024a9ced4179f701f907d0b6ab9c55d5240ba595243a428b7b3920d47aade0b8d2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,185 @@ 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.29.0] - 2026-09-23
|
|
9
|
+
|
|
10
|
+
Twenty-three QA reports against v5.27.0, each one a wrong answer a reader
|
|
11
|
+
could act on, plus what eight review rounds found in the fixes themselves.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Model payloads carry method counts and the model's own methods
|
|
16
|
+
uncapped.** `instance_method_count`, `class_method_count` and
|
|
17
|
+
`source_instance_methods` sit beside the capped `instance_methods` and
|
|
18
|
+
`class_methods` lists, on both tiers, so a reader can tell a cut list from
|
|
19
|
+
a whole one.
|
|
20
|
+
- **The schema payload carries `declared_tables`**, what `db/schema.rb`
|
|
21
|
+
declares, on both tiers: nil for an app whose tables come from
|
|
22
|
+
`structure.sql` or the migrations.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **"Mounted Engines" is "Mounted Apps" everywhere it is printed**: the
|
|
27
|
+
`engines`, `routes` and `onboard` tools, the generated context files, and
|
|
28
|
+
the MCP `rails://engines` resource name. The stack overview line reads
|
|
29
|
+
`Mounted:` rather than `Engines:`. Half of what the list holds are plain
|
|
30
|
+
Rack apps. The payload key stays `mounted_engines`.
|
|
31
|
+
- **A mount whose path the source does not spell out carries `path: nil`**
|
|
32
|
+
rather than the string `"unknown"`, and is printed without a path.
|
|
33
|
+
- **`routes` reads a blank `controller` as no filter**, and a name that
|
|
34
|
+
normalizes to nothing (`_controller`) as matching nothing, where the empty
|
|
35
|
+
string used to match every route key.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **A Sidekiq worker answers to its own name, and the bracket carries the
|
|
40
|
+
limit that governs it.** `job:"Billing::Invoices::CreateWorker"` answered
|
|
41
|
+
"No jobs found" for a worker the same tool had just listed, because the
|
|
42
|
+
single-job lookup read the ActiveJob list only. It reads both lists now, and
|
|
43
|
+
a name in neither is a not-found that names what exists. The listing prints
|
|
44
|
+
its "workers the introspector did not see" caveat whenever it prints
|
|
45
|
+
workers, rather than only when `config/sidekiq.yml` happens to exist, and a
|
|
46
|
+
`sidekiq_throttle` prints under the worker it throttles - 438 of 522 workers
|
|
47
|
+
on one app declared one and none of them showed it.
|
|
48
|
+
- **onboard's async section reads the workers out of the hash it already
|
|
49
|
+
had.** The section counted jobs, mailers and channels, so an app whose
|
|
50
|
+
background work is 522 Sidekiq workers read as "4 mailers." and the section
|
|
51
|
+
disappeared entirely when workers were the only async code. The "not
|
|
52
|
+
covered" line no longer prints next to a worker list it contradicts.
|
|
53
|
+
- **What an ActiveInteraction declares is read in one place.** A filter
|
|
54
|
+
declared inside another filter's block (`string :title` inside `hash
|
|
55
|
+
:order_params do`) is a key of that hash, not an input of the class:
|
|
56
|
+
`service_pattern` showed four inputs where `.filters` has two, and
|
|
57
|
+
`generate_test` passed the other two to `.run`, which drops them silently. A
|
|
58
|
+
subclass of the app's own base interaction is an interaction too, with its
|
|
59
|
+
parent's filters first, so `generate_test` stops emitting `.call`, which
|
|
60
|
+
`ActiveInteraction::Base` does not define. `GenericMacroListener` records
|
|
61
|
+
each call's own offset and its enclosing call's, which is what both tools
|
|
62
|
+
pair filters by.
|
|
63
|
+
- **generate_test names the constant the file declares.** A path camelizes
|
|
64
|
+
through Ruby's inflector, which has not read the app's
|
|
65
|
+
`config/initializers/inflections.rb` on the static tier, so
|
|
66
|
+
`ai_reports/build.rb` gave `AiReports::Build` where the app defines
|
|
67
|
+
`AIReports::Build` - a constant nothing defines, in a spec that dies on
|
|
68
|
+
load.
|
|
69
|
+
- **service_pattern looks for callers where the app keeps code.** The scan
|
|
70
|
+
named six `app/` directories, so a caller in `app/tools` or under `lib/` was
|
|
71
|
+
invisible. It reads every `app/` and `lib/` tree, plus whatever else a
|
|
72
|
+
booted app autoloads from, and says when the twenty-entry cap or its own
|
|
73
|
+
file ceiling left the list partial.
|
|
74
|
+
- **schema tells a missing migration from a typo.** Booted, a table declared
|
|
75
|
+
in `db/schema.rb` and absent from the connected database answered "Did you
|
|
76
|
+
mean 'comments'?". The payload carries the declared tables beside the live
|
|
77
|
+
ones, so the answer names the migration that has not run, and the listing
|
|
78
|
+
header says the two counts disagree instead of pairing a live table count
|
|
79
|
+
with the file's version stamp.
|
|
80
|
+
- **A validator under app/models/concerns is not a concern.** The type came
|
|
81
|
+
from the directory alone, so 37 `ActiveModel::Validator` subclasses on one
|
|
82
|
+
app were listed as model concerns used by nothing. They are listed as
|
|
83
|
+
validators - following the app's own validator base class, not one level of
|
|
84
|
+
compare - and looked up by the `validates_with`, or the validation option,
|
|
85
|
+
that wires them, in a model or in a concern's `included` block. The keys
|
|
86
|
+
`validates` reads for itself (`on:`, `if:` and the like) name no
|
|
87
|
+
validator.
|
|
88
|
+
- **dependency_graph counts both header numbers over the same models.** The
|
|
89
|
+
model count was app-wide and the association count covered the fifty nodes
|
|
90
|
+
that survived the cap. Both are app-wide now, and the truncation note says
|
|
91
|
+
how many of the associations the cut graph draws.
|
|
92
|
+
- **get_context reads the views Rails would resolve.** It handed `GetView`
|
|
93
|
+
the last segment of the controller path, so `Api::V1::Admin::OrdersController`
|
|
94
|
+
picked up `app/views/orders`, a directory of templates a background service
|
|
95
|
+
renders. The answer names the directory its views came from, and a
|
|
96
|
+
flat-directory fallback is labelled as one.
|
|
97
|
+
- **analyze_feature finds a test by its path.** A spec whose feature word is a
|
|
98
|
+
directory (`spec/services/billing/invoices/create_spec.rb`) was dropped,
|
|
99
|
+
while the gap checker beside it already matched on the path. The suite's
|
|
100
|
+
own words stay out of the match - the `spec/` root, the type directory it
|
|
101
|
+
files a test under (`models/`, `requests/`), and the `_spec` suffix - so
|
|
102
|
+
`--feature models` is not every model spec.
|
|
103
|
+
- **routes answers an exact controller key with its own routes.** A substring
|
|
104
|
+
filter returned a nested sibling's routes too (`api/v1/admin/orders` swept
|
|
105
|
+
in `api/v1/admin/orders/ai_data`), and `get_context` inherited it. A short
|
|
106
|
+
name still matches every controller that carries it.
|
|
107
|
+
- **A template at the root of app/views is listed.** Its filename became a
|
|
108
|
+
directory group that matched nothing, so the header counted a file the body
|
|
109
|
+
never printed, and the controller-miss hint suggested a directory that does
|
|
110
|
+
not exist.
|
|
111
|
+
- **A word in a quoted string is not an instance variable.** `view` reported
|
|
112
|
+
a chat handle inside a Ruby string literal as a template's ivar; the reader
|
|
113
|
+
strips string literals and keeps interpolation, in ERB tags and in the
|
|
114
|
+
Ruby template handlers (Jbuilder, Builder, `.ruby`), and `get_view`'s
|
|
115
|
+
hydrator reads through the same method. Whichever quote opens first owns
|
|
116
|
+
the literal, so an apostrophe inside `"Don't"` does not swallow the ivar
|
|
117
|
+
beside it.
|
|
118
|
+
- **env_config tells a re-assignment from a tuple.** Two unconditional
|
|
119
|
+
assignments of one key rendered as `:file, :test`, which reads exactly like
|
|
120
|
+
`:mem_cache_store, { pool_size: 5 }`. The winner is named, with what it
|
|
121
|
+
overrode.
|
|
122
|
+
- **env keeps one default per call site.** One label for every site said a
|
|
123
|
+
variable was optional while one of its reads was `ENV.fetch` with no
|
|
124
|
+
default, which raises `KeyError`. A fetch whose fallback is an expression
|
|
125
|
+
says its default is computed at runtime rather than claiming it has none,
|
|
126
|
+
and an `ENV["X"]` read says it is nil when unset, apart from the fetch
|
|
127
|
+
that raises.
|
|
128
|
+
- **A Rack app attached with `match ... to:` is found.** `mount` is that call
|
|
129
|
+
with a name derived, and the exact-path form is what an app writes when an
|
|
130
|
+
unanchored mount would swallow a sibling path. It reaches `engines` and
|
|
131
|
+
`routes`, which names the mounted apps it counts instead of calling them
|
|
132
|
+
engine mounts, and the booted tier lists every Rack endpoint rather than
|
|
133
|
+
`Rails::Engine` subclasses alone. A mount inside a `namespace` or `scope`
|
|
134
|
+
carries that prefix; one whose enclosing scope, or its `path:`, is an expression is listed
|
|
135
|
+
with no path rather than an unprefixed one, and every list prints it
|
|
136
|
+
without one. `engines` follows every file `config/routes.rb` draws,
|
|
137
|
+
through the walk the static `routes` answer uses, so on the static tier
|
|
138
|
+
the two name one set of mounted apps; booted, `routes` reads the live
|
|
139
|
+
route table. `engines`, `onboard`, the MCP resource and the
|
|
140
|
+
generated context files head the same list "Mounted Apps", because half of
|
|
141
|
+
what it holds are not engines. On the static tier a `match ... to: SomeApp`
|
|
142
|
+
is counted once, as the mount it is, rather than also as a construct the
|
|
143
|
+
walk could not expand, and so are `get "/status" => StatusApp` and
|
|
144
|
+
`mount ActionCable.server => "/cable"`, which the walk did not see at all.
|
|
145
|
+
- **config calls a zero-byte initializer empty** rather than "all commented
|
|
146
|
+
out".
|
|
147
|
+
- **Smaller corrections in the same pass.** `validate_semantics` asks the
|
|
148
|
+
loaded model class before calling a callback method missing, and on the
|
|
149
|
+
static tier makes no claim once the payload's method list was cut at its
|
|
150
|
+
cap, so an inherited method past the cap is no longer reported as missing.
|
|
151
|
+
`get_controllers` points
|
|
152
|
+
`rails_get_view` at the controller's full path rather than its last segment,
|
|
153
|
+
which is the directory Rails resolves. The stack overview's `Engines:` line
|
|
154
|
+
is `Mounted:`, because half of what it lists are plain Rack apps.
|
|
155
|
+
- **diagnose stops reading a display cap as a model's whole interface.** A
|
|
156
|
+
method past the thirtieth was reported as not existing, in the same answer
|
|
157
|
+
whose Method Trace printed its definition. Booted, diagnose asks the loaded
|
|
158
|
+
model class, which knows a concern's methods and a gem's as well as the
|
|
159
|
+
model's own. Statically, the model's own methods travel uncapped beside the
|
|
160
|
+
capped display list, and where a concern or a parent could define the
|
|
161
|
+
method the answer declines rather than guesses. `model_details` says how
|
|
162
|
+
many of the model's methods it is showing.
|
|
163
|
+
- **A CamelCase controller name resolves everywhere.** The needle was
|
|
164
|
+
downcased without being underscored, so "GiftCards" never equalled the
|
|
165
|
+
route key's own "gift_cards": `Payload.find_controller` missed it, and every
|
|
166
|
+
tool that resolves a controller through it missed it too. One normalization
|
|
167
|
+
now serves the payload, the routes tool and the MCP resource, and the
|
|
168
|
+
resource answers a name that resolves to nothing with an error naming what
|
|
169
|
+
exists rather than a zero-route success document.
|
|
170
|
+
- **security_scan runs the brakeman the machine has.** The app's bundle
|
|
171
|
+
narrows the load path, so a machine with brakeman installed was told to add
|
|
172
|
+
it to the Gemfile while the other tier scanned the same app. When the
|
|
173
|
+
in-process require fails and the gem is installed, the scan runs it as its
|
|
174
|
+
own process outside the bundle - reading the report from a file of its own,
|
|
175
|
+
since a gem manager's binstub can print to stdout first - and renders the
|
|
176
|
+
result the same way, with a line saying which brakeman answered and from
|
|
177
|
+
where. When the outside run writes no report, the answer carries the last
|
|
178
|
+
line brakeman printed about why. With no brakeman anywhere, the message
|
|
179
|
+
says that instead of guessing, and the availability answer is keyed by
|
|
180
|
+
tier rather than decided once per process.
|
|
181
|
+
- **An exact search with a space keeps its context lines on ripgrep 13.**
|
|
182
|
+
The literal was escaped with Ruby's `\ `, which ripgrep 13 (Ubuntu 22.04,
|
|
183
|
+
Debian 12) rejects, so the search fell back to the Ruby scan and dropped
|
|
184
|
+
context lines and files with no listed extension. The space goes through
|
|
185
|
+
unescaped now, which both engines read the same way.
|
|
186
|
+
|
|
8
187
|
## [5.28.0] - 2026-09-22
|
|
9
188
|
|
|
10
189
|
Thirty-six changes an architecture survey of v5.27.0 asked for, nine of them
|
data/CONTEXT.md
CHANGED
|
@@ -88,6 +88,43 @@ Three senses inside the gem, and the payload one is wider than either everyday R
|
|
|
88
88
|
|
|
89
89
|
The callable interface of a class as this gem reports it: the class's own public instance methods - a class nested in the same file is a separate owner, not part of the interface - minus framework-shaped `_` names, read source-first, with reflection minus the app-owned base as the honest fallback. `ActionResolver` is the one answer; controller and mailer are configurations of it, and a channel's "stream methods" are a narrower selection of the same reading.
|
|
90
90
|
|
|
91
|
+
## Mounted app
|
|
92
|
+
|
|
93
|
+
A Rack app the routing table attaches at a path, engine or not. `mount App =>
|
|
94
|
+
"/path"` is `match("/path", to: App, via: :all, anchor: false)` with a name
|
|
95
|
+
derived, so both spellings build the same endpoint and both are this. The
|
|
96
|
+
payload key is `mounted_engines` for the sections that predate the widening;
|
|
97
|
+
what it holds is every controller-less, non-dynamic endpoint the route set
|
|
98
|
+
carries, which is what the count beside it has always counted.
|
|
99
|
+
|
|
100
|
+
Distinct from a **Rails engine**, which is a `Rails::Engine` subclass whether
|
|
101
|
+
or not anything mounts it, and which `rails_get_engines` lists separately under
|
|
102
|
+
its loaded classes. The record key is `engine:` for the same reason the section
|
|
103
|
+
key is `mounted_engines:` - it predates the widening, and what it holds is the
|
|
104
|
+
mounted app's constant, engine or not.
|
|
105
|
+
|
|
106
|
+
## Interaction filter
|
|
107
|
+
|
|
108
|
+
What an ActiveInteraction service declares as its interface, and not the
|
|
109
|
+
**filter chain** above, which is controllers. `Interaction` is the one answer
|
|
110
|
+
for both tools that read it (`rails_get_service_pattern`,
|
|
111
|
+
`rails_generate_test`), because a static answer that differs from the booted
|
|
112
|
+
one is the divergence the module exists to end.
|
|
113
|
+
|
|
114
|
+
**A nested filter is not an input.** `string :title` inside `hash
|
|
115
|
+
:order_params do ... end` is a key of that hash: it never reaches `.filters`,
|
|
116
|
+
and handing it to `.run` as a keyword argument is dropped without a word. So
|
|
117
|
+
nesting is kept on the record rather than flattened, and only the top level is
|
|
118
|
+
an input.
|
|
119
|
+
|
|
120
|
+
**The chain is the class's, not the file's.** An interaction is often a
|
|
121
|
+
subclass of the app's own base interaction rather than of
|
|
122
|
+
`ActiveInteraction::Base`, and its filters are then the parent's first and its
|
|
123
|
+
own after, which is the order `.filters` answers in. Following that chain needs
|
|
124
|
+
the parent's source, which only the caller can find, so it arrives as
|
|
125
|
+
`lookup` - a callable from a constant name to that class's source, and nil to
|
|
126
|
+
stop at the one file.
|
|
127
|
+
|
|
91
128
|
## Filter chain
|
|
92
129
|
|
|
93
130
|
Which filters a controller runs, and which of them a given action runs. `ActionFilters` is the one answer, and every controller surface reads its filter line from there, so no two answers can disagree about what a class inherits or skips. `for_controller` answers about the class; `for` answers about one of its actions. Both return `own`, `inherited` and `skipped`.
|
data/docs/INTROSPECTORS.md
CHANGED
|
@@ -126,7 +126,7 @@ end
|
|
|
126
126
|
|
|
127
127
|
| Introspector | Key | What it extracts |
|
|
128
128
|
|:-------------|:----|:-----------------|
|
|
129
|
-
| JobIntrospector | `:jobs` | Background jobs, Sidekiq workers under `app/workers`, and mailers: queue, retries, `sidekiq_options`, schedules, and the `file:` each one is defined in |
|
|
129
|
+
| JobIntrospector | `:jobs` | Background jobs, Sidekiq workers under `app/workers`, and mailers: queue, retries, `sidekiq_options`, any `sidekiq_throttle`, schedules, and the `file:` each one is defined in |
|
|
130
130
|
| RakeTaskIntrospector | `:rake_tasks` | Custom rake tasks |
|
|
131
131
|
|
|
132
132
|
### Security & Auth
|
|
@@ -193,7 +193,7 @@ Passed to `SourceIntrospector.walk(path, key => Listener)` when a specific file
|
|
|
193
193
|
|
|
194
194
|
| Listener | What it detects |
|
|
195
195
|
|:---------|:---------------|
|
|
196
|
-
| GenericMacroListener | Any receiver-less macro you name: `GenericMacroListener.new(:devise, :rate_limit)`. Returns args, values (with a source-slice fallback), options, option values and option nodes |
|
|
196
|
+
| GenericMacroListener | Any receiver-less macro you name: `GenericMacroListener.new(:devise, :rate_limit)`. Returns args, values (with a source-slice fallback), options, option values and option nodes, plus the nesting: `parent_offset` is the offset of the target macro call whose block this one sits in, paired against each call's own `offset` rather than its line |
|
|
197
197
|
| ChainedCallListener | Calls on a receiver: `ChainedCallListener.new(:includes)`, or `receiver: :inflect` to pin the receiver. Reports the receiver name |
|
|
198
198
|
| ConfigAssignmentListener | `config.key = value` and `config.a.b = value` in initializers and `config/environments/*.rb`, plus bare `config.jwt do ... end` section references. Takes a root name (`:config` by default, e.g. `:DatabaseCleaner`) |
|
|
199
199
|
| ClassDefinitionListener | Class definitions with their superclass, namespaces resolved |
|
|
@@ -202,7 +202,7 @@ Passed to `SourceIntrospector.walk(path, key => Listener)` when a specific file
|
|
|
202
202
|
| SchemaDslListener | `schema.rb`: `create_table`, `t.string`, `t.index`, `add_foreign_key`, `create_enum` |
|
|
203
203
|
| MigrationDslListener | Migration DSL: `create_table`, `add_column`, `add_index`, `add_reference`, and friends |
|
|
204
204
|
| RoutesDslListener | `config/routes.rb`, resolving namespace/scope/resources nesting into flat routes; routing concerns (`concern` definitions replayed at each `concerns:` site), `with_options` defaults merged under each inner call, and the `as:`, `param:`, `module:`, `path:` and `only:`/`except:` options |
|
|
205
|
-
| MountListener | `mount Sidekiq::Web, at: "/sidekiq"
|
|
205
|
+
| MountListener | `mount Sidekiq::Web, at: "/sidekiq"`, the hash form, and a Rack app attached with `match "/metrics", to: MetricsApp` - `mount` is that call with a name derived. Paths carry the enclosing `namespace`/`scope` prefix; a scope whose own name is an expression yields no path rather than an unprefixed one |
|
|
206
206
|
| GemfileDslListener | `gem "name", "version"` and `group :development do ... end` |
|
|
207
207
|
| RakeTaskDslListener | `namespace`, `desc`, `task` in `.rake` files |
|
|
208
208
|
| EnvAccessListener | `ENV["KEY"]`, `ENV.fetch("KEY")`, `ENV.fetch("KEY", default)` |
|
|
@@ -233,6 +233,19 @@ Regex is the right tool, and stays, for:
|
|
|
233
233
|
|
|
234
234
|
Every remaining regex over `.rb` content carries a one-line comment saying which of these it is. If you add one without a reason, convert it instead.
|
|
235
235
|
|
|
236
|
+
### Readers built on the listeners
|
|
237
|
+
|
|
238
|
+
Some questions take more than one walk to answer, and the answer has to be the
|
|
239
|
+
same wherever it is asked. Those live as their own modules under
|
|
240
|
+
`Introspectors/`, and a tool calls one rather than repeating the walk:
|
|
241
|
+
|
|
242
|
+
| Module | What it answers |
|
|
243
|
+
|:-------|:---------------|
|
|
244
|
+
| `DeclaredConstant` | The constant a source file calls its own class, against the one its path camelizes to |
|
|
245
|
+
| `TableName` | The table a model reads, from its own declarations |
|
|
246
|
+
| `SuperclassChain` | What a class inherits from, followed through the app's own sources: the chain from a file's class up to a named base, and the constant-to-source lookup over the app's autoload roots that walks it |
|
|
247
|
+
| `Interaction` | Whether a class runs as an ActiveInteraction, following its superclass chain through the app's own sources, and the filters it takes - inherited ones first, one per name, each carrying the filters nested inside its block. See the **Interaction filter** entry in `CONTEXT.md` |
|
|
248
|
+
|
|
236
249
|
### Confidence tagging
|
|
237
250
|
|
|
238
251
|
Every AST result carries a confidence tag:
|
data/docs/TOOLS.md
CHANGED
|
@@ -116,7 +116,7 @@ Full-stack feature analysis: models + controllers + routes + services + jobs + v
|
|
|
116
116
|
|
|
117
117
|
### `rails_get_context`
|
|
118
118
|
|
|
119
|
-
Composite context: schema + model + controller + routes + views for a resource.
|
|
119
|
+
Composite context: schema + model + controller + routes + views for a resource. Views come from the directory Rails resolves for the controller; a flat-directory fallback is labelled.
|
|
120
120
|
|
|
121
121
|
| Parameter | Type | Default | Description |
|
|
122
122
|
|:----------|:-----|:--------|:------------|
|
|
@@ -140,7 +140,10 @@ Narrative app walkthrough for getting up to speed.
|
|
|
140
140
|
|
|
141
141
|
### `rails_get_schema`
|
|
142
142
|
|
|
143
|
-
Database schema with column types, indexes, defaults, encrypted hints.
|
|
143
|
+
Database schema with column types, indexes, defaults, encrypted hints. Booted,
|
|
144
|
+
a table `db/schema.rb` declares and the connected database does not have is
|
|
145
|
+
named as a migration that has not run, and the listing header says when the
|
|
146
|
+
two table counts disagree.
|
|
144
147
|
|
|
145
148
|
| Parameter | Type | Default | Description |
|
|
146
149
|
|:----------|:-----|:--------|:------------|
|
|
@@ -149,7 +152,7 @@ Database schema with column types, indexes, defaults, encrypted hints.
|
|
|
149
152
|
|
|
150
153
|
### `rails_get_model_details`
|
|
151
154
|
|
|
152
|
-
AST-parsed model internals. Every result carries `[VERIFIED]` or `[INFERRED]` confidence tag.
|
|
155
|
+
AST-parsed model internals. Every result carries `[VERIFIED]` or `[INFERRED]` confidence tag. The method list says how many of the model's methods it is showing.
|
|
153
156
|
|
|
154
157
|
| Parameter | Type | Default | Description |
|
|
155
158
|
|:----------|:-----|:--------|:------------|
|
|
@@ -176,7 +179,10 @@ its concerns), not the order Rails registered them in.
|
|
|
176
179
|
|
|
177
180
|
### `rails_get_concern`
|
|
178
181
|
|
|
179
|
-
Concern methods, source code, and which models include it.
|
|
182
|
+
Concern methods, source code, and which models include it. A class under
|
|
183
|
+
`app/models/concerns` that subclasses `ActiveModel::Validator` is listed as a
|
|
184
|
+
validator rather than a concern, and its users are the models that name it in
|
|
185
|
+
`validates_with`.
|
|
180
186
|
|
|
181
187
|
| Parameter | Type | Default | Description |
|
|
182
188
|
|:----------|:-----|:--------|:------------|
|
|
@@ -201,7 +207,11 @@ Controller actions with inherited filters, render map, strong params. Includes s
|
|
|
201
207
|
|
|
202
208
|
### `rails_get_routes`
|
|
203
209
|
|
|
204
|
-
Routes with code-ready helpers (`post_path(@record)`) and required params.
|
|
210
|
+
Routes with code-ready helpers (`post_path(@record)`) and required params. A
|
|
211
|
+
fully qualified controller key answers with its own routes only; a short name
|
|
212
|
+
still matches every controller that carries it. Rack apps attached with `mount`
|
|
213
|
+
or `match ... to:` are named with the path they answer on, when the source
|
|
214
|
+
spells one out.
|
|
205
215
|
|
|
206
216
|
| Parameter | Type | Default | Description |
|
|
207
217
|
|:----------|:-----|:--------|:------------|
|
|
@@ -216,7 +226,7 @@ Routes with code-ready helpers (`post_path(@record)`) and required params.
|
|
|
216
226
|
|
|
217
227
|
### `rails_get_view`
|
|
218
228
|
|
|
219
|
-
View templates with instance variables, Turbo frames, Stimulus controllers, partial locals. Includes schema hints for detected ivars.
|
|
229
|
+
View templates with instance variables, Turbo frames, Stimulus controllers, partial locals. Includes schema hints for detected ivars. A template directly under `app/views` is grouped as `(app/views root)`, which `path:` reaches and `controller:` does not.
|
|
220
230
|
|
|
221
231
|
| Parameter | Type | Default | Description |
|
|
222
232
|
|:----------|:-----|:--------|:------------|
|
|
@@ -299,7 +309,9 @@ Brakeman static analysis: SQL injection, XSS, mass assignment, command injection
|
|
|
299
309
|
|:----------|:-----|:--------|:------------|
|
|
300
310
|
| `detail` | enum | `standard` | `summary`, `standard`, `full` |
|
|
301
311
|
|
|
302
|
-
> Requires the `brakeman` gem.
|
|
312
|
+
> Requires the `brakeman` gem. When it cannot be loaded, the answer says which
|
|
313
|
+
> case it is: brakeman is nowhere on the machine, or it is installed and the
|
|
314
|
+
> app's bundle does not carry it, which `--no-boot` scans around.
|
|
303
315
|
|
|
304
316
|
### `rails_performance_check`
|
|
305
317
|
|
|
@@ -349,7 +361,7 @@ Notable gems with versions, categories, and config file locations.
|
|
|
349
361
|
|
|
350
362
|
### `rails_get_env`
|
|
351
363
|
|
|
352
|
-
Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`; files matching `sensitive_patterns` (`config/database.yml`, credentials, keys) are never read, and the answer says so.
|
|
364
|
+
Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`; files matching `sensitive_patterns` (`config/database.yml`, credentials, keys) are never read, and the answer says so. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's.
|
|
353
365
|
|
|
354
366
|
| Parameter | Type | Default | Description |
|
|
355
367
|
|:----------|:-----|:--------|:------------|
|
|
@@ -365,7 +377,12 @@ Application and framework helpers with view cross-references.
|
|
|
365
377
|
|
|
366
378
|
### `rails_get_service_pattern`
|
|
367
379
|
|
|
368
|
-
Service object interface, dependencies, side effects, callers.
|
|
380
|
+
Service object interface, dependencies, side effects, callers. An
|
|
381
|
+
ActiveInteraction's inputs include the ones it inherits, with the filters
|
|
382
|
+
nested inside a `hash` filter shown under it. Callers are read from every
|
|
383
|
+
`app/` and `lib/` tree, and on a booted app from any other directory it
|
|
384
|
+
autoloads, and the page says when the twenty-caller display cap or the scan's
|
|
385
|
+
own file ceiling left the list partial.
|
|
369
386
|
|
|
370
387
|
| Parameter | Type | Default | Description |
|
|
371
388
|
|:----------|:-----|:--------|:------------|
|
|
@@ -376,7 +393,9 @@ Service object interface, dependencies, side effects, callers.
|
|
|
376
393
|
|
|
377
394
|
Background job queue, retries, guard clauses, broadcasts, schedules. Sidekiq
|
|
378
395
|
workers under `app/workers` are listed alongside the ActiveJob jobs, with
|
|
379
|
-
their `sidekiq_options` and `perform` signature.
|
|
396
|
+
their `sidekiq_options`, any `sidekiq_throttle`, and the `perform` signature. A
|
|
397
|
+
worker that inherits its Sidekiq mixin from a base worker is one of them, and
|
|
398
|
+
`job:` answers a worker name as well as a job name.
|
|
380
399
|
|
|
381
400
|
| Parameter | Type | Default | Description |
|
|
382
401
|
|:----------|:-----|:--------|:------------|
|
|
@@ -414,7 +433,7 @@ ActionMailer mailers: every mailer class with its delivery actions and delivery
|
|
|
414
433
|
|
|
415
434
|
### `rails_get_engines`
|
|
416
435
|
|
|
417
|
-
|
|
436
|
+
What `config/routes.rb` mounts - engines and plain Rack apps alike, with known-engine descriptions, each with the path it answers on when the source spells one out - and loaded engine classes with route/model counts.
|
|
418
437
|
|
|
419
438
|
*No parameters.*
|
|
420
439
|
|
|
@@ -506,7 +525,7 @@ Reverse file tail with level filtering and sensitive data redaction.
|
|
|
506
525
|
|
|
507
526
|
### `rails_diagnose`
|
|
508
527
|
|
|
509
|
-
One-call error diagnosis with classification, context, git blame, and log correlation.
|
|
528
|
+
One-call error diagnosis with classification, context, git blame, and log correlation. It does not call a method undefined when the model's method list could be missing one - a concern's, a parent's, or anything past the payload's own cap.
|
|
510
529
|
|
|
511
530
|
| Parameter | Type | Default | Description |
|
|
512
531
|
|:----------|:-----|:--------|:------------|
|
data/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -256,6 +256,19 @@ bundle add brakeman --group development
|
|
|
256
256
|
|
|
257
257
|
Without it, the tool reports "not installed" but the gem works fine otherwise.
|
|
258
258
|
|
|
259
|
+
### "Installed on this machine but not in this app's bundle"
|
|
260
|
+
|
|
261
|
+
The scan runs under the app's own bundle, so a brakeman installed globally is
|
|
262
|
+
not on its load path. The tool falls back on its own: it runs the installed
|
|
263
|
+
brakeman as a separate process outside the bundle and says so under the
|
|
264
|
+
results. This message means that fallback produced no report either - the gem
|
|
265
|
+
is there and the run failed. Run `brakeman` in the app directory to see what
|
|
266
|
+
it hit, or add it to the Gemfile so the scan runs in-process:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
bundle add brakeman --group development
|
|
270
|
+
```
|
|
271
|
+
|
|
259
272
|
---
|
|
260
273
|
|
|
261
274
|
## Performance issues
|
|
@@ -66,21 +66,16 @@ module RailsAiContext
|
|
|
66
66
|
|
|
67
67
|
private
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
# What config/routes.rb and the files it draws mount, on both tiers,
|
|
70
|
+
# through the route introspector's own walk: on the static tier the
|
|
71
|
+
# routes section reads the same walk, and booted it reads the live
|
|
72
|
+
# route table, which also holds what a gem mounts for itself.
|
|
73
73
|
def discover_mounted_engines
|
|
74
|
-
routes_path = File.join(root, "config/routes.rb")
|
|
75
|
-
return [] unless File.exist?(routes_path)
|
|
76
|
-
|
|
77
|
-
ast_data = SourceIntrospector.walk(routes_path, { mounts: -> { Listeners::MountListener.new } })
|
|
78
74
|
engines = []
|
|
79
75
|
|
|
80
|
-
|
|
76
|
+
RouteIntrospector.new(app).static_mounts.each do |mount|
|
|
81
77
|
engine_name = mount[:engine]
|
|
82
|
-
|
|
83
|
-
info = { engine: engine_name, path: path }
|
|
78
|
+
info = { engine: engine_name, path: mount[:path] }
|
|
84
79
|
known = KNOWN_ENGINES[engine_name]
|
|
85
80
|
if known
|
|
86
81
|
info[:category] = known[:category].to_s
|
|
@@ -98,17 +98,39 @@ module RailsAiContext
|
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
# `:memory_store if Rails.root.join(...).exist?, else :null_store`.
|
|
101
|
+
#
|
|
102
|
+
# Two unconditional assignments of one key are not branches: Rails runs
|
|
103
|
+
# both lines and the last one wins. Comma-joining them read exactly like
|
|
104
|
+
# a single assignment whose value is a comma list
|
|
105
|
+
# (`:mem_cache_store, { pool_size: 5 }`), so the winner is named and the
|
|
106
|
+
# assignment it overrode is named after it.
|
|
101
107
|
def branch_values(entries)
|
|
102
108
|
return one_line(entries.first[:source].to_s) if entries.size == 1
|
|
103
109
|
|
|
104
|
-
entries.
|
|
110
|
+
unconditional, conditional = entries.partition { |entry| entry[:condition].nil? }
|
|
111
|
+
rendered = conditional.map { |entry|
|
|
105
112
|
value = one_line(entry[:source].to_s)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
text = entry[:condition] == "else" ? "else #{value}" : "#{value} if #{entry[:condition]}"
|
|
114
|
+
[ entry[:location].to_i, text ]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if unconditional.any?
|
|
118
|
+
# The last line the file runs is the value in force, whatever ran
|
|
119
|
+
# before it, so the winner is taken before any de-duplication - a
|
|
120
|
+
# value that repeats is still the one that ran last.
|
|
121
|
+
winner = unconditional.last
|
|
122
|
+
overridden = unconditional[0..-2].map { |entry| one_line(entry[:source].to_s) }.uniq
|
|
123
|
+
overridden -= [ one_line(winner[:source].to_s) ]
|
|
124
|
+
text = one_line(winner[:source].to_s)
|
|
125
|
+
text += " (overrides #{overridden.join(', ')})" if overridden.any?
|
|
126
|
+
rendered << [ winner[:location].to_i, text ]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Source order, because that is run order: a conditional assignment
|
|
130
|
+
# printed after the unconditional one that follows it reads as the
|
|
131
|
+
# value in force.
|
|
132
|
+
rendered.each_with_index.sort_by { |(line, _), index| [ line, index ] }
|
|
133
|
+
.map { |(_, text), _| text }.uniq.join(", ")
|
|
112
134
|
end
|
|
113
135
|
|
|
114
136
|
# The booted app has already resolved the branch, and two tools reading
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Introspectors
|
|
5
|
+
# What an ActiveInteraction service declares as its interface: whether the
|
|
6
|
+
# class is one, and the filters it takes.
|
|
7
|
+
#
|
|
8
|
+
# Two things the filter macros alone do not say, and every consumer needs.
|
|
9
|
+
#
|
|
10
|
+
# A filter declared inside another filter's block is a key of that filter,
|
|
11
|
+
# not an input of the class: `string :title` inside `hash :order_params do
|
|
12
|
+
# ... end` never reaches `.filters`, and passing it to `.run` drops it
|
|
13
|
+
# silently. So nesting is kept, never flattened.
|
|
14
|
+
#
|
|
15
|
+
# An interaction is often a subclass of the app's own base interaction
|
|
16
|
+
# rather than of ActiveInteraction::Base, and its filters are then its own
|
|
17
|
+
# plus the ones it inherits, parent's first, which is the order
|
|
18
|
+
# `.filters` answers in. Following that chain needs the parent's source,
|
|
19
|
+
# which only the caller can find, so it comes in as `lookup` - a callable
|
|
20
|
+
# from a constant name to that class's source, or nil to stop at the one
|
|
21
|
+
# file. `SuperclassChain.lookup_for` builds the one both consumers pass.
|
|
22
|
+
#
|
|
23
|
+
# Reading the loaded constant's `.filters` would answer both on a booted
|
|
24
|
+
# app, but the two consumers (rails_get_service_pattern, rails_generate_test)
|
|
25
|
+
# answer from source in both tiers, and a static answer that differs from
|
|
26
|
+
# the booted one is the divergence this module exists to end.
|
|
27
|
+
module Interaction
|
|
28
|
+
BASE = "ActiveInteraction::Base"
|
|
29
|
+
|
|
30
|
+
# The filter macros ActiveInteraction defines. `interface` and `record`
|
|
31
|
+
# included: they are filters like any other.
|
|
32
|
+
FILTERS = %w[
|
|
33
|
+
array boolean date date_time decimal file float hash integer
|
|
34
|
+
interface object record string symbol time
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
# `nested` is the filters declared inside this one's block; `declared_by`
|
|
38
|
+
# the class that declares it, which is not this file's class for an
|
|
39
|
+
# inherited one.
|
|
40
|
+
Filter = Data.define(:macro, :name, :options, :declared_by, :nested)
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# @param source [String] the file's source
|
|
45
|
+
# @param lookup [#call, nil] constant name -> that class's source
|
|
46
|
+
# @return [Boolean] whether the class this source declares runs as an
|
|
47
|
+
# ActiveInteraction
|
|
48
|
+
def interaction?(source, lookup: nil)
|
|
49
|
+
chain(source, lookup: lookup).any?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The filters the class takes, inherited ones first, each with the
|
|
53
|
+
# filters nested inside it.
|
|
54
|
+
#
|
|
55
|
+
# @return [Array<Filter>] empty when the source declares no interaction
|
|
56
|
+
def filters(source, lookup: nil)
|
|
57
|
+
interface(source, lookup: lookup) || []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Both answers from one walk of the chain, for a caller that needs to
|
|
61
|
+
# know whether the class is an interaction and what it takes: nil when
|
|
62
|
+
# it is not one, the filters when it is - and an interaction with no
|
|
63
|
+
# filters answers [], which is not nil.
|
|
64
|
+
#
|
|
65
|
+
# @return [Array<Filter>, nil]
|
|
66
|
+
def interface(source, lookup: nil)
|
|
67
|
+
links = chain(source, lookup: lookup)
|
|
68
|
+
return nil if links.empty?
|
|
69
|
+
|
|
70
|
+
one_per_name(links.reverse.flat_map { |link| own_filters(link) })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The classes from ActiveInteraction::Base down to this one, nearest
|
|
74
|
+
# first, or empty when the chain never reaches it.
|
|
75
|
+
#
|
|
76
|
+
# @return [Array<SuperclassChain::Link>]
|
|
77
|
+
def chain(source, lookup: nil)
|
|
78
|
+
SuperclassChain.to(source, bases: [ BASE ], lookup: lookup)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# `.filters` is a hash keyed by name, so a subclass that redeclares its
|
|
82
|
+
# parent's filter replaces it and keeps the parent's position. Rendering
|
|
83
|
+
# both printed the name twice, and generating `run(token: nil, token:
|
|
84
|
+
# nil)` from it is a duplicate keyword argument.
|
|
85
|
+
def one_per_name(filters)
|
|
86
|
+
slots = {}
|
|
87
|
+
filters.each_with_index do |filter, index|
|
|
88
|
+
existing = slots[filter.name]
|
|
89
|
+
slots[filter.name] = [ existing ? existing.first : index, filter ]
|
|
90
|
+
end
|
|
91
|
+
slots.values.sort_by(&:first).map(&:last)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The filters one class declares, nested ones attached to the filter
|
|
95
|
+
# whose block they sit in, paired by the parent call's own offset in the
|
|
96
|
+
# source.
|
|
97
|
+
def own_filters(link)
|
|
98
|
+
records = SourceIntrospector.walk_source(
|
|
99
|
+
link.source, { filters: -> { Listeners::GenericMacroListener.new(FILTERS) } }
|
|
100
|
+
)[:filters] || []
|
|
101
|
+
|
|
102
|
+
# Children first, so a filter is built once, with what it holds. One
|
|
103
|
+
# macro call declares one filter that can take a block (`hash :a, :b
|
|
104
|
+
# do` is not a shape ActiveInteraction accepts), so the call's own
|
|
105
|
+
# offset names the parent of everything inside it.
|
|
106
|
+
children = records.group_by { |record| record[:parent_offset] }
|
|
107
|
+
build_filters(children[nil] || [], children, link.name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_filters(records, children, declared_by)
|
|
111
|
+
records.flat_map do |record|
|
|
112
|
+
nested = build_filters(children[record[:offset]] || [], children, declared_by)
|
|
113
|
+
Array(record[:args]).map do |name|
|
|
114
|
+
Filter.new(
|
|
115
|
+
macro: record[:macro].to_s,
|
|
116
|
+
name: name.to_s,
|
|
117
|
+
options: record[:option_values] || {},
|
|
118
|
+
declared_by: declared_by,
|
|
119
|
+
nested: nested
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
private_class_method :chain, :own_filters, :build_filters, :one_per_name
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|