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
@@ -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
- def input
40
- @context
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
@@ -6,6 +6,7 @@ module Kumi
6
6
  # It holds all the top-level declarations parsed from the source.
7
7
  Root = Struct.new(:inputs, :attributes, :traits) do
8
8
  include Node
9
+
9
10
  def children = [inputs, attributes, traits]
10
11
  end
11
12
  end
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kumi
4
- VERSION = "0.0.3"
4
+ VERSION = "0.0.5"
5
5
  end
@@ -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
- categories = {
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
- categories.each do |title, functions|
47
- output << "\n## #{title}\n"
48
- functions.sort.each do |name|
49
- signature = Kumi::FunctionRegistry.signature(name)
50
- output << "* **`#{name}`**: #{signature[:description]}"
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.3
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 Ruby DSL for defining business rules and calculations with
150
- static dependency checks, cycle detection, domain validation, and human‑readable
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
@@ -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"