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.
- checksums.yaml +7 -0
- data/README.md +381 -0
- data/lib/igniter/extensions/contracts/aggregate_pack.rb +103 -0
- data/lib/igniter/extensions/contracts/audit/builder.rb +132 -0
- data/lib/igniter/extensions/contracts/audit/event.rb +34 -0
- data/lib/igniter/extensions/contracts/audit/snapshot.rb +44 -0
- data/lib/igniter/extensions/contracts/audit_pack.rb +60 -0
- data/lib/igniter/extensions/contracts/branch_pack.rb +199 -0
- data/lib/igniter/extensions/contracts/capabilities/declaration.rb +31 -0
- data/lib/igniter/extensions/contracts/capabilities/error.rb +35 -0
- data/lib/igniter/extensions/contracts/capabilities/policy.rb +20 -0
- data/lib/igniter/extensions/contracts/capabilities/report.rb +47 -0
- data/lib/igniter/extensions/contracts/capabilities/violation.rb +30 -0
- data/lib/igniter/extensions/contracts/capabilities_pack.rb +146 -0
- data/lib/igniter/extensions/contracts/collection_pack.rb +212 -0
- data/lib/igniter/extensions/contracts/commerce_pack.rb +91 -0
- data/lib/igniter/extensions/contracts/compose_pack.rb +213 -0
- data/lib/igniter/extensions/contracts/content_addressing/cache.rb +59 -0
- data/lib/igniter/extensions/contracts/content_addressing/content_key.rb +63 -0
- data/lib/igniter/extensions/contracts/content_addressing/declaration.rb +47 -0
- data/lib/igniter/extensions/contracts/content_addressing_pack.rb +90 -0
- data/lib/igniter/extensions/contracts/creator/profile.rb +196 -0
- data/lib/igniter/extensions/contracts/creator/report.rb +85 -0
- data/lib/igniter/extensions/contracts/creator/scaffold.rb +461 -0
- data/lib/igniter/extensions/contracts/creator/scope.rb +79 -0
- data/lib/igniter/extensions/contracts/creator/wizard.rb +269 -0
- data/lib/igniter/extensions/contracts/creator/workflow.rb +189 -0
- data/lib/igniter/extensions/contracts/creator/workflow_step.rb +51 -0
- data/lib/igniter/extensions/contracts/creator/write_result.rb +48 -0
- data/lib/igniter/extensions/contracts/creator/write_step.rb +63 -0
- data/lib/igniter/extensions/contracts/creator/writer.rb +131 -0
- data/lib/igniter/extensions/contracts/creator_pack.rb +128 -0
- data/lib/igniter/extensions/contracts/dataflow/aggregate_operators.rb +119 -0
- data/lib/igniter/extensions/contracts/dataflow/aggregate_state.rb +60 -0
- data/lib/igniter/extensions/contracts/dataflow/builder.rb +66 -0
- data/lib/igniter/extensions/contracts/dataflow/collection_result.rb +70 -0
- data/lib/igniter/extensions/contracts/dataflow/diff.rb +37 -0
- data/lib/igniter/extensions/contracts/dataflow/item_result.rb +44 -0
- data/lib/igniter/extensions/contracts/dataflow/result.rb +58 -0
- data/lib/igniter/extensions/contracts/dataflow/session.rb +173 -0
- data/lib/igniter/extensions/contracts/dataflow/window_filter.rb +49 -0
- data/lib/igniter/extensions/contracts/dataflow_pack.rb +66 -0
- data/lib/igniter/extensions/contracts/debug/pack_audit.rb +181 -0
- data/lib/igniter/extensions/contracts/debug/pack_snapshot.rb +46 -0
- data/lib/igniter/extensions/contracts/debug/profile_snapshot.rb +50 -0
- data/lib/igniter/extensions/contracts/debug/report.rb +50 -0
- data/lib/igniter/extensions/contracts/debug_pack.rb +115 -0
- data/lib/igniter/extensions/contracts/differential/divergence.rb +37 -0
- data/lib/igniter/extensions/contracts/differential/formatter.rb +85 -0
- data/lib/igniter/extensions/contracts/differential/report.rb +83 -0
- data/lib/igniter/extensions/contracts/differential/runner.rb +136 -0
- data/lib/igniter/extensions/contracts/differential_pack.rb +61 -0
- data/lib/igniter/extensions/contracts/execution_report_pack.rb +38 -0
- data/lib/igniter/extensions/contracts/incremental/formatter.rb +60 -0
- data/lib/igniter/extensions/contracts/incremental/node_state.rb +30 -0
- data/lib/igniter/extensions/contracts/incremental/result.rb +65 -0
- data/lib/igniter/extensions/contracts/incremental/session.rb +146 -0
- data/lib/igniter/extensions/contracts/incremental_pack.rb +40 -0
- data/lib/igniter/extensions/contracts/invariants/builder.rb +27 -0
- data/lib/igniter/extensions/contracts/invariants/cases_report.rb +47 -0
- data/lib/igniter/extensions/contracts/invariants/error.rb +34 -0
- data/lib/igniter/extensions/contracts/invariants/invariant.rb +30 -0
- data/lib/igniter/extensions/contracts/invariants/report.rb +45 -0
- data/lib/igniter/extensions/contracts/invariants/suite.rb +36 -0
- data/lib/igniter/extensions/contracts/invariants/violation.rb +39 -0
- data/lib/igniter/extensions/contracts/invariants_pack.rb +88 -0
- data/lib/igniter/extensions/contracts/journal_pack.rb +55 -0
- data/lib/igniter/extensions/contracts/language/formula_pack.rb +185 -0
- data/lib/igniter/extensions/contracts/language/piecewise_pack.rb +166 -0
- data/lib/igniter/extensions/contracts/language/scale_pack.rb +147 -0
- data/lib/igniter/extensions/contracts/lookup_pack.rb +50 -0
- data/lib/igniter/extensions/contracts/mcp/creator_session.rb +105 -0
- data/lib/igniter/extensions/contracts/mcp/tool_argument.rb +35 -0
- data/lib/igniter/extensions/contracts/mcp/tool_definition.rb +33 -0
- data/lib/igniter/extensions/contracts/mcp/tool_result.rb +28 -0
- data/lib/igniter/extensions/contracts/mcp_pack.rb +335 -0
- data/lib/igniter/extensions/contracts/provenance/builder.rb +80 -0
- data/lib/igniter/extensions/contracts/provenance/lineage.rb +59 -0
- data/lib/igniter/extensions/contracts/provenance/node_trace.rb +53 -0
- data/lib/igniter/extensions/contracts/provenance/text_formatter.rb +62 -0
- data/lib/igniter/extensions/contracts/provenance_pack.rb +52 -0
- data/lib/igniter/extensions/contracts/reactive/builder.rb +43 -0
- data/lib/igniter/extensions/contracts/reactive/dispatch_result.rb +59 -0
- data/lib/igniter/extensions/contracts/reactive/engine.rb +79 -0
- data/lib/igniter/extensions/contracts/reactive/event.rb +36 -0
- data/lib/igniter/extensions/contracts/reactive/matcher.rb +20 -0
- data/lib/igniter/extensions/contracts/reactive/plan.rb +58 -0
- data/lib/igniter/extensions/contracts/reactive/subscription.rb +29 -0
- data/lib/igniter/extensions/contracts/reactive_pack.rb +169 -0
- data/lib/igniter/extensions/contracts/saga/compensation.rb +25 -0
- data/lib/igniter/extensions/contracts/saga/compensation_record.rb +28 -0
- data/lib/igniter/extensions/contracts/saga/compensation_set.rb +47 -0
- data/lib/igniter/extensions/contracts/saga/formatter.rb +39 -0
- data/lib/igniter/extensions/contracts/saga/result.rb +56 -0
- data/lib/igniter/extensions/contracts/saga/runner.rb +124 -0
- data/lib/igniter/extensions/contracts/saga_pack.rb +56 -0
- data/lib/igniter/extensions/contracts.rb +445 -0
- data/lib/igniter/extensions.rb +6 -0
- data/lib/igniter-extensions.rb +3 -0
- metadata +152 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Incremental
|
|
7
|
+
class Result
|
|
8
|
+
attr_reader :execution_result, :changed_nodes, :skipped_nodes, :backdated_nodes,
|
|
9
|
+
:changed_outputs, :recomputed_count
|
|
10
|
+
|
|
11
|
+
def initialize(execution_result:, changed_nodes:, skipped_nodes:, backdated_nodes:, changed_outputs:,
|
|
12
|
+
recomputed_count:)
|
|
13
|
+
@execution_result = execution_result
|
|
14
|
+
@changed_nodes = changed_nodes.freeze
|
|
15
|
+
@skipped_nodes = skipped_nodes.freeze
|
|
16
|
+
@backdated_nodes = backdated_nodes.freeze
|
|
17
|
+
@changed_outputs = changed_outputs.freeze
|
|
18
|
+
@recomputed_count = recomputed_count
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def outputs_changed?
|
|
23
|
+
changed_outputs.any?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fully_memoized?
|
|
27
|
+
recomputed_count.zero?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def summary
|
|
31
|
+
parts = []
|
|
32
|
+
parts << "#{changed_nodes.length} changed" if changed_nodes.any?
|
|
33
|
+
parts << "#{skipped_nodes.length} skipped" if skipped_nodes.any?
|
|
34
|
+
parts << "#{backdated_nodes.length} backdated" if backdated_nodes.any?
|
|
35
|
+
parts << "#{recomputed_count} recomputed"
|
|
36
|
+
parts.join(", ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def explain
|
|
40
|
+
Formatter.format(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
alias to_s explain
|
|
44
|
+
|
|
45
|
+
def output(name)
|
|
46
|
+
execution_result.output(name)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
{
|
|
51
|
+
changed_nodes: changed_nodes,
|
|
52
|
+
skipped_nodes: skipped_nodes,
|
|
53
|
+
backdated_nodes: backdated_nodes,
|
|
54
|
+
changed_outputs: changed_outputs,
|
|
55
|
+
recomputed_count: recomputed_count,
|
|
56
|
+
outputs_changed: outputs_changed?,
|
|
57
|
+
fully_memoized: fully_memoized?,
|
|
58
|
+
execution_result: execution_result.to_h
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Incremental
|
|
7
|
+
class Session
|
|
8
|
+
attr_reader :compiled_graph, :profile
|
|
9
|
+
|
|
10
|
+
def initialize(compiled_graph:, profile:)
|
|
11
|
+
@compiled_graph = compiled_graph
|
|
12
|
+
@profile = profile
|
|
13
|
+
@node_states = {}
|
|
14
|
+
@last_result = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run(inputs:)
|
|
18
|
+
normalized_inputs = Igniter::Contracts::NamedValues.new(inputs)
|
|
19
|
+
current_values = Igniter::Contracts::MutableNamedValues.new
|
|
20
|
+
current_outputs = Igniter::Contracts::MutableNamedValues.new
|
|
21
|
+
current_states = {}
|
|
22
|
+
|
|
23
|
+
changed_nodes = []
|
|
24
|
+
skipped_nodes = []
|
|
25
|
+
backdated_nodes = []
|
|
26
|
+
recomputed_count = 0
|
|
27
|
+
|
|
28
|
+
compiled_graph.operations.each do |operation|
|
|
29
|
+
if operation.output?
|
|
30
|
+
current_outputs.write(operation.name, current_values.fetch(operation.name))
|
|
31
|
+
next
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if operation.kind == :input
|
|
35
|
+
state = resolve_input_state(operation, normalized_inputs)
|
|
36
|
+
current_states[operation.name] = state
|
|
37
|
+
current_values.write(operation.name, state.value)
|
|
38
|
+
changed_nodes << operation.name if changed_value?(operation.name, state.value)
|
|
39
|
+
next
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
dependency_versions = dependency_names_for(operation).each_with_object({}) do |dependency_name, memo|
|
|
43
|
+
if current_states.key?(dependency_name)
|
|
44
|
+
memo[dependency_name] =
|
|
45
|
+
current_states.fetch(dependency_name).value_version
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
previous = @node_states[operation.name]
|
|
50
|
+
|
|
51
|
+
if previous && previous.dep_snapshot == dependency_versions
|
|
52
|
+
current_states[operation.name] = previous
|
|
53
|
+
current_values.write(operation.name, previous.value)
|
|
54
|
+
skipped_nodes << operation.name
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
handler = profile.runtime_handler(operation.kind)
|
|
59
|
+
value = handler.call(
|
|
60
|
+
operation: operation,
|
|
61
|
+
state: current_values,
|
|
62
|
+
outputs: current_outputs,
|
|
63
|
+
inputs: normalized_inputs,
|
|
64
|
+
profile: profile
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
value_version =
|
|
68
|
+
if previous && previous.value == value
|
|
69
|
+
backdated_nodes << operation.name
|
|
70
|
+
previous.value_version
|
|
71
|
+
else
|
|
72
|
+
changed_nodes << operation.name if changed_value?(operation.name, value)
|
|
73
|
+
previous ? previous.value_version + 1 : 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
current_states[operation.name] = NodeState.new(
|
|
77
|
+
name: operation.name,
|
|
78
|
+
value: value,
|
|
79
|
+
value_version: value_version,
|
|
80
|
+
dep_snapshot: dependency_versions
|
|
81
|
+
)
|
|
82
|
+
current_values.write(operation.name, value)
|
|
83
|
+
recomputed_count += 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
execution_result = Igniter::Contracts::ExecutionResult.new(
|
|
87
|
+
state: current_values.snapshot,
|
|
88
|
+
outputs: current_outputs.snapshot,
|
|
89
|
+
profile_fingerprint: profile.fingerprint,
|
|
90
|
+
compiled_graph: compiled_graph
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
result = Result.new(
|
|
94
|
+
execution_result: execution_result,
|
|
95
|
+
changed_nodes: changed_nodes.uniq,
|
|
96
|
+
skipped_nodes: skipped_nodes.uniq,
|
|
97
|
+
backdated_nodes: backdated_nodes.uniq,
|
|
98
|
+
changed_outputs: changed_outputs_for(execution_result),
|
|
99
|
+
recomputed_count: recomputed_count
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@node_states = current_states.freeze
|
|
103
|
+
@last_result = result
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def resolve_input_state(operation, normalized_inputs)
|
|
109
|
+
value = normalized_inputs.fetch(operation.name)
|
|
110
|
+
previous = @node_states[operation.name]
|
|
111
|
+
value_version = previous && previous.value == value ? previous.value_version : (previous&.value_version || 0) + 1
|
|
112
|
+
|
|
113
|
+
NodeState.new(
|
|
114
|
+
name: operation.name,
|
|
115
|
+
value: value,
|
|
116
|
+
value_version: value_version
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def dependency_names_for(operation)
|
|
121
|
+
names = []
|
|
122
|
+
names.concat(Array(operation.attributes[:depends_on])) if operation.attribute?(:depends_on)
|
|
123
|
+
names << operation.attributes[:from] if operation.attribute?(:from)
|
|
124
|
+
names.map(&:to_sym).uniq
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def changed_outputs_for(execution_result)
|
|
128
|
+
previous_outputs = @last_result&.execution_result&.outputs
|
|
129
|
+
execution_result.outputs.keys.each_with_object({}) do |name, memo|
|
|
130
|
+
previous = previous_outputs&.[](name)
|
|
131
|
+
current = execution_result.outputs[name]
|
|
132
|
+
next if previous == current
|
|
133
|
+
|
|
134
|
+
memo[name] = { from: previous, to: current }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def changed_value?(name, value)
|
|
139
|
+
previous = @node_states[name]
|
|
140
|
+
previous.nil? || previous.value != value
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "incremental/node_state"
|
|
4
|
+
require_relative "incremental/formatter"
|
|
5
|
+
require_relative "incremental/result"
|
|
6
|
+
require_relative "incremental/session"
|
|
7
|
+
|
|
8
|
+
module Igniter
|
|
9
|
+
module Extensions
|
|
10
|
+
module Contracts
|
|
11
|
+
module IncrementalPack
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def manifest
|
|
15
|
+
Igniter::Contracts::PackManifest.new(
|
|
16
|
+
name: :extensions_incremental,
|
|
17
|
+
metadata: { category: :orchestration }
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def install_into(kernel)
|
|
22
|
+
kernel
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def session(environment, compiled_graph: nil, &block)
|
|
26
|
+
graph = compiled_graph || environment.compile(&block)
|
|
27
|
+
ensure_installed!(environment.profile)
|
|
28
|
+
Incremental::Session.new(compiled_graph: graph, profile: environment.profile)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ensure_installed!(profile)
|
|
32
|
+
return if profile.pack_names.include?(:extensions_incremental)
|
|
33
|
+
|
|
34
|
+
raise Igniter::Contracts::Error,
|
|
35
|
+
"IncrementalPack is not installed in profile #{profile.fingerprint}; add Igniter::Extensions::Contracts::IncrementalPack"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Invariants
|
|
7
|
+
class Builder
|
|
8
|
+
attr_reader :suite
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@suite = Suite.new(invariants: [])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def invariant(name, &block)
|
|
15
|
+
@suite = suite.invariant(name, &block)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.build(&block)
|
|
19
|
+
builder = new
|
|
20
|
+
builder.instance_eval(&block) if block
|
|
21
|
+
builder.suite
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Invariants
|
|
7
|
+
class CasesReport
|
|
8
|
+
attr_reader :reports
|
|
9
|
+
|
|
10
|
+
def initialize(reports:)
|
|
11
|
+
@reports = reports.freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def valid?
|
|
16
|
+
reports.all?(&:valid?)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def invalid_cases
|
|
20
|
+
reports.each_with_index.filter_map do |report, index|
|
|
21
|
+
next if report.valid?
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
index: index,
|
|
25
|
+
report: report.to_h
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def summary
|
|
31
|
+
return "all cases valid" if valid?
|
|
32
|
+
|
|
33
|
+
"#{invalid_cases.length} invalid case(s)"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_h
|
|
37
|
+
{
|
|
38
|
+
valid: valid?,
|
|
39
|
+
case_count: reports.length,
|
|
40
|
+
invalid_cases: invalid_cases
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Invariants
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
class InvariantError < Error
|
|
10
|
+
attr_reader :violations
|
|
11
|
+
|
|
12
|
+
def initialize(message = nil, violations: [])
|
|
13
|
+
@violations = Array(violations).freeze
|
|
14
|
+
super(message || default_message)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
message: message,
|
|
20
|
+
violations: violations.map(&:to_h)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def default_message
|
|
27
|
+
names = violations.map { |violation| ":#{violation.name}" }.join(", ")
|
|
28
|
+
"#{violations.length} invariant(s) violated: #{names}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Invariants
|
|
7
|
+
class Invariant
|
|
8
|
+
attr_reader :name, :block
|
|
9
|
+
|
|
10
|
+
def initialize(name, &block)
|
|
11
|
+
raise ArgumentError, "invariant #{name.inspect} requires a block" unless block
|
|
12
|
+
|
|
13
|
+
@name = name.to_sym
|
|
14
|
+
@block = block
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def check(outputs)
|
|
19
|
+
passed = block.call(**outputs)
|
|
20
|
+
return nil if passed
|
|
21
|
+
|
|
22
|
+
Violation.new(name: name, outputs: outputs)
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
Violation.new(name: name, outputs: outputs, error: e)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Invariants
|
|
7
|
+
class Report
|
|
8
|
+
attr_reader :suite, :outputs, :violations, :execution_result
|
|
9
|
+
|
|
10
|
+
def initialize(suite:, outputs:, violations:, execution_result: nil)
|
|
11
|
+
@suite = suite
|
|
12
|
+
@outputs = outputs.transform_keys(&:to_sym).freeze
|
|
13
|
+
@violations = Array(violations).freeze
|
|
14
|
+
@execution_result = execution_result
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid?
|
|
19
|
+
violations.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def invalid?
|
|
23
|
+
!valid?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def summary
|
|
27
|
+
return "valid" if valid?
|
|
28
|
+
|
|
29
|
+
"invalid - #{violations.length} invariant(s) violated: #{violations.map(&:name).join(", ")}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{
|
|
34
|
+
valid: valid?,
|
|
35
|
+
outputs: outputs,
|
|
36
|
+
invariants: suite.names,
|
|
37
|
+
violations: violations.map(&:to_h),
|
|
38
|
+
execution_result: execution_result&.to_h
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Invariants
|
|
7
|
+
class Suite
|
|
8
|
+
attr_reader :invariants
|
|
9
|
+
|
|
10
|
+
def initialize(invariants:)
|
|
11
|
+
@invariants = invariants.freeze
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def invariant(name, &block)
|
|
16
|
+
self.class.new(invariants: invariants + [Invariant.new(name, &block)])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def empty?
|
|
20
|
+
invariants.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def names
|
|
24
|
+
invariants.map(&:name)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{
|
|
29
|
+
invariants: names
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Invariants
|
|
7
|
+
class Violation
|
|
8
|
+
attr_reader :name, :outputs, :error
|
|
9
|
+
|
|
10
|
+
def initialize(name:, outputs:, error: nil)
|
|
11
|
+
@name = name.to_sym
|
|
12
|
+
@outputs = outputs.transform_keys(&:to_sym).freeze
|
|
13
|
+
@error = error
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def passed?
|
|
18
|
+
false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def failed?
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_h
|
|
26
|
+
{
|
|
27
|
+
name: name,
|
|
28
|
+
outputs: outputs,
|
|
29
|
+
error: error && {
|
|
30
|
+
type: error.class.name,
|
|
31
|
+
message: error.message
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "invariants/error"
|
|
4
|
+
require_relative "invariants/invariant"
|
|
5
|
+
require_relative "invariants/violation"
|
|
6
|
+
require_relative "invariants/suite"
|
|
7
|
+
require_relative "invariants/builder"
|
|
8
|
+
require_relative "invariants/report"
|
|
9
|
+
require_relative "invariants/cases_report"
|
|
10
|
+
|
|
11
|
+
module Igniter
|
|
12
|
+
module Extensions
|
|
13
|
+
module Contracts
|
|
14
|
+
module InvariantsPack
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def manifest
|
|
18
|
+
Igniter::Contracts::PackManifest.new(
|
|
19
|
+
name: :extensions_invariants,
|
|
20
|
+
metadata: { category: :validation }
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def install_into(kernel)
|
|
25
|
+
kernel
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build(&block)
|
|
29
|
+
Invariants::Builder.build(&block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check(target, invariants:)
|
|
33
|
+
execution_result = unwrap_execution_result(target)
|
|
34
|
+
outputs = execution_result.outputs.to_h
|
|
35
|
+
violations = invariants.invariants.filter_map { |invariant| invariant.check(outputs) }
|
|
36
|
+
|
|
37
|
+
Invariants::Report.new(
|
|
38
|
+
suite: invariants,
|
|
39
|
+
outputs: outputs,
|
|
40
|
+
violations: violations,
|
|
41
|
+
execution_result: execution_result
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate!(target, invariants:)
|
|
46
|
+
report = check(target, invariants: invariants)
|
|
47
|
+
raise Invariants::InvariantError.new(nil, violations: report.violations) if report.invalid?
|
|
48
|
+
|
|
49
|
+
report
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def run(environment, inputs:, invariants:, compiled_graph: nil, &block)
|
|
53
|
+
graph =
|
|
54
|
+
if block
|
|
55
|
+
environment.compile(&block)
|
|
56
|
+
else
|
|
57
|
+
compiled_graph || raise(ArgumentError, "invariant run requires a block or compiled_graph")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result = environment.execute(graph, inputs: inputs)
|
|
61
|
+
check(result, invariants: invariants)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def verify_cases(environment, cases:, invariants:, compiled_graph: nil, &block)
|
|
65
|
+
graph =
|
|
66
|
+
if block
|
|
67
|
+
environment.compile(&block)
|
|
68
|
+
else
|
|
69
|
+
compiled_graph || raise(ArgumentError, "verify_cases requires a block or compiled_graph")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
reports = Array(cases).map do |inputs|
|
|
73
|
+
result = environment.execute(graph, inputs: inputs)
|
|
74
|
+
check(result, invariants: invariants)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
Invariants::CasesReport.new(reports: reports)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def unwrap_execution_result(target)
|
|
81
|
+
return target.execution_result if target.respond_to?(:execution_result)
|
|
82
|
+
|
|
83
|
+
target
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module JournalPack
|
|
7
|
+
class << self
|
|
8
|
+
def manifest
|
|
9
|
+
Igniter::Contracts::PackManifest.new(
|
|
10
|
+
name: :extensions_journal,
|
|
11
|
+
registry_contracts: [
|
|
12
|
+
Igniter::Contracts::PackManifest.effect(:journal),
|
|
13
|
+
Igniter::Contracts::PackManifest.executor(:journaled_inline)
|
|
14
|
+
]
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def install_into(kernel)
|
|
19
|
+
kernel.effects.register(:journal, method(:apply_journal_effect))
|
|
20
|
+
kernel.executors.register(:journaled_inline, method(:execute_journaled_inline))
|
|
21
|
+
kernel
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def journal
|
|
25
|
+
@journal ||= {
|
|
26
|
+
effects: [],
|
|
27
|
+
executions: [],
|
|
28
|
+
results: []
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset_journal!
|
|
33
|
+
journal.each_value(&:clear)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def apply_journal_effect(invocation:)
|
|
37
|
+
journal[:effects] << invocation.to_h
|
|
38
|
+
invocation.payload
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def execute_journaled_inline(invocation:)
|
|
42
|
+
journal[:executions] << invocation.to_h
|
|
43
|
+
result = invocation.runtime.execute(
|
|
44
|
+
invocation.compiled_graph,
|
|
45
|
+
inputs: invocation.inputs,
|
|
46
|
+
profile: invocation.profile
|
|
47
|
+
)
|
|
48
|
+
journal[:results] << result.to_h
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|