rails-ai-context 5.36.4 → 5.36.6

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +732 -612
  3. data/docs/COMPATIBILITY.md +42 -29
  4. data/docs/GUIDE.md +2 -1
  5. data/docs/INTROSPECTORS.md +6 -6
  6. data/docs/TOOLS.md +3 -2
  7. data/lib/rails_ai_context/action_outcome.rb +2 -2
  8. data/lib/rails_ai_context/concern_macros.rb +11 -8
  9. data/lib/rails_ai_context/concern_membership.rb +85 -9
  10. data/lib/rails_ai_context/concern_paths.rb +1 -1
  11. data/lib/rails_ai_context/configuration.rb +1 -1
  12. data/lib/rails_ai_context/data/docs/index.json +0 -1
  13. data/lib/rails_ai_context/fingerprinter.rb +1 -1
  14. data/lib/rails_ai_context/introspector.rb +1 -2
  15. data/lib/rails_ai_context/introspectors/action_presence.rb +1 -1
  16. data/lib/rails_ai_context/introspectors/action_source.rb +4 -4
  17. data/lib/rails_ai_context/introspectors/controller_introspector.rb +1 -1
  18. data/lib/rails_ai_context/introspectors/convention_introspector.rb +4 -3
  19. data/lib/rails_ai_context/introspectors/declared_constant.rb +11 -4
  20. data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
  21. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +4 -4
  22. data/lib/rails_ai_context/introspectors/job_introspector.rb +2 -2
  23. data/lib/rails_ai_context/introspectors/listeners/associations_listener.rb +5 -2
  24. data/lib/rails_ai_context/introspectors/listeners/enums_listener.rb +6 -2
  25. data/lib/rails_ai_context/introspectors/listeners/generic_macro_listener.rb +4 -1
  26. data/lib/rails_ai_context/introspectors/listeners/mixins_listener.rb +0 -26
  27. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +3 -3
  28. data/lib/rails_ai_context/introspectors/listeners/validations_listener.rb +4 -2
  29. data/lib/rails_ai_context/introspectors/model_introspector.rb +167 -43
  30. data/lib/rails_ai_context/introspectors/performance_introspector.rb +55 -33
  31. data/lib/rails_ai_context/introspectors/route_introspector.rb +1 -1
  32. data/lib/rails_ai_context/introspectors/service_classes.rb +1 -1
  33. data/lib/rails_ai_context/introspectors/source_scan.rb +1 -1
  34. data/lib/rails_ai_context/introspectors/table_name.rb +18 -6
  35. data/lib/rails_ai_context/introspectors/test_introspector.rb +2 -2
  36. data/lib/rails_ai_context/path_resolver.rb +1 -1
  37. data/lib/rails_ai_context/redaction.rb +1 -1
  38. data/lib/rails_ai_context/route_coverage.rb +1 -1
  39. data/lib/rails_ai_context/tools/analyze_feature.rb +64 -68
  40. data/lib/rails_ai_context/tools/dependency_graph.rb +5 -9
  41. data/lib/rails_ai_context/tools/diagnose.rb +12 -6
  42. data/lib/rails_ai_context/tools/generate_test.rb +2 -2
  43. data/lib/rails_ai_context/tools/get_concern.rb +1 -1
  44. data/lib/rails_ai_context/tools/get_controllers.rb +1 -1
  45. data/lib/rails_ai_context/tools/get_edit_context.rb +19 -11
  46. data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
  47. data/lib/rails_ai_context/tools/get_service_pattern.rb +1 -1
  48. data/lib/rails_ai_context/tools/get_test_info.rb +1 -1
  49. data/lib/rails_ai_context/tools/migration_advisor.rb +160 -55
  50. data/lib/rails_ai_context/tools/validate_semantics.rb +1 -1
  51. data/lib/rails_ai_context/version.rb +1 -1
  52. metadata +1 -1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,141 @@ 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.36.6] - 2026-10-22
