The Graph Critic
The Graph Critic: reviews a plan's dependency structure BEFORE it runs, the way you'd review a class diagram. God tasks, deep chains, and orphans are design smells in a graph exactly as they are in objects - and they're cheaper to fix before execution than after.
Plans & Graphs
Round 4
Sandi Metz
exit 0
bundle exec ruby examples/graph_critic.rb
a real captured run
GRAPH CRITIC: 10 tasks reviewed before execution
[god task] join
gathers 5 dependencies - does it join, or does it do everything? consider staged joins so each has one reason to wait
[deep chain] publish
sits 5 levels down - every level is latency and a failure domain; could any middle link merge with a neighbor?
[orphan] lonely
no dependencies, no dependents - is it in the wrong plan, or is the connection that justifies it missing?
prescription: fix ONE - start with the god task. five ingests
joining at once usually means 'join' hides a pipeline: stage the
joins (a+b, c+d+e, then both) and each join gets one reason to
change. rerun the critic; the chain may resolve itself.
source
# frozen_string_literal: true # The Graph Critic: reviews a plan's dependency structure BEFORE it # runs, the way you'd review a class diagram. God tasks, deep chains, # and orphans are design smells in a graph exactly as they are in # objects - and they're cheaper to fix before execution than after. # # bundle exec ruby examples/graph_critic.rb # # Runs offline; no task is executed. The review IS the program. require class="s">"bundler/setup" require class="s">"agentic" def task_named(name) Agentic:class="y">:Task.new( description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"work"} ) end # A plan with three deliberate smells orchestrator = Agentic:class="y">:PlanOrchestrator.new tasks = {} %w[ingest_a ingest_b ingest_c ingest_d ingest_e clean join report publish lonely].each do |name| tasks[name] = task_named(name) end orchestrator.add_task(tasks[class="s">"ingest_a"]) orchestrator.add_task(tasks[class="s">"ingest_b"]) orchestrator.add_task(tasks[class="s">"ingest_c"]) orchestrator.add_task(tasks[class="s">"ingest_d"]) orchestrator.add_task(tasks[class="s">"ingest_e"]) # the god task: everything funnels through join orchestrator.add_task(tasks[class="s">"join"], %w[ingest_a ingest_b ingest_c ingest_d ingest_e].map { |n| tasks[n] }) # a chain hanging off it orchestrator.add_task(tasks[class="s">"clean"], [tasks[class="s">"join"]]) orchestrator.add_task(tasks[class="s">"report"], [tasks[class="s">"clean"]]) orchestrator.add_task(tasks[class="s">"publish"], [tasks[class="s">"report"]]) # and a task nobody references orchestrator.add_task(tasks[class="s">"lonely"]) # --- the critique ---------------------------------------------------------- # This example's original feature request, granted: a read-only view of # the plan's topology. No more crowbar. graph = orchestrator.graph dependencies = graph[class="y">:dependencies] names = graph[class="y">:tasks].transform_values(&class="y">:description) dependents = Hash.new { |h, k| h[k] = [] } dependencies.each { |task_id, deps| deps.each { |dep| dependents[dep] << task_id } } # Depth now ships precomputed in the snapshot depth_of = ->(task_id) { graph[class="y">:stats][class="y">:depth][task_id] } findings = [] dependencies.each do |task_id, deps| if deps.size >= 4 findings << {smell: class="s">"god task", task: names[task_id], note: class="s">"gathers #{deps.size} dependencies - does it join, or does it do everything? " \ class="s">"consider staged joins so each has one reason to wait"} end end deepest = dependencies.keys.max_by { |task_id| depth_of.call(task_id) } if depth_of.call(deepest) >= 4 findings << {smell: class="s">"deep chain", task: names[deepest], note: class="s">"sits #{depth_of.call(deepest)} levels down - every level is latency and a failure " \ class="s">"domain; could any middle link merge with a neighbor?"} end dependencies.each do |task_id, deps| if deps.empty? && dependents[task_id].empty? && dependencies.size > 1 findings << {smell: class="s">"orphan", task: names[task_id], note: class="s">"no dependencies, no dependents - is it in the wrong plan, or is the " \ class="s">"connection that justifies it missing?"} end end puts class="s">"GRAPH CRITIC: #{dependencies.size} tasks reviewed before execution" puts findings.each do |finding| puts class="s">" [#{finding[class="y">:smell]}] #{finding[class="y">:task]}" puts class="s">" #{finding[class="y">:note]}" puts end puts class="s">"prescription: fix ONE - start with the god task. five ingests" puts class="s">"joining at once usually means 'join' hides a pipeline: stage the" puts class="s">"joins (a+b, c+d+e, then both) and each join gets one reason to" puts class="s">"change. rerun the critic; the chain may resolve itself."