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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +109 -2
  3. data/README.md +169 -213
  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 +20 -4
  31. data/lib/kumi/explain.rb +8 -8
  32. data/lib/kumi/function_registry/collection_functions.rb +103 -0
  33. data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
  34. data/lib/kumi/parser/expression_converter.rb +80 -12
  35. data/lib/kumi/parser/parser.rb +2 -0
  36. data/lib/kumi/parser/sugar.rb +117 -16
  37. data/lib/kumi/schema.rb +3 -1
  38. data/lib/kumi/schema_instance.rb +69 -3
  39. data/lib/kumi/syntax/declarations.rb +3 -0
  40. data/lib/kumi/syntax/expressions.rb +4 -0
  41. data/lib/kumi/syntax/root.rb +1 -0
  42. data/lib/kumi/syntax/terminal_expressions.rb +3 -0
  43. data/lib/kumi/types/compatibility.rb +8 -0
  44. data/lib/kumi/types/validator.rb +1 -1
  45. data/lib/kumi/version.rb +1 -1
  46. data/scripts/generate_function_docs.rb +22 -10
  47. metadata +10 -6
  48. data/CHANGELOG.md +0 -25
  49. data/test_impossible_cascade.rb +0 -51
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 243e6f2e2f7f65919f41fae1eddf877796e6221674e41dc5d32a36c708efab03
4
- data.tar.gz: 8649def0a1299dd416d00480daa9fb0dfe3b6e418c4ea6c1ad869c873ad3eedf
3
+ metadata.gz: 5d4a3efa20727ebc89e39753f240bb6ed8fc3621b32771ea3b6d9096bdc21af5
4
+ data.tar.gz: 4c0a147c66d2b2ece9bec196b665d7b5fe613bb021caa5ec3efc00f0e0e6ed94
5
5
  SHA512:
