igniter 0.4.5 → 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 (154) 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/llm_tools.rb +237 -0
  22. data/examples/mesh.rb +239 -0
  23. data/examples/mesh_discovery.rb +267 -0
  24. data/examples/mesh_gossip.rb +162 -0
  25. data/examples/ringcentral_routing.rb +1 -1
  26. data/lib/igniter/agents/ai/alert_agent.rb +111 -0
  27. data/lib/igniter/agents/ai/chain_agent.rb +127 -0
  28. data/lib/igniter/agents/ai/critic_agent.rb +163 -0
  29. data/lib/igniter/agents/ai/evaluator_agent.rb +193 -0
  30. data/lib/igniter/agents/ai/evolution_agent.rb +286 -0
  31. data/lib/igniter/agents/ai/health_check_agent.rb +122 -0
  32. data/lib/igniter/agents/ai/observer_agent.rb +184 -0
  33. data/lib/igniter/agents/ai/planner_agent.rb +210 -0
  34. data/lib/igniter/agents/ai/router_agent.rb +131 -0
  35. data/lib/igniter/agents/ai/self_reflection_agent.rb +175 -0
  36. data/lib/igniter/agents/observability/metrics_agent.rb +130 -0
  37. data/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
  38. data/lib/igniter/agents/proactive_agent.rb +208 -0
  39. data/lib/igniter/agents/reliability/retry_agent.rb +99 -0
  40. data/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
  41. data/lib/igniter/agents.rb +56 -0
  42. data/lib/igniter/application/app_config.rb +32 -0
  43. data/lib/igniter/application/autoloader.rb +18 -0
  44. data/lib/igniter/application/generator.rb +157 -0
  45. data/lib/igniter/application/scheduler.rb +109 -0
  46. data/lib/igniter/application/yml_loader.rb +39 -0
  47. data/lib/igniter/application.rb +174 -0
  48. data/lib/igniter/capabilities.rb +68 -0
  49. data/lib/igniter/compiler/validators/dependencies_validator.rb +50 -2
  50. data/lib/igniter/compiler/validators/remote_validator.rb +2 -0
  51. data/lib/igniter/consensus/cluster.rb +183 -0
  52. data/lib/igniter/consensus/errors.rb +14 -0
  53. data/lib/igniter/consensus/executors.rb +43 -0
  54. data/lib/igniter/consensus/node.rb +320 -0
  55. data/lib/igniter/consensus/read_query.rb +30 -0
  56. data/lib/igniter/consensus/state_machine.rb +58 -0
  57. data/lib/igniter/consensus.rb +58 -0
  58. data/lib/igniter/content_addressing.rb +133 -0
  59. data/lib/igniter/contract.rb +12 -0
  60. data/lib/igniter/dataflow/aggregate_operators.rb +147 -0
  61. data/lib/igniter/dataflow/aggregate_state.rb +77 -0
  62. data/lib/igniter/dataflow/diff.rb +37 -0
  63. data/lib/igniter/dataflow/diff_state.rb +81 -0
  64. data/lib/igniter/dataflow/incremental_collection_result.rb +39 -0
  65. data/lib/igniter/dataflow/window_filter.rb +48 -0
  66. data/lib/igniter/dataflow.rb +65 -0
  67. data/lib/igniter/dsl/contract_builder.rb +71 -7
  68. data/lib/igniter/executor.rb +60 -0
  69. data/lib/igniter/extensions/capabilities.rb +39 -0
  70. data/lib/igniter/extensions/content_addressing.rb +5 -0
  71. data/lib/igniter/extensions/dataflow.rb +117 -0
  72. data/lib/igniter/extensions/mesh.rb +31 -0
  73. data/lib/igniter/fingerprint.rb +43 -0
  74. data/lib/igniter/integrations/llm/config.rb +48 -4
  75. data/lib/igniter/integrations/llm/executor.rb +221 -28
  76. data/lib/igniter/integrations/llm/providers/anthropic.rb +37 -4
  77. data/lib/igniter/integrations/llm/providers/openai.rb +34 -5
  78. data/lib/igniter/integrations/llm/transcription/providers/assemblyai.rb +200 -0
  79. data/lib/igniter/integrations/llm/transcription/providers/base.rb +122 -0
  80. data/lib/igniter/integrations/llm/transcription/providers/deepgram.rb +162 -0
  81. data/lib/igniter/integrations/llm/transcription/providers/openai.rb +102 -0
  82. data/lib/igniter/integrations/llm/transcription/transcriber.rb +145 -0
  83. data/lib/igniter/integrations/llm/transcription/transcript_result.rb +29 -0
  84. data/lib/igniter/integrations/llm.rb +37 -1
  85. data/lib/igniter/memory/agent_memory.rb +104 -0
  86. data/lib/igniter/memory/episode.rb +29 -0
  87. data/lib/igniter/memory/fact.rb +27 -0
  88. data/lib/igniter/memory/memorable.rb +90 -0
  89. data/lib/igniter/memory/reflection_cycle.rb +96 -0
  90. data/lib/igniter/memory/reflection_record.rb +28 -0
  91. data/lib/igniter/memory/store.rb +115 -0
  92. data/lib/igniter/memory/stores/in_memory.rb +136 -0
  93. data/lib/igniter/memory/stores/sqlite.rb +284 -0
  94. data/lib/igniter/memory.rb +80 -0
  95. data/lib/igniter/mesh/announcer.rb +55 -0
  96. data/lib/igniter/mesh/config.rb +45 -0
  97. data/lib/igniter/mesh/discovery.rb +39 -0
  98. data/lib/igniter/mesh/errors.rb +31 -0
  99. data/lib/igniter/mesh/gossip.rb +47 -0
  100. data/lib/igniter/mesh/peer.rb +21 -0
  101. data/lib/igniter/mesh/peer_registry.rb +51 -0
  102. data/lib/igniter/mesh/poller.rb +77 -0
  103. data/lib/igniter/mesh/router.rb +109 -0
  104. data/lib/igniter/mesh.rb +85 -0
  105. data/lib/igniter/metrics/collector.rb +131 -0
  106. data/lib/igniter/metrics/prometheus_exporter.rb +104 -0
  107. data/lib/igniter/metrics/snapshot.rb +8 -0
  108. data/lib/igniter/metrics.rb +37 -0
  109. data/lib/igniter/model/aggregate_node.rb +34 -0
  110. data/lib/igniter/model/collection_node.rb +3 -2
  111. data/lib/igniter/model/compute_node.rb +13 -0
  112. data/lib/igniter/model/remote_node.rb +18 -2
  113. data/lib/igniter/node_cache.rb +231 -0
  114. data/lib/igniter/replication/bootstrapper.rb +61 -0
  115. data/lib/igniter/replication/bootstrappers/gem.rb +32 -0
  116. data/lib/igniter/replication/bootstrappers/git.rb +39 -0
  117. data/lib/igniter/replication/bootstrappers/tarball.rb +56 -0
  118. data/lib/igniter/replication/expansion_plan.rb +38 -0
  119. data/lib/igniter/replication/expansion_planner.rb +142 -0
  120. data/lib/igniter/replication/manifest.rb +45 -0
  121. data/lib/igniter/replication/network_topology.rb +123 -0
  122. data/lib/igniter/replication/node_role.rb +42 -0
  123. data/lib/igniter/replication/reflective_replication_agent.rb +238 -0
  124. data/lib/igniter/replication/replication_agent.rb +87 -0
  125. data/lib/igniter/replication/role_registry.rb +73 -0
  126. data/lib/igniter/replication/ssh_session.rb +77 -0
  127. data/lib/igniter/replication.rb +54 -0
  128. data/lib/igniter/runtime/execution.rb +18 -0
  129. data/lib/igniter/runtime/input_validator.rb +6 -2
  130. data/lib/igniter/runtime/resolver.rb +254 -16
  131. data/lib/igniter/runtime/stores/redis_store.rb +41 -4
  132. data/lib/igniter/server/client.rb +44 -1
  133. data/lib/igniter/server/config.rb +13 -6
  134. data/lib/igniter/server/handlers/event_handler.rb +4 -0
  135. data/lib/igniter/server/handlers/execute_handler.rb +6 -0
  136. data/lib/igniter/server/handlers/liveness_handler.rb +20 -0
  137. data/lib/igniter/server/handlers/manifest_handler.rb +34 -0
  138. data/lib/igniter/server/handlers/metrics_handler.rb +51 -0
  139. data/lib/igniter/server/handlers/peers_handler.rb +115 -0
  140. data/lib/igniter/server/handlers/readiness_handler.rb +47 -0
  141. data/lib/igniter/server/http_server.rb +54 -17
  142. data/lib/igniter/server/router.rb +54 -21
  143. data/lib/igniter/server/server_logger.rb +52 -0
  144. data/lib/igniter/server.rb +6 -0
  145. data/lib/igniter/skill/feedback.rb +116 -0
  146. data/lib/igniter/skill/output_schema.rb +110 -0
  147. data/lib/igniter/skill.rb +218 -0
  148. data/lib/igniter/temporal.rb +84 -0
  149. data/lib/igniter/tool/discoverable.rb +151 -0
  150. data/lib/igniter/tool.rb +52 -0
  151. data/lib/igniter/tool_registry.rb +144 -0
  152. data/lib/igniter/version.rb +1 -1
  153. data/lib/igniter.rb +17 -0
  154. metadata +122 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 71035dd880de5a8a81bcc14cd0bf1097dbbb03ec9c2a915e36ae821bb3e25d70
