igniter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE.txt +21 -0
- data/README.md +264 -0
- data/docs/API_V2.md +242 -0
- data/docs/ARCHITECTURE_V2.md +317 -0
- data/docs/EXECUTION_MODEL_V2.md +245 -0
- data/docs/IGNITER_CONCEPTS.md +81 -0
- data/examples/README.md +77 -0
- data/examples/basic_pricing.rb +27 -0
- data/examples/composition.rb +39 -0
- data/examples/diagnostics.rb +28 -0
- data/lib/igniter/compiler/compiled_graph.rb +78 -0
- data/lib/igniter/compiler/graph_compiler.rb +60 -0
- data/lib/igniter/compiler/validator.rb +205 -0
- data/lib/igniter/compiler.rb +10 -0
- data/lib/igniter/contract.rb +117 -0
- data/lib/igniter/diagnostics/report.rb +174 -0
- data/lib/igniter/diagnostics.rb +8 -0
- data/lib/igniter/dsl/contract_builder.rb +95 -0
- data/lib/igniter/dsl.rb +8 -0
- data/lib/igniter/errors.rb +53 -0
- data/lib/igniter/events/bus.rb +39 -0
- data/lib/igniter/events/event.rb +53 -0
- data/lib/igniter/events.rb +9 -0
- data/lib/igniter/extensions/auditing/timeline.rb +99 -0
- data/lib/igniter/extensions/auditing.rb +10 -0
- data/lib/igniter/extensions/introspection/graph_formatter.rb +73 -0
- data/lib/igniter/extensions/introspection/runtime_formatter.rb +102 -0
- data/lib/igniter/extensions/introspection.rb +11 -0
- data/lib/igniter/extensions/reactive/engine.rb +36 -0
- data/lib/igniter/extensions/reactive/matcher.rb +21 -0
- data/lib/igniter/extensions/reactive/reaction.rb +17 -0
- data/lib/igniter/extensions/reactive.rb +12 -0
- data/lib/igniter/extensions.rb +10 -0
- data/lib/igniter/model/composition_node.rb +22 -0
- data/lib/igniter/model/compute_node.rb +21 -0
- data/lib/igniter/model/graph.rb +15 -0
- data/lib/igniter/model/input_node.rb +27 -0
- data/lib/igniter/model/node.rb +22 -0
- data/lib/igniter/model/output_node.rb +21 -0
- data/lib/igniter/model.rb +13 -0
- data/lib/igniter/runtime/cache.rb +58 -0
- data/lib/igniter/runtime/execution.rb +142 -0
- data/lib/igniter/runtime/input_validator.rb +145 -0
- data/lib/igniter/runtime/invalidator.rb +52 -0
- data/lib/igniter/runtime/node_state.rb +31 -0
- data/lib/igniter/runtime/resolver.rb +114 -0
- data/lib/igniter/runtime/result.rb +105 -0
- data/lib/igniter/runtime.rb +14 -0
- data/lib/igniter/version.rb +5 -0
- data/lib/igniter.rb +20 -0
- data/sig/igniter.rbs +4 -0
- metadata +126 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Igniter v2 Architecture
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Igniter v2 is a Ruby library for describing business logic as a validated dependency graph and executing that graph with:
|
|
6
|
+
|
|
7
|
+
- deterministic resolution
|
|
8
|
+
- lazy evaluation
|
|
9
|
+
- selective invalidation
|
|
10
|
+
- transparent events
|
|
11
|
+
- optional extensions built on top of the event stream
|
|
12
|
+
|
|
13
|
+
The core design principle is strict separation between:
|
|
14
|
+
|
|
15
|
+
- model time: describing the graph
|
|
16
|
+
- compile time: validating and freezing the graph
|
|
17
|
+
- runtime: resolving the graph against inputs
|
|
18
|
+
|
|
19
|
+
## Design Principles
|
|
20
|
+
|
|
21
|
+
1. Small, hard core.
|
|
22
|
+
The kernel should be minimal, strict, and easy to test.
|
|
23
|
+
|
|
24
|
+
2. Compile first, execute second.
|
|
25
|
+
No runtime should deal with half-built DSL objects.
|
|
26
|
+
|
|
27
|
+
3. Explicit data flow.
|
|
28
|
+
Dependencies, output exposure, and composition mappings are always declared.
|
|
29
|
+
|
|
30
|
+
4. Extensions over hooks.
|
|
31
|
+
Auditing, reactions, tracing, and introspection consume runtime events instead of being deeply embedded in execution.
|
|
32
|
+
|
|
33
|
+
5. Stable identities.
|
|
34
|
+
Nodes should have stable `id`, `path`, and `kind`. Runtime logic must not depend on Ruby object identity alone.
|
|
35
|
+
|
|
36
|
+
## Layered Architecture
|
|
37
|
+
|
|
38
|
+
### 1. `Igniter::Model`
|
|
39
|
+
|
|
40
|
+
Pure compile-time domain objects. No lazy execution, no caching, no observers.
|
|
41
|
+
|
|
42
|
+
Primary objects:
|
|
43
|
+
|
|
44
|
+
- `Igniter::Model::Graph`
|
|
45
|
+
- `Igniter::Model::Node`
|
|
46
|
+
- `Igniter::Model::InputNode`
|
|
47
|
+
- `Igniter::Model::ComputeNode`
|
|
48
|
+
- `Igniter::Model::OutputNode`
|
|
49
|
+
- `Igniter::Model::CompositionNode`
|
|
50
|
+
- `Igniter::Model::Dependency`
|
|
51
|
+
|
|
52
|
+
Responsibilities:
|
|
53
|
+
|
|
54
|
+
- represent graph topology
|
|
55
|
+
- store node metadata
|
|
56
|
+
- store dependency declarations
|
|
57
|
+
- store source location metadata for diagnostics
|
|
58
|
+
- expose graph traversal primitives
|
|
59
|
+
|
|
60
|
+
Constraints:
|
|
61
|
+
|
|
62
|
+
- immutable after compilation
|
|
63
|
+
- no implicit mutation during runtime
|
|
64
|
+
|
|
65
|
+
### 2. `Igniter::Compiler`
|
|
66
|
+
|
|
67
|
+
Transforms draft model definitions into a validated `CompiledGraph`.
|
|
68
|
+
|
|
69
|
+
Primary objects:
|
|
70
|
+
|
|
71
|
+
- `Igniter::Compiler::GraphCompiler`
|
|
72
|
+
- `Igniter::Compiler::CompiledGraph`
|
|
73
|
+
- `Igniter::Compiler::Validator`
|
|
74
|
+
- `Igniter::Compiler::ResolutionPlan`
|
|
75
|
+
|
|
76
|
+
Responsibilities:
|
|
77
|
+
|
|
78
|
+
- validate node uniqueness
|
|
79
|
+
- validate paths and namespaces
|
|
80
|
+
- validate dependency references
|
|
81
|
+
- detect cycles
|
|
82
|
+
- validate composition mappings
|
|
83
|
+
- compute topological order
|
|
84
|
+
- freeze the result
|
|
85
|
+
|
|
86
|
+
Compiler output:
|
|
87
|
+
|
|
88
|
+
- stable node registry by id and path
|
|
89
|
+
- dependency index
|
|
90
|
+
- reverse dependency index
|
|
91
|
+
- topological resolution plan
|
|
92
|
+
- output registry
|
|
93
|
+
|
|
94
|
+
### 3. `Igniter::Runtime`
|
|
95
|
+
|
|
96
|
+
Executes a compiled graph for one input set.
|
|
97
|
+
|
|
98
|
+
Primary objects:
|
|
99
|
+
|
|
100
|
+
- `Igniter::Runtime::Execution`
|
|
101
|
+
- `Igniter::Runtime::Resolver`
|
|
102
|
+
- `Igniter::Runtime::Cache`
|
|
103
|
+
- `Igniter::Runtime::Invalidator`
|
|
104
|
+
- `Igniter::Runtime::NodeState`
|
|
105
|
+
- `Igniter::Runtime::Result`
|
|
106
|
+
- `Igniter::Runtime::ExecutorRegistry`
|
|
107
|
+
|
|
108
|
+
Responsibilities:
|
|
109
|
+
|
|
110
|
+
- hold input values
|
|
111
|
+
- resolve requested outputs or nodes
|
|
112
|
+
- cache node states
|
|
113
|
+
- invalidate downstream nodes on input changes
|
|
114
|
+
- emit lifecycle events
|
|
115
|
+
- expose execution result
|
|
116
|
+
|
|
117
|
+
Non-responsibilities:
|
|
118
|
+
|
|
119
|
+
- graph validation
|
|
120
|
+
- DSL parsing
|
|
121
|
+
- auditing persistence
|
|
122
|
+
- reactive policy decisions
|
|
123
|
+
|
|
124
|
+
### 4. `Igniter::DSL`
|
|
125
|
+
|
|
126
|
+
Thin syntax layer that produces a graph draft or a builder input for the compiler.
|
|
127
|
+
|
|
128
|
+
Primary objects:
|
|
129
|
+
|
|
130
|
+
- `Igniter::DSL::Contract`
|
|
131
|
+
- `Igniter::DSL::Builder`
|
|
132
|
+
- `Igniter::DSL::Reference`
|
|
133
|
+
|
|
134
|
+
Responsibilities:
|
|
135
|
+
|
|
136
|
+
- provide ergonomic declaration syntax
|
|
137
|
+
- map user declarations to model/compiler input
|
|
138
|
+
- attach source-location metadata for errors
|
|
139
|
+
|
|
140
|
+
Rules:
|
|
141
|
+
|
|
142
|
+
- DSL must not contain execution logic
|
|
143
|
+
- DSL must not decide invalidation or cache behavior
|
|
144
|
+
- DSL should prefer explicit references over `method_missing`
|
|
145
|
+
|
|
146
|
+
### 5. `Igniter::Events`
|
|
147
|
+
|
|
148
|
+
Canonical runtime event schema.
|
|
149
|
+
|
|
150
|
+
Primary objects:
|
|
151
|
+
|
|
152
|
+
- `Igniter::Events::Event`
|
|
153
|
+
- `Igniter::Events::Bus`
|
|
154
|
+
- `Igniter::Events::Subscriber`
|
|
155
|
+
|
|
156
|
+
Responsibilities:
|
|
157
|
+
|
|
158
|
+
- publish structured execution events
|
|
159
|
+
- provide extension point for diagnostics and reactive features
|
|
160
|
+
|
|
161
|
+
### 6. `Igniter::Extensions`
|
|
162
|
+
|
|
163
|
+
Optional packages built on top of the event stream and compiled/runtime APIs.
|
|
164
|
+
|
|
165
|
+
Initial extension namespaces:
|
|
166
|
+
|
|
167
|
+
- `Igniter::Extensions::Auditing`
|
|
168
|
+
- `Igniter::Extensions::Reactive`
|
|
169
|
+
- `Igniter::Extensions::Introspection`
|
|
170
|
+
|
|
171
|
+
## Runtime Boundaries
|
|
172
|
+
|
|
173
|
+
The runtime is split by responsibility:
|
|
174
|
+
|
|
175
|
+
- `Execution`: public session object
|
|
176
|
+
- `Resolver`: resolves one node using dependencies
|
|
177
|
+
- `Cache`: stores `NodeState` by node id
|
|
178
|
+
- `Invalidator`: marks downstream nodes stale
|
|
179
|
+
- `Result`: output facade for callers
|
|
180
|
+
- `Bus`: emits execution events
|
|
181
|
+
|
|
182
|
+
This is deliberate. The old shape concentrated orchestration, state mutation, notifications, and invalidation in one class. In v2, each concern gets a dedicated object.
|
|
183
|
+
|
|
184
|
+
## Node Model
|
|
185
|
+
|
|
186
|
+
Every compiled node should have:
|
|
187
|
+
|
|
188
|
+
- `id`
|
|
189
|
+
- `kind`
|
|
190
|
+
- `name`
|
|
191
|
+
- `path`
|
|
192
|
+
- `dependencies`
|
|
193
|
+
- `metadata`
|
|
194
|
+
|
|
195
|
+
Candidate node kinds for v2:
|
|
196
|
+
|
|
197
|
+
- `:input`
|
|
198
|
+
- `:compute`
|
|
199
|
+
- `:output`
|
|
200
|
+
- `:composition`
|
|
201
|
+
|
|
202
|
+
Optional later kinds:
|
|
203
|
+
|
|
204
|
+
- `:constant`
|
|
205
|
+
- `:projection`
|
|
206
|
+
- `:group`
|
|
207
|
+
|
|
208
|
+
### Why reduce node kinds
|
|
209
|
+
|
|
210
|
+
The kernel should start with the smallest set that explains the execution model clearly. Extra node kinds should be added only when they materially simplify the model rather than encode DSL convenience.
|
|
211
|
+
|
|
212
|
+
## Composition Strategy
|
|
213
|
+
|
|
214
|
+
Composition is a first-class node kind.
|
|
215
|
+
|
|
216
|
+
A composition node:
|
|
217
|
+
|
|
218
|
+
- references another compiled contract
|
|
219
|
+
- defines an input mapping from parent execution to child execution
|
|
220
|
+
- returns either a child `Result` or a collection of child `Result` objects
|
|
221
|
+
|
|
222
|
+
Composition rules:
|
|
223
|
+
|
|
224
|
+
- parent and child graphs are independently compiled
|
|
225
|
+
- child execution has its own cache and event stream
|
|
226
|
+
- parent events may carry child execution correlation metadata
|
|
227
|
+
|
|
228
|
+
## Extension Strategy
|
|
229
|
+
|
|
230
|
+
Extensions must subscribe to events and read runtime state through stable APIs.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
|
|
234
|
+
- auditing stores a timeline of events and snapshots
|
|
235
|
+
- reactive runs side effects in response to selected events
|
|
236
|
+
- introspection formats compiled graphs and runtime state
|
|
237
|
+
|
|
238
|
+
The kernel should not know persistence formats, storage adapters, or replay UIs.
|
|
239
|
+
|
|
240
|
+
## Error Model
|
|
241
|
+
|
|
242
|
+
Errors should be typed and predictable.
|
|
243
|
+
|
|
244
|
+
Primary families:
|
|
245
|
+
|
|
246
|
+
- `Igniter::CompileError`
|
|
247
|
+
- `Igniter::ValidationError`
|
|
248
|
+
- `Igniter::CycleError`
|
|
249
|
+
- `Igniter::InputError`
|
|
250
|
+
- `Igniter::ResolutionError`
|
|
251
|
+
- `Igniter::CompositionError`
|
|
252
|
+
|
|
253
|
+
Compile errors should include source metadata when available:
|
|
254
|
+
|
|
255
|
+
- contract class
|
|
256
|
+
- node path
|
|
257
|
+
- line number
|
|
258
|
+
- declaration snippet or declaration type
|
|
259
|
+
|
|
260
|
+
## Packaging Rules
|
|
261
|
+
|
|
262
|
+
The public surface should be intentionally small:
|
|
263
|
+
|
|
264
|
+
- `require "igniter"`
|
|
265
|
+
- `Igniter::Contract`
|
|
266
|
+
- `Igniter.compile`
|
|
267
|
+
- `Igniter.execute`
|
|
268
|
+
|
|
269
|
+
Autoloading must be optional convenience, not a hard dependency for correctness.
|
|
270
|
+
|
|
271
|
+
The gem must be valid and packageable without:
|
|
272
|
+
|
|
273
|
+
- a `.git` directory
|
|
274
|
+
- Rails
|
|
275
|
+
- optional extensions
|
|
276
|
+
|
|
277
|
+
## Initial Directory Shape
|
|
278
|
+
|
|
279
|
+
```text
|
|
280
|
+
lib/
|
|
281
|
+
igniter.rb
|
|
282
|
+
igniter/
|
|
283
|
+
version.rb
|
|
284
|
+
errors.rb
|
|
285
|
+
contract.rb
|
|
286
|
+
model/
|
|
287
|
+
compiler/
|
|
288
|
+
runtime/
|
|
289
|
+
events/
|
|
290
|
+
dsl/
|
|
291
|
+
extensions/
|
|
292
|
+
auditing/
|
|
293
|
+
reactive/
|
|
294
|
+
introspection/
|
|
295
|
+
spec/
|
|
296
|
+
compiler/
|
|
297
|
+
runtime/
|
|
298
|
+
integration/
|
|
299
|
+
docs/
|
|
300
|
+
ARCHITECTURE_V2.md
|
|
301
|
+
EXECUTION_MODEL_V2.md
|
|
302
|
+
API_V2.md
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Non-Goals for the First Rewrite
|
|
306
|
+
|
|
307
|
+
These should not block the first working kernel:
|
|
308
|
+
|
|
309
|
+
- Rails integration
|
|
310
|
+
- persistence adapters
|
|
311
|
+
- replay UI
|
|
312
|
+
- async execution
|
|
313
|
+
- distributed execution
|
|
314
|
+
- type inference
|
|
315
|
+
- speculative optimization
|
|
316
|
+
|
|
317
|
+
The first target is a strict, reliable, inspectable synchronous engine.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# Igniter v2 Execution Model
|
|
2
|
+
|
|
3
|
+
## Execution Lifecycle
|
|
4
|
+
|
|
5
|
+
Each contract instance owns one runtime execution session.
|
|
6
|
+
|
|
7
|
+
Lifecycle:
|
|
8
|
+
|
|
9
|
+
1. caller provides inputs
|
|
10
|
+
2. contract builds or reuses a compiled graph
|
|
11
|
+
3. execution is created with input state, cache, and event bus
|
|
12
|
+
4. caller requests one output or all outputs
|
|
13
|
+
5. runtime resolves only required nodes
|
|
14
|
+
6. cache stores node states
|
|
15
|
+
7. input updates invalidate downstream nodes
|
|
16
|
+
8. subsequent resolution reuses valid states and recomputes stale states only
|
|
17
|
+
|
|
18
|
+
## Core Runtime Objects
|
|
19
|
+
|
|
20
|
+
### `Execution`
|
|
21
|
+
|
|
22
|
+
Public runtime session.
|
|
23
|
+
|
|
24
|
+
Responsibilities:
|
|
25
|
+
|
|
26
|
+
- own compiled graph
|
|
27
|
+
- own input store
|
|
28
|
+
- own cache
|
|
29
|
+
- own event bus
|
|
30
|
+
- expose `resolve`, `resolve_all`, `update_inputs`, `result`
|
|
31
|
+
|
|
32
|
+
### `Resolver`
|
|
33
|
+
|
|
34
|
+
Single responsibility: resolve a node into a `NodeState`.
|
|
35
|
+
|
|
36
|
+
Responsibilities:
|
|
37
|
+
|
|
38
|
+
- resolve dependencies first
|
|
39
|
+
- execute the node's callable or adapter
|
|
40
|
+
- wrap success/failure into `NodeState`
|
|
41
|
+
- emit start/success/failure events
|
|
42
|
+
|
|
43
|
+
### `NodeState`
|
|
44
|
+
|
|
45
|
+
Represents runtime state for one compiled node.
|
|
46
|
+
|
|
47
|
+
Fields:
|
|
48
|
+
|
|
49
|
+
- `node_id`
|
|
50
|
+
- `path`
|
|
51
|
+
- `status`
|
|
52
|
+
- `value`
|
|
53
|
+
- `error`
|
|
54
|
+
- `version`
|
|
55
|
+
- `resolved_at`
|
|
56
|
+
- `stale`
|
|
57
|
+
|
|
58
|
+
Statuses:
|
|
59
|
+
|
|
60
|
+
- `:pending`
|
|
61
|
+
- `:running`
|
|
62
|
+
- `:succeeded`
|
|
63
|
+
- `:failed`
|
|
64
|
+
- `:stale`
|
|
65
|
+
|
|
66
|
+
### `Cache`
|
|
67
|
+
|
|
68
|
+
Stores `NodeState` by node id.
|
|
69
|
+
|
|
70
|
+
Responsibilities:
|
|
71
|
+
|
|
72
|
+
- fetch current state
|
|
73
|
+
- write new state
|
|
74
|
+
- mark state stale
|
|
75
|
+
- answer freshness queries
|
|
76
|
+
|
|
77
|
+
### `Invalidator`
|
|
78
|
+
|
|
79
|
+
Knows downstream dependency edges and invalidates affected nodes after input changes.
|
|
80
|
+
|
|
81
|
+
Responsibilities:
|
|
82
|
+
|
|
83
|
+
- walk reverse dependency graph
|
|
84
|
+
- mark stale states
|
|
85
|
+
- emit invalidation events
|
|
86
|
+
|
|
87
|
+
## Resolution Rules
|
|
88
|
+
|
|
89
|
+
### Lazy by default
|
|
90
|
+
|
|
91
|
+
`result.total` should resolve only the nodes required for `total`.
|
|
92
|
+
|
|
93
|
+
`result.to_h` may resolve all declared outputs, but should still use lazy node-level resolution internally.
|
|
94
|
+
|
|
95
|
+
### Cached by default
|
|
96
|
+
|
|
97
|
+
If a node is already resolved and not stale, the cached state is returned.
|
|
98
|
+
|
|
99
|
+
### Deterministic order
|
|
100
|
+
|
|
101
|
+
When a set of nodes must be resolved together, Igniter uses the compiler-generated topological order. This gives:
|
|
102
|
+
|
|
103
|
+
- predictable behavior
|
|
104
|
+
- deterministic event ordering
|
|
105
|
+
- easier testing and auditing
|
|
106
|
+
|
|
107
|
+
## Input Update Rules
|
|
108
|
+
|
|
109
|
+
When inputs change:
|
|
110
|
+
|
|
111
|
+
1. validate input keys
|
|
112
|
+
2. update input values
|
|
113
|
+
3. find all downstream nodes
|
|
114
|
+
4. mark cached downstream states as stale
|
|
115
|
+
5. emit `input_updated` and `node_invalidated` events
|
|
116
|
+
|
|
117
|
+
No recomputation happens during invalidation itself unless explicitly requested by the caller.
|
|
118
|
+
|
|
119
|
+
## Failure Rules
|
|
120
|
+
|
|
121
|
+
Failures are stored as node state, not hidden in logs.
|
|
122
|
+
|
|
123
|
+
If dependency resolution fails:
|
|
124
|
+
|
|
125
|
+
- dependent node resolves to failed state
|
|
126
|
+
- failure is explicit
|
|
127
|
+
- dependent nodes can choose fail-fast behavior
|
|
128
|
+
|
|
129
|
+
Default kernel policy:
|
|
130
|
+
|
|
131
|
+
- input node failures are validation failures
|
|
132
|
+
- compute node failures wrap exceptions in `ResolutionError`
|
|
133
|
+
- output nodes mirror source node state
|
|
134
|
+
|
|
135
|
+
## Composition Execution
|
|
136
|
+
|
|
137
|
+
Composition node resolution:
|
|
138
|
+
|
|
139
|
+
1. resolve parent-side mapping dependencies
|
|
140
|
+
2. build child inputs
|
|
141
|
+
3. instantiate child execution
|
|
142
|
+
4. resolve child outputs inside the child execution
|
|
143
|
+
5. return child `Result`
|
|
144
|
+
|
|
145
|
+
Composition should not flatten child state into the parent cache. Child execution remains isolated.
|
|
146
|
+
|
|
147
|
+
Recommended metadata on composition events:
|
|
148
|
+
|
|
149
|
+
- `parent_execution_id`
|
|
150
|
+
- `child_execution_id`
|
|
151
|
+
- `composition_node_id`
|
|
152
|
+
- `child_contract`
|
|
153
|
+
|
|
154
|
+
## Event Contract
|
|
155
|
+
|
|
156
|
+
Canonical kernel events:
|
|
157
|
+
|
|
158
|
+
- `execution_started`
|
|
159
|
+
- `execution_finished`
|
|
160
|
+
- `execution_failed`
|
|
161
|
+
- `input_updated`
|
|
162
|
+
- `node_started`
|
|
163
|
+
- `node_succeeded`
|
|
164
|
+
- `node_failed`
|
|
165
|
+
- `node_invalidated`
|
|
166
|
+
|
|
167
|
+
Suggested event fields:
|
|
168
|
+
|
|
169
|
+
- `event_id`
|
|
170
|
+
- `execution_id`
|
|
171
|
+
- `timestamp`
|
|
172
|
+
- `type`
|
|
173
|
+
- `node_id`
|
|
174
|
+
- `node_name`
|
|
175
|
+
- `path`
|
|
176
|
+
- `status`
|
|
177
|
+
- `payload`
|
|
178
|
+
|
|
179
|
+
Current payload examples:
|
|
180
|
+
|
|
181
|
+
- composition success payload includes `child_execution_id` and `child_graph`
|
|
182
|
+
- `execution_failed` includes `graph`, `targets`, and `error`
|
|
183
|
+
- `node_invalidated` includes `cause`
|
|
184
|
+
|
|
185
|
+
## Public Resolution API
|
|
186
|
+
|
|
187
|
+
Recommended public behavior:
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
contract = PriceContract.new(order_total: 100, country: "UA")
|
|
191
|
+
|
|
192
|
+
contract.result.total
|
|
193
|
+
contract.result.to_h
|
|
194
|
+
|
|
195
|
+
contract.update_inputs(order_total: 120)
|
|
196
|
+
contract.result.total
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The runtime contract should be:
|
|
200
|
+
|
|
201
|
+
- reads are lazy
|
|
202
|
+
- writes invalidate
|
|
203
|
+
- recomputation is explicit via subsequent reads
|
|
204
|
+
|
|
205
|
+
## Concurrency
|
|
206
|
+
|
|
207
|
+
The first version should be synchronous and single-threaded.
|
|
208
|
+
|
|
209
|
+
Reasons:
|
|
210
|
+
|
|
211
|
+
- deterministic semantics first
|
|
212
|
+
- simpler cache invariants
|
|
213
|
+
- easier event ordering
|
|
214
|
+
- easier debugging
|
|
215
|
+
|
|
216
|
+
Thread-safe or parallel execution can be added later behind explicit executors.
|
|
217
|
+
|
|
218
|
+
## Kernel Invariants
|
|
219
|
+
|
|
220
|
+
These invariants should be enforced by tests:
|
|
221
|
+
|
|
222
|
+
1. compiled graph is immutable
|
|
223
|
+
2. node path is stable
|
|
224
|
+
3. same fresh node is not recomputed twice
|
|
225
|
+
4. stale downstream nodes are recomputed on next read
|
|
226
|
+
5. unrelated nodes are not invalidated
|
|
227
|
+
6. event order is deterministic
|
|
228
|
+
7. failures remain inspectable in cache/result
|
|
229
|
+
8. composition creates isolated child executions
|
|
230
|
+
|
|
231
|
+
## Testing Strategy
|
|
232
|
+
|
|
233
|
+
Minimum runtime test matrix:
|
|
234
|
+
|
|
235
|
+
- resolves a linear graph
|
|
236
|
+
- resolves a branching graph
|
|
237
|
+
- skips unrelated nodes
|
|
238
|
+
- caches resolved nodes
|
|
239
|
+
- invalidates only downstream nodes
|
|
240
|
+
- preserves unaffected cached nodes
|
|
241
|
+
- captures compute exceptions as failed node state
|
|
242
|
+
- resolves nested composition
|
|
243
|
+
- emits expected events in order
|
|
244
|
+
- exposes machine-readable execution/result/event payloads
|
|
245
|
+
- exposes diagnostics reports for both success and failure flows
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Igniter: Concepts and Principles
|
|
2
|
+
|
|
3
|
+
This document describes the high-level concepts, philosophy, and architectural principles behind the Igniter framework.
|
|
4
|
+
|
|
5
|
+
## What is Igniter?
|
|
6
|
+
|
|
7
|
+
**Igniter** is a Ruby framework for building **declarative, auditable, and reactive business processes**.
|
|
8
|
+
|
|
9
|
+
It allows you to describe complex business logic not as a sequence of imperative steps, but as a **dependency graph** of
|
|
10
|
+
data and computations. Igniter handles the orchestration of this graph: it determines *when* and in *what order* to
|
|
11
|
+
perform computations, and it does so lazily—only when a result is actually needed.
|
|
12
|
+
|
|
13
|
+
The core idea is to separate the description of **WHAT** needs to be done (the graph's structure) from **HOW** it's
|
|
14
|
+
done (the Ruby logic within the computations).
|
|
15
|
+
|
|
16
|
+
## Philosophy
|
|
17
|
+
|
|
18
|
+
1. **Declarative Structure, Imperative Logic.**
|
|
19
|
+
The process structure (inputs, outputs, dependencies) is described using a simple and limited DSL. The computations
|
|
20
|
+
and business rules themselves are written in pure, powerful, and familiar Ruby. We are not inventing a new language,
|
|
21
|
+
but providing a framework for organizing existing code.
|
|
22
|
+
|
|
23
|
+
2. **Explicit is better than Implicit.**
|
|
24
|
+
Dependencies between components are always declared explicitly (`depends_on: ...`). The framework avoids "magic," automatic
|
|
25
|
+
dependency injection, or hidden behaviors. If node `A` depends on node `B`, it's always visible in the code.
|
|
26
|
+
|
|
27
|
+
3. **Separation of Concerns.**
|
|
28
|
+
Igniter is architecturally divided into independent modules:
|
|
29
|
+
* **Definition:** The static "description" of a contract.
|
|
30
|
+
* **Runtime:** The "live" execution and computation of the graph.
|
|
31
|
+
* **DSL:** The tools for building the definition graph.
|
|
32
|
+
* **Auditing:** Recording and replaying the execution history.
|
|
33
|
+
* **Reactive:** Reacting to events within the graph.
|
|
34
|
+
|
|
35
|
+
4. **Transparency and Debugging "out of the box."**
|
|
36
|
+
The framework is designed so that its execution is easy to trace. Built-in graph/runtime introspection and
|
|
37
|
+
auditing snapshots are not add-ons, but an integral part of the core.
|
|
38
|
+
|
|
39
|
+
## Key Concepts
|
|
40
|
+
|
|
41
|
+
#### Contract
|
|
42
|
+
|
|
43
|
+
The main unit of work in Igniter. A class inheriting from `Igniter::Contract` that encapsulates a single business process.
|
|
44
|
+
|
|
45
|
+
#### Definition Graph
|
|
46
|
+
|
|
47
|
+
The static "blueprint" or "plan" of a contract. It is created once when the class is loaded, within the
|
|
48
|
+
`define do ... end` block. This graph describes all the nodes, their types, and the dependencies between them. It is
|
|
49
|
+
immutable during execution.
|
|
50
|
+
|
|
51
|
+
#### Runtime Graph
|
|
52
|
+
|
|
53
|
+
The "live" representation of the contract at runtime. It contains `Runtime::NodeState` objects, which store the computed
|
|
54
|
+
values, statuses (`:succeeded`, `:failed`, `:stale`), and errors for each node.
|
|
55
|
+
|
|
56
|
+
#### Nodes
|
|
57
|
+
|
|
58
|
+
The basic building blocks of the graph. The main node types in the DSL are:
|
|
59
|
+
|
|
60
|
+
* **`input`**: The entry point for data into the contract.
|
|
61
|
+
* **`compute`**: The main workhorse node. It performs data transformation. It can be a simple computation (with a `Proc`
|
|
62
|
+
or method) or a composition of another contract.
|
|
63
|
+
* **`output`**: The public interface of the contract. It declares which internal nodes are the official result of the
|
|
64
|
+
process.
|
|
65
|
+
* **`composition`**: A special kind of `compute` that encapsulates and executes another contract, allowing for the
|
|
66
|
+
construction of a process hierarchy.
|
|
67
|
+
* **`reaction`**: A node describing a side effect that should occur in response to an event on the graph (e.g., on the
|
|
68
|
+
successful computation of another node).
|
|
69
|
+
|
|
70
|
+
## Contract Lifecycle
|
|
71
|
+
|
|
72
|
+
1. **Definition:** Ruby loads the contract class. The `context` block is executed, building the static
|
|
73
|
+
compiled graph.
|
|
74
|
+
2. **Initialization:** `MyContract.new(inputs)` is called. An instance of the contract and its execution context (
|
|
75
|
+
`Runtime::Execution`) are created. The input data is stored.
|
|
76
|
+
3. **Execution:** a result reader such as `contract.result.total` or `contract.resolve_all` is called. Igniter begins to lazily traverse the dependency graph, starting from the
|
|
77
|
+
`output` nodes. It only computes the nodes necessary to produce the result. Computation results are cached.
|
|
78
|
+
4. **Result:** After resolution completes, the `contract.result` object provides access to the outputs and the overall
|
|
79
|
+
status (`success?`/`failed?`).
|
|
80
|
+
5. **Update and Re-computation:** When input data is changed with `contract.update_inputs(...)`, Igniter invalidates only the parts of the graph that depend on the changed input and re-computes only
|
|
81
|
+
them.
|
data/examples/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
These scripts are intended to be runnable entry points for new users.
|
|
4
|
+
Each one can be executed directly from the project root with `ruby examples/<name>.rb`.
|
|
5
|
+
|
|
6
|
+
## Available Scripts
|
|
7
|
+
|
|
8
|
+
### `basic_pricing.rb`
|
|
9
|
+
|
|
10
|
+
Run:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
ruby examples/basic_pricing.rb
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Shows:
|
|
17
|
+
|
|
18
|
+
- defining a basic contract
|
|
19
|
+
- lazy output resolution through `result`
|
|
20
|
+
- selective recomputation after `update_inputs`
|
|
21
|
+
|
|
22
|
+
Expected output:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
gross_total=120.0
|
|
26
|
+
updated_gross_total=180.0
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `composition.rb`
|
|
30
|
+
|
|
31
|
+
Run:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
ruby examples/composition.rb
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Shows:
|
|
38
|
+
|
|
39
|
+
- nested contracts through `compose`
|
|
40
|
+
- returning child results through an output
|
|
41
|
+
- serializing composed output values with `result.to_h`
|
|
42
|
+
|
|
43
|
+
Expected output:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
pricing={:pricing=>{:gross_total=>120.0}}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `diagnostics.rb`
|
|
50
|
+
|
|
51
|
+
Run:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ruby examples/diagnostics.rb
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Shows:
|
|
58
|
+
|
|
59
|
+
- diagnostics text summary
|
|
60
|
+
- machine-readable `result.as_json`
|
|
61
|
+
- execution state visibility after a successful run
|
|
62
|
+
|
|
63
|
+
Expected output shape:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
Diagnostics PriceContract
|
|
67
|
+
Execution <uuid>
|
|
68
|
+
Status: succeeded
|
|
69
|
+
Outputs: gross_total=120.0
|
|
70
|
+
...
|
|
71
|
+
---
|
|
72
|
+
{:graph=>"PriceContract", ...}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Validation
|
|
76
|
+
|
|
77
|
+
These scripts are exercised by [example_scripts_spec.rb](/Users/alex/dev/hotfix/igniter/spec/igniter/example_scripts_spec.rb), so the documented commands and outputs stay aligned with the code.
|