6
- metadata.gz: e677d95d1ef34f8144b1f829259ad716e8f3822ab216957cf34a0058b4f7651843232202dc485a62e4c44077761a91b4db419963b649fae4a429042bf49be410
7
- data.tar.gz: 3e7795238333d362f35c71b9478b01f4ff396110dc517a320dc304288a1ab8deac0a3af8b99c3d8f87cfd47a9a91f1bbd46509eb0055db4faa042b9b6ca14bd9
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
@@ -3,287 +3,243 @@
3
3
  [![CI](https://github.com/amuta/kumi/workflows/CI/badge.svg)](https://github.com/amuta/kumi/actions)
4
4
  [![Gem Version](https://badge.fury.io/rb/kumi.svg)](https://badge.fury.io/rb/kumi)
5
5
 
6
- Kumi is a declarative rule‑and‑calculation DSL for Ruby that turns scattered business logic into a statically‑checked dependency graph.
6
+ Kumi is a computational rules engine for Ruby (plus static validation, dependency tracking, and more)
7
7
 
8
- Every input, trait, and formula is compiled into a typed AST node, so the entire graph is explicit and introspectable.
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
- ## How to get started
12
+ ## What can you build?
14
13
 
15
- Install Kumi and try running the examples below or explore the `./examples` directory of this repository.
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
- def calculate_loan_approval(credit_score, income, debt_ratio)
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
- integer :credit_score
45
- float :income
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
- value :interest_rate do
55
- on :approved, 3.5
56
- 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
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
- runner = LoanApproval.from(credit_score: 750, income: 60_000, debt_to_income_ratio: 0.25)
62
- puts runner[:approved] # => true
63
- 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
64
61
  ```
65
62
 
66
- This gets you:
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
- ## Static Analysis & Safety
65
+ Is is well-suited for scenarios with complex, interdependent calculations, enforcing ...
74
66
 
75
- Kumi analyzes your rules to catch logical impossibilities:
67
+ ## Installation
76
68
 
77
- ```ruby
78
- module ImpossibleLogic
79
- extend Kumi::Schema
69
+ ```bash
70
+ # Requires Ruby 3.0+
71
+ # No external dependencies
72
+ gem install kumi
73
+ ```
80
74
 
81
- schema do
82
- input {} # No inputs needed
75
+ ## Core Features
83
76
 
84
- value :x, 100
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
- value :result do
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
- # Kumi::Errors::SemanticError: conjunction `x_less_than_100 AND y_greater_than_1000` is impossible
99
- ```
81
+ Kumi schemas are built from four simple primitives that compose into powerful business logic:
100
82
 
101
- Cycle detection:
83
+ **Inputs** define the data flowing into your schema with built-in validation:
102
84
  ```ruby
103
- module CircularDependency
104
- extend Kumi::Schema
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
- ## Performance
119
-
120
- Kumi has microsecond evaluation times through automatic memoization:
121
-
122
- ### Deep Dependency Chains
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
- Here's how the memoization works:
98
+ **Traits** are boolean conditions that enable branching logic:
140
99
  ```ruby
141
- module ProductPricing
142
- extend Kumi::Schema
143
-
144
- schema do
145
- input do
146
- float :base_price
147
- float :tax_rate
148
- integer :quantity
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
- Architecture notes:
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
- module UserProfile
180
- 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)
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
- trait :adult, input.age >= 18
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
- # Invalid data raises detailed errors
198
- UserProfile.from(age: 200, status: "invalid", score: -10)
199
- # => Kumi::Errors::DomainViolationError: Domain constraint violations...
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 ShippingCost
128
+ module CommissionCalculator
205
129
  extend Kumi::Schema
206
-
130
+
207
131
  schema do
208
132
  input do
209
- float :order_total
210
- 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]
211
136
  end
212
-
213
- trait :premium_member, input.membership_level == "premium"
214
- trait :large_order, input.order_total >= 100
215
-
216
- value :shipping_cost do
217
- on :premium_member, 0.0
218
- on :large_order, 5.0
219
- 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
220
177
  end
221
178
  end
222
179
  end
223
180
 
224
- runner = ShippingCost.from(order_total: 75, membership_level: "standard")
225
- puts runner[:shipping_cost] # => 15.0
181
+ # => conjunction `capped_senior AND uncapped_veteran` is impossible
226
182
  ```
227
183
 
228
- ### Functions
229
- ```ruby
230
- module Statistics
231
- extend Kumi::Schema
184
+ ### Automatic Memoization
232
185
 
233
- schema do
234
- input do
235
- array :scores, elem: { type: :float }
236
- end
186
+ Each value is computed exactly once:
237
187
 
238
- value :total, fn(:sum, input.scores)
239
- value :count, fn(:size, input.scores)
240
- value :average, total / count
241
- value :max_score, fn(:max, input.scores)
242
- end
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
- runner = Statistics.from(scores: [85.5, 92.0, 78.5, 96.0])
246
- puts runner[:average] # => 88.0
247
- 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)
248
197
  ```
249
198
 
250
- ## Introspection
199
+ ### Introspection
251
200
 
252
- You can see exactly how any value was computed:
201
+ See exactly how any value was calculated:
253
202
 
254
203
  ```ruby
255
- module TaxCalculator
256
- 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
+ ```
257
211
 
258
- schema do
259
- input do
260
- float :income
261
- float :tax_rate
262
- float :deductions
263
- end
212
+ ## Suggested Use Cases
264
213
 
265
- value :taxable_income, input.income - input.deductions
266
- value :tax_amount, taxable_income * input.tax_rate
267
- end
268
- 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
269
220
 
270
- 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
271
226
 
272
- puts Kumi::Explain.call(TaxCalculator, :taxable_income, inputs: inputs)
273
- # => taxable_income = input.income - deductions = (input.income = 100 000) - (deductions = 12 000) => 88 000
227
+ ## Performance
274
228
 
275
- puts Kumi::Explain.call(TaxCalculator, :tax_amount, inputs: inputs)
276
- # => tax_amount = taxable_income × input.tax_rate = (taxable_income = 88 000) × (input.tax_rate = 0.25) => 22 000
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
- ## Try It Yourself
234
+ ## Learn More
280
235
 
281
- Run the performance benchmarks:
282
- ```bash
283
- bundle exec ruby examples/wide_schema_compilation_and_evaluation_benchmark.rb
284
- bundle exec ruby examples/deep_schema_compilation_and_evaluation_benchmark.rb
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
- ## DSL Syntax Reference
243
+ ## License
288
244
 
289
- 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"