igniter 0.2.0 → 0.3.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +224 -1
  4. data/docs/API_V2.md +238 -1
  5. data/docs/BACKLOG.md +166 -0
  6. data/docs/BRANCHES_V1.md +213 -0
  7. data/docs/COLLECTIONS_V1.md +303 -0
  8. data/docs/EXECUTION_MODEL_V2.md +79 -0
  9. data/docs/PATTERNS.md +222 -0
  10. data/docs/STORE_ADAPTERS.md +126 -0
  11. data/examples/README.md +124 -0
  12. data/examples/async_store.rb +47 -0
  13. data/examples/collection.rb +43 -0
  14. data/examples/collection_partial_failure.rb +50 -0
  15. data/examples/marketing_ergonomics.rb +57 -0
  16. data/examples/ringcentral_routing.rb +278 -0
  17. data/lib/igniter/compiler/compiled_graph.rb +82 -0
  18. data/lib/igniter/compiler/graph_compiler.rb +12 -2
  19. data/lib/igniter/compiler/type_resolver.rb +54 -0
  20. data/lib/igniter/compiler/validation_context.rb +61 -0
  21. data/lib/igniter/compiler/validation_pipeline.rb +30 -0
  22. data/lib/igniter/compiler/validator.rb +1 -187
  23. data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
  24. data/lib/igniter/compiler/validators/dependencies_validator.rb +151 -0
  25. data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
  26. data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
  27. data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
  28. data/lib/igniter/compiler.rb +8 -0
  29. data/lib/igniter/contract.rb +136 -4
  30. data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
  31. data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
  32. data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
  33. data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
  34. data/lib/igniter/diagnostics/report.rb +84 -8
  35. data/lib/igniter/dsl/contract_builder.rb +208 -5
  36. data/lib/igniter/dsl/schema_builder.rb +73 -0
  37. data/lib/igniter/dsl.rb +1 -0
  38. data/lib/igniter/errors.rb +11 -0
  39. data/lib/igniter/events/bus.rb +5 -0
  40. data/lib/igniter/events/event.rb +29 -0
  41. data/lib/igniter/executor.rb +74 -0
  42. data/lib/igniter/executor_registry.rb +44 -0
  43. data/lib/igniter/extensions/auditing/timeline.rb +4 -0
  44. data/lib/igniter/extensions/introspection/graph_formatter.rb +29 -3
  45. data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
  46. data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
  47. data/lib/igniter/extensions/introspection.rb +1 -0
  48. data/lib/igniter/extensions/reactive/engine.rb +49 -2
  49. data/lib/igniter/extensions/reactive/reaction.rb +3 -2
  50. data/lib/igniter/model/branch_node.rb +40 -0
  51. data/lib/igniter/model/collection_node.rb +25 -0
  52. data/lib/igniter/model/composition_node.rb +2 -2
  53. data/lib/igniter/model/compute_node.rb +58 -2
  54. data/lib/igniter/model/input_node.rb +2 -2
  55. data/lib/igniter/model/output_node.rb +24 -4
  56. data/lib/igniter/model.rb +2 -0
  57. data/lib/igniter/runtime/cache.rb +64 -25
  58. data/lib/igniter/runtime/collection_result.rb +111 -0
  59. data/lib/igniter/runtime/deferred_result.rb +40 -0
  60. data/lib/igniter/runtime/execution.rb +261 -11
  61. data/lib/igniter/runtime/input_validator.rb +2 -24
  62. data/lib/igniter/runtime/invalidator.rb +1 -1
  63. data/lib/igniter/runtime/job_worker.rb +18 -0
  64. data/lib/igniter/runtime/node_state.rb +20 -0
  65. data/lib/igniter/runtime/planner.rb +126 -0
  66. data/lib/igniter/runtime/resolver.rb +269 -15
  67. data/lib/igniter/runtime/result.rb +14 -2
  68. data/lib/igniter/runtime/runner_factory.rb +20 -0
  69. data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
  70. data/lib/igniter/runtime/runners/store_runner.rb +29 -0
  71. data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
  72. data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
  73. data/lib/igniter/runtime/stores/file_store.rb +43 -0
  74. data/lib/igniter/runtime/stores/memory_store.rb +40 -0
  75. data/lib/igniter/runtime/stores/redis_store.rb +44 -0
  76. data/lib/igniter/runtime.rb +12 -0
  77. data/lib/igniter/type_system.rb +44 -0
  78. data/lib/igniter/version.rb +1 -1
  79. data/lib/igniter.rb +23 -0
  80. metadata +43 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9146f111caa1071ea7ba8ede536e39a00450193c60748a176ab2e6a0cb0fc15
