igniter 0.2.0 → 0.3.0

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +224 -1
  4. data/docs/API_V2.md +238 -1
  5. data/docs/BACKLOG.md +166 -0
  6. data/docs/BRANCHES_V1.md +213 -0
  7. data/docs/COLLECTIONS_V1.md +303 -0
  8. data/docs/EXECUTION_MODEL_V2.md +79 -0
  9. data/docs/PATTERNS.md +222 -0
  10. data/docs/STORE_ADAPTERS.md +126 -0
  11. data/examples/README.md +124 -0
  12. data/examples/async_store.rb +47 -0
  13. data/examples/collection.rb +43 -0
  14. data/examples/collection_partial_failure.rb +50 -0
  15. data/examples/marketing_ergonomics.rb +57 -0
  16. data/examples/ringcentral_routing.rb +278 -0
  17. data/lib/igniter/compiler/compiled_graph.rb +82 -0
  18. data/lib/igniter/compiler/graph_compiler.rb +12 -2
  19. data/lib/igniter/compiler/type_resolver.rb +54 -0
  20. data/lib/igniter/compiler/validation_context.rb +61 -0
  21. data/lib/igniter/compiler/validation_pipeline.rb +30 -0
  22. data/lib/igniter/compiler/validator.rb +1 -187
  23. data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
  24. data/lib/igniter/compiler/validators/dependencies_validator.rb +151 -0
  25. data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
  26. data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
  27. data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
  28. data/lib/igniter/compiler.rb +8 -0
  29. data/lib/igniter/contract.rb +136 -4
  30. data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
  31. data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
  32. data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
  33. data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
  34. data/lib/igniter/diagnostics/report.rb +84 -8
  35. data/lib/igniter/dsl/contract_builder.rb +208 -5
  36. data/lib/igniter/dsl/schema_builder.rb +73 -0
  37. data/lib/igniter/dsl.rb +1 -0
  38. data/lib/igniter/errors.rb +11 -0
  39. data/lib/igniter/events/bus.rb +5 -0
  40. data/lib/igniter/events/event.rb +29 -0
  41. data/lib/igniter/executor.rb +74 -0
  42. data/lib/igniter/executor_registry.rb +44 -0
  43. data/lib/igniter/extensions/auditing/timeline.rb +4 -0
  44. data/lib/igniter/extensions/introspection/graph_formatter.rb +29 -3
  45. data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
  46. data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
  47. data/lib/igniter/extensions/introspection.rb +1 -0
  48. data/lib/igniter/extensions/reactive/engine.rb +49 -2
  49. data/lib/igniter/extensions/reactive/reaction.rb +3 -2
  50. data/lib/igniter/model/branch_node.rb +40 -0
  51. data/lib/igniter/model/collection_node.rb +25 -0
  52. data/lib/igniter/model/composition_node.rb +2 -2
  53. data/lib/igniter/model/compute_node.rb +58 -2
  54. data/lib/igniter/model/input_node.rb +2 -2
  55. data/lib/igniter/model/output_node.rb +24 -4
  56. data/lib/igniter/model.rb +2 -0
  57. data/lib/igniter/runtime/cache.rb +64 -25
  58. data/lib/igniter/runtime/collection_result.rb +111 -0
  59. data/lib/igniter/runtime/deferred_result.rb +40 -0
  60. data/lib/igniter/runtime/execution.rb +261 -11
  61. data/lib/igniter/runtime/input_validator.rb +2 -24
  62. data/lib/igniter/runtime/invalidator.rb +1 -1
  63. data/lib/igniter/runtime/job_worker.rb +18 -0
  64. data/lib/igniter/runtime/node_state.rb +20 -0
  65. data/lib/igniter/runtime/planner.rb +126 -0
  66. data/lib/igniter/runtime/resolver.rb +269 -15
  67. data/lib/igniter/runtime/result.rb +14 -2
  68. data/lib/igniter/runtime/runner_factory.rb +20 -0
  69. data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
  70. data/lib/igniter/runtime/runners/store_runner.rb +29 -0
  71. data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
  72. data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
  73. data/lib/igniter/runtime/stores/file_store.rb +43 -0
  74. data/lib/igniter/runtime/stores/memory_store.rb +40 -0
  75. data/lib/igniter/runtime/stores/redis_store.rb +44 -0
  76. data/lib/igniter/runtime.rb +12 -0
  77. data/lib/igniter/type_system.rb +44 -0
  78. data/lib/igniter/version.rb +1 -1
  79. data/lib/igniter.rb +23 -0
  80. metadata +43 -2
