igniter 0.3.0 → 0.4.0

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/README.md +2 -2
  4. data/docs/API_V2.md +58 -0
  5. data/docs/DISTRIBUTED_CONTRACTS_V1.md +493 -0
  6. data/examples/README.md +3 -0
  7. data/examples/distributed_workflow.rb +52 -0
  8. data/examples/ringcentral_routing.rb +26 -35
  9. data/lib/igniter/compiler/compiled_graph.rb +20 -0
  10. data/lib/igniter/compiler/validation_pipeline.rb +3 -1
  11. data/lib/igniter/compiler/validators/await_validator.rb +53 -0
  12. data/lib/igniter/compiler/validators/dependencies_validator.rb +43 -1
  13. data/lib/igniter/compiler/validators/remote_validator.rb +58 -0
  14. data/lib/igniter/compiler.rb +2 -0
  15. data/lib/igniter/contract.rb +75 -8
  16. data/lib/igniter/diagnostics/report.rb +102 -3
  17. data/lib/igniter/dsl/contract_builder.rb +109 -8
  18. data/lib/igniter/errors.rb +6 -1
  19. data/lib/igniter/extensions/introspection/graph_formatter.rb +4 -0
  20. data/lib/igniter/integrations/llm/config.rb +69 -0
  21. data/lib/igniter/integrations/llm/context.rb +74 -0
  22. data/lib/igniter/integrations/llm/executor.rb +159 -0
  23. data/lib/igniter/integrations/llm/providers/anthropic.rb +148 -0
  24. data/lib/igniter/integrations/llm/providers/base.rb +33 -0
  25. data/lib/igniter/integrations/llm/providers/ollama.rb +137 -0
  26. data/lib/igniter/integrations/llm/providers/openai.rb +153 -0
  27. data/lib/igniter/integrations/llm.rb +59 -0
  28. data/lib/igniter/integrations/rails/cable_adapter.rb +49 -0
  29. data/lib/igniter/integrations/rails/contract_job.rb +76 -0
  30. data/lib/igniter/integrations/rails/generators/contract/contract_generator.rb +22 -0
  31. data/lib/igniter/integrations/rails/generators/install/install_generator.rb +33 -0
  32. data/lib/igniter/integrations/rails/railtie.rb +25 -0
  33. data/lib/igniter/integrations/rails/webhook_concern.rb +49 -0
  34. data/lib/igniter/integrations/rails.rb +12 -0
  35. data/lib/igniter/model/await_node.rb +21 -0
  36. data/lib/igniter/model/branch_node.rb +9 -3
  37. data/lib/igniter/model/collection_node.rb +9 -3
  38. data/lib/igniter/model/remote_node.rb +26 -0
  39. data/lib/igniter/model.rb +2 -0
  40. data/lib/igniter/runtime/execution.rb +2 -2
  41. data/lib/igniter/runtime/input_validator.rb +5 -3
  42. data/lib/igniter/runtime/resolver.rb +91 -8
  43. data/lib/igniter/runtime/stores/active_record_store.rb +13 -1
  44. data/lib/igniter/runtime/stores/file_store.rb +50 -2
  45. data/lib/igniter/runtime/stores/memory_store.rb +55 -2
  46. data/lib/igniter/runtime/stores/redis_store.rb +13 -1
  47. data/lib/igniter/server/client.rb +123 -0
  48. data/lib/igniter/server/config.rb +27 -0
  49. data/lib/igniter/server/handlers/base.rb +105 -0
  50. data/lib/igniter/server/handlers/contracts_handler.rb +15 -0
  51. data/lib/igniter/server/handlers/event_handler.rb +28 -0
  52. data/lib/igniter/server/handlers/execute_handler.rb +37 -0
  53. data/lib/igniter/server/handlers/health_handler.rb +32 -0
  54. data/lib/igniter/server/handlers/status_handler.rb +27 -0
  55. data/lib/igniter/server/http_server.rb +109 -0
  56. data/lib/igniter/server/rack_app.rb +35 -0
  57. data/lib/igniter/server/registry.rb +56 -0
  58. data/lib/igniter/server/router.rb +75 -0
  59. data/lib/igniter/server.rb +67 -0
  60. data/lib/igniter/version.rb +1 -1
  61. data/lib/igniter.rb +4 -0
  62. metadata +36 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cac7a900895eb96dbf1b12c619e8ff0ea0cb54557b8d605694710984b4baa37