4
- data.tar.gz: 6f120c1d14c286dea458b58dbefbdd1d908a3eca77edad22fe933b145103639a
3
+ metadata.gz: 5cac7a900895eb96dbf1b12c619e8ff0ea0cb54557b8d605694710984b4baa37
4
+ data.tar.gz: 16b610374e8bbfaedeae1e01b57878e52ff2399ff4a566a62ff5926203ece4c5
5
5
  SHA512:
6
- metadata.gz: d6de8e1d3228aaa17148abba206fe1185458ef316bd9a7153e3b02b460297bc7d838df47f6c3a95b266a56759bbc3aab3a2de0af68d0834005b863ae1a64f66a
7
- data.tar.gz: 74f0b01d50f7d301c5519af7e54ce4042b02736321af2ae6d9beba3cb79dcbf1464e6ec59751668025cc0ad9012cce5a42ec36ee2fb1b46d23d43306e4366656
6
+ metadata.gz: 28cddf140914921cf31d05255122aeebcb892f5cae91daf27eb37ec4433be7e96b640a9c82175ef4e2995085cd036762d38963d275e4916b1ecbc5f9225cf18c
7
+ data.tar.gz: 2fbea23399d3d17d453ea83b9bf747a3a73d45eeb3cf01546d6e73e64c9354808414c7821c6dab28b070133a11d31440108aca3fe63a5a4ee0838407b207732c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-03-19
4
+
5
+ - Add executor metadata and global executor registry for self-describing, schema-friendly execution steps.
6
+ - Split compiler validation into a pluggable validation pipeline and add shared type compatibility checks.
7
+ - Introduce planner/runner runtime architecture with `:inline`, `:thread_pool`, and store-backed execution modes.
8
+ - Add deferred nodes, pending state, snapshot/restore, token-based resume, worker-style resume flow, and reference file/ActiveRecord/Redis store adapters.
9
+ - Expand the DSL with `with`, matcher-style `guard`, `scope`, `namespace`, `branch`, `collection`, `expose`, `on_failure`, and `on_exit`.
10
+ - Add `branch` and `collection` as graph primitives with compile-time validation, nested runtime support, and item-level collection events.
11
+ - Improve diagnostics and auditing with collection summaries, partial-failure visibility, item-level failure reporting, and richer markdown/text output.
12
+ - Add production-like runnable examples for async resume, ergonomic domain contracts, collection partial failure, and nested branch + collection routing.
13
+ - Add design docs for branches, collections, store adapters, and orchestration patterns.
14
+
3
15
  ## [0.2.0] - 2026-03-18
4
16
 
5
17
  - Complete the `arbor` to `igniter` rename across runtime, docs, examples, console setup, and shipped signatures.
data/README.md CHANGED
@@ -9,7 +9,10 @@ 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
13
  - graph and runtime introspection
14
+ - async-capable pending nodes with snapshot/restore
15
+ - store-backed execution resume flows
13
16
 
14
17
  The repository now contains a working v2 core built around explicit compile-time and runtime boundaries.
15
18
 
@@ -66,18 +69,25 @@ contract.diagnostics_text
66
69
  - Diagnostics: build compact text, markdown, or structured reports for triage.
67
70
  - Reactive: subscribe declaratively to runtime events.
68
71
  - Introspection: render graphs as text or Mermaid and inspect runtime state.
72
+ - Ergonomics: use compact DSL helpers for common lookup, transform, guard, export, and side-effect patterns.
69
73
 
70
74
  ## Quick Start Recipes
