defmodule Bloccs.Web.Coverage do @moduledoc """ Structural coverage for the dashboard, computed from the runtime `Bloccs.Introspect.Network` (so it needs no parsed manifest). Mirrors `Bloccs.Coverage`'s obligation model — every in-port, out-port, and edge — and compares it against a *reached* set derived from `Bloccs.Trace` events (recorded live or loaded from a `.bloccs-trace` file). """ alias Bloccs.Introspect.Network @type obligation :: {:port_in, atom(), atom()} | {:port_out, atom(), atom()} | {:edge, {atom(), atom()}, {atom(), atom()}} @type report :: %{ obligations: [obligation()], reached: [obligation()], unreached: [obligation()], total: non_neg_integer(), reached_count: non_neg_integer(), percent: integer() } @doc "Every coverage obligation in a running network." @spec obligations(Network.t()) :: [obligation()] def obligations(%Network{nodes: nodes, edges: edges}) do port_obs = Enum.flat_map(nodes, fn n -> ins = Enum.map(n.ports_in, &{:port_in, n.id, &1.name}) outs = Enum.map(n.ports_out, &{:port_out, n.id, &1.name}) ins ++ outs end) edge_obs = Enum.map(edges, fn %{from: from, to: to} -> {:edge, from, to} end) port_obs ++ edge_obs end @doc "Compare a network's obligations against a reached obligation set." @spec report(Network.t(), [obligation()]) :: report() def report(%Network{} = network, reached) do all = obligations(network) reached_set = MapSet.new(reached) reached_present = Enum.filter(all, &MapSet.member?(reached_set, &1)) unreached = Enum.reject(all, &MapSet.member?(reached_set, &1)) %{ obligations: all, reached: reached_present, unreached: unreached, total: length(all), reached_count: length(reached_present), percent: percent(length(reached_present), length(all)) } end @doc "The set of node ids touched by any reached port obligation." @spec reached_nodes(report()) :: MapSet.t(atom()) def reached_nodes(%{reached: reached}) do reached |> Enum.flat_map(fn {:port_in, node, _} -> [node] {:port_out, node, _} -> [node] {:edge, _, _} -> [] end) |> MapSet.new() end @doc "The set of `{from_node, to_node}` pairs for reached edges (for the overlay)." @spec reached_edges(report()) :: MapSet.t({atom(), atom()}) def reached_edges(%{reached: reached}) do reached |> Enum.flat_map(fn {:edge, {fnode, _}, {tnode, _}} -> [{fnode, tnode}] _ -> [] end) |> MapSet.new() end @doc "Per-node glyph state for the overlay: reached → `:ok`, otherwise `:idle`." @spec node_states(Network.t(), report()) :: %{atom() => :ok | :idle} def node_states(%Network{nodes: nodes} = _network, report) do reached = reached_nodes(report) Map.new(nodes, fn n -> {n.id, if(MapSet.member?(reached, n.id), do: :ok, else: :idle)} end) end defp percent(_n, 0), do: 0 defp percent(n, total), do: round(n / total * 100) end