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.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +238 -218
  4. data/docs/DISTRIBUTED_CONTRACTS_V1.md +493 -0
  5. data/docs/LLM_V1.md +335 -0
  6. data/docs/PATTERNS.md +189 -0
  7. data/docs/SERVER_V1.md +313 -0
  8. data/examples/README.md +129 -0
  9. data/examples/agents.rb +150 -0
  10. data/examples/differential.rb +161 -0
  11. data/examples/distributed_server.rb +94 -0
  12. data/examples/distributed_workflow.rb +52 -0
  13. data/examples/effects.rb +184 -0
  14. data/examples/invariants.rb +179 -0
  15. data/examples/order_pipeline.rb +163 -0
  16. data/examples/provenance.rb +122 -0
  17. data/examples/saga.rb +110 -0
  18. data/lib/igniter/agent/mailbox.rb +96 -0
  19. data/lib/igniter/agent/message.rb +21 -0
  20. data/lib/igniter/agent/ref.rb +86 -0
  21. data/lib/igniter/agent/runner.rb +129 -0
  22. data/lib/igniter/agent/state_holder.rb +23 -0
  23. data/lib/igniter/agent.rb +155 -0
  24. data/lib/igniter/compiler/compiled_graph.rb +12 -0
  25. data/lib/igniter/compiler/validation_pipeline.rb +3 -1
  26. data/lib/igniter/compiler/validators/await_validator.rb +53 -0
  27. data/lib/igniter/compiler/validators/callable_validator.rb +21 -3
  28. data/lib/igniter/compiler/validators/dependencies_validator.rb +41 -1
  29. data/lib/igniter/compiler/validators/remote_validator.rb +58 -0
  30. data/lib/igniter/compiler.rb +2 -0
  31. data/lib/igniter/contract.rb +59 -8
  32. data/lib/igniter/differential/divergence.rb +29 -0
  33. data/lib/igniter/differential/formatter.rb +96 -0
  34. data/lib/igniter/differential/report.rb +86 -0
  35. data/lib/igniter/differential/runner.rb +130 -0
  36. data/lib/igniter/differential.rb +51 -0
  37. data/lib/igniter/dsl/contract_builder.rb +74 -4
  38. data/lib/igniter/effect.rb +91 -0
  39. data/lib/igniter/effect_registry.rb +78 -0
  40. data/lib/igniter/errors.rb +17 -2
  41. data/lib/igniter/execution_report/builder.rb +54 -0
  42. data/lib/igniter/execution_report/formatter.rb +50 -0
  43. data/lib/igniter/execution_report/node_entry.rb +24 -0
  44. data/lib/igniter/execution_report/report.rb +65 -0
  45. data/lib/igniter/execution_report.rb +32 -0
  46. data/lib/igniter/extensions/differential.rb +114 -0
  47. data/lib/igniter/extensions/execution_report.rb +27 -0
  48. data/lib/igniter/extensions/invariants.rb +116 -0
  49. data/lib/igniter/extensions/provenance.rb +45 -0
  50. data/lib/igniter/extensions/saga.rb +74 -0
  51. data/lib/igniter/integrations/agents.rb +18 -0
  52. data/lib/igniter/integrations/llm/config.rb +69 -0
  53. data/lib/igniter/integrations/llm/context.rb +74 -0
  54. data/lib/igniter/integrations/llm/executor.rb +159 -0
  55. data/lib/igniter/integrations/llm/providers/anthropic.rb +148 -0
  56. data/lib/igniter/integrations/llm/providers/base.rb +33 -0
  57. data/lib/igniter/integrations/llm/providers/ollama.rb +137 -0
  58. data/lib/igniter/integrations/llm/providers/openai.rb +153 -0
  59. data/lib/igniter/integrations/llm.rb +59 -0
  60. data/lib/igniter/integrations/rails/cable_adapter.rb +49 -0
  61. data/lib/igniter/integrations/rails/contract_job.rb +76 -0
  62. data/lib/igniter/integrations/rails/generators/contract/contract_generator.rb +22 -0
  63. data/lib/igniter/integrations/rails/generators/install/install_generator.rb +33 -0
  64. data/lib/igniter/integrations/rails/railtie.rb +25 -0
  65. data/lib/igniter/integrations/rails/webhook_concern.rb +49 -0
  66. data/lib/igniter/integrations/rails.rb +12 -0
  67. data/lib/igniter/invariant.rb +50 -0
  68. data/lib/igniter/model/await_node.rb +21 -0
  69. data/lib/igniter/model/effect_node.rb +37 -0
  70. data/lib/igniter/model/remote_node.rb +26 -0
  71. data/lib/igniter/model.rb +3 -0
  72. data/lib/igniter/property_testing/formatter.rb +66 -0
  73. data/lib/igniter/property_testing/generators.rb +115 -0
  74. data/lib/igniter/property_testing/result.rb +45 -0
  75. data/lib/igniter/property_testing/run.rb +43 -0
  76. data/lib/igniter/property_testing/runner.rb +47 -0
  77. data/lib/igniter/property_testing.rb +64 -0
  78. data/lib/igniter/provenance/builder.rb +97 -0
  79. data/lib/igniter/provenance/lineage.rb +82 -0
  80. data/lib/igniter/provenance/node_trace.rb +65 -0
  81. data/lib/igniter/provenance/text_formatter.rb +70 -0
  82. data/lib/igniter/provenance.rb +29 -0
  83. data/lib/igniter/registry.rb +67 -0
  84. data/lib/igniter/runtime/execution.rb +2 -2
  85. data/lib/igniter/runtime/input_validator.rb +5 -3
  86. data/lib/igniter/runtime/resolver.rb +58 -1
  87. data/lib/igniter/runtime/stores/active_record_store.rb +13 -1
  88. data/lib/igniter/runtime/stores/file_store.rb +50 -2
  89. data/lib/igniter/runtime/stores/memory_store.rb +55 -2
  90. data/lib/igniter/runtime/stores/redis_store.rb +13 -1
  91. data/lib/igniter/saga/compensation.rb +31 -0
  92. data/lib/igniter/saga/compensation_record.rb +20 -0
  93. data/lib/igniter/saga/executor.rb +85 -0
  94. data/lib/igniter/saga/formatter.rb +49 -0
  95. data/lib/igniter/saga/result.rb +47 -0
  96. data/lib/igniter/saga.rb +56 -0
  97. data/lib/igniter/server/client.rb +123 -0
  98. data/lib/igniter/server/config.rb +27 -0
  99. data/lib/igniter/server/handlers/base.rb +105 -0
  100. data/lib/igniter/server/handlers/contracts_handler.rb +15 -0
  101. data/lib/igniter/server/handlers/event_handler.rb +28 -0
  102. data/lib/igniter/server/handlers/execute_handler.rb +37 -0
  103. data/lib/igniter/server/handlers/health_handler.rb +32 -0
  104. data/lib/igniter/server/handlers/status_handler.rb +27 -0
  105. data/lib/igniter/server/http_server.rb +109 -0
  106. data/lib/igniter/server/rack_app.rb +35 -0
  107. data/lib/igniter/server/registry.rb +56 -0
  108. data/lib/igniter/server/router.rb +75 -0
  109. data/lib/igniter/server.rb +67 -0
  110. data/lib/igniter/stream_loop.rb +80 -0
  111. data/lib/igniter/supervisor.rb +167 -0
  112. data/lib/igniter/version.rb +1 -1
  113. data/lib/igniter.rb +14 -0
  114. 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
