feat(orchestrator): CeremonyOrchestrator + Chronicle.Consumer stubs

CeremonyOrchestrator: GenServer providing a PubSub broadcast
interface for ceremony status changes. LiveView subscribes to
these broadcasts for real-time updates. A K8s CRD watcher will
feed events into this in a future sprint; for now the init log
makes the stub state explicit.

Chronicle.Consumer: stub for the Ops DB projector that will
consume Chronicle events and hydrate the Ecto tables. Projector
design (idempotent, checkpointed, catch-up on restart) per
DESIGN-OPS-DB-CHAIN-OF-CUSTODY-0001 §2.5.

Both modules document the orchestrator/engine distinction:
guildhall orchestrates, substrate decides.

Both are now supervised by their respective application trees
(Guildhall.Orchestrator.Supervisor, Guildhall.Chronicle.Supervisor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tyler J King <tking@guildhouse.dev>
This commit is contained in:
Tyler J King 2026-04-18 07:21:45 -04:00
parent 69297f1ac0
commit 48a7495ef5
4 changed files with 77 additions and 14 deletions

View file

@ -1,19 +1,13 @@
defmodule Guildhall.Chronicle.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: Guildhall.Chronicle.Worker.start_link(arg)
# {Guildhall.Chronicle.Worker, arg}
Guildhall.Chronicle.Consumer
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Guildhall.Chronicle.Supervisor]
Supervisor.start_link(children, opts)
end

View file

@ -0,0 +1,26 @@
defmodule Guildhall.Chronicle.Consumer do
@moduledoc """
Consumes Chronicle events and projects them into the Ops DB.
See DESIGN-OPS-DB-CHAIN-OF-CUSTODY-0001 §2.5 for the projector
design idempotent insert, checkpoint recovery, catch-up on
restart, backpressure handling.
Stub the transport (gRPC vs NATS vs Kafka) and the Chronicle
event schema are not yet finalized. This module will subscribe
to the chosen transport and call into the Ops DB repo for each
event, updating `projector_checkpoint` after a successful batch.
"""
use GenServer
require Logger
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
Logger.info("Chronicle.Consumer started (stub — no transport configured)")
{:ok, %{last_entry_id: nil}}
end
end

View file

@ -1,19 +1,13 @@
defmodule Guildhall.Orchestrator.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: Guildhall.Orchestrator.Worker.start_link(arg)
# {Guildhall.Orchestrator.Worker, arg}
Guildhall.Orchestrator.CeremonyOrchestrator
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Guildhall.Orchestrator.Supervisor]
Supervisor.start_link(children, opts)
end

View file

@ -0,0 +1,49 @@
defmodule Guildhall.Orchestrator.CeremonyOrchestrator do
@moduledoc """
Coordinates ceremony workflows by (eventually) watching substrate
CeremonyRequest CRDs and notifying witnesses via PubSub.
Currently a stub the K8s CRD watcher will be wired in once the
`:k8s` package is added and a kubeconfig is available.
### Orchestrator vs engine
This module ORCHESTRATES (notifies humans, collects signatures,
broadcasts status). The substrate `CeremonyEngine` DECIDES
(evaluates Accord conditions, advances the state machine).
See DESIGN-ORG-OPS-FRAMEWORK-0001 §3.2 for the separation.
"""
use GenServer
require Logger
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Broadcast a ceremony status change to all LiveView subscribers.
Called by the K8s watcher (future) or manually in tests. Fans
out on both the per-ceremony topic and the wildcard topic.
"""
def broadcast_ceremony_status(ceremony_id, status) do
Phoenix.PubSub.broadcast(
Guildhall.PubSub,
"ceremony:#{ceremony_id}",
{:ceremony_status, ceremony_id, status}
)
Phoenix.PubSub.broadcast(
Guildhall.PubSub,
"ceremony:*",
{:ceremony_status, ceremony_id, status}
)
end
@impl true
def init(_opts) do
Logger.info("CeremonyOrchestrator started (stub — no K8s watcher yet)")
{:ok, %{watcher: nil}}
end
end