igniter 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +217 -0
  3. data/docs/APPLICATION_V1.md +253 -0
  4. data/docs/CAPABILITIES_V1.md +207 -0
  5. data/docs/CONSENSUS_V1.md +477 -0
  6. data/docs/CONTENT_ADDRESSING_V1.md +221 -0
  7. data/docs/DATAFLOW_V1.md +274 -0
  8. data/docs/MESH_V1.md +732 -0
  9. data/docs/NODE_CACHE_V1.md +324 -0
  10. data/docs/PROACTIVE_AGENTS_V1.md +293 -0
  11. data/docs/SERVER_V1.md +200 -1
  12. data/docs/SKILLS_V1.md +213 -0
  13. data/docs/STORE_ADAPTERS.md +41 -13
  14. data/docs/TEMPORAL_V1.md +174 -0
  15. data/docs/TOOLS_V1.md +347 -0
  16. data/docs/TRANSCRIPTION_V1.md +403 -0
  17. data/examples/README.md +37 -0
  18. data/examples/consensus.rb +239 -0
  19. data/examples/dataflow.rb +308 -0
  20. data/examples/elocal_webhook.rb +1 -0
  21. data/examples/incremental.rb +142 -0
  22. data/examples/llm_tools.rb +237 -0
  23. data/examples/mesh.rb +239 -0
  24. data/examples/mesh_discovery.rb +267 -0
  25. data/examples/mesh_gossip.rb +162 -0
  26. data/examples/ringcentral_routing.rb +1 -1
  27. data/lib/igniter/agents/ai/alert_agent.rb +111 -0
  28. data/lib/igniter/agents/ai/chain_agent.rb +127 -0
  29. data/lib/igniter/agents/ai/critic_agent.rb +163 -0
  30. data/lib/igniter/agents/ai/evaluator_agent.rb +193 -0
  31. data/lib/igniter/agents/ai/evolution_agent.rb +286 -0
  32. data/lib/igniter/agents/ai/health_check_agent.rb +122 -0
  33. data/lib/igniter/agents/ai/observer_agent.rb +184 -0
  34. data/lib/igniter/agents/ai/planner_agent.rb +210 -0
  35. data/lib/igniter/agents/ai/router_agent.rb +131 -0
  36. data/lib/igniter/agents/ai/self_reflection_agent.rb +175 -0
  37. data/lib/igniter/agents/observability/metrics_agent.rb +130 -0
  38. data/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
  39. data/lib/igniter/agents/proactive_agent.rb +208 -0
  40. data/lib/igniter/agents/reliability/retry_agent.rb +99 -0
  41. data/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
  42. data/lib/igniter/agents.rb +56 -0
  43. data/lib/igniter/application/app_config.rb +32 -0
  44. data/lib/igniter/application/autoloader.rb +18 -0
  45. data/lib/igniter/application/generator.rb +157 -0
  46. data/lib/igniter/application/scheduler.rb +109 -0
  47. data/lib/igniter/application/yml_loader.rb +39 -0
  48. data/lib/igniter/application.rb +174 -0
  49. data/lib/igniter/capabilities.rb +68 -0
  50. data/lib/igniter/compiler/validators/dependencies_validator.rb +50 -2
  51. data/lib/igniter/compiler/validators/remote_validator.rb +2 -0
  52. data/lib/igniter/consensus/cluster.rb +183 -0
  53. data/lib/igniter/consensus/errors.rb +14 -0
  54. data/lib/igniter/consensus/executors.rb +43 -0
  55. data/lib/igniter/consensus/node.rb +320 -0
  56. data/lib/igniter/consensus/read_query.rb +30 -0
  57. data/lib/igniter/consensus/state_machine.rb +58 -0
  58. data/lib/igniter/consensus.rb +58 -0
  59. data/lib/igniter/content_addressing.rb +133 -0
  60. data/lib/igniter/contract.rb +12 -0
  61. data/lib/igniter/dataflow/aggregate_operators.rb +147 -0
  62. data/lib/igniter/dataflow/aggregate_state.rb +77 -0
  63. data/lib/igniter/dataflow/diff.rb +37 -0
  64. data/lib/igniter/dataflow/diff_state.rb +81 -0
  65. data/lib/igniter/dataflow/incremental_collection_result.rb +39 -0
  66. data/lib/igniter/dataflow/window_filter.rb +48 -0
  67. data/lib/igniter/dataflow.rb +65 -0
  68. data/lib/igniter/dsl/contract_builder.rb +71 -7
  69. data/lib/igniter/executor.rb +60 -0
  70. data/lib/igniter/extensions/capabilities.rb +39 -0
  71. data/lib/igniter/extensions/content_addressing.rb +5 -0
  72. data/lib/igniter/extensions/dataflow.rb +117 -0
  73. data/lib/igniter/extensions/incremental.rb +50 -0
  74. data/lib/igniter/extensions/mesh.rb +31 -0
  75. data/lib/igniter/fingerprint.rb +43 -0
  76. data/lib/igniter/incremental/formatter.rb +81 -0
  77. data/lib/igniter/incremental/result.rb +69 -0
  78. data/lib/igniter/incremental/tracker.rb +108 -0
  79. data/lib/igniter/incremental.rb +50 -0
  80. data/lib/igniter/integrations/llm/config.rb +48 -4
  81. data/lib/igniter/integrations/llm/executor.rb +221 -28
  82. data/lib/igniter/integrations/llm/providers/anthropic.rb +37 -4
  83. data/lib/igniter/integrations/llm/providers/openai.rb +34 -5
  84. data/lib/igniter/integrations/llm/transcription/providers/assemblyai.rb +200 -0
  85. data/lib/igniter/integrations/llm/transcription/providers/base.rb +122 -0
  86. data/lib/igniter/integrations/llm/transcription/providers/deepgram.rb +162 -0
  87. data/lib/igniter/integrations/llm/transcription/providers/openai.rb +102 -0
  88. data/lib/igniter/integrations/llm/transcription/transcriber.rb +145 -0
  89. data/lib/igniter/integrations/llm/transcription/transcript_result.rb +29 -0
  90. data/lib/igniter/integrations/llm.rb +37 -1
  91. data/lib/igniter/memory/agent_memory.rb +104 -0
  92. data/lib/igniter/memory/episode.rb +29 -0
  93. data/lib/igniter/memory/fact.rb +27 -0
  94. data/lib/igniter/memory/memorable.rb +90 -0
  95. data/lib/igniter/memory/reflection_cycle.rb +96 -0
  96. data/lib/igniter/memory/reflection_record.rb +28 -0
  97. data/lib/igniter/memory/store.rb +115 -0
  98. data/lib/igniter/memory/stores/in_memory.rb +136 -0
  99. data/lib/igniter/memory/stores/sqlite.rb +284 -0
  100. data/lib/igniter/memory.rb +80 -0
  101. data/lib/igniter/mesh/announcer.rb +55 -0
  102. data/lib/igniter/mesh/config.rb +45 -0
  103. data/lib/igniter/mesh/discovery.rb +39 -0
  104. data/lib/igniter/mesh/errors.rb +31 -0
  105. data/lib/igniter/mesh/gossip.rb +47 -0
  106. data/lib/igniter/mesh/peer.rb +21 -0
  107. data/lib/igniter/mesh/peer_registry.rb +51 -0
  108. data/lib/igniter/mesh/poller.rb +77 -0
  109. data/lib/igniter/mesh/router.rb +109 -0
  110. data/lib/igniter/mesh.rb +85 -0
  111. data/lib/igniter/metrics/collector.rb +131 -0
  112. data/lib/igniter/metrics/prometheus_exporter.rb +104 -0
  113. data/lib/igniter/metrics/snapshot.rb +8 -0
  114. data/lib/igniter/metrics.rb +37 -0
  115. data/lib/igniter/model/aggregate_node.rb +34 -0
  116. data/lib/igniter/model/collection_node.rb +3 -2
  117. data/lib/igniter/model/compute_node.rb +13 -0
  118. data/lib/igniter/model/remote_node.rb +18 -2
  119. data/lib/igniter/node_cache.rb +231 -0
  120. data/lib/igniter/replication/bootstrapper.rb +61 -0
  121. data/lib/igniter/replication/bootstrappers/gem.rb +32 -0
  122. data/lib/igniter/replication/bootstrappers/git.rb +39 -0
  123. data/lib/igniter/replication/bootstrappers/tarball.rb +56 -0
  124. data/lib/igniter/replication/expansion_plan.rb +38 -0
  125. data/lib/igniter/replication/expansion_planner.rb +142 -0
  126. data/lib/igniter/replication/manifest.rb +45 -0
  127. data/lib/igniter/replication/network_topology.rb +123 -0
  128. data/lib/igniter/replication/node_role.rb +42 -0
  129. data/lib/igniter/replication/reflective_replication_agent.rb +238 -0
  130. data/lib/igniter/replication/replication_agent.rb +87 -0
  131. data/lib/igniter/replication/role_registry.rb +73 -0
  132. data/lib/igniter/replication/ssh_session.rb +77 -0
  133. data/lib/igniter/replication.rb +54 -0
  134. data/lib/igniter/runtime/cache.rb +35 -6
  135. data/lib/igniter/runtime/execution.rb +26 -2
  136. data/lib/igniter/runtime/input_validator.rb +6 -2
  137. data/lib/igniter/runtime/node_state.rb +7 -2
  138. data/lib/igniter/runtime/resolver.rb +323 -31
  139. data/lib/igniter/runtime/stores/redis_store.rb +41 -4
  140. data/lib/igniter/server/client.rb +44 -1
  141. data/lib/igniter/server/config.rb +13 -6
  142. data/lib/igniter/server/handlers/event_handler.rb +4 -0
  143. data/lib/igniter/server/handlers/execute_handler.rb +6 -0
  144. data/lib/igniter/server/handlers/liveness_handler.rb +20 -0
  145. data/lib/igniter/server/handlers/manifest_handler.rb +34 -0
  146. data/lib/igniter/server/handlers/metrics_handler.rb +51 -0
  147. data/lib/igniter/server/handlers/peers_handler.rb +115 -0
  148. data/lib/igniter/server/handlers/readiness_handler.rb +47 -0
  149. data/lib/igniter/server/http_server.rb +54 -17
  150. data/lib/igniter/server/router.rb +54 -21
  151. data/lib/igniter/server/server_logger.rb +52 -0
  152. data/lib/igniter/server.rb +6 -0
  153. data/lib/igniter/skill/feedback.rb +116 -0
  154. data/lib/igniter/skill/output_schema.rb +110 -0
  155. data/lib/igniter/skill.rb +218 -0
  156. data/lib/igniter/temporal.rb +84 -0
  157. data/lib/igniter/tool/discoverable.rb +151 -0
  158. data/lib/igniter/tool.rb +52 -0
  159. data/lib/igniter/tool_registry.rb +144 -0
  160. data/lib/igniter/version.rb +1 -1
  161. data/lib/igniter.rb +17 -0
  162. metadata +128 -1
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "igniter/dataflow/diff"
4
+ require "igniter/dataflow/diff_state"
5
+ require "igniter/dataflow/window_filter"
6
+ require "igniter/dataflow/incremental_collection_result"
7
+ require "igniter/dataflow/aggregate_operators"
8
+ require "igniter/dataflow/aggregate_state"
9
+ require "igniter/model/aggregate_node"
10
+
11
+ module Igniter
12
+ # Incremental Dataflow — Phase 1: differential collection processing.
13
+ #
14
+ # Adds `mode: :incremental` to the `collection` DSL node. In this mode the
15
+ # resolver tracks per-item state between `update_inputs` calls and re-runs
16
+ # child contracts only for items that were added or changed. Removed items
17
+ # are retracted automatically. Unchanged items reuse their cached results
18
+ # with zero re-computation cost.
19
+ #
20
+ # Optional `window:` filter limits the active item set before diff computation:
21
+ #
22
+ # window: { last: 100 }
23
+ # Keep only the last 100 items (most recent last in the array).
24
+ #
25
+ # window: { seconds: 300, field: :received_at }
26
+ # Keep items where item[:received_at] >= Time.now - 300.
27
+ #
28
+ # == Usage
29
+ #
30
+ # require "igniter/extensions/dataflow"
31
+ #
32
+ # class SensorPipeline < Igniter::Contract
33
+ # define do
34
+ # input :readings, type: :array
35
+ #
36
+ # collection :processed,
37
+ # with: :readings,
38
+ # each: SensorContract,
39
+ # key: :sensor_id,
40
+ # mode: :incremental,
41
+ # window: { last: 500 }
42
+ #
43
+ # output :processed
44
+ # end
45
+ # end
46
+ #
47
+ # contract = SensorPipeline.new(readings: initial_readings)
48
+ # contract.resolve_all
49
+ #
50
+ # contract.update_inputs(readings: updated_readings)
51
+ # contract.resolve_all
52
+ #
53
+ # result = contract.result.processed
54
+ # result.diff.added # => [:sensor_42] — child contract was re-run
55
+ # result.diff.changed # => [:sensor_7] — child contract was re-run
56
+ # result.diff.removed # => [:sensor_3] — retracted from result
57
+ # result.diff.unchanged # => [:sensor_1, :sensor_2] — result reused
58
+ #
59
+ # # Convenience: push events as a diff instead of replacing the full array
60
+ # contract.feed_diff(:readings, add: [new_reading], remove: [:sensor_3])
61
+ #
62
+ module Dataflow
63
+ class DataflowError < Igniter::Error; end
64
+ end
65
+ end
@@ -96,10 +96,6 @@ module Igniter
96
96
  compute(name, with: from, call: callable, **{ category: :project }.merge(metadata))