71
75
 
72
76
  The repository contains runnable examples in [`examples/`](examples).
73
77
  They also have matching specs, so they stay in sync with the implementation.
74
78
  The examples folder also has its own quick index in [`examples/README.md`](examples/README.md).
79
+ There is also a short patterns guide in [`docs/PATTERNS.md`](docs/PATTERNS.md).
75
80
 
76
81
  | Example | Run | Shows |
77
82
  | --- | --- | --- |
78
83
  | `basic_pricing.rb` | `ruby examples/basic_pricing.rb` | basic contract, lazy resolution, input updates |
79
84
  | `composition.rb` | `ruby examples/composition.rb` | nested contracts and composed results |
80
85
  | `diagnostics.rb` | `ruby examples/diagnostics.rb` | diagnostics text plus machine-readable output |
86
+ | `async_store.rb` | `ruby examples/async_store.rb` | pending execution, file-backed store, worker-style resume |
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
+ | `collection.rb` | `ruby examples/collection.rb` | declarative fan-out, stable item keys, and `CollectionResult` |
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 |
81
91
 
82
92
  There are also matching living examples in `spec/igniter/examples_spec.rb`.
83
93
  Those are useful if you want to read the examples in test form.
@@ -155,6 +165,195 @@ contract.execution.as_json
155
165
  contract.events.map(&:as_json)
