kumi 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +109 -2
- data/README.md +169 -213
- data/documents/DSL.md +3 -3
- data/documents/SYNTAX.md +17 -26
- data/examples/federal_tax_calculator_2024.rb +36 -38
- data/examples/game_of_life.rb +97 -0
- data/examples/simple_rpg_game.rb +1000 -0
- data/examples/static_analysis_errors.rb +178 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
- data/lib/kumi/analyzer/analysis_state.rb +37 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
- data/lib/kumi/analyzer/passes/definition_validator.rb +4 -3
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
- data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
- data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
- data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
- data/lib/kumi/analyzer.rb +42 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +20 -4
- data/lib/kumi/explain.rb +8 -8
- data/lib/kumi/function_registry/collection_functions.rb +103 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/sugar.rb +117 -16
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/declarations.rb +3 -0
- data/lib/kumi/syntax/expressions.rb +4 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/terminal_expressions.rb +3 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +10 -6
- data/CHANGELOG.md +0 -25
- data/test_impossible_cascade.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d4a3efa20727ebc89e39753f240bb6ed8fc3621b32771ea3b6d9096bdc21af5
|
4
|
+
data.tar.gz: 4c0a147c66d2b2ece9bec196b665d7b5fe613bb021caa5ec3efc00f0e0e6ed94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4e029e73067c2403290e1af7d9c841a1f8fad6782636275ec24b0626e3086684765e3010c727f839febd4238a61fe3723388ab899e4dece18701a720608a39df
|
7
|
+
data.tar.gz: e9a83993e264eca2a67821a9bc6d3f7bd59815c9c593fcd119fbbaed93fb92c345211023366aab076a35ef248b3580f84531aa52e1726463345e77e32c810ecb
|
data/CLAUDE.md
CHANGED
@@ -27,6 +27,19 @@ Kumi is a declarative decision-modeling compiler for Ruby that transforms comple
|
|
27
27
|
- `gem build kumi.gemspec` - Build the gem
|
28
28
|
- `gem install ./kumi-*.gem` - Install locally built gem
|
29
29
|
|
30
|
+
### Kumi CLI
|
31
|
+
- `./bin/kumi -i` - Start interactive REPL mode for rapid schema testing
|
32
|
+
- `./bin/kumi -f schema.rb -d data.json` - Execute schema file with input data
|
33
|
+
- `./bin/kumi -f schema.rb -k key1,key2` - Extract specific keys from schema
|
34
|
+
- `./bin/kumi -f schema.rb -e key_name` - Explain how a key is computed
|
35
|
+
- `./bin/kumi -f schema.rb -d data.json -o json` - Output results in JSON format
|
36
|
+
|
37
|
+
**CLI Features**:
|
38
|
+
- Interactive REPL with schema loading, data manipulation, and live evaluation
|
39
|
+
- File-based schema execution with JSON/YAML input data support
|
40
|
+
- Selective key extraction and explain functionality for debugging
|
41
|
+
- Multiple output formats (pretty, JSON, YAML) for integration
|
42
|
+
|
30
43
|
## Architecture Overview
|
31
44
|
|
32
45
|
### Core Components
|
@@ -91,6 +104,58 @@ Kumi is a declarative decision-modeling compiler for Ruby that transforms comple
|
|
91
104
|
- `domain/enum_analyzer.rb` - Enumeration domain analysis and validation
|
92
105
|
- `domain/violation_formatter.rb` - Formats domain violation error messages
|
93
106
|
|
107
|
+
## DSL Syntax Requirements
|
108
|
+
|
109
|
+
### Critical Syntax Rules
|
110
|
+
|
111
|
+
**Module Definition Structure** (REQUIRED for CLI):
|
112
|
+
```ruby
|
113
|
+
# CORRECT - CLI can find and load this
|
114
|
+
module SchemaName
|
115
|
+
extend Kumi::Schema
|
116
|
+
|
117
|
+
schema do
|
118
|
+
# schema definition here
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# INCORRECT - CLI cannot load standalone schemas
|
123
|
+
schema do # This won't work with CLI
|
124
|
+
# schema definition
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
**Function Call Syntax**:
|
129
|
+
- **Symbol style**: `fn(:function_name, arg1, arg2)` - Always works, explicit
|
130
|
+
- **Method style**: `fn.function_name(arg1, arg2)` - Also works, more readable
|
131
|
+
- **Incorrect**: `fn()` - Empty function calls cause parse errors
|
132
|
+
- **Incorrect**: `fn.function_name()` - Empty method calls cause parse errors
|
133
|
+
|
134
|
+
**Arithmetic Operations**:
|
135
|
+
- **Sugar Syntax**: `input.field1 + input.field2` - Works for input fields and value references
|
136
|
+
- **Function Syntax**: `fn(:add, input.field1, input.field2)` - Always works, more explicit
|
137
|
+
- **Mixed**: Use sugar for simple operations, functions for complex ones
|
138
|
+
|
139
|
+
**Cascade Condition Syntax**:
|
140
|
+
```ruby
|
141
|
+
# CORRECT - use symbols for trait references in cascades
|
142
|
+
value :status do
|
143
|
+
on trait_name, "Result"
|
144
|
+
base "Default"
|
145
|
+
end
|
146
|
+
|
147
|
+
# INCORRECT - bare identifiers don't work in cascade conditions
|
148
|
+
value :status do
|
149
|
+
on trait_name, "Result" # This will fail
|
150
|
+
base "Default"
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
**UnsatDetector Considerations**:
|
155
|
+
- Cascades with mutually exclusive conditions are valid (e.g., `amount < 100` vs `amount >= 100`)
|
156
|
+
- Values that depend on such cascades are also valid (fixed in recent update)
|
157
|
+
- Use clear, non-contradictory trait names to avoid confusion
|
158
|
+
|
94
159
|
### Key Patterns
|
95
160
|
|
96
161
|
**DSL Structure**:
|
@@ -120,8 +185,8 @@ end
|
|
120
185
|
In cascade expressions (`value :name do ... end`), trait references use **symbols**, not bare identifiers:
|
121
186
|
```ruby
|
122
187
|
value :status do
|
123
|
-
on
|
124
|
-
on
|
188
|
+
on adult, "Adult Status" # ✅ Correct - use trait_name symbol
|
189
|
+
on verified, "Verified User"
|
125
190
|
base "Unverified"
|
126
191
|
end
|
127
192
|
|
@@ -199,6 +264,48 @@ The `examples/` directory contains comprehensive examples showing Kumi usage pat
|
|
199
264
|
10. `documents/DSL.md` - Concise DSL syntax reference
|
200
265
|
11. `documents/AST.md` - AST node types and structure reference
|
201
266
|
12. `documents/SYNTAX.md` - Comprehensive sugar vs sugar-free syntax comparison with examples
|
267
|
+
13. `lib/kumi/cli.rb` - CLI implementation with REPL and file execution
|
268
|
+
14. `examples/simple_tax_schema.rb` - CLI-compatible schema example
|
269
|
+
|
270
|
+
## CLI Usage and Best Practices
|
271
|
+
|
272
|
+
### Schema File Requirements
|
273
|
+
- **Must use module structure**: `module SchemaName; extend Kumi::Schema; schema do ... end; end`
|
274
|
+
- **Must have proper require**: `require_relative "../lib/kumi"` at the top
|
275
|
+
- **Must have input block**: Even empty `input {}` blocks are required
|
276
|
+
- **Avoid inline definitions**: Don't define schemas directly in methods or blocks
|
277
|
+
|
278
|
+
### CLI Development Workflow
|
279
|
+
1. **Start with REPL**: Use `./bin/kumi -i` for rapid prototyping and testing
|
280
|
+
2. **Test with files**: Create `.rb` schema files and `.json/.yaml` data files
|
281
|
+
3. **Iterate quickly**: Use `-k key1,key2` to focus on specific outputs
|
282
|
+
4. **Debug with explain**: Use `-e key_name` to understand computation flow
|
283
|
+
5. **Validate with different data**: Test edge cases with varied input data
|
284
|
+
|
285
|
+
### Common CLI Patterns
|
286
|
+
```bash
|
287
|
+
# Interactive development
|
288
|
+
./bin/kumi -i
|
289
|
+
kumi> schema examples/my_schema.rb
|
290
|
+
kumi> data test_data.json
|
291
|
+
kumi> get result
|
292
|
+
|
293
|
+
# File-based execution
|
294
|
+
./bin/kumi -f examples/tax_schema.rb -d examples/tax_data.json -k total_tax,effective_rate
|
295
|
+
|
296
|
+
# Debugging computations
|
297
|
+
./bin/kumi -f examples/complex_schema.rb -d examples/data.json -e complex_calculation
|
298
|
+
|
299
|
+
# Output formats for integration
|
300
|
+
./bin/kumi -f schema.rb -d data.json -k results -o json | jq '.results'
|
301
|
+
```
|
302
|
+
|
303
|
+
### Troubleshooting Schema Issues
|
304
|
+
- **Parse Errors**: Check function syntax (avoid empty `fn()` calls)
|
305
|
+
- **Module Not Found**: Ensure proper module structure and naming
|
306
|
+
- **UnsatDetector Errors**: Review trait logic for contradictions
|
307
|
+
- **Type Errors**: Check input block type declarations match usage
|
308
|
+
- **Runtime Errors**: Use explain to trace computation dependencies
|
202
309
|
|
203
310
|
## Input Block System Details
|
204
311
|
|
data/README.md
CHANGED
@@ -3,287 +3,243 @@
|
|
3
3
|
[](https://github.com/amuta/kumi/actions)
|
4
4
|
[](https://badge.fury.io/rb/kumi)
|
5
5
|
|
6
|
-
Kumi is a
|
6
|
+
Kumi is a computational rules engine for Ruby (plus static validation, dependency tracking, and more)
|
7
7
|
|
8
|
-
|
8
|
+
It is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
|
9
9
|
|
10
|
-
Note: The examples here are small for the sake of readability. I would not recommend using this gem unless you need to keep track of 100+ conditions/variables.
|
11
10
|
|
12
11
|
|
13
|
-
##
|
12
|
+
## What can you build?
|
14
13
|
|
15
|
-
|
16
|
-
```
|
17
|
-
gem install kumi
|
18
|
-
```
|
19
|
-
|
20
|
-
## Example
|
14
|
+
Calculate U.S. federal taxes in 30 lines of validated, readable code:
|
21
15
|
|
22
|
-
**Instead of scattered logic:**
|
23
16
|
```ruby
|
24
|
-
|
25
|
-
good_credit = credit_score >= 700
|
26
|
-
sufficient_income = income >= 50_000
|
27
|
-
low_debt = debt_ratio <= 0.3
|
28
|
-
|
29
|
-
if good_credit && sufficient_income && low_debt
|
30
|
-
{ approved: true, rate: 3.5 }
|
31
|
-
else
|
32
|
-
{ approved: false, rate: nil }
|
33
|
-
end
|
34
|
-
end
|
35
|
-
```
|
36
|
-
|
37
|
-
**You can write:**
|
38
|
-
```ruby
|
39
|
-
module LoanApproval
|
17
|
+
module FederalTax2024
|
40
18
|
extend Kumi::Schema
|
41
|
-
|
19
|
+
|
42
20
|
schema do
|
43
21
|
input do
|
44
|
-
|
45
|
-
|
46
|
-
float :debt_to_income_ratio
|
22
|
+
float :income
|
23
|
+
string :filing_status, domain: %w[single married_joint]
|
47
24
|
end
|
48
|
-
|
49
|
-
trait :good_credit, input.credit_score >= 700
|
50
|
-
trait :sufficient_income, input.income >= 50_000
|
51
|
-
trait :low_debt, input.debt_to_income_ratio <= 0.3
|
52
|
-
trait :approved, good_credit & sufficient_income & low_debt
|
53
25
|
|
54
|
-
|
55
|
-
|
56
|
-
|
26
|
+
# Standard deduction by filing status
|
27
|
+
trait :single, input.filing_status == "single"
|
28
|
+
trait :married, input.filing_status == "married_joint"
|
29
|
+
|
30
|
+
value :std_deduction do
|
31
|
+
on single, 14_600
|
32
|
+
on married, 29_200
|
33
|
+
base 21_900 # head_of_household
|
34
|
+
end
|
35
|
+
|
36
|
+
value :taxable_income, fn(:max, [input.income - std_deduction, 0])
|
37
|
+
|
38
|
+
# Federal tax brackets
|
39
|
+
value :fed_breaks do
|
40
|
+
on single, [11_600, 47_150, 100_525, 191_950, 243_725, 609_350, Float::INFINITY]
|
41
|
+
on married, [23_200, 94_300, 201_050, 383_900, 487_450, 731_200, Float::INFINITY]
|
57
42
|
end
|
43
|
+
|
44
|
+
value :fed_rates, [0.10, 0.12, 0.22, 0.24, 0.32, 0.35, 0.37]
|
45
|
+
value :fed_calc, fn(:piecewise_sum, taxable_income, fed_breaks, fed_rates)
|
46
|
+
value :fed_tax, fed_calc[0]
|
47
|
+
|
48
|
+
# FICA taxes
|
49
|
+
value :ss_tax, fn(:min, [input.income, 168_600]) * 0.062
|
50
|
+
value :medicare_tax, input.income * 0.0145
|
51
|
+
|
52
|
+
value :total_tax, fed_tax + ss_tax + medicare_tax
|
53
|
+
value :after_tax, input.income - total_tax
|
58
54
|
end
|
59
55
|
end
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
|
57
|
+
# Use it
|
58
|
+
result = FederalTax2024.from(income: 100_000, filing_status: "single")
|
59
|
+
result[:total_tax] # => 21,491.00
|
60
|
+
result[:after_tax] # => 78,509.00
|
64
61
|
```
|
65
62
|
|
66
|
-
|
67
|
-
- Static analysis catches impossible logic combinations at compile time
|
68
|
-
- Automatic dependency tracking prevents circular references
|
69
|
-
- Type safety with domain constraints (age: 0..150, status: %w[active inactive])
|
70
|
-
- Microsecond performance not much different than optimized pure ruby
|
71
|
-
- Introspectable - see exactly how any value was computed
|
63
|
+
Real tax calculation with brackets, deductions, and FICA caps. Validation happens during schema definition.
|
72
64
|
|
73
|
-
|
65
|
+
Is is well-suited for scenarios with complex, interdependent calculations, enforcing ...
|
74
66
|
|
75
|
-
|
67
|
+
## Installation
|
76
68
|
|
77
|
-
```
|
78
|
-
|
79
|
-
|
69
|
+
```bash
|
70
|
+
# Requires Ruby 3.0+
|
71
|
+
# No external dependencies
|
72
|
+
gem install kumi
|
73
|
+
```
|
80
74
|
|
81
|
-
|
82
|
-
input {} # No inputs needed
|
75
|
+
## Core Features
|
83
76
|
|
84
|
-
|
85
|
-
trait :x_less_than_100, x < 100 # false: 100 < 100
|
86
|
-
|
87
|
-
value :y, x * 10 # 1000
|
88
|
-
trait :y_greater_than_1000, y > 1000 # false: 1000 > 1000
|
77
|
+
Here's a concise "Key Concepts" section for your README:
|
89
78
|
|
90
|
-
|
91
|
-
# This is impossible
|
92
|
-
on :x_less_than_100 & :y_greater_than_1000, "Impossible!"
|
93
|
-
base "Default"
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
79
|
+
## Key Concepts
|
97
80
|
|
98
|
-
|
99
|
-
```
|
81
|
+
Kumi schemas are built from four simple primitives that compose into powerful business logic:
|
100
82
|
|
101
|
-
|
83
|
+
**Inputs** define the data flowing into your schema with built-in validation:
|
102
84
|
```ruby
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
schema do
|
107
|
-
input { float :base }
|
108
|
-
|
109
|
-
# These create a circular dependency
|
110
|
-
value :monthly_rate, yearly_rate / 12
|
111
|
-
value :yearly_rate, monthly_rate * 12
|
112
|
-
end
|
85
|
+
input do
|
86
|
+
float :price, domain: 0..1000.0 # Validates range
|
87
|
+
string :category, domain: %w[standard premium] # Validates inclusion
|
113
88
|
end
|
114
|
-
|
115
|
-
# Kumi::Errors::SemanticError: cycle detected involving: monthly_rate → yearly_rate → monthly_rate
|
116
89
|
```
|
117
90
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
```
|
124
|
-
=== Evaluation Performance (with Memoization) ===
|
125
|
-
eval 50-deep: 817,497 i/s (1.22 μs/i)
|
126
|
-
eval 100-deep: 509,567 i/s (1.96 μs/i)
|
127
|
-
eval 150-deep: 376,429 i/s (2.66 μs/i)
|
128
|
-
eval 200-deep: 282,243 i/s (3.54 μs/i)
|
129
|
-
```
|
130
|
-
|
131
|
-
### Wide Complex Schemas
|
132
|
-
```
|
133
|
-
=== Evaluation Performance (with Memoization) ===
|
134
|
-
eval 1,000-wide: 127,652 i/s (7.83 μs/i)
|
135
|
-
eval 5,000-wide: 26,604 i/s (37.59 μs/i)
|
136
|
-
eval 10,000-wide: 13,670 i/s (73.15 μs/i)
|
91
|
+
**Values** are computed attributes that automatically memoize their results
|
92
|
+
```ruby
|
93
|
+
value :subtotal, input.price * input.quantity
|
94
|
+
value :tax_rate, 0.08
|
95
|
+
value :tax_amount, subtotal * tax_rate
|
137
96
|
```
|
138
97
|
|
139
|
-
|
98
|
+
**Traits** are boolean conditions that enable branching logic:
|
140
99
|
```ruby
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
end
|
150
|
-
|
151
|
-
value :unit_price_with_tax, input.base_price * (1 + input.tax_rate)
|
152
|
-
value :total_before_discount, unit_price_with_tax * input.quantity
|
153
|
-
value :bulk_discount, input.quantity >= 10 ? 0.1 : 0.0
|
154
|
-
value :final_total, total_before_discount * (1 - bulk_discount)
|
155
|
-
end
|
100
|
+
trait :bulk_order, input.quantity >= 100
|
101
|
+
trait :premium_customer, input.tier == "premium"
|
102
|
+
|
103
|
+
value :discount do
|
104
|
+
on bulk_order & premium_customer, 0.25 # 25% for bulk premium orders
|
105
|
+
on bulk_order, 0.15 # 15% for bulk orders
|
106
|
+
on premium_customer, 0.10 # 10% for premium customers
|
107
|
+
base 0.0 # No discount otherwise
|
156
108
|
end
|
157
|
-
|
158
|
-
runner = ProductPricing.from(base_price: 100.0, tax_rate: 0.08, quantity: 15)
|
159
|
-
|
160
|
-
# First access: computes and caches all intermediate values
|
161
|
-
puts runner[:final_total] # => 1458.0 (computed + cached)
|
162
|
-
|
163
|
-
# Subsequent accesses: pure cache lookups (microsecond performance)
|
164
|
-
puts runner[:unit_price_with_tax] # => 108.0 (from cache)
|
165
|
-
puts runner[:bulk_discount] # => 0.1 (from cache)
|
166
|
-
puts runner[:final_total] # => 1458.0 (from cache)
|
167
109
|
```
|
168
110
|
|
169
|
-
|
170
|
-
- Compile-once, evaluate-many: Schema compilation happens once, evaluations are pure computation
|
171
|
-
- `EvaluationWrapper` caches computed values automatically for subsequent access
|
172
|
-
- Stack-safe algorithms: Iterative cycle detection and dependency resolution prevent stack overflow
|
173
|
-
- Type-safe execution: No runtime type checking overhead after compilation
|
111
|
+
**Functions** provide computational building blocks:
|
174
112
|
|
175
|
-
## DSL Features
|
176
|
-
|
177
|
-
### Domain Constraints
|
178
113
|
```ruby
|
179
|
-
|
180
|
-
|
114
|
+
value :final_price, [subtotal - discount_amount, 0].max
|
115
|
+
value :monthly_payment, fn(:pmt, rate: 0.05/12, nper: 36, pv: -loan_amount)
|
116
|
+
```
|
117
|
+
Note: You can find a list all core functions [FUNCTIONS.md](documents/FUNCTIONS.md)
|
181
118
|
|
182
|
-
schema do
|
183
|
-
input do
|
184
|
-
integer :age, domain: 0..150
|
185
|
-
string :status, domain: %w[active inactive suspended]
|
186
|
-
float :score, domain: 0.0..100.0
|
187
|
-
end
|
188
119
|
|
189
|
-
|
190
|
-
trait :active_user, input.status == "active"
|
191
|
-
end
|
192
|
-
end
|
120
|
+
These primitives are statically analyzed during schema definition, catching logical errors before runtime and ensuring your business rules are internally consistent.
|
193
121
|
|
194
|
-
# Valid data works fine
|
195
|
-
UserProfile.from(age: 25, status: "active", score: 85.5)
|
196
122
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
```
|
123
|
+
### Static Analysis
|
124
|
+
|
125
|
+
Kumi catches real business logic errors during schema definition:
|
201
126
|
|
202
|
-
### Cascade Logic
|
203
127
|
```ruby
|
204
|
-
module
|
128
|
+
module CommissionCalculator
|
205
129
|
extend Kumi::Schema
|
206
|
-
|
130
|
+
|
207
131
|
schema do
|
208
132
|
input do
|
209
|
-
float :
|
210
|
-
|
133
|
+
float :sales_amount, domain: 0..Float::INFINITY
|
134
|
+
integer :years_experience, domain: 0..50
|
135
|
+
string :region, domain: %w[east west north south]
|
211
136
|
end
|
212
|
-
|
213
|
-
|
214
|
-
trait :
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
137
|
+
|
138
|
+
# Commission tiers based on experience
|
139
|
+
trait :junior, input.years_experience < 2
|
140
|
+
trait :senior, input.years_experience >= 5
|
141
|
+
trait :veteran, input.years_experience >= 10
|
142
|
+
|
143
|
+
# Base commission rates
|
144
|
+
value :base_rate do
|
145
|
+
on veteran, 0.08
|
146
|
+
on senior, 0.06
|
147
|
+
on junior, 0.04
|
148
|
+
base 0.05
|
149
|
+
end
|
150
|
+
|
151
|
+
# Regional multipliers
|
152
|
+
value :regional_bonus do
|
153
|
+
on input.region == "west", 1.2 # West coast bonus
|
154
|
+
on input.region == "east", 1.1 # East coast bonus
|
155
|
+
base 1.0
|
156
|
+
end
|
157
|
+
|
158
|
+
# Problem: Veteran bonus conflicts with senior cap
|
159
|
+
value :experience_bonus do
|
160
|
+
on veteran, 2.0 # Veterans get 2x bonus
|
161
|
+
on senior, 1.5 # Seniors get 1.5x bonus
|
162
|
+
base 1.0
|
163
|
+
end
|
164
|
+
|
165
|
+
value :total_rate, base_rate * regional_bonus * experience_bonus
|
166
|
+
|
167
|
+
# Business rule error: Veterans (10+ years) are also seniors (5+ years)
|
168
|
+
# This creates impossible logic in commission caps
|
169
|
+
trait :capped_senior, :senior & (total_rate <= 0.10) # Senior cap
|
170
|
+
trait :uncapped_veteran, :veteran & (total_rate > 0.10) # Veteran override
|
171
|
+
|
172
|
+
value :final_commission do
|
173
|
+
on capped_senior & uncapped_veteran, "Impossible!" # Can't be both
|
174
|
+
on uncapped_veteran, input.sales_amount * total_rate
|
175
|
+
on capped_senior, input.sales_amount * 0.10
|
176
|
+
base input.sales_amount * total_rate
|
220
177
|
end
|
221
178
|
end
|
222
179
|
end
|
223
180
|
|
224
|
-
|
225
|
-
puts runner[:shipping_cost] # => 15.0
|
181
|
+
# => conjunction `capped_senior AND uncapped_veteran` is impossible
|
226
182
|
```
|
227
183
|
|
228
|
-
###
|
229
|
-
```ruby
|
230
|
-
module Statistics
|
231
|
-
extend Kumi::Schema
|
184
|
+
### Automatic Memoization
|
232
185
|
|
233
|
-
|
234
|
-
input do
|
235
|
-
array :scores, elem: { type: :float }
|
236
|
-
end
|
186
|
+
Each value is computed exactly once:
|
237
187
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
end
|
188
|
+
```ruby
|
189
|
+
runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
|
190
|
+
|
191
|
+
# First access computes full dependency chain
|
192
|
+
runner[:total_tax] # => 52,937.50
|
244
193
|
|
245
|
-
|
246
|
-
|
247
|
-
|
194
|
+
# Subsequent access uses cached values
|
195
|
+
runner[:fed_tax] # => 37,437.50 (cached)
|
196
|
+
runner[:after_tax] # => 197,062.50 (cached)
|
248
197
|
```
|
249
198
|
|
250
|
-
|
199
|
+
### Introspection
|
251
200
|
|
252
|
-
|
201
|
+
See exactly how any value was calculated:
|
253
202
|
|
254
203
|
```ruby
|
255
|
-
|
256
|
-
|
204
|
+
Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_status: "single"})
|
205
|
+
# => fed_tax = fed_calc[0]
|
206
|
+
# = (fed_calc = piecewise_sum(taxable_income, fed_breaks, fed_rates)
|
207
|
+
# = piecewise_sum(85,400, [11,600, 47,150, ...], [0.10, 0.12, ...])
|
208
|
+
# = [15,099.50, 0.22])
|
209
|
+
# = 15,099.50
|
210
|
+
```
|
257
211
|
|
258
|
-
|
259
|
-
input do
|
260
|
-
float :income
|
261
|
-
float :tax_rate
|
262
|
-
float :deductions
|
263
|
-
end
|
212
|
+
## Suggested Use Cases
|
264
213
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
214
|
+
- Complex interdependent business rules
|
215
|
+
- Tax calculation engines (as demonstrated)
|
216
|
+
- Insurance premium calculators
|
217
|
+
- Loan amortization schedules
|
218
|
+
- Commission structures with complex tiers
|
219
|
+
- Pricing engines with multiple discount rules
|
269
220
|
|
270
|
-
|
221
|
+
**Not suitable for:**
|
222
|
+
- Simple conditional statements
|
223
|
+
- Sequential procedural workflows
|
224
|
+
- Rules that change during execution
|
225
|
+
- High-frequency real-time processing
|
271
226
|
|
272
|
-
|
273
|
-
# => taxable_income = input.income - deductions = (input.income = 100 000) - (deductions = 12 000) => 88 000
|
227
|
+
## Performance
|
274
228
|
|
275
|
-
|
276
|
-
|
277
|
-
|
229
|
+
Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:
|
230
|
+
- 50-deep dependency chain: **740,000/sec** (analysis <50ms)
|
231
|
+
- 1,000 attributes: **131,000/sec** (analysis <50ms)
|
232
|
+
- 10,000 attributes: **14,200/sec** (analysis ~300ms)
|
278
233
|
|
279
|
-
##
|
234
|
+
## Learn More
|
280
235
|
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
236
|
+
- [DSL Syntax Reference](documents/SYNTAX.md)
|
237
|
+
- [Examples](examples/)/
|
238
|
+
|
239
|
+
## Contributing
|
240
|
+
|
241
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/amuta/kumi.
|
286
242
|
|
287
|
-
##
|
243
|
+
## License
|
288
244
|
|
289
|
-
See [
|
245
|
+
MIT License. See [LICENSE](LICENSE).
|
data/documents/DSL.md
CHANGED
@@ -109,13 +109,13 @@ For conditional logic, a `value` takes a block to create a **cascade**. Cascades
|
|
109
109
|
```ruby
|
110
110
|
value :access_level do
|
111
111
|
# `on` implies AND: user must be :premium AND :verified.
|
112
|
-
on
|
112
|
+
on premium,verified, "Full Access"
|
113
113
|
|
114
114
|
# `on_any` implies OR: user can be :staff OR :admin.
|
115
|
-
on_any
|
115
|
+
on_any staff,admin, "Elevated Access"
|
116
116
|
|
117
117
|
# `on_none` implies NOT (A OR B): user is neither :blocked NOR :suspended.
|
118
|
-
on_none
|
118
|
+
on_none blocked,suspended, "Limited Access"
|
119
119
|
|
120
120
|
# `base` is the default if no other conditions match.
|
121
121
|
base "No Access"
|