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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d70d38d90890f77551608abf6bb1989a7f51cd4c5973261f915145ef4abe05af
4
+ data.tar.gz: 1a842b9b1a9e7a7aa76ba3526055ec80adc482448fc55a31af47de8a6ba8d936
5
+ SHA512:
6
+ metadata.gz: ddea43ae41d6914a88e5cc973565e53099c1ca19ec1b8b4f11df312e135839b414582cd41f72e513b7c01a3f57be49ef25802e76b98e16cedbc84675e53ac1ce
7
+ data.tar.gz: 73905d60c99ebf3814607eebc3e27544d403aff0b47ce77559dec53fdf6c384bf8d93d92e487134a9b849ac86e3f1127e7c87d5c4f7113bc3aad71cb85009d06
data/README.md ADDED
@@ -0,0 +1,381 @@
1
+ # igniter-extensions
2
+
3
+ Contracts-native extension packs for Igniter.
4
+
5
+ This package now focuses only on packs built on top of `Igniter::Contracts`.
6
+
7
+ Primary entrypoints:
8
+
9
+ - `require "igniter-extensions"`
10
+ - `require "igniter/extensions/contracts"`
11
+
12
+ Contracts-facing external packs now live here too:
13
+
14
+ - `Igniter::Extensions::Contracts::ExecutionReportPack`
15
+ - `Igniter::Extensions::Contracts::LookupPack`
16
+ - `Igniter::Extensions::Contracts::AggregatePack`
17
+ - `Igniter::Extensions::Contracts::AuditPack`
18
+ - `Igniter::Extensions::Contracts::BranchPack`
19
+ - `Igniter::Extensions::Contracts::CapabilitiesPack`
20
+ - `Igniter::Extensions::Contracts::CollectionPack`
21
+ - `Igniter::Extensions::Contracts::CommercePack`
22
+ - `Igniter::Extensions::Contracts::ComposePack`
23
+ - `Igniter::Extensions::Contracts::ContentAddressingPack`
24
+ - `Igniter::Extensions::Contracts::CreatorPack`
25
+ - `Igniter::Extensions::Contracts::DataflowPack`
26
+ - `Igniter::Extensions::Contracts::DebugPack`
27
+ - `Igniter::Extensions::Contracts::DifferentialPack`
28
+ - `Igniter::Extensions::Contracts::JournalPack`
29
+ - `Igniter::Extensions::Contracts::InvariantsPack`
30
+ - `Igniter::Extensions::Contracts::McpPack`
31
+ - `Igniter::Extensions::Contracts::ProvenancePack`
32
+ - `Igniter::Extensions::Contracts::ReactivePack`
33
+ - `Igniter::Extensions::Contracts::SagaPack`
34
+
35
+ Those packs install into `Igniter::Contracts` through the public facade only:
36
+
37
+ ```ruby
38
+ require "igniter/extensions/contracts"
39
+
40
+ environment = Igniter::Extensions::Contracts.with
41
+
42
+ result = environment.run(inputs: { rates: { ua: 0.2 } }) do
43
+ input :rates
44
+ lookup :tax_rate, from: :rates, dig: %i[eu ua], default: 0.2
45
+ output :tax_rate
46
+ end
47
+ ```
48
+
49
+ Default helpers like `Igniter::Extensions::Contracts.with` currently install the
50
+ safe default packs (`ExecutionReportPack` and `LookupPack`). Operational packs
51
+ like `JournalPack` stay opt-in:
52
+
53
+ ```ruby
54
+ environment = Igniter::Extensions::Contracts.with(
55
+ Igniter::Extensions::Contracts::JournalPack
56
+ )
57
+ ```
58
+
59
+ `BranchPack` adds a contracts-native decision DSL that still lowers to ordinary
60
+ `compute` semantics:
61
+
62
+ ```ruby
63
+ environment = Igniter::Contracts.with(
64
+ Igniter::Contracts::ProjectPack,
65
+ Igniter::Extensions::Contracts::BranchPack
66
+ )
67
+
68
+ result = environment.run(inputs: { country: "DE", vip: true }) do
69
+ input :country
70
+ input :vip
71
+
72
+ branch :delivery_strategy, on: :country, depends_on: [:vip] do
73
+ on "UA", id: :local, value: :local
74
+ on matches: /\A[A-Z]{2}\z/, id: :international do |vip:|
75
+ vip ? :priority_international : :international
76
+ end
77
+ default value: :fallback
78
+ end
79
+
80
+ project :delivery_mode, from: :delivery_strategy, key: :value
81
+ output :delivery_mode
82
+ end
83
+ ```
84
+
85
+ `ComposePack` adds explicit nested contract invocation without restoring legacy
86
+ composition semantics into the kernel:
87
+
88
+ ```ruby
89
+ environment = Igniter::Contracts.with(
90
+ Igniter::Extensions::Contracts::ComposePack
91
+ )
92
+
93
+ pricing_contract = environment.compile do
94
+ input :amount
95
+ input :tax_rate
96
+ compute :total, depends_on: %i[amount tax_rate] do |amount:, tax_rate:|
97
+ amount + (amount * tax_rate)
98
+ end
99
+ output :total
100
+ end
101
+
102
+ result = environment.run(inputs: { subtotal: 100, rate: 0.2 }) do
103
+ input :subtotal
104
+ input :rate
105
+
106
+ compose :pricing_total,
107
+ contract: pricing_contract,
108
+ inputs: { amount: :subtotal, tax_rate: :rate },
109
+ output: :total
110
+
111
+ output :pricing_total
112
+ end
113
+ ```
114
+
115
+ The important forward-compatibility rule is that `ComposePack` keeps local
116
+ execution as the default, but also exposes `via:` for a custom invocation
117
+ adapter. That gives `igniter-application` or `igniter-cluster` room to add
118
+ remote compose later without rewriting the DSL contract.
119
+
120
+ `CollectionPack` follows the same idea for keyed collection execution:
121
+
122
+ ```ruby
123
+ environment = Igniter::Contracts.with(
124
+ Igniter::Extensions::Contracts::CollectionPack
125
+ )
126
+
127
+ result = environment.run(inputs: {
128
+ items: [{ sku: "a", amount: 10 }, { sku: "b", amount: 20 }],
129
+ tax_rate: 0.2
130
+ }) do
131
+ input :items
132
+ input :tax_rate
133
+
134
+ collection :priced_items, from: :items, key: :sku, inputs: { tax_rate: :tax_rate } do
135
+ input :sku
136
+ input :amount
137
+ input :tax_rate
138
+
139
+ compute :total, depends_on: %i[amount tax_rate] do |amount:, tax_rate:|
140
+ amount + (amount * tax_rate)
141
+ end
142
+
143
+ output :total
144
+ end
145
+
146
+ output :priced_items
147
+ end
148
+ ```
149
+
150
+ It returns a `CollectionResult` keyed by item identity, and keeps `via:` open
151
+ for a future remote/distributed collection invoker without changing the user DSL.
152
+
153
+ Applied presets can sit on top of those packs too:
154
+
155
+ ```ruby
156
+ environment = Igniter::Extensions::Contracts.with_preset(:commerce)
157
+ ```
158
+
159
+ For explicit content-addressed reuse, the contracts-side replacement is
160
+ `ContentAddressingPack`:
161
+
162
+ ```ruby
163
+ environment = Igniter::Contracts.with(
164
+ Igniter::Extensions::Contracts::ContentAddressingPack
165
+ )
166
+
167
+ tax = Igniter::Extensions::Contracts.content_addressed(
168
+ fingerprint: "tax_v1"
169
+ ) do |country:, amount:|
170
+ { ua: 0.2, us: 0.1 }.fetch(country) * amount
171
+ end
172
+ ```
173
+
174
+ For developer-focused observability, `DebugPack` can bundle profile,
175
+ compilation, execution, diagnostics, and provenance into one report:
176
+
177
+ ```ruby
178
+ environment = Igniter::Extensions::Contracts.with(
179
+ Igniter::Extensions::Contracts::DebugPack
180
+ )
181
+
182
+ report = Igniter::Extensions::Contracts.debug_report(
183
+ environment,
184
+ inputs: { amount: 10 }
185
+ ) do
186
+ input :amount
187
+ output :amount
188
+ end
189
+ ```
190
+
191
+ It can also audit a custom pack before finalize, which is the first bridge
192
+ toward a future `CreatorPack` workflow:
193
+
194
+ ```ruby
195
+ audit = Igniter::Extensions::Contracts.audit_pack(MyDraftPack, environment)
196
+
197
+ audit.ok?
198
+ audit.missing_node_definitions
199
+ audit.missing_registry_contracts
200
+ audit.finalize_error
201
+ ```
202
+
203
+ `CreatorPack` now adds a minimal scaffold/report workflow on top of that:
204
+
205
+ ```ruby
206
+ scaffold = Igniter::Extensions::Contracts.scaffold_pack(
207
+ name: :slug,
208
+ profile: :feature_node,
209
+ scope: :app_local,
210
+ namespace: "MyCompany::IgniterPacks"
211
+ )
212
+
213
+ report = Igniter::Extensions::Contracts.creator_report(
214
+ name: :slug,
215
+ profile: :feature_node
216
+ )
217
+
218
+ workflow = Igniter::Extensions::Contracts.creator_workflow(
219
+ name: :slug,
220
+ profile: :feature_node,
221
+ scope: :standalone_gem
222
+ )
223
+ ```
224
+
225
+ Available authoring profiles:
226
+
227
+ - `:feature_node`
228
+ - `:operational_adapter`
229
+ - `:diagnostic_bundle`
230
+ - `:bundle_pack`
231
+
232
+ Available target scopes:
233
+
234
+ - `:app_local`
235
+ - `:monorepo_package`
236
+ - `:standalone_gem`
237
+
238
+ The workflow helper turns those decisions into an explicit authoring ladder:
239
+
240
+ - profile/scope selection
241
+ - scaffold generation
242
+ - implementation
243
+ - audit validation
244
+ - packaging readiness
245
+
246
+ It also separates recommended runtime dependency packs from development-only
247
+ tooling packs, so authoring guidance does not accidentally become runtime
248
+ bundle surface.
249
+
250
+ There is also a stateful wizard layer that can hold partial decisions before
251
+ you are ready to generate files:
252
+
253
+ ```ruby
254
+ wizard = Igniter::Extensions::Contracts.creator_wizard(
255
+ name: :delivery,
256
+ capabilities: %i[effect executor]
257
+ )
258
+
259
+ wizard.current_decision
260
+ wizard.branching_hints
261
+ wizard.recommended_examples
262
+ completed = wizard.apply(scope: :standalone_gem)
263
+ ```
264
+
265
+ For file generation, `CreatorPack` also exposes a multi-step writer with
266
+ explicit planning:
267
+
268
+ ```ruby
269
+ writer = Igniter::Extensions::Contracts.creator_writer(
270
+ name: :slug,
271
+ profile: :feature_node,
272
+ scope: :app_local,
273
+ root: "/tmp/my_pack"
274
+ )
275
+
276
+ plan = writer.plan
277
+ result = writer.write
278
+ ```
279
+
280
+ By default the writer uses `:skip_existing`, so existing files are preserved
281
+ unless you explicitly opt into `mode: :overwrite`.
282
+
283
+ `McpPack` is the first thin tooling adapter over those stabilized surfaces:
284
+
285
+ ```ruby
286
+ environment = Igniter::Extensions::Contracts.with(
287
+ Igniter::Extensions::Contracts::McpPack
288
+ )
289
+
290
+ Igniter::Extensions::Contracts.mcp_tools
291
+ result = Igniter::Extensions::Contracts.mcp_call(
292
+ :creator_wizard,
293
+ target: environment,
294
+ name: :delivery,
295
+ capabilities: %i[effect executor]
296
+ )
297
+ ```
298
+
299
+ The goal is to adapt existing debug/creator primitives for external tools, not
300
+ to invent a second authoring stack.
301
+
302
+ For stepwise external tooling, `McpPack` also exposes a serialized
303
+ `creator_session` flow:
304
+
305
+ ```ruby
306
+ session = Igniter::Extensions::Contracts.mcp_creator_session(
307
+ target: environment,
308
+ name: :delivery,
309
+ capabilities: %i[effect executor]
310
+ )
311
+
312
+ updated = Igniter::Extensions::Contracts.mcp_call(
313
+ :creator_session_apply,
314
+ target: environment,
315
+ session: session.to_h.fetch(:payload).fetch(:session),
316
+ updates: { scope: :standalone_gem }
317
+ )
318
+ ```
319
+
320
+ You can also drive scaffolding directly from capabilities:
321
+
322
+ ```ruby
323
+ Igniter::Extensions::Contracts.scaffold_pack(
324
+ name: :delivery,
325
+ capabilities: %i[effect executor]
326
+ )
327
+ ```
328
+
329
+ Older extension activators still exist for migration scenarios:
330
+
331
+ - `require "igniter/extensions/auditing"`
332
+ - `require "igniter/extensions/capabilities"`
333
+ - `require "igniter/extensions/dataflow"`
334
+ - `require "igniter/extensions/saga"`
335
+ - `require "igniter/extensions/provenance"`
336
+ - `require "igniter/extensions/differential"`
337
+ - `require "igniter/extensions/incremental"`
338
+ - `require "igniter/extensions/reactive"`
339
+ - `require "igniter/extensions/invariants"`
340
+
341
+ Those activators are migration context, not the long-term extension model.
342
+
343
+ The first canonical activator-to-pack migration target is now explicit:
344
+
345
+ - `require "igniter/extensions/execution_report"`
346
+ -> `Igniter::Extensions::Contracts::ExecutionReportPack`
347
+ - `require "igniter/extensions/auditing"`
348
+ -> `Igniter::Extensions::Contracts::AuditPack`
349
+ - `require "igniter/extensions/capabilities"`
350
+ -> `Igniter::Extensions::Contracts::CapabilitiesPack`
351
+ - `require "igniter/extensions/dataflow"`
352
+ -> `Igniter::Extensions::Contracts::DataflowPack`
353
+ - `require "igniter/extensions/provenance"`
354
+ -> `Igniter::Extensions::Contracts::ProvenancePack`
355
+ - `require "igniter/extensions/saga"`
356
+ -> `Igniter::Extensions::Contracts::SagaPack`
357
+ - `require "igniter/extensions/incremental"`
358
+ -> `Igniter::Extensions::Contracts::IncrementalPack`
359
+ - `require "igniter/extensions/differential"`
360
+ -> `Igniter::Extensions::Contracts::DifferentialPack`
361
+ - `require "igniter/extensions/reactive"`
362
+ -> `Igniter::Extensions::Contracts::ReactivePack`
363
+ - `require "igniter/extensions/invariants"`
364
+ -> `Igniter::Extensions::Contracts::InvariantsPack`
365
+
366
+ See [examples/contracts/auditing.rb](../../examples/contracts/auditing.rb)
367
+ and [examples/contracts/capabilities.rb](../../examples/contracts/capabilities.rb)
368
+ and [examples/contracts/dataflow.rb](../../examples/contracts/dataflow.rb)
369
+ and [examples/contracts/differential.rb](../../examples/contracts/differential.rb)
370
+ and [examples/contracts/invariants.rb](../../examples/contracts/invariants.rb)
371
+ and [examples/contracts/provenance.rb](../../examples/contracts/provenance.rb)
372
+ and [examples/contracts/reactive.rb](../../examples/contracts/reactive.rb)
373
+ and [examples/contracts/saga.rb](../../examples/contracts/saga.rb)
374
+ and [examples/contracts/incremental.rb](../../examples/contracts/incremental.rb)
375
+ for runnable migration walkthroughs.
376
+
377
+ Docs:
378
+
379
+ - [Guide](../../docs/guide/README.md)
380
+ - [Core guide](../../docs/guide/core.md)
381
+ - [Dev](../../docs/dev/README.md)
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module AggregatePack
7
+ AGGREGATE_KEYWORDS = %i[count sum avg].freeze
8
+
9
+ class << self
10
+ def manifest
11
+ Igniter::Contracts::PackManifest.new(
12
+ name: :extensions_aggregate,
13
+ registry_contracts: AGGREGATE_KEYWORDS.map { |kind| Igniter::Contracts::PackManifest.dsl_keyword(kind) }
14
+ )
15
+ end
16
+
17
+ def install_into(kernel)
18
+ install_dsl_keywords(kernel)
19
+ kernel
20
+ end
21
+
22
+ def install_dsl_keywords(kernel)
23
+ kernel.dsl_keywords.register(:count, count_keyword)
24
+ kernel.dsl_keywords.register(:sum, sum_keyword)
25
+ kernel.dsl_keywords.register(:avg, avg_keyword)
26
+ end
27
+
28
+ def count_keyword
29
+ Igniter::Contracts::DslKeyword.new(:count) do |name, from:, builder:, matching: nil|
30
+ builder.add_operation(
31
+ kind: :compute,
32
+ name: name,
33
+ depends_on: [from.to_sym],
34
+ callable: lambda do |**values|
35
+ items = AggregatePack.enumerable_source(values.fetch(from.to_sym), source_name: from.to_sym,
36
+ operation_name: :count)
37
+
38
+ if matching
39
+ items.count { |item| matching.call(item) }
40
+ else
41
+ items.count
42
+ end
43
+ end
44
+ )
45
+ end
46
+ end
47
+
48
+ def sum_keyword
49
+ Igniter::Contracts::DslKeyword.new(:sum) do |name, from:, builder:, using: nil|
50
+ builder.add_operation(
51
+ kind: :compute,
52
+ name: name,
53
+ depends_on: [from.to_sym],
54
+ callable: lambda do |**values|
55
+ items = AggregatePack.enumerable_source(values.fetch(from.to_sym), source_name: from.to_sym,
56
+ operation_name: :sum)
57
+ items.reduce(0) do |total, item|
58
+ total + AggregatePack.extract_value(item, using)
59
+ end
60
+ end
61
+ )
62
+ end
63
+ end
64
+
65
+ def avg_keyword
66
+ Igniter::Contracts::DslKeyword.new(:avg) do |name, from:, builder:, using: nil|
67
+ builder.add_operation(
68
+ kind: :compute,
69
+ name: name,
70
+ depends_on: [from.to_sym],
71
+ callable: lambda do |**values|
72
+ items = AggregatePack.enumerable_source(values.fetch(from.to_sym), source_name: from.to_sym,
73
+ operation_name: :avg)
74
+ projected = items.map { |item| AggregatePack.extract_value(item, using) }
75
+ next nil if projected.empty?
76
+
77
+ projected.sum.to_f / projected.length
78
+ end
79
+ )
80
+ end
81
+ end
82
+
83
+ def enumerable_source(source, source_name:, operation_name:)
84
+ return source.to_a if source.respond_to?(:to_a)
85
+
86
+ raise TypeError, "#{operation_name} source #{source_name} is not enumerable"
87
+ end
88
+
89
+ def extract_value(item, projection)
90
+ return item if projection.nil?
91
+ return projection.call(item) if projection.respond_to?(:call)
92
+
93
+ key = projection.to_sym
94
+ return item.fetch(key) if item.respond_to?(:key?) && item.key?(key)
95
+ return item.fetch(key.to_s) if item.respond_to?(:key?) && item.key?(key.to_s)
96
+
97
+ item.public_send(key)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Audit
7
+ module Builder
8
+ module_function
9
+
10
+ def build(target)
11
+ result = unwrap_result(target)
12
+ compiled_graph = result.compiled_graph
13
+ outputs = result.outputs.to_h
14
+ state_values = result.state.to_h
15
+
16
+ Snapshot.new(
17
+ graph: graph_name(compiled_graph),
18
+ profile_fingerprint: result.profile_fingerprint,
19
+ events: build_events(compiled_graph, state_values: state_values, outputs: outputs),
20
+ states: build_states(compiled_graph, state_values: state_values, outputs: outputs),
21
+ children: build_children(state_values),
22
+ output_names: compiled_graph.operations.select(&:output?).map(&:name)
23
+ )
24
+ end
25
+
26
+ def unwrap_result(target)
27
+ return target.execution_result if target.respond_to?(:execution_result)
28
+
29
+ target
30
+ end
31
+
32
+ def graph_name(compiled_graph)
33
+ operation_names = compiled_graph.operations.reject(&:output?).map(&:name)
34
+ return "contracts_graph" if operation_names.empty?
35
+
36
+ "contracts_graph(#{operation_names.join(",")})"
37
+ end
38
+
39
+ def build_events(compiled_graph, state_values:, outputs:)
40
+ compiled_graph.operations.each_with_index.map do |operation, index|
41
+ Event.new(
42
+ event_id: "#{operation.kind}:#{operation.name}:#{index}",
43
+ type: event_type_for(operation),
44
+ node_name: operation.name,
45
+ path: [operation.name],
46
+ status: status_for(operation, state_values: state_values, outputs: outputs),
47
+ payload: payload_for(operation, state_values: state_values, outputs: outputs)
48
+ )
49
+ end
50
+ end
51
+
52
+ def build_states(compiled_graph, state_values:, outputs:)
53
+ compiled_graph.operations.each_with_object({}) do |operation, memo|
54
+ next if operation.output?
55
+
56
+ value = operation.output? ? outputs[operation.name] : state_values[operation.name]
57
+ memo[operation.name] = {
58
+ path: [operation.name],
59
+ kind: operation.kind,
60
+ status: status_for(operation, state_values: state_values, outputs: outputs),
61
+ value: serialize_value(value),
62
+ dependencies: dependency_names_for(operation)
63
+ }
64
+ end
65
+ end
66
+
67
+ def build_children(state_values)
68
+ state_values.each_with_object([]) do |(name, value), memo|
69
+ next unless nested_execution_result?(value)
70
+
71
+ memo << {
72
+ node_name: name,
73
+ snapshot: build(value).to_h
74
+ }
75
+ end
76
+ end
77
+
78
+ def event_type_for(operation)
79
+ return :output_observed if operation.output?
80
+
81
+ :"#{operation.kind}_observed"
82
+ end
83
+
84
+ def status_for(operation, state_values:, outputs:)
85
+ collection = operation.output? ? outputs : state_values
86
+ collection.key?(operation.name) ? :succeeded : :missing
87
+ end
88
+
89
+ def payload_for(operation, state_values:, outputs:)
90
+ value = operation.output? ? outputs[operation.name] : state_values[operation.name]
91
+ payload = {
92
+ kind: operation.kind,
93
+ value: serialize_value(value)
94
+ }
95
+
96
+ dependencies = dependency_names_for(operation)
97
+ payload[:dependencies] = dependencies if dependencies.any?
98
+ payload
99
+ end
100
+
101
+ def dependency_names_for(operation)
102
+ names = []
103
+ names.concat(Array(operation.attributes[:depends_on])) if operation.attribute?(:depends_on)
104
+ names << operation.attributes[:from] if operation.attribute?(:from)
105
+ names.map(&:to_sym).uniq
106
+ end
107
+
108
+ def serialize_value(value)
109
+ case value
110
+ when Igniter::Contracts::ExecutionResult
111
+ {
112
+ type: :execution_result,
113
+ profile_fingerprint: value.profile_fingerprint,
114
+ outputs: value.outputs.to_h
115
+ }
116
+ when Array
117
+ value.map { |item| serialize_value(item) }
118
+ when Hash
119
+ value.transform_keys(&:to_sym).transform_values { |item| serialize_value(item) }
120
+ else
121
+ value
122
+ end
123
+ end
124
+
125
+ def nested_execution_result?(value)
126
+ value.is_a?(Igniter::Contracts::ExecutionResult)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Audit
7
+ class Event
8
+ attr_reader :event_id, :type, :node_name, :path, :status, :payload
9
+
10
+ def initialize(event_id:, type:, node_name:, path:, status:, payload: {})
11
+ @event_id = event_id.to_s
12
+ @type = type.to_sym
13
+ @node_name = node_name.to_sym
14
+ @path = Array(path).map(&:to_sym).freeze
15
+ @status = status.to_sym
16
+ @payload = payload.freeze
17
+ freeze
18
+ end
19
+
20
+ def to_h
21
+ {
22
+ event_id: event_id,
23
+ type: type,
24
+ node_name: node_name,
25
+ path: path,
26
+ status: status,
27
+ payload: payload
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Extensions
5
+ module Contracts
6
+ module Audit
7
+ class Snapshot
8
+ attr_reader :graph, :profile_fingerprint, :event_count, :events, :states, :children, :output_names
9
+
10
+ def initialize(graph:, profile_fingerprint:, events:, states:, children:, output_names:)
11
+ @graph = graph.to_s
12
+ @profile_fingerprint = profile_fingerprint
13
+ @events = events.freeze
14
+ @states = states.transform_keys(&:to_sym).freeze
15
+ @children = children.freeze
16
+ @output_names = output_names.map(&:to_sym).freeze
17
+ @event_count = @events.length
18
+ freeze
19
+ end
20
+
21
+ def event_types
22
+ events.map(&:type).uniq
23
+ end
24
+
25
+ def state(name)
26
+ states.fetch(name.to_sym)
27
+ end
28
+
29
+ def to_h
30
+ {
31
+ graph: graph,
32
+ profile_fingerprint: profile_fingerprint,
33
+ event_count: event_count,
34
+ output_names: output_names,
35
+ events: events.map(&:to_h),
36
+ states: states,
37
+ children: children
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end