Powered by AppSignal & Oban Pro

The connectome, from its JSON export

livebooks/connectome.livemd

The connectome, from its JSON export

Mix.install([
  {:kino, "~> 0.19"},
  {:jason, "~> 1.4"}
])

What this notebook is

beam_mcp exports a composed MCP system's call graph twice -- declared (what can happen, from the catalog and :xref) and observed (what did happen, from a telemetry span on the one dispatch site) -- and diffs the two into four classes. Every export is canonical JSON (docs/connectome-canonical.md): the same graph gives the same bytes and the same hash — the digest the bytes name, SHA-256 here — whoever wrote it.

This notebook renders those exports and nothing else. It does not install beam_mcp, and no cell names it: a reader with only the JSON -- a reviewer, an auditor, a consumer on another platform -- sees exactly what a reader with the package sees. Run it from a checkout of the repository; the four files it reads sit beside it under exports/, and each is held by a test to what the package produces from its fixtures today. To render your own system, point the next cell at your own exports.

The fixture is small on purpose: a server fx declaring two tools, one resource, one prompt and six modules, and a run of the collector in which the running catalog carried a third tool the declaration never mentioned.

read = fn name ->
  __DIR__
  |> Path.join("exports/#{name}")
  |> File.read!()
  |> Jason.decode!()
end

declared = read.("fx.declared.json")
observed = read.("fx.observed.json")
sidecar = read.("fx.observed.sidecar.json")
diff = read.("fx.diff.json")

Kino.Markdown.new("""
| export | schema | nodes | edges |
| -- | -- | -- | -- |
| declared | #{declared["schema_version"]} | #{length(declared["nodes"])} | #{length(declared["edges"])} |
| observed | #{observed["schema_version"]} | #{length(observed["nodes"])} | #{length(observed["edges"])} |
| observed sidecar | #{sidecar["schema_version"]} | -- | #{length(sidecar["weights"])} weights |
| diff | #{diff["schema_version"]} | -- | #{diff["classes"] |> Map.values() |> Enum.map(&length/1) |> Enum.sum()} classified |
""")

The renderer

One module, from the JSON's own vocabulary: a node's kind picks its shape, an edge's kind picks its arrow, and a weight from the sidecar (the observed graph carries none in its hashed bytes -- the sidecar is the unsigned companion) labels the arrow it belongs to. Node ids are the graph's, shown verbatim; Mermaid gets a short alias for each because an id like fx/resource/r:%2F%2Fa is not a Mermaid identifier.