156
166
  ```
157
167
 
168
+ ### 5. Async Store And Resume
169
+
170
+ ```ruby
171
+ class AsyncQuoteExecutor < Igniter::Executor
172
+ input :order_total, type: :numeric
173
+
174
+ def call(order_total:)
175
+ defer(token: "quote-#{order_total}", payload: { kind: "pricing_quote" })
176
+ end
177
+ end
178
+
179
+ class AsyncPricingContract < Igniter::Contract
180
+ run_with runner: :store
181
+
182
+ define do
183
+ input :order_total, type: :numeric
184
+
185
+ compute :quote_total, depends_on: [:order_total], call: AsyncQuoteExecutor
186
+
187
+ compute :gross_total, depends_on: [:quote_total] do |quote_total:|
188
+ quote_total * 1.2
189
+ end
190
+
191
+ output :gross_total
192
+ end
193
+ end
194
+
195
+ contract = AsyncPricingContract.new(order_total: 100)
196
+ deferred = contract.result.gross_total
197
+ execution_id = contract.execution.events.execution_id
198
+
199
+ resumed = AsyncPricingContract.resume_from_store(
200
+ execution_id,
201
+ token: deferred.token,
202
+ value: 150
203
+ )
204
+
205
+ resumed.result.gross_total
206
+ # => 180.0
207
+ ```
208
+
209
+ ### 6. Ergonomic DSL
210
+
211
+ ```ruby
212
+ class MarketingQuoteContract < Igniter::Contract
213
+ define do
214
+ input :service, type: :string
215
+ input :zip_code, type: :string
216
+
217
+ const :vendor_id, "eLocal"
218
+
219
+ scope :routing do
220
+ map :trade_name, from: :service do |service:|
221
+ %w[heating cooling ventilation air_conditioning].include?(service.downcase) ? "HVAC" : service
222
+ end
223
+ end
224
+
225
+ scope :pricing do
226
+ lookup :trade, with: :trade_name do |trade_name:|
227
+ { name: trade_name, base_bid: 45.0 }
228
+ end
229
+ end
230
+
231
+ namespace :validation do
232
+ guard :zip_supported, with: :zip_code, in: %w[60601 10001], message: "Unsupported zip"
233
+ end
234
+
235
+ compute :quote, with: %i[vendor_id trade zip_supported zip_code] do |vendor_id:, trade:, zip_supported:, zip_code:|
236
+ zip_supported
237
+ { vendor_id: vendor_id, trade: trade[:name], zip_code: zip_code, bid: trade[:base_bid] }
238
+ end
239
+
240
+ expose :quote, as: :response
241
+ end
242
+
243
+ on_success :response do |value:, **|
244
+ puts "Persist #{value.inspect}"
245
+ end
246
+ end
247
+
248
+ contract = MarketingQuoteContract.new(service: "heating", zip_code: "60601")
249
+
250
+ contract.explain_plan
251
+ contract.result.response
252
+ ```
253
+
254
+ You can also use matcher-style guards directly:
255
+
256
+ ```ruby
257
+ guard :usa_only, with: :country_code, eq: "USA", message: "Unsupported country"
258
+ guard :supported_country, with: :country_code, in: %w[USA CAN], message: "Unsupported country"
259
+ guard :valid_zip, with: :zip_code, matches: /\A\d{5}\z/, message: "Invalid zip"
260
+ ```
261
+
262
+ ### 7. Declarative Branching
263
+
264
+ ```ruby
265
+ class DeliveryContract < Igniter::Contract
266
+ define do
267
+ input :country
268
+ input :order_total
269
+
270
+ branch :delivery_strategy, with: :country, inputs: {
271
+ country: :country,
272
+ order_total: :order_total
273
+ } do
274
+ on "US", contract: USDeliveryContract
275
+ on "UA", contract: LocalDeliveryContract
276
+ default contract: DefaultDeliveryContract
277
+ end
278
+
279
+ export :price, :eta, from: :delivery_strategy
280
+ end
281
+ end
282
+ ```
283
+
284
+ ### 8. Branch + Collection Routing
285
+
286
+ ```ruby
287
+ class RingcentralWebhookContract < Igniter::Contract
288
+ define do
289
+ input :payload
290
+
291
+ scope :parse do
292
+ map :body, from: :payload do |payload:|
293
+ payload.fetch("body", {})
294
+ end
295
+
296
+ map :telephony_status, from: :body do |body:|
297
+ body["telephonyStatus"]
298
+ end
299
+
300
+ map :active_calls, from: :body do |body:|
301
+ body["activeCalls"] || []
302
+ end
303
+ end
304
+
305
+ branch :status_route, with: :telephony_status, inputs: {
306
+ extension_id: :extension_id,
307
+ telephony_status: :telephony_status,
308
+ active_calls: :active_calls
309
+ } do
310
+ on "CallConnected", contract: CallConnectedContract
311
+ on "NoCall", contract: NoCallContract
312
+ default contract: UnknownStatusContract
313
+ end
314
+
315
+ export :routing_summary, from: :status_route
316
+ end
317
+ end
318
+ ```
319
+
320
+ In nested flows, diagnostics stay attached to the execution that actually owns the node:
321
+
322
+ - the parent execution sees the top-level `branch_selected`
323
+ - collection item events live on the selected child execution
324
+ - collection summaries are easiest to read from the child contract diagnostics
325
+
326
+ `branch` is a graph primitive for explicit routing. It selects one child contract from ordered cases and resolves only the chosen branch.
327
+
328
+ ### 8. Declarative Collections
329
+
330
+ ```ruby
331
+ class TechnicianBatchContract < Igniter::Contract
332
+ define do
333
+ input :technician_inputs, type: :array
334
+
335
+ collection :technicians,
336
+ with: :technician_inputs,
337
+ each: TechnicianContract,
338
+ key: :technician_id,
339
+ mode: :collect
340
+
341
+ output :technicians
342
+ end
343
+ end
344
+ ```
345
+
346
+ `collection` is a graph primitive for explicit fan-out. It runs one child contract per item hash and returns a `CollectionResult` keyed by stable item identity.
347
+
348
+ In `mode: :collect`, a collection can succeed overall while still containing failed items. In that case:
349
+
350
+ - `result.summary` gives collection-level status such as `:partial_failure`
351
+ - `result.items_summary` gives compact per-item status
352
+ - `result.failed_items` gives only failed item details
353
+ - `contract.diagnostics_text` and `contract.diagnostics_markdown` include collection failure summaries
354
+
355
+ See `examples/collection_partial_failure.rb` for a runnable example.
356
+
158
357
  ## Composition Example
159
358
 
160
359
  ```ruby
@@ -195,7 +394,22 @@ class NotifyingContract < Igniter::Contract
195
394
  output :order_total
