0.01
The project is in a healthy, maintained state
TraitEngine replaces nested if/else logic with a concise DSL that maps data sources to derived attributes using reusable traits, transformations, and decision tables.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies
 Project Readme

Kumi

CI Gem Version

Kumi is a declarative rules-and-calculation DSL for Ruby. It compiles business logic into a typed, analyzable dependency graph with vector semantics over nested data, performs static checks at definition time, lowers to a compact IR, and executes deterministically.

It handles complex, interdependent calculations with validation and consistency checking.

What can you build?

Calculate U.S. federal taxes:

module FederalTax2024
  extend Kumi::Schema
  
  schema do
    input do
      float  :income
      string :filing_status, domain: %w[single married_joint]
    end
    
    # Standard deduction by filing status
    trait :single,  input.filing_status == "single"
    trait :married, input.filing_status == "married_joint"
    
    value :std_deduction do
      on single,  14_600
      on married, 29_200
      base        21_900  # head_of_household
    end
    
    value :taxable_income, fn(:max, [input.income - std_deduction, 0])
    
    # Federal tax brackets
    value :fed_breaks do
      on single,  [11_600, 47_150, 100_525, 191_950, 243_725, 609_350, Float::INFINITY]
      on married, [23_200, 94_300, 201_050, 383_900, 487_450, 731_200, Float::INFINITY]
    end
    
    value :fed_rates, [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37]
    value :fed_calc,  fn(:piecewise_sum, taxable_income, fed_breaks, fed_rates)
    value :fed_tax,   fed_calc[0]
    
    # FICA taxes
    value :ss_tax, fn(:min, [input.income, 168_600]) * 0.062
    value :medicare_tax, input.income * 0.0145
    
    value :total_tax, fed_tax + ss_tax + medicare_tax
    value :after_tax, input.income - total_tax
  end
end

# Use it
result = FederalTax2024.from(income: 100_000, filing_status: "single")
result[:total_tax]   # => 21,491.00
result[:after_tax]   # => 78,509.00

Real tax calculation with brackets, deductions, and FICA caps.

Validation happens during schema definition.

Installation

# Requires Ruby 3.1+
# No external dependencies
gem install kumi

Core Features

Schema Primitives - Four building blocks for business logic

Schema Primitives

Kumi schemas are built from four primitives:

Inputs define the data flowing into your schema with built-in validation:

input do
  float :price, domain: 0..1000.0      # Validates range
  integer :quantity, domain: 1..10000   # Validates range
  string :tier, domain: %w[standard premium]  # Validates inclusion
end

Values are computed attributes that automatically memoize their results

value :subtotal, input.price * input.quantity
value :tax_rate, 0.08
value :tax_amount, subtotal * tax_rate

Traits are boolean conditions for branching logic:

trait :bulk_order, input.quantity >= 100
trait :premium_customer, input.tier == "premium"

value :discount do
  on bulk_order, premium_customer, 0.25  # 25% for bulk premium orders
  on bulk_order, 0.15                     # 15% for bulk orders
  on premium_customer, 0.10               # 10% for premium customers
  base 0.0                                # No discount otherwise
end

Functions are computational building blocks:

value :final_price, [subtotal - discount_amount, 0].max
value :monthly_payment, fn(:pmt, rate: 0.05/12, nper: 36, pv: -loan_amount)

Note: You can find a list all core functions in docs/FUNCTIONS.md

Analysis & Introspection - Static verification, runtime inspection, and metadata extraction

Analysis & Introspection

Kumi provides comprehensive analysis capabilities - catching errors at definition time and exposing schema structure for tooling and debugging.

Static Analysis: Catch Errors Early

Kumi catches many types of business logic errors that cause runtime failures or silent bugs:

module InsurancePolicyPricer
  extend Kumi::Schema
  
  schema do
    input do
      integer :age, domain: 18..80
      string :risk_category, domain: %w[low medium high]
      float :coverage_amount, domain: 50_000..2_000_000
      integer :years_experience, domain: 0..50
      boolean :has_claims
    end
    
    # Risk assessment with subtle interdependencies
    trait :young_driver, input.age < 25
    trait :experienced, input.years_experience >= 5
    trait :high_risk, input.risk_category == "high"
    trait :senior_driver, input.age >= 65
    
    # Base premium calculation
    value :base_premium, input.coverage_amount * 0.02
    
    # Experience adjustment with subtle circular reference
    value :experience_factor do
      on experienced & young_driver, experience_discount * 0.8  # ❌ Uses experience_discount before it's defined
      on experienced, 0.85
      on young_driver, 1.25
      base 1.0
    end
    
    # Risk multipliers that create impossible combinations
    value :risk_multiplier do
      on high_risk & experienced, 1.5    # High risk but experienced
      on high_risk, 2.0                  # Just high risk
      on low_risk & young_driver, 0.9    # ❌ low_risk is undefined (typo for input.risk_category)
      base 1.0
    end
    
    # Claims history impact
    trait :claims_free, fn(:not, input.has_claims)
    trait :perfect_record, claims_free & experienced & fn(:not, young_driver)
    
    # Discount calculation with type error
    value :experience_discount do
      on perfect_record, input.years_experience + "%" # ❌ String concatenation with integer
      on claims_free, 0.95
      base 1.0
    end
    
    # Premium calculation chain
    value :adjusted_premium, base_premium * experience_factor * risk_multiplier
    
    # Age-based impossible logic
    trait :mature_professional, senior_driver & experienced & young_driver  # ❌ Can't be senior AND young
    
    # Final premium with self-referencing cascade
    value :final_premium do
      on mature_professional, adjusted_premium * 0.8
      on senior_driver, adjusted_premium * senior_adjustment  # ❌ senior_adjustment undefined
      base final_premium * 1.1  # ❌ Self-reference in base case
    end
    
    # Monthly payment calculation with function arity error
    value :monthly_payment, fn(:divide, final_premium)  # ❌ divide needs 2 arguments, got 1
  end
