rails-ai-context 5.28.0 → 5.30.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 +1443 -0
- data/CONTEXT.md +37 -0
- data/CONTRIBUTING.md +2 -2
- data/README.md +2 -2
- data/SECURITY.md +1 -22
- data/docs/ARCHITECTURE.md +9 -6
- data/docs/CLI.md +9 -0
- data/docs/COMPATIBILITY.md +26 -16
- data/docs/CONFIGURATION.md +19 -13
- data/docs/GUIDE.md +19 -18
- data/docs/INTROSPECTORS.md +53 -18
- data/docs/STANDALONE.md +1 -1
- data/docs/TOOLS.md +91 -30
- data/docs/TROUBLESHOOTING.md +17 -3
- data/exe/rails-ai-context +72 -81
- data/lib/generators/rails_ai_context/install/install_generator.rb +10 -16
- data/lib/rails_ai_context/action_filters.rb +10 -6
- data/lib/rails_ai_context/action_outcome.rb +242 -0
- data/lib/rails_ai_context/app_commands.rb +36 -0
- data/lib/rails_ai_context/app_kind.rb +25 -4
- data/lib/rails_ai_context/ast_cache.rb +52 -10
- data/lib/rails_ai_context/base_mixins.rb +175 -0
- data/lib/rails_ai_context/brackets.rb +123 -0
- data/lib/rails_ai_context/cli/entry_boot.rb +67 -39
- data/lib/rails_ai_context/cli/tool_runner.rb +51 -33
- data/lib/rails_ai_context/concern_macros.rb +314 -25
- data/lib/rails_ai_context/concern_membership.rb +25 -1
- data/lib/rails_ai_context/concern_paths.rb +125 -6
- data/lib/rails_ai_context/configuration.rb +25 -13
- data/lib/rails_ai_context/detail_level.rb +2 -8
- data/lib/rails_ai_context/doctor.rb +102 -111
- data/lib/rails_ai_context/erb_source.rb +6 -0
- data/lib/rails_ai_context/file_walk.rb +37 -0
- data/lib/rails_ai_context/fingerprinter.rb +12 -12
- data/lib/rails_ai_context/gem_lock.rb +56 -10
- data/lib/rails_ai_context/git_ignore.rb +211 -0
- data/lib/rails_ai_context/hydrators/hydration_formatter.rb +3 -4
- data/lib/rails_ai_context/hydrators/schema_hint_builder.rb +4 -3
- data/lib/rails_ai_context/install/ai_tool.rb +9 -9
- data/lib/rails_ai_context/install/selection_record.rb +21 -89
- data/lib/rails_ai_context/install/surface.rb +16 -0
- data/lib/rails_ai_context/install_mode.rb +12 -0
- data/lib/rails_ai_context/introspector.rb +15 -15
- data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -13
- data/lib/rails_ai_context/introspectors/action_presence.rb +127 -0
- data/lib/rails_ai_context/introspectors/action_resolver.rb +77 -31
- data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -13
- data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +40 -58
- data/lib/rails_ai_context/introspectors/active_support_introspector.rb +15 -20
- data/lib/rails_ai_context/introspectors/api_introspector.rb +211 -49
- data/lib/rails_ai_context/introspectors/asset_pipeline_introspector.rb +14 -53
- data/lib/rails_ai_context/introspectors/ast_walk.rb +22 -0
- data/lib/rails_ai_context/introspectors/auth_introspector.rb +38 -35
- data/lib/rails_ai_context/introspectors/autoload_introspector.rb +15 -14
- data/lib/rails_ai_context/introspectors/base.rb +21 -0
- data/lib/rails_ai_context/introspectors/call_site_expansion.rb +517 -0
- data/lib/rails_ai_context/introspectors/component_introspector.rb +141 -114
- data/lib/rails_ai_context/introspectors/config_introspector.rb +1 -13
- data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +7 -25
- data/lib/rails_ai_context/introspectors/controller_filters.rb +49 -10
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +141 -32
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +38 -38
- data/lib/rails_ai_context/introspectors/credentials_introspector.rb +5 -15
- data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +1 -15
- data/lib/rails_ai_context/introspectors/declared_constant.rb +103 -26
- data/lib/rails_ai_context/introspectors/devops_introspector.rb +1 -13
- data/lib/rails_ai_context/introspectors/eager_load.rb +1 -1
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +34 -25
- data/lib/rails_ai_context/introspectors/env_config_introspector.rb +30 -20
- data/lib/rails_ai_context/introspectors/env_introspector.rb +13 -29
- data/lib/rails_ai_context/introspectors/env_references.rb +92 -0
- data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +102 -129
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +57 -17
- data/lib/rails_ai_context/introspectors/gemfile_gems.rb +27 -0
- data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +115 -0
- data/lib/rails_ai_context/introspectors/helper_names.rb +48 -0
- data/lib/rails_ai_context/introspectors/i18n_introspector.rb +14 -17
- data/lib/rails_ai_context/introspectors/includers.rb +52 -0
- data/lib/rails_ai_context/introspectors/initializer_introspector.rb +5 -15
- data/lib/rails_ai_context/introspectors/interaction.rb +127 -0
- data/lib/rails_ai_context/introspectors/job_introspector.rb +585 -160
- data/lib/rails_ai_context/introspectors/listener_registration.rb +0 -7
- data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +14 -7
- data/lib/rails_ai_context/introspectors/listeners/autoload_paths_listener.rb +102 -0
- data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +211 -19
- data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +12 -13
- data/lib/rails_ai_context/introspectors/listeners/component_structure_listener.rb +3 -10
- data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +2 -2
- data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +15 -3
- data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +2 -12
- data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +16 -0
- data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +5 -3
- data/lib/rails_ai_context/introspectors/listeners/mailbox_routing_listener.rb +1 -1
- data/lib/rails_ai_context/introspectors/listeners/methods_listener.rb +1 -1
- data/lib/rails_ai_context/introspectors/listeners/middleware_config_listener.rb +47 -5
- data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +29 -48
- data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +108 -0
- data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +44 -6
- data/lib/rails_ai_context/introspectors/listeners/model_reference_listener.rb +1 -1
- data/lib/rails_ai_context/introspectors/listeners/mount_listener.rb +132 -24
- data/lib/rails_ai_context/introspectors/listeners/preview_paths_listener.rb +48 -0
- data/lib/rails_ai_context/introspectors/listeners/rake_task_dsl_listener.rb +6 -25
- data/lib/rails_ai_context/introspectors/listeners/route_files_listener.rb +93 -0
- data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +159 -35
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +5 -32
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +22 -8
- data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +82 -7
- data/lib/rails_ai_context/introspectors/listeners/with_options_scope.rb +105 -0
- data/lib/rails_ai_context/introspectors/middleware_introspector.rb +125 -70
- data/lib/rails_ai_context/introspectors/migration_introspector.rb +1 -13
- data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +259 -0
- data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +285 -0
- data/lib/rails_ai_context/introspectors/migration_replay.rb +220 -207
- data/lib/rails_ai_context/introspectors/model_introspector.rb +676 -129
- data/lib/rails_ai_context/introspectors/module_aliases.rb +157 -0
- data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +84 -78
- data/lib/rails_ai_context/introspectors/node_source.rb +46 -0
- data/lib/rails_ai_context/introspectors/observability_introspector.rb +1 -13
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +122 -96
- data/lib/rails_ai_context/introspectors/rake_task_introspector.rb +2 -10
- data/lib/rails_ai_context/introspectors/retry_policy.rb +38 -0
- data/lib/rails_ai_context/introspectors/route_introspector.rb +275 -52
- data/lib/rails_ai_context/introspectors/schema_conventions.rb +87 -9
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +89 -25
- data/lib/rails_ai_context/introspectors/schema_reader.rb +11 -5
- data/lib/rails_ai_context/introspectors/security_introspector.rb +12 -42
- data/lib/rails_ai_context/introspectors/seeds_introspector.rb +23 -33
- data/lib/rails_ai_context/introspectors/service_classes.rb +101 -0
- data/lib/rails_ai_context/introspectors/source_calls.rb +86 -0
- data/lib/rails_ai_context/introspectors/source_introspector.rb +17 -1
- data/lib/rails_ai_context/introspectors/source_scan.rb +12 -2
- data/lib/rails_ai_context/introspectors/static_tier.rb +1 -1
- data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +408 -79
- data/lib/rails_ai_context/introspectors/structure_sql_reader.rb +123 -32
- data/lib/rails_ai_context/introspectors/superclass_chain.rb +149 -0
- data/lib/rails_ai_context/introspectors/table_name.rb +152 -9
- data/lib/rails_ai_context/introspectors/test_introspector.rb +57 -73
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +61 -82
- data/lib/rails_ai_context/introspectors/view_introspector.rb +59 -60
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +286 -95
- data/lib/rails_ai_context/mcp_config_generator.rb +1 -6
- data/lib/rails_ai_context/option_text.rb +17 -0
- data/lib/rails_ai_context/path_resolver.rb +246 -10
- data/lib/rails_ai_context/payload.rb +43 -7
- data/lib/rails_ai_context/percent.rb +17 -0
- data/lib/rails_ai_context/portable_path.rb +7 -0
- data/lib/rails_ai_context/redaction.rb +288 -49
- data/lib/rails_ai_context/resources.rb +1 -1
- data/lib/rails_ai_context/route_coverage.rb +50 -4
- data/lib/rails_ai_context/run_cache.rb +37 -0
- data/lib/rails_ai_context/safe_file.rb +6 -2
- data/lib/rails_ai_context/safe_path.rb +0 -2
- data/lib/rails_ai_context/schema_adapter.rb +80 -15
- data/lib/rails_ai_context/schema_hint.rb +1 -13
- data/lib/rails_ai_context/serializers/base.rb +13 -0
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +8 -12
- data/lib/rails_ai_context/serializers/claude_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +98 -58
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +4 -5
- data/lib/rails_ai_context/serializers/context_mode_dispatch.rb +2 -7
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +1 -7
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +10 -89
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -7
- data/lib/rails_ai_context/serializers/json_serializer.rb +1 -7
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +76 -45
- data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +3 -9
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +2 -17
- data/lib/rails_ai_context/serializers/section_facts.rb +85 -5
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +3 -3
- data/lib/rails_ai_context/serializers/stack_overview_helper.rb +54 -42
- data/lib/rails_ai_context/serializers/test_command_detection.rb +1 -6
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -3
- data/lib/rails_ai_context/server.rb +3 -0
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +22 -30
- data/lib/rails_ai_context/test_framework.rb +234 -0
- data/lib/rails_ai_context/tools/analyze_feature.rb +53 -40
- data/lib/rails_ai_context/tools/base_tool.rb +173 -78
- data/lib/rails_ai_context/tools/dependency_graph.rb +144 -43
- data/lib/rails_ai_context/tools/diagnose.rb +59 -66
- data/lib/rails_ai_context/tools/generate_test.rb +517 -134
- data/lib/rails_ai_context/tools/get_active_support.rb +30 -15
- data/lib/rails_ai_context/tools/get_api.rb +43 -8
- data/lib/rails_ai_context/tools/get_callbacks.rb +9 -4
- data/lib/rails_ai_context/tools/get_component_catalog.rb +34 -11
- data/lib/rails_ai_context/tools/get_concern.rb +241 -136
- data/lib/rails_ai_context/tools/get_config.rb +48 -18
- data/lib/rails_ai_context/tools/get_context.rb +32 -20
- data/lib/rails_ai_context/tools/get_controllers.rb +16 -15
- data/lib/rails_ai_context/tools/get_conventions.rb +39 -59
- data/lib/rails_ai_context/tools/get_edit_context.rb +6 -3
- data/lib/rails_ai_context/tools/get_engines.rb +19 -6
- data/lib/rails_ai_context/tools/get_env.rb +69 -90
- data/lib/rails_ai_context/tools/get_env_config.rb +1 -1
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +7 -1
- data/lib/rails_ai_context/tools/get_gems.rb +27 -20
- data/lib/rails_ai_context/tools/get_helper_methods.rb +33 -37
- data/lib/rails_ai_context/tools/get_i18n.rb +10 -5
- data/lib/rails_ai_context/tools/get_job_pattern.rb +265 -96
- data/lib/rails_ai_context/tools/get_mailers.rb +51 -3
- data/lib/rails_ai_context/tools/get_model_details.rb +105 -46
- data/lib/rails_ai_context/tools/get_partial_interface.rb +68 -80
- data/lib/rails_ai_context/tools/get_routes.rb +77 -8
- data/lib/rails_ai_context/tools/get_schema.rb +163 -56
- data/lib/rails_ai_context/tools/get_service_pattern.rb +165 -88
- data/lib/rails_ai_context/tools/get_stimulus.rb +74 -32
- data/lib/rails_ai_context/tools/get_test_info.rb +100 -51
- data/lib/rails_ai_context/tools/get_turbo_map.rb +44 -34
- data/lib/rails_ai_context/tools/get_view.rb +72 -73
- data/lib/rails_ai_context/tools/migration_advisor.rb +9 -6
- data/lib/rails_ai_context/tools/onboard.rb +57 -72
- data/lib/rails_ai_context/tools/query.rb +13 -26
- data/lib/rails_ai_context/tools/read_logs.rb +5 -22
- data/lib/rails_ai_context/tools/review_changes.rb +4 -4
- data/lib/rails_ai_context/tools/runtime_info.rb +5 -20
- data/lib/rails_ai_context/tools/safe_call.rb +16 -1
- data/lib/rails_ai_context/tools/search_code.rb +148 -74
- data/lib/rails_ai_context/tools/search_docs.rb +9 -28
- data/lib/rails_ai_context/tools/security_scan.rb +283 -52
- data/lib/rails_ai_context/tools/session_context.rb +7 -2
- data/lib/rails_ai_context/tools/validate.rb +8 -11
- data/lib/rails_ai_context/tools/validate_semantics.rb +138 -121
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +27 -3
- data/lib/rails_ai_context/view_file.rb +41 -11
- data/lib/rails_ai_context.rb +30 -3
- data/server.json +1 -1
- metadata +38 -13
- data/demo/demo-trace.gif +0 -0
- data/demo/demo-trace.tape +0 -21
- data/demo/demo.gif +0 -0
- data/demo/demo.tape +0 -33
- data/docs/social-preview.html +0 -164
- data/gemfiles/mcp_floor.gemfile +0 -19
- data/gemfiles/prism_floor.gemfile +0 -13
- data/gemfiles/prism_head.gemfile +0 -12
- data/gemfiles/rails.gemfile +0 -42
- data/lib/rails_ai_context/introspectors/listeners/mongoid_fields_listener.rb +0 -27
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,1449 @@ 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.30.0] - 2026-09-28
|
|
9
|
+
|
|
10
|
+
A pass over the whole tree for code that did not need to exist, with one base
|
|
11
|
+
class for the introspectors and one for the serializers, and one reader where
|
|
12
|
+
several files each kept a copy. Then four real-app QA passes in every tier
|
|
13
|
+
(Mastodon, Discourse, OpenProject, Canvas, Forem, Whitehall, Consul,
|
|
14
|
+
OpenFoodNetwork, Huginn, Errbit, Diaspora, Plots2 and a private API app) and
|
|
15
|
+
five review rounds found the wrong answers below. Their fixes grew `lib/` by
|
|
16
|
+
about 8,500 lines, most of it reading what the gem used to guess.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`rails_get_engines` and the generated files name the app's own in-repo
|
|
21
|
+
engines, plugins and modules**: an "In-Repo Engines" section with each
|
|
22
|
+
one's path and the models the model scan found under it (unavailable, not
|
|
23
|
+
zero, when the model scan failed), and
|
|
24
|
+
in the compact files one Stack line, `- In-repo engines: 4 (catalog,
|
|
25
|
+
dfc_provider, order_management, web)`, capped at five names.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **`.github/copilot-instructions.md` is built the way CLAUDE.md is.** It was
|
|
30
|
+
the one root file off the shared compact renderer. It now carries the same
|
|
31
|
+
sections (Async, Migrations, Commands, Rules), per-model counts, "Key models
|
|
32
|
+
(N total)" with 15 models where it listed 25, and the `Generated by
|
|
33
|
+
rails-ai-context` line; "Conventions" becomes "Rules". Its tool reference is
|
|
34
|
+
the compact list plus the roster of every tool name, where it had a
|
|
35
|
+
Copilot-only table with one described row per tool and a "detail parameter -
|
|
36
|
+
ALWAYS start with summary" section. The file is capped by `claude_max_lines`
|
|
37
|
+
and ends with the trim note when it runs over. Its docs promised a 500-line
|
|
38
|
+
cap that nothing enforced.
|
|
39
|
+
- **`claude_max_lines` counts content lines, the `BEGIN`/`END` markers
|
|
40
|
+
included, and must be positive.** Blank lines between sections do not spend
|
|
41
|
+
the budget, so a file that fit before still fits and still ends with the
|
|
42
|
+
full MCP tools guide. A file over the budget keeps its Commands, Warnings,
|
|
43
|
+
Rules and MCP tools sections whole and trims the data sections above them,
|
|
44
|
+
with the trim note at the cut and no heading left without its body; it
|
|
45
|
+
trimmed from the end, so the tool list and the last rules were what went. A
|
|
46
|
+
budget too small for those sections alone still trims from the end. Zero or
|
|
47
|
+
a negative number raises `ArgumentError` when set in Ruby, the way
|
|
48
|
+
`cache_ttl` and `max_tool_response_chars` already do; in
|
|
49
|
+
`.rails-ai-context.yml` it warns and keeps the default of 150.
|
|
50
|
+
- **The four compact files print their sections in one order**: Stack, Key
|
|
51
|
+
models, Gems, Architecture, Commands, Warnings, Rules, Tools. AGENTS.md moves
|
|
52
|
+
its MCP tools guide from the middle to the end and its Warnings block above
|
|
53
|
+
Rules, which is where CLAUDE.md already had them.
|
|
54
|
+
- **`views.layout_mapping` is gone from `.ai-context.json`.** It globbed the
|
|
55
|
+
layouts a second time to hold the names `views.layouts` already carries.
|
|
56
|
+
- **`rails_read_logs` and `rails_runtime_info` print sizes through
|
|
57
|
+
ActiveSupport**: binary units and three significant digits, so read_logs
|
|
58
|
+
says "2.29 MB" where it said "2.4 MB", runtime_info says "1 GB" where it said
|
|
59
|
+
"1.0 GB", and a count under a kilobyte reads "512 Bytes" where it read
|
|
60
|
+
"512 B". Both print in English whatever the app's `I18n.locale` is, and in
|
|
61
|
+
the app's own locale only when it offers no English at all. The two tools
|
|
62
|
+
used to disagree on what a megabyte was.
|
|
63
|
+
- **`rails-ai-context tool --list` trims each description at a word boundary
|
|
64
|
+
to at most 79 characters, ellipsis included**, and a cut that lands on
|
|
65
|
+
trailing punctuation drops it before the ellipsis.
|
|
66
|
+
- **`tools/list` is sorted by MCP tool name, and `rails-ai-context tool --list`
|
|
67
|
+
prints its short names in that order.** That is the order v5.29.0 happened
|
|
68
|
+
to print; it no longer depends on which tools a process loaded first.
|
|
69
|
+
- **A failed section is recorded in one place.** The introspection loop turns
|
|
70
|
+
a failed section into its `{ error: }` entry and prints a warning, so a
|
|
71
|
+
section that used to fail without a warning now shows one, and `doctor`
|
|
72
|
+
names the exception class ("database_stats: StandardError: Database not
|
|
73
|
+
found"). The entry itself is unchanged. Only `credentials` still records its
|
|
74
|
+
own failure, to keep the exception message out of the output.
|
|
75
|
+
- **`init` and `version` answer an unexpected error like every other
|
|
76
|
+
command**: `Error: <message>` on stderr and exit 1. One rescue around
|
|
77
|
+
Thor's command invocation replaced eight copies; Thor's own refusals keep
|
|
78
|
+
their wording. With `DEBUG` set, the first 15 backtrace frames follow the
|
|
79
|
+
`Error:` line.
|
|
80
|
+
- **The doctor's view-size hint names only `config.max_view_total_size`**, the
|
|
81
|
+
setting its check compares against, and the generated initializer's comment
|
|
82
|
+
for `max_view_file_size` says no check reads it. The option is still
|
|
83
|
+
accepted.
|
|
84
|
+
- **The gem requires `thor >= 1.2`.** Thor 1.0 and 1.1 raise `NameError` on
|
|
85
|
+
require under every Ruby this gem supports.
|
|
86
|
+
- **The packaged gem leaves out `demo/`**, 525KB of GIFs the README loads from
|
|
87
|
+
GitHub, `gemfiles/`, the CI's own Gemfiles, and `docs/social-preview.html`, a
|
|
88
|
+
one-off screenshot page. The .gem drops from about 1.29MB to about 780KB.
|
|
89
|
+
- **The security policy says only the latest 5.x release gets fixes**, in
|
|
90
|
+
place of a version table that stopped at 5.7.x.
|
|
91
|
+
- **The release runs the same Ruby and Rails matrix as CI**, from one shared
|
|
92
|
+
workflow whose test legs get read-only repository access. It used to skip
|
|
93
|
+
Ruby 3.2 with Rails 8.0 and 8.1, which CI already ran green.
|
|
94
|
+
- **Introspection reads each source once where it used to read it again**:
|
|
95
|
+
auth, Active Storage, turbo views, seeds, conventions, job channels, the
|
|
96
|
+
Gemfile, ActiveRecord and Mongoid models, each migration the schema replays,
|
|
97
|
+
`db/migrate`, and the doctor's view count. Where one walk now answers several
|
|
98
|
+
lists (Active Storage's attachments, validations and variants), a walk that
|
|
99
|
+
fails leaves those lists empty together.
|
|
100
|
+
- **With `DEBUG` set, a skipped scan prints `<label> failed: <message>`**, the
|
|
101
|
+
shape every other debug line in the gem already had.
|
|
102
|
+
|
|
103
|
+
- **A static route count says how many in-repo engine route files it did not
|
|
104
|
+
read**, everywhere a route count is printed: `rails_get_routes`, CLAUDE.md,
|
|
105
|
+
the rule files, `rails_onboard` and the rake summary. An engine draws its
|
|
106
|
+
routes into its own table and the app mounts it at boot, so the static tier
|
|
107
|
+
cannot place those paths.
|
|
108
|
+
- **`rails_get_i18n` and the generated files say what the in-repo engines'
|
|
109
|
+
own locale directories hold that they did not read**: the tool and the
|
|
110
|
+
full file count the files, and the compact files' I18n line ends
|
|
111
|
+
`; 29 in-repo engine locale dirs not read`.
|
|
112
|
+
- **The component catalog reads every `app/components`**, packs and in-repo
|
|
113
|
+
engines included, and its header counts what it could not place, as in `998
|
|
114
|
+
components (862 ViewComponent, 0 Phlex, 136 of no known base class)` on
|
|
115
|
+
OpenProject, which read 559 from its root tree alone, with a base that other
|
|
116
|
+
components inherit from (Consul's two `BaseComponent`s) named on a bases
|
|
117
|
+
line and not counted; the generated files print the same line from the same
|
|
118
|
+
numbers. `component:` takes a name with or without its namespace or
|
|
119
|
+
`Component` suffix, and a short name several components share lists them and
|
|
120
|
+
asks for the full one.
|
|
121
|
+
- **`rails_get_active_support` lists validator classes apart from concerns**,
|
|
122
|
+
so its concern total matches `rails_get_concern`, and a concerns directory
|
|
123
|
+
holding only validators says so.
|
|
124
|
+
- **A condition prints the line the file holds.** A callback, concern
|
|
125
|
+
callback, validation or controller filter whose condition is a lambda, a
|
|
126
|
+
proc or a method call prints that source on one line where it printed
|
|
127
|
+
`[INFERRED]`, and a symbol prints as a symbol (`if: :published?` and
|
|
128
|
+
`on: :create`, where they read `if: published?` and `on: create`), in a
|
|
129
|
+
list as well (`if: [:a?, :b?]`). A
|
|
130
|
+
callback with `if:` or `unless:` now names the condition in
|
|
131
|
+
`rails_get_model_details` and `rails_get_callbacks`, each declaration its
|
|
132
|
+
own when one method is declared twice, and a filter that runs names its
|
|
133
|
+
own condition in every surface that lists filters, where only a skip's
|
|
134
|
+
printed. The same
|
|
135
|
+
goes for any validation option that is not a literal (a regexp, a
|
|
136
|
+
constant) and for an option nested in another one, as in
|
|
137
|
+
`inclusion: { in: proc { ... } }`.
|
|
138
|
+
- **`rails_dependency_graph` draws no node for an association whose
|
|
139
|
+
`class_name` is computed at runtime**, where it drew one named `_INFERRED_`,
|
|
140
|
+
leaves them out of its association count, and names up to ten of them in
|
|
141
|
+
a note that says how many the count leaves out. The same goes for an
|
|
142
|
+
association it cannot name. Its footer says how many edges the page drew
|
|
143
|
+
beside the associations they came from, so the numbers reconcile with the
|
|
144
|
+
diagram.
|
|
145
|
+
- **An association whose name is computed is marked `(computed)` wherever it
|
|
146
|
+
prints** (`` `owner_name` (computed) ``, and
|
|
147
|
+
`` `:"#{name.underscore}_search_data"` (computed) ``), where it printed
|
|
148
|
+
`[INFERRED]` (six times in Discourse's CLAUDE.md). No tool reads it as a
|
|
149
|
+
literal: `rails_dependency_graph` draws no node for it,
|
|
150
|
+
`rails_generate_test` writes no matcher for it, where it wrote a spec that
|
|
151
|
+
did not pass, and the related-model and N+1 checks name no model for it. A
|
|
152
|
+
computed `class_name` or `through:` prints its source too.
|
|
153
|
+
- **A `class_name` written with a leading `::` names the class it names**
|
|
154
|
+
(`class_name: "::Token::API"`), in every tool and generated file.
|
|
155
|
+
- **The generated files rank "Key models" by association count**, most
|
|
156
|
+
connected first and then by name, through the same function `rails_onboard`
|
|
157
|
+
uses, and the heading says so. A subclass ranks by the associations it adds
|
|
158
|
+
to its parent, so one that only repeats them sinks and one with many of its
|
|
159
|
+
own (OpenProject's `User`) stays: Huginn's list was `Agent` and 14 of its
|
|
160
|
+
subclasses, with no `User`, `Event` or `Scenario`, and Discourse's was
|
|
161
|
+
headed by `Chat::NullUser`. They listed the first 15 by name, and
|
|
162
|
+
`rails_onboard` broke ties in file order.
|
|
163
|
+
- **The controller count beside a route count reads "routed controllers"** in
|
|
164
|
+
CLAUDE.md, AGENTS.md, `rails_onboard` and `rake ai:inspect`, since it counts
|
|
165
|
+
the controllers the routes name, not the controller files.
|
|
166
|
+
- **A Stimulus controller is named by its Stimulus identifier**, from its path
|
|
167
|
+
under the `controllers` directory (`users--tools--ajax`), and a lookup takes
|
|
168
|
+
the identifier, the file path with slashes or underscores, or the file name
|
|
169
|
+
(`ajax_controller.js`), a file name several controllers share listing them
|
|
170
|
+
all. Its "Used in" list reads the same files that confirm a name, views
|
|
171
|
+
first. Outside the rails-new `app/javascript/controllers` home the app's own
|
|
172
|
+
loader decides the name, so a name the app registers in its JavaScript
|
|
173
|
+
(`application.register("x", Klass)`, `preregister('x', Klass)`, literal
|
|
174
|
+
strings, the class as the whole argument, a module imported through the
|
|
175
|
+
app's alias table in tsconfig or jsconfig `compilerOptions.paths`, read
|
|
176
|
+
through `extends`, or a literal vite, webpack or rspack `resolve.alias`
|
|
177
|
+
(matched as the bundlers match, an exact key exact), test files not counted)
|
|
178
|
+
is used, and otherwise a derived name is checked against the identifiers the
|
|
179
|
+
app writes in any template or Ruby file under its code roots and `lib`
|
|
180
|
+
(`data-controller`, a target, an action, a `content_controller`-style
|
|
181
|
+
helper, a `data: { controller: }` hash in a component, form object or model)
|
|
182
|
+
and takes the one they use; one nothing confirms carries a note that it is
|
|
183
|
+
inferred. No two controllers share an identifier across code roots: an
|
|
184
|
+
in-repo plugin's `foo_controller.js` beside the app's reads `chat--foo`,
|
|
185
|
+
marked inferred. A controller the app registers from an installed package
|
|
186
|
+
(Errbit's `reveal` from `@stimulus-components/reveal`) is listed as
|
|
187
|
+
registered from that package, where `rails_get_stimulus` said none while
|
|
188
|
+
`rails_get_view` showed it. OpenProject's 161 read the names its code writes
|
|
189
|
+
(`admin--custom-fields`, not the path's `dynamic--admin--custom-fields`),
|
|
190
|
+
and the few nothing confirms are marked inferred, named by their directory's
|
|
191
|
+
rule when the app shows one (a loader importing the directory by a derived
|
|
192
|
+
path, or confirmed neighbours that all drop its segment), so OpenProject's 8
|
|
193
|
+
read `generic-dialog-close`, not `dynamic--generic-dialog-close`. The empty
|
|
194
|
+
answer names the directories searched, and `rails_get_conventions` labels
|
|
195
|
+
the pattern `Stimulus controllers` with no directory after it.
|
|
196
|
+
- **The frontend framework is picked by config files and component counts**, a
|
|
197
|
+
tsconfig read with its comments, trailing commas and `extends` (a `tsc
|
|
198
|
+
--init` file read `TypeScript: disabled`), and a second framework the app
|
|
199
|
+
also uses is named. An Angular app that also depends on React read as React,
|
|
200
|
+
and so did a Preact or Solid app that lists `react` for compatibility. Each
|
|
201
|
+
framework reports its own version, and a frontend root counts the components
|
|
202
|
+
its framework writes (OpenProject's `frontend` has 305 `*.component.ts`; it
|
|
203
|
+
read 3).
|
|
204
|
+
- **`rails_get_test_info` names both frameworks** (`rspec, minitest`) when an
|
|
205
|
+
app runs both.
|
|
206
|
+
- **The conventions' Locales line names the locales each file declares** (a
|
|
207
|
+
`validates_timeliness.en.yml` read as a locale named
|
|
208
|
+
`validates_timeliness`), a class named from the root scope (`class ::Foo`
|
|
209
|
+
inside a module) is the top-level constant, and an unread `database.yml`
|
|
210
|
+
default is never shown as another site's default.
|
|
211
|
+
- **The generated files' Multi-Database section labels each adapter the way
|
|
212
|
+
every other surface does** (`PostgreSQL`, `MySQL by database.yml default`),
|
|
213
|
+
where it printed the raw adapter name, and prints `unknown` for one it
|
|
214
|
+
cannot read instead of nothing.
|
|
215
|
+
- **`rails_get_partial_interface(partial:)` given a bare name that several
|
|
216
|
+
partials share lists them**, where it answered with the first by path, and a
|
|
217
|
+
call to a helper the app defines (in `app/helpers` of any code root, or a
|
|
218
|
+
module those helpers include from any autoload root; a module's class
|
|
219
|
+
methods are not helpers) is not one of a partial's locals: Discourse's
|
|
220
|
+
`layouts/head` listed `canonical_link_tag` and three more.
|
|
221
|
+
- **A context run reads more of the app, for more time where there is more to
|
|
222
|
+
read.** Where a kind of code lives is resolved once per root, each file is
|
|
223
|
+
parsed once per run and answered from its stat after that, a mixin is walked
|
|
224
|
+
once for every model that includes it, and a CLI tool call loads only the
|
|
225
|
+
tool it names; none of it outlives a run, so a directory created while a
|
|
226
|
+
server runs is seen by the next one, and a file's stat stands in for reading
|
|
227
|
+
it only once the file is two seconds older than the read, so a same-size
|
|
228
|
+
edit right after a parse on a coarse Linux clock is read again. The CLI
|
|
229
|
+
picks between a built-in tool and a custom one of the same name as the MCP
|
|
230
|
+
server does: the built-in, unless `skip_tools` names it. On the static tier,
|
|
231
|
+
`context --format all` takes about the same CPU as 5.29.0 on Canvas (17.4s
|
|
232
|
+
against 18.1s), Mastodon (2.8s against 2.7s) and Errbit, a sixth more on
|
|
233
|
+
Huginn (1.1s against 1.0s), a quarter more on a private API app (3.1s
|
|
234
|
+
against 2.5s; booted 5.4s against 4.7s), where the `Services:` line reads
|
|
235
|
+
all its services, and half again on OpenProject (9.4s against 6.4s), whose
|
|
236
|
+
30 in-repo modules are now read for models, Stimulus identifiers, concerns,
|
|
237
|
+
mailers and model bases.
|
|
238
|
+
- **The generated files' `Async:` line counts Sidekiq workers** beside jobs and
|
|
239
|
+
mailers, and `rails_get_job_pattern` names the directories it read under
|
|
240
|
+
every listing and also searches `app/sidekiq` for what enqueues a job. The
|
|
241
|
+
`Jobs:` line lists what the job scan found, jobs and workers alike, up to 12
|
|
242
|
+
names and a count of the rest, where it globbed `app/jobs/*.rb`.
|
|
243
|
+
- **A worker entry says what a job entry says**: its line count, the
|
|
244
|
+
`retry_on` and `discard_on` it declares and what it calls. Where a base
|
|
245
|
+
class owns `perform` and the worker defines `execute`, as Discourse's jobs
|
|
246
|
+
do, `execute` is read instead.
|
|
247
|
+
- **A class in a job directory that reaches neither ActiveJob nor Sidekiq but
|
|
248
|
+
defines `perform` is listed as `[unknown base]`**, as a Resque job is, and
|
|
249
|
+
counted under an `unknown` queue rather than the default one, in both tiers.
|
|
250
|
+
- **A job's "Enqueued By" list names only the files that enqueue that job, and
|
|
251
|
+
every one of them.** It is read off the call nodes, with one enqueue rule
|
|
252
|
+
shared by the job and service side effects, the controller render map and
|
|
253
|
+
`rails_search_code`'s "Calls internally", so a call in a comment does not
|
|
254
|
+
count and Sidekiq's `perform_in`, `perform_at` and `perform_bulk` and
|
|
255
|
+
ActiveJob's `perform_all_later` do (a private API app listed 3 of the 6
|
|
256
|
+
files that enqueue one of its workers); a call through the app's own
|
|
257
|
+
`enqueue`, `enqueue_in` or `enqueue_at` helper counts, the job named by a
|
|
258
|
+
symbol, a string or the class (Discourse's `Jobs.enqueue(:process_post)`
|
|
259
|
+
lists `app/models/post.rb`, where Discourse jobs had no callers); all of
|
|
260
|
+
`app/` and `lib/` is searched, rake tasks, `bin/` and `script/` programs and
|
|
261
|
+
all of `db/` included, and a list capped at twenty says how many more there
|
|
262
|
+
are; a job named relative to the caller's namespace (`SyncJob` inside
|
|
263
|
+
`module Admin`, in a method or a class-body block such as a callback) is
|
|
264
|
+
credited to the job Ruby resolves it to, a compact `class A::B` nesting only
|
|
265
|
+
itself as Ruby does, not a top-level one of the same short name; and a job
|
|
266
|
+
nothing enqueues says so in one line where the section was left out. A call
|
|
267
|
+
on another job whose name ends the same way (`Admin::SyncJob.perform_async`
|
|
268
|
+
for `SyncJob`) counted, and the job's own file was left out by a substring
|
|
269
|
+
of its name, which dropped any file whose name contained it and kept the
|
|
270
|
+
job's own file under a folder spelled with an acronym; the file is now left
|
|
271
|
+
out by the constant it declares.
|
|
272
|
+
- **A job shows the queue and Sidekiq options it inherits from the app's own
|
|
273
|
+
base classes**, its own settings winning key by key, each inherited one
|
|
274
|
+
named once with its base and its value when short. Diaspora's `Mail::*`
|
|
275
|
+
workers showed no queue where `Mail::NotifierBaseWorker` sets `queue: low`,
|
|
276
|
+
Mastodon's `Fasp::*` workers none where their base sets `fasp`, and
|
|
277
|
+
Whitehall's `PublishingApi*Job`s none where theirs sets `publishing_api`.
|
|
278
|
+
- **A job queue computed by a block or an expression says so, with its
|
|
279
|
+
source**: `queue_as -> { ... }` read `dynamic` in the booted tier and showed
|
|
280
|
+
no queue without boot.
|
|
281
|
+
- **A class is a job base only when its own chain reaches ActiveJob or a
|
|
282
|
+
Sidekiq mixin.** OpenProject's `Meetings::PDF::Common::Styles::Base`, a PDF
|
|
283
|
+
style class under `app/workers`, was listed as a job base.
|
|
284
|
+
- **A mailer kept in `app/services` is a mailer and not also a service**, and
|
|
285
|
+
a service not found is offered the constants the files declare
|
|
286
|
+
(`ActivityPub::...`, where it offered `Activitypub::...`).
|
|
287
|
+
- **A module other classes in the app include, extend or prepend is not a
|
|
288
|
+
service object**, unless it has an entry point of its own (a class method
|
|
289
|
+
other than the `included`, `extended` or `prepended` hook,
|
|
290
|
+
`module_function`, `extend self`). Whitehall's `AssetManager::ServiceHelper`
|
|
291
|
+
was listed as a service, as were 11 mixins in a private API app and 11 in
|
|
292
|
+
OpenProject; the generated `Services:` line follows.
|
|
293
|
+
- **`rails_get_service_pattern` and `rails_get_helper_methods` find a service
|
|
294
|
+
or helper by the name the app writes when its path uses a registered
|
|
295
|
+
acronym.** Mastodon's 20 `ActivityPub::...` services, in `activitypub/`,
|
|
296
|
+
answered not found and suggested `Activitypub::...`.
|
|
297
|
+
- **The generated files' `Services:` line reads the same scan
|
|
298
|
+
`rails_get_service_pattern` lists from**, so a service in a subdirectory, a
|
|
299
|
+
pack or an engine appears; it globbed `app/services/*.rb`, which gave a
|
|
300
|
+
private API app, whose services are all namespaced, no line at all. It and
|
|
301
|
+
the `Jobs:` line show 12 names and say how many more there are, in every
|
|
302
|
+
tier: the static tier dropped both with the conventions it cannot read.
|
|
303
|
+
- **A blank name given to a by-name lookup is answered as blank**, naming what
|
|
304
|
+
the tool looks up ("The concern name is blank"), in the helper, service,
|
|
305
|
+
job, mailer, component and concern tools: Discourse's
|
|
306
|
+
`rails_get_helper_methods(helper: "")` crashed on a file named `helper.rb`.
|
|
307
|
+
A string passed on the command line or in a direct call where a tool takes a
|
|
308
|
+
list (`files`, `include`, `checks`) is a one-item list, or a list when
|
|
309
|
+
comma-separated, where it failed with `undefined method 'any?'`; over MCP
|
|
310
|
+
the schema asks for the list itself.
|
|
311
|
+
- **The generated Commands section and `rails_onboard`'s Getting Started name
|
|
312
|
+
only commands the app has**, from one decision: `bin/dev` where it exists,
|
|
313
|
+
otherwise `bin/rails server`, and `bin/rails db:migrate` where the app has
|
|
314
|
+
`bin/rails` and `db/migrate`, and Getting Started builds the database from
|
|
315
|
+
the app's schema or migrations, never with `db:setup`, which also runs the
|
|
316
|
+
seeds. A private API app was told to run `bin/dev`, which it does not have.
|
|
317
|
+
- **The booted schema names a bigint column `bigint`**, as `db/schema.rb`
|
|
318
|
+
does; a private API app's `filesize` column read `integer`. A PostgreSQL
|
|
319
|
+
array column keeps its array flag in both tiers and in schema hints, its
|
|
320
|
+
default read as `[]` where the booted tier printed `{}`, a booted expression
|
|
321
|
+
index reads as a key list, as the dumps give it (`rails_migration_advisor`
|
|
322
|
+
crashed on any table that has one), and schema hints print a structure.sql
|
|
323
|
+
app's primary key as `id` (a composite one as `a, b`), where Discourse's
|
|
324
|
+
read ``(pk: `["id"]`)``.
|
|
325
|
+
- **Static routes read the route files `config.paths["config/routes.rb"]`
|
|
326
|
+
registers**, in order (an assignment, `<<`, `concat`, `Rails.root.join`, a
|
|
327
|
+
literal `Dir[...]` glob, kept inside the app), and name a mounted app built
|
|
328
|
+
by a call (`Flipper::UI.app`). OpenFoodNetwork read 69 of its 340 app
|
|
329
|
+
routes, and its Rswag, DfcProvider, Flipper UI and Sidekiq mounts were
|
|
330
|
+
missing. A file pulled in by `draw` is read inside the scope its `draw` sits
|
|
331
|
+
in, and again under each scope that draws it: Forem's `draw :api` inside
|
|
332
|
+
`namespace :api` put `/articles/search` under the web `articles` controller
|
|
333
|
+
without `/api`, and its v0 routes were dropped. `match ... via:` is one
|
|
334
|
+
route answering each verb it names (`GET|POST`, `ANY` for `via: :all`, a
|
|
335
|
+
`via:` from an enclosing scope too), where static routes dropped it. Routes
|
|
336
|
+
take the controller from `scope(controller:)` or their own `controller:`
|
|
337
|
+
(Canvas read 297 made-up controllers such as `accounts/:account_id`), routes
|
|
338
|
+
drawn through an app class (`ApiRouteSet::V1.draw(self)`) take the path and
|
|
339
|
+
name prefixes it returns (Canvas's 971 `/api/v1` routes), a path under an
|
|
340
|
+
optional scope is written as Rails normalizes it, and a route name another
|
|
341
|
+
file already took is left off, as Rails does; a singular `resource` names
|
|
342
|
+
its show route and a root route takes its `as:` as Rails does, an engine
|
|
343
|
+
lists what it could not expand and every path it is mounted at, a controller
|
|
344
|
+
filter lists engine rows like the app's own, and every route count says how
|
|
345
|
+
many more routes sit in engine tables.
|
|
346
|
+
- **The auth section's CORS flag is the api section's decision** (a
|
|
347
|
+
commented-out `cors.rb` is not configured in either), so `.ai-context.json`
|
|
348
|
+
no longer says OpenProject has no CORS beside its `rack-cors.rb`.
|
|
349
|
+
- **The locale note counts every in-repo gem, engine or plugin with
|
|
350
|
+
`config/locales`**, those without `app/` too (Discourse: 46 directories,
|
|
351
|
+
where it said 28).
|
|
352
|
+
- **The booted database and connection-pool sections list replicas**
|
|
353
|
+
(`configs_for` with `include_hidden: true`), as the static tier does.
|
|
354
|
+
- **`rails_security_scan` and `doctor` read the app's `Gemfile.lock` before
|
|
355
|
+
saying its bundle lacks brakeman.** `doctor` checks every file the tools
|
|
356
|
+
refuse, names each one it finds, fails on one never committed on purpose
|
|
357
|
+
that is not ignored and warns on the rest.
|
|
358
|
+
- **`rails_validate` and `rails_generate_test` decide whether a controller has
|
|
359
|
+
an action with one reader**, over its ancestors, included modules,
|
|
360
|
+
`define_method` names and templates at every ancestor's view prefix; the
|
|
361
|
+
route-to-action check calls an action missing only when the app's source
|
|
362
|
+
holds every ancestor and mixin, and otherwise names the unread gem or
|
|
363
|
+
ancestor and says the action is unverified; a namespaced controller is
|
|
364
|
+
checked at all, and a route to a method ending in `?` or `!` finds it
|
|
365
|
+
(Mastodon's `requests#merged?` was flagged).
|
|
366
|
+
- **An engine's root, an initializer's source and an autoload directory in the
|
|
367
|
+
context are app-relative, or the gem and version**, the gem's own
|
|
368
|
+
initializers and autoload directories are left out, and is left out rather
|
|
369
|
+
than written as an absolute path from the generating machine.
|
|
370
|
+
- **Routes an app draws into an engine (`Spree::Core::Engine.routes.draw`) are
|
|
371
|
+
listed with that engine**, under the path the app mounts it at and named
|
|
372
|
+
through its route proxy (`spree.admin_orders_path`, after the mount's `as:`
|
|
373
|
+
or the engine's `engine_name`), and are not in the app's route count on
|
|
374
|
+
either tier. Booted, the listing is each mounted engine's whole table, read
|
|
375
|
+
under its mount the way `bin/rails routes` prints it; statically, it is what
|
|
376
|
+
the app's route files draw into the engine. Controller actions, route-helper
|
|
377
|
+
validation and generated tests read these routes too.
|
|
378
|
+
`rails_get_routes(controller: "spree/admin/orders")` answers with OFN's 15
|
|
379
|
+
such routes, a short name such as `orders` lists them beside the app's own
|
|
380
|
+
matches, and the listing names the 219 it draws into Spree apart, PATCH and
|
|
381
|
+
PUT merged as in the app count.
|
|
382
|
+
- **A mounted app is listed once per app and path, with its path** (the
|
|
383
|
+
compact `Mounted:` line names each app once, with its path count, before its
|
|
384
|
+
five-name cap), when the mount sits in a `scope path: nil` or in both
|
|
385
|
+
branches of an `if`: Discourse's "Mounted Apps (4)" listed `Sidekiq::Web`
|
|
386
|
+
and `Logster::Web` twice each with no path.
|
|
387
|
+
- **`rails_get_env` and the context file's `env` section read ENV names from
|
|
388
|
+
one scan**, and a default is redacted by the source rule, so an empty one,
|
|
389
|
+
an address, a URL or a hostname prints as written where it read
|
|
390
|
+
`[FILTERED]`, which includes the ERB in `config/database.yml` and every
|
|
391
|
+
other `config/**/*.yml`, skipping `<%#` comments. Config YAML on
|
|
392
|
+
`sensitive_patterns` is read for the names in its ERB tags only, never a
|
|
393
|
+
value or a default; credentials and keys are still never opened.
|
|
394
|
+
`rails_get_env` gains 19 names on a private API app, 13 on Mastodon
|
|
395
|
+
and 9 on Huginn (its `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_SOCKET`).
|
|
396
|
+
- **A connected app with no tables yet is described as connected**, where the
|
|
397
|
+
schema note said "no DB connection, no schema.rb".
|
|
398
|
+
- **`rails_security_scan` reports brakeman's error message**, where it quoted
|
|
399
|
+
the last backtrace frame.
|
|
400
|
+
- **A commented-out Gemfile line is not a gem** wherever the Gemfile is read:
|
|
401
|
+
`rails_get_env`'s services, `rails_get_helper_methods` and the Mongoid check
|
|
402
|
+
read it through one AST reader.
|
|
403
|
+
- **A loaded engine's model count counts models**, by the rule the models
|
|
404
|
+
section uses, where it counted every `.rb` under the engine's `app/models`:
|
|
405
|
+
Active Storage read 15 and has 3, Solid Queue read 26 and has 11.
|
|
406
|
+
|
|
407
|
+
### Fixed
|
|
408
|
+
|
|
409
|
+
- **The static tier lists the filters a controller's concerns declare.** An
|
|
410
|
+
`include` adds its concern's `before_action`s where the line stands, and
|
|
411
|
+
each prints the concern it came from (`_(from Localized via
|
|
412
|
+
ApplicationController)_`), in `rails_get_controllers`,
|
|
413
|
+
`rails_analyze_feature` and the generated "Global before_actions".
|
|
414
|
+
Mastodon's `AboutController` listed none of `WebAppControllerConcern`'s
|
|
415
|
+
four filters or the `set_locale` its parent's `Localized` wraps every
|
|
416
|
+
action in, and `FollowerAccountsController` had no `set_account`. An
|
|
417
|
+
included module whose file is not in the app is named as not read.
|
|
418
|
+
- **An included module is found in any `app/*` directory**, as Rails autoloads
|
|
419
|
+
it, for a model as for a controller: Consul's `Budget::Investment` carries
|
|
420
|
+
the `after_save` its `Budget::Reclassification` declares, OpenProject's
|
|
421
|
+
`WorkPackage`, which read 0 validations and 17 unread concerns, reads its
|
|
422
|
+
`app/models/work_package/validations.rb`, and a model that includes a helper
|
|
423
|
+
from `app/helpers` no longer reports it as not read.
|
|
424
|
+
- **A partial index prints its WHERE clause.** The static tier carries the
|
|
425
|
+
`where:` of a schema.rb index, the WHERE of a structure.sql `CREATE INDEX`
|
|
426
|
+
and the `where:` a replayed migration passes, and `rails_get_schema` and
|
|
427
|
+
`.claude/rules` print it: Forem's `canonical_url` reads `[unique where
|
|
428
|
+
(published IS TRUE)]` and Discourse's `unique_index_categories_on_slug` ends
|
|
429
|
+
`where ((slug)::text <> ''::text)`, where both read as unique over the whole
|
|
430
|
+
table; a column two partial unique indexes cover shows each condition.
|
|
431
|
+
- **"Missing FK Indexes" reports a column only when an association or a
|
|
432
|
+
foreign key uses it.** Any `*_id` column whose type matched some table's
|
|
433
|
+
primary key counted, so an app with one string key reported every external
|
|
434
|
+
id: a private API app listed 111, among them `users.stripe_customer_id` and
|
|
435
|
+
integer ids other services assign. A column now counts when an association
|
|
436
|
+
names it (a `belongs_to`, a `has_many` or `has_one` from the other side, or
|
|
437
|
+
a `has_and_belongs_to_many` join table, named the way Rails names it), when
|
|
438
|
+
an `add_foreign_key` names it in schema.rb, structure.sql or the migrations,
|
|
439
|
+
or when `acts_as_tree`, `acts_as_nested_set` or `has_closure_tree` declares
|
|
440
|
+
it as the parent key. The associations are the ones the models section
|
|
441
|
+
lists, so a model's concerns and inherited bases count, and so do models
|
|
442
|
+
that subclass `ActiveRecord::Base`. Against 5.29.0: a private API app lists
|
|
443
|
+
6 where it listed 111, all six real; Mastodon 16 where it listed 20;
|
|
444
|
+
Diaspora 15 where it listed 13; OpenProject 43 where it listed 36; Discourse
|
|
445
|
+
166 where it listed 2, because its models subclass `ActiveRecord::Base` and
|
|
446
|
+
none were read before. `rails_performance_check` and the "Performance: N
|
|
447
|
+
issues detected" line in the generated files follow (135 to 30 on a private
|
|
448
|
+
API app). A `has_and_belongs_to_many` record in the models section now
|
|
449
|
+
carries a declared `join_table` and `association_foreign_key` on both tiers,
|
|
450
|
+
so a custom join table is read by name booted as well as static.
|
|
451
|
+
- **The gem's own warnings never reach the JSON-RPC stream.** While the stdio
|
|
452
|
+
transport owns stdout, including while live reload starts, a warning goes to
|
|
453
|
+
stderr rather than `Rails.logger`, which an app may have pointed at stdout.
|
|
454
|
+
- **CLAUDE.md, AGENTS.md and .cursorrules keep the blank lines between
|
|
455
|
+
sections.** The line cap split every line on newlines, and
|
|
456
|
+
`"".split("\n", -1)` is `[]`, so each blank separator vanished.
|
|
457
|
+
- **`rails_validate` no longer pins the whole app's missing foreign-key
|
|
458
|
+
indexes on one file.** At `level: "rails"`, a model file it could not map to
|
|
459
|
+
a model matched every finding of a check that filtered on a key the
|
|
460
|
+
performance introspector never emits, so that check is gone; it never fired
|
|
461
|
+
for a mapped model. The check that reports a model's own missing
|
|
462
|
+
foreign-key index is unchanged.
|
|
463
|
+
- **`rails_get_stimulus` lists HAML and Slim views under "Used in views".** It
|
|
464
|
+
only ever globbed ERB.
|
|
465
|
+
- **`multi_database.replicas` lists the `replica: true` entries of
|
|
466
|
+
`config/database.yml` when the app is not booted**, where it was always
|
|
467
|
+
empty.
|
|
468
|
+
- **A partial is a template.** `views.partials` listed any file or directory
|
|
469
|
+
under `app/views` whose name began with an underscore (an image named
|
|
470
|
+
`_logo.png` was a partial), and `rails_get_partial_interface` offered the
|
|
471
|
+
same names as available partials.
|
|
472
|
+
- **A custom middleware reports the class its file is named for.** A helper
|
|
473
|
+
class nested in it or declared beside it could answer `has_call_method` and
|
|
474
|
+
`initializes_app` in its place.
|
|
475
|
+
- **An escaped symbol reads as the characters it names.** A symbol written
|
|
476
|
+
`:"first\tname"` was reported as its raw source text, backslash and all:
|
|
477
|
+
column, index and table names, enum names, `attribute` names and types,
|
|
478
|
+
callback, association and scope names, keyword option keys and values,
|
|
479
|
+
`params.require` keys, permitted strong-params keys, `action_name ==` filter
|
|
480
|
+
conditions, CORS origins and resources, model constant arrays, route
|
|
481
|
+
`namespace` and `scope` segments, and rake task names and their arguments.
|
|
482
|
+
- **`config/database.yml` is read as YAML in the static tier.** It was scanned
|
|
483
|
+
line by line, and the scan disagreed with the file format on every shape
|
|
484
|
+
that is not the generated one: an entry sharing its adapter through `<<:
|
|
485
|
+
*default` came back with only its name, a database called `replica` (as
|
|
486
|
+
Mastodon names its read replica) was read as a config key of the one before
|
|
487
|
+
it, so the primary was labelled the replica and the app read as
|
|
488
|
+
single-database, and an env given only a `url:` listed no database at all.
|
|
489
|
+
The file is now parsed by `YAML` with aliases on, after its ERB tags are
|
|
490
|
+
replaced without being run, and the databases come out of the env hash by
|
|
491
|
+
the same rule Rails applies: every value a Hash means one database per key,
|
|
492
|
+
anything else is a single primary. An adapter the file computes in ERB is
|
|
493
|
+
reported as the literal the ERB falls back to, labelled so (`MySQL by
|
|
494
|
+
database.yml default`), or as unknown when there is none; it is never picked
|
|
495
|
+
from the Gemfile when the app bundles more than one adapter gem, as Huginn
|
|
496
|
+
does. Without a committed `config/database.yml`, the adapter is read from
|
|
497
|
+
the schema first, a structure.sql dialect and then column types only one
|
|
498
|
+
database has (`jsonb`, arrays, `hstore`; `mediumtext`), then from the app's
|
|
499
|
+
`database.yml.*` example files, then the Gemfile, so Canvas reads PostgreSQL
|
|
500
|
+
where it read `PostgreSQL or SQLite`; `postgis` reads as PostgreSQL, and
|
|
501
|
+
when nothing decides, the reason names only the files the app has. With
|
|
502
|
+
nothing to decide between them, the answer names the candidates (`MySQL or
|
|
503
|
+
SQLite`, and in `rails_get_schema` `MySQL or SQLite, database.yml does not
|
|
504
|
+
say which`). A file that is not valid YAML reports no databases instead of a
|
|
505
|
+
misreading. A boot that fails part way now falls back to the file for the
|
|
506
|
+
database and replica lists, where it reported none. By the same rule, an env
|
|
507
|
+
that mixes plain keys with a nested `secondary:` hash is one database, as
|
|
508
|
+
Rails reads it, so that nested entry is no longer reported as a replica.
|
|
509
|
+
- **An association reads the options of a `with_options` block around it.**
|
|
510
|
+
The static tier ignored the block, so `rails_get_model_details`,
|
|
511
|
+
`rails_dependency_graph` and `.ai-context.json` named classes the app does
|
|
512
|
+
not have (Mastodon's `EmailDomainBlock` parent and child read as `Parent`
|
|
513
|
+
and `Child`, `Appeal`'s reviewers as `ApprovedByAccount`), and
|
|
514
|
+
`rails_validate` reported a `has_many` inside `with_options dependent:
|
|
515
|
+
:destroy` as missing `:dependent`; Mastodon's `Account` drew 42 such
|
|
516
|
+
warnings. It follows ActiveSupport: a block without a parameter passes its
|
|
517
|
+
options to the associations inside it, a block with one (`do |assoc|
|
|
518
|
+
assoc.has_many ... end`) passes them only to the calls on that parameter,
|
|
519
|
+
and the association's own options still win over the block's.
|
|
520
|
+
- **`rails_validate` compares an escaped symbol by its real characters.** A
|
|
521
|
+
`validates`, `permit` or callback symbol written with an escape no longer
|
|
522
|
+
reads as a missing column.
|
|
523
|
+
- **`rails_get_gems` names a config file only when the app has one**, and
|
|
524
|
+
finds an initializer with a load-order prefix
|
|
525
|
+
(`config/initializers/3_omniauth.rb`, `009-omniauth.rb`). It printed
|
|
526
|
+
`config/initializers/omniauth.rb`, `config/initializers/redis.rb` and
|
|
527
|
+
`config/storage.yml` whether or not they existed.
|
|
528
|
+
- **`rails_get_service_pattern` leaves concerns out.** A module under a
|
|
529
|
+
`concerns` directory at any depth of a services root, or one that extends
|
|
530
|
+
`ActiveSupport::Concern`, was counted and listed as a service object,
|
|
531
|
+
while `rails_get_concern` already listed it as a concern; the generated
|
|
532
|
+
`Services:` line follows.
|
|
533
|
+
- **When no services directory is found, the answer names every place the scan
|
|
534
|
+
reads**, in-repo engines, plugins, modules and `extra_app_paths` included;
|
|
535
|
+
it named only `app/services`, packs and engines.
|
|
536
|
+
- **`rails_search_code` hides a generated file the same way on both
|
|
537
|
+
backends.** Without ripgrep, the Ruby fallback matched only the exact paths
|
|
538
|
+
the installer writes, so a nested `AGENTS.md`, `CLAUDE.md`, `.cursorrules`,
|
|
539
|
+
`opencode.json` or `.ai-context.json` that ripgrep hid showed up in results.
|
|
540
|
+
- **A HAML or Ruby-hash `data-controller` names its controllers.** A view
|
|
541
|
+
written `%div{ "data-controller" => "hello" }` carried no Stimulus
|
|
542
|
+
controller in `rails_get_view` or in the cross-controller composition list,
|
|
543
|
+
while the `data-controller="hello"` spelling did.
|
|
544
|
+
- **A view symlinked out of `app/views` stays out of the turbo payload.** The
|
|
545
|
+
frame and stream scans already skipped one; `permanent_elements`,
|
|
546
|
+
`turbo_drive_settings` and the Turbo Native conditionals counted it.
|
|
547
|
+
|
|
548
|
+
- **A `retry_on` or `discard_on` prints as one entry with its options**, in
|
|
549
|
+
the same words wherever a job or worker is listed, asked for by name, or
|
|
550
|
+
read from `.ai-context.json`, even when it is written across lines. The
|
|
551
|
+
payload's job and worker records carry one `retries` list in place of
|
|
552
|
+
`retry_on` and `discard_on`.
|
|
553
|
+
- **`config/sidekiq.yml` is read as YAML**, so a path named in a comment and
|
|
554
|
+
an environment key are no longer listed as queues (OpenFoodNetwork read
|
|
555
|
+
`default, mailers, config`), and a weighted queue (`[critical, 8]`) is
|
|
556
|
+
named by its queue.
|
|
557
|
+
- **A page past the end of a list says only that.** `rails_get_i18n` and
|
|
558
|
+
`rails_get_mailers` asked for such a page said the app had no locale files
|
|
559
|
+
or no mailers, beside the count that proved otherwise.
|
|
560
|
+
- **`rails-ai-context tool env_config --environment production` shows that
|
|
561
|
+
environment.** The binary took the flag as `RAILS_ENV` only, so the tool
|
|
562
|
+
named it as current and listed every environment. After the name of a tool
|
|
563
|
+
that declares an `environment` parameter, the flag is now that tool's filter
|
|
564
|
+
and leaves `RAILS_ENV` alone, so a booted app is not booted into an
|
|
565
|
+
environment it has no database for (`--environment prod` exited 1 with "The
|
|
566
|
+
`prod` database is not configured" and now answers "Did you mean
|
|
567
|
+
'production'?"); `RAILS_ENV` stays settable through the variable, or with
|
|
568
|
+
`--environment` before the command name.
|
|
569
|
+
- **The route total agrees across surfaces.** `.ai-context.json` and the
|
|
570
|
+
generated files counted one route twice where two controllers answer on
|
|
571
|
+
the same path and action (Forem's two API versions), so they read 769
|
|
572
|
+
beside `rails_get_routes`' 765 plus 3 framework routes.
|
|
573
|
+
- **A share short of the whole never prints as 100%.** Translation coverage,
|
|
574
|
+
the doctor readiness score, connection-pool use and the cache hit rate are
|
|
575
|
+
floored, so a locale missing one key reads `99.9%` where it read `100.0%`
|
|
576
|
+
beside the line naming the missing key.
|
|
577
|
+
- **An authorization library is named only when the app bundles it.** A
|
|
578
|
+
bare `app/policies` directory reads "policies in app/policies" with the
|
|
579
|
+
policy classes listed, and an `Ability` class without cancancan is named
|
|
580
|
+
as that file, where OpenProject, which bundles neither, read
|
|
581
|
+
`Auth: Pundit`.
|
|
582
|
+
- **A Brakeman finding from `rails_validate` prints under the file it
|
|
583
|
+
belongs to**, where every finding landed under the last file listed.
|
|
584
|
+
- **The ActiveStorage stack line counts models and attachments apart** (`14
|
|
585
|
+
models with 26 attachments` on a private API app, which read `26 models with
|
|
586
|
+
attachments`).
|
|
587
|
+
- **`doctor` finds the brakeman `rails_security_scan` runs**, including one
|
|
588
|
+
installed outside the app's bundle, where it said brakeman was missing
|
|
589
|
+
beside a scan that ran.
|
|
590
|
+
- **`rails_get_config`'s middleware section splits the app's own classes
|
|
591
|
+
from what it adds to the stack**, the way the generated files do, where it
|
|
592
|
+
listed every gem middleware as custom.
|
|
593
|
+
- **The static schema reads the migrations it was missing.** The replay
|
|
594
|
+
follows the files a migration requires under its own `db/`, an in-repo
|
|
595
|
+
engine's included (`require`, `require_relative`, `Dir[...]`, with
|
|
596
|
+
`__dir__`, `File.join`, `File.expand_path` and `Rails.root` read as literal
|
|
597
|
+
paths, never outside a `db/` directory) and replays every `db/migrate` the
|
|
598
|
+
repository owns, its in-repo engines' included. A `create_table` that names
|
|
599
|
+
its table through its class takes the file's name when a class declared
|
|
600
|
+
there agrees (an acronym the app inflects, such as `OAuth`, included), a
|
|
601
|
+
call to a `create_table` wrapper the migrations define is read as one, and
|
|
602
|
+
an index the migration leaves unnamed takes the name Rails gives it. A
|
|
603
|
+
reference (`t.references`, `t.belongs_to`, `add_reference`) gets the index
|
|
604
|
+
Rails gives it and a polymorphic one its type column, a column declared with
|
|
605
|
+
`index:` gets its index, an explicit `type:` gives the reference its type,
|
|
606
|
+
and `rename_column` and `rename_table` rename the indexes Rails named for
|
|
607
|
+
them. A reference's index follows the Rails version its migration names (a
|
|
608
|
+
polymorphic pair's name changed in 6.1; a 4.2 migration adds none unless
|
|
609
|
+
asked), and a column whose name is computed is left out and counted. The
|
|
610
|
+
replay reads `remove_reference`, `remove_index`, `remove_foreign_key` and
|
|
611
|
+
`remove_columns` by Rails' own lookup (a dropped column takes its indexes
|
|
612
|
+
and foreign key), applies `change_table` to the table it names, where a
|
|
613
|
+
column it added could land on the last table the file created, replays a
|
|
614
|
+
migration's private methods where `up` or `change` calls them, reads
|
|
615
|
+
`db/post_migrate`, and replays a helper from an included module when its
|
|
616
|
+
body is one schema statement, counting any other helper call (and a class
|
|
617
|
+
method on the app's own constants, such as a column dropper) in the note. A
|
|
618
|
+
positional `change_column_default :t, :col, value` is read as well as the
|
|
619
|
+
`from:`/`to:` form. `create_join_table` called on the migration object a
|
|
620
|
+
table file is given is replayed; `t.timestamps` and `add_timestamps` keep
|
|
621
|
+
`null:` and `default:`, a `create_table` block's `t.timestamps index: true`
|
|
622
|
+
gives Rails' two indexes, and timestamps allow NULL in a migration older
|
|
623
|
+
than 5.0; a proc default reads as its source, as schema.rb and structure.sql
|
|
624
|
+
give it. `rails_get_schema` prints where the answer came from at every
|
|
625
|
+
detail level, including how many names were read from files and how many
|
|
626
|
+
`create_table` calls stayed unnamed. OpenProject goes from 37 tables and 39
|
|
627
|
+
indexes to 208 and 556, and `rails_get_schema(table: "work_packages")`
|
|
628
|
+
answers where it said the table did not exist.
|
|
629
|
+
- **A route target with a leading slash is absolute**, as Rails reads it:
|
|
630
|
+
OpenProject's `controller: "/admin/settings/..."` inside its admin
|
|
631
|
+
namespaces read `admin/settings/admin/settings/...`.
|
|
632
|
+
- **`app/concerns` is read as a concerns directory**, in the root tree, packs
|
|
633
|
+
and in-repo engines, as Rails autoloads it. Huginn keeps its model concerns
|
|
634
|
+
there and read 1 concern where it has 28, and `rails_get_model_details`
|
|
635
|
+
called 8 of `Agent`'s concerns unread; `rails_get_concern(type: "other")`
|
|
636
|
+
lists them. `type:` takes every type the listing prints, in either spelling
|
|
637
|
+
(`service`, `LibStatic`), and only types that hold a concern are listed, and
|
|
638
|
+
a type the app lacks is answered with the ones it has, where the CLI dropped
|
|
639
|
+
such a value and listed every concern. A module a concern declares inside
|
|
640
|
+
itself and mixes in (`DryRunnable::Wrapper`) is looked up from the enclosing
|
|
641
|
+
namespace outward, and a mixin in an autoload root the app declares
|
|
642
|
+
(`autoload_lib`, `autoload_paths`) is read like any concern, so Discourse's
|
|
643
|
+
`RateLimiter::OnCreateRecord` adds its callbacks to `Post` and `Topic`. A
|
|
644
|
+
macro, callback or mixin inside a method of such a module applies only to
|
|
645
|
+
the classes that call that method, in their own body or through a concern's
|
|
646
|
+
`included do` block, as at runtime: every OpenProject model that includes
|
|
647
|
+
the module holding `acts_as_watchable` would otherwise show `watchers` and
|
|
648
|
+
`favorites`. The macros are read with the call's literal arguments (a
|
|
649
|
+
parameter the method reassigns or a block shadows keeps its own value),
|
|
650
|
+
taking the `if`, `unless` or `case` branch they decide (a private API app's
|
|
651
|
+
own macro called with `:picture, has_one: true` gives `has_one :picture`,
|
|
652
|
+
not a `name` pair); a declaration only a condition the literals cannot
|
|
653
|
+
decide holds is not listed or counted, and is named with its condition under
|
|
654
|
+
"Only under a condition the source does not decide" (OpenProject's
|
|
655
|
+
`WorkPackage` showed a `has_many custom_comments` it does not have). A
|
|
656
|
+
called method the reader cannot read costs only itself, named as unread; a
|
|
657
|
+
literal hash argument binds as keywords (`validates_translation :summary,
|
|
658
|
+
presence: true`) and is read through `merge`, `slice`, `except` and
|
|
659
|
+
`reject`, so Consul's translated validations read as `presence` and `length`
|
|
660
|
+
under their condition, an unused `options = {}` no longer swallows the next
|
|
661
|
+
declaration, a parameter reassigned in a block keeps its own value, and what
|
|
662
|
+
a macro method declares inside a block it evaluates on another receiver
|
|
663
|
+
(`translation_class.instance_eval { validates ... }`) is named under
|
|
664
|
+
"Declared on another class" with the condition it runs under, and not
|
|
665
|
+
counted as the model's (Consul's `Proposal` read 20 validation lines, 10 of
|
|
666
|
+
them its translation class's). What a mixin hook declares (`def
|
|
667
|
+
self.included(base)` and the like, in `class_eval`, as `base.has_many`,
|
|
668
|
+
`base.scope` or `base.delegate`, or through a helper it calls) applies to
|
|
669
|
+
every includer, each hook only for its own way of mixing in (`included` on
|
|
670
|
+
include, `prepended` on prepend, `extended` on extend). A module the app
|
|
671
|
+
mixes into `ActiveRecord::Base` or `ApplicationRecord` from an initializer,
|
|
672
|
+
`lib`, `lib_static` or a plugin's `init.rb` (a reopened class,
|
|
673
|
+
`send(:include)`, an `on_load` hook, a required file) is every model's
|
|
674
|
+
mixin, read again on each run so `watch` and the MCP server see one added
|
|
675
|
+
later, a model's own `extend` of an app module is read the same way
|
|
676
|
+
(Canvas's `resolves_root_account` gives `belongs_to :root_account`), a
|
|
677
|
+
module the model's own file declares is not reported unread (OpenProject's
|
|
678
|
+
`WorkPackage` gains `journals`, `attachments` and `custom_values`), a mixin
|
|
679
|
+
in a path gem inside the repo is read like the app's own code (Canvas's
|
|
680
|
+
`gems/broadcast_policy` and `gems/workflow`), and one from a gem installed
|
|
681
|
+
outside the repo is named as unread. One rule answers which classes mix in a
|
|
682
|
+
module, for concerns, services and join tables alike, and a subclass
|
|
683
|
+
inherits what its parent's concerns call into (OpenProject's
|
|
684
|
+
`WorkPackage::InexistentWorkPackage` lacked the `journals` its parent has).
|
|
685
|
+
- **Index hints follow how an index is used**, their clauses parted by
|
|
686
|
+
semicolons so a clause listing several columns reads as one, each clause
|
|
687
|
+
printed once. A column is `[indexed]` only when it leads an index and
|
|
688
|
+
`[unique]` only when a unique index covers it alone; one that trails another
|
|
689
|
+
key reads `[in index after x]` and one in a composite unique index `[unique
|
|
690
|
+
with <partners>]`, each partner set once. Each column of a composite unique
|
|
691
|
+
index read `[unique]` and every column of a composite index `[indexed]`.
|
|
692
|
+
`rails_performance_check` uses the same rule `rails_validate` does, so a
|
|
693
|
+
foreign key that only trails another column is reported as unindexed and a
|
|
694
|
+
polymorphic pair must lead an index together, and `rails_migration_advisor`
|
|
695
|
+
warns of a duplicate index only when the name `add_index` would generate
|
|
696
|
+
exists.
|
|
697
|
+
- **The `structure.sql` reader keeps an expression index key whole** and the
|
|
698
|
+
keys after it, and no longer reads `DESC`, `NULLS LAST` or an operator
|
|
699
|
+
class as a column: Discourse's `(COALESCE(parent_category_id,
|
|
700
|
+
'-1'::integer), name)` read as four columns with `name` lost. It also
|
|
701
|
+
reads each column's default, the way the `schema.rb` reader reports one
|
|
702
|
+
(a literal as its value, a cast string as the string, json and arrays as
|
|
703
|
+
Rails dumps them, an expression as `-> { "CURRENT_TIMESTAMP" }`, a serial
|
|
704
|
+
key's `nextval` as none): Discourse's schema had no default hints at all
|
|
705
|
+
and now has 583.
|
|
706
|
+
- **Both search backends return the same lines.** The Ruby fallback matched
|
|
707
|
+
case-insensitively, searched only a list of extensions and read
|
|
708
|
+
`excluded_paths` as path prefixes, so on a private API app one model name
|
|
709
|
+
gave 6653 lines with ripgrep and 50113 without it. It now searches every
|
|
710
|
+
non-hidden, non-binary file, case-sensitively, and reads `excluded_paths` as
|
|
711
|
+
gitignore patterns; `search_extensions` defaults to unset and, when set,
|
|
712
|
+
still narrows the fallback.
|
|
713
|
+
- **The Ruby search fallback reads what ripgrep reads, and fast.** It reads
|
|
714
|
+
the ignore files of every directory above the app (the git ones as far as
|
|
715
|
+
the nearest `.git`, a worktree's or submodule's `.git` file included),
|
|
716
|
+
matches ignore patterns case-sensitively, leaves symlinks alone, walks the
|
|
717
|
+
tree once without entering an ignored directory, and decides a binary file
|
|
718
|
+
from its first 64 KiB: 14.5 s to 2.5 s on a private API app and 27.5 s to
|
|
719
|
+
8.6 s on Canvas, with the same lines ripgrep returns. A ripgrep match in a
|
|
720
|
+
file that is not UTF-8 is reported as a match, where it came back as one
|
|
721
|
+
fake `error:0: invalid byte sequence` row.
|
|
722
|
+
- **`rails_search_code` never answers "No matches" while matches exist.** A
|
|
723
|
+
`limit` smaller than one match and its context lines held only context, so
|
|
724
|
+
the page came back empty; it now starts at the next match and says so.
|
|
725
|
+
- **A standalone install lets the app's `Gemfile.lock` win.** An app locking
|
|
726
|
+
an older `json` than the one this gem's dependencies pulled in failed to
|
|
727
|
+
boot and fell back to the static tier. Every gem the binstub activated for
|
|
728
|
+
itself now leaves `$LOAD_PATH` before `Bundler.setup` and comes back behind
|
|
729
|
+
the app's gems but ahead of Ruby's own lib directories, so a gem with a
|
|
730
|
+
default-gem twin (prism, json) never loads half from each; after a boot that
|
|
731
|
+
fails before the app's bundle is set up, the load path goes back to its
|
|
732
|
+
pre-boot order, and after one that fails later, such as an initializer that
|
|
733
|
+
raises, the app's bundle stays in front, so ActiveSupport finishes loading
|
|
734
|
+
and mcp never loads from two versions (every tool and the MCP server
|
|
735
|
+
answered with a load error there). When the app locks one of this gem's
|
|
736
|
+
dependencies at a version outside its requirement, a warning names it. A
|
|
737
|
+
standalone install no longer points at commands it does not have: `doctor`'s
|
|
738
|
+
fixes, the "to regenerate" line of the full-mode context files and the
|
|
739
|
+
`session_context` note name `rails-ai-context context`, `init` and `tool`
|
|
740
|
+
there, and `rails ai:context`, the install generator and `rails ai:tool`
|
|
741
|
+
where the app bundles the gem. `tool --list` honours `--no-boot`, and the
|
|
742
|
+
CLI no longer warns `could not restore gemspec(s): json-schema` on every
|
|
743
|
+
run: it re-registers the gemspecs it activated for itself rather than a
|
|
744
|
+
hand-kept list.
|
|
745
|
+
- **The test framework is read from the suite the app has**: Ruby spec and
|
|
746
|
+
test files, their helpers and `spec/factories`, not from whether a `spec/`
|
|
747
|
+
directory exists. With no tests anywhere, the answer says so and names the
|
|
748
|
+
framework the bundle holds (`no tests yet (rspec-rails in the bundle)`),
|
|
749
|
+
where it said the app ran rspec or minitest. Whitehall's and Plots2's
|
|
750
|
+
`spec/` hold only JavaScript tests, so minitest apps read as RSpec and
|
|
751
|
+
`rails_generate_test` wrote them an RSpec spec. `doctor` reads the same
|
|
752
|
+
answer (`minitest test suite found`, or `No test suite found` where it said
|
|
753
|
+
`No test directory found`), and an app that runs both is told to run `bundle
|
|
754
|
+
exec rspec` where it was told `rails test`.
|
|
755
|
+
- **Tests are found and written where the app keeps them.**
|
|
756
|
+
`rails_get_test_info(model:)` and `(controller:)` look in the directories
|
|
757
|
+
the app's tests live in, so Whitehall's `test/functional` and
|
|
758
|
+
`test/unit/app/models` tests are found, where the tool said "No test file
|
|
759
|
+
found" for files its own no-argument answer counted, and
|
|
760
|
+
`rails_generate_test` writes into the directory most of the app's model or
|
|
761
|
+
controller tests share, under the file-name suffix most of them use, falling
|
|
762
|
+
back to the convention when there are none, and names the subject's existing
|
|
763
|
+
test rather than writing a second one. A controller's lookup also lists the
|
|
764
|
+
system, feature, request and integration tests named for it or reaching its
|
|
765
|
+
routes, each labelled by kind (Mastodon's `AboutController` said "No test
|
|
766
|
+
file found" beside `spec/system/about_spec.rb`). A model's tests are looked
|
|
767
|
+
for only where the app keeps model tests: a private API app's
|
|
768
|
+
`rails_get_test_info(model: "User")` answered with a serializer spec sharing
|
|
769
|
+
the basename, where its model spec is `spec/models/user_model_spec.rb`. A
|
|
770
|
+
generated controller test asserts what each action answers, `create`,
|
|
771
|
+
`update` and `destroy` included, `respond_with` read by format and verb as
|
|
772
|
+
the responder answers and an early bare `return` counted as an answer (an
|
|
773
|
+
API `create` rendering `status: :created` or a `head :no_content` destroy no
|
|
774
|
+
longer gets the scaffold's redirect), read from its body and its
|
|
775
|
+
`respond_to` block for the format the test sends, the branch valid params
|
|
776
|
+
take where a save splits it (a redirect as `:redirect` with its target when
|
|
777
|
+
the test can name it, `head` as its status, a JSON or plain render with its
|
|
778
|
+
media type, a conditional or unreadable action only as not a server error,
|
|
779
|
+
with a comment saying why), where it asserted `:success` for Plots2's
|
|
780
|
+
redirecting `verify_email`. A controller under a gem's controller (Devise)
|
|
781
|
+
keeps its routed actions, unverified; RSpec controller specs include
|
|
782
|
+
`Devise::Test::ControllerHelpers` (Devise setup in `spec/support` counts), a
|
|
783
|
+
test of a Devise controller sets `devise.mapping` from Devise's route names,
|
|
784
|
+
a `Post` model gets `let(:post_record)` so the `post` request method is not
|
|
785
|
+
shadowed, a destroy test builds its record before reading its id, a redirect
|
|
786
|
+
target is asserted only when it names a route, and `rails_generate_test`
|
|
787
|
+
looks for an existing test where `rails_get_test_info` does. A namespaced
|
|
788
|
+
model is built from the controller's namespace outward (OpenFoodNetwork's
|
|
789
|
+
`LineItemsController` got a skip), an `on:` validation is checked in its
|
|
790
|
+
context and a conditioned one is skipped with its condition named, and a
|
|
791
|
+
glob segment is passed. It covers only the actions the controller has (its
|
|
792
|
+
own and inherited public actions, template-backed ones, and names a
|
|
793
|
+
`define_method` or a concern may build), naming each routed action it leaves
|
|
794
|
+
out, so a `resources` route no longer gets a `show` test for a controller
|
|
795
|
+
with no `show`. It passes every required path segment and builds its record
|
|
796
|
+
under its route parent (Whitehall's contacts under organisations raised
|
|
797
|
+
`UrlGenerationError`), leaves optional segments out, fills a custom segment
|
|
798
|
+
from the column of the same name on the record or its parent (Plots2's
|
|
799
|
+
`graph/file/:uid/:id` gets `uid: @csvfile.uid`), and skips with the segment
|
|
800
|
+
named when it cannot fill one; one written among an app's controller specs
|
|
801
|
+
is a `type: :controller` spec. A generated minitest controller test uses the
|
|
802
|
+
test class and data style (factories or fixtures) most of the app's
|
|
803
|
+
controller tests use, each test's superclass read through Prism and followed
|
|
804
|
+
through an app base class in the test tree (`::ActionController::TestCase`
|
|
805
|
+
counts), and a model's fixtures are found under its pluralized class name or
|
|
806
|
+
its `set_fixture_class` mapping (read from the test and spec helpers and
|
|
807
|
+
support files), a namespaced model's under the names Rails gives them
|
|
808
|
+
(`admin_users(:one)`, where it wrote `admin/users(:one)`): Plots2's `Node`,
|
|
809
|
+
whose table is `node`, got a TODO beside a `nodes.yml`.
|
|
810
|
+
- **`rails_get_test_info` counts the examples a test file runs, and lists
|
|
811
|
+
exactly those** (`it`, `specify`, `scenario`, `test`, `def test_*`,
|
|
812
|
+
one-liners too, never a `describe` or `context` line: a private API app's
|
|
813
|
+
`User` spec read 283 tests and runs 136), `rails_onboard` counts factory
|
|
814
|
+
definitions and the files they sit in ("203 factories in 134 files", a floor
|
|
815
|
+
marked "+" where a name is computed in a loop), and a fixtures directory
|
|
816
|
+
that also holds `file_fixture` files reads "1 YAML fixture set, 485 other
|
|
817
|
+
files" where it read "1 file".
|
|
818
|
+
- **`rails_get_test_info` renders each factory file's traits as a list item**
|
|
819
|
+
(`- **users.rb:** admin, with_posts`), where it printed a Ruby array.
|
|
820
|
+
- **The declared Ruby version falls back to `.ruby-version` and the `ruby`
|
|
821
|
+
line of `.tool-versions`** when neither the lockfile nor the Gemfile names
|
|
822
|
+
one, and the gems payload says which source answered
|
|
823
|
+
(`declared_ruby_version_source`).
|
|
824
|
+
- **The Rails version is read from `railties`** when an app bundles it without
|
|
825
|
+
the `rails` gem, as Discourse does.
|
|
826
|
+
- **`--no-boot` reads the `config/database.yml` section for `RAILS_ENV` or
|
|
827
|
+
`RACK_ENV`**, where it always read `development`.
|
|
828
|
+
- **Code in an in-repo engine, plugin or module is read like `app/`**:
|
|
829
|
+
models, controllers, views, concerns, services, jobs, mailers, helpers and
|
|
830
|
+
serializers under `plugins/*`, `modules/*` and `gems/plugins/*`. A view
|
|
831
|
+
there reaches `rails_get_view`, `rails_get_partial_interface`,
|
|
832
|
+
`rails_get_turbo_map` and the view lookups, and can be read by path
|
|
833
|
+
(OpenProject: 318 to 492 templates). A directory
|
|
834
|
+
counts when it has its own `app/` with a Rails directory in it and a
|
|
835
|
+
gemspec, a `plugin.rb` or an `engine.rb` says Ruby loads it. Discourse goes
|
|
836
|
+
from 223 to 359 models and OpenProject from 169 to 274, and
|
|
837
|
+
`rails_get_schema` stops calling a plugin's table model-less and pointing at
|
|
838
|
+
the Gemfile.
|
|
839
|
+
- **A model reads the table prefix its namespace gives it**, from a `def
|
|
840
|
+
self.table_name_prefix` on the namespace or an engine's `isolate_namespace`,
|
|
841
|
+
as Rails' `full_table_name_prefix` does; an explicit `self.table_name =`
|
|
842
|
+
still wins, read as the literal when its only interpolations are
|
|
843
|
+
`table_name_prefix` and `table_name_suffix` (the class's own, else
|
|
844
|
+
`config.active_record`'s), and a derived name takes
|
|
845
|
+
`config.active_record.table_name_prefix` and `_suffix` when nothing nearer
|
|
846
|
+
sets one. A class nested in a concrete model takes that model's singular
|
|
847
|
+
table as a prefix, as Rails' `compute_table_name` does: OpenProject's
|
|
848
|
+
`Project::Phase` reads `project_phases` where it read `phases`, and its
|
|
849
|
+
`Principal`, `User`, `Group` and four more read `users` where they read
|
|
850
|
+
`principals`. 51 of OpenFoodNetwork's models (its Spree models and their
|
|
851
|
+
calculators) read `orders` where the table is `spree_orders`, and
|
|
852
|
+
Discourse's `DiscourseRssPolling::RssFeed` read `rss_feeds`, so
|
|
853
|
+
`rails_get_schema` called their tables model-less.
|
|
854
|
+
- **`rails_get_schema` names the gem that owns a table with no model file**,
|
|
855
|
+
one line per gem the lockfile or Gemfile has (good_job, doorkeeper,
|
|
856
|
+
friendly_id, closure_tree, paper_trail, acts-as-taggable-on, pghero, the
|
|
857
|
+
Solid gems, Active Storage, Action Text, Action Mailbox, noticed,
|
|
858
|
+
delayed_job), where it listed the table as model-less. Both lists, and the
|
|
859
|
+
warning for a table with no model file, cover the whole schema at every
|
|
860
|
+
detail level, where they covered only the current page of the standard view
|
|
861
|
+
(a private API app's standard view named only one table), and `format:"json"`
|
|
862
|
+
returns them in `tables_without_model_file` and `gem_owned_tables`. A
|
|
863
|
+
`has_and_belongs_to_many` `join_table:` built from the table name prefix and
|
|
864
|
+
suffix reads as the table it names, so a join table is not called model-less
|
|
865
|
+
either.
|
|
866
|
+
- **A migration's literal `execute("DROP TABLE ...")` drops the table from the
|
|
867
|
+
replayed schema**, MySQL's backtick-quoted names included. OpenProject's
|
|
868
|
+
`meeting_contents` and `meeting_content_journals` were listed, and four
|
|
869
|
+
missing-index suggestions came with them.
|
|
870
|
+
- **Jobs are read from `app/jobs`, `app/workers` and `app/sidekiq` alike.** An
|
|
871
|
+
app with its workers in the Sidekiq 7 generator directory, or its ActiveJob
|
|
872
|
+
classes in `app/workers`, answered "No jobs found" (Whitehall has 31,
|
|
873
|
+
OpenProject 103 with its modules). A class is a worker when a Sidekiq mixin
|
|
874
|
+
is anywhere up its chain and a job when its chain reaches ActiveJob,
|
|
875
|
+
wherever it lives, so a Sidekiq job under `app/jobs` shows the queue its
|
|
876
|
+
`sidekiq_options` names rather than the default. An abstract base, one
|
|
877
|
+
another job inherits from and whose name says it is a base (`Jobs::Base`,
|
|
878
|
+
`ApplicationWorker`), is no longer counted as a job, in either tier. A job
|
|
879
|
+
is named by the constant its file declares
|
|
880
|
+
when that names its own namespace: Discourse's read `Regular::AnonymizeUser`
|
|
881
|
+
for `Jobs::AnonymizeUser`.
|
|
882
|
+
- **A mailer is listed wherever it lives and whatever it declares.** One
|
|
883
|
+
outside `app/mailers` is found by its parent chain anywhere under `app/`
|
|
884
|
+
(Canvas keeps `Mailer < ActionMailer::Base` in `app/models` and got "_No
|
|
885
|
+
mailers found._"), a chain is followed through the app's autoload roots to
|
|
886
|
+
`ActionMailer::Base` or a gem mailer, a class under `app/mailers` whose
|
|
887
|
+
chain reaches no mailer is not one (Diaspora listed twelve header builders
|
|
888
|
+
with `Actions: set_headers`), and one with no action of its own is listed
|
|
889
|
+
with the parent it takes its actions from and any class methods it has
|
|
890
|
+
(Whitehall's `MultiNotifications`, Consul's `DeviseMailer` and Diaspora's
|
|
891
|
+
`DiasporaDeviseMailer`). A mailer a gem defines is not listed as the app's,
|
|
892
|
+
and an ActionMailer setting such as `mailer_name` is not a mailer's
|
|
893
|
+
interface.
|
|
894
|
+
- **A by-name lookup answers the name it was given, or says it has no such
|
|
895
|
+
thing** and suggests the closest, by Ruby's own spell checker over full
|
|
896
|
+
names and last segments, where it offered whatever shared the first three
|
|
897
|
+
characters. `rails_get_mailers(mailer:)`,
|
|
898
|
+
`rails_get_i18n(locale:)` and `rails_get_env_config(environment:)` matched
|
|
899
|
+
substrings and answered with another thing's data under the name asked for:
|
|
900
|
+
`Mailer` got `UserMailer`'s actions, `prod` got production, and `e` got the
|
|
901
|
+
first locale by name. `rails_get_routes(controller:)` answers the
|
|
902
|
+
controller whose trailing segments the name spells, where `users` swept in
|
|
903
|
+
`admin/users/roles`, and its miss message lists at most 20 controllers.
|
|
904
|
+
`rails_get_turbo_map(controller:)` matches whole path segments, so
|
|
905
|
+
`blog_posts/` views no longer count as `posts`, and
|
|
906
|
+
`rails_get_test_info(controller:)` no longer offers a top-level request spec
|
|
907
|
+
as a namespaced controller's test. A short name two namespaces share
|
|
908
|
+
(`ReportMailer` for `Admin::ReportMailer` and `Staff::ReportMailer`) names
|
|
909
|
+
both and asks for the full one.
|
|
910
|
+
- **The booted tier leaves out a model a gem defines**, wherever the bundle is
|
|
911
|
+
installed (a private app counted `PaperTrail::Version`, 139 where the static
|
|
912
|
+
tier said 138), its Concerns list is what the model's source includes and
|
|
913
|
+
what a gem macro includes into that model alone, labelled by the macro
|
|
914
|
+
(`devise :a, :b` read statically as Devise's modules), without the modules
|
|
915
|
+
gems put into every model, a booted mailer lists only the actions a mailer
|
|
916
|
+
class defines, conventions claim model concerns only from a concerns file
|
|
917
|
+
that is a module, and `rails_onboard` names Mongoid for a Mongoid app, where
|
|
918
|
+
Errbit read "on unknown".
|
|
919
|
+
- **Neither tier lists a mailer's or a controller's action callbacks,
|
|
920
|
+
predicates or bang methods as actions**, nor a controller method that needs
|
|
921
|
+
an argument, which Rails could not dispatch. The booted tier listed what
|
|
922
|
+
`action_methods` returns; a private API app's `BaseController` listed its own
|
|
923
|
+
`before_action` target and a predicate. A controller lists the actions it
|
|
924
|
+
inherits from every app base, whatever the base is called and however its
|
|
925
|
+
name is qualified (OpenProject's
|
|
926
|
+
`CustomFields::Hierarchy::ItemsBaseController`, written inside
|
|
927
|
+
`Admin::Settings`), in its actions and its filter chain; an inherited public
|
|
928
|
+
method that no route names for the controller is a shared helper and not an
|
|
929
|
+
action (paper_trail's `info_for_paper_trail` read as an action of every EF
|
|
930
|
+
api controller), and an unrouted base controller lists only the actions its
|
|
931
|
+
subclasses are routed for, a controller under `app/controllers` no route
|
|
932
|
+
names lists only what it defines itself, a controller lists the public
|
|
933
|
+
actions an included app concern offers (Whitehall's
|
|
934
|
+
`Admin::ContactTranslationsController` lacked `create`, `update` and
|
|
935
|
+
`destroy`), and the booted tier drops a method that needs an argument and
|
|
936
|
+
settles inherited actions by the routes, as the static tier does; a routed
|
|
937
|
+
action with a template and no method is an action (OpenFoodNetwork's
|
|
938
|
+
`Admin::OrderCyclesController` lacked `incoming`, `outgoing` and
|
|
939
|
+
`checkout_options`); and a file under `app/controllers` not named
|
|
940
|
+
`*_controller.rb` is a controller only when it subclasses one, so
|
|
941
|
+
OpenProject's Grape API classes stay out.
|
|
942
|
+
- **A Stimulus controller outside `app/javascript/controllers` is found**:
|
|
943
|
+
under `app/webpacker`, `app/frontend`, `frontend`, `client`, component
|
|
944
|
+
sidecars in `app/components`, and the same homes in packs and in-repo
|
|
945
|
+
engines and plugins, named `*_controller` or `*.controller`, in JS or TS.
|
|
946
|
+
Outside the rails-new home a file counts only when it imports from
|
|
947
|
+
`@hotwired/stimulus` or `stimulus`, or extends a class that does (an app
|
|
948
|
+
file reached relatively or through the app's alias table, or an installed
|
|
949
|
+
Stimulus package), so an AngularJS `*.controller.js`, OpenProject's Angular
|
|
950
|
+
`activity-base.controller.ts` or a React `*_controller.tsx` does not. Forem
|
|
951
|
+
read 1 of its 23, OpenFoodNetwork 0 of 73, OpenProject 0 of 161.
|
|
952
|
+
- **Editing a Stimulus controller outside `app/javascript/controllers`, or a
|
|
953
|
+
job in `app/workers` or `app/sidekiq`, invalidates the cached context**, so
|
|
954
|
+
tools stop answering from stale data.
|
|
955
|
+
- **The app's architecture lists `stimulus` and `hotwire` wherever it keeps its
|
|
956
|
+
Stimulus controllers.** OpenFoodNetwork and OpenProject read as having none.
|
|
957
|
+
A React component named `*_controller.tsx` still does not count.
|
|
958
|
+
- **`rails_get_stimulus` counts a view as a use only when its markup names the
|
|
959
|
+
controller** in a `data-controller` attribute, a target, an action or a
|
|
960
|
+
`content_controller`-style helper. An i18n key or a word in prose with the
|
|
961
|
+
same text counted.
|
|
962
|
+
- **`rails_get_turbo_map` says how many Turbo Stream responses it showed of
|
|
963
|
+
how many**, and points at `detail:"full"` for the rest, where entries
|
|
964
|
+
dropped off after 15 with no sign; one declared in a controller concern is
|
|
965
|
+
named without the `Concerns::` segment the autoload root does not carry.
|
|
966
|
+
- **The generated files promise Stimulus auto-registration only where
|
|
967
|
+
stimulus-loading does it**, for controllers in `app/javascript/controllers`;
|
|
968
|
+
anywhere else they say a new controller is registered the way the
|
|
969
|
+
existing ones are.
|
|
970
|
+
- **A markup comment is not markup.** A HAML `-#` comment, with the lines
|
|
971
|
+
indented under it, and an ERB `<%# %>` comment are no longer read for
|
|
972
|
+
partials, instance variables, Stimulus identifiers, slots, fields or helper
|
|
973
|
+
calls: OpenFoodNetwork's `create_linked_variant.turbo_stream.haml` listed a
|
|
974
|
+
partial named `the`, from `-# Pre-render the form`.
|
|
975
|
+
- **A `render` call's partial is read with or without parentheses**, wherever
|
|
976
|
+
its keyword sits: `render(partial: 'x')`, `render 'x'`, `render(template:
|
|
977
|
+
'x')`, `render collection: @items, partial: 'x'`, the `:partial =>` form and
|
|
978
|
+
calls over several lines, in ERB, HAML and Slim; a JavaScript `.render(...)`
|
|
979
|
+
in an inline script and a partial name built at runtime are not read. A
|
|
980
|
+
`partial:` inside `locals:` or a component's constructor is not the call's
|
|
981
|
+
partial, and an escaped quote in an earlier argument no longer hides it; a
|
|
982
|
+
turbo frame id or stream name holding a comma inside a string is read whole,
|
|
983
|
+
and a Stimulus `static values` default holding a brace, a paren or a regex
|
|
984
|
+
literal is read whole, in source order. OpenFoodNetwork's views gained 51
|
|
985
|
+
partial references and Consul's 23.
|
|
986
|
+
- **A layout's yields are read in ERB, HAML and Slim**: the unnamed one as
|
|
987
|
+
`(main)`, `yield(:x)`, `(yield :x).presence` and `content_for?(:x)`, read
|
|
988
|
+
only where Ruby runs (ERB tags, HAML code lines and attribute hashes, Slim
|
|
989
|
+
code and attribute values), and a `content_for` that sets content, with a
|
|
990
|
+
block or a value, parenthesised or not, is not a yield. Canvas's six layouts
|
|
991
|
+
reported none.
|
|
992
|
+
- **A Stimulus action descriptor is read with its options, event filters and
|
|
993
|
+
neighbours** (`click->x#open:prevent`, `keydown.enter->`, `resize@window->`,
|
|
994
|
+
several descriptors in one `data-action`), read only from an action value (a
|
|
995
|
+
`data-action` attribute, an `action:` in a data hash, an action-named key or
|
|
996
|
+
Ruby variable such as `confirm_actions:` or `hiddenFieldAction:`), so an
|
|
997
|
+
apostrophe in page text no longer hides the actions after it, and prose, a
|
|
998
|
+
Ruby comment or a Rails route list (`%w[posts#create]`) confirms nothing,
|
|
999
|
+
where OpenFoodNetwork's `bulk-form` and `modal` uses were missed.
|
|
1000
|
+
- **`url_for(controller: "x")` is not a Stimulus controller** in view answers.
|
|
1001
|
+
- **`data-turbo` counts read the HAML and Ruby-hash spellings**
|
|
1002
|
+
(`"data-turbo": "false"`, `'data-turbo' => 'false'`), not only
|
|
1003
|
+
`data-turbo="false"`.
|
|
1004
|
+
- **A model whose `inclusion: { in: [...] }` list mixes strings with `nil` no
|
|
1005
|
+
longer fails** `rails_get_model_details` and `rails_context` with
|
|
1006
|
+
`ArgumentError: comparison of String with nil failed`. Whitehall's `Edition`
|
|
1007
|
+
and its 13 STI subclasses failed.
|
|
1008
|
+
- **A validation or callback inside `with_options` carries the block's
|
|
1009
|
+
options**, so a validation scoped `on: :publish` no longer reads as always
|
|
1010
|
+
required, and a macro sent to the block's parameter is kept.
|
|
1011
|
+
- **A scope written `scope :x, lambda { ... }` or with a `do ... end` block
|
|
1012
|
+
shows its body** and is marked verified where it was empty and `[INFERRED]`.
|
|
1013
|
+
Every scope body prints on one line, folded the way Ruby's lexer reads its
|
|
1014
|
+
newlines, a string literal written across lines respelt as the same string
|
|
1015
|
+
on one line: statements are separated by `;`, a call split at the dot joins
|
|
1016
|
+
back, and a comment inside it is left out. A body carrying a heredoc prints
|
|
1017
|
+
as the file wrote it.
|
|
1018
|
+
- **A Mongoid app counts `embeds_many` and `embeds_one` as associations
|
|
1019
|
+
everywhere, and lists only documents as models.** `rails_dependency_graph`
|
|
1020
|
+
draws their edges, and Errbit's `App` read "1 association" beside 5 edges; a
|
|
1021
|
+
plain Ruby class under `app/models` (Errbit's `ErrorReport` and `Issue`) is
|
|
1022
|
+
not a model.
|
|
1023
|
+
- **Both tiers list every declared validation the same way, with its
|
|
1024
|
+
conditions**: `validates_with`, `validates_date`, `validates_translation`
|
|
1025
|
+
and the rest, each with its `if:`, `unless:` and `on:`, and the presence
|
|
1026
|
+
validation Rails adds for a required `belongs_to`, marked `_(implicit from
|
|
1027
|
+
belongs_to)_` by `optional:`, `required:`, `belongs_to_required_by_default`
|
|
1028
|
+
(read from `config/application.rb` and every initializer) and
|
|
1029
|
+
`load_defaults`; an `optional:` written as an expression prints it and makes
|
|
1030
|
+
the implicit presence conditional on it, and a hand-written presence rule on
|
|
1031
|
+
the same name shows in its place. A validator no source line declares is
|
|
1032
|
+
listed too: the booted tier adds each reflection validator no line covers,
|
|
1033
|
+
marked by what added it (`has_secure_password`, `devise :validatable`, a gem
|
|
1034
|
+
module), and the static tier reads `has_secure_password` and `devise
|
|
1035
|
+
:validatable` as the validators they add, `has_secure_password`'s by the
|
|
1036
|
+
Rails version `Gemfile.lock` names (`length` and `confirmation` before 7.1,
|
|
1037
|
+
`confirmation` from 7.1, and an assumption named when the lockfile says
|
|
1038
|
+
none), so a private API app's `User` shows its password `confirmation`. A
|
|
1039
|
+
validation whose attribute is computed, as in a loop, says so, and a custom
|
|
1040
|
+
`validate :method` prints its `on:`, `if:` or `unless:` condition in both
|
|
1041
|
+
tiers. A private API app's booted `Document` dropped its `if:` conditions
|
|
1042
|
+
and printed its `validates_with` line as `document on`, the static tier
|
|
1043
|
+
left `validates_with` out (Plots2's `User` read 0 validations), and a
|
|
1044
|
+
hand-written presence rule on a `belongs_to` name was labelled implicit.
|
|
1045
|
+
- **An inheritance column set to `nil`, `false` or `:_type_disabled` reads as
|
|
1046
|
+
STI off**, where the conventions claimed STI. A validator option set to
|
|
1047
|
+
`false` (`presence: false`) is no validation, in both tiers; a class's own
|
|
1048
|
+
`belongs_to_required_by_default` beats the app's and passes to its
|
|
1049
|
+
subclasses (OpenFoodNetwork's `Subscription` showed 7 implicit presence
|
|
1050
|
+
lines); a computed enum prints its source marked computed, and a frozen
|
|
1051
|
+
literal one its values; and one `after_commit` declared for several events
|
|
1052
|
+
is one callback showing its events.
|
|
1053
|
+
- **`rails_validate` judges only the `has_many` associations the model's file
|
|
1054
|
+
declares**, and treats encrypted attributes (`has_encrypted`, `encrypts`,
|
|
1055
|
+
`attr_encrypted`) and store keys as attributes with no column of their own,
|
|
1056
|
+
so it no longer asks for `dependent:` on paper_trail's `has_many :versions`.
|
|
1057
|
+
- **`rails_dependency_graph` resolves an association's class from the owner's
|
|
1058
|
+
namespace outward, as Rails' `compute_type` does**, a leading `::` meaning
|
|
1059
|
+
top level (kept as written in both tiers), and the missing FK index check,
|
|
1060
|
+
both habtm join-table readers and the counter-cache check resolve it the
|
|
1061
|
+
same way; every tool that derives a model from a name spells it as the app
|
|
1062
|
+
does, acronyms included (`analyze_feature` named models that do not exist);
|
|
1063
|
+
two associations of one kind to one class each get an arrow labelled by
|
|
1064
|
+
name; and a through association that names no association of its model, or
|
|
1065
|
+
whose name is computed at run time, draws no node and is named in a note
|
|
1066
|
+
saying which: Consul's `Budget` pointed at a top-level `Investment`,
|
|
1067
|
+
`Ballot`, `Group` and `Heading`, and OpenFoodNetwork's `Spree::Order` at
|
|
1068
|
+
`Payment`, `Shipment` and `Adjustment`. It draws a through edge per join
|
|
1069
|
+
model, where Mastodon's `Account` lost its through `Follow`, `Block` and
|
|
1070
|
+
`Mute` edges behind the first one; a through association with `source_type:`
|
|
1071
|
+
points at the class `source_type` names; and the "pass model:" hint is left
|
|
1072
|
+
out when a model was passed.
|
|
1073
|
+
- **A block callback a concern declares is kept beside the model's own block
|
|
1074
|
+
callback of the same kind**: Discourse's `Topic` lost the `after_create do`
|
|
1075
|
+
of `RateLimiter::OnCreateRecord`.
|
|
1076
|
+
- **A static route honours the action its `action:` option names** (`get
|
|
1077
|
+
:delete, action: :deletion_dialog` read as `delete`).
|
|
1078
|
+
- **A component is typed by following its superclass chain through the app's
|
|
1079
|
+
own base classes**, so one two or more steps from `ViewComponent::Base` or a
|
|
1080
|
+
Phlex base is no longer "unknown", and a sidecar file that reopens its parent
|
|
1081
|
+
namespace is named for the class the file is named for. A component nested
|
|
1082
|
+
in a module is listed by its full constant (`Admin::RowComponent`, where
|
|
1083
|
+
three different classes all read `RowComponent`), which renames most of
|
|
1084
|
+
OpenProject's catalog.
|
|
1085
|
+
- **A concern that nests its own `ActiveModel::Validator` is a concern**, not
|
|
1086
|
+
a validator, and every surface that lists concerns or helpers names each one
|
|
1087
|
+
by the constant its file declares, in-repo plugins included (Discourse's
|
|
1088
|
+
`DiscourseSubscriptions::Group` read `Group`, and
|
|
1089
|
+
`rails_get_helper_methods(helper: "DiscourseChatIntegration::Helper")`
|
|
1090
|
+
answered not found): `rails_get_active_support` read `Featurable` for
|
|
1091
|
+
`Edition::Featurable`, and an inflected namespace (`SDG::TagList` in
|
|
1092
|
+
`concerns/sdg/tag_list.rb`) is no longer spelled `Sdg::TagList`.
|
|
1093
|
+
- **A module that extends `ActiveSupport::Concern` is a concern wherever the
|
|
1094
|
+
app autoloads it from**, and the default `excluded_concerns` patterns each
|
|
1095
|
+
match a whole namespace, so an app module named only like a framework's
|
|
1096
|
+
(Whitehall's `ActiveRecordLikeInterface`) is listed, not hidden; listed with
|
|
1097
|
+
its path under a type naming its root (`Service Concerns`), in
|
|
1098
|
+
`rails_get_concern` and `rails_get_active_support` alike: OpenProject read
|
|
1099
|
+
82 concerns and has 329, Canvas 9 and 26, a private API app none and 2.
|
|
1100
|
+
- **A concern's includes and methods are its own body's**, not those of
|
|
1101
|
+
classes nested in its file: Huginn's `LiquidDroppable` read `Includes:
|
|
1102
|
+
Enumerable` and its nested `Drop` classes' `to_s`, `each` and `as_json`, and
|
|
1103
|
+
`Agents::ImapFolderAgent` was credited with `Scrubbed`. A concern whose
|
|
1104
|
+
methods are all private reads `0 public methods (N private)` and lists them,
|
|
1105
|
+
where it read `0 methods`.
|
|
1106
|
+
- **A concern's "Included By" lists the classes whose `include` resolves to
|
|
1107
|
+
it** from their own namespace, each once, named by its constant, searching
|
|
1108
|
+
every autoload root and naming them when it finds none: Mastodon's
|
|
1109
|
+
`Admin::ExportControllerConcern` listed 9 includers, 7 of them including
|
|
1110
|
+
`Settings::ExportControllerConcern`, and Discourse listed
|
|
1111
|
+
`SubscriptionsController` twice.
|
|
1112
|
+
- **Component previews are read from the directories the config sets**
|
|
1113
|
+
(`view_component.previews.paths`, `preview_paths`, `preview_path`) as well
|
|
1114
|
+
as the defaults, and a preview links to its component by path or by the one
|
|
1115
|
+
component it renders: OpenProject read no component with a preview, and 39
|
|
1116
|
+
have one.
|
|
1117
|
+
- **A boot error no longer repeats in full under every answer.** The footer
|
|
1118
|
+
and each `[UNAVAILABLE]` note keep its first 140 characters, and the whole
|
|
1119
|
+
error stays in the startup banner and `doctor`. On Discourse it was 61% of
|
|
1120
|
+
the output.
|
|
1121
|
+
- **`rails_get_view` does not count a partial under `app/views/layouts` as a
|
|
1122
|
+
layout.** Forem read 19 layouts where it has 4.
|
|
1123
|
+
- **An abstract base is not counted as a service, a job or a mailer.** A class
|
|
1124
|
+
other classes inherit from and whose name says it is a base
|
|
1125
|
+
(`ApplicationService`, `BaseService`, `Jobs::Base`, `ApplicationMailer`)
|
|
1126
|
+
leaves `rails_get_service_pattern`, `rails_get_job_pattern`,
|
|
1127
|
+
`rails_get_mailers` and the generated files by one rule in both tiers, and
|
|
1128
|
+
one sentence above each listing names what it left out; a base answers when
|
|
1129
|
+
asked for by name with what it hands down, printed without the comments
|
|
1130
|
+
written inside a call and never counting a call inside a method as a
|
|
1131
|
+
declaration: a job base its queue, options, retry policy, mixins, throttle
|
|
1132
|
+
and callbacks, a mailer base its layout, helpers, defaults, callbacks and
|
|
1133
|
+
mixins as the file writes them, and either one the methods it defines and
|
|
1134
|
+
the classes that inherit it. Mastodon's generated files listed `BaseService`
|
|
1135
|
+
as a service, and Consul's and OpenProject's `ApplicationMailer` helpers
|
|
1136
|
+
read as emails.
|
|
1137
|
+
- **A class file is named by the constant it declares.** A model, job or
|
|
1138
|
+
service whose file declares a namespaced constant at a path that does not
|
|
1139
|
+
spell it is listed under that constant, where it was dropped or named from
|
|
1140
|
+
the path: Discourse's `DiscourseGithubPlugin::GithubCommit` and six other
|
|
1141
|
+
plugin models were missing, and `Jobs::DiscourseChatAddTypeField` read
|
|
1142
|
+
`Onceoff::AddTypeField`.
|
|
1143
|
+
- **A bare superclass resolves from the enclosing namespace outward**, as
|
|
1144
|
+
Ruby resolves it, so `module Bim::Bcf; class NonExistentComment < Comment`
|
|
1145
|
+
inherits `Bim::Bcf::Comment`, and a class that shares a name with a model in
|
|
1146
|
+
another namespace is no longer counted as a model.
|
|
1147
|
+
- **What a job or service hands work to is read from the call nodes**, so
|
|
1148
|
+
`.run`, `.perform_now`, any bang method (`GithubBadges.grant!`), a receiver
|
|
1149
|
+
written `::Fully::Qualified` and a call split across lines are listed under
|
|
1150
|
+
its calls, and a call written in a comment or a string is not.
|
|
1151
|
+
- **`rails_get_api` reports a CORS initializer whose origins come from a block
|
|
1152
|
+
or an expression**, where it said "not detected", and says when the block is
|
|
1153
|
+
exactly the request `Origin` header echoed back, which allows any origin. A
|
|
1154
|
+
`resource` path that is not a literal prints as its source. A CORS
|
|
1155
|
+
initializer with no `allow` block is reported with the middleware it
|
|
1156
|
+
inserts, or as a file whose `Rack::Cors` or `allow` block is commented out;
|
|
1157
|
+
a file that only has `cors` in its name is not a CORS initializer. An `else`
|
|
1158
|
+
branch reads "otherwise" and each `elsif` keeps its own condition, where a
|
|
1159
|
+
private API app's second `allow` block read `* if else Rails.env.staging?`.
|
|
1160
|
+
- **CORS and Rack::Attack are found under the initializer name the app gave
|
|
1161
|
+
them** (`rack-attack.rb`, `rack-cors.rb`, `008-rack-cors.rb`), where only
|
|
1162
|
+
`cors.rb` and `rack_attack.rb` were read.
|
|
1163
|
+
- **A notable gem's note names a config file only when the app has it.** It
|
|
1164
|
+
points at the file the app does have, or drops the clause naming the path
|
|
1165
|
+
and keeps the description; it named `config/initializers/cors.rb`,
|
|
1166
|
+
`app/assets/config/manifest.js` and `app/javascript/controllers/` on apps
|
|
1167
|
+
without them.
|
|
1168
|
+
- **`rails_get_api` reads Grape versions under `app/api/v*` and `lib/api/v*`**
|
|
1169
|
+
and names the directories each version came from.
|
|
1170
|
+
- **An ActiveRecord attribute coder in a `serializers/` directory is not
|
|
1171
|
+
reported as the app's serialization layer.**
|
|
1172
|
+
- **GraphQL counts leave out base classes**, judged from the class the file
|
|
1173
|
+
is named for rather than from a `base_` file name: a class that declares
|
|
1174
|
+
`graphql_name` is concrete, and a base is one others inherit from, or one
|
|
1175
|
+
named like a base that subclasses graphql-ruby and adds nothing to the
|
|
1176
|
+
schema. The enums, inputs, scalars and connections that subclass
|
|
1177
|
+
graphql-ruby directly still count (Canvas: 176 types, 102 mutations). An
|
|
1178
|
+
app with no `queries/` directory is pointed at its query root file rather
|
|
1179
|
+
than told "0 queries".
|
|
1180
|
+
- **Middleware changes are read wherever the app makes them**: through
|
|
1181
|
+
`Rails.configuration.middleware`, `Rails.application.middleware` and
|
|
1182
|
+
`app.middleware`, the app's own `MyApp::Application.config.middleware` and
|
|
1183
|
+
`config.middleware` (an engine's own stack, `MyEngine.config.middleware`, is
|
|
1184
|
+
left alone), with `insert`, `insert_before`, `insert_after`, `unshift`,
|
|
1185
|
+
`swap`, `move_before`, `move_after` and `delete`, in `config/application.rb`
|
|
1186
|
+
and `config/environments/*.rb` as well as the initializers. A class the app
|
|
1187
|
+
owns is listed with its file, under the name its file declares, wherever the
|
|
1188
|
+
app autoloads it from (`lib/middleware`, `app/lib/middlewares`) or wherever
|
|
1189
|
+
its config or initializers declare it with a `call` method (Mastodon's
|
|
1190
|
+
`TelemetryLoggingMiddleware`, Discourse's `Discourse::Cors`);
|
|
1191
|
+
`config.exceptions_app` is named apart from the stack; one it does not own
|
|
1192
|
+
is listed under "Stack changes from the app's config" as inserted, moved,
|
|
1193
|
+
swapped in or removed, each with the file that does it. Discourse read no
|
|
1194
|
+
custom middleware and 1 insertion; it has 16 classes and 27 stack changes
|
|
1195
|
+
(19 inserted, 2 swapped in, 1 moved, 5 removed).
|
|
1196
|
+
- **The autoload roots include the ones `config/application.rb` adds**
|
|
1197
|
+
(`autoload_paths`, `eager_load_paths`, `autoload_lib`, `config.paths.add`),
|
|
1198
|
+
when they are written as literal paths inside the app, so a class there is
|
|
1199
|
+
found as the app's own. OpenProject keeps middleware in `lib_static`.
|
|
1200
|
+
|
|
1201
|
+
### Removed
|
|
1202
|
+
|
|
1203
|
+
- **Internal helpers nothing in the gem called**: `DetailLevel.at_least?`,
|
|
1204
|
+
`SchemaHint#verified?`, `#column_names` and `#association_names`,
|
|
1205
|
+
`Fingerprinter.watched_files`, `Payload.mailer_file`,
|
|
1206
|
+
`Payload.rails_engines` and `Payload.databases`, `SafePath::REFUSALS`,
|
|
1207
|
+
`ViewFile::Result` (a view lookup returns `SafePath::Resolution`, which has
|
|
1208
|
+
the same fields), `Introspectors::ListenerRegistration.register`,
|
|
1209
|
+
`Listeners::MongoidFieldsListener` (the generic macro listener reads
|
|
1210
|
+
Mongoid's macros), and `StackOverviewHelper#database_adapter_label`. None
|
|
1211
|
+
was documented.
|
|
1212
|
+
|
|
1213
|
+
### Security
|
|
1214
|
+
|
|
1215
|
+
- **Secret files a Figaro, direnv or config-gem app keeps are refused like the
|
|
1216
|
+
rest.** `config/application.yml`, `.envrc`, `config/settings.local.yml`,
|
|
1217
|
+
`config/settings/*.local.yml`, `config/secrets*.yml`,
|
|
1218
|
+
`config/secrets*.yml.enc`, `*.p8` and `*.env` (a compose `env_file:` such as
|
|
1219
|
+
`docker/secrets.env`) join the sensitive-file list, and `rails_search_code`
|
|
1220
|
+
refuses one given as its `path` (a file path is now taken, as ripgrep takes
|
|
1221
|
+
it), so `rails_get_edit_context` and `rails_search_code` no longer return
|
|
1222
|
+
the values in them. On a private API app both returned JWT signing secrets
|
|
1223
|
+
from `config/application.yml`.
|
|
1224
|
+
- **The Ruby search fallback skips what ripgrep skips**: sensitive files, and
|
|
1225
|
+
whatever `.ignore` and `.rgignore` files ignore, and inside a git repository
|
|
1226
|
+
whatever the root and nested `.gitignore` files, `.git/info/exclude` and the
|
|
1227
|
+
global excludes file ignore, with ripgrep's precedence: any `.rgignore` over
|
|
1228
|
+
any `.ignore` over any `.gitignore`, then `.git/info/exclude`, then the
|
|
1229
|
+
global excludes file, the deepest file deciding only within one type, and a
|
|
1230
|
+
directory they ignore never entered. Without ripgrep on the PATH the
|
|
1231
|
+
fallback returned lines from gitignored secret files; it now returns
|
|
1232
|
+
ripgrep's rows, context lines and line cap, matching each line without its
|
|
1233
|
+
newline.
|
|
1234
|
+
- **Every line `rails_search_code` and `rails_get_edit_context` return passes
|
|
1235
|
+
through redaction, and redaction filters secrets, never code.** A string
|
|
1236
|
+
literal of eight or more characters under a secret-named key (Ruby, YAML,
|
|
1237
|
+
JSON written `"key":"value"`, or a setter on any receiver such as
|
|
1238
|
+
`config.secret_key_base = '...'`), four or more under a password-type key
|
|
1239
|
+
(`password`, `passwd`, `pass`, `pin`, `passphrase`), letters-only and
|
|
1240
|
+
digits-only ones included, and the unquoted value of a secret-named
|
|
1241
|
+
`NAME=value` in any file (shell, Dockerfile, compose, env or code; inside a
|
|
1242
|
+
string or URL only the value is replaced, and a setter symbol, a regex
|
|
1243
|
+
lookbehind or an interpolation is left alone), a value in an example file
|
|
1244
|
+
(`*.example`, `*.sample`, `*.template`, and YAML ones like `*.yml.example`)
|
|
1245
|
+
unless it is plainly a placeholder, a cipher key name (`cbc_key`,
|
|
1246
|
+
`hmac_key`) counted as secret-named, a credential-looking value under any
|
|
1247
|
+
other `*_KEY` name (a 16+ character run mixing letters and digits, or a
|
|
1248
|
+
known token format, never a cache, i18n or storage key), a literal assigned
|
|
1249
|
+
through `ENV["NAME"] =`, `||=` or another op-assign or held by an instance
|
|
1250
|
+
variable or constant, a literal passed through a call or block that names a
|
|
1251
|
+
secret (`ENV.fetch` default, `let(:api_key) { }`, `default:` under a secret
|
|
1252
|
+
option), never a ternary's branches, a PGP private key block, Anthropic and
|
|
1253
|
+
OpenAI keys, unless it is plainly not a credential (a message, URL, path,
|
|
1254
|
+
placeholder, version, date, header, env or parameter name, or a
|
|
1255
|
+
translation), and a value in a known credential format (AWS keys, JWTs,
|
|
1256
|
+
vendor tokens) read `[FILTERED]` in any file; a PEM private key is filtered
|
|
1257
|
+
whole, across lines too and with armor headers before its body, with the
|
|
1258
|
+
line count kept, while a BEGIN marker named in a regex or string with no key
|
|
1259
|
+
body after it is code. `rails_search_code` shows a match that falls only
|
|
1260
|
+
inside a filtered value as the context row a wrong guess would show, so a
|
|
1261
|
+
pattern cannot confirm a secret's prefix. Log lines read `NAME=value` with
|
|
1262
|
+
the source pattern and decide each name and value pair once; they also
|
|
1263
|
+
filter a connection URL's password and a PEM body on the lines after its
|
|
1264
|
+
marker. Both tools redact the whole file before they cut a window or a row,
|
|
1265
|
+
so a window or a context line inside a key still hides it. Code is returned
|
|
1266
|
+
as written: a call that passes a token along (`issue(token:
|
|
1267
|
+
TokenService.run(user: @user).result)`) or reads one (`password:
|
|
1268
|
+
params[:password]`) is never rewritten. A `password_confirmation` literal is
|
|
1269
|
+
filtered like the password, in logs and config too, and log redaction keeps
|
|
1270
|
+
the quotes and the variable name around `[FILTERED]`.
|
|
1271
|
+
|
|
1272
|
+
## [5.29.0] - 2026-09-23
|
|
1273
|
+
|
|
1274
|
+
Twenty-three QA reports against v5.27.0, each one a wrong answer a reader
|
|
1275
|
+
could act on, plus what eight review rounds found in the fixes themselves.
|
|
1276
|
+
|
|
1277
|
+
### Added
|
|
1278
|
+
|
|
1279
|
+
- **Model payloads carry method counts and the model's own methods
|
|
1280
|
+
uncapped.** `instance_method_count`, `class_method_count` and
|
|
1281
|
+
`source_instance_methods` sit beside the capped `instance_methods` and
|
|
1282
|
+
`class_methods` lists, on both tiers, so a reader can tell a cut list from
|
|
1283
|
+
a whole one.
|
|
1284
|
+
- **The schema payload carries `declared_tables`**, what `db/schema.rb`
|
|
1285
|
+
declares, on both tiers: nil for an app whose tables come from
|
|
1286
|
+
`structure.sql` or the migrations.
|
|
1287
|
+
|
|
1288
|
+
### Changed
|
|
1289
|
+
|
|
1290
|
+
- **"Mounted Engines" is "Mounted Apps" everywhere it is printed**: the
|
|
1291
|
+
`engines`, `routes` and `onboard` tools, the generated context files, and
|
|
1292
|
+
the MCP `rails://engines` resource name. The stack overview line reads
|
|
1293
|
+
`Mounted:` rather than `Engines:`. Half of what the list holds are plain
|
|
1294
|
+
Rack apps. The payload key stays `mounted_engines`.
|
|
1295
|
+
- **A mount whose path the source does not spell out carries `path: nil`**
|
|
1296
|
+
rather than the string `"unknown"`, and is printed without a path.
|
|
1297
|
+
- **`routes` reads a blank `controller` as no filter**, and a name that
|
|
1298
|
+
normalizes to nothing (`_controller`) as matching nothing, where the empty
|
|
1299
|
+
string used to match every route key.
|
|
1300
|
+
|
|
1301
|
+
### Fixed
|
|
1302
|
+
|
|
1303
|
+
- **A Sidekiq worker answers to its own name, and the bracket carries the
|
|
1304
|
+
limit that governs it.** `job:"Billing::Invoices::CreateWorker"` answered
|
|
1305
|
+
"No jobs found" for a worker the same tool had just listed, because the
|
|
1306
|
+
single-job lookup read the ActiveJob list only. It reads both lists now, and
|
|
1307
|
+
a name in neither is a not-found that names what exists. The listing prints
|
|
1308
|
+
its "workers the introspector did not see" caveat whenever it prints
|
|
1309
|
+
workers, rather than only when `config/sidekiq.yml` happens to exist, and a
|
|
1310
|
+
`sidekiq_throttle` prints under the worker it throttles - 438 of 522 workers
|
|
1311
|
+
on one app declared one and none of them showed it.
|
|
1312
|
+
- **onboard's async section reads the workers out of the hash it already
|
|
1313
|
+
had.** The section counted jobs, mailers and channels, so an app whose
|
|
1314
|
+
background work is 522 Sidekiq workers read as "4 mailers." and the section
|
|
1315
|
+
disappeared entirely when workers were the only async code. The "not
|
|
1316
|
+
covered" line no longer prints next to a worker list it contradicts.
|
|
1317
|
+
- **What an ActiveInteraction declares is read in one place.** A filter
|
|
1318
|
+
declared inside another filter's block (`string :title` inside `hash
|
|
1319
|
+
:order_params do`) is a key of that hash, not an input of the class:
|
|
1320
|
+
`service_pattern` showed four inputs where `.filters` has two, and
|
|
1321
|
+
`generate_test` passed the other two to `.run`, which drops them silently. A
|
|
1322
|
+
subclass of the app's own base interaction is an interaction too, with its
|
|
1323
|
+
parent's filters first, so `generate_test` stops emitting `.call`, which
|
|
1324
|
+
`ActiveInteraction::Base` does not define. `GenericMacroListener` records
|
|
1325
|
+
each call's own offset and its enclosing call's, which is what both tools
|
|
1326
|
+
pair filters by.
|
|
1327
|
+
- **generate_test names the constant the file declares.** A path camelizes
|
|
1328
|
+
through Ruby's inflector, which has not read the app's
|
|
1329
|
+
`config/initializers/inflections.rb` on the static tier, so
|
|
1330
|
+
`ai_reports/build.rb` gave `AiReports::Build` where the app defines
|
|
1331
|
+
`AIReports::Build` - a constant nothing defines, in a spec that dies on
|
|
1332
|
+
load.
|
|
1333
|
+
- **service_pattern looks for callers where the app keeps code.** The scan
|
|
1334
|
+
named six `app/` directories, so a caller in `app/tools` or under `lib/` was
|
|
1335
|
+
invisible. It reads every `app/` and `lib/` tree, plus whatever else a
|
|
1336
|
+
booted app autoloads from, and says when the twenty-entry cap or its own
|
|
1337
|
+
file ceiling left the list partial.
|
|
1338
|
+
- **schema tells a missing migration from a typo.** Booted, a table declared
|
|
1339
|
+
in `db/schema.rb` and absent from the connected database answered "Did you
|
|
1340
|
+
mean 'comments'?". The payload carries the declared tables beside the live
|
|
1341
|
+
ones, so the answer names the migration that has not run, and the listing
|
|
1342
|
+
header says the two counts disagree instead of pairing a live table count
|
|
1343
|
+
with the file's version stamp.
|
|
1344
|
+
- **A validator under app/models/concerns is not a concern.** The type came
|
|
1345
|
+
from the directory alone, so 37 `ActiveModel::Validator` subclasses on one
|
|
1346
|
+
app were listed as model concerns used by nothing. They are listed as
|
|
1347
|
+
validators - following the app's own validator base class, not one level of
|
|
1348
|
+
compare - and looked up by the `validates_with`, or the validation option,
|
|
1349
|
+
that wires them, in a model or in a concern's `included` block. The keys
|
|
1350
|
+
`validates` reads for itself (`on:`, `if:` and the like) name no
|
|
1351
|
+
validator.
|
|
1352
|
+
- **dependency_graph counts both header numbers over the same models.** The
|
|
1353
|
+
model count was app-wide and the association count covered the fifty nodes
|
|
1354
|
+
that survived the cap. Both are app-wide now, and the truncation note says
|
|
1355
|
+
how many of the associations the cut graph draws.
|
|
1356
|
+
- **get_context reads the views Rails would resolve.** It handed `GetView`
|
|
1357
|
+
the last segment of the controller path, so `Api::V1::Admin::OrdersController`
|
|
1358
|
+
picked up `app/views/orders`, a directory of templates a background service
|
|
1359
|
+
renders. The answer names the directory its views came from, and a
|
|
1360
|
+
flat-directory fallback is labelled as one.
|
|
1361
|
+
- **analyze_feature finds a test by its path.** A spec whose feature word is a
|
|
1362
|
+
directory (`spec/services/billing/invoices/create_spec.rb`) was dropped,
|
|
1363
|
+
while the gap checker beside it already matched on the path. The suite's
|
|
1364
|
+
own words stay out of the match - the `spec/` root, the type directory it
|
|
1365
|
+
files a test under (`models/`, `requests/`), and the `_spec` suffix - so
|
|
1366
|
+
`--feature models` is not every model spec.
|
|
1367
|
+
- **routes answers an exact controller key with its own routes.** A substring
|
|
1368
|
+
filter returned a nested sibling's routes too (`api/v1/admin/orders` swept
|
|
1369
|
+
in `api/v1/admin/orders/ai_data`), and `get_context` inherited it. A short
|
|
1370
|
+
name still matches every controller that carries it.
|
|
1371
|
+
- **A template at the root of app/views is listed.** Its filename became a
|
|
1372
|
+
directory group that matched nothing, so the header counted a file the body
|
|
1373
|
+
never printed, and the controller-miss hint suggested a directory that does
|
|
1374
|
+
not exist.
|
|
1375
|
+
- **A word in a quoted string is not an instance variable.** `view` reported
|
|
1376
|
+
a chat handle inside a Ruby string literal as a template's ivar; the reader
|
|
1377
|
+
strips string literals and keeps interpolation, in ERB tags and in the
|
|
1378
|
+
Ruby template handlers (Jbuilder, Builder, `.ruby`), and `get_view`'s
|
|
1379
|
+
hydrator reads through the same method. Whichever quote opens first owns
|
|
1380
|
+
the literal, so an apostrophe inside `"Don't"` does not swallow the ivar
|
|
1381
|
+
beside it.
|
|
1382
|
+
- **env_config tells a re-assignment from a tuple.** Two unconditional
|
|
1383
|
+
assignments of one key rendered as `:file, :test`, which reads exactly like
|
|
1384
|
+
`:mem_cache_store, { pool_size: 5 }`. The winner is named, with what it
|
|
1385
|
+
overrode.
|
|
1386
|
+
- **env keeps one default per call site.** One label for every site said a
|
|
1387
|
+
variable was optional while one of its reads was `ENV.fetch` with no
|
|
1388
|
+
default, which raises `KeyError`. A fetch whose fallback is an expression
|
|
1389
|
+
says its default is computed at runtime rather than claiming it has none,
|
|
1390
|
+
and an `ENV["X"]` read says it is nil when unset, apart from the fetch
|
|
1391
|
+
that raises.
|
|
1392
|
+
- **A Rack app attached with `match ... to:` is found.** `mount` is that call
|
|
1393
|
+
with a name derived, and the exact-path form is what an app writes when an
|
|
1394
|
+
unanchored mount would swallow a sibling path. It reaches `engines` and
|
|
1395
|
+
`routes`, which names the mounted apps it counts instead of calling them
|
|
1396
|
+
engine mounts, and the booted tier lists every Rack endpoint rather than
|
|
1397
|
+
`Rails::Engine` subclasses alone. A mount inside a `namespace` or `scope`
|
|
1398
|
+
carries that prefix; one whose enclosing scope, or its `path:`, is an expression is listed
|
|
1399
|
+
with no path rather than an unprefixed one, and every list prints it
|
|
1400
|
+
without one. `engines` follows every file `config/routes.rb` draws,
|
|
1401
|
+
through the walk the static `routes` answer uses, so on the static tier
|
|
1402
|
+
the two name one set of mounted apps; booted, `routes` reads the live
|
|
1403
|
+
route table. `engines`, `onboard`, the MCP resource and the
|
|
1404
|
+
generated context files head the same list "Mounted Apps", because half of
|
|
1405
|
+
what it holds are not engines. On the static tier a `match ... to: SomeApp`
|
|
1406
|
+
is counted once, as the mount it is, rather than also as a construct the
|
|
1407
|
+
walk could not expand, and so are `get "/status" => StatusApp` and
|
|
1408
|
+
`mount ActionCable.server => "/cable"`, which the walk did not see at all.
|
|
1409
|
+
- **config calls a zero-byte initializer empty** rather than "all commented
|
|
1410
|
+
out".
|
|
1411
|
+
- **Smaller corrections in the same pass.** `validate_semantics` asks the
|
|
1412
|
+
loaded model class before calling a callback method missing, and on the
|
|
1413
|
+
static tier makes no claim once the payload's method list was cut at its
|
|
1414
|
+
cap, so an inherited method past the cap is no longer reported as missing.
|
|
1415
|
+
`get_controllers` points
|
|
1416
|
+
`rails_get_view` at the controller's full path rather than its last segment,
|
|
1417
|
+
which is the directory Rails resolves. The stack overview's `Engines:` line
|
|
1418
|
+
is `Mounted:`, because half of what it lists are plain Rack apps.
|
|
1419
|
+
- **diagnose stops reading a display cap as a model's whole interface.** A
|
|
1420
|
+
method past the thirtieth was reported as not existing, in the same answer
|
|
1421
|
+
whose Method Trace printed its definition. Booted, diagnose asks the loaded
|
|
1422
|
+
model class, which knows a concern's methods and a gem's as well as the
|
|
1423
|
+
model's own. Statically, the model's own methods travel uncapped beside the
|
|
1424
|
+
capped display list, and where a concern or a parent could define the
|
|
1425
|
+
method the answer declines rather than guesses. `model_details` says how
|
|
1426
|
+
many of the model's methods it is showing.
|
|
1427
|
+
- **A CamelCase controller name resolves everywhere.** The needle was
|
|
1428
|
+
downcased without being underscored, so "GiftCards" never equalled the
|
|
1429
|
+
route key's own "gift_cards": `Payload.find_controller` missed it, and every
|
|
1430
|
+
tool that resolves a controller through it missed it too. One normalization
|
|
1431
|
+
now serves the payload, the routes tool and the MCP resource, and the
|
|
1432
|
+
resource answers a name that resolves to nothing with an error naming what
|
|
1433
|
+
exists rather than a zero-route success document.
|
|
1434
|
+
- **security_scan runs the brakeman the machine has.** The app's bundle
|
|
1435
|
+
narrows the load path, so a machine with brakeman installed was told to add
|
|
1436
|
+
it to the Gemfile while the other tier scanned the same app. When the
|
|
1437
|
+
in-process require fails and the gem is installed, the scan runs it as its
|
|
1438
|
+
own process outside the bundle - reading the report from a file of its own,
|
|
1439
|
+
since a gem manager's binstub can print to stdout first - and renders the
|
|
1440
|
+
result the same way, with a line saying which brakeman answered and from
|
|
1441
|
+
where. When the outside run writes no report, the answer carries the last
|
|
1442
|
+
line brakeman printed about why. With no brakeman anywhere, the message
|
|
1443
|
+
says that instead of guessing, and the availability answer is keyed by
|
|
1444
|
+
tier rather than decided once per process.
|
|
1445
|
+
- **An exact search with a space keeps its context lines on ripgrep 13.**
|
|
1446
|
+
The literal was escaped with Ruby's `\ `, which ripgrep 13 (Ubuntu 22.04,
|
|
1447
|
+
Debian 12) rejects, so the search fell back to the Ruby scan and dropped
|
|
1448
|
+
context lines and files with no listed extension. The space goes through
|
|
1449
|
+
unescaped now, which both engines read the same way.
|
|
1450
|
+
|
|
8
1451
|
## [5.28.0] - 2026-09-22
|
|
9
1452
|
|
|
10
1453
|
Thirty-six changes an architecture survey of v5.27.0 asked for, nine of them
|