196
395
  end
197
396
 
198
- react_to :node_succeeded, path: "order_total" do |event:, **|
397
+ on_success :order_total do |value:, **|
398
+ puts "Resolved #{value}"
399
+ end
400
+ end
401
+ ```
402
+
403
+ Or attach directly to a node event when you want the node value:
404
+
405
+ ```ruby
406
+ class NotifyingContract < Igniter::Contract
407
+ define do
408
+ input :order_total, type: :numeric
409
+ output :order_total
410
+ end
411
+
412
+ effect "order_total" do |event:, value:, **|
199
413
  puts "Resolved #{event.path}"
200
414
  end
201
415
  end
@@ -212,6 +426,7 @@ contract.result.gross_total
212
426
 
213
427
  contract.result.states
214
428
  contract.result.explain(:gross_total)
429
+ contract.explain_plan
215
430
  contract.execution.to_h
216
431
  contract.execution.as_json
217
432
  contract.result.as_json
@@ -227,6 +442,10 @@ contract.audit_snapshot
227
442
  - [Architecture v2](docs/ARCHITECTURE_V2.md)
228
443
  - [Execution Model v2](docs/EXECUTION_MODEL_V2.md)
229
444
  - [API Draft v2](docs/API_V2.md)
445
+ - [Patterns](docs/PATTERNS.md)
446
+ - [Branches v1](docs/BRANCHES_V1.md)
447
+ - [Collections v1](docs/COLLECTIONS_V1.md)
448
+ - [Store Adapters](docs/STORE_ADAPTERS.md)
230
449
  - [Concepts and Principles](docs/IGNITER_CONCEPTS.md)
231
450
 
232
451
  ## Direction
@@ -251,6 +470,10 @@ rake spec
251
470
  Current baseline:
252
471
 
253
472
  - synchronous runtime
473
+ - parallel thread-pool runner
474
+ - pending/deferred runtime states
475
+ - snapshot/restore execution lifecycle
476
+ - store-backed resume flow
254
477
  - compile-time graph validation
255
478
  - typed inputs
256
479
  - composition
data/docs/API_V2.md CHANGED
@@ -54,6 +54,7 @@ contract.result.gross_total
54
54
  contract.result.to_h
55
55
  contract.success?
56
56
  contract.failed?
57
+ contract.pending?
57
58
 
58
59
  contract.update_inputs(order_total: 120)
59
60
  contract.result.gross_total
@@ -67,9 +68,11 @@ Suggested instance methods:
67
68
  - `update_inputs`
68
69
  - `events`
69
70
  - `execution`
71
+ - `explain_plan`
70
72
  - `diagnostics`
71
73
  - `success?`
72
74
  - `failed?`
75
+ - `pending?`
73
76
 
74
77
  ## Result API
75
78
 
@@ -82,6 +85,7 @@ Suggested methods:
82
85
  - `as_json`
83
86
  - `success?`
84
87
  - `failed?`
88
+ - `pending?`
85
89
  - `errors`
86
90
  - `states`
87
91
  - `explain`
@@ -119,6 +123,75 @@ Method form:
119
123
  compute :vat_rate, depends_on: [:country], call: :resolve_vat_rate
