The runtime, per-invocation, N-way surfacing mechanism ADR-0008 introduces --
NOT a new correctness oracle. pairwise_agreement/1 restates, over real rows
returned from a real run, exactly the row-set/row-order/error-shape
distinction classes test/ggen_igniter_cross_engine_equivalence_properties_test.exs
and test/ggen_igniter_engine_parity_test.exs already establish statically:
- Row-SET equality via
MapSet.new/1over each candidate's rawrows(mirroringggen_igniter_cross_engine_equivalence_properties_test.exs's ownrows_as_set/1method, for the identical reason that test names -- order is a separately-tracked, already-known-divergent axis). - Row-ORDER equality as a SEPARATE raw-list
==check, deliberately kept apart from set equality per the confirmedsparql-hexORDER BYrow-reversal bug documented inlib/ggen_igniter/query.ex:4-16's moduledoc. A future divergence of that exact shape shows up here asorder_equal?: falsewithrow_set_equal?: true, never collapsed into one ambiguous boolean.
Two structs:
t()-- one comparison run:query(the SPARQL text run, verbatim),candidates([CandidateResult.t()], one per engineGgenIgniter. EngineRegistry.run_all/4fanned out to),pairwise_agreement(a map keyed by{engine_a, engine_b}engine-atom pairs,engine_aordered beforeengine_bperGgenIgniter.Engine.valid_names/0's own sort so a pair never appears twice under swapped keys),generated_at(DateTime.t()).CandidateResult.t()-- one engine's real outcome:engine(atom),status(:ok | :error | :timeout),rows([map()] | nil,nilonly whenstatus != :ok),row_count,elapsed_us(System.monotonic_time(:microsecond)delta; always a real non-negative integer, even on:error/:timeout, so the report can show how long a failing engine took before it failed),error(String.t() | nil, the realException.message/1or exit-reason text on a non-:okstatus).
to_markdown/1 and to_json/1 render either one report or a list of
reports (GgenIgniter.EngineRegistry.run_all/4's caller runs one report per
named --query; mix ggen_igniter.sync --engine oxigraph,sparql with more
than one --query name=path.rq produces more than one report over one
invocation). No new templating path: to_markdown/1 follows this repo's
existing GgenIgniter.Render.render/2 convention (lib/ggen_igniter/ render.ex:8-11, stdlib EEx.eval_string/2 against bindings) rather than
inventing a second one. to_json/1 uses Jason (already a dependency, see
mix.exs's {:jason, "~> 1.4"}) -- no new dependency either.
Summary
Functions
Computes t().pairwise_agreement over every status: :ok pair in
candidates (a status: :error/:timeout candidate has no real rows to
compare, so it is excluded from every pair rather than silently compared
against nil). Every unordered pair is computed exactly once, keyed
{engine_a, engine_b} with engine_a preceding engine_b in the input
list's own order (never both {a, b} and {b, a}).
Renders one report, or a list of reports (see moduledoc), as pretty-printed JSON.
The plain, JSON-safe map to_json/1 encodes -- exposed separately so a caller (e.g. mix ggen_igniter.sync's --engine-report) can wrap it (adding a "query_name" field) without re-parsing the encoded string.
Renders one report, or a list of reports (see moduledoc), as a Markdown document (stdlib EEx.eval_string/2, GgenIgniter.Render.render/2's own convention -- no second templating path).
Types
@type t() :: %GgenIgniter.EngineComparisonReport{ candidates: [GgenIgniter.EngineComparisonReport.CandidateResult.t()], generated_at: DateTime.t(), pairwise_agreement: %{ required({atom(), atom()}) => %{ row_set_equal?: boolean(), row_count_diff: integer(), order_equal?: boolean() } }, query: String.t() }
Functions
@spec pairwise_agreement([GgenIgniter.EngineComparisonReport.CandidateResult.t()]) :: %{ required({atom(), atom()}) => %{ row_set_equal?: boolean(), row_count_diff: integer(), order_equal?: boolean() } }
Computes t().pairwise_agreement over every status: :ok pair in
candidates (a status: :error/:timeout candidate has no real rows to
compare, so it is excluded from every pair rather than silently compared
against nil). Every unordered pair is computed exactly once, keyed
{engine_a, engine_b} with engine_a preceding engine_b in the input
list's own order (never both {a, b} and {b, a}).
Examples
iex> a = %GgenIgniter.EngineComparisonReport.CandidateResult{engine: :oxigraph, status: :ok, rows: [%{"s" => "x"}], row_count: 1}
iex> b = %GgenIgniter.EngineComparisonReport.CandidateResult{engine: :sparql, status: :ok, rows: [%{"s" => "x"}], row_count: 1}
iex> GgenIgniter.EngineComparisonReport.pairwise_agreement([a, b])
%{{:oxigraph, :sparql} => %{row_set_equal?: true, row_count_diff: 0, order_equal?: true}}
Renders one report, or a list of reports (see moduledoc), as pretty-printed JSON.
The plain, JSON-safe map to_json/1 encodes -- exposed separately so a caller (e.g. mix ggen_igniter.sync's --engine-report) can wrap it (adding a "query_name" field) without re-parsing the encoded string.
Renders one report, or a list of reports (see moduledoc), as a Markdown document (stdlib EEx.eval_string/2, GgenIgniter.Render.render/2's own convention -- no second templating path).