Summary
On Rails (and any Zeitwerk-autoloaded Ruby project) the import resolver finds almost nothing, which collapses the entire downstream graph: cross-file imports/depends_on edges are missing, layer assignment falls back to path heuristics, and the LLM batch agents have no neighborMap data to work with. Run /understand on a non-trivial Rails monolith and you'll see roughly one edge per node — a fraction of the real connection density.
Repro
Run /understand on any Rails 6/7/8 app. I tested on a ~1,200-file Rails 8 codebase (PlaceCal):
| Metric |
Value |
| Files analyzed |
1,158 |
imports edges emitted |
22 |
calls edges emitted |
4 |
inherits edges emitted |
68 (before LLM review pass) |
Total non-contains/exports edges |
~150 |
By comparison, the same project's actual ActiveRecord association graph alone has hundreds of cross-file relationships.
Root cause
packages/core/src/languages/configs/ruby.js defines the Ruby language config but provides no resolver for class-name → file-path mapping. The structural extractor reads tree-sitter output and looks for explicit require/require_relative statements, of which a Rails app has almost none — Zeitwerk autoloads everything.
So when a controller references a constant:
class PartnersController < ApplicationController
def show
@partner = Partner.find(params[:id]) # <-- this is a cross-file reference
end
end
the scanner sees Partner as a free identifier and emits no edge. The downstream effect is that batchImportData is empty for ~99% of Ruby files, so file-analyzer subagents have no inter-batch signal and the architecture-analyzer falls back to path patterns.
Proposal
A Rails-aware (or more generally, Zeitwerk-aware) resolver bundled with the Ruby plugin. Two concrete pieces:
-
Constant → file resolver. At scan time, build an index of ClassName::ChildClass → app/models/class_name/child_class.rb using Zeitwerk's standard inflection rules (app/, lib/, plus configurable load paths). When tree-sitter sees a constant reference, look it up in the index and emit an imports (or depends_on) edge. The lookup is fast and entirely deterministic.
-
ActiveRecord DSL recognizer. belongs_to, has_many, has_one, has_and_belongs_to_many are first-class structural facts. A small tree-sitter query over each model file can emit them as depends_on (or a new associates edge type) to the singularized target. Similarly validates_* and scope are noise; delegate :foo, to: :bar is signal worth surfacing.
Stretch:
config/routes.rb resolver. Parse the routes DSL and emit routes edges from URL patterns to controller actions. This is the single biggest grouping signal in any Rails app and is currently invisible.
- RSpec convention pairings.
spec/models/partner_spec.rb tests app/models/partner.rb. Filename mirroring is a strong enough heuristic to emit tested_by edges without LLM intervention — the LLM review pass currently does this manually 60+ times per project.
Workarounds I've tried
/understand --review (the LLM graph-reviewer pass) recovered ~270 edges by reading whole files and pattern-matching, which is the right shape but the wrong layer — this should be a deterministic scanner job, not an LLM tax paid on every run.
- Tightening
.understandignore to pack each batch with more related files helps the in-batch LLM but does nothing for cross-batch edges.
Why this matters
Rails is one of the most-deployed server frameworks in the world and the second-most-common language in your languages/configs/ directory. The current output for Rails projects is misleading enough that the dashboard and tour underrepresent how the app actually fits together — and the gap isn't visible to users unless they happen to know what edges should be there.
Summary
On Rails (and any Zeitwerk-autoloaded Ruby project) the import resolver finds almost nothing, which collapses the entire downstream graph: cross-file
imports/depends_onedges are missing, layer assignment falls back to path heuristics, and the LLM batch agents have noneighborMapdata to work with. Run/understandon a non-trivial Rails monolith and you'll see roughly one edge per node — a fraction of the real connection density.Repro
Run
/understandon any Rails 6/7/8 app. I tested on a ~1,200-file Rails 8 codebase (PlaceCal):importsedges emittedcallsedges emittedinheritsedges emittedcontains/exportsedgesBy comparison, the same project's actual ActiveRecord association graph alone has hundreds of cross-file relationships.
Root cause
packages/core/src/languages/configs/ruby.jsdefines the Ruby language config but provides no resolver for class-name → file-path mapping. The structural extractor reads tree-sitter output and looks for explicitrequire/require_relativestatements, of which a Rails app has almost none — Zeitwerk autoloads everything.So when a controller references a constant:
the scanner sees
Partneras a free identifier and emits no edge. The downstream effect is thatbatchImportDatais empty for ~99% of Ruby files, so file-analyzer subagents have no inter-batch signal and the architecture-analyzer falls back to path patterns.Proposal
A Rails-aware (or more generally, Zeitwerk-aware) resolver bundled with the Ruby plugin. Two concrete pieces:
Constant → file resolver. At scan time, build an index of
ClassName::ChildClass → app/models/class_name/child_class.rbusing Zeitwerk's standard inflection rules (app/,lib/, plus configurable load paths). When tree-sitter sees a constant reference, look it up in the index and emit animports(ordepends_on) edge. The lookup is fast and entirely deterministic.ActiveRecord DSL recognizer.
belongs_to,has_many,has_one,has_and_belongs_to_manyare first-class structural facts. A small tree-sitter query over each model file can emit them asdepends_on(or a newassociatesedge type) to the singularized target. Similarlyvalidates_*andscopeare noise;delegate :foo, to: :baris signal worth surfacing.Stretch:
config/routes.rbresolver. Parse the routes DSL and emitroutesedges from URL patterns to controller actions. This is the single biggest grouping signal in any Rails app and is currently invisible.spec/models/partner_spec.rbtestsapp/models/partner.rb. Filename mirroring is a strong enough heuristic to emittested_byedges without LLM intervention — the LLM review pass currently does this manually 60+ times per project.Workarounds I've tried
/understand --review(the LLM graph-reviewer pass) recovered ~270 edges by reading whole files and pattern-matching, which is the right shape but the wrong layer — this should be a deterministic scanner job, not an LLM tax paid on every run..understandignoreto pack each batch with more related files helps the in-batch LLM but does nothing for cross-batch edges.Why this matters
Rails is one of the most-deployed server frameworks in the world and the second-most-common language in your
languages/configs/directory. The current output for Rails projects is misleading enough that the dashboard and tour underrepresent how the app actually fits together — and the gap isn't visible to users unless they happen to know what edges should be there.