4
- data.tar.gz: a6bb66b9f865230e09c21e5db140ecf3b6705a934b2a5172f3dd2d6198dd5371
3
+ metadata.gz: 72a53df8eec907ddc95b8ab3b17890be79b00acebc04a94a92b6426573829f35
4
+ data.tar.gz: 9facff923baee1ef67e72c194fdbf390f79b516dcf3377ebc69dc74222bc6603
5
5
  SHA512:
6
- metadata.gz: 0d59ee3a5cab0e01e5f0bdae733eb991296be4ff0ef107b305e1795da1c0de2629a629dbeef18326ca606e142376542eb919470cd66ac1baee7db4210eeccd4c
7
- data.tar.gz: 79d4cabcebcfa3b93cb9fd409c1f4173001125f59dde18bb79a21a8021f45877613de5023de0caf6a0400418bfe71541a61ec8a42125d9aab526ddc23be3c211
6
+ metadata.gz: 000ef1ec945e8d4e2ea392708326927be8f9e8d6f6293b537c5b26f4a37ac5c81d0dec3da4c4b0603f5e1e5d78949f558aaadbbb6e062bb8029d130bfd2e4256
7
+ data.tar.gz: d79723b7f2a0938fcd70af41af44d4d3057834c366195ae7201ef003e9d36818137e1c8483a2d8ee9c48c517b988a658576d1b604cff22a49a21b32fe96b4ca0
data/README.md CHANGED
@@ -13,6 +13,10 @@ Igniter is a Ruby gem for expressing business logic as a validated dependency gr
13
13
  - runtime auditing, diagnostics reports, and reactive side effects
