# `GgenIgniter.EngineRegistry`
[🔗](https://github.com/seanchatmangpt/ggen_igniter/blob/v26.9.8/lib/ggen_igniter/engine_registry.ex#L1)

ADR-0008's orchestration layer *over* `GgenIgniter.Engine.fetch!/1`, not a
replacement for it. Single-engine callers (the default, unchanged path
through `lib/mix/tasks/ggen_igniter.sync.ex`'s `run_via_reactor/3` ->
`dispatch_reactor_reconcile/2` -> `GgenIgniter.Reactors.ReconcileReactor`'s
own internal `target_opts[:engine] || "oxigraph"` -> `Engine.fetch!/1`
resolution) never touch this module at all -- it exists only for the
`--engine` comma-separated/`"all"` comparison-mode path.

Two real, distinct entry points:

- `resolve/1` (and `resolve/2`, taking the raw sync CLI `opts` so the
  `"all"` branch can run a real qlever-reachability precondition check --
  see below) -- parses one of three `--engine` value shapes into a
  validated, deduplicated list of engine atoms.
- `run_all/4` -- fans every resolved engine atom out to
  `GgenIgniter.Engine.fetch!/1`'s already-resolved module and that module's
  existing `prepare!/2`/`run/2` (`lib/ggen_igniter/engine.ex:21-22`'s
  `@callback`s), via `Task.async_stream/3` mirroring
  `GgenIgniter.Reactors.ReconcileReactor`'s own real concurrency precedent
  (`lib/ggen_igniter/reactors/reconcile_reactor.ex:1743`,
  `|> Task.async_stream(&actuate_one(&1, ...), max_concurrency: ...,
  timeout: :infinity)`), returning a `GgenIgniter.EngineComparisonReport.t()`.

## Why not a formal behaviour for this module itself

`GgenIgniter.Engine` is already a real `@callback`-based behaviour for the
*per-engine* `prepare!/2`/`run/2` contract. This module is a different kind
of thing: a one-shot orchestration layer *over* that existing behaviour,
not a second engine contract -- a second formal `@callback` here would just
wrap `Engine.fetch!/1`'s existing dispatch in ceremony. See
`docs/architecture/adr/0008-evidence-ranked-multi-engine-registry.md`
("Why not a formal behaviour for the registry itself") for the full
reasoning this module implements verbatim.

## `"all"`'s qlever-inclusion precondition -- real, not simulated

`mix ggen_igniter.doctor`'s check 8 (`lib/mix/tasks/ggen_igniter.doctor.ex`,
`check_qlever_reachable/2`) already establishes this repo's precedent for a
real, non-simulated QLever reachability probe (load the named `--store-id`
store from the loaded ontology graph, run a real `ASK { ?s ?p ?o }` against
it, `rescue` a real connection/parse failure). `qlever_reachable?/1` below
reuses that exact real technique (same real `GgenIgniter.Query.Qlever.
load_store!/2` + `GgenIgniter.Query.Qlever.run/2` + real `ASK` probe,
`rescue` -> `false`) rather than inventing a second, simulated one --
`check_qlever_reachable/2` itself stays untouched (it is `defp`, shaped
around a `pack_dir` argument and a human-readable `{:ok, msg} | {:error,
msg}` result the doctor task's own reporting needs, not the plain boolean
this module's silent-exclusion-with-warning behavior needs).

# `resolve`

```elixir
@spec resolve(
  String.t(),
  keyword()
) :: {:ok, [atom()]} | {:error, term()}
```

Parses `engine_spec` (one of: a single engine name, a comma-separated list,
or the literal `"all"`) into a validated, deduplicated list of engine
atoms, in the order named (never re-sorted, except `"all"`'s own expansion
-- see below) -- order matters downstream: `mix ggen_igniter.sync`'s
comparison mode treats the FIRST resolved engine as the primary engine
whose rows actually get rendered/actuated.

`resolve/1` (no `opts`) is equivalent to `resolve(engine_spec, [])`: every
shape except `"all"` behaves identically either way, since only `"all"`'s
qlever-inclusion precondition needs `--store-id`/`--ontology`/`--pack`
context. Passing `opts: []` to `"all"` is not an error -- it just means
qlever's `--store-id` precondition is trivially unsatisfied, so `"all"`
silently excludes qlever with a logged warning, exactly as if
`--store-id` had genuinely been omitted.

## Examples

    iex> GgenIgniter.EngineRegistry.resolve("oxigraph")
    {:ok, [:oxigraph]}

    iex> GgenIgniter.EngineRegistry.resolve("oxigraph,sparql")
    {:ok, [:oxigraph, :sparql]}

    iex> GgenIgniter.EngineRegistry.resolve("oxigraph, oxigraph, sparql")
    {:ok, [:oxigraph, :sparql]}

    iex> GgenIgniter.EngineRegistry.resolve("nope")
    {:error, "invalid --engine name(s): nope, must be one of: oxigraph, qlever, sparql"}

# `run_all`

```elixir
@spec run_all(String.t(), RDF.Graph.t(), [atom()], keyword()) ::
  GgenIgniter.EngineComparisonReport.t()
```

Fans `query_text` out to every engine in `engines` (each resolved via
`GgenIgniter.Engine.fetch!/1`, exactly as the single-engine path already
does), concurrently via `Task.async_stream/3`
(`max_concurrency: length(engines)`, mirroring `GgenIgniter.Reactors.
ReconcileReactor`'s `:actuate` step's own real concurrency precedent),
timing each candidate with `System.monotonic_time(:microsecond)`. Every
invocation is wrapped in `rescue` so one engine's crash becomes a
`%CandidateResult{status: :error}` entry (never aborting the others) --
this generalizes the six known-real per-engine error-shape divergences
`test/ggen_igniter_engine_parity_test.exs` already pins (raw `MatchError`
on CONSTRUCT/malformed query on `sparql`-hex, `Protocol.UndefinedError` on
bare `FILTER NOT EXISTS`, ...) into per-run, per-engine data. A task that
exceeds `opts[:timeout]` (default `30_000` ms) is killed
(`on_timeout: :kill_task`) and becomes `%CandidateResult{status: :timeout}`
instead of hanging the whole comparison run.

`graph` is the already-loaded `%RDF.Graph{}` (`GgenIgniter.Ontology.
load!/1`'s return) -- each engine's own `prepare!/2` (already implemented,
reused verbatim here) turns it into whatever context that engine's `run/2`
actually needs (the graph itself for `oxigraph`/`sparql`, a resolved
`Gno.Store.Adapters.Qlever` struct for `qlever`, via `opts[:store_id]`).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