@@ -9,196 +9,10 @@ module Igniter
9
9
 
10
10
  def initialize(graph)
11
11
  @graph = graph
12
- @runtime_nodes_by_name = {}
13
12
  end
14
13
 
15
14
  def call
16
- validate_unique_ids!
17
- index_runtime_nodes!
18
- validate_outputs!
19
- validate_unique_paths!
20
- validate_dependencies!
21
- validate_callable_signatures!
22
-
23
- self
24
- end
25
-
26
- def runtime_nodes
27
- @runtime_nodes ||= @graph.nodes.reject { |node| node.kind == :output }
28
- end
29
-
30
- def outputs
31
- @outputs ||= @graph.nodes.select { |node| node.kind == :output }
32
- end
33
-
34
- def runtime_nodes_by_name
35
- @runtime_nodes_by_name
36
- end
37
-
38
- private
39
-
40
- def validate_unique_ids!
41
- seen = {}
42
-
43
- @graph.nodes.each do |node|
44
- if seen.key?(node.id)
45
- raise validation_error(node, "Duplicate node id: #{node.id}")
46
- end
47
-
48
- seen[node.id] = true
49
- end
50
- end
51
-
52
- def validate_unique_paths!
53
- seen = {}
54
-
55
- runtime_nodes.each do |node|
56
- if seen.key?(node.path)
57
- raise validation_error(node, "Duplicate node path: #{node.path}")
58
- end
59
-
60
- seen[node.path] = true
61
- end
62
-
63
- outputs.each do |output|
64
- if seen.key?(output.path)
65
- raise validation_error(output, "Duplicate node path: #{output.path}")
66
- end
67
-
68
- seen[output.path] = true
69
- end
70
- end
71
-
72
- def index_runtime_nodes!
73
- runtime_nodes.each do |node|
74
- raise validation_error(node, "Duplicate node name: #{node.name}") if @runtime_nodes_by_name.key?(node.name)
75
-
76
- @runtime_nodes_by_name[node.name] = node
77
- end
78
- end
79
-
80
- def validate_outputs!
81
- raise ValidationError, "Graph must define at least one output" if outputs.empty?
82
-
83
- seen = {}
84
- outputs.each do |output|
85
- raise validation_error(output, "Duplicate output name: #{output.name}") if seen.key?(output.name)
86
- raise validation_error(output, "Unknown output source '#{output.source}' for output '#{output.name}'") unless @runtime_nodes_by_name.key?(output.source)
87
-
88
- seen[output.name] = true
89
- end
90
- end
91
-
92
- def validate_dependencies!
93
- runtime_nodes.each do |node|
94
- validate_composition_node!(node) if node.kind == :composition
95
-
96
- node.dependencies.each do |dependency_name|
97
- next if @runtime_nodes_by_name.key?(dependency_name)
98
-
99
- raise validation_error(node, "Unknown dependency '#{dependency_name}' for node '#{node.name}'")
100
- end
101
- end
102
- end
103
-
104
- def validate_composition_node!(node)
105
- contract_class = node.contract_class
106
-
107
- unless contract_class.is_a?(Class) && contract_class <= Igniter::Contract
108
- raise validation_error(node, "Composition '#{node.name}' must reference an Igniter::Contract subclass")
109
- end
110
-
111
- unless contract_class.compiled_graph
112
- raise validation_error(node, "Composition '#{node.name}' references an uncompiled contract")
113
- end
114
-
115
- validate_composition_input_mapping!(node, contract_class.compiled_graph)
116
- end
117
-
118
- def validate_composition_input_mapping!(node, child_graph)
119
- child_input_nodes = child_graph.nodes.select { |child_node| child_node.kind == :input }
120
- child_input_names = child_input_nodes.map(&:name)
121
-
122
- unknown_inputs = node.input_mapping.keys - child_input_names
123
- unless unknown_inputs.empty?
124
- raise validation_error(
125
- node,
126
- "Composition '#{node.name}' maps unknown child inputs: #{unknown_inputs.sort.join(', ')}"
127
- )
128
- end
129
-
130
- missing_required_inputs = child_input_nodes
131
- .select(&:required?)
132
- .reject { |child_input| node.input_mapping.key?(child_input.name) }
133
- .map(&:name)
134
-
135
- return if missing_required_inputs.empty?
136
-
137
- raise validation_error(
138
- node,
139
- "Composition '#{node.name}' is missing mappings for required child inputs: #{missing_required_inputs.sort.join(', ')}"
140
- )
141
- end
142
-
143
- def validate_callable_signatures!
144
- runtime_nodes.each do |node|
145
- next unless node.kind == :compute
146
- next unless node.callable.is_a?(Proc)
147
-
148
- validate_proc_signature!(node)
149
- end
150
- end
151
-
152
- def validate_proc_signature!(node)
153
- parameters = node.callable.parameters
154
- positional_kinds = %i[req opt rest]
155
- positional = parameters.select { |kind, _name| positional_kinds.include?(kind) }
156
-
157
- unless positional.empty?
158
- raise validation_error(
159
- node,
160
- "Compute '#{node.name}' proc must use keyword arguments for dependencies, got positional parameters"
161
- )
162
- end
163
-
164
- accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
165
- accepted_keywords = parameters
166
- .select { |kind, _name| %i[key keyreq].include?(kind) }
167
- .map(&:last)
168
- required_keywords = parameters
169
- .select { |kind, _name| kind == :keyreq }
170
- .map(&:last)
171
-
172
- missing_dependencies = required_keywords - node.dependencies
173
- unless missing_dependencies.empty?
174
- raise validation_error(
175
- node,
176
- "Compute '#{node.name}' requires undeclared dependencies: #{missing_dependencies.sort.join(', ')}"
177
- )
178
- end
179
-
180
- return if accepts_any_keywords
181
-
182
- unknown_dependencies = node.dependencies - accepted_keywords
183
- return if unknown_dependencies.empty?
184
-
185
- raise validation_error(
186
- node,
187
- "Compute '#{node.name}' declares unsupported dependencies for its proc: #{unknown_dependencies.sort.join(', ')}"
188
- )
189
- end
190
-
191
- def validation_error(node, message)
192
- ValidationError.new(
193
- message,
194
- context: {
195
- graph: @graph.name,
196
- node_id: node.id,
197
- node_name: node.name,
198
- node_path: node.path,
199
- source_location: node.source_location
200
- }
201
- )
15
+ ValidationPipeline.call(ValidationContext.new(@graph))
202
16
  end
