kumi 0.0.0 → 0.0.3
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/.rubocop.yml +113 -3
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +387 -0
- data/README.md +257 -20
- data/docs/development/README.md +120 -0
- data/docs/development/error-reporting.md +361 -0
- data/documents/AST.md +126 -0
- data/documents/DSL.md +154 -0
- data/documents/FUNCTIONS.md +132 -0
- data/documents/SYNTAX.md +367 -0
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
- data/examples/federal_tax_calculator_2024.rb +112 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
- data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
- data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
- data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
- data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
- data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
- data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
- data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
- data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
- data/lib/kumi/analyzer.rb +54 -0
- data/lib/kumi/atom_unsat_solver.rb +349 -0
- data/lib/kumi/compiled_schema.rb +41 -0
- data/lib/kumi/compiler.rb +127 -0
- data/lib/kumi/domain/enum_analyzer.rb +53 -0
- data/lib/kumi/domain/range_analyzer.rb +83 -0
- data/lib/kumi/domain/validator.rb +84 -0
- data/lib/kumi/domain/violation_formatter.rb +40 -0
- data/lib/kumi/domain.rb +8 -0
- data/lib/kumi/error_reporter.rb +164 -0
- data/lib/kumi/error_reporting.rb +95 -0
- data/lib/kumi/errors.rb +116 -0
- data/lib/kumi/evaluation_wrapper.rb +20 -0
- data/lib/kumi/explain.rb +282 -0
- data/lib/kumi/export/deserializer.rb +39 -0
- data/lib/kumi/export/errors.rb +12 -0
- data/lib/kumi/export/node_builders.rb +140 -0
- data/lib/kumi/export/node_registry.rb +38 -0
- data/lib/kumi/export/node_serializers.rb +156 -0
- data/lib/kumi/export/serializer.rb +23 -0
- data/lib/kumi/export.rb +33 -0
- data/lib/kumi/function_registry/collection_functions.rb +92 -0
- data/lib/kumi/function_registry/comparison_functions.rb +31 -0
- data/lib/kumi/function_registry/conditional_functions.rb +36 -0
- data/lib/kumi/function_registry/function_builder.rb +92 -0
- data/lib/kumi/function_registry/logical_functions.rb +42 -0
- data/lib/kumi/function_registry/math_functions.rb +72 -0
- data/lib/kumi/function_registry/string_functions.rb +54 -0
- data/lib/kumi/function_registry/type_functions.rb +51 -0
- data/lib/kumi/function_registry.rb +138 -0
- data/lib/kumi/input/type_matcher.rb +92 -0
- data/lib/kumi/input/validator.rb +52 -0
- data/lib/kumi/input/violation_creator.rb +50 -0
- data/lib/kumi/input.rb +8 -0
- data/lib/kumi/parser/build_context.rb +25 -0
- data/lib/kumi/parser/dsl.rb +12 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
- data/lib/kumi/parser/expression_converter.rb +58 -0
- data/lib/kumi/parser/guard_rails.rb +43 -0
- data/lib/kumi/parser/input_builder.rb +94 -0
- data/lib/kumi/parser/input_proxy.rb +29 -0
- data/lib/kumi/parser/parser.rb +66 -0
- data/lib/kumi/parser/schema_builder.rb +172 -0
- data/lib/kumi/parser/sugar.rb +108 -0
- data/lib/kumi/schema.rb +49 -0
- data/lib/kumi/schema_instance.rb +43 -0
- data/lib/kumi/syntax/declarations.rb +23 -0
- data/lib/kumi/syntax/expressions.rb +30 -0
- data/lib/kumi/syntax/node.rb +46 -0
- data/lib/kumi/syntax/root.rb +12 -0
- data/lib/kumi/syntax/terminal_expressions.rb +27 -0
- data/lib/kumi/syntax.rb +9 -0
- data/lib/kumi/types/builder.rb +21 -0
- data/lib/kumi/types/compatibility.rb +86 -0
- data/lib/kumi/types/formatter.rb +24 -0
- data/lib/kumi/types/inference.rb +40 -0
- data/lib/kumi/types/normalizer.rb +70 -0
- data/lib/kumi/types/validator.rb +35 -0
- data/lib/kumi/types.rb +64 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +7 -3
- data/scripts/generate_function_docs.rb +59 -0
- data/test_impossible_cascade.rb +51 -0
- metadata +93 -10
- data/sig/kumi.rbs +0 -4
data/README.md
CHANGED
@@ -1,39 +1,276 @@
|
|
1
|
-
# Kumi
|
1
|
+
# Kumi
|
2
2
|
|
3
|
-
|
3
|
+
Kumi is a declarative rule‑and‑calculation DSL for Ruby that turns scattered business logic into a statically‑checked dependency graph.
|
4
4
|
|
5
|
-
|
5
|
+
Every input, trait, and formula is compiled into a typed AST node, so the entire graph is explicit and introspectable.
|
6
6
|
|
7
|
-
##
|
7
|
+
## Example
|
8
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
|
+
```
|
10
23
|
|
11
|
-
|
24
|
+
**You can write:**
|
25
|
+
```ruby
|
26
|
+
module LoanApproval
|
27
|
+
extend Kumi::Schema
|
12
28
|
|
13
|
-
|
14
|
-
|
29
|
+
schema do
|
30
|
+
input do
|
31
|
+
integer :credit_score
|
32
|
+
float :income
|
33
|
+
float :debt_to_income_ratio
|
34
|
+
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
|
+
|
41
|
+
value :interest_rate do
|
42
|
+
on :approved, 3.5
|
43
|
+
base 0.0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
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
|
15
51
|
```
|
16
52
|
|
17
|
-
|
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
|
18
59
|
|
19
|
-
|
20
|
-
|
60
|
+
## Static Analysis & Safety
|
61
|
+
|
62
|
+
Kumi analyzes your rules to catch logical impossibilities:
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
module ImpossibleLogic
|
66
|
+
extend Kumi::Schema
|
67
|
+
|
68
|
+
schema do
|
69
|
+
input {} # No inputs needed
|
70
|
+
|
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
|
76
|
+
|
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
|
84
|
+
|
85
|
+
# Kumi::Errors::SemanticError: conjunction `x_less_than_100 AND y_greater_than_1000` is impossible
|
86
|
+
```
|
87
|
+
|
88
|
+
Cycle detection:
|
89
|
+
```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
|
100
|
+
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
|
+
```
|
117
|
+
|
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)
|
124
|
+
```
|
125
|
+
|
126
|
+
Here's how the memoization works:
|
127
|
+
```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
|
143
|
+
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
|
+
```
|
155
|
+
|
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
|
163
|
+
|
164
|
+
### Domain Constraints
|
165
|
+
```ruby
|
166
|
+
module UserProfile
|
167
|
+
extend Kumi::Schema
|
168
|
+
|
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
|
+
|
176
|
+
trait :adult, input.age >= 18
|
177
|
+
trait :active_user, input.status == "active"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Valid data works fine
|
182
|
+
UserProfile.from(age: 25, status: "active", score: 85.5)
|
183
|
+
|
184
|
+
# Invalid data raises detailed errors
|
185
|
+
UserProfile.from(age: 200, status: "invalid", score: -10)
|
186
|
+
# => Kumi::Errors::DomainViolationError: Domain constraint violations...
|
21
187
|
```
|
22
188
|
|
23
|
-
|
189
|
+
### Cascade Logic
|
190
|
+
```ruby
|
191
|
+
module ShippingCost
|
192
|
+
extend Kumi::Schema
|
24
193
|
|
25
|
-
|
194
|
+
schema do
|
195
|
+
input do
|
196
|
+
float :order_total
|
197
|
+
string :membership_level
|
198
|
+
end
|
26
199
|
|
27
|
-
|
200
|
+
trait :premium_member, input.membership_level == "premium"
|
201
|
+
trait :large_order, input.order_total >= 100
|
28
202
|
|
29
|
-
|
203
|
+
value :shipping_cost do
|
204
|
+
on :premium_member, 0.0
|
205
|
+
on :large_order, 5.0
|
206
|
+
base 15.0
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
30
210
|
|
31
|
-
|
211
|
+
runner = ShippingCost.from(order_total: 75, membership_level: "standard")
|
212
|
+
puts runner[:shipping_cost] # => 15.0
|
213
|
+
```
|
32
214
|
|
33
|
-
|
215
|
+
### Functions
|
216
|
+
```ruby
|
217
|
+
module Statistics
|
218
|
+
extend Kumi::Schema
|
34
219
|
|
35
|
-
|
220
|
+
schema do
|
221
|
+
input do
|
222
|
+
array :scores, elem: { type: :float }
|
223
|
+
end
|
224
|
+
|
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
|
231
|
+
|
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
|
235
|
+
```
|
236
|
+
|
237
|
+
## Introspection
|
238
|
+
|
239
|
+
You can see exactly how any value was computed:
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
module TaxCalculator
|
243
|
+
extend Kumi::Schema
|
244
|
+
|
245
|
+
schema do
|
246
|
+
input do
|
247
|
+
float :income
|
248
|
+
float :tax_rate
|
249
|
+
float :deductions
|
250
|
+
end
|
251
|
+
|
252
|
+
value :taxable_income, input.income - input.deductions
|
253
|
+
value :tax_amount, taxable_income * input.tax_rate
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
inputs = { income: 100_000, tax_rate: 0.25, deductions: 12_000 }
|
258
|
+
|
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
|
261
|
+
|
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
|
+
```
|
265
|
+
|
266
|
+
## Try It Yourself
|
267
|
+
|
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
|
+
```
|
36
273
|
|
37
|
-
##
|
274
|
+
## DSL Syntax Reference
|
38
275
|
|
39
|
-
|
276
|
+
See [`documents/SYNTAX.md`](documents/SYNTAX.md) for complete syntax documentation with sugar vs sugar-free examples.
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# Development Guides
|
2
|
+
|
3
|
+
This directory contains detailed guides for developing and maintaining Kumi. These guides complement the high-level information in the main `CLAUDE.md` file.
|
4
|
+
|
5
|
+
## Guide Index
|
6
|
+
|
7
|
+
### Architecture & Design
|
8
|
+
- [Error Reporting Standards](error-reporting.md) - Comprehensive guide to unified error reporting
|
9
|
+
- [Analyzer Pass Development](analyzer-passes.md) - How to create new analyzer passes
|
10
|
+
- [Type System Integration](type-system.md) - Working with Kumi's type inference and checking
|
11
|
+
|
12
|
+
### Code Quality & Standards
|
13
|
+
- [Testing Standards](testing-standards.md) - Testing patterns and requirements
|
14
|
+
- [Code Organization](code-organization.md) - File structure and class design patterns
|
15
|
+
- [RuboCop Guidelines](rubocop-guidelines.md) - Code style and quality requirements
|
16
|
+
|
17
|
+
### Common Tasks
|
18
|
+
- [Adding New Functions](adding-functions.md) - Extending the FunctionRegistry
|
19
|
+
- [DSL Extension Patterns](dsl-extensions.md) - Adding new DSL constructs
|
20
|
+
- [Performance Considerations](performance.md) - Guidelines for maintaining performance
|
21
|
+
|
22
|
+
### Integration & Compatibility
|
23
|
+
- [Backward Compatibility](backward-compatibility.md) - Maintaining compatibility during changes
|
24
|
+
- [Migration Patterns](migration-patterns.md) - Safe patterns for evolving APIs
|
25
|
+
- [Zeitwerk Integration](zeitwerk.md) - Autoloading patterns and requirements
|
26
|
+
|
27
|
+
## Quick Reference
|
28
|
+
|
29
|
+
### Key Principles
|
30
|
+
1. **Unified Error Reporting**: All errors must provide clear location information
|
31
|
+
2. **Multi-pass Analysis**: Each analyzer pass has single responsibility
|
32
|
+
3. **Backward Compatibility**: Changes maintain existing API compatibility
|
33
|
+
4. **Type Safety**: Optional but comprehensive type checking
|
34
|
+
5. **Ruby Integration**: Leverage Ruby idioms while maintaining structure
|
35
|
+
|
36
|
+
### Common Commands
|
37
|
+
```bash
|
38
|
+
# Run all tests
|
39
|
+
bundle exec rspec
|
40
|
+
|
41
|
+
# Run specific test categories
|
42
|
+
bundle exec rspec spec/integration/
|
43
|
+
bundle exec rspec spec/kumi/analyzer/
|
44
|
+
|
45
|
+
# Check code quality
|
46
|
+
bundle exec rubocop
|
47
|
+
bundle exec rubocop -a
|
48
|
+
|
49
|
+
# Validate error reporting
|
50
|
+
bundle exec ruby test_location_improvements.rb
|
51
|
+
```
|
52
|
+
|
53
|
+
### File Templates
|
54
|
+
|
55
|
+
**New Analyzer Pass**:
|
56
|
+
```ruby
|
57
|
+
# frozen_string_literal: true
|
58
|
+
|
59
|
+
module Kumi
|
60
|
+
module Analyzer
|
61
|
+
module Passes
|
62
|
+
class MyNewPass < PassBase
|
63
|
+
def run(errors)
|
64
|
+
# Implementation with proper error reporting
|
65
|
+
report_error(errors, "message", location: node.loc, type: :semantic)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
**New Integration Test**:
|
74
|
+
```ruby
|
75
|
+
# frozen_string_literal: true
|
76
|
+
|
77
|
+
RSpec.describe "My Feature Integration" do
|
78
|
+
it "validates the feature works correctly" do
|
79
|
+
schema = build_schema do
|
80
|
+
input { integer :field }
|
81
|
+
value :result, input.field * 2
|
82
|
+
end
|
83
|
+
|
84
|
+
expect(schema.from(field: 5).fetch(:result)).to eq(10)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
## Contributing Guidelines
|
90
|
+
|
91
|
+
### Before Making Changes
|
92
|
+
1. Check relevant development guide in this directory
|
93
|
+
2. Review `CLAUDE.md` for high-level architecture understanding
|
94
|
+
3. Run existing tests to ensure baseline functionality
|
95
|
+
4. Consider backward compatibility implications
|
96
|
+
|
97
|
+
### After Making Changes
|
98
|
+
1. Update relevant development guides if patterns change
|
99
|
+
2. Add or update tests for new functionality
|
100
|
+
3. Run full test suite: `bundle exec rspec`
|
101
|
+
4. Check code quality: `bundle exec rubocop`
|
102
|
+
5. Verify error reporting quality with integration tests
|
103
|
+
|
104
|
+
### Adding New Guides
|
105
|
+
When adding new development guides:
|
106
|
+
1. Create focused, actionable guides for specific development tasks
|
107
|
+
2. Include code examples and common patterns
|
108
|
+
3. Reference related files and tests
|
109
|
+
4. Update this README index
|
110
|
+
5. Cross-reference from main `CLAUDE.md` if needed
|
111
|
+
|
112
|
+
## Guide Maintenance
|
113
|
+
|
114
|
+
These guides should be kept up-to-date as the codebase evolves:
|
115
|
+
- **Review quarterly** for accuracy and completeness
|
116
|
+
- **Update immediately** when patterns or APIs change significantly
|
117
|
+
- **Expand based on common questions** during development
|
118
|
+
- **Consolidate** overlapping or redundant information
|
119
|
+
|
120
|
+
The goal is to make Kumi development efficient and consistent while maintaining high code quality.
|