9
+
10
+ ### Removed
11
+
12
+ - **The mixin reader no longer reads `prepend_mod_with` and its siblings.**
13
+ An app defines those macros, not Rails, and each app picks its own module
14
+ namespace, while the listener assumed one application's `EE::` and `JH::`.
15
+ A class that calls them now lists only the mixins it writes with `include`,
16
+ `prepend` and `extend`.
17
+
18
+ ### Fixed
19
+
20
+ - **`rails_migration_advisor` puts an association in the class its table
21
+ belongs to** (#500). Every STI subclass reads its base's table, so the
22
+ `belongs_to` went to whichever subclass sorted first (`CallForEvidence`, not
23
+ `Edition`) and the base and every other subclass had no association. The
24
+ `has_many` on the other end had the same problem: a `users` table owned by
25
+ `Principal` got its subclass `AnonymousUser`. A subclass that sets a table
26
+ of its own still owns that one, and the Strong Migrations `ignored_columns`
27
+ step for `remove_column` names the base too, where it named the subclass
28
+ that shares the table's name. A base that raised while the app was read is
29
+ still the owner: its entry now keeps its file, table and parent, as the
30
+ static tier's now does. A subclass whose file the booted app cannot load
31
+ keeps its parent too, so it is not read as a second owner.
32
+ - **`rails_migration_advisor` writes the `class_name` Rails needs to reach the
33
+ class that owns the table** (#502). The `has_many` had none, so
34
+ `has_many :budget_phases` looked for a `BudgetPhase` where `Budget::Phase`
35
+ reads the table. Rails looks a name up in the model's namespace first, so
36
+ `has_many :comments` on `Admin::Post` now says `class_name: "::Comment"`
37
+ where an `Admin::Comment` would shadow it, and a name that already reaches
38
+ its class gets none. The static tier cannot see the app's acronyms, so there
39
+ `ai_match` still names `AIMatch`: a redundant `class_name` runs, and a
40
+ missing one raises.
41
+ - **A model file that declares a second model lists both, each with its own
42
+ declarations** (#503). On the static tier the second class was missing, and
43
+ its associations, validations and enums were the first class's:
44
+ `PaymentRefund` beside `Payment` in payment.rb answered "No matches" while
45
+ `Payment` gained a `belongs_to :payment`. Each class now takes its table
46
+ from its own name, an abstract class beside a model no longer drops the
47
+ model, and a model assigned with `Class.new` is listed too. A class that
48
+ shares a file is listed when it reaches a record base through the app's own
49
+ classes; one on a gem's base is not guessed at. A model's own file still
50
+ wins over another file that declares the same class, and what that other
51
+ file adds is not merged in.
52
+
53
+ A module nested in the model's file and included by it is read as the
54
+ model's: its scopes and macros were missing, and its associations,
55
+ validations and enums had been counted only because every line of the file
56
+ was. It is named as Ruby reaches it, `Payment::Trackable` where the static
57
+ tier said `Trackable`.
58
+
59
+ The booted tier gave each class in the file the validations of all of them.
60
+ A Mongoid document beside another was missing, and the first took its
61
+ fields, associations and validations. `rails_performance_check` counted the
62
+ next class's `has_many` as the first one's. Each now reads its own
63
+ declarations and those of the modules in its file that it includes.
64
+ - **Two models whose names differ only in case stay apart.** An app with both
65
+ `Checkin` and `CheckIn` had every association to either drawn to one of them
66
+ in `rails_dependency_graph`, and named that one in `rails_analyze_feature`,
67
+ `rails_get_controllers`, `rails_get_turbo_map`, `rails_validate`,
68
+ `rails_performance_check` and `rails_migration_advisor`. An exact spelling
69
+ now wins over a match that ignores case, even one in a nearer namespace. The
70
+ lookup behind them builds its index of model names once per run, where it
71
+ rebuilt it for every association it resolved.
72
+ - **`rails_analyze_feature` finds a model by its class name.** A feature such
73
+ as `AIMatchResult` was downcased into one word, `aimatchresult`, which no
74
+ name's words match, so the answer was "No matches" and then suggested that
75
+ same name. A class name now matches split at its capitals, which
76
+ `AIMatchResult` needs, and also as written, which a name joined by an app
77
+ acronym needs (`Token::ICal` in `token/ical.rb`). A model's test is looked
78
+ for by the name of its own file, which carries the app's inflection, so
79
+ `Token::ICal` no longer reads as untested beside its `ical_token_spec.rb`.
80
+ - **`rails_performance_check` names a namespaced model's `Model.all` by its
81
+ qualified name.** `Admin::Post.all` was reported as `Post.all`, and a
82
+ `Post.all` that reaches no model, or reaches a class that is not one, was
83
+ reported too. The constant is now resolved through the controller's
84
+ `Module.nesting`, so a compact `class Admin::PostsController` reaches
85
+ `::Post` where a nested one reaches `Admin::Post`.
86
+
87
+ ## [5.36.5] - 2026-10-19
88
+
89
+ ### Added
90
+
91
+ - **`rails_get_edit_context` takes a `line`.** It shows `context_lines` either
92
+ side of that line, so a line from a stack trace needs no text to search for.
93
+ `near` is needed only without one.
94
+
95
+ ### Fixed
96
+
97
+ - **`rails_diagnose` shows the code around the line it was given** (#495). It
98
+ searched for the method name and opened on its first mention, a comment
99
+ included, so the failing line was missing from the code shown in 18 of 30
100
+ real calls on one app. It also reads a setter's name (`title=`) and an index
101
+ call's (`[]`), and names their nil receiver.
102
+ - **`rails_migration_advisor` points a foreign key at the key of a table not
103
+ keyed by `id`** (#496): `foreign_key: { primary_key: :bid }`, where
104
+ `foreign_key: true` made PostgreSQL refuse the migration. A table with a
105
+ composite primary key, and a `t.references` table the schema lacks, get no
106
+ foreign key and a line saying why.
107
+ - **`rails_migration_advisor` types a reference the way the key it points at
108
+ is typed** (#501). A reference to an `integer` key gets `type: :integer`
109
+ and the key's `limit:` when it has one, and on MySQL one to an `unsigned`
110
+ key gets `unsigned: true`. It wrote Rails' default signed bigint, and MySQL
111
+ and MariaDB refuse a foreign key whose column differs from the key, so the
112
+ migration failed. PostgreSQL, which accepted the bigint, now gets the
113
+ matching type too. A `serial` key gives `integer` and a `smallserial` one
114
+ `smallint`, where the reference used to get a sequence of its own that
115
+ filled it with a made-up owner. SQLite, where every key is an integer,
116
+ still gets no type.
117
+ - **`rails_analyze_feature` lists every service, view and spec whose name
118
+ matches** (#497), wherever it sorts. Only the files it reads stop at 500 per
119
+ directory, and the closing note says which directory past them was matched
120
+ by name alone. The test gap list reads the name of every test, so it is no
121
+ longer left out on a suite of more than 500.
122
+ - **`conventions` counts the TypeScript, JSX and Vue files of
123
+ `app/javascript`** (#498), and the `.ruby` templates of `app/views`. One
124
+ app's `app/javascript` read 78 files; it holds 922. An edit to one of those
125
+ files, or to a `.jbuilder` or `.builder` view, now refreshes the cached
126
+ answers, as one to a `.js` or `.erb` file did.
127
+ - **`rails_migration_advisor`'s Strong Migrations warning for `add_association`
128
+ rewrites the `add_reference` it wrote** (#499). It named the reference as the
129
+ table (`add_foreign_key :listings, :owner`) and added a second key after the
130
+ migration had added one. On PostgreSQL it now gives the reference with
131
+ `disable_ddl_transaction!` and a concurrent index, the key in its own
132
+ `add_foreign_key ... validate: false` (strong_migrations before 2.5.2 flags any
133
+ foreign key inside `add_reference`), then `validate_foreign_key` with
134
+ the real table and column. On MySQL it moves the key to a separate
135
+ migration, as strong_migrations 2.7 and later require; an older locked version,
136
+ which does not check a MySQL key, gets no warning. A reference with no
137
+ foreign key gets only the concurrent index on PostgreSQL and nothing on
138
+ MySQL, and SQLite, which strong_migrations does not check, gets nothing. A
139
+ reference whose column already exists gets no warning, since its migration
140
+ fails first, and an undecided database gets one line when the reference has
141
+ a key.
142
+
8
143
  ## [5.36.4] - 2026-10-16
9
144
 
10
145
  ### Added
@@ -1433,15 +1568,14 @@ Ten tools that each answered one section of the app's configuration leave
1433
1568
 
1434
1569
  ### Added
1435
1570
 
1436
- - **A static model answer names the plugin patch modules it did not
1437
- read.** An app plugin that adds to a core model through its own DSL
1438
- (OpenProject's `patches %i[WorkPackage]`, which includes
1439
- `<Plugin>::Patches::WorkPackagePatch` in `to_prepare`) gave a
1440
- `rails_get_model_details` answer with none of what the patches add and no
1441
- sign anything was left out. A module named `*::Patches::<Model>Patch` in
1442
- the app's, its code roots' or its in-repo gems' `lib` is now named in one
1443
- `[UNAVAILABLE]` line, the way `rails_get_controllers` names an included
1444
- module it did not read.
1571
+ - **A static model answer names the plugin patch modules it did not read.** An
1572
+ app plugin that adds to a core model through its own DSL (`patches
1573
+ %i[WorkPackage]`, which includes `<Plugin>::Patches::WorkPackagePatch` in
1574
+ `to_prepare`) gave a `rails_get_model_details` answer with none of what the
1575
+ patches add and no sign anything was left out. A module named
1576
+ `*::Patches::<Model>Patch` in the app's, its code roots' or its in-repo
1577
+ gems' `lib` is now named in one `[UNAVAILABLE]` line, the way
1578
+ `rails_get_controllers` names an included module it did not read.
1445
1579
  - **`rails_get_model_details` names each `has_attached_file` attachment
1446
1580
  under Macros.**
1447
1581
  - **The controller listing marks `permit!` and names a parent's strong
@@ -1480,23 +1614,23 @@ Ten tools that each answered one section of the app's configuration leave
1480
1614
  *_params` was listed, so a method that builds associations counted while
1481
1615
  `article_params_json`, which calls `permit`, did not. A method named
1482
1616
  `*_params` now counts when it calls `permit`, `permit!` or `expect` on
1483
- params (directly, through a local or an ivar, on each item a block walks,
1484
- or on `ActionController::Parameters.new`), or when what it returns comes
1485
- from `super` (`super.merge(...)`), another strong params method (a call
1486
- runs the nearest class's method of that name, so one the controller or a
1487
- parent class defines counts whatever its name, such as Mastodon's
1488
- `params_slice`; any other named `*_params` or `permitted_*`, such as a
1489
- concern's or Pundit's `permitted_attributes(@post)`; and Devise's
1490
- `devise_parameter_sanitizer`), or a call given the whole params
1491
- (`PermittedAttributes::Enterprise.new(params).call`); a `before_action`
1492
- that writes into another one's params is not one. A method of any other
1493
- name counts only when it returns the permitted params themselves, through
1494
- a local, an `if`, a `case` or an explicit `return`, so a helper that
1495
- permits to build a URL, or to read one value, is left out. An action that
1496
- permits inline is left out, a public `*_params` helper is kept, `permit!`
1497
- on a `slice` lists the keys it slices, a computed filter
1498
- (`settings_attributes: UserSettings.keys`) is shown as written, not as a
1499
- scalar, and a key `require` computes is not printed as one.
1617
+ params (directly, through a local or an ivar, on each item a block walks, or
1618
+ on `ActionController::Parameters.new`), or when what it returns comes from
1619
+ `super` (`super.merge(...)`), another strong params method (a call runs the
1620
+ nearest class's method of that name, so one the controller or a parent class
1621
+ defines counts whatever its name, such as `params_slice`; any other named
1622
+ `*_params` or `permitted_*`, such as a concern's or Pundit's
1623
+ `permitted_attributes(@post)`; and Devise's `devise_parameter_sanitizer`),
1624
+ or a call given the whole params
1625
+ (`PermittedAttributes::Enterprise.new(params).call`); a `before_action` that
1626
+ writes into another one's params is not one. A method of any other name
1627
+ counts only when it returns the permitted params themselves, through a
1628
+ local, an `if`, a `case` or an explicit `return`, so a helper that permits
1629
+ to build a URL, or to read one value, is left out. An action that permits
1630
+ inline is left out, a public `*_params` helper is kept, `permit!` on a
1631
+ `slice` lists the keys it slices, a computed filter (`settings_attributes:
1632
+ UserSettings.keys`) is shown as written, not as a scalar, and a key
1633
+ `require` computes is not printed as one.
1500
1634
 
1501
1635
  ### Fixed
1502
1636
 
@@ -1574,12 +1708,12 @@ Ten tools that each answered one section of the app's configuration leave
1574
1708
  `print_result`.** The gem's rake helpers were top-level methods guarded by
1575
1709
  `unless defined?`, so an app's `print_result` in lib/tasks ran instead and
1576
1710
  the task aborted. They now live in a module.
1577
- - **A byte-size label does not depend on the app's I18n.**
1578
- `rails_read_logs` threw its whole answer away when an app's I18n backend
1579
- raised after a failed boot (Discourse's backend queries the database),
1580
- and `rails_runtime_info` and `rails_get_test_info` could do the same.
1711
+ - **A byte-size label does not depend on the app's I18n.** `rails_read_logs`
1712
+ threw its whole answer away when an app's I18n backend raised after a failed
1713
+ boot (a backend that queries the database), and `rails_runtime_info` and
1714
+ `rails_get_test_info` could do the same.
1581
1715
  - **The static fallback loads after a Bundler.setup that failed part way.**
1582
- An app that activates a gem before `bundler/setup` (Consul's
1716
+ An app that activates a gem before `bundler/setup` (one app's
1583
1717
  `require "sassc-embedded"`) at a version its lock does not pin made
1584
1718
  Bundler raise after it had marked activesupport loaded but before it added
1585
1719
  the load paths, so the standalone CLI and `serve` died with `cannot load
@@ -3525,16 +3659,17 @@ Ten tools that each answered one section of the app's configuration leave
3525
3659
  directory such as `app/domain`, or under a path `config/application.rb` adds
3526
3660
  to `eager_load_paths` or `autoload_paths`, when its superclass is a model. A
3527
3661
  class there that is not a model is left out even when it does not load, and
3528
- the lib subdirectories `autoload_lib(ignore:)` names are not read. The static
3529
- tier also finds models under a symlinked directory or file in `app/models`
3530
- that points inside the app, a model assigned `Class.new(ApplicationRecord)`
3531
- with what its block declares (the `self.table_name` it sets included), and
3532
- reads a job assigned `Class.new(ApplicationJob)`, or a class whose parent is
3533
- such a base, the same way. An assignment counts only when the file's path
3534
- names it, so an error constant such as `PaymentError = Class.new(StandardError)`
3535
- is never listed as a service, serializer or model. The static tier also
3536
- merges a module prepended after the class body (`Note.prepend(EE::Note)`,
3537
- GitLab's `prepend_mod_with("Note")`). (#344, #347, #413, #416)
3662
+ the lib subdirectories `autoload_lib(ignore:)` names are not read. The
3663
+ static tier also finds models under a symlinked directory or file in
3664
+ `app/models` that points inside the app, a model assigned
3665
+ `Class.new(ApplicationRecord)` with what its block declares (the
3666
+ `self.table_name` it sets included), and reads a job assigned
3667
+ `Class.new(ApplicationJob)`, or a class whose parent is such a base, the
3668
+ same way. An assignment counts only when the file's path names it, so an
3669
+ error constant such as `PaymentError = Class.new(StandardError)` is never
3670
+ listed as a service, serializer or model. The static tier also merges a
3671
+ module prepended after the class body (`Note.prepend(EE::Note)`,
3672
+ `prepend_mod_with("Note")`). (#344, #347, #413, #416)
3538
3673
  - **Booted `model_details` no longer credits every model with a gem's base
3539
3674
  mixins**, such as Kaminari's two modules and `page` and `default_per_page` on
3540
3675
  the app's abstract base, or on a model that subclasses ActiveRecord::Base
@@ -3783,12 +3918,12 @@ Ten tools that each answered one section of the app's configuration leave
3783
3918
  and its concerns' class methods, the last added first, then the same for
3784
3919
  each base, then what every model has: class methods an initializer writes in
3785
3920
  `ActiveRecord::Base.class_eval` or a reopened base, the file Rails loads
3786
- later winning (Canvas's `validates_locale` adds its `before_validation` to
3921
+ later winning (a `validates_locale` that adds its `before_validation` to
3787
3922
  Account, User and Course), then the modules mixed into the base. One an
3788
3923
  initializer writes on `ApplicationRecord` replaces ApplicationRecord's own.
3789
- A class's own class methods are its `def self.`, an `alias_method` in
3790
- `class << self` (it runs what its original name ran at the alias), and those
3791
- a Concern's `included` block or a hook writes on the class (`def self.x` or
3924
+ A class's own class methods are its `def self.`, an `alias_method` in `class
3925
+ << self` (it runs what its original name ran at the alias), and those a
3926
+ Concern's `included` block or a hook writes on the class (`def self.x` or
3792
3927
  `class << self`, in the block, in `base.class_eval` or in `class << base`),
3793
3928
  each from its line where that code runs. A module counts from the line
3794
3929
  adding it, where that line runs: an `extend`, `singleton_class.include` or
@@ -3807,12 +3942,12 @@ Ten tools that each answered one section of the app's configuration leave
3807
3942
  like any concern: `include` runs its `included` block, `extend` its methods
3808
3943
  and `self.extended` only. A module a plugin's lib loads by a glob `require`
3809
3944
  is read from beside the file naming it, and from its definition rather than
3810
- an empty namespace stub above it, so OpenProject's journalized models list
3945
+ an empty namespace stub above it, so journalized models list
3811
3946
  `save_journals`. A Mongoid document's own callbacks list in the same run
3812
3947
  order, one in a class method where the document calls it. Neither tier
3813
- evaluates `send`, `define_singleton_method` or a module included in
3814
- `class << self` for this, so the list can miss what they register or show
3815
- the definition they replace. (#253)
3948
+ evaluates `send`, `define_singleton_method` or a module included in `class
3949
+ << self` for this, so the list can miss what they register or show the
3950
+ definition they replace. (#253)
3816
3951
  - **A lambda or Proc given to `queue_as` reads as what ActiveJob does with
3817
3952
  it.** ActiveJob never calls it; it names the queue after the Proc's text.
3818
3953
  Both tiers now say so, with the source, where the booted tier printed the
@@ -3832,32 +3967,31 @@ Ten tools that each answered one section of the app's configuration leave
3832
3967
  ### Fixed
3833
3968
 
3834
3969
  - **A concern's declarations name the module the include resolves to.**
3835
- `rails_get_callbacks` on OpenProject's WorkPackage lists
3836
- `after_save :save_journals` from `Acts::Journalized::SaveHooks`, where it
3837
- said `SaveHooks`.
3838
- - **A module declared in its outer module's file is read from there.**
3839
- Canvas's `Role::AssociationHelper` lives in role.rb, and its `included`
3840
- hook adds `before_save :resolve_cross_account_role` to Enrollment and its
3841
- subclasses, AccountUser and RoleOverride; the static tier called the
3842
- module unread and missed the callback. `rails_get_callbacks`,
3843
- `rails_get_model_details` and the controller filters and actions find it
3844
- there and read that module, not the outer class.
3970
+ `rails_get_callbacks` on a WorkPackage model lists `after_save
3971
+ :save_journals` from `Acts::Journalized::SaveHooks`, where it said
3972
+ `SaveHooks`.
3973
+ - **A module declared in its outer module's file is read from there.** A
3974
+ `Role::AssociationHelper` can live in role.rb, and its `included` hook adds
3975
+ `before_save :resolve_cross_account_role` to Enrollment and its subclasses,
3976
+ AccountUser and RoleOverride; the static tier called the module unread and
3977
+ missed the callback. `rails_get_callbacks`, `rails_get_model_details` and
3978
+ the controller filters and actions find it there and read that module, not
3979
+ the outer class.
3845
3980
  - **`rails_get_callbacks` lists models of one callback count by name**, so a
3846
3981
  model gaining a callback no longer moves the sections around it.
3847
- - **A model macro written over `*args` reads each call's own arguments.**
3848
- The list binds to the call's positionals past the method's other
3849
- parameters, less the options hash the body takes off the end
3850
- (`args.extract_options!`), plus literals its leading statements push. A
3851
- block over the list is read once per item, so Canvas's
3852
- `validates_locale :locale, :browser_locale, allow_nil: true` lists an
3853
- inclusion validation on each field with its own `if: :<field>_changed?`, in
3854
- place of the macro's own row and one on a computed `field`. An options
3855
- hash, list or parameter the method changes in place (`options[:x] = 1`,
3856
- `options[:x] ||= 1`, `reverse_merge!`, `delete`) is not read as the call's
3857
- value. A declaration it decides, the method name included (`before_save
3858
- name` after `name.strip!`), is held back and listed under "Not decided from
3859
- the source (the method name or a condition)", the heading that was "Only
3860
- under a condition the source does not decide".
3982
+ - **A model macro written over `*args` reads each call's own arguments.** The
3983
+ list binds to the call's positionals past the method's other parameters,
3984
+ less the options hash the body takes off the end (`args.extract_options!`),
3985
+ plus literals its leading statements push. A block over the list is read
3986
+ once per item, so `validates_locale :locale, :browser_locale, allow_nil:
3987
+ true` lists an inclusion validation on each field with its own `if:
3988
+ :<field>_changed?`, in place of the macro's own row and one on a computed
3989
+ `field`. An options hash, list or parameter the method changes in place
3990
+ (`options[:x] = 1`, `options[:x] ||= 1`, `reverse_merge!`, `delete`) is not
3991
+ read as the call's value. A declaration it decides, the method name included
3992
+ (`before_save name` after `name.strip!`), is held back and listed under "Not
3993
+ decided from the source (the method name or a condition)", the heading that
3994
+ was "Only under a condition the source does not decide".
3861
3995
  - **A `load_defaults` written after `belongs_to_required_by_default = false`
3862
3996
  turns the default back on** in the static tier, as Rails does, so the
3863
3997
  implicit presence of a required `belongs_to` is listed. A literal assignment
@@ -3990,7 +4124,7 @@ Ten tools that each answered one section of the app's configuration leave
3990
4124
  without `--line`, and prints the tier footer once.
3991
4125
  - **`rails_validate` names a `belongs_to` whose key column the table lacks**
3992
4126
  instead of calling that column unindexed, and says nothing about a table's
3993
- indexes when its schema calls a helper the reader cannot interpret (Canvas's
4127
+ indexes when its schema calls a helper the reader cannot interpret (such as
3994
4128
  `t.replica_identity_index`), since that helper may add the index.
3995
4129
  - **A multi-name `t.references :a, :b, :c` adds every column**, with its index
3996
4130
  and foreign key, in the schema.rb reader and the migration replay. The replay
@@ -4070,11 +4204,10 @@ Ten tools that each answered one section of the app's configuration leave
4070
4204
 
4071
4205
  A pass over the whole tree for code that did not need to exist, with one base
4072
4206
  class for the introspectors and one for the serializers, and one reader where
4073
- several files each kept a copy. Then four real-app QA passes in every tier
4074
- (Mastodon, Discourse, OpenProject, Canvas, Forem, Whitehall, Consul,
4075
- OpenFoodNetwork, Huginn, Errbit, Diaspora, Plots2 and a private API app) and
4076
- five review rounds found the wrong answers below. Their fixes grew `lib/` by
4077
- about 8,500 lines, most of it reading what the gem used to guess.
4207
+ several files each kept a copy. Then four real-app QA passes in every tier,
4208
+ against thirteen open-source and private Rails apps, and five review rounds
4209
+ found the wrong answers below. Their fixes grew `lib/` by about 8,500 lines,
4210
+ most of it reading what the gem used to guess.
4078
4211
 
4079
4212
  ### Added
4080
4213
 
@@ -4083,7 +4216,7 @@ about 8,500 lines, most of it reading what the gem used to guess.
4083
4216
  one's path and the models the model scan found under it (unavailable, not
4084
4217
  zero, when the model scan failed), and
4085
4218
  in the compact files one Stack line, `- In-repo engines: 4 (catalog,
4086
- dfc_provider, order_management, web)`, capped at five names.
4219
+ feed_provider, order_management, web)`, capped at five names.
4087
4220
 
4088
4221
  ### Changed
4089
4222
 
@@ -4172,11 +4305,11 @@ about 8,500 lines, most of it reading what the gem used to guess.
4172
4305
  `; 29 in-repo engine locale dirs not read`.
4173
4306
  - **The component catalog reads every `app/components`**, packs and in-repo
4174
4307
  engines included, and its header counts what it could not place, as in `998
4175
- components (862 ViewComponent, 0 Phlex, 136 of no known base class)` on
4176
- OpenProject, which read 559 from its root tree alone, with a base that other
4177
- components inherit from (Consul's two `BaseComponent`s) named on a bases
4178
- line and not counted; the generated files print the same line from the same
4179
- numbers. `component:` takes a name with or without its namespace or
4308
+ components (862 ViewComponent, 0 Phlex, 136 of no known base class)` on one
4309
+ app, which read 559 from its root tree alone, with a base that other
4310
+ components inherit from (another app's two `BaseComponent`s) named on a
4311
+ bases line and not counted; the generated files print the same line from the
4312
+ same numbers. `component:` takes a name with or without its namespace or
4180
4313
  `Component` suffix, and a short name several components share lists them and
4181
4314
  asks for the full one.
4182
4315
  - **`rails_get_active_support` lists validator classes apart from concerns**,
@@ -4204,9 +4337,9 @@ about 8,500 lines, most of it reading what the gem used to guess.
4204
4337
  beside the associations they came from, so the numbers reconcile with the
4205
4338
  diagram.
4206
4339
  - **An association whose name is computed is marked `(computed)` wherever it
4207
- prints** (`` `owner_name` (computed) ``, and
4208
- `` `:"#{name.underscore}_search_data"` (computed) ``), where it printed
4209
- `[INFERRED]` (six times in Discourse's CLAUDE.md). No tool reads it as a
4340
+ prints** (`` `owner_name` (computed) ``, and ``
4341
+ `:"#{name.underscore}_search_data"` (computed) ``), where it printed
4342
+ `[INFERRED]` (six times in one app's CLAUDE.md). No tool reads it as a
4210
4343
  literal: `rails_dependency_graph` draws no node for it,
4211
4344
  `rails_generate_test` writes no matcher for it, where it wrote a spec that
4212
4345
  did not pass, and the related-model and N+1 checks name no model for it. A
@@ -4217,10 +4350,10 @@ about 8,500 lines, most of it reading what the gem used to guess.
4217
4350
  connected first and then by name, through the same function `rails_onboard`
4218
4351
  uses, and the heading says so. A subclass ranks by the associations it adds
4219
4352
  to its parent, so one that only repeats them sinks and one with many of its
4220
- own (OpenProject's `User`) stays: Huginn's list was `Agent` and 14 of its
4221
- subclasses, with no `User`, `Event` or `Scenario`, and Discourse's was
4222
- headed by `Chat::NullUser`. They listed the first 15 by name, and
4223
- `rails_onboard` broke ties in file order.
4353
+ own (such as a `User`) stays: one app's list was `Agent` and 14 of its
4354
+ subclasses, with no `User`, `Event` or `Scenario`, and another's was headed
4355
+ by `Chat::NullUser`. They listed the first 15 by name, and `rails_onboard`
4356
+ broke ties in file order.
4224
4357
  - **The controller count beside a route count reads "routed controllers"** in
4225
4358
  CLAUDE.md, AGENTS.md, `rails_onboard` and `rake ai:inspect`, since it counts
4226
4359
  the controllers the routes name, not the controller files.
@@ -4244,14 +4377,14 @@ about 8,500 lines, most of it reading what the gem used to guess.
4244
4377
  inferred. No two controllers share an identifier across code roots: an
4245
4378
  in-repo plugin's `foo_controller.js` beside the app's reads `chat--foo`,
4246
4379
  marked inferred. A controller the app registers from an installed package
4247
- (Errbit's `reveal` from `@stimulus-components/reveal`) is listed as
4248
- registered from that package, where `rails_get_stimulus` said none while
4249
- `rails_get_view` showed it. OpenProject's 161 read the names its code writes
4380
+ (`reveal` from `@stimulus-components/reveal`) is listed as registered from
4381
+ that package, where `rails_get_stimulus` said none while `rails_get_view`
4382
+ showed it. One app's 161 read the names its code writes
4250
4383
  (`admin--custom-fields`, not the path's `dynamic--admin--custom-fields`),
4251
4384
  and the few nothing confirms are marked inferred, named by their directory's
4252
4385
  rule when the app shows one (a loader importing the directory by a derived
4253
- path, or confirmed neighbours that all drop its segment), so OpenProject's 8
4254
- read `generic-dialog-close`, not `dynamic--generic-dialog-close`. The empty
4386
+ path, or confirmed neighbours that all drop its segment), so its 8 read
4387
+ `generic-dialog-close`, not `dynamic--generic-dialog-close`. The empty
4255
4388
  answer names the directories searched, and `rails_get_conventions` labels
4256
4389
  the pattern `Stimulus controllers` with no directory after it.
4257
4390
  - **The frontend framework is picked by config files and component counts**, a
@@ -4260,8 +4393,8 @@ about 8,500 lines, most of it reading what the gem used to guess.
4260
4393
  also uses is named. An Angular app that also depends on React read as React,
4261
4394
  and so did a Preact or Solid app that lists `react` for compatibility. Each
4262
4395
  framework reports its own version, and a frontend root counts the components
4263
- its framework writes (OpenProject's `frontend` has 305 `*.component.ts`; it
4264
- read 3).
4396
+ its framework writes (one app's `frontend` has 305 `*.component.ts`; it read
4397
+ 3).
4265
4398
  - **`rails_get_test_info` names both frameworks** (`rspec, minitest`) when an
4266
4399
  app runs both.
4267
4400
  - **The conventions' Locales line names the locales each file declares** (a
@@ -4277,7 +4410,7 @@ about 8,500 lines, most of it reading what the gem used to guess.
4277
4410
  partials share lists them**, where it answered with the first by path, and a
4278
4411
  call to a helper the app defines (in `app/helpers` of any code root, or a
4279
4412
  module those helpers include from any autoload root; a module's class
4280
- methods are not helpers) is not one of a partial's locals: Discourse's
4413
+ methods are not helpers) is not one of a partial's locals: one app's
4281
4414
  `layouts/head` listed `canonical_link_tag` and three more.
4282
4415
  - **A context run reads more of the app, for more time where there is more to
4283
4416
  read.** Where a kind of code lives is resolved once per root, each file is
@@ -4289,13 +4422,13 @@ about 8,500 lines, most of it reading what the gem used to guess.
4289
4422
  edit right after a parse on a coarse Linux clock is read again. The CLI
4290
4423
  picks between a built-in tool and a custom one of the same name as the MCP
4291
4424
  server does: the built-in, unless `skip_tools` names it. On the static tier,
4292
- `context --format all` takes about the same CPU as 5.29.0 on Canvas (17.4s
4293
- against 18.1s), Mastodon (2.8s against 2.7s) and Errbit, a sixth more on
4294
- Huginn (1.1s against 1.0s), a quarter more on a private API app (3.1s
4295
- against 2.5s; booted 5.4s against 4.7s), where the `Services:` line reads
4296
- all its services, and half again on OpenProject (9.4s against 6.4s), whose
4297
- 30 in-repo modules are now read for models, Stimulus identifiers, concerns,
4298
- mailers and model bases.
4425
+ `context --format all` takes about the same CPU as 5.29.0 on three of the
4426
+ apps tested (17.4s against 18.1s, 2.8s against 2.7s, and one more), a sixth
4427
+ more on a small app (1.1s against 1.0s), a quarter more on a private API app
4428
+ (3.1s against 2.5s; booted 5.4s against 4.7s), where the `Services:` line
4429
+ reads all its services, and half again on a large app (9.4s against 6.4s),
4430
+ whose 30 in-repo modules are now read for models, Stimulus identifiers,
4431
+ concerns, mailers and model bases.
4299
4432
  - **The generated files' `Async:` line counts Sidekiq workers** beside jobs and
4300
4433
  mailers, and `rails_get_job_pattern` names the directories it read under
4301
4434
  every listing and also searches `app/sidekiq` for what enqueues a job. The
@@ -4303,7 +4436,7 @@ about 8,500 lines, most of it reading what the gem used to guess.
4303
4436
  names and a count of the rest, where it globbed `app/jobs/*.rb`.
4304
4437
  - **A worker entry says what a job entry says**: its line count, the
4305
4438
  `retry_on` and `discard_on` it declares and what it calls. Where a base
4306
- class owns `perform` and the worker defines `execute`, as Discourse's jobs
4439
+ class owns `perform` and the worker defines `execute`, as some apps' jobs
4307
4440
  do, `execute` is read instead.
4308
4441
  - **A class in a job directory that reaches neither ActiveJob nor Sidekiq but
4309
4442
  defines `perform` is listed as `[unknown base]`**, as a Resque job is, and
@@ -4316,8 +4449,8 @@ about 8,500 lines, most of it reading what the gem used to guess.
4316
4449
  ActiveJob's `perform_all_later` do (a private API app listed 3 of the 6
4317
4450
  files that enqueue one of its workers); a call through the app's own
4318
4451
  `enqueue`, `enqueue_in` or `enqueue_at` helper counts, the job named by a
4319
- symbol, a string or the class (Discourse's `Jobs.enqueue(:process_post)`
4320
- lists `app/models/post.rb`, where Discourse jobs had no callers); all of
4452
+ symbol, a string or the class (`Jobs.enqueue(:process_post)` lists
4453
+ `app/models/post.rb`, where jobs enqueued that way had no callers); all of
4321
4454
  `app/` and `lib/` is searched, rake tasks, `bin/` and `script/` programs and
4322
4455
  all of `db/` included, and a list capped at twenty says how many more there
4323
4456
  are; a job named relative to the caller's namespace (`SyncJob` inside
@@ -4332,28 +4465,28 @@ about 8,500 lines, most of it reading what the gem used to guess.
4332
4465
  out by the constant it declares.
4333
4466
  - **A job shows the queue and Sidekiq options it inherits from the app's own
4334
4467
  base classes**, its own settings winning key by key, each inherited one
4335
- named once with its base and its value when short. Diaspora's `Mail::*`
4336
- workers showed no queue where `Mail::NotifierBaseWorker` sets `queue: low`,
4337
- Mastodon's `Fasp::*` workers none where their base sets `fasp`, and
4338
- Whitehall's `PublishingApi*Job`s none where theirs sets `publishing_api`.
4468
+ named once with its base and its value when short. `Mail::*` workers showed
4469
+ no queue where `Mail::NotifierBaseWorker` sets `queue: low`, `Fasp::*`
4470
+ workers none where their base sets `fasp`, and `PublishingApi*Job`s none
4471
+ where theirs sets `publishing_api`.
4339
4472
  - **A job queue computed by a block or an expression says so, with its
4340
4473
  source**: `queue_as -> { ... }` read `dynamic` in the booted tier and showed
4341
4474
  no queue without boot.
4342
4475
  - **A class is a job base only when its own chain reaches ActiveJob or a
4343
- Sidekiq mixin.** OpenProject's `Meetings::PDF::Common::Styles::Base`, a PDF
4344
- style class under `app/workers`, was listed as a job base.
4476
+ Sidekiq mixin.** A `Meetings::PDF::Common::Styles::Base`, a PDF style class
4477
+ under `app/workers`, was listed as a job base.
4345
4478
  - **A mailer kept in `app/services` is a mailer and not also a service**, and
4346
4479
  a service not found is offered the constants the files declare
4347
4480
  (`ActivityPub::...`, where it offered `Activitypub::...`).
4348
4481
  - **A module other classes in the app include, extend or prepend is not a
4349
4482
  service object**, unless it has an entry point of its own (a class method
4350
4483
  other than the `included`, `extended` or `prepended` hook,
4351
- `module_function`, `extend self`). Whitehall's `AssetManager::ServiceHelper`
4352
- was listed as a service, as were 11 mixins in a private API app and 11 in
4353
- OpenProject; the generated `Services:` line follows.
4484
+ `module_function`, `extend self`). An `AssetManager::ServiceHelper` was
4485
+ listed as a service, as were 11 mixins in a private API app and 11 in
4486
+ another app; the generated `Services:` line follows.
4354
4487
  - **`rails_get_service_pattern` and `rails_get_helper_methods` find a service
4355
4488
  or helper by the name the app writes when its path uses a registered
4356
- acronym.** Mastodon's 20 `ActivityPub::...` services, in `activitypub/`,
4489
+ acronym.** One app's 20 `ActivityPub::...` services, in `activitypub/`,
4357
4490
  answered not found and suggested `Activitypub::...`.
4358
4491
  - **The generated files' `Services:` line reads the same scan
4359
4492
  `rails_get_service_pattern` lists from**, so a service in a subdirectory, a
@@ -4363,12 +4496,12 @@ about 8,500 lines, most of it reading what the gem used to guess.
4363
4496
  tier: the static tier dropped both with the conventions it cannot read.
4364
4497
  - **A blank name given to a by-name lookup is answered as blank**, naming what
4365
4498
  the tool looks up ("The concern name is blank"), in the helper, service,
4366
- job, mailer, component and concern tools: Discourse's
4367
- `rails_get_helper_methods(helper: "")` crashed on a file named `helper.rb`.
4368
- A string passed on the command line or in a direct call where a tool takes a
4369
- list (`files`, `include`, `checks`) is a one-item list, or a list when
4370
- comma-separated, where it failed with `undefined method 'any?'`; over MCP
4371
- the schema asks for the list itself.
4499
+ job, mailer, component and concern tools: `rails_get_helper_methods(helper:
4500
+ "")` crashed on a file named `helper.rb`. A string passed on the command
4501
+ line or in a direct call where a tool takes a list (`files`, `include`,
4502
+ `checks`) is a one-item list, or a list when comma-separated, where it
4503
+ failed with `undefined method 'any?'`; over MCP the schema asks for the list
4504
+ itself.
4372
4505
  - **The generated Commands section and `rails_onboard`'s Getting Started name
4373
4506
  only commands the app has**, from one decision: `bin/dev` where it exists,
4374
4507
  otherwise `bin/rails server`, and `bin/rails db:migrate` where the app has
@@ -4381,35 +4514,35 @@ about 8,500 lines, most of it reading what the gem used to guess.
4381
4514
  default read as `[]` where the booted tier printed `{}`, a booted expression
4382
4515
  index reads as a key list, as the dumps give it (`rails_migration_advisor`
4383
4516
  crashed on any table that has one), and schema hints print a structure.sql
4384
- app's primary key as `id` (a composite one as `a, b`), where Discourse's
4385
- read ``(pk: `["id"]`)``.
4517
+ app's primary key as `id` (a composite one as `a, b`), where one app's read
4518
+ ``(pk: `["id"]`)``.
4386
4519
  - **Static routes read the route files `config.paths["config/routes.rb"]`
4387
4520
  registers**, in order (an assignment, `<<`, `concat`, `Rails.root.join`, a
4388
4521
  literal `Dir[...]` glob, kept inside the app), and name a mounted app built
4389
- by a call (`Flipper::UI.app`). OpenFoodNetwork read 69 of its 340 app
4390
- routes, and its Rswag, DfcProvider, Flipper UI and Sidekiq mounts were
4391
- missing. A file pulled in by `draw` is read inside the scope its `draw` sits
4392
- in, and again under each scope that draws it: Forem's `draw :api` inside
4393
- `namespace :api` put `/articles/search` under the web `articles` controller
4394
- without `/api`, and its v0 routes were dropped. `match ... via:` is one
4395
- route answering each verb it names (`GET|POST`, `ANY` for `via: :all`, a
4396
- `via:` from an enclosing scope too), where static routes dropped it. Routes
4397
- take the controller from `scope(controller:)` or their own `controller:`
4398
- (Canvas read 297 made-up controllers such as `accounts/:account_id`), routes
4399
- drawn through an app class (`ApiRouteSet::V1.draw(self)`) take the path and
4400
- name prefixes it returns (Canvas's 971 `/api/v1` routes), a path under an
4401
- optional scope is written as Rails normalizes it, and a route name another
4402
- file already took is left off, as Rails does; a singular `resource` names
4403
- its show route and a root route takes its `as:` as Rails does, an engine
4404
- lists what it could not expand and every path it is mounted at, a controller
4522
+ by a call (`Flipper::UI.app`). One app read 69 of its 340 app routes, and
4523
+ its Rswag, FeedProvider, Flipper UI and Sidekiq mounts were missing. A file
4524
+ pulled in by `draw` is read inside the scope its `draw` sits in, and again
4525
+ under each scope that draws it: one app's `draw :api` inside `namespace
4526
+ :api` put `/articles/search` under the web `articles` controller without
4527
+ `/api`, and its v0 routes were dropped. `match ... via:` is one route
4528
+ answering each verb it names (`GET|POST`, `ANY` for `via: :all`, a `via:`
4529
+ from an enclosing scope too), where static routes dropped it. Routes take
4530
+ the controller from `scope(controller:)` or their own `controller:` (one app
4531
+ read 297 made-up controllers such as `accounts/:account_id`), routes drawn
4532
+ through an app class (`ApiRouteSet::V1.draw(self)`) take the path and name
4533
+ prefixes it returns (its 971 `/api/v1` routes), a path under an optional
4534
+ scope is written as Rails normalizes it, and a route name another file
4535
+ already took is left off, as Rails does; a singular `resource` names its
4536
+ show route and a root route takes its `as:` as Rails does, an engine lists
4537
+ what it could not expand and every path it is mounted at, a controller
4405
4538
  filter lists engine rows like the app's own, and every route count says how
4406
4539
  many more routes sit in engine tables.
4407
4540
  - **The auth section's CORS flag is the api section's decision** (a
4408
4541
  commented-out `cors.rb` is not configured in either), so `.ai-context.json`
4409
- no longer says OpenProject has no CORS beside its `rack-cors.rb`.
4542
+ no longer says an app has no CORS beside its `rack-cors.rb`.
4410
4543
  - **The locale note counts every in-repo gem, engine or plugin with
4411
- `config/locales`**, those without `app/` too (Discourse: 46 directories,
4412
- where it said 28).
4544
+ `config/locales`**, those without `app/` too (one app: 46 directories, where
4545
+ it said 28).
4413
4546
  - **The booted database and connection-pool sections list replicas**
4414
4547
  (`configs_for` with `include_hidden: true`), as the static tier does.
4415
4548
  - **`rails_security_scan` and `doctor` read the app's `Gemfile.lock` before
@@ -4423,28 +4556,28 @@ about 8,500 lines, most of it reading what the gem used to guess.
4423
4556
  holds every ancestor and mixin, and otherwise names the unread gem or
4424
4557
  ancestor and says the action is unverified; a namespaced controller is
4425
4558
  checked at all, and a route to a method ending in `?` or `!` finds it
4426
- (Mastodon's `requests#merged?` was flagged).
4559
+ (`requests#merged?` was flagged).
4427
4560
  - **An engine's root, an initializer's source and an autoload directory in the
4428
4561
  context are app-relative, or the gem and version**, the gem's own
4429
4562
  initializers and autoload directories are left out, and is left out rather
4430
4563
  than written as an absolute path from the generating machine.
4431
- - **Routes an app draws into an engine (`Spree::Core::Engine.routes.draw`) are
4564
+ - **Routes an app draws into an engine (`Shop::Core::Engine.routes.draw`) are
4432
4565
  listed with that engine**, under the path the app mounts it at and named
4433
- through its route proxy (`spree.admin_orders_path`, after the mount's `as:`
4566
+ through its route proxy (`shop.admin_orders_path`, after the mount's `as:`
4434
4567
  or the engine's `engine_name`), and are not in the app's route count on
4435
4568
  either tier. Booted, the listing is each mounted engine's whole table, read
4436
4569
  under its mount the way `bin/rails routes` prints it; statically, it is what
4437
4570
  the app's route files draw into the engine. Controller actions, route-helper
4438
4571
  validation and generated tests read these routes too.
4439
- `rails_get_routes(controller: "spree/admin/orders")` answers with OFN's 15
4440
- such routes, a short name such as `orders` lists them beside the app's own
4441
- matches, and the listing names the 219 it draws into Spree apart, PATCH and
4442
- PUT merged as in the app count.
4572
+ `rails_get_routes(controller: "shop/admin/orders")` answers with one app's
4573
+ 15 such routes, a short name such as `orders` lists them beside the app's
4574
+ own matches, and the listing names the 219 it draws into the engine apart,
4575
+ PATCH and PUT merged as in the app count.
4443
4576
  - **A mounted app is listed once per app and path, with its path** (the
4444
4577
  compact `Mounted:` line names each app once, with its path count, before its
4445
4578
  five-name cap), when the mount sits in a `scope path: nil` or in both
4446
- branches of an `if`: Discourse's "Mounted Apps (4)" listed `Sidekiq::Web`
4447
- and `Logster::Web` twice each with no path.
4579
+ branches of an `if`: one app's "Mounted Apps (4)" listed `Sidekiq::Web` and
4580
+ `Logster::Web` twice each with no path.
4448
4581
  - **`rails_get_env` and the context file's `env` section read ENV names from
4449
4582
  one scan**, and a default is redacted by the source rule, so an empty one,
4450
4583
  an address, a URL or a hostname prints as written where it read
@@ -4452,8 +4585,8 @@ about 8,500 lines, most of it reading what the gem used to guess.
4452
4585
  other `config/**/*.yml`, skipping `<%#` comments. Config YAML on
4453
4586
  `sensitive_patterns` is read for the names in its ERB tags only, never a
4454
4587
  value or a default; credentials and keys are still never opened.
4455
- `rails_get_env` gains 19 names on a private API app, 13 on Mastodon
4456
- and 9 on Huginn (its `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_SOCKET`).
4588
+ `rails_get_env` gains 19 names on a private API app, 13 on another and 9 on
4589
+ a third (its `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_SOCKET`).
4457
4590
  - **A connected app with no tables yet is described as connected**, where the
4458
4591
  schema note said "no DB connection, no schema.rb".
4459
4592
  - **`rails_security_scan` reports brakeman's error message**, where it quoted
@@ -4471,24 +4604,24 @@ about 8,500 lines, most of it reading what the gem used to guess.
4471
4604
  `include` adds its concern's `before_action`s where the line stands, and
4472
4605
  each prints the concern it came from (`_(from Localized via
4473
4606
  ApplicationController)_`), in `rails_get_controllers`,
4474
- `rails_analyze_feature` and the generated "Global before_actions".
4475
- Mastodon's `AboutController` listed none of `WebAppControllerConcern`'s
4476
- four filters or the `set_locale` its parent's `Localized` wraps every
4477
- action in, and `FollowerAccountsController` had no `set_account`. An
4478
- included module whose file is not in the app is named as not read.
4607
+ `rails_analyze_feature` and the generated "Global before_actions". An
4608
+ `AboutController` listed none of `WebAppControllerConcern`'s four filters or
4609
+ the `set_locale` its parent's `Localized` wraps every action in, and
4610
+ `FollowerAccountsController` had no `set_account`. An included module whose
4611
+ file is not in the app is named as not read.
4479
4612
  - **An included module is found in any `app/*` directory**, as Rails autoloads
4480
- it, for a model as for a controller: Consul's `Budget::Investment` carries
4481
- the `after_save` its `Budget::Reclassification` declares, OpenProject's
4482
- `WorkPackage`, which read 0 validations and 17 unread concerns, reads its
4613
+ it, for a model as for a controller: a `Budget::Investment` carries the
4614
+ `after_save` its `Budget::Reclassification` declares, a `WorkPackage`, which
4615
+ read 0 validations and 17 unread concerns, reads its
4483
4616
  `app/models/work_package/validations.rb`, and a model that includes a helper
4484
4617
  from `app/helpers` no longer reports it as not read.
4485
4618
  - **A partial index prints its WHERE clause.** The static tier carries the
4486
4619
  `where:` of a schema.rb index, the WHERE of a structure.sql `CREATE INDEX`
4487
4620
  and the `where:` a replayed migration passes, and `rails_get_schema` and
4488
- `.claude/rules` print it: Forem's `canonical_url` reads `[unique where
4489
- (published IS TRUE)]` and Discourse's `unique_index_categories_on_slug` ends
4490
- `where ((slug)::text <> ''::text)`, where both read as unique over the whole
4491
- table; a column two partial unique indexes cover shows each condition.
4621
+ `.claude/rules` print it: a `canonical_url` index reads `[unique where
4622
+ (published IS TRUE)]` and a `unique_index_categories_on_slug` ends `where
4623
+ ((slug)::text <> ''::text)`, where both read as unique over the whole table;
4624
+ a column two partial unique indexes cover shows each condition.
4492
4625
  - **"Missing FK Indexes" reports a column only when an association or a
4493
4626
  foreign key uses it.** Any `*_id` column whose type matched some table's
4494
4627
  primary key counted, so an app with one string key reported every external
@@ -4501,14 +4634,14 @@ about 8,500 lines, most of it reading what the gem used to guess.
4501
4634
  it as the parent key. The associations are the ones the models section
4502
4635
  lists, so a model's concerns and inherited bases count, and so do models
4503
4636
  that subclass `ActiveRecord::Base`. Against 5.29.0: a private API app lists
4504
- 6 where it listed 111, all six real; Mastodon 16 where it listed 20;
4505
- Diaspora 15 where it listed 13; OpenProject 43 where it listed 36; Discourse
4506
- 166 where it listed 2, because its models subclass `ActiveRecord::Base` and
4507
- none were read before. `rails_performance_check` and the "Performance: N
4508
- issues detected" line in the generated files follow (135 to 30 on a private
4509
- API app). A `has_and_belongs_to_many` record in the models section now
4510
- carries a declared `join_table` and `association_foreign_key` on both tiers,
4511
- so a custom join table is read by name booted as well as static.
4637
+ 6 where it listed 111, all six real; three other apps 16 where they listed
4638
+ 20, 15 where 13 and 43 where 36; a fourth 166 where it listed 2, because its
4639
+ models subclass `ActiveRecord::Base` and none were read before.
4640
+ `rails_performance_check` and the "Performance: N issues detected" line in
4641
+ the generated files follow (135 to 30 on a private API app). A
4642
+ `has_and_belongs_to_many` record in the models section now carries a
4643
+ declared `join_table` and `association_foreign_key` on both tiers, so a
4644
+ custom join table is read by name booted as well as static.
4512
4645
  - **The gem's own warnings never reach the JSON-RPC stream.** While the stdio
4513
4646
  transport owns stdout, including while live reload starts, a warning goes to
4514
4647
  stderr rather than `Rails.logger`, which an app may have pointed at stdout.
@@ -4543,41 +4676,42 @@ about 8,500 lines, most of it reading what the gem used to guess.
4543
4676
  - **`config/database.yml` is read as YAML in the static tier.** It was scanned
4544
4677
  line by line, and the scan disagreed with the file format on every shape
4545
4678
  that is not the generated one: an entry sharing its adapter through `<<:
4546
- *default` came back with only its name, a database called `replica` (as
4547
- Mastodon names its read replica) was read as a config key of the one before
4548
- it, so the primary was labelled the replica and the app read as
4549
- single-database, and an env given only a `url:` listed no database at all.
4550
- The file is now parsed by `YAML` with aliases on, after its ERB tags are
4551
- replaced without being run, and the databases come out of the env hash by
4552
- the same rule Rails applies: every value a Hash means one database per key,
4553
- anything else is a single primary. An adapter the file computes in ERB is
4554
- reported as the literal the ERB falls back to, labelled so (`MySQL by
4555
- database.yml default`), or as unknown when there is none; it is never picked
4556
- from the Gemfile when the app bundles more than one adapter gem, as Huginn
4557
- does. Without a committed `config/database.yml`, the adapter is read from
4558
- the schema first, a structure.sql dialect and then column types only one
4559
- database has (`jsonb`, arrays, `hstore`; `mediumtext`), then from the app's
4560
- `database.yml.*` example files, then the Gemfile, so Canvas reads PostgreSQL
4561
- where it read `PostgreSQL or SQLite`; `postgis` reads as PostgreSQL, and
4562
- when nothing decides, the reason names only the files the app has. With
4563
- nothing to decide between them, the answer names the candidates (`MySQL or
4564
- SQLite`, and in `rails_get_schema` `MySQL or SQLite, database.yml does not
4565
- say which`). A file that is not valid YAML reports no databases instead of a
4566
- misreading. A boot that fails part way now falls back to the file for the
4567
- database and replica lists, where it reported none. By the same rule, an env
4568
- that mixes plain keys with a nested `secondary:` hash is one database, as
4569
- Rails reads it, so that nested entry is no longer reported as a replica.
4679
+ *default` came back with only its name, a database called `replica` (as some
4680
+ apps name a read replica) was read as a config key of the one before it, so
4681
+ the primary was labelled the replica and the app read as single-database,
4682
+ and an env given only a `url:` listed no database at all. The file is now
4683
+ parsed by `YAML` with aliases on, after its ERB tags are replaced without
4684
+ being run, and the databases come out of the env hash by the same rule Rails
4685
+ applies: every value a Hash means one database per key, anything else is a
4686
+ single primary. An adapter the file computes in ERB is reported as the
4687
+ literal the ERB falls back to, labelled so (`MySQL by database.yml
4688
+ default`), or as unknown when there is none; it is never picked from the
4689
+ Gemfile when the app bundles more than one adapter gem. Without a committed
4690
+ `config/database.yml`, the adapter is read from the schema first, a
4691
+ structure.sql dialect and then column types only one database has (`jsonb`,
4692
+ arrays, `hstore`; `mediumtext`), then from the app's `database.yml.*`
4693
+ example files, then the Gemfile, so an app with no committed file reads
4694
+ PostgreSQL where it read `PostgreSQL or SQLite`; `postgis` reads as
4695
+ PostgreSQL, and when nothing decides, the reason names only the files the
4696
+ app has. With nothing to decide between them, the answer names the
4697
+ candidates (`MySQL or SQLite`, and in `rails_get_schema` `MySQL or SQLite,
4698
+ database.yml does not say which`). A file that is not valid YAML reports no
4699
+ databases instead of a misreading. A boot that fails part way now falls back
4700
+ to the file for the database and replica lists, where it reported none. By
4701
+ the same rule, an env that mixes plain keys with a nested `secondary:` hash
4702
+ is one database, as Rails reads it, so that nested entry is no longer
4703
+ reported as a replica.
4570
4704
  - **An association reads the options of a `with_options` block around it.**
4571
4705
  The static tier ignored the block, so `rails_get_model_details`,
4572
4706
  `rails_dependency_graph` and `.ai-context.json` named classes the app does
4573
- not have (Mastodon's `EmailDomainBlock` parent and child read as `Parent`
4574
- and `Child`, `Appeal`'s reviewers as `ApprovedByAccount`), and
4575
- `rails_validate` reported a `has_many` inside `with_options dependent:
4576
- :destroy` as missing `:dependent`; Mastodon's `Account` drew 42 such
4577
- warnings. It follows ActiveSupport: a block without a parameter passes its
4578
- options to the associations inside it, a block with one (`do |assoc|
4579
- assoc.has_many ... end`) passes them only to the calls on that parameter,
4580
- and the association's own options still win over the block's.
4707
+ not have (an `EmailDomainBlock` parent and child read as `Parent` and
4708
+ `Child`, `Appeal`'s reviewers as `ApprovedByAccount`), and `rails_validate`
4709
+ reported a `has_many` inside `with_options dependent: :destroy` as missing
4710
+ `:dependent`; one `Account` model drew 42 such warnings. It follows
4711
+ ActiveSupport: a block without a parameter passes its options to the
4712
+ associations inside it, a block with one (`do |assoc| assoc.has_many ...
4713
+ end`) passes them only to the calls on that parameter, and the association's
4714
+ own options still win over the block's.
4581
4715
  - **`rails_validate` compares an escaped symbol by its real characters.** A
4582
4716
  `validates`, `permit` or callback symbol written with an escape no longer
4583
4717
  reads as a missing column.
@@ -4612,9 +4746,9 @@ about 8,500 lines, most of it reading what the gem used to guess.
4612
4746
  payload's job and worker records carry one `retries` list in place of
4613
4747
  `retry_on` and `discard_on`.
4614
4748
  - **`config/sidekiq.yml` is read as YAML**, so a path named in a comment and
4615
- an environment key are no longer listed as queues (OpenFoodNetwork read
4616
- `default, mailers, config`), and a weighted queue (`[critical, 8]`) is
4617
- named by its queue.
4749
+ an environment key are no longer listed as queues (one app read `default,
4750
+ mailers, config`), and a weighted queue (`[critical, 8]`) is named by its
4751
+ queue.
4618
4752
  - **A page past the end of a list says only that.** `rails_get_i18n` and
4619
4753
  `rails_get_mailers` asked for such a page said the app had no locale files
4620
4754
  or no mailers, beside the count that proved otherwise.
@@ -4628,18 +4762,17 @@ about 8,500 lines, most of it reading what the gem used to guess.
4628
4762
  'production'?"); `RAILS_ENV` stays settable through the variable, or with
4629
4763
  `--environment` before the command name.
4630
4764
  - **The route total agrees across surfaces.** `.ai-context.json` and the
4631
- generated files counted one route twice where two controllers answer on
4632
- the same path and action (Forem's two API versions), so they read 769
4633
- beside `rails_get_routes`' 765 plus 3 framework routes.
4765
+ generated files counted one route twice where two controllers answer on the
4766
+ same path and action (two API versions), so they read 769 beside
4767
+ `rails_get_routes`' 765 plus 3 framework routes.
4634
4768
  - **A share short of the whole never prints as 100%.** Translation coverage,
4635
4769
  the doctor readiness score, connection-pool use and the cache hit rate are
4636
4770
  floored, so a locale missing one key reads `99.9%` where it read `100.0%`
4637
4771
  beside the line naming the missing key.
4638
- - **An authorization library is named only when the app bundles it.** A
4639
- bare `app/policies` directory reads "policies in app/policies" with the
4640
- policy classes listed, and an `Ability` class without cancancan is named
4641
- as that file, where OpenProject, which bundles neither, read
4642
- `Auth: Pundit`.
4772
+ - **An authorization library is named only when the app bundles it.** A bare
4773
+ `app/policies` directory reads "policies in app/policies" with the policy
4774
+ classes listed, and an `Ability` class without cancancan is named as that
4775
+ file, where an app that bundles neither read `Auth: Pundit`.
4643
4776
  - **A Brakeman finding from `rails_validate` prints under the file it
4644
4777
  belongs to**, where every finding landed under the last file listed.
4645
4778
  - **The ActiveStorage stack line counts models and attachments apart** (`14
@@ -4684,15 +4817,15 @@ about 8,500 lines, most of it reading what the gem used to guess.
4684
4817
  than 5.0; a proc default reads as its source, as schema.rb and structure.sql
4685
4818
  give it. `rails_get_schema` prints where the answer came from at every
4686
4819
  detail level, including how many names were read from files and how many
4687
- `create_table` calls stayed unnamed. OpenProject goes from 37 tables and 39
4820
+ `create_table` calls stayed unnamed. One app goes from 37 tables and 39
4688
4821
  indexes to 208 and 556, and `rails_get_schema(table: "work_packages")`
4689
4822
  answers where it said the table did not exist.
4690
4823
  - **A route target with a leading slash is absolute**, as Rails reads it:
4691
- OpenProject's `controller: "/admin/settings/..."` inside its admin
4692
- namespaces read `admin/settings/admin/settings/...`.
4824
+ `controller: "/admin/settings/..."` inside admin namespaces read
4825
+ `admin/settings/admin/settings/...`.
4693
4826
  - **`app/concerns` is read as a concerns directory**, in the root tree, packs
4694
- and in-repo engines, as Rails autoloads it. Huginn keeps its model concerns
4695
- there and read 1 concern where it has 28, and `rails_get_model_details`
4827
+ and in-repo engines, as Rails autoloads it. An app that keeps its model
4828
+ concerns there read 1 concern where it has 28, and `rails_get_model_details`
4696
4829
  called 8 of `Agent`'s concerns unread; `rails_get_concern(type: "other")`
4697
4830
  lists them. `type:` takes every type the listing prints, in either spelling
4698
4831
  (`service`, `LibStatic`), and only types that hold a concern are listed, and
@@ -4700,49 +4833,48 @@ about 8,500 lines, most of it reading what the gem used to guess.
4700
4833
  such a value and listed every concern. A module a concern declares inside
4701
4834
  itself and mixes in (`DryRunnable::Wrapper`) is looked up from the enclosing
4702
4835
  namespace outward, and a mixin in an autoload root the app declares
4703
- (`autoload_lib`, `autoload_paths`) is read like any concern, so Discourse's
4836
+ (`autoload_lib`, `autoload_paths`) is read like any concern, so a
4704
4837
  `RateLimiter::OnCreateRecord` adds its callbacks to `Post` and `Topic`. A
4705
4838
  macro, callback or mixin inside a method of such a module applies only to
4706
4839
  the classes that call that method, in their own body or through a concern's
4707
- `included do` block, as at runtime: every OpenProject model that includes
4708
- the module holding `acts_as_watchable` would otherwise show `watchers` and
4709
- `favorites`. The macros are read with the call's literal arguments (a
4710
- parameter the method reassigns or a block shadows keeps its own value),
4711
- taking the `if`, `unless` or `case` branch they decide (a private API app's
4712
- own macro called with `:picture, has_one: true` gives `has_one :picture`,
4713
- not a `name` pair); a declaration only a condition the literals cannot
4714
- decide holds is not listed or counted, and is named with its condition under
4715
- "Only under a condition the source does not decide" (OpenProject's
4716
- `WorkPackage` showed a `has_many custom_comments` it does not have). A
4717
- called method the reader cannot read costs only itself, named as unread; a
4718
- literal hash argument binds as keywords (`validates_translation :summary,
4719
- presence: true`) and is read through `merge`, `slice`, `except` and
4720
- `reject`, so Consul's translated validations read as `presence` and `length`
4721
- under their condition, an unused `options = {}` no longer swallows the next
4722
- declaration, a parameter reassigned in a block keeps its own value, and what
4723
- a macro method declares inside a block it evaluates on another receiver
4724
- (`translation_class.instance_eval { validates ... }`) is named under
4725
- "Declared on another class" with the condition it runs under, and not
4726
- counted as the model's (Consul's `Proposal` read 20 validation lines, 10 of
4727
- them its translation class's). What a mixin hook declares (`def
4728
- self.included(base)` and the like, in `class_eval`, as `base.has_many`,
4729
- `base.scope` or `base.delegate`, or through a helper it calls) applies to
4730
- every includer, each hook only for its own way of mixing in (`included` on
4731
- include, `prepended` on prepend, `extended` on extend). A module the app
4732
- mixes into `ActiveRecord::Base` or `ApplicationRecord` from an initializer,
4733
- `lib`, `lib_static` or a plugin's `init.rb` (a reopened class,
4734
- `send(:include)`, an `on_load` hook, a required file) is every model's
4735
- mixin, read again on each run so `watch` and the MCP server see one added
4736
- later, a model's own `extend` of an app module is read the same way
4737
- (Canvas's `resolves_root_account` gives `belongs_to :root_account`), a
4738
- module the model's own file declares is not reported unread (OpenProject's
4739
- `WorkPackage` gains `journals`, `attachments` and `custom_values`), a mixin
4740
- in a path gem inside the repo is read like the app's own code (Canvas's
4741
- `gems/broadcast_policy` and `gems/workflow`), and one from a gem installed
4742
- outside the repo is named as unread. One rule answers which classes mix in a
4743
- module, for concerns, services and join tables alike, and a subclass
4744
- inherits what its parent's concerns call into (OpenProject's
4745
- `WorkPackage::InexistentWorkPackage` lacked the `journals` its parent has).
4840
+ `included do` block, as at runtime: every model that includes the module
4841
+ holding `acts_as_watchable` would otherwise show `watchers` and `favorites`.
4842
+ The macros are read with the call's literal arguments (a parameter the
4843
+ method reassigns or a block shadows keeps its own value), taking the `if`,
4844
+ `unless` or `case` branch they decide (a private API app's own macro called
4845
+ with `:picture, has_one: true` gives `has_one :picture`, not a `name` pair);
4846
+ a declaration only a condition the literals cannot decide holds is not
4847
+ listed or counted, and is named with its condition under "Only under a
4848
+ condition the source does not decide" (a `WorkPackage` showed a `has_many
4849
+ custom_comments` it does not have). A called method the reader cannot read
4850
+ costs only itself, named as unread; a literal hash argument binds as
4851
+ keywords (`validates_translation :summary, presence: true`) and is read
4852
+ through `merge`, `slice`, `except` and `reject`, so translated validations
4853
+ read as `presence` and `length` under their condition, an unused `options =
4854
+ {}` no longer swallows the next declaration, a parameter reassigned in a
4855
+ block keeps its own value, and what a macro method declares inside a block
4856
+ it evaluates on another receiver (`translation_class.instance_eval {
4857
+ validates ... }`) is named under "Declared on another class" with the
4858
+ condition it runs under, and not counted as the model's (a `Proposal` read
4859
+ 20 validation lines, 10 of them its translation class's). What a mixin hook
4860
+ declares (`def self.included(base)` and the like, in `class_eval`, as
4861
+ `base.has_many`, `base.scope` or `base.delegate`, or through a helper it
4862
+ calls) applies to every includer, each hook only for its own way of mixing
4863
+ in (`included` on include, `prepended` on prepend, `extended` on extend). A
4864
+ module the app mixes into `ActiveRecord::Base` or `ApplicationRecord` from
4865
+ an initializer, `lib`, `lib_static` or a plugin's `init.rb` (a reopened
4866
+ class, `send(:include)`, an `on_load` hook, a required file) is every
4867
+ model's mixin, read again on each run so `watch` and the MCP server see one
4868
+ added later, a model's own `extend` of an app module is read the same way
4869
+ (`resolves_root_account` gives `belongs_to :root_account`), a module the
4870
+ model's own file declares is not reported unread (a `WorkPackage` gains
4871
+ `journals`, `attachments` and `custom_values`), a mixin in a path gem inside
4872
+ the repo is read like the app's own code (`gems/broadcast_policy` and
4873
+ `gems/workflow`), and one from a gem installed outside the repo is named as
4874
+ unread. One rule answers which classes mix in a module, for concerns,
4875
+ services and join tables alike, and a subclass inherits what its parent's
4876
+ concerns call into (`WorkPackage::InexistentWorkPackage` lacked the
4877
+ `journals` its parent has).
4746
4878
  - **Index hints follow how an index is used**, their clauses parted by
4747
4879
  semicolons so a clause listing several columns reads as one, each clause
4748
4880
  printed once. A column is `[indexed]` only when it leads an index and
@@ -4756,14 +4888,13 @@ about 8,500 lines, most of it reading what the gem used to guess.
4756
4888
  warns of a duplicate index only when the name `add_index` would generate
4757
4889
  exists.
4758
4890
  - **The `structure.sql` reader keeps an expression index key whole** and the
4759
- keys after it, and no longer reads `DESC`, `NULLS LAST` or an operator
4760
- class as a column: Discourse's `(COALESCE(parent_category_id,
4761
- '-1'::integer), name)` read as four columns with `name` lost. It also
4762
- reads each column's default, the way the `schema.rb` reader reports one
4763
- (a literal as its value, a cast string as the string, json and arrays as
4764
- Rails dumps them, an expression as `-> { "CURRENT_TIMESTAMP" }`, a serial
4765
- key's `nextval` as none): Discourse's schema had no default hints at all
4766
- and now has 583.
4891
+ keys after it, and no longer reads `DESC`, `NULLS LAST` or an operator class
4892
+ as a column: `(COALESCE(parent_category_id, '-1'::integer), name)` read as
4893
+ four columns with `name` lost. It also reads each column's default, the way
4894
+ the `schema.rb` reader reports one (a literal as its value, a cast string as
4895
+ the string, json and arrays as Rails dumps them, an expression as `-> {
4896
+ "CURRENT_TIMESTAMP" }`, a serial key's `nextval` as none): one app's schema
4897
+ had no default hints at all and now has 583.
4767
4898
  - **Both search backends return the same lines.** The Ruby fallback matched
4768
4899
  case-insensitively, searched only a list of extensions and read
4769
4900
  `excluded_paths` as path prefixes, so on a private API app one model name
@@ -4777,9 +4908,9 @@ about 8,500 lines, most of it reading what the gem used to guess.
4777
4908
  matches ignore patterns case-sensitively, leaves symlinks alone, walks the
4778
4909
  tree once without entering an ignored directory, and decides a binary file
4779
4910
  from its first 64 KiB: 14.5 s to 2.5 s on a private API app and 27.5 s to
4780
- 8.6 s on Canvas, with the same lines ripgrep returns. A ripgrep match in a
4781
- file that is not UTF-8 is reported as a match, where it came back as one
4782
- fake `error:0: invalid byte sequence` row.
4911
+ 8.6 s on one large app, with the same lines ripgrep returns. A ripgrep match
4912
+ in a file that is not UTF-8 is reported as a match, where it came back as
4913
+ one fake `error:0: invalid byte sequence` row.
4783
4914
  - **`rails_search_code` never answers "No matches" while matches exist.** A
4784
4915
  `limit` smaller than one match and its context lines held only context, so
4785
4916
  the page came back empty; it now starts at the next match and says so.
@@ -4807,67 +4938,66 @@ about 8,500 lines, most of it reading what the gem used to guess.
4807
4938
  test files, their helpers and `spec/factories`, not from whether a `spec/`
4808
4939
  directory exists. With no tests anywhere, the answer says so and names the
4809
4940
  framework the bundle holds (`no tests yet (rspec-rails in the bundle)`),
4810
- where it said the app ran rspec or minitest. Whitehall's and Plots2's
4811
- `spec/` hold only JavaScript tests, so minitest apps read as RSpec and
4812
- `rails_generate_test` wrote them an RSpec spec. `doctor` reads the same
4813
- answer (`minitest test suite found`, or `No test suite found` where it said
4814
- `No test directory found`), and an app that runs both is told to run `bundle
4815
- exec rspec` where it was told `rails test`.
4941
+ where it said the app ran rspec or minitest. Two minitest apps whose `spec/`
4942
+ holds only JavaScript tests read as RSpec, and `rails_generate_test` wrote
4943
+ them an RSpec spec. `doctor` reads the same answer (`minitest test suite
4944
+ found`, or `No test suite found` where it said `No test directory found`),
4945
+ and an app that runs both is told to run `bundle exec rspec` where it was
4946
+ told `rails test`.
4816
4947
  - **Tests are found and written where the app keeps them.**
4817
4948
  `rails_get_test_info(model:)` and `(controller:)` look in the directories
4818
- the app's tests live in, so Whitehall's `test/functional` and
4819
- `test/unit/app/models` tests are found, where the tool said "No test file
4820
- found" for files its own no-argument answer counted, and
4821
- `rails_generate_test` writes into the directory most of the app's model or
4822
- controller tests share, under the file-name suffix most of them use, falling
4823
- back to the convention when there are none, and names the subject's existing
4824
- test rather than writing a second one. A controller's lookup also lists the
4825
- system, feature, request and integration tests named for it or reaching its
4826
- routes, each labelled by kind (Mastodon's `AboutController` said "No test
4827
- file found" beside `spec/system/about_spec.rb`). A model's tests are looked
4828
- for only where the app keeps model tests: a private API app's
4829
- `rails_get_test_info(model: "User")` answered with a serializer spec sharing
4830
- the basename, where its model spec is `spec/models/user_model_spec.rb`. A
4831
- generated controller test asserts what each action answers, `create`,
4832
- `update` and `destroy` included, `respond_with` read by format and verb as
4833
- the responder answers and an early bare `return` counted as an answer (an
4834
- API `create` rendering `status: :created` or a `head :no_content` destroy no
4835
- longer gets the scaffold's redirect), read from its body and its
4836
- `respond_to` block for the format the test sends, the branch valid params
4837
- take where a save splits it (a redirect as `:redirect` with its target when
4838
- the test can name it, `head` as its status, a JSON or plain render with its
4839
- media type, a conditional or unreadable action only as not a server error,
4840
- with a comment saying why), where it asserted `:success` for Plots2's
4841
- redirecting `verify_email`. A controller under a gem's controller (Devise)
4842
- keeps its routed actions, unverified; RSpec controller specs include
4843
- `Devise::Test::ControllerHelpers` (Devise setup in `spec/support` counts), a
4844
- test of a Devise controller sets `devise.mapping` from Devise's route names,
4845
- a `Post` model gets `let(:post_record)` so the `post` request method is not
4846
- shadowed, a destroy test builds its record before reading its id, a redirect
4847
- target is asserted only when it names a route, and `rails_generate_test`
4848
- looks for an existing test where `rails_get_test_info` does. A namespaced
4849
- model is built from the controller's namespace outward (OpenFoodNetwork's
4850
- `LineItemsController` got a skip), an `on:` validation is checked in its
4851
- context and a conditioned one is skipped with its condition named, and a
4852
- glob segment is passed. It covers only the actions the controller has (its
4853
- own and inherited public actions, template-backed ones, and names a
4854
- `define_method` or a concern may build), naming each routed action it leaves
4855
- out, so a `resources` route no longer gets a `show` test for a controller
4856
- with no `show`. It passes every required path segment and builds its record
4857
- under its route parent (Whitehall's contacts under organisations raised
4858
- `UrlGenerationError`), leaves optional segments out, fills a custom segment
4859
- from the column of the same name on the record or its parent (Plots2's
4860
- `graph/file/:uid/:id` gets `uid: @csvfile.uid`), and skips with the segment
4861
- named when it cannot fill one; one written among an app's controller specs
4862
- is a `type: :controller` spec. A generated minitest controller test uses the
4863
- test class and data style (factories or fixtures) most of the app's
4949
+ the app's tests live in, so `test/functional` and `test/unit/app/models`
4950
+ tests are found, where the tool said "No test file found" for files its own
4951
+ no-argument answer counted, and `rails_generate_test` writes into the
4952
+ directory most of the app's model or controller tests share, under the
4953
+ file-name suffix most of them use, falling back to the convention when there
4954
+ are none, and names the subject's existing test rather than writing a second
4955
+ one. A controller's lookup also lists the system, feature, request and
4956
+ integration tests named for it or reaching its routes, each labelled by kind
4957
+ (an `AboutController` said "No test file found" beside
4958
+ `spec/system/about_spec.rb`). A model's tests are looked for only where the
4959
+ app keeps model tests: a private API app's `rails_get_test_info(model:
4960
+ "User")` answered with a serializer spec sharing the basename, where its
4961
+ model spec is `spec/models/user_model_spec.rb`. A generated controller test
4962
+ asserts what each action answers, `create`, `update` and `destroy` included,
4963
+ `respond_with` read by format and verb as the responder answers and an early
4964
+ bare `return` counted as an answer (an API `create` rendering `status:
4965
+ :created` or a `head :no_content` destroy no longer gets the scaffold's
4966
+ redirect), read from its body and its `respond_to` block for the format the
4967
+ test sends, the branch valid params take where a save splits it (a redirect
4968
+ as `:redirect` with its target when the test can name it, `head` as its
4969
+ status, a JSON or plain render with its media type, a conditional or
4970
+ unreadable action only as not a server error, with a comment saying why),
4971
+ where it asserted `:success` for a redirecting `verify_email`. A controller
4972
+ under a gem's controller (Devise) keeps its routed actions, unverified;
4973
+ RSpec controller specs include `Devise::Test::ControllerHelpers` (Devise
4974
+ setup in `spec/support` counts), a test of a Devise controller sets
4975
+ `devise.mapping` from Devise's route names, a `Post` model gets
4976
+ `let(:post_record)` so the `post` request method is not shadowed, a destroy
4977
+ test builds its record before reading its id, a redirect target is asserted
4978
+ only when it names a route, and `rails_generate_test` looks for an existing
4979
+ test where `rails_get_test_info` does. A namespaced model is built from the
4980
+ controller's namespace outward (a namespaced `LineItemsController` got a
4981
+ skip), an `on:` validation is checked in its context and a conditioned one
4982
+ is skipped with its condition named, and a glob segment is passed. It covers
4983
+ only the actions the controller has (its own and inherited public actions,
4984
+ template-backed ones, and names a `define_method` or a concern may build),
4985
+ naming each routed action it leaves out, so a `resources` route no longer
4986
+ gets a `show` test for a controller with no `show`. It passes every required
4987
+ path segment and builds its record under its route parent (contacts under
4988
+ organisations raised `UrlGenerationError`), leaves optional segments out,
4989
+ fills a custom segment from the column of the same name on the record or its
4990
+ parent (`graph/file/:uid/:id` gets `uid: @csvfile.uid`), and skips with the
4991
+ segment named when it cannot fill one; one written among an app's controller
4992
+ specs is a `type: :controller` spec. A generated minitest controller test
4993
+ uses the test class and data style (factories or fixtures) most of the app's
4864
4994
  controller tests use, each test's superclass read through Prism and followed
4865
4995
  through an app base class in the test tree (`::ActionController::TestCase`
4866
4996
  counts), and a model's fixtures are found under its pluralized class name or
4867
4997
  its `set_fixture_class` mapping (read from the test and spec helpers and
4868
4998
  support files), a namespaced model's under the names Rails gives them
4869
- (`admin_users(:one)`, where it wrote `admin/users(:one)`): Plots2's `Node`,
4870
- whose table is `node`, got a TODO beside a `nodes.yml`.
4999
+ (`admin_users(:one)`, where it wrote `admin/users(:one)`): a `Node`, whose
5000
+ table is `node`, got a TODO beside a `nodes.yml`.
4871
5001
  - **`rails_get_test_info` counts the examples a test file runs, and lists
4872
5002
  exactly those** (`it`, `specify`, `scenario`, `test`, `def test_*`,
4873
5003
  one-liners too, never a `describe` or `context` line: a private API app's
@@ -4883,20 +5013,19 @@ about 8,500 lines, most of it reading what the gem used to guess.
4883
5013
  one, and the gems payload says which source answered
4884
5014
  (`declared_ruby_version_source`).
4885
5015
  - **The Rails version is read from `railties`** when an app bundles it without
4886
- the `rails` gem, as Discourse does.
5016
+ the `rails` gem.
4887
5017
  - **`--no-boot` reads the `config/database.yml` section for `RAILS_ENV` or
4888
5018
  `RACK_ENV`**, where it always read `development`.
4889
- - **Code in an in-repo engine, plugin or module is read like `app/`**:
4890
- models, controllers, views, concerns, services, jobs, mailers, helpers and
5019
+ - **Code in an in-repo engine, plugin or module is read like `app/`**: models,
5020
+ controllers, views, concerns, services, jobs, mailers, helpers and
4891
5021
  serializers under `plugins/*`, `modules/*` and `gems/plugins/*`. A view
4892
5022
  there reaches `rails_get_view`, `rails_get_partial_interface`,
4893
- `rails_get_turbo_map` and the view lookups, and can be read by path
4894
- (OpenProject: 318 to 492 templates). A directory
4895
- counts when it has its own `app/` with a Rails directory in it and a
4896
- gemspec, a `plugin.rb` or an `engine.rb` says Ruby loads it. Discourse goes
4897
- from 223 to 359 models and OpenProject from 169 to 274, and
4898
- `rails_get_schema` stops calling a plugin's table model-less and pointing at
4899
- the Gemfile.
5023
+ `rails_get_turbo_map` and the view lookups, and can be read by path (one
5024
+ app: 318 to 492 templates). A directory counts when it has its own `app/`
5025
+ with a Rails directory in it and a gemspec, a `plugin.rb` or an `engine.rb`
5026
+ says Ruby loads it. One app goes from 223 to 359 models and another from 169
5027
+ to 274, and `rails_get_schema` stops calling a plugin's table model-less and
5028
+ pointing at the Gemfile.
4900
5029
  - **A model reads the table prefix its namespace gives it**, from a `def
4901
5030
  self.table_name_prefix` on the namespace or an engine's `isolate_namespace`,
4902
5031
  as Rails' `full_table_name_prefix` does; an explicit `self.table_name =`
@@ -4905,13 +5034,12 @@ about 8,500 lines, most of it reading what the gem used to guess.
4905
5034
  `config.active_record`'s), and a derived name takes
4906
5035
  `config.active_record.table_name_prefix` and `_suffix` when nothing nearer
4907
5036
  sets one. A class nested in a concrete model takes that model's singular
4908
- table as a prefix, as Rails' `compute_table_name` does: OpenProject's
4909
- `Project::Phase` reads `project_phases` where it read `phases`, and its
4910
- `Principal`, `User`, `Group` and four more read `users` where they read
4911
- `principals`. 51 of OpenFoodNetwork's models (its Spree models and their
4912
- calculators) read `orders` where the table is `spree_orders`, and
4913
- Discourse's `DiscourseRssPolling::RssFeed` read `rss_feeds`, so
4914
- `rails_get_schema` called their tables model-less.
5037
+ table as a prefix, as Rails' `compute_table_name` does: a `Project::Phase`
5038
+ reads `project_phases` where it read `phases`, and its `Principal`, `User`,
5039
+ `Group` and four more read `users` where they read `principals`. 51 of one
5040
+ app's models (its engine models and their calculators) read `orders` where
5041
+ the table is `shop_orders`, and a plugin's `RssPolling::RssFeed` read
5042
+ `rss_feeds`, so `rails_get_schema` called their tables model-less.
4915
5043
  - **`rails_get_schema` names the gem that owns a table with no model file**,
4916
5044
  one line per gem the lockfile or Gemfile has (good_job, doorkeeper,
4917
5045
  friendly_id, closure_tree, paper_trail, acts-as-taggable-on, pghero, the
@@ -4925,33 +5053,31 @@ about 8,500 lines, most of it reading what the gem used to guess.
4925
5053
  suffix reads as the table it names, so a join table is not called model-less
4926
5054
  either.
4927
5055
  - **A migration's literal `execute("DROP TABLE ...")` drops the table from the
4928
- replayed schema**, MySQL's backtick-quoted names included. OpenProject's
5056
+ replayed schema**, MySQL's backtick-quoted names included. An app's
4929
5057
  `meeting_contents` and `meeting_content_journals` were listed, and four
4930
5058
  missing-index suggestions came with them.
4931
5059
  - **Jobs are read from `app/jobs`, `app/workers` and `app/sidekiq` alike.** An
4932
5060
  app with its workers in the Sidekiq 7 generator directory, or its ActiveJob
4933
- classes in `app/workers`, answered "No jobs found" (Whitehall has 31,
4934
- OpenProject 103 with its modules). A class is a worker when a Sidekiq mixin
4935
- is anywhere up its chain and a job when its chain reaches ActiveJob,
4936
- wherever it lives, so a Sidekiq job under `app/jobs` shows the queue its
4937
- `sidekiq_options` names rather than the default. An abstract base, one
4938
- another job inherits from and whose name says it is a base (`Jobs::Base`,
4939
- `ApplicationWorker`), is no longer counted as a job, in either tier. A job
4940
- is named by the constant its file declares
4941
- when that names its own namespace: Discourse's read `Regular::AnonymizeUser`
4942
- for `Jobs::AnonymizeUser`.
5061
+ classes in `app/workers`, answered "No jobs found" (one app has 31, another
5062
+ 103 with its modules). A class is a worker when a Sidekiq mixin is anywhere
5063
+ up its chain and a job when its chain reaches ActiveJob, wherever it lives,
5064
+ so a Sidekiq job under `app/jobs` shows the queue its `sidekiq_options`
5065
+ names rather than the default. An abstract base, one another job inherits
5066
+ from and whose name says it is a base (`Jobs::Base`, `ApplicationWorker`),
5067
+ is no longer counted as a job, in either tier. A job is named by the
5068
+ constant its file declares when that names its own namespace: one app's read
5069
+ `Regular::AnonymizeUser` for `Jobs::AnonymizeUser`.
4943
5070
  - **A mailer is listed wherever it lives and whatever it declares.** One
4944
- outside `app/mailers` is found by its parent chain anywhere under `app/`
4945
- (Canvas keeps `Mailer < ActionMailer::Base` in `app/models` and got "_No
5071
+ outside `app/mailers` is found by its parent chain anywhere under `app/` (an
5072
+ app that keeps `Mailer < ActionMailer::Base` in `app/models` and got "_No
4946
5073
  mailers found._"), a chain is followed through the app's autoload roots to
4947
5074
  `ActionMailer::Base` or a gem mailer, a class under `app/mailers` whose
4948
- chain reaches no mailer is not one (Diaspora listed twelve header builders
5075
+ chain reaches no mailer is not one (one app listed twelve header builders
4949
5076
  with `Actions: set_headers`), and one with no action of its own is listed
4950
5077
  with the parent it takes its actions from and any class methods it has
4951
- (Whitehall's `MultiNotifications`, Consul's `DeviseMailer` and Diaspora's
4952
- `DiasporaDeviseMailer`). A mailer a gem defines is not listed as the app's,
4953
- and an ActionMailer setting such as `mailer_name` is not a mailer's
4954
- interface.
5078
+ (`MultiNotifications`, a `DeviseMailer` and an `AppDeviseMailer`). A mailer
5079
+ a gem defines is not listed as the app's, and an ActionMailer setting such
5080
+ as `mailer_name` is not a mailer's interface.
4955
5081
  - **A by-name lookup answers the name it was given, or says it has no such
4956
5082
  thing** and suggests the closest, by Ruby's own spell checker over full
4957
5083
  names and last segments, where it offered whatever shared the first three
@@ -4976,30 +5102,29 @@ about 8,500 lines, most of it reading what the gem used to guess.
4976
5102
  gems put into every model, a booted mailer lists only the actions a mailer
4977
5103
  class defines, conventions claim model concerns only from a concerns file
4978
5104
  that is a module, and `rails_onboard` names Mongoid for a Mongoid app, where
4979
- Errbit read "on unknown".
5105
+ a Mongoid app read "on unknown".
4980
5106
  - **Neither tier lists a mailer's or a controller's action callbacks,
4981
5107
  predicates or bang methods as actions**, nor a controller method that needs
4982
5108
  an argument, which Rails could not dispatch. The booted tier listed what
4983
- `action_methods` returns; a private API app's `BaseController` listed its own
4984
- `before_action` target and a predicate. A controller lists the actions it
4985
- inherits from every app base, whatever the base is called and however its
4986
- name is qualified (OpenProject's
4987
- `CustomFields::Hierarchy::ItemsBaseController`, written inside
4988
- `Admin::Settings`), in its actions and its filter chain; an inherited public
4989
- method that no route names for the controller is a shared helper and not an
4990
- action (paper_trail's `info_for_paper_trail` read as an action of every EF
4991
- api controller), and an unrouted base controller lists only the actions its
4992
- subclasses are routed for, a controller under `app/controllers` no route
4993
- names lists only what it defines itself, a controller lists the public
4994
- actions an included app concern offers (Whitehall's
5109
+ `action_methods` returns; a private API app's `BaseController` listed its
5110
+ own `before_action` target and a predicate. A controller lists the actions
5111
+ it inherits from every app base, whatever the base is called and however its
5112
+ name is qualified (`CustomFields::Hierarchy::ItemsBaseController`, written
5113
+ inside `Admin::Settings`), in its actions and its filter chain; an inherited
5114
+ public method that no route names for the controller is a shared helper and
5115
+ not an action (paper_trail's `info_for_paper_trail` read as an action of
5116
+ every controller in a private API app), and an unrouted base controller
5117
+ lists only the actions its subclasses are routed for, a controller under
5118
+ `app/controllers` no route names lists only what it defines itself, a
5119
+ controller lists the public actions an included app concern offers (an
4995
5120
  `Admin::ContactTranslationsController` lacked `create`, `update` and
4996
5121
  `destroy`), and the booted tier drops a method that needs an argument and
4997
5122
  settles inherited actions by the routes, as the static tier does; a routed
4998
- action with a template and no method is an action (OpenFoodNetwork's
5123
+ action with a template and no method is an action (an
4999
5124
  `Admin::OrderCyclesController` lacked `incoming`, `outgoing` and
5000
5125
  `checkout_options`); and a file under `app/controllers` not named
5001
- `*_controller.rb` is a controller only when it subclasses one, so
5002
- OpenProject's Grape API classes stay out.
5126
+ `*_controller.rb` is a controller only when it subclasses one, so Grape API
5127
+ classes stay out.
5003
5128
  - **A Stimulus controller outside `app/javascript/controllers` is found**:
5004
5129
  under `app/webpacker`, `app/frontend`, `frontend`, `client`, component
5005
5130
  sidecars in `app/components`, and the same homes in packs and in-repo
@@ -5007,15 +5132,15 @@ about 8,500 lines, most of it reading what the gem used to guess.
5007
5132
  Outside the rails-new home a file counts only when it imports from
5008
5133
  `@hotwired/stimulus` or `stimulus`, or extends a class that does (an app
5009
5134
  file reached relatively or through the app's alias table, or an installed
5010
- Stimulus package), so an AngularJS `*.controller.js`, OpenProject's Angular
5011
- `activity-base.controller.ts` or a React `*_controller.tsx` does not. Forem
5012
- read 1 of its 23, OpenFoodNetwork 0 of 73, OpenProject 0 of 161.
5135
+ Stimulus package), so an AngularJS `*.controller.js`, an Angular
5136
+ `activity-base.controller.ts` or a React `*_controller.tsx` does not. Three
5137
+ apps read 1 of 23, 0 of 73 and 0 of 161.
5013
5138
  - **Editing a Stimulus controller outside `app/javascript/controllers`, or a
5014
5139
  job in `app/workers` or `app/sidekiq`, invalidates the cached context**, so
5015
5140
  tools stop answering from stale data.
5016
- - **The app's architecture lists `stimulus` and `hotwire` wherever it keeps its
5017
- Stimulus controllers.** OpenFoodNetwork and OpenProject read as having none.
5018
- A React component named `*_controller.tsx` still does not count.
5141
+ - **The app's architecture lists `stimulus` and `hotwire` wherever it keeps
5142
+ its Stimulus controllers.** Two apps that keep them elsewhere read as having
5143
+ none. A React component named `*_controller.tsx` still does not count.
5019
5144
  - **`rails_get_stimulus` counts a view as a use only when its markup names the
5020
5145
  controller** in a `data-controller` attribute, a target, an action or a
5021
5146
  `content_controller`-style helper. An i18n key or a word in prose with the
@@ -5031,8 +5156,8 @@ about 8,500 lines, most of it reading what the gem used to guess.
5031
5156
  - **A markup comment is not markup.** A HAML `-#` comment, with the lines
5032
5157
  indented under it, and an ERB `<%# %>` comment are no longer read for
5033
5158
  partials, instance variables, Stimulus identifiers, slots, fields or helper
5034
- calls: OpenFoodNetwork's `create_linked_variant.turbo_stream.haml` listed a
5035
- partial named `the`, from `-# Pre-render the form`.
5159
+ calls: a `create_linked_variant.turbo_stream.haml` listed a partial named
5160
+ `the`, from `-# Pre-render the form`.
5036
5161
  - **A `render` call's partial is read with or without parentheses**, wherever
5037
5162
  its keyword sits: `render(partial: 'x')`, `render 'x'`, `render(template:
5038
5163
  'x')`, `render collection: @items, partial: 'x'`, the `:partial =>` form and
@@ -5042,14 +5167,14 @@ about 8,500 lines, most of it reading what the gem used to guess.
5042
5167
  partial, and an escaped quote in an earlier argument no longer hides it; a
5043
5168
  turbo frame id or stream name holding a comma inside a string is read whole,
5044
5169
  and a Stimulus `static values` default holding a brace, a paren or a regex
5045
- literal is read whole, in source order. OpenFoodNetwork's views gained 51
5046
- partial references and Consul's 23.
5170
+ literal is read whole, in source order. One app's views gained 51 partial
5171
+ references and another's 23.
5047
5172
  - **A layout's yields are read in ERB, HAML and Slim**: the unnamed one as
5048
5173
  `(main)`, `yield(:x)`, `(yield :x).presence` and `content_for?(:x)`, read
5049
5174
  only where Ruby runs (ERB tags, HAML code lines and attribute hashes, Slim
5050
5175
  code and attribute values), and a `content_for` that sets content, with a
5051
- block or a value, parenthesised or not, is not a yield. Canvas's six layouts
5052
- reported none.
5176
+ block or a value, parenthesised or not, is not a yield. One app's six
5177
+ layouts reported none.
5053
5178
  - **A Stimulus action descriptor is read with its options, event filters and
5054
5179
  neighbours** (`click->x#open:prevent`, `keydown.enter->`, `resize@window->`,
5055
5180
  several descriptors in one `data-action`), read only from an action value (a
@@ -5057,14 +5182,14 @@ about 8,500 lines, most of it reading what the gem used to guess.
5057
5182
  Ruby variable such as `confirm_actions:` or `hiddenFieldAction:`), so an
5058
5183
  apostrophe in page text no longer hides the actions after it, and prose, a
5059
5184
  Ruby comment or a Rails route list (`%w[posts#create]`) confirms nothing,
5060
- where OpenFoodNetwork's `bulk-form` and `modal` uses were missed.
5185
+ where one app's `bulk-form` and `modal` uses were missed.
5061
5186
  - **`url_for(controller: "x")` is not a Stimulus controller** in view answers.
5062
5187
  - **`data-turbo` counts read the HAML and Ruby-hash spellings**
5063
5188
  (`"data-turbo": "false"`, `'data-turbo' => 'false'`), not only
5064
5189
  `data-turbo="false"`.
5065
5190
  - **A model whose `inclusion: { in: [...] }` list mixes strings with `nil` no
5066
5191
  longer fails** `rails_get_model_details` and `rails_context` with
5067
- `ArgumentError: comparison of String with nil failed`. Whitehall's `Edition`
5192
+ `ArgumentError: comparison of String with nil failed`. One app's `Edition`
5068
5193
  and its 13 STI subclasses failed.
5069
5194
  - **A validation or callback inside `with_options` carries the block's
5070
5195
  options**, so a validation scoped `on: :publish` no longer reads as always
@@ -5078,8 +5203,8 @@ about 8,500 lines, most of it reading what the gem used to guess.
5078
5203
  as the file wrote it.
5079
5204
  - **A Mongoid app counts `embeds_many` and `embeds_one` as associations
5080
5205
  everywhere, and lists only documents as models.** `rails_dependency_graph`
5081
- draws their edges, and Errbit's `App` read "1 association" beside 5 edges; a
5082
- plain Ruby class under `app/models` (Errbit's `ErrorReport` and `Issue`) is
5206
+ draws their edges, and one app's `App` read "1 association" beside 5 edges;
5207
+ a plain Ruby class under `app/models` (such as `ErrorReport` or `Issue`) is
5083
5208
  not a model.
5084
5209
  - **Both tiers list every declared validation the same way, with its
5085
5210
  conditions**: `validates_with`, `validates_date`, `validates_translation`
@@ -5100,17 +5225,17 @@ about 8,500 lines, most of it reading what the gem used to guess.
5100
5225
  validation whose attribute is computed, as in a loop, says so, and a custom
5101
5226
  `validate :method` prints its `on:`, `if:` or `unless:` condition in both
5102
5227
  tiers. A private API app's booted `Document` dropped its `if:` conditions
5103
- and printed its `validates_with` line as `document on`, the static tier
5104
- left `validates_with` out (Plots2's `User` read 0 validations), and a
5228
+ and printed its `validates_with` line as `document on`, the static tier left
5229
+ `validates_with` out (one app's `User` read 0 validations), and a
5105
5230
  hand-written presence rule on a `belongs_to` name was labelled implicit.
5106
5231
  - **An inheritance column set to `nil`, `false` or `:_type_disabled` reads as
5107
5232
  STI off**, where the conventions claimed STI. A validator option set to
5108
5233
  `false` (`presence: false`) is no validation, in both tiers; a class's own
5109
5234
  `belongs_to_required_by_default` beats the app's and passes to its
5110
- subclasses (OpenFoodNetwork's `Subscription` showed 7 implicit presence
5111
- lines); a computed enum prints its source marked computed, and a frozen
5112
- literal one its values; and one `after_commit` declared for several events
5113
- is one callback showing its events.
5235
+ subclasses (a `Subscription` showed 7 implicit presence lines); a computed
5236
+ enum prints its source marked computed, and a frozen literal one its values;
5237
+ and one `after_commit` declared for several events is one callback showing
5238
+ its events.
5114
5239
  - **`rails_validate` judges only the `has_many` associations the model's file
5115
5240
  declares**, and treats encrypted attributes (`has_encrypted`, `encrypts`,
5116
5241
  `attr_encrypted`) and store keys as attributes with no column of their own,
@@ -5124,63 +5249,62 @@ about 8,500 lines, most of it reading what the gem used to guess.
5124
5249
  two associations of one kind to one class each get an arrow labelled by
5125
5250
  name; and a through association that names no association of its model, or
5126
5251
  whose name is computed at run time, draws no node and is named in a note
5127
- saying which: Consul's `Budget` pointed at a top-level `Investment`,
5128
- `Ballot`, `Group` and `Heading`, and OpenFoodNetwork's `Spree::Order` at
5129
- `Payment`, `Shipment` and `Adjustment`. It draws a through edge per join
5130
- model, where Mastodon's `Account` lost its through `Follow`, `Block` and
5131
- `Mute` edges behind the first one; a through association with `source_type:`
5132
- points at the class `source_type` names; and the "pass model:" hint is left
5133
- out when a model was passed.
5252
+ saying which: a `Budget` pointed at a top-level `Investment`, `Ballot`,
5253
+ `Group` and `Heading`, and a `Shop::Order` at `Payment`, `Shipment` and
5254
+ `Adjustment`. It draws a through edge per join model, where an `Account`
5255
+ lost its through `Follow`, `Block` and `Mute` edges behind the first one; a
5256
+ through association with `source_type:` points at the class `source_type`
5257
+ names; and the "pass model:" hint is left out when a model was passed.
5134
5258
  - **A block callback a concern declares is kept beside the model's own block
5135
- callback of the same kind**: Discourse's `Topic` lost the `after_create do`
5136
- of `RateLimiter::OnCreateRecord`.
5259
+ callback of the same kind**: a `Topic` lost the `after_create do` of
5260
+ `RateLimiter::OnCreateRecord`.
5137
5261
  - **A static route honours the action its `action:` option names** (`get
5138
5262
  :delete, action: :deletion_dialog` read as `delete`).
5139
5263
  - **A component is typed by following its superclass chain through the app's
5140
5264
  own base classes**, so one two or more steps from `ViewComponent::Base` or a
5141
- Phlex base is no longer "unknown", and a sidecar file that reopens its parent
5142
- namespace is named for the class the file is named for. A component nested
5143
- in a module is listed by its full constant (`Admin::RowComponent`, where
5144
- three different classes all read `RowComponent`), which renames most of
5145
- OpenProject's catalog.
5265
+ Phlex base is no longer "unknown", and a sidecar file that reopens its
5266
+ parent namespace is named for the class the file is named for. A component
5267
+ nested in a module is listed by its full constant (`Admin::RowComponent`,
5268
+ where three different classes all read `RowComponent`), which renames most
5269
+ of a large app's catalog.
5146
5270
  - **A concern that nests its own `ActiveModel::Validator` is a concern**, not
5147
5271
  a validator, and every surface that lists concerns or helpers names each one
5148
- by the constant its file declares, in-repo plugins included (Discourse's
5149
- `DiscourseSubscriptions::Group` read `Group`, and
5150
- `rails_get_helper_methods(helper: "DiscourseChatIntegration::Helper")`
5151
- answered not found): `rails_get_active_support` read `Featurable` for
5152
- `Edition::Featurable`, and an inflected namespace (`SDG::TagList` in
5153
- `concerns/sdg/tag_list.rb`) is no longer spelled `Sdg::TagList`.
5272
+ by the constant its file declares, in-repo plugins included
5273
+ (`Subscriptions::Group` read `Group`, and `rails_get_helper_methods(helper:
5274
+ "ChatIntegration::Helper")` answered not found): `rails_get_active_support`
5275
+ read `Featurable` for `Edition::Featurable`, and an inflected namespace
5276
+ (`SDG::TagList` in `concerns/sdg/tag_list.rb`) is no longer spelled
5277
+ `Sdg::TagList`.
5154
5278
  - **A module that extends `ActiveSupport::Concern` is a concern wherever the
5155
5279
  app autoloads it from**, and the default `excluded_concerns` patterns each
5156
5280
  match a whole namespace, so an app module named only like a framework's
5157
- (Whitehall's `ActiveRecordLikeInterface`) is listed, not hidden; listed with
5158
- its path under a type naming its root (`Service Concerns`), in
5159
- `rails_get_concern` and `rails_get_active_support` alike: OpenProject read
5160
- 82 concerns and has 329, Canvas 9 and 26, a private API app none and 2.
5281
+ (`ActiveRecordLikeInterface`) is listed, not hidden; listed with its path
5282
+ under a type naming its root (`Service Concerns`), in `rails_get_concern`
5283
+ and `rails_get_active_support` alike: one app read 82 concerns and has 329,
5284
+ another 9 and 26, a private API app none and 2.
5161
5285
  - **A concern's includes and methods are its own body's**, not those of
5162
- classes nested in its file: Huginn's `LiquidDroppable` read `Includes:
5163
- Enumerable` and its nested `Drop` classes' `to_s`, `each` and `as_json`, and
5286
+ classes nested in its file: a `LiquidDroppable` read `Includes: Enumerable`
5287
+ and its nested `Drop` classes' `to_s`, `each` and `as_json`, and
5164
5288
  `Agents::ImapFolderAgent` was credited with `Scrubbed`. A concern whose
5165
5289
  methods are all private reads `0 public methods (N private)` and lists them,
5166
5290
  where it read `0 methods`.
5167
5291
  - **A concern's "Included By" lists the classes whose `include` resolves to
5168
5292
  it** from their own namespace, each once, named by its constant, searching
5169
- every autoload root and naming them when it finds none: Mastodon's
5293
+ every autoload root and naming them when it finds none: an
5170
5294
  `Admin::ExportControllerConcern` listed 9 includers, 7 of them including
5171
- `Settings::ExportControllerConcern`, and Discourse listed
5295
+ `Settings::ExportControllerConcern`, and another app listed
5172
5296
  `SubscriptionsController` twice.
5173
5297
  - **Component previews are read from the directories the config sets**
5174
5298
  (`view_component.previews.paths`, `preview_paths`, `preview_path`) as well
5175
5299
  as the defaults, and a preview links to its component by path or by the one
5176
- component it renders: OpenProject read no component with a preview, and 39
5177
- have one.
5300
+ component it renders: one app read no component with a preview, and 39 have
5301
+ one.
5178
5302
  - **A boot error no longer repeats in full under every answer.** The footer
5179
5303
  and each `[UNAVAILABLE]` note keep its first 140 characters, and the whole
5180
- error stays in the startup banner and `doctor`. On Discourse it was 61% of
5181
- the output.
5304
+ error stays in the startup banner and `doctor`. On one app it was 61% of the
5305
+ output.
5182
5306
  - **`rails_get_view` does not count a partial under `app/views/layouts` as a
5183
- layout.** Forem read 19 layouts where it has 4.
5307
+ layout.** One app read 19 layouts where it has 4.
5184
5308
  - **An abstract base is not counted as a service, a job or a mailer.** A class
5185
5309
  other classes inherit from and whose name says it is a base
5186
5310
  (`ApplicationService`, `BaseService`, `Jobs::Base`, `ApplicationMailer`)
@@ -5192,14 +5316,13 @@ about 8,500 lines, most of it reading what the gem used to guess.
5192
5316
  declaration: a job base its queue, options, retry policy, mixins, throttle
5193
5317
  and callbacks, a mailer base its layout, helpers, defaults, callbacks and
5194
5318
  mixins as the file writes them, and either one the methods it defines and
5195
- the classes that inherit it. Mastodon's generated files listed `BaseService`
5196
- as a service, and Consul's and OpenProject's `ApplicationMailer` helpers
5197
- read as emails.
5319
+ the classes that inherit it. One app's generated files listed `BaseService`
5320
+ as a service, and two apps' `ApplicationMailer` helpers read as emails.
5198
5321
  - **A class file is named by the constant it declares.** A model, job or
5199
5322
  service whose file declares a namespaced constant at a path that does not
5200
5323
  spell it is listed under that constant, where it was dropped or named from
5201
- the path: Discourse's `DiscourseGithubPlugin::GithubCommit` and six other
5202
- plugin models were missing, and `Jobs::DiscourseChatAddTypeField` read
5324
+ the path: a plugin's `GithubPlugin::GithubCommit` and six other plugin
5325
+ models were missing, and `Jobs::ChatAddTypeField` read
5203
5326
  `Onceoff::AddTypeField`.
5204
5327
  - **A bare superclass resolves from the enclosing namespace outward**, as
5205
5328
  Ruby resolves it, so `module Bim::Bcf; class NonExistentComment < Comment`
@@ -5235,7 +5358,7 @@ about 8,500 lines, most of it reading what the gem used to guess.
5235
5358
  `graphql_name` is concrete, and a base is one others inherit from, or one
5236
5359
  named like a base that subclasses graphql-ruby and adds nothing to the
5237
5360
  schema. The enums, inputs, scalars and connections that subclass
5238
- graphql-ruby directly still count (Canvas: 176 types, 102 mutations). An
5361
+ graphql-ruby directly still count (one app: 176 types, 102 mutations). An
5239
5362
  app with no `queries/` directory is pointed at its query root file rather
5240
5363
  than told "0 queries".
5241
5364
  - **Middleware changes are read wherever the app makes them**: through
@@ -5247,17 +5370,17 @@ about 8,500 lines, most of it reading what the gem used to guess.
5247
5370
  and `config/environments/*.rb` as well as the initializers. A class the app
5248
5371
  owns is listed with its file, under the name its file declares, wherever the
5249
5372
  app autoloads it from (`lib/middleware`, `app/lib/middlewares`) or wherever
5250
- its config or initializers declare it with a `call` method (Mastodon's
5251
- `TelemetryLoggingMiddleware`, Discourse's `Discourse::Cors`);
5252
- `config.exceptions_app` is named apart from the stack; one it does not own
5253
- is listed under "Stack changes from the app's config" as inserted, moved,
5254
- swapped in or removed, each with the file that does it. Discourse read no
5255
- custom middleware and 1 insertion; it has 16 classes and 27 stack changes
5256
- (19 inserted, 2 swapped in, 1 moved, 5 removed).
5373
+ its config or initializers declare it with a `call` method (a
5374
+ `TelemetryLoggingMiddleware`, an `App::Cors`); `config.exceptions_app` is
5375
+ named apart from the stack; one it does not own is listed under "Stack
5376
+ changes from the app's config" as inserted, moved, swapped in or removed,
5377
+ each with the file that does it. One app read no custom middleware and 1
5378
+ insertion; it has 16 classes and 27 stack changes (19 inserted, 2 swapped
5379
+ in, 1 moved, 5 removed).
5257
5380
  - **The autoload roots include the ones `config/application.rb` adds**
5258
5381
  (`autoload_paths`, `eager_load_paths`, `autoload_lib`, `config.paths.add`),
5259
5382
  when they are written as literal paths inside the app, so a class there is
5260
- found as the app's own. OpenProject keeps middleware in `lib_static`.
5383
+ found as the app's own. An app can keep middleware in `lib_static`.
5261
5384
 
5262
5385
  ### Removed
5263
5386
 
@@ -5851,12 +5974,12 @@ filing.
5851
5974
 
5852
5975
  ### Fixed
5853
5976
 
5854
- Defects found by a ninth QA round of v5.25.0 against Mastodon (issues #160
5855
- to #181), the sibling defects behind them, and what nine review rounds and
5856
- repeated from-scratch verification found in the fixes themselves. Most of the
5857
- entries below are older than the twenty-two reports; the reports are where the
5858
- looking started. One entry is a consumer-visible key rename and is under
5859
- Changed.
5977
+ Defects found by a ninth QA round of v5.25.0 against a large open-source app
5978
+ (issues #160 to #181), the sibling defects behind them, and what nine review
5979
+ rounds and repeated from-scratch verification found in the fixes themselves.
5980
+ Most of the entries below are older than the twenty-two reports; the reports
5981
+ are where the looking started. One entry is a consumer-visible key rename and
5982
+ is under Changed.
5860
5983
 
5861
5984
  - **`excluded_association_names` was ignored everywhere except the booted
5862
5985
  ActiveRecord path.** The key had one call site, inside the reflection-only
@@ -5893,8 +6016,8 @@ Changed.
5893
6016
  `after_rollback`.** `after_create_commit :x` and `after_commit :y, on:
5894
6017
  :create` run at the same point, and the gem keeps each declaration as the
5895
6018
  file wrote it, but the synthesized key was missing from the execution order
5896
- so it sorted to the end. Mastodon's `Status` printed the second half of
5897
- three events after the rollback callbacks.
6019
+ so it sorted to the end. A `Status` model printed the second half of three
6020
+ events after the rollback callbacks.
5898
6021
  - **Callbacks were named after the wrong thing, and `around` callbacks
5899
6022
  disappeared.** The concern section re-parsed the file with a line regex
5900
6023
  whose colon was optional, so `after_create do` printed as `after_create :do`
@@ -5914,7 +6037,7 @@ Changed.
5914
6037
  section. It walks the source with the same listener the model and callback
5915
6038
  tools use, and renders each declaration through the one helper they share.
5916
6039
  - **The static tier answered `0 assoc` for a model whose associations all live
5917
- in concerns.** It walked the model file alone, so Mastodon's `Account`
6040
+ in concerns.** It walked the model file alone, so one `Account` model
5918
6041
  reported no associations where 68 are declared across the 21 concerns it
5919
6042
  includes, and nothing marked the gap. Both tiers now walk the included
5920
6043
  concern files with the same listeners and merge what they declare, tagged
@@ -5961,7 +6084,7 @@ Changed.
5961
6084
  empty string.** The static tier kept the key on presence alone, so
5962
6085
  `generate_test` wrote `it { is_expected.to have_many(:replies).dependent(:)
5963
6086
  }` into the file the tool tells you to paste, and that line does not parse;
5964
- the model details printed a blank `dependent:` for the same row. Mastodon
6087
+ the model details printed a blank `dependent:` for the same row. One app
5965
6088
  declares it nineteen times. A nil value is dropped now, the way the booted
5966
6089
  tier already dropped it, and `optional: false` and `polymorphic: false` keep
5967
6090
  their keys.
@@ -6015,7 +6138,7 @@ Changed.
6015
6138
  - **A second `validates` on the same attribute and kind was dropped.**
6016
6139
  `rails_get_model_details` collapsed validations on kind and attribute alone,
6017
6140
  so a model that validates one attribute twice under different conditions
6018
- lost the second rule; Mastodon's `Account` lost its local-username length
6141
+ lost the second rule; one `Account` model lost its local-username length
6019
6142
  limit and its `uri` exclusion. The options are part of the key now, and only
6020
6143
  a byte-identical repeat is collapsed.
6021
6144
  - **`rails_get_model_details` printed `validates` as the kind of a
@@ -6033,7 +6156,7 @@ Changed.
6033
6156
  to a class named nowhere, and a `[VERIFIED]` block for a model that does not
6034
6157
  exist. Two owners of the same association name also collapsed onto one
6035
6158
  entry. A class whose name is not the constant it lives at is no longer a
6036
- model, so Mastodon's model count drops from 117 to 114.
6159
+ model, so one app's model count drops from 117 to 114.
6037
6160
  - **A gem's model was written into `.ai-context.json` with an app path that
6038
6161
  does not exist.** Doorkeeper's three models were recorded at
6039
6162
  `app/models/doorkeeper/access_grant.rb` and friends, the invented path the
@@ -6052,14 +6175,14 @@ Changed.
6052
6175
  off the same file.
6053
6176
  - **The static tier derived every table from the file name alone.** An
6054
6177
  explicit `self.table_name =`, a `table_name_prefix` declared by the
6055
- enclosing module and an STI child's parent table were all ignored, so
6056
- Mastodon's `Admin::ActionLog` answered `action_logs` and rendered no
6057
- columns, and `admin_action_logs` had no model in the schema listing.
6058
- `TableName` reads those declarations out of source and the model walk
6059
- composes them in Rails' own order. The readers that turned a model name into
6060
- a table go through the same module now, so `rails_get_schema(table:
6061
- "Admin::ActionLog")` finds the table and `rails_migration_advisor` stops
6062
- rejecting `admin/action_logs` as an invalid name.
6178
+ enclosing module and an STI child's parent table were all ignored, so an
6179
+ `Admin::ActionLog` answered `action_logs` and rendered no columns, and
6180
+ `admin_action_logs` had no model in the schema listing. `TableName` reads
6181
+ those declarations out of source and the model walk composes them in Rails'
6182
+ own order. The readers that turned a model name into a table go through the
6183
+ same module now, so `rails_get_schema(table: "Admin::ActionLog")` finds the
6184
+ table and `rails_migration_advisor` stops rejecting `admin/action_logs` as
6185
+ an invalid name.
6063
6186
  - **`remove_column` advice could name an STI child's file.** The
6064
6187
  `ignored_columns` step took the first model recorded against the table, and
6065
6188
  every class in an STI family records the same one, so `AdminUser` could win
@@ -6091,23 +6214,22 @@ Changed.
6091
6214
  rather than guessed.
6092
6215
  - **A counter_cache suggestion named a model that does not exist.** The row
6093
6216
  derived the far side of a `has_many` from the association name, so `has_many
6094
- :remarks, class_name: "Comment"` sent the reader to a `Remark`, and
6095
- Mastodon's `Poll` named `Vote` and `Voter` for `PollVote`. It reads the
6096
- association's own `class_name` now, drops the row when no model in the app
6097
- answers it, skips a `has_many ... through:` because there is no `belongs_to`
6098
- on the far side to carry the counter, and names a polymorphic inverse by the
6099
- association's `:as` option.
6217
+ :remarks, class_name: "Comment"` sent the reader to a `Remark`, and a `Poll`
6218
+ named `Vote` and `Voter` for `PollVote`. It reads the association's own
6219
+ `class_name` now, drops the row when no model in the app answers it, skips a
6220
+ `has_many ... through:` because there is no `belongs_to` on the far side to
6221
+ carry the counter, and names a polymorphic inverse by the association's
6222
+ `:as` option.
6100
6223
  - **Two n+1 findings on one association printed as one line twice.** The
6101
6224
  controller and the action appeared only at full detail, so the default
6102
6225
  listing showed identical rows and the section count did not reconcile with
6103
6226
  what a reader could see. Every row carries its call site now.
6104
- - **"Orphaned tables" named tables that have a model.** On Mastodon the
6105
- warning listed seven tables of which one was real: four belong to Doorkeeper
6106
- and PgHero, whose models live in the gem, and two are
6107
- `has_and_belongs_to_many` join tables Rails builds no model for by design.
6108
- Join tables are recognised from the association records and left out, and
6109
- the line claims only what it can prove, that no model file in this app
6110
- declares the table.
6227
+ - **"Orphaned tables" named tables that have a model.** On one app the warning
6228
+ listed seven tables of which one was real: four belong to Doorkeeper and
6229
+ PgHero, whose models live in the gem, and two are `has_and_belongs_to_many`
6230
+ join tables Rails builds no model for by design. Join tables are recognised
6231
+ from the association records and left out, and the line claims only what it
6232
+ can prove, that no model file in this app declares the table.
6111
6233
  - **Routing concerns dropped their routes, and `with_options` and `module:`
6112
6234
  named controllers that do not exist.** A `concern :x do ... end` body was
6113
6235
  stored nowhere, so the controllers declared only inside it had no static
@@ -6115,17 +6237,17 @@ Changed.
6115
6237
  `concerns:` site with the scope as it stands there, the way Rails re-runs it
6116
6238
  on the mapper. `with_options` is an OptionMerger, not a routing method, so
6117
6239
  it became a frame of default options that every call inside merges under its
6118
- own - which is what stops six of Mastodon's account routes being filed under
6119
- a controller named `@:username`, and stops `with_options only: [:index]`
6120
- expanding a resource into all eight RESTful rows. Those defaults reach a
6121
- `namespace` or `scope` inside the block too, where a `module:` or `as:` from
6122
- them replaces the block's own name, as `defaults.merge!(options)` does in
6123
- the mapper. `module:` on a resource moves the controller and never the path,
6124
- so `admin/distributions` is `admin/announcements/distributions` and
6240
+ own - which is what stops six account routes being filed under a controller
6241
+ named `@:username`, and stops `with_options only: [:index]` expanding a
6242
+ resource into all eight RESTful rows. Those defaults reach a `namespace` or
6243
+ `scope` inside the block too, where a `module:` or `as:` from them replaces
6244
+ the block's own name, as `defaults.merge!(options)` does in the mapper.
6245
+ `module:` on a resource moves the controller and never the path, so
6246
+ `admin/distributions` is `admin/announcements/distributions` and
6125
6247
  `admin/terms_of_service/distributions` again. `as:` and `param:` on a
6126
6248
  resource are read too, so the new rows carry the helper name and the member
6127
6249
  segment the app actually has (`/users/:account_username`, not
6128
- `/users/:account_id`). On Mastodon the static total falls, 658 routes to 657
6250
+ `/users/:account_id`). On one app the static total falls, 658 routes to 657
6129
6251
  and 22 undisclosed dynamic constructs to 21: 69 real rows arrive and 76
6130
6252
  fabricated ones go.
6131
6253
  - **A collection route helper was named after the resource pluralized.**
@@ -6133,9 +6255,9 @@ Changed.
6133
6255
  :photos, as: :image` named it `images`, neither of which any app defines.
6134
6256
  Rails builds the collection name from the `as:` value, or the resource name,
6135
6257
  exactly as written, and appends `_index` when that name is already singular.
6136
- The static walker does the same now, so Mastodon's `resources :following`
6137
- reports `following_index`. A collection block under a singular `resource`
6138
- stays singular.
6258
+ The static walker does the same now, so `resources :following` reports
6259
+ `following_index`. A collection block under a singular `resource` stays
6260
+ singular.
6139
6261
  - **`controllers` with `detail:"full"` crashed on an app whose controller had
6140
6262
  two strong-params methods under a parent other than
6141
6263
  `ApplicationController`.** The grouping fingerprint sorted the hashes the
@@ -6172,7 +6294,7 @@ Changed.
6172
6294
  still taken as written.
6173
6295
  - **A controller with no public actions rendered as a name and a trailing
6174
6296
  dash.** The listing and `analyze_feature` now say `(no public actions)`; on
6175
- Mastodon that covers six base controllers.
6297
+ one app that covers six base controllers.
6176
6298
  - **A controller file the walk could not read was listed as a controller with
6177
6299
  no actions.** The listings rendered an entry carrying an error as `(no
6178
6300
  public actions)`, and the summary listing as `0 actions`, while asking for
@@ -6182,7 +6304,7 @@ Changed.
6182
6304
  in.** `rails_get_controllers(detail: "full")` built the heading from the
6183
6305
  first segment of the first member's name, so five top-level controllers were
6184
6306
  filed under `CustomCssController::*` and `OAuth::UserinfoController` under
6185
- `Api::*`; 38 of Mastodon's 309 controllers appeared only inside such a
6307
+ `Api::*`; 38 of one app's 309 controllers appeared only inside such a
6186
6308
  heading and their real constant was nowhere in the answer. The heading now
6187
6309
  names the longest namespace every member shares, or just the count when they
6188
6310
  share none, and the group lists every member by its full constant.
@@ -6198,7 +6320,7 @@ Changed.
6198
6320
  skips it, while the booted tier struck the same filter through. The record
6199
6321
  now carries the skip, one renderer draws the listing for the tool and the
6200
6322
  generated markdown, and an ancestor's own skip no longer reaches a child as
6201
- an inherited filter - Mastodon's Api::V1 controllers listed
6323
+ an inherited filter - one app's Api::V1 controllers listed
6202
6324
  `require_functional!` that way. A skip that names `only:` or `except:`
6203
6325
  covers those actions only, so an action the skip does not name still lists
6204
6326
  the filter it runs. A skip naming actions also keeps the filter in every
@@ -6207,13 +6329,12 @@ Changed.
6207
6329
  through as if it never ran.
6208
6330
  - **A per-action filter chain dropped a filter whose only evidence was a skip
6209
6331
  constrained to other actions.** The whole-controller answer kept it and
6210
- named the actions it loses, so one tool contradicted itself on one tier:
6211
- Mastodon's `Auth::RegistrationsController` said `check_self_destruct!
6212
- (skipped on: edit, update)` for the class and said nothing at all for `new`.
6213
- A skip whose `only:`/`except:` leaves the queried action alone now reads as
6214
- evidence the filter runs there, with no skipped tail on the row. A real
6215
- `if:`/`unless:` on the same name, on the class or on an ancestor, still
6216
- wins.
6332
+ named the actions it loses, so one tool contradicted itself on one tier: an
6333
+ `Auth::RegistrationsController` said `check_self_destruct! (skipped on:
6334
+ edit, update)` for the class and said nothing at all for `new`. A skip whose
6335
+ `only:`/`except:` leaves the queried action alone now reads as evidence the
6336
+ filter runs there, with no skipped tail on the row. A real `if:`/`unless:`
6337
+ on the same name, on the class or on an ancestor, still wins.
6217
6338
  - **A skip of an inherited filter inverted the per-action answer.** The booted
6218
6339
  payload read a `skip_before_action :authenticate!, only: [ :index ]` as a
6219
6340
  constraint on the filter itself, so `authenticate!` was named on the one
@@ -6260,7 +6381,7 @@ Changed.
6260
6381
  - **The full controller listing grouped unrelated controllers by the parent's
6261
6382
  source spelling.** The static walk stores a superclass as written, so two
6262
6383
  namespaces that each define a `BaseController` looked identical to the group
6263
- key while the rendered chain resolved it. On Mastodon,
6384
+ key while the rendered chain resolved it. On one app,
6264
6385
  `Admin::DashboardController` and seven `Settings::Exports` controllers
6265
6386
  shared one group headed by a constant the app does not define, and the seven
6266
6387
  were given a filter chain that is not theirs. The key and the `Inherits:`
@@ -6270,21 +6391,21 @@ Changed.
6270
6391
  - **The full controller listing copied the whole introspection payload once
6271
6392
  per controller.** Resolving a parent reached for the shared cache inside the
6272
6393
  loop, and every read of that cache deep-copies the entire context under a
6273
- mutex. On Mastodon's 309 controllers the `--detail full` listing made 608
6394
+ mutex. On an app with 309 controllers the `--detail full` listing made 608
6274
6395
  copies and spent 8.2 of its 8.6 seconds in them. The listing reads the
6275
6396
  payload once and passes it down. Rendered output is unchanged and the run
6276
6397
  takes 4.3 seconds.
6277
6398
  - **`rails_get_schema` and `rails_get_routes` copied the whole payload once
6278
6399
  per row.** Every read of the shared cache deep-copies the introspection
6279
6400
  payload under a mutex, and both listings read it inside their loop, over
6280
- tables and over controller headings. Mastodon's 116 tables cost 350 copies
6401
+ tables and over controller headings. One app's 116 tables cost 350 copies
6281
6402
  and 7.5 of an 8.1 second run; its 657 static routes cost 297 copies and 6.0
6282
6403
  of 6.4 seconds. Both read the payload once and pass it down now, and the
6283
6404
  runs take 5.5 and 4.3 seconds with the rendered output unchanged.
6284
6405
  - **The same copy-per-row shape was in `rails_get_callbacks` and a
6285
6406
  `rails_search_code` trace.** The full callback listing resolved the model
6286
6407
  file through the shared cache once per callback of every model, 152 copies
6287
- on Mastodon, and a trace read it once per controller file among the call
6408
+ on one app, and a trace read it once per controller file among the call
6288
6409
  sites, 70 for `current_user`. Each reads once for the whole listing now, and
6289
6410
  a single-controller `rails_get_controllers` request went from five reads to
6290
6411
  one.
@@ -6310,12 +6431,12 @@ Changed.
6310
6431
  - **Static `get_api` named serializers by camelizing the file path, so an app
6311
6432
  acronym came out miscased.** `ActivityPub::AcceptFollowSerializer` was
6312
6433
  reported as `Activitypub::AcceptFollowSerializer` and
6313
- `REST::AccountSerializer` as `Rest::AccountSerializer`; 137 of Mastodon's
6314
- 144 names were wrong. `camelize` reads the global inflector, and the static
6315
- tier never loads the app's `acronym` registrations. The list now comes from
6434
+ `REST::AccountSerializer` as `Rest::AccountSerializer`; 137 of one app's 144
6435
+ names were wrong. `camelize` reads the global inflector, and the static tier
6436
+ never loads the app's `acronym` registrations. The list now comes from
6316
6437
  `SourceScan.classes`, which reads the class each file declares, so it needs
6317
6438
  no inflector, covers packs and in-repo engines, and stops walking
6318
- `app/serializers/concerns` (Mastodon goes from 144 names to 143).
6439
+ `app/serializers/concerns` (that app goes from 144 names to 143).
6319
6440
  `MarkdownSerializer#api` writes this list into the generated context file,
6320
6441
  so the wrong names were committed into apps' context, not only printed. The
6321
6442
  jbuilder count in the same method now goes through `PathResolver.view_dirs`
@@ -6329,14 +6450,14 @@ Changed.
6329
6450
  app's.** `rails_migration_advisor` read `Rails.version` from its own
6330
6451
  process, so a standalone `--no-boot` run had no such constant and every
6331
6452
  migration came out as `ActiveRecord::Migration[7.1]`, while a run inside a
6332
- bundle reported whichever Rails the gem had loaded. Mastodon is on 8.1.3.1
6333
- and got 7.1, and that bracket is not cosmetic: it selects a migration
6334
- compatibility mode. The version now comes from the app's context, the same
6335
- hash the tool already reads for the database adapter, so both tiers stamp
6336
- the app's own major.minor. When neither the context nor a loaded Rails names
6337
- a version, the tool stamps its supported floor and says so in a Note line
6338
- above the code. That note states only what the tool observed, not a cause it
6339
- never checked.
6453
+ bundle reported whichever Rails the gem had loaded. An app on 8.1.3.1 got
6454
+ 7.1, and that bracket is not cosmetic: it selects a migration compatibility
6455
+ mode. The version now comes from the app's context, the same hash the tool
6456
+ already reads for the database adapter, so both tiers stamp the app's own
6457
+ major.minor. When neither the context nor a loaded Rails names a version,
6458
+ the tool stamps its supported floor and says so in a Note line above the
6459
+ code. That note states only what the tool observed, not a cause it never
6460
+ checked.
6340
6461
  - **`migration_advisor` printed an empty "Affected Models" section for any
6341
6462
  model whose table is not the camelized form of its class.** The section
6342
6463
  camelized the table back into a model name, so `Admin::ActionLog`
@@ -6359,11 +6480,10 @@ Changed.
6359
6480
  bearer-token TODO there too instead of a fixture `sign_in`.
6360
6481
  - **A generated request spec raised `UnknownAttributeError` before it could
6361
6482
  fail a validation.** `generate_test` built its placeholder record out of the
6362
- controller's permitted params, so on Mastodon's
6363
- `Api::V1::AccountsController` eight of the nine keys handed to
6364
- `Account.create!` were not columns. The record is built from the permitted
6365
- names that are columns, a TODO names the ones left out, and the request
6366
- params still carry every permitted name.
6483
+ controller's permitted params, so on an `Api::V1::AccountsController` eight
6484
+ of the nine keys handed to `Account.create!` were not columns. The record is
6485
+ built from the permitted names that are columns, a TODO names the ones left
6486
+ out, and the request params still carry every permitted name.
6367
6487
  - **`rails_generate_test` rendered an empty `describe "associations" do end`
6368
6488
  in the static tier.** The rspec branch matched the macro against
6369
6489
  `"belongs_to"` and its siblings while the static walk reported it as a
@@ -6396,7 +6516,7 @@ Changed.
6396
6516
  files, an app with no users fixture gets a TODO instead of a name, and an
6397
6517
  app with no fixtures gets `ModelName.new`.
6398
6518
  - **`get_test_info` counted only the spec directories a hardcoded list
6399
- named.** Mastodon reported 675 of its 1090 spec files and 6 of its 22 spec
6519
+ named.** One app reported 675 of its 1090 spec files and 6 of its 22 spec
6400
6520
  directories; `spec/lib`, `spec/workers`, `spec/serializers` and nine others
6401
6521
  had no row at all. Categories are now read from whatever the app keeps under
6402
6522
  `spec/` and `test/`: one row per top-level directory holding test files, a
@@ -6413,7 +6533,7 @@ Changed.
6413
6533
  word character, which also fixes `--exact-match` on `@user`, `match_type:
6414
6534
  "definition"` and `"class"` on predicate and bang names, trace mode listing
6415
6535
  `presave!` as a call site of `save!`, and method source lookup for `[]`,
6416
- `foo=` and `<=>`. Against Mastodon, `def reblog?` now reports the 3 lines
6536
+ `foo=` and `<=>`. Against one app, `def reblog?` now reports the 3 lines
6417
6537
  ripgrep finds instead of 6.
6418
6538
  - **`format: "json"` on `rails_get_schema` returned markdown.** The parameter
6419
6539
  is declared, schema-validated and accepted without a warning, but only a
@@ -6433,11 +6553,11 @@ Changed.
6433
6553
  when the cap was reached instead of passing the cap off as a total. The page
6434
6554
  size is sized in matches, the pagination hint and the per-file group
6435
6555
  headings carry the same two facts, and trace mode marks a caller list that
6436
- stopped at the cap. Against Mastodon, `def reblog?` with `--context-lines 5`
6437
- reports 3 matches rather than 30, and `def call` reports its 103 matches once
6438
- the context lines no longer fill the cap. At the default two lines of context
6439
- the same search reaches the cap at 40 and says so, which is the floor the cap
6440
- can honestly claim rather than a total.
6556
+ stopped at the cap. Against one app, `def reblog?` with `--context-lines 5`
6557
+ reports 3 matches rather than 30, and `def call` reports its 103 matches
6558
+ once the context lines no longer fill the cap. At the default two lines of
6559
+ context the same search reaches the cap at 40 and says so, which is the
6560
+ floor the cap can honestly claim rather than a total.
6441
6561
  - **A ripgrep run that failed, or that recovered from an unreadable file,
6442
6562
  answered wrong.** The exit status of the `rg` run was discarded, so a run
6443
6563
  that failed outright looked the same as a run that matched nothing, and a
@@ -6472,7 +6592,7 @@ Changed.
6472
6592
  - **Static `get_i18n` reported the locale files as the app's available
6473
6593
  locales.** Rails builds `available_locales` from `config/locales` only while
6474
6594
  the app leaves the setting alone; once it assigns
6475
- `config.i18n.available_locales`, that list is the answer. Mastodon ships
6595
+ `config.i18n.available_locales`, that list is the answer. One app ships
6476
6596
  files for 106 locales and enables 97, so nine disabled locales were listed
6477
6597
  as available, seven got their own coverage row, and asking for one of them
6478
6598
  by name answered with a full locale report. The static answer now reads the
@@ -6590,8 +6710,8 @@ Changed.
6590
6710
  uses, was not. The banner prints after the listener is running.
6591
6711
  - **An app that exits during boot answered nothing at all.** The binary set
6592
6712
  `RAILS_ENV` only when `--environment` was passed, so the documented default
6593
- never reached apps that read the variable in `config/boot.rb`. Mastodon
6594
- aborts there, and an abort during boot was the one failure mode the static
6713
+ never reached apps that read the variable in `config/boot.rb`. Some apps
6714
+ abort there, and an abort during boot was the one failure mode the static
6595
6715
  tier did not cover, so the run ended with exit 1 and an empty stdout. The
6596
6716
  binary fills in `development` when nothing else set it, and an initializer's
6597
6717
  `exit` degrades to the static tier like every other boot failure. The rake
@@ -6714,7 +6834,7 @@ Changed.
6714
6834
  `ENV \` followed by indented `KEY=value` lines declared no variables, and
6715
6835
  `ENV A=1 B=2` kept only the first name with the rest swallowed into its
6716
6836
  value. `env --detail full` now joins continuations into whole instructions
6717
- and gives each assignment its own row. Against Mastodon the Dockerfile
6837
+ and gives each assignment its own row. Against one app the Dockerfile
6718
6838
  section names all 13 `ENV` variables where it named none before.
6719
6839
  - **The gems payload said the app had no Ruby version.** The lockfile reader
6720
6840
  required exactly three spaces before the `ruby` line of the `RUBY VERSION`
@@ -6798,7 +6918,7 @@ Changed.
6798
6918
  itself with a number that did not reconcile with the files under `app/views`
6799
6919
  the way the payload listing's does.
6800
6920
  - **Generated context files never said they were produced without booting.** A
6801
- static run writes different numbers (111 models against 114 on Mastodon, 657
6921
+ static run writes different numbers (111 models against 114 on one app, 657
6802
6922
  routes against 723) and nothing in CLAUDE.md, AGENTS.md, the rules files or
6803
6923
  `.ai-context.json` let a reader tell which they had. The context records the
6804
6924
  tier it was answered in, every generated file with a header carries one
@@ -6878,7 +6998,7 @@ Changed.
6878
6998
  `ApplicationRecord` was not walked at all. The booted tier listed the base's
6879
6999
  concerns off the ancestor chain while the static tier listed none, which is
6880
7000
  two answers to one question. Both tiers now walk every class the model
6881
- inherits from, up to `ActiveRecord::Base`. On Mastodon that is 111 of 111
7001
+ inherits from, up to `ActiveRecord::Base`. On one app that is 111 of 111
6882
7002
  models naming `Remotable`, which `ApplicationRecord` includes, against 51
6883
7003
  before; the model count, the tables, the callbacks and the scopes are
6884
7004
  unchanged.
@@ -7032,13 +7152,13 @@ duplicated mechanisms behind them.
7032
7152
  lives, so one in `config/application.rb` or an environment file, which runs
7033
7153
  before the load, keeps them too.
7034
7154
  - **The static tier named the app after its directory.** `onboard` and every
7035
- generated context file were headed "mastodon" for an app that declares
7036
- `module Mastodon`. The name now comes from the module enclosing
7037
- `class Application < Rails::Application` in `config/application.rb`, and
7038
- falls back to the directory only when that file names nothing.
7155
+ generated context file were headed "social" for an app that declares `module
7156
+ Social`. The name now comes from the module enclosing `class Application <
7157
+ Rails::Application` in `config/application.rb`, and falls back to the
7158
+ directory only when that file names nothing.
7039
7159
  - **The static tier counted every class under `app/models` as a model.**
7040
7160
  Namespace modules, form objects, filters and plain service classes were
7041
- listed and rendered as models of a table (Mastodon answered 195 models
7161
+ listed and rendered as models of a table (one app answered 195 models
7042
7162
  against 117 booted). A static model is now a class whose superclass chain
7043
7163
  reaches `ApplicationRecord`, a namespaced `*ApplicationRecord` or
7044
7164
  `ActiveRecord::Base`, STI subclasses included.
@@ -7255,8 +7375,8 @@ duplicated mechanisms behind them.
7255
7375
 
7256
7376
  ### Fixed
7257
7377
 
7258
- Defects found by a QA round against GitLab, OpenProject, Canvas LMS,
7259
- Discourse and Mastodon. Every one of them exits 0.
7378
+ Defects found by a QA round against five large open-source Rails apps. Every
7379
+ one of them exits 0.
7260
7380
 
7261
7381
  - **The commands that read the app degrade the way `tool` does.** `context`,
7262
7382
  `inspect`, `facts`, `preset`, `watch` and `init` called a bare boot guard,
@@ -7270,18 +7390,18 @@ Discourse and Mastodon. Every one of them exits 0.
7270
7390
  the context writer asked `Rails.application` for the output directory,
7271
7391
  which raises `NameError` on the path where Rails is never loaded at all.
7272
7392
  - **`tool --list` reads the app's config when the app cannot boot.** Boot is
7273
- what normally loads `.rails-ai-context.yml`, so the listing fell back to
7274
- the gem's defaults: on Mastodon it advertised 45 tools while the MCP server
7275
- offered 43 and the CLI itself answered `Unknown tool 'query'` for one it
7276
- had just listed.
7277
- - **`search_extensions` is documented as the fallback's list, which is what
7278
- it is.** Making ripgrep honour it did make the two backends agree, and it
7279
- cost the reach that makes the tool useful: on Mastodon `gem 'devise'` went
7280
- from 5 results to none, because a Gemfile carries no listed extension - and
7281
- the same for a Rakefile, a `.md`, a `.sql`. The docs, the attr comment and
7282
- the line the install generator writes now say plainly that the list is the
7283
- Ruby fallback's and that ripgrep searches every file.
7284
- - **A table the replay cannot name is not reported.** Canvas has a migration
7393
+ what normally loads `.rails-ai-context.yml`, so the listing fell back to the
7394
+ gem's defaults: on one app it advertised 45 tools while the MCP server
7395
+ offered 43 and the CLI itself answered `Unknown tool 'query'` for one it had
7396
+ just listed.
7397
+ - **`search_extensions` is documented as the fallback's list, which is what it
7398
+ is.** Making ripgrep honour it did make the two backends agree, and it cost
7399
+ the reach that makes the tool useful: on one app `gem 'devise'` went from 5
7400
+ results to none, because a Gemfile carries no listed extension - and the
7401
+ same for a Rakefile, a `.md`, a `.sql`. The docs, the attr comment and the
7402
+ line the install generator writes now say plainly that the list is the Ruby
7403
+ fallback's and that ripgrep searches every file.
7404
+ - **A table the replay cannot name is not reported.** One app has a migration
7285
7405
  that calls `create_table table_name do |t|` with a local computed at run
7286
7406
  time; the replay kept the entry under a nil key, and the first serializer
7287
7407
  to sort the table names took the whole context run down. Reachable only
@@ -7290,7 +7410,7 @@ Discourse and Mastodon. Every one of them exits 0.
7290
7410
  `default_locale` the app configures but ships no file for, every locale
7291
7411
  scored zero and all of them were named as untranslated.
7292
7412
  - **A mixin in a nested concerns directory is not a model.** The skip only saw
7293
- the top-level `concerns/` that Rails autoloads, so OpenProject's
7413
+ the top-level `concerns/` that Rails autoloads, so one app's
7294
7414
  `app/models/queries/operators/concerns` contributed four mixins to a model
7295
7415
  count of 978 where the app has 974. A nested `concerns/` is an ordinary
7296
7416
  namespace, though, so the directory name alone does not decide it: a file
@@ -7302,42 +7422,42 @@ Discourse and Mastodon. Every one of them exits 0.
7302
7422
  A guard spec derives all three lists from `INTROSPECTOR_MAP`.
7303
7423
 
7304
7424
  - **A controller is named by the constant its source declares.** Zeitwerk
7305
- resolves a path through the app's own inflector, which the static tier
7306
- never loads, so camelizing the path invented `Activitypub::` for the 12
7307
- controllers Mastodon declares as `ActivityPub::` and `Oauth::` for the 4
7308
- it declares as `OAuth::` - names that appear nowhere in an app that
7309
- registers those acronyms, and a `NameError` for anyone who uses one. The
7310
- path stays the answer where the source does not carry the whole name
7425
+ resolves a path through the app's own inflector, which the static tier never
7426
+ loads, so camelizing the path invented `Activitypub::` for the 12
7427
+ controllers an app declares as `ActivityPub::` and `Oauth::` for the 4 it
7428
+ declares as `OAuth::` - names that appear nowhere in an app that registers
7429
+ those acronyms, and a `NameError` for anyone who uses one. The path stays
7430
+ the answer where the source does not carry the whole name
7311
7431
  (`application_cable/channel.rb`) and where the declared name does not name
7312
7432
  that file, since Prism recovers a syntax error into a partial tree.
7313
7433
  - **An interceptor is told from a mailer by its framework hook.**
7314
7434
  `app/mailers` is also where ActionMailer interceptors live, and every `.rb`
7315
- under it counted: OpenProject's `Interceptors::DefaultHeaders` arrived as a
7316
- mailer with `delivering_email` - a hook the framework calls - listed as an
7317
- email an agent could send, 2 of its 11 entries. What an interceptor has and
7318
- a mailer does not is one of `delivering_email`, `previewing_email` or
7319
- `delivered_email`; requiring a class instead would have dropped GitLab's 20
7435
+ under it counted: An `Interceptors::DefaultHeaders` arrived as a mailer with
7436
+ `delivering_email` - a hook the framework calls - listed as an email an
7437
+ agent could send, 2 of its 11 entries. What an interceptor has and a mailer
7438
+ does not is one of `delivering_email`, `previewing_email` or
7439
+ `delivered_email`; requiring a class instead would have dropped one app's 20
7320
7440
  `Emails::*` modules, which hold every notification it sends.
7321
- - **Migration replay reads what a migration does, not what it undoes.** A
7322
- `def down` says how to reverse the change, so replaying it alongside `up`
7441
+ - **Migration replay reads what a migration does, not what it undoes.** A `def
7442
+ down` says how to reverse the change, so replaying it alongside `up`
7323
7443
  cancelled the migration out. On a migrations-only app the ordinary
7324
- up-creates/down-drops pair erased two tables OpenProject really has (35
7325
- to 37), and the reverse pair would have invented one it dropped. The same
7326
- now holds for the block spellings, `reversible { |dir| dir.down { ... } }`
7327
- and `revert`, and for the `t.timestamps` inside them - those were found by
7328
- a separate walk over the whole file, so a down body's timestamps landed on
7329
- the last table created on the way up.
7444
+ up-creates/down-drops pair erased two tables the app really has (35 to 37),
7445
+ and the reverse pair would have invented one it dropped. The same now holds
7446
+ for the block spellings, `reversible { |dir| dir.down { ... } }` and
7447
+ `revert`, and for the `t.timestamps` inside them - those were found by a
7448
+ separate walk over the whole file, so a down body's timestamps landed on the
7449
+ last table created on the way up.
7330
7450
  - **i18n coverage groups the locales below its own rounding floor instead of
7331
7451
  listing them.** A language-name lookup table under `config/locales`
7332
7452
  contributes a top-level key per language, and Rails does load each as an
7333
7453
  available locale. Scoring them produced a row each saying every key was
7334
- missing: on Discourse, 138 of 186 coverage rows described a translation
7335
- effort nobody had started. Those 138 are now summarised in one line, with a
7336
- shared key count stated once. The rows still exist - asking for one by name
7337
- answers with its numbers, so a translation genuinely started below the floor
7338
- is not written off as untranslated. The available list still matches a
7339
- booted Rails, and a locale's keys are read from its own file and any shared
7340
- file together, so one that lives outside the naming convention is scored.
7454
+ missing: on one app, 138 of 186 coverage rows described a translation effort
7455
+ nobody had started. Those 138 are now summarised in one line, with a shared
7456
+ key count stated once. The rows still exist - asking for one by name answers
7457
+ with its numbers, so a translation genuinely started below the floor is not
7458
+ written off as untranslated. The available list still matches a booted
7459
+ Rails, and a locale's keys are read from its own file and any shared file
7460
+ together, so one that lives outside the naming convention is scored.
7341
7461
  - **A recorded tool selection no longer switches off `.ai-context.json`.**
7342
7462
  The install generator always records one, and every later `rails
7343
7463
  ai:context` passed that list to the serializer. The machine artifact is
@@ -7358,21 +7478,21 @@ Discourse and Mastodon. Every one of them exits 0.
7358
7478
  asks Ruby where the class was defined - and the consumers read it through
7359
7479
  `Payload`.
7360
7480
  - **The locale files are indexed once, not once per locale.** Asking every
7361
- file about every locale is O(locales x files): on Discourse, 187 over 108,
7362
- it took the i18n answer from 4.6 seconds to four and a half minutes. Each
7363
- entry also records the locales it serves, so asking for one by name finds
7364
- its files even when the filename spells the gem rather than the locale.
7481
+ file about every locale is O(locales x files): on one app, 187 over 108, it
7482
+ took the i18n answer from 4.6 seconds to four and a half minutes. Each entry
7483
+ also records the locales it serves, so asking for one by name finds its
7484
+ files even when the filename spells the gem rather than the locale.
7365
7485
 
7366
7486
  - **Every consumer that turns a controller name back into a path reads the
7367
7487
  file it was read from.** Carrying `file:` fixed six of them and left five
7368
- answering a confident negative, which is the answer an agent acts on
7369
- without checking. On Mastodon's 18 inflected controllers: `rails_test_info`
7370
- reported 16 specs that exist as missing, `rails_get_routes` reported 4
7371
- routes that exist as absent, `rails_get_view` 2 views, and
7372
- `rails_generate_test` wrote a spec whose body was `skip "no routes found"`
7373
- for a controller with a route. `rails_analyze_feature` listed them as
7374
- untested. `Payload.controller_route_key` is the one derivation now, and a
7375
- spec drives all five tools with an inflected name.
7488
+ answering a confident negative, which is the answer an agent acts on without
7489
+ checking. On one app's 18 inflected controllers: `rails_test_info` reported
7490
+ 16 specs that exist as missing, `rails_get_routes` reported 4 routes that
7491
+ exist as absent, `rails_get_view` 2 views, and `rails_generate_test` wrote a
7492
+ spec whose body was `skip "no routes found"` for a controller with a route.
7493
+ `rails_analyze_feature` listed them as untested.
7494
+ `Payload.controller_route_key` is the one derivation now, and a spec drives
7495
+ all five tools with an inflected name.
7376
7496
  - **The install path's files require their own stdlib.** `init` loads six
7377
7497
  files before Rails and before the entry file, on purpose - so the entry
7378
7498
  file's `require "set"` never runs for them. `legacy_cleanup`'s `[...].to_set`
@@ -7384,22 +7504,22 @@ Discourse and Mastodon. Every one of them exits 0.
7384
7504
  - the shape this gem has shipped as a defect before. Entries and size are
7385
7505
  now facts, and a store that answers `#stats` gets its hash rendered as pairs
7386
7506
  rather than a Ruby literal.
7387
- - **A model's table comes from its file, not from its name.** Naming a model by
7388
- the constant its source declares made `underscore` stop being the way back:
7389
- `OAuthClientConfig` underscores to `o_auth_client_config`, and the file is
7390
- `oauth_client_config.rb`. `rails_model_details` reported the table as
7391
- `o_auth_client_configs`, which no app has, so the columns section vanished
7392
- and the schema hint pointed at nothing - on Canvas that hit
7507
+ - **A model's table comes from its file, not from its name.** Naming a model
7508
+ by the constant its source declares made `underscore` stop being the way
7509
+ back: `OAuthClientConfig` underscores to `o_auth_client_config`, and the
7510
+ file is `oauth_client_config.rb`. `rails_model_details` reported the table
7511
+ as `o_auth_client_configs`, which no app has, so the columns section
7512
+ vanished and the schema hint pointed at nothing - on one app that hit
7393
7513
  `OAuthClientConfig`, `OAuthRequest`, `AuthenticationProvider::OAuth` and
7394
- `OAuth2`, on OpenProject `OAuthClient` and `OAuthClientToken`. The file's own
7514
+ `OAuth2`, on another `OAuthClient` and `OAuthClientToken`. The file's own
7395
7515
  name already carries the inflection, because Zeitwerk resolved the constant
7396
7516
  from it. `rails_generate_test`, `rails_get_callbacks`, `rails_test_info` and
7397
7517
  the five `rails_validate` checks that key models by path read it too.
7398
7518
  - **An abstract base class is not a model in the static tier either.** A
7399
7519
  booted Rails rejects `abstract_class?`, and a namespaced base class is one:
7400
- GitLab's `Ci::ApplicationRecord`, `PackageMetadata::ApplicationRecord` and
7520
+ `Ci::ApplicationRecord`, `PackageMetadata::ApplicationRecord` and
7401
7521
  `SecApplicationRecord` were counted, so the same app answered 895 models
7402
- without a boot and 888 with one. OpenProject: 974 against 971.
7522
+ without a boot and 888 with one. Another app: 974 against 971.
7403
7523
  - **A model carries its file too, and is named by the constant its source
7404
7524
  declares.** `rails_model_details` rebuilt `app/models/<underscored>.rb` from
7405
7525
  the name in four places, so for a model in a pack or an engine the custom
@@ -7413,13 +7533,13 @@ Discourse and Mastodon. Every one of them exits 0.
7413
7533
  gave a constant the app never declares and the undefined-ivar check stopped
7414
7534
  running for that whole tree without saying so.
7415
7535
  - **A test-gap claim is not made from a scan that stopped early.**
7416
- `rails_analyze_feature` scans the first 500 files per glob; Discourse has
7417
- 1,672 specs, so the scan never reached the ones covering the feature and
7418
- every controller in it was reported as having no test. The Tests section
7419
- disclosed the cap, the gaps section asserted through it.
7536
+ `rails_analyze_feature` scans the first 500 files per glob; on an app with
7537
+ 1,672 specs the scan never reached the ones covering the feature and every
7538
+ controller in it was reported as having no test. The Tests section disclosed
7539
+ the cap, the gaps section asserted through it.
7420
7540
  - **"Global before_actions" in the generated files means global.** The scan
7421
7541
  matched `skip_before_action` too, and ignored `only:` / `except:` / `if:`.
7422
- Mastodon's `CLAUDE.md` listed five, of which three were false - including
7542
+ One app's `CLAUDE.md` listed five, of which three were false - including
7423
7543
  `verify_authenticity_token`, which that controller skips.
7424
7544
 
7425
7545
  ### Added
@@ -7622,7 +7742,7 @@ Discourse and Mastodon. Every one of them exits 0.
7622
7742
  - **A model's `*_url` and `*_path` columns are not reported as missing route
7623
7743
  helpers.** `rails_validate` read every receiverless call ending in `_url` as
7624
7744
  a route helper, and an attribute reader has no `def` for the local-method
7625
- escape to find, so `shared_inbox_url` on Mastodon's `accounts` warned as a
7745
+ escape to find, so `shared_inbox_url` on an `accounts` table warned as a
7626
7746
  broken route. The check now consults the model's own column list. (#143)
7627
7747
 
7628
7748
  ## [5.21.2] - 2026-08-12