agentic examples

The Contract Cop

The Contract Cop: RuboCop for capability specs. Contracts are the most-read documents in this framework - six tools consume them - so they deserve a style guide with teeth: named cops, an offense report, and autocorrection for everything mechanical. Style is not vanity; it's the cost of reading, paid down in advance.

Data & Pipelines Round 11 Bozhidar Batsov exit 0

source on github

bundle exec ruby examples/contract_cop.rb

a real captured run

CONTRACT COP (7 cops on the beat)

  inspecting quote_shipping... 8 offenses:
    Naming/SnakeCaseName              capability name 'QuoteShipping' is not snake_case
    Naming/SnakeCaseFields            input :Mode is not snake_case
    Naming/SnakeCaseFields            input :weightKg is not snake_case
    Documentation/Description         capability has no description
    Style/EnumOrder                   input :Mode enum is not sorted
    Lint/UntypedField                 input :ref has no type (and won't project into schemas)
    Lint/OpaqueRuleWithoutMessage     rule :check1 is opaque AND messageless - violations will say nothing
    Metrics/RequiredInputCount        7 required inputs (max 5) - is this one capability or three?

  after autocorrect (4 offenses fixed mechanically):
    Documentation/Description         capability has no description
    Lint/UntypedField                 input :ref has no type (and won't project into schemas)
    Lint/OpaqueRuleWithoutMessage     rule :check1 is opaque AND messageless - violations will say nothing
    Metrics/RequiredInputCount        7 required inputs (max 5) - is this one capability or three?

  what autocorrect fixed, it fixed safely: names snake_cased,
  enums sorted - transformations with exactly one right answer.
  what remains is the honest residue: a missing description
  (only the author knows what it does), an untyped field
  (guessing types is how bugs get typed), an opaque messageless
  rule, and seven required inputs' worth of scope creep. a linter's
  job splits exactly there - automate the mechanical, and make
  the judgment calls impossible to not-see. style is applied
  empathy for the next reader, and contracts have six readers.

source

# frozen_string_literal: true

# The Contract Cop: RuboCop for capability specs. Contracts are the
# most-read documents in this framework - six tools consume them -
# so they deserve a style guide with teeth: named cops, an offense
# report, and autocorrection for everything mechanical. Style is not
# vanity; it's the cost of reading, paid down in advance.
#
#   bundle exec ruby examples/contract_cop.rb
#
# Runs offline; a messy contract walks in, autocorrect walks it out.

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

# The defendant: a contract written at 6pm on a Friday
MESSY = {
  name: class="s">"QuoteShipping", description: class="s">"", version: class="s">"1.0.0",
  inputs: {
    Mode: {type: class="s">"string", required: true, enum: %w[sea air road]},
    weightKg: {type: class="s">"number", required: true, min: 0, max: 5_000},
    customs_code: {type: class="s">"string"},
    ref: {},
    a: {type: class="s">"string", required: true},
    b: {type: class="s">"string", required: true},
    c: {type: class="s">"string", required: true},
    d: {type: class="s">"string", required: true},
    e: {type: class="s">"string", required: true}
  },
  rules: {
    check1: {fields: [class="y">:weightKg], check: ->(i) { i[class="y">:weightKg] < 5_000 }}
  }
}.freeze

# Each cop: name, check (spec-hash in, offenses out), correctable?
COPS = {
  class="s">"Naming/SnakeCaseName" => {
    check: ->(s) { (s[class="y">:name] =~ /\A[a-z][a-z0-9_]*\z/) ? [] : [class="s">"capability name '#{s[class="y">:name]}' is not snake_case"] },
    correct: ->(s) { s[class="y">:name] = s[class="y">:name].gsub(/([a-z])([A-Z])/, '\1_\2').downcase }
  },
  class="s">"Naming/SnakeCaseFields" => {
    check: ->(s) { s[class="y">:inputs].keys.reject { |k| k =~ /\A[a-z][a-z0-9_]*\z/ }.map { |k| class="s">"input :#{k} is not snake_case" } },
    correct: ->(s) { s[class="y">:inputs].transform_keys! { |k| k.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym } }
  },
  class="s">"Documentation/Description" => {
    check: ->(s) { s[class="y">:description].to_s.empty? ? [class="s">"capability has no description"] : [] },
    correct: nil # a human must actually say what it does
  },
  class="s">"Style/EnumOrder" => {
    check: ->(s) { s[class="y">:inputs].select { |_, d| d[class="y">:enum] && d[class="y">:enum] != d[class="y">:enum].sort }.map { |k, _| class="s">"input :#{k} enum is not sorted" } },
    correct: ->(s) { s[class="y">:inputs].each_value { |d| d[class="y">:enum] = d[class="y">:enum].sort if d[class="y">:enum] } }
  },
  class="s">"Lint/UntypedField" => {
    check: ->(s) { s[class="y">:inputs].select { |_, d| d[class="y">:type].nil? }.map { |k, _| class="s">"input :#{k} has no type (and won't project into schemas)" } },
    correct: nil # guessing a type is how bugs get typed
  },
  class="s">"Lint/OpaqueRuleWithoutMessage" => {
    check: ->(s) {
      s[class="y">:rules].select { |_, d| d.respond_to?(class="y">:call) || (d[class="y">:check] && !d[class="y">:message]) }
        .map { |k, _| class="s">"rule :#{k} is opaque AND messageless - violations will say nothing" }
    },
    correct: nil # the message is the author's testimony; can't forge it
  },
  class="s">"Metrics/RequiredInputCount" => {
    check: ->(s) {
      required = s[class="y">:inputs].count { |_, d| d[class="y">:required] }
      (required > 5) ? [class="s">"#{required} required inputs (max 5) - is this one capability or three?"] : []
    },
    correct: nil
  }
}.freeze

def inspect_spec(spec_hash)
  COPS.flat_map { |cop, definition| definition[class="y">:check].call(spec_hash).map { |offense| [cop, offense] } }
end

puts class="s">"CONTRACT COP (#{COPS.size} cops on the beat)"
puts
offenses = inspect_spec(MESSY)
puts class="s">"  inspecting quote_shipping... #{offenses.size} offenses:"
offenses.each { |cop, offense| puts format(class="s">"    %-33s %s", cop, offense) }
puts

# --- autocorrect what's mechanical ----------------------------------------------
corrected = {name: MESSY[class="y">:name].dup, description: MESSY[class="y">:description].dup, version: MESSY[class="y">:version],
             inputs: MESSY[class="y">:inputs].transform_values(&class="y">:dup).dup, rules: MESSY[class="y">:rules].dup}
corrected[class="y">:inputs].each_value { |d| d[class="y">:enum] = d[class="y">:enum].dup if d[class="y">:enum] }
COPS.each_value { |definition| definition[class="y">:correct]&.call(corrected) }

remaining = inspect_spec(corrected)
puts class="s">"  after autocorrect (#{offenses.size - remaining.size} offenses fixed mechanically):"
remaining.each { |cop, offense| puts format(class="s">"    %-33s %s", cop, offense) }
puts
puts class="s">"  what autocorrect fixed, it fixed safely: names snake_cased,"
puts class="s">"  enums sorted - transformations with exactly one right answer."
puts class="s">"  what remains is the honest residue: a missing description"
puts class="s">"  (only the author knows what it does), an untyped field"
puts class="s">"  (guessing types is how bugs get typed), an opaque messageless"
puts class="s">"  rule, and seven required inputs' worth of scope creep. a linter's"
puts class="s">"  job splits exactly there - automate the mechanical, and make"
puts class="s">"  the judgment calls impossible to not-see. style is applied"
puts class="s">"  empathy for the next reader, and contracts have six readers."