agentic examples

Plan Flog

Plan Flog: flog gives every Ruby method a pain score; this gives every plan one. Fan-in hurts (joins hide coupling), depth hurts (chains hide latency), unlabeled edges hurt (anonymous data flow), and orphans hurt (dead code that runs). One number per task, one number per plan, and a threshold that means "refactor me". Yes, it's opinionated. So is flog. That's the point.

Testing & Verification Round 11 Ryan Davis (zenspider) exit 0

source on github

bundle exec ruby examples/plan_flog.rb

a real captured run

PLAN FLOG (pain points per plan; > 12 total means refactor me)

  tidy pipeline        0.0  fine
  labeled diamond      1.5  fine         ledger=1.5
  the monster         24.5  REFACTOR ME  do_everything=14.7  orphan=5.0

  the monster's breakdown, because a score you can't argue with
  is a score you can't learn from: do_everything costs 14.7 - five
  EXTRA join inputs at 1.5 coupling each, plus six anonymous ones
  at 1.2. the orphan costs 5.0 flat: it runs on every execution
  and feeds nothing, which is either a bug or a billing strategy.
  and the tidy pipeline scores 0.0, because a pipe is free and
  boring plans should be.

  numbers don't refactor code and they don't refactor plans -
  but they end the meeting about whether the monster is fine.
  it's a 25. it's not fine.

source

# frozen_string_literal: true

# Plan Flog: flog gives every Ruby method a pain score; this gives
# every plan one. Fan-in hurts (joins hide coupling), depth hurts
# (chains hide latency), unlabeled edges hurt (anonymous data flow),
# and orphans hurt (dead code that runs). One number per task, one
# number per plan, and a threshold that means "refactor me". Yes,
# it's opinionated. So is flog. That's the point.
#
#   bundle exec ruby examples/plan_flog.rb
#
# Runs offline; three plans walk in, one gets told the truth.

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">"w"})
end

def tidy_pipeline
  o = Agentic:class="y">:PlanOrchestrator.new
  fetch, clean, render = %w[fetch clean render].map { |n| task_named(n) }
  o.add_task(fetch)
  o.add_task(clean, [fetch])
  o.add_task(render, [clean])
  o
end

def labeled_diamond
  o = Agentic:class="y">:PlanOrchestrator.new
  orders, refunds, ledger, report = %w[orders refunds ledger report].map { |n| task_named(n) }
  o.add_task(orders)
  o.add_task(refunds)
  o.add_task(ledger, needs: {sales: orders, credits: refunds})
  o.add_task(report, [ledger])
  o
end

def the_monster
  o = Agentic:class="y">:PlanOrchestrator.new
  sources = 6.times.map { |i| task_named(class="s">"src#{i}") }
  sources.each { |s| o.add_task(s) }
  god = task_named(class="s">"do_everything")
  o.add_task(god, sources) # six unlabeled inputs
  chain = god
  4.times do |i|
    step = task_named(class="s">"then#{i}")
    o.add_task(step, [chain])
    chain = step
  end
  o.add_task(task_named(class="s">"orphan")) # added in a refactor, feeds nothing... but runs
  o
end

# The scoring, flog-style: pain per structural sin
def flog(graph)
  stats = graph[class="y">:stats]
  labeled = graph[class="y">:edges].count { |e| e[class="y">:label] }
  scores = graph[class="y">:tasks].keys.to_h do |id|
    fan_in = graph[class="y">:dependencies][id].size
    fan_out = graph[class="y">:edges].count { |e| e[class="y">:from] == id }
    unlabeled_in = (fan_in >= 2) ? graph[class="y">:edges].count { |e| e[class="y">:to] == id && !e[class="y">:label] } : 0
    orphan = (stats[class="y">:roots].include?(id) && stats[class="y">:leaves].include?(id) && graph[class="y">:tasks].size > 1) ? 5.0 : 0
    score = [fan_in - 1, 0].max * 1.5 +         # a pipe is free; every EXTRA join input is coupling
      [fan_out - 2, 0].max * 1.0 +              # fan-out past 2 spreads blame
      (stats[class="y">:depth][id] - 3).clamp(0, 99) * 0.8 + # depth past 3 hides latency
      unlabeled_in * 1.2 +                      # anonymous inputs, where they can be confused
      orphan                                    # runs, feeds nothing: pay attention
    [id, score]
  end
  [scores, labeled]
end

PLANS = {
  class="s">"tidy pipeline" => tidy_pipeline,
  class="s">"labeled diamond" => labeled_diamond,
  class="s">"the monster" => the_monster
}.freeze

puts class="s">"PLAN FLOG (pain points per plan; > 12 total means refactor me)"
puts
PLANS.each do |name, orchestrator|
  graph = orchestrator.graph
  scores, = flog(graph)
  total = scores.values.sum
  names = graph[class="y">:tasks].transform_values(&class="y">:description)
  worst = scores.max_by(2) { |_, s| s }.select { |_, s| s > 0 }

  verdict = if total > 12
    class="s">"REFACTOR ME"
  else
    ((total > 6) ? class="s">"watch it" : class="s">"fine")
  end
  puts format(class="s">"  %-18s %5.1f  %-12s %s", name, total, verdict,
    worst.map { |id, s| class="s">"#{names[id]}=#{s.round(1)}" }.join(class="s">"  "))
end

puts
monster = the_monster.graph
scores, = flog(monster)
names = monster[class="y">:tasks].transform_values(&class="y">:description)
top = scores.max_by { |_, s| s }
total = scores.values.sum
puts class="s">"  the monster's breakdown, because a score you can't argue with"
puts class="s">"  is a score you can't learn from: #{names[top[0]]} costs #{top[1].round(1)} - five"
puts class="s">"  EXTRA join inputs at 1.5 coupling each, plus six anonymous ones"
puts class="s">"  at 1.2. the orphan costs 5.0 flat: it runs on every execution"
puts class="s">"  and feeds nothing, which is either a bug or a billing strategy."
puts class="s">"  and the tidy pipeline scores 0.0, because a pipe is free and"
puts class="s">"  boring plans should be."
puts
puts class="s">"  numbers don't refactor code and they don't refactor plans -"
puts class="s">"  but they end the meeting about whether the monster is fine."
puts class="s">"  it's a #{total.round}. it's not fine."