97
97
  end
98
98
 
99
- def aggregate(name, depends_on: nil, with: nil, call: nil, executor: nil, **metadata, &block)
100
- compute(name, depends_on: depends_on, with: with, call: call, executor: executor, **{ category: :aggregate }.merge(metadata), &block)
101
- end
102
-
103
99
  def guard(name, depends_on: nil, with: nil, call: nil, executor: nil, message: nil,
104
100
  eq: UNDEFINED_GUARD_MATCHER, in: UNDEFINED_GUARD_MATCHER, matches: UNDEFINED_GUARD_MATCHER,
105
101
  **metadata, &block)
@@ -210,7 +206,7 @@ module Igniter
210
206
  )
211
207
  end
212
208
 
213
- def collection(name, with:, each:, key:, mode: :collect, depends_on: nil, map_inputs: nil, using: nil, **metadata)
209
+ def collection(name, with:, each:, key:, mode: :collect, window: nil, depends_on: nil, map_inputs: nil, using: nil, **metadata)
214
210
  raise CompileError, "collection :#{name} cannot use both `map_inputs:` and `using:`" if map_inputs && using
215
211
 
216
212
  add_node(
@@ -221,6 +217,7 @@ module Igniter
221
217
  contract_class: each,
222
218
  key_name: key,
223
219
  mode: mode,
220
+ window: window,
224
221
  context_dependencies: normalize_dependencies(depends_on: depends_on, with: nil),
225
222
  input_mapper: map_inputs || using,
226
223
  path: scoped_path(name),
@@ -229,10 +226,55 @@ module Igniter
229
226
  )
