agentic examples

Gentle Deprecations

Gentle Deprecations: the hard part of maintaining a framework isn't adding the better name - it's the two years of not breaking anyone who used the old one. This shims a renamed contract field through three release phases: translate-and-warn (once per call site, with the caller named), count everything for the migration report, and a strict mode that turns warnings into CI failures ON YOUR schedule, not the users'.

Scheduling & Concurrency Round 12 Rafael França exit 0

source on github

bundle exec ruby examples/gentle_deprecations.rb

a real captured run

GENTLE DEPRECATIONS (rename shipped; nobody broken; everybody counted)


  the migration report (deprecation is data about your users):
    weight at legacy_billing_job (gentle_deprecations.rb:76) 3 call(s)
    weight at legacy_admin_panel (gentle_deprecations.rb:78) 2 call(s)
    migrated call sites warn nothing and appear nowhere.

  strict mode (release N+2, or CI today): DEPRECATED input :weight (use :weight_kg) - strict mode refuses it

  the choreography, straight from the Rails playbook: release N
  adds the new name and the shim - old code runs, warns once per
  call site (per-site, or your logs become the outage), and the
  tally tells you exactly who still needs a PR. release N+1 you
  chase the tally to zero. release N+2 flips strict and deletes
  the shim on YOUR schedule - because the deadline was enforced
  by CI failures in the laggards' builds, not by breaking their
  production. renames are cheap; broken trust compounds.

source

# frozen_string_literal: true

# Gentle Deprecations: the hard part of maintaining a framework isn't
# adding the better name - it's the two years of not breaking anyone
# who used the old one. This shims a renamed contract field through
# three release phases: translate-and-warn (once per call site, with
# the caller named), count everything for the migration report, and
# a strict mode that turns warnings into CI failures ON YOUR schedule,
# not the users'.
#
#   bundle exec ruby examples/gentle_deprecations.rb
#
# Runs offline; three "apps" call the API from three code sites.

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

Agentic.logger.level = class="y">:fatal

# v2 renamed weight: -> weight_kg:. The contract only knows the new world.
CONTRACT = Agentic:class="y">:CapabilitySpecification.new(
  name: class="s">"quote_shipping", description: class="s">"Quote a shipment", version: class="s">"2.0.0",
  inputs: {
    mode: {type: class="s">"string", required: true, enum: %w[air sea]},
    weight_kg: {type: class="s">"number", required: true, min: 1}
  },
  outputs: {price_cents: {type: class="s">"number", required: true}}
)

# The shim: old names translated at the door, warned once per call
# site, tallied for the report. Deprecation is DATA about your users.
class DeprecationShim
  RENAMES = {weight: class="y">:weight_kg}.freeze

  attr_reader class="y">:hits

  def initialize(strict: false)
    @strict = strict
    @warned = {}
    @hits = Hash.new(0)
  end

  def translate(inputs)
    RENAMES.each do |old_name, new_name|
      next unless inputs.key?(old_name)

      # The interesting frame is the USER's: skip the shim and the API
      # boundary, blame the first frame that belongs to neither
      site_location = caller_locations.find { |l| !l.label.include?(class="s">"translate") && !%w[each quote].include?(l.label) }
      site = class="s">"#{site_location.label} (#{site_location.to_s[/[^\/]+:\d+/]})"
      @hits[class="s">"#{old_name} at #{site}"] += 1
      if @strict
        raise ArgumentError, class="s">"DEPRECATED input :#{old_name} (use :#{new_name}) - strict mode refuses it"
      end
      unless @warned[class="s">"#{old_name}-#{site}"]
        @warned[class="s">"#{old_name}-#{site}"] = true
        warn class="s">"  DEPRECATION: :#{old_name} is now :#{new_name} (called from #{site}; this warning shows once per site)"
      end
      inputs = inputs.dup
      inputs[new_name] = inputs.delete(old_name)
    end
    inputs
  end
end

SHIM = DeprecationShim.new
VALIDATOR = Agentic:class="y">:CapabilityValidator.new(CONTRACT)

def quote(inputs)
  inputs = SHIM.translate(inputs)
  VALIDATOR.validate_inputs!(inputs)
  {price_cents: (inputs[class="y">:weight_kg] * ((inputs[class="y">:mode] == class="s">"air") ? 9 : 2) * 100).round}
end

# Three call sites: one migrated, two still on the old name
def legacy_billing_job = quote(mode: class="s">"air", weight: 12)

def legacy_admin_panel = quote(mode: class="s">"sea", weight: 400)

def migrated_checkout = quote(mode: class="s">"air", weight_kg: 3)

puts class="s">"GENTLE DEPRECATIONS (rename shipped; nobody broken; everybody counted)"
puts
3.times { legacy_billing_job }
2.times { legacy_admin_panel }
4.times { migrated_checkout }

puts
puts class="s">"  the migration report (deprecation is data about your users):"
SHIM.hits.each { |site, count| puts format(class="s">"    %-46s %d call(s)", site, count) }
puts class="s">"    migrated call sites warn nothing and appear nowhere."
puts

# Release N+2: strict mode - the same shim becomes the enforcement
strict = DeprecationShim.new(strict: true)
begin
  strict.translate(mode: class="s">"air", weight: 12)
rescue ArgumentError => e
  puts class="s">"  strict mode (release N+2, or CI today): #{e.message}"
end
puts
puts class="s">"  the choreography, straight from the Rails playbook: release N"
puts class="s">"  adds the new name and the shim - old code runs, warns once per"
puts class="s">"  call site (per-site, or your logs become the outage), and the"
puts class="s">"  tally tells you exactly who still needs a PR. release N+1 you"
puts class="s">"  chase the tally to zero. release N+2 flips strict and deletes"
puts class="s">"  the shim on YOUR schedule - because the deadline was enforced"
puts class="s">"  by CI failures in the laggards' builds, not by breaking their"
puts class="s">"  production. renames are cheap; broken trust compounds."