igniter 0.2.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +224 -1
- data/docs/API_V2.md +296 -1
- data/docs/BACKLOG.md +166 -0
- data/docs/BRANCHES_V1.md +213 -0
- data/docs/COLLECTIONS_V1.md +303 -0
- data/docs/EXECUTION_MODEL_V2.md +79 -0
- data/docs/PATTERNS.md +222 -0
- data/docs/STORE_ADAPTERS.md +126 -0
- data/examples/README.md +127 -0
- data/examples/async_store.rb +47 -0
- data/examples/collection.rb +43 -0
- data/examples/collection_partial_failure.rb +50 -0
- data/examples/marketing_ergonomics.rb +57 -0
- data/examples/ringcentral_routing.rb +269 -0
- data/lib/igniter/compiler/compiled_graph.rb +90 -0
- data/lib/igniter/compiler/graph_compiler.rb +12 -2
- data/lib/igniter/compiler/type_resolver.rb +54 -0
- data/lib/igniter/compiler/validation_context.rb +61 -0
- data/lib/igniter/compiler/validation_pipeline.rb +30 -0
- data/lib/igniter/compiler/validator.rb +1 -187
- data/lib/igniter/compiler/validators/callable_validator.rb +107 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +153 -0
- data/lib/igniter/compiler/validators/outputs_validator.rb +66 -0
- data/lib/igniter/compiler/validators/type_compatibility_validator.rb +84 -0
- data/lib/igniter/compiler/validators/uniqueness_validator.rb +60 -0
- data/lib/igniter/compiler.rb +8 -0
- data/lib/igniter/contract.rb +152 -4
- data/lib/igniter/diagnostics/auditing/report/console_formatter.rb +80 -0
- data/lib/igniter/diagnostics/auditing/report/markdown_formatter.rb +22 -0
- data/lib/igniter/diagnostics/introspection/formatters/mermaid_formatter.rb +58 -0
- data/lib/igniter/diagnostics/introspection/formatters/text_tree_formatter.rb +44 -0
- data/lib/igniter/diagnostics/report.rb +186 -11
- data/lib/igniter/dsl/contract_builder.rb +271 -5
- data/lib/igniter/dsl/schema_builder.rb +73 -0
- data/lib/igniter/dsl.rb +1 -0
- data/lib/igniter/errors.rb +11 -0
- data/lib/igniter/events/bus.rb +5 -0
- data/lib/igniter/events/event.rb +29 -0
- data/lib/igniter/executor.rb +74 -0
- data/lib/igniter/executor_registry.rb +44 -0
- data/lib/igniter/extensions/auditing/timeline.rb +4 -0
- data/lib/igniter/extensions/introspection/graph_formatter.rb +33 -3
- data/lib/igniter/extensions/introspection/plan_formatter.rb +55 -0
- data/lib/igniter/extensions/introspection/runtime_formatter.rb +18 -3
- data/lib/igniter/extensions/introspection.rb +1 -0
- data/lib/igniter/extensions/reactive/engine.rb +49 -2
- data/lib/igniter/extensions/reactive/reaction.rb +3 -2
- data/lib/igniter/model/branch_node.rb +46 -0
- data/lib/igniter/model/collection_node.rb +31 -0
- data/lib/igniter/model/composition_node.rb +2 -2
- data/lib/igniter/model/compute_node.rb +58 -2
- data/lib/igniter/model/input_node.rb +2 -2
- data/lib/igniter/model/output_node.rb +24 -4
- data/lib/igniter/model.rb +2 -0
- data/lib/igniter/runtime/cache.rb +64 -25
- data/lib/igniter/runtime/collection_result.rb +111 -0
- data/lib/igniter/runtime/deferred_result.rb +40 -0
- data/lib/igniter/runtime/execution.rb +261 -11
- data/lib/igniter/runtime/input_validator.rb +2 -24
- data/lib/igniter/runtime/invalidator.rb +1 -1
- data/lib/igniter/runtime/job_worker.rb +18 -0
- data/lib/igniter/runtime/node_state.rb +20 -0
- data/lib/igniter/runtime/planner.rb +126 -0
- data/lib/igniter/runtime/resolver.rb +310 -15
- data/lib/igniter/runtime/result.rb +14 -2
- data/lib/igniter/runtime/runner_factory.rb +20 -0
- data/lib/igniter/runtime/runners/inline_runner.rb +21 -0
- data/lib/igniter/runtime/runners/store_runner.rb +29 -0
- data/lib/igniter/runtime/runners/thread_pool_runner.rb +37 -0
- data/lib/igniter/runtime/stores/active_record_store.rb +41 -0
- data/lib/igniter/runtime/stores/file_store.rb +43 -0
- data/lib/igniter/runtime/stores/memory_store.rb +40 -0
- data/lib/igniter/runtime/stores/redis_store.rb +44 -0
- data/lib/igniter/runtime.rb +12 -0
- data/lib/igniter/type_system.rb +44 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +23 -0
- metadata +43 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 659dfe833fdf98b7d1b446e08d02464545fd0fe9b61badd5cf8e7ca73beb6a1b
|
|
4
|
+
data.tar.gz: 5af168ce8c6fad1c18dd0d9703281ea64062c3a6d3e954b12172f29210e12c5b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5865bc3fb30c137baa5bdf87b63f9023c95c50afa87701a052a47726d139ab6113ed52820f629d0b45de421247778d560133e2c2d2f5e756681a14af2cac0026
|
|
7
|
+
data.tar.gz: de8fa4b7f92a566016f21d81acdbf9d5ec4f4f0fa458ac99b18f8c5cced124390276ea541a1f252067009fd4e3ccf688d97d239080b8995227fc697f0d5b9e6e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
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
|
+
|
|
12
|
+
## [0.3.0] - 2026-03-19
|
|
13
|
+
|
|
14
|
+
- Add executor metadata and global executor registry for self-describing, schema-friendly execution steps.
|
|
15
|
+
- Split compiler validation into a pluggable validation pipeline and add shared type compatibility checks.
|
|
16
|
+
- Introduce planner/runner runtime architecture with `:inline`, `:thread_pool`, and store-backed execution modes.
|
|
17
|
+
- Add deferred nodes, pending state, snapshot/restore, token-based resume, worker-style resume flow, and reference file/ActiveRecord/Redis store adapters.
|
|
18
|
+
- Expand the DSL with `with`, matcher-style `guard`, `scope`, `namespace`, `branch`, `collection`, `expose`, `on_failure`, and `on_exit`.
|
|
19
|
+
- Add `branch` and `collection` as graph primitives with compile-time validation, nested runtime support, and item-level collection events.
|
|
20
|
+
- Improve diagnostics and auditing with collection summaries, partial-failure visibility, item-level failure reporting, and richer markdown/text output.
|
|
21
|
+
- Add production-like runnable examples for async resume, ergonomic domain contracts, collection partial failure, and nested branch + collection routing.
|
|
22
|
+
- Add design docs for branches, collections, store adapters, and orchestration patterns.
|
|
23
|
+
|
|
3
24
|
## [0.2.0] - 2026-03-18
|
|
4
25
|
|
|
5
26
|
- 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`, `project`, `aggregate`, `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`, `project`, `aggregate`, `using:`/`map_inputs`, 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
|
-
|
|
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,133 @@ 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
|
+
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
|
+
|
|
155
|
+
guard :business_hours_valid, depends_on: %i[vendor current_time], message: "Closed" do |vendor:, current_time:|
|
|
156
|
+
current_time.between?(vendor.start_at, vendor.stop_at)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
expose :bid_details, as: :response
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Short dependency alias:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
compute :zip_code, with: :zip_code_raw do |zip_code_raw:|
|
|
166
|
+
ZipCode.find_by_code!(zip_code_raw)
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Matcher-style guards:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
guard :usa_only, with: :country_code, eq: "USA", message: "Unsupported country"
|
|
174
|
+
guard :supported_country, with: :country_code, in: %w[USA CAN], message: "Unsupported country"
|
|
175
|
+
guard :valid_zip, with: :zip_code, matches: /\A\d{5}\z/, message: "Invalid zip"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Declarative routing:
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
branch :delivery_strategy, with: :country, inputs: {
|
|
182
|
+
country: :country,
|
|
183
|
+
order_total: :order_total
|
|
184
|
+
} do
|
|
185
|
+
on "US", contract: USDeliveryContract
|
|
186
|
+
on "UA", contract: LocalDeliveryContract
|
|
187
|
+
default contract: DefaultDeliveryContract
|
|
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
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Declarative fan-out:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
collection :technicians,
|
|
210
|
+
with: :technician_inputs,
|
|
211
|
+
each: TechnicianContract,
|
|
212
|
+
key: :technician_id,
|
|
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
|
|
251
|
+
```
|
|
252
|
+
|
|
122
253
|
Rules:
|
|
123
254
|
|
|
124
255
|
- one compute node has one callable
|
|
@@ -149,6 +280,99 @@ compose :pricing, contract: PriceContract, inputs: {
|
|
|
149
280
|
output :pricing, from: :pricing
|
|
150
281
|
```
|
|
151
282
|
|
|
283
|
+
Child output export:
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
output :gross_total, from: "pricing.gross_total"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Bulk child output export:
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
export :gross_total, :vat_rate, from: :pricing
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Branch output export:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
export :price, :eta, from: :delivery_strategy
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Collection output:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
output :technicians
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
This returns a `CollectionResult` rather than a plain array.
|
|
308
|
+
|
|
309
|
+
Suggested `CollectionResult` surface:
|
|
310
|
+
|
|
311
|
+
- `keys`
|
|
312
|
+
- `successes`
|
|
313
|
+
- `failures`
|
|
314
|
+
- `summary`
|
|
315
|
+
- `items_summary`
|
|
316
|
+
- `failed_items`
|
|
317
|
+
- `to_h`
|
|
318
|
+
- `as_json`
|
|
319
|
+
|
|
320
|
+
Nested routing and fan-out can be combined:
|
|
321
|
+
|
|
322
|
+
```ruby
|
|
323
|
+
branch :status_route, with: :telephony_status, inputs: {
|
|
324
|
+
extension_id: :extension_id,
|
|
325
|
+
telephony_status: :telephony_status,
|
|
326
|
+
active_calls: :active_calls
|
|
327
|
+
} do
|
|
328
|
+
on "CallConnected", contract: CallConnectedContract
|
|
329
|
+
on "NoCall", contract: NoCallContract
|
|
330
|
+
default contract: UnknownStatusContract
|
|
331
|
+
end
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Inside the selected branch contract:
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
collection :calls,
|
|
338
|
+
with: :call_inputs,
|
|
339
|
+
each: CallEventContract,
|
|
340
|
+
key: :session_id,
|
|
341
|
+
mode: :collect
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Diagnostics and audit stay local to the execution that owns the node:
|
|
345
|
+
|
|
346
|
+
- the parent execution records top-level branch selection
|
|
347
|
+
- collection item events belong to the selected child execution
|
|
348
|
+
- collection summaries should usually be read from the child contract diagnostics
|
|
349
|
+
|
|
350
|
+
Pass-through or aliased output exposure:
|
|
351
|
+
|
|
352
|
+
```ruby
|
|
353
|
+
expose :bid_details, as: :response
|
|
354
|
+
expose :gross_total
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Grouped node paths:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
scope :availability do
|
|
361
|
+
lookup :vendor, with: %i[trade vendor_id], call: LookupVendor
|
|
362
|
+
lookup :zip_code, with: :zip_code_raw, call: LookupZipCode
|
|
363
|
+
compute :geo_bids, with: %i[zip_code vendor], call: LookupGeoBids
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
namespace :validation do
|
|
367
|
+
guard :valid_zip, with: :zip_code, matches: /\A\d{5}\z/, message: "Invalid zip"
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`scope` and `namespace` currently improve structure and introspection by prefixing node paths.
|
|
372
|
+
They do not yet introduce a separate runtime boundary.
|
|
373
|
+
|
|
374
|
+
Branch nodes introduce explicit control flow and behave like composition-like nested results.
|
|
375
|
+
|
|
152
376
|
Collection composition can be added later, but should not complicate the first kernel API.
|
|
153
377
|
|
|
154
378
|
## Introspection API
|
|
@@ -161,6 +385,8 @@ PriceContract.graph.to_h
|
|
|
161
385
|
PriceContract.graph.to_mermaid
|
|
162
386
|
|
|
163
387
|
contract.execution.states
|
|
388
|
+
contract.execution.plan
|
|
389
|
+
contract.explain_plan
|
|
164
390
|
contract.execution.to_h
|
|
165
391
|
contract.execution.as_json
|
|
166
392
|
contract.result.as_json
|
|
@@ -168,6 +394,7 @@ contract.events.map(&:as_json)
|
|
|
168
394
|
contract.diagnostics.to_h
|
|
169
395
|
contract.diagnostics.to_text
|
|
170
396
|
contract.diagnostics.to_markdown
|
|
397
|
+
contract.snapshot
|
|
171
398
|
```
|
|
172
399
|
|
|
173
400
|
The main rule is that introspection reads stable compile/runtime objects rather than poking through private internals.
|
|
@@ -191,6 +418,66 @@ Or:
|
|
|
191
418
|
contract.subscribe(auditor)
|
|
192
419
|
```
|
|
193
420
|
|
|
421
|
+
Reactive side-effect shorthand:
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
class LoggingContract < Igniter::Contract
|
|
425
|
+
define do
|
|
426
|
+
input :order_total
|
|
427
|
+
output :order_total
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
effect "order_total" do |event:, value:, **|
|
|
431
|
+
AuditLog.create!(path: event.path, value: value)
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Final-output success shorthand:
|
|
437
|
+
|
|
438
|
+
```ruby
|
|
439
|
+
class PersistingContract < Igniter::Contract
|
|
440
|
+
define do
|
|
441
|
+
input :order_total
|
|
442
|
+
compute :gross_total, depends_on: [:order_total] do |order_total:|
|
|
443
|
+
order_total * 1.2
|
|
444
|
+
end
|
|
445
|
+
expose :gross_total, as: :response
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
on_success :response do |value:, contract:, **|
|
|
449
|
+
AuditLog.create!(response: value, inputs: contract.execution.inputs)
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Async/store-backed flow:
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
contract = AsyncPricingContract.new(order_total: 100)
|
|
458
|
+
deferred = contract.result.gross_total
|
|
459
|
+
|
|
460
|
+
AsyncPricingContract.resume_from_store(
|
|
461
|
+
contract.execution.events.execution_id,
|
|
462
|
+
token: deferred.token,
|
|
463
|
+
value: 150
|
|
464
|
+
)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Reference store adapters:
|
|
468
|
+
|
|
469
|
+
```ruby
|
|
470
|
+
Igniter.execution_store = Igniter::Runtime::Stores::ActiveRecordStore.new(
|
|
471
|
+
record_class: IgniterExecutionSnapshot
|
|
472
|
+
)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
Igniter.execution_store = Igniter::Runtime::Stores::RedisStore.new(
|
|
477
|
+
redis: Redis.new(url: ENV.fetch("REDIS_URL"))
|
|
478
|
+
)
|
|
479
|
+
```
|
|
480
|
+
|
|
194
481
|
Where a subscriber responds to:
|
|
195
482
|
|
|
196
483
|
```ruby
|
|
@@ -238,5 +525,13 @@ Each Igniter error also carries structured context when available:
|
|
|
238
525
|
- collection composition
|
|
239
526
|
- advanced typed schemas
|
|
240
527
|
- retries
|
|
241
|
-
- async executors
|
|
242
528
|
- Rails-specific DSL sugar
|
|
529
|
+
|
|
530
|
+
### Now Present In v2 Core
|
|
531
|
+
|
|
532
|
+
- executor registry
|
|
533
|
+
- schema-driven graph compilation
|
|
534
|
+
- thread-pool runner
|
|
535
|
+
- deferred/pending executor protocol
|
|
536
|
+
- execution snapshot/restore
|
|
537
|
+
- store-backed resume flow
|