14
14
  - graph and runtime introspection (text, Mermaid)
15
15
  - ergonomic DSL helpers: `const`, `lookup`, `map`, `project`, `aggregate`, `guard`, `export`, `expose`, `effect`, `on_success`, `scope`, `namespace`
16
+ - `Igniter::Application` — application scaffold with YAML config, autoloading, scheduler, and `igniter-server new` generator
17
+ - capability-based security: declare executor resource requirements, enforce `Policy` at runtime
18
+ - temporal contracts: reproducible historical execution via an explicit `as_of` time input
19
+ - content-addressed computation: `pure` executors cached by input fingerprint across executions and processes
16
20
 
17
21
  ## Installation
18
22
 
@@ -69,6 +73,10 @@ contract.diagnostics_text # compact execution summary
69
73
  - **Diagnostics**: compact text, Markdown, or structured reports for triage.
70
74
  - **Reactive**: subscribe declaratively to runtime events with `effect`, `on_success`, `on_failure`.
71
75
  - **Introspection**: render graphs as text or Mermaid; inspect runtime state.
76
+ - **Capabilities**: executors declare what resources they need (`:network`, `:database`, …); `Policy` denies them at runtime.
77
+ - **Temporal contracts**: inject `as_of` time input automatically; replay any historical computation with the original timestamp.
78
+ - **Content addressing**: `pure` executors get a universal cache key — identical inputs return a cached result across executions, processes, and deployments.
79
+ - **Incremental dataflow**: `mode: :incremental` on collection nodes — only added/changed items run, unchanged items reuse cached results, removed items are retracted. O(change) not O(total).
72
80
 
