igniter-extensions 0.5.2

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 (100) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +381 -0
  3. data/lib/igniter/extensions/contracts/aggregate_pack.rb +103 -0
  4. data/lib/igniter/extensions/contracts/audit/builder.rb +132 -0
  5. data/lib/igniter/extensions/contracts/audit/event.rb +34 -0
  6. data/lib/igniter/extensions/contracts/audit/snapshot.rb +44 -0
  7. data/lib/igniter/extensions/contracts/audit_pack.rb +60 -0
  8. data/lib/igniter/extensions/contracts/branch_pack.rb +199 -0
  9. data/lib/igniter/extensions/contracts/capabilities/declaration.rb +31 -0
  10. data/lib/igniter/extensions/contracts/capabilities/error.rb +35 -0
  11. data/lib/igniter/extensions/contracts/capabilities/policy.rb +20 -0
  12. data/lib/igniter/extensions/contracts/capabilities/report.rb +47 -0
  13. data/lib/igniter/extensions/contracts/capabilities/violation.rb +30 -0
  14. data/lib/igniter/extensions/contracts/capabilities_pack.rb +146 -0
  15. data/lib/igniter/extensions/contracts/collection_pack.rb +212 -0
  16. data/lib/igniter/extensions/contracts/commerce_pack.rb +91 -0
  17. data/lib/igniter/extensions/contracts/compose_pack.rb +213 -0
  18. data/lib/igniter/extensions/contracts/content_addressing/cache.rb +59 -0
  19. data/lib/igniter/extensions/contracts/content_addressing/content_key.rb +63 -0
  20. data/lib/igniter/extensions/contracts/content_addressing/declaration.rb +47 -0
  21. data/lib/igniter/extensions/contracts/content_addressing_pack.rb +90 -0
  22. data/lib/igniter/extensions/contracts/creator/profile.rb +196 -0
  23. data/lib/igniter/extensions/contracts/creator/report.rb +85 -0
  24. data/lib/igniter/extensions/contracts/creator/scaffold.rb +461 -0
  25. data/lib/igniter/extensions/contracts/creator/scope.rb +79 -0
  26. data/lib/igniter/extensions/contracts/creator/wizard.rb +269 -0
  27. data/lib/igniter/extensions/contracts/creator/workflow.rb +189 -0
  28. data/lib/igniter/extensions/contracts/creator/workflow_step.rb +51 -0
  29. data/lib/igniter/extensions/contracts/creator/write_result.rb +48 -0
  30. data/lib/igniter/extensions/contracts/creator/write_step.rb +63 -0
  31. data/lib/igniter/extensions/contracts/creator/writer.rb +131 -0
  32. data/lib/igniter/extensions/contracts/creator_pack.rb +128 -0
  33. data/lib/igniter/extensions/contracts/dataflow/aggregate_operators.rb +119 -0
  34. data/lib/igniter/extensions/contracts/dataflow/aggregate_state.rb +60 -0
  35. data/lib/igniter/extensions/contracts/dataflow/builder.rb +66 -0
  36. data/lib/igniter/extensions/contracts/dataflow/collection_result.rb +70 -0
  37. data/lib/igniter/extensions/contracts/dataflow/diff.rb +37 -0
  38. data/lib/igniter/extensions/contracts/dataflow/item_result.rb +44 -0
  39. data/lib/igniter/extensions/contracts/dataflow/result.rb +58 -0
  40. data/lib/igniter/extensions/contracts/dataflow/session.rb +173 -0
  41. data/lib/igniter/extensions/contracts/dataflow/window_filter.rb +49 -0
  42. data/lib/igniter/extensions/contracts/dataflow_pack.rb +66 -0
  43. data/lib/igniter/extensions/contracts/debug/pack_audit.rb +181 -0
  44. data/lib/igniter/extensions/contracts/debug/pack_snapshot.rb +46 -0
  45. data/lib/igniter/extensions/contracts/debug/profile_snapshot.rb +50 -0
  46. data/lib/igniter/extensions/contracts/debug/report.rb +50 -0
  47. data/lib/igniter/extensions/contracts/debug_pack.rb +115 -0
  48. data/lib/igniter/extensions/contracts/differential/divergence.rb +37 -0
  49. data/lib/igniter/extensions/contracts/differential/formatter.rb +85 -0
  50. data/lib/igniter/extensions/contracts/differential/report.rb +83 -0
  51. data/lib/igniter/extensions/contracts/differential/runner.rb +136 -0
  52. data/lib/igniter/extensions/contracts/differential_pack.rb +61 -0
  53. data/lib/igniter/extensions/contracts/execution_report_pack.rb +38 -0
  54. data/lib/igniter/extensions/contracts/incremental/formatter.rb +60 -0
  55. data/lib/igniter/extensions/contracts/incremental/node_state.rb +30 -0
  56. data/lib/igniter/extensions/contracts/incremental/result.rb +65 -0
  57. data/lib/igniter/extensions/contracts/incremental/session.rb +146 -0
  58. data/lib/igniter/extensions/contracts/incremental_pack.rb +40 -0
  59. data/lib/igniter/extensions/contracts/invariants/builder.rb +27 -0
  60. data/lib/igniter/extensions/contracts/invariants/cases_report.rb +47 -0
  61. data/lib/igniter/extensions/contracts/invariants/error.rb +34 -0
  62. data/lib/igniter/extensions/contracts/invariants/invariant.rb +30 -0
  63. data/lib/igniter/extensions/contracts/invariants/report.rb +45 -0
  64. data/lib/igniter/extensions/contracts/invariants/suite.rb +36 -0
  65. data/lib/igniter/extensions/contracts/invariants/violation.rb +39 -0
  66. data/lib/igniter/extensions/contracts/invariants_pack.rb +88 -0
  67. data/lib/igniter/extensions/contracts/journal_pack.rb +55 -0
  68. data/lib/igniter/extensions/contracts/language/formula_pack.rb +185 -0
  69. data/lib/igniter/extensions/contracts/language/piecewise_pack.rb +166 -0
  70. data/lib/igniter/extensions/contracts/language/scale_pack.rb +147 -0
  71. data/lib/igniter/extensions/contracts/lookup_pack.rb +50 -0
  72. data/lib/igniter/extensions/contracts/mcp/creator_session.rb +105 -0
  73. data/lib/igniter/extensions/contracts/mcp/tool_argument.rb +35 -0
  74. data/lib/igniter/extensions/contracts/mcp/tool_definition.rb +33 -0
  75. data/lib/igniter/extensions/contracts/mcp/tool_result.rb +28 -0
  76. data/lib/igniter/extensions/contracts/mcp_pack.rb +335 -0
  77. data/lib/igniter/extensions/contracts/provenance/builder.rb +80 -0
  78. data/lib/igniter/extensions/contracts/provenance/lineage.rb +59 -0
  79. data/lib/igniter/extensions/contracts/provenance/node_trace.rb +53 -0
  80. data/lib/igniter/extensions/contracts/provenance/text_formatter.rb +62 -0
  81. data/lib/igniter/extensions/contracts/provenance_pack.rb +52 -0
  82. data/lib/igniter/extensions/contracts/reactive/builder.rb +43 -0
  83. data/lib/igniter/extensions/contracts/reactive/dispatch_result.rb +59 -0
  84. data/lib/igniter/extensions/contracts/reactive/engine.rb +79 -0
  85. data/lib/igniter/extensions/contracts/reactive/event.rb +36 -0
  86. data/lib/igniter/extensions/contracts/reactive/matcher.rb +20 -0
  87. data/lib/igniter/extensions/contracts/reactive/plan.rb +58 -0
  88. data/lib/igniter/extensions/contracts/reactive/subscription.rb +29 -0
  89. data/lib/igniter/extensions/contracts/reactive_pack.rb +169 -0
  90. data/lib/igniter/extensions/contracts/saga/compensation.rb +25 -0
  91. data/lib/igniter/extensions/contracts/saga/compensation_record.rb +28 -0
  92. data/lib/igniter/extensions/contracts/saga/compensation_set.rb +47 -0
  93. data/lib/igniter/extensions/contracts/saga/formatter.rb +39 -0
  94. data/lib/igniter/extensions/contracts/saga/result.rb +56 -0
  95. data/lib/igniter/extensions/contracts/saga/runner.rb +124 -0
  96. data/lib/igniter/extensions/contracts/saga_pack.rb +56 -0
  97. data/lib/igniter/extensions/contracts.rb +445 -0
  98. data/lib/igniter/extensions.rb +6 -0
  99. data/lib/igniter-extensions.rb +3 -0
  100. metadata +152 -0
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "audit/event"
4
+ require_relative "audit/snapshot"
5
+ require_relative "audit/builder"
6
+
7
+ module Igniter
8
+ module Extensions
9
+ module Contracts
10
+ module AuditPack
11
+ module_function
12
+
13
+ REPORT_CONTRIBUTOR = Module.new do
14
+ module_function
15
+
16
+ def augment(report:, result:, profile:) # rubocop:disable Lint/UnusedMethodArgument
17
+ snapshot = AuditPack.snapshot(result)
18
+ report.add_section(:audit_summary, {
19
+ graph: snapshot.graph,
20
+ event_count: snapshot.event_count,
21
+ event_types: snapshot.event_types,
22
+ state_count: snapshot.states.length,
23
+ output_names: snapshot.output_names
24
+ })
25
+ end
26
+ end
27
+
28
+ def manifest
29
+ Igniter::Contracts::PackManifest.new(
30
+ name: :extensions_audit,
31
+ registry_contracts: [Igniter::Contracts::PackManifest.diagnostic(:audit_summary)],
32
+ metadata: { category: :developer }
33
+ )
34
+ end
35
+
36
+ def install_into(kernel)
37
+ kernel.diagnostics_contributors.register(:audit_summary, REPORT_CONTRIBUTOR)
38
+ kernel
39
+ end
40
+
41
+ def snapshot(result)
42
+ Audit::Builder.build(result)
43
+ end
44
+
45
+ def report(environment, inputs: nil, compiled_graph: nil, &block)
46
+ result =
47
+ if block
48
+ environment.run(inputs: inputs || {}, &block)
49
+ elsif compiled_graph
50
+ environment.execute(compiled_graph, inputs: inputs || {})
51
+ else
52
+ raise ArgumentError, "audit_report requires a block or compiled_graph"
53
+ end
54
+
55
+ snapshot(result)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module BranchPack
7
+ UNSET = Object.new.freeze
8
+
9
+ class << self
10
+ def manifest
11
+ Igniter::Contracts::PackManifest.new(
12
+ name: :extensions_branch,
13
+ registry_contracts: [Igniter::Contracts::PackManifest.dsl_keyword(:branch)]
14
+ )
15
+ end
16
+
17
+ def install_into(kernel)
18
+ kernel.dsl_keywords.register(:branch, branch_keyword)
19
+ kernel
20
+ end
21
+
22
+ def branch_keyword
23
+ Igniter::Contracts::DslKeyword.new(:branch) do |name, on:, builder:, depends_on: [], &block|
24
+ raise ArgumentError, "branch :#{name} requires a block" unless block
25
+
26
+ selector_name = on.to_sym
27
+ dependency_names = [selector_name, *Array(depends_on).map(&:to_sym)].uniq
28
+ definition = Definition.new(name: name, selector_name: selector_name)
29
+ definition.instance_eval(&block)
30
+ definition.validate!
31
+
32
+ builder.add_operation(
33
+ kind: :compute,
34
+ name: name,
35
+ depends_on: dependency_names,
36
+ callable: lambda do |**values|
37
+ definition.resolve(values)
38
+ end
39
+ )
40
+ end
41
+ end
42
+
43
+ def invoke_value(callable_or_value, kwargs)
44
+ return callable_or_value unless callable_or_value.respond_to?(:call)
45
+
46
+ parameters = callable_or_value.parameters
47
+ accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
48
+ return callable_or_value.call(**kwargs) if accepts_any_keywords
49
+
50
+ accepted = parameters.select { |kind, _name| %i[key keyreq].include?(kind) }.map(&:last)
51
+ callable_or_value.call(**kwargs.slice(*accepted))
52
+ end
53
+
54
+ class Definition
55
+ def initialize(name:, selector_name:)
56
+ @name = name.to_sym
57
+ @selector_name = selector_name.to_sym
58
+ @cases = []
59
+ @default_case = nil
60
+ end
61
+
62
+ def on(match = UNSET, id: nil, value: UNSET, **options, &block)
63
+ matcher_kind, matcher_value = normalize_match(match, options)
64
+ resolved_value = normalize_value(value, block)
65
+
66
+ @cases << {
67
+ id: (id || :"case_#{@cases.length + 1}").to_sym,
68
+ matcher: matcher_kind,
69
+ match: matcher_value,
70
+ value: resolved_value
71
+ }
72
+ end
73
+
74
+ def default(id: :default, value: UNSET, &block)
75
+ raise ArgumentError, "branch :#{@name} can define only one default" if @default_case
76
+
77
+ @default_case = {
78
+ id: id.to_sym,
79
+ matcher: :default,
80
+ match: :default,
81
+ value: normalize_value(value, block)
82
+ }
83
+ end
84
+
85
+ def validate!
86
+ raise ArgumentError, "branch :#{@name} requires at least one on clause" if @cases.empty?
87
+ raise ArgumentError, "branch :#{@name} requires a default clause" unless @default_case
88
+
89
+ duplicate_ids = (@cases.map { |entry| entry.fetch(:id) } + [@default_case.fetch(:id)])
90
+ .group_by { |id| id }
91
+ .select { |_id, group| group.length > 1 }
92
+ .keys
93
+ unless duplicate_ids.empty?
94
+ raise ArgumentError,
95
+ "branch :#{@name} has duplicate case ids: #{duplicate_ids.join(", ")}"
96
+ end
97
+
98
+ overlapping = overlapping_literals
99
+ return if overlapping.empty?
100
+
101
+ raise ArgumentError,
102
+ "branch :#{@name} has overlapping literal matches: #{overlapping.map(&:inspect).join(", ")}"
103
+ end
104
+
105
+ def resolve(kwargs)
106
+ selector_value = kwargs.fetch(@selector_name)
107
+ selected = @cases.find { |entry| match?(entry, selector_value) } || @default_case
108
+
109
+ {
110
+ case: selected.fetch(:id),
111
+ value: BranchPack.invoke_value(selected.fetch(:value), kwargs),
112
+ matcher: selected.fetch(:matcher),
113
+ matched_on: selected.fetch(:match),
114
+ selector: @selector_name,
115
+ selector_value: selector_value
116
+ }
117
+ end
118
+
119
+ private
120
+
121
+ def normalize_match(match, options)
122
+ matcher_options = options.slice(:eq, :in, :matches)
123
+ provided = matcher_options.reject { |_key, value| value.equal?(UNSET) || value.nil? }
124
+
125
+ raise ArgumentError, "branch :#{@name} on cannot combine positional match with eq:, in:, or matches:" if !match.equal?(UNSET) && !provided.empty?
126
+
127
+ raise ArgumentError, "branch :#{@name} on requires a positional match or one of eq:, in:, or matches:" if match.equal?(UNSET) && provided.empty?
128
+
129
+ if provided.length > 1
130
+ raise ArgumentError,
131
+ "branch :#{@name} on supports only one matcher option at a time"
132
+ end
133
+
134
+ return [:eq, match] unless match.equal?(UNSET)
135
+
136
+ matcher_kind, matcher_value = provided.first
137
+
138
+ case matcher_kind
139
+ when :eq
140
+ [:eq, matcher_value]
141
+ when :in
142
+ array = Array(matcher_value)
143
+ raise ArgumentError, "branch :#{@name} in: requires a non-empty array" if array.empty?
144
+
145
+ [:in, array.freeze]
146
+ when :matches
147
+ raise ArgumentError, "branch :#{@name} matches: requires a Regexp" unless matcher_value.is_a?(Regexp)
148
+
149
+ [:matches, matcher_value]
150
+ else
151
+ raise ArgumentError, "branch :#{@name} received an unknown matcher #{matcher_kind.inspect}"
152
+ end
153
+ end
154
+
155
+ def normalize_value(value, block)
156
+ if !value.equal?(UNSET) && block
157
+ raise ArgumentError,
158
+ "branch :#{@name} case cannot combine value: with a block"
159
+ end
160
+ raise ArgumentError, "branch :#{@name} case requires value: or a block" if value.equal?(UNSET) && !block
161
+
162
+ block || value
163
+ end
164
+
165
+ def overlapping_literals
166
+ literals = @cases.flat_map do |entry|
167
+ case entry.fetch(:matcher)
168
+ when :eq
169
+ [entry.fetch(:match)]
170
+ when :in
171
+ entry.fetch(:match)
172
+ else
173
+ []
174
+ end
175
+ end
176
+
177
+ literals.group_by { |value| value }
178
+ .select { |_value, group| group.length > 1 }
179
+ .keys
180
+ end
181
+
182
+ def match?(entry, selector_value)
183
+ case entry.fetch(:matcher)
184
+ when :eq
185
+ selector_value == entry.fetch(:match)
186
+ when :in
187
+ entry.fetch(:match).include?(selector_value)
188
+ when :matches
189
+ !!(selector_value.to_s =~ entry.fetch(:match))
190
+ else
191
+ false
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Capabilities
7
+ class Declaration
8
+ attr_reader :callable, :capabilities
9
+
10
+ def initialize(callable:, capabilities:)
11
+ @callable = callable
12
+ @capabilities = Array(capabilities).flatten.compact.map(&:to_sym).uniq.freeze
13
+ freeze
14
+ end
15
+
16
+ def call(**kwargs)
17
+ callable.call(**kwargs)
18
+ end
19
+
20
+ def declared_capabilities
21
+ capabilities
22
+ end
23
+
24
+ def pure?
25
+ capabilities.include?(:pure)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Capabilities
7
+ class Error < StandardError; end
8
+
9
+ class CapabilityViolationError < Error
10
+ attr_reader :report
11
+
12
+ def initialize(message = nil, report:)
13
+ @report = report
14
+ super(message || default_message)
15
+ end
16
+
17
+ def to_h
18
+ {
19
+ message: message,
20
+ report: report.to_h
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def default_message
27
+ return "capability policy violated" if report.violations.empty?
28
+
29
+ report.violations.map(&:message).join("; ")
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Capabilities
7
+ class Policy
8
+ attr_reader :denied, :required, :on_undeclared
9
+
10
+ def initialize(denied: [], required: [], on_undeclared: :ignore)
11
+ @denied = Array(denied).map(&:to_sym).uniq.freeze
12
+ @required = Array(required).map(&:to_sym).uniq.freeze
13
+ @on_undeclared = on_undeclared.to_sym
14
+ freeze
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Capabilities
7
+ class Report
8
+ attr_reader :required_capabilities, :profile_capabilities, :violations, :undeclared_nodes
9
+
10
+ def initialize(required_capabilities:, profile_capabilities:, violations:, undeclared_nodes:)
11
+ @required_capabilities = required_capabilities.transform_keys(&:to_sym).transform_values do |value|
12
+ Array(value).map(&:to_sym).freeze
13
+ end.freeze
14
+ @profile_capabilities = Array(profile_capabilities).map(&:to_sym).uniq.freeze
15
+ @violations = Array(violations).freeze
16
+ @undeclared_nodes = Array(undeclared_nodes).map(&:to_sym).freeze
17
+ freeze
18
+ end
19
+
20
+ def valid?
21
+ violations.empty?
22
+ end
23
+
24
+ def invalid?
25
+ !valid?
26
+ end
27
+
28
+ def summary
29
+ return "valid" if valid?
30
+
31
+ "invalid - #{violations.length} capability violation(s)"
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ valid: valid?,
37
+ required_capabilities: required_capabilities,
38
+ profile_capabilities: profile_capabilities,
39
+ undeclared_nodes: undeclared_nodes,
40
+ violations: violations.map(&:to_h)
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Capabilities
7
+ class Violation
8
+ attr_reader :kind, :node_name, :capabilities, :message
9
+
10
+ def initialize(kind:, node_name:, capabilities:, message:)
11
+ @kind = kind.to_sym
12
+ @node_name = node_name.to_sym
13
+ @capabilities = Array(capabilities).map(&:to_sym).freeze
14
+ @message = message.to_s
15
+ freeze
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ kind: kind,
21
+ node_name: node_name,
22
+ capabilities: capabilities,
23
+ message: message
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "capabilities/declaration"
4
+ require_relative "capabilities/violation"
5
+ require_relative "capabilities/report"
6
+ require_relative "capabilities/policy"
7
+ require_relative "capabilities/error"
8
+
9
+ module Igniter
10
+ module Extensions
11
+ module Contracts
12
+ module CapabilitiesPack
13
+ module_function
14
+
15
+ def manifest
16
+ Igniter::Contracts::PackManifest.new(
17
+ name: :extensions_capabilities,
18
+ metadata: { category: :validation }
19
+ )
20
+ end
21
+
22
+ def install_into(kernel)
23
+ kernel
24
+ end
25
+
26
+ def declare(*capabilities, callable: nil, &block)
27
+ target = callable || block
28
+ raise ArgumentError, "capability declaration requires a callable or block" unless target
29
+
30
+ Capabilities::Declaration.new(callable: target, capabilities: capabilities)
31
+ end
32
+
33
+ def pure(callable: nil, &block)
34
+ declare(:pure, callable: callable, &block)
35
+ end
36
+
37
+ def policy(denied: [], required: [], on_undeclared: :ignore)
38
+ Capabilities::Policy.new(
39
+ denied: denied,
40
+ required: required,
41
+ on_undeclared: on_undeclared
42
+ )
43
+ end
44
+
45
+ def required_capabilities(compiled_graph)
46
+ compiled_graph.operations.reject(&:output?).each_with_object({}) do |operation, memo|
47
+ capabilities = capabilities_for_operation(operation)
48
+ memo[operation.name] = capabilities unless capabilities.empty?
49
+ end
50
+ end
51
+
52
+ def capabilities_for(compiled_graph, node_name)
53
+ operation = compiled_graph.operations.find { |entry| entry.name == node_name.to_sym }
54
+ return [] unless operation
55
+
56
+ capabilities_for_operation(operation)
57
+ end
58
+
59
+ def profile_capabilities(profile)
60
+ profile.pack_manifests
61
+ .flat_map do |manifest_entry|
62
+ manifest_entry.provides_capabilities.empty? ? Array(manifest_entry.metadata[:capabilities]) : manifest_entry.provides_capabilities
63
+ end
64
+ .map(&:to_sym)
65
+ .uniq
66
+ end
67
+
68
+ def report(compiled_graph, profile: nil, policy: nil)
69
+ requirements = required_capabilities(compiled_graph)
70
+ undeclared_nodes = compiled_graph.operations.reject(&:output?).map(&:name) - requirements.keys
71
+ violations = violations_for(requirements, policy: policy, undeclared_nodes: undeclared_nodes)
72
+ maybe_warn_about_undeclared(policy, undeclared_nodes)
73
+
74
+ Capabilities::Report.new(
75
+ required_capabilities: requirements,
76
+ profile_capabilities: profile ? profile_capabilities(profile) : [],
77
+ violations: violations,
78
+ undeclared_nodes: undeclared_nodes
79
+ )
80
+ end
81
+
82
+ def check!(compiled_graph, policy:, profile: nil)
83
+ result = report(compiled_graph, profile: profile, policy: policy)
84
+ raise Capabilities::CapabilityViolationError.new(nil, report: result) if result.invalid?
85
+
86
+ result
87
+ end
88
+
89
+ def capabilities_for_operation(operation)
90
+ capabilities = Array(operation.attributes[:capabilities]).map(&:to_sym)
91
+ callable = operation.attributes[:callable]
92
+ capabilities.concat(Array(callable.declared_capabilities).map(&:to_sym)) if callable.respond_to?(:declared_capabilities)
93
+ capabilities.uniq
94
+ end
95
+
96
+ def violations_for(requirements, policy:, undeclared_nodes:)
97
+ return [] unless policy
98
+
99
+ violations = []
100
+
101
+ requirements.each do |node_name, capabilities|
102
+ denied = capabilities & policy.denied
103
+ if denied.any?
104
+ violations << Capabilities::Violation.new(
105
+ kind: :denied_capability,
106
+ node_name: node_name,
107
+ capabilities: denied,
108
+ message: "node #{node_name} uses denied capabilities: #{denied.join(", ")}"
109
+ )
110
+ end
111
+
112
+ missing = policy.required - capabilities
113
+ next unless missing.any?
114
+
115
+ violations << Capabilities::Violation.new(
116
+ kind: :missing_required_capability,
117
+ node_name: node_name,
118
+ capabilities: missing,
119
+ message: "node #{node_name} is missing required capabilities: #{missing.join(", ")}"
120
+ )
121
+ end
122
+
123
+ if policy.on_undeclared == :error
124
+ undeclared_nodes.each do |node_name|
125
+ violations << Capabilities::Violation.new(
126
+ kind: :undeclared_capabilities,
127
+ node_name: node_name,
128
+ capabilities: [],
129
+ message: "node #{node_name} does not declare capabilities"
130
+ )
131
+ end
132
+ end
133
+
134
+ violations
135
+ end
136
+
137
+ def maybe_warn_about_undeclared(policy, undeclared_nodes)
138
+ return unless policy&.on_undeclared == :warn
139
+ return if undeclared_nodes.empty?
140
+
141
+ Warning.warn("WARNING: undeclared capabilities for nodes: #{undeclared_nodes.join(", ")}\n")
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end