defmodule Wiring do
  @moduledoc """
  Mermaid source from a canonical connectome export. Nothing here is inferred: every
  node, edge, weight and class comes from the JSON it is handed.
  """

  @arrow %{"invoke" => "-->", "read" => "-.->", "message" => "==>", "supervise" => "--o"}

  @doc "A wiring diagram of one graph; `weights:` takes the observed sidecar."
  def graph(graph, opts \\ []) do
    weights = weights(opts[:weights])
    alias_of = aliases(graph["nodes"])

    lines =
      Enum.map(graph["nodes"], &node(&1, alias_of)) ++
        Enum.map(graph["edges"], fn e ->
          label =
            case Map.get(weights, {e["from"], e["to"], e["kind"]}) do
              nil -> ""
              w -> "|#{w}|"
            end

          "  #{alias_of[e["from"]]} #{@arrow[e["kind"]]}#{label} #{alias_of[e["to"]]}"
        end)

    Kino.Mermaid.new(Enum.join(["graph LR" | lines], "\n"))
  end

  @doc """
  The diff over the union of both graphs' nodes: one arrow per classified label, coloured
  by class; a node only one side knows is drawn dashed.
  """
  def diff(diff, declared, observed) do
    d_ids = MapSet.new(declared["nodes"], & &1["id"])
    o_ids = MapSet.new(observed["nodes"], & &1["id"])
    nodes = Enum.uniq_by(declared["nodes"] ++ observed["nodes"], & &1["id"])
    alias_of = aliases(nodes)

    classified =
      for {class, edges} <- diff["classes"], e <- edges, do: {class, e}

    node_lines = Enum.map(nodes, &node(&1, alias_of))

    edge_lines =
      Enum.map(classified, fn {_class, e} ->
        "  #{alias_of[e["from"]]} #{@arrow[e["kind"]]} #{alias_of[e["to"]]}"
      end)

    link_styles =
      classified
      |> Enum.with_index()
      |> Enum.map(fn {{class, _}, i} -> "  linkStyle #{i} stroke:#{colour(class)},stroke-width:2px" end)

    one_side =
      for %{"id" => id} <- nodes,
          not (MapSet.member?(d_ids, id) and MapSet.member?(o_ids, id)),
          do: "  style #{alias_of[id]} stroke-dasharray: 5 5"

    Kino.Mermaid.new(
      Enum.join(["graph LR" | node_lines ++ edge_lines ++ link_styles ++ one_side], "\n")
    )
  end

  @doc "The legend for `diff/3`, in the same colours."
  def legend do
    Kino.Markdown.new(
      Enum.map_join(
        [
          {"declared_and_observed", "declared, and seen in the window"},
          {"declared_never_observed", "dead authority: declared, never seen"},
          {"observed_but_undeclared", "a drift finding: seen, never declared"},
          {"changed_sign", "in both, a sign supplied on both sides, the two different"}
        ],
        "\n",
        fn {class, meaning} ->
          "- <span style=\"color:#{colour(class)}\">&#9644;</span> `#{class}` -- #{meaning}"
        end
      ) <> "\n- a dashed node is known to one side only"
    )
  end

  defp colour("declared_and_observed"), do: "#2a9d8f"
  defp colour("declared_never_observed"), do: "#8d99ae"
  defp colour("observed_but_undeclared"), do: "#e63946"
  defp colour("changed_sign"), do: "#f4a261"

  defp aliases(nodes) do
    nodes
    |> Enum.map(& &1["id"])
    |> Enum.with_index()
    |> Map.new(fn {id, i} -> {id, "n#{i}"} end)
  end

  defp node(%{"id" => id, "kind" => kind}, alias_of) do
    {open, close} =
      case kind do
        "server" -> {"[[", "]]"}
        "tool" -> {"([", "])"}
        "resource" -> {"[(", ")]"}
        "prompt" -> {">", "]"}
        _ -> {"[", "]"}
      end

    ~s(  #{alias_of[id]}#{open}"#{String.replace(id, ~s("), "#quot;")}"#{close})
  end

  defp weights(nil), do: %{}

  defp weights(%{"weights" => rows}),
    do: Map.new(rows, &{{&1["from"], &1["to"], &1["kind"]}, &1["weight"]})
end

Declared: what can happen

The catalog's tools hang off the server node by invoke edges; its resource and its prompt are nodes with no edge, because the declared builder writes no read edge -- a resource declared is not a resource read. The modules and the invoke edges between them are what :xref found in the host's beams. Signs are the host's to populate and the package never does, so every edge here reads unset in the JSON and the diagram draws none.

Wiring.graph(declared)

Observed: what did happen

Edge identity only -- never a payload byte -- with the count of each edge in the window from the sidecar. Three calls to echo, one to write, and one to a tool the declaration does not know.

Wiring.graph(observed, weights: sidecar)

The diff: every edge of either graph, in exactly one class

Wiring.legend()
Wiring.diff(diff, declared, observed)

The classes as rows, in the diff's own order (labels sorted by their canonical bytes):

diff["classes"]
|> Enum.flat_map(fn {class, edges} ->
  Enum.map(edges, &%{class: class, from: &1["from"], kind: &1["kind"], to: &1["to"]})
end)
|> Kino.DataTable.new(keys: [:class, :from, :kind, :to], name: "classified labels")

Coverage: integers the consumer divides

A float has no canonical bytes, so the diff carries counts and the consumer -- this notebook, here -- does the division. The two figures a reader wants first are completeness (observed edges whose both ends are declared nodes, over observed edges: how much of what ran was even nameable) and endpoint coverage (declared edges whose both ends the window saw as nodes, over declared edges: how much of the declaration the window reached at all).

c = diff["coverage"]

ratio = fn num, den ->
  if den == 0, do: "#{num} / 0 (undefined)", else: "#{num} / #{den} = #{Float.round(num / den, 3)}"
end

Kino.DataTable.new(
  [
    %{figure: "declared edges observed", value: ratio.(c["declared_and_observed"], c["declared_edges"])},
    %{figure: "observed edges declared", value: ratio.(c["declared_and_observed"], c["observed_edges"])},
    %{figure: "completeness", value: ratio.(c["observed_endpoint_declared"], c["observed_edges"])},
    %{figure: "endpoint coverage", value: ratio.(c["declared_endpoint_covered"], c["declared_edges"])}
  ],
  keys: [:figure, :value],
  name: "coverage, divided here"
)

Into Neo4j, without a fourth exporter

The package stops at canonical JSON: its two other renderings (DOT, GraphML) are already the largest surface no hash covers — the hash is over the canonical bytes alone — and a Cypher exporter would be a third such place for the next escaping defect, verifiable by nobody against the hash. A Neo4j user loads the canonical bytes directly with APOC's JSON loader -- one statement over the nodes array and one over edges, each node keyed by its id, each edge by the from/to ids the bytes already carry. This cell builds those two statements from the export it read above; it does not run them (there is no Neo4j here), so it shows the statements and the node and edge counts they would load, which are the export's own.

cypher_nodes = """
CALL apoc.load.json($url) YIELD value
UNWIND value.nodes AS n
MERGE (v:Node {id: n.id})
SET v.kind = n.kind, v.level = n.level, v.labels = apoc.convert.toJson(n.labels)
"""

cypher_edges = """
CALL apoc.load.json($url) YIELD value
UNWIND value.edges AS e
MATCH (a:Node {id: e.from}), (b:Node {id: e.to})
MERGE (a)-[r:EDGE {kind: e.kind, provenance: e.provenance}]->(b)
SET r.sign = e.sign
"""

%{
  statements: [cypher_nodes, cypher_edges],
  nodes_to_load: length(declared["nodes"]),
  edges_to_load: length(declared["edges"])
}

What this notebook does not do

It renders what an export says and never what a renderer might guess: no layout hint, no weight, no sign and no class is drawn that the JSON did not carry. It computes no diff of its own -- the diff is the package's, hashed, and this is a view of it. And it holds no tools, no domain and no policy, which is beam_mcp's thesis applied to its own pictures.