- - selective invalidation after input updates
7
- - typed input validation
8
- - nested contract composition
9
- - runtime auditing
10
- - diagnostics reports
11
- - reactive side effects
12
- - ergonomic DSL helpers (`with`, `const`, `lookup`, `map`, `project`, `aggregate`, `guard`, `export`, `expose`, `effect`, `on_success`, `scope`, `namespace`, `branch`, `collection`)
13
- - graph and runtime introspection
14
- - async-capable pending nodes with snapshot/restore
15
- - store-backed execution resume flows
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, type: :string
34
- input :vat_rate, type: :numeric, default: 0.2
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: declare inputs, compute nodes, outputs, and compositions.
64
- - Compiler: validate dependency graphs before runtime.
65
- - Runtime: cache resolved nodes and invalidate only affected downstream nodes.
66
- - Typed inputs: validate types, defaults, and required fields.
67
- - Composition: execute nested contracts with isolated child executions.
68
- - Auditing: collect execution timelines and snapshots.
69
- - Diagnostics: build compact text, markdown, or structured reports for triage.
70
- - Reactive: subscribe declaratively to runtime events.
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.
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
- The repository contains runnable examples in [`examples/`](examples).
77
- They also have matching specs, so they stay in sync with the implementation.
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, type: :string
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, type: :string
108
+ input :country, type: :string
126
109
 
127
110
  compose :pricing, contract: PriceContract, inputs: {
128
111
  order_total: :order_total,
129
- country: :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 And Introspection
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 = PriceContract.new(order_total: 100, country: "UA")
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 And Resume
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 = AsyncPricingContract.new(order_total: 100)
196
- deferred = contract.result.gross_total
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, type: :string
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 air_conditioning].include?(service.downcase) ? "HVAC" : service
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 zip_code] do |vendor_id:, trade:, zip_supported:, zip_code:|
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
- You can also use matcher-style guards directly:
221
+ Matcher-style guard shortcuts:
255
222
 