203
17
  end
204
18
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ module Validators
6
+ class CallableValidator
7
+ def self.call(context)
8
+ new(context).call
9
+ end
10
+
11
+ def initialize(context)
12
+ @context = context
13
+ end
14
+
15
+ def call
16
+ @context.runtime_nodes.each do |node|
17
+ next unless node.kind == :compute
18
+
19
+ validate_callable_signature!(node)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def validate_callable_signature!(node)
26
+ callable = node.callable
27
+
28
+ case callable
29
+ when Proc
30
+ validate_parameters_signature!(node, callable.parameters, "proc")
31
+ when Class
32
+ validate_class_callable_signature!(node, callable)
33
+ when Symbol, String
34
+ nil
35
+ else
36
+ validate_object_callable_signature!(node, callable)
37
+ end
38
+ end
39
+
40
+ def validate_class_callable_signature!(node, callable)
41
+ if callable <= Igniter::Executor
42
+ validate_executor_inputs!(node, callable)
43
+ validate_parameters_signature!(node, callable.instance_method(:call).parameters, callable.name || "executor")
44
+ elsif callable.respond_to?(:call)
45
+ validate_parameters_signature!(node, callable.method(:call).parameters, callable.name || "callable class")
46
+ else
47
+ raise @context.validation_error(node, "Compute '#{node.name}' class callable must respond to `.call`")
48
+ end
49
+ end
50
+
51
+ def validate_object_callable_signature!(node, callable)
52
+ unless callable.respond_to?(:call)
53
+ raise @context.validation_error(node, "Compute '#{node.name}' callable object must respond to `call`")
54
+ end
55
+
56
+ validate_parameters_signature!(node, callable.method(:call).parameters, callable.class.name || "callable object")
57
+ end
58
+
59
+ def validate_executor_inputs!(node, executor_class)
60
+ declared_inputs = executor_class.executor_inputs
61
+ required_inputs = declared_inputs.select { |_name, config| config[:required] }.keys
62
+ missing_dependencies = required_inputs - node.dependencies
63
+
64
+ return if missing_dependencies.empty?
65
+
66
+ raise @context.validation_error(
67
+ node,
68
+ "Compute '#{node.name}' executor requires undeclared dependencies: #{missing_dependencies.sort.join(', ')}"
69
+ )
70
+ end
71
+
72
+ def validate_parameters_signature!(node, parameters, callable_label)
73
+ positional_kinds = %i[req opt rest]
74
+ positional = parameters.select { |kind, _name| positional_kinds.include?(kind) }
75
+ unless positional.empty?
76
+ raise @context.validation_error(
77
+ node,
78
+ "Compute '#{node.name}' #{callable_label} must use keyword arguments for dependencies, got positional parameters"
79
+ )
80
+ end
81
+
82
+ accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
83
+ accepted_keywords = parameters.select { |kind, _name| %i[key keyreq].include?(kind) }.map(&:last)
84
+ required_keywords = parameters.select { |kind, _name| kind == :keyreq }.map(&:last)
85
+
86
+ missing_dependencies = required_keywords - node.dependencies
87
+ unless missing_dependencies.empty?
88
+ raise @context.validation_error(
89
+ node,
90
+ "Compute '#{node.name}' #{callable_label} requires undeclared dependencies: #{missing_dependencies.sort.join(', ')}"
91
+ )
92
+ end
93
+
94
+ return if accepts_any_keywords
95
+
96
+ unknown_dependencies = node.dependencies - accepted_keywords
97
+ return if unknown_dependencies.empty?
98
+
99
+ raise @context.validation_error(
100
+ node,
101
+ "Compute '#{node.name}' declares unsupported dependencies for its #{callable_label}: #{unknown_dependencies.sort.join(', ')}"
102
+ )
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ module Validators
6
+ class DependenciesValidator
7
+ def self.call(context)
8
+ new(context).call
9
+ end
10
+
11
+ def initialize(context)
12
+ @context = context
13
+ end
14
+
15
+ def call
16
+ @context.runtime_nodes.each do |node|
17
+ validate_composition_node!(node) if node.kind == :composition
18
+ validate_branch_node!(node) if node.kind == :branch
19
+ validate_collection_node!(node) if node.kind == :collection
20
+
21
+ node.dependencies.each do |dependency_name|
22
+ next if @context.dependency_resolvable?(dependency_name)
23
+
24
+ raise @context.validation_error(node, "Unknown dependency '#{dependency_name}' for node '#{node.name}'")
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def validate_composition_node!(node)
32
+ contract_class = node.contract_class
33
+
34
+ unless contract_class.is_a?(Class) && contract_class <= Igniter::Contract
35
+ raise @context.validation_error(node, "Composition '#{node.name}' must reference an Igniter::Contract subclass")
36
+ end
37
+
38
+ unless contract_class.compiled_graph
39
+ raise @context.validation_error(node, "Composition '#{node.name}' references an uncompiled contract")
40
+ end
41
+
42
+ validate_composition_input_mapping!(node, contract_class.compiled_graph)
43
+ end
44
+
45
+ def validate_composition_input_mapping!(node, child_graph)
46
+ child_input_nodes = child_graph.nodes.select { |child_node| child_node.kind == :input }
47
+ child_input_names = child_input_nodes.map(&:name)
48
+
49
+ unknown_inputs = node.input_mapping.keys - child_input_names
50
+ unless unknown_inputs.empty?
51
+ raise @context.validation_error(
52
+ node,
53
+ "Composition '#{node.name}' maps unknown child inputs: #{unknown_inputs.sort.join(', ')}"
54
+ )
55
+ end
56
+
57
+ missing_required_inputs = child_input_nodes
58
+ .select(&:required?)
59
+ .reject { |child_input| node.input_mapping.key?(child_input.name) }
60
+ .map(&:name)
61
+
62
+ return if missing_required_inputs.empty?
63
+
64
+ raise @context.validation_error(
65
+ node,
66
+ "Composition '#{node.name}' is missing mappings for required child inputs: #{missing_required_inputs.sort.join(', ')}"
67
+ )
68
+ end
69
+
70
+ def validate_branch_node!(node)
71
+ validate_branch_structure!(node)
72
+
73
+ node.possible_contracts.each do |contract_class|
74
+ validate_branch_contract!(node, contract_class)
75
+ validate_branch_input_mapping!(node, contract_class.compiled_graph)
76
+ end
77
+ end
78
+
79
+ def validate_branch_structure!(node)
80
+ raise @context.validation_error(node, "Branch '#{node.name}' must define at least one `on` case") if node.cases.empty?
81
+ raise @context.validation_error(node, "Branch '#{node.name}' must define a `default` contract") unless node.default_contract
82
+
83
+ duplicate_matches = node.cases.group_by { |entry| entry[:match] }.select { |_match, entries| entries.size > 1 }.keys
84
+ return if duplicate_matches.empty?
85
+
86
+ raise @context.validation_error(
87
+ node,
88
+ "Branch '#{node.name}' has duplicate case values: #{duplicate_matches.map(&:inspect).join(', ')}"
89
+ )
90
+ end
91
+
92
+ def validate_branch_contract!(node, contract_class)
93
+ unless contract_class.is_a?(Class) && contract_class <= Igniter::Contract
94
+ raise @context.validation_error(node, "Branch '#{node.name}' must reference Igniter::Contract subclasses")
95
+ end
96
+
97
+ unless contract_class.compiled_graph
98
+ raise @context.validation_error(node, "Branch '#{node.name}' references an uncompiled contract")
99
+ end
100
+ end
101
+
102
+ def validate_branch_input_mapping!(node, child_graph)
103
+ child_input_nodes = child_graph.nodes.select { |child_node| child_node.kind == :input }
104
+ child_input_names = child_input_nodes.map(&:name)
105
+
106
+ unknown_inputs = node.input_mapping.keys - child_input_names
107
+ unless unknown_inputs.empty?
108
+ raise @context.validation_error(
109
+ node,
110
+ "Branch '#{node.name}' maps unknown child inputs: #{unknown_inputs.sort.join(', ')}"
111
+ )
112
+ end
113
+
114
+ missing_required_inputs = child_input_nodes
115
+ .select(&:required?)
116
+ .reject { |child_input| node.input_mapping.key?(child_input.name) }
117
+ .map(&:name)
118
+
119
+ return if missing_required_inputs.empty?
120
+
121
+ raise @context.validation_error(
122
+ node,
123
+ "Branch '#{node.name}' is missing mappings for required child inputs: #{missing_required_inputs.sort.join(', ')}"
124
+ )
125
+ end
126
+
127
+ def validate_collection_node!(node)
128
+ unless node.contract_class.is_a?(Class) && node.contract_class <= Igniter::Contract
129
+ raise @context.validation_error(node, "Collection '#{node.name}' must reference an Igniter::Contract subclass")
130
+ end
131
+
132
+ unless node.contract_class.compiled_graph
133
+ raise @context.validation_error(node, "Collection '#{node.name}' references an uncompiled contract")
134
+ end
135
+
136
+ unless %i[collect fail_fast].include?(node.mode)
137
+ raise @context.validation_error(node, "Collection '#{node.name}' mode must be `:collect` or `:fail_fast`")
138
+ end
139
+
140
+ child_input_names = node.contract_class.compiled_graph.nodes.select { |child_node| child_node.kind == :input }.map(&:name)
141
+ return if child_input_names.include?(node.key_name)
142
+
143
+ raise @context.validation_error(
144
+ node,
145
+ "Collection '#{node.name}' key '#{node.key_name}' must be a child contract input"
146
+ )
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ module Validators
6
+ class OutputsValidator
7
+ def self.call(context)
8
+ new(context).call
9
+ end
10
+
11
+ def initialize(context)
12
+ @context = context
13
+ end
14
+
15
+ def call
16
+ raise ValidationError, "Graph must define at least one output" if @context.outputs.empty?
17
+
18
+ @context.outputs.each { |output| validate_output_source!(output) }
19
+ end
20
+
21
+ private
22
+
23
+ def validate_output_source!(output)
24
+ if output.composition_output?
25
+ validate_nested_output_source!(output)
26
+ else
27
+ return if @context.runtime_nodes_by_name.key?(output.source)
28
+
29
+ raise @context.validation_error(output, "Unknown output source '#{output.source}' for output '#{output.name}'")
30
+ end
31
+ end
32
+
33
+ def validate_nested_output_source!(output)
34
+ parent_node = @context.runtime_nodes_by_name[output.source_root]
35
+ unless %i[composition branch].include?(parent_node&.kind)
36
+ raise @context.validation_error(
37
+ output,
38
+ "Output '#{output.name}' references unknown nested source '#{output.source}'"
39
+ )
40
+ end
41
+
42
+ validate_nested_output_presence!(output, parent_node)
43
+ end
44
+
45
+ def validate_nested_output_presence!(output, parent_node)
46
+ child_graphs =
47
+ case parent_node.kind
48
+ when :composition
49
+ [parent_node.contract_class.compiled_graph]
50
+ when :branch
51
+ parent_node.possible_contracts.map(&:compiled_graph)
52
+ else
53
+ []
54
+ end
55
+
56
+ return if child_graphs.all? { |graph| graph.outputs_by_name.key?(output.child_output_name) }
57
+
58
+ raise @context.validation_error(
59
+ output,
60
+ "Output '#{output.name}' references unknown child output '#{output.child_output_name}' on nested source '#{parent_node.name}'"
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ module Validators
6
+ class TypeCompatibilityValidator
7
+ def self.call(context)
8
+ new(context).call
9
+ end
10
+
11
+ def initialize(context)
12
+ @context = context
13
+ @resolver = TypeResolver.new(context)
14
+ end
15
+
16
+ def call
17
+ validate_executor_dependency_types!
18
+ validate_composition_input_types!
19
+ end
20
+
21
+ private
22
+
23
+ def validate_executor_dependency_types!
24
+ @context.runtime_nodes.each do |node|
25
+ next unless node.kind == :compute
26
+ next unless node.callable.is_a?(Class) && node.callable <= Igniter::Executor
27
+
28
+ node.callable.executor_inputs.each do |dependency_name, config|
29
+ next unless node.dependencies.include?(dependency_name)
30
+
31
+ validate_edge_type!(
32
+ node: node,
33
+ dependency_name: dependency_name,
34
+ target_type: config[:type],
35
+ label: "executor input '#{dependency_name}'"
36
+ )
37
+ end
38
+ end
39
+ end
40
+
41
+ def validate_composition_input_types!
42
+ @context.runtime_nodes.each do |node|
43
+ next unless node.kind == :composition
44
+
45
+ child_inputs = node.contract_class.compiled_graph.nodes.select { |child_node| child_node.kind == :input }
46
+ child_inputs.each do |child_input|
47
+ dependency_name = node.input_mapping[child_input.name]
48
+ next unless dependency_name
49
+
50
+ validate_edge_type!(
51
+ node: node,
52
+ dependency_name: dependency_name,
53
+ target_type: child_input.type,
54
+ label: "composition input '#{child_input.name}'"
55
+ )
56
+ end
57
+ end
58
+ end
59
+
60
+ def validate_edge_type!(node:, dependency_name:, target_type:, label:)
61
+ return unless target_type
62
+
63
+ unless TypeSystem.supported?(target_type)
64
+ raise @context.validation_error(node, "Unsupported target type '#{target_type}' for #{label}")
65
+ end
66
+
67
+ source_type = @resolver.call(dependency_name)
68
+ return if source_type.nil?
69
+
70
+ unless TypeSystem.supported?(source_type)
71
+ raise @context.validation_error(node, "Unsupported source type '#{source_type}' for dependency '#{dependency_name}'")
72
+ end
73
+
74
+ return if TypeSystem.compatible?(source_type, target_type)
75
+
76
+ raise @context.validation_error(
77
+ node,
78
+ "Type mismatch for #{label}: dependency '#{dependency_name}' is #{source_type}, expected #{target_type}"
79
+ )
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Compiler
5
+ module Validators
6
+ class UniquenessValidator
7
+ def self.call(context)
8
+ new(context).call
9
+ end
10
+
11
+ def initialize(context)
12
+ @context = context
13
+ end
14
+
15
+ def call
16
+ validate_unique_ids!
17
+ validate_unique_names!
18
+ validate_unique_paths!
19
+ end
20
+
21
+ private
22
+
23
+ def validate_unique_ids!
24
+ seen = {}
25
+ @context.graph.nodes.each do |node|
26
+ raise @context.validation_error(node, "Duplicate node id: #{node.id}") if seen.key?(node.id)
27
+
28
+ seen[node.id] = true
29
+ end
30
+ end
31
+
32
+ def validate_unique_names!
33
+ seen_runtime = {}
34
+ @context.runtime_nodes.each do |node|
35
+ raise @context.validation_error(node, "Duplicate node name: #{node.name}") if seen_runtime.key?(node.name)
36
+
37
+ seen_runtime[node.name] = true
38
+ end
39
+
40
+ seen_outputs = {}
41
+ @context.outputs.each do |output|
42
+ raise @context.validation_error(output, "Duplicate output name: #{output.name}") if seen_outputs.key?(output.name)
43
+
44
+ seen_outputs[output.name] = true
45
+ end
46
+ end
47
+
48
+ def validate_unique_paths!
49
+ seen = {}
50
+
51
+ (@context.runtime_nodes + @context.outputs).each do |node|
52
+ raise @context.validation_error(node, "Duplicate node path: #{node.path}") if seen.key?(node.path)
53
+
54
+ seen[node.path] = true
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -1,6 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "compiler/compiled_graph"
4
+ require_relative "compiler/validation_context"
5
+ require_relative "compiler/type_resolver"
6
+ require_relative "compiler/validators/uniqueness_validator"
7
+ require_relative "compiler/validators/outputs_validator"
8
+ require_relative "compiler/validators/dependencies_validator"
9
+ require_relative "compiler/validators/callable_validator"
10
+ require_relative "compiler/validators/type_compatibility_validator"
11
+ require_relative "compiler/validation_pipeline"
4
12
  require_relative "compiler/validator"
5
13
  require_relative "compiler/graph_compiler"
6
14