230
227
  end
231
228
 
232
- def remote(name, contract:, node:, inputs:, timeout: 30, **metadata) # rubocop:disable Metrics/MethodLength
229
+ # Declares a maintained aggregate over an incremental collection node.
230
+ #
231
+ # The aggregate updates in O(change) time: only added/changed/removed items
232
+ # affect the result; unchanged items contribute zero work.
233
+ #
234
+ # @param name [Symbol] output name
235
+ # @param from [Symbol] name of an upstream incremental collection node
236
+ # @param count [Proc, nil] ->(item) { bool } — count items matching predicate (nil = count all)
237
+ # @param sum [Proc, nil] ->(item) { numeric }
238
+ # @param avg [Proc, nil] ->(item) { numeric }
239
+ # @param min [Proc, nil] ->(item) { numeric } (O(n) on removal)
240
+ # @param max [Proc, nil] ->(item) { numeric } (O(n) on removal)
241
+ # @param group_count [Proc, nil] ->(item) { group_key } — Hash{key => count}
242
+ # @param initial [Object, nil] initial accumulator for custom aggregates
243
+ # @param add [Proc, nil] ->(acc, item) { new_acc } — custom add
244
+ # @param remove [Proc, nil] ->(acc, item) { new_acc } — custom remove
245
+ #
246
+ # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
247
+ def aggregate(name, from:, count: nil, sum: nil, avg: nil, min: nil, max: nil,
248
+ group_count: nil, initial: nil, add: nil, remove: nil, **metadata)
249
+ # rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
250
+ operator = build_aggregate_operator(
251
+ count: count, sum: sum, avg: avg, min: min, max: max,
252
+ group_count: group_count, initial: initial, add: add, remove: remove
253
+ )
254
+ add_node(
255
+ Model::AggregateNode.new(
256
+ id: next_id,
257
+ name: name,
258
+ source_collection: from,
259
+ operator: operator,
260
+ path: scoped_path(name),
261
+ metadata: with_source_location(metadata)
262
+ )
263
+ )
264
+ end
265
+
266
+ def remote(name, contract:, inputs:, node: nil, timeout: 30, # rubocop:disable Metrics/MethodLength,Metrics/ParameterLists,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
267
+ capability: nil, pinned_to: nil, **metadata)
233
268
  raise CompileError, "remote :#{name} requires inputs: Hash" unless inputs.is_a?(Hash)
