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(andresolve/2, taking the raw sync CLIoptsso the"all"branch can run a real qlever-reachability precondition check -- see below) -- parses one of three--enginevalue shapes into a validated, deduplicated list of engine atoms.run_all/4-- fans every resolved engine atom out toGgenIgniter.Engine.fetch!/1's already-resolved module and that module's existingprepare!/2/run/2(lib/ggen_igniter/engine.ex:21-22's@callbacks), viaTask.async_stream/3mirroringGgenIgniter.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 aGgenIgniter.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).
Summary
Functions
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.
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.
Functions
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"}
@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]).