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,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Creator
7
+ class Wizard
8
+ PROFILE_EXAMPLES = {
9
+ feature_node: ["examples/contracts/build_your_own_pack.rb"],
10
+ diagnostic_bundle: ["examples/contracts/debug.rb", "examples/contracts/debug_pack_authoring.rb"],
11
+ operational_adapter: ["examples/contracts/build_effect_executor_pack.rb", "examples/contracts/journal.rb"],
12
+ bundle_pack: ["examples/contracts/compose_your_own_packs.rb", "examples/contracts/commerce.rb"]
13
+ }.freeze
14
+
15
+ attr_reader :name, :kind, :namespace, :profile, :capabilities, :scope, :root, :mode, :pack, :target_profile
16
+
17
+ def initialize(name: nil, kind: nil, namespace: "MyCompany::IgniterPacks", profile: nil, capabilities: nil,
18
+ scope: nil, root: nil, mode: :skip_existing, pack: nil, target_profile: nil)
19
+ @name = normalize_name(name)
20
+ @kind = kind&.to_sym
21
+ @namespace = normalize_namespace(namespace)
22
+ @profile = profile&.to_sym
23
+ @capabilities = Array(capabilities).map(&:to_sym).uniq.freeze
24
+ @scope = scope&.to_sym
25
+ @root = root&.to_s
26
+ @mode = mode.to_sym
27
+ @pack = pack
28
+ @target_profile = target_profile
29
+ freeze
30
+ end
31
+
32
+ def apply(**updates)
33
+ self.class.new(
34
+ name: updates.fetch(:name, name),
35
+ kind: updates.fetch(:kind, kind),
36
+ namespace: updates.fetch(:namespace, namespace),
37
+ profile: updates.fetch(:profile, profile),
38
+ capabilities: updates.fetch(:capabilities, capabilities),
39
+ scope: updates.fetch(:scope, scope),
40
+ root: updates.fetch(:root, root),
41
+ mode: updates.fetch(:mode, mode),
42
+ pack: updates.fetch(:pack, pack),
43
+ target_profile: updates.fetch(:target_profile, target_profile)
44
+ )
45
+ end
46
+
47
+ def authoring_profile
48
+ return nil unless profile || kind || !capabilities.empty?
49
+
50
+ Profile.build(
51
+ profile: profile,
52
+ kind: kind,
53
+ capabilities: capabilities
54
+ )
55
+ end
56
+
57
+ def target_scope
58
+ return nil unless scope
59
+
60
+ Scope.build(scope)
61
+ end
62
+
63
+ def ready_for_workflow?
64
+ !name.nil? && !authoring_profile.nil? && !target_scope.nil?
65
+ end
66
+
67
+ def ready_for_writer?
68
+ ready_for_workflow? && !effective_root.nil?
69
+ end
70
+
71
+ def effective_root
72
+ return root unless root.nil? || root.empty?
73
+ return nil unless scope
74
+
75
+ suggested_root
76
+ end
77
+
78
+ def suggested_root
79
+ case scope
80
+ when :standalone_gem
81
+ name ? "./#{name}" : "./my_pack"
82
+ when :app_local, :monorepo_package
83
+ "."
84
+ end
85
+ end
86
+
87
+ def pending_decisions
88
+ decisions = []
89
+ unless name
90
+ decisions << {
91
+ key: :name,
92
+ prompt: "Choose a short pack name",
93
+ options: [],
94
+ hints: ["pick the public noun you want pack users to reach for"]
95
+ }
96
+ end
97
+
98
+ unless authoring_profile
99
+ decisions << {
100
+ key: :profile,
101
+ prompt: "Choose an authoring profile or provide capabilities",
102
+ options: profile_options,
103
+ hints: ["feature packs add graph semantics; operational packs add effect/executor behavior; bundles compose packs without semantic mutation"]
104
+ }
105
+ end
106
+
107
+ unless target_scope
108
+ decisions << {
109
+ key: :scope,
110
+ prompt: "Choose the target scope for the pack",
111
+ options: scope_options,
112
+ hints: branching_hints
113
+ }
114
+ end
115
+
116
+ if ready_for_workflow? && root.nil?
117
+ decisions << {
118
+ key: :root,
119
+ prompt: "Choose the filesystem root for generated files",
120
+ options: [suggested_root].compact,
121
+ hints: ["default root follows the chosen scope; override it only if the host repo layout needs it"]
122
+ }
123
+ end
124
+
125
+ decisions
126
+ end
127
+
128
+ def current_decision
129
+ pending_decisions.first
130
+ end
131
+
132
+ def recommended_packs
133
+ return { runtime: [], development: [] } unless authoring_profile
134
+
135
+ {
136
+ runtime: authoring_profile.runtime_dependency_hints,
137
+ development: authoring_profile.development_dependency_hints
138
+ }
139
+ end
140
+
141
+ def recommended_examples
142
+ return [] unless authoring_profile
143
+
144
+ PROFILE_EXAMPLES.fetch(authoring_profile.name, PROFILE_EXAMPLES.fetch(fallback_profile_name, []))
145
+ end
146
+
147
+ def branching_hints
148
+ return [] unless authoring_profile
149
+
150
+ hints = []
151
+ case authoring_profile.kind
152
+ when :operational
153
+ hints << "operational adapters usually want dev-time help from Igniter::Extensions::Contracts::JournalPack"
154
+ hints << "prefer standalone_gem when the adapter will be reused across hosts"
155
+ when :bundle
156
+ if authoring_profile.capability?(:diagnostic)
157
+ hints << "diagnostic bundles usually compose Igniter::Extensions::Contracts::ExecutionReportPack and Igniter::Extensions::Contracts::ProvenancePack"
158
+ hints << "keep DebugPack as a development helper unless the bundle truly needs a runtime-facing debug surface"
159
+ else
160
+ hints << "bundle packs should stay thin and explicit about which packs they compose"
161
+ end
162
+ when :feature
163
+ hints << "feature node packs often start app-local before being promoted into a monorepo package or gem"
164
+ end
165
+
166
+ hints.concat(authoring_profile.development_hints)
167
+ hints.uniq
168
+ end
169
+
170
+ def workflow
171
+ unless ready_for_workflow?
172
+ raise ArgumentError, "creator wizard is missing decisions: #{pending_decisions.map do |decision|
173
+ decision.fetch(:key)
174
+ end.join(", ")}"
175
+ end
176
+
177
+ CreatorPack.workflow(
178
+ name: name,
179
+ kind: kind,
180
+ namespace: namespace,
181
+ profile: profile,
182
+ capabilities: capabilities,
183
+ scope: scope,
184
+ pack: pack,
185
+ target_profile: target_profile
186
+ )
187
+ end
188
+
189
+ def writer
190
+ raise ArgumentError, "creator wizard needs a root before writing scaffold files" unless ready_for_writer?
191
+
192
+ workflow.writer(root: effective_root, mode: mode)
193
+ end
194
+
195
+ def to_h
196
+ {
197
+ name: name,
198
+ kind: kind,
199
+ namespace: namespace,
200
+ profile: profile,
201
+ capabilities: capabilities,
202
+ scope: scope,
203
+ root: root,
204
+ suggested_root: suggested_root,
205
+ effective_root: effective_root,
206
+ mode: mode,
207
+ ready_for_workflow: ready_for_workflow?,
208
+ ready_for_writer: ready_for_writer?,
209
+ authoring_profile: authoring_profile&.to_h,
210
+ target_scope: target_scope&.to_h,
211
+ recommended_packs: recommended_packs,
212
+ recommended_examples: recommended_examples,
213
+ branching_hints: branching_hints,
214
+ pending_decisions: pending_decisions
215
+ }
216
+ end
217
+
218
+ private
219
+
220
+ def fallback_profile_name
221
+ case authoring_profile&.kind
222
+ when :operational
223
+ :operational_adapter
224
+ when :bundle
225
+ authoring_profile&.capability?(:diagnostic) ? :diagnostic_bundle : :bundle_pack
226
+ else
227
+ :feature_node
228
+ end
229
+ end
230
+
231
+ def profile_options
232
+ Profile.available.map do |available_profile|
233
+ built = Profile.build(profile: available_profile)
234
+ {
235
+ key: available_profile,
236
+ kind: built.kind,
237
+ summary: built.summary,
238
+ capabilities: built.capabilities
239
+ }
240
+ end
241
+ end
242
+
243
+ def scope_options
244
+ Scope.available.map do |available_scope|
245
+ built = Scope.build(available_scope)
246
+ {
247
+ key: available_scope,
248
+ root: built.root,
249
+ summary: built.packaging_hints.first
250
+ }
251
+ end
252
+ end
253
+
254
+ def normalize_name(value)
255
+ return nil if value.nil?
256
+
257
+ normalized = value.to_s.strip.gsub(/_pack\z/, "").downcase
258
+ normalized.empty? ? nil : normalized
259
+ end
260
+
261
+ def normalize_namespace(value)
262
+ normalized = value.to_s.strip
263
+ normalized.empty? ? "MyCompany::IgniterPacks" : normalized
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "workflow_step"
4
+ require_relative "writer"
5
+
6
+ module Igniter
7
+ module Extensions
8
+ module Contracts
9
+ module Creator
10
+ class Workflow
11
+ attr_reader :report
12
+
13
+ def initialize(report:)
14
+ @report = report
15
+ freeze
16
+ end
17
+
18
+ def scaffold
19
+ report.scaffold
20
+ end
21
+
22
+ def audit
23
+ report.audit
24
+ end
25
+
26
+ def profile
27
+ scaffold.profile
28
+ end
29
+
30
+ def scope
31
+ scaffold.scope
32
+ end
33
+
34
+ def recommended_packs
35
+ {
36
+ runtime: profile.runtime_dependency_hints,
37
+ development: profile.development_dependency_hints
38
+ }
39
+ end
40
+
41
+ def stages
42
+ [
43
+ design_stage,
44
+ scaffold_stage,
45
+ implementation_stage,
46
+ validation_stage,
47
+ packaging_stage
48
+ ]
49
+ end
50
+
51
+ def current_stage
52
+ stages.find { |stage| !stage.complete? } || stages.last
53
+ end
54
+
55
+ def ready_for_packaging?
56
+ packaging_stage.status == :ready
57
+ end
58
+
59
+ def writer(root:, mode: :skip_existing)
60
+ Writer.new(workflow: self, root: root, mode: mode)
61
+ end
62
+
63
+ def to_h
64
+ {
65
+ scaffold: scaffold.to_h,
66
+ report: report.to_h,
67
+ recommended_packs: recommended_packs,
68
+ current_stage: current_stage.to_h,
69
+ ready_for_packaging: ready_for_packaging?,
70
+ stages: stages.map(&:to_h)
71
+ }
72
+ end
73
+
74
+ private
75
+
76
+ def design_stage
77
+ hints = []
78
+ hints << "runtime pack recommendations: #{profile.runtime_dependency_hints.join(", ")}" unless profile.runtime_dependency_hints.empty?
79
+ hints << "development pack recommendations: #{profile.development_dependency_hints.join(", ")}" unless profile.development_dependency_hints.empty?
80
+
81
+ WorkflowStep.new(
82
+ key: :select_profile,
83
+ status: :complete,
84
+ title: "Select Authoring Profile",
85
+ summary: "#{profile.name} -> #{profile.summary}; target scope #{scope.name}",
86
+ hints: hints
87
+ )
88
+ end
89
+
90
+ def scaffold_stage
91
+ WorkflowStep.new(
92
+ key: :generate_scaffold,
93
+ status: :complete,
94
+ title: "Generate Scaffold",
95
+ summary: "generated #{scaffold.files.size} files rooted at #{scaffold.pack_file_path}",
96
+ hints: scaffold.files.keys
97
+ )
98
+ end
99
+
100
+ def implementation_stage
101
+ if audit&.ok?
102
+ WorkflowStep.new(
103
+ key: :implement_pack,
104
+ status: :complete,
105
+ title: "Implement Pack",
106
+ summary: "#{scaffold.pack_constant} satisfies the current install/finalize seam checks"
107
+ )
108
+ elsif audit
109
+ WorkflowStep.new(
110
+ key: :implement_pack,
111
+ status: :needs_attention,
112
+ title: "Implement Pack",
113
+ summary: "fill the missing pack seams in #{scaffold.pack_file_path}",
114
+ hints: implementation_hints
115
+ )
116
+ else
117
+ WorkflowStep.new(
118
+ key: :implement_pack,
119
+ status: :ready,
120
+ title: "Implement Pack",
121
+ summary: "fill in #{scaffold.pack_file_path} and keep the pack on public contracts only",
122
+ hints: report.next_steps
123
+ )
124
+ end
125
+ end
126
+
127
+ def validation_stage
128
+ if audit&.ok?
129
+ WorkflowStep.new(
130
+ key: :validate_pack,
131
+ status: :complete,
132
+ title: "Validate Pack",
133
+ summary: "audit passed; the pack finalizes cleanly against the current contracts profile"
134
+ )
135
+ elsif audit
136
+ WorkflowStep.new(
137
+ key: :validate_pack,
138
+ status: :needs_attention,
139
+ title: "Validate Pack",
140
+ summary: "audit_pack found missing seams before finalize",
141
+ hints: [audit.explain]
142
+ )
143
+ else
144
+ WorkflowStep.new(
145
+ key: :validate_pack,
146
+ status: :ready,
147
+ title: "Validate Pack",
148
+ summary: "run audit_pack once the implementation is in place",
149
+ hints: ["Igniter::Extensions::Contracts.audit_pack(#{scaffold.pack_constant}, environment)"]
150
+ )
151
+ end
152
+ end
153
+
154
+ def packaging_stage
155
+ if audit&.ok?
156
+ WorkflowStep.new(
157
+ key: :package_pack,
158
+ status: :ready,
159
+ title: "Package Pack",
160
+ summary: "the pack is ready for #{scope.name} packaging review",
161
+ hints: scope.packaging_hints
162
+ )
163
+ else
164
+ WorkflowStep.new(
165
+ key: :package_pack,
166
+ status: :pending,
167
+ title: "Package Pack",
168
+ summary: "finish implementation and validation before packaging for #{scope.name}",
169
+ hints: scope.packaging_hints
170
+ )
171
+ end
172
+ end
173
+
174
+ def implementation_hints
175
+ hints = []
176
+ hints << "define node kinds: #{audit.missing_node_definitions.join(", ")}" unless audit.missing_node_definitions.empty?
177
+ hints << "register DSL keywords: #{audit.missing_dsl_keywords.join(", ")}" unless audit.missing_dsl_keywords.empty?
178
+ hints << "register runtime handlers: #{audit.missing_runtime_handlers.join(", ")}" unless audit.missing_runtime_handlers.empty?
179
+ audit.missing_registry_contracts.each do |registry, keys|
180
+ hints << "register #{registry}: #{keys.join(", ")}"
181
+ end
182
+ hints << "resolve finalize error: #{audit.finalize_error}" if audit.finalize_error
183
+ hints
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Creator
7
+ class WorkflowStep
8
+ VALID_STATUSES = %i[complete ready needs_attention pending].freeze
9
+
10
+ attr_reader :key, :status, :title, :summary, :hints
11
+
12
+ def initialize(key:, status:, title:, summary:, hints: [])
13
+ @key = key.to_sym
14
+ @status = normalize_status(status)
15
+ @title = title
16
+ @summary = summary
17
+ @hints = Array(hints).map(&:to_s).uniq.freeze
18
+ freeze
19
+ end
20
+
21
+ def complete?
22
+ status == :complete
23
+ end
24
+
25
+ def actionable?
26
+ %i[ready needs_attention].include?(status)
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ key: key,
32
+ status: status,
33
+ title: title,
34
+ summary: summary,
35
+ hints: hints
36
+ }
37
+ end
38
+
39
+ private
40
+
41
+ def normalize_status(status)
42
+ value = status.to_sym
43
+ return value if VALID_STATUSES.include?(value)
44
+
45
+ raise ArgumentError, "unsupported creator workflow step status #{status.inspect}"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Creator
7
+ class WriteResult
8
+ attr_reader :root, :mode, :steps
9
+
10
+ def initialize(root:, mode:, steps:)
11
+ @root = root
12
+ @mode = mode.to_sym
13
+ @steps = steps.freeze
14
+ freeze
15
+ end
16
+
17
+ def files_written
18
+ steps.count { |step| step.kind == :file && %i[written unchanged].include?(step.status) }
19
+ end
20
+
21
+ def files_skipped
22
+ steps.count { |step| step.kind == :file && step.status == :skipped }
23
+ end
24
+
25
+ def directories_created
26
+ steps.count { |step| step.kind == :directory && step.status == :created }
27
+ end
28
+
29
+ def success?
30
+ steps.none?(&:actionable?)
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ root: root,
36
+ mode: mode,
37
+ files_written: files_written,
38
+ files_skipped: files_skipped,
39
+ directories_created: directories_created,
40
+ success: success?,
41
+ steps: steps.map(&:to_h)
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Creator
7
+ class WriteStep
8
+ VALID_KINDS = %i[directory file].freeze
9
+ VALID_STATUSES = %i[pending created written skipped unchanged].freeze
10
+
11
+ attr_reader :kind, :relative_path, :absolute_path, :status, :reason
12
+
13
+ def initialize(kind:, relative_path:, absolute_path:, status:, reason: nil)
14
+ @kind = normalize_kind(kind)
15
+ @relative_path = relative_path.to_s
16
+ @absolute_path = absolute_path.to_s
17
+ @status = normalize_status(status)
18
+ @reason = reason
19
+ freeze
20
+ end
21
+
22
+ def actionable?
23
+ status == :pending
24
+ end
25
+
26
+ def written?
27
+ %i[created written unchanged].include?(status)
28
+ end
29
+
30
+ def skipped?
31
+ status == :skipped
32
+ end
33
+
34
+ def to_h
35
+ {
36
+ kind: kind,
37
+ relative_path: relative_path,
38
+ absolute_path: absolute_path,
39
+ status: status,
40
+ reason: reason
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ def normalize_kind(kind)
47
+ value = kind.to_sym
48
+ return value if VALID_KINDS.include?(value)
49
+
50
+ raise ArgumentError, "unsupported creator write step kind #{kind.inspect}"
51
+ end
52
+
53
+ def normalize_status(status)
54
+ value = status.to_sym
55
+ return value if VALID_STATUSES.include?(value)
56
+
57
+ raise ArgumentError, "unsupported creator write step status #{status.inspect}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end