igniter 0.3.1 → 0.4.3
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 +25 -0
- data/README.md +238 -218
- data/docs/DISTRIBUTED_CONTRACTS_V1.md +493 -0
- data/docs/LLM_V1.md +335 -0
- data/docs/PATTERNS.md +189 -0
- data/docs/SERVER_V1.md +313 -0
- data/examples/README.md +129 -0
- data/examples/agents.rb +150 -0
- data/examples/differential.rb +161 -0
- data/examples/distributed_server.rb +94 -0
- data/examples/distributed_workflow.rb +52 -0
- data/examples/effects.rb +184 -0
- data/examples/invariants.rb +179 -0
- data/examples/order_pipeline.rb +163 -0
- data/examples/provenance.rb +122 -0
- data/examples/saga.rb +110 -0
- data/lib/igniter/agent/mailbox.rb +96 -0
- data/lib/igniter/agent/message.rb +21 -0
- data/lib/igniter/agent/ref.rb +86 -0
- data/lib/igniter/agent/runner.rb +129 -0
- data/lib/igniter/agent/state_holder.rb +23 -0
- data/lib/igniter/agent.rb +155 -0
- data/lib/igniter/compiler/compiled_graph.rb +12 -0
- data/lib/igniter/compiler/validation_pipeline.rb +3 -1
- data/lib/igniter/compiler/validators/await_validator.rb +53 -0
- data/lib/igniter/compiler/validators/callable_validator.rb +21 -3
- data/lib/igniter/compiler/validators/dependencies_validator.rb +41 -1
- data/lib/igniter/compiler/validators/remote_validator.rb +58 -0
- data/lib/igniter/compiler.rb +2 -0
- data/lib/igniter/contract.rb +59 -8
- data/lib/igniter/differential/divergence.rb +29 -0
- data/lib/igniter/differential/formatter.rb +96 -0
- data/lib/igniter/differential/report.rb +86 -0
- data/lib/igniter/differential/runner.rb +130 -0
- data/lib/igniter/differential.rb +51 -0
- data/lib/igniter/dsl/contract_builder.rb +74 -4
- data/lib/igniter/effect.rb +91 -0
- data/lib/igniter/effect_registry.rb +78 -0
- data/lib/igniter/errors.rb +17 -2
- data/lib/igniter/execution_report/builder.rb +54 -0
- data/lib/igniter/execution_report/formatter.rb +50 -0
- data/lib/igniter/execution_report/node_entry.rb +24 -0
- data/lib/igniter/execution_report/report.rb +65 -0
- data/lib/igniter/execution_report.rb +32 -0
- data/lib/igniter/extensions/differential.rb +114 -0
- data/lib/igniter/extensions/execution_report.rb +27 -0
- data/lib/igniter/extensions/invariants.rb +116 -0
- data/lib/igniter/extensions/provenance.rb +45 -0
- data/lib/igniter/extensions/saga.rb +74 -0
- data/lib/igniter/integrations/agents.rb +18 -0
- data/lib/igniter/integrations/llm/config.rb +69 -0
- data/lib/igniter/integrations/llm/context.rb +74 -0
- data/lib/igniter/integrations/llm/executor.rb +159 -0
- data/lib/igniter/integrations/llm/providers/anthropic.rb +148 -0
- data/lib/igniter/integrations/llm/providers/base.rb +33 -0
- data/lib/igniter/integrations/llm/providers/ollama.rb +137 -0
- data/lib/igniter/integrations/llm/providers/openai.rb +153 -0
- data/lib/igniter/integrations/llm.rb +59 -0
- data/lib/igniter/integrations/rails/cable_adapter.rb +49 -0
- data/lib/igniter/integrations/rails/contract_job.rb +76 -0
- data/lib/igniter/integrations/rails/generators/contract/contract_generator.rb +22 -0
- data/lib/igniter/integrations/rails/generators/install/install_generator.rb +33 -0
- data/lib/igniter/integrations/rails/railtie.rb +25 -0
- data/lib/igniter/integrations/rails/webhook_concern.rb +49 -0
- data/lib/igniter/integrations/rails.rb +12 -0
- data/lib/igniter/invariant.rb +50 -0
- data/lib/igniter/model/await_node.rb +21 -0
- data/lib/igniter/model/effect_node.rb +37 -0
- data/lib/igniter/model/remote_node.rb +26 -0
- data/lib/igniter/model.rb +3 -0
- data/lib/igniter/property_testing/formatter.rb +66 -0
- data/lib/igniter/property_testing/generators.rb +115 -0
- data/lib/igniter/property_testing/result.rb +45 -0
- data/lib/igniter/property_testing/run.rb +43 -0
- data/lib/igniter/property_testing/runner.rb +47 -0
- data/lib/igniter/property_testing.rb +64 -0
- data/lib/igniter/provenance/builder.rb +97 -0
- data/lib/igniter/provenance/lineage.rb +82 -0
- data/lib/igniter/provenance/node_trace.rb +65 -0
- data/lib/igniter/provenance/text_formatter.rb +70 -0
- data/lib/igniter/provenance.rb +29 -0
- data/lib/igniter/registry.rb +67 -0
- data/lib/igniter/runtime/execution.rb +2 -2
- data/lib/igniter/runtime/input_validator.rb +5 -3
- data/lib/igniter/runtime/resolver.rb +58 -1
- data/lib/igniter/runtime/stores/active_record_store.rb +13 -1
- data/lib/igniter/runtime/stores/file_store.rb +50 -2
- data/lib/igniter/runtime/stores/memory_store.rb +55 -2
- data/lib/igniter/runtime/stores/redis_store.rb +13 -1
- data/lib/igniter/saga/compensation.rb +31 -0
- data/lib/igniter/saga/compensation_record.rb +20 -0
- data/lib/igniter/saga/executor.rb +85 -0
- data/lib/igniter/saga/formatter.rb +49 -0
- data/lib/igniter/saga/result.rb +47 -0
- data/lib/igniter/saga.rb +56 -0
- data/lib/igniter/server/client.rb +123 -0
- data/lib/igniter/server/config.rb +27 -0
- data/lib/igniter/server/handlers/base.rb +105 -0
- data/lib/igniter/server/handlers/contracts_handler.rb +15 -0
- data/lib/igniter/server/handlers/event_handler.rb +28 -0
- data/lib/igniter/server/handlers/execute_handler.rb +37 -0
- data/lib/igniter/server/handlers/health_handler.rb +32 -0
- data/lib/igniter/server/handlers/status_handler.rb +27 -0
- data/lib/igniter/server/http_server.rb +109 -0
- data/lib/igniter/server/rack_app.rb +35 -0
- data/lib/igniter/server/registry.rb +56 -0
- data/lib/igniter/server/router.rb +75 -0
- data/lib/igniter/server.rb +67 -0
- data/lib/igniter/stream_loop.rb +80 -0
- data/lib/igniter/supervisor.rb +167 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +14 -0
- metadata +92 -2
data/README.md
CHANGED
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Igniter is a Ruby gem for expressing business logic as a validated dependency graph and executing that graph with:
|
|
4
4
|
|
|
5
|
-
- lazy output resolution
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
The repository now contains a working v2 core built around explicit compile-time and runtime boundaries.
|
|
5
|
+
- lazy output resolution and selective cache invalidation
|
|
6
|
+
- typed input validation with defaults and required fields
|
|
7
|
+
- nested contract composition with isolated child executions
|
|
8
|
+
- declarative routing (`branch`) and fan-out (`collection`)
|
|
9
|
+
- distributed workflows: `await` events across process boundaries
|
|
10
|
+
- multi-node deployments via `igniter-server` and the `remote:` DSL
|
|
11
|
+
- LLM compute nodes with Ollama, Anthropic, and OpenAI providers
|
|
12
|
+
- Rails integration: ActiveJob, ActionCable, webhook handlers, generators
|
|
13
|
+
- runtime auditing, diagnostics reports, and reactive side effects
|
|
14
|
+
- graph and runtime introspection (text, Mermaid)
|
|
15
|
+
- ergonomic DSL helpers: `const`, `lookup`, `map`, `project`, `aggregate`, `guard`, `export`, `expose`, `effect`, `on_success`, `scope`, `namespace`
|
|
18
16
|
|
|
19
17
|
## Installation
|
|
20
18
|
|
|
@@ -30,8 +28,8 @@ require "igniter"
|
|
|
30
28
|
class PriceContract < Igniter::Contract
|
|
31
29
|
define do
|
|
32
30
|
input :order_total, type: :numeric
|
|
33
|
-
input :country,
|
|
34
|
-
input :vat_rate,
|
|
31
|
+
input :country, type: :string
|
|
32
|
+
input :vat_rate, type: :numeric, default: 0.2
|
|
35
33
|
|
|
36
34
|
compute :effective_vat_rate, depends_on: %i[country vat_rate] do |country:, vat_rate:|
|
|
37
35
|
country == "UA" ? vat_rate : 0.0
|
|
@@ -46,51 +44,36 @@ class PriceContract < Igniter::Contract
|
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
contract = PriceContract.new(order_total: 100, country: "UA")
|
|
49
|
-
|
|
50
|
-
contract.result.gross_total
|
|
51
|
-
# => 120.0
|
|
47
|
+
contract.result.gross_total # => 120.0
|
|
52
48
|
|
|
53
49
|
contract.update_inputs(order_total: 150)
|
|
54
|
-
contract.result.gross_total
|
|
55
|
-
# => 180.0
|
|
50
|
+
contract.result.gross_total # => 180.0
|
|
56
51
|
|
|
57
|
-
contract.diagnostics_text
|
|
58
|
-
# => compact execution summary
|
|
52
|
+
contract.diagnostics_text # compact execution summary
|
|
59
53
|
```
|
|
60
54
|
|
|
61
55
|
## Features
|
|
62
56
|
|
|
63
|
-
- Contracts
|
|
64
|
-
- Compiler
|
|
65
|
-
- Runtime
|
|
66
|
-
- Typed inputs
|
|
67
|
-
- Composition
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
57
|
+
- **Contracts**: declare inputs, compute nodes, outputs, and compositions in a validated graph.
|
|
58
|
+
- **Compiler**: validate dependency graphs, types, and cycles before runtime; errors are surfaced at load time.
|
|
59
|
+
- **Runtime**: cache resolved nodes and invalidate only affected downstream nodes on input change.
|
|
60
|
+
- **Typed inputs**: validate types, defaults, and required fields at execution boundaries.
|
|
61
|
+
- **Composition**: execute nested contracts with isolated child executions.
|
|
62
|
+
- **Branch**: declarative routing — select one child contract from ordered cases at runtime.
|
|
63
|
+
- **Collection**: declarative fan-out — run one child contract per item in an array.
|
|
64
|
+
- **Distributed workflows**: `await` external events; resume via `deliver_event`.
|
|
65
|
+
- **igniter-server**: host contracts as a TCP/Rack HTTP service; call remote contracts with the `remote:` DSL.
|
|
66
|
+
- **LLM integration**: compute nodes powered by Ollama, Anthropic, or OpenAI providers.
|
|
67
|
+
- **Rails integration**: Railtie, ActiveJob base class, ActionCable adapter, webhook controller mixin.
|
|
68
|
+
- **Auditing**: collect execution timelines and snapshots.
|
|
69
|
+
- **Diagnostics**: compact text, Markdown, or structured reports for triage.
|
|
70
|
+
- **Reactive**: subscribe declaratively to runtime events with `effect`, `on_success`, `on_failure`.
|
|
71
|
+
- **Introspection**: render graphs as text or Mermaid; inspect runtime state.
|
|
73
72
|
|
|
74
73
|
## Quick Start Recipes
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
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).
|
|
80
|
-
|
|
81
|
-
| Example | Run | Shows |
|
|
82
|
-
| --- | --- | --- |
|
|
83
|
-
| `basic_pricing.rb` | `ruby examples/basic_pricing.rb` | basic contract, lazy resolution, input updates |
|
|
84
|
-
| `composition.rb` | `ruby examples/composition.rb` | nested contracts and composed results |
|
|
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 |
|
|
91
|
-
|
|
92
|
-
There are also matching living examples in `spec/igniter/examples_spec.rb`.
|
|
93
|
-
Those are useful if you want to read the examples in test form.
|
|
75
|
+
Runnable examples live in [`examples/`](examples) and are smoke-tested by `spec/igniter/example_scripts_spec.rb`.
|
|
76
|
+
See [`examples/README.md`](examples/README.md) for a quick index and [`docs/PATTERNS.md`](docs/PATTERNS.md) for composable patterns.
|
|
94
77
|
|
|
95
78
|
### 1. Basic Pricing Contract
|
|
96
79
|
|
|
@@ -98,7 +81,7 @@ Those are useful if you want to read the examples in test form.
|
|
|
98
81
|
class PriceContract < Igniter::Contract
|
|
99
82
|
define do
|
|
100
83
|
input :order_total, type: :numeric
|
|
101
|
-
input :country,
|
|
84
|
+
input :country, type: :string
|
|
102
85
|
|
|
103
86
|
compute :vat_rate, depends_on: [:country] do |country:|
|
|
104
87
|
country == "UA" ? 0.2 : 0.0
|
|
@@ -122,11 +105,11 @@ PriceContract.new(order_total: 100, country: "UA").result.gross_total
|
|
|
122
105
|
class CheckoutContract < Igniter::Contract
|
|
123
106
|
define do
|
|
124
107
|
input :order_total, type: :numeric
|
|
125
|
-
input :country,
|
|
108
|
+
input :country, type: :string
|
|
126
109
|
|
|
127
110
|
compose :pricing, contract: PriceContract, inputs: {
|
|
128
111
|
order_total: :order_total,
|
|
129
|
-
country:
|
|
112
|
+
country: :country
|
|
130
113
|
}
|
|
131
114
|
|
|
132
115
|
output :pricing
|
|
@@ -137,7 +120,7 @@ CheckoutContract.new(order_total: 100, country: "UA").result.pricing.gross_total
|
|
|
137
120
|
# => 120.0
|
|
138
121
|
```
|
|
139
122
|
|
|
140
|
-
### 3. Diagnostics
|
|
123
|
+
### 3. Diagnostics and Introspection
|
|
141
124
|
|
|
142
125
|
```ruby
|
|
143
126
|
contract = PriceContract.new(order_total: 100, country: "UA")
|
|
@@ -149,28 +132,24 @@ contract.diagnostics.to_h
|
|
|
149
132
|
contract.diagnostics_text
|
|
150
133
|
contract.diagnostics_markdown
|
|
151
134
|
contract.audit_snapshot
|
|
135
|
+
|
|
136
|
+
PriceContract.graph.to_text
|
|
137
|
+
PriceContract.graph.to_mermaid
|
|
152
138
|
```
|
|
153
139
|
|
|
154
140
|
### 4. Machine-Readable Data
|
|
155
141
|
|
|
156
142
|
```ruby
|
|
157
|
-
contract
|
|
158
|
-
contract.result.gross_total
|
|
159
|
-
|
|
160
|
-
contract.result.to_h
|
|
161
|
-
# => { gross_total: 120.0 }
|
|
162
|
-
|
|
143
|
+
contract.result.to_h # => { gross_total: 120.0 }
|
|
163
144
|
contract.result.as_json
|
|
164
145
|
contract.execution.as_json
|
|
165
146
|
contract.events.map(&:as_json)
|
|
166
147
|
```
|
|
167
148
|
|
|
168
|
-
### 5. Async Store
|
|
149
|
+
### 5. Async Store and Resume
|
|
169
150
|
|
|
170
151
|
```ruby
|
|
171
152
|
class AsyncQuoteExecutor < Igniter::Executor
|
|
172
|
-
input :order_total, type: :numeric
|
|
173
|
-
|
|
174
153
|
def call(order_total:)
|
|
175
154
|
defer(token: "quote-#{order_total}", payload: { kind: "pricing_quote" })
|
|
176
155
|
end
|
|
@@ -181,29 +160,22 @@ class AsyncPricingContract < Igniter::Contract
|
|
|
181
160
|
|
|
182
161
|
define do
|
|
183
162
|
input :order_total, type: :numeric
|
|
184
|
-
|
|
185
163
|
compute :quote_total, depends_on: [:order_total], call: AsyncQuoteExecutor
|
|
186
|
-
|
|
187
164
|
compute :gross_total, depends_on: [:quote_total] do |quote_total:|
|
|
188
165
|
quote_total * 1.2
|
|
189
166
|
end
|
|
190
|
-
|
|
191
167
|
output :gross_total
|
|
192
168
|
end
|
|
193
169
|
end
|
|
194
170
|
|
|
195
|
-
contract
|
|
196
|
-
deferred
|
|
171
|
+
contract = AsyncPricingContract.new(order_total: 100)
|
|
172
|
+
deferred = contract.result.gross_total
|
|
197
173
|
execution_id = contract.execution.events.execution_id
|
|
198
174
|
|
|
199
175
|
resumed = AsyncPricingContract.resume_from_store(
|
|
200
|
-
execution_id,
|
|
201
|
-
token: deferred.token,
|
|
202
|
-
value: 150
|
|
176
|
+
execution_id, token: deferred.token, value: 150
|
|
203
177
|
)
|
|
204
|
-
|
|
205
|
-
resumed.result.gross_total
|
|
206
|
-
# => 180.0
|
|
178
|
+
resumed.result.gross_total # => 180.0
|
|
207
179
|
```
|
|
208
180
|
|
|
209
181
|
### 6. Ergonomic DSL
|
|
@@ -211,14 +183,14 @@ resumed.result.gross_total
|
|
|
211
183
|
```ruby
|
|
212
184
|
class MarketingQuoteContract < Igniter::Contract
|
|
213
185
|
define do
|
|
214
|
-
input :service,
|
|
186
|
+
input :service, type: :string
|
|
215
187
|
input :zip_code, type: :string
|
|
216
188
|
|
|
217
189
|
const :vendor_id, "eLocal"
|
|
218
190
|
|
|
219
191
|
scope :routing do
|
|
220
192
|
map :trade_name, from: :service do |service:|
|
|
221
|
-
%w[heating cooling ventilation
|
|
193
|
+
%w[heating cooling ventilation].include?(service.downcase) ? "HVAC" : service
|
|
222
194
|
end
|
|
223
195
|
end
|
|
224
196
|
|
|
@@ -232,7 +204,7 @@ class MarketingQuoteContract < Igniter::Contract
|
|
|
232
204
|
guard :zip_supported, with: :zip_code, in: %w[60601 10001], message: "Unsupported zip"
|
|
233
205
|
end
|
|
234
206
|
|
|
235
|
-
compute :quote, with: %i[vendor_id trade zip_supported
|
|
207
|
+
compute :quote, with: %i[vendor_id trade zip_code zip_supported] do |vendor_id:, trade:, zip_code:, zip_supported:|
|
|
236
208
|
zip_supported
|
|
237
209
|
{ vendor_id: vendor_id, trade: trade[:name], zip_code: zip_code, bid: trade[:base_bid] }
|
|
238
210
|
end
|
|
@@ -244,19 +216,14 @@ class MarketingQuoteContract < Igniter::Contract
|
|
|
244
216
|
puts "Persist #{value.inspect}"
|
|
245
217
|
end
|
|
246
218
|
end
|
|
247
|
-
|
|
248
|
-
contract = MarketingQuoteContract.new(service: "heating", zip_code: "60601")
|
|
249
|
-
|
|
250
|
-
contract.explain_plan
|
|
251
|
-
contract.result.response
|
|
252
219
|
```
|
|
253
220
|
|
|
254
|
-
|
|
221
|
+
Matcher-style guard shortcuts:
|
|
255
222
|
|
|
256
223
|
```ruby
|
|
257
|
-
guard :usa_only,
|
|
258
|
-
guard :supported_country, with: :country_code, in: %w[USA CAN],
|
|
259
|
-
guard :valid_zip,
|
|
224
|
+
guard :usa_only, with: :country_code, eq: "USA", message: "Unsupported country"
|
|
225
|
+
guard :supported_country, with: :country_code, in: %w[USA CAN], message: "Unsupported country"
|
|
226
|
+
guard :valid_zip, with: :zip_code, matches: /\A\d{5}\z/, message: "Invalid zip"
|
|
260
227
|
```
|
|
261
228
|
|
|
262
229
|
### 7. Declarative Branching
|
|
@@ -268,12 +235,12 @@ class DeliveryContract < Igniter::Contract
|
|
|
268
235
|
input :order_total
|
|
269
236
|
|
|
270
237
|
branch :delivery_strategy, with: :country, inputs: {
|
|
271
|
-
country:
|
|
238
|
+
country: :country,
|
|
272
239
|
order_total: :order_total
|
|
273
240
|
} do
|
|
274
241
|
on "US", contract: USDeliveryContract
|
|
275
242
|
on "UA", contract: LocalDeliveryContract
|
|
276
|
-
default
|
|
243
|
+
default contract: DefaultDeliveryContract
|
|
277
244
|
end
|
|
278
245
|
|
|
279
246
|
export :price, :eta, from: :delivery_strategy
|
|
@@ -281,163 +248,225 @@ class DeliveryContract < Igniter::Contract
|
|
|
281
248
|
end
|
|
282
249
|
```
|
|
283
250
|
|
|
284
|
-
### 8.
|
|
251
|
+
### 8. Declarative Collections
|
|
285
252
|
|
|
286
253
|
```ruby
|
|
287
|
-
class
|
|
254
|
+
class TechnicianBatchContract < Igniter::Contract
|
|
288
255
|
define do
|
|
289
|
-
input :
|
|
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
|
|
256
|
+
input :technician_inputs, type: :array
|
|
304
257
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
on "CallConnected", contract: CallConnectedContract
|
|
311
|
-
on "NoCall", contract: NoCallContract
|
|
312
|
-
default contract: UnknownStatusContract
|
|
313
|
-
end
|
|
258
|
+
collection :technicians,
|
|
259
|
+
with: :technician_inputs,
|
|
260
|
+
each: TechnicianContract,
|
|
261
|
+
key: :technician_id,
|
|
262
|
+
mode: :collect
|
|
314
263
|
|
|
315
|
-
|
|
264
|
+
output :technicians
|
|
316
265
|
end
|
|
317
266
|
end
|
|
318
267
|
```
|
|
319
268
|
|
|
320
|
-
In
|
|
269
|
+
In `mode: :collect`, an execution succeeds overall while items may individually fail:
|
|
321
270
|
|
|
322
|
-
-
|
|
323
|
-
-
|
|
324
|
-
-
|
|
271
|
+
- `result.summary` — collection-level status (`:partial_failure` when any item failed)
|
|
272
|
+
- `result.items_summary` — compact per-item status hash
|
|
273
|
+
- `result.failed_items` — failed-item error details
|
|
274
|
+
- `result.successes` — hash of succeeded items only
|
|
325
275
|
|
|
326
|
-
`
|
|
276
|
+
See [`examples/collection_partial_failure.rb`](examples/collection_partial_failure.rb).
|
|
327
277
|
|
|
328
|
-
###
|
|
278
|
+
### 9. Distributed Contracts
|
|
279
|
+
|
|
280
|
+
Use `await` to suspend execution until an external event arrives. `correlate_by` identifies
|
|
281
|
+
which execution should receive the event, so events can be delivered from any process:
|
|
329
282
|
|
|
330
283
|
```ruby
|
|
331
|
-
class
|
|
284
|
+
class LeadWorkflow < Igniter::Contract
|
|
285
|
+
correlate_by :request_id
|
|
286
|
+
|
|
332
287
|
define do
|
|
333
|
-
input :
|
|
288
|
+
input :request_id
|
|
334
289
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
each: TechnicianContract,
|
|
338
|
-
key: :technician_id,
|
|
339
|
-
mode: :collect
|
|
290
|
+
await :crm_data, event: :crm_received
|
|
291
|
+
await :billing_data, event: :billing_received
|
|
340
292
|
|
|
341
|
-
|
|
293
|
+
aggregate :report, with: %i[crm_data billing_data] do |crm_data:, billing_data:|
|
|
294
|
+
{ crm: crm_data, billing: billing_data }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
output :report
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
on_success :report do |value:, **|
|
|
301
|
+
puts "Report ready: #{value.inspect}"
|
|
342
302
|
end
|
|
343
303
|
end
|
|
344
|
-
```
|
|
345
304
|
|
|
346
|
-
|
|
305
|
+
store = Igniter::Runtime::Stores::MemoryStore.new
|
|
347
306
|
|
|
348
|
-
|
|
307
|
+
# Launch — suspends waiting for both events
|
|
308
|
+
execution = LeadWorkflow.start({ request_id: "r1" }, store: store)
|
|
309
|
+
execution.pending? # => true
|
|
349
310
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
311
|
+
# Deliver events from any process or webhook handler
|
|
312
|
+
LeadWorkflow.deliver_event(:crm_received,
|
|
313
|
+
correlation: { request_id: "r1" },
|
|
314
|
+
payload: { company: "Acme Corp", tier: "enterprise" },
|
|
315
|
+
store: store)
|
|
354
316
|
|
|
355
|
-
|
|
317
|
+
LeadWorkflow.deliver_event(:billing_received,
|
|
318
|
+
correlation: { request_id: "r1" },
|
|
319
|
+
payload: { mrr: 500 },
|
|
320
|
+
store: store)
|
|
321
|
+
# => prints "Report ready: { crm: ..., billing: ... }"
|
|
322
|
+
```
|
|
356
323
|
|
|
357
|
-
|
|
324
|
+
See [`examples/distributed_server.rb`](examples/distributed_server.rb) and [`docs/DISTRIBUTED_CONTRACTS_V1.md`](docs/DISTRIBUTED_CONTRACTS_V1.md).
|
|
358
325
|
|
|
359
|
-
|
|
360
|
-
class PricingContract < Igniter::Contract
|
|
361
|
-
define do
|
|
362
|
-
input :order_total, type: :numeric
|
|
326
|
+
### 10. igniter-server
|
|
363
327
|
|
|
364
|
-
|
|
365
|
-
order_total * 1.2
|
|
366
|
-
end
|
|
328
|
+
Host contracts as an HTTP service and call them from another graph with the `remote:` DSL:
|
|
367
329
|
|
|
368
|
-
|
|
330
|
+
```ruby
|
|
331
|
+
# --- Service node on port 4568 ---
|
|
332
|
+
require "igniter/server"
|
|
333
|
+
|
|
334
|
+
class ScoringContract < Igniter::Contract
|
|
335
|
+
define do
|
|
336
|
+
input :value
|
|
337
|
+
compute :score, depends_on: :value, call: ->(value:) { value * 1.5 }
|
|
338
|
+
output :score
|
|
369
339
|
end
|
|
370
340
|
end
|
|
371
341
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
input :order_total, type: :numeric
|
|
342
|
+
Igniter::Server.configure { |c| c.port = 4568; c.register "ScoringContract", ScoringContract }
|
|
343
|
+
Igniter::Server.start
|
|
375
344
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
345
|
+
# --- Orchestrator on port 4567 ---
|
|
346
|
+
require "igniter/server"
|
|
379
347
|
|
|
380
|
-
|
|
348
|
+
class PipelineContract < Igniter::Contract
|
|
349
|
+
define do
|
|
350
|
+
input :data
|
|
351
|
+
remote :scored,
|
|
352
|
+
contract: "ScoringContract",
|
|
353
|
+
node: "http://localhost:4568",
|
|
354
|
+
inputs: { value: :data }
|
|
355
|
+
output :scored
|
|
381
356
|
end
|
|
382
357
|
end
|
|
383
358
|
|
|
384
|
-
|
|
385
|
-
|
|
359
|
+
Igniter::Server.configure { |c| c.port = 4567; c.register "PipelineContract", PipelineContract }
|
|
360
|
+
Igniter::Server.start
|
|
386
361
|
```
|
|
387
362
|
|
|
388
|
-
|
|
363
|
+
**CLI:**
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
igniter-server start --port 4568 --require ./contracts.rb
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**Rack / Puma (`config.ru`):**
|
|
389
370
|
|
|
390
371
|
```ruby
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
372
|
+
require "igniter/server"
|
|
373
|
+
require_relative "contracts"
|
|
374
|
+
Igniter::Server.configure { |c| c.register "ScoringContract", ScoringContract }
|
|
375
|
+
run Igniter::Server.rack_app
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**REST API:**
|
|
379
|
+
|
|
380
|
+
| Method | Path | Description |
|
|
381
|
+
|--------|------|-------------|
|
|
382
|
+
| `POST` | `/v1/contracts/:name/execute` | Execute a contract synchronously |
|
|
383
|
+
| `POST` | `/v1/contracts/:name/events` | Deliver an event to a distributed contract |
|
|
384
|
+
| `GET` | `/v1/executions/:id` | Poll execution status |
|
|
385
|
+
| `GET` | `/v1/health` | Health check with registered contracts list |
|
|
386
|
+
| `GET` | `/v1/contracts` | List contracts with inputs and outputs |
|
|
387
|
+
|
|
388
|
+
See [`docs/SERVER_V1.md`](docs/SERVER_V1.md) for the full API reference, deployment patterns, and security notes.
|
|
389
|
+
|
|
390
|
+
### 11. LLM Integration
|
|
391
|
+
|
|
392
|
+
Use language models as first-class compute nodes. Supported providers: Ollama (local, zero API cost),
|
|
393
|
+
Anthropic (Claude), OpenAI (and compatible APIs: Groq, Mistral, Azure OpenAI):
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
require "igniter/integrations/llm"
|
|
397
|
+
|
|
398
|
+
Igniter::LLM.configure do |c|
|
|
399
|
+
c.default_provider = :anthropic
|
|
400
|
+
c.anthropic.api_key = ENV["ANTHROPIC_API_KEY"]
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
class ClassifyExecutor < Igniter::LLM::Executor
|
|
404
|
+
provider :anthropic
|
|
405
|
+
model "claude-haiku-4-5-20251001"
|
|
406
|
+
system_prompt "Classify feedback into: bug_report, feature_request, question."
|
|
396
407
|
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
def call(feedback:)
|
|
409
|
+
complete("Classify: #{feedback}")
|
|
399
410
|
end
|
|
400
411
|
end
|
|
401
|
-
```
|
|
402
412
|
|
|
403
|
-
|
|
413
|
+
class DraftResponseExecutor < Igniter::LLM::Executor
|
|
414
|
+
provider :anthropic
|
|
415
|
+
model "claude-haiku-4-5-20251001"
|
|
416
|
+
system_prompt "You are a customer success agent. Write one professional response sentence."
|
|
404
417
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
define do
|
|
408
|
-
input :order_total, type: :numeric
|
|
409
|
-
output :order_total
|
|
418
|
+
def call(feedback:, category:)
|
|
419
|
+
complete("Feedback: #{feedback}\nCategory: #{category}\nDraft a response.")
|
|
410
420
|
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
class FeedbackTriageContract < Igniter::Contract
|
|
424
|
+
define do
|
|
425
|
+
input :feedback
|
|
411
426
|
|
|
412
|
-
|
|
413
|
-
|
|
427
|
+
compute :category, depends_on: :feedback, with: ClassifyExecutor
|
|
428
|
+
compute :response, depends_on: %i[feedback category], with: DraftResponseExecutor
|
|
429
|
+
|
|
430
|
+
output :category
|
|
431
|
+
output :response
|
|
414
432
|
end
|
|
415
433
|
end
|
|
416
434
|
```
|
|
417
435
|
|
|
418
|
-
|
|
436
|
+
Multi-step reasoning with conversation history:
|
|
419
437
|
|
|
420
438
|
```ruby
|
|
421
|
-
|
|
422
|
-
|
|
439
|
+
class MultiStepExecutor < Igniter::LLM::Executor
|
|
440
|
+
def call(text:, prior_analysis:)
|
|
441
|
+
ctx = Context.empty(system: self.class.system_prompt)
|
|
442
|
+
.append_user("Initial: #{text}")
|
|
443
|
+
.append_assistant(prior_analysis)
|
|
444
|
+
chat(context: ctx)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
```
|
|
423
448
|
|
|
424
|
-
|
|
425
|
-
contract.result.gross_total
|
|
449
|
+
See [`examples/llm/research_agent.rb`](examples/llm/research_agent.rb), [`examples/llm/tool_use.rb`](examples/llm/tool_use.rb), and [`docs/LLM_V1.md`](docs/LLM_V1.md).
|
|
426
450
|
|
|
427
|
-
|
|
428
|
-
contract.result.explain(:gross_total)
|
|
429
|
-
contract.explain_plan
|
|
430
|
-
contract.execution.to_h
|
|
431
|
-
contract.execution.as_json
|
|
432
|
-
contract.result.as_json
|
|
433
|
-
contract.events.map(&:as_json)
|
|
434
|
-
contract.diagnostics.to_h
|
|
435
|
-
contract.diagnostics_text
|
|
436
|
-
contract.diagnostics_markdown
|
|
437
|
-
contract.audit_snapshot
|
|
438
|
-
```
|
|
451
|
+
## Examples
|
|
439
452
|
|
|
440
|
-
|
|
453
|
+
| Example | Run | Shows |
|
|
454
|
+
|---------|-----|-------|
|
|
455
|
+
| `basic_pricing.rb` | `ruby examples/basic_pricing.rb` | Basic contract, lazy resolution, input updates |
|
|
456
|
+
| `composition.rb` | `ruby examples/composition.rb` | Nested contracts and composed results |
|
|
457
|
+
| `diagnostics.rb` | `ruby examples/diagnostics.rb` | Diagnostics text and machine-readable output |
|
|
458
|
+
| `async_store.rb` | `ruby examples/async_store.rb` | Pending execution, file-backed store, worker-style resume |
|
|
459
|
+
| `marketing_ergonomics.rb` | `ruby examples/marketing_ergonomics.rb` | `const`, `lookup`, `map`, `guard`, `scope`, `namespace`, `expose`, `on_success`, `explain_plan` |
|
|
460
|
+
| `collection.rb` | `ruby examples/collection.rb` | Fan-out, stable item keys, `CollectionResult` |
|
|
461
|
+
| `collection_partial_failure.rb` | `ruby examples/collection_partial_failure.rb` | `:collect` mode, partial failure summary, collection diagnostics |
|
|
462
|
+
| `ringcentral_routing.rb` | `ruby examples/ringcentral_routing.rb` | `branch`, nested `collection`, `project`, `aggregate`, diagnostics |
|
|
463
|
+
| `order_pipeline.rb` | `ruby examples/order_pipeline.rb` | `guard` + `collection` + `branch` + `export` in one flow |
|
|
464
|
+
| `distributed_server.rb` | `ruby examples/distributed_server.rb` | `await`, `correlate_by`, `start`, `deliver_event`, `on_success` |
|
|
465
|
+
| `server/node1.rb` + `node2.rb` | run both, then curl | Two-node igniter-server with `remote:` DSL |
|
|
466
|
+
| `llm/research_agent.rb` | `ruby examples/llm/research_agent.rb` | Multi-step LLM pipeline with Ollama |
|
|
467
|
+
| `llm/tool_use.rb` | `ruby examples/llm/tool_use.rb` | LLM tool declarations, chained LLM nodes, `Context` |
|
|
468
|
+
|
|
469
|
+
## Design Docs
|
|
441
470
|
|
|
442
471
|
- [Architecture v2](docs/ARCHITECTURE_V2.md)
|
|
443
472
|
- [Execution Model v2](docs/EXECUTION_MODEL_V2.md)
|
|
@@ -445,42 +474,33 @@ contract.audit_snapshot
|
|
|
445
474
|
- [Patterns](docs/PATTERNS.md)
|
|
446
475
|
- [Branches v1](docs/BRANCHES_V1.md)
|
|
447
476
|
- [Collections v1](docs/COLLECTIONS_V1.md)
|
|
477
|
+
- [Distributed Contracts v1](docs/DISTRIBUTED_CONTRACTS_V1.md)
|
|
448
478
|
- [Store Adapters](docs/STORE_ADAPTERS.md)
|
|
479
|
+
- [igniter-server v1](docs/SERVER_V1.md)
|
|
480
|
+
- [LLM Integration v1](docs/LLM_V1.md)
|
|
449
481
|
- [Concepts and Principles](docs/IGNITER_CONCEPTS.md)
|
|
450
482
|
|
|
451
|
-
## Direction
|
|
452
|
-
|
|
453
|
-
The v2 rewrite is based on these rules:
|
|
454
|
-
|
|
455
|
-
- model, compiler, runtime, DSL, and extensions are separate layers
|
|
456
|
-
- graph validation happens before runtime
|
|
457
|
-
- auditing and reactive behavior are extensions over events, not runtime internals
|
|
458
|
-
- the first target is a deterministic synchronous kernel
|
|
459
|
-
|
|
460
|
-
## Status
|
|
461
|
-
|
|
462
|
-
The public Ruby surface in `lib/` now contains only the v2 core exposed from `require "igniter"`.
|
|
463
|
-
|
|
464
483
|
## Development
|
|
465
484
|
|
|
466
485
|
```bash
|
|
467
|
-
rake
|
|
486
|
+
rake # specs + RuboCop
|
|
487
|
+
rake spec # tests only
|
|
488
|
+
rake rubocop # lint only
|
|
489
|
+
rake build # build gem
|
|
468
490
|
```
|
|
469
491
|
|
|
470
|
-
Current baseline:
|
|
471
|
-
|
|
472
|
-
- synchronous runtime
|
|
473
|
-
-
|
|
474
|
-
-
|
|
475
|
-
-
|
|
476
|
-
-
|
|
477
|
-
-
|
|
478
|
-
-
|
|
479
|
-
-
|
|
480
|
-
-
|
|
481
|
-
- diagnostics
|
|
482
|
-
- reactive subscriptions
|
|
483
|
-
- graph/runtime introspection
|
|
492
|
+
Current feature baseline:
|
|
493
|
+
|
|
494
|
+
- synchronous runtime + parallel thread-pool runner
|
|
495
|
+
- pending / deferred node states with snapshot / restore
|
|
496
|
+
- store-backed resume flow (MemoryStore, FileStore)
|
|
497
|
+
- compile-time graph validation, typed inputs, cycle detection
|
|
498
|
+
- composition, branch, collection, guard, scope / namespace
|
|
499
|
+
- distributed workflows: `await`, `correlate_by`, `start`, `deliver_event`
|
|
500
|
+
- igniter-server: TCP server, Rack adapter, CLI, `remote:` DSL
|
|
501
|
+
- LLM compute nodes: Ollama, Anthropic, OpenAI providers
|
|
502
|
+
- Rails integration: Railtie, ActiveJob, ActionCable, webhook controller mixin
|
|
503
|
+
- auditing, diagnostics, reactive subscriptions, graph introspection
|
|
484
504
|
|
|
485
505
|
## License
|
|
486
506
|
|