234
269
  raise CompileError, "remote :#{name} requires a contract: name" if contract.nil? || contract.to_s.strip.empty?
235
- raise CompileError, "remote :#{name} requires a node: URL" if node.nil? || node.to_s.strip.empty?
270
+
271
+ if capability && pinned_to
272
+ raise CompileError, "remote :#{name}: capability: and pinned_to: are mutually exclusive"
273
+ end
274
+
275
+ if capability.nil? && pinned_to.nil? && (node.nil? || node.to_s.strip.empty?)
276
+ raise CompileError, "remote :#{name} requires a node: URL"
277
+ end
236
278
 
237
279
  add_node(
238
280
  Model::RemoteNode.new(
@@ -242,6 +284,8 @@ module Igniter
242
284
  node_url: node.to_s,
243
285
  input_mapping: inputs,
244
286
  timeout: timeout,
287
+ capability: capability,
288
+ pinned_to: pinned_to,
245
289
  path: scoped_path(name),
246
290
  metadata: with_source_location(metadata)
247
291
  )
@@ -399,6 +443,26 @@ module Igniter
399
443
  "#{@scope_stack.join('.')}.output.#{name}"
400
444
  end
401
445
 
446
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
447
+ def build_aggregate_operator(count:, sum:, avg:, min:, max:, group_count:, initial:, add:, remove:)
448
+ if add && remove
449
+ Dataflow::AggregateOperators.custom(initial: initial || 0, add: add, remove: remove)
450
+ elsif sum
451
+ Dataflow::AggregateOperators.sum(projection: sum)
452
+ elsif avg
453
+ Dataflow::AggregateOperators.avg(projection: avg)
454
+ elsif min
455
+ Dataflow::AggregateOperators.min(projection: min)
456
+ elsif max
457
+ Dataflow::AggregateOperators.max(projection: max)
458
+ elsif group_count
459
+ Dataflow::AggregateOperators.group_count(projection: group_count)
460
+ else
461
+ Dataflow::AggregateOperators.count(filter: count)
462
+ end
463
+ end
464
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
465
+
402
466
  class BranchBuilder
403
467
  def self.build(&block)
404
468
  new.tap { |builder| builder.instance_eval(&block) }.to_h
@@ -7,6 +7,7 @@ module Igniter
7
7
  super
8
8
  subclass.instance_variable_set(:@executor_inputs, executor_inputs.transform_values(&:dup))
9
9
  subclass.instance_variable_set(:@executor_metadata, executor_metadata.dup)
10
+ # capabilities and fingerprint are NOT inherited — each subclass declares its own
10
11
  end
11
12
 
12
13
  def input(name, required: true, type: nil, **metadata)
@@ -51,6 +52,65 @@ module Igniter
51
52
  new.call(**dependencies)
52
53
  end
53
54
 
55
+ # ─── Capabilities DSL ────────────────────────────────────────────────────
56
+ #
57
+ # Declare what this executor is allowed to do. Capabilities are purely
58
+ # declarative — enforcement requires Igniter::Capabilities::Policy.
59
+ #
60
+ # Known capabilities:
61
+ # :pure — deterministic, no side effects; enables content-addressed caching
62
+ # :network — makes outbound HTTP/TCP connections
63
+ # :database — reads or writes a database
64
+ # :filesystem — reads or writes files
65
+ # :external_api — calls a third-party API
66
+ # :messaging — publishes to a message queue or broker
67
+ # :cache — reads or writes an external cache
68
+ #
69
+ # Example:
70
+ # class PaymentExecutor < Igniter::Executor
71
+ # capabilities :network, :external_api
72
+ # end
73
+ def capabilities(*caps)
74
+ if caps.empty?
75
+ @declared_capabilities ||= []
76
+ else
77
+ existing = @declared_capabilities || []
78
+ @declared_capabilities = (existing + caps.flatten.map(&:to_sym)).uniq.freeze
79
+ end
80
+ end
81
+
82
+ def declared_capabilities
83
+ @declared_capabilities || []
84
+ end
85
+
86
+ # Shorthand for `capabilities :pure`.
87
+ # A pure executor is fully deterministic: same inputs → same output, always.
88
+ # Pure executors participate in content-addressed cross-execution caching.
89
+ def pure
90
+ capabilities(:pure)
91
+ end
92
+
93
+ def pure?
94
+ declared_capabilities.include?(:pure)
95
+ end
96
+
97
+ # ─── Content-addressing fingerprint ──────────────────────────────────────
98
+ #
99
+ # Optional explicit version string used as part of the content-addressing key.
100
+ # Set a new value to invalidate the content cache after changing executor logic
101
+ # while keeping the class name stable.
102
+ #
103
+ # fingerprint "tax_calculator_v2"
104
+ def fingerprint(value = nil)
105
+ return @content_fingerprint || name || "anonymous_executor" if value.nil?
106
+
107
+ @content_fingerprint = value.to_s.freeze
108
+ end
109
+
110
+ def content_fingerprint
111
+ @content_fingerprint || name || "anonymous_executor"
112
+ end
113
+
54
114
  private
55
115
 
56
116
  def metadata_value(key, value)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "igniter/capabilities"
4
+
5
+ # Patches CompiledGraph with capability introspection methods.
6
+ # The Resolver integration is handled via guard-claused hooks in resolver.rb.
7
+ module Igniter
8
+ module Compiler
9
+ class CompiledGraph
10
+ # Returns a Hash of { node_name => [capabilities] } for every compute/effect
11
+ # node whose executor declares at least one capability.
12
+ def required_capabilities
13
+ (nodes + outputs).each_with_object({}) do |node, memo|
14
+ caps = node_capabilities(node)
15
+ memo[node.name] = caps unless caps.empty?
16
+ end
17
+ end
18
+
19
+ # Returns declared capabilities for a single node by name.
20
+ def capabilities_for(node_name)
21
+ sym = node_name.to_sym
22
+ target = (nodes + outputs).find { |n| n.name == sym }
23
+ return [] unless target
24
+
25
+ node_capabilities(target)
26
+ end
27
+
28
+ private
29
+
30
+ def node_capabilities(node)
31
+ callable = node.respond_to?(:callable) ? node.callable : nil
32
+ callable ||= node.respond_to?(:adapter_class) ? node.adapter_class : nil
33
+ return [] unless callable.is_a?(Class) && callable <= Igniter::Executor
34
+
35
+ callable.declared_capabilities
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loading this file activates content-addressed caching for pure executors.
4
+ # The Resolver picks up content addressing via a guard clause when this constant is defined.
5
+ require "igniter/content_addressing"
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "igniter"
4
+ require "igniter/dataflow"
5
+
6
+ # Activates incremental dataflow support for all contracts.
7
+ #
8
+ # After requiring this file:
9
+ #
10
+ # - `collection` DSL accepts `mode: :incremental` and `window:` options.
11
+ # - Contracts gain `#feed_diff` — push event-style diffs instead of full arrays.
12
+ # - Contracts gain `#collection_diff` — inspect what changed after the last resolve.
13
+ #
14
+ # == Usage
15
+ #
16
+ # require "igniter/extensions/dataflow"
17
+ #
18
+ # class SensorPipeline < Igniter::Contract
19
+ # define do
20
+ # input :readings, type: :array
21
+ #
22
+ # collection :processed,
23
+ # with: :readings,
24
+ # each: SensorContract,
25
+ # key: :sensor_id,
26
+ # mode: :incremental,
27
+ # window: { last: 500 }
28
+ #
29
+ # output :processed
30
+ # end
31
+ # end
32
+ #
33
+ # contract = SensorPipeline.new(readings: initial_readings)
34
+ # contract.resolve_all
35
+ #
36
+ # # Push new events without replacing the full array
37
+ # contract.feed_diff(:readings, add: [new_reading], remove: [:sensor_old])
38
+ # contract.resolve_all
39
+ #
40
+ # # Inspect what changed
41
+ # diff = contract.collection_diff(:processed)
42
+ # diff.added # => [:sensor_42]
43
+ # diff.removed # => [:sensor_old]
44
+ # diff.changed # => []
45
+ # diff.unchanged # => [:sensor_1, :sensor_2, ...]
46
+ #
47
+ module Igniter
48
+ module Extensions
49
+ module Dataflow
50
+ module InstanceMethods
51
+ # Push a diff to a collection input without replacing the full array.
52
+ #
53
+ # Automatically finds the incremental collection node that uses +input_name+
54
+ # as its source dependency, uses its key_name to merge the diff into the
55
+ # current input array, then calls update_inputs.
56
+ #
57
+ # @param input_name [Symbol, String] the contract input holding the collection
58
+ # @param add [Array<Hash>] new items to append
59
+ # @param remove [Array] keys or Hash items to remove (matched by key_name)
60
+ # @param update [Array<Hash>] updated versions of existing items (replace by key)
61
+ # @return [self]
62
+ #
63
+ # @raise [ArgumentError] if no incremental collection node uses the given input
64
+ def feed_diff(input_name, add: [], remove: [], update: [])
65
+ sym = input_name.to_sym
66
+ key_name = _incremental_node_for(sym).key_name
67
+ current = execution.inputs[sym].dup || []
68
+ items = _apply_diff(current, key_name, add: add, remove: remove, update: update)
69
+ execution.update_inputs(sym => items)
70
+ self
71
+ end
72
+
73
+ # Returns the Diff from the last resolve for a named collection node.
74
+ #
75
+ # @param collection_name [Symbol, String] the collection node name
76
+ # @return [Igniter::Dataflow::Diff, nil] nil if not yet resolved or not incremental
77
+ def collection_diff(collection_name)
78
+ state = execution.cache.fetch(collection_name.to_sym)
79
+ value = state&.value
80
+ value.respond_to?(:diff) ? value.diff : nil
81
+ end
82
+
83
+ private
84
+
85
+ def _apply_diff(items, key_name, add:, remove:, update:)
86
+ remove_keys = Array(remove).map { |e| e.is_a?(Hash) ? e.fetch(key_name) : e }.to_set
87
+ result = items.reject { |item| remove_keys.include?(item.fetch(key_name)) }
88
+ _apply_updates(result, key_name, update)
89
+ result.concat(Array(add))
90
+ end
91
+
92
+ def _apply_updates(items, key_name, updates)
93
+ Array(updates).each do |updated|
94
+ k = updated.fetch(key_name)
95
+ idx = items.index { |item| item.fetch(key_name) == k }
96
+ idx ? items[idx] = updated : items << updated
97
+ end
98
+ end
99
+
100
+ def _incremental_node_for(input_name)
101
+ node = execution.compiled_graph.nodes.find do |n|
102
+ n.kind == :collection &&
103
+ n.mode == :incremental &&
104
+ n.source_dependency == input_name
105
+ end
106
+ return node if node
107
+
108
+ raise ArgumentError,
109
+ "No incremental collection node found for input '#{input_name}'. " \
110
+ "Ensure the collection is declared with mode: :incremental."
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ Igniter::Contract.include(Igniter::Extensions::Dataflow::InstanceMethods)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "igniter"
4
+ require "igniter/incremental"
5
+
6
+ module Igniter
7
+ module Extensions
8
+ # Patches Igniter::Contract with:
9
+ # - Instance method: resolve_incrementally(new_inputs = {}) → Incremental::Result
10
+ #
11
+ # The method updates inputs, re-resolves, and returns a structured report
12
+ # of which nodes changed, were memoized, or were backdated.
13
+ #
14
+ # Applied globally via:
15
+ # Igniter::Contract.include(Igniter::Extensions::Incremental)
16
+ #
17
+ module Incremental
18
+ def self.included(base)
19
+ base.include(InstanceMethods)
20
+ end
21
+
22
+ module InstanceMethods
23
+ # Update inputs and re-resolve, returning a detailed incremental result.
24
+ #
25
+ # If called without arguments on an already-resolved contract, it
26
+ # re-resolves with current inputs (useful for observing memoization).
27
+ #
28
+ # @param new_inputs [Hash] input values to update before re-resolving
29
+ # @return [Igniter::Incremental::Result]
30
+ def resolve_incrementally(new_inputs = {})
31
+ unless execution.cache.values.any?
32
+ raise Igniter::Incremental::IncrementalError,
33
+ "Contract has not been executed yet — call resolve_all first, " \
34
+ "then resolve_incrementally to get incremental results"
35
+ end
36
+
37
+ tracker = Igniter::Incremental::Tracker.new(execution)
38
+ tracker.start!
39
+
40
+ update_inputs(new_inputs) if new_inputs.any?
41
+ resolve_all
42
+
43
+ tracker.build_result
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ Igniter::Contract.include(Igniter::Extensions::Incremental)
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Igniter Mesh — Phase 1 (Static) + Phase 2 (Dynamic Discovery)
4
+ #
5
+ # Extends the remote: DSL with capability-based and pinned routing modes, and
6
+ # enables dynamic peer discovery so that the peer topology can be maintained
7
+ # without static add_peer declarations.
8
+ #
9
+ # Usage:
10
+ # require "igniter/extensions/mesh"
11
+ #
12
+ # # Phase 1 — static topology (still works):
13
+ # Igniter::Mesh.configure do |c|
14
+ # c.add_peer "orders-node",
15
+ # url: "http://orders.internal:4567",
16
+ # capabilities: [:orders, :inventory]
17
+ # end
18
+ #
19
+ # # Phase 2 — dynamic discovery:
20
+ # Igniter::Mesh.configure do |c|
21
+ # c.peer_name = "api-node"
22
+ # c.local_url = "http://api.internal:4567"
23
+ # c.local_capabilities = [:api]
24
+ # c.seeds = %w[http://orders.internal:4567 http://audit.internal:4567]
25
+ # c.discovery_interval = 30
26
+ # end
27
+ # Igniter::Mesh.start_discovery!
28
+ #
29
+ require "igniter"
30
+ require "igniter/server"
31
+ require "igniter/mesh"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ # Mixin that enables stable cross-execution cache keys for objects used as
5
+ # compute node dependencies (e.g. ActiveRecord models).
6
+ #
7
+ # Include this module in any class whose instances are passed as node deps:
8
+ #
9
+ # class Trade < ApplicationRecord
10
+ # include Igniter::Fingerprint
11
+ # end
12
+ #
13
+ # The Rails Railtie includes this automatically in ApplicationRecord when
14
+ # the igniter-rails integration is loaded.
15
+ #
16
+ # == Custom fingerprints
17
+ #
18
+ # Override #igniter_fingerprint for non-AR objects or custom invalidation logic:
19
+ #
20
+ # class PricingConfig
21
+ # include Igniter::Fingerprint
22
+ #
23
+ # def igniter_fingerprint
24
+ # "PricingConfig:#{version}:#{market}"
25
+ # end
26
+ # end
27
+ module Fingerprint
28
+ # Returns a stable string that uniquely identifies this object's state.
29
+ # Changing the returned value invalidates any NodeCache entry that depends on it.
30
+ #
31
+ # Default: "{ClassName}:{id}:{updated_at_unix}" — works for any AR record.
32
+ # Returns "{ClassName}:{id}" for objects without updated_at.
33
+ def igniter_fingerprint
34
+ if respond_to?(:updated_at) && updated_at
35
+ "#{self.class.name}:#{id}:#{updated_at.to_i}"
36
+ elsif respond_to?(:id)
37
+ "#{self.class.name}:#{id}"
38
+ else
39
+ "#{self.class.name}:#{object_id}"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Incremental
5
+ # Renders an Incremental::Result as a human-readable text block.
6
+ #
7
+ # Example output:
8
+ #
9
+ # Incremental Execution Report
10
+ # ─────────────────────────────────────────
11
+ # Recomputed: 1 node(s)
12
+ # Skipped: 2 node(s) (deps unchanged)
13
+ # Backdated: 0 node(s) (value unchanged)
14
+ #
15
+ # CHANGED OUTPUTS (1):
16
+ # :converted_price 1.05 → 1.12
17
+ #
18
+ # SKIPPED NODES (memoized):
19
+ # :tier_discount :adjusted_price
20
+ #
21
+ module Formatter
22
+ VALUE_MAX = 60
23
+ LINE = "─" * 42
24
+
25
+ class << self
26
+ def format(result) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
27
+ lines = []
28
+ lines << "Incremental Execution Report"
29
+ lines << LINE
30
+ lines << "Recomputed: #{result.recomputed_count} node(s)"
31
+ lines << "Skipped: #{result.skipped_nodes.size} node(s) (deps unchanged)"
32
+ lines << "Backdated: #{result.backdated_nodes.size} node(s) (value unchanged)"
33
+ lines << ""
34
+
35
+ if result.changed_outputs.any?
36
+ lines << "CHANGED OUTPUTS (#{result.changed_outputs.size}):"
37
+ result.changed_outputs.each do |name, diff|
38
+ lines << " :#{name} #{fmt(diff[:from])} → #{fmt(diff[:to])}"
39
+ end
40
+ else
41
+ lines << "No output values changed."
42
+ end
43
+ lines << ""
44
+
45
+ if result.skipped_nodes.any?
46
+ lines << "SKIPPED (memoized, #{result.skipped_nodes.size}):"
47
+ lines << " #{result.skipped_nodes.map { |n| ":#{n}" }.join(" ")}"
48
+ lines << ""
49
+ end
50
+
51
+ if result.backdated_nodes.any?
52
+ lines << "BACKDATED (recomputed → same value, #{result.backdated_nodes.size}):"
53
+ lines << " #{result.backdated_nodes.map { |n| ":#{n}" }.join(" ")}"
54
+ lines << ""
55
+ end
56
+
57
+ if result.changed_nodes.any?
58
+ lines << "CHANGED (#{result.changed_nodes.size}):"
59
+ lines << " #{result.changed_nodes.map { |n| ":#{n}" }.join(" ")}"
60
+ end
61
+
62
+ lines.join("\n")
63
+ end
64
+
65
+ private
66
+
67
+ def fmt(value) # rubocop:disable Metrics/CyclomaticComplexity
68
+ str = case value
69
+ when nil then "nil"
70
+ when String then value.inspect
71
+ when Symbol then value.inspect
72
+ when Hash then "{#{value.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")}}"
73
+ when Array then "[#{value.map(&:inspect).join(", ")}]"
74
+ else value.inspect
75
+ end
76
+ str.length > VALUE_MAX ? "#{str[0, VALUE_MAX - 3]}..." : str
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end