4
- data.tar.gz: 16b610374e8bbfaedeae1e01b57878e52ff2399ff4a566a62ff5926203ece4c5
3
+ metadata.gz: 00ad08d39fff726f2d88cefe18758dca71dc7b6fbccce872199347e23bf86e9a
4
+ data.tar.gz: 42791bdf591fbab256bb32a7da1b37466317e0c190d3693830ee82885f3fe238
5
5
  SHA512:
6
- metadata.gz: 28cddf140914921cf31d05255122aeebcb892f5cae91daf27eb37ec4433be7e96b640a9c82175ef4e2995085cd036762d38963d275e4916b1ecbc5f9225cf18c
7
- data.tar.gz: 2fbea23399d3d17d453ea83b9bf747a3a73d45eeb3cf01546d6e73e64c9354808414c7821c6dab28b070133a11d31440108aca3fe63a5a4ee0838407b207732c
6
+ metadata.gz: 45034ac4e94ee4c1864a8ca5222505d2192c3cf4667c1ba6149161cf15079c7000a80c75e2dad6baafb79dfde0b9cb5657fd0ffd2d869f3600210ec3429268a9
7
+ data.tar.gz: 4f797eb253d945fd459f2ef29b64a4330616ecfcc0d8d6f1dfbbf98da24bfe090da6e6c28cda2ddf8203f71c52af98ec275cb157829febd032ab9846ccc883e8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2026-03-19
4
+
5
+ - Add DX-oriented DSL helpers `project` and `aggregate` for compact extraction and summary nodes.
6
+ - Extend `branch` and `collection` with `map_inputs:` and named `using:` mappers to reduce orchestration wiring noise.
7
+ - Allow `collection` mapper mode to iterate over hash-like sources directly without a preparatory `to_a` node.
8
+ - Add diagnostics-only output presenters via `present` for compact human-facing summaries without changing raw machine-readable outputs.
9
+ - Improve diagnostics formatting for nested branch/collection outputs and clean up inline value rendering for hashes and symbol-heavy summaries.
10
+ - Validate and exercise the new DX surface against private production-like scheduler migration POCs.
11
+
3
12
  ## [0.3.0] - 2026-03-19
4
13
 
5
14
  - Add executor metadata and global executor registry for self-describing, schema-friendly execution steps.
data/README.md CHANGED
@@ -9,7 +9,7 @@ Igniter is a Ruby gem for expressing business logic as a validated dependency gr
9
9
  - runtime auditing
10
10
  - diagnostics reports
11
11
  - reactive side effects
12
- - ergonomic DSL helpers (`with`, `const`, `lookup`, `map`, `guard`, `export`, `expose`, `effect`, `on_success`, `scope`, `namespace`, `branch`, `collection`)
12
+ - ergonomic DSL helpers (`with`, `const`, `lookup`, `map`, `project`, `aggregate`, `guard`, `export`, `expose`, `effect`, `on_success`, `scope`, `namespace`, `branch`, `collection`)
13
13
  - graph and runtime introspection
14
14
  - async-capable pending nodes with snapshot/restore
15
15
  - store-backed execution resume flows
@@ -87,7 +87,7 @@ There is also a short patterns guide in [`docs/PATTERNS.md`](docs/PATTERNS.md).
87
87
  | `marketing_ergonomics.rb` | `ruby examples/marketing_ergonomics.rb` | compact domain DSL with `with`, matcher-style `guard`, `scope`/`namespace`, `expose`, `on_success`, and `explain_plan` |
88
88
  | `collection.rb` | `ruby examples/collection.rb` | declarative fan-out, stable item keys, and `CollectionResult` |
89
89
  | `collection_partial_failure.rb` | `ruby examples/collection_partial_failure.rb` | `:collect` mode, partial failure summary, and collection diagnostics |