256
223
  ```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"
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: :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 contract: DefaultDeliveryContract
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. Branch + Collection Routing
251
+ ### 8. Declarative Collections
285
252
 
286
253
  ```ruby
287
- class RingcentralWebhookContract < Igniter::Contract
254
+ class TechnicianBatchContract < Igniter::Contract
288
255
  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
256
+ input :technician_inputs, type: :array
304
257
 
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
258
+ collection :technicians,
259
+ with: :technician_inputs,
260
+ each: TechnicianContract,
261
+ key: :technician_id,
262
+ mode: :collect
314
263
 
315
- export :routing_summary, from: :status_route
264
+ output :technicians
316
265
  end
317
266
  end
318
267
  ```
319
268
 
320
- In nested flows, diagnostics stay attached to the execution that actually owns the node:
269
+ In `mode: :collect`, an execution succeeds overall while items may individually fail:
321
270
 
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
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
- `branch` is a graph primitive for explicit routing. It selects one child contract from ordered cases and resolves only the chosen branch.
276
+ See [`examples/collection_partial_failure.rb`](examples/collection_partial_failure.rb).
327
277
 
328
- ### 8. Declarative Collections
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 TechnicianBatchContract < Igniter::Contract
284
+ class LeadWorkflow < Igniter::Contract
285
+ correlate_by :request_id
286
+
332
287
  define do
333
- input :technician_inputs, type: :array
288
+ input :request_id
334
289
 
335
- collection :technicians,
336
- with: :technician_inputs,
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
- output :technicians
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
- `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.
305
+ store = Igniter::Runtime::Stores::MemoryStore.new
347
306
 
348
- In `mode: :collect`, a collection can succeed overall while still containing failed items. In that case:
307
+ # Launch suspends waiting for both events
308
+ execution = LeadWorkflow.start({ request_id: "r1" }, store: store)
309
+ execution.pending? # => true
349
310
 
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
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
- See `examples/collection_partial_failure.rb` for a runnable example.
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
- ## Composition Example
324
+ See [`examples/distributed_server.rb`](examples/distributed_server.rb) and [`docs/DISTRIBUTED_CONTRACTS_V1.md`](docs/DISTRIBUTED_CONTRACTS_V1.md).
358
325
 
359
- ```ruby
360
- class PricingContract < Igniter::Contract
361
- define do
362
- input :order_total, type: :numeric
326
+ ### 10. igniter-server
363
327
 
364
- compute :gross_total, depends_on: [:order_total] do |order_total:|
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
- output :gross_total
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
- class CheckoutContract < Igniter::Contract
373
- define do
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
- compose :pricing, contract: PricingContract, inputs: {
377
- order_total: :order_total
378
- }
345
+ # --- Orchestrator on port 4567 ---
346
+ require "igniter/server"
379
347
 
380
- output :pricing
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
- CheckoutContract.new(order_total: 100).result.pricing.gross_total
385
- # => 120.0
359
+ Igniter::Server.configure { |c| c.port = 4567; c.register "PipelineContract", PipelineContract }
360
+ Igniter::Server.start
386
361
  ```
387
362
 
388
- ## Reactive Example
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
- class NotifyingContract < Igniter::Contract
392
- define do
393
- input :order_total, type: :numeric
394
- output :order_total
395
- end
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
- on_success :order_total do |value:, **|
398
- puts "Resolved #{value}"
408
+ def call(feedback:)
409
+ complete("Classify: #{feedback}")
399
410
  end
400
411
  end
401
- ```
402
412
 
403
- Or attach directly to a node event when you want the node value:
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
- ```ruby
406
- class NotifyingContract < Igniter::Contract
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
- effect "order_total" do |event:, value:, **|
413
- puts "Resolved #{event.path}"
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
- ## Introspection
436
+ Multi-step reasoning with conversation history:
419
437
 
420
438
  ```ruby
421
- PriceContract.graph.to_text
422
- PriceContract.graph.to_mermaid
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
- contract = PriceContract.new(order_total: 100, country: "UA")
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
- contract.result.states
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
- ## v2 Design Docs
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 spec
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
- - parallel thread-pool runner
474
- - pending/deferred runtime states
475
- - snapshot/restore execution lifecycle
476
- - store-backed resume flow
477
- - compile-time graph validation
478
- - typed inputs
479
- - composition
480
- - auditing
481
- - diagnostics reporting
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