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,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
class Result
|
|
8
|
+
attr_reader :processed, :diff, :aggregates, :inputs
|
|
9
|
+
|
|
10
|
+
def initialize(processed:, aggregates:, inputs:)
|
|
11
|
+
@processed = processed
|
|
12
|
+
@diff = processed.diff
|
|
13
|
+
@aggregates = aggregates.transform_keys(&:to_sym).freeze
|
|
14
|
+
@inputs = inputs
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
alias collection processed
|
|
19
|
+
|
|
20
|
+
def aggregate(name)
|
|
21
|
+
aggregates.fetch(name.to_sym)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def output(name)
|
|
25
|
+
aggregate(name)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def summary
|
|
29
|
+
{
|
|
30
|
+
diff: diff.to_h,
|
|
31
|
+
aggregates: aggregates
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
processed: processed.to_h,
|
|
38
|
+
aggregates: aggregates,
|
|
39
|
+
inputs: inputs.to_h
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def method_missing(name, *args)
|
|
44
|
+
return super unless args.empty?
|
|
45
|
+
return processed if name.to_sym == :processed
|
|
46
|
+
return aggregate(name) if aggregates.key?(name.to_sym)
|
|
47
|
+
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def respond_to_missing?(name, include_private = false)
|
|
52
|
+
name.to_sym == :processed || aggregates.key?(name.to_sym) || super
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
class Session
|
|
8
|
+
attr_reader :environment, :compiled_graph, :source, :key_name, :window, :context, :last_result
|
|
9
|
+
|
|
10
|
+
def initialize(environment:, compiled_graph:, source:, key:, window: nil, context: [],
|
|
11
|
+
aggregate_operators: {})
|
|
12
|
+
@environment = environment
|
|
13
|
+
@compiled_graph = compiled_graph
|
|
14
|
+
@source = source.to_sym
|
|
15
|
+
@key_name = key.to_sym
|
|
16
|
+
@window = window
|
|
17
|
+
@context = Array(context).map(&:to_sym).freeze
|
|
18
|
+
@aggregate_states = aggregate_operators.transform_values { |operator| AggregateState.new(operator) }
|
|
19
|
+
@item_sessions = {}
|
|
20
|
+
@snapshots = {}
|
|
21
|
+
@cached_items = {}
|
|
22
|
+
@last_inputs = nil
|
|
23
|
+
@last_result = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run(inputs:)
|
|
27
|
+
normalized_inputs = Igniter::Contracts::NamedValues.new(inputs)
|
|
28
|
+
items = filtered_items(normalized_inputs.fetch(source))
|
|
29
|
+
diff = compute_diff(items)
|
|
30
|
+
processed_items = build_processed_items(items, normalized_inputs, diff)
|
|
31
|
+
delete_removed_sessions(diff.removed)
|
|
32
|
+
|
|
33
|
+
collection_result = CollectionResult.new(items: processed_items, diff: diff)
|
|
34
|
+
update_aggregate_states(collection_result)
|
|
35
|
+
|
|
36
|
+
@snapshots = snapshot_items(items)
|
|
37
|
+
@cached_items = processed_items.dup
|
|
38
|
+
@last_inputs = normalized_inputs
|
|
39
|
+
@last_result = Result.new(
|
|
40
|
+
processed: collection_result,
|
|
41
|
+
aggregates: @aggregate_states.transform_values(&:value),
|
|
42
|
+
inputs: normalized_inputs
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def feed_diff(add: [], remove: [], update: [], inputs: {})
|
|
47
|
+
current_inputs = @last_inputs.to_h
|
|
48
|
+
current_items = Array(current_inputs.fetch(source, []))
|
|
49
|
+
merged_items = apply_diff(current_items, add: add, remove: remove, update: update)
|
|
50
|
+
|
|
51
|
+
run(inputs: current_inputs.merge(inputs).merge(source => merged_items))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def collection_diff
|
|
55
|
+
@last_result&.diff
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def filtered_items(items)
|
|
61
|
+
normalized_items = Array(items).map { |item| normalize_item(item) }
|
|
62
|
+
WindowFilter.new(window).apply(normalized_items)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def normalize_item(item)
|
|
66
|
+
raise TypeError, "dataflow items must be Hash-like" unless item.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
item.transform_keys(&:to_sym)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_processed_items(items, normalized_inputs, diff)
|
|
72
|
+
items.each_with_object({}) do |item, memo|
|
|
73
|
+
key = extract_key(item)
|
|
74
|
+
memo[key] =
|
|
75
|
+
if diff.unchanged.include?(key)
|
|
76
|
+
@cached_items.fetch(key)
|
|
77
|
+
else
|
|
78
|
+
resolve_item(item, normalized_inputs)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def resolve_item(item, normalized_inputs)
|
|
84
|
+
key = extract_key(item)
|
|
85
|
+
session = @item_sessions[key] ||= Igniter::Extensions::Contracts::IncrementalPack.session(
|
|
86
|
+
environment,
|
|
87
|
+
compiled_graph: compiled_graph
|
|
88
|
+
)
|
|
89
|
+
item_inputs = context.each_with_object({}) do |name, memo|
|
|
90
|
+
memo[name] = normalized_inputs.fetch(name)
|
|
91
|
+
end.merge(item)
|
|
92
|
+
incremental_result = session.run(inputs: item_inputs)
|
|
93
|
+
|
|
94
|
+
ItemResult.new(
|
|
95
|
+
key: key,
|
|
96
|
+
inputs: item_inputs,
|
|
97
|
+
execution_result: incremental_result.execution_result,
|
|
98
|
+
incremental_result: incremental_result
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def compute_diff(items)
|
|
103
|
+
current_keys = items.to_h { |item| [extract_key(item), item] }
|
|
104
|
+
added = []
|
|
105
|
+
changed = []
|
|
106
|
+
unchanged = []
|
|
107
|
+
|
|
108
|
+
items.each do |item|
|
|
109
|
+
key = extract_key(item)
|
|
110
|
+
fingerprint = fingerprint(item)
|
|
111
|
+
|
|
112
|
+
if !@snapshots.key?(key)
|
|
113
|
+
added << key
|
|
114
|
+
elsif @snapshots.fetch(key) != fingerprint
|
|
115
|
+
changed << key
|
|
116
|
+
else
|
|
117
|
+
unchanged << key
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
removed = @snapshots.keys.reject { |key| current_keys.key?(key) }
|
|
122
|
+
|
|
123
|
+
Diff.new(added: added, removed: removed, changed: changed, unchanged: unchanged)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def snapshot_items(items)
|
|
127
|
+
items.to_h { |item| [extract_key(item), fingerprint(item)] }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def fingerprint(item)
|
|
131
|
+
item.sort_by { |name, _| name.to_s }
|
|
132
|
+
.map { |name, value| "#{name}:#{value.inspect}" }
|
|
133
|
+
.hash
|
|
134
|
+
.to_s
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def extract_key(item)
|
|
138
|
+
item.fetch(key_name)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def delete_removed_sessions(removed_keys)
|
|
142
|
+
removed_keys.each do |key|
|
|
143
|
+
@item_sessions.delete(key)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def update_aggregate_states(collection_result)
|
|
148
|
+
@aggregate_states.each_value do |state|
|
|
149
|
+
state.apply_diff!(collection_result.diff, collection_result)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def apply_diff(items, add:, remove:, update:)
|
|
154
|
+
remove_keys = Array(remove).map do |entry|
|
|
155
|
+
entry.is_a?(Hash) ? normalize_item(entry).fetch(key_name) : entry
|
|
156
|
+
end
|
|
157
|
+
result = items.map { |item| normalize_item(item) }
|
|
158
|
+
.reject { |item| remove_keys.include?(item.fetch(key_name)) }
|
|
159
|
+
|
|
160
|
+
Array(update).each do |entry|
|
|
161
|
+
updated = normalize_item(entry)
|
|
162
|
+
key = updated.fetch(key_name)
|
|
163
|
+
index = result.index { |item| item.fetch(key_name) == key }
|
|
164
|
+
index ? result[index] = updated : result << updated
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
result.concat(Array(add).map { |entry| normalize_item(entry) })
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Dataflow
|
|
7
|
+
class WindowFilter
|
|
8
|
+
def initialize(options)
|
|
9
|
+
@options = options
|
|
10
|
+
validate!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def apply(items)
|
|
14
|
+
return items unless @options
|
|
15
|
+
|
|
16
|
+
if @options.key?(:last)
|
|
17
|
+
items.last(@options.fetch(:last))
|
|
18
|
+
else
|
|
19
|
+
cutoff = Time.now - @options.fetch(:seconds)
|
|
20
|
+
field = @options.fetch(:field).to_sym
|
|
21
|
+
items.select { |item| item.fetch(field) >= cutoff }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def validate!
|
|
28
|
+
return unless @options
|
|
29
|
+
raise ArgumentError, "window must be a Hash" unless @options.is_a?(Hash)
|
|
30
|
+
|
|
31
|
+
if @options.key?(:last)
|
|
32
|
+
return if @options.fetch(:last).is_a?(Integer) && @options.fetch(:last).positive?
|
|
33
|
+
|
|
34
|
+
raise ArgumentError, "window { last: } must be a positive Integer"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if @options.key?(:seconds)
|
|
38
|
+
raise ArgumentError, "window { seconds: } requires a :field key" unless @options.key?(:field)
|
|
39
|
+
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "window must use :last or :seconds"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "incremental_pack"
|
|
4
|
+
require_relative "dataflow/aggregate_operators"
|
|
5
|
+
require_relative "dataflow/aggregate_state"
|
|
6
|
+
require_relative "dataflow/builder"
|
|
7
|
+
require_relative "dataflow/collection_result"
|
|
8
|
+
require_relative "dataflow/diff"
|
|
9
|
+
require_relative "dataflow/item_result"
|
|
10
|
+
require_relative "dataflow/result"
|
|
11
|
+
require_relative "dataflow/session"
|
|
12
|
+
require_relative "dataflow/window_filter"
|
|
13
|
+
|
|
14
|
+
module Igniter
|
|
15
|
+
module Extensions
|
|
16
|
+
module Contracts
|
|
17
|
+
module DataflowPack
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def manifest
|
|
21
|
+
Igniter::Contracts::PackManifest.new(
|
|
22
|
+
name: :extensions_dataflow,
|
|
23
|
+
metadata: { category: :orchestration }
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def install_into(kernel)
|
|
28
|
+
kernel
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def session(environment, source:, key:, window: nil, context: [], compiled_graph: nil, &block)
|
|
32
|
+
ensure_installed!(environment.profile)
|
|
33
|
+
Igniter::Extensions::Contracts::IncrementalPack.ensure_installed!(environment.profile)
|
|
34
|
+
|
|
35
|
+
item_graph, aggregate_operators =
|
|
36
|
+
if compiled_graph
|
|
37
|
+
raise ArgumentError, "DataflowPack.session accepts either compiled_graph: or a block, not both" if block
|
|
38
|
+
|
|
39
|
+
[compiled_graph, {}]
|
|
40
|
+
else
|
|
41
|
+
builder = Dataflow::Builder.new(source: source, key: key, window: window, context: context)
|
|
42
|
+
builder.instance_eval(&block) if block
|
|
43
|
+
builder.build!(environment)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Dataflow::Session.new(
|
|
47
|
+
environment: environment,
|
|
48
|
+
compiled_graph: item_graph,
|
|
49
|
+
source: source,
|
|
50
|
+
key: key,
|
|
51
|
+
window: window,
|
|
52
|
+
context: context,
|
|
53
|
+
aggregate_operators: aggregate_operators
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ensure_installed!(profile)
|
|
58
|
+
return if profile.pack_names.include?(:extensions_dataflow)
|
|
59
|
+
|
|
60
|
+
raise Igniter::Contracts::Error,
|
|
61
|
+
"DataflowPack is not installed in profile #{profile.fingerprint}; add Igniter::Extensions::Contracts::DataflowPack"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pack_snapshot"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Extensions
|
|
7
|
+
module Contracts
|
|
8
|
+
module Debug
|
|
9
|
+
class PackAudit
|
|
10
|
+
REGISTRIES = {
|
|
11
|
+
node_kinds: ->(kernel) { kernel.nodes.to_h.keys.sort },
|
|
12
|
+
dsl_keywords: ->(kernel) { kernel.dsl_keywords.to_h.keys.sort },
|
|
13
|
+
validators: ->(kernel) { kernel.validators.entries.map(&:key).sort },
|
|
14
|
+
normalizers: ->(kernel) { kernel.normalizers.entries.map(&:key).sort },
|
|
15
|
+
runtime_handlers: ->(kernel) { kernel.runtime_handlers.to_h.keys.sort },
|
|
16
|
+
diagnostics_contributors: ->(kernel) { kernel.diagnostics_contributors.entries.map(&:key).sort },
|
|
17
|
+
effects: ->(kernel) { kernel.effects.to_h.keys.sort },
|
|
18
|
+
executors: ->(kernel) { kernel.executors.to_h.keys.sort }
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :pack_snapshot,
|
|
22
|
+
:installed_in_target_profile,
|
|
23
|
+
:target_profile_fingerprint,
|
|
24
|
+
:draft_registered_keys,
|
|
25
|
+
:missing_node_definitions,
|
|
26
|
+
:missing_dsl_keywords,
|
|
27
|
+
:missing_runtime_handlers,
|
|
28
|
+
:missing_registry_contracts,
|
|
29
|
+
:install_error,
|
|
30
|
+
:finalize_error
|
|
31
|
+
|
|
32
|
+
def self.build(pack, profile: nil)
|
|
33
|
+
manifest = pack.manifest
|
|
34
|
+
baseline_snapshot = registry_snapshot(Igniter::Contracts.build_kernel)
|
|
35
|
+
draft_snapshot, install_error = install_snapshot(pack)
|
|
36
|
+
|
|
37
|
+
new(
|
|
38
|
+
pack_snapshot: PackSnapshot.new(manifest),
|
|
39
|
+
installed_in_target_profile: profile ? profile.pack_names.include?(manifest.name) : false,
|
|
40
|
+
target_profile_fingerprint: profile&.fingerprint,
|
|
41
|
+
draft_registered_keys: registry_delta(baseline_snapshot, draft_snapshot),
|
|
42
|
+
missing_node_definitions: missing_node_definitions(manifest, draft_snapshot),
|
|
43
|
+
missing_dsl_keywords: missing_dsl_keywords(manifest, draft_snapshot),
|
|
44
|
+
missing_runtime_handlers: missing_runtime_handlers(manifest, draft_snapshot),
|
|
45
|
+
missing_registry_contracts: missing_registry_contracts(manifest, draft_snapshot),
|
|
46
|
+
install_error: install_error,
|
|
47
|
+
finalize_error: finalize_error_for(pack)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.install_snapshot(pack)
|
|
52
|
+
kernel = Igniter::Contracts.build_kernel
|
|
53
|
+
error = nil
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
kernel.install(pack)
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
error = "#{e.class}: #{e.message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
[registry_snapshot(kernel), error]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.finalize_error_for(pack)
|
|
65
|
+
kernel = Igniter::Contracts.build_kernel
|
|
66
|
+
kernel.install(pack)
|
|
67
|
+
kernel.finalize
|
|
68
|
+
nil
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
"#{e.class}: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.registry_snapshot(kernel)
|
|
74
|
+
REGISTRIES.to_h { |name, reader| [name, reader.call(kernel)] }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.registry_delta(before, after)
|
|
78
|
+
REGISTRIES.keys.to_h do |name|
|
|
79
|
+
[name, after.fetch(name) - before.fetch(name)]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.missing_node_definitions(manifest, snapshot)
|
|
84
|
+
manifest.node_contracts.map(&:kind).reject { |kind| snapshot.fetch(:node_kinds).include?(kind) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.missing_dsl_keywords(manifest, snapshot)
|
|
88
|
+
required = manifest.node_contracts.select(&:requires_dsl).map(&:kind)
|
|
89
|
+
required.reject { |kind| snapshot.fetch(:dsl_keywords).include?(kind) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.missing_runtime_handlers(manifest, snapshot)
|
|
93
|
+
required = manifest.node_contracts.select(&:requires_runtime).map(&:kind)
|
|
94
|
+
required.reject { |kind| snapshot.fetch(:runtime_handlers).include?(kind) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.missing_registry_contracts(manifest, snapshot)
|
|
98
|
+
manifest.registry_contracts.each_with_object({}) do |contract, memo|
|
|
99
|
+
registry = normalize_registry(contract.registry)
|
|
100
|
+
available = snapshot.fetch(registry)
|
|
101
|
+
next if available.include?(contract.key)
|
|
102
|
+
|
|
103
|
+
memo[registry] ||= []
|
|
104
|
+
memo[registry] << contract.key
|
|
105
|
+
end.transform_values(&:sort)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.normalize_registry(name)
|
|
109
|
+
case name.to_sym
|
|
110
|
+
when :nodes
|
|
111
|
+
:node_kinds
|
|
112
|
+
else
|
|
113
|
+
name.to_sym
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def initialize(pack_snapshot:, installed_in_target_profile:, target_profile_fingerprint:,
|
|
118
|
+
draft_registered_keys:, missing_node_definitions:, missing_dsl_keywords:, missing_runtime_handlers:, missing_registry_contracts:, install_error:, finalize_error:)
|
|
119
|
+
@pack_snapshot = pack_snapshot
|
|
120
|
+
@installed_in_target_profile = installed_in_target_profile
|
|
121
|
+
@target_profile_fingerprint = target_profile_fingerprint
|
|
122
|
+
@draft_registered_keys = draft_registered_keys
|
|
123
|
+
@missing_node_definitions = missing_node_definitions.freeze
|
|
124
|
+
@missing_dsl_keywords = missing_dsl_keywords.freeze
|
|
125
|
+
@missing_runtime_handlers = missing_runtime_handlers.freeze
|
|
126
|
+
@missing_registry_contracts = missing_registry_contracts.transform_values(&:freeze).freeze
|
|
127
|
+
@install_error = install_error
|
|
128
|
+
@finalize_error = finalize_error
|
|
129
|
+
freeze
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def name
|
|
133
|
+
pack_snapshot.name
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ok?
|
|
137
|
+
install_error.nil? &&
|
|
138
|
+
finalize_error.nil? &&
|
|
139
|
+
missing_node_definitions.empty? &&
|
|
140
|
+
missing_dsl_keywords.empty? &&
|
|
141
|
+
missing_runtime_handlers.empty? &&
|
|
142
|
+
missing_registry_contracts.empty?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def to_h
|
|
146
|
+
{
|
|
147
|
+
pack: pack_snapshot.to_h,
|
|
148
|
+
installed_in_target_profile: installed_in_target_profile,
|
|
149
|
+
target_profile_fingerprint: target_profile_fingerprint,
|
|
150
|
+
draft_registered_keys: draft_registered_keys,
|
|
151
|
+
missing: {
|
|
152
|
+
node_definitions: missing_node_definitions,
|
|
153
|
+
dsl_keywords: missing_dsl_keywords,
|
|
154
|
+
runtime_handlers: missing_runtime_handlers,
|
|
155
|
+
registry_contracts: missing_registry_contracts
|
|
156
|
+
},
|
|
157
|
+
install_error: install_error,
|
|
158
|
+
finalize_error: finalize_error,
|
|
159
|
+
ok: ok?
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def explain
|
|
164
|
+
return "#{name} looks complete" if ok?
|
|
165
|
+
|
|
166
|
+
parts = []
|
|
167
|
+
parts << "install_error=#{install_error}" if install_error
|
|
168
|
+
parts << "missing node definitions: #{missing_node_definitions.join(", ")}" unless missing_node_definitions.empty?
|
|
169
|
+
parts << "missing DSL keywords: #{missing_dsl_keywords.join(", ")}" unless missing_dsl_keywords.empty?
|
|
170
|
+
parts << "missing runtime handlers: #{missing_runtime_handlers.join(", ")}" unless missing_runtime_handlers.empty?
|
|
171
|
+
missing_registry_contracts.each do |registry, keys|
|
|
172
|
+
parts << "missing #{registry}: #{keys.join(", ")}"
|
|
173
|
+
end
|
|
174
|
+
parts << "finalize_error=#{finalize_error}" if finalize_error
|
|
175
|
+
parts.join("; ")
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Debug
|
|
7
|
+
class PackSnapshot
|
|
8
|
+
attr_reader :manifest
|
|
9
|
+
|
|
10
|
+
def initialize(manifest)
|
|
11
|
+
@manifest = manifest
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
manifest.name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def metadata
|
|
20
|
+
manifest.metadata
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def node_kinds
|
|
24
|
+
manifest.node_contracts.map(&:kind)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def registry_contracts
|
|
28
|
+
manifest.registry_contracts.each_with_object({}) do |contract, memo|
|
|
29
|
+
memo[contract.registry] ||= []
|
|
30
|
+
memo[contract.registry] << contract.key
|
|
31
|
+
end.transform_values(&:sort)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
name: name,
|
|
37
|
+
metadata: metadata,
|
|
38
|
+
node_kinds: node_kinds,
|
|
39
|
+
registry_contracts: registry_contracts
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "pack_snapshot"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Extensions
|
|
7
|
+
module Contracts
|
|
8
|
+
module Debug
|
|
9
|
+
class ProfileSnapshot
|
|
10
|
+
attr_reader :profile
|
|
11
|
+
|
|
12
|
+
def initialize(profile:)
|
|
13
|
+
@profile = profile
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def pack_names
|
|
18
|
+
profile.pack_names.sort
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def packs
|
|
22
|
+
profile.pack_manifests.map { |manifest| PackSnapshot.new(manifest) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def registry_keys
|
|
26
|
+
{
|
|
27
|
+
node_kinds: profile.nodes.keys.sort,
|
|
28
|
+
dsl_keywords: profile.dsl_keywords.keys.sort,
|
|
29
|
+
validators: profile.validators.map(&:key).sort,
|
|
30
|
+
normalizers: profile.normalizers.map(&:key).sort,
|
|
31
|
+
runtime_handlers: profile.runtime_handlers.keys.sort,
|
|
32
|
+
diagnostics_contributors: profile.diagnostics_contributors.map(&:key).sort,
|
|
33
|
+
effects: profile.effects.keys.sort,
|
|
34
|
+
executors: profile.executors.keys.sort
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def to_h
|
|
39
|
+
{
|
|
40
|
+
fingerprint: profile.fingerprint,
|
|
41
|
+
pack_names: pack_names,
|
|
42
|
+
registry_keys: registry_keys,
|
|
43
|
+
packs: packs.map(&:to_h)
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "profile_snapshot"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Extensions
|
|
7
|
+
module Contracts
|
|
8
|
+
module Debug
|
|
9
|
+
class Report
|
|
10
|
+
attr_reader :profile_snapshot,
|
|
11
|
+
:compilation_report,
|
|
12
|
+
:execution_result,
|
|
13
|
+
:diagnostics_report,
|
|
14
|
+
:provenance_summary
|
|
15
|
+
|
|
16
|
+
def initialize(profile_snapshot:, compilation_report: nil, execution_result: nil, diagnostics_report: nil,
|
|
17
|
+
provenance_summary: nil)
|
|
18
|
+
@profile_snapshot = profile_snapshot
|
|
19
|
+
@compilation_report = compilation_report
|
|
20
|
+
@execution_result = execution_result
|
|
21
|
+
@diagnostics_report = diagnostics_report
|
|
22
|
+
@provenance_summary = provenance_summary
|
|
23
|
+
freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ok?
|
|
27
|
+
compilation_report.nil? || compilation_report.ok?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def invalid?
|
|
31
|
+
!ok?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
payload = {
|
|
36
|
+
profile: profile_snapshot.to_h,
|
|
37
|
+
ok: ok?
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
payload[:compilation] = compilation_report.to_h if compilation_report
|
|
41
|
+
payload[:execution] = execution_result.to_h if execution_result
|
|
42
|
+
payload[:diagnostics] = diagnostics_report.to_h if diagnostics_report
|
|
43
|
+
payload[:provenance] = provenance_summary if provenance_summary
|
|
44
|
+
payload
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|