end

# Static analysis catches these errors:
# ❌ Circular reference: experience_factor → experience_discount → experience_factor
# ❌ Undefined reference: low_risk (should be input.risk_category == "low")
# ❌ Type mismatch: integer + string in experience_discount
# ❌ Impossible conjunction: senior_driver & young_driver
# ❌ Undefined reference: senior_adjustment
# ❌ Self-reference cycle: final_premium references itself in base case
# ❌ Function arity error: divide expects 2 arguments, got 1

Mutual Recursion: Kumi supports mutual recursion when cascade conditions are mutually exclusive:

trait :is_forward, input.operation == "forward"
trait :is_reverse, input.operation == "reverse"

# Safe mutual recursion - conditions are mutually exclusive
value :forward_processor do
  on is_forward, input.value * 2        # Direct calculation
  on is_reverse, reveAnalysisrse_processor + 10  # Delegates to reverse (safe)
  base "invalid operation"
end

value :reverse_processor do
  on is_forward, forward_processor - 5   # Delegates to forward (safe) 
  on is_reverse, input.value / 2         # Direct calculation
  base "invalid operation"
end

# Usage examples:
# operation="forward", value=10  => forward: 20, reverse: 15
# operation="reverse", value=10  => forward: 15, reverse: 5  
# operation="unknown", value=10  => both: "invalid operation"

This compiles because operation can only be "forward" or "reverse", never both. Each recursion executes one step before hitting a direct calculation.

Runtime Introspection: Debug and Understand

Explainability: Trace exactly how any value is computed, step-by-step. This is invaluable for debugging complex logic and auditing results.

Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_status: "single"})
# => fed_tax = fed_calc[0]
#    = (fed_calc = piecewise_sum(taxable_income, fed_breaks, fed_rates)
#       = piecewise_sum(85_400, [11_600, 47_150, ...], [0.10, 0.12, ...])
#       = [15_099.50, 0.22])
#    = 15_099.50

Schema Metadata API: Build Tooling

Programmatically access the analyzed structure of your schema to build tools like form generators, documentation sites, or custom validators.

metadata = FederalTax2024.schema_metadata

# Processed, tool-friendly metadata
metadata.inputs           # => { name: { type: :string, domain: ... } }
metadata.values           # => { name: { dependencies: [...], expression: "..." } }
metadata.traits           # => { name: { condition: "...", dependencies: [...] } }

# Raw analyzer state for deep analysis
metadata.dependencies     # Dependency graph between all declarations
metadata.evaluation_order # Topologically sorted computation order

# Export to standard formats
metadata.to_h             # => Serializable hash for JSON/APIs
metadata.to_json_schema   # => JSON Schema for input validation

AST Visualization: See the Structure

For deep debugging, you can print the raw Abstract Syntax Tree (AST) of a schema.

puts Kumi::Support::SExpressionPrinter.print(FederalTax2024.__syntax_tree__)
# => (Root
#      inputs: [
#        (InputDeclaration :income :float)
#        (InputDeclaration :filing_status :string domain: ["single", "married_joint"])
#      ]
#      traits: [...]
#      attributes: [...])
Hierarchical Broadcasting - Vectorization over nested data structures

Hierarchical Broadcasting

Kumi broadcasts operations over hierarchical data structures with two complementary access modes for maximum flexibility.

See docs/features/hierarchical-broadcasting.md for detailed documentation.

Business Scenario: E-commerce checkout with dynamic pricing rules

"As an e-commerce platform, I need to calculate order totals with complex discount rules:

  • Premium members get 15% off electronics
  • Bulk orders (5+ items) get 10% off that item
  • Free shipping on orders over $100
  • Calculate: item subtotals, total discounts, shipping, final total

The challenge: Each order has different items, quantities, categories, and customer tiers. The discount logic involves multiple conditions - some items qualify for multiple discounts, others for none. Traditional pricing code requires nested if-statements and manual calculations."

