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
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."