kumi 0.0.3 → 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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +109 -2
  3. data/README.md +174 -205
  4. data/documents/DSL.md +3 -3
  5. data/documents/SYNTAX.md +17 -26
  6. data/examples/federal_tax_calculator_2024.rb +36 -38
  7. data/examples/game_of_life.rb +97 -0
  8. data/examples/simple_rpg_game.rb +1000 -0
  9. data/examples/static_analysis_errors.rb +178 -0
  10. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  11. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  12. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  13. data/lib/kumi/analyzer/passes/definition_validator.rb +4 -3
  14. data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
  15. data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
  16. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  17. data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
  18. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  19. data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
  20. data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
  21. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  22. data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
  23. data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
  24. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
  25. data/lib/kumi/analyzer.rb +42 -24
  26. data/lib/kumi/atom_unsat_solver.rb +45 -0
  27. data/lib/kumi/cli.rb +449 -0
  28. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  29. data/lib/kumi/error_reporter.rb +6 -6
  30. data/lib/kumi/evaluation_wrapper.rb +22 -4
  31. data/lib/kumi/explain.rb +9 -10
  32. data/lib/kumi/function_registry/collection_functions.rb +103 -0
  33. data/lib/kumi/function_registry/string_functions.rb +1 -1
  34. data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
  35. data/lib/kumi/parser/expression_converter.rb +80 -12
  36. data/lib/kumi/parser/guard_rails.rb +2 -2
  37. data/lib/kumi/parser/parser.rb +2 -0
  38. data/lib/kumi/parser/schema_builder.rb +1 -1
  39. data/lib/kumi/parser/sugar.rb +117 -16
  40. data/lib/kumi/schema.rb +3 -1
  41. data/lib/kumi/schema_instance.rb +69 -3
  42. data/lib/kumi/syntax/declarations.rb +3 -0
  43. data/lib/kumi/syntax/expressions.rb +4 -0
  44. data/lib/kumi/syntax/root.rb +1 -0
  45. data/lib/kumi/syntax/terminal_expressions.rb +3 -0
  46. data/lib/kumi/types/compatibility.rb +8 -0
  47. data/lib/kumi/types/validator.rb +1 -1
  48. data/lib/kumi/version.rb +1 -1
  49. data/scripts/generate_function_docs.rb +22 -10
  50. metadata +10 -6
  51. data/CHANGELOG.md +0 -25
  52. data/test_impossible_cascade.rb +0 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46d129da8737c28750b4aa51f1234bbce39b047eac599d248735ae6b5eff199c
4
- data.tar.gz: 045e55121094f5e8a5a2cb2512b745c2d82c9de7f3dc0fcb5d1f9fbe88764683
3
+ metadata.gz: 5d4a3efa20727ebc89e39753f240bb6ed8fc3621b32771ea3b6d9096bdc21af5
4
+ data.tar.gz: 4c0a147c66d2b2ece9bec196b665d7b5fe613bb021caa5ec3efc00f0e0e6ed94
5
5
  SHA512:
6
- metadata.gz: 207e8e9c14f083c660af9d16e418f8f7b2ee753989adb22c107acfb062bdedb334160a7978f6ad33dce463dd8faee6b7016068689f0c3678ebf52d117328cef2
7
- data.tar.gz: 7cddb20e40da3b55361281abaebd37a12276b1e4e6418453aa4f199ec680eaff714060411f0b0b5a3faf0e199161f3dddc3f2c94da2bd8e3a3e2e2ec4321a871
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 :adult, "Adult Status" # ✅ Correct - use :trait_name symbol
124
- on :verified, "Verified User"
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
@@ -1,276 +1,245 @@
1
1
  # Kumi
2
2
 
