kumi 0.0.6 → 0.0.8

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +34 -177
  3. data/README.md +41 -7
  4. data/docs/SYNTAX.md +2 -7
  5. data/docs/features/array-broadcasting.md +1 -1
  6. data/docs/schema_metadata/broadcasts.md +53 -0
  7. data/docs/schema_metadata/cascades.md +45 -0
  8. data/docs/schema_metadata/declarations.md +54 -0
  9. data/docs/schema_metadata/dependencies.md +57 -0
  10. data/docs/schema_metadata/evaluation_order.md +29 -0
  11. data/docs/schema_metadata/examples.md +95 -0
  12. data/docs/schema_metadata/inferred_types.md +46 -0
  13. data/docs/schema_metadata/inputs.md +86 -0
  14. data/docs/schema_metadata.md +108 -0
  15. data/examples/game_of_life.rb +1 -1
  16. data/examples/static_analysis_errors.rb +7 -7
  17. data/lib/kumi/analyzer.rb +20 -20
  18. data/lib/kumi/compiler.rb +44 -50
  19. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  20. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  21. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  22. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  23. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  24. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  25. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  26. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  27. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  29. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  30. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  31. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  32. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  33. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  34. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  35. data/lib/kumi/core/compiled_schema.rb +43 -0
  36. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  37. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  38. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  39. data/lib/kumi/core/domain/validator.rb +82 -0
  40. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  41. data/lib/kumi/core/error_reporter.rb +166 -0
  42. data/lib/kumi/core/error_reporting.rb +97 -0
  43. data/lib/kumi/core/errors.rb +120 -0
  44. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  45. data/lib/kumi/core/explain.rb +295 -0
  46. data/lib/kumi/core/export/deserializer.rb +41 -0
  47. data/lib/kumi/core/export/errors.rb +14 -0
  48. data/lib/kumi/core/export/node_builders.rb +142 -0
  49. data/lib/kumi/core/export/node_registry.rb +54 -0
  50. data/lib/kumi/core/export/node_serializers.rb +158 -0
  51. data/lib/kumi/core/export/serializer.rb +25 -0
  52. data/lib/kumi/core/export.rb +35 -0
  53. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  54. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  55. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  56. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  57. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  58. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  59. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  60. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  61. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  62. data/lib/kumi/core/input/type_matcher.rb +97 -0
  63. data/lib/kumi/core/input/validator.rb +51 -0
  64. data/lib/kumi/core/input/violation_creator.rb +52 -0
  65. data/lib/kumi/core/json_schema/generator.rb +65 -0
  66. data/lib/kumi/core/json_schema/validator.rb +27 -0
  67. data/lib/kumi/core/json_schema.rb +16 -0
  68. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  69. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  70. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  71. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  72. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  73. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  74. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  75. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  76. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  77. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  78. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  79. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  80. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  81. data/lib/kumi/core/ruby_parser.rb +12 -0
  82. data/lib/kumi/core/schema_instance.rb +111 -0
  83. data/lib/kumi/core/types/builder.rb +23 -0
  84. data/lib/kumi/core/types/compatibility.rb +96 -0
  85. data/lib/kumi/core/types/formatter.rb +26 -0
  86. data/lib/kumi/core/types/inference.rb +42 -0
  87. data/lib/kumi/core/types/normalizer.rb +72 -0
  88. data/lib/kumi/core/types/validator.rb +37 -0
  89. data/lib/kumi/core/types.rb +66 -0
  90. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  91. data/lib/kumi/errors.rb +1 -112
  92. data/lib/kumi/registry.rb +37 -0
  93. data/lib/kumi/schema.rb +13 -7
  94. data/lib/kumi/schema_metadata.rb +524 -0
  95. data/lib/kumi/syntax/array_expression.rb +6 -6
  96. data/lib/kumi/syntax/call_expression.rb +4 -4
  97. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  98. data/lib/kumi/syntax/case_expression.rb +4 -4
  99. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  100. data/lib/kumi/syntax/hash_expression.rb +4 -4
  101. data/lib/kumi/syntax/input_declaration.rb +5 -5
  102. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  103. data/lib/kumi/syntax/input_reference.rb +5 -5
  104. data/lib/kumi/syntax/literal.rb +4 -4
  105. data/lib/kumi/syntax/node.rb +34 -34
  106. data/lib/kumi/syntax/root.rb +6 -6
  107. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  108. data/lib/kumi/syntax/value_declaration.rb +4 -4
  109. data/lib/kumi/version.rb +1 -1
  110. data/lib/kumi.rb +14 -0
  111. data/migrate_to_core_iterative.rb +938 -0
  112. data/scripts/generate_function_docs.rb +9 -9
  113. metadata +85 -69
  114. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
  115. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  116. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  117. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -251
  118. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  119. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  120. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  121. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  122. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  123. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -110
  124. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  125. data/lib/kumi/analyzer/passes/type_checker.rb +0 -162
  126. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  127. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  128. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -406
  129. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  130. data/lib/kumi/atom_unsat_solver.rb +0 -394
  131. data/lib/kumi/compiled_schema.rb +0 -41
  132. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  133. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  134. data/lib/kumi/domain/range_analyzer.rb +0 -83
  135. data/lib/kumi/domain/validator.rb +0 -80
  136. data/lib/kumi/domain/violation_formatter.rb +0 -40
  137. data/lib/kumi/error_reporter.rb +0 -164
  138. data/lib/kumi/error_reporting.rb +0 -95
  139. data/lib/kumi/evaluation_wrapper.rb +0 -38
  140. data/lib/kumi/explain.rb +0 -281
  141. data/lib/kumi/export/deserializer.rb +0 -39
  142. data/lib/kumi/export/errors.rb +0 -12
  143. data/lib/kumi/export/node_builders.rb +0 -140
  144. data/lib/kumi/export/node_registry.rb +0 -52
  145. data/lib/kumi/export/node_serializers.rb +0 -156
  146. data/lib/kumi/export/serializer.rb +0 -23
  147. data/lib/kumi/export.rb +0 -33
  148. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  149. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  150. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  151. data/lib/kumi/function_registry/function_builder.rb +0 -93
  152. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  153. data/lib/kumi/function_registry/math_functions.rb +0 -72
  154. data/lib/kumi/function_registry/string_functions.rb +0 -54
  155. data/lib/kumi/function_registry/type_functions.rb +0 -51
  156. data/lib/kumi/input/type_matcher.rb +0 -95
  157. data/lib/kumi/input/validator.rb +0 -49
  158. data/lib/kumi/input/violation_creator.rb +0 -50
  159. data/lib/kumi/parser/build_context.rb +0 -25
  160. data/lib/kumi/parser/declaration_reference_proxy.rb +0 -36
  161. data/lib/kumi/parser/dsl.rb +0 -12
  162. data/lib/kumi/parser/dsl_cascade_builder.rb +0 -136
  163. data/lib/kumi/parser/expression_converter.rb +0 -126
  164. data/lib/kumi/parser/guard_rails.rb +0 -43
  165. data/lib/kumi/parser/input_builder.rb +0 -125
  166. data/lib/kumi/parser/input_field_proxy.rb +0 -46
  167. data/lib/kumi/parser/input_proxy.rb +0 -29
  168. data/lib/kumi/parser/nested_input.rb +0 -15
  169. data/lib/kumi/parser/parser.rb +0 -68
  170. data/lib/kumi/parser/schema_builder.rb +0 -173
  171. data/lib/kumi/parser/sugar.rb +0 -261
  172. data/lib/kumi/schema_instance.rb +0 -109
  173. data/lib/kumi/types/builder.rb +0 -21
  174. data/lib/kumi/types/compatibility.rb +0 -94
  175. data/lib/kumi/types/formatter.rb +0 -24
  176. data/lib/kumi/types/inference.rb +0 -40
  177. data/lib/kumi/types/normalizer.rb +0 -70
  178. data/lib/kumi/types/validator.rb +0 -35
  179. data/lib/kumi/types.rb +0 -64
  180. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -0,0 +1,641 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ # Enhanced constraint solver that can detect mathematical impossibilities