Kumi Solution (16 lines of declarative pricing logic):

module OrderPricing
  extend Kumi::Schema
  
  schema do
    input do
      array :items do
        float   :price
        integer :quantity
        string  :category
      end
      string :customer_tier
      float  :shipping_threshold
    end
    
    # Calculate item subtotals and discount eligibility
    value :subtotals, input.items.price * input.items.quantity
    trait :electronics, input.items.category == "electronics"
    trait :bulk_item, input.items.quantity >= 5
    trait :premium_customer, input.customer_tier == "premium"
    
    # Apply layered discounts (premium + bulk can stack)
    trait :premium_electronics, premium_customer & electronics
    trait :stacked_discount, premium_electronics & bulk_item
    
    value :discounted_prices do
      on stacked_discount, input.items.price * 0.75      # 15% + 10% = 25% off
      on premium_electronics, input.items.price * 0.85   # 15% off
      on bulk_item, input.items.price * 0.90             # 10% off
      base input.items.price                              # No discount
    end
    
    value :final_subtotals, discounted_prices * input.items.quantity
    
    # Order totals and conditional shipping
    value :subtotal, fn(:sum, final_subtotals)
    value :total_savings, fn(:sum, subtotals) - subtotal
    value :shipping, subtotal > input.shipping_threshold ? 0.0 : 9.99
    value :total, subtotal + shipping
  end
end

Mixed Access Modes: Both object and element access can be used together in the same schema:

module UserAnalytics
  extend Kumi::Schema
  
  schema do
    input do
      # Object access mode - structured business data
      array :users do
        string :name
        integer :age
      end
      
      # Element access mode - multi-dimensional raw arrays
      array :recent_purchases do
        element :integer, :days_ago
      end
    end

    # Object access works normally
    value :user_names, input.users.name
    value :avg_age, fn(:avg, input.users.age)
    
    # Element access handles nested arrays with progressive path traversal
    value :all_purchase_days, fn(:flatten, input.recent_purchases.days_ago)
    value :recent_flags, input.recent_purchases.days_ago < 5
    trait :has_recent_purchase, fn(:any?, fn(:flatten, recent_flags))
    
    # Progressive dimensional analysis - each path level goes deeper
    value :purchase_dimensions, [
      fn(:size, input.recent_purchases),           # Number of purchase groups
      fn(:size, input.recent_purchases.days_ago)  # Total individual purchase days
    ]
    
    # Mixed usage in conditions
    trait :adult_users, input.users.age >= 18
    value :adult_count, fn(:count_if, adult_users)
  end
end

# Works with mixed data structures
result = UserAnalytics.from({
  users: [{ name: "Alice", age: 25 }, { name: "Bob", age: 17 }],
  recent_purchases: [[1, 3, 7], [2, 4], [10, 15]]  # Nested arrays
})

result[:user_names]          # => ["Alice", "Bob"]
result[:has_recent_purchase] # => true (some purchases < 5 days ago)
result[:adult_count]         # => 1
result[:purchase_dimensions] # => [3, 8] (3 groups, 8 total days)
Memoization - Each value computed exactly once

Memoization

Each value is computed exactly once:

runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")

# First access computes full dependency chain
runner[:total_tax]     # => 53,155.20

# Subsequent access uses cached values
runner[:fed_tax]       # => 39,077.00 (cached)
runner[:after_tax]     # => 196,844.80 (cached)

Usage

Suitable for:

  • Complex interdependent business rules
  • Tax calculation engines
  • Insurance premium calculators
  • Commission structures with complex tiers
  • Pricing engines with multiple discount rules

Not suitable for:

  • Basic conditional statements
  • Sequential procedural workflows
  • High-frequency processing

JavaScript Transpiler (V 0.0.10)

Note: On the current 0.0.11 this is disabled but will be back in later versions. reason: IR/Interpreter update. Transpiles compiled schemas to standalone JavaScript code. See docs/features/javascript-transpiler.md for details.

Kumi::Js.export_to_file(FederalTax2024, "federal-tax-2024.js")
const { schema } = require('./federal-tax-2024.js');
const calculator = schema.from({ income: 100_000, filing_status: "single" });
console.log(calculator.fetch('total_tax'));   // 21491

Generated JavaScript includes only functions used by the schema.

All tests run in dual mode to verify compiled schemas produce identical results in both Ruby and JavaScript.

Performance

Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:

  • 50-deep dependency chain: 740,000/sec (analysis <50ms)
  • 1,000 attributes: 131,000/sec (analysis <50ms)
  • 10,000 attributes: 14,200/sec (analysis ~300ms)

See docs/features/performance.md for detailed benchmarks.

What Kumi does not guarantee

Lambdas (e.g. -> inside the schema), external IO, floating-point vs bignum, time-zone math differences, etc.

Learn More

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/amuta/kumi.

License

MIT License. See LICENSE.