agentic examples

The Relation Diff

The Relation Diff: round 8's semver advisor classified declaration changes but had to shrug at rules - lambdas can't be compared. Now relations are data, so the RULES diff too: a tightened limit is breaking, a loosened one compatible, a new rule breaking, a removed one compatible, and a changed relation TYPE is a different law entirely. The last opaque corner of the contract joins semver.

Developer Experience Round 10 Piotr Solnica exit 0

source on github

bundle exec ruby examples/relation_diff.rb

a real captured run

RELATION DIFF: quote_shipping rules, v1 -> v2

  BREAKING   rule :fits limit tightened 6000 -> 4000 - previously legal calls rejected
  BREAKING   rule :customs now also demands incoterm - callers satisfying v1 fail v2
  BREAKING   rule :one_auth changed LAW: mutually_exclusive -> requires - not an edit, a replacement
  BREAKING   rule :speedy added (sum_lte) - a new law existing callers never agreed to
  OPAQUE     rule :audited is a lambda in both versions - the diff cannot see inside; treat as breaking unless proven
  COMPATIBLE rule :legacy removed - every v1-legal call remains legal

  verdict: 4 breaking rule change(s) -> major version bump.

  round 8's advisor ended every report with a shrug: "3 breaking
  changes IN THE DECLARATIONS" - rules were lambdas, invisible to
  any diff. relations closed that: the limit, the fields, and the
  law itself are data, so tightening 6000->4000 is as diffable as
  a max: change. note the one law-change row: same rule id, same
  fields, different relation - that's not an edit, it's a new
  contract wearing an old name, and the diff says so. the lambda
  rule still gets the shrug (OPAQUE, presumed breaking) - which is
  now a choice you make per rule, not a ceiling on the tool.

source

# frozen_string_literal: true

# The Relation Diff: round 8's semver advisor classified declaration
# changes but had to shrug at rules - lambdas can't be compared. Now
# relations are data, so the RULES diff too: a tightened limit is
# breaking, a loosened one compatible, a new rule breaking, a removed
# one compatible, and a changed relation TYPE is a different law
# entirely. The last opaque corner of the contract joins semver.
#
#   bundle exec ruby examples/relation_diff.rb
#
# Runs offline; v2 contains one of every interesting rule change.

require class="s">"bundler/setup"
require class="s">"agentic"

V1_RULES = {
  fits: {relation: class="y">:sum_lte, fields: [class="y">:weight, class="y">:volume], limit: 6_000},
  customs: {relation: class="y">:requires, fields: [class="y">:express, class="y">:customs_code]},
  one_auth: {relation: class="y">:mutually_exclusive, fields: [class="y">:api_key, class="y">:oauth_token]},
  legacy: {relation: class="y">:requires, fields: [class="y">:fragile, class="y">:packaging]},
  audited: {message: class="s">"audited accounts only", fields: [class="y">:account], check: ->(i) { true }}
}.freeze

V2_RULES = {
  fits: {relation: class="y">:sum_lte, fields: [class="y">:weight, class="y">:volume], limit: 4_000},   # tightened
  customs: {relation: class="y">:requires, fields: [class="y">:express, class="y">:customs_code, class="y">:incoterm]}, # widened scope
  one_auth: {relation: class="y">:requires, fields: [class="y">:api_key, class="y">:oauth_token]},      # DIFFERENT LAW
  speedy: {relation: class="y">:sum_lte, fields: [class="y">:weight], limit: 100},            # new rule
  # legacy: removed
  audited: {message: class="s">"audited accounts only", fields: [class="y">:account], check: ->(i) { i[class="y">:account] != class="s">"test" }}
}.freeze

def classify(v1, v2)
  changes = []
  (v1.keys & v2.keys).each do |id|
    old_rule, new_rule = v1[id], v2[id]
    if old_rule[class="y">:relation] && new_rule[class="y">:relation]
      if old_rule[class="y">:relation] != new_rule[class="y">:relation]
        changes << [class="y">:breaking, class="s">"rule :#{id} changed LAW: #{old_rule[class="y">:relation]} -> #{new_rule[class="y">:relation]} - not an edit, a replacement"]
        next
      end
      case new_rule[class="y">:relation]
      when class="y">:sum_lte
        changes << [class="y">:breaking, class="s">"rule :#{id} limit tightened #{old_rule[class="y">:limit]} -> #{new_rule[class="y">:limit]} - previously legal calls rejected"] if new_rule[class="y">:limit] < old_rule[class="y">:limit]
        changes << [class="y">:compatible, class="s">"rule :#{id} limit loosened #{old_rule[class="y">:limit]} -> #{new_rule[class="y">:limit]}"] if new_rule[class="y">:limit] > old_rule[class="y">:limit]
      when class="y">:requires
        added = new_rule[class="y">:fields] - old_rule[class="y">:fields]
        removed = old_rule[class="y">:fields] - new_rule[class="y">:fields]
        changes << [class="y">:breaking, class="s">"rule :#{id} now also demands #{added.join(", class="s">")} - callers satisfying v1 fail v2"] if added.any?
        changes << [class="y">:compatible, class="s">"rule :#{id} no longer demands #{removed.join(", class="s">")}"] if removed.any? && added.none?
      when class="y">:mutually_exclusive
        changes << [class="y">:breaking, class="s">"rule :#{id} exclusion widened to #{new_rule[class="y">:fields].join(", class="s">")}"] if (new_rule[class="y">:fields] - old_rule[class="y">:fields]).any?
      end
    elsif old_rule[class="y">:relation].nil? && new_rule[class="y">:relation].nil?
      changes << [class="y">:opaque, class="s">"rule :#{id} is a lambda in both versions - the diff cannot see inside; treat as breaking unless proven"]
    end
  end
  (v2.keys - v1.keys).each do |id|
    changes << [class="y">:breaking, class="s">"rule :#{id} added (#{V2_RULES[id][class="y">:relation]}) - a new law existing callers never agreed to"]
  end
  (v1.keys - v2.keys).each do |id|
    changes << [class="y">:compatible, class="s">"rule :#{id} removed - every v1-legal call remains legal"]
  end
  changes
end

changes = classify(V1_RULES, V2_RULES)
breaking = changes.count { |kind, _| kind == class="y">:breaking }

puts class="s">"RELATION DIFF: quote_shipping rules, v1 -> v2"
puts
order = {breaking: 0, opaque: 1, compatible: 2}
changes.sort_by { |kind, _| order[kind] }.each do |kind, message|
  puts format(class="s">"  %-10s %s", kind.to_s.upcase, message)
end

puts
puts class="s">"  verdict: #{breaking} breaking rule change(s) -> major version bump."
puts
puts class="s">"  round 8's advisor ended every report with a shrug: \"3 breaking"
puts class="s">"  changes IN THE DECLARATIONS\" - rules were lambdas, invisible to"
puts class="s">"  any diff. relations closed that: the limit, the fields, and the"
puts class="s">"  law itself are data, so tightening 6000->4000 is as diffable as"
puts class="s">"  a max: change. note the one law-change row: same rule id, same"
puts class="s">"  fields, different relation - that's not an edit, it's a new"
puts class="s">"  contract wearing an old name, and the diff says so. the lambda"
puts class="s">"  rule still gets the shrug (OPAQUE, presumed breaking) - which is"
puts class="s">"  now a choice you make per rule, not a ceiling on the tool."