6
+ # across dependency chains by tracking variable relationships.
7
+ #
8
+ # This solver extends the basic AtomUnsatSolver by:
9
+ # 1. Building a graph of mathematical relationships between variables
10
+ # 2. Iteratively propagating constraints through multi-step dependency chains
11
+ # 3. Detecting contradictions that span multiple variables across complex relationships
12
+ #
13
+ # Capabilities:
14
+ # - Single-step relationships: y = x + 10, x == 50, y == 40 (impossible)
15
+ # - Multi-step chains: x -> y -> z -> w, with constraints on x and w
16
+ # - Identity relationships: y = x, handles both bindings and field references
17
+ # - Mathematical operations: add, subtract, multiply, divide
18
+ # - Forward and reverse constraint propagation
19
+ #
20
+ # @example Multi-step chain detection
21
+ # # Given: v1 = seed + 1, v2 = v1 + 2, v3 = v2 + 3, seed == 0, v3 == 10
22
+ # # The solver detects this is impossible since v3 must equal 6
23
+ module ConstraintRelationshipSolver
24
+ # Represents a mathematical relationship between variables
25
+ # @!attribute [r] target
26
+ # @return [Symbol] the dependent variable
27
+ # @!attribute [r] operation
28
+ # @return [Symbol] the mathematical operation (:add, :subtract, :multiply, :divide)
29
+ # @!attribute [r] operands
30
+ # @return [Array] the operands (variables or constants)
31
+ Relationship = Struct.new(:target, :operation, :operands)
32
+
33
+ # Represents a derived constraint from propagating through relationships
34
+ # @!attribute [r] variable
35
+ # @return [Symbol] the variable being constrained
36
+ # @!attribute [r] operation
37
+ # @return [Symbol] the constraint operation (:==, :>, :<, etc.)
38
+ # @!attribute [r] value
39
+ # @return [Object] the constraint value
40
+ # @!attribute [r] derivation_path
41
+ # @return [Array<Symbol>] the variables this constraint was derived through
42
+ DerivedConstraint = Struct.new(:variable, :operation, :value, :derivation_path)
43
+
44
+ module_function
45
+
46
+ # Enhanced unsatisfiability check that includes relationship analysis
47
+ #
48
+ # @param atoms [Array<Atom>] basic constraint atoms
49
+ # @param definitions [Hash] variable definitions for relationship building
50
+ # @param input_meta [Hash] input field metadata with domain constraints
51
+ # @param debug [Boolean] enable debug output
52
+ # @return [Boolean] true if constraints are unsatisfiable
53
+ def unsat?(atoms, definitions, input_meta: {}, debug: false)
54
+ # First run the standard unsat solver
55
+ return true if Kumi::Core::AtomUnsatSolver.unsat?(atoms, debug: debug)
56
+
57
+ # Then check for relationship-based contradictions
58
+ relationships = build_relationships(definitions)
59
+ return false if relationships.empty?
60
+
61
+ # Check for range-based impossibilities
62
+ return true if range_violation?(atoms, relationships, input_meta, debug: debug)
63
+
64
+ # Propagate constraints through relationships
65
+ derived_constraints = propagate_constraints(atoms, relationships, debug: debug)
66
+
67
+ # Check if any derived constraints violate domain constraints
68
+ return true if domain_constraint_violation?(derived_constraints, input_meta, debug: debug)
69
+
70
+ # Check if any derived constraints create contradictions
71
+ all_constraints = atoms + derived_constraints.map do |dc|
72
+ Kumi::Core::AtomUnsatSolver::Atom.new(dc.operation, dc.variable, dc.value)
73
+ end
74
+
75
+ Kumi::Core::AtomUnsatSolver.unsat?(all_constraints, debug: debug)
76
+ end
77
+
78
+ # Builds mathematical relationships from variable definitions
79
+ #
80
+ # @param definitions [Hash] variable name to AST node mapping
81
+ # @return [Array<Relationship>] mathematical relationships between variables
82
+ def build_relationships(definitions)
83
+ relationships = []
84
+
85
+ definitions.each do |var_name, definition|
86
+ next unless definition&.expression
87
+
88
+ relationship = extract_relationship(var_name, definition.expression)
89
+ relationships << relationship if relationship
90
+ end
91
+
92
+ relationships
93
+ end
94
+
95
+ # Extracts mathematical relationship from an AST expression
96
+ #
97
+ # @param target [Symbol] the variable being defined
98
+ # @param expression [Object] the AST expression defining the variable
99
+ # @return [Relationship, nil] the relationship or nil if not extractable
100
+ def extract_relationship(target, expression)
101
+ case expression
102
+ when Kumi::Syntax::CallExpression
103
+ extract_call_relationship(target, expression)
104
+ when Kumi::Syntax::DeclarationReference
105
+ # Simple alias: target = other_variable
106
+ Relationship.new(target, :identity, [expression.name])
107
+ when Kumi::Syntax::InputReference
108
+ # Direct field reference: target = input.field
109
+ # Create identity relationship so we can propagate constraints
110
+ Relationship.new(target, :identity, [expression.name])
111
+ end
112
+ end
113
+
114
+ # Extracts relationship from a function call expression
115
+ #
116
+ # @param target [Symbol] the variable being defined
117
+ # @param call_expr [CallExpression] the function call expression
118
+ # @return [Relationship, nil] the relationship or nil if not supported
119
+ def extract_call_relationship(target, call_expr)
120
+ case call_expr.fn_name
121
+ when :add
122
+ operands = extract_operands(call_expr.args)
123
+ return nil unless operands
124
+
125
+ Relationship.new(target, :add, operands)
126
+ when :subtract
127
+ operands = extract_operands(call_expr.args)
128
+ return nil unless operands
129
+
130
+ Relationship.new(target, :subtract, operands)
131
+ when :multiply
132
+ operands = extract_operands(call_expr.args)
133
+ return nil unless operands
134
+
135
+ Relationship.new(target, :multiply, operands)
136
+ when :divide
137
+ operands = extract_operands(call_expr.args)
138
+ return nil unless operands
139
+
140
+ Relationship.new(target, :divide, operands)
141
+ else
142
+ # Unsupported operation for relationship extraction
143
+ nil
144
+ end
145
+ end
146
+
147
+ # Extracts operands from function arguments
148
+ #
149
+ # @param args [Array] function call arguments
150
+ # @return [Array, nil] operands (variables as symbols, constants as values) or nil if not extractable
151
+ def extract_operands(args)
152
+ return nil if args.empty?
153
+
154
+ args.map do |arg|
155
+ case arg
156
+ when Kumi::Syntax::DeclarationReference
157
+ arg.name
158
+ when Kumi::Syntax::Literal
159
+ arg.value
160
+ when Kumi::Syntax::InputReference
161
+ # Use the field name directly to match how atoms represent input fields
162
+ arg.name
163
+ else
164
+ # Unknown operand type
165
+ return nil
166
+ end
167
+ end
168
+ end
169
+
170
+ # Propagates constraints through mathematical relationships to derive new constraints
171
+ # Uses iterative propagation to handle multi-step dependency chains
172
+ #
173
+ # @param atoms [Array<Atom>] original constraint atoms
174
+ # @param relationships [Array<Relationship>] mathematical relationships
175
+ # @param debug [Boolean] enable debug output
176
+ # @return [Array<DerivedConstraint>] derived constraints
177
+ def propagate_constraints(atoms, relationships, debug: false)
178
+ all_derived_constraints = []
179
+ current_atoms = atoms.dup
180
+ max_iterations = relationships.size + 1 # Prevent infinite loops
181
+ iteration = 0
182
+
183
+ loop do
184
+ iteration += 1
185
+ constraint_map = build_constraint_map(current_atoms)
186
+ round_derived = []
187
+
188
+ # Forward propagation: from operand constraints to target constraints
189
+ relationships.each do |rel|
190
+ derived = derive_constraints_for_relationship(rel, constraint_map, debug: debug)
191
+ round_derived.concat(derived)
192
+
193
+ # Reverse propagation: from target constraints to operand constraints
194
+ derived = reverse_derive_constraints(rel, constraint_map, debug: debug)
195
+ round_derived.concat(derived)
196
+ end
197
+
198
+ # Check if we derived any new constraints this round
199
+ new_constraints = round_derived.reject do |dc|
200
+ # Check if this constraint already exists in current_atoms or all_derived_constraints
201
+ existing_atom = current_atoms.find do |atom|
202
+ atom.lhs == dc.variable && atom.op == dc.operation && atom.rhs == dc.value
203
+ end
204
+ existing_derived = all_derived_constraints.find do |existing_dc|
205
+ existing_dc.variable == dc.variable &&
206
+ existing_dc.operation == dc.operation &&
207
+ existing_dc.value == dc.value
208
+ end
209
+ existing_atom || existing_derived
210
+ end
211
+
212
+ break if new_constraints.empty? || iteration > max_iterations
213
+
214
+ puts "Iteration #{iteration}: derived #{new_constraints.size} new constraints" if debug
215
+
216
+ # Add new constraints to our working set for next iteration
217
+ new_atoms = new_constraints.map do |dc|
218
+ Kumi::Core::AtomUnsatSolver::Atom.new(dc.operation, dc.variable, dc.value)
219
+ end
220
+ current_atoms.concat(new_atoms)
221
+ all_derived_constraints.concat(new_constraints)
222
+ end
223
+
224
+ puts "Total derived #{all_derived_constraints.size} constraints in #{iteration} iterations" if debug
225
+ all_derived_constraints
226
+ end
227
+
228
+ # Builds a map from variables to their constraints
229
+ #
230
+ # @param atoms [Array<Atom>] constraint atoms
231
+ # @return [Hash] variable name to array of constraints
232
+ def build_constraint_map(atoms)
233
+ constraint_map = Hash.new { |h, k| h[k] = [] }
234
+
235
+ atoms.each do |atom|
236
+ constraint_map[atom.lhs] << atom if atom.lhs.is_a?(Symbol)
237
+ end
238
+
239
+ constraint_map
240
+ end
241
+
242
+ # Derives constraints on target variable from operand constraints
243
+ #
244
+ # @param relationship [Relationship] the mathematical relationship
245
+ # @param constraint_map [Hash] variable to constraints mapping
246
+ # @param debug [Boolean] enable debug output
247
+ # @return [Array<DerivedConstraint>] derived constraints on target
248
+ def derive_constraints_for_relationship(relationship, constraint_map, debug: false)
249
+ derived = []
250
+
251
+ # Handle different operand patterns
252
+ if relationship.operands.size == 2
253
+ # Case 1: One variable and one constant (e.g., x + 5)
254
+ var_operand = relationship.operands.find { |op| op.is_a?(Symbol) }
255
+ const_operand = relationship.operands.find { |op| op.is_a?(Numeric) }
256
+
257
+ if var_operand && const_operand && constraint_map[var_operand].any?
258
+ constraint_map[var_operand].each do |constraint|
259
+ next unless constraint.op == :==
260
+
261
+ derived_value = apply_operation(relationship.operation, constraint.rhs, const_operand,
262
+ var_operand == relationship.operands[0])
263
+ next unless derived_value
264
+
265
+ derived << DerivedConstraint.new(
266
+ relationship.target,
267
+ :==,
268
+ derived_value,
269
+ [var_operand]
270
+ )
271
+ puts "Derived: #{relationship.target} == #{derived_value} (from #{var_operand} == #{constraint.rhs})" if debug
272
+ end
273
+ end
274
+
275
+ # Case 2: Two variables (e.g., x + y) - handle when one has an equality constraint
276
+ var1 = relationship.operands[0] if relationship.operands[0].is_a?(Symbol)
277
+ var2 = relationship.operands[1] if relationship.operands[1].is_a?(Symbol)
278
+
279
+ if var1 && var2 && var1 != var2
280
+ # If we have constraints on var1, try to derive constraints involving var2
281
+ constraint_map[var1].each do |constraint|
282
+ next unless constraint.op == :==
283
+
284
+ # For now, only handle addition with two variables: target = var1 + var2
285
+ # If var1 == value, then target == value + var2
286
+ next unless relationship.operation == :add && constraint_map[var2].any?
287
+
288
+ constraint_map[var2].each do |var2_constraint|
289
+ next unless var2_constraint.op == :==
290
+
291
+ derived_value = constraint.rhs + var2_constraint.rhs
292
+ derived << DerivedConstraint.new(
293
+ relationship.target,
294
+ :==,
295
+ derived_value,
296
+ [var1, var2]
297
+ )
298
+ if debug
299
+ puts "Derived: #{relationship.target} == #{derived_value} (from #{var1} == #{constraint.rhs} and #{var2} == #{var2_constraint.rhs})"
300
+ end
301
+ end
302
+ end
303
+ end
304
+ elsif relationship.operands.size == 1
305
+ # Case 3: Identity relationship (target = operand)
306
+ operand = relationship.operands[0]
307
+ if operand.is_a?(Symbol) && constraint_map[operand].any?
308
+ constraint_map[operand].each do |constraint|
309
+ next unless constraint.op == :==
310
+
311
+ derived << DerivedConstraint.new(
312
+ relationship.target,
313
+ :==,
314
+ constraint.rhs,
315
+ [operand]
316
+ )
317
+ puts "Derived: #{relationship.target} == #{constraint.rhs} (identity from #{operand})" if debug
318
+ end
319
+ end
320
+ end
321
+
322
+ derived
323
+ end
324
+
325
+ # Derives constraints on operand variables from target constraints (reverse propagation)
326
+ #
327
+ # @param relationship [Relationship] the mathematical relationship
328
+ # @param constraint_map [Hash] variable to constraints mapping
329
+ # @param debug [Boolean] enable debug output
330
+ # @return [Array<DerivedConstraint>] derived constraints on operands
331
+ def reverse_derive_constraints(relationship, constraint_map, debug: false)
332
+ derived = []
333
+
334
+ # For simple operations with one variable and one constant
335
+ if relationship.operands.size == 2
336
+ var_operand = relationship.operands.find { |op| op.is_a?(Symbol) }
337
+ const_operand = relationship.operands.find { |op| op.is_a?(Numeric) }
338
+
339
+ if var_operand && const_operand && constraint_map[relationship.target].any?
340
+ constraint_map[relationship.target].each do |constraint|
341
+ next unless constraint.op == :==
342
+
343
+ derived_value = reverse_operation(relationship.operation, constraint.rhs, const_operand,
344
+ var_operand == relationship.operands[0])
345
+ next unless derived_value
346
+
347
+ derived << DerivedConstraint.new(
348
+ var_operand,
349
+ :==,
350
+ derived_value,
351
+ [relationship.target]
352
+ )
353
+ puts "Reverse derived: #{var_operand} == #{derived_value} (from #{relationship.target} == #{constraint.rhs})" if debug
354
+ end
355
+ end
356
+ elsif relationship.operands.size == 1 && relationship.operation == :identity
357
+ # Handle reverse propagation for identity relationships
358
+ operand = relationship.operands[0]
359
+ if operand.is_a?(Symbol) && constraint_map[relationship.target].any?
360
+ constraint_map[relationship.target].each do |constraint|
361
+ next unless constraint.op == :==
362
+
363
+ derived << DerivedConstraint.new(
364
+ operand,
365
+ :==,
366
+ constraint.rhs,
367
+ [relationship.target]
368
+ )
369
+ puts "Reverse derived: #{operand} == #{constraint.rhs} (identity from #{relationship.target})" if debug
370
+ end
371
+ end
372
+ end
373
+
374
+ derived
375
+ end
376
+
377
+ # Applies mathematical operation to derive target value
378
+ #
379
+ # @param operation [Symbol] the operation (:add, :subtract, :multiply, :divide)
380
+ # @param var_value [Numeric] the value of the variable operand
381
+ # @param const_value [Numeric] the constant operand value
382
+ # @param var_is_first [Boolean] whether variable is first operand
383
+ # @return [Numeric, nil] the derived value or nil if not computable
384
+ def apply_operation(operation, var_value, const_value, var_is_first)
385
+ case operation
386
+ when :add
387
+ var_value + const_value
388
+ when :subtract
389
+ var_is_first ? var_value - const_value : const_value - var_value
390
+ when :multiply
391
+ var_value * const_value
392
+ when :divide
393
+ return nil if const_value.zero?
394
+
395
+ var_is_first ? var_value / const_value : const_value / var_value
396
+ end
397
+ end
398
+
399
+ # Applies reverse mathematical operation to derive operand value
400
+ #
401
+ # @param operation [Symbol] the operation (:add, :subtract, :multiply, :divide)
402
+ # @param target_value [Numeric] the value of the target variable
403
+ # @param const_value [Numeric] the constant operand value
404
+ # @param var_is_first [Boolean] whether variable is first operand
405
+ # @return [Numeric, nil] the derived operand value or nil if not computable
406
+ def reverse_operation(operation, target_value, const_value, var_is_first)
407
+ case operation
408
+ when :add
409
+ target_value - const_value
410
+ when :subtract
411
+ var_is_first ? target_value + const_value : const_value - target_value
412
+ when :multiply
413
+ return nil if const_value.zero?
414
+
415
+ target_value / const_value
416
+ when :divide
417
+ var_is_first ? target_value * const_value : const_value / target_value
418
+ end
419
+ end
420
+
421
+ # Checks for range violations through mathematical operations
422
+ #
423
+ # @param atoms [Array<Atom>] basic constraint atoms
424
+ # @param relationships [Array<Relationship>] mathematical relationships
425
+ # @param input_meta [Hash] input field metadata with domain constraints
426
+ # @param debug [Boolean] enable debug output
427
+ # @return [Boolean] true if range violation exists
428
+ def range_violation?(atoms, relationships, input_meta, debug: false)
429
+ return false if input_meta.empty?
430
+
431
+ # Build range map for all variables
432
+ range_map = build_range_map(relationships, input_meta, debug: debug)
433
+ return false if range_map.empty?
434
+
435
+ # Check if any constraint violates computed ranges
436
+ atoms.each do |atom|
437
+ next unless atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Numeric)
438
+ next unless %i[> < >= <=].include?(atom.op)
439
+
440
+ range = range_map[atom.lhs]
441
+ next unless range
442
+
443
+ if range_constraint_impossible?(range, atom.op, atom.rhs)
444
+ puts "Range violation: #{atom.lhs} #{atom.op} #{atom.rhs} impossible with range #{range}" if debug
445
+ return true
446
+ end
447
+ end
448
+
449
+ false
450
+ end
451
+
452
+ # Builds a map of variable ranges based on mathematical relationships and input domains
453
+ #
454
+ # @param relationships [Array<Relationship>] mathematical relationships
455
+ # @param input_meta [Hash] input field metadata with domain constraints
456
+ # @param debug [Boolean] enable debug output
457
+ # @return [Hash] variable name to range mapping
458
+ def build_range_map(relationships, input_meta, debug: false)
459
+ range_map = {}
460
+
461
+ # Start with input field ranges
462
+ input_meta.each do |field_name, meta|
463
+ domain = meta[:domain]
464
+ next unless domain.is_a?(Range) && domain.first.is_a?(Numeric) && domain.last.is_a?(Numeric)
465
+
466
+ range_map[field_name] = domain
467
+ puts "Input range: #{field_name} -> #{domain}" if debug
468
+ end
469
+
470
+ # Propagate ranges through mathematical relationships
471
+ # Use iterative approach to handle chains
472
+ max_iterations = relationships.size + 1
473
+ iteration = 0
474
+
475
+ loop do
476
+ iteration += 1
477
+ new_ranges = {}
478
+
479
+ relationships.each do |rel|
480
+ range = compute_range_for_relationship(rel, range_map)
481
+ next unless range
482
+
483
+ if range_map[rel.target] != range
484
+ new_ranges[rel.target] = range
485
+ puts "Computed range: #{rel.target} -> #{range} (from #{rel.operation})" if debug
486
+ end
487
+ end
488
+
489
+ break if new_ranges.empty? || iteration > max_iterations
490
+
491
+ range_map.merge!(new_ranges)
492
+ end
493
+
494
+ range_map
495
+ end
496
+
497
+ # Computes the range for a variable based on its mathematical relationship
498
+ #
499
+ # @param relationship [Relationship] the mathematical relationship
500
+ # @param range_map [Hash] current variable to range mapping
501
+ # @return [Range, nil] computed range or nil if not computable
502
+ def compute_range_for_relationship(relationship, range_map)
503
+ # Handle identity relationships (simple variable copy)
504
+ if relationship.operation == :identity && relationship.operands.size == 1
505
+ var_operand = relationship.operands.first
506
+ return range_map[var_operand] if range_map.key?(var_operand)
507
+
508
+ return nil
509
+ end
510
+
511
+ return nil unless relationship.operands.size == 2
512
+
513
+ # Handle variable + constant pattern
514
+ var_operand = relationship.operands.find { |op| op.is_a?(Symbol) }
515
+ const_operand = relationship.operands.find { |op| op.is_a?(Numeric) }
516
+
517
+ return nil unless var_operand && const_operand
518
+
519
+ var_range = range_map[var_operand]
520
+ return nil unless var_range
521
+
522
+ transform_range(var_range, relationship.operation, const_operand, var_operand == relationship.operands[0])
523
+ end
524
+
525
+ # Transforms a range through a mathematical operation
526
+ #
527
+ # @param range [Range] the input range
528
+ # @param operation [Symbol] the mathematical operation
529
+ # @param constant [Numeric] the constant operand
530
+ # @param var_is_first [Boolean] whether variable is first operand
531
+ # @return [Range, nil] transformed range or nil if not computable
532
+ def transform_range(range, operation, constant, var_is_first)
533
+ min_val = range.first
534
+ max_val = range.last
535
+
536
+ case operation
537
+ when :add
538
+ (min_val + constant)..(max_val + constant)
539
+ when :subtract
540
+ if var_is_first
541
+ (min_val - constant)..(max_val - constant)
542
+ else
543
+ (constant - max_val)..(constant - min_val)
544
+ end
545
+ when :multiply
546
+ if constant >= 0
547
+ (min_val * constant)..(max_val * constant)
548
+ else
549
+ (max_val * constant)..(min_val * constant)
550
+ end
551
+ when :divide
552
+ return nil if constant.zero?
553
+
554
+ if var_is_first
555
+ if constant.positive?
556
+ (min_val / constant)..(max_val / constant)
557
+ else
558
+ (max_val / constant)..(min_val / constant)
559
+ end
560
+ else
561
+ # constant / range - more complex, skip for now
562
+ nil
563
+ end
564
+ end
565
+ end
566
+
567
+ # Checks if a constraint is impossible given a variable's range
568
+ #
569
+ # @param range [Range] the variable's possible range
570
+ # @param operator [Symbol] the constraint operator
571
+ # @param value [Numeric] the constraint value
572
+ # @return [Boolean] true if constraint is impossible
573
+ def range_constraint_impossible?(range, operator, value)
574
+ case operator
575
+ when :>
576
+ # x > value is impossible if max(x) <= value
577
+ range.last <= value
578
+ when :>=
579
+ # x >= value is impossible if max(x) < value
580
+ range.last < value
581
+ when :<
582
+ # x < value is impossible if min(x) >= value
583
+ range.first >= value
584
+ when :<=
585
+ # x <= value is impossible if min(x) > value
586
+ range.first > value
587
+ else
588
+ false
589
+ end
590
+ end
591
+
592
+ # Checks if any derived constraints violate input field domain constraints
593
+ #
594
+ # @param derived_constraints [Array<DerivedConstraint>] constraints derived from relationships
595
+ # @param input_meta [Hash] input field metadata with domain constraints
596
+ # @param debug [Boolean] enable debug output
597
+ # @return [Boolean] true if domain constraint violation exists
598
+ def domain_constraint_violation?(derived_constraints, input_meta, debug: false)
599
+ return false if input_meta.empty?
600
+
601
+ derived_constraints.each do |constraint|
602
+ # Only check equality constraints for now
603
+ next unless constraint.operation == :==
604
+
605
+ # Check if this variable corresponds to an input field with domain constraints
606
+ field_meta = input_meta[constraint.variable]
607
+ next unless field_meta&.dig(:domain)
608
+
609
+ domain = field_meta[:domain]
610
+ value = constraint.value
611
+
612
+ if violates_domain?(value, domain)
613
+ puts "Domain violation detected: #{constraint.variable} == #{value} violates domain #{domain.inspect}" if debug
614
+ return true
615
+ end
616
+ end
617
+
618
+ false
619
+ end
620
+
621
+ # Checks if a value violates a domain constraint
622
+ #
623
+ # @param value [Object] the value to check
624
+ # @param domain [Range, Array, Proc] the domain constraint
625
+ # @return [Boolean] true if value violates domain
626
+ def violates_domain?(value, domain)
627
+ case domain
628
+ when Range
629
+ !domain.include?(value)
630
+ when Array
631
+ !domain.include?(value)
632
+ when Proc
633
+ # For Proc domains, we can't statically analyze
634
+ false
635
+ else
636
+ false
637
+ end
638
+ end
639
+ end
640
+ end
641
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Domain
6
+ class EnumAnalyzer
7
+ def self.analyze(enum)
8
+ {
9
+ type: :enumeration,
10
+ values: enum,
11
+ size: enum.size,
12
+ sample_values: generate_samples(enum),
13
+ invalid_samples: generate_invalid_samples(enum)
14
+ }
15
+ end
16
+
17
+ def self.generate_samples(enum)
18
+ enum.sample([enum.size, 3].min)
19
+ end
20
+
21
+ def self.generate_invalid_samples(enum)
22
+ case enum.first
23
+ when String
24
+ generate_string_invalid_samples(enum)
25
+ when Integer
26
+ generate_integer_invalid_samples(enum)
27
+ when Symbol
28
+ generate_symbol_invalid_samples(enum)
29
+ else
30
+ generate_default_invalid_samples
31
+ end
32
+ end
33
+
34
+ private_class_method def self.generate_string_invalid_samples(enum)
35
+ candidates = ["invalid_string", "", "not_in_list"]
36
+ candidates.reject { |v| enum.include?(v) }
37
+ end
38
+
39
+ private_class_method def self.generate_integer_invalid_samples(enum)
40
+ candidates = [-999, 0, 999]
41
+ candidates.reject { |v| enum.include?(v) }
42
+ end
43
+
44
+ private_class_method def self.generate_symbol_invalid_samples(enum)
45
+ candidates = %i[invalid_symbol not_in_list]
46
+ candidates.reject { |v| enum.include?(v) }
47
+ end
48
+
49
+ private_class_method def self.generate_default_invalid_samples
50
+ [nil, "invalid", -1]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end