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,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Mcp
7
+ class ToolDefinition
8
+ attr_reader :name, :summary, :mutating, :target, :arguments
9
+
10
+ def initialize(name:, summary:, mutating: false, target: nil, arguments: [])
11
+ @name = name.to_sym
12
+ @summary = summary
13
+ @mutating = mutating == true
14
+ @target = target&.to_sym
15
+ @arguments = arguments.freeze
16
+ freeze
17
+ end
18
+
19
+ def to_h
20
+ payload = {
21
+ name: name,
22
+ summary: summary,
23
+ mutating: mutating,
24
+ arguments: arguments.map(&:to_h)
25
+ }
26
+ payload[:target] = target if target
27
+ payload
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Mcp
7
+ class ToolResult
8
+ attr_reader :tool_name, :payload, :mutating
9
+
10
+ def initialize(tool_name:, payload:, mutating:)
11
+ @tool_name = tool_name.to_sym
12
+ @payload = payload
13
+ @mutating = mutating == true
14
+ freeze
15
+ end
16
+
17
+ def to_h
18
+ {
19
+ tool_name: tool_name,
20
+ mutating: mutating,
21
+ payload: payload
22
+ }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mcp/tool_definition"
4
+ require_relative "mcp/tool_argument"
5
+ require_relative "mcp/tool_result"
6
+ require_relative "mcp/creator_session"
7
+
8
+ module Igniter
9
+ module Extensions
10
+ module Contracts
11
+ module McpPack
12
+ module_function
13
+
14
+ def manifest
15
+ Igniter::Contracts::PackManifest.new(
16
+ name: :extensions_mcp,
17
+ requires_packs: [DebugPack, CreatorPack],
18
+ metadata: { category: :tooling }
19
+ )
20
+ end
21
+
22
+ def install_into(kernel)
23
+ kernel
24
+ end
25
+
26
+ def tools
27
+ @tools ||= [
28
+ Mcp::ToolDefinition.new(
29
+ name: :inspect_profile,
30
+ summary: "Return a structured profile snapshot.",
31
+ target: :profile_or_environment
32
+ ),
33
+ Mcp::ToolDefinition.new(
34
+ name: :inspect_pack,
35
+ summary: "Return a structured installed-pack snapshot.",
36
+ target: :profile_or_environment,
37
+ arguments: [
38
+ Mcp::ToolArgument.new(name: :pack, type: :pack_reference,
39
+ summary: "Installed pack name or pack module.", required: true)
40
+ ]
41
+ ),
42
+ Mcp::ToolDefinition.new(
43
+ name: :audit_pack,
44
+ summary: "Audit a custom pack against creator/debug quality seams.",
45
+ target: :optional_profile_or_environment,
46
+ arguments: [
47
+ Mcp::ToolArgument.new(name: :pack, type: :pack_reference, summary: "Custom pack module to audit.",
48
+ required: true)
49
+ ]
50
+ ),
51
+ Mcp::ToolDefinition.new(
52
+ name: :debug_report,
53
+ summary: "Compile or execute and return a structured debug report.",
54
+ target: :environment,
55
+ arguments: [
56
+ Mcp::ToolArgument.new(name: :inputs, type: :map,
57
+ summary: "Runtime inputs used for execution when a graph is available."),
58
+ Mcp::ToolArgument.new(name: :compiled_graph, type: :compiled_graph,
59
+ summary: "Previously compiled graph to execute instead of compiling a block.")
60
+ ]
61
+ ),
62
+ Mcp::ToolDefinition.new(
63
+ name: :creator_wizard,
64
+ summary: "Build a stateful creator wizard payload.",
65
+ target: :optional_profile_or_environment,
66
+ arguments: creator_arguments(include_scope: true, include_root: true)
67
+ ),
68
+ Mcp::ToolDefinition.new(
69
+ name: :creator_session_start,
70
+ summary: "Create a serialized creator session payload.",
71
+ target: :optional_profile_or_environment,
72
+ arguments: creator_arguments(include_scope: true, include_root: true)
73
+ ),
74
+ Mcp::ToolDefinition.new(
75
+ name: :creator_session_apply,
76
+ summary: "Apply updates to a serialized creator session payload.",
77
+ target: :optional_profile_or_environment,
78
+ arguments: [
79
+ Mcp::ToolArgument.new(name: :session, type: :session_state,
80
+ summary: "Previously serialized creator session payload.", required: true),
81
+ Mcp::ToolArgument.new(name: :updates, type: :map,
82
+ summary: "Partial wizard updates to merge into the session.", required: true)
83
+ ]
84
+ ),
85
+ Mcp::ToolDefinition.new(
86
+ name: :creator_session_workflow,
87
+ summary: "Build workflow payload from a serialized creator session.",
88
+ target: :optional_profile_or_environment,
89
+ arguments: [
90
+ Mcp::ToolArgument.new(name: :session, type: :session_state,
91
+ summary: "Previously serialized creator session payload.", required: true)
92
+ ]
93
+ ),
94
+ Mcp::ToolDefinition.new(
95
+ name: :creator_session_write_plan,
96
+ summary: "Build writer plan payload from a serialized creator session.",
97
+ target: :optional_profile_or_environment,
98
+ arguments: [
99
+ Mcp::ToolArgument.new(name: :session, type: :session_state,
100
+ summary: "Previously serialized creator session payload.", required: true)
101
+ ]
102
+ ),
103
+ Mcp::ToolDefinition.new(
104
+ name: :creator_session_write,
105
+ summary: "Write scaffold files from a serialized creator session.",
106
+ mutating: true,
107
+ target: :optional_profile_or_environment,
108
+ arguments: [
109
+ Mcp::ToolArgument.new(name: :session, type: :session_state,
110
+ summary: "Previously serialized creator session payload.", required: true)
111
+ ]
112
+ ),
113
+ Mcp::ToolDefinition.new(
114
+ name: :creator_workflow,
115
+ summary: "Build a creator workflow payload.",
116
+ target: :optional_profile_or_environment,
117
+ arguments: creator_arguments(include_scope: true)
118
+ ),
119
+ Mcp::ToolDefinition.new(
120
+ name: :creator_write_plan,
121
+ summary: "Build a creator writer plan payload.",
122
+ target: :optional_profile_or_environment,
123
+ arguments: creator_arguments(include_scope: true, include_root: true, require_name: true,
124
+ require_root: true)
125
+ ),
126
+ Mcp::ToolDefinition.new(
127
+ name: :creator_write,
128
+ summary: "Write a creator scaffold to disk.",
129
+ mutating: true,
130
+ target: :optional_profile_or_environment,
131
+ arguments: creator_arguments(include_scope: true, include_root: true, require_name: true,
132
+ require_root: true)
133
+ )
134
+ ].freeze
135
+ end
136
+
137
+ def tool_catalog
138
+ tools.map(&:to_h)
139
+ end
140
+
141
+ def call(tool_name, target: nil, **arguments, &block)
142
+ definition = tool_definition(tool_name)
143
+ payload = dispatch(definition.name, target: target, **arguments, &block)
144
+ Mcp::ToolResult.new(
145
+ tool_name: definition.name,
146
+ payload: payload,
147
+ mutating: definition.mutating
148
+ )
149
+ end
150
+
151
+ def tool_definition(tool_name)
152
+ tools.find { |definition| definition.name == tool_name.to_sym } ||
153
+ raise(ArgumentError, "unknown MCP tool #{tool_name.inspect}")
154
+ end
155
+
156
+ def dispatch(tool_name, target: nil, **arguments, &block)
157
+ case tool_name.to_sym
158
+ when :inspect_profile
159
+ profile_from(target).then { |profile| DebugPack.profile_snapshot(profile).to_h }
160
+ when :inspect_pack
161
+ profile = profile_from(target)
162
+ DebugPack.pack_snapshot(arguments.fetch(:pack), profile: profile).to_h
163
+ when :audit_pack
164
+ DebugPack.audit(arguments.fetch(:pack), profile: profile_from(target, optional: true)).to_h
165
+ when :debug_report
166
+ environment = environment_from(target)
167
+ DebugPack.report(
168
+ environment,
169
+ inputs: arguments[:inputs],
170
+ compiled_graph: arguments[:compiled_graph],
171
+ &block
172
+ ).to_h
173
+ when :creator_wizard
174
+ CreatorPack.wizard(
175
+ name: arguments[:name],
176
+ kind: arguments[:kind],
177
+ namespace: arguments.fetch(:namespace, "MyCompany::IgniterPacks"),
178
+ profile: arguments[:profile],
179
+ capabilities: arguments[:capabilities],
180
+ scope: arguments[:scope],
181
+ root: arguments[:root],
182
+ mode: arguments.fetch(:mode, :skip_existing),
183
+ pack: arguments[:pack],
184
+ target_profile: profile_from(target, optional: true)
185
+ ).to_h
186
+ when :creator_session_start
187
+ creator_session_from(arguments, target: target).to_h
188
+ when :creator_session_apply
189
+ session = session_from(arguments.fetch(:session) { arguments.fetch("session") }, target: target)
190
+ session.apply(**symbolize_keys(arguments.fetch(:updates) { arguments.fetch("updates") })).to_h
191
+ when :creator_session_workflow
192
+ session_from(arguments.fetch(:session) { arguments.fetch("session") }, target: target).workflow_payload
193
+ when :creator_session_write_plan
194
+ session_from(arguments.fetch(:session) { arguments.fetch("session") }, target: target).write_plan_payload
195
+ when :creator_session_write
196
+ session_from(arguments.fetch(:session) { arguments.fetch("session") }, target: target).write_payload
197
+ when :creator_workflow
198
+ CreatorPack.workflow(
199
+ name: arguments.fetch(:name),
200
+ kind: arguments[:kind],
201
+ namespace: arguments.fetch(:namespace, "MyCompany::IgniterPacks"),
202
+ profile: arguments[:profile],
203
+ capabilities: arguments[:capabilities],
204
+ scope: arguments.fetch(:scope, :monorepo_package),
205
+ pack: arguments[:pack],
206
+ target_profile: profile_from(target, optional: true)
207
+ ).to_h
208
+ when :creator_write_plan
209
+ CreatorPack.writer(
210
+ name: arguments.fetch(:name),
211
+ kind: arguments[:kind],
212
+ namespace: arguments.fetch(:namespace, "MyCompany::IgniterPacks"),
213
+ profile: arguments[:profile],
214
+ capabilities: arguments[:capabilities],
215
+ scope: arguments.fetch(:scope, :monorepo_package),
216
+ pack: arguments[:pack],
217
+ target_profile: profile_from(target, optional: true),
218
+ root: arguments.fetch(:root),
219
+ mode: arguments.fetch(:mode, :skip_existing)
220
+ ).plan.to_h
221
+ when :creator_write
222
+ CreatorPack.write(
223
+ name: arguments.fetch(:name),
224
+ kind: arguments[:kind],
225
+ namespace: arguments.fetch(:namespace, "MyCompany::IgniterPacks"),
226
+ profile: arguments[:profile],
227
+ capabilities: arguments[:capabilities],
228
+ scope: arguments.fetch(:scope, :monorepo_package),
229
+ pack: arguments[:pack],
230
+ target_profile: profile_from(target, optional: true),
231
+ root: arguments.fetch(:root),
232
+ mode: arguments.fetch(:mode, :skip_existing)
233
+ ).to_h
234
+ else
235
+ raise ArgumentError, "unsupported MCP tool #{tool_name.inspect}"
236
+ end
237
+ end
238
+
239
+ def profile_from(target, optional: false)
240
+ profile =
241
+ case target
242
+ when nil
243
+ nil
244
+ else
245
+ target.respond_to?(:profile) ? target.profile : target
246
+ end
247
+
248
+ return profile if optional || profile
249
+
250
+ raise ArgumentError, "McpPack tool requires an environment or profile target"
251
+ end
252
+
253
+ def environment_from(target)
254
+ return target if target.respond_to?(:profile) && target.respond_to?(:execute)
255
+
256
+ raise ArgumentError, "McpPack debug_report requires an environment target"
257
+ end
258
+
259
+ def creator_arguments(include_scope: false, include_root: false, require_name: false, require_root: false)
260
+ arguments = [
261
+ Mcp::ToolArgument.new(name: :name, type: :string, summary: "Pack name without the trailing _pack suffix.",
262
+ required: require_name),
263
+ Mcp::ToolArgument.new(name: :kind, type: :symbol, summary: "Explicit pack kind when not inferred.",
264
+ enum: %i[feature operational bundle]),
265
+ Mcp::ToolArgument.new(name: :namespace, type: :string,
266
+ summary: "Ruby namespace for generated pack constants.", default: "MyCompany::IgniterPacks"),
267
+ Mcp::ToolArgument.new(name: :profile, type: :symbol, summary: "Named creator profile.",
268
+ enum: CreatorPack.available_profiles),
269
+ Mcp::ToolArgument.new(name: :capabilities, type: :symbol_array,
270
+ summary: "Capabilities used to infer or refine the profile."),
271
+ Mcp::ToolArgument.new(name: :pack, type: :pack_reference,
272
+ summary: "Custom pack module for audit-aware creator flows."),
273
+ Mcp::ToolArgument.new(name: :mode, type: :symbol, summary: "Writer behavior when files already exist.",
274
+ default: :skip_existing, enum: %i[skip_existing overwrite])
275
+ ]
276
+ if include_scope
277
+ arguments << Mcp::ToolArgument.new(name: :scope, type: :symbol, summary: "Target packaging scope.",
278
+ required: false, enum: CreatorPack.available_scopes)
279
+ end
280
+ if include_root
281
+ arguments << Mcp::ToolArgument.new(name: :root, type: :string,
282
+ summary: "Filesystem root for generated files.", required: require_root)
283
+ end
284
+ if include_scope && require_root
285
+ arguments.find { |argument| argument.name == :scope }&.tap do |argument|
286
+ arguments[arguments.index(argument)] = Mcp::ToolArgument.new(
287
+ name: :scope,
288
+ type: :symbol,
289
+ summary: "Target packaging scope.",
290
+ required: true,
291
+ enum: CreatorPack.available_scopes
292
+ )
293
+ end
294
+ end
295
+ arguments
296
+ end
297
+
298
+ def creator_session_from(arguments, target:)
299
+ Mcp::CreatorSession.new(
300
+ name: arguments[:name],
301
+ kind: arguments[:kind],
302
+ namespace: arguments.fetch(:namespace, "MyCompany::IgniterPacks"),
303
+ profile: arguments[:profile],
304
+ capabilities: arguments[:capabilities],
305
+ scope: arguments[:scope],
306
+ root: arguments[:root],
307
+ mode: arguments.fetch(:mode, :skip_existing),
308
+ pack: arguments[:pack],
309
+ target_profile: profile_from(target, optional: true)
310
+ )
311
+ end
312
+
313
+ def session_from(payload, target:)
314
+ Mcp::CreatorSession.from_h(
315
+ symbolize_keys(payload),
316
+ target_profile: profile_from(target, optional: true)
317
+ )
318
+ end
319
+
320
+ def symbolize_keys(value)
321
+ case value
322
+ when Hash
323
+ value.each_with_object({}) do |(key, nested), memo|
324
+ memo[key.respond_to?(:to_sym) ? key.to_sym : key] = symbolize_keys(nested)
325
+ end
326
+ when Array
327
+ value.map { |item| symbolize_keys(item) }
328
+ else
329
+ value
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Provenance
7
+ class Builder
8
+ def self.build(output_name, result)
9
+ new(result).build(output_name)
10
+ end
11
+
12
+ def initialize(result)
13
+ @result = result
14
+ @compiled_graph = result.compiled_graph
15
+ @operations = @compiled_graph.operations.reject(&:output?).each_with_object({}) do |operation, memo|
16
+ memo[operation.name] = operation
17
+ end
18
+ end
19
+
20
+ def build(output_name)
21
+ output_name = output_name.to_sym
22
+
23
+ raise Igniter::Contracts::Error, "execution result does not carry a compiled graph" unless @compiled_graph
24
+
25
+ unless output_names.include?(output_name)
26
+ raise Igniter::Contracts::Error,
27
+ "no output named '#{output_name}' in compiled graph"
28
+ end
29
+
30
+ source_operation = @operations.fetch(output_name) do
31
+ raise Igniter::Contracts::Error,
32
+ "source node '#{output_name}' for output '#{output_name}' not found in compiled graph"
33
+ end
34
+
35
+ Lineage.new(build_trace(source_operation, {}))
36
+ end
37
+
38
+ private
39
+
40
+ def output_names
41
+ @output_names ||= @compiled_graph.operations.select(&:output?).map(&:name)
42
+ end
43
+
44
+ def build_trace(operation, memo)
45
+ return memo[operation.name] if memo.key?(operation.name)
46
+
47
+ memo[operation.name] = nil
48
+
49
+ contributing = dependency_names_for(operation).each_with_object({}) do |dependency_name, acc|
50
+ dependency_operation = @operations[dependency_name]
51
+ next unless dependency_operation
52
+
53
+ acc[dependency_name] = build_trace(dependency_operation, memo)
54
+ end
55
+
56
+ trace = NodeTrace.new(
57
+ name: operation.name,
58
+ kind: operation.kind,
59
+ value: resolved_value_for(operation),
60
+ contributing: contributing
61
+ )
62
+
63
+ memo[operation.name] = trace
64
+ end
65
+
66
+ def dependency_names_for(operation)
67
+ names = []
68
+ names.concat(Array(operation.attributes[:depends_on])) if operation.attribute?(:depends_on)
69
+ names << operation.attributes[:from] if operation.attribute?(:from)
70
+ names.map(&:to_sym).uniq
71
+ end
72
+
73
+ def resolved_value_for(operation)
74
+ @result.state.fetch(operation.name)
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Provenance
7
+ class Lineage
8
+ attr_reader :trace
9
+
10
+ def initialize(trace)
11
+ @trace = trace
12
+ freeze
13
+ end
14
+
15
+ def output_name
16
+ trace.name
17
+ end
18
+
19
+ def value
20
+ trace.value
21
+ end
22
+
23
+ def contributing_inputs
24
+ trace.contributing_inputs
25
+ end
26
+
27
+ def sensitive_to?(input_name)
28
+ trace.sensitive_to?(input_name)
29
+ end
30
+
31
+ def path_to(input_name)
32
+ trace.path_to(input_name)
33
+ end
34
+
35
+ def explain
36
+ TextFormatter.format(trace)
37
+ end
38
+
39
+ alias to_s explain
40
+
41
+ def to_h
42
+ serialize(trace)
43
+ end
44
+
45
+ private
46
+
47
+ def serialize(trace)
48
+ {
49
+ node: trace.name,
50
+ kind: trace.kind,
51
+ value: trace.value,
52
+ contributing: trace.contributing.transform_values { |dependency| serialize(dependency) }
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Provenance
7
+ class NodeTrace
8
+ attr_reader :name, :kind, :value, :contributing
9
+
10
+ def initialize(name:, kind:, value:, contributing: {})
11
+ @name = name.to_sym
12
+ @kind = kind.to_sym
13
+ @value = value
14
+ @contributing = contributing.freeze
15
+ freeze
16
+ end
17
+
18
+ def input?
19
+ kind == :input
20
+ end
21
+
22
+ def leaf?
23
+ contributing.empty?
24
+ end
25
+
26
+ def contributing_inputs
27
+ return { name => value } if input?
28
+
29
+ contributing.each_value.with_object({}) do |trace, memo|
30
+ memo.merge!(trace.contributing_inputs)
31
+ end
32
+ end
33
+
34
+ def sensitive_to?(input_name)
35
+ contributing_inputs.key?(input_name.to_sym)
36
+ end
37
+
38
+ def path_to(input_name)
39
+ target = input_name.to_sym
40
+ return [name] if name == target
41
+
42
+ contributing.each_value do |trace|
43
+ path = trace.path_to(target)
44
+ return [name] + path if path
45
+ end
46
+
47
+ nil
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Provenance
7
+ module TextFormatter
8
+ VALUE_MAX_LENGTH = 60
9
+
10
+ module_function
11
+
12
+ def format(trace)
13
+ lines = []
14
+ render(trace, lines, prefix: "", is_root: true, is_last: true)
15
+ lines.join("\n")
16
+ end
17
+
18
+ def render(trace, lines, prefix:, is_root:, is_last:)
19
+ if is_root
20
+ connector = ""
21
+ child_padding = ""
22
+ elsif is_last
23
+ connector = "└─ "
24
+ child_padding = " "
25
+ else
26
+ connector = "├─ "
27
+ child_padding = "│ "
28
+ end
29
+
30
+ child_prefix = prefix + child_padding
31
+ lines << "#{prefix}#{connector}#{trace.name} = #{format_value(trace.value)} [#{trace.kind}]"
32
+
33
+ dependencies = trace.contributing.values
34
+ dependencies.each_with_index do |dependency, index|
35
+ render(
36
+ dependency,
37
+ lines,
38
+ prefix: child_prefix,
39
+ is_root: false,
40
+ is_last: index == dependencies.length - 1
41
+ )
42
+ end
43
+ end
44
+
45
+ def format_value(value)
46
+ rendered = case value
47
+ when nil then "nil"
48
+ when String, Symbol then value.inspect
49
+ when Hash then "{#{value.map { |key, entry| "#{key}: #{entry.inspect}" }.join(", ")}}"
50
+ when Array then "[#{value.map(&:inspect).join(", ")}]"
51
+ else value.inspect
52
+ end
53
+
54
+ return rendered if rendered.length <= VALUE_MAX_LENGTH
55
+
56
+ "#{rendered[0, VALUE_MAX_LENGTH - 3]}..."
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end