73
81
  ## Quick Start Recipes
74
82
 
@@ -363,6 +371,10 @@ Igniter::Server.start
363
371
  **CLI:**
364
372
 
365
373
  ```bash
374
+ # Generate a new application scaffold
375
+ igniter-server new my_app
376
+
377
+ # Start a server directly
366
378
  igniter-server start --port 4568 --require ./contracts.rb
367
379
  ```
368
380
 
@@ -448,6 +460,199 @@ end
448
460
 
449
461
  See [`examples/llm/research_agent.rb`](examples/llm/research_agent.rb), [`examples/llm/tool_use.rb`](examples/llm/tool_use.rb), and [`docs/LLM_V1.md`](docs/LLM_V1.md).
450
462
 
463
+ ### 12. Igniter::Application
464
+
465
+ Package contracts, executors, scheduler, and server config into a single coherent entry point:
466
+
467
+ ```bash
468
+ # Scaffold a new application
469
+ igniter-server new my_app
470
+ cd my_app && bundle install && bin/start
471
+ ```
472
+
473
+ ```ruby
474
+ require "igniter/application"
475
+
476
+ class MyApp < Igniter::Application
477
+ config_file "application.yml" # optional YAML base config
478
+
479
+ configure do |c|
480
+ c.port = ENV.fetch("PORT", 4567).to_i
481
+ c.store = Igniter::Runtime::Stores::MemoryStore.new
482
+ end
483
+
484
+ executors_path "executors/" # eager-require all executors
485
+ contracts_path "contracts/" # eager-require all contracts
486
+
487
+ register "OrderContract", OrderContract
488
+
489
+ schedule :cleanup, every: "1h" do
490
+ puts "[cleanup] #{Time.now.strftime("%H:%M")}"
491
+ end
492
+
493
+ schedule :report, every: "1d", at: "09:00" do
494
+ DailyReportContract.new.resolve_all(date: Date.today)
495
+ end
496
+ end
497
+
498
+ MyApp.start # blocking built-in HTTP server
499
+ # or
500
+ MyApp.rack_app # Rack app for Puma / Unicorn
501
+ ```
502
+
503
+ **`application.yml`** — base config loaded before the `configure` block (block always wins):
504
+
505
+ ```yaml
506
+ server:
507
+ port: 4567
508
+ host: "0.0.0.0"
509
+ log_format: json # text (default) or json
510
+ drain_timeout: 30
511
+ ```
512
+
513
+ **Scheduler interval formats:** `30` (seconds), `"30s"`, `"5m"`, `"2h"`, `"1d"`, `{ hours: 1, minutes: 30 }`
514
+
515
+ See [`docs/APPLICATION_V1.md`](docs/APPLICATION_V1.md) for the full reference and companion app example.
516
+
517
+ ### 13. Capability-Based Security
518
+
519
+ Declare what external resources an executor needs, then deny specific capabilities at the
520
+ policy level — without touching the executors themselves:
521
+
522
+ ```ruby
523
+ require "igniter/capabilities"
524
+
525
+ class DbLookup < Igniter::Executor
526
+ capabilities :database
527
+
528
+ def call(id:)
529
+ DB.find(id)
530
+ end
531
+ end
532
+
533
+ class PureCalc < Igniter::Executor
534
+ pure # shorthand for capabilities(:pure)
535
+
536
+ def call(x:, y:) = x + y
537
+ end
538
+
539
+ # Inspect the graph's surface area before deploying
540
+ MyContract.compiled_graph.required_capabilities
541
+ # => { fetch: [:database], total: [:pure] }
542
+
543
+ # Enforce policy at boot time
544
+ Igniter::Capabilities.policy = Igniter::Capabilities::Policy.new(denied: [:database])
545
+
546
+ MyContract.new(id: 1).resolve_all
547
+ # => CapabilityViolationError: Node 'fetch' uses denied capabilities: database
548
+ ```
549
+
550
+ See [`docs/CAPABILITIES_V1.md`](docs/CAPABILITIES_V1.md).
551
+
552
+ ### 14. Temporal Contracts
553
+
554
+ Make time an explicit input so every execution is fully reproducible:
555
+
556
+ ```ruby
557
+ require "igniter/temporal"
558
+
559
+ class TaxRateContract < Igniter::Contract
560
+ include Igniter::Temporal
561
+
562
+ define do
563
+ input :country
564
+ # `as_of` is injected automatically (default: Time.now)
565
+
566
+ temporal_compute :rate, depends_on: :country do |country:, as_of:|
567
+ HISTORICAL_RATES.dig(country, as_of.year) || 0.0
568
+ end
569
+
570
+ output :rate
571
+ end
572
+ end
573
+
574
+ # Current rate
575
+ TaxRateContract.new(country: "UA").result.rate
576
+ # => 0.22
577
+
578
+ # Reproduce the exact 2024 result
579
+ TaxRateContract.new(country: "UA", as_of: Time.new(2024, 1, 1)).result.rate
580
+ # => 0.20
581
+ ```
582
+
583
+ See [`docs/TEMPORAL_V1.md`](docs/TEMPORAL_V1.md).
584
+
585
+ ### 15. Content-Addressed Computation
586
+
587
+ `pure` executors are cached by a fingerprint of their logic + inputs. Identical computation
588
+ is never repeated — within an execution, across executions, or across processes:
589
+
590
+ ```ruby
591
+ require "igniter/extensions/content_addressing"
592
+
593
+ class TaxCalculator < Igniter::Executor
594
+ pure
595
+ fingerprint "tax_calc_v1" # bump to invalidate the cache when logic changes
596
+
597
+ def call(country:, amount:)
598
+ TAX_RATES[country] * amount
599
+ end
600
+ end
601
+
602
+ # First execution — computes and caches
603
+ InvoiceContract.new(country: "UA", amount: 1000).result.tax # computed
604
+
605
+ # Second execution — served from cache; TaxCalculator is never called
606
+ InvoiceContract.new(country: "UA", amount: 1000).result.tax # cache hit
607
+
608
+ # Distributed cache (Redis) — shared across all nodes
609
+ Igniter::ContentAddressing.cache = RedisContentCache.new(Redis.new)
610
+ ```
611
+
612
+ See [`docs/CONTENT_ADDRESSING_V1.md`](docs/CONTENT_ADDRESSING_V1.md).
613
+
614
+ ### 16. Incremental Dataflow — O(change) Collection Processing
615
+
616
+ `mode: :incremental` on a collection node makes the runtime diff the input array on
617
+ every `resolve_all`. Only added/changed items have their child contract re-run;
618
+ unchanged items reuse the cached result; removed items are retracted automatically.
619
+
620
+ ```ruby
621
+ require "igniter/extensions/dataflow"
622
+
623
+ class SensorPipeline < Igniter::Contract
624
+ define do
625
+ input :readings, type: :array
626
+
627
+ collection :processed,
628
+ with: :readings,
629
+ each: SensorAnalysis,
630
+ key: :sensor_id,
631
+ mode: :incremental,
632
+ window: { last: 1000 } # bounded memory
633
+
634
+ output :processed
635
+ end
636
+ end
637
+
638
+ pipeline = SensorPipeline.new(readings: initial_batch)
639
+ pipeline.resolve_all # all N items run once
640
+
641
+ # Push only the delta — no full-array replacement needed
642
+ pipeline.feed_diff(:readings,
643
+ add: [{ sensor_id: "new-1", value: 10 }],
644
+ update: [{ sensor_id: "tmp-2", value: 90 }],
645
+ remove: ["hum-1"]
646
+ )
647
+ pipeline.resolve_all # only 2 child contracts run (new-1 + tmp-2)
648
+
649
+ diff = pipeline.collection_diff(:processed)
650
+ diff.processed_count # => 2
651
+ diff.unchanged.size # => N - 2
652
+ ```
653
+
654
+ See [`docs/DATAFLOW_V1.md`](docs/DATAFLOW_V1.md).
655
+
451
656
  ## Examples
