The Refactoring Dojo
The Refactoring Dojo: a student submits a method, three critic agents review it from three distinct perspectives, and the sensei prescribes the ONE smallest next step. Today's student: this gem itself, submitting its second-longest method (schedule_task, 90 lines).
Testing & Verification
Round 2
Sandi Metz
exit 0
bundle exec ruby examples/refactoring_dojo.rb
a real captured run
REFACTORING DOJO submission: #schedule_task (52 lines) from plan_orchestrator.rb rule keeper says: - 52 lines; the rule is five. Every extra line is a place for a bug to live. squint tester says: - squinting shows 4 levels of shape change - each ridge is a concept asking for its own method. name watcher says: - no complaints. rare. sensei's prescription: 2 findings, ONE next step - the smallest one: start where the squint test hurts: squinting shows 4 levels of shape change - each ridge is a concept asking for its own method. make that change, run the tests, come back. refactoring is many small safe steps, not one brave rewrite.
source
# frozen_string_literal: true # The Refactoring Dojo: a student submits a method, three critic agents # review it from three distinct perspectives, and the sensei prescribes # the ONE smallest next step. Today's student: this gem itself, # submitting its second-longest method (schedule_task, 90 lines). # # bundle exec ruby examples/refactoring_dojo.rb [file] [method] # # Runs offline: the critics measure; they don't guess. require class="s">"bundler/setup" require class="s">"agentic" # These examples read the agentic SOURCE - resolve the installed gem's own directory AGENTIC_SRC = Gem:class="y">:Specification.find_by_name(class="s">"agentic").gem_dir require class="s">"prism" file = ARGV[0] || File.join(AGENTIC_SRC, class="s">"lib/agentic/plan_orchestrator.rb") method_name = (ARGV[1] || class="s">"schedule_task").to_sym # Find the student's submission def find_def(node, name) return node if node.is_a?(Prism:class="y">:DefNode) && node.name == name node&.child_nodes&.each do |child| found = find_def(child, name) return found if found end nil end submission = find_def(Prism.parse_file(file).value, method_name) || abort(class="s">"no method #{method_name} in #{file}") source = submission.slice lines = source.lines def critic(name, &impl) spec = Agentic:class="y">:CapabilitySpecification.new( name: name, description: class="s">"The #{name} critic", version: class="s">"1.0.0", inputs: {source: {type: class="s">"string", required: true}}, outputs: {findings: {type: class="s">"array", required: true}} ) Agentic.register_capability( spec, Agentic:class="y">:CapabilityProvider.new(capability: spec, implementation: impl) ) Agentic:class="y">:Agent.build { |a| a.name = name }.tap { |a| a.add_capability(name) } end critics = [] critics << critic(class="s">"rule_keeper") do |input| body = input[class="y">:source].lines findings = [] if body.size > 5 findings << class="s">"#{body.size} lines; the rule is five. Every extra line is a place for a bug to live." end params = body.first[/\((.*)\)/, 1].to_s.split(class="s">",").size if params > 4 findings << class="s">"#{params} parameters; four is the ceiling before a parameter object is cheaper." end {findings: findings} end critics << critic(class="s">"squint_tester") do |input| body = input[class="y">:source].lines indents = body.map { |l| l[/\A */].size }.reject(&class="y">:zero?) depth = (indents.max - indents.min) / 2 findings = [] if depth >= 3 findings << class="s">"squinting shows #{depth} levels of shape change - each ridge is a concept asking for its own method." end branches = body.count { |l| l.strip.start_with?(class="s">"if ", class="s">"elsif ", class="s">"unless ", class="s">"when ", class="s">"rescue") } if branches >= 4 findings << class="s">"#{branches} branch points in one method - this method makes decisions AND does work; split the two." end {findings: findings} end critics << critic(class="s">"name_watcher") do |input| body = input[class="y">:source] findings = [] vague = body.scan(/\b(data|info|result|temp|obj|thing)\b/).flatten.tally vague.each do |word, count| findings << class="s">"'#{word}' appears #{count}x - a name that could mean anything means nothing. What IS it?" end if body.match?(/def \w+_and_\w+/) findings << class="s">"the name contains 'and' - a confession that this is two methods in one costume." end {findings: findings} end # The circle convenes: each critic rides its own task and reviews in parallel orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 3) seats = critics.to_h do |c| task = Agentic:class="y">:Task.new( description: c.name, agent_spec: {class="s">"name" => c.name, class="s">"instructions" => class="s">"Review the submission"}, payload: c ) orchestrator.add_task(task, agent: ->(t) { t.payload.execute_capability(t.payload.name, {source: source})[class="y">:findings] }) [c.name, task] end run = orchestrator.execute_plan scrolls = seats.transform_values { |task| run.results[task.id].output } puts class="s">"REFACTORING DOJO" puts class="s">"submission: ##{method_name} (#{lines.size} lines) from #{File.basename(file)}" puts scrolls.each do |critic_name, findings| puts class="s">"#{critic_name.tr("_class="s">", " class="s">")} says:" findings.each { |f| puts class="s">" - #{f}" } puts class="s">" - no complaints. rare." if findings.empty? puts end total = scrolls.values.sum(&class="y">:size) puts class="s">"sensei's prescription:" if total.zero? puts class="s">" ship it, then find a harder kata." else first_move = scrolls[class="s">"squint_tester"]&.first || scrolls.values.flatten.first puts class="s">" #{total} findings, ONE next step - the smallest one:" puts class="s">" start where the squint test hurts: #{first_move}" puts class="s">" make that change, run the tests, come back. refactoring is many small" puts class="s">" safe steps, not one brave rewrite." end