The Plan Merge
The Plan Merge: base, ours, theirs - a three-way merge of plan wire formats. Independent changes combine; the same edge rewired two different ways is a CONFLICT, reported in topology vocabulary, not JSON-line vocabulary. Round 7 gave plans diff; this gives them merge.
Plans & Graphs
Round 8
Xavier Noria
exit 0
bundle exec ruby examples/plan_merge.rb
a real captured run
PLAN MERGE (base + ours + theirs)
cleanly merged:
tasks: fetch, parse, rank, publish, dedupe, moderate, audit
+ parse -> dedupe (entries)
+ dedupe -> rank (candidates)
+ parse -> moderate (entries)
+ moderate -> rank (safe_entries)
+ publish -> audit
CONFLICTS (both branches rewired the same seam):
seam parse -> rank:
ours: parse -> dedupe -> ...
theirs: parse -> moderate -> ...
resolution is a DESIGN decision - should dedupe run before
moderation, after it, or fused? no textual merge can answer
that, which is why the conflict is reported in topology terms:
the humans must decide the order of the new stages.
source
# frozen_string_literal: true # The Plan Merge: base, ours, theirs - a three-way merge of plan wire # formats. Independent changes combine; the same edge rewired two # different ways is a CONFLICT, reported in topology vocabulary, not # JSON-line vocabulary. Round 7 gave plans diff; this gives them merge. # # bundle exec ruby examples/plan_merge.rb # # Runs offline; two teammates edit the same pipeline. require class="s">"bundler/setup" require class="s">"agentic" # The wire format from plan_roundtrip: tasks + labeled edges BASE = { class="s">"tasks" => [class="s">"fetch", class="s">"parse", class="s">"rank", class="s">"publish"], class="s">"edges" => [ {class="s">"from" => class="s">"fetch", class="s">"to" => class="s">"parse", class="s">"label" => nil}, {class="s">"from" => class="s">"parse", class="s">"to" => class="s">"rank", class="s">"label" => class="s">"entries"}, {class="s">"from" => class="s">"rank", class="s">"to" => class="s">"publish", class="s">"label" => nil} ] }.freeze # Ours: adds dedupe between parse and rank OURS = { class="s">"tasks" => [class="s">"fetch", class="s">"parse", class="s">"dedupe", class="s">"rank", class="s">"publish"], class="s">"edges" => [ {class="s">"from" => class="s">"fetch", class="s">"to" => class="s">"parse", class="s">"label" => nil}, {class="s">"from" => class="s">"parse", class="s">"to" => class="s">"dedupe", class="s">"label" => class="s">"entries"}, {class="s">"from" => class="s">"dedupe", class="s">"to" => class="s">"rank", class="s">"label" => class="s">"candidates"}, {class="s">"from" => class="s">"rank", class="s">"to" => class="s">"publish", class="s">"label" => nil} ] }.freeze # Theirs: adds moderation between parse and rank (same seam!) # and independently adds an audit leaf off publish THEIRS = { class="s">"tasks" => [class="s">"fetch", class="s">"parse", class="s">"moderate", class="s">"rank", class="s">"publish", class="s">"audit"], class="s">"edges" => [ {class="s">"from" => class="s">"fetch", class="s">"to" => class="s">"parse", class="s">"label" => nil}, {class="s">"from" => class="s">"parse", class="s">"to" => class="s">"moderate", class="s">"label" => class="s">"entries"}, {class="s">"from" => class="s">"moderate", class="s">"to" => class="s">"rank", class="s">"label" => class="s">"safe_entries"}, {class="s">"from" => class="s">"rank", class="s">"to" => class="s">"publish", class="s">"label" => nil}, {class="s">"from" => class="s">"publish", class="s">"to" => class="s">"audit", class="s">"label" => nil} ] }.freeze def edge_map(wire) wire[class="s">"edges"].to_h { |e| [[e[class="s">"from"], e[class="s">"to"]], e[class="s">"label"]] } end def merge(base, ours, theirs) base_edges = edge_map(base) our_edges = edge_map(ours) their_edges = edge_map(theirs) conflicts = [] merged_tasks = (base[class="s">"tasks"] | ours[class="s">"tasks"] | theirs[class="s">"tasks"]) # An edge's fate in each branch: kept, removed, or added all_keys = (base_edges.keys | our_edges.keys | their_edges.keys) merged_edges = all_keys.filter_map do |key| in_base = base_edges.key?(key) in_ours = our_edges.key?(key) in_theirs = their_edges.key?(key) if in_base && !in_ours && !in_theirs # Both branches removed this edge - but did they replace it the # same way? If both rewired the same seam differently, conflict. our_replacement = our_edges.keys.find { |k| k[0] == key[0] && !base_edges.key?(k) } their_replacement = their_edges.keys.find { |k| k[0] == key[0] && !base_edges.key?(k) } if our_replacement && their_replacement && our_replacement != their_replacement conflicts << {seam: key, ours: our_replacement, theirs: their_replacement} end nil elsif in_base && in_ours && in_theirs [key, base_edges[key]] # unchanged everywhere elsif !in_base [key, (our_edges[key] || their_edges[key])] # added by one branch else [key, (in_ours ? our_edges[key] : their_edges[key])] # kept by one, removed by other -> keep? no: removed wins end end [{class="s">"tasks" => merged_tasks, class="s">"edges" => merged_edges.map { |(from, to), label| {class="s">"from" => from, class="s">"to" => to, class="s">"label" => label} }}, conflicts] end merged, conflicts = merge(BASE, OURS, THEIRS) puts class="s">"PLAN MERGE (base + ours + theirs)" puts puts class="s">" cleanly merged:" puts class="s">" tasks: #{merged["tasksclass="s">"].join(", class="s">")}" (merged[class="s">"edges"] - BASE[class="s">"edges"]).each do |e| puts class="s">" + #{e["fromclass="s">"]} -> #{e["toclass="s">"]}#{e["labelclass="s">"] ? " (#{e[class="s">"label"]})class="s">" : "class="s">"}" end puts if conflicts.any? puts class="s">" CONFLICTS (both branches rewired the same seam):" conflicts.each do |c| puts class="s">" seam #{c[class="y">:seam][0]} -> #{c[class="y">:seam][1]}:" puts class="s">" ours: #{c[class="y">:seam][0]} -> #{c[class="y">:ours][1]} -> ..." puts class="s">" theirs: #{c[class="y">:seam][0]} -> #{c[class="y">:theirs][1]} -> ..." end puts puts class="s">" resolution is a DESIGN decision - should dedupe run before" puts class="s">" moderation, after it, or fused? no textual merge can answer" puts class="s">" that, which is why the conflict is reported in topology terms:" puts class="s">" the humans must decide the order of the new stages." end