The Coupling Cartographer
The Coupling Cartographer: which files lean on which? Every file is surveyed for the constants it DEFINES and the constants it REFERENCES; a fan-in task joins the two into a dependency graph and reports the load-bearing walls and the heaviest leaners.
Data & Pipelines
Round 4
Xavier Noria
exit 0
bundle exec ruby examples/coupling_cartographer.rb
a real captured run
COUPLING ATLAS of /opt/hostedtoolcache/Ruby/3.3.11/x64/lib/ruby/gems/3.3.0/bundler/gems/agentic-0fe3f91c1ef9/lib (68 files) load-bearing walls (most depended-upon): 8 files lean on agentic/llm_config.rb 7 files lean on agentic/errors.rb 5 files lean on agentic/verification/verification_strategy.rb 5 files lean on agentic/agent_capability_registry.rb 5 files lean on agentic/cli/capabilities.rb 5 files lean on agentic/cli/agent.rb heaviest leaners (most dependencies out): agentic/cli.rb leans on 15 files agentic.rb leans on 12 files agentic/task_planner.rb leans on 6 files agentic/task.rb leans on 6 files agentic/plan_orchestrator.rb leans on 6 files agentic/capabilities/examples.rb leans on 6 files mutual dependencies (each file references the other): agentic.rb <-> agentic/llm_client.rb
source
# frozen_string_literal: true # The Coupling Cartographer: which files lean on which? Every file is # surveyed for the constants it DEFINES and the constants it REFERENCES; # a fan-in task joins the two into a dependency graph and reports the # load-bearing walls and the heaviest leaners. # # bundle exec ruby examples/coupling_cartographer.rb [lib_dir] # # Runs offline; Prism supplies both sides of every edge. require class="s">"bundler/setup" require class="s">"agentic" require class="s">"prism" LIB = ARGV.first ? File.expand_path(ARGV.first) : File.join(Gem:class="y">:Specification.find_by_name(class="s">"agentic").gem_dir, class="s">"lib") # the installed gem, wherever bundler put it # Collects constants defined and referenced in one parse tree def survey_constants(node, namespace, defined, referenced) return unless node case node when Prism:class="y">:ModuleNode, Prism:class="y">:ClassNode name = node.constant_path.slice full = [namespace, name].reject(&class="y">:empty?).join(class="s">"::") defined << full node.child_nodes.each { |child| survey_constants(child, full, defined, referenced) } return when Prism:class="y">:ConstantReadNode referenced << node.name.to_s when Prism:class="y">:ConstantPathNode referenced << node.slice end node.child_nodes.each { |child| survey_constants(child, namespace, defined, referenced) } end orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 8) files = Dir[File.join(LIB, class="s">"**", class="s">"*.rb")].sort if files.empty? puts class="s">" no ruby files under #{LIB} - an empty survey proves nothing" exit 1 end surveys = files.map do |path| task = Agentic:class="y">:Task.new( description: path.delete_prefix(class="s">"#{LIB}/"), agent_spec: {class="s">"name" => class="s">"Surveyor", class="s">"instructions" => class="s">"chart the constants"}, payload: path ) orchestrator.add_task(task, agent: ->(t) { defined = [] referenced = [] survey_constants(Prism.parse_file(t.payload).value, class="s">"", defined, referenced) {defined: defined, referenced: referenced.uniq - defined} }) task end atlas = Agentic:class="y">:Task.new( description: class="s">"the atlas", agent_spec: {class="s">"name" => class="s">"Cartographer", class="s">"instructions" => class="s">"join the maps"} ) orchestrator.add_task(atlas, surveys, agent: ->(t) { charts = surveys.to_h { |s| [s.description, t.output_of(s)] } # Who owns each constant (by trailing segment, since references are # often relative: LlmClient rather than Agentic::LlmClient) owners = {} charts.each do |file, chart| chart[class="y">:defined].each { |const| owners[const.split(class="s">"::").last] = file } end edges = Hash.new { |h, k| h[k] = [] } charts.each do |file, chart| chart[class="y">:referenced].each do |ref| owner = owners[ref.split(class="s">"::").last] edges[file] << owner if owner && owner != file end end # Copy without the default proc: a Hash.new {} that leaks to readers # invents keys on every miss - including during iteration edges = edges.transform_values(&class="y">:uniq) inbound = Hash.new(0) edges.each_value { |targets| targets.each { |target| inbound[target] += 1 } } {edges: edges, inbound: inbound} }) result = orchestrator.execute_plan atlas_data = result.results[atlas.id].output puts class="s">"COUPLING ATLAS of #{LIB} (#{files.size} files)" puts puts class="s">"load-bearing walls (most depended-upon):" atlas_data[class="y">:inbound].sort_by { |_, count| -count }.first(6).each do |file, count| puts format(class="s">" %2d files lean on %s", count, file) end puts puts class="s">"heaviest leaners (most dependencies out):" atlas_data[class="y">:edges].sort_by { |_, targets| -targets.size }.first(6).each do |file, targets| puts format(class="s">" %-40s leans on %2d files", file, targets.size) end mutual = atlas_data[class="y">:edges].flat_map { |file, targets| targets.filter_map { |target| [file, target].sort if atlas_data[class="y">:edges][target]&.include?(file) } }.uniq puts if mutual.empty? puts class="s">"no mutual dependencies - every edge points one way. rare, and good." else puts class="s">"mutual dependencies (each file references the other):" mutual.each { |a, b| puts class="s">" #{a} <-> #{b}" } end