120
124
  ```
121
125
 
126
+ Executor registry form:
127
+
128
+ ```ruby
129
+ Igniter.register_executor("pricing.multiply", MultiplyExecutor)
130
+
131
+ compute :gross_total,
132
+ depends_on: %i[order_total multiplier],
133
+ executor: "pricing.multiply"
134
+ ```
135
+
136
+ Ergonomic helper forms:
137
+
138
+ ```ruby
139
+ const :vendor_id, "eLocal"
140
+
141
+ lookup :trade, depends_on: [:trade_name] do |trade_name:|
142
+ Trade.enabled.find_by!(name: trade_name)
143
+ end
144
+
145
+ map :normalized_trade_name, from: :service do |service:|
146
+ service.downcase == "heating" ? "HVAC" : service
147
+ end
148
+
149
+ guard :business_hours_valid, depends_on: %i[vendor current_time], message: "Closed" do |vendor:, current_time:|
150
+ current_time.between?(vendor.start_at, vendor.stop_at)
151
+ end
152
+
153
+ expose :bid_details, as: :response
154
+ ```
155
+
156
+ Short dependency alias:
157
+
158
+ ```ruby
159
+ compute :zip_code, with: :zip_code_raw do |zip_code_raw:|
160
+ ZipCode.find_by_code!(zip_code_raw)
161
+ end
162
+ ```
163
+
164
+ Matcher-style guards:
165
+
166
+ ```ruby
167
+ guard :usa_only, with: :country_code, eq: "USA", message: "Unsupported country"
168
+ guard :supported_country, with: :country_code, in: %w[USA CAN], message: "Unsupported country"
169
+ guard :valid_zip, with: :zip_code, matches: /\A\d{5}\z/, message: "Invalid zip"
170
+ ```
171
+
172
+ Declarative routing:
173
+
174
+ ```ruby
175
+ branch :delivery_strategy, with: :country, inputs: {
176
+ country: :country,
177
+ order_total: :order_total
178
+ } do
179
+ on "US", contract: USDeliveryContract
180
+ on "UA", contract: LocalDeliveryContract
181
+ default contract: DefaultDeliveryContract
182
+ end
183
+ ```
184
+
185
+ Declarative fan-out:
186
+
187
+ ```ruby
188
+ collection :technicians,
189
+ with: :technician_inputs,
190
+ each: TechnicianContract,
191
+ key: :technician_id,
192
+ mode: :collect
193
+ ```
194
+
122
195
  Rules:
123
196
 
124
197
  - one compute node has one callable
@@ -149,6 +222,99 @@ compose :pricing, contract: PriceContract, inputs: {
149
222
  output :pricing, from: :pricing
150
223
  ```
151
224
 
225
+ Child output export:
226
+
227
+ ```ruby
228
+ output :gross_total, from: "pricing.gross_total"
229
+ ```
230
+
231
+ Bulk child output export:
232
+
233
+ ```ruby
234
+ export :gross_total, :vat_rate, from: :pricing
235
+ ```
236
+
237
+ Branch output export:
238
+
239
+ ```ruby
240
+ export :price, :eta, from: :delivery_strategy
241
+ ```
242
+
243
+ Collection output:
244
+
245
+ ```ruby
246
+ output :technicians
247
+ ```
248
+
249
+ This returns a `CollectionResult` rather than a plain array.
250
+
251
+ Suggested `CollectionResult` surface:
252
+
253
+ - `keys`
254
+ - `successes`
255
+ - `failures`
256
+ - `summary`
257
+ - `items_summary`
258
+ - `failed_items`
259
+ - `to_h`
260
+ - `as_json`
261
+
262
+ Nested routing and fan-out can be combined:
263
+
264
+ ```ruby
265
+ branch :status_route, with: :telephony_status, inputs: {
266
+ extension_id: :extension_id,
267
+ telephony_status: :telephony_status,
268
+ active_calls: :active_calls
269
+ } do
270
+ on "CallConnected", contract: CallConnectedContract
271
+ on "NoCall", contract: NoCallContract
272
+ default contract: UnknownStatusContract
273
+ end
274
+ ```
275
+
276
+ Inside the selected branch contract:
277
+
278
+ ```ruby
279
+ collection :calls,
280
+ with: :call_inputs,
281
+ each: CallEventContract,
282
+ key: :session_id,
283
+ mode: :collect
284
+ ```
285
+
286
+ Diagnostics and audit stay local to the execution that owns the node:
287
+
288
+ - the parent execution records top-level branch selection
289
+ - collection item events belong to the selected child execution
290
+ - collection summaries should usually be read from the child contract diagnostics
291
+
292
+ Pass-through or aliased output exposure:
293
+
294
+ ```ruby
295
+ expose :bid_details, as: :response
296
+ expose :gross_total
297
+ ```
298
+
299
+ Grouped node paths:
300
+
301
+ ```ruby
302
+ scope :availability do
303
+ lookup :vendor, with: %i[trade vendor_id], call: LookupVendor
304
+ lookup :zip_code, with: :zip_code_raw, call: LookupZipCode
305
+ compute :geo_bids, with: %i[zip_code vendor], call: LookupGeoBids
306
+ end
307
+
308
+ namespace :validation do
309
+ guard :valid_zip, with: :zip_code, matches: /\A\d{5}\z/, message: "Invalid zip"
310
+ end
311
+ ```
312
+
313
+ `scope` and `namespace` currently improve structure and introspection by prefixing node paths.
314
+ They do not yet introduce a separate runtime boundary.
315
+
316
+ Branch nodes introduce explicit control flow and behave like composition-like nested results.
317
+
152
318
  Collection composition can be added later, but should not complicate the first kernel API.
