The RBS Export
The RBS Export: a capability contract already knows its types - it validates them at runtime on every call. RBS is the same knowledge written down for tools that read instead of run: steep, IDEs, docs. This generates .rbs signatures from contracts, so the type checker and the validator can never disagree - they're projections of one declaration.
Data & Pipelines
Round 14
Soutaro Matsumoto
exit 0
bundle exec ruby examples/rbs_export.rb
a real captured run
THE RBS EXPORT (contracts already know their types; write them down)
# Quote a shipment (v2.1.0)
# Enum/bounds/rules are enforced at runtime by CapabilityValidator;
# RBS carries the SHAPE, the validator carries the LAW.
class QuoteShippingCapability
def call: ({ mode: String, weight_kg: Numeric, ?express: bool, ?customs_code: String } inputs) -> { price_cents: Integer, carrier: String }
end
# Route a ticket (v1.1.0)
# Enum/bounds/rules are enforced at runtime by CapabilityValidator;
# RBS carries the SHAPE, the validator carries the LAW.
class ClassifyTicketCapability
def call: ({ text: String, ?urgency: Numeric } inputs) -> { queue: String }
end
agreement spot-check against the validator:
omitting ?-marked keys (express, customs_code): accepted - agrees
omitting an unmarked key (weight_kg): rejected - agrees
the division of labor, stated precisely: RBS carries the SHAPE
(keys, types, optionality - what steep and your IDE can check
before anything runs), and the validator carries the LAW (enums,
bounds, cross-field rules - what needs values to judge). neither
replaces the other; both project from ONE declaration, which is
why they cannot drift the way hand-written sig files against
hand-written validations always, always do. gradual typing works
when the types come from where the truth already lives.
source
# frozen_string_literal: true # The RBS Export: a capability contract already knows its types - # it validates them at runtime on every call. RBS is the same # knowledge written down for tools that read instead of run: steep, # IDEs, docs. This generates .rbs signatures from contracts, so the # type checker and the validator can never disagree - they're # projections of one declaration. # # bundle exec ruby examples/rbs_export.rb # # Runs offline; the signatures are printed and self-checked. require class="s">"bundler/setup" require class="s">"agentic" # Contract type -> RBS type. Optional inputs may be omitted entirely, # so they project as optional KEYS (key: ?), while nilability is a # separate question the contract answers with its type check. RBS_TYPES = { class="s">"string" => class="s">"String", class="s">"number" => class="s">"Numeric", class="s">"integer" => class="s">"Integer", class="s">"boolean" => class="s">"bool", class="s">"array" => class="s">"Array[untyped]", class="s">"object" => class="s">"Hash[Symbol, untyped]", class="s">"hash" => class="s">"Hash[Symbol, untyped]", nil => class="s">"untyped" }.freeze def rbs_record(declared) fields = declared.map { |key, decl| marker = decl[class="y">:required] ? class="s">"" : class="s">"?" class="s">"#{marker}#{key}: #{RBS_TYPES.fetch(decl[class="y">:type], "untypedclass="s">")}" } class="s">"{ #{fields.join(", class="s">")} }" end def to_rbs(spec) method_name = spec.name.gsub(/[^a-z0-9_]/, class="s">"_") <<~RBS # #{spec.description} (v#{spec.version}) # Enum/bounds/rules are enforced at runtime by CapabilityValidator; # RBS carries the SHAPE, the validator carries the LAW. class #{method_name.split(class="s">"_").map(&class="y">:capitalize).join}Capability def call: (#{rbs_record(spec.inputs)} inputs) -> #{rbs_record(spec.outputs)} end RBS end SPECS = [ Agentic:class="y">:CapabilitySpecification.new( name: class="s">"quote_shipping", description: class="s">"Quote a shipment", version: class="s">"2.1.0", inputs: { mode: {type: class="s">"string", required: true, enum: %w[air sea road]}, weight_kg: {type: class="s">"number", required: true, min: 1, max: 5_000}, express: {type: class="s">"boolean"}, customs_code: {type: class="s">"string"} }, outputs: {price_cents: {type: class="s">"integer", required: true}, carrier: {type: class="s">"string", required: true}} ), Agentic:class="y">:CapabilitySpecification.new( name: class="s">"classify_ticket", description: class="s">"Route a ticket", version: class="s">"1.1.0", inputs: {text: {type: class="s">"string", required: true, non_empty: true}, urgency: {type: class="s">"number"}}, outputs: {queue: {type: class="s">"string", required: true}} ) ].freeze puts class="s">"THE RBS EXPORT (contracts already know their types; write them down)" puts SPECS.each do |spec| to_rbs(spec).lines.each { |line| puts class="s">" #{line}" } puts end # --- the agreement check: what RBS says optional, the validator permits --------- # (Same discipline as round 10's projection prover: two renderings of # one declaration must be spot-checked against each other.) spec = SPECS.first validator = Agentic:class="y">:CapabilityValidator.new(spec) optional_omitted = {mode: class="s">"air", weight_kg: 100} # express, customs_code omitted required_omitted = {mode: class="s">"air"} # weight_kg missing validator.validate_inputs!(optional_omitted) agreement_a = true agreement_b = begin validator.validate_inputs!(required_omitted) false # validator allowed what RBS marks required - disagreement! rescue Agentic:class="y">:Errors:class="y">:ValidationError true end puts class="s">" agreement spot-check against the validator:" puts class="s">" omitting ?-marked keys (express, customs_code): accepted #{agreement_a ? "- agreesclass="s">" : "DISAGREESclass="s">"}" puts class="s">" omitting an unmarked key (weight_kg): rejected #{agreement_b ? "- agreesclass="s">" : "DISAGREESclass="s">"}" puts puts class="s">" the division of labor, stated precisely: RBS carries the SHAPE" puts class="s">" (keys, types, optionality - what steep and your IDE can check" puts class="s">" before anything runs), and the validator carries the LAW (enums," puts class="s">" bounds, cross-field rules - what needs values to judge). neither" puts class="s">" replaces the other; both project from ONE declaration, which is" puts class="s">" why they cannot drift the way hand-written sig files against" puts class="s">" hand-written validations always, always do. gradual typing works" puts class="s">" when the types come from where the truth already lives." exit((agreement_a && agreement_b) ? 0 : 1)