rails-ai-context 5.27.0 → 5.28.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 +189 -0
- data/docs/ARCHITECTURE.md +2 -2
- data/exe/rails-ai-context +13 -4
- data/lib/rails_ai_context/action_filters.rb +1 -2
- data/lib/rails_ai_context/boot_manager.rb +23 -2
- data/lib/rails_ai_context/cli/entry_boot.rb +6 -0
- data/lib/rails_ai_context/cli/tool_runner.rb +10 -22
- data/lib/rails_ai_context/concern_macros.rb +1 -2
- data/lib/rails_ai_context/concern_paths.rb +6 -1
- data/lib/rails_ai_context/detail_level.rb +7 -4
- data/lib/rails_ai_context/doctor.rb +1 -2
- data/lib/rails_ai_context/fingerprinter.rb +1 -2
- data/lib/rails_ai_context/hydrators/controller_hydrator.rb +2 -4
- data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +1 -1
- data/lib/rails_ai_context/hydrators/view_hydrator.rb +1 -2
- data/lib/rails_ai_context/install/cleanup.rb +23 -7
- data/lib/rails_ai_context/install/program.rb +11 -11
- data/lib/rails_ai_context/install_mode.rb +1 -2
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -2
- data/lib/rails_ai_context/introspectors/action_resolver.rb +45 -13
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +2 -4
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +4 -8
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +8 -16
- data/lib/rails_ai_context/introspectors/api_introspector.rb +7 -15
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +2 -8
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +11 -22
- data/lib/rails_ai_context/introspectors/autoload_introspector.rb +6 -12
- data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -15
- data/lib/rails_ai_context/introspectors/config_introspector.rb +9 -64
- data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +5 -10
- data/lib/rails_ai_context/introspectors/controller_filters.rb +1 -2
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +23 -114
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +6 -50
- data/lib/rails_ai_context/introspectors/credentials_introspector.rb +3 -6
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +3 -6
- data/lib/rails_ai_context/introspectors/declared_constant.rb +2 -4
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +35 -56
- data/lib/rails_ai_context/introspectors/eager_load.rb +1 -2
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +2 -4
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +3 -6
- data/lib/rails_ai_context/introspectors/env_introspector.rb +2 -4
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +12 -31
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +2 -4
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +48 -48
- data/lib/rails_ai_context/introspectors/initializer_introspector.rb +4 -8
- data/lib/rails_ai_context/introspectors/job_introspector.rb +66 -27
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +3 -8
- data/lib/rails_ai_context/introspectors/listeners/class_definition_listener.rb +2 -2
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +3 -6
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +2 -4
- data/lib/rails_ai_context/introspectors/migration_replay.rb +21 -36
- data/lib/rails_ai_context/introspectors/model_introspector.rb +2 -15
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +5 -10
- data/lib/rails_ai_context/introspectors/observability_introspector.rb +6 -12
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +5 -30
- data/lib/rails_ai_context/introspectors/route_introspector.rb +11 -21
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +6 -12
- data/lib/rails_ai_context/introspectors/schema_reader.rb +1 -2
- data/lib/rails_ai_context/introspectors/security_introspector.rb +9 -18
- data/lib/rails_ai_context/introspectors/source_introspector.rb +1 -2
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +6 -51
- data/lib/rails_ai_context/introspectors/table_name.rb +1 -2
- data/lib/rails_ai_context/introspectors/test_introspector.rb +20 -46
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +19 -51
- data/lib/rails_ai_context/introspectors/view_introspector.rb +5 -10
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +3 -3
- data/lib/rails_ai_context/legacy_cleanup.rb +3 -1
- data/lib/rails_ai_context/mcp_config_generator.rb +42 -67
- data/lib/rails_ai_context/migration_status.rb +1 -2
- data/lib/rails_ai_context/package_json.rb +98 -0
- data/lib/rails_ai_context/payload.rb +45 -0
- data/lib/rails_ai_context/portable_path.rb +1 -2
- data/lib/rails_ai_context/redaction.rb +26 -10
- data/lib/rails_ai_context/schema_adapter.rb +2 -2
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +6 -34
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +1 -37
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -40
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +41 -0
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +7 -1
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +5 -10
- data/lib/rails_ai_context/tools/analyze_feature.rb +38 -72
- data/lib/rails_ai_context/tools/base_tool.rb +32 -35
- data/lib/rails_ai_context/tools/diagnose.rb +2 -4
- data/lib/rails_ai_context/tools/generate_test.rb +42 -48
- data/lib/rails_ai_context/tools/get_api.rb +1 -6
- data/lib/rails_ai_context/tools/get_callbacks.rb +1 -5
- data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -5
- data/lib/rails_ai_context/tools/get_concern.rb +43 -57
- data/lib/rails_ai_context/tools/get_context.rb +2 -1
- data/lib/rails_ai_context/tools/get_controllers.rb +24 -34
- data/lib/rails_ai_context/tools/get_conventions.rb +25 -29
- data/lib/rails_ai_context/tools/get_env.rb +39 -93
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -5
- data/lib/rails_ai_context/tools/get_helper_methods.rb +58 -51
- data/lib/rails_ai_context/tools/get_job_pattern.rb +4 -15
- data/lib/rails_ai_context/tools/get_model_details.rb +8 -24
- data/lib/rails_ai_context/tools/get_partial_interface.rb +9 -20
- data/lib/rails_ai_context/tools/get_routes.rb +1 -5
- data/lib/rails_ai_context/tools/get_schema.rb +3 -9
- data/lib/rails_ai_context/tools/get_service_pattern.rb +64 -42
- data/lib/rails_ai_context/tools/get_stimulus.rb +10 -13
- data/lib/rails_ai_context/tools/get_test_info.rb +3 -9
- data/lib/rails_ai_context/tools/get_turbo_map.rb +4 -11
- data/lib/rails_ai_context/tools/get_view.rb +3 -9
- data/lib/rails_ai_context/tools/migration_advisor.rb +2 -4
- data/lib/rails_ai_context/tools/onboard.rb +1 -223
- data/lib/rails_ai_context/tools/performance_check.rb +28 -35
- data/lib/rails_ai_context/tools/runtime_info.rb +4 -11
- data/lib/rails_ai_context/tools/search_code.rb +5 -16
- data/lib/rails_ai_context/tools/search_docs.rb +1 -2
- data/lib/rails_ai_context/tools/section_fetch.rb +1 -2
- data/lib/rails_ai_context/tools/security_scan.rb +1 -5
- data/lib/rails_ai_context/tools/session_context.rb +1 -2
- data/lib/rails_ai_context/tools/validate.rb +1 -2
- data/lib/rails_ai_context/tools/validate_semantics.rb +10 -16
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +5 -34
- data/lib/rails_ai_context.rb +15 -0
- data/server.json +1 -1
- metadata +2 -2
- data/lib/rails_ai_context/serializers/section_guard.rb +0 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78848e75315236681b6b31bba337665e75a143d862d2d1ff828f48f50b1756f5
|
|
4
|
+
data.tar.gz: 8af5164b8ee4ea9900d82450d2a74905dcf1b831c02d7ba95c5c869274c18e9e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6dbfa116e75a4675314e6c4a06ada6c2c7d7b8c7f0d9fb5ca0abe9945add65f1729b0101200debf5c25ea2190e66b57f41761b4307d8414d77ccfc62dc292e9d
|
|
7
|
+
data.tar.gz: 86984bc5566480da28d88b7ef475138bb85c9ba459fcd87e58f8a4afd03239a365ee30d032412701291ec0bd60f7f0e0cccfa882fee0150e6baed74c966a33b1
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,195 @@ 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.28.0] - 2026-09-22
|
|
9
|
+
|
|
10
|
+
Thirty-six changes an architecture survey of v5.27.0 asked for, nine of them
|
|
11
|
+
defects where a user got a wrong answer, plus what three review rounds found in
|
|
12
|
+
the fixes themselves.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Services, helpers, jobs and concerns are found wherever the app keeps
|
|
17
|
+
them.** The four tools globbed `app/<kind>` off the app root, so a packwerk
|
|
18
|
+
app whose services live under `packs/billing/app/services` was told it had no
|
|
19
|
+
services directory and may not use the pattern. Directory discovery reads
|
|
20
|
+
`PathResolver.dirs_for`, which already resolved packs, engines and
|
|
21
|
+
`extra_app_paths` for the payload side. `ConcernPaths.resolve` and
|
|
22
|
+
`ActiveSupportIntrospector` read the same resolver, so a concern's listing,
|
|
23
|
+
its lookup and its "Used By" line now agree on one app. Counts move on any
|
|
24
|
+
app with packs or engines. Each walk names the directory a file came from, so
|
|
25
|
+
two files of the same basename under different roots no longer collide, and a
|
|
26
|
+
tool that finds nothing names all three directory patterns it searched
|
|
27
|
+
instead of the conventional one alone.
|
|
28
|
+
- **`RailsAiContext.debug_fail`.** About 258 rescue bodies in `lib/`
|
|
29
|
+
hand-rolled the same three lines: rescue the error, write a `DEBUG`-gated
|
|
30
|
+
warning to stderr, return a fallback. They call one method now, with every
|
|
31
|
+
message string and every fallback value unchanged. `exe/rails-ai-context`
|
|
32
|
+
shims it beside `log_warn`, because the standalone install path loads seven
|
|
33
|
+
files by hand and a converted rescue inside `install_mode.rb` would otherwise
|
|
34
|
+
raise `NoMethodError` after the removed-tool cleanup had already deleted
|
|
35
|
+
files.
|
|
36
|
+
- **`PackageJson`.** GemLock's twin for the node side: `package.json` parsed
|
|
37
|
+
once per root, `dependencies` merged with `devDependencies`, capped by
|
|
38
|
+
`configuration.max_file_size` rather than by a constant no caller can reach,
|
|
39
|
+
and empty for anything it cannot read. A scope named after the package
|
|
40
|
+
counts, so an app reaching tailwind through `@tailwindcss/vite` keeps its
|
|
41
|
+
answer.
|
|
42
|
+
|
|
43
|
+
### Removed
|
|
44
|
+
|
|
45
|
+
- **`config.job_processor`, gone from `.ai-context.json` and from
|
|
46
|
+
`get_config`.** `ConfigIntrospector` parsed `config/sidekiq.yml` a second
|
|
47
|
+
time to publish it, and nothing read it: no renderer, no spec, no template.
|
|
48
|
+
The two readers disagreed, so a `:queues:` block written with weights,
|
|
49
|
+
`- [critical, 2]`, lost `critical` on the config side and kept it on the jobs
|
|
50
|
+
side, and one sidekiq.yml shipped two queue lists in one payload. The same
|
|
51
|
+
concurrency and queues stay under `jobs.sidekiq_config`, which
|
|
52
|
+
`get_job_pattern` already consumes, and `config.queue_adapter` still names
|
|
53
|
+
the adapter. Both sections sit in the standard and full presets, so no preset
|
|
54
|
+
loses the fact.
|
|
55
|
+
- **`action_bindings` and `outlet_controllers`, gone from every Stimulus
|
|
56
|
+
controller entry.** Filling `action_bindings` read every ERB, HAML and Slim
|
|
57
|
+
file under `app/views` and `app/components` on every generation, and
|
|
58
|
+
`outlet_controllers` restated the outlets list one line above it. Nothing in
|
|
59
|
+
`lib/` read either key: `get_stimulus` renders outlets, and the compact
|
|
60
|
+
serializer touches the section through `controllers` and `total_controllers`.
|
|
61
|
+
Both keys leave the artifact, and one full pass over the view and component
|
|
62
|
+
trees leaves every generation.
|
|
63
|
+
- **`Serializers::SectionGuard`, `DetailLevel::SCHEMA_ENUM` and
|
|
64
|
+
`Install::Program.select_tool_mode`.** SectionGuard was 15 lines forwarding
|
|
65
|
+
to `Tools::SectionFetch.usable?`; both callers sat in `SchemaAdapter`, which
|
|
66
|
+
is not a serializer, and `RouteCoverage` already called SectionFetch
|
|
67
|
+
directly. SCHEMA_ENUM went with its last reference when
|
|
68
|
+
`DetailLevel.schema(description)` replaced the same four-line input-schema
|
|
69
|
+
hash in 21 tools; every published schema is byte-identical, and
|
|
70
|
+
`rails_onboard` keeps its own literal because its levels are quick, standard
|
|
71
|
+
and full. `select_tool_mode` was a shim for `select_setup(surface).tool_mode`
|
|
72
|
+
with no caller left in `lib/`, and it cannot express the MCP-config-only
|
|
73
|
+
mode. `ModelIntrospector#sti_bases` and
|
|
74
|
+
`ControllerIntrospector#extract_permit_details` go with them; only their own
|
|
75
|
+
recursion and their own specs were calling them.
|
|
76
|
+
|
|
77
|
+
### Changed
|
|
78
|
+
|
|
79
|
+
- **`.github/instructions/rails-context.instructions.md` prints every gem
|
|
80
|
+
category.** `.first(6)` on the grouped Hash returned the first six pairs, so
|
|
81
|
+
the seventh category onward was dropped with nothing saying so, while
|
|
82
|
+
`.cursor/rules/rails-project.mdc` rendered the same grouping uncapped. One
|
|
83
|
+
payload, two gem lists.
|
|
84
|
+
- **`.cursor/rules/rails-project.mdc` bolds `Global before_actions:`** like the
|
|
85
|
+
Claude and Copilot files. The three serializers each hand-wrote the same
|
|
86
|
+
ten-step overview and had drifted; `StackOverviewHelper#overview_lines`
|
|
87
|
+
states the body once and each renderer keeps only its frontmatter, title and
|
|
88
|
+
MCP hint.
|
|
89
|
+
- **Puma settings are read inside blocks.** The hand-rolled recursion stopped
|
|
90
|
+
at the top level, so a `workers` line guarded by an environment conditional,
|
|
91
|
+
which is what the generated `puma.rb` ships, was invisible and the answer
|
|
92
|
+
read as no workers configured. `MethodCallListener` sees a receiverless call
|
|
93
|
+
at any depth. The cost of the widening is that nesting no longer hides a
|
|
94
|
+
setting, so a name set twice reports the last one wherever it sits. Also
|
|
95
|
+
`threads ENV.fetch("RAILS_MIN_THREADS") { 5 }, ENV.fetch("RAILS_MAX_THREADS")
|
|
96
|
+
{ 5 }` reports 5 and 5 where it reported nothing, and the digit match is
|
|
97
|
+
anchored away from word characters, so `ENV.fetch("PORT_2", 3000)` no longer
|
|
98
|
+
reads as 2.
|
|
99
|
+
- **`get_conventions`' frontend stack string reads parsed dependencies.**
|
|
100
|
+
`detect_frontend_stack` matched bare substrings against the raw file, with no
|
|
101
|
+
quotes at all, so `vite-plugin-ruby` on its own reported Vite. The markers
|
|
102
|
+
carry alternates where an exact name would have lost an answer the substring
|
|
103
|
+
caught: svelte or `@sveltejs/kit`, `@hotwired/turbo` or
|
|
104
|
+
`@hotwired/turbo-rails`.
|
|
105
|
+
- **`api_introspector`'s codegen list reads parsed dependencies.** It searched
|
|
106
|
+
the file for the quoted tool name, so `openapi-typescript`, `orval` or
|
|
107
|
+
`@graphql-codegen/cli` named anywhere in `package.json`, an `overrides` block
|
|
108
|
+
included, counted as a client generator the app runs.
|
|
109
|
+
- **`rails_generate_test` resolves a controller by the rule
|
|
110
|
+
`rails_get_controllers` uses.** It camelized the string and looked the result
|
|
111
|
+
up, so with `Admin::GiftCardsController` as the only gift-card controller,
|
|
112
|
+
`rails_get_controllers(controller: "gift_cards")` resolved and
|
|
113
|
+
`rails_generate_test(controller: "gift_cards")` answered "not found".
|
|
114
|
+
`Payload.find_controller` answers for both, with the camelize kept as the
|
|
115
|
+
fallback for a payload carrying no controllers at all.
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
Nine defects the survey confirmed against v5.27.0, and the defects three review
|
|
120
|
+
rounds found in the work that fixed them.
|
|
121
|
+
|
|
122
|
+
- **`onboard(detail: "quick")` no longer prints a guessed domain noun.** Eleven
|
|
123
|
+
ordered regexes read model, job and service names and stated the result as
|
|
124
|
+
plain fact with no confidence marker, so one model named `Message` made an
|
|
125
|
+
app "a messaging app". Quick mode now states what it measured: versions,
|
|
126
|
+
table, model and job counts, the frontend and the test framework.
|
|
127
|
+
- **`api_namespaces` invented a namespace and missed one.** The two tiers
|
|
128
|
+
derived the list their own way. The booted regex kept a trailing slash on
|
|
129
|
+
`/api/users`, matched unanchored so `namespace :admin { namespace :api }`
|
|
130
|
+
reported `/api/v1` for an app that serves `/admin/api/v1`, and dropped a
|
|
131
|
+
route sitting at exactly `/api`. The static spelling got the segment boundary
|
|
132
|
+
wrong, so `/apidocs` reported `/api`. Both tiers read one anchored form.
|
|
133
|
+
- **Turbo drive settings were double-counted for layouts.** `app/views/layouts`
|
|
134
|
+
was walked again after `app/views/**/*` had already reached it. The permanent
|
|
135
|
+
element pass threw its duplicate away through `uniq`; the drive settings did
|
|
136
|
+
not, so one attribute in a layout and one in a view printed as 3.
|
|
137
|
+
- **The asset pipeline reported the wrong bundler and CSS framework.** Three
|
|
138
|
+
readers asked whether a package was present by searching the raw file for its
|
|
139
|
+
quoted name, so an app pinning CVE fixes in an `overrides` block was told
|
|
140
|
+
esbuild is its bundler and postcss its CSS framework, while the one reader
|
|
141
|
+
that parsed the file called the same app vite. Both answers landed in one
|
|
142
|
+
payload.
|
|
143
|
+
- **`analyze_feature`'s Jobs, Mailers and Channels sections came from a glob.**
|
|
144
|
+
Each job's queue was read off its own `queue_as`, so a job inheriting the
|
|
145
|
+
queue from `ApplicationJob` was reported on a queue it does not use. The
|
|
146
|
+
three sections read the payload now, which carries what the booted tier
|
|
147
|
+
resolved, mailer actions and channel stream methods included. A section whose
|
|
148
|
+
payload entry is not there, because the `:jobs` introspector is off, is left
|
|
149
|
+
out rather than rendered from a directory walk.
|
|
150
|
+
- **`performance_check` attributed queries to methods that are not actions.** A
|
|
151
|
+
`def` inside a nested class became a call site, and a one-line
|
|
152
|
+
`private def set_post` leaked its body into the action above it.
|
|
153
|
+
`ActionResolver` answers what an action is here, as it does everywhere else.
|
|
154
|
+
A second collision went with it: each body was found by searching the whole
|
|
155
|
+
file for the first `def <name>`, so a nested class defining the same name
|
|
156
|
+
earlier handed back its body and the real action was never scanned. The body
|
|
157
|
+
is cut from the lines the owner-filtered walk recorded, and `get_controllers`
|
|
158
|
+
and `get_context` share the seam. An empty owner filter falls back to the
|
|
159
|
+
line scan rather than answering nothing, and `def self.index` no longer
|
|
160
|
+
stands in for `def index`.
|
|
161
|
+
- **A model whose only rule is `validate :method` printed its bullets under the
|
|
162
|
+
wrong heading.** `## Validations` was emitted only when the reflected list
|
|
163
|
+
had entries, and the custom bullets come from a second list that is disjoint
|
|
164
|
+
from it on both tiers, so they landed under Associations.
|
|
165
|
+
- **Two ERB readers missed `<%== ... %>` and read a commented ivar as used.**
|
|
166
|
+
`check_instance_variable_usage` and `extract_local_variable_references` each
|
|
167
|
+
carried their own copy of the tag regex, byte for byte the same and neither
|
|
168
|
+
matching `ErbSource::TAG`. Both read `<%[=\-]?`, one optional character, so
|
|
169
|
+
`<%== title %>` parsed as a body of "= title" and the local never reached a
|
|
170
|
+
partial's expected locals. The comment skip was a second answer too:
|
|
171
|
+
`get_partial_interface` re-implemented it and `validate_semantics` never had
|
|
172
|
+
one, so `<%# @ghost %>` was reported as an ivar used in the view and not set
|
|
173
|
+
in the controller. Both readers ask `ErbSource` now.
|
|
174
|
+
- **`rails_get_controllers` refused a name the VFS resource accepts.** VFS
|
|
175
|
+
resolved five ways, the tool two, so `controller: "gift_cards"` failed
|
|
176
|
+
against `Admin::GiftCardsController` while the resource answered off the same
|
|
177
|
+
payload. Both read `Payload.find_controller`, so a route key, an unambiguous
|
|
178
|
+
basename and the singularize and classify spellings now resolve in the tool
|
|
179
|
+
too.
|
|
180
|
+
- **Two files behind one name no longer hide each other.** Once services,
|
|
181
|
+
helpers and concerns were read from packs and engines, a name could have more
|
|
182
|
+
than one file behind it. The ambiguous-service list printed
|
|
183
|
+
`app/services/<path>` for files that live under a pack, so two candidates
|
|
184
|
+
printed as two identical lines naming a file that exists in neither, and the
|
|
185
|
+
suggestion that followed handed back the name it had just refused. Paths
|
|
186
|
+
print from the app root, and a narrowing suggestion is offered only when some
|
|
187
|
+
candidate's relative path is unique. `rails_get_helper_methods` rendered the
|
|
188
|
+
first match as the whole module and listed the others under "Also defined
|
|
189
|
+
in" even when they declare different modules, so a file declaring
|
|
190
|
+
`Reports::DashboardHelper` was named under an `Admin::DashboardHelper`
|
|
191
|
+
heading; matches are split by the module each file declares.
|
|
192
|
+
`rails_get_concern` broke on the first directory that resolved, so an app
|
|
193
|
+
with both `app/models/concerns/trackable.rb` and
|
|
194
|
+
`app/controllers/concerns/trackable.rb` saw only the controller one. Every
|
|
195
|
+
"available" list is deduplicated where it is built.
|
|
196
|
+
|
|
8
197
|
## [5.27.0] - 2026-09-22
|
|
9
198
|
|
|
10
199
|
### Added
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -110,11 +110,12 @@ flowchart LR
|
|
|
110
110
|
|
|
111
111
|
The `Introspector` orchestrator runs configured introspectors and merges results.
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
Four modules answer questions every introspector used to answer for itself:
|
|
114
114
|
|
|
115
115
|
- **SourceScan** - one walk over a kind of app source, across every directory `PathResolver` resolves: `paths` stats, `each` reads, `classes` names by the declared constant
|
|
116
116
|
- **EagerLoad** - loads a directory's constants for a booted-tier walk, one file at a time, so an unloadable file costs only itself
|
|
117
117
|
- **GemLock** - which gems the app resolved and at what version, read once per lockfile and matched by exact name across GEM, GIT and PATH
|
|
118
|
+
- **PackageJson** - which npm packages the app depends on, read once per `package.json`: present means named in `dependencies` or `devDependencies`, so an `overrides` pin is not a dependency, and `@tailwindcss/vite` counts as tailwindcss
|
|
118
119
|
|
|
119
120
|
Two more answer a question a tool asks:
|
|
120
121
|
|
|
@@ -198,7 +199,6 @@ Result: controller and view tools automatically include relevant schema informat
|
|
|
198
199
|
| `ToolGuideHelper` | MCP/CLI tool reference sections |
|
|
199
200
|
| `TestCommandDetection` | Test framework detection |
|
|
200
201
|
| `SectionFacts` | The facts every surface states about an app - auth, assets, associations, the filter chain, an unread entry's row, the static-tier notice - each rendered in one place |
|
|
201
|
-
| `SectionGuard` | Whether a section resolved, so a refused one is not rendered |
|
|
202
202
|
| `SectionMarkerWriter` | Writes a managed section into a file the user also owns |
|
|
203
203
|
| `ContextModeDispatch` | Picks full or compact rendering for a run |
|
|
204
204
|
|
data/exe/rails-ai-context
CHANGED
|
@@ -592,11 +592,20 @@ class RailsAiContextCLI < Thor
|
|
|
592
592
|
install/program mcp_config_generator legacy_cleanup
|
|
593
593
|
].each { |file| require_relative "../lib/rails_ai_context/#{file}" }
|
|
594
594
|
|
|
595
|
-
|
|
595
|
+
# Both are defined in the entry file, which is the thing that must not
|
|
596
|
+
# load yet. With no Rails in the process log_warn only writes to stderr.
|
|
597
|
+
unless ::RailsAiContext.respond_to?(:log_warn)
|
|
598
|
+
::RailsAiContext.define_singleton_method(:log_warn) { |message| $stderr.puts(message) }
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
return if ::RailsAiContext.respond_to?(:debug_fail)
|
|
596
602
|
|
|
597
|
-
#
|
|
598
|
-
#
|
|
599
|
-
::RailsAiContext.define_singleton_method(:
|
|
603
|
+
# Every rescue in the files above routes through this, so without it a
|
|
604
|
+
# rescue that fires raises NoMethodError over the error it was catching.
|
|
605
|
+
::RailsAiContext.define_singleton_method(:debug_fail) do |error, fallback = nil, label:|
|
|
606
|
+
$stderr.puts("[rails-ai-context] #{label} failed: #{error.message}") if ENV["DEBUG"]
|
|
607
|
+
fallback
|
|
608
|
+
end
|
|
600
609
|
end
|
|
601
610
|
end
|
|
602
611
|
|
|
@@ -345,8 +345,7 @@ module RailsAiContext
|
|
|
345
345
|
end
|
|
346
346
|
end.uniq { |skip| skip[:name] }
|
|
347
347
|
rescue => e
|
|
348
|
-
|
|
349
|
-
[]
|
|
348
|
+
RailsAiContext.debug_fail(e, [], label: "ActionFilters skip_source_records")
|
|
350
349
|
end
|
|
351
350
|
|
|
352
351
|
# ApplicationController is deliberately not in the listing: it would sit in
|
|
@@ -39,11 +39,32 @@ module RailsAiContext
|
|
|
39
39
|
status == :booted
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
#
|
|
42
|
+
# Bundler::GemRequireError names the gem it failed to require and
|
|
43
|
+
# nothing about why; the incompatibility that actually raised is in
|
|
44
|
+
# `cause`. Walk to the deepest one, capped so a cycle cannot hang.
|
|
45
|
+
def root_cause
|
|
46
|
+
cause = error&.cause
|
|
47
|
+
10.times do
|
|
48
|
+
break unless cause&.cause
|
|
49
|
+
|
|
50
|
+
cause = cause.cause
|
|
51
|
+
end
|
|
52
|
+
cause
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# One-line summary safe to relay to an AI client or a terminal. It
|
|
56
|
+
# carries the cause too: the footer this feeds is the only place most
|
|
57
|
+
# callers ever see, and a gem name with no reason is not actionable.
|
|
43
58
|
def failure_summary
|
|
44
59
|
return nil if booted?
|
|
45
60
|
|
|
46
|
-
|
|
61
|
+
summary = one_line(error)
|
|
62
|
+
cause = root_cause
|
|
63
|
+
cause ? "#{summary} (cause: #{one_line(cause)})" : summary
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def one_line(e)
|
|
67
|
+
"#{e.class}: #{e.message.to_s.lines.first&.strip}"
|
|
47
68
|
end
|
|
48
69
|
|
|
49
70
|
# An unguarded `RailsAiContext.configure` in config/initializers has
|
|
@@ -129,6 +129,12 @@ module RailsAiContext
|
|
|
129
129
|
messages << " #{result.failure_summary}"
|
|
130
130
|
if ENV["DEBUG"]
|
|
131
131
|
Array(result.error.backtrace).first(15).each { |line| messages << " #{line}" }
|
|
132
|
+
# The wrapper's frames stop at the require; the frames that name the
|
|
133
|
+
# incompatible call are the cause's.
|
|
134
|
+
if (cause = result.root_cause)
|
|
135
|
+
messages << " Raised by:"
|
|
136
|
+
Array(cause.backtrace).first(15).each { |line| messages << " #{line}" }
|
|
137
|
+
end
|
|
132
138
|
else
|
|
133
139
|
messages << " Run with DEBUG=1 for the full backtrace."
|
|
134
140
|
end
|
|
@@ -218,9 +218,9 @@ module RailsAiContext
|
|
|
218
218
|
# `--files=a.rb b.rb` is the same call as `--files a.rb b.rb`;
|
|
219
219
|
# honouring only one of the two spellings docs/CLI.md teaches
|
|
220
220
|
# left the other still dropping every file after the first.
|
|
221
|
-
trailing =
|
|
221
|
+
trailing, consumed = array_values(args, i + 1)
|
|
222
222
|
result[key] = Array(coerce_value(value, prop, key)) + trailing
|
|
223
|
-
i += 1 +
|
|
223
|
+
i += 1 + consumed
|
|
224
224
|
next
|
|
225
225
|
end
|
|
226
226
|
result[key] = coerce_value(value, prop, key)
|
|
@@ -244,21 +244,12 @@ module RailsAiContext
|
|
|
244
244
|
next
|
|
245
245
|
end
|
|
246
246
|
|
|
247
|
-
if prop[:type] == "array"
|
|
248
|
-
values = collect_array_values(args, i + 1)
|
|
249
|
-
unless values.empty?
|
|
250
|
-
result[key] = values
|
|
251
|
-
i += 1 + values_consumed(args, i + 1)
|
|
252
|
-
next
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
|
|
256
247
|
# A bare `--files` with nothing after it is a mistake, not a
|
|
257
248
|
# request for `files: true` - an array param holding a Boolean
|
|
258
249
|
# reaches the tool as a type it never accepts.
|
|
259
250
|
if prop[:type] == "array"
|
|
260
|
-
result[key] =
|
|
261
|
-
i += 1
|
|
251
|
+
result[key], consumed = array_values(args, i + 1)
|
|
252
|
+
i += 1 + consumed
|
|
262
253
|
next
|
|
263
254
|
end
|
|
264
255
|
|
|
@@ -335,15 +326,12 @@ module RailsAiContext
|
|
|
335
326
|
# stopping at a rake-style `key=value` token so `--include a model=Post`
|
|
336
327
|
# does not swallow the second parameter. Each token is still
|
|
337
328
|
# comma-split, so the documented `a.rb,b.rb` form keeps working and
|
|
338
|
-
# mixing the two spellings does not fabricate a path.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def values_consumed(args, from)
|
|
346
|
-
args[from..].to_a.take_while { |a| !a.start_with?("--") && !a.include?("=") }.size
|
|
329
|
+
# mixing the two spellings does not fabricate a path. The token count
|
|
330
|
+
# comes back with the values because the cursor advances by tokens: a
|
|
331
|
+
# comma token yields more values than it consumed.
|
|
332
|
+
def array_values(args, from)
|
|
333
|
+
tokens = args[from..].to_a.take_while { |a| !a.start_with?("--") && !a.include?("=") }
|
|
334
|
+
[ tokens.flat_map { |a| Array(coerce_value(a, { type: "array" })) }, tokens.size ]
|
|
347
335
|
end
|
|
348
336
|
|
|
349
337
|
# Coerce a string value to the type specified in the JSON Schema property.
|
|
@@ -88,8 +88,7 @@ module RailsAiContext
|
|
|
88
88
|
# A permission bit, a directory in place of a file and a bug in a
|
|
89
89
|
# listener all land in `unresolved` alike, so the cause is worth
|
|
90
90
|
# saying where the booted walk already says it.
|
|
91
|
-
|
|
92
|
-
nil
|
|
91
|
+
RailsAiContext.debug_fail(e, nil, label: "concern introspection of #{path}")
|
|
93
92
|
end
|
|
94
93
|
|
|
95
94
|
def tagged(entry, concern_name)
|
|
@@ -23,7 +23,12 @@ module RailsAiContext
|
|
|
23
23
|
configured = RailsAiContext.configuration.concern_paths
|
|
24
24
|
dirs =
|
|
25
25
|
if configured.nil?
|
|
26
|
-
|
|
26
|
+
# Every app tree the app has, not just the one at the root: a
|
|
27
|
+
# packwerk pack or an in-repo engine keeps its own app/*/concerns.
|
|
28
|
+
# `dirs_for` cannot take the glob directly - it tests each candidate
|
|
29
|
+
# with `Dir.exist?`, which a literal `app/*/concerns` never passes -
|
|
30
|
+
# so the app trees are resolved first and globbed here.
|
|
31
|
+
PathResolver.dirs_for(root, "app").flat_map { |app_dir| Dir.glob(File.join(app_dir, "*", "concerns")) }
|
|
27
32
|
else
|
|
28
33
|
# A path that is already absolute is taken as given; `File.join`
|
|
29
34
|
# would graft it onto the root and point at nothing.
|
|
@@ -15,10 +15,13 @@ module RailsAiContext
|
|
|
15
15
|
|
|
16
16
|
ORDER = { SUMMARY => 0, STANDARD => 1, FULL => 2 }.freeze
|
|
17
17
|
|
|
18
|
-
# The
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
18
|
+
# The `detail` property tools publish in their input schema. Tools pass
|
|
19
|
+
# their own wording and get the type and the enum from here, so the
|
|
20
|
+
# advertised values and the normalizer cannot drift apart. Spelling the
|
|
21
|
+
# values in a tool instead fails the enum-ownership spec.
|
|
22
|
+
def self.schema(description)
|
|
23
|
+
{ type: "string", enum: ALL, description: description }
|
|
24
|
+
end
|
|
22
25
|
|
|
23
26
|
def self.valid?(detail)
|
|
24
27
|
ALL.include?(detail.to_s)
|
|
@@ -444,8 +444,7 @@ module RailsAiContext
|
|
|
444
444
|
fix: introspector_failure_hint(failures))
|
|
445
445
|
end
|
|
446
446
|
rescue StandardError, ScriptError => e
|
|
447
|
-
|
|
448
|
-
nil
|
|
447
|
+
RailsAiContext.debug_fail(e, nil, label: "check_introspector_health")
|
|
449
448
|
end
|
|
450
449
|
|
|
451
450
|
MAX_SHOWN_FAILURES = 3
|
|
@@ -156,8 +156,7 @@ module RailsAiContext
|
|
|
156
156
|
return false unless spec
|
|
157
157
|
spec.source.is_a?(Bundler::Source::Path)
|
|
158
158
|
rescue => e
|
|
159
|
-
|
|
160
|
-
false
|
|
159
|
+
RailsAiContext.debug_fail(e, false, label: "local_gem_path?")
|
|
161
160
|
end
|
|
162
161
|
end
|
|
163
162
|
end
|
|
@@ -16,8 +16,7 @@ module RailsAiContext
|
|
|
16
16
|
|
|
17
17
|
ModelHints.resolve(model_names, context: context)
|
|
18
18
|
rescue => e
|
|
19
|
-
|
|
20
|
-
HydrationResult.new
|
|
19
|
+
RailsAiContext.debug_fail(e, HydrationResult.new, label: "ControllerHydrator")
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
# Detect model names referenced in a controller source file using Prism AST.
|
|
@@ -28,8 +27,7 @@ module RailsAiContext
|
|
|
28
27
|
Introspectors::ListenerRegistration.dispatcher_for(listener).dispatch(parse_result.value)
|
|
29
28
|
listener.results
|
|
30
29
|
rescue => e
|
|
31
|
-
|
|
32
|
-
[]
|
|
30
|
+
RailsAiContext.debug_fail(e, [], label: "detect_model_references")
|
|
33
31
|
end
|
|
34
32
|
private_class_method :detect_model_references
|
|
35
33
|
end
|
|
@@ -13,7 +13,7 @@ module RailsAiContext
|
|
|
13
13
|
schema_data = context[:schema]
|
|
14
14
|
return nil unless models_data.is_a?(Hash) && schema_data.is_a?(Hash)
|
|
15
15
|
|
|
16
|
-
model_key =
|
|
16
|
+
model_key = Payload.fuzzy_find_key(models_data.keys, model_name)
|
|
17
17
|
return nil unless model_key
|
|
18
18
|
|
|
19
19
|
model_info = models_data[model_key]
|
|
@@ -17,8 +17,7 @@ module RailsAiContext
|
|
|
17
17
|
ModelHints.resolve(model_names, context: context,
|
|
18
18
|
describe: ->(name) { "@#{name.underscore} used in view but '#{name}' model not found" })
|
|
19
19
|
rescue => e
|
|
20
|
-
|
|
21
|
-
HydrationResult.new
|
|
20
|
+
RailsAiContext.debug_fail(e, HydrationResult.new, label: "ViewHydrator")
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
# Convert an instance variable name to a model name by convention.
|
|
@@ -15,26 +15,42 @@ module RailsAiContext
|
|
|
15
15
|
# @param keeping [Array<Symbol>] the tools still selected, whose files
|
|
16
16
|
# must survive even when a dropped tool names the same path
|
|
17
17
|
# @param root [String, Pathname] project root
|
|
18
|
-
# @return [
|
|
19
|
-
# trailing slash so callers can print them the way they
|
|
18
|
+
# @return [Hash] { removed: [paths], failed: [paths] }, directories
|
|
19
|
+
# marked with a trailing slash so callers can print them the way they
|
|
20
|
+
# always have. Both rm calls swallow their errors, so a path is only
|
|
21
|
+
# reported removed once it is gone.
|
|
20
22
|
def remove(tools:, keeping:, root:)
|
|
21
23
|
kept = Array(keeping).flat_map { |key| AiTool.find(key)&.context_paths || [] }.to_set
|
|
24
|
+
removed = []
|
|
25
|
+
failed = []
|
|
22
26
|
|
|
23
|
-
Array(tools).
|
|
27
|
+
Array(tools).each do |key|
|
|
24
28
|
paths = AiTool.find(key)&.context_paths || []
|
|
25
|
-
paths.
|
|
26
|
-
|
|
29
|
+
paths.each do |relative|
|
|
30
|
+
next if kept.include?(relative)
|
|
31
|
+
|
|
32
|
+
label, gone = remove_path(relative, root)
|
|
33
|
+
next unless label
|
|
34
|
+
|
|
35
|
+
(gone ? removed : failed) << label
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
{ removed: removed, failed: failed }
|
|
27
40
|
end
|
|
28
41
|
|
|
42
|
+
# @return [Array(String, Boolean), nil] the path's label and whether it
|
|
43
|
+
# is gone, or nil when there was nothing there to remove. A directory
|
|
44
|
+
# whose children went but which itself survives counts as not gone.
|
|
29
45
|
def remove_path(relative, root)
|
|
30
46
|
full = File.join(root.to_s, relative)
|
|
31
47
|
|
|
32
48
|
if File.directory?(full)
|
|
33
49
|
FileUtils.rm_rf(full)
|
|
34
|
-
"#{relative}/"
|
|
50
|
+
[ "#{relative}/", !File.exist?(full) ]
|
|
35
51
|
elsif File.exist?(full)
|
|
36
52
|
FileUtils.rm_f(full)
|
|
37
|
-
relative
|
|
53
|
+
[ relative, !File.exist?(full) ]
|
|
38
54
|
end
|
|
39
55
|
end
|
|
40
56
|
private_class_method :remove_path
|
|
@@ -72,13 +72,6 @@ module RailsAiContext
|
|
|
72
72
|
setup
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# The tool-mode half of the same question, for a caller that predates
|
|
76
|
-
# the setup struct. It cannot express "MCP config only", so every
|
|
77
|
-
# entry point in this gem calls `select_setup` instead.
|
|
78
|
-
def select_tool_mode(surface)
|
|
79
|
-
select_setup(surface).tool_mode
|
|
80
|
-
end
|
|
81
|
-
|
|
82
75
|
def setup_label(setup)
|
|
83
76
|
return "MCP config only (no context files)" unless setup.context_files
|
|
84
77
|
|
|
@@ -118,14 +111,15 @@ module RailsAiContext
|
|
|
118
111
|
to_remove.each do |key|
|
|
119
112
|
tool = AiTool.find(key)
|
|
120
113
|
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
outcome = Cleanup.remove(tools: [ key ], keeping: selected.map(&:to_sym), root: root)
|
|
115
|
+
outcome[:removed].each { |path| surface.say " Removed #{path}", :ok }
|
|
116
|
+
outcome[:failed].each { |path| surface.say "Could not remove #{path} - check its permissions", :warn }
|
|
123
117
|
|
|
124
118
|
# Merge-safe MCP config cleanup - removes only the rails-ai-context entry
|
|
125
119
|
cleaned = RailsAiContext::McpConfigGenerator.remove(tools: [ key ], output_dir: root.to_s)
|
|
126
|
-
cleaned.each { |f| surface.say " Removed MCP entry from #{relative_to(f, root)}", :
|
|
120
|
+
cleaned.each { |f| surface.say " Removed MCP entry from #{relative_to(f, root)}", :ok }
|
|
127
121
|
|
|
128
|
-
surface.say " #{tool.name} files removed", :ok if tool
|
|
122
|
+
surface.say " #{tool.name} files removed", :ok if tool && outcome[:failed].empty?
|
|
129
123
|
end
|
|
130
124
|
end
|
|
131
125
|
|
|
@@ -147,6 +141,9 @@ module RailsAiContext
|
|
|
147
141
|
|
|
148
142
|
File.open(gitignore, "a") { |f| lines.each { |line| f.puts line } }
|
|
149
143
|
surface.say "Updated .gitignore", :ok
|
|
144
|
+
rescue SystemCallError, IOError => e
|
|
145
|
+
RailsAiContext.log_warn "[rails-ai-context] could not write .gitignore: #{e.message}"
|
|
146
|
+
surface.say "Could not update .gitignore - add .ai-context.json and .codex/config.toml by hand", :warn
|
|
150
147
|
end
|
|
151
148
|
|
|
152
149
|
# `standalone: nil` lets the generator detect the install mode from
|
|
@@ -159,6 +156,9 @@ module RailsAiContext
|
|
|
159
156
|
result = generator.call
|
|
160
157
|
result[:written].each { |f| surface.say "Created/Updated #{relative_to(f, root)}", :ok }
|
|
161
158
|
result[:skipped].each { |f| surface.say "#{relative_to(f, root)} unchanged - skipped", :muted }
|
|
159
|
+
result[:failed].each do |f|
|
|
160
|
+
surface.say "Could not write #{relative_to(f, root)} - that tool will not auto-discover the MCP server", :warn
|
|
161
|
+
end
|
|
162
162
|
surface.say "Skipped MCP config files (CLI-only mode)", :muted if tool_mode == :cli
|
|
163
163
|
result
|
|
164
164
|
end
|
|
@@ -28,8 +28,7 @@ module RailsAiContext
|
|
|
28
28
|
lock = RailsAiContext::GemLock.for(root)
|
|
29
29
|
lock.missing? ? false : !lock.present?("rails-ai-context")
|
|
30
30
|
rescue => e
|
|
31
|
-
|
|
32
|
-
false
|
|
31
|
+
RailsAiContext.debug_fail(e, false, label: "standalone install detection")
|
|
33
32
|
end
|
|
34
33
|
end
|
|
35
34
|
end
|
|
@@ -51,8 +51,7 @@ module RailsAiContext
|
|
|
51
51
|
entry[:callbacks] = callbacks if callbacks.any?
|
|
52
52
|
entry
|
|
53
53
|
rescue => e
|
|
54
|
-
|
|
55
|
-
nil
|
|
54
|
+
RailsAiContext.debug_fail(e, nil, label: "extract_mailboxes")
|
|
56
55
|
end.compact.sort_by { |m| m[:name] }
|
|
57
56
|
end
|
|
58
57
|
end
|