452
657
 
453
658
  | Example | Run | Shows |
@@ -465,6 +670,8 @@ See [`examples/llm/research_agent.rb`](examples/llm/research_agent.rb), [`exampl
465
670
  | `server/node1.rb` + `node2.rb` | run both, then curl | Two-node igniter-server with `remote:` DSL |
466
671
  | `llm/research_agent.rb` | `ruby examples/llm/research_agent.rb` | Multi-step LLM pipeline with Ollama |
467
672
  | `llm/tool_use.rb` | `ruby examples/llm/tool_use.rb` | LLM tool declarations, chained LLM nodes, `Context` |
673
+ | `companion/demo.rb` | `ruby examples/companion/demo.rb` | End-to-end voice AI pipeline using `Igniter::Application` |
674
+ | `dataflow.rb` | `ruby examples/dataflow.rb` | Incremental sensor pipeline: `mode: :incremental`, `feed_diff`, sliding window |
468
675
 
469
676
  ## Design Docs
470
677
 
@@ -478,6 +685,11 @@ See [`examples/llm/research_agent.rb`](examples/llm/research_agent.rb), [`exampl
478
685
  - [Store Adapters](docs/STORE_ADAPTERS.md)
479
686
  - [igniter-server v1](docs/SERVER_V1.md)
480
687
  - [LLM Integration v1](docs/LLM_V1.md)
688
+ - [Application scaffold v1](docs/APPLICATION_V1.md)
689
+ - [Capabilities v1](docs/CAPABILITIES_V1.md)
690
+ - [Temporal Contracts v1](docs/TEMPORAL_V1.md)
691
+ - [Content Addressing v1](docs/CONTENT_ADDRESSING_V1.md)
692
+ - [Incremental Dataflow v1](docs/DATAFLOW_V1.md)
481
693
  - [Concepts and Principles](docs/IGNITER_CONCEPTS.md)
482
694
 
483
695
  ## Development
@@ -500,7 +712,12 @@ Current feature baseline:
500
712
  - igniter-server: TCP server, Rack adapter, CLI, `remote:` DSL
501
713
  - LLM compute nodes: Ollama, Anthropic, OpenAI providers
502
714
  - Rails integration: Railtie, ActiveJob, ActionCable, webhook controller mixin
715
+ - `Igniter::Application`: YAML config, autoloading, scheduler, generator (`igniter-server new`)
503
716
  - auditing, diagnostics, reactive subscriptions, graph introspection
717
+ - capability-based security: `capabilities`, `pure`, `Policy`, `CapabilityViolationError`
718
+ - temporal contracts: `include Igniter::Temporal`, `temporal_compute`, `as_of` input, historical reproduction
719
+ - content-addressed computation: `pure`, `fingerprint`, universal `ContentKey`, pluggable `ContentCache`
720
+ - incremental dataflow: `mode: :incremental`, `window:`, `feed_diff`, `collection_diff`, `DiffState`, `IncrementalCollectionResult`
504
721
 
505
722
  ## License
506
723
 
@@ -0,0 +1,253 @@
1
+ # Igniter::Application v1
2
+
3
+ `Igniter::Application` is an optional base class that packages contracts, executors, YAML config, a background scheduler, and server startup into a single coherent entry point. It replaces the raw `Igniter::Server.configure` boilerplate and provides a conventional project layout.
4
+
5
+ ---
6
+
7
+ ## Quick Start
8
+
9
+ ### 1. Generate a scaffold
10
+
11
+ ```bash
12
+ igniter-server new my_app
13
+ cd my_app
14
+ bundle install
15
+ bin/start
16
+ ```
17
+
18
+ This creates:
19
+
20
+ ```
21
+ my_app/
22
+ ├── application.rb ← Application class (entry point)
23
+ ├── application.yml ← base config (port, log_format, etc.)
24
+ ├── Gemfile
25
+ ├── config.ru ← Rack entry point for Puma / Unicorn
26
+ ├── bin/start ← convenience start script
27
+ ├── contracts/ ← put Contract subclasses here
28
+ └── executors/ ← put Executor subclasses here
29
+ ```
30
+
31
+ ### 2. Define your Application
32
+
33
+ ```ruby
34
+ # application.rb
35
+ require "igniter"
36
+ require "igniter/server"
37
+ require "igniter/application"
38
+
39
+ Dir[File.join(__dir__, "executors/**/*.rb")].sort.each { |f| require f }
40
+ Dir[File.join(__dir__, "contracts/**/*.rb")].sort.each { |f| require f }
41
+
42
+ class MyApp < Igniter::Application
43
+ config_file File.join(__dir__, "application.yml") # optional
44
+
45
+ configure do |c|
46
+ c.port = ENV.fetch("PORT", 4567).to_i
47
+ c.store = Igniter::Runtime::Stores::MemoryStore.new
48
+ end
49
+
50
+ register "OrderContract", OrderContract
51
+ register "InvoiceContract", InvoiceContract
52
+
53
+ schedule :cleanup, every: "1h" do
54
+ puts "[cleanup] #{Time.now.strftime("%H:%M")}"
55
+ end
56
+
57
+ schedule :daily_report, every: "1d", at: "09:00" do
58
+ DailyReportContract.new.resolve_all(date: Date.today)
59
+ end
60
+ end
61
+
62
+ MyApp.start if $PROGRAM_NAME == __FILE__
63
+ ```
64
+
65
+ ### 3. Run
66
+
67
+ ```bash
68
+ # Built-in HTTP server (blocking)
69
+ ruby application.rb
70
+
71
+ # Rack / Puma
72
+ bundle exec puma config.ru
73
+ ```
74
+
75
+ ---
76
+
77
+ ## DSL Reference
78
+
79
+ ### `config_file(path)`
80
+
81
+ Load a YAML file as the base configuration. Applied **before** the `configure` block — values in the block always win.
82
+
83
+ ```ruby
84
+ config_file File.join(__dir__, "application.yml")
85
+ ```
86
+
87
+ ### `configure { |c| ... }`
88
+
89
+ Block receives an `AppConfig` instance. May be called multiple times; blocks are applied in order.
90
+
91
+ ```ruby
92
+ configure do |c|
93
+ c.host = "0.0.0.0"
94
+ c.port = 4567
95
+ c.log_format = :json # :text (default) or :json
96
+ c.drain_timeout = 30 # seconds for SIGTERM drain
97
+ c.store = my_store # any store adapter
98
+ c.metrics_collector = Igniter::Metrics::Collector.new
99
+ end
100
+ ```
101
+
102
+ ### `executors_path(path)` / `contracts_path(path)`
103
+
104
+ Eagerly require all `.rb` files under the given directory on startup.
105
+
106
+ ```ruby
107
+ executors_path "executors/"
108
+ contracts_path "contracts/"
109
+ ```
110
+
111
+ ### `register(name, contract_class)`
112
+
113
+ Register a contract for HTTP dispatch.
114
+
115
+ ```ruby
116
+ register "OrderContract", OrderContract
117
+ ```
118
+
119
+ ### `schedule(name, every:, at: nil) { ... }`
120
+
121
+ Define a recurring background job. Starts automatically with `start` / `rack_app`.
122
+
123
+ ```ruby
124
+ schedule :heartbeat, every: "30s" do
125
+ HealthCheckContract.new.resolve_all
126
+ end
127
+
128
+ schedule :daily_report, every: "1d", at: "09:00" do
129
+ DailyReportContract.new.resolve_all(date: Date.today)
130
+ end
131
+ ```
132
+
133
+ **Interval formats:**
134
+
135
+ | Format | Meaning |
136
+ |--------|---------|
137
+ | `30` | 30 seconds (Integer) |
138
+ | `"30s"` | 30 seconds |
139
+ | `"5m"` | 5 minutes |
140
+ | `"2h"` | 2 hours |
141
+ | `"1d"` | 1 day |
142
+ | `{ hours: 1, minutes: 30 }` | 90 minutes |
143
+
144
+ `at: "HH:MM"` delays the first run until the next occurrence of that wall-clock time, then repeats with `every:`.
145
+
146
+ ---
147
+
148
+ ## application.yml Reference
149
+
150
+ ```yaml
151
+ server:
152
+ port: 4567
153
+ host: "0.0.0.0"
154
+ log_format: text # "text" (default) or "json"
155
+ drain_timeout: 30 # seconds for graceful SIGTERM shutdown
156
+ ```
157
+
158
+ Keys under `server:` map 1-to-1 to `AppConfig` attributes. Values from YAML are applied first; the `configure` block runs afterwards and overrides anything.
159
+
160
+ ENV variables are not expanded in YAML — read them in the `configure` block:
161
+
162
+ ```ruby
163
+ configure do |c|
164
+ c.port = ENV.fetch("PORT", 4567).to_i
165
+ end
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Lifecycle
171
+
172
+ ### `MyApp.start`
173
+
174
+ Builds the config, registers contracts, starts background jobs, then starts the built-in pure-Ruby HTTP server (blocking). Registers `at_exit` to stop the scheduler.
175
+
176
+ ### `MyApp.rack_app`
177
+
178
+ Same as `start` but returns a Rack-compatible application instead of blocking. Use with Puma, Unicorn, or any Rack server:
179
+
180
+ ```ruby
181
+ # config.ru
182
+ require_relative "application"
183
+ run MyApp.rack_app
184
+ ```
185
+
186
+ ```bash
187
+ bundle exec puma config.ru -p 4567
188
+ ```
189
+
190
+ ### `MyApp.config`
191
+
192
+ Returns the `AppConfig` instance (populated after the first call to `start` or `rack_app`).
193
+
194
+ ---
195
+
196
+ ## Build Order
197
+
198
+ 1. YAML file loaded → values applied to `AppConfig`
199
+ 2. `executors_path` / `contracts_path` directories required
200
+ 3. `configure` blocks run in declaration order (override YAML)
201
+ 4. `Server::Config` built from `AppConfig`
202
+ 5. Contracts registered on the server registry
203
+ 6. Scheduler started (one thread per job)
204
+
205
+ ---
206
+
207
+ ## Subclass Isolation
208
+
209
+ Each `Igniter::Application` subclass gets its own isolated set of registered contracts, scheduled jobs, and configure blocks via the `inherited` hook. Subclasses do not share state:
210
+
211
+ ```ruby
212
+ class AppA < Igniter::Application
213
+ register "ContractA", ContractA
214
+ end
215
+
216
+ class AppB < Igniter::Application
217
+ register "ContractB", ContractB
218
+ end
219
+ # AppA and AppB have completely independent registries
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Companion App Example
225
+
226
+ `examples/companion/` is a full production-style application built with `Igniter::Application`. It implements a distributed voice AI assistant pipeline:
227
+
228
+ ```
229
+ ESP32 microphone → ASR → Intent → Chat (LLM) → TTS → ESP32 speaker
230
+ ```
231
+
232
+ **Single-process demo (mock executors, no hardware):**
233
+
234
+ ```bash
235
+ ruby examples/companion/demo.rb
236
+ ```
237
+
238
+ **Orchestrator node (HP t740, real Ollama):**
239
+
240
+ ```bash
241
+ # Requires: ollama serve (llama3.1:8b pulled)
242
+ bundle exec ruby examples/companion/application.rb
243
+ ```
244
+
245
+ **See also:** [`examples/companion/README.md`](../examples/companion/README.md)
246
+
247
+ ---
248
+
249
+ ## Integration with igniter-server
250
+
251
+ `Igniter::Application` is a thin wrapper around `Igniter::Server`. The underlying `Server::Config` and `HttpServer` are the same — you can still use `Igniter::Server.configure` directly when you don't need the Application scaffold.
252
+
253
+ The two approaches are compatible in the same process: `Igniter::Application#start` calls `Igniter::Server::HttpServer.new(config).start` internally.