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

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/1` over each candidate's raw `rows`
  (mirroring `ggen_igniter_cross_engine_equivalence_properties_test.exs`'s own
  `rows_as_set/1` method, 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 confirmed `sparql`-hex `ORDER BY`
  row-reversal bug documented in `lib/ggen_igniter/query.ex:4-16`'s
  moduledoc. A future divergence of that exact shape shows up here as
  `order_equal?: false` with `row_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 engine `GgenIgniter.
  EngineRegistry.run_all/4` fanned out to), `pairwise_agreement` (a map
  keyed by `{engine_a, engine_b}` engine-atom pairs, `engine_a` ordered
  before `engine_b` per `GgenIgniter.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`,
  `nil` only when `status != :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
  real `Exception.message/1` or exit-reason text on a non-`:ok` status).

`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.

# `t`

```elixir
@type t() :: %GgenIgniter.EngineComparisonReport{
  candidates: [GgenIgniter.EngineComparisonReport.CandidateResult.t()],
  generated_at: DateTime.t(),
  pairwise_agreement: %{
    required({atom(), atom()}) =&gt; %{
      row_set_equal?: boolean(),
      row_count_diff: integer(),
      order_equal?: boolean()
    }
  },
  query: String.t()
}
```

# `pairwise_agreement`

```elixir
@spec pairwise_agreement([GgenIgniter.EngineComparisonReport.CandidateResult.t()]) ::
  %{
    required({atom(), atom()}) =&gt; %{
      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}}

# `to_json`

```elixir
@spec to_json(t() | [t()]) :: String.t()
```

Renders one report, or a list of reports (see moduledoc), as pretty-printed JSON.

# `to_json_map`

```elixir
@spec to_json_map(t()) :: map()
```

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.

# `to_markdown`

```elixir
@spec to_markdown(t() | [t()]) :: String.t()
```

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).

---

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