90
- | `ringcentral_routing.rb` | `ruby examples/ringcentral_routing.rb` | top-level `branch`, nested `collection`, per-item routing, and nested diagnostics semantics |
90
+ | `ringcentral_routing.rb` | `ruby examples/ringcentral_routing.rb` | top-level `branch`, nested `collection`, `project`, `aggregate`, `using:`/`map_inputs`, and nested diagnostics semantics |
91
91
 
92
92
  There are also matching living examples in `spec/igniter/examples_spec.rb`.
93
93
  Those are useful if you want to read the examples in test form.
data/docs/API_V2.md CHANGED
@@ -146,6 +146,12 @@ map :normalized_trade_name, from: :service do |service:|
146
146
  service.downcase == "heating" ? "HVAC" : service
147
147
  end
148
148
 
149
+ project :telephony_status, from: :body, key: "telephonyStatus"
150
+
151
+ aggregate :available_slots, with: :technicians do |technicians:|
152
+ technicians.successes.values.sum { |item| item.result.summary[:available_slots] }
153
+ end
154
+
149
155
  guard :business_hours_valid, depends_on: %i[vendor current_time], message: "Closed" do |vendor:, current_time:|
150
156
  current_time.between?(vendor.start_at, vendor.stop_at)
151
157
  end
@@ -180,6 +186,21 @@ branch :delivery_strategy, with: :country, inputs: {
180
186
  on "UA", contract: LocalDeliveryContract
181
187
  default contract: DefaultDeliveryContract
182
188
  end
189
+
190
+ branch :status_route,
191
+ with: :telephony_status,
192
+ depends_on: %i[extension_id active_calls],
193
+ map_inputs: ->(selector:, extension_id:, active_calls:) {
194
+ {
195
+ extension_id: extension_id,
196
+ telephony_status: selector,
197
+ active_calls: active_calls
198
+ }
199
+ } do
200
+ on "CallConnected", contract: CallConnectedContract
201
+ on "NoCall", contract: NoCallContract
202
+ default contract: UnknownStatusContract
203
+ end
183
204
  ```
184
205
 
185
206
  Declarative fan-out:
@@ -190,6 +211,43 @@ collection :technicians,
190
211
  each: TechnicianContract,
191
212
  key: :technician_id,
192
213
  mode: :collect
214
+
215
+ collection :calls,
216
+ with: :active_calls,
217
+ each: CallEventContract,
218
+ key: :session_id,
219
+ mode: :collect,
220
+ map_inputs: ->(item:) {
221
+ {
222
+ session_id: item.fetch("telephonySessionId"),
223
+ direction: item.fetch("direction"),
224
+ from: item.fetch("from"),
225
+ to: item.fetch("to"),
226
+ start_time: item.fetch("startTime")
227
+ }
228
+ }
229
+ ```
230
+
231
+ For repeated mappers, prefer `using:` over inline lambdas:
232
+
233
+ ```ruby
234
+ collection :company_locations,
235
+ with: :locations_map,
236
+ each: CompanyLocationSchedulerContract,
237
+ key: :location_id,
238
+ depends_on: %i[services_map property_type date],
239
+ using: :build_company_location_inputs
240
+ ```
241
+
242
+ ```ruby
243
+ branch :status_route,
244
+ with: :telephony_status,
245
+ depends_on: %i[extension_id active_calls],
246
+ using: :build_status_route_inputs do
247
+ on "CallConnected", contract: CallConnectedContract
248
+ on "NoCall", contract: NoCallContract
249
+ default contract: UnknownStatusContract
250
+ end
193
251
  ```
194
252
 
195
253
  Rules:
@@ -0,0 +1,493 @@
1
+ # Distributed Contracts v1
2
+
3
+ ## Goal
4
+
5
+ `Distributed Contracts` extend `igniter` from in-process orchestration to long-running, event-driven workflow execution.
6
+
7
+ The feature should make distributed workflows:
8
+
9
+ - explicit
10
+ - correlation-aware
11
+ - resumable
12
+ - observable end to end
13
+ - compatible with the existing graph model
14
+
15
+ It should avoid pushing cross-system workflow state into ad hoc service objects, background jobs, or loosely related tables.
16
+
17
+ ## Problem Shape
18
+
19
+ Typical legacy pain looks like this:
20
+
21
+ - one business flow starts from an inbound request
22
+ - later signals arrive from different systems
23
+ - signals must be matched by business identity, not by process memory
24
+ - some steps complete immediately, others wait minutes or hours
25
+ - failures are not local, they are causal and temporal
26
+
27
+ Examples:
28
+
29
+ - vendor request -> callrail webhook -> ringcentral webhook -> operator match -> order creation -> billing decision -> invoice
30
+ - lead accepted now, enrichment later
31
+ - call started now, attribution resolved later
32
+
33
+ `igniter` already has useful building blocks:
34
+
35
+ - `pending`
36
+ - snapshot / restore
37
+ - store-backed execution
38
+ - token-based resume
39
+ - diagnostics and audit
40
+
41
+ Distributed Contracts v1 should formalize these into a workflow model.
42
+
43
+ ## Core Principles
44
+
45
+ ### Correlation first
46
+
47
+ The primary question is not:
48
+
49
+ - "which service should run next?"
50
+
51
+ It is:
52
+
53
+ - "which execution does this external signal belong to?"
54
+
55
+ Distributed execution must be resumable by stable business correlation keys.
56
+
57
+ ### External signals are graph-visible
58
+
59
+ Waiting for an external system should be represented in the graph model, not hidden inside generic polling or callback code.
60
+
61
+ ### Transport stays outside
62
+
63
+ Controllers, jobs, consumers, and webhook adapters remain outside `igniter`.
64
+
65
+ They should translate transport concerns into workflow operations such as:
66
+
67
+ - start workflow
68
+ - deliver signal
69
+ - resume execution
70
+
71
+ `igniter` should not become a message bus.
72
+
73
+ ## Proposed v1 Surface
74
+
75
+ ### Starting a distributed workflow
76
+
77
+ ```ruby
78
+ execution = CallLifecycleContract.start(
79
+ request_id: "req-123",
80
+ company_id: "42",
81
+ vendor_lead_id: "lead-77"
82
+ )
83
+ ```
84
+
85
+ This is conceptually similar to `new(...).resolve`, but v1 should expose a clearer workflow-oriented entry point.
86
+
87
+ ### Correlation metadata
88
+
89
+ ```ruby
90
+ class CallLifecycleContract < Igniter::Contract
91
+ correlate_by :request_id, :company_id, :vendor_lead_id
92
+ end
93
+ ```
94
+
95
+ This should define the business keys used to find the execution later.
96
+
97
+ Suggested v1 requirements:
98
+
99
+ - correlation keys are declared explicitly
100
+ - correlation values come from initial inputs and/or delivered signals
101
+ - the execution store can index and look up executions by correlation values
102
+
103
+ ### Awaiting an external signal
104
+
105
+ ```ruby
106
+ await :callrail_call,
107
+ event: :callrail_webhook_received
108
+
109
+ await :ringcentral_match,
110
+ event: :ringcentral_call_matched
111
+ ```
112
+
113
+ This is preferable to generic `defer` for distributed flows because it makes the waiting state explicit in the graph.
114
+
115
+ ### Delivering an external signal
116
+
117
+ ```ruby
118
+ CallLifecycleContract.deliver_event(
119
+ :callrail_webhook_received,
120
+ correlation: { request_id: "req-123", company_id: "42" },
121
+ payload: { call_id: "cr-1", started_at: "2026-03-19T10:00:00Z" }
122
+ )
123
+ ```
124
+
125
+ Expected behavior:
126
+
127
+ 1. Find the matching execution by correlation.
128
+ 2. Find a matching `await` node for the event.
129
+ 3. Bind the payload to that waiting node.
130
+ 4. Resume the workflow.
131
+
132
+ ### Diagnostics for waiting workflows
133
+
134
+ ```ruby
135
+ contract.diagnostics.to_h
136
+ ```
137
+
138
+ Should make waiting state explicit:
139
+
140
+ - current status
141
+ - known correlation keys
142
+ - expected external events
143
+ - last delivered event
144
+ - current blocked / waiting nodes
145
+
146
+ ## Scope of v1
147
+
148
+ Supported:
149
+
150
+ - explicit correlation keys
151
+ - explicit external wait nodes
152
+ - event delivery by correlation
153
+ - store-backed lookup and resume
154
+ - diagnostics for waiting state
155
+ - audit trail of delivered external signals
156
+
157
+ Not supported in v1:
158
+
159
+ - message bus integration
160
+ - retries / dead-letter routing
161
+ - event schema registry
162
+ - compensation / saga semantics
163
+ - multi-execution joins
164
+ - time-based wakeups or cron waits
165
+ - out-of-order signal reconciliation beyond simple correlation lookup
166
+
167
+ ## DSL
168
+
169
+ ### Basic shape
170
+
171
+ ```ruby
172
+ class CallLifecycleContract < Igniter::Contract
173
+ correlate_by :request_id, :company_id, :vendor_lead_id
174
+
175
+ define do
176
+ input :request_id, type: :string
177
+ input :company_id, type: :string
178
+ input :vendor_lead_id, type: :string
179
+
180
+ await :callrail_call, event: :callrail_webhook_received
181
+ await :ringcentral_match, event: :ringcentral_call_matched
182
+
183
+ aggregate :billing_context, with: %i[callrail_call ringcentral_match] do |callrail_call:, ringcentral_match:|
184
+ {
185
+ call_id: callrail_call[:call_id],
186
+ operator_id: ringcentral_match[:operator_id],
187
+ channel: ringcentral_match[:channel]
188
+ }
189
+ end
190
+
191
+ output :billing_context
192
+ end
193
+ end
194
+ ```
195
+
196
+ ### Signal delivery
197
+
198
+ ```ruby
199
+ CallLifecycleContract.deliver_event(
200
+ :ringcentral_call_matched,
201
+ correlation: {
202
+ request_id: "req-123",
203
+ company_id: "42",
204
+ vendor_lead_id: "lead-77"
205
+ },
206
+ payload: {
207
+ operator_id: 55,
208
+ channel: "paid_search"
209
+ }
210
+ )
211
+ ```
212
+
213
+ ## Why `await` instead of reusing generic `defer`
214
+
215
+ `defer` is useful as a low-level runtime primitive.
216
+
217
+ But distributed workflows need more explicit semantics:
218
+
219
+ - what event is being awaited
220
+ - how to resume it
221
+ - how to show it in diagnostics
222
+ - how to route external signals safely
223
+
224
+ `await` should be modeled as a graph primitive, not just a custom executor returning `DeferredResult`.
225
+
226
+ ## Runtime Semantics
227
+
228
+ ### Starting execution
229
+
230
+ 1. Create a normal execution.
231
+ 2. Persist workflow metadata:
232
+ - execution id
233
+ - graph name
234
+ - correlation keys
235
+ - workflow status
236
+ 3. Resolve until:
237
+ - success
238
+ - failure
239
+ - waiting on one or more `await` nodes
240
+
241
+ ### Await resolution
242
+
243
+ When an `await` node is reached:
244
+
245
+ 1. Mark node status as `waiting_for_event`
246
+ 2. Persist expected event metadata
247
+ 3. Stop downstream execution until signal arrives
248
+
249
+ ### Event delivery
250
+
251
+ When `deliver_event` is called:
252
+
253
+ 1. Resolve execution lookup by correlation
254
+ 2. Validate event name
255
+ 3. Match event to a waiting `await` node
256
+ 4. Persist audit metadata for the incoming signal
257
+ 5. Resume execution with delivered payload
258
+
259
+ ### Terminal states
260
+
261
+ Suggested workflow-level statuses:
262
+
263
+ - `running`
264
+ - `waiting`
265
+ - `succeeded`
266
+ - `failed`
267
+
268
+ Later possible additions:
269
+
270
+ - `cancelled`
271
+ - `timed_out`
272
+ - `abandoned`
273
+
274
+ ## Graph Model
275
+
276
+ Distributed Contracts v1 should introduce a dedicated node kind:
277
+
278
+ - `:await`
279
+
280
+ Suggested internal shape:
281
+
282
+ ```ruby
283
+ AwaitNode.new(
284
+ name: :ringcentral_match,
285
+ event: :ringcentral_call_matched
286
+ )
287
+ ```
288
+
289
+ This should not be modeled as generic compute.
290
+
291
+ ## Correlation Model
292
+
293
+ Correlation should be explicit and inspectable.
294
+
295
+ Suggested v1 API:
296
+
297
+ ```ruby
298
+ correlate_by :request_id, :company_id, :vendor_lead_id
299
+ ```
300
+
301
+ Suggested stored metadata:
302
+
303
+ ```ruby
304
+ {
305
+ request_id: "req-123",
306
+ company_id: "42",
307
+ vendor_lead_id: "lead-77"
308
+ }
309
+ ```
310
+
311
+ V1 should assume exact equality matching.
312
+
313
+ Future versions may allow:
314
+
315
+ - computed correlation values
316
+ - partial lookup
317
+ - secondary indexes
318
+
319
+ ## Store Requirements
320
+
321
+ The execution store needs more than snapshot persistence.
322
+
323
+ Distributed Contracts v1 should require:
324
+
325
+ - fetch by `execution_id`
326
+ - save snapshot
327
+ - delete snapshot
328
+ - lookup execution ids by correlation keys
329
+ - persist workflow metadata
330
+
331
+ Conceptually:
332
+
333
+ ```ruby
334
+ store.save(snapshot:, metadata:)
335
+ store.find_by_correlation(graph: "CallLifecycleContract", correlation: { ... })
336
+ ```
337
+
338
+ This probably means a new workflow-aware store interface, not just extending the current minimal snapshot store informally.
339
+
340
+ ## Compile-Time Validation
341
+
342
+ The compiler should validate:
343
+
344
+ - declared correlation keys exist as contract inputs
345
+ - `await` node names are unique
346
+ - awaited event names are unique within a graph unless explicitly allowed
347
+ - `await` nodes are valid dependencies for downstream nodes
348
+
349
+ Suggested v1 restriction:
350
+
351
+ - one `await` node per event name within a contract
352
+
353
+ This keeps signal delivery unambiguous.
354
+
355
+ ## Runtime Validation
356
+
357
+ At runtime, delivery should validate:
358
+
359
+ - the target execution exists
360
+ - the execution belongs to the expected contract graph
361
+ - the event is currently awaited
362
+ - the same awaited signal is not delivered twice unless idempotency policy allows it
363
+
364
+ Suggested runtime errors:
365
+
366
+ - `Igniter::WorkflowNotFoundError`
367
+ - `Igniter::AwaitedEventMismatchError`
368
+ - `Igniter::DuplicateSignalError`
369
+
370
+ ## Events and Audit
371
+
372
+ Distributed Contracts v1 should add workflow-aware events.
373
+
374
+ Suggested additions:
375
+
376
+ - `workflow_started`
377
+ - `workflow_waiting`
378
+ - `workflow_resumed`
379
+ - `external_event_received`
380
+ - `await_satisfied`
381
+
382
+ Suggested payload for incoming external signal:
383
+
384
+ ```ruby
385
+ {
386
+ event: :ringcentral_call_matched,
387
+ correlation: {
388
+ request_id: "req-123",
389
+ company_id: "42",
390
+ vendor_lead_id: "lead-77"
391
+ }
392
+ }
393
+ ```
394
+
395
+ Audit should make it possible to answer:
396
+
397
+ - which external events were received
398
+ - in what order
399
+ - which waiting node they satisfied
400
+ - why the workflow is still blocked
401
+
402
+ ## Introspection
403
+
404
+ ### Graph
405
+
406
+ `await` nodes should render distinctly from compute, branch, collection, and composition.
407
+
408
+ The graph should show:
409
+
410
+ - awaited event name
411
+ - correlation keys at the workflow level
412
+
413
+ ### Plan
414
+
415
+ Before execution:
416
+
417
+ - `await` nodes look like normal pending nodes
418
+
419
+ When waiting:
420
+
421
+ - plan should show explicit waiting state
422
+ - ideally also the awaited event name
423
+
424
+ ### Diagnostics
425
+
426
+ Diagnostics should surface:
427
+
428
+ - workflow status
429
+ - correlation keys
430
+ - awaited events
431
+ - satisfied events
432
+ - last external event
433
+ - blocked nodes
434
+
435
+ Example conceptual shape:
436
+
437
+ ```ruby
438
+ {
439
+ status: :waiting,
440
+ correlation: {
441
+ request_id: "req-123",
442
+ company_id: "42"
443
+ },
444
+ waiting_on: [
445
+ { node: :ringcentral_match, event: :ringcentral_call_matched }
446
+ ],
447
+ last_external_event: :callrail_webhook_received
448
+ }
449
+ ```
450
+
451
+ ## Relation to Existing Pending Support
452
+
453
+ Distributed Contracts v1 should build on existing pending/store/resume support instead of replacing it.
454
+
455
+ Conceptually:
456
+
457
+ - `await` is a higher-level workflow primitive
458
+ - internally it may still use pending state and resume mechanics
459
+ - `deliver_event` is a safer, domain-oriented wrapper over generic token resume
460
+
461
+ This keeps the runtime coherent and incremental.
462
+
463
+ ## Recommended Architectural Pattern
464
+
465
+ Do not start with one giant distributed contract from request to invoice.
466
+
467
+ Prefer stage-oriented workflow design:
468
+
469
+ - `InboundLeadContract`
470
+ - `CallAttributionContract`
471
+ - `OrderBillingContract`
472
+ - optional orchestration shell above them
473
+
474
+ This keeps contracts:
475
+
476
+ - understandable
477
+ - diagnosable
478
+ - evolvable
479
+
480
+ ## Future Extensions
481
+
482
+ Possible later additions:
483
+
484
+ - event payload schema validation
485
+ - timeout / expiry policies on `await`
486
+ - idempotency keys for signal delivery
487
+ - compensation hooks
488
+ - `await_any` / `await_all`
489
+ - workflow versioning and migration
490
+ - cross-workflow linking
491
+ - visual workflow tracing
492
+
493
+ These should not be part of v1.
data/examples/README.md CHANGED
@@ -181,6 +181,9 @@ Shows:
181
181
 
182
182
  - top-level routing via `branch`
183
183
  - nested fan-out via `collection`
184
+ - trivial field extraction via `project`
185
+ - compact summary building via `aggregate`
186
+ - item input shaping via `collection map_inputs:` or `using:`
184
187
  - per-item nested routing via another `branch`
185
188
  - `CollectionResult` summary on the selected child contract
186
189
  - the practical boundary between parent diagnostics and child diagnostics
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.join(__dir__, "../lib")
4
+ require "igniter"
5
+
6
+ store = Igniter::Runtime::Stores::MemoryStore.new
7
+
8
+ class LeadWorkflow < Igniter::Contract
9
+ correlate_by :request_id, :company_id
10
+
11
+ define do
12
+ input :request_id
13
+ input :company_id
14
+
15
+ await :crm_data, event: :crm_webhook_received
16
+ await :billing_data, event: :billing_data_fetched
17
+
18
+ aggregate :report, with: %i[crm_data billing_data] do |crm_data:, billing_data:|
19
+ { crm: crm_data, billing: billing_data }
20
+ end
21
+
22
+ output :report
23
+ end
24
+ end
25
+
26
+ puts "==> Starting execution..."
27
+ execution = LeadWorkflow.start(
28
+ { request_id: "req-1", company_id: "co-42" },
29
+ store: store
30
+ )
31
+ puts "pending? #{execution.pending?}"
32
+
33
+ puts "\n==> Delivering CRM event..."
34
+ execution = LeadWorkflow.deliver_event(
35
+ :crm_webhook_received,
36
+ correlation: { request_id: "req-1", company_id: "co-42" },
37
+ payload: { name: "Acme Corp", tier: "enterprise" },
38
+ store: store
39
+ )
40
+ puts "still pending? #{execution.pending?}"
41
+
42
+ puts "\n==> Delivering billing event..."
43
+ execution = LeadWorkflow.deliver_event(
44
+ :billing_data_fetched,
45
+ correlation: { request_id: "req-1", company_id: "co-42" },
46
+ payload: { plan: "pro", mrr: 500 },
47
+ store: store
48
+ )
49
+
50
+ puts "\n==> Final result:"
51
+ puts "success? #{execution.success?}"
52
+ puts "report: #{execution.result.report.inspect}"