153
319
 
154
320
  ## Introspection API
@@ -161,6 +327,8 @@ PriceContract.graph.to_h
161
327
  PriceContract.graph.to_mermaid
162
328
 
163
329
  contract.execution.states
330
+ contract.execution.plan
331
+ contract.explain_plan
164
332
  contract.execution.to_h
165
333
  contract.execution.as_json
166
334
  contract.result.as_json
@@ -168,6 +336,7 @@ contract.events.map(&:as_json)
168
336
  contract.diagnostics.to_h
169
337
  contract.diagnostics.to_text
170
338
  contract.diagnostics.to_markdown
339
+ contract.snapshot
171
340
  ```
172
341
 
173
342
  The main rule is that introspection reads stable compile/runtime objects rather than poking through private internals.
@@ -191,6 +360,66 @@ Or:
191
360
  contract.subscribe(auditor)
192
361
  ```
193
362
 
363
+ Reactive side-effect shorthand:
364
+
365
+ ```ruby
366
+ class LoggingContract < Igniter::Contract
367
+ define do
368
+ input :order_total
369
+ output :order_total
370
+ end
371
+
372
+ effect "order_total" do |event:, value:, **|
373
+ AuditLog.create!(path: event.path, value: value)
374
+ end
375
+ end
376
+ ```
377
+
378
+ Final-output success shorthand:
379
+
380
+ ```ruby
381
+ class PersistingContract < Igniter::Contract
382
+ define do
383
+ input :order_total
384
+ compute :gross_total, depends_on: [:order_total] do |order_total:|
385
+ order_total * 1.2
386
+ end
387
+ expose :gross_total, as: :response
388
+ end
389
+
390
+ on_success :response do |value:, contract:, **|
391
+ AuditLog.create!(response: value, inputs: contract.execution.inputs)
392
+ end
393
+ end
394
+ ```
395
+
396
+ Async/store-backed flow:
397
+
398
+ ```ruby
399
+ contract = AsyncPricingContract.new(order_total: 100)
400
+ deferred = contract.result.gross_total
401
+
402
+ AsyncPricingContract.resume_from_store(
403
+ contract.execution.events.execution_id,
404
+ token: deferred.token,
405
+ value: 150
406
+ )
407
+ ```
408
+
409
+ Reference store adapters:
410
+
411
+ ```ruby
412
+ Igniter.execution_store = Igniter::Runtime::Stores::ActiveRecordStore.new(
413
+ record_class: IgniterExecutionSnapshot
414
+ )
415
+ ```
416
+
417
+ ```ruby
418
+ Igniter.execution_store = Igniter::Runtime::Stores::RedisStore.new(
419
+ redis: Redis.new(url: ENV.fetch("REDIS_URL"))
420
+ )
421
+ ```
422
+
194
423
  Where a subscriber responds to:
195
424
 
196
425
  ```ruby
@@ -238,5 +467,13 @@ Each Igniter error also carries structured context when available:
238
467
  - collection composition
239
468
  - advanced typed schemas
240
469
  - retries
241
- - async executors
242
470
  - Rails-specific DSL sugar
471
+
472
+ ### Now Present In v2 Core
473
+
474
+ - executor registry
475
+ - schema-driven graph compilation
476
+ - thread-pool runner
477
+ - deferred/pending executor protocol
478
+ - execution snapshot/restore
479
+ - store-backed resume flow