igniter 0.4.5 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (638) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -3
  3. data/README.md +164 -409
  4. data/bin/igniter-stack +94 -0
  5. data/docs/README.md +62 -0
  6. data/examples/README.md +74 -312
  7. data/examples/agent_orchestration.rb +76 -0
  8. data/examples/agents.rb +2 -1
  9. data/examples/catalog.rb +412 -0
  10. data/examples/consensus.rb +241 -0
  11. data/examples/dataflow.rb +309 -0
  12. data/examples/distributed_workflow.rb +1 -1
  13. data/examples/effects.rb +10 -9
  14. data/examples/elocal_webhook.rb +1 -0
  15. data/examples/incremental.rb +4 -3
  16. data/examples/introspection.rb +49 -0
  17. data/examples/invariants.rb +4 -3
  18. data/examples/llm_tools.rb +237 -0
  19. data/examples/mesh.rb +241 -0
  20. data/examples/mesh_discovery.rb +287 -0
  21. data/examples/mesh_gossip.rb +164 -0
  22. data/examples/reactive_auditing.rb +50 -0
  23. data/examples/ringcentral_routing.rb +1 -1
  24. data/examples/run.rb +163 -0
  25. data/lib/igniter/monorepo_packages.rb +17 -0
  26. data/lib/igniter/stack.rb +5 -0
  27. data/lib/igniter.rb +48 -15
  28. data/packages/igniter-agents/README.md +22 -0
  29. data/{lib → packages/igniter-agents/lib}/igniter/agent/ref.rb +1 -0
  30. data/{lib → packages/igniter-agents/lib}/igniter/agent/runner.rb +12 -0
  31. data/{lib → packages/igniter-agents/lib}/igniter/agent.rb +6 -0
  32. data/packages/igniter-agents/lib/igniter/agents/observability/metrics_agent.rb +130 -0
  33. data/packages/igniter-agents/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
  34. data/packages/igniter-agents/lib/igniter/agents/proactive/alert_agent.rb +111 -0
  35. data/packages/igniter-agents/lib/igniter/agents/proactive/health_check_agent.rb +122 -0
  36. data/packages/igniter-agents/lib/igniter/agents/proactive_agent.rb +208 -0
  37. data/packages/igniter-agents/lib/igniter/agents/reliability/retry_agent.rb +99 -0
  38. data/packages/igniter-agents/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
  39. data/packages/igniter-agents/lib/igniter/agents.rb +23 -0
  40. data/packages/igniter-agents/lib/igniter/ai/agents/chain_agent.rb +129 -0
  41. data/packages/igniter-agents/lib/igniter/ai/agents/critic_agent.rb +165 -0
  42. data/packages/igniter-agents/lib/igniter/ai/agents/evaluator_agent.rb +195 -0
  43. data/packages/igniter-agents/lib/igniter/ai/agents/evolution_agent.rb +288 -0
  44. data/packages/igniter-agents/lib/igniter/ai/agents/observer_agent.rb +186 -0
  45. data/packages/igniter-agents/lib/igniter/ai/agents/planner_agent.rb +212 -0
  46. data/packages/igniter-agents/lib/igniter/ai/agents/router_agent.rb +133 -0
  47. data/packages/igniter-agents/lib/igniter/ai/agents/self_reflection_agent.rb +177 -0
  48. data/packages/igniter-agents/lib/igniter/ai/agents.rb +25 -0
  49. data/{lib → packages/igniter-agents/lib}/igniter/registry.rb +2 -0
  50. data/packages/igniter-agents/lib/igniter/runtime/registry_agent_adapter.rb +102 -0
  51. data/{lib → packages/igniter-agents/lib}/igniter/supervisor.rb +3 -0
  52. data/packages/igniter-agents/lib/igniter-agents.rb +7 -0
  53. data/packages/igniter-ai/README.md +20 -0
  54. data/packages/igniter-ai/lib/igniter/ai/config.rb +113 -0
  55. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/context.rb +2 -2
  56. data/packages/igniter-ai/lib/igniter/ai/executor.rb +352 -0
  57. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/anthropic.rb +42 -9
  58. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/base.rb +1 -1
  59. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/ollama.rb +4 -4
  60. data/{lib/igniter/integrations/llm → packages/igniter-ai/lib/igniter/ai}/providers/openai.rb +39 -10
  61. data/packages/igniter-ai/lib/igniter/ai/skill/feedback.rb +116 -0
  62. data/packages/igniter-ai/lib/igniter/ai/skill/output_schema.rb +110 -0
  63. data/packages/igniter-ai/lib/igniter/ai/skill/runtime_contract.rb +87 -0
  64. data/packages/igniter-ai/lib/igniter/ai/skill.rb +107 -0
  65. data/packages/igniter-ai/lib/igniter/ai/tool_registry.rb +79 -0
  66. data/packages/igniter-ai/lib/igniter/ai/transcription/providers/assemblyai.rb +200 -0
  67. data/packages/igniter-ai/lib/igniter/ai/transcription/providers/base.rb +122 -0
  68. data/packages/igniter-ai/lib/igniter/ai/transcription/providers/deepgram.rb +162 -0
  69. data/packages/igniter-ai/lib/igniter/ai/transcription/providers/openai.rb +102 -0
  70. data/packages/igniter-ai/lib/igniter/ai/transcription/transcriber.rb +145 -0
  71. data/packages/igniter-ai/lib/igniter/ai/transcription/transcript_result.rb +29 -0
  72. data/packages/igniter-ai/lib/igniter/ai.rb +98 -0
  73. data/packages/igniter-ai/lib/igniter-ai.rb +3 -0
  74. data/packages/igniter-app/README.md +19 -0
  75. data/packages/igniter-app/lib/igniter/app/app_config.rb +43 -0
  76. data/packages/igniter-app/lib/igniter/app/app_host.rb +56 -0
  77. data/packages/igniter-app/lib/igniter/app/app_host_config.rb +27 -0
  78. data/packages/igniter-app/lib/igniter/app/app_host_pack.rb +13 -0
  79. data/packages/igniter-app/lib/igniter/app/autoloader.rb +18 -0
  80. data/packages/igniter-app/lib/igniter/app/cluster_app_host.rb +95 -0
  81. data/packages/igniter-app/lib/igniter/app/cluster_app_host_config.rb +78 -0
  82. data/packages/igniter-app/lib/igniter/app/credentials/config_loader.rb +152 -0
  83. data/packages/igniter-app/lib/igniter/app/credentials/credential.rb +48 -0
  84. data/packages/igniter-app/lib/igniter/app/credentials/credential_policy.rb +38 -0
  85. data/packages/igniter-app/lib/igniter/app/credentials/events/credential_event.rb +179 -0
  86. data/packages/igniter-app/lib/igniter/app/credentials/events.rb +12 -0
  87. data/packages/igniter-app/lib/igniter/app/credentials/lease_request.rb +153 -0
  88. data/packages/igniter-app/lib/igniter/app/credentials/policies/ephemeral_lease_policy.rb +35 -0
  89. data/packages/igniter-app/lib/igniter/app/credentials/policies/local_only_policy.rb +33 -0
  90. data/packages/igniter-app/lib/igniter/app/credentials/policies.rb +13 -0
  91. data/packages/igniter-app/lib/igniter/app/credentials/store.rb +21 -0
  92. data/packages/igniter-app/lib/igniter/app/credentials/stores/file_store.rb +114 -0
  93. data/packages/igniter-app/lib/igniter/app/credentials/trail.rb +254 -0
  94. data/packages/igniter-app/lib/igniter/app/credentials.rb +20 -0
  95. data/packages/igniter-app/lib/igniter/app/dev_output_sync.rb +4 -0
  96. data/packages/igniter-app/lib/igniter/app/diagnostics/app_host_contributor.rb +71 -0
  97. data/packages/igniter-app/lib/igniter/app/diagnostics/cluster_app_host_contributor.rb +97 -0
  98. data/packages/igniter-app/lib/igniter/app/diagnostics/credential_contributor.rb +66 -0
  99. data/packages/igniter-app/lib/igniter/app/diagnostics/evolution_contributor.rb +74 -0
  100. data/packages/igniter-app/lib/igniter/app/diagnostics/ignite_contributor.rb +121 -0
  101. data/packages/igniter-app/lib/igniter/app/diagnostics/loader_contributor.rb +68 -0
  102. data/packages/igniter-app/lib/igniter/app/diagnostics/orchestration_contributor.rb +200 -0
  103. data/packages/igniter-app/lib/igniter/app/diagnostics/runtime_contributor.rb +68 -0
  104. data/packages/igniter-app/lib/igniter/app/diagnostics/scheduler_contributor.rb +72 -0
  105. data/packages/igniter-app/lib/igniter/app/diagnostics/sdk_contributor.rb +284 -0
  106. data/packages/igniter-app/lib/igniter/app/diagnostics.rb +62 -0
  107. data/packages/igniter-app/lib/igniter/app/evolution/approval_decision.rb +115 -0
  108. data/packages/igniter-app/lib/igniter/app/evolution/approval_request.rb +36 -0
  109. data/packages/igniter-app/lib/igniter/app/evolution/plan.rb +72 -0
  110. data/packages/igniter-app/lib/igniter/app/evolution/planner.rb +85 -0
  111. data/packages/igniter-app/lib/igniter/app/evolution/result.rb +45 -0
  112. data/packages/igniter-app/lib/igniter/app/evolution/runner.rb +102 -0
  113. data/packages/igniter-app/lib/igniter/app/evolution/store.rb +21 -0
  114. data/packages/igniter-app/lib/igniter/app/evolution/stores/file_store.rb +241 -0
  115. data/packages/igniter-app/lib/igniter/app/evolution/trail.rb +108 -0
  116. data/packages/igniter-app/lib/igniter/app/evolution.rb +11 -0
  117. data/packages/igniter-app/lib/igniter/app/filesystem_loader_adapter.rb +21 -0
  118. data/packages/igniter-app/lib/igniter/app/generator.rb +636 -0
  119. data/packages/igniter-app/lib/igniter/app/generators/cluster.rb +1367 -0
  120. data/packages/igniter-app/lib/igniter/app/generators/dashboard.rb +152 -0
  121. data/packages/igniter-app/lib/igniter/app/generators/playground.rb +1227 -0
  122. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/README.md.erb +37 -0
  123. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/app.rb.erb +19 -0
  124. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/contexts/home_context.rb.erb +54 -0
  125. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/frontend/application.js.erb +3 -0
  126. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/spec/dashboard_app_spec.rb.erb +79 -0
  127. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/support/stack_overview.rb.erb +23 -0
  128. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/handlers/home_handler.rb.erb +27 -0
  129. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/views/home_page.arb.erb +44 -0
  130. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/views/home_page.rb.erb +56 -0
  131. data/packages/igniter-app/lib/igniter/app/generators/templates/dashboard/web/views/layout.arb.erb +17 -0
  132. data/packages/igniter-app/lib/igniter/app/host_adapter.rb +26 -0
  133. data/packages/igniter-app/lib/igniter/app/host_config.rb +40 -0
  134. data/packages/igniter-app/lib/igniter/app/host_registry.rb +43 -0
  135. data/packages/igniter-app/lib/igniter/app/loader_adapter.rb +15 -0
  136. data/packages/igniter-app/lib/igniter/app/loader_pack.rb +8 -0
  137. data/packages/igniter-app/lib/igniter/app/loader_registry.rb +39 -0
  138. data/packages/igniter-app/lib/igniter/app/observability/operator_action_handler.rb +147 -0
  139. data/packages/igniter-app/lib/igniter/app/observability/operator_console_handler.rb +747 -0
  140. data/packages/igniter-app/lib/igniter/app/observability/operator_overview_handler.rb +350 -0
  141. data/packages/igniter-app/lib/igniter/app/observability.rb +5 -0
  142. data/packages/igniter-app/lib/igniter/app/observability_pack.rb +71 -0
  143. data/packages/igniter-app/lib/igniter/app/operator/dispatcher.rb +40 -0
  144. data/packages/igniter-app/lib/igniter/app/operator/handler_registry.rb +40 -0
  145. data/packages/igniter-app/lib/igniter/app/operator/handler_result.rb +67 -0
  146. data/packages/igniter-app/lib/igniter/app/operator/handlers/base.rb +79 -0
  147. data/packages/igniter-app/lib/igniter/app/operator/handlers/ignite_handler.rb +108 -0
  148. data/packages/igniter-app/lib/igniter/app/operator/handlers/orchestration_handler.rb +33 -0
  149. data/packages/igniter-app/lib/igniter/app/operator/handlers.rb +5 -0
  150. data/packages/igniter-app/lib/igniter/app/operator/lifecycle_contract.rb +55 -0
  151. data/packages/igniter-app/lib/igniter/app/operator/policy.rb +157 -0
  152. data/packages/igniter-app/lib/igniter/app/operator.rb +17 -0
  153. data/packages/igniter-app/lib/igniter/app/orchestration/action_result_builder.rb +65 -0
  154. data/packages/igniter-app/lib/igniter/app/orchestration/followup_request.rb +36 -0
  155. data/packages/igniter-app/lib/igniter/app/orchestration/handler_registry.rb +58 -0
  156. data/packages/igniter-app/lib/igniter/app/orchestration/handlers.rb +106 -0
  157. data/packages/igniter-app/lib/igniter/app/orchestration/inbox.rb +283 -0
  158. data/packages/igniter-app/lib/igniter/app/orchestration/inbox_query.rb +293 -0
  159. data/packages/igniter-app/lib/igniter/app/orchestration/lane_registry.rb +100 -0
  160. data/packages/igniter-app/lib/igniter/app/orchestration/operator_query.rb +449 -0
  161. data/packages/igniter-app/lib/igniter/app/orchestration/plan.rb +68 -0
  162. data/packages/igniter-app/lib/igniter/app/orchestration/planner.rb +89 -0
  163. data/packages/igniter-app/lib/igniter/app/orchestration/policies.rb +125 -0
  164. data/packages/igniter-app/lib/igniter/app/orchestration/policy_registry.rb +63 -0
  165. data/packages/igniter-app/lib/igniter/app/orchestration/result.rb +43 -0
  166. data/packages/igniter-app/lib/igniter/app/orchestration/routing_registry.rb +43 -0
  167. data/packages/igniter-app/lib/igniter/app/orchestration/runner.rb +50 -0
  168. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_event_query.rb +205 -0
  169. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_overview_builder.rb +286 -0
  170. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_query_overview_builder.rb +20 -0
  171. data/packages/igniter-app/lib/igniter/app/orchestration/runtime_result_builder.rb +23 -0
  172. data/packages/igniter-app/lib/igniter/app/orchestration.rb +113 -0
  173. data/packages/igniter-app/lib/igniter/app/runtime.rb +4 -0
  174. data/packages/igniter-app/lib/igniter/app/runtime_context.rb +101 -0
  175. data/packages/igniter-app/lib/igniter/app/runtime_pack.rb +16 -0
  176. data/packages/igniter-app/lib/igniter/app/scaffold_pack.rb +6 -0
  177. data/packages/igniter-app/lib/igniter/app/scheduler.rb +109 -0
  178. data/packages/igniter-app/lib/igniter/app/scheduler_adapter.rb +17 -0
  179. data/packages/igniter-app/lib/igniter/app/scheduler_pack.rb +8 -0
  180. data/packages/igniter-app/lib/igniter/app/scheduler_registry.rb +39 -0
  181. data/packages/igniter-app/lib/igniter/app/stack.rb +1726 -0
  182. data/packages/igniter-app/lib/igniter/app/stack_pack.rb +3 -0
  183. data/packages/igniter-app/lib/igniter/app/threaded_scheduler_adapter.rb +35 -0
  184. data/packages/igniter-app/lib/igniter/app/yml_loader.rb +43 -0
  185. data/packages/igniter-app/lib/igniter/app.rb +2367 -0
  186. data/packages/igniter-app/lib/igniter/ignite/bootstrap_agent.rb +334 -0
  187. data/packages/igniter-app/lib/igniter/ignite/bootstrap_target.rb +79 -0
  188. data/packages/igniter-app/lib/igniter/ignite/deployment_intent.rb +82 -0
  189. data/packages/igniter-app/lib/igniter/ignite/ignition_agent.rb +1011 -0
  190. data/packages/igniter-app/lib/igniter/ignite/ignition_plan.rb +83 -0
  191. data/packages/igniter-app/lib/igniter/ignite/ignition_report.rb +144 -0
  192. data/packages/igniter-app/lib/igniter/ignite/store.rb +19 -0
  193. data/packages/igniter-app/lib/igniter/ignite/stores/file_store.rb +112 -0
  194. data/packages/igniter-app/lib/igniter/ignite/trail.rb +215 -0
  195. data/packages/igniter-app/lib/igniter/ignite.rb +11 -0
  196. data/packages/igniter-app/lib/igniter-app.rb +5 -0
  197. data/packages/igniter-cluster/README.md +9 -0
  198. data/packages/igniter-cluster/lib/igniter/cluster/agent_route_resolver.rb +58 -0
  199. data/packages/igniter-cluster/lib/igniter/cluster/consensus/cluster.rb +187 -0
  200. data/packages/igniter-cluster/lib/igniter/cluster/consensus/errors.rb +16 -0
  201. data/packages/igniter-cluster/lib/igniter/cluster/consensus/executors.rb +45 -0
  202. data/packages/igniter-cluster/lib/igniter/cluster/consensus/node.rb +322 -0
  203. data/packages/igniter-cluster/lib/igniter/cluster/consensus/read_query.rb +32 -0
  204. data/packages/igniter-cluster/lib/igniter/cluster/consensus/state_machine.rb +60 -0
  205. data/packages/igniter-cluster/lib/igniter/cluster/consensus.rb +18 -0
  206. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics/governance_contributor.rb +90 -0
  207. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics/identity_contributor.rb +98 -0
  208. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics/routing_contributor.rb +674 -0
  209. data/packages/igniter-cluster/lib/igniter/cluster/diagnostics.rb +24 -0
  210. data/packages/igniter-cluster/lib/igniter/cluster/events/envelope.rb +136 -0
  211. data/packages/igniter-cluster/lib/igniter/cluster/events/hook_support.rb +33 -0
  212. data/packages/igniter-cluster/lib/igniter/cluster/events/log.rb +102 -0
  213. data/packages/igniter-cluster/lib/igniter/cluster/events/projection_feed.rb +98 -0
  214. data/packages/igniter-cluster/lib/igniter/cluster/events/read_model_projector.rb +32 -0
  215. data/packages/igniter-cluster/lib/igniter/cluster/events.rb +131 -0
  216. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_decision.rb +41 -0
  217. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_policy.rb +66 -0
  218. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_queue.rb +88 -0
  219. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_request.rb +62 -0
  220. data/packages/igniter-cluster/lib/igniter/cluster/governance/admission_workflow.rb +214 -0
  221. data/packages/igniter-cluster/lib/igniter/cluster/governance/checkpoint.rb +141 -0
  222. data/packages/igniter-cluster/lib/igniter/cluster/governance/compaction_record.rb +33 -0
  223. data/packages/igniter-cluster/lib/igniter/cluster/governance/stores/checkpoint_store.rb +89 -0
  224. data/packages/igniter-cluster/lib/igniter/cluster/governance/stores/file_store.rb +249 -0
  225. data/packages/igniter-cluster/lib/igniter/cluster/governance/trail.rb +164 -0
  226. data/packages/igniter-cluster/lib/igniter/cluster/governance.rb +12 -0
  227. data/packages/igniter-cluster/lib/igniter/cluster/identity/capability_attestation.rb +114 -0
  228. data/packages/igniter-cluster/lib/igniter/cluster/identity/manifest.rb +139 -0
  229. data/packages/igniter-cluster/lib/igniter/cluster/identity/node_identity.rb +106 -0
  230. data/packages/igniter-cluster/lib/igniter/cluster/identity.rb +5 -0
  231. data/packages/igniter-cluster/lib/igniter/cluster/mesh/announcer.rb +88 -0
  232. data/packages/igniter-cluster/lib/igniter/cluster/mesh/checkpoint_gossip.rb +60 -0
  233. data/packages/igniter-cluster/lib/igniter/cluster/mesh/config.rb +146 -0
  234. data/packages/igniter-cluster/lib/igniter/cluster/mesh/discovery.rb +44 -0
  235. data/packages/igniter-cluster/lib/igniter/cluster/mesh/errors.rb +36 -0
  236. data/packages/igniter-cluster/lib/igniter/cluster/mesh/gossip.rb +62 -0
  237. data/packages/igniter-cluster/lib/igniter/cluster/mesh/mesh_ql.rb +470 -0
  238. data/packages/igniter-cluster/lib/igniter/cluster/mesh/node_observation.rb +281 -0
  239. data/packages/igniter-cluster/lib/igniter/cluster/mesh/observation_query.rb +284 -0
  240. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer.rb +51 -0
  241. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_capacity_report.rb +42 -0
  242. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_identity_envelope.rb +158 -0
  243. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_metadata.rb +122 -0
  244. data/packages/igniter-cluster/lib/igniter/cluster/mesh/peer_registry.rb +81 -0
  245. data/packages/igniter-cluster/lib/igniter/cluster/mesh/placement_decision.rb +64 -0
  246. data/packages/igniter-cluster/lib/igniter/cluster/mesh/placement_planner.rb +154 -0
  247. data/packages/igniter-cluster/lib/igniter/cluster/mesh/placement_policy.rb +103 -0
  248. data/packages/igniter-cluster/lib/igniter/cluster/mesh/poller.rb +92 -0
  249. data/packages/igniter-cluster/lib/igniter/cluster/mesh/rebalance_plan.rb +66 -0
  250. data/packages/igniter-cluster/lib/igniter/cluster/mesh/rebalance_planner.rb +153 -0
  251. data/packages/igniter-cluster/lib/igniter/cluster/mesh/repair_loop.rb +169 -0
  252. data/packages/igniter-cluster/lib/igniter/cluster/mesh/router.rb +306 -0
  253. data/packages/igniter-cluster/lib/igniter/cluster/mesh/workload_signal.rb +46 -0
  254. data/packages/igniter-cluster/lib/igniter/cluster/mesh/workload_tracker.rb +215 -0
  255. data/packages/igniter-cluster/lib/igniter/cluster/mesh.rb +452 -0
  256. data/packages/igniter-cluster/lib/igniter/cluster/ownership/claim.rb +69 -0
  257. data/packages/igniter-cluster/lib/igniter/cluster/ownership/errors.rb +19 -0
  258. data/packages/igniter-cluster/lib/igniter/cluster/ownership/owner_client.rb +76 -0
  259. data/packages/igniter-cluster/lib/igniter/cluster/ownership/registry.rb +98 -0
  260. data/packages/igniter-cluster/lib/igniter/cluster/ownership/resolver.rb +62 -0
  261. data/packages/igniter-cluster/lib/igniter/cluster/ownership.rb +81 -0
  262. data/packages/igniter-cluster/lib/igniter/cluster/projection_store.rb +62 -0
  263. data/packages/igniter-cluster/lib/igniter/cluster/rag/chunk.rb +49 -0
  264. data/packages/igniter-cluster/lib/igniter/cluster/rag/fanout_retriever.rb +93 -0
  265. data/packages/igniter-cluster/lib/igniter/cluster/rag/knowledge_shard.rb +140 -0
  266. data/packages/igniter-cluster/lib/igniter/cluster/rag/net_http_adapter.rb +85 -0
  267. data/packages/igniter-cluster/lib/igniter/cluster/rag/ranker.rb +46 -0
  268. data/packages/igniter-cluster/lib/igniter/cluster/rag/retrieval_query.rb +30 -0
  269. data/packages/igniter-cluster/lib/igniter/cluster/rag/retrieval_result.rb +77 -0
  270. data/packages/igniter-cluster/lib/igniter/cluster/rag.rb +38 -0
  271. data/packages/igniter-cluster/lib/igniter/cluster/remote_adapter.rb +101 -0
  272. data/packages/igniter-cluster/lib/igniter/cluster/replication/bootstrapper.rb +63 -0
  273. data/packages/igniter-cluster/lib/igniter/cluster/replication/bootstrappers/gem.rb +39 -0
  274. data/packages/igniter-cluster/lib/igniter/cluster/replication/bootstrappers/git.rb +46 -0
  275. data/packages/igniter-cluster/lib/igniter/cluster/replication/bootstrappers/tarball.rb +62 -0
  276. data/packages/igniter-cluster/lib/igniter/cluster/replication/capability_query.rb +675 -0
  277. data/packages/igniter-cluster/lib/igniter/cluster/replication/expansion_plan.rb +40 -0
  278. data/packages/igniter-cluster/lib/igniter/cluster/replication/expansion_planner.rb +154 -0
  279. data/packages/igniter-cluster/lib/igniter/cluster/replication/manifest.rb +47 -0
  280. data/packages/igniter-cluster/lib/igniter/cluster/replication/network_topology.rb +150 -0
  281. data/packages/igniter-cluster/lib/igniter/cluster/replication/node_profile.rb +134 -0
  282. data/packages/igniter-cluster/lib/igniter/cluster/replication/reflective_replication_agent.rb +259 -0
  283. data/packages/igniter-cluster/lib/igniter/cluster/replication/replication_agent.rb +89 -0
  284. data/packages/igniter-cluster/lib/igniter/cluster/replication/ssh_session.rb +79 -0
  285. data/packages/igniter-cluster/lib/igniter/cluster/replication.rb +38 -0
  286. data/packages/igniter-cluster/lib/igniter/cluster/routed_agent_adapter.rb +79 -0
  287. data/packages/igniter-cluster/lib/igniter/cluster/routing_plan_executor.rb +427 -0
  288. data/packages/igniter-cluster/lib/igniter/cluster/routing_plan_result.rb +38 -0
  289. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_plan.rb +34 -0
  290. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_planner.rb +76 -0
  291. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_result.rb +34 -0
  292. data/packages/igniter-cluster/lib/igniter/cluster/trust/admission_runner.rb +125 -0
  293. data/packages/igniter-cluster/lib/igniter/cluster/trust/trust_assessment.rb +37 -0
  294. data/packages/igniter-cluster/lib/igniter/cluster/trust/trust_store.rb +58 -0
  295. data/packages/igniter-cluster/lib/igniter/cluster/trust/verifier.rb +80 -0
  296. data/packages/igniter-cluster/lib/igniter/cluster/trust.rb +9 -0
  297. data/packages/igniter-cluster/lib/igniter/cluster.rb +71 -0
  298. data/packages/igniter-cluster/lib/igniter-cluster.rb +3 -0
  299. data/packages/igniter-core/README.md +21 -0
  300. data/packages/igniter-core/lib/igniter/core/capabilities.rb +70 -0
  301. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/compiled_graph.rb +40 -2
  302. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validation_pipeline.rb +1 -0
  303. data/packages/igniter-core/lib/igniter/core/compiler/validators/agent_validator.rb +142 -0
  304. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/dependencies_validator.rb +90 -3
  305. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/remote_validator.rb +2 -0
  306. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler.rb +1 -0
  307. data/packages/igniter-core/lib/igniter/core/content_addressing.rb +133 -0
  308. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/contract.rb +88 -6
  309. data/packages/igniter-core/lib/igniter/core/dataflow/aggregate_operators.rb +147 -0
  310. data/packages/igniter-core/lib/igniter/core/dataflow/aggregate_state.rb +77 -0
  311. data/packages/igniter-core/lib/igniter/core/dataflow/diff.rb +37 -0
  312. data/packages/igniter-core/lib/igniter/core/dataflow/diff_state.rb +81 -0
  313. data/packages/igniter-core/lib/igniter/core/dataflow/incremental_collection_result.rb +39 -0
  314. data/packages/igniter-core/lib/igniter/core/dataflow/window_filter.rb +48 -0
  315. data/packages/igniter-core/lib/igniter/core/dataflow.rb +65 -0
  316. data/packages/igniter-core/lib/igniter/core/diagnostics/agent_contributor.rb +241 -0
  317. data/packages/igniter-core/lib/igniter/core/diagnostics/capability_contributor.rb +162 -0
  318. data/packages/igniter-core/lib/igniter/core/diagnostics/orchestration_contributor.rb +75 -0
  319. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/report.rb +81 -6
  320. data/packages/igniter-core/lib/igniter/core/diagnostics.rb +58 -0
  321. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dsl/contract_builder.rb +230 -9
  322. data/packages/igniter-core/lib/igniter/core/dto/record.rb +189 -0
  323. data/packages/igniter-core/lib/igniter/core/dto.rb +8 -0
  324. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/effect.rb +4 -0
  325. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/errors.rb +26 -3
  326. data/packages/igniter-core/lib/igniter/core/executor.rb +134 -0
  327. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/introspection/graph_formatter.rb +32 -1
  328. data/packages/igniter-core/lib/igniter/core/extensions/introspection/plan_formatter.rb +85 -0
  329. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/introspection/runtime_formatter.rb +26 -0
  330. data/packages/igniter-core/lib/igniter/core/extensions/invariants.rb +70 -0
  331. data/packages/igniter-core/lib/igniter/core/fingerprint.rb +43 -0
  332. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental.rb +4 -4
  333. data/packages/igniter-core/lib/igniter/core/memory/agent_memory.rb +104 -0
  334. data/packages/igniter-core/lib/igniter/core/memory/episode.rb +29 -0
  335. data/packages/igniter-core/lib/igniter/core/memory/fact.rb +27 -0
  336. data/packages/igniter-core/lib/igniter/core/memory/memorable.rb +90 -0
  337. data/packages/igniter-core/lib/igniter/core/memory/reflection_cycle.rb +96 -0
  338. data/packages/igniter-core/lib/igniter/core/memory/reflection_record.rb +28 -0
  339. data/packages/igniter-core/lib/igniter/core/memory/store.rb +115 -0
  340. data/packages/igniter-core/lib/igniter/core/memory/stores/in_memory.rb +136 -0
  341. data/packages/igniter-core/lib/igniter/core/memory/stores/sqlite.rb +286 -0
  342. data/packages/igniter-core/lib/igniter/core/memory.rb +80 -0
  343. data/packages/igniter-core/lib/igniter/core/metrics/collector.rb +131 -0
  344. data/packages/igniter-core/lib/igniter/core/metrics/prometheus_exporter.rb +104 -0
  345. data/packages/igniter-core/lib/igniter/core/metrics/snapshot.rb +8 -0
  346. data/packages/igniter-core/lib/igniter/core/metrics.rb +37 -0
  347. data/packages/igniter-core/lib/igniter/core/model/agent_interaction_contract.rb +172 -0
  348. data/packages/igniter-core/lib/igniter/core/model/agent_node.rb +86 -0
  349. data/packages/igniter-core/lib/igniter/core/model/aggregate_node.rb +34 -0
  350. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/branch_node.rb +37 -1
  351. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/collection_node.rb +3 -2
  352. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/compute_node.rb +13 -0
  353. data/packages/igniter-core/lib/igniter/core/model/remote_node.rb +91 -0
  354. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model.rb +2 -0
  355. data/packages/igniter-core/lib/igniter/core/node_cache.rb +231 -0
  356. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing.rb +8 -8
  357. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/builder.rb +30 -1
  358. data/packages/igniter-core/lib/igniter/core/runtime/agent_adapter.rb +41 -0
  359. data/packages/igniter-core/lib/igniter/core/runtime/agent_result_contract.rb +91 -0
  360. data/packages/igniter-core/lib/igniter/core/runtime/agent_route.rb +60 -0
  361. data/packages/igniter-core/lib/igniter/core/runtime/agent_route_resolver.rb +26 -0
  362. data/packages/igniter-core/lib/igniter/core/runtime/agent_session.rb +922 -0
  363. data/packages/igniter-core/lib/igniter/core/runtime/agent_session_query.rb +379 -0
  364. data/packages/igniter-core/lib/igniter/core/runtime/agent_transport.rb +30 -0
  365. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/cache.rb +6 -3
  366. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/deferred_result.rb +27 -1
  367. data/packages/igniter-core/lib/igniter/core/runtime/execution.rb +913 -0
  368. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/input_validator.rb +6 -2
  369. data/packages/igniter-core/lib/igniter/core/runtime/job_worker.rb +39 -0
  370. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/node_state.rb +4 -2
  371. data/packages/igniter-core/lib/igniter/core/runtime/orchestration_overview.rb +213 -0
  372. data/packages/igniter-core/lib/igniter/core/runtime/orchestration_runtime_state.rb +176 -0
  373. data/packages/igniter-core/lib/igniter/core/runtime/orchestration_transition_query.rb +208 -0
  374. data/packages/igniter-core/lib/igniter/core/runtime/planner.rb +301 -0
  375. data/packages/igniter-core/lib/igniter/core/runtime/proxy_agent_adapter.rb +124 -0
  376. data/packages/igniter-core/lib/igniter/core/runtime/remote_adapter.rb +26 -0
  377. data/packages/igniter-core/lib/igniter/core/runtime/resolver.rb +951 -0
  378. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/result.rb +2 -0
  379. data/packages/igniter-core/lib/igniter/core/runtime/stores/redis_store.rb +93 -0
  380. data/packages/igniter-core/lib/igniter/core/runtime/stores/sqlite_store.rb +155 -0
  381. data/packages/igniter-core/lib/igniter/core/runtime/stream_result.rb +171 -0
  382. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime.rb +15 -0
  383. data/packages/igniter-core/lib/igniter/core/temporal.rb +84 -0
  384. data/packages/igniter-core/lib/igniter/core/tool/discoverable.rb +151 -0
  385. data/packages/igniter-core/lib/igniter/core/tool.rb +56 -0
  386. data/{lib/igniter → packages/igniter-core/lib/igniter/core}/version.rb +1 -1
  387. data/packages/igniter-core/lib/igniter/core.rb +23 -0
  388. data/packages/igniter-core/lib/igniter-core.rb +3 -0
  389. data/packages/igniter-extensions/README.md +21 -0
  390. data/packages/igniter-extensions/lib/igniter/extensions/auditing.rb +3 -0
  391. data/packages/igniter-extensions/lib/igniter/extensions/capabilities.rb +39 -0
  392. data/packages/igniter-extensions/lib/igniter/extensions/content_addressing.rb +5 -0
  393. data/packages/igniter-extensions/lib/igniter/extensions/dataflow.rb +117 -0
  394. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/differential.rb +1 -1
  395. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/execution_report.rb +1 -1
  396. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/incremental.rb +1 -1
  397. data/packages/igniter-extensions/lib/igniter/extensions/introspection.rb +3 -0
  398. data/packages/igniter-extensions/lib/igniter/extensions/invariants.rb +3 -0
  399. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/provenance.rb +1 -1
  400. data/packages/igniter-extensions/lib/igniter/extensions/reactive.rb +3 -0
  401. data/{lib → packages/igniter-extensions/lib}/igniter/extensions/saga.rb +1 -1
  402. data/packages/igniter-extensions/lib/igniter/extensions.rb +8 -0
  403. data/packages/igniter-extensions/lib/igniter-extensions.rb +3 -0
  404. data/packages/igniter-frontend/README.md +224 -0
  405. data/packages/igniter-frontend/lib/igniter/frontend/app.rb +90 -0
  406. data/packages/igniter-frontend/lib/igniter/frontend/app_access.rb +36 -0
  407. data/packages/igniter-frontend/lib/igniter/frontend/arbre/component.rb +120 -0
  408. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/action_group.rb +53 -0
  409. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/badge.rb +91 -0
  410. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/boolean.rb +53 -0
  411. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/breadcrumbs.rb +71 -0
  412. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/card.rb +114 -0
  413. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/conversation_panel.rb +61 -0
  414. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/datetime.rb +42 -0
  415. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/display_value_support.rb +38 -0
  416. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/empty_state.rb +39 -0
  417. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/event_list.rb +44 -0
  418. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/filters.rb +183 -0
  419. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/indicator.rb +59 -0
  420. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/json_panel.rb +36 -0
  421. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/key_value_list.rb +40 -0
  422. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/loading_state.rb +43 -0
  423. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/metric_grid.rb +37 -0
  424. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/number.rb +53 -0
  425. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/page_header.rb +53 -0
  426. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/pagination.rb +143 -0
  427. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/panel.rb +67 -0
  428. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/percentage.rb +79 -0
  429. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/resource_list.rb +38 -0
  430. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/scenario_card.rb +48 -0
  431. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/shell_columns.rb +67 -0
  432. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/sidebar_shell.rb +106 -0
  433. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/table_with.rb +203 -0
  434. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/tabs.rb +147 -0
  435. data/packages/igniter-frontend/lib/igniter/frontend/arbre/components/viz.rb +185 -0
  436. data/packages/igniter-frontend/lib/igniter/frontend/arbre/page.rb +74 -0
  437. data/packages/igniter-frontend/lib/igniter/frontend/arbre/raw_text_node.rb +40 -0
  438. data/packages/igniter-frontend/lib/igniter/frontend/arbre/template_page.rb +243 -0
  439. data/packages/igniter-frontend/lib/igniter/frontend/arbre.rb +48 -0
  440. data/packages/igniter-frontend/lib/igniter/frontend/arbre_page.rb +7 -0
  441. data/packages/igniter-frontend/lib/igniter/frontend/assets.rb +101 -0
  442. data/packages/igniter-frontend/lib/igniter/frontend/builder.rb +124 -0
  443. data/packages/igniter-frontend/lib/igniter/frontend/component.rb +24 -0
  444. data/packages/igniter-frontend/lib/igniter/frontend/components.rb +7 -0
  445. data/packages/igniter-frontend/lib/igniter/frontend/context.rb +53 -0
  446. data/packages/igniter-frontend/lib/igniter/frontend/form_builder.rb +63 -0
  447. data/packages/igniter-frontend/lib/igniter/frontend/handler.rb +92 -0
  448. data/packages/igniter-frontend/lib/igniter/frontend/javascript.rb +353 -0
  449. data/packages/igniter-frontend/lib/igniter/frontend/page.rb +24 -0
  450. data/packages/igniter-frontend/lib/igniter/frontend/request.rb +61 -0
  451. data/packages/igniter-frontend/lib/igniter/frontend/response.rb +67 -0
  452. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/realtime/adapters.rb +226 -0
  453. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/realtime/presets.rb +147 -0
  454. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/realtime.rb +259 -0
  455. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/surfaces.rb +1074 -0
  456. data/packages/igniter-frontend/lib/igniter/frontend/tailwind/ui.rb +1438 -0
  457. data/packages/igniter-frontend/lib/igniter/frontend/tailwind.rb +180 -0
  458. data/packages/igniter-frontend/lib/igniter/frontend/version.rb +9 -0
  459. data/packages/igniter-frontend/lib/igniter/frontend.rb +35 -0
  460. data/packages/igniter-frontend/lib/igniter-frontend.rb +3 -0
  461. data/packages/igniter-rails/README.md +96 -0
  462. data/packages/igniter-rails/lib/igniter/plugins/rails/generators/contract/templates/contract.rb.tt +22 -0
  463. data/packages/igniter-rails/lib/igniter/plugins/rails/generators/install/templates/igniter.rb.tt +16 -0
  464. data/packages/igniter-rails/lib/igniter-rails.rb +3 -0
  465. data/packages/igniter-schema-rendering/README.md +27 -0
  466. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/page.rb +35 -0
  467. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/patcher.rb +47 -0
  468. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/renderer.rb +268 -0
  469. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/schema.rb +172 -0
  470. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/store.rb +53 -0
  471. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/submission_normalizer.rb +117 -0
  472. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/submission_processor.rb +91 -0
  473. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/submission_validator.rb +62 -0
  474. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering/version.rb +9 -0
  475. data/packages/igniter-schema-rendering/lib/igniter/schema_rendering.rb +20 -0
  476. data/packages/igniter-schema-rendering/lib/igniter-schema-rendering.rb +3 -0
  477. data/packages/igniter-sdk/README.md +25 -0
  478. data/packages/igniter-sdk/lib/igniter/sdk/channels/base.rb +84 -0
  479. data/packages/igniter-sdk/lib/igniter/sdk/channels/delivery_result.rb +61 -0
  480. data/packages/igniter-sdk/lib/igniter/sdk/channels/message.rb +101 -0
  481. data/packages/igniter-sdk/lib/igniter/sdk/channels/telegram.rb +161 -0
  482. data/packages/igniter-sdk/lib/igniter/sdk/channels/webhook.rb +213 -0
  483. data/packages/igniter-sdk/lib/igniter/sdk/channels.rb +17 -0
  484. data/packages/igniter-sdk/lib/igniter/sdk/data/store.rb +31 -0
  485. data/packages/igniter-sdk/lib/igniter/sdk/data/stores/file.rb +113 -0
  486. data/packages/igniter-sdk/lib/igniter/sdk/data/stores/in_memory.rb +63 -0
  487. data/packages/igniter-sdk/lib/igniter/sdk/data/stores/sqlite.rb +144 -0
  488. data/packages/igniter-sdk/lib/igniter/sdk/data.rb +34 -0
  489. data/packages/igniter-sdk/lib/igniter/sdk/tools/agent_bootstrap_tool.rb +151 -0
  490. data/packages/igniter-sdk/lib/igniter/sdk/tools/local_workflow_selector_tool.rb +269 -0
  491. data/packages/igniter-sdk/lib/igniter/sdk/tools/system_discovery_tool.rb +198 -0
  492. data/packages/igniter-sdk/lib/igniter/sdk/tools.rb +9 -0
  493. data/packages/igniter-sdk/lib/igniter/sdk.rb +86 -0
  494. data/packages/igniter-sdk/lib/igniter-sdk.rb +3 -0
  495. data/packages/igniter-server/README.md +9 -0
  496. data/packages/igniter-server/lib/igniter/server/agent_session_store.rb +98 -0
  497. data/packages/igniter-server/lib/igniter/server/agent_transport.rb +95 -0
  498. data/packages/igniter-server/lib/igniter/server/app_host.rb +3 -0
  499. data/packages/igniter-server/lib/igniter/server/client.rb +256 -0
  500. data/packages/igniter-server/lib/igniter/server/config.rb +70 -0
  501. data/packages/igniter-server/lib/igniter/server/handlers/agent_message_handler.rb +107 -0
  502. data/packages/igniter-server/lib/igniter/server/handlers/agent_session_handler.rb +125 -0
  503. data/{lib → packages/igniter-server/lib}/igniter/server/handlers/event_handler.rb +4 -0
  504. data/{lib → packages/igniter-server/lib}/igniter/server/handlers/execute_handler.rb +6 -0
  505. data/packages/igniter-server/lib/igniter/server/handlers/liveness_handler.rb +20 -0
  506. data/packages/igniter-server/lib/igniter/server/handlers/manifest_handler.rb +77 -0
  507. data/packages/igniter-server/lib/igniter/server/handlers/metrics_handler.rb +51 -0
  508. data/packages/igniter-server/lib/igniter/server/handlers/peers_handler.rb +136 -0
  509. data/packages/igniter-server/lib/igniter/server/handlers/readiness_handler.rb +47 -0
  510. data/packages/igniter-server/lib/igniter/server/http_server.rb +222 -0
  511. data/{lib → packages/igniter-server/lib}/igniter/server/rack_app.rb +27 -2
  512. data/packages/igniter-server/lib/igniter/server/remote_adapter.rb +27 -0
  513. data/packages/igniter-server/lib/igniter/server/router.rb +291 -0
  514. data/packages/igniter-server/lib/igniter/server/server_logger.rb +54 -0
  515. data/packages/igniter-server/lib/igniter/server.rb +130 -0
  516. data/packages/igniter-server/lib/igniter-server.rb +3 -0
  517. metadata +638 -168
  518. data/docs/API_V2.md +0 -537
  519. data/docs/ARCHITECTURE_V2.md +0 -317
  520. data/docs/BACKLOG.md +0 -166
  521. data/docs/BRANCHES_V1.md +0 -213
  522. data/docs/COLLECTIONS_V1.md +0 -303
  523. data/docs/DISTRIBUTED_CONTRACTS_V1.md +0 -493
  524. data/docs/EXECUTION_MODEL_V2.md +0 -324
  525. data/docs/IGNITER_CONCEPTS.md +0 -81
  526. data/docs/LLM_V1.md +0 -335
  527. data/docs/PATTERNS.md +0 -411
  528. data/docs/SERVER_V1.md +0 -313
  529. data/docs/STORE_ADAPTERS.md +0 -126
  530. data/lib/igniter/diagnostics.rb +0 -8
  531. data/lib/igniter/executor.rb +0 -74
  532. data/lib/igniter/extensions/introspection/plan_formatter.rb +0 -55
  533. data/lib/igniter/extensions/invariants.rb +0 -116
  534. data/lib/igniter/integrations/agents.rb +0 -18
  535. data/lib/igniter/integrations/llm/config.rb +0 -69
  536. data/lib/igniter/integrations/llm/executor.rb +0 -159
  537. data/lib/igniter/integrations/llm.rb +0 -59
  538. data/lib/igniter/model/remote_node.rb +0 -26
  539. data/lib/igniter/runtime/execution.rb +0 -398
  540. data/lib/igniter/runtime/job_worker.rb +0 -18
  541. data/lib/igniter/runtime/planner.rb +0 -126
  542. data/lib/igniter/runtime/resolver.rb +0 -520
  543. data/lib/igniter/runtime/stores/redis_store.rb +0 -56
  544. data/lib/igniter/server/client.rb +0 -123
  545. data/lib/igniter/server/config.rb +0 -27
  546. data/lib/igniter/server/http_server.rb +0 -109
  547. data/lib/igniter/server/router.rb +0 -75
  548. data/lib/igniter/server.rb +0 -67
  549. /data/{lib → packages/igniter-agents/lib}/igniter/agent/mailbox.rb +0 -0
  550. /data/{lib → packages/igniter-agents/lib}/igniter/agent/message.rb +0 -0
  551. /data/{lib → packages/igniter-agents/lib}/igniter/agent/state_holder.rb +0 -0
  552. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/graph_compiler.rb +0 -0
  553. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/type_resolver.rb +0 -0
  554. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validation_context.rb +0 -0
  555. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validator.rb +0 -0
  556. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/await_validator.rb +0 -0
  557. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/callable_validator.rb +0 -0
  558. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/outputs_validator.rb +0 -0
  559. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/type_compatibility_validator.rb +0 -0
  560. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/compiler/validators/uniqueness_validator.rb +0 -0
  561. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/auditing/report/console_formatter.rb +0 -0
  562. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/auditing/report/markdown_formatter.rb +0 -0
  563. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/introspection/formatters/mermaid_formatter.rb +0 -0
  564. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/diagnostics/introspection/formatters/text_tree_formatter.rb +0 -0
  565. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/divergence.rb +0 -0
  566. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/formatter.rb +0 -0
  567. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/report.rb +0 -0
  568. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential/runner.rb +0 -0
  569. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/differential.rb +0 -0
  570. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dsl/schema_builder.rb +0 -0
  571. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/dsl.rb +0 -0
  572. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/effect_registry.rb +0 -0
  573. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/events/bus.rb +0 -0
  574. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/events/event.rb +0 -0
  575. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/events.rb +0 -0
  576. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/builder.rb +0 -0
  577. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/formatter.rb +0 -0
  578. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/node_entry.rb +0 -0
  579. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report/report.rb +0 -0
  580. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/execution_report.rb +0 -0
  581. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/executor_registry.rb +0 -0
  582. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/auditing/timeline.rb +0 -0
  583. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/auditing.rb +0 -0
  584. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/introspection.rb +0 -0
  585. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive/engine.rb +0 -0
  586. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive/matcher.rb +0 -0
  587. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive/reaction.rb +0 -0
  588. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions/reactive.rb +0 -0
  589. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/extensions.rb +0 -0
  590. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental/formatter.rb +0 -0
  591. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental/result.rb +0 -0
  592. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/incremental/tracker.rb +0 -0
  593. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/invariant.rb +0 -0
  594. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/await_node.rb +0 -0
  595. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/composition_node.rb +0 -0
  596. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/effect_node.rb +0 -0
  597. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/graph.rb +0 -0
  598. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/input_node.rb +0 -0
  599. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/node.rb +0 -0
  600. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/model/output_node.rb +0 -0
  601. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/formatter.rb +0 -0
  602. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/generators.rb +0 -0
  603. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/result.rb +0 -0
  604. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/run.rb +0 -0
  605. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/property_testing/runner.rb +0 -0
  606. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/lineage.rb +0 -0
  607. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/node_trace.rb +0 -0
  608. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance/text_formatter.rb +0 -0
  609. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/provenance.rb +0 -0
  610. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/collection_result.rb +0 -0
  611. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/invalidator.rb +0 -0
  612. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runner_factory.rb +0 -0
  613. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runners/inline_runner.rb +0 -0
  614. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runners/store_runner.rb +0 -0
  615. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/runners/thread_pool_runner.rb +0 -0
  616. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/stores/active_record_store.rb +0 -0
  617. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/stores/file_store.rb +0 -0
  618. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/runtime/stores/memory_store.rb +0 -0
  619. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/compensation.rb +0 -0
  620. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/compensation_record.rb +0 -0
  621. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/executor.rb +0 -0
  622. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/formatter.rb +0 -0
  623. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga/result.rb +0 -0
  624. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/saga.rb +0 -0
  625. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/stream_loop.rb +0 -0
  626. /data/{lib/igniter → packages/igniter-core/lib/igniter/core}/type_system.rb +0 -0
  627. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/cable_adapter.rb +0 -0
  628. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/contract_job.rb +0 -0
  629. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/generators/contract/contract_generator.rb +0 -0
  630. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/generators/install/install_generator.rb +0 -0
  631. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/railtie.rb +0 -0
  632. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails/webhook_concern.rb +0 -0
  633. /data/{lib/igniter/integrations → packages/igniter-rails/lib/igniter/plugins}/rails.rb +0 -0
  634. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/base.rb +0 -0
  635. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/contracts_handler.rb +0 -0
  636. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/health_handler.rb +0 -0
  637. /data/{lib → packages/igniter-server/lib}/igniter/server/handlers/status_handler.rb +0 -0
  638. /data/{lib → packages/igniter-server/lib}/igniter/server/registry.rb +0 -0