3
- Kumi is a declarative rule‑and‑calculation DSL for Ruby that turns scattered business logic into a statically‑checked dependency graph.
3
+ [![CI](https://github.com/amuta/kumi/workflows/CI/badge.svg)](https://github.com/amuta/kumi/actions)
4
+ [![Gem Version](https://badge.fury.io/rb/kumi.svg)](https://badge.fury.io/rb/kumi)
4
5
 
5
- Every input, trait, and formula is compiled into a typed AST node, so the entire graph is explicit and introspectable.
6
+ Kumi is a computational rules engine for Ruby (plus static validation, dependency tracking, and more)
6
7
 
7
- ## Example
8
+ It is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
8
9
 
9
- **Instead of scattered logic:**
10
- ```ruby
11
- def calculate_loan_approval(credit_score, income, debt_ratio)
12
- good_credit = credit_score >= 700
13
- sufficient_income = income >= 50_000
14
- low_debt = debt_ratio <= 0.3
15
-
16
- if good_credit && sufficient_income && low_debt
17
- { approved: true, rate: 3.5 }
18
- else
19
- { approved: false, rate: nil }
20
- end
21
- end
22
- ```
23
10
 
24
- **You can write:**
11
+
12
+ ## What can you build?
13
+
14
+ Calculate U.S. federal taxes in 30 lines of validated, readable code:
15
+
25
16
  ```ruby
26
- module LoanApproval
17
+ module FederalTax2024
27
18
  extend Kumi::Schema
28
-
19
+
29
20
  schema do
30
21
  input do
31
- integer :credit_score
32
- float :income
33
- float :debt_to_income_ratio
22
+ float :income
23
+ string :filing_status, domain: %w[single married_joint]
34
24
  end
35
-
36
- trait :good_credit, input.credit_score >= 700
37
- trait :sufficient_income, input.income >= 50_000
38
- trait :low_debt, input.debt_to_income_ratio <= 0.3
39
- trait :approved, good_credit & sufficient_income & low_debt
40
25
 
41
- value :interest_rate do
42
- on :approved, 3.5
43
- base 0.0
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
44
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]
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
45
54
  end
46
55
  end
47
56
 
48
- runner = LoanApproval.from(credit_score: 750, income: 60_000, debt_to_income_ratio: 0.25)
49
- puts runner[:approved] # => true
50
- puts runner[:interest_rate] # => 3.5
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
51
61
  ```
52
62
 
53
- This gets you:
54
- - Static analysis catches impossible logic combinations at compile time
55
- - Automatic dependency tracking prevents circular references
56
- - Type safety with domain constraints (age: 0..150, status: %w[active inactive])
57
- - Microsecond performance not much different than optimized pure ruby
58
- - Introspectable - see exactly how any value was computed
63
+ Real tax calculation with brackets, deductions, and FICA caps. Validation happens during schema definition.
59
64
 
60
- ## Static Analysis & Safety
65
+ Is is well-suited for scenarios with complex, interdependent calculations, enforcing ...
61
66
 
62
- Kumi analyzes your rules to catch logical impossibilities:
67
+ ## Installation
63
68
 
64
- ```ruby
65
- module ImpossibleLogic
66
- extend Kumi::Schema
69
+ ```bash
70
+ # Requires Ruby 3.0+
71
+ # No external dependencies
72
+ gem install kumi
73
+ ```
67
74
 
68
- schema do
69
- input {} # No inputs needed
75
+ ## Core Features
70
76
 
71
- value :x, 100
72
- trait :x_less_than_100, x < 100 # false: 100 < 100
73
-
74
- value :y, x * 10 # 1000
75
- trait :y_greater_than_1000, y > 1000 # false: 1000 > 1000
77
+ Here's a concise "Key Concepts" section for your README:
76
78
 
77
- value :result do
78
- # This is impossible
79
- on :x_less_than_100 & :y_greater_than_1000, "Impossible!"
80
- base "Default"
81
- end
82
- end
83
- end
79
+ ## Key Concepts
84
80
 
85
- # Kumi::Errors::SemanticError: conjunction `x_less_than_100 AND y_greater_than_1000` is impossible
86
- ```
81
+ Kumi schemas are built from four simple primitives that compose into powerful business logic:
87
82
 
88
- Cycle detection:
83
+ **Inputs** define the data flowing into your schema with built-in validation:
89
84
  ```ruby
90
- module CircularDependency
91
- extend Kumi::Schema
92
-
93
- schema do
94
- input { float :base }
95
-
96
- # These create a circular dependency
97
- value :monthly_rate, yearly_rate / 12
98
- value :yearly_rate, monthly_rate * 12
99
- end
85
+ input do
86
+ float :price, domain: 0..1000.0 # Validates range
87
+ string :category, domain: %w[standard premium] # Validates inclusion
100
88
  end
101
-
102
- # Kumi::Errors::SemanticError: cycle detected involving: monthly_rate → yearly_rate → monthly_rate
103
- ```
104
-
105
- ## Performance
106
-
107
- Kumi has microsecond evaluation times through automatic memoization:
108
-
109
- ### Deep Dependency Chains
110
- ```
111
- === Evaluation Performance (with Memoization) ===
112
- eval 50-deep: 817,497 i/s (1.22 μs/i)
113
- eval 100-deep: 509,567 i/s (1.96 μs/i)
114
- eval 150-deep: 376,429 i/s (2.66 μs/i)
115
- eval 200-deep: 282,243 i/s (3.54 μs/i)
116
89
  ```
117
90
 
118
- ### Wide Complex Schemas
119
- ```
120
- === Evaluation Performance (with Memoization) ===
121
- eval 1,000-wide: 127,652 i/s (7.83 μs/i)
122
- eval 5,000-wide: 26,604 i/s (37.59 μs/i)
123
- 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
124
96
  ```
125
97
 
126
- Here's how the memoization works:
98
+ **Traits** are boolean conditions that enable branching logic:
127
99
  ```ruby
128
- module ProductPricing
129
- extend Kumi::Schema
130
-
131
- schema do
132
- input do
133
- float :base_price
134
- float :tax_rate
135
- integer :quantity
136
- end
137
-
138
- value :unit_price_with_tax, input.base_price * (1 + input.tax_rate)
139
- value :total_before_discount, unit_price_with_tax * input.quantity
140
- value :bulk_discount, input.quantity >= 10 ? 0.1 : 0.0
141
- value :final_total, total_before_discount * (1 - bulk_discount)
142
- 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
143
108
  end
144
-
145
- runner = ProductPricing.from(base_price: 100.0, tax_rate: 0.08, quantity: 15)
146
-
147
- # First access: computes and caches all intermediate values
148
- puts runner[:final_total] # => 1458.0 (computed + cached)
149
-
150
- # Subsequent accesses: pure cache lookups (microsecond performance)
151
- puts runner[:unit_price_with_tax] # => 108.0 (from cache)
152
- puts runner[:bulk_discount] # => 0.1 (from cache)
153
- puts runner[:final_total] # => 1458.0 (from cache)
154
109
  ```
155
110
 
156
- Architecture notes:
157
- - Compile-once, evaluate-many: Schema compilation happens once, evaluations are pure computation
158
- - `EvaluationWrapper` caches computed values automatically for subsequent access
159
- - Stack-safe algorithms: Iterative cycle detection and dependency resolution prevent stack overflow
160
- - Type-safe execution: No runtime type checking overhead after compilation
161
-
162
- ## DSL Features
111
+ **Functions** provide computational building blocks:
163
112
 
164
- ### Domain Constraints
165
113
  ```ruby
166
- module UserProfile
167
- extend Kumi::Schema
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)
168
118
 
169
- schema do
170
- input do
171
- integer :age, domain: 0..150
172
- string :status, domain: %w[active inactive suspended]
173
- float :score, domain: 0.0..100.0
174
- end
175
119
 
176
- trait :adult, input.age >= 18
177
- trait :active_user, input.status == "active"
178
- end
179
- end
120
+ These primitives are statically analyzed during schema definition, catching logical errors before runtime and ensuring your business rules are internally consistent.
180
121
 
181
- # Valid data works fine
182
- UserProfile.from(age: 25, status: "active", score: 85.5)
183
122
 
184
- # Invalid data raises detailed errors
185
- UserProfile.from(age: 200, status: "invalid", score: -10)
186
- # => Kumi::Errors::DomainViolationError: Domain constraint violations...
187
- ```
123
+ ### Static Analysis
124
+
125
+ Kumi catches real business logic errors during schema definition:
188
126
 
189
- ### Cascade Logic
190
127
  ```ruby
191
- module ShippingCost
128
+ module CommissionCalculator
192
129
  extend Kumi::Schema
193
-
130
+
194
131
  schema do
195
132
  input do
196
- float :order_total
197
- string :membership_level
133
+ float :sales_amount, domain: 0..Float::INFINITY
134
+ integer :years_experience, domain: 0..50
135
+ string :region, domain: %w[east west north south]
198
136
  end
199
-
200
- trait :premium_member, input.membership_level == "premium"
201
- trait :large_order, input.order_total >= 100
202
-
203
- value :shipping_cost do
204
- on :premium_member, 0.0
205
- on :large_order, 5.0
206
- base 15.0
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
207
177
  end
208
178
  end
209
179
  end
210
180
 
211
- runner = ShippingCost.from(order_total: 75, membership_level: "standard")
212
- puts runner[:shipping_cost] # => 15.0
181
+ # => conjunction `capped_senior AND uncapped_veteran` is impossible
213
182
  ```
214
183
 
215
- ### Functions
216
- ```ruby
217
- module Statistics
218
- extend Kumi::Schema
184
+ ### Automatic Memoization
219
185
 
220
- schema do
221
- input do
222
- array :scores, elem: { type: :float }
223
- end
186
+ Each value is computed exactly once:
224
187
 
225
- value :total, fn(:sum, input.scores)
226
- value :count, fn(:size, input.scores)
227
- value :average, total / count
228
- value :max_score, fn(:max, input.scores)
229
- end
230
- 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
231
193
 
232
- runner = Statistics.from(scores: [85.5, 92.0, 78.5, 96.0])
233
- puts runner[:average] # => 88.0
234
- puts runner[:max_score] # => 96.0
194
+ # Subsequent access uses cached values
195
+ runner[:fed_tax] # => 37,437.50 (cached)
196
+ runner[:after_tax] # => 197,062.50 (cached)
235
197
  ```
236
198
 
237
- ## Introspection
199
+ ### Introspection
238
200
 
239
- You can see exactly how any value was computed:
201
+ See exactly how any value was calculated:
240
202
 
241
203
  ```ruby
242
- module TaxCalculator
243
- extend Kumi::Schema
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
+ ```
244
211
 
245
- schema do
246
- input do
247
- float :income
248
- float :tax_rate
249
- float :deductions
250
- end
212
+ ## Suggested Use Cases
251
213
 
252
- value :taxable_income, input.income - input.deductions
253
- value :tax_amount, taxable_income * input.tax_rate
254
- end
255
- end
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
256
220
 
257
- inputs = { income: 100_000, tax_rate: 0.25, deductions: 12_000 }
221
+ **Not suitable for:**
222
+ - Simple conditional statements
223
+ - Sequential procedural workflows
224
+ - Rules that change during execution
225
+ - High-frequency real-time processing
258
226
 
259
- puts Kumi::Explain.call(TaxCalculator, :taxable_income, inputs: inputs)
260
- # => taxable_income = input.income - deductions = (input.income = 100 000) - (deductions = 12 000) => 88 000
227
+ ## Performance
261
228
 
262
- puts Kumi::Explain.call(TaxCalculator, :tax_amount, inputs: inputs)
263
- # => tax_amount = taxable_income × input.tax_rate = (taxable_income = 88 000) × (input.tax_rate = 0.25) => 22 000
264
- ```
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)
265
233
 
266
- ## Try It Yourself
234
+ ## Learn More
267
235
 
268
- Run the performance benchmarks:
269
- ```bash
270
- bundle exec ruby examples/wide_schema_compilation_and_evaluation_benchmark.rb
271
- bundle exec ruby examples/deep_schema_compilation_and_evaluation_benchmark.rb
272
- ```
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.
273
242
 
274
- ## DSL Syntax Reference
243
+ ## License
275
244
 
276
- See [`documents/SYNTAX.md`](documents/SYNTAX.md) for complete syntax documentation with sugar vs sugar-free examples.
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 :premium, :verified, "Full Access"
112
+ on premium,verified, "Full Access"
113
113
 
114
114
  # `on_any` implies OR: user can be :staff OR :admin.
115
- on_any :staff, :admin, "Elevated Access"
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 :blocked, :suspended, "Limited Access"
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"