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.
- checksums.yaml +4 -4
- data/CLAUDE.md +109 -2
- data/README.md +174 -205
- 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 +22 -4
- data/lib/kumi/explain.rb +9 -10
- data/lib/kumi/function_registry/collection_functions.rb +103 -0
- data/lib/kumi/function_registry/string_functions.rb +1 -1
- data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/guard_rails.rb +2 -2
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +1 -1
- 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
data/lib/kumi/schema_instance.rb
CHANGED
@@ -9,8 +9,10 @@ module Kumi
|
|
9
9
|
# instance.slice(:tax_due) # alias for evaluate(*keys)
|
10
10
|
# instance.explain(:tax_due) # pretty trace string
|
11
11
|
# instance.input # original context (read‑only)
|
12
|
-
|
12
|
+
|
13
13
|
class SchemaInstance
|
14
|
+
attr_reader :compiled_schema, :analysis, :context
|
15
|
+
|
14
16
|
def initialize(compiled_schema, analysis, context)
|
15
17
|
@compiled_schema = compiled_schema # Kumi::CompiledSchema
|
16
18
|
@analysis = analysis # Analyzer result (for deps)
|
@@ -36,8 +38,72 @@ module Kumi
|
|
36
38
|
evaluate(key_name)[key_name]
|
37
39
|
end
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
+
# Update input values and clear affected cached computations
|
42
|
+
def update(**changes)
|
43
|
+
changes.each do |field, value|
|
44
|
+
# Validate field exists
|
45
|
+
raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
|
46
|
+
|
47
|
+
# Validate domain constraints
|
48
|
+
validate_domain_constraint(field, value)
|
49
|
+
|
50
|
+
# Update the input data
|
51
|
+
@context[field] = value
|
52
|
+
|
53
|
+
# Clear affected cached values using transitive closure by default
|
54
|
+
if ENV["KUMI_SIMPLE_CACHE"] == "true"
|
55
|
+
# Simple fallback: clear all cached values
|
56
|
+
@context.clear_cache
|
57
|
+
else
|
58
|
+
# Default: selective cache clearing using precomputed transitive closure
|
59
|
+
affected_keys = find_dependent_declarations_optimized(field)
|
60
|
+
affected_keys.each { |key| @context.clear_cache(key) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
self # Return self for chaining
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def input_field_exists?(field)
|
70
|
+
# Check if field is declared in input block
|
71
|
+
input_meta = @analysis&.state&.dig(:input_meta) || {}
|
72
|
+
input_meta.key?(field) || @context.key?(field)
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_domain_constraint(field, value)
|
76
|
+
input_meta = @analysis&.state&.dig(:input_meta) || {}
|
77
|
+
field_meta = input_meta[field]
|
78
|
+
return unless field_meta&.dig(:domain)
|
79
|
+
|
80
|
+
domain = field_meta[:domain]
|
81
|
+
return unless violates_domain?(value, domain)
|
82
|
+
|
83
|
+
raise ArgumentError, "value #{value} is not in domain #{domain}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def violates_domain?(value, domain)
|
87
|
+
case domain
|
88
|
+
when Range
|
89
|
+
!domain.include?(value)
|
90
|
+
when Array
|
91
|
+
!domain.include?(value)
|
92
|
+
when Proc
|
93
|
+
# For Proc domains, we can't statically analyze
|
94
|
+
false
|
95
|
+
else
|
96
|
+
false
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_dependent_declarations_optimized(field)
|
101
|
+
# Use precomputed transitive closure for true O(1) lookup!
|
102
|
+
transitive_dependents = @analysis&.state&.dig(:transitive_dependents)
|
103
|
+
return [] unless transitive_dependents
|
104
|
+
|
105
|
+
# This is truly O(1) - just array lookup, no traversal needed
|
106
|
+
transitive_dependents[field] || []
|
41
107
|
end
|
42
108
|
end
|
43
109
|
end
|
@@ -5,17 +5,20 @@ module Kumi
|
|
5
5
|
module Declarations
|
6
6
|
Attribute = Struct.new(:name, :expression) do
|
7
7
|
include Node
|
8
|
+
|
8
9
|
def children = [expression]
|
9
10
|
end
|
10
11
|
|
11
12
|
Trait = Struct.new(:name, :expression) do
|
12
13
|
include Node
|
14
|
+
|
13
15
|
def children = [expression]
|
14
16
|
end
|
15
17
|
|
16
18
|
# For field metadata declarations inside input blocks
|
17
19
|
FieldDecl = Struct.new(:name, :domain, :type) do
|
18
20
|
include Node
|
21
|
+
|
19
22
|
def children = []
|
20
23
|
end
|
21
24
|
end
|
@@ -5,20 +5,24 @@ module Kumi
|
|
5
5
|
module Expressions
|
6
6
|
CallExpression = Struct.new(:fn_name, :args) do
|
7
7
|
include Node
|
8
|
+
|
8
9
|
def children = args
|
9
10
|
end
|
10
11
|
CascadeExpression = Struct.new(:cases) do
|
11
12
|
include Node
|
13
|
+
|
12
14
|
def children = cases
|
13
15
|
end
|
14
16
|
|
15
17
|
WhenCaseExpression = Struct.new(:condition, :result) do
|
16
18
|
include Node
|
19
|
+
|
17
20
|
def children = [condition, result]
|
18
21
|
end
|
19
22
|
|
20
23
|
ListExpression = Struct.new(:elements) do
|
21
24
|
include Node
|
25
|
+
|
22
26
|
def children = elements
|
23
27
|
|
24
28
|
def size
|
data/lib/kumi/syntax/root.rb
CHANGED
@@ -9,17 +9,20 @@ module Kumi
|
|
9
9
|
|
10
10
|
Literal = Struct.new(:value) do
|
11
11
|
include Node
|
12
|
+
|
12
13
|
def children = []
|
13
14
|
end
|
14
15
|
|
15
16
|
# For field usage/reference in expressions (input.field_name)
|
16
17
|
FieldRef = Struct.new(:name) do
|
17
18
|
include Node
|
19
|
+
|
18
20
|
def children = []
|
19
21
|
end
|
20
22
|
|
21
23
|
Binding = Struct.new(:name) do
|
22
24
|
include Node
|
25
|
+
|
23
26
|
def children = []
|
24
27
|
end
|
25
28
|
end
|
@@ -12,6 +12,10 @@ module Kumi
|
|
12
12
|
# Exact match
|
13
13
|
return true if type1 == type2
|
14
14
|
|
15
|
+
# Generic array compatibility: :array is compatible with any structured array
|
16
|
+
return true if (type1 == :array && Validator.array_type?(type2)) ||
|
17
|
+
(type2 == :array && Validator.array_type?(type1))
|
18
|
+
|
15
19
|
# Numeric compatibility
|
16
20
|
return true if numeric_compatible?(type1, type2)
|
17
21
|
|
@@ -32,6 +36,10 @@ module Kumi
|
|
32
36
|
return type2 if type1 == :any
|
33
37
|
return type1 if type2 == :any
|
34
38
|
|
39
|
+
# Generic array unification: structured array is more specific than :array
|
40
|
+
return type2 if type1 == :array && Validator.array_type?(type2)
|
41
|
+
return type1 if type2 == :array && Validator.array_type?(type1)
|
42
|
+
|
35
43
|
# Numeric unification
|
36
44
|
if numeric_compatible?(type1, type2)
|
37
45
|
return :integer if type1 == :integer && type2 == :integer
|
data/lib/kumi/types/validator.rb
CHANGED
@@ -4,7 +4,7 @@ module Kumi
|
|
4
4
|
module Types
|
5
5
|
# Validates type definitions and structures
|
6
6
|
class Validator
|
7
|
-
VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime].freeze
|
7
|
+
VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array].freeze
|
8
8
|
|
9
9
|
def self.valid_type?(type)
|
10
10
|
return true if VALID_TYPES.include?(type)
|
data/lib/kumi/version.rb
CHANGED
@@ -30,10 +30,25 @@ end
|
|
30
30
|
# Main documentation generation logic.
|
31
31
|
def generate_docs
|
32
32
|
output = []
|
33
|
+
add_header(output)
|
34
|
+
add_function_categories(output)
|
35
|
+
output.join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_header(output)
|
33
39
|
output << "# Kumi Standard Function Library Reference"
|
34
40
|
output << "\nKumi provides a rich library of built-in functions for use within `value` and `trait` expressions via `fn(...)`."
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_function_categories(output)
|
44
|
+
function_categories.each do |title, functions|
|
45
|
+
output << "\n## #{title}\n"
|
46
|
+
add_functions_for_category(output, functions)
|
47
|
+
end
|
48
|
+
end
|
35
49
|
|
36
|
-
|
50
|
+
def function_categories
|
51
|
+
{
|
37
52
|
"Logical Functions" => Kumi::FunctionRegistry.logical_operations,
|
38
53
|
"Comparison Functions" => Kumi::FunctionRegistry.comparison_operators,
|
39
54
|
"Math Functions" => Kumi::FunctionRegistry.math_operations,
|
@@ -42,17 +57,14 @@ def generate_docs
|
|
42
57
|
"Conditional Functions" => Kumi::FunctionRegistry.conditional_operations,
|
43
58
|
"Type & Hash Functions" => Kumi::FunctionRegistry.type_operations
|
44
59
|
}
|
60
|
+
end
|
45
61
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
output << " * **Usage**: #{generate_signature(name, signature)}"
|
52
|
-
end
|
62
|
+
def add_functions_for_category(output, functions)
|
63
|
+
functions.sort.each do |name|
|
64
|
+
signature = Kumi::FunctionRegistry.signature(name)
|
65
|
+
output << "* **`#{name}`**: #{signature[:description]}"
|
66
|
+
output << " * **Usage**: #{generate_signature(name, signature)}"
|
53
67
|
end
|
54
|
-
|
55
|
-
output.join("\n")
|
56
68
|
end
|
57
69
|
|
58
70
|
# Execute the script and print the documentation.
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- André Muta
|
@@ -31,7 +31,6 @@ extra_rdoc_files: []
|
|
31
31
|
files:
|
32
32
|
- ".rspec"
|
33
33
|
- ".rubocop.yml"
|
34
|
-
- CHANGELOG.md
|
35
34
|
- CLAUDE.md
|
36
35
|
- LICENSE.txt
|
37
36
|
- README.md
|
@@ -44,16 +43,21 @@ files:
|
|
44
43
|
- documents/SYNTAX.md
|
45
44
|
- examples/deep_schema_compilation_and_evaluation_benchmark.rb
|
46
45
|
- examples/federal_tax_calculator_2024.rb
|
46
|
+
- examples/game_of_life.rb
|
47
|
+
- examples/simple_rpg_game.rb
|
48
|
+
- examples/static_analysis_errors.rb
|
47
49
|
- examples/wide_schema_compilation_and_evaluation_benchmark.rb
|
48
50
|
- lib/generators/trait_engine/templates/schema_spec.rb.erb
|
49
51
|
- lib/kumi.rb
|
50
52
|
- lib/kumi/analyzer.rb
|
53
|
+
- lib/kumi/analyzer/analysis_state.rb
|
51
54
|
- lib/kumi/analyzer/constant_evaluator.rb
|
52
55
|
- lib/kumi/analyzer/passes/definition_validator.rb
|
53
56
|
- lib/kumi/analyzer/passes/dependency_resolver.rb
|
54
57
|
- lib/kumi/analyzer/passes/input_collector.rb
|
55
58
|
- lib/kumi/analyzer/passes/name_indexer.rb
|
56
59
|
- lib/kumi/analyzer/passes/pass_base.rb
|
60
|
+
- lib/kumi/analyzer/passes/semantic_constraint_validator.rb
|
57
61
|
- lib/kumi/analyzer/passes/toposorter.rb
|
58
62
|
- lib/kumi/analyzer/passes/type_checker.rb
|
59
63
|
- lib/kumi/analyzer/passes/type_consistency_checker.rb
|
@@ -61,8 +65,10 @@ files:
|
|
61
65
|
- lib/kumi/analyzer/passes/unsat_detector.rb
|
62
66
|
- lib/kumi/analyzer/passes/visitor_pass.rb
|
63
67
|
- lib/kumi/atom_unsat_solver.rb
|
68
|
+
- lib/kumi/cli.rb
|
64
69
|
- lib/kumi/compiled_schema.rb
|
65
70
|
- lib/kumi/compiler.rb
|
71
|
+
- lib/kumi/constraint_relationship_solver.rb
|
66
72
|
- lib/kumi/domain.rb
|
67
73
|
- lib/kumi/domain/enum_analyzer.rb
|
68
74
|
- lib/kumi/domain/range_analyzer.rb
|
@@ -120,7 +126,6 @@ files:
|
|
120
126
|
- lib/kumi/types/validator.rb
|
121
127
|
- lib/kumi/version.rb
|
122
128
|
- scripts/generate_function_docs.rb
|
123
|
-
- test_impossible_cascade.rb
|
124
129
|
homepage: https://github.com/amuta/kumi
|
125
130
|
licenses:
|
126
131
|
- MIT
|
@@ -146,7 +151,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
151
|
requirements: []
|
147
152
|
rubygems_version: 3.7.1
|
148
153
|
specification_version: 4
|
149
|
-
summary: A declarative
|
150
|
-
|
151
|
-
explainability.
|
154
|
+
summary: A declarative DSL that transforms business logic into a statically-checked
|
155
|
+
dependency graph
|
152
156
|
test_files: []
|
data/CHANGELOG.md
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
## [Unreleased]
|
2
|
-
|
3
|
-
### Changed
|
4
|
-
- **BREAKING**: Replaced `StrictCycleChecker` with `AtomUnsatSolver` for stack-safe UNSAT detection
|
5
|
-
- Refactored cycle detection to use iterative Kahn's topological sort algorithm instead of recursive DFS
|
6
|
-
- Added support for always-false comparison detection (e.g., `100 < 100`)
|
7
|
-
|
8
|
-
### Added
|
9
|
-
- **Evaluation memoization**: `EvaluationWrapper` struct with `@__schema_cache__` provides automatic caching of computed bindings
|
10
|
-
- **Massive performance improvements**: 369x-1,379x speedup on deep dependency chains through intelligent memoization
|
11
|
-
- Depth-safe UNSAT detection handles 30k+ node graphs without stack overflow
|
12
|
-
- Comprehensive test suite for large graph scenarios (acyclic ladders, cycles, mixed constraints)
|
13
|
-
- Enhanced documentation with YARD comments for `AtomUnsatSolver` module
|
14
|
-
- Extracted `StrictInequalitySolver` module for clear separation of cycle detection logic
|
15
|
-
|
16
|
-
### Performance
|
17
|
-
- **Stack-safe UNSAT detection**: Eliminates `SystemStackError` in constraint analysis for 30k+ node graphs
|
18
|
-
- **Fixed gather_atoms recursion**: Made AST traversal iterative to handle deep dependency chains
|
19
|
-
- Sub-millisecond performance on 10k-20k node constraint graphs with cycle detection
|
20
|
-
- Maintained identical UNSAT detection correctness for all existing scenarios
|
21
|
-
- **Note**: Deep schemas (2500+ dependencies) may still hit Ruby stack limits in compilation/evaluation phases
|
22
|
-
|
23
|
-
## [0.1.0] - 2025-07-01
|
24
|
-
|
25
|
-
- Initial release
|
data/test_impossible_cascade.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# frozen_string_literal: true
|
3
|
-
|
4
|
-
require_relative "lib/kumi"
|
5
|
-
|
6
|
-
# This test demonstrates that the UnsatDetector still correctly catches
|
7
|
-
# genuinely impossible cascade conditions after our fix
|
8
|
-
|
9
|
-
puts "🧪 Testing UnsatDetector Catches Impossible Cascade Conditions"
|
10
|
-
puts "=" * 60
|
11
|
-
|
12
|
-
begin
|
13
|
-
impossible_schema = Class.new do
|
14
|
-
extend Kumi::Schema
|
15
|
-
|
16
|
-
schema do
|
17
|
-
input do
|
18
|
-
integer :age, domain: 0..150
|
19
|
-
end
|
20
|
-
|
21
|
-
# These traits are individually satisfiable
|
22
|
-
trait :very_young, input.age, :<, 25
|
23
|
-
trait :very_old, input.age, :>, 65
|
24
|
-
|
25
|
-
# This cascade condition combines contradictory traits - should be caught!
|
26
|
-
value :impossible_condition do
|
27
|
-
on :very_young, :very_old, "Impossible: young AND old" # age < 25 AND age > 65
|
28
|
-
base "Normal"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
puts "❌ ERROR: Should have caught impossible cascade condition!"
|
34
|
-
|
35
|
-
rescue Kumi::Errors::SemanticError => e
|
36
|
-
if e.message.include?("conjunction") && e.message.include?("logically impossible")
|
37
|
-
puts "✅ CORRECTLY CAUGHT impossible cascade condition!"
|
38
|
-
puts " Error: #{e.message}"
|
39
|
-
puts
|
40
|
-
puts " This proves the UnsatDetector still works for genuinely impossible conditions"
|
41
|
-
puts " while allowing valid mutually exclusive cascades."
|
42
|
-
else
|
43
|
-
puts "❌ UNEXPECTED ERROR: #{e.message}"
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
puts
|
48
|
-
puts "🎉 UnsatDetector Fix Validation Complete!"
|
49
|
-
puts " ✅ Valid mutually exclusive cascades: WORK"
|
50
|
-
puts " ✅ Impossible cascade conditions: CAUGHT"
|
51
|
-
puts " ✅ Existing functionality: PRESERVED"
|