The Structural Diff
The Structural Diff: two versions of a plan's wire format, diffed as TOPOLOGY - tasks added and removed, edges rewired, labels renamed. A line diff of plan JSON tells you bytes changed; this tells you what changed about the plan.
Plans & Graphs
Round 7
Xavier Noria
exit 0
bundle exec ruby examples/plan_structural_diff.rb
a real captured run
PLAN STRUCTURAL DIFF (v1 -> v2) + task dedupe entries + edge parse entries -> dedupe entries + edge dedupe entries -> rank entries - edge parse entries -> rank entries 4 structural changes. the review question is no longer 'what do these 40 changed JSON lines mean' but 'should ranking consume deduped candidates instead of raw entries' - which is a question a human can actually answer.
source
# frozen_string_literal: true # The Structural Diff: two versions of a plan's wire format, diffed as # TOPOLOGY - tasks added and removed, edges rewired, labels renamed. # A line diff of plan JSON tells you bytes changed; this tells you what # changed about the plan. # # bundle exec ruby examples/plan_structural_diff.rb # # Runs offline; v1 and v2 are built in-process via the round-trip wire # format, as they would be loaded from two commits of plan.json. require class="s">"bundler/setup" require class="s">"agentic" def step(name) Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"work"}) end def wire(orchestrator) graph = orchestrator.graph names = graph[class="y">:tasks].transform_values(&class="y">:description) { class="s">"tasks" => graph[class="y">:order].map { |id| names[id] }, class="s">"edges" => graph[class="y">:edges].map { |e| {class="s">"from" => names[e[class="y">:from]], class="s">"to" => names[e[class="y">:to]], class="s">"label" => e[class="y">:label]&.to_s} } } end # --- version 1: last sprint's pipeline --------------------------------------- v1 = Agentic:class="y">:PlanOrchestrator.new fetch = step(class="s">"fetch feed") parse = step(class="s">"parse entries") rank = step(class="s">"rank entries") publish = step(class="s">"publish digest") v1.add_task(fetch) v1.add_task(parse, [fetch]) v1.add_task(rank, needs: {entries: parse}) v1.add_task(publish, [rank]) # --- version 2: this sprint's - dedupe added, ranking rewired ---------------- v2 = Agentic:class="y">:PlanOrchestrator.new fetch2 = step(class="s">"fetch feed") parse2 = step(class="s">"parse entries") dedupe2 = step(class="s">"dedupe entries") rank2 = step(class="s">"rank entries") publish2 = step(class="s">"publish digest") v2.add_task(fetch2) v2.add_task(parse2, [fetch2]) v2.add_task(dedupe2, needs: {entries: parse2}) v2.add_task(rank2, needs: {candidates: dedupe2}) v2.add_task(publish2, [rank2]) # --- the diff: sets of names and labeled edges -------------------------------- def structural_diff(before, after) edge_key = ->(e) { [e[class="s">"from"], e[class="s">"to"]] } before_edges = before[class="s">"edges"].to_h { |e| [edge_key.call(e), e[class="s">"label"]] } after_edges = after[class="s">"edges"].to_h { |e| [edge_key.call(e), e[class="s">"label"]] } { tasks_added: after[class="s">"tasks"] - before[class="s">"tasks"], tasks_removed: before[class="s">"tasks"] - after[class="s">"tasks"], edges_added: (after_edges.keys - before_edges.keys), edges_removed: (before_edges.keys - after_edges.keys), labels_changed: before_edges.keys.intersection(after_edges.keys) .reject { |k| before_edges[k] == after_edges[k] } .map { |k| [k, before_edges[k], after_edges[k]] } } end diff = structural_diff(wire(v1), wire(v2)) puts class="s">"PLAN STRUCTURAL DIFF (v1 -> v2)" puts diff[class="y">:tasks_added].each { |t| puts class="s">" + task #{t}" } diff[class="y">:tasks_removed].each { |t| puts class="s">" - task #{t}" } diff[class="y">:edges_added].each { |(from, to)| puts class="s">" + edge #{from} -> #{to}" } diff[class="y">:edges_removed].each { |(from, to)| puts class="s">" - edge #{from} -> #{to}" } diff[class="y">:labels_changed].each { |(from, to), old, new| puts class="s">" ~ label #{from} -> #{to}: #{old.inspect} => #{new.inspect}" } puts total = diff.values.sum(&class="y">:size) puts class="s">" #{total} structural changes. the review question is no longer" puts class="s">" 'what do these 40 changed JSON lines mean' but 'should ranking" puts class="s">" consume deduped candidates instead of raw entries' - which is" puts class="s">" a question a human can actually answer."