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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "debug/profile_snapshot"
|
|
4
|
+
require_relative "debug/pack_audit"
|
|
5
|
+
require_relative "debug/report"
|
|
6
|
+
|
|
7
|
+
module Igniter
|
|
8
|
+
module Extensions
|
|
9
|
+
module Contracts
|
|
10
|
+
module DebugPack
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
REPORT_CONTRIBUTOR = Module.new do
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def augment(report:, result:, profile:)
|
|
17
|
+
snapshot = DebugPack.profile_snapshot(profile)
|
|
18
|
+
report.add_section(:debug, {
|
|
19
|
+
profile_fingerprint: profile.fingerprint,
|
|
20
|
+
pack_names: snapshot.pack_names,
|
|
21
|
+
registry_keys: snapshot.registry_keys,
|
|
22
|
+
output_names: result.outputs.keys.sort,
|
|
23
|
+
state_keys: result.state.keys.sort,
|
|
24
|
+
operation_count: result.compiled_graph.operations.length
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def manifest
|
|
30
|
+
Igniter::Contracts::PackManifest.new(
|
|
31
|
+
name: :extensions_debug,
|
|
32
|
+
requires_packs: [ExecutionReportPack, ProvenancePack],
|
|
33
|
+
registry_contracts: [Igniter::Contracts::PackManifest.diagnostic(:debug)],
|
|
34
|
+
metadata: { category: :developer }
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def install_into(kernel)
|
|
39
|
+
kernel.diagnostics_contributors.register(:debug, REPORT_CONTRIBUTOR)
|
|
40
|
+
kernel
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def profile_snapshot(profile)
|
|
44
|
+
Debug::ProfileSnapshot.new(profile: profile)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def pack_snapshot(pack_or_name, profile:)
|
|
48
|
+
manifest =
|
|
49
|
+
case pack_or_name
|
|
50
|
+
when Symbol, String
|
|
51
|
+
profile.pack_manifest(pack_or_name)
|
|
52
|
+
else
|
|
53
|
+
pack_or_name.respond_to?(:manifest) ? pack_or_name.manifest : profile.pack_manifest(pack_or_name)
|
|
54
|
+
end
|
|
55
|
+
raise ArgumentError, "unknown pack #{pack_or_name}" unless manifest
|
|
56
|
+
|
|
57
|
+
Debug::PackSnapshot.new(manifest)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def audit(pack, profile: nil)
|
|
61
|
+
Debug::PackAudit.build(pack, profile: profile)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def snapshot(result, profile:)
|
|
65
|
+
diagnostics = Igniter::Contracts.diagnose(result, profile: profile)
|
|
66
|
+
Debug::Report.new(
|
|
67
|
+
profile_snapshot: profile_snapshot(profile),
|
|
68
|
+
execution_result: result,
|
|
69
|
+
diagnostics_report: diagnostics,
|
|
70
|
+
provenance_summary: provenance_summary(result)
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def report(environment, inputs: nil, compiled_graph: nil, &block)
|
|
75
|
+
compilation =
|
|
76
|
+
if block
|
|
77
|
+
environment.compilation_report(&block)
|
|
78
|
+
elsif compiled_graph
|
|
79
|
+
nil
|
|
80
|
+
else
|
|
81
|
+
raise ArgumentError, "debug_report requires a block or compiled_graph"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
graph = compiled_graph || compilation&.compiled_graph
|
|
85
|
+
if inputs.nil? || compilation&.invalid?
|
|
86
|
+
return Debug::Report.new(profile_snapshot: profile_snapshot(environment.profile),
|
|
87
|
+
compilation_report: compilation)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
result = environment.execute(graph, inputs: inputs)
|
|
91
|
+
diagnostics = environment.diagnose(result)
|
|
92
|
+
|
|
93
|
+
Debug::Report.new(
|
|
94
|
+
profile_snapshot: profile_snapshot(environment.profile),
|
|
95
|
+
compilation_report: compilation,
|
|
96
|
+
execution_result: result,
|
|
97
|
+
diagnostics_report: diagnostics,
|
|
98
|
+
provenance_summary: provenance_summary(result)
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def provenance_summary(result)
|
|
103
|
+
result.outputs.keys.sort.each_with_object({}) do |output_name, memo|
|
|
104
|
+
lineage = ProvenancePack.lineage(result, output_name)
|
|
105
|
+
memo[output_name] = {
|
|
106
|
+
value: lineage.value,
|
|
107
|
+
contributing_inputs: lineage.contributing_inputs,
|
|
108
|
+
trace: lineage.to_h
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Differential
|
|
7
|
+
class Divergence
|
|
8
|
+
attr_reader :output_name, :primary_value, :candidate_value, :kind
|
|
9
|
+
|
|
10
|
+
def initialize(output_name:, primary_value:, candidate_value:, kind:)
|
|
11
|
+
@output_name = output_name.to_sym
|
|
12
|
+
@primary_value = primary_value
|
|
13
|
+
@candidate_value = candidate_value
|
|
14
|
+
@kind = kind.to_sym
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def delta
|
|
19
|
+
return nil unless primary_value.is_a?(Numeric) && candidate_value.is_a?(Numeric)
|
|
20
|
+
|
|
21
|
+
candidate_value - primary_value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{
|
|
26
|
+
output_name: output_name,
|
|
27
|
+
primary_value: primary_value,
|
|
28
|
+
candidate_value: candidate_value,
|
|
29
|
+
kind: kind,
|
|
30
|
+
delta: delta
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Differential
|
|
7
|
+
module Formatter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
VALUE_MAX = 60
|
|
11
|
+
|
|
12
|
+
def format(report)
|
|
13
|
+
lines = []
|
|
14
|
+
lines << "Primary: #{report.primary_name}"
|
|
15
|
+
lines << "Candidate: #{report.candidate_name}"
|
|
16
|
+
lines << "Match: #{report.match? ? "YES" : "NO"}"
|
|
17
|
+
|
|
18
|
+
if report.primary_error
|
|
19
|
+
lines << ""
|
|
20
|
+
lines << "PRIMARY ERROR: #{report.primary_error.fetch(:message)}"
|
|
21
|
+
return lines.join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if report.candidate_error
|
|
25
|
+
lines << ""
|
|
26
|
+
lines << "CANDIDATE ERROR: #{report.candidate_error.fetch(:message)}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
lines << ""
|
|
30
|
+
|
|
31
|
+
if report.divergences.empty? && report.primary_only.empty? && report.candidate_only.empty?
|
|
32
|
+
lines << "All shared outputs match."
|
|
33
|
+
else
|
|
34
|
+
append_divergences(report, lines)
|
|
35
|
+
append_only_section("CANDIDATE ONLY", report.candidate_only, lines)
|
|
36
|
+
append_only_section("PRIMARY ONLY", report.primary_only, lines)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
lines.join("\n")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def append_divergences(report, lines)
|
|
43
|
+
return if report.divergences.empty?
|
|
44
|
+
|
|
45
|
+
lines << "DIVERGENCES (#{report.divergences.size}):"
|
|
46
|
+
report.divergences.each do |divergence|
|
|
47
|
+
lines << " :#{divergence.output_name}"
|
|
48
|
+
lines << " primary: #{fmt(divergence.primary_value)}"
|
|
49
|
+
lines << " candidate: #{fmt(divergence.candidate_value)}"
|
|
50
|
+
next if divergence.delta.nil?
|
|
51
|
+
|
|
52
|
+
delta = divergence.delta
|
|
53
|
+
lines << " delta: #{delta >= 0 ? "+#{delta}" : delta}"
|
|
54
|
+
end
|
|
55
|
+
lines << ""
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def append_only_section(label, values, lines)
|
|
59
|
+
return if values.empty?
|
|
60
|
+
|
|
61
|
+
lines << "#{label} (#{values.size}):"
|
|
62
|
+
values.each do |name, value|
|
|
63
|
+
lines << " :#{name} = #{fmt(value)}"
|
|
64
|
+
end
|
|
65
|
+
lines << ""
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fmt(value)
|
|
69
|
+
string =
|
|
70
|
+
case value
|
|
71
|
+
when nil then "nil"
|
|
72
|
+
when String, Symbol then value.inspect
|
|
73
|
+
when Hash then "{#{value.map { |key, item| "#{key}: #{item.inspect}" }.join(", ")}}"
|
|
74
|
+
when Array then "[#{value.map(&:inspect).join(", ")}]"
|
|
75
|
+
else
|
|
76
|
+
value.inspect
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
string.length > VALUE_MAX ? "#{string[0, VALUE_MAX - 3]}..." : string
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Differential
|
|
7
|
+
class Report
|
|
8
|
+
attr_reader :primary_name, :candidate_name, :inputs,
|
|
9
|
+
:primary_outputs, :candidate_outputs,
|
|
10
|
+
:divergences, :primary_only, :candidate_only,
|
|
11
|
+
:primary_error, :candidate_error
|
|
12
|
+
|
|
13
|
+
def initialize(
|
|
14
|
+
primary_name:,
|
|
15
|
+
candidate_name:,
|
|
16
|
+
inputs:,
|
|
17
|
+
primary_outputs:,
|
|
18
|
+
candidate_outputs:,
|
|
19
|
+
divergences:,
|
|
20
|
+
primary_only:,
|
|
21
|
+
candidate_only:,
|
|
22
|
+
primary_error: nil,
|
|
23
|
+
candidate_error: nil
|
|
24
|
+
)
|
|
25
|
+
@primary_name = primary_name.to_s
|
|
26
|
+
@candidate_name = candidate_name.to_s
|
|
27
|
+
@inputs = inputs.transform_keys(&:to_sym).freeze
|
|
28
|
+
@primary_outputs = primary_outputs.transform_keys(&:to_sym).freeze
|
|
29
|
+
@candidate_outputs = candidate_outputs.transform_keys(&:to_sym).freeze
|
|
30
|
+
@divergences = divergences.freeze
|
|
31
|
+
@primary_only = primary_only.transform_keys(&:to_sym).freeze
|
|
32
|
+
@candidate_only = candidate_only.transform_keys(&:to_sym).freeze
|
|
33
|
+
@primary_error = primary_error&.freeze
|
|
34
|
+
@candidate_error = candidate_error&.freeze
|
|
35
|
+
freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def match?
|
|
39
|
+
divergences.empty? &&
|
|
40
|
+
primary_only.empty? &&
|
|
41
|
+
candidate_only.empty? &&
|
|
42
|
+
primary_error.nil? &&
|
|
43
|
+
candidate_error.nil?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def summary
|
|
47
|
+
return "match" if match?
|
|
48
|
+
|
|
49
|
+
parts = []
|
|
50
|
+
parts << "#{divergences.size} value(s) differ" if divergences.any?
|
|
51
|
+
parts << "#{primary_only.size} output(s) only in primary" if primary_only.any?
|
|
52
|
+
parts << "#{candidate_only.size} output(s) only in candidate" if candidate_only.any?
|
|
53
|
+
parts << "candidate error: #{candidate_error.fetch(:message)}" if candidate_error
|
|
54
|
+
parts << "primary error: #{primary_error.fetch(:message)}" if primary_error
|
|
55
|
+
"diverged - #{parts.join(", ")}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def explain
|
|
59
|
+
Formatter.format(self)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
alias to_s explain
|
|
63
|
+
|
|
64
|
+
def to_h
|
|
65
|
+
{
|
|
66
|
+
primary: primary_name,
|
|
67
|
+
candidate: candidate_name,
|
|
68
|
+
inputs: inputs,
|
|
69
|
+
match: match?,
|
|
70
|
+
primary_outputs: primary_outputs,
|
|
71
|
+
candidate_outputs: candidate_outputs,
|
|
72
|
+
divergences: divergences.map(&:to_h),
|
|
73
|
+
primary_only: primary_only,
|
|
74
|
+
candidate_only: candidate_only,
|
|
75
|
+
primary_error: primary_error,
|
|
76
|
+
candidate_error: candidate_error
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Differential
|
|
7
|
+
class Runner
|
|
8
|
+
def initialize(primary_name:, candidate_name:, tolerance: nil)
|
|
9
|
+
@primary_name = primary_name
|
|
10
|
+
@candidate_name = candidate_name
|
|
11
|
+
@tolerance = tolerance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def compare(
|
|
15
|
+
inputs:,
|
|
16
|
+
primary_environment: nil,
|
|
17
|
+
primary_compiled_graph: nil,
|
|
18
|
+
primary_result: nil,
|
|
19
|
+
candidate_environment: nil,
|
|
20
|
+
candidate_compiled_graph: nil,
|
|
21
|
+
candidate_result: nil
|
|
22
|
+
)
|
|
23
|
+
primary_result, primary_error = resolve_execution(
|
|
24
|
+
result: primary_result,
|
|
25
|
+
environment: primary_environment,
|
|
26
|
+
compiled_graph: primary_compiled_graph,
|
|
27
|
+
inputs: inputs,
|
|
28
|
+
label: @primary_name
|
|
29
|
+
)
|
|
30
|
+
candidate_result, candidate_error = resolve_execution(
|
|
31
|
+
result: candidate_result,
|
|
32
|
+
environment: candidate_environment,
|
|
33
|
+
compiled_graph: candidate_compiled_graph,
|
|
34
|
+
inputs: inputs,
|
|
35
|
+
label: @candidate_name
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
primary_outputs = primary_result ? primary_result.outputs.to_h : {}
|
|
39
|
+
candidate_outputs = candidate_result ? candidate_result.outputs.to_h : {}
|
|
40
|
+
|
|
41
|
+
build_report(
|
|
42
|
+
inputs: inputs,
|
|
43
|
+
primary_outputs: primary_outputs,
|
|
44
|
+
candidate_outputs: candidate_outputs,
|
|
45
|
+
primary_error: primary_error,
|
|
46
|
+
candidate_error: candidate_error
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def resolve_execution(result:, environment:, compiled_graph:, inputs:, label:)
|
|
53
|
+
return [result, nil] if result
|
|
54
|
+
|
|
55
|
+
if environment.nil? || compiled_graph.nil?
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"#{label} comparison requires either an execution result or environment + compiled_graph"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
[environment.execute(compiled_graph, inputs: inputs), nil]
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
[nil, serialize_error(e)]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_report(inputs:, primary_outputs:, candidate_outputs:, primary_error:, candidate_error:)
|
|
68
|
+
common = primary_outputs.keys & candidate_outputs.keys
|
|
69
|
+
divergences = compare_common(primary_outputs, candidate_outputs, common)
|
|
70
|
+
|
|
71
|
+
Report.new(
|
|
72
|
+
primary_name: @primary_name,
|
|
73
|
+
candidate_name: @candidate_name,
|
|
74
|
+
inputs: inputs,
|
|
75
|
+
primary_outputs: primary_outputs,
|
|
76
|
+
candidate_outputs: candidate_outputs,
|
|
77
|
+
divergences: divergences,
|
|
78
|
+
primary_only: slice_missing(primary_outputs, candidate_outputs),
|
|
79
|
+
candidate_only: slice_missing(candidate_outputs, primary_outputs),
|
|
80
|
+
primary_error: primary_error,
|
|
81
|
+
candidate_error: candidate_error
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def compare_common(primary_outputs, candidate_outputs, names)
|
|
86
|
+
names.filter_map do |name|
|
|
87
|
+
primary_value = primary_outputs.fetch(name)
|
|
88
|
+
candidate_value = candidate_outputs.fetch(name)
|
|
89
|
+
next if values_match?(primary_value, candidate_value)
|
|
90
|
+
|
|
91
|
+
Divergence.new(
|
|
92
|
+
output_name: name,
|
|
93
|
+
primary_value: primary_value,
|
|
94
|
+
candidate_value: candidate_value,
|
|
95
|
+
kind: divergence_kind(primary_value, candidate_value)
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def slice_missing(source, other)
|
|
101
|
+
(source.keys - other.keys).each_with_object({}) do |name, memo|
|
|
102
|
+
memo[name] = source.fetch(name)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def values_match?(left, right)
|
|
107
|
+
return true if left == right
|
|
108
|
+
return false unless @tolerance
|
|
109
|
+
return false unless left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
110
|
+
|
|
111
|
+
(left - right).abs <= @tolerance
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def divergence_kind(left, right)
|
|
115
|
+
left.instance_of?(right.class) ? :value_mismatch : :type_mismatch
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def serialize_error(error)
|
|
119
|
+
payload =
|
|
120
|
+
if error.respond_to?(:to_h)
|
|
121
|
+
error.to_h
|
|
122
|
+
else
|
|
123
|
+
{}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
type: error.class.name,
|
|
128
|
+
message: error.message,
|
|
129
|
+
details: payload
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "differential/divergence"
|
|
4
|
+
require_relative "differential/formatter"
|
|
5
|
+
require_relative "differential/report"
|
|
6
|
+
require_relative "differential/runner"
|
|
7
|
+
|
|
8
|
+
module Igniter
|
|
9
|
+
module Extensions
|
|
10
|
+
module Contracts
|
|
11
|
+
module DifferentialPack
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def manifest
|
|
15
|
+
Igniter::Contracts::PackManifest.new(
|
|
16
|
+
name: :extensions_differential,
|
|
17
|
+
metadata: { category: :developer }
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def install_into(kernel)
|
|
22
|
+
kernel
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def compare(
|
|
26
|
+
inputs:,
|
|
27
|
+
primary_environment: nil,
|
|
28
|
+
primary_compiled_graph: nil,
|
|
29
|
+
primary_result: nil,
|
|
30
|
+
candidate_environment: nil,
|
|
31
|
+
candidate_compiled_graph: nil,
|
|
32
|
+
candidate_result: nil,
|
|
33
|
+
tolerance: nil,
|
|
34
|
+
primary_name: "primary",
|
|
35
|
+
candidate_name: "candidate"
|
|
36
|
+
)
|
|
37
|
+
Differential::Runner.new(
|
|
38
|
+
primary_name: primary_name,
|
|
39
|
+
candidate_name: candidate_name,
|
|
40
|
+
tolerance: tolerance
|
|
41
|
+
).compare(
|
|
42
|
+
inputs: inputs,
|
|
43
|
+
primary_environment: primary_environment,
|
|
44
|
+
primary_compiled_graph: primary_compiled_graph,
|
|
45
|
+
primary_result: primary_result,
|
|
46
|
+
candidate_environment: candidate_environment,
|
|
47
|
+
candidate_compiled_graph: candidate_compiled_graph,
|
|
48
|
+
candidate_result: candidate_result
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def shadow(**arguments)
|
|
53
|
+
on_divergence = arguments.delete(:on_divergence)
|
|
54
|
+
report = compare(**arguments)
|
|
55
|
+
on_divergence.call(report) if on_divergence && !report.match?
|
|
56
|
+
report
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module ExecutionReportPack
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
REPORT_CONTRIBUTOR = Module.new do
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def augment(report:, result:, profile:)
|
|
13
|
+
report.add_section(:execution_report, {
|
|
14
|
+
profile_fingerprint: profile.fingerprint,
|
|
15
|
+
pack_names: profile.pack_names.sort,
|
|
16
|
+
output_count: result.outputs.length,
|
|
17
|
+
state_count: result.state.length,
|
|
18
|
+
outputs: result.outputs.to_h,
|
|
19
|
+
state_keys: result.state.keys.sort
|
|
20
|
+
})
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def manifest
|
|
25
|
+
Igniter::Contracts::PackManifest.new(
|
|
26
|
+
name: :extensions_execution_report,
|
|
27
|
+
registry_contracts: [Igniter::Contracts::PackManifest.diagnostic(:execution_report)]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def install_into(kernel)
|
|
32
|
+
kernel.diagnostics_contributors.register(:execution_report, REPORT_CONTRIBUTOR)
|
|
33
|
+
kernel
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Incremental
|
|
7
|
+
module Formatter
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
LINE = "─" * 42
|
|
11
|
+
VALUE_MAX = 60
|
|
12
|
+
|
|
13
|
+
def format(result)
|
|
14
|
+
lines = []
|
|
15
|
+
lines << "Contracts Incremental Report"
|
|
16
|
+
lines << LINE
|
|
17
|
+
lines << "Recomputed: #{result.recomputed_count} node(s)"
|
|
18
|
+
lines << "Skipped: #{result.skipped_nodes.length} node(s)"
|
|
19
|
+
lines << "Backdated: #{result.backdated_nodes.length} node(s)"
|
|
20
|
+
lines << ""
|
|
21
|
+
|
|
22
|
+
if result.changed_outputs.any?
|
|
23
|
+
lines << "CHANGED OUTPUTS (#{result.changed_outputs.length}):"
|
|
24
|
+
result.changed_outputs.each do |name, diff|
|
|
25
|
+
lines << " :#{name} #{fmt(diff[:from])} -> #{fmt(diff[:to])}"
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
lines << "No output values changed."
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
lines << ""
|
|
32
|
+
if result.skipped_nodes.any?
|
|
33
|
+
lines << "SKIPPED: #{result.skipped_nodes.map do |name|
|
|
34
|
+
":#{name}"
|
|
35
|
+
end.join(" ")}"
|
|
36
|
+
end
|
|
37
|
+
if result.backdated_nodes.any?
|
|
38
|
+
lines << "BACKDATED: #{result.backdated_nodes.map do |name|
|
|
39
|
+
":#{name}"
|
|
40
|
+
end.join(" ")}"
|
|
41
|
+
end
|
|
42
|
+
if result.changed_nodes.any?
|
|
43
|
+
lines << "CHANGED: #{result.changed_nodes.map do |name|
|
|
44
|
+
":#{name}"
|
|
45
|
+
end.join(" ")}"
|
|
46
|
+
end
|
|
47
|
+
lines.compact.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def fmt(value)
|
|
51
|
+
rendered = value.inspect
|
|
52
|
+
return rendered if rendered.length <= VALUE_MAX
|
|
53
|
+
|
|
54
|
+
"#{rendered[0, VALUE_MAX - 3]}..."
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Incremental
|
|
7
|
+
class NodeState
|
|
8
|
+
attr_reader :name, :value, :value_version, :dep_snapshot
|
|
9
|
+
|
|
10
|
+
def initialize(name:, value:, value_version:, dep_snapshot: {})
|
|
11
|
+
@name = name.to_sym
|
|
12
|
+
@value = value
|
|
13
|
+
@value_version = Integer(value_version)
|
|
14
|
+
@dep_snapshot = dep_snapshot.transform_keys(&:to_sym).freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
name: name,
|
|
21
|
+
value: value,
|
|
22
|
+
value_version: value_version,
|
|
23
|
+
dep_snapshot: dep_snapshot
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|