rails-ai-context 5.32.2 → 5.33.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 +1287 -0
- data/CONTEXT.md +26 -2
- data/README.md +13 -9
- data/Rakefile +6 -0
- data/app/controllers/rails_ai_context/mcp_controller.rb +114 -84
- data/docs/ARCHITECTURE.md +6 -4
- data/docs/CLI.md +79 -6
- data/docs/COMPATIBILITY.md +65 -1
- data/docs/CONFIGURATION.md +30 -11
- data/docs/CUSTOM_TOOLS.md +2 -1
- data/docs/FAQ.md +16 -6
- data/docs/GUIDE.md +117 -71
- data/docs/INTROSPECTORS.md +8 -7
- data/docs/QUICKSTART.md +3 -3
- data/docs/RECIPES.md +1 -1
- data/docs/SECURITY.md +109 -25
- data/docs/SETUP.md +30 -6
- data/docs/STANDALONE.md +10 -3
- data/docs/TOOLS.md +145 -34
- data/docs/TROUBLESHOOTING.md +90 -12
- data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
- data/docs/adr/0005-workspace-mcp-entries.md +39 -0
- data/exe/rails-ai-context +733 -104
- data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
- data/lib/rails_ai_context/ast_cache.rb +7 -0
- data/lib/rails_ai_context/base_mixins.rb +8 -8
- data/lib/rails_ai_context/boot_manager.rb +22 -3
- data/lib/rails_ai_context/change_watch.rb +14 -11
- data/lib/rails_ai_context/cli/app_root.rb +401 -0
- data/lib/rails_ai_context/cli/entry_boot.rb +53 -20
- data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
- data/lib/rails_ai_context/code_reloader.rb +115 -1
- data/lib/rails_ai_context/concern_membership.rb +3 -1
- data/lib/rails_ai_context/concern_paths.rb +8 -3
- data/lib/rails_ai_context/configuration.rb +41 -9
- data/lib/rails_ai_context/context_file_report.rb +19 -2
- data/lib/rails_ai_context/database_file.rb +26 -0
- data/lib/rails_ai_context/doctor.rb +1122 -184
- data/lib/rails_ai_context/engine.rb +11 -1
- data/lib/rails_ai_context/erb_source.rb +19 -0
- data/lib/rails_ai_context/executable.rb +21 -0
- data/lib/rails_ai_context/facts_formatter.rb +5 -2
- data/lib/rails_ai_context/file_walk.rb +61 -10
- data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
- data/lib/rails_ai_context/fingerprinter.rb +40 -12
- data/lib/rails_ai_context/gem_lock.rb +176 -26
- data/lib/rails_ai_context/git_ignore.rb +22 -0
- data/lib/rails_ai_context/install/ai_tool.rb +31 -5
- data/lib/rails_ai_context/install/cleanup.rb +21 -0
- data/lib/rails_ai_context/install/program.rb +209 -35
- data/lib/rails_ai_context/install/selection_record.rb +258 -97
- data/lib/rails_ai_context/install/surface.rb +6 -1
- data/lib/rails_ai_context/install/validation_hook.rb +282 -0
- data/lib/rails_ai_context/install/workspace.rb +166 -0
- data/lib/rails_ai_context/install_mode.rb +71 -13
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
- data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
- data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
- data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +10 -3
- data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +56 -8
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +14 -2
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
- data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +2 -1
- data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
- data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
- data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +46 -1
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +3 -1
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +14 -3
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
- data/lib/rails_ai_context/introspectors/migration_replay.rb +2 -2
- data/lib/rails_ai_context/introspectors/model_introspector.rb +91 -5
- data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
- data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +57 -4
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +13 -9
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +130 -11
- data/lib/rails_ai_context/introspectors/view_introspector.rb +3 -3
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
- data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
- data/lib/rails_ai_context/live_reload.rb +20 -20
- data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
- data/lib/rails_ai_context/mcp_edge.rb +185 -32
- data/lib/rails_ai_context/middleware.rb +5 -1
- data/lib/rails_ai_context/migration_status.rb +88 -25
- data/lib/rails_ai_context/output_guard.rb +48 -2
- data/lib/rails_ai_context/package_json.rb +1 -1
- data/lib/rails_ai_context/path_resolver.rb +46 -13
- data/lib/rails_ai_context/payload.rb +55 -7
- data/lib/rails_ai_context/pending_migrations.rb +5 -1
- data/lib/rails_ai_context/portable_path.rb +3 -2
- data/lib/rails_ai_context/presets.rb +15 -11
- data/lib/rails_ai_context/redaction.rb +254 -51
- data/lib/rails_ai_context/resources.rb +55 -20
- data/lib/rails_ai_context/route_coverage.rb +8 -0
- data/lib/rails_ai_context/safe_file.rb +14 -1
- data/lib/rails_ai_context/safe_path.rb +101 -4
- data/lib/rails_ai_context/schema_adapter.rb +8 -0
- data/lib/rails_ai_context/schema_version.rb +9 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +86 -32
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +17 -15
- data/lib/rails_ai_context/serializers/guide_examples.rb +141 -0
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +142 -29
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +36 -14
- data/lib/rails_ai_context/serializers/section_facts.rb +35 -1
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
- data/lib/rails_ai_context/server.rb +304 -71
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +112 -43
- data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
- data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
- data/lib/rails_ai_context/tools/base_tool.rb +352 -41
- data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
- data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
- data/lib/rails_ai_context/tools/diagnose.rb +218 -11
- data/lib/rails_ai_context/tools/generate_test.rb +339 -39
- data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
- data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
- data/lib/rails_ai_context/tools/get_concern.rb +91 -27
- data/lib/rails_ai_context/tools/get_config.rb +13 -4
- data/lib/rails_ai_context/tools/get_context.rb +71 -14
- data/lib/rails_ai_context/tools/get_controllers.rb +157 -40
- data/lib/rails_ai_context/tools/get_conventions.rb +26 -4
- data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
- data/lib/rails_ai_context/tools/get_env.rb +18 -9
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
- data/lib/rails_ai_context/tools/get_gems.rb +1 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +9 -5
- data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
- data/lib/rails_ai_context/tools/get_job_pattern.rb +2 -1
- data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +16 -6
- data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
- data/lib/rails_ai_context/tools/get_routes.rb +19 -6
- data/lib/rails_ai_context/tools/get_schema.rb +51 -18
- data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
- data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
- data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
- data/lib/rails_ai_context/tools/get_view.rb +14 -14
- data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
- data/lib/rails_ai_context/tools/onboard.rb +46 -6
- data/lib/rails_ai_context/tools/performance_check.rb +4 -2
- data/lib/rails_ai_context/tools/query.rb +914 -86
- data/lib/rails_ai_context/tools/read_logs.rb +69 -15
- data/lib/rails_ai_context/tools/review_changes.rb +204 -43
- data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
- data/lib/rails_ai_context/tools/safe_call.rb +35 -14
- data/lib/rails_ai_context/tools/search_code.rb +243 -67
- data/lib/rails_ai_context/tools/search_docs.rb +3 -3
- data/lib/rails_ai_context/tools/security_scan.rb +141 -31
- data/lib/rails_ai_context/tools/session_context.rb +38 -4
- data/lib/rails_ai_context/tools/validate.rb +259 -40
- data/lib/rails_ai_context/tools/validate_semantics.rb +129 -55
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +67 -40
- data/lib/rails_ai_context/view_file.rb +41 -3
- data/lib/rails_ai_context/watcher.rb +51 -13
- data/lib/rails_ai_context.rb +21 -0
- data/server.json +1 -1
- metadata +13 -1
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,1293 @@ 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.33.0] - 2026-10-11
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- **Redaction filters a secret passed as a flag, set under a dashed header
|
|
13
|
+
name, or sent after an auth scheme.** Every tool that shows the app's
|
|
14
|
+
source returned `mysql --password=...`, `-- --token=...` and
|
|
15
|
+
`redis-server --requirepass=...` with the value in place, since a dash
|
|
16
|
+
before the name hid it; `{ "X-Api-Key" => "..." }`,
|
|
17
|
+
`req["X-Api-Key"] = "..."` and `"x-auth-token": "..."`, since the dashes
|
|
18
|
+
hid the secret word; and `conn.headers["Authorization"] = "Bearer ..."`
|
|
19
|
+
and `{ "Authorization" => "Basic ..." }`, since "Authorization" is no
|
|
20
|
+
secret word. A flag's value is now filtered as any `NAME=value` is, with
|
|
21
|
+
Redis's `requirepass` and `masterauth` among the password names and a
|
|
22
|
+
regex that names the flag (`/--password=.+/`) left as code; a quoted name
|
|
23
|
+
joined by dashes reads as the name it spells with underscores, so
|
|
24
|
+
`"Content-Type"` and `"X-Request-Id"` stay; and the credential after
|
|
25
|
+
`Bearer`, `Basic` or `Token` goes, the scheme kept, when it is 16 or more
|
|
26
|
+
letters and digits, both, read past a UUID's dashes and base64's `+`,
|
|
27
|
+
`/` and `=`, so `Bearer #{token}`, `Bearer YOUR_TOKEN` and
|
|
28
|
+
`Basic realm=...` stay. `rails_read_logs` kept a `Basic` credential after
|
|
29
|
+
`Authorization: `, the whole header when it was quoted, a hash or JSON
|
|
30
|
+
key or `HTTP_AUTHORIZATION`, and a dashed header key's value in a logged
|
|
31
|
+
hash (`{"X-Api-Key"=>"..."}`); each is filtered now. A Stimulus target,
|
|
32
|
+
action, controller, outlet or class is read as a descriptor, so
|
|
33
|
+
`data: { password_visibility_target: "input" }` keeps its value.
|
|
34
|
+
- **Redaction filters the password Postgres, mysql and redis-cli read from
|
|
35
|
+
the environment, a camel-cased secret name, and a flag's secret given as
|
|
36
|
+
the next word.** A password word counted only as a part of a name between
|
|
37
|
+
underscores, a secret word only in snake case, and a flag's value only
|
|
38
|
+
after `=`, so `PGPASSWORD: ...` in a compose file, `PGPASSWORD=... psql`
|
|
39
|
+
in a script, `ENV["PGPASSWORD"] = "..."`, `MYSQL_PWD` and
|
|
40
|
+
`REDISCLI_AUTH`, `const accessToken = "..."`, `{"clientSecret": "..."}`,
|
|
41
|
+
`secretAccessKey: "..."` and `redis-server --requirepass ...` in a
|
|
42
|
+
Procfile kept their values, in source and in a log line alike. A long
|
|
43
|
+
password word that ends a name now reads with a name joined on before it
|
|
44
|
+
(`PGPASSWORD`, `dbpassword`, `userPassword`; not `ONEPASSWORD_VAULT`),
|
|
45
|
+
the two client variables as password names, a camel-cased name with
|
|
46
|
+
`Secret` or `Token` among its words as a secret's (`accessToken`,
|
|
47
|
+
`jwtSecretKey`), as `apiKey` already was a key's, and the word after a
|
|
48
|
+
secret-named flag, bare or quoted, as its value when it has a digit. So
|
|
49
|
+
`the --password option`, `--token TOKEN`, `--password $DB_PASSWORD`, a
|
|
50
|
+
`--no-password` switch, a path (`--private-key ~/.ssh/id_ed25519`), a
|
|
51
|
+
file or an id (`--token-file`, `--secret-id`) and BuildKit's
|
|
52
|
+
`--secret id=...,env=...` stay as written, as do a switch's value
|
|
53
|
+
(`showPassword=false`) and a value that is its key's own name
|
|
54
|
+
(`currentPassword: "current_password"`).
|
|
55
|
+
- **Redaction reads a long word in time that grows with its length, not its
|
|
56
|
+
square.** A call's receiver was tried from every character of a run of
|
|
57
|
+
letters, digits and dots, so filtering a line holding a 16 KB hex string
|
|
58
|
+
took 1.5 to 2 seconds on Ruby 3.1 and 3.2 and a 64 KB one over 20; on
|
|
59
|
+
Ruby 3.3 and later a 1 MB one took 2.5 seconds, past the one second a
|
|
60
|
+
Rails 8 app gives a regular expression, and the tool answered with that
|
|
61
|
+
error. A name was split at every underscore of a long word holding a
|
|
62
|
+
secret word in many of its parts, and a path check retried from every
|
|
63
|
+
slash. The receiver is now read from where its run starts, a name is taken
|
|
64
|
+
whole, and a path's first segment at once: each of those lines takes a
|
|
65
|
+
fraction of a second.
|
|
66
|
+
- **`rails_get_view` filters the secrets in a controller's full listing.**
|
|
67
|
+
`controller:"posts", detail:"full"` printed each template and partial as
|
|
68
|
+
written, `<% api_key = "..." %>` and a `render` call's `token: "..."`
|
|
69
|
+
among them, while the same template read by `path:` and every layout had
|
|
70
|
+
them filtered. Each file in the listing is now filtered as one read by
|
|
71
|
+
path is, a mailer's views (`controller:"user_mailer"`) included.
|
|
72
|
+
- **`rails_security_scan` shows a warning's code as the file has it.** At
|
|
73
|
+
`detail:"full"` the code was Brakeman's own line, which writes the value
|
|
74
|
+
of each variable and constant it can follow in place of the name: a token
|
|
75
|
+
assigned to `api_token` on the line above, or to a constant in an
|
|
76
|
+
initializer, sat in the `system(...)` call as a bare literal, with no name
|
|
77
|
+
left for the redaction to know it by. The answer now shows the statement
|
|
78
|
+
the warning names, read from its file and filtered as every other slice of
|
|
79
|
+
the app's source is: a call written across lines whole, up to 12 lines,
|
|
80
|
+
and a template's line fenced as the template it is. A warning in a file
|
|
81
|
+
the tools may not read shows no code.
|
|
82
|
+
- **`rails_search_code` in trace mode checks its `path` like every other
|
|
83
|
+
mode.** `match_type: "trace"` joined the path to the root unchecked, so
|
|
84
|
+
`path: "../../elsewhere"` searched outside the app and printed the source
|
|
85
|
+
it found there.
|
|
86
|
+
- **A directory linked in from outside the app's git repository is not
|
|
87
|
+
read.** A walk or a pack follows a directory link only into the work tree
|
|
88
|
+
holding the app (into the app root outside a repository); a pack linked
|
|
89
|
+
from anywhere, a home directory included, used to be read.
|
|
90
|
+
- **`rails_review_changes` hands git a commit, never the caller's ref.**
|
|
91
|
+
The ref went to `git diff` and `git log` as an argument, and one that
|
|
92
|
+
starts with a dash is read as an option: `ref: "--output=<path>"` made
|
|
93
|
+
the read-only tool write the diff to any path the caller named. A ref now
|
|
94
|
+
has to name a commit, and git sees that commit's SHA. A ref that starts
|
|
95
|
+
with a dash is refused, and one that names no commit says so, where it
|
|
96
|
+
answered "No changes found".
|
|
97
|
+
- **`rails_review_changes` filters the secrets in the diffs it shows, and
|
|
98
|
+
shows no line of a sensitive file.** Each file's diff was printed as git
|
|
99
|
+
gave it, so a key, token or password a change added or took out reached
|
|
100
|
+
the caller whole, as no other tool's source did, and so did every line of
|
|
101
|
+
a changed `config/database.yml`, `.env` or key file. A diff's lines now go
|
|
102
|
+
through the same redaction, read as the file they come from, a hunk
|
|
103
|
+
header's line and a removed validation's included, and a file on
|
|
104
|
+
`sensitive_patterns` is listed with its line counts alone.
|
|
105
|
+
- **Redaction reads a secret a YAML or JSON file names on another line,
|
|
106
|
+
and a YAML key written as a symbol.** Heroku's app.json gives a
|
|
107
|
+
variable's value under its name (`"STRIPE_SECRET_KEY": { "value": "..." }`),
|
|
108
|
+
render.yaml and a Kubernetes manifest under a sibling (`- key:
|
|
109
|
+
CLIENT_SECRET` over `value: ...`), and config/sidekiq.yml writes its keys
|
|
110
|
+
as symbols (`:password: ...`); every tool that shows a file kept each of
|
|
111
|
+
those values. In a YAML or JSON file read whole, a `value` under a
|
|
112
|
+
mapping a secret's name names is now filtered, a boolean and the next
|
|
113
|
+
entry's value kept, and a symbol key reads as its name.
|
|
114
|
+
- **The gems section names a git source without its credentials.** A
|
|
115
|
+
Gemfile's `git: "https://user:token@host/..."` went into the gems answer,
|
|
116
|
+
the `rails://gems` resource and `.ai-context.json` as written. The URL now
|
|
117
|
+
passes through the redaction a source line does, so its userinfo, or a
|
|
118
|
+
token format in its place, reads `[FILTERED]`.
|
|
119
|
+
- **The context file names a Procfile's commands with their secrets
|
|
120
|
+
filtered.** Each command went into the DevOps section as written, so
|
|
121
|
+
`redis-server --requirepass ...` or `DB_PASSWORD=... bundle exec sidekiq`
|
|
122
|
+
reached `.ai-context.json`, which `rails ai:context` asks the team to
|
|
123
|
+
commit. A command now passes through the redaction a Procfile line read by
|
|
124
|
+
any tool does.
|
|
125
|
+
- **`rails_get_edit_context` names an endless method without its body, and
|
|
126
|
+
looks for `near` in what it shows.** The line above the code quoted the
|
|
127
|
+
enclosing method's line as written, and an endless method's line is its
|
|
128
|
+
whole body: `def headers = { "Authorization" => "Bearer ..." }` printed
|
|
129
|
+
the credential the code under it had filtered. And `near` was looked for
|
|
130
|
+
in the file as written, so a guess at a secret's start brought back the
|
|
131
|
+
window where a wrong guess said "not found". The label is now built from
|
|
132
|
+
the filtered lines, an endless method named alone, and `near` is matched
|
|
133
|
+
against the lines as they are returned.
|
|
134
|
+
- **`rails_query` never serialises a whole row.** On PostgreSQL,
|
|
135
|
+
`row_to_json(users.*)`, `to_json(u)`, `u::text`, `array_agg(u)`,
|
|
136
|
+
`json_agg(u)`, `hstore(u)`, `ROW(u.*)`, `format('%s', u)` and a bare
|
|
137
|
+
`SELECT u FROM users u` returned every column, password digests and
|
|
138
|
+
tokens included, since no column name appeared for the checks to catch.
|
|
139
|
+
A query is now planned first with `EXPLAIN (VERBOSE, FORMAT JSON)` in the
|
|
140
|
+
same read-only transaction - planning only, nothing runs - and refused
|
|
141
|
+
when the plan reads a whole row of a table that has a sensitive column,
|
|
142
|
+
or uses a sensitive column other than as a plain output column. A
|
|
143
|
+
whole-row read of a table with no sensitive column still works.
|
|
144
|
+
- **A sensitive column cannot leave through an alias, an expression or a
|
|
145
|
+
view.** `SELECT api_token AS c`, `upper(api_token)`,
|
|
146
|
+
`json_build_object('t', api_token)` and `(SELECT api_token FROM users
|
|
147
|
+
LIMIT 1) AS leaked` reached the result under a name the redaction did not
|
|
148
|
+
recognise. One rule now decides what is sensitive - a configured or
|
|
149
|
+
built-in redacted name, an `encrypts` column, or the result heuristic,
|
|
150
|
+
minus `query_allowed_columns` - and a query naming any real sensitive
|
|
151
|
+
column of the schema is refused before it runs, on every adapter, as is
|
|
152
|
+
a column-alias list (`t(a, b, c)`) that would rename a wildcard's
|
|
153
|
+
columns. Rails' own bookkeeping tables are left out of that set, so
|
|
154
|
+
`ar_internal_metadata.key` does not make the word `key` refused, and on
|
|
155
|
+
PostgreSQL a name inside a string literal (`LIKE '%secret%'`, a JSON key)
|
|
156
|
+
is data, allowed only when the query's plan was read and cleared. On PostgreSQL the result is also redacted by where each column
|
|
157
|
+
comes from, so a view that renames `api_token` to `t` returns
|
|
158
|
+
`[FILTERED]`, and a sensitive column passed through a materialized CTE, a
|
|
159
|
+
subquery or a set operation, where its origin cannot be pinned, is
|
|
160
|
+
refused.
|
|
161
|
+
- **`rails_query` refuses every spelling of UNION.** The check matched
|
|
162
|
+
only `UNION SELECT` and `UNION ALL SELECT`; MySQL and MariaDB also take
|
|
163
|
+
`UNION DISTINCT SELECT` and a parenthesised `UNION (SELECT ...)`, and
|
|
164
|
+
either one returned another table's rows under the first SELECT's
|
|
165
|
+
column names - a password digest under `posts.user_id` - past the
|
|
166
|
+
redaction that reads names. Any UNION is refused now, on every adapter,
|
|
167
|
+
with that reason, where it was called an "SQL injection pattern".
|
|
168
|
+
- **Session- and server-affecting functions no longer run under the
|
|
169
|
+
read-only transaction.** `pg_terminate_backend`, `pg_reload_conf`,
|
|
170
|
+
`pg_stat_reset*`, `pg_advisory_lock*`, `set_config`, `pg_notify`,
|
|
171
|
+
`txid_current`, the replication and WAL-control functions, and MySQL's
|
|
172
|
+
`GET_LOCK` / `RELEASE_LOCK` / `IS_FREE_LOCK` passed `SET TRANSACTION READ
|
|
173
|
+
ONLY` and left their effect - a terminated backend, a held advisory lock -
|
|
174
|
+
on the pooled connection. A name list refuses the known ones on every
|
|
175
|
+
adapter, and on PostgreSQL any VOLATILE function outside a short harmless
|
|
176
|
+
list (`random`, `clock_timestamp`, `gen_random_uuid`, `pg_sleep` and its
|
|
177
|
+
variants) is refused from the plan, closing the family without a list
|
|
178
|
+
that has to grow.
|
|
179
|
+
- **The endpoint inside the app refuses outside development and test
|
|
180
|
+
unless the app opts in.** The mounted engine and `auto_mount` answered
|
|
181
|
+
every tool to whoever could reach a production or staging app. Both now
|
|
182
|
+
answer 403 with a JSON-RPC error that names the environment and the
|
|
183
|
+
setting, and log the refusal once per process;
|
|
184
|
+
`config.allow_http_in_production = true` lets them answer, for an
|
|
185
|
+
endpoint behind the app's own authentication. Doctor's "MCP HTTP
|
|
186
|
+
endpoint" check fails `auto_mount` with it on and warns for the engine.
|
|
187
|
+
- **No walk follows a symlink out of the app.** `rails_get_stimulus`
|
|
188
|
+
parsed a controller linked in from outside the app and printed its
|
|
189
|
+
targets and values, and so did the view, layout, partial, helper,
|
|
190
|
+
JavaScript, locale, environment-file, seed and concern scans and the
|
|
191
|
+
booted source reads. A file whose real path is outside the app is now
|
|
192
|
+
neither listed nor read, unless it sits in a directory the app links in,
|
|
193
|
+
such as a pack.
|
|
194
|
+
- **A log, partial or concern refused on policy is an error result.**
|
|
195
|
+
`rails_read_logs file:`, `rails_get_partial_interface partial:` and
|
|
196
|
+
`rails_get_concern name:` answered "not found" for a file that links out
|
|
197
|
+
of the app, and their lists still offered it. They now answer `Path not
|
|
198
|
+
allowed: <name>` with `isError: true` (exit 1 on the CLI), no list offers
|
|
199
|
+
a refused entry, and `rails_diagnose` returns its diagnosis for a refused
|
|
200
|
+
`file:` as an error result.
|
|
201
|
+
|
|
202
|
+
### Added
|
|
203
|
+
|
|
204
|
+
- **`rails-ai-context init` sets up a folder of apps.** Run in a folder that
|
|
205
|
+
is no app but holds apps one or two levels down - the folder an editor or
|
|
206
|
+
agent is often opened at - it asks once and sets up every app: each keeps
|
|
207
|
+
its own `.rails-ai-context.yml` and context files, generated by the install
|
|
208
|
+
that app's server runs, and the folder's MCP configs get one server per
|
|
209
|
+
app, `rails-ai-context-<app folder>`, pointed at it with `--app-path`
|
|
210
|
+
(#429). Paths stay relative so a committed config works on every machine,
|
|
211
|
+
spelled from `${workspaceFolder}` for Cursor and VS Code, which name the
|
|
212
|
+
folder their config sits in. Each app keeps its own install form, decided
|
|
213
|
+
from its own Gemfile.lock, and an in-Gemfile app's entry sets
|
|
214
|
+
`BUNDLE_GEMFILE` to the Gemfile its bundle reads: its own (a linked one by
|
|
215
|
+
the link's name), else the shared one its `config/boot.rb` names, else the
|
|
216
|
+
one `bundle exec` run inside the app would find. A bare
|
|
217
|
+
`rails-ai-context` entry left in the folder is replaced, a re-run drops
|
|
218
|
+
the entry of an app whose folder is gone or that the gem renamed, removal
|
|
219
|
+
and cleanup take every `rails-ai-context-*` entry that runs the gem's
|
|
220
|
+
server (a hand-made one that does not stays, and so does an entry with no
|
|
221
|
+
command, an HTTP one, under either name), `--app-path` naming a folder of
|
|
222
|
+
apps sets that folder up, and `doctor` inside an app finds the folder's
|
|
223
|
+
config one or two levels up, says when one there does not parse, and
|
|
224
|
+
checks each Codex env snapshot in it. `init` warns when an app declares a
|
|
225
|
+
Ruby other than the one the folder runs. See ADR-0005 for what each AI
|
|
226
|
+
tool does with a server's working directory, and why.
|
|
227
|
+
- **`RAILS_AI_CONTEXT_SERVER_NAME`** names the server, for one process, in
|
|
228
|
+
place of the default `config.server_name`; a name the app configured wins.
|
|
229
|
+
A workspace entry sets it to put its app first: VS Code names every tool
|
|
230
|
+
after the announced name and keeps 13 characters of it, so every app's
|
|
231
|
+
tools would otherwise start `rails-ai-cont`. An app whose bundle pins an
|
|
232
|
+
older gem ignores the variable and starts.
|
|
233
|
+
- **CONFIGURATION.md lists the environment variables the gem reads**, among
|
|
234
|
+
them `RAILS_AI_CONTEXT_BOOT_TIMEOUT`, which appeared only in error
|
|
235
|
+
messages.
|
|
236
|
+
|
|
237
|
+
### Fixed
|
|
238
|
+
|
|
239
|
+
- **`rails_security_scan` echoes a long file or check name shortened.** A
|
|
240
|
+
100,000-character `files` entry came back whole in "File(s) not found"
|
|
241
|
+
and in the scope of a clean answer, and a long `checks` entry came back
|
|
242
|
+
whole in brakeman's "Could not find specified check", from the scan in
|
|
243
|
+
this process and the one outside the bundle alike. Each now shows its
|
|
244
|
+
first 80 characters and its length. A check name brakeman does not know
|
|
245
|
+
is answered the same from either scan - "Brakeman scan failed: Could not
|
|
246
|
+
find specified check" - where the scan outside the bundle told the caller
|
|
247
|
+
to add brakeman to the Gemfile - and a `files` path that names nothing is
|
|
248
|
+
answered before the scan, which on a large app takes minutes, not after it.
|
|
249
|
+
- **A call sees an edit inside a directory the app links in from its
|
|
250
|
+
repository.** The check each call makes for edits stopped at the link, while
|
|
251
|
+
the readers walk through it, so with `app/javascript/controllers/shared`
|
|
252
|
+
linked to a monorepo's shared controllers, `rails_get_stimulus` answered
|
|
253
|
+
from before an edit there until `cache_ttl` ran out, 60 seconds by
|
|
254
|
+
default, and `rails_search_code` already read the edit. The check now
|
|
255
|
+
follows such a link as the readers do: only into the app's repository,
|
|
256
|
+
never back up over the tree it walks, each linked directory once, so a
|
|
257
|
+
link that leads round in a circle ends. Where a link leads is asked at
|
|
258
|
+
every check, so one whose directory appears later is followed too. The
|
|
259
|
+
fuller check that runs once `cache_ttl` is out follows a linked directory
|
|
260
|
+
whatever its name, where it asked only a name with no extension
|
|
261
|
+
(`billing.v2` was passed over).
|
|
262
|
+
- **The pre-commit hook no longer stops a commit for JavaScript the app's
|
|
263
|
+
own toolchain compiles.** `rails_validate` failed a Flow file
|
|
264
|
+
(`Typed.js:3:21: SyntaxError: Unexpected token ':'`), a MobX store's
|
|
265
|
+
decorators (`Invalid or unexpected token`) and any syntax a Babel config
|
|
266
|
+
adds, since node parses none of them. A decorator is now skipped as JSX
|
|
267
|
+
is, naming its line. A file that declares Flow (`// @flow`), or one Babel
|
|
268
|
+
compiles - a `babel.config.*` or `.babelrc*` file, or a `"babel"` key in
|
|
269
|
+
`package.json`, in its folder or one above it up to the top of the
|
|
270
|
+
repository - is skipped with node's error named beside it, since that may
|
|
271
|
+
be the toolchain's syntax or the file's own mistake. Type annotations in a
|
|
272
|
+
`.js` file nothing compiles, and a plain syntax error, still fail.
|
|
273
|
+
- **A `doctor` fix typed in a folder inside the app runs there too.** From
|
|
274
|
+
`blog/app/models` the fixes were the ones for the app's root:
|
|
275
|
+
`bin/rails db:prepare` was no file there, and `git rm --cached .env`
|
|
276
|
+
matched nothing. A command now goes to the root first - `cd ../.. &&
|
|
277
|
+
bin/rails db:prepare`, `git -C ../.. rm --cached .env`, `rails-ai-context
|
|
278
|
+
--app-path ../.. init` - and a `.gitignore` to edit, a config to make
|
|
279
|
+
valid and the engine a task runs in are named from where `doctor` was
|
|
280
|
+
typed; the report still names the app's files from its root. The binary
|
|
281
|
+
tells the bundle's copy it hands the run to where it was typed, and
|
|
282
|
+
`rails ai:doctor` reads it from the shell's `PWD`, since Rails' `rails`
|
|
283
|
+
command and rake move up to the root before the task runs.
|
|
284
|
+
- **`doctor` gives the edit by hand the install gives for a config the
|
|
285
|
+
install leaves as it is.** For a `.vscode/mcp.json` holding comments it
|
|
286
|
+
said to run the install, which left the file as it was and said to add
|
|
287
|
+
the entry by hand, and `doctor` then said to run the install again. It
|
|
288
|
+
now names the entry to add, as the install writes it (`Add
|
|
289
|
+
{"servers":{...}} to .vscode/mcp.json by hand`), for VS Code's and
|
|
290
|
+
OpenCode's configs, a workspace's included. For a Codex entry kept as an
|
|
291
|
+
inline table or with dotted keys, a stale env snapshot in one included,
|
|
292
|
+
it names the table to write by hand, where an entry the install had to
|
|
293
|
+
rewrite sent the reader back to the install too; and it asks for a JSON
|
|
294
|
+
config that is not UTF-8 to be saved as UTF-8 before the install runs,
|
|
295
|
+
as the install now does too, where it said to add the entry by hand to a
|
|
296
|
+
file JSON still could not read.
|
|
297
|
+
- **On Windows the MCP configs start the server through `cmd /c`, and
|
|
298
|
+
`doctor` reads a config for the platform it runs on.** A gem's executables
|
|
299
|
+
on Windows are batch files (`bundle.bat`, `rails-ai-context.bat`), which
|
|
300
|
+
Claude Code, Codex and the other clients that start their server without a
|
|
301
|
+
shell do not run, so no entry the install wrote started there. Each entry
|
|
302
|
+
is now `cmd /c bundle exec rails-ai-context serve` (`cmd /c
|
|
303
|
+
rails-ai-context serve` standalone) on Windows - `cmd.exe` is a program
|
|
304
|
+
every client starts, and it runs the batch file - and stays as it was on
|
|
305
|
+
macOS and Linux. A config may be committed, so `doctor` fails a `cmd /c` entry on
|
|
306
|
+
macOS and Linux, where there is no `cmd`, and on Windows warns about a
|
|
307
|
+
bare entry whose command PATH finds only as a `.bat` or `.cmd`; running
|
|
308
|
+
the install again rewrites either for the machine it runs on. A team on
|
|
309
|
+
both keeps the MCP configs out of git, or runs the install on each machine.
|
|
310
|
+
- **The pre-commit hook is written with plain newlines on Windows**, where
|
|
311
|
+
the generator wrote it in text mode, so each line ended in CRLF and the sh
|
|
312
|
+
that Git for Windows runs a hook with read the CR as part of every
|
|
313
|
+
command. The stdio server ends each JSON-RPC frame with a plain newline
|
|
314
|
+
there too, rather than the CRLF a text-mode stdout writes.
|
|
315
|
+
- **`doctor` reads PATH on Windows**, where the variable is named `Path`:
|
|
316
|
+
the copy of the environment it reads from before Bundler's changes is a
|
|
317
|
+
plain Hash, which asked for `PATH` found nothing, so every MCP config's
|
|
318
|
+
command read as not on PATH there.
|
|
319
|
+
- **`doctor` and `search_code` find ripgrep on Windows**, which has no
|
|
320
|
+
`which` to ask, so ripgrep read as missing there and every search took the
|
|
321
|
+
slower Ruby path. PATH is searched in Ruby now, with Windows' `PATHEXT`
|
|
322
|
+
extensions, and `doctor`'s fix names `winget install
|
|
323
|
+
BurntSushi.ripgrep.MSVC`. ripgrep is told to separate paths with `/`, since
|
|
324
|
+
on Windows it joins the files it finds with backslashes, which neither the
|
|
325
|
+
app root nor the sensitive patterns that keep a file out of the answer
|
|
326
|
+
(`config/database.yml`) would match.
|
|
327
|
+
- **A Windows path that starts with a drive is read as absolute.** Every
|
|
328
|
+
check for an absolute path looked for a leading `/`, so on Windows
|
|
329
|
+
`C:/...` passed as relative: the context the app commits could carry a
|
|
330
|
+
path under the developer's home directory, an engine or code root outside
|
|
331
|
+
the app was looked for inside it, and a tool handed `C:/Windows/win.ini`
|
|
332
|
+
did not refuse it as the absolute path it is. A failing tool's `At:` line
|
|
333
|
+
split its frame at the drive letter's colon and so named the file by its
|
|
334
|
+
whole path, and `doctor` named an absolute SQLite path from database.yml
|
|
335
|
+
as though it were relative to the app.
|
|
336
|
+
- **A file whose lines end in CRLF, as a Windows checkout's do, is read and
|
|
337
|
+
written in them.** `search_code` ends a line before its CRLF, so `end$`
|
|
338
|
+
finds a line that ends `end` (it matched no line of such a file) and
|
|
339
|
+
`\s$` no longer matches every one. The initializer's `config.ai_tools`
|
|
340
|
+
line was not found or inserted in a CRLF initializer; a context file's
|
|
341
|
+
managed block was written in LF beside CRLF text, leaving a blank line
|
|
342
|
+
under its END marker on every run; and the CRLF pre-commit hook an
|
|
343
|
+
earlier version wrote on Windows, which sh cannot run, read as changed by
|
|
344
|
+
hand, so the install never rewrote it.
|
|
345
|
+
- **On Windows the stdio server keeps its stdout across Bundler's re-exec.**
|
|
346
|
+
Windows has no `exec`: Ruby starts the new image as a child that inherits
|
|
347
|
+
the standard handles and no other, so the descriptor the server saved its
|
|
348
|
+
real stdout on was lost, and a server whose app pins another Bundler
|
|
349
|
+
answered its client nothing. An exec inside the boot now puts the real
|
|
350
|
+
stdout back first.
|
|
351
|
+
- **On Windows `security_scan` stops a run that does not end**, where
|
|
352
|
+
sending it TERM raised `EINVAL`; the run is stopped with KILL alone there.
|
|
353
|
+
A `search_code` pattern longer than Windows takes on a command line (about
|
|
354
|
+
32,000 characters) is searched in Ruby, where the error answered with the
|
|
355
|
+
whole command line, pattern and all.
|
|
356
|
+
- **On Windows an app whose `app/` is a directory link holds source.** The
|
|
357
|
+
check for Ruby files under `app/`, which decides that a tree with no
|
|
358
|
+
`config/` is an app to read from source (an engine's root, a command run
|
|
359
|
+
with `--no-boot`), walked in through a trailing separator. That follows a
|
|
360
|
+
link on macOS and Linux; Windows' `lstat` answers for the link itself, so
|
|
361
|
+
the walk found nothing there and the command said no Rails app was found.
|
|
362
|
+
- **"No Rails app found in" names an `--app-path` as it was typed**, as the
|
|
363
|
+
hint under it does, where it named the folder through any symlink on the
|
|
364
|
+
way (`/private/var/...` for `/var/...` on macOS). The warning that an app
|
|
365
|
+
boots against another bundle under `bundle exec` is printed only where
|
|
366
|
+
there is an app to boot.
|
|
367
|
+
- **A stdio server whose app hangs while booting answers its client within
|
|
368
|
+
about 21 seconds, from the static tier.** A stdio client waits about 30
|
|
369
|
+
seconds for `initialize` (Claude Code, the MCP Inspector) and `serve` gave
|
|
370
|
+
the boot 60, so an initializer blocked on a service that was down never
|
|
371
|
+
reached the static tier for a real client. `serve` over stdio and
|
|
372
|
+
`rails ai:serve` now give the boot 20 seconds unless
|
|
373
|
+
`RAILS_AI_CONTEXT_BOOT_TIMEOUT` says otherwise; `rails ai:serve`, which
|
|
374
|
+
has no static tier, exits with the boot's error while the client still
|
|
375
|
+
listens. Other commands keep 60.
|
|
376
|
+
- **A call sees every edit made before it, with live reload or without.**
|
|
377
|
+
Inside `cache_ttl` the cache was served without asking whether files had
|
|
378
|
+
moved, relying on live reload - which needs the `listen` gem a new Rails 8
|
|
379
|
+
app does not bundle, and even then delivers an edit only after its
|
|
380
|
+
1.5-second debounce. An agent that edited a model and asked about it at
|
|
381
|
+
once was answered from before the edit: a new association missing, a
|
|
382
|
+
syntax error unreported; without live reload an association added to a
|
|
383
|
+
model never appeared until a restart. Every server, the endpoints mounted
|
|
384
|
+
in the app included, now checks the app's files at the start of each tool
|
|
385
|
+
call and resource read and, when they moved, reloads the app's code and
|
|
386
|
+
resets the caches. A call shares only a check that began after it arrived,
|
|
387
|
+
so calls one after another each see an edit made between them. The check
|
|
388
|
+
costs about a millisecond on a small app and about 30 ms at 10,000 files.
|
|
389
|
+
Live reload now only tells clients that files changed.
|
|
390
|
+
- **Reading pending migrations writes nothing to the database.** The pending
|
|
391
|
+
list came from `MigrationContext#open`, which creates `schema_migrations`
|
|
392
|
+
and `ar_internal_metadata` in a database that lacks them. Pending is now
|
|
393
|
+
the migration files minus the versions `schema_migrations` holds. A SQLite
|
|
394
|
+
database whose file is not there is reported missing without connecting,
|
|
395
|
+
since connecting created an empty file that `db:prepare` then migrated
|
|
396
|
+
from nothing instead of loading the schema and seeds.
|
|
397
|
+
- **`doctor`'s secrets check asks git.** Inside a git repository every
|
|
398
|
+
ignore rule counts: any `.gitignore` up to the repository root,
|
|
399
|
+
`.git/info/exclude` and the global excludes. A tracked secret fails as
|
|
400
|
+
committed, with `git rm --cached` and rotating it as the fix. Outside git,
|
|
401
|
+
the app's `.gitignore` is read as before.
|
|
402
|
+
- **`rails_security_scan` runs a brakeman installed outside the app's
|
|
403
|
+
bundle.** It runs the gem's own `bin/brakeman` with the app's Ruby, where
|
|
404
|
+
the scan produced no report while `doctor` passed it. `doctor` runs that
|
|
405
|
+
script's `--version` the same way and warns when it does not answer.
|
|
406
|
+
- **`rails_validate` reads ERB trim tags as Rails does.** `<%-` and `-%>`
|
|
407
|
+
were compiled as a unary minus, so every template using them failed, and
|
|
408
|
+
the installed pre-commit hook blocked the commit. An `if` left open and an
|
|
409
|
+
extra `end` still fail.
|
|
410
|
+
- **An ERB syntax error names the template line it is on.** Errors were
|
|
411
|
+
reported a line low, two with a `frozen_string_literal` comment. A keyword
|
|
412
|
+
left unpaired was blamed on a line past the template's end; it is now
|
|
413
|
+
reported on its own line, and a bracket left open at the end of the
|
|
414
|
+
template.
|
|
415
|
+
- **`rails_validate` says why a path is refused.** `app/models/../models/x.rb`
|
|
416
|
+
and absolute paths were refused as "outside Rails root"; the message now
|
|
417
|
+
names the `..` or the absolute path. A directory is called a directory,
|
|
418
|
+
and only a file other than the input is suggested.
|
|
419
|
+
- **`level:"rails"` stops reporting columns that macros declare.**
|
|
420
|
+
`has_secure_password`, its confirmation and challenge, `has_one_attached`,
|
|
421
|
+
`has_many_attached` and `has_rich_text` were each reported as a missing
|
|
422
|
+
column with a migration to add it, in `validates` and in `permit` lists.
|
|
423
|
+
- **`level:"rails"` knows route helpers that are not rows of the route
|
|
424
|
+
table.** Helpers in app/helpers, `helper_method` names, `direct` routes
|
|
425
|
+
and Rails' own routes such as `rails_blob_url` are no longer reported as
|
|
426
|
+
missing.
|
|
427
|
+
- **An ivar set by an ancestor's or a concern's `before_action` is not
|
|
428
|
+
reported as unset.** The view check now reads the controller, its
|
|
429
|
+
ancestors and the modules they include.
|
|
430
|
+
- **No warning asks for a `.turbo_stream.erb` beside a `turbo_stream_from`
|
|
431
|
+
view.** Nothing renders such a template.
|
|
432
|
+
- **`rails_review_changes` with a ref diffs from the merge base.** A
|
|
433
|
+
validation the ref added after the branch left it no longer reads as one
|
|
434
|
+
the branch removed, and an edited `validates` line is not reported as
|
|
435
|
+
removed.
|
|
436
|
+
- **`rails_review_changes` reviews an app in a subfolder of its repository
|
|
437
|
+
on its own.** Files are named from the app root, with their diffs and the
|
|
438
|
+
right type; another app's files and commits are left out.
|
|
439
|
+
- **`rails_review_changes` reads a migration's table however the call is
|
|
440
|
+
written.** `change_table "orders"` and
|
|
441
|
+
`add_column("orders", :coupon_id, :integer)` now get the "Affects tables"
|
|
442
|
+
line and the missing-index warning. The line reads every schema method
|
|
443
|
+
that takes a table, through Rails 8.1: `remove_columns :users, :name`
|
|
444
|
+
names users, as do `rename_index`, the comment, constraint and index
|
|
445
|
+
methods; `drop_table` names each table it drops, `rename_table` the new
|
|
446
|
+
name too, and `create_join_table :users, :roles` the join table it makes,
|
|
447
|
+
roles_users, and both tables. A call on a `change_table` block's table
|
|
448
|
+
(`t.remove_index :number`) or a commented-out line names no table.
|
|
449
|
+
- **The pre-commit hook no longer blocks every commit when the app cannot
|
|
450
|
+
boot, and shows why validation failed.** It validates through
|
|
451
|
+
`bundle exec rails-ai-context tool validate`, which checks from source
|
|
452
|
+
when the boot fails, and its stderr is shown. Re-running the generator
|
|
453
|
+
upgrades an unchanged earlier hook.
|
|
454
|
+
- **The pre-commit hook checks staged `.js`, `.mjs` and `.cjs` files.** A
|
|
455
|
+
broken Stimulus controller is now blocked.
|
|
456
|
+
- **A file saved mid-edit no longer crashes the server.** With web-console
|
|
457
|
+
in the bundle, saving two files that did not compile made the next
|
|
458
|
+
`rails_get_model_details` segfault in about half the runs: Ruby crashed
|
|
459
|
+
compiling them under web-console's raise hook. A file Prism finds a
|
|
460
|
+
syntax error in is no longer required while introspecting, nor when a
|
|
461
|
+
tool looks a class up by name - the receiver or the exception class
|
|
462
|
+
`rails_diagnose` is given, a mixin `rails_get_concern` checks, a model
|
|
463
|
+
`rails_validate` asks about a callback - nor is one whose class body
|
|
464
|
+
names its constant (a subclass of a broken `ApplicationRecord`). The
|
|
465
|
+
answer gives the error, and the log names each file once.
|
|
466
|
+
- **`rails_get_controllers` names a controller whose file does not
|
|
467
|
+
compile.** A controller left unloaded by a syntax error in its file, or in
|
|
468
|
+
a file its class body needs (its parent controller), was listed with the
|
|
469
|
+
actions and filters its source read as, with no word that Rails cannot
|
|
470
|
+
load it. Its entry now gives the error, in every listing and in its own
|
|
471
|
+
answer, as `rails_get_model_details` does for a model, and so do the
|
|
472
|
+
`controllers/{name}` and `controllers/{name}/{action}` resources, where
|
|
473
|
+
an action read built its answer from the broken source.
|
|
474
|
+
- **A tool call that raises a `SyntaxError` or a `LoadError` answers with an
|
|
475
|
+
error and leaves the server running.** The rescue that turns a tool's
|
|
476
|
+
failure into an error result caught `StandardError` only, so a
|
|
477
|
+
`ScriptError` out of an app file a tool reached went past it and past
|
|
478
|
+
the MCP SDK, and ended a stdio server with every tool in it. It is now
|
|
479
|
+
that call's `Tool ... failed: SyntaxError: ...` answer.
|
|
480
|
+
- **Live reload loads no app code on the watching thread.** Listen's
|
|
481
|
+
callback only marks a reload as pending; the next tool call reloads on its
|
|
482
|
+
own thread. `watch` reloads and regenerates on its main thread and keeps
|
|
483
|
+
going past an error from the app's code.
|
|
484
|
+
- **A server that cannot reload code says so once the code changed.** Under
|
|
485
|
+
`RAILS_ENV=test`, or production with eager loading, every answer ends with
|
|
486
|
+
a note naming the changed files, since what reflection reads (associations,
|
|
487
|
+
enums) is as of boot, and a JSON answer or resource body carries it under
|
|
488
|
+
`_stale_code`. The server instructions no longer promise that reflection
|
|
489
|
+
follows edits there.
|
|
490
|
+
- **A resource read from the static tier says so**, under `_static_tier` in
|
|
491
|
+
its JSON body, as a tool's JSON answer does.
|
|
492
|
+
- **A Ruby file saved twice within one second at the same size is named in
|
|
493
|
+
every answer.** Bootsnap's compile cache knows a file by its size and the
|
|
494
|
+
second of its mtime, so the reload such a save sets off got the code of
|
|
495
|
+
the earlier save, and an answer showing the earlier save's associations
|
|
496
|
+
read as current. Bootsnap has no way to drop one entry, so a server that
|
|
497
|
+
reloads now ends every answer with a note naming the file until it is
|
|
498
|
+
saved in another second, and COMPATIBILITY.md lists the limit and
|
|
499
|
+
`DISABLE_BOOTSNAP_COMPILE_CACHE=1`.
|
|
500
|
+
- **`rails_query` runs a read-only query that names a write keyword only
|
|
501
|
+
inside a string.** `WHERE title LIKE '%update%'`, `= 'put into box'` and
|
|
502
|
+
the `replace()` string function were refused as writes; a keyword or
|
|
503
|
+
`INTO` is now judged outside string literals, and `SELECT ... INTO`,
|
|
504
|
+
`REPLACE INTO`, `INTO OUTFILE` and writes inside a CTE are still refused.
|
|
505
|
+
- **`rails_query` runs `SHOW CREATE TABLE` and `SHOW CREATE VIEW`** on
|
|
506
|
+
MySQL and MariaDB, showing the whole definition, with credentials in it
|
|
507
|
+
redacted.
|
|
508
|
+
`SHOW CREATE USER`, `SHOW CREATE DATABASE` and `SHOW GRANTS` stay refused.
|
|
509
|
+
- **Every command a `doctor` fix gives runs where `doctor` was typed.**
|
|
510
|
+
Typed in an app's parent folder with `--app-path blog`, as `init`'s
|
|
511
|
+
folder-of-apps summary suggests, the fixes did nothing or the wrong thing
|
|
512
|
+
there: `bin/rails db:migrate` was no file, `rails-ai-context init` set up
|
|
513
|
+
every app in the folder, `git rm --cached` found no repository, and a
|
|
514
|
+
`.gitignore` line spelled from the parent folder (`blog/.env.local`)
|
|
515
|
+
ignored nothing in blog's own `.gitignore`. The binary's commands now
|
|
516
|
+
carry `--app-path blog`, the app's other commands run after `cd blog &&`,
|
|
517
|
+
git runs as `git -C blog`, and a `.gitignore` line names `blog/.gitignore`
|
|
518
|
+
with the pattern as that file reads it (`.env.local`).
|
|
519
|
+
- **The install finds a Codex server however its name is spelled, and never
|
|
520
|
+
declares it twice.** A quoted table (`[mcp_servers."rails-ai-context"]`),
|
|
521
|
+
an inline table or dotted keys went unseen: a second table was appended,
|
|
522
|
+
and Codex refused the whole file ("duplicate key") while re-runs said
|
|
523
|
+
"(unchanged)" and `doctor` passed it. A quoted table is now replaced where
|
|
524
|
+
it stands, and two tables become one. An entry of the gem's in another
|
|
525
|
+
form is left as it is, with what to do by hand. `doctor` fails a server
|
|
526
|
+
declared twice and warns about an entry the install does not rewrite.
|
|
527
|
+
- **An MCP config with trailing commas is read as VS Code reads it** (json
|
|
528
|
+
2.9 or later). Every run left `.vscode/mcp.json` as it was, kept asking
|
|
529
|
+
for the entry by hand after it had been added, and `doctor` failed the
|
|
530
|
+
file.
|
|
531
|
+
- **`init` in a folder holding one app names the folder's files as the
|
|
532
|
+
folder's.** It printed `backend/.mcp.json` for the folder's `.mcp.json`.
|
|
533
|
+
Each app's `.gitignore` is now named by its app, and an app's legacy-file
|
|
534
|
+
`rm -f` line works when pasted where `init` ran.
|
|
535
|
+
- **Removing one of two dropped tools keeps the file the other still
|
|
536
|
+
uses.** Removing OpenCode's files while also dropping, but keeping, Codex
|
|
537
|
+
CLI's deleted `AGENTS.md`.
|
|
538
|
+
- **An MCP config is read the way its client reads it.** Claude Code reads
|
|
539
|
+
`.mcp.json` as plain JSON, so a hand-edited one with a trailing comma or a
|
|
540
|
+
comment loaded no server while the install called it "(unchanged)" and
|
|
541
|
+
`doctor` passed it. Such a file is now reported, by the install and by
|
|
542
|
+
`doctor`, as one Claude Code cannot read; Cursor's `.cursor/mcp.json` is
|
|
543
|
+
held to plain JSON too. VS Code's `.vscode/mcp.json` and OpenCode's
|
|
544
|
+
`opencode.json`, which read JSONC, keep taking comments and trailing
|
|
545
|
+
commas.
|
|
546
|
+
- **A Codex config with a byte that is not UTF-8 in a quoted server name no
|
|
547
|
+
longer stops `init`, or drops `doctor`'s MCP and Codex rows.** Codex
|
|
548
|
+
refuses a config that is not UTF-8 at all, so the install says so and
|
|
549
|
+
`doctor` fails the file.
|
|
550
|
+
- **A directory the app links in from its own repository is read as the
|
|
551
|
+
app's own.** With `app/javascript/controllers` linked to a monorepo
|
|
552
|
+
package every Stimulus controller was missing; the code, view and
|
|
553
|
+
JavaScript walks now follow such a link as Zeitwerk, the view resolver and
|
|
554
|
+
the bundlers do, and an edit inside one refreshes the cache.
|
|
555
|
+
- **A path through a linked-in pack is accepted by every tool that takes a
|
|
556
|
+
path.** `rails_get_edit_context`, `rails_validate` and `rails_search_code`
|
|
557
|
+
refused `packs/billing/app/models/invoice.rb`, which
|
|
558
|
+
`rails_get_model_details` named, and a search of the app never read the
|
|
559
|
+
pack.
|
|
560
|
+
- **`rails_get_partial_interface` names a linked-in pack's partial as the
|
|
561
|
+
app spells it**, finds its render sites, and points at a `rails_get_view`
|
|
562
|
+
path that resolves.
|
|
563
|
+
- **A link that resolves nowhere is never offered back as the file to try**,
|
|
564
|
+
nor listed as a Stimulus controller or a view.
|
|
565
|
+
- **Every tool refuses a path in the same words, and echoes a long one
|
|
566
|
+
shortened.** `Path not allowed: <path>`, with `(sensitive file)` when the
|
|
567
|
+
name is the reason, where some tools said "Access denied" or "outside
|
|
568
|
+
Rails root"; a 100 KB path came back as a 100 KB refusal.
|
|
569
|
+
- **The mounted engine keeps its MCP sessions across a code reload.** Every
|
|
570
|
+
edit in development dropped them (404 "Session not found") and left one
|
|
571
|
+
more session-reaper thread running.
|
|
572
|
+
- **The mounted engine (`mount RailsAiContext::Engine`) no longer deadlocks
|
|
573
|
+
when clients call it while app code reloads in development.** Since 5.16.1
|
|
574
|
+
it answered on a thread `ActionController::Live` starts, which needs a
|
|
575
|
+
share of the lock Rails reloads code under. Once an edit had a reload
|
|
576
|
+
waiting, no new share was granted, and the reload waited for the request,
|
|
577
|
+
which waited for its Live thread: with six clients calling while a file
|
|
578
|
+
changed every 0.3 seconds, every call hung and `rails server` ignored
|
|
579
|
+
SIGTERM, in every run on Rails 8.1 and in three of four longer runs on
|
|
580
|
+
7.0. The Live thread also kept the database connection a call had used, so
|
|
581
|
+
from Rails 8.0 each call after an edit sat out the reload's ten-second
|
|
582
|
+
wait for it: 10.3 seconds a call, where it now takes 0.4. The engine now
|
|
583
|
+
runs each call on the request's own thread and sends the answer whole; the
|
|
584
|
+
same runs answer every call and stop on SIGTERM. The cost is streaming:
|
|
585
|
+
what a custom tool sends during a call, such as progress or log
|
|
586
|
+
notifications, arrives with its answer, and a request a tool makes of the
|
|
587
|
+
client mid-call (sampling, elicitation, roots) fails at once, saying why,
|
|
588
|
+
where it could only have waited out its timeout. `subscriptions/listen`,
|
|
589
|
+
whose notification stream an answer sent whole cannot keep open, is
|
|
590
|
+
answered as a method the engine does not have. stdio and the standalone
|
|
591
|
+
HTTP server still stream.
|
|
592
|
+
- **The engine and `auto_mount` answer the hosts the app's `config.hosts`
|
|
593
|
+
allows.** The MCP SDK answered only loopback names, so
|
|
594
|
+
`allow_http_in_production` could not serve an app by its own name, and
|
|
595
|
+
`myapp.localhost` was refused in development. Rails' host authorization
|
|
596
|
+
judges the Host now; a cross-origin request is still refused.
|
|
597
|
+
- **A stdio message over 4 MiB is answered, named on stderr, and ends the
|
|
598
|
+
server with status 1**, where it exited 0 with nothing said.
|
|
599
|
+
- **Email addresses in any script are redacted from log lines**
|
|
600
|
+
(`jürgen@example.com`, `anna@münchen.de`).
|
|
601
|
+
- **A missing view, an unknown resource URI and an unknown tool are
|
|
602
|
+
answered with what exists**, in `data.available` and, for a tool, the
|
|
603
|
+
nearest names in the message.
|
|
604
|
+
- **A generated length test sets a value the validation rejects.** A length
|
|
605
|
+
validation with no maximum was tested with `""`, so
|
|
606
|
+
`length: { minimum: 10 }, allow_blank: true` got the one value it allows
|
|
607
|
+
and the test failed on a valid record. The value is now one past the
|
|
608
|
+
maximum (or the `is:` or range end), else one short of the minimum; `""`
|
|
609
|
+
is used only for a minimum of 1 that does not allow blanks.
|
|
610
|
+
- **Generated tests call `FactoryBot.create` where the suite does not
|
|
611
|
+
include FactoryBot's methods.** A bare `create(:article)` works only where
|
|
612
|
+
rails_helper, a support file or test_helper includes
|
|
613
|
+
`FactoryBot::Syntax::Methods`; without it every generated example failed
|
|
614
|
+
with "undefined method `create'".
|
|
615
|
+
- **A controller whose base class inherits `ActionController::API` is an
|
|
616
|
+
API controller with the app unbooted too.**
|
|
617
|
+
`Api::V1::PostsController < BaseController < ActionController::API` read
|
|
618
|
+
as an ordinary controller under `--no-boot`.
|
|
619
|
+
- **`rails_get_partial_interface` reads `render "comments/form"` as the
|
|
620
|
+
comments form.** `posts/_form` took the comment form's call in
|
|
621
|
+
`posts/show`, and its `comment` local.
|
|
622
|
+
- **`rails 'ai:tool[...]'` keeps an argument that contains a bracket.**
|
|
623
|
+
`pattern=params\[:id\]` was dropped, and the answer was "Pattern is
|
|
624
|
+
required".
|
|
625
|
+
- **The stack overview counts the databases it does not name.**
|
|
626
|
+
"Databases: 5 (primary, analytics, cache)" read as the whole list; it now
|
|
627
|
+
ends "...2 more".
|
|
628
|
+
- **A filtered `rails_get_turbo_map` judges each stream against the whole
|
|
629
|
+
app.** A `controller:` filter called a stream unheard, and warned about
|
|
630
|
+
it, when its subscribers were in another controller's views.
|
|
631
|
+
- **`rails_session_context` keeps a conversation's latest 200 queries, each
|
|
632
|
+
long param shortened, and says how many it dropped.** One client could
|
|
633
|
+
grow the server's memory without limit, and an answer echoed a 100 KB
|
|
634
|
+
argument back in full.
|
|
635
|
+
- **Source a tool shows has its secrets filtered, as search results did.**
|
|
636
|
+
`rails_search_code` and `rails_get_edit_context` filtered a literal key
|
|
637
|
+
from the lines they showed, while an action's source in
|
|
638
|
+
`rails_get_controllers` (and the methods and params methods it calls), a
|
|
639
|
+
concern's methods, a callback's body, a template in `rails_get_view` and
|
|
640
|
+
`rails_get_partial_interface`, a test file in `rails_get_test_info`, a
|
|
641
|
+
job's guard clauses, a `rails_security_scan` code line and the
|
|
642
|
+
`controllers/{name}/{action}` and `views/{path}` resources showed the same
|
|
643
|
+
line as written.
|
|
644
|
+
- **`rails_get_controllers` and the `controllers/{name}/{action}` resource
|
|
645
|
+
find an action a concern or a parent controller gives.** Rails counts
|
|
646
|
+
every public method of an `ApplicationController` concern among each
|
|
647
|
+
controller's actions; the action list takes one only where a route names
|
|
648
|
+
it, so a lookup by name of an unrouted one answered "not found". The
|
|
649
|
+
answer now reads it from the file that defines it and names the parent
|
|
650
|
+
controller (`inherited_from` in the resource). A callback, a predicate, a
|
|
651
|
+
method that takes an argument, or one a private def lower down hides is
|
|
652
|
+
still not an action. Under `--no-boot` the action list also takes a routed
|
|
653
|
+
method of a parent's concern, as the booted list did.
|
|
654
|
+
- **An action a concern or a parent controller defines shows its source.**
|
|
655
|
+
`rails_get_controllers(controller:, action:)` read only the controller's
|
|
656
|
+
own file, so an action from an included concern (namespaced ones too) or
|
|
657
|
+
from a base controller answered "Could not extract source code" with an
|
|
658
|
+
absolute path. It now follows Ruby's lookup through the app's modules and
|
|
659
|
+
parent controllers and names the file the source came from, in both
|
|
660
|
+
tiers. The `rails-ai-context://controllers/{name}/{action}` resource,
|
|
661
|
+
described as the action's source and metadata, never carried the source;
|
|
662
|
+
it now has `file`, `lines` and `source`.
|
|
663
|
+
- **A Rails 8 controller's concerns no longer list the route helpers Rails
|
|
664
|
+
mixes in.** `Rails::Engine::LazyRouteSet::MountedHelpers` appeared as a
|
|
665
|
+
concern of every controller in the booted `controllers` payload (the
|
|
666
|
+
`rails://controllers` resource and `.ai-context.json`), and not in the
|
|
667
|
+
static tier's.
|
|
668
|
+
- **`tool --json` and `JSON=1 rails 'ai:tool[...]'` print the envelope and
|
|
669
|
+
nothing else.** Sidekiq's "connecting to Redis" line, and on the rake task
|
|
670
|
+
an initializer's `puts`, landed on stdout ahead of it. What the app prints
|
|
671
|
+
while it boots or a tool runs now goes to stderr, as under `serve`.
|
|
672
|
+
- **An invalid tool argument is reported as one when the app's locale files
|
|
673
|
+
do not load.** With a locale file that did not parse, every invalid
|
|
674
|
+
argument came back as a JSON-RPC internal error with an I18n backtrace on
|
|
675
|
+
stderr, over stdio and HTTP. An exception that still escapes a tool is
|
|
676
|
+
named in one stderr line, its backtrace under `DEBUG`.
|
|
677
|
+
- **A preset prints each tool's header above that tool's answer, on
|
|
678
|
+
stdout**, from the binary and the rake task; the headers went to stderr,
|
|
679
|
+
so a redirect of both streams put them out of order.
|
|
680
|
+
- **A tool `skip_tools` turned off says so** on the CLI, the rake task and
|
|
681
|
+
the MCP server, which answered "Unknown tool" and "Tool not found".
|
|
682
|
+
- **Every command answers `--help` and `-h`**, exit 0, where each failed with
|
|
683
|
+
Thor's "was called with arguments" (`inspect`'s naming `inspect_app`) and
|
|
684
|
+
`tool`/`preset` read the flag as a name. `doctor --no-boot` and
|
|
685
|
+
`serve --transport bogus` are refused in words, the transport before the
|
|
686
|
+
boot. `tool --list` in an app whose boot fails says so in one line. An
|
|
687
|
+
unknown param's suggestion keeps the form it was typed in.
|
|
688
|
+
- **An unknown format is refused before any work, with every valid one named,
|
|
689
|
+
`all` included**: `rails 'ai:context_for[bogus]'` printed "Writing bogus
|
|
690
|
+
context file..." and then a raw `ArgumentError`.
|
|
691
|
+
- **An unknown `context_mode` or `tool_mode` is refused** from
|
|
692
|
+
`CONTEXT_MODE`, the YAML and the initializer, where it was kept and read
|
|
693
|
+
as the default.
|
|
694
|
+
- **The `ai:facts` footer names a command that gives the full JSON**,
|
|
695
|
+
`rails ai:context:json`, since `ai:inspect` prints a text summary, and the
|
|
696
|
+
`ai:inspect` table no longer pairs `.codex/config.toml` with
|
|
697
|
+
`rails ai:context:codex`.
|
|
698
|
+
- **`ruby -W` prints no warning from the gem's own files.**
|
|
699
|
+
- **`doctor` checks that each MCP config's command can start the server.** A
|
|
700
|
+
config passed when it parsed, so after `bundle remove rails-ai-context`
|
|
701
|
+
every `bundle exec rails-ai-context serve` entry still read as valid, an
|
|
702
|
+
in-Gemfile app whose `.mcp.json` ran the bare binary was told it
|
|
703
|
+
"activates via bundler", and a `path:` copy without the executable passed
|
|
704
|
+
while `bundle exec` could not find it. Each entry is now checked without
|
|
705
|
+
starting it: `bundle exec` needs `bundle` on the PATH the client gives the
|
|
706
|
+
server, a lockfile (the one the entry's `BUNDLE_GEMFILE` names, if set)
|
|
707
|
+
that carries the gem, and a copy that lists its executable; a bare
|
|
708
|
+
`rails-ai-context` must be on that PATH, and in an app whose bundle
|
|
709
|
+
carries the gem it is a warning. The fix names the command to rerun, a
|
|
710
|
+
config that holds no rails-ai-context server says so, and a JSON config
|
|
711
|
+
that does not parse is named in the install's words.
|
|
712
|
+
- **`doctor` checks the PATH a Codex env snapshot saved.** Codex starts the
|
|
713
|
+
server with only the env its config sets, and an rbenv or asdf snapshot
|
|
714
|
+
saves `PATH` alone, so checking `GEM_HOME` passed a snapshot whose Ruby
|
|
715
|
+
had been removed. Each snapshot's `PATH` now has to reach the command its
|
|
716
|
+
entry runs, in the app's config and in a folder-of-apps config above it,
|
|
717
|
+
and a failure names the directory that is gone and where to rerun
|
|
718
|
+
`rails-ai-context init`. A saved `GEM_HOME` has to exist too, and so does
|
|
719
|
+
each directory on a saved `GEM_PATH` that the shell's own `GEM_PATH` does
|
|
720
|
+
not name: RubyGems passes over a directory that is not there, and `init`
|
|
721
|
+
saves the shell's `GEM_PATH` again, so a warning about one could never
|
|
722
|
+
clear.
|
|
723
|
+
- **A context file is out of date when a context run would rewrite it.**
|
|
724
|
+
`doctor` compared file times, so touching a model raised "CLAUDE.md may be
|
|
725
|
+
stale" and `rails ai:context`, which leaves unchanged files alone, could
|
|
726
|
+
not clear it; it ignored `output_dir`; and after an upgrade it called
|
|
727
|
+
files stamped with the old version up to date. It now runs the context
|
|
728
|
+
generation into a scratch copy, names the files it would write and why
|
|
729
|
+
(the version that wrote them, or the folders changed since), and the fix
|
|
730
|
+
it names clears it. The app's files are not touched.
|
|
731
|
+
- **`doctor` reads pending migrations in every database.** Only the primary
|
|
732
|
+
was asked, so a pending migration for a second database passed. Each
|
|
733
|
+
database the environment configures is asked through its own connection,
|
|
734
|
+
as `db:migrate:status` does, without writing to it, the message names the
|
|
735
|
+
database, and the Migrations row counts every database's files.
|
|
736
|
+
- **A development database that does not exist fails `doctor`.** It scored
|
|
737
|
+
93 and `--strict` exited 0. A Database row now fails, naming a database
|
|
738
|
+
that does not exist (fix: `bin/rails db:prepare`) or one that does not
|
|
739
|
+
answer, with the error.
|
|
740
|
+
- **`doctor` reads the bundle, suite and code the tools read.** It reported
|
|
741
|
+
"Gemfile.lock not found" for an app whose `config/boot.rb` names a shared
|
|
742
|
+
Gemfile above it, and in an engine's `test/dummy` it counted only the
|
|
743
|
+
dummy's models, controllers, views and tests and found no migrations. It
|
|
744
|
+
now finds the lockfile as the tools do, counts the engine's code, suite
|
|
745
|
+
and migrations with the dummy's, and names the engine's root as the place
|
|
746
|
+
to run the db task it suggests.
|
|
747
|
+
- **`doctor`'s secrets check is about the files that hold secrets.** Every
|
|
748
|
+
stock app was warned about committing `config/credentials.yml.enc`,
|
|
749
|
+
`config/database.yml`, `config/cable.yml` and `config/storage.yml`. A
|
|
750
|
+
committed key or env file (`config/master.key`,
|
|
751
|
+
`config/credentials/*.key`, `.env*` and the like) still fails; a config
|
|
752
|
+
such as `config/database.yml` warns only when it holds a literal password,
|
|
753
|
+
token or URL with a password, naming the key and line; encrypted
|
|
754
|
+
credentials are not reported.
|
|
755
|
+
- **`doctor` agrees with the boot about a pinned dependency.** With
|
|
756
|
+
`prism 1.3.0` locked, the boot warned that the gem needs `>= 1.4` while the
|
|
757
|
+
Prism parser row passed, and the same held for `mcp` and the MCP server
|
|
758
|
+
row. Both rows now warn with the locked and needed versions and name
|
|
759
|
+
`bundle update <gem>` as the fix.
|
|
760
|
+
- **`rails_validate` checks a Stimulus controller as the ES module it is.**
|
|
761
|
+
On Node 22, `node -c` exits 0 for any `.js` file that imports when its
|
|
762
|
+
package declares no type, broken or not, so every Stimulus controller
|
|
763
|
+
passed. The source now goes to node on stdin with its module type spelled
|
|
764
|
+
out, decided as Node decides it, and nothing is written next to the app.
|
|
765
|
+
A failure names the line, the column and node's message, where it printed
|
|
766
|
+
the first lines of node's output and cut the message off. Without node, a
|
|
767
|
+
file whose brackets balance is listed as skipped rather than passed, and
|
|
768
|
+
JSX is skipped rather than failed. `.mjs` and `.cjs` files are checked too.
|
|
769
|
+
- **`rails_get_callbacks` lists the callbacks turbo-rails' broadcast macros
|
|
770
|
+
declare.** A model with `broadcasts_refreshes`, `broadcasts_to`,
|
|
771
|
+
`broadcasts` or `broadcasts_refreshes_to` read as having none of the commit
|
|
772
|
+
callbacks they add. Each is listed under its event as
|
|
773
|
+
`broadcasts_refreshes (turbo-rails)`, with the method turbo-rails runs at
|
|
774
|
+
`detail:"full"`, and `broadcasts_refreshes` is a macro on
|
|
775
|
+
`rails_get_model_details` too. `detail:"full"` also prints a block or
|
|
776
|
+
lambda callback's declaration, where it printed `[inline_block]` alone.
|
|
777
|
+
- **A scope declared in a loop over a literal list is a scope.**
|
|
778
|
+
`KINDS.each { |k| scope k, -> { ... } }` gave no scope statically, and
|
|
779
|
+
booted the scopes read as class methods. Each element is now a scope,
|
|
780
|
+
tagged `[INFERRED]`.
|
|
781
|
+
- **`rails_diagnose` calls a `NoMethodError` a nil reference only when the
|
|
782
|
+
receiver is nil.** On any other receiver it is an undefined method, and
|
|
783
|
+
booted, the answer names the receiver's closest method; a private method
|
|
784
|
+
is said to be private.
|
|
785
|
+
- **`rails_diagnose` reads an error as a Rails log writes it.**
|
|
786
|
+
`Pundit::AuthorizationNotPerformedError (ProductsController)` came back as
|
|
787
|
+
`Error: Unknown`. Authorization not performed and authorization denied are
|
|
788
|
+
classified, and an error with no rule names the gem or app file that
|
|
789
|
+
defines its class.
|
|
790
|
+
- **`rails_diagnose`'s log correlation finds the request.** It searched the
|
|
791
|
+
log's last 15 lines and printed the matching line alone. It now reads the
|
|
792
|
+
log's last megabyte and shows the latest entry: path, action, parameters,
|
|
793
|
+
status, request id, the error and the first frames, all redacted. It says
|
|
794
|
+
so when there is no entry.
|
|
795
|
+
- **`rails_dependency_graph` counts only an `as:` declaration as implementing
|
|
796
|
+
a polymorphic interface, and lists foreign-key cycles only.** An
|
|
797
|
+
association and its inverse were listed as a cycle, sixteen of them on an
|
|
798
|
+
app with none; when there are none the answer now says so.
|
|
799
|
+
- **`rails_get_concern` lists private methods and `helper_method`, and says
|
|
800
|
+
when an include is conditional.** On a booted app, a conditionally
|
|
801
|
+
included module that is not defined is said never to have been mixed in.
|
|
802
|
+
- **`rails_get_env` skips YAML comment lines and reads a block's literal
|
|
803
|
+
default.** A default in a `sensitive_patterns` file is said to be present
|
|
804
|
+
and unread, where a lone site read as having none.
|
|
805
|
+
- **`rails_runtime_info` says its pool and cache are the MCP server
|
|
806
|
+
process's own.** It also names the shared store another environment keeps
|
|
807
|
+
the app's cache in.
|
|
808
|
+
- **An in-Gemfile install from a copy outside git has its executable.** The
|
|
809
|
+
gemspec listed its files with `git ls-files`, which lists nothing outside a
|
|
810
|
+
checkout, so a `path:` entry pointing at a vendored or unpacked copy left
|
|
811
|
+
the gem without `rails-ai-context` and every MCP config's
|
|
812
|
+
`bundle exec rails-ai-context serve` failed to start. Outside a checkout
|
|
813
|
+
the files on disk are the list.
|
|
814
|
+
- **`rails_review_changes` shows what a migration's tables hold now.** Each
|
|
815
|
+
affected table's line was the first line of `rails_get_schema`, which is
|
|
816
|
+
its heading (`products: ## Table: products`); it is now the table's
|
|
817
|
+
current columns, or "not in the schema yet" for one the migration
|
|
818
|
+
creates. The routes line drops the heading marks it carried.
|
|
819
|
+
- **A folder of apps keeps its MCP configs true as apps come and go**
|
|
820
|
+
(#429). Folders named outside ASCII all make the name `app`, so the gem
|
|
821
|
+
numbered its own entries (`rails-ai-context-app-2`) and then never
|
|
822
|
+
recognised them: when one app went, the other kept a second server, or
|
|
823
|
+
one naming a folder that was gone. A numbered name of the gem's own is
|
|
824
|
+
now dropped like any other. An app folder that starts with a dash is
|
|
825
|
+
written `./-api`, where `--app-path -api` read as an option and the
|
|
826
|
+
server never started. A folder named in Latin-1 crashed `init` with
|
|
827
|
+
"invalid byte sequence in UTF-8"; JSON and TOML hold UTF-8 alone, so that
|
|
828
|
+
app is now left out of the folder's configs, with a message, and the
|
|
829
|
+
other apps' entries are written. A config holding comments is named for
|
|
830
|
+
them under json 3 too, which refuses comments json 2 passed over, where
|
|
831
|
+
it was blamed on a trailing comma.
|
|
832
|
+
- **Commands the binary prints read as a person would type them.** A path
|
|
833
|
+
holding a space or a character a shell reads is single-quoted, letters
|
|
834
|
+
outside ASCII stay as they are (`--app-path 'my apps/шоп'`, where every
|
|
835
|
+
one was backslashed), and a path that starts with a dash is spelled from
|
|
836
|
+
the current directory. The several-apps refusal says `<folder> is no
|
|
837
|
+
Rails app, and holds 2 below it`. The boot-failure hint and the static
|
|
838
|
+
tier's banner name the doctor command as the binary was run, with its
|
|
839
|
+
`--app-path` and under `bundle exec` when it ran so: a workspace's
|
|
840
|
+
server runs with `--app-path`, and a bare `doctor` where its client
|
|
841
|
+
stands finds the folder of apps.
|
|
842
|
+
- **The pre-commit hook checks a staged file whatever its name.** git
|
|
843
|
+
quotes a name holding a character outside ASCII, a quote or a backslash
|
|
844
|
+
in the list the hook read, so no `.rb` test matched it and the file went
|
|
845
|
+
unchecked: a broken `db/seeds/brokén.rb` committed. Names are now read
|
|
846
|
+
NUL-separated. validate takes its files comma-separated, so a name
|
|
847
|
+
holding a comma is named and left unchecked rather than passed in two
|
|
848
|
+
halves. With several apps in one hook each app's lines are headed by its
|
|
849
|
+
name, and the second app's question asks to add it. The question and the
|
|
850
|
+
hook say what it checks - that the staged Ruby, ERB and JavaScript files
|
|
851
|
+
parse - where
|
|
852
|
+
they promised hallucinated columns and missing models, which a commit
|
|
853
|
+
never checked.
|
|
854
|
+
- **The binary runs an app's own copy of the gem when its bundle has one.**
|
|
855
|
+
In an app whose Gemfile carries rails-ai-context at another version or
|
|
856
|
+
from a `path:` copy, the global binary loaded its own files before the
|
|
857
|
+
boot and the bundle's over them: "already initialized constant" warnings,
|
|
858
|
+
and a command that ran half of each copy (`version` said one release, the
|
|
859
|
+
server announced another). It now runs the command as
|
|
860
|
+
`bundle exec rails-ai-context` would, from where it was typed, and says
|
|
861
|
+
so on stderr: that is the copy the app's MCP configs start. Only a run
|
|
862
|
+
that boots the app does so, since the boot runs the app's bundle anyway:
|
|
863
|
+
`--no-boot` stays on the installed copy, as bundle exec would evaluate
|
|
864
|
+
the app's Gemfile and start whatever copy the app names, code nobody has
|
|
865
|
+
read on a repo just cloned. `init` asks its questions on the installed
|
|
866
|
+
copy and has the app's copy write only the context files, so an
|
|
867
|
+
MCP-only setup, by flag or at the prompt, runs nothing of the app's, and
|
|
868
|
+
the MCP configs it writes are not written again from the bundle's
|
|
869
|
+
environment. A
|
|
870
|
+
bundle not installed yet is left alone too (`bundle check` says so), so
|
|
871
|
+
its failed boot still serves the static tier. The bundle is the one a
|
|
872
|
+
`BUNDLE_GEMFILE` you set names (a dual boot's `Gemfile.next`), and a
|
|
873
|
+
command typed in a subdirectory, or in a folder holding the one app,
|
|
874
|
+
runs from the app root, where a released copy, which does not look for
|
|
875
|
+
the root, finds the app. Hints that name another command (`doctor`,
|
|
876
|
+
`facts`' pointer to `inspect`) name it as typed: no `bundle exec` the
|
|
877
|
+
user did not type, the `--app-path` they did.
|
|
878
|
+
- **The binary loads no gem the app pins before the app boots.** Run from a
|
|
879
|
+
subdirectory of an app (#427) or a folder holding one, it asked of every
|
|
880
|
+
directory it passed whether a non-Rails framework lived there, which
|
|
881
|
+
parsed the Gemfile with the prism gem; it read config/boot.rb the same
|
|
882
|
+
way. A Rails 8 app locks prism, json and securerandom, so a version
|
|
883
|
+
other than the newest installed loaded over ours at the boot: dozens of
|
|
884
|
+
"already initialized constant" lines, then a mixed library. The Gemfile
|
|
885
|
+
and config/boot.rb are now read with Ruby's own Ripper before the boot,
|
|
886
|
+
a directory is checked for a framework only once it holds an app, and
|
|
887
|
+
json, strscan and securerandom load when first used.
|
|
888
|
+
- **The install summary names a preset that exists:** it suggested
|
|
889
|
+
`rails 'ai:preset[arch]'`, which answers "Unknown preset". It is
|
|
890
|
+
`ai:preset[architecture]`.
|
|
891
|
+
- **Commands run from a subdirectory of an app find the app** (#427), the way
|
|
892
|
+
`bin/rails` and Bundler do: the nearest directory above with
|
|
893
|
+
`config/application.rb`, `config/environment.rb` or a `bin/rails` that
|
|
894
|
+
boots an app or an engine. A packwerk pack, `$HOME`, `node_modules`,
|
|
895
|
+
`vendor/bundle` and a gem install are walked past, never stopped at. The
|
|
896
|
+
app is named on stderr, so `serve` keeps stdout clean.
|
|
897
|
+
- **Commands run from a folder of apps say which they could read** (#428):
|
|
898
|
+
one app below is used, several are refused with the exact `--app-path`
|
|
899
|
+
command for each, and `tool --list` still lists.
|
|
900
|
+
- **`init --app-path` sets up the app it names** (#424). The config, every MCP
|
|
901
|
+
config and the `.gitignore` entries went to the current directory and the
|
|
902
|
+
context files to the named app; from a directory that is no app, it
|
|
903
|
+
refused outright. The commands it prints afterwards name the app the same
|
|
904
|
+
way. Under `bundle exec`, a command that reads an app other than the
|
|
905
|
+
bundle's says it boots against that bundle, and the install form every
|
|
906
|
+
file names - the MCP command, the commands in context files and tool
|
|
907
|
+
hints - is read from the app's own Gemfile.lock, not the bundle's.
|
|
908
|
+
- **`tool --list --app-path` lists the app's custom tools** (#426). It never
|
|
909
|
+
booted the named app, so tools from its initializer were missing.
|
|
910
|
+
- **A wrong `--app-path` inside an app names the app**, and a relative one
|
|
911
|
+
that misses because the client started inside an app names the folder it
|
|
912
|
+
was written for.
|
|
913
|
+
- **The install generator offers the pre-commit hook in a submodule, a
|
|
914
|
+
monorepo app and under `core.hooksPath`** (#425). It looked for a `.git`
|
|
915
|
+
directory in the app root and skipped the step silently otherwise; the
|
|
916
|
+
hooks directory is now git's own answer. A monorepo's hook validates each
|
|
917
|
+
app's staged files from inside that app, from a linked worktree too, and
|
|
918
|
+
installing in a second app adds it to the same hook. A deleted file no
|
|
919
|
+
longer blocks the commit: there is nothing left to validate, and an
|
|
920
|
+
exported `CDPATH` cannot send the hook into another folder of the same
|
|
921
|
+
name. Running the generator again brings a hook an earlier version wrote,
|
|
922
|
+
and nobody changed since, up to date. A `core.hooksPath` outside the
|
|
923
|
+
repository, which every repository using it shares, a repository that does
|
|
924
|
+
not track the app (a dotfiles repository at `$HOME`), and any other hook
|
|
925
|
+
that runs `rails-ai-context` are left alone with a message.
|
|
926
|
+
- **The pre-commit hook starts a `path:` copy of the gem from a submodule
|
|
927
|
+
or a linked worktree.** git hands every command a hook runs the
|
|
928
|
+
committing repository's `GIT_DIR` and `GIT_INDEX_FILE`, and the gemspec's
|
|
929
|
+
`git ls-files` read that repository: in an app kept as a submodule the
|
|
930
|
+
gem's file list was the app's, and every commit failed with "can't find
|
|
931
|
+
executable rails-ai-context". The gemspec now asks git about its own
|
|
932
|
+
directory alone.
|
|
933
|
+
- **An app with a Gemfile and no lockfile yet is read by what its Gemfile
|
|
934
|
+
names.** It counted as an in-Gemfile install, so a fresh clone that never
|
|
935
|
+
named the gem got `bundle exec` commands that could not start. A Gemfile
|
|
936
|
+
that pulls in others (`eval_gemfile`, `gemspec`) still counts as one.
|
|
937
|
+
- **Codex's config keeps the tables around the gem's own.** A server section
|
|
938
|
+
ran on into the next table when no blank line separated them, so updating
|
|
939
|
+
it replaced the user's table too; a comment above the next table now stays
|
|
940
|
+
with it, a section a formatter wrapped over several lines is still read as
|
|
941
|
+
the gem's, values are written as TOML strings rather than Ruby literals, a
|
|
942
|
+
file with Windows line endings keeps them, a byte order mark no longer
|
|
943
|
+
hides the first section, and one that is not ASCII is read whatever the
|
|
944
|
+
locale, by `doctor` too.
|
|
945
|
+
- **A JSON config the install cannot write back faithfully is left alone**,
|
|
946
|
+
with the entry to add by hand, said on the terminal: one that does not
|
|
947
|
+
parse (VS Code's and OpenCode's take trailing commas) was replaced by a
|
|
948
|
+
file holding only the gem's server, one with comments lost them, and one
|
|
949
|
+
that is JSON but not an object, or not UTF-8, raised out of the install. A
|
|
950
|
+
byte order mark at the head of a config is kept, where it cost the file
|
|
951
|
+
its contents, and an entry of the user's under the gem's own name - the
|
|
952
|
+
HTTP one SETUP.md describes - is kept in place of the gem's, where it was
|
|
953
|
+
overwritten. `doctor` says to make such a file valid JSON holding an
|
|
954
|
+
object before running the install again, where it called one that parsed
|
|
955
|
+
valid whatever it held.
|
|
956
|
+
- **In a C or POSIX locale, a container's default, the binary reads paths
|
|
957
|
+
and files as UTF-8 when they are**, as Rails does once it boots. An app
|
|
958
|
+
folder or a folder of apps whose path holds characters outside ASCII, a
|
|
959
|
+
home directory holding them while the binary looked for the app, and an
|
|
960
|
+
initializer holding them stopped the binary with an encoding error. A
|
|
961
|
+
folder named in another encoding is read byte by byte, as before.
|
|
962
|
+
- **A hand-written `app/models/AGENTS.md` or `app/controllers/AGENTS.md`
|
|
963
|
+
keeps its text.** With OpenCode or Codex selected, `rails ai:context`,
|
|
964
|
+
`rails-ai-context context`, the installer and `init` replaced the file
|
|
965
|
+
with the gem's listing without a word. These names are ones teams write
|
|
966
|
+
by hand, so they now follow the root files' rule: a file the gem
|
|
967
|
+
generated whole (it carries `> Auto-generated by rails-ai-context. Do not
|
|
968
|
+
edit manually.`) is rewritten whole, a file holding the markers gets only
|
|
969
|
+
its block replaced, and any other file keeps its text and gets the
|
|
970
|
+
listing between markers above it, with a note that claims the block
|
|
971
|
+
rather than the file. Dropping the AI tool takes out only that block.
|
|
972
|
+
- **`tool_mode :cli` reaches every generated file.** The root files followed
|
|
973
|
+
it, but the split rule files and OpenCode's per-directory `AGENTS.md`
|
|
974
|
+
still said "ALWAYS use MCP tools" and "Use `rails_get_model_details` MCP
|
|
975
|
+
tool", the protocol's third rule named `rails_get_conventions`, the "All
|
|
976
|
+
45 tools" list gave MCP names, and the tools rule files called themselves
|
|
977
|
+
MCP tools in their Cursor description and Copilot name. In CLI mode each
|
|
978
|
+
of them names the command now (`rails 'ai:tool[model_details]'
|
|
979
|
+
model=Name`), the trim note included; MCP mode reads as before.
|
|
980
|
+
- **A static-tier CLAUDE.md no longer shows an empty Architecture
|
|
981
|
+
heading.** Without a boot there is no conventions section, so the
|
|
982
|
+
heading stood over a blank line before the services and jobs it held
|
|
983
|
+
and read as a section that came up empty; the names now follow it
|
|
984
|
+
directly, and the blank line only separates them from a pattern list.
|
|
985
|
+
- **The generated files count every database's tables and migrations.** In
|
|
986
|
+
an app with a second database, CLAUDE.md, AGENTS.md, `.cursorrules`,
|
|
987
|
+
copilot-instructions.md and the overview rule files counted the
|
|
988
|
+
primary's alone ("Migrations: 3 total" with 7 on disk), and
|
|
989
|
+
`.claude/rules/rails-schema.md` listed only the primary's tables, its
|
|
990
|
+
`paths:` missing `db/analytics_schema.rb` and `db/analytics_migrate/**`.
|
|
991
|
+
The Database and Migrations lines now name each other database after the
|
|
992
|
+
primary (`analytics: SQLite - 3 tables`, `analytics: 4 total, 1 pending`,
|
|
993
|
+
pending read from that database's dump as `rails_get_schema` reads it),
|
|
994
|
+
the schema rule lists each database's tables under its own heading and
|
|
995
|
+
triggers on each one's dump and migrations, and full mode's schema and
|
|
996
|
+
migrations sections cover them all. A database whose every table is
|
|
997
|
+
Solid Queue's, Solid Cache's or Solid Cable's, as a Rails 8 app's queue,
|
|
998
|
+
cache and cable are, is named for its framework in one phrase (`queue:
|
|
999
|
+
Solid Queue (13 tables)`); its tables, the framework's own, stay out of
|
|
1000
|
+
the schema rule and full mode's dump, and its dump out of the rule's
|
|
1001
|
+
`paths:`.
|
|
1002
|
+
- **Claude Code and Copilot read the tools guide once.** Both load the root
|
|
1003
|
+
file and the always-on tools rule file on every request, and both files
|
|
1004
|
+
carried the whole guide and the protocol, about 95 lines twice. With root
|
|
1005
|
+
files on, `.claude/rules/rails-mcp-tools.md` and
|
|
1006
|
+
`.github/instructions/rails-mcp-tools.instructions.md` now hold what the
|
|
1007
|
+
root file leaves out, the detail levels and the table of every tool, and
|
|
1008
|
+
point to it for the rest; with `generate_root_files` off they still carry
|
|
1009
|
+
the whole guide. Cursor's agent-requested rule is unchanged.
|
|
1010
|
+
- **The models rule files no longer contradict the tools guide.** They said
|
|
1011
|
+
"Read model files for business logic/methods" beside the guide's "NEVER
|
|
1012
|
+
read reference files - ... model files"; they now say to read a model
|
|
1013
|
+
file only when you edit it, the guide's own rule.
|
|
1014
|
+
- **A small `claude_max_lines` no longer cuts the tools guide.** Below
|
|
1015
|
+
about 110 lines the budget was a hard cap that trimmed the file from the
|
|
1016
|
+
end, so at 60 the four compact files kept the data and lost the
|
|
1017
|
+
anti-hallucination protocol and the whole tool reference to one line and
|
|
1018
|
+
the trim note, though the docs said those sections are kept whole. The
|
|
1019
|
+
title block and the Commands, Warnings, Rules and Tools sections are now
|
|
1020
|
+
never cut: the data sections between them go first, down to none, so
|
|
1021
|
+
those sections set the smallest file a budget can give.
|
|
1022
|
+
- **Full mode keeps the anti-hallucination protocol and the tools guide.**
|
|
1023
|
+
`context_mode :full` (`rails ai:context:full`, `CONTEXT_MODE=full`) wrote
|
|
1024
|
+
CLAUDE.md, AGENTS.md and copilot-instructions.md without them, so OpenCode
|
|
1025
|
+
and Codex, which load AGENTS.md alone, got neither, and `tool_mode` had no
|
|
1026
|
+
effect on those files. Each now ends with the compact files' tools guide,
|
|
1027
|
+
protocol included while `anti_hallucination_rules` is on; Copilot's gains
|
|
1028
|
+
the behavioral rules the other two carry. The dump itself says what the
|
|
1029
|
+
compact files already did: column defaults, indexes and foreign keys,
|
|
1030
|
+
each model's concerns, scopes, callbacks, enum values and constants, and
|
|
1031
|
+
the Stimulus controllers. It lists the app's routes as the count has
|
|
1032
|
+
them, each PATCH/PUT pair one entry, and counts the framework's instead of
|
|
1033
|
+
listing ~27 Active Storage, Action Mailbox and Turbo routes; a helper with
|
|
1034
|
+
no methods no longer ends in a bare colon, and the Overview and Puma
|
|
1035
|
+
headings appear only with something under them.
|
|
1036
|
+
- **A full-mode file names the command that regenerates it in full mode.**
|
|
1037
|
+
Its last line said to run `rails ai:context`, which writes compact files
|
|
1038
|
+
again unless `config.context_mode` is `:full`. It now says
|
|
1039
|
+
`CONTEXT_MODE=full rails ai:context`, which keeps the AI tool selection
|
|
1040
|
+
as `rails ai:context:full` does not; a standalone install, whose only
|
|
1041
|
+
way into full mode is `context_mode: full` in `.rails-ai-context.yml`,
|
|
1042
|
+
keeps `rails-ai-context context`. Copilot's full file shares the line.
|
|
1043
|
+
- **Every generated file states the route count the same way.** CLAUDE.md
|
|
1044
|
+
said "12 app routes across 3 routed controllers (39 total incl.
|
|
1045
|
+
framework)" while `.claude/rules/rails-context.md`,
|
|
1046
|
+
`.cursor/rules/rails-project.mdc` and
|
|
1047
|
+
`.github/instructions/rails-context.instructions.md` said "Routes: 39";
|
|
1048
|
+
an app with no routes of its own read "0 app routes" beside "Routes: 27".
|
|
1049
|
+
The rule files now print CLAUDE.md's line, from one place
|
|
1050
|
+
(`RouteCoverage.summary`), as `rails ai:inspect` does.
|
|
1051
|
+
- **The tool guide's examples name the app's own model, controller and
|
|
1052
|
+
files.** Every app's workflows said `rails_get_context(model:"Post")`,
|
|
1053
|
+
`app/models/post.rb`, `PostsController`, `publishable?` and
|
|
1054
|
+
`shared/status_badge`, right beside the rule never to name what a tool
|
|
1055
|
+
call has not verified, so an app of Authors and Books read them as its
|
|
1056
|
+
own. They now use the app's most connected model, its controller, a
|
|
1057
|
+
template and a partial it renders and a method its source defines, and
|
|
1058
|
+
an obvious placeholder (`YourModel`) where the app has none. The tools
|
|
1059
|
+
table's mailers row says `mailer:"X"` like the rest, where it said
|
|
1060
|
+
`UserMailer`.
|
|
1061
|
+
- **The tool guide names tools as `tools/list` does.** It sent the AI to
|
|
1062
|
+
`get_schema` and `get_model_details` and called `get_context` the power
|
|
1063
|
+
tool, none of which exists, and the `rails_diagnose` example put a
|
|
1064
|
+
backtick inside its code span, which cut it in two.
|
|
1065
|
+
- **The generated files count, list and document the tools the server
|
|
1066
|
+
serves.** `config.skip_tools` and `config.custom_tools` reached the server
|
|
1067
|
+
and the CLI but not the context files: with two tools skipped and one
|
|
1068
|
+
added, CLAUDE.md said 45, listed both skipped tools, and its tools table
|
|
1069
|
+
offered `rails 'ai:tool[query]'`, which answers "Unknown tool", while the
|
|
1070
|
+
custom tool went unmentioned. Every count, the name list and the tools
|
|
1071
|
+
table now read the server's own set, a custom tool getting a row with the
|
|
1072
|
+
first sentence of its description, and the installer's and `init`'s
|
|
1073
|
+
summaries count the same. No workflow step, rule, protocol line or
|
|
1074
|
+
pointer names a skipped tool either: it is left out, or said without the
|
|
1075
|
+
tool, so with `rails_get_context` skipped the model workflow starts from
|
|
1076
|
+
`rails_get_model_details`, and with `rails_validate` skipped no file asks
|
|
1077
|
+
the AI to validate with it.
|
|
1078
|
+
- **With `config.output_dir` set, the OpenCode per-directory files say why
|
|
1079
|
+
they were not written.** They are written only into a directory that
|
|
1080
|
+
exists, since OpenCode reads them beside the code, and the run reported
|
|
1081
|
+
`app/models not present` for an app that has one; it is the output
|
|
1082
|
+
directory that lacks it, and the reason now says `no app/models under
|
|
1083
|
+
output_dir`.
|
|
1084
|
+
- **The split rule files end with a newline.** The fourteen files under
|
|
1085
|
+
`.claude/rules/`, `.cursor/rules/`, `.github/instructions/` and
|
|
1086
|
+
`app/*/AGENTS.md` stopped at their last character, so an editor or an
|
|
1087
|
+
end-of-file fixer rewrote each one and git flagged it. Files written by
|
|
1088
|
+
an earlier version are rewritten once.
|
|
1089
|
+
- **A context file whose markers do not pair up is left as it is.** A lost
|
|
1090
|
+
`<!-- END rails-ai-context -->` had a fresh section prepended and the old
|
|
1091
|
+
one kept below it, so CLAUDE.md carried the gem's section twice from then
|
|
1092
|
+
on. With a marker missing, a second pair pasted in or END before BEGIN,
|
|
1093
|
+
nothing says where the gem's section ends, so a run now writes nothing to
|
|
1094
|
+
that file and names it `unpaired rails-ai-context marker, left as it is`.
|
|
1095
|
+
A regenerated section keeps a double backslash, where it was written as
|
|
1096
|
+
one.
|
|
1097
|
+
- **`rails_query` refuses a `DO $$ … $$` block as a statement it does not
|
|
1098
|
+
run**, not as "multiple statements": a semicolon inside a string or a
|
|
1099
|
+
block body is data now, and one between two statements is still caught.
|
|
1100
|
+
- **`rails_query` says when the row limit held rows back.** A capped result
|
|
1101
|
+
gave no sign the query had more; every format now ends with a note naming
|
|
1102
|
+
the limit, in CSV after a blank line so a parser still reads clean rows.
|
|
1103
|
+
- **`rails_query` bounds a MariaDB query with `max_statement_time`.**
|
|
1104
|
+
MariaDB ignores MySQL's `MAX_EXECUTION_TIME` hint, so `query_timeout` did
|
|
1105
|
+
nothing there and `SELECT SLEEP(8)` ran to the end, holding the
|
|
1106
|
+
connection. The statement is now wrapped in MariaDB's own `SET STATEMENT
|
|
1107
|
+
max_statement_time = <seconds> FOR …`, which leaves nothing on the pooled
|
|
1108
|
+
connection, and a cut query says "Query exceeded N second timeout".
|
|
1109
|
+
- **The selection record changes only what moved.** `init`, `rails
|
|
1110
|
+
ai:context` and the generator rewrote `.rails-ai-context.yml` through
|
|
1111
|
+
`to_yaml`, dropping every comment, and re-indented `config.ai_tools` in
|
|
1112
|
+
the initializer. They now edit `ai_tools`, `tool_mode` and
|
|
1113
|
+
`context_files` in place, keep comments, and write nothing when no value
|
|
1114
|
+
changed.
|
|
1115
|
+
- **The generated initializer passes a new Rails app's Rubocop**, and its
|
|
1116
|
+
comments say what the code does.
|
|
1117
|
+
- **The installer writes the context files in the mode it was just given.**
|
|
1118
|
+
A first install in CLI mode wrote files describing MCP tools via `rails
|
|
1119
|
+
ai:serve`.
|
|
1120
|
+
- **Install and context summaries name files from the app root, or from
|
|
1121
|
+
where the command was typed** (`init --app-path ../b` prints
|
|
1122
|
+
`../b/.mcp.json`), never as absolute paths. The `rm -f` line for the
|
|
1123
|
+
v5.0.0 leftover rule files does the same, so pasting it removes them.
|
|
1124
|
+
- **`init` asks every question before it boots the app, and writes nothing
|
|
1125
|
+
until the app's bundle is set up**; an MCP-only setup boots nothing. With
|
|
1126
|
+
an app that pins an older `json`, `init` printed four "already
|
|
1127
|
+
initialized constant" warnings; it prints none.
|
|
1128
|
+
- **`init` and `context` write Codex's environment as the binary was
|
|
1129
|
+
started**, not with the `GEM_HOME` and bundle `bin` directory the app's
|
|
1130
|
+
boot put into the process.
|
|
1131
|
+
- **Dropping an AI tool says what was kept and what was deleted** ("Kept
|
|
1132
|
+
AGENTS.md - Codex CLI uses it too", "Removed opencode.json
|
|
1133
|
+
(rails-ai-context was its only server)"), and a dot-directory the cleanup
|
|
1134
|
+
emptied is removed.
|
|
1135
|
+
- **Switching to CLI mode offers to take the gem out of the MCP configs**,
|
|
1136
|
+
and the CLI-mode summary no longer lists `.codex/config.toml`. The
|
|
1137
|
+
initializer is named whenever one of its lines moved, in `init` and on
|
|
1138
|
+
the first `rails ai:context`, which also records an MCP-only answer where
|
|
1139
|
+
it used to ask again on every run.
|
|
1140
|
+
- **Install summaries say only what applies**: re-runs show the current
|
|
1141
|
+
selection, only tools not yet picked are offered, there is no standalone
|
|
1142
|
+
pitch inside a bundle, the Codex gitignore hint appears only when it
|
|
1143
|
+
applies, an MCP-only install lists no `rails ai:context`, and
|
|
1144
|
+
`--defaults` no longer says "No tools selected". The setup answer is
|
|
1145
|
+
confirmed in the words the question offered it, and an unchanged file
|
|
1146
|
+
reads `(unchanged)` in every line.
|
|
1147
|
+
- **`rails ai:context:<tool>` writes that tool's MCP config in MCP mode**,
|
|
1148
|
+
and the binary's `context` keeps the MCP configs current as `rails
|
|
1149
|
+
ai:context` does: after `bundle remove rails-ai-context`, configs that
|
|
1150
|
+
started `bundle exec` are rewritten to the binary.
|
|
1151
|
+
- **The installer run at a mountable engine's root installs into that
|
|
1152
|
+
root.** It writes no initializer the host apps would run, no context
|
|
1153
|
+
files under `test/dummy`, and no hook calling a task the engine lacks,
|
|
1154
|
+
and it reports its MCP config once. The context files and the tools'
|
|
1155
|
+
hints there name `bundle exec rails-ai-context`, since `rails ai:serve`
|
|
1156
|
+
and `rails 'ai:tool[...]'` exist only in the dummy app. `doctor` reads
|
|
1157
|
+
that install, run as `bin/rails app:ai:doctor` or as
|
|
1158
|
+
`bundle exec rails-ai-context doctor` at the engine's root (which now
|
|
1159
|
+
checks the dummy app `bin/rails` boots instead of refusing), and its fixes
|
|
1160
|
+
name the commands that run there (a command of the dummy app's own, in
|
|
1161
|
+
`test/dummy`). It names every path from where it was
|
|
1162
|
+
typed: at the engine's root, the dummy app's files as `test/dummy/...` and
|
|
1163
|
+
the engine's own as `Gemfile.lock` and `app/views`; typed inside
|
|
1164
|
+
`test/dummy`, from `../..`.
|
|
1165
|
+
- **The standalone binary uses a `listen` installed beside it** for
|
|
1166
|
+
`watch` and for `serve`'s live reload, behind a `listen` the app's bundle
|
|
1167
|
+
locks, and `doctor` checks the same one. Where there is none, a
|
|
1168
|
+
standalone install is told `gem install listen`, and `serve` says each
|
|
1169
|
+
tool call still checks for changed files.
|
|
1170
|
+
- **Routes the app's own route files declare are app routes.**
|
|
1171
|
+
`devise_for` and the `rails/health` check were hidden as framework
|
|
1172
|
+
routes, so `rails_get_routes(controller: "devise/sessions")` answered "No
|
|
1173
|
+
routes"; a controller filter now finds its routes. The static tier draws
|
|
1174
|
+
no `new` or `edit` route for an API-only app, and the routes heading
|
|
1175
|
+
names the actions that skip a filter.
|
|
1176
|
+
- **Model details name the `devise` modules**, and Mongoid model details
|
|
1177
|
+
name the collection and the fields Mongoid adds.
|
|
1178
|
+
- **Next-step hints name only controllers the app has**, and under
|
|
1179
|
+
`tool_mode: :cli` they name the command, not an MCP call.
|
|
1180
|
+
- **N+1 risks follow a loaded collection into the loops and partials that
|
|
1181
|
+
walk it**, instead of matching an association name anywhere in any view,
|
|
1182
|
+
and each risk names its view.
|
|
1183
|
+
- **`rails_get_config` no longer calls a hand-written Authentication
|
|
1184
|
+
concern the Rails 8 generator.**
|
|
1185
|
+
- **The migration advisor warns about what the app's migrations already
|
|
1186
|
+
hold**: a pending migration that adds the column, a colliding migration
|
|
1187
|
+
name, `create_table` on an existing table, `null: false` with no default
|
|
1188
|
+
on a table with rows, and an unknown type ("Did you mean string?").
|
|
1189
|
+
Self-references use `to_table:`, and dropping a column lists the code
|
|
1190
|
+
that names it.
|
|
1191
|
+
- **`rails_get_schema` reads pending migrations from the connection**
|
|
1192
|
+
(an unmigrated database no longer reads "Pending migrations: none"),
|
|
1193
|
+
keeps each model on its own database's table, and marks encrypted, array
|
|
1194
|
+
and generated columns. A MySQL boolean default reads `true` or `false` on
|
|
1195
|
+
Rails 8.0 and 8.1, and SQLite row counts stop at a cap instead of
|
|
1196
|
+
reading every table whole.
|
|
1197
|
+
- **`rails_get_context` counts the instance variables a `before_action`
|
|
1198
|
+
sets.**
|
|
1199
|
+
- **`rails_validate level:"rails"` keeps its Brakeman findings when an app
|
|
1200
|
+
file does not parse**, and `DEBUG=1` no longer floods stderr from
|
|
1201
|
+
Brakeman, in validate and security_scan alike.
|
|
1202
|
+
- **`rails_search_code` shows the first match under a low
|
|
1203
|
+
`max_search_results`**; `definition` and `class` searches read a regex,
|
|
1204
|
+
and `definition` finds a name anywhere in a method name.
|
|
1205
|
+
- **`rails_analyze_feature`** lists Sidekiq workers, a service's own public
|
|
1206
|
+
methods and the files that call it, finds a model's test anywhere in the
|
|
1207
|
+
suite, and writes `after_commit (on: :create)`.
|
|
1208
|
+
- **`rails_get_test_info`'s RSpec template uses shoulda-matchers only when
|
|
1209
|
+
the app bundles it.**
|
|
1210
|
+
- **`rails_generate_test`** gives an existing test file the cases to add,
|
|
1211
|
+
gives a job a job test, signs a controller test in with the app's helper
|
|
1212
|
+
behind a login filter (or says it must), invents no params for a
|
|
1213
|
+
controller with no strong params, and writes a real system test for
|
|
1214
|
+
`type: "system"`.
|
|
1215
|
+
- **`rails_get_partial_interface` finds Turbo Stream, broadcast and
|
|
1216
|
+
jbuilder render sites** and jbuilder locals, lists a partial's render
|
|
1217
|
+
sites by the format each one looks up, and lists no helper as a local;
|
|
1218
|
+
`rails_get_view` lists those partials. A call names a format the way
|
|
1219
|
+
Rails reads one, as `formats:` among the options it renders with: a
|
|
1220
|
+
`format:` passed to the partial, or any key inside `locals:`, is a local,
|
|
1221
|
+
and so is every key of `render "shared/date", date: d, format: :short`.
|
|
1222
|
+
- **Render maps and frame sources keep their closing brackets.**
|
|
1223
|
+
- **`rails_get_turbo_map` wires a stream by the records and names it is
|
|
1224
|
+
built from**, says "can't tell" for what it cannot resolve, and says when
|
|
1225
|
+
a stream filter matches nothing.
|
|
1226
|
+
- **`rails_get_stimulus`'s markup** wires actions to the events the app's
|
|
1227
|
+
views use, leaves out change callbacks and the controller's own helpers,
|
|
1228
|
+
and adds class and outlet attributes.
|
|
1229
|
+
- **`rails_get_controllers`** says when an action needs a controller and
|
|
1230
|
+
which controller a bare name resolved to, answers for
|
|
1231
|
+
ApplicationController, counts template formats, and lists the implicit
|
|
1232
|
+
`format.turbo_stream` render.
|
|
1233
|
+
- **A 100 KB argument is answered at once and echoed shortened**, where
|
|
1234
|
+
`rails_analyze_feature` took 14 seconds and echoed it whole. v5.32.2
|
|
1235
|
+
repeated such an argument whole in 62 answers across 29 tools, among them
|
|
1236
|
+
the not-found and invalid-value answers of `rails_search_docs`,
|
|
1237
|
+
`rails_get_component_catalog`, `rails_get_concern`,
|
|
1238
|
+
`rails_review_changes`, `rails_generate_test`, `rails_get_controllers` and
|
|
1239
|
+
`rails_get_context`; each now shows its first 80 characters and its
|
|
1240
|
+
length. `rails_migration_advisor`, which wrote the argument into the
|
|
1241
|
+
migration twice over, refuses a name longer than 256 characters and
|
|
1242
|
+
options longer than 1,024. A `rails_review_changes` ref too long to hand
|
|
1243
|
+
git at all (128 KB on Linux, some 32,000 characters on Windows) is an
|
|
1244
|
+
unknown ref, where the answer was the error starting git raised.
|
|
1245
|
+
- **`rails_onboard` counts every database the app declares**, naming the
|
|
1246
|
+
framework's own databases in one line; it and
|
|
1247
|
+
`rails_get_frontend_stack` count every broadcast, and onboard includes
|
|
1248
|
+
STI payment subclasses.
|
|
1249
|
+
- **`rails_get_conventions`' create skeleton checks permission the app's
|
|
1250
|
+
way** and keeps controller namespaces.
|
|
1251
|
+
- **`rails_get_edit_context`'s not-found answer no longer offers the same
|
|
1252
|
+
path back.**
|
|
1253
|
+
- **The documented engine mount loads where the gem is not.** README,
|
|
1254
|
+
GUIDE and SETUP showed a mount that raised `NameError` in production
|
|
1255
|
+
with the gem in the development group, and after the gem was removed.
|
|
1256
|
+
They now show `mount RailsAiContext::Engine, at: "/mcp" if
|
|
1257
|
+
defined?(RailsAiContext::Engine)`.
|
|
1258
|
+
- **`serve --transport http` stops on SIGTERM or Ctrl-C every time and
|
|
1259
|
+
exits 0.** About one stop in five hung once a client had connected:
|
|
1260
|
+
Puma's graceful stop ran inside the signal handler and deadlocked. A
|
|
1261
|
+
request still running is cut off after 3 seconds, and `bin/rails
|
|
1262
|
+
ai:serve_http` no longer prints `bin/rails aborted! SignalException`.
|
|
1263
|
+
- **`rails server` stops with an MCP client connected to the mounted
|
|
1264
|
+
engine.** The engine answers the server-push GET with 405 instead of
|
|
1265
|
+
holding a thread per client.
|
|
1266
|
+
- **Each HTTP client keeps its own session record**, and a
|
|
1267
|
+
`rails_session_context` reset clears the caller's alone. The record holds
|
|
1268
|
+
the call the client made, not the tools a composite tool called inside
|
|
1269
|
+
it, and `mark` refuses an empty mark or one naming no tool.
|
|
1270
|
+
- **The HTTP error frame is JSON even when the app's `to_json` raises**
|
|
1271
|
+
(Rails 7.0-8.0 with json 3).
|
|
1272
|
+
- **`auto_mount` answers while a migration is pending**; the mounted
|
|
1273
|
+
engine, inside routing, still does not, as the docs say.
|
|
1274
|
+
- **The `http_bind` warning and docs say what a non-loopback bind
|
|
1275
|
+
serves.**
|
|
1276
|
+
- **`rails_read_logs` works on logs with non-ASCII lines.** On Ruby before
|
|
1277
|
+
3.4 the redaction patterns raised `RegexpError` on such a line, breaking
|
|
1278
|
+
read_logs and `rails_diagnose`'s log section. `search:` now reaches back
|
|
1279
|
+
through the last 4 MB of a log and says how many lines it searched, where
|
|
1280
|
+
it searched only the last `lines` lines.
|
|
1281
|
+
- **The view resource labels a template by the language of its source**,
|
|
1282
|
+
where every view read was `text/html`.
|
|
1283
|
+
- **A resource read for a name the app does not have fails with JSON-RPC
|
|
1284
|
+
-32602**, naming the URI and the names that exist, where it came back as
|
|
1285
|
+
a successful read holding an `error` key.
|
|
1286
|
+
- **A commented MCP config reads the same under json 3.** json 3 refuses
|
|
1287
|
+
the comments json 2 passed over, so with it - what a new Rails 8.1 app
|
|
1288
|
+
resolves - a `.vscode/mcp.json` holding a comment was reported on every
|
|
1289
|
+
run as a file that does not parse, and doctor flagged it, even when its
|
|
1290
|
+
entries were current. Configs are now read with comments allowed on every
|
|
1291
|
+
json, so a current one is left alone as unchanged, and one that needs
|
|
1292
|
+
new entries is still left as it is, since writing it back would drop the
|
|
1293
|
+
comments.
|
|
1294
|
+
|
|
8
1295
|
## [5.32.2] - 2026-10-07
|
|
9
1296
|
|
|
10
1297
|
### Security
|