@@ -0,0 +1,951 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Runtime
5
+ class Resolver
6
+ def initialize(execution)
7
+ @execution = execution
8
+ end
9
+
10
+ def resolve(node_name)
11
+ node = @execution.compiled_graph.fetch_node(node_name)
12
+ resolution_status, cached = @execution.cache.begin_resolution(node)
13
+ return cached if resolution_status == :cached
14
+
15
+ @execution.events.emit(:node_started, node: node, status: :running)
16
+
17
+ state = case node.kind
18
+ when :input
19
+ resolve_input(node)
20
+ when :compute
21
+ resolve_compute(node)
22
+ when :composition
23
+ resolve_composition(node)
24
+ when :branch
25
+ resolve_branch(node)
26
+ when :collection
27
+ resolve_collection(node)
28
+ when :agent
29
+ resolve_agent(node)
30
+ when :effect
31
+ resolve_effect(node)
32
+ when :await
33
+ resolve_await(node)
34
+ when :aggregate
35
+ resolve_aggregate(node)
36
+ when :remote
37
+ resolve_remote(node)
38
+ else
39
+ raise ResolutionError, "Unsupported node kind: #{node.kind}"
40
+ end
41
+
42
+ @execution.cache.write(state)
43
+ emit_resolution_event(node, state)
44
+ state
45
+ rescue PendingDependencyError => e
46
+ payload = e.deferred_result.payload
47
+ payload = payload.merge(routing_trace: e.explanation) if e.respond_to?(:explanation) && e.explanation
48
+
49
+ state = NodeState.new(
50
+ node: node,
51
+ status: :pending,
52
+ value: build_pending_value(
53
+ node,
54
+ token: e.deferred_result.token,
55
+ payload: payload,
56
+ source_node: e.deferred_result.source_node,
57
+ waiting_on: e.deferred_result.waiting_on || node.name
58
+ ),
59
+ details: pending_details(node, payload)
60
+ )
61
+ @execution.cache.write(state)
62
+ @execution.events.emit(:node_pending, node: node, status: :pending, payload: pending_payload(state))
63
+ state
64
+ rescue StandardError => e
65
+ state = NodeState.new(node: node, status: :failed, error: normalize_error(e, node))
66
+ @execution.cache.write(state)
67
+ @execution.events.emit(:node_failed, node: node, status: :failed, payload: failure_payload(state))
68
+ state
69
+ end
70
+
71
+ private
72
+
73
+ def resolve_input(node)
74
+ NodeState.new(node: node, status: :succeeded, value: @execution.fetch_input!(node.name))
75
+ end
76
+
77
+ def resolve_aggregate(node)
78
+ unless defined?(Igniter::Dataflow)
79
+ raise ResolutionError,
80
+ "Aggregate nodes require the dataflow extension. " \
81
+ "Add: require 'igniter/extensions/dataflow'"
82
+ end
83
+
84
+ collection_result = resolve_dependency_value(node.source_collection)
85
+ unless collection_result.respond_to?(:diff)
86
+ raise ResolutionError,
87
+ "Aggregate '#{node.name}' requires an incremental collection. " \
88
+ "Ensure '#{node.source_collection}' uses mode: :incremental"
89
+ end
90
+
91
+ agg_state = @execution.aggregate_state_for(node.name)
92
+ agg_state.apply_diff!(collection_result.diff, collection_result)
93
+
94
+ NodeState.new(node: node, status: :succeeded, value: agg_state.value)
95
+ end
96
+
97
+ def resolve_effect(node)
98
+ dependencies = node.dependencies.each_with_object({}) do |dep, memo|
99
+ memo[dep] = resolve_dependency_value(dep)
100
+ end
101
+
102
+ value = node.adapter_class.new(
103
+ execution: @execution,
104
+ contract: @execution.contract_instance
105
+ ).call(**dependencies)
106
+
107
+ NodeState.new(node: node, status: :succeeded, value: value)
108
+ end
109
+
110
+ def resolve_agent(node)
111
+ inputs = node.input_mapping.each_with_object({}) do |(message_input, dep_name), memo|
112
+ memo[message_input] = resolve_dependency_value(dep_name)
113
+ end
114
+
115
+ response =
116
+ if node.mode == :cast
117
+ @execution.agent_adapter.cast(
118
+ node: node,
119
+ inputs: inputs,
120
+ execution: @execution
121
+ )
122
+ else
123
+ @execution.agent_adapter.call(
124
+ node: node,
125
+ inputs: inputs,
126
+ execution: @execution
127
+ )
128
+ end
129
+
130
+ case response[:status]
131
+ when :succeeded
132
+ validate_agent_success_mode!(node)
133
+ NodeState.new(
134
+ node: node,
135
+ status: :succeeded,
136
+ value: response.fetch(:output, nil),
137
+ details: agent_details(response)
138
+ )
139
+ when :pending
140
+ validate_agent_pending_mode!(node)
141
+ payload = merge_agent_trace(response[:payload] || {}, response[:agent_trace])
142
+ deferred = normalize_agent_deferred(response, node, payload)
143
+ raise PendingDependencyError.new(deferred, response[:message] || "Agent node '#{node.name}' is pending")
144
+ when :failed
145
+ error_message = response.dig(:error, :message) || response.dig(:error, "message") || "agent call failed"
146
+ raise ResolutionError.new(
147
+ "Agent #{node.agent_name}: #{error_message}",
148
+ context: { agent_trace: response[:agent_trace] }.compact
149
+ )
150
+ else
151
+ raise ResolutionError.new(
152
+ "Agent #{node.agent_name}: unexpected status '#{response[:status]}'",
153
+ context: { agent_trace: response[:agent_trace] }.compact
154
+ )
155
+ end
156
+ end
157
+
158
+ def resolve_await(node)
159
+ deferred = Runtime::DeferredResult.build(
160
+ payload: { event: node.event_name },
161
+ source_node: node.name,
162
+ waiting_on: node.name
163
+ )
164
+ raise PendingDependencyError.new(deferred, "Waiting for external event '#{node.event_name}'")
165
+ end
166
+
167
+ def resolve_remote(node)
168
+ inputs = node.input_mapping.each_with_object({}) do |(child_input, dep_name), memo|
169
+ memo[child_input] = resolve_dependency_value(dep_name)
170
+ end
171
+
172
+ response = @execution.remote_adapter.call(
173
+ node: node,
174
+ inputs: inputs,
175
+ execution: @execution
176
+ )
177
+
178
+ case response[:status]
179
+ when :succeeded
180
+ NodeState.new(node: node, status: :succeeded, value: response[:outputs])
181
+ when :failed
182
+ error_message = response.dig(:error, :message) || response.dig(:error, "message")
183
+ raise ResolutionError, "Remote #{node.contract_name}: #{error_message}"
184
+ else
185
+ raise ResolutionError, "Remote #{node.contract_name}: unexpected status '#{response[:status]}'"
186
+ end
187
+ end
188
+
189
+ def resolve_compute(node) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
190
+ # Capability policy check — raises CapabilityViolationError if denied.
191
+ check_capability_policy!(node)
192
+
193
+ # Running state preserves dep_snapshot + value_version from the stale state.
194
+ # These are used for memoization (skip recompute) and value backdating.
195
+ running_state = @execution.cache.fetch(node.name)
196
+ old_dep_snapshot = running_state&.dep_snapshot
197
+ old_value = running_state&.value
198
+ old_value_version = running_state&.value_version || 0
199
+
200
+ # Resolve all dependencies (may recursively recompute upstream nodes).
201
+ dependencies = node.dependencies.each_with_object({}) do |dependency_name, memo|
202
+ memo[dependency_name] = resolve_dependency_value(dependency_name)
203
+ end
204
+
205
+ # Build snapshot of current dep value_versions (only regular nodes, not outputs).
206
+ current_dep_snapshot = build_dep_snapshot(node)
207
+
208
+ # Memoization: if all dep value_versions are unchanged, skip the compute entirely.
209
+ if old_dep_snapshot && old_value && old_value_version.positive? &&
210
+ dep_snapshot_match?(current_dep_snapshot, old_dep_snapshot)
211
+ @execution.events.emit(:node_skipped, node: node, status: :succeeded,
212
+ payload: { reason: :deps_unchanged })
213
+ return NodeState.new(node: node, status: :succeeded, value: old_value,
214
+ value_version: old_value_version,
215
+ dep_snapshot: current_dep_snapshot)
216
+ end
217
+
218
+ # Content-addressed cache: pure executor + same dep values → reuse across executions.
219
+ if (content_key = build_content_key(node, dependencies))
220
+ cached_value = Igniter::ContentAddressing.cache.fetch(content_key)
221
+ if cached_value
222
+ @execution.events.emit(:node_content_cache_hit, node: node, status: :succeeded,
223
+ payload: { key: content_key.to_s })
224
+ return NodeState.new(node: node, status: :succeeded, value: cached_value,
225
+ dep_snapshot: current_dep_snapshot)
226
+ end
227
+ end
228
+
229
+ # TTL cache: any compute node + same dep fingerprint → reuse across executions.
230
+ ttl_key = build_ttl_cache_key(node, dependencies)
231
+ is_coalescing_leader = false
232
+
233
+ if ttl_key
234
+ if (cached_value = Igniter::NodeCache.cache.fetch(ttl_key))
235
+ @execution.events.emit(:node_ttl_cache_hit, node: node, status: :succeeded,
236
+ payload: { key: ttl_key.to_s })
237
+ return NodeState.new(node: node, status: :succeeded, value: cached_value,
238
+ dep_snapshot: current_dep_snapshot)
239
+ end
240
+
241
+ # Coalescing: if another execution is already computing this node for the same
242
+ # inputs, join as a follower instead of duplicating the work.
243
+ if node.coalesce? && (lock = Igniter::NodeCache.coalescing_lock)
244
+ role, flight = lock.acquire(ttl_key.hex)
245
+ if role == :follower
246
+ coalesced_value, coalesced_error = lock.wait(flight)
247
+ raise coalesced_error if coalesced_error
248
+
249
+ # Follower timed out — coalesced_value is nil, fall through to compute independently
250
+ unless coalesced_value.nil? && coalesced_error.nil? && !flight.done
251
+ @execution.events.emit(:node_coalesced, node: node, status: :succeeded,
252
+ payload: { key: ttl_key.to_s })
253
+ return NodeState.new(node: node, status: :succeeded, value: coalesced_value,
254
+ dep_snapshot: current_dep_snapshot)
255
+ end
256
+ else
257
+ is_coalescing_leader = true
258
+ end
259
+ end
260
+ end
261
+
262
+ value = call_compute(node.callable, dependencies)
263
+ if deferred_result?(value)
264
+ return NodeState.new(node: node, status: :pending,
265
+ value: normalize_deferred_result(value, node))
266
+ end
267
+
268
+ value = normalize_guard_value(node, value)
269
+
270
+ # Value backdating: if the output is unchanged, preserve value_version so that
271
+ # downstream nodes whose dep_snapshots reference this node won't see it as changed.
272
+ if old_value && old_value_version.positive? && value == old_value
273
+ @execution.events.emit(:node_backdated, node: node, status: :succeeded,
274
+ payload: { reason: :value_unchanged })
275
+ store_ttl_result(ttl_key, value, node, is_coalescing_leader)
276
+ return NodeState.new(node: node, status: :succeeded, value: value,
277
+ value_version: old_value_version,
278
+ dep_snapshot: current_dep_snapshot)
279
+ end
280
+
281
+ # Store in content cache for future executions.
282
+ Igniter::ContentAddressing.cache.store(content_key, value) if content_key
283
+
284
+ # Store in TTL cache and notify any coalescing followers.
285
+ store_ttl_result(ttl_key, value, node, is_coalescing_leader)
286
+
287
+ NodeState.new(node: node, status: :succeeded, value: value,
288
+ dep_snapshot: current_dep_snapshot)
289
+ rescue StandardError => e
290
+ # If this execution was the coalescing leader, notify followers of the failure
291
+ # so they are unblocked (they will re-raise the error through their own path).
292
+ Igniter::NodeCache.coalescing_lock&.finish!(ttl_key&.hex, error: e) if is_coalescing_leader
293
+ raise
294
+ end
295
+
296
+ def call_compute(callable, dependencies)
297
+ case callable
298
+ when Proc
299
+ callable.call(**dependencies)
300
+ when Class
301
+ call_compute_class(callable, dependencies)
302
+ when Symbol, String
303
+ @execution.contract_instance.public_send(callable.to_sym, **dependencies)
304
+ else
305
+ call_compute_object(callable, dependencies)
306
+ end
307
+ end
308
+
309
+ def call_compute_class(callable, dependencies)
310
+ if callable <= Igniter::Executor
311
+ callable.new(execution: @execution, contract: @execution.contract_instance).call(**dependencies)
312
+ elsif callable.respond_to?(:call)
313
+ callable.call(**dependencies)
314
+ else
315
+ raise ResolutionError, "Unsupported callable: #{callable}"
316
+ end
317
+ end
318
+
319
+ def call_compute_object(callable, dependencies)
320
+ raise ResolutionError, "Unsupported callable: #{callable.class}" unless callable.respond_to?(:call)
321
+
322
+ callable.call(**dependencies)
323
+ end
324
+
325
+ def resolve_composition(node)
326
+ child_inputs = node.input_mapping.each_with_object({}) do |(child_input_name, dependency_name), memo|
327
+ memo[child_input_name] = resolve_dependency_value(dependency_name)
328
+ end
329
+
330
+ child_contract = node.contract_class.new(child_inputs)
331
+ child_contract.resolve_all
332
+ child_error = child_contract.result.errors.values.first
333
+ raise child_error if child_error
334
+
335
+ NodeState.new(node: node, status: :succeeded, value: child_contract.result)
336
+ end
337
+
338
+ def resolve_branch(node)
339
+ selector_value = resolve_dependency_value(node.selector_dependency)
340
+ selected_case = node.match_case(selector_value)
341
+ selected_contract = selected_case ? selected_case[:contract] : node.default_contract
342
+ matched_case = selected_case ? node.case_payload(selected_case) : :default
343
+
344
+ raise BranchSelectionError, "Branch '#{node.name}' has no matching case and no default" unless selected_contract
345
+
346
+ context_values = node.context_dependencies.each_with_object({}) do |dependency_name, memo|
347
+ memo[dependency_name] = resolve_dependency_value(dependency_name)
348
+ end
349
+
350
+ child_inputs = if node.input_mapper?
351
+ map_branch_inputs(node, selector_value, context_values)
352
+ else
353
+ node.input_mapping.each_with_object({}) do |(child_input_name, dependency_name), memo|
354
+ memo[child_input_name] = resolve_dependency_value(dependency_name)
355
+ end
356
+ end
357
+
358
+ @execution.events.emit(
359
+ :branch_selected,
360
+ node: node,
361
+ status: :succeeded,
362
+ payload: {
363
+ selector: node.selector_dependency,
364
+ selector_value: selector_value,
365
+ matcher: selected_case ? selected_case[:matcher] : :default,
366
+ matched_case: matched_case,
367
+ selected_contract: selected_contract.name || "AnonymousContract"
368
+ }
369
+ )
370
+
371
+ child_contract = selected_contract.new(child_inputs)
372
+ child_contract.resolve_all
373
+ child_error = child_contract.result.errors.values.first
374
+ raise child_error if child_error
375
+
376
+ NodeState.new(node: node, status: :succeeded, value: child_contract.result)
377
+ end
378
+
379
+ def map_branch_inputs(node, selector_value, context_values)
380
+ mapper = node.input_mapper
381
+
382
+ if mapper.is_a?(Symbol) || mapper.is_a?(String)
383
+ return @execution.contract_instance.public_send(mapper, selector: selector_value, **context_values)
384
+ end
385
+
386
+ mapper.call(selector: selector_value, **context_values)
387
+ end
388
+
389
+ def resolve_collection(node)
390
+ return resolve_incremental_collection(node) if node.mode == :incremental
391
+
392
+ items = resolve_dependency_value(node.source_dependency)
393
+ context_values = node.context_dependencies.each_with_object({}) do |dependency_name, memo|
394
+ memo[dependency_name] = resolve_dependency_value(dependency_name)
395
+ end
396
+ normalized_items = normalize_collection_items(node, items, context_values)
397
+ collection_items = {}
398
+
399
+ normalized_items.each do |item_inputs|
400
+ item_key = extract_collection_key(node, item_inputs)
401
+ emit_collection_item_event(:collection_item_started, node, item_key, item_inputs: item_inputs)
402
+ child_contract = node.contract_class.new(item_inputs)
403
+ begin
404
+ child_contract.resolve_all
405
+ rescue Igniter::Error
406
+ nil
407
+ end
408
+ child_error = child_contract.execution.cache.values.find(&:failed?)&.error
409
+
410
+ if child_error
411
+ collection_items[item_key] = Runtime::CollectionResult::Item.new(
412
+ key: item_key,
413
+ status: :failed,
414
+ error: child_error
415
+ )
416
+ emit_collection_item_event(
417
+ :collection_item_failed,
418
+ node,
419
+ item_key,
420
+ error: child_error.message,
421
+ error_type: child_error.class.name,
422
+ child_execution_id: child_contract.execution.events.execution_id
423
+ )
424
+ raise child_error if node.mode == :fail_fast
425
+ else
426
+ collection_items[item_key] = Runtime::CollectionResult::Item.new(
427
+ key: item_key,
428
+ status: :succeeded,
429
+ result: child_contract.result
430
+ )
431
+ emit_collection_item_event(
432
+ :collection_item_succeeded,
433
+ node,
434
+ item_key,
435
+ child_execution_id: child_contract.execution.events.execution_id
436
+ )
437
+ end
438
+ end
439
+
440
+ NodeState.new(
441
+ node: node,
442
+ status: :succeeded,
443
+ value: Runtime::CollectionResult.new(items: collection_items, mode: node.mode)
444
+ )
445
+ end
446
+
447
+ def resolve_incremental_collection(node) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
448
+ unless defined?(Igniter::Dataflow)
449
+ raise ResolutionError.new(
450
+ "Collection '#{node.name}' uses mode: :incremental — " \
451
+ "add `require 'igniter/extensions/dataflow'` to activate it",
452
+ context: collection_context(node)
453
+ )
454
+ end
455
+
456
+ items = resolve_dependency_value(node.source_dependency)
457
+ context_values = node.context_dependencies.each_with_object({}) do |dep_name, memo|
458
+ memo[dep_name] = resolve_dependency_value(dep_name)
459
+ end
460
+
461
+ normalized_items = normalize_collection_items(node, items, context_values)
462
+ normalized_items = Igniter::Dataflow::WindowFilter.new(node.window).apply(normalized_items) if node.window
463
+
464
+ diff_state = @execution.diff_state_for(node.name)
465
+ key_fn = ->(item) { extract_collection_key(node, item) }
466
+ diff = diff_state.compute_diff(normalized_items, key_fn)
467
+
468
+ collection_items = {}
469
+
470
+ # Reuse cached results for unchanged items (no child contract re-run)
471
+ diff.unchanged.each do |key|
472
+ cached = diff_state.cached_item_for(key)
473
+ collection_items[key] = cached if cached
474
+ emit_collection_item_event(:collection_item_reused, node, key)
475
+ end
476
+
477
+ # Retract removed items from the diff state
478
+ diff.removed.each { |key| diff_state.retract!(key) }
479
+
480
+ # Run child contracts only for added + changed items
481
+ to_process = normalized_items.select { |item| diff.added.include?(key_fn.call(item)) || diff.changed.include?(key_fn.call(item)) }
482
+
483
+ to_process.each do |item_inputs|
484
+ item_key = key_fn.call(item_inputs)
485
+ emit_collection_item_event(:collection_item_started, node, item_key, item_inputs: item_inputs)
486
+ child_contract = node.contract_class.new(item_inputs)
487
+ begin
488
+ child_contract.resolve_all
489
+ rescue Igniter::Error
490
+ nil
491
+ end
492
+ child_error = child_contract.execution.cache.values.find(&:failed?)&.error
493
+
494
+ result_item = if child_error
495
+ emit_collection_item_event(:collection_item_failed, node, item_key, error: child_error.message, error_type: child_error.class.name, child_execution_id: child_contract.execution.events.execution_id)
496
+ Runtime::CollectionResult::Item.new(key: item_key, status: :failed, error: child_error)
497
+ else
498
+ emit_collection_item_event(:collection_item_succeeded, node, item_key, child_execution_id: child_contract.execution.events.execution_id)
499
+ Runtime::CollectionResult::Item.new(key: item_key, status: :succeeded, result: child_contract.result)
500
+ end
501
+
502
+ collection_items[item_key] = result_item
503
+ diff_state.update!(item_key, item_inputs, result_item)
504
+ end
505
+
506
+ # Preserve the input array ordering in the result
507
+ ordered_items = normalized_items.each_with_object({}) do |item_inputs, memo|
508
+ key = key_fn.call(item_inputs)
509
+ memo[key] = collection_items[key] if collection_items.key?(key)
510
+ end
511
+
512
+ NodeState.new(
513
+ node: node,
514
+ status: :succeeded,
515
+ value: Igniter::Dataflow::IncrementalCollectionResult.new(items: ordered_items, diff: diff)
516
+ )
517
+ end
518
+
519
+ def resolve_dependency_value(dependency_name)
520
+ if @execution.compiled_graph.node?(dependency_name)
521
+ dependency_state = resolve(dependency_name)
522
+ raise dependency_state.error if dependency_state.failed?
523
+
524
+ if dependency_state.pending?
525
+ raise PendingDependencyError.new(dependency_state.value,
526
+ context: pending_context(dependency_state.node))
527
+ end
528
+
529
+ dependency_state.value
530
+ elsif @execution.compiled_graph.outputs_by_name.key?(dependency_name.to_sym)
531
+ output = @execution.compiled_graph.fetch_output(dependency_name)
532
+ value = @execution.send(:resolve_exported_output, output)
533
+ raise PendingDependencyError, value if deferred_result?(value)
534
+
535
+ value
536
+ else
537
+ raise ResolutionError, "Unknown dependency: #{dependency_name}"
538
+ end
539
+ end
540
+
541
+ def deferred_result?(value)
542
+ value.is_a?(Runtime::DeferredResult)
543
+ end
544
+
545
+ def normalize_deferred_result(value, node)
546
+ build_pending_value(
547
+ node,
548
+ token: value.token,
549
+ payload: value.payload,
550
+ source_node: value.source_node || node.name,
551
+ waiting_on: value.waiting_on
552
+ )
553
+ end
554
+
555
+ def emit_resolution_event(node, state)
556
+ event_type =
557
+ if state.failed?
558
+ :node_failed
559
+ elsif state.pending?
560
+ :node_pending
561
+ else
562
+ :node_succeeded
563
+ end
564
+
565
+ payload = state.pending? ? pending_payload(state) : success_payload(node, state)
566
+ @execution.events.emit(event_type, node: node, status: state.status, payload: payload)
567
+ end
568
+
569
+ def emit_collection_item_event(type, node, item_key, payload = {})
570
+ @execution.events.emit(
571
+ type,
572
+ node: node,
573
+ payload: payload.merge(item_key: item_key)
574
+ )
575
+ end
576
+
577
+ def pending_payload(state)
578
+ return {} unless state.value.is_a?(Runtime::DeferredResult)
579
+
580
+ state.value.to_h
581
+ end
582
+
583
+ def pending_details(node, payload)
584
+ return {} unless node.kind == :agent
585
+
586
+ agent_trace = payload[:agent_trace] || payload["agent_trace"]
587
+ agent_session = payload[:agent_session] || payload["agent_session"]
588
+ {
589
+ agent_trace: agent_trace,
590
+ agent_session: agent_session
591
+ }.compact
592
+ end
593
+
594
+ def pending_context(node)
595
+ {
596
+ graph: @execution.compiled_graph.name,
597
+ node_id: node.id,
598
+ node_name: node.name,
599
+ node_path: node.path,
600
+ source_location: node.source_location
601
+ }
602
+ end
603
+
604
+ def success_payload(node, state)
605
+ payload = {}
606
+
607
+ if %i[composition branch].include?(node.kind) && state.value.is_a?(Igniter::Runtime::Result)
608
+ payload[:child_execution_id] = state.value.execution.events.execution_id
609
+ payload[:child_graph] = state.value.execution.compiled_graph.name
610
+ end
611
+
612
+ if node.kind == :agent && state.details[:agent_trace]
613
+ payload[:agent_trace] = state.details[:agent_trace]
614
+ end
615
+
616
+ payload
617
+ end
618
+
619
+ def failure_payload(state)
620
+ payload = { error: state.error.message }
621
+ context = state.error.respond_to?(:context) ? state.error.context : {}
622
+ payload[:routing_trace] = context[:routing_trace] if context[:routing_trace]
623
+ payload[:agent_trace] = context[:agent_trace] if context[:agent_trace]
624
+ payload
625
+ end
626
+
627
+ def agent_details(response)
628
+ trace = response[:agent_trace]
629
+ session = response[:agent_session] || response[:session]
630
+ {
631
+ agent_trace: trace,
632
+ agent_session: session
633
+ }.compact
634
+ end
635
+
636
+ def merge_agent_trace(payload, agent_trace)
637
+ return payload unless agent_trace
638
+
639
+ payload.merge(agent_trace: agent_trace)
640
+ end
641
+
642
+ def merge_agent_session(payload, agent_session)
643
+ return payload unless agent_session
644
+
645
+ payload.merge(agent_session: agent_session)
646
+ end
647
+
648
+ def normalize_agent_session(response, node, payload, token:)
649
+ raw_session = response[:agent_session] || response[:session]
650
+ default_session = Runtime::AgentSession.new(
651
+ token: token,
652
+ node_name: node.name,
653
+ node_path: node.path,
654
+ agent_name: node.agent_name,
655
+ message_name: node.message_name,
656
+ mode: node.mode,
657
+ reply_mode: node.reply_mode,
658
+ finalizer: node.finalizer,
659
+ tool_loop_policy: node.tool_loop_policy,
660
+ session_policy: node.session_policy,
661
+ node_url: node.node_url,
662
+ capability: node.capability,
663
+ capability_query: node.capability_query,
664
+ pinned_to: node.pinned_to,
665
+ waiting_on: node.name,
666
+ source_node: node.name,
667
+ trace: response[:agent_trace],
668
+ payload: session_payload_from(payload),
669
+ turn: 1,
670
+ phase: default_agent_session_phase(node),
671
+ history: [
672
+ {
673
+ turn: 1,
674
+ event: :opened,
675
+ token: token,
676
+ waiting_on: node.name,
677
+ payload: session_payload_from(payload),
678
+ phase: default_agent_session_phase(node)
679
+ }
680
+ ]
681
+ )
682
+
683
+ if raw_session.respond_to?(:to_h) || raw_session.is_a?(Hash)
684
+ session_hash = raw_session.respond_to?(:to_h) ? raw_session.to_h : raw_session
685
+ merged = default_session.to_h.merge(session_hash.transform_keys(&:to_sym))
686
+ return Runtime::AgentSession.from_h(merged).to_h
687
+ end
688
+
689
+ default_session.to_h
690
+ end
691
+
692
+ def normalize_agent_deferred(response, node, payload)
693
+ base_deferred = response[:deferred_result]
694
+ token = base_deferred&.token
695
+ source_node = base_deferred&.source_node || node.name
696
+ waiting_on = base_deferred&.waiting_on || node.name
697
+ base_payload = base_deferred&.payload || payload
698
+ merged_payload = merge_agent_trace(base_payload, response[:agent_trace])
699
+
700
+ deferred = build_pending_value(
701
+ node,
702
+ token: token,
703
+ payload: merged_payload,
704
+ source_node: source_node,
705
+ waiting_on: waiting_on
706
+ )
707
+
708
+ session = normalize_agent_session(response, node, deferred.payload, token: deferred.token)
709
+ final_payload = merge_agent_session(deferred.payload, session)
710
+
711
+ build_pending_value(
712
+ node,
713
+ token: deferred.token,
714
+ payload: final_payload,
715
+ source_node: deferred.source_node,
716
+ waiting_on: deferred.waiting_on
717
+ )
718
+ end
719
+
720
+ def session_payload_from(payload)
721
+ return {} unless payload.is_a?(Hash)
722
+
723
+ payload.each_with_object({}) do |(key, value), memo|
724
+ next if %i[agent_trace agent_session].include?(key.to_sym)
725
+
726
+ memo[key] = value
727
+ end
728
+ end
729
+
730
+ def validate_agent_pending_mode!(node)
731
+ return unless node.reply_mode == :single
732
+
733
+ raise ResolutionError.new(
734
+ "Agent #{node.agent_name}: reply mode :single cannot return pending. Use reply: :deferred or :stream.",
735
+ context: {
736
+ graph: @execution.compiled_graph.name,
737
+ node_name: node.name,
738
+ node_path: node.path,
739
+ agent_reply_mode: node.reply_mode
740
+ }
741
+ )
742
+ end
743
+
744
+ def validate_agent_success_mode!(node)
745
+ return unless node.reply_mode == :stream
746
+
747
+ raise ResolutionError.new(
748
+ "Agent #{node.agent_name}: reply mode :stream requires session-based pending delivery before completion.",
749
+ context: {
750
+ graph: @execution.compiled_graph.name,
751
+ node_name: node.name,
752
+ node_path: node.path,
753
+ agent_reply_mode: node.reply_mode
754
+ }
755
+ )
756
+ end
757
+
758
+ def default_agent_session_phase(node)
759
+ node.reply_mode == :stream ? :streaming : :waiting
760
+ end
761
+
762
+ def build_pending_value(node, token:, payload:, source_node:, waiting_on:)
763
+ result_class = node.kind == :agent && node.reply_mode == :stream ? Runtime::StreamResult : Runtime::DeferredResult
764
+
765
+ result_class.build(
766
+ token: token,
767
+ payload: payload,
768
+ source_node: source_node,
769
+ waiting_on: waiting_on
770
+ )
771
+ end
772
+
773
+ def normalize_error(error, node)
774
+ # Trust any Igniter::Error that already carries node context.
775
+ return error if error.is_a?(Igniter::Error) && error.node_name
776
+
777
+ # Domain-specific subclasses (IncidentError, DeferredCapabilityError,
778
+ # InvariantError, …) carry semantics the caller depends on — preserve
779
+ # their type unchanged. Only bare Igniter::ResolutionError instances
780
+ # (raised with just a message inside an executor) get enriched.
781
+ return error if error.is_a?(Igniter::Error) && !error.instance_of?(Igniter::ResolutionError)
782
+
783
+ node_context = {
784
+ graph: @execution.compiled_graph.name,
785
+ node_id: node.id,
786
+ node_name: node.name,
787
+ node_path: node.path,
788
+ source_location: node.source_location,
789
+ execution_id: @execution.events.execution_id
790
+ }
791
+
792
+ existing_context = error.respond_to?(:context) ? error.context : {}
793
+
794
+ ResolutionError.new(error.message, context: node_context.merge(existing_context))
795
+ end
796
+
797
+ # ─── Capabilities ──────────────────────────────────────────────────────────
798
+
799
+ def check_capability_policy!(node)
800
+ return unless defined?(Igniter::Capabilities) && Igniter::Capabilities.policy
801
+ return unless node.callable.is_a?(Class) && node.callable <= Igniter::Executor
802
+
803
+ Igniter::Capabilities.policy.check!(node.name, node.callable)
804
+ end
805
+
806
+ # ─── Content addressing ────────────────────────────────────────────────────
807
+
808
+ # Returns a ContentKey for pure executors when content addressing is loaded.
809
+ # Returns nil for non-pure executors, Procs, or when the extension is absent.
810
+ def build_content_key(node, dep_values)
811
+ return unless defined?(Igniter::ContentAddressing)
812
+ return unless node.callable.is_a?(Class) && node.callable <= Igniter::Executor
813
+ return unless node.callable.pure?
814
+
815
+ Igniter::ContentAddressing::ContentKey.compute(node.callable, dep_values)
816
+ end
817
+
818
+ # ─── TTL cache ─────────────────────────────────────────────────────────────
819
+
820
+ # Returns a NodeCache::CacheKey when TTL caching is active for this node.
821
+ # Returns nil when NodeCache is not loaded, no backend is configured,
822
+ # or the node has no cache_ttl declared.
823
+ def build_ttl_cache_key(node, dep_values)
824
+ return unless defined?(Igniter::NodeCache)
825
+ return unless Igniter::NodeCache.cache
826
+ return unless node.respond_to?(:cache_ttl) && node.cache_ttl
827
+
828
+ dep_hex = Igniter::NodeCache::Fingerprinter.call(dep_values)
829
+ Igniter::NodeCache::CacheKey.new(
830
+ @execution.compiled_graph.name,
831
+ node.name,
832
+ dep_hex
833
+ )
834
+ end
835
+
836
+ # Stores a computed value in the TTL cache and signals any coalescing followers.
837
+ def store_ttl_result(ttl_key, value, node, is_leader)
838
+ return unless ttl_key
839
+
840
+ Igniter::NodeCache.cache.store(ttl_key, value, ttl: node.cache_ttl)
841
+ Igniter::NodeCache.coalescing_lock&.finish!(ttl_key.hex, value: value) if is_leader
842
+ end
843
+
844
+ def build_dep_snapshot(node)
845
+ node.dependencies.each_with_object({}) do |dep_name, memo|
846
+ next unless @execution.compiled_graph.node?(dep_name)
847
+
848
+ dep_state = @execution.cache.fetch(dep_name.to_sym)
849
+ memo[dep_name] = dep_state&.value_version
850
+ end
851
+ end
852
+
853
+ def dep_snapshot_match?(current, old)
854
+ return false if current.size != old.size
855
+
856
+ current.all? { |name, vv| old[name] == vv }
857
+ end
858
+
859
+ def normalize_guard_value(node, value)
860
+ return value unless node.respond_to?(:guard?) && node.guard?
861
+ return true if value
862
+
863
+ raise ResolutionError.new(
864
+ node.metadata[:guard_message] || "Guard '#{node.name}' failed",
865
+ context: {
866
+ graph: @execution.compiled_graph.name,
867
+ node_id: node.id,
868
+ node_name: node.name,
869
+ node_path: node.path,
870
+ source_location: node.source_location
871
+ }
872
+ )
873
+ end
874
+
875
+ def normalize_collection_items(node, items, context_values = {})
876
+ items = items.to_a if node.input_mapper? && items.is_a?(Hash)
877
+
878
+ unless items.is_a?(Array)
879
+ raise CollectionInputError.new(
880
+ "Collection '#{node.name}' expects an array, got #{items.class}",
881
+ context: collection_context(node)
882
+ )
883
+ end
884
+
885
+ mapped_items = if node.input_mapper?
886
+ items.map { |item| map_collection_item_inputs(node, item, context_values) }
887
+ else
888
+ items
889
+ end
890
+
891
+ mapped_items.each do |item|
892
+ next if item.is_a?(Hash)
893
+
894
+ raise CollectionInputError.new(
895
+ "Collection '#{node.name}' expects item hashes, got #{item.class}",
896
+ context: collection_context(node)
897
+ )
898
+ end
899
+
900
+ ensure_unique_collection_keys!(node, mapped_items)
901
+ mapped_items.map { |item| item.transform_keys(&:to_sym) }
902
+ end
903
+
904
+ def map_collection_item_inputs(node, item, context_values)
905
+ mapper = node.input_mapper
906
+
907
+ if mapper.is_a?(Symbol) || mapper.is_a?(String)
908
+ return @execution.contract_instance.public_send(mapper, item: item, **context_values)
909
+ end
910
+
911
+ mapper.call(item: item, **context_values)
912
+ end
913
+
914
+ def extract_collection_key(node, item_inputs)
915
+ item_inputs.fetch(node.key_name)
916
+ rescue KeyError
917
+ raise CollectionKeyError.new(
918
+ "Collection '#{node.name}' item is missing key '#{node.key_name}'",
919
+ context: collection_context(node)
920
+ )
921
+ end
922
+
923
+ def ensure_unique_collection_keys!(node, items)
924
+ keys = items.map do |item|
925
+ item.fetch(node.key_name) do
926
+ raise CollectionKeyError.new("Collection '#{node.name}' item is missing key '#{node.key_name}'",
927
+ context: collection_context(node))
928
+ end
929
+ end
930
+
931
+ duplicates = keys.group_by(&:itself).select { |_key, entries| entries.size > 1 }.keys
932
+ return if duplicates.empty?
933
+
934
+ raise CollectionKeyError.new(
935
+ "Collection '#{node.name}' has duplicate keys: #{duplicates.join(", ")}",
936
+ context: collection_context(node)
937
+ )
938
+ end
939
+
940
+ def collection_context(node)
941
+ {
942
+ graph: @execution.compiled_graph.name,
943
+ node_id: node.id,
944
+ node_name: node.name,
945
+ node_path: node.path,
946
+ source_location: node.source_location
947
+ }
948
+ end
949
+ end
950
+ end
951
+ end