The Plan Kata
The Plan Kata: red, green, refactor - for a plan. The "tests" are assertions about the graph (one root, one leaf, labeled joins, nothing too deep), written BEFORE any tasks exist. Each step adds the smallest thing that moves a red line green, and the refactor step changes structure with the assertions standing guard. You've TDD'd methods; plans deserve the same discipline.
Testing & Verification
Round 11
Katrina Owen
exit 0
bundle exec ruby examples/plan_kata.rb
a real captured run
THE PLAN KATA (assertions first, tasks second)
step: empty plan (the honest starting point)
has exactly one entry point RED
has exactly one deliverable RED
every join names its inputs green
no deeper than four stages green
no orphan tasks green
-> 2 red
step: add the entry point
has exactly one entry point green
has exactly one deliverable green
every join names its inputs green
no deeper than four stages green
no orphan tasks green
-> 0 red
step: add parse
has exactly one entry point green
has exactly one deliverable green
every join names its inputs green
no deeper than four stages green
no orphan tasks green
-> 0 red
step: bolt on a price feed (two sins)
has exactly one entry point RED
has exactly one deliverable green
every join names its inputs RED
no deeper than four stages green
no orphan tasks green
-> 2 red
step: refactor in place: rewire, relabel
has exactly one entry point green
has exactly one deliverable green
every join names its inputs green
no deeper than four stages green
no orphan tasks green
-> 0 red
the kata's shape is the point: the assertions existed before
the plan did, every addition was the smallest thing that moved
a line, and the two deliberate sins were CAUGHT and NAMED by
tests written when nobody was defensive about the design yet.
and the refactor was a real refactor this time: rewire_task
(round 12, this kata's own ask) changed the plan's shape without
demolishing its identity - red, green, REFACTOR, with all three
words meaning what they say. kata complete, all green.
source
# frozen_string_literal: true # The Plan Kata: red, green, refactor - for a plan. The "tests" are # assertions about the graph (one root, one leaf, labeled joins, # nothing too deep), written BEFORE any tasks exist. Each step adds # the smallest thing that moves a red line green, and the refactor # step changes structure with the assertions standing guard. You've # TDD'd methods; plans deserve the same discipline. # # bundle exec ruby examples/plan_kata.rb # # Runs offline; exits 1 if the kata ends with a red assertion. 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 # The test list, written first - what a GOOD ingest plan looks like, # structurally, before we know what the tasks are ASSERTIONS = { class="s">"has exactly one entry point" => ->(g) { g[class="y">:stats][class="y">:roots].size == 1 }, class="s">"has exactly one deliverable" => ->(g) { g[class="y">:stats][class="y">:leaves].size == 1 }, class="s">"every join names its inputs" => ->(g) { g[class="y">:dependencies].select { |_, d| d.size >= 2 }.keys.all? { |id| g[class="y">:edges].select { |e| e[class="y">:to] == id }.all? { |e| e[class="y">:label] } } }, class="s">"no deeper than four stages" => ->(g) { g[class="y">:stats][class="y">:max_depth] <= 4 }, class="s">"no orphan tasks" => ->(g) { g[class="y">:tasks].size < 2 || (g[class="y">:stats][class="y">:roots] & g[class="y">:stats][class="y">:leaves]).empty? } }.freeze def check(orchestrator) graph = orchestrator.graph ASSERTIONS.transform_values { |assertion| assertion.call(graph) } end def report(step, results) reds = results.count { |_, ok| !ok } puts class="s">" step: #{step}" results.each { |name, ok| puts format(class="s">" %-32s %s", name, ok ? class="s">"green" : class="s">"RED") } puts format(class="s">" -> %d red", reds) puts end puts class="s">"THE PLAN KATA (assertions first, tasks second)" puts # RED: no tasks at all - most assertions can't hold on emptiness o = Agentic:class="y">:PlanOrchestrator.new report(class="s">"empty plan (the honest starting point)", check(o)) # GREEN, smallest step: one task satisfies one-root-one-leaf trivially ingest = task_named(class="s">"ingest") o.add_task(ingest) report(class="s">"add the entry point", check(o)) # Grow: parse feeds off ingest; deliverable moves - still green parse = task_named(class="s">"parse") o.add_task(parse, [ingest]) report(class="s">"add parse", check(o)) # RED on purpose: a second source creates a second root, and an # unlabeled join - two assertions object, and they name the problem prices = task_named(class="s">"prices") merge = task_named(class="s">"merge") o.add_task(prices) o.add_task(merge, [parse, prices]) report(class="s">"bolt on a price feed (two sins)", check(o)) # GREEN again: REFACTOR IN PLACE - the round-12 release gave plans # rewire_task, so fixing the shape no longer means demolishing it. # Route the price feed through the one door, and give the merge its # labels; the assertions stand guard the whole time. o.rewire_task(prices, [ingest]) o.rewire_task(merge, needs: {parsed: parse, prices: prices}) report_task = task_named(class="s">"report") o.add_task(report_task, [merge]) final = check(o) report(class="s">"refactor in place: rewire, relabel", final) reds = final.count { |_, ok| !ok } puts class="s">" the kata's shape is the point: the assertions existed before" puts class="s">" the plan did, every addition was the smallest thing that moved" puts class="s">" a line, and the two deliberate sins were CAUGHT and NAMED by" puts class="s">" tests written when nobody was defensive about the design yet." puts class="s">" and the refactor was a real refactor this time: rewire_task" puts class="s">" (round 12, this kata's own ask) changed the plan's shape without" puts class="s">" demolishing its identity - red, green, REFACTOR, with all three" puts class="s">" words meaning what they say. #{(reds == 0) ? "kata complete, all green.class="s">" : "KATA INCOMPLETE.class="s">"}